diff --git a/Changes.md b/Changes.md index 72c2b52..bc45bcc 100644 --- a/Changes.md +++ b/Changes.md @@ -2,15 +2,26 @@ ## v0.12.0 (TBD) -New features: +This update adds several new features to gameplay and the Level Editor. -* **Level Difficulty Setting:** in the Level Properties you can choose - from Peaceful, Normal or Hard for your level. - * On Peaceful, Azulians and Birds don't attack the player, acting like - pre-0.11.0 versions that ignored the player character. - * On Hard difficulty, Azulians have an infinite aggro radius (they'll - immediately hunt the player from any distance on level start) and - they are hostile to all player creatures. +A **Game Rules** feature has been added to the Level Editor which allows +customizing certain gameplay features while that level is being played. +These settings are available in the Level Properties window of the editor: + +* The **Difficulty** rule can modify the behavior of enemy doodads + when the level is played. Choose between Peaceful, Normal, or Hard. + * On Peaceful, Azulians and Birds don't attack the player, acting + like pre-0.11.0 versions that ignored the player character. + * On Hard difficulty, Azulians have an infinite aggro radius + (they'll immediately hunt the player from any distance on + level start) and they are hostile to _all_ player creatures. +* **Survival Mode** changes the definition of "high score" for levels + where the player is very likely to die at least once. + * The silver high score (respawned at checkpoint) will be for the + _longest_ time survived on the level rather than the fastest time + to complete it. + * The gold high score (got to an Exit Flag without dying once) is + still rewarded to the fastest time completing the level. An update to the Level Editor's toolbar: @@ -18,11 +29,20 @@ An update to the Level Editor's toolbar: from the game's built-in fonts. * New **Pan Tool** to be able to scroll the level safely by dragging with your mouse or finger. +* New **Flood Tool** (or paint bucket tool) can be used to replace + contiguous areas of your level from one color to another. * The toolbar buttons are smaller and rearranged. On medium-size screens or larger, the toolbar buttons are drawn side-by-side in two columns. On narrower screens with less real estate, it will use a single column when it fits better. +New doodads: + +* A technical doodad for **Reset Level Timer** resets the timer to zero, + one time, when touched by the player. If the doodad receives a power + signal from a linked doodad, it can reset the level timer again if the + player touches it once more. + Updates to the JavaScript API for custom doodads: * New global integer `Level.Difficulty` is available to doodad scripts to @@ -30,10 +50,13 @@ Updates to the JavaScript API for custom doodads: * Peaceful (-1): `Level.Difficulty < 0` * Normal (0): `Level.Difficulty == 0` * Hard (1): `Level.Difficulty > 1` +* New function `Level.ResetTimer()` resets the in-game timer to zero. New cheat codes: * `test load screen` tests the loading screen UI for a few seconds. +* `master key` allows playing locked Story Mode levels without unlocking + them first by completing the earlier levels. Other changes: @@ -42,6 +65,17 @@ Other changes: * Fixed a bug where making the app window bigger during a loading screen caused the Editor to not adapt to the larger window. * Don't show the _autosave.doodad in the Doodad Dropper. +* The Azulians have had their jump heights buffed slightly. +* Birds no longer register as solid when colliding with other birds (or + more generally, characters unaffected by gravity). + +Bugs fixed: + +* When modifying your Palette to rename a color or add an additional + color, it wasn't possible to draw with that new color without fully + exiting and reloading the editor - this is now resolved. +* The palette editor will try and prevent the user from giving the same + name to different colors. ## v0.11.0 (Feb 21 2022) diff --git a/dev-assets/doodads/bird/bird.js b/dev-assets/doodads/bird/bird.js index 00de17d..67fa32d 100644 --- a/dev-assets/doodads/bird/bird.js +++ b/dev-assets/doodads/bird/bird.js @@ -31,7 +31,7 @@ function main() { return; } - if (e.Actor.IsMobile() && e.InHitbox) { + if (e.Actor.IsMobile() && e.Actor.HasGravity() && e.InHitbox) { return false; } }); diff --git a/dev-assets/doodads/regions/Makefile b/dev-assets/doodads/regions/Makefile index 6ea450f..1c3f568 100644 --- a/dev-assets/doodads/regions/Makefile +++ b/dev-assets/doodads/regions/Makefile @@ -28,6 +28,10 @@ build: doodad edit-doodad --tag "color=invisible" reg-warp-door.doodad doodad install-script ../warp-door/warp-door.js reg-warp-door.doodad + # Reset Level Timer + doodad convert -t "Reset Level Timer" timer-64.png reg-reset-timer.doodad + doodad install-script reset-timer.js reg-reset-timer.doodad + for i in *.doodad; do\ doodad edit-doodad --tag "category=technical" $${i};\ done diff --git a/dev-assets/doodads/regions/reset-timer.js b/dev-assets/doodads/regions/reset-timer.js new file mode 100644 index 0000000..eb12c60 --- /dev/null +++ b/dev-assets/doodads/regions/reset-timer.js @@ -0,0 +1,30 @@ +// Reset Level Timer. +function main() { + Self.Hide(); + + // Reset the level timer only once. + let hasReset = false; + + Events.OnCollide((e) => { + if (!e.Settled) { + return; + } + + // Only care if it's the player. + if (!e.Actor.IsPlayer()) { + return; + } + + if (e.InHitbox && !hasReset) { + Level.ResetTimer(); + hasReset = true; + } + }); + + // Receive a power signal resets the doodad. + Message.Subscribe("power", (powered) => { + if (powered) { + hasReset = true; + } + }); +} diff --git a/dev-assets/doodads/regions/timer-64.png b/dev-assets/doodads/regions/timer-64.png new file mode 100644 index 0000000..4c55879 Binary files /dev/null and b/dev-assets/doodads/regions/timer-64.png differ diff --git a/pkg/commands.go b/pkg/commands.go index 3060495..ae770f9 100644 --- a/pkg/commands.go +++ b/pkg/commands.go @@ -336,6 +336,13 @@ func (c Command) RunScript(d *Doodle, code string) (goja.Value, error) { d.FlashError("Command.RunScript: Panic: %s", err) } }() + + // If we're in Play Mode, consider it cheating if the player is + // messing with any in-game structures. + if scene, ok := d.Scene.(*PlayScene); ok { + scene.SetCheated() + } + out, err := d.shell.js.RunString(code) return out, err } diff --git a/pkg/modal/end_level.go b/pkg/modal/end_level.go index c43462f..3119377 100644 --- a/pkg/modal/end_level.go +++ b/pkg/modal/end_level.go @@ -175,8 +175,8 @@ func makeEndLevel(m *Modal, cfg ConfigEndLevel) *ui.Window { Font: balance.MenuFont, })) button.Handle(ui.Click, func(ed ui.EventData) error { - btn.F() m.Dismiss(false) + btn.F() return nil }) button.Compute(engine) diff --git a/pkg/modal/modal.go b/pkg/modal/modal.go index e27e29c..9b58e6f 100644 --- a/pkg/modal/modal.go +++ b/pkg/modal/modal.go @@ -138,8 +138,8 @@ func (m *Modal) Then(f func()) *Modal { // Dismiss the modal and optionally call the callback function. func (m *Modal) Dismiss(call bool) { + Reset() if call && m.callback != nil { m.callback() } - Reset() } diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 818b1b0..f757a7e 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -224,6 +224,7 @@ func (s *PlayScene) setupAsync(d *Doodle) error { // Handle a doodad changing the player character. s.drawing.OnSetPlayerCharacter = s.SetPlayerCharacter + s.drawing.OnResetTimer = s.ResetTimer // Given a filename or map data to play? if s.Level != nil { @@ -265,11 +266,6 @@ func (s *PlayScene) setupAsync(d *Doodle) error { // Load in the player character. s.setupPlayer(balance.PlayerCharacterDoodad) - // Run all the actor scripts' main() functions. - if err := s.drawing.InstallScripts(); err != nil { - log.Error("PlayScene.Setup: failed to drawing.InstallScripts: %s", err) - } - if s.CanEdit { d.Flash("Entered Play Mode. Press 'E' to edit this map.") } else { @@ -286,6 +282,11 @@ func (s *PlayScene) setupAsync(d *Doodle) error { // Gamepad: put into GameplayMode. gamepad.SetMode(gamepad.GameplayMode) + // Run all the actor scripts' main() functions. + if err := s.drawing.InstallScripts(); err != nil { + log.Error("PlayScene.Setup: failed to drawing.InstallScripts: %s", err) + } + s.startTime = time.Now() s.perfectRun = true s.running = true @@ -321,6 +322,11 @@ func (s *PlayScene) SetPlayerCharacter(filename string) { } } +// ResetTimer sets the level elapsed timer back to zero. +func (s *PlayScene) ResetTimer() { + s.startTime = time.Now() +} + // setupPlayer creates and configures the Player Character in the level. func (s *PlayScene) setupPlayer(playerCharacterFilename string) { // Find the spawn point of the player. Search the level for the @@ -492,7 +498,12 @@ func (s *PlayScene) BeatLevel() { ) } -// FailLevel handles a level failure triggered by a doodad. +/* +FailLevel handles a level failure triggered by a doodad or fire pixel. + +If the Survival GameRule is set, this ends the level with a note on how long the +player had survived for and they get a silver rating. +*/ func (s *PlayScene) FailLevel(message string) { if s.Player.Invulnerable() || s.godMode || s.godModeUntil.After(time.Now()) { return @@ -500,6 +511,19 @@ func (s *PlayScene) FailLevel(message string) { s.SetImperfect() s.d.FlashError(message) + if s.Level.GameRule.Survival { + s.ShowEndLevelModal( + true, + "Level Completed", + fmt.Sprintf( + "%s\nCongrats on surviving for %s!", + message, + savegame.FormatDuration(time.Since(s.startTime)), + ), + ) + return + } + s.ShowEndLevelModal( false, "You've died!", diff --git a/pkg/uix/canvas.go b/pkg/uix/canvas.go index 06cd283..2680699 100644 --- a/pkg/uix/canvas.go +++ b/pkg/uix/canvas.go @@ -95,6 +95,9 @@ type Canvas struct { // The filename.doodad is given. OnSetPlayerCharacter func(filename string) + // Handler for when a doodad script calls Level.ResetTimer(). + OnResetTimer func() + /******** * Editable canvas private variables. ********/ diff --git a/pkg/uix/scripting.go b/pkg/uix/scripting.go index 911faab..1ecddb7 100644 --- a/pkg/uix/scripting.go +++ b/pkg/uix/scripting.go @@ -67,6 +67,13 @@ func (w *Canvas) MakeScriptAPI(vm *scripting.VM) { vm.Set("Level", map[string]interface{}{ "Difficulty": w.level.GameRule.Difficulty, + "ResetTimer": func() { + if w.OnResetTimer != nil { + w.OnResetTimer() + } else { + log.Error("Level.ResetTimer: caller was not ready") + } + }, }) }