From 38a23f00b23bf460937a8f7982a2a3c12c892785 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 27 Mar 2022 11:51:14 -0700 Subject: [PATCH] Reset Timer Doodad + Various Fixes * Bird is not solid when colliding with other birds. * If the dev shell is used to run JavaScript during Play Mode, consider it cheating (so player can't `$ d.Scene.ResetTimer()` for example) * On Survival Mode levels, DieByFire immediately opens the End Level (silver score) modal rather than respawn from checkpoint, so levels don't need checkpoint contraptions to end the level. * During level loading screens, wait and call doodads' main() function until the very end. --- Changes.md | 50 ++++++++++++++++++---- dev-assets/doodads/bird/bird.js | 2 +- dev-assets/doodads/regions/Makefile | 4 ++ dev-assets/doodads/regions/reset-timer.js | 30 +++++++++++++ dev-assets/doodads/regions/timer-64.png | Bin 0 -> 901 bytes pkg/commands.go | 7 +++ pkg/modal/end_level.go | 2 +- pkg/modal/modal.go | 2 +- pkg/play_scene.go | 36 +++++++++++++--- pkg/uix/canvas.go | 3 ++ pkg/uix/scripting.go | 7 +++ 11 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 dev-assets/doodads/regions/reset-timer.js create mode 100644 dev-assets/doodads/regions/timer-64.png 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 0000000000000000000000000000000000000000..4c5587974741a99717bc55269fcf4eb0fd0f1fb5 GIT binary patch literal 901 zcmV;01A6?4P)EX>4Tx04R}tkv&MmKp2MKrivmJ1uIB#$WWauh>AFB6^c+H)C#RSm|Xe?O&XFE z7e~Rh;NZ_<)xpJCR|i)?5c~mgbaGO3krKa43N2#1*Znzq)ttqEfJi*c4AUmwAfDc| z4bJ<-VOEq?;&b9LlP*a7$aTfzH_io@1)do;)2VslFtJ!@W2KE*(bR~ih@+~eQ@)V# zSmnIMSu0mr^Pc>Lp`5<5%ynABNMI35kRU=q6(y8mBSyPUiiH%N$9?<}*DsMvAy)~E z91EyGgY5dj|KN9Tt^DMKmlTQvoiC2_F#>e$0*#vEd>=bb;{*sk16O*>U#SB#pQP7X zTJ#9$+XgPKTbi;5TCi1XOrEd$s6h?M(NpVNALh2OR~mM1{yODA>N z%?jOTLI46FU~R~py;UVk-`?xHC&s?EPRJs__OSEV-}BgC{hDhs%0ct@So_d8(}uv8 zwp(&QZDHYB^1inpvL8-B$N|@cZOP7z|Ir~KV;-(MVsjwH-bKQw9P}%3Wai}8B?*@7 z^WCb!oc%Y(u~H#qYLAtwRR910;LAE7Mh{%~BW(8iwH*8EwLp!wcZxc7UZ@TsPUM0x+0Fod80MHOg7B0Sq zhV8?Ld4Ebnq-E_OVwZLyvxU>=fEFakZ9(V+%31@J=z)~Hpz=s9EkHvAAp`*s009sH0T8e(1b?0; b1lGnMaXU~x8$4z=00000NkvXXu0mjfriPUd literal 0 HcmV?d00001 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") + } + }, }) }