From 8965a7d86a4e25ee30e94aa66a760113302af702 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 30 Dec 2019 18:13:28 -0800 Subject: [PATCH] Doodads: Crumbly Floor, Start Flag & State Blocks Add new doodads: * Start Flag: place this in a level to set the spawn point of the player character. If no flag is found, the player spawns at 0,0 in the top corner of the map. Only use one Start Flag per level, otherwise the player will randomly spawn at one of them. * Crumbly Floor: a solid floor that begins to shake and then fall apart after a moment when a mobile character steps on it. The floor respawns after 5 seconds. * State Blocks: blue and orange blocks that toggle between solid and pass-thru whenever a State Button is activated. * State Button: a solid "ON/OFF" block that toggles State Blocks back and forth when touched. Only activates if touched on the side or bottom; acts as a solid floor when walked on from the top. New features for doodad scripts: * Actor scripts: call SetMobile(true) to mark an actor as a mobile mob (i.e. player character or enemy). Other doodads can check if the actor colliding with them IsMobile so they don't activate if placed too close to other (non-mobile) doodads in a level. The Blue and Red Azulians are the only mobile characters so far. * Message.Broadcast allows sending a pub/sub message out to ALL doodads in the level, instead of only to linked doodads as Message.Publish does. This is used for the State Blocks to globally communicate on/off status without needing to link them all together manually. --- cmd/doodad/commands/convert.go | 4 +- cmd/doodad/commands/edit_level.go | 2 +- cmd/doodle/main.go | 2 +- dev-assets/doodads/azulian/azulian-red.js | 1 + dev-assets/doodads/azulian/azulian.js | 1 + dev-assets/doodads/build.sh | 28 ++++++ .../doodads/crumbly-floor/crumbly-floor.js | 60 ++++++++++++ dev-assets/doodads/crumbly-floor/fall1.png | Bin 0 -> 1021 bytes dev-assets/doodads/crumbly-floor/fall2.png | Bin 0 -> 986 bytes dev-assets/doodads/crumbly-floor/fall3.png | Bin 0 -> 957 bytes dev-assets/doodads/crumbly-floor/fall4.png | Bin 0 -> 855 bytes dev-assets/doodads/crumbly-floor/fallen.png | Bin 0 -> 652 bytes dev-assets/doodads/crumbly-floor/floor.png | Bin 0 -> 868 bytes dev-assets/doodads/crumbly-floor/shake1.png | Bin 0 -> 897 bytes dev-assets/doodads/crumbly-floor/shake2.png | Bin 0 -> 891 bytes dev-assets/doodads/doors/electric-door.js | 2 +- dev-assets/doodads/objects/start-flag.png | Bin 0 -> 6541 bytes dev-assets/doodads/on-off/blue-button.png | Bin 0 -> 741 bytes dev-assets/doodads/on-off/blue-off.png | Bin 0 -> 648 bytes dev-assets/doodads/on-off/blue-on.png | Bin 0 -> 683 bytes dev-assets/doodads/on-off/orange-button.png | Bin 0 -> 751 bytes dev-assets/doodads/on-off/orange-off.png | Bin 0 -> 650 bytes dev-assets/doodads/on-off/orange-on.png | Bin 0 -> 687 bytes dev-assets/doodads/on-off/state-block-blue.js | 28 ++++++ .../doodads/on-off/state-block-orange.js | 28 ++++++ dev-assets/doodads/on-off/state-button.js | 47 ++++++++++ docs/Shell.md | 3 + pkg/collision/actors_test.go | 2 +- pkg/commands.go | 14 +++ pkg/doodads/actor.go | 28 +++++- pkg/doodads/drawing.go | 15 --- pkg/editor_ui.go | 4 +- pkg/editor_ui_doodad.go | 6 +- pkg/editor_ui_palette.go | 12 +-- pkg/editor_ui_toolbar.go | 16 ++-- pkg/guitest_scene.go | 34 +++---- pkg/level/chunk_test.go | 2 +- pkg/main_scene.go | 2 +- pkg/play_scene.go | 88 ++++++++++++------ pkg/scene.go | 2 +- pkg/scripting/pubsub.go | 13 ++- pkg/scripting/scripting.go | 2 +- pkg/scripting/vm.go | 2 +- pkg/sprites/sprites.go | 4 +- pkg/uix/actor.go | 29 ++++++ pkg/uix/actor_collision.go | 79 +++++++++++----- pkg/uix/canvas_actors.go | 2 +- pkg/uix/canvas_editable.go | 2 +- pkg/uix/canvas_strokes.go | 4 +- pkg/wallpaper/texture.go | 2 +- pkg/wallpaper/wallpaper.go | 2 +- wasm/main_wasm.go | 4 +- 52 files changed, 451 insertions(+), 125 deletions(-) create mode 100644 dev-assets/doodads/crumbly-floor/crumbly-floor.js create mode 100644 dev-assets/doodads/crumbly-floor/fall1.png create mode 100644 dev-assets/doodads/crumbly-floor/fall2.png create mode 100644 dev-assets/doodads/crumbly-floor/fall3.png create mode 100644 dev-assets/doodads/crumbly-floor/fall4.png create mode 100644 dev-assets/doodads/crumbly-floor/fallen.png create mode 100644 dev-assets/doodads/crumbly-floor/floor.png create mode 100644 dev-assets/doodads/crumbly-floor/shake1.png create mode 100644 dev-assets/doodads/crumbly-floor/shake2.png create mode 100644 dev-assets/doodads/objects/start-flag.png create mode 100644 dev-assets/doodads/on-off/blue-button.png create mode 100644 dev-assets/doodads/on-off/blue-off.png create mode 100644 dev-assets/doodads/on-off/blue-on.png create mode 100644 dev-assets/doodads/on-off/orange-button.png create mode 100644 dev-assets/doodads/on-off/orange-off.png create mode 100644 dev-assets/doodads/on-off/orange-on.png create mode 100644 dev-assets/doodads/on-off/state-block-blue.js create mode 100644 dev-assets/doodads/on-off/state-block-orange.js create mode 100644 dev-assets/doodads/on-off/state-button.js diff --git a/cmd/doodad/commands/convert.go b/cmd/doodad/commands/convert.go index a1c188e..978bcb4 100644 --- a/cmd/doodad/commands/convert.go +++ b/cmd/doodad/commands/convert.go @@ -11,11 +11,11 @@ import ( "image/png" - "git.kirsle.net/go/render" "git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/go/render" "github.com/urfave/cli" "golang.org/x/image/bmp" ) @@ -322,7 +322,7 @@ func imageToChunker(img image.Image, chroma render.Color, palette *level.Palette newColors[color.String()] = swatch } - chunker.Set(render.NewPoint(int32(x), int32(y)), swatch) + chunker.Set(render.NewPoint(x, y), swatch) } } diff --git a/cmd/doodad/commands/edit_level.go b/cmd/doodad/commands/edit_level.go index 0ea138b..7e219c3 100644 --- a/cmd/doodad/commands/edit_level.go +++ b/cmd/doodad/commands/edit_level.go @@ -3,9 +3,9 @@ package commands import ( "fmt" - "git.kirsle.net/go/render" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/go/render" "github.com/urfave/cli" ) diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index 5949223..f6e459f 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -8,11 +8,11 @@ import ( "sort" "time" - "git.kirsle.net/go/render/sdl" doodle "git.kirsle.net/apps/doodle/pkg" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/branding" + "git.kirsle.net/go/render/sdl" "github.com/urfave/cli" _ "image/png" diff --git a/dev-assets/doodads/azulian/azulian-red.js b/dev-assets/doodads/azulian/azulian-red.js index a138408..f631e40 100644 --- a/dev-assets/doodads/azulian/azulian-red.js +++ b/dev-assets/doodads/azulian/azulian-red.js @@ -7,6 +7,7 @@ function main() { var direction = "right"; + Self.SetMobile(true); Self.SetGravity(true); Self.AddAnimation("walk-left", 100, ["red-wl1", "red-wl2", "red-wl3", "red-wl4"]); Self.AddAnimation("walk-right", 100, ["red-wr1", "red-wr2", "red-wr3", "red-wr4"]); diff --git a/dev-assets/doodads/azulian/azulian.js b/dev-assets/doodads/azulian/azulian.js index 1599b48..a01881c 100644 --- a/dev-assets/doodads/azulian/azulian.js +++ b/dev-assets/doodads/azulian/azulian.js @@ -7,6 +7,7 @@ function main() { var animStart = animEnd = 0; var animFrame = animStart; + Self.SetMobile(true); Self.SetGravity(true); Self.SetHitbox(7, 4, 17, 28); Self.AddAnimation("walk-left", 100, ["blu-wl1", "blu-wl2", "blu-wl3", "blu-wl4"]); diff --git a/dev-assets/doodads/build.sh b/dev-assets/doodads/build.sh index c50d5dd..5a72c6a 100755 --- a/dev-assets/doodads/build.sh +++ b/dev-assets/doodads/build.sh @@ -115,6 +115,33 @@ objects() { doodad convert -t "Exit Flag" exit-flag.png exit-flag.doodad doodad install-script exit-flag.js exit-flag.doodad + doodad convert -t "Start Flag" start-flag.png start-flag.doodad + + cp *.doodad ../../../assets/doodads/ + + cd ../crumbly-floor + + doodad convert -t "Crumbly Floor" floor.png shake1.png shake2.png \ + fall1.png fall2.png fall3.png fall4.png fallen.png \ + crumbly-floor.doodad + doodad install-script crumbly-floor.js crumbly-floor.doodad + cp *.doodad ../../../assets/doodads/ + + cd .. +} + +onoff() { + cd on-off/ + + doodad convert -t "State Button" blue-button.png orange-button.png state-button.doodad + doodad install-script state-button.js state-button.doodad + + doodad convert -t "State Block (Blue)" blue-on.png blue-off.png state-block-blue.doodad + doodad install-script state-block-blue.js state-block-blue.doodad + + doodad convert -t "State Block (Orange)" orange-off.png orange-on.png state-block-orange.doodad + doodad install-script state-block-orange.js state-block-orange.doodad + cp *.doodad ../../../assets/doodads/ cd .. @@ -126,5 +153,6 @@ doors trapdoors azulians objects +onoff doodad edit-doodad -quiet -lock -author "Noah" ../../assets/doodads/*.doodad doodad edit-doodad -hide ../../assets/doodads/azu-blu.doodad diff --git a/dev-assets/doodads/crumbly-floor/crumbly-floor.js b/dev-assets/doodads/crumbly-floor/crumbly-floor.js new file mode 100644 index 0000000..155be86 --- /dev/null +++ b/dev-assets/doodads/crumbly-floor/crumbly-floor.js @@ -0,0 +1,60 @@ +// Crumbly Floor. +function main() { + Self.SetHitbox(0, 0, 65, 7); + + Self.AddAnimation("shake", 100, ["shake1", "shake2", "floor", "shake1", "shake2", "floor"]); + Self.AddAnimation("fall", 100, ["fall1", "fall2", "fall3", "fall4"]); + + // Recover time for the floor to respawn. + var recover = 5000; + + // States of the floor. + var stateSolid = 0; + var stateShaking = 1; + var stateFalling = 2; + var stateFallen = 3; + var state = stateSolid; + + // Started the animation? + var startedAnimation = false; + + Events.OnCollide(function(e) { + // Only trigger for mobile characters. + if (!e.Actor.IsMobile()) { + return; + } + + // If the floor is falling, the player passes right thru. + if (state === stateFalling || state === stateFallen) { + return; + } + + // Floor is solid until it begins to fall. + if (e.InHitbox && (state === stateSolid || state === stateShaking)) { + // Only activate when touched from the top. + if (e.Overlap.Y > 0) { + return false; + } + + // Begin the animation sequence if we're in the solid state. + if (state === stateSolid) { + state = stateShaking; + Self.PlayAnimation("shake", function() { + state = stateFalling; + Self.PlayAnimation("fall", function() { + state = stateFallen; + Self.ShowLayerNamed("fallen"); + + // Recover after a while. + setTimeout(function() { + Self.ShowLayer(0); + state = stateSolid; + }, recover); + }); + }) + } + + return false; + } + }); +} diff --git a/dev-assets/doodads/crumbly-floor/fall1.png b/dev-assets/doodads/crumbly-floor/fall1.png new file mode 100644 index 0000000000000000000000000000000000000000..bc9710c3ae72e969b73fd46d463cd39a4ce41912 GIT binary patch literal 1021 zcmeAS@N?(olHy`uVBq!ia0vp^jv&mz1|<8wpLAtlU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJhrAngg+8%-@1Lo><#9L&Eb_FP2;aY1oBjy*Yuhb+Farc8;A3Qjo zboJoT$Z+;>M}a>Kg@vU-B@>1IPZxKe)^6_t2Xe zqiRs3vw%r-(qs1j&G+xl`q^1%t|`K96`%7vS%TvoQ}3+YdimRBk6Acun8Tw^$FF2I zp1c zvU2qk-hu*-9ubY|yN~b2zTNw`dfCnU{HMB#PBoc`NCTsVEy>&6h2cL4F4((#GEjuG zz$3Dlfq`2Xgc%uT&5-~KvX^-Jy0Sm!krQV#G`}yC0Thxfag8W(&d<$F%`0JWE=o-- zNlj5G&n(GMaQE~L2yf&Q2S)c*PZ!6Kid%1|ZVWnXz|;0tF8^Zqi7@@AlD8ge|FmnJI^O$JT(y_(IkmXH z{`eccyWhqC&szJp<87qf`VNik#GXJe&Ct-PKmL9G>sqrf-``7f>8wp2zvABa6$Z`f zs8Qq7RlP8!_56>kr+=1w_?P$l%IWpRI;;oy*R5KW{&~$Pdfa(S6h84 zNavVD?%KDut6%Tx?*0A$!u69%hKwb_Vs)3MwC0~WbevNA9B?4a;e|7phSH!;uDfM*qb6Mw<&;$Uj_P@pe literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/crumbly-floor/fall2.png b/dev-assets/doodads/crumbly-floor/fall2.png new file mode 100644 index 0000000000000000000000000000000000000000..1524b6d991e736b351dbfe6010b7ee7dc3c02b2d GIT binary patch literal 986 zcmV<0110>4P)EX>4Tx04R}tkv&MmKpe$iTcsiu2P;Ss$xxjvh+jBr6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfadlF3krMxx6k5c1aNLh~_a1le0DryARI_6YP&La) zCE`LRyD9`<5yBAq5y6ziOnpuilkgm0_we!cF2=LG&;2=il$^-`pFljzbi*RvAfDc| zbk6(4VOEqB;&b9rgDyz?$aUG}H_ioz{X8>lq*L?6VPc`s#&R38qM;H`5=RwPqkMnH zWrgz=XSG~q&3p0}hH~1 zax9<%6_Voz|AXJ%n)!)wHz^bcI$v!2V;BhT0*#t&e;?a+;{*si16NwhU#SB#pQP7X zTJ#9$-3BhMTbjHFTnGy0}1(02=TuerT7_i_3Fq^Yaq4RCM> zj1(w)-Q(R|?Y;ebrrF;Q%8GKzf;1Ex00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru;|v}b1~#d?iOT>002y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00FW|L_t(|+U=ITalFhDVXMeg3aQ)5XW@Zkew5s)MzQfqC|13Bj;BC;&Y-81XDro(>c zzI(o0lZeRa>CwIa{PvN&-@ROV4yBaaz2os1#@DfJe&@?I=`(W9DMg3(gDqMNPlR5) zMJF2x;E9oQPP%w4fEU|~uXB8PeIM62a)c*>KHQ42EKBp;V-r5?cdcJr`B*V*+!2B@jGE=0U2ARqz0=cUd+5bPvpd^~NC1yWfXF=~ zhitjmi>Cz;5qa<|+sgi=cLzAElchz{??*y2Y?%?M}X>g2s#HaGmn z7+WFI*351#tjLNsxhRntk$OHl7rop@&&>$&D_h(C`Iw!{XwinC_({FM*@0000000000000000000000000000000Pqj_1QHi;YHCWq7ytkO07*qo IM6N<$fEX>4Tx04R}tkv&MmKpe$iTcsiu2P;Ss$xxjvh+jBr6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfadlF3krMxx6k5c1aNLh~_a1le0DryARI_6YP&La) zCE`LRyD9`<5yBAq5y6ziOnpuilkgm0_we!cF2=LG&;2=il$^-`pFljzbi*RvAfDc| zbk6(4VOEqB;&b9rgDyz?$aUG}H_ioz{X8>lq*L?6VPc`s#&R38qM;H`5=RwPqkMnH zWrgz=XSG~q&3p0}hH~1 zax9<%6_Voz|AXJ%n)!)wHz^bcI$v!2V;BhT0*#t&e;?a+;{*si16NwhU#SB#pQP7X zTJ#9$-3BhMTbjHFTnGy0}1(02=TuerT7_i_3Fq^Yaq4RCM> zj1(w)-Q(R|?Y;ebrrF;Q%8GKzf;1Ex00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru;|v}b20EY(#ozz{02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00EUrL_t(|+U?iDZNnfG#&KTMEwVz7xnzipQ8Gc#9iYdY zaHps|L@qvd1EP*fTE?xl%lI%1 zl79AbJ}%?qef~54`S!Yuy?^~kO<|#)xCZB(H^!Lj^(wVNecFoeQo5mBSZl+3ro%G=*h>(# z*FUUXqG9i8-;FO`+CJgt<33Vbc&VW`uY{HWv^}XcqR=#lVUW^EGTvv=8mYC`KMG@- zCSM95d_Ol&+&?4@A=kO%0JIL>>T*8%+(}}s^=WwV=UYxP?IEOFT3U{-bS2b=;hgh5 z!KFvA_72h0+obi0wcb$g3d+5M#P1MKtsyk8ExNaJ&X>;l|2REzHAw3dsC~uVEA7ow zwhAEkKSxh-TQ}UC7wwPZeU{qE!VRxo4Qsc^mfJ@5xeXk85e000000000000000 f000000DJNaoeVRA5@15q00000NkvXXu0mjfjoYSb literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/crumbly-floor/fall4.png b/dev-assets/doodads/crumbly-floor/fall4.png new file mode 100644 index 0000000000000000000000000000000000000000..97e8ee0a411680b6958eda2260f596436505919f GIT binary patch literal 855 zcmV-d1E~CoP)EX>4Tx04R}tkv&MmKpe$iTcsiu2P;Ss$xxjvh+jBr6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfadlF3krMxx6k5c1aNLh~_a1le0DryARI_6YP&La) zCE`LRyD9`<5yBAq5y6ziOnpuilkgm0_we!cF2=LG&;2=il$^-`pFljzbi*RvAfDc| zbk6(4VOEqB;&b9rgDyz?$aUG}H_ioz{X8>lq*L?6VPc`s#&R38qM;H`5=RwPqkMnH zWrgz=XSG~q&3p0}hH~1 zax9<%6_Voz|AXJ%n)!)wHz^bcI$v!2V;BhT0*#t&e;?a+;{*si16NwhU#SB#pQP7X zTJ#9$-3BhMTbjHFTnGy0}1(02=TuerT7_i_3Fq^Yaq4RCM> zj1(w)-Q(R|?Y;ebrrF;Q%8GKzf;1Ex00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru;|v}b2Lh24Og{hs02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00A#aL_t(|+U?lQ5rQxfhT&ivwBV78MNo$6z$&O9*b+zk zSx?R|3MQK@QS^OgyoeK$FPopyIRIEOO_R(&^Jm_B@!m^LyTkb?!}%!MzASwoLJ&>c z{i&7y)Jkgm@rScOQeLzcNNy0%0ce8Ij!_lB+KVhgh*g0mj7R^91AuZ?Q}>V53fj_P zivXN+ZXCyEPeBw_!crLX!D~+x?e9On#&efL%4w1s###umBmHFpOLpvB??Q+LkUIOx zRiz0ac0|>@)LB;Z!RidJSvg(THC91fuD2#@@Y;&7s36U)baS->uo!&tBv_`O+Zn4q zWQXxpWmtPMer@3DYqKVR=l>o`c5SN-w8~}1^85}Vh~@obw-sz%0spj=0RR9100000 h000000002+3-0%??gF?RbwU6D002ovPDHLkV1jpoZPEY$ literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/crumbly-floor/fallen.png b/dev-assets/doodads/crumbly-floor/fallen.png new file mode 100644 index 0000000000000000000000000000000000000000..dd5e57f357fc9decc291d98dd48998b8ceaeb5f5 GIT binary patch literal 652 zcmeAS@N?(olHy`uVBq!ia0vp^jv&mz1|<8wpLAtlU~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJhrAngg+8%-@1Lo><#9L&Eb_FP2;aY1oBjy*Yuhb+Farc8;A3Qjo zboJoT$Z+;>M}a>Kg@vU-B@>1IPZxKe)^6_t2Xe zqiRs3vw%r-(qs1j&G+xl`q^1%t|`K96`%7vS%TvoQ}3+YdimRBk6Acun8Tw^$FF2I zp1c zvU2qk-hu*-9ubY|yN~b2zTNw`dfCnU{HMB#PBoc`NCTsVEy>&6h2cL4F4((#GEjuG zz$3Dlfq`2Xgc%uT&5-~KvX^-Jy0Sm!krUVE@c3}9A1EYQ;u=xnoS&PUnpeW$T$Gwv zlA5AWo>`Ki;O^-g5Z=fq4pfxl>EaktaqH~`MnMJ!78Zjq@Bb88M7NYqx>6kMF+stx giG@>0g)&B7w7NMF`#`6p00i_>zopr0Q(R6EdT%j literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/crumbly-floor/floor.png b/dev-assets/doodads/crumbly-floor/floor.png new file mode 100644 index 0000000000000000000000000000000000000000..f89f16ea50e7f622b27a4a049e4e6a6db588c749 GIT binary patch literal 868 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 z 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 XZ-#CO$x|&V00000NkvXXu0mjf-qJT! literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/on-off/blue-off.png b/dev-assets/doodads/on-off/blue-off.png new file mode 100644 index 0000000000000000000000000000000000000000..6275e2b86b300a6edcee0eb6ce5994a426b301bf GIT binary patch literal 648 zcmV;30(bq1P)EX>4Tx04R}tkv&MmKpe$iTcsiu2P;Ss$xxjvh+jBr6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfadlF3krMxx6k5c1aNLh~_a1le0DryARI_6YP&La) zCE`LRyD9`<5yBAq5y6ziOnpuilkgm0_we!cF2=LG&;2=il$^-`pFljzbi*RvAfDc| zbk6(4VOEqB;&b9rgDyz?$aUG}H_ioz{X8>lq*L?6VPc`s#&R38qM;H`5=RwPqkMnH zWrgz=XSG~q&3p0}hH~1 zax9<%6_Voz|AXJ%n)!)wHz^bcI$v!2V;BhT0*#t&e;?a+;{*si16NwhU#SB#pQP7X zTJ#9$-3BhMTbjHFTnGy0}1(02=TuerT7_i_3Fq^Yaq4RCM> zj1(w)-Q(R|?Y;ebrrF;Q%8GKzf;1Ex00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru;|v}a4i_xRd8z;a02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{003Y~L_t(o!|jwY4uCKW1MPgaZ{oFOX$PQ;m=1SaqDYM* zU#-c}yPAEeCtvkPU5~*e0hJOB7)M?7$Ad4(tHzzz&Gp5$sn~ i%N5P8`2TQQK$1@5h(;1h{|PVv0000EX>4Tx04R}tkv&MmKpe$iTcsiu2P;Ss$xxjvh+jBr6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfadlF3krMxx6k5c1aNLh~_a1le0DryARI_6YP&La) zCE`LRyD9`<5yBAq5y6ziOnpuilkgm0_we!cF2=LG&;2=il$^-`pFljzbi*RvAfDc| zbk6(4VOEqB;&b9rgDyz?$aUG}H_ioz{X8>lq*L?6VPc`s#&R38qM;H`5=RwPqkMnH zWrgz=XSG~q&3p0}hH~1 zax9<%6_Voz|AXJ%n)!)wHz^bcI$v!2V;BhT0*#t&e;?a+;{*si16NwhU#SB#pQP7X zTJ#9$-3BhMTbjHFTnGy0}1(02=TuerT7_i_3Fq^Yaq4RCM> zj1(w)-Q(R|?Y;ebrrF;Q%8GKzf;1Ex00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru;|v}a4mgJxMDqXu02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{004tYL_t(o!|j;S4S*mJLwkft&RHDJnRJ$01wV|4A1YDf z5z~H?-U~FOAXqh~gs;_$0D!mw6d;lT%mKibp#cr335a;MO*^UH-tH@(cxX^A)(4rX z@L?t@j^m{X6+XNt7)(MCf)IpYvtqMivtqOQWhS;)`D8u>Lnop9C-49OfElcKDb6dC R_0RwS002ovPDHLkV1h1D7g+!R literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/on-off/orange-button.png b/dev-assets/doodads/on-off/orange-button.png new file mode 100644 index 0000000000000000000000000000000000000000..ec5ef16c1d141d60d8c2b02487901a69048a07b3 GIT binary patch literal 751 zcmVEX>4Tx04R}tkv&MmKpe$iTcsiu2P;Ss$xxjvh+jBr6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfadlF3krMxx6k5c1aNLh~_a1le0DryARI_6YP&La) zCE`LRyD9`<5yBAq5y6ziOnpuilkgm0_we!cF2=LG&;2=il$^-`pFljzbi*RvAfDc| zbk6(4VOEqB;&b9rgDyz?$aUG}H_ioz{X8>lq*L?6VPc`s#&R38qM;H`5=RwPqkMnH zWrgz=XSG~q&3p0}hH~1 zax9<%6_Voz|AXJ%n)!)wHz^bcI$v!2V;BhT0*#t&e;?a+;{*si16NwhU#SB#pQP7X zTJ#9$-3BhMTbjHFTnGy0}1(02=TuerT7_i_3Fq^Yaq4RCM> zj1(w)-Q(R|?Y;ebrrF;Q%8GKzf;1Ex00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru;|v}a5Df+DzYhQa02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{0075HL_t(o!|hjX3d0}_eNT20cR0_~v+OMOD%p>zq);|( z>$H%7peZ!Zmrsc#vOwqst!C;(a>nIcWwzl3o`amQ$x0>l(C zVeYW(xOa9iY|8EKw_5!nCNzM(^Dq5rcj(*Xbu|!2FPj#7vMV-c2jki&Fk9J}qgY*C zq-6OpQNxD#XB`td5cSW$`M)(b+>wB*G13Brc!^l=u*u3ME1Rq=JE|(CejO(t>E_Z~ h&j7l-8Nxua`vwB$aRbfzH+292002ovPDHLkV1nUJK8yeW literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/on-off/orange-off.png b/dev-assets/doodads/on-off/orange-off.png new file mode 100644 index 0000000000000000000000000000000000000000..1ce8f1ebe33526d52aa9300fa43a7229ca69b95b GIT binary patch literal 650 zcmV;50(Jd~P)EX>4Tx04R}tkv&MmKpe$iTcsiu2P;Ss$xxjvh+jBr6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfadlF3krMxx6k5c1aNLh~_a1le0DryARI_6YP&La) zCE`LRyD9`<5yBAq5y6ziOnpuilkgm0_we!cF2=LG&;2=il$^-`pFljzbi*RvAfDc| zbk6(4VOEqB;&b9rgDyz?$aUG}H_ioz{X8>lq*L?6VPc`s#&R38qM;H`5=RwPqkMnH zWrgz=XSG~q&3p0}hH~1 zax9<%6_Voz|AXJ%n)!)wHz^bcI$v!2V;BhT0*#t&e;?a+;{*si16NwhU#SB#pQP7X zTJ#9$-3BhMTbjHFTnGy0}1(02=TuerT7_i_3Fq^Yaq4RCM> zj1(w)-Q(R|?Y;ebrrF;Q%8GKzf;1Ex00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru;|v}a4<=*`NYMZQ02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{003f1L_t(o!|jwo3cw%?1ULIx-_+OIONAo#*ww?#$ppa= zb}T*gOTC)vU61o${ZV_qm}I=`T?4E(1xHX{=}l(ACv3um0k8u*06VY)umd|FZAY^2 kq?XGqG?ml;!#sc_-4l&Q5=FR-6#xJL07*qoM6N<$f-RX0kpKVy literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/on-off/orange-on.png b/dev-assets/doodads/on-off/orange-on.png new file mode 100644 index 0000000000000000000000000000000000000000..c56e09ea48740edbc0566598f7bc56349fb234f1 GIT binary patch literal 687 zcmV;g0#N;lP)EX>4Tx04R}tkv&MmKpe$iTcsiu2P;Ss$xxjvh+jBr6^c+H)C#RSm|Xe=O&XFE z7e~Rh;NZt%)xpJCR|i)?5c~jfadlF3krMxx6k5c1aNLh~_a1le0DryARI_6YP&La) zCE`LRyD9`<5yBAq5y6ziOnpuilkgm0_we!cF2=LG&;2=il$^-`pFljzbi*RvAfDc| zbk6(4VOEqB;&b9rgDyz?$aUG}H_ioz{X8>lq*L?6VPc`s#&R38qM;H`5=RwPqkMnH zWrgz=XSG~q&3p0}hH~1 zax9<%6_Voz|AXJ%n)!)wHz^bcI$v!2V;BhT0*#t&e;?a+;{*si16NwhU#SB#pQP7X zTJ#9$-3BhMTbjHFTnGy0}1(02=TuerT7_i_3Fq^Yaq4RCM> zj1(w)-Q(R|?Y;ebrrF;Q%8GKzf;1Ex00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru;|v}a4>JkTtU3Sy02y>eSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{004(cL_t(o!|j;S4S*mFM6ZNN>MRcDOgamz;D?0N4}}l| z#N2)GMcS8gAqRq_xFe#oWQ72@mJt9JL^43=4-isMhY2(Vgnt{8m1_IAOLhEHf!k(n z_e@P4dZKt7(2qJ))ZslrV-kW8gdhZy6_XW{6_eF3J&|AK;TQw3Y!GMv2}S?_zzN9y VDbBO|<5>Uz002ovPDHLkV1kX=7@q(D literal 0 HcmV?d00001 diff --git a/dev-assets/doodads/on-off/state-block-blue.js b/dev-assets/doodads/on-off/state-block-blue.js new file mode 100644 index 0000000..0b287e3 --- /dev/null +++ b/dev-assets/doodads/on-off/state-block-blue.js @@ -0,0 +1,28 @@ +// Blue State Block +function main() { + Self.SetHitbox(0, 0, 33, 33); + + // Blue block is ON by default. + var state = true; + + Message.Subscribe("broadcast:state-change", function(newState) { + state = !newState; + console.warn("BLUE BLOCK Received state=%+v, set mine to %+v", newState, state); + + // Layer 0: ON + // Layer 1: OFF + if (state) { + Self.ShowLayer(0); + } else { + Self.ShowLayer(1); + } + }); + + Events.OnCollide(function(e) { + if (e.Actor.IsMobile() && e.InHitbox) { + if (state) { + return false; + } + } + }); +} diff --git a/dev-assets/doodads/on-off/state-block-orange.js b/dev-assets/doodads/on-off/state-block-orange.js new file mode 100644 index 0000000..634d5e8 --- /dev/null +++ b/dev-assets/doodads/on-off/state-block-orange.js @@ -0,0 +1,28 @@ +// Orange State Block +function main() { + Self.SetHitbox(0, 0, 33, 33); + + // Orange block is OFF by default. + var state = false; + + Message.Subscribe("broadcast:state-change", function(newState) { + state = newState; + console.warn("ORANGE BLOCK Received state=%+v, set mine to %+v", newState, state); + + // Layer 0: OFF + // Layer 1: ON + if (state) { + Self.ShowLayer(1); + } else { + Self.ShowLayer(0); + } + }); + + Events.OnCollide(function(e) { + if (e.Actor.IsMobile() && e.InHitbox) { + if (state) { + return false; + } + } + }); +} diff --git a/dev-assets/doodads/on-off/state-button.js b/dev-assets/doodads/on-off/state-button.js new file mode 100644 index 0000000..51a2af3 --- /dev/null +++ b/dev-assets/doodads/on-off/state-button.js @@ -0,0 +1,47 @@ +// State Block Control Button +function main() { + console.log("%s initialized!", Self.Doodad.Title); + Self.SetHitbox(0, 0, 33, 33); + + // When the button is activated, don't keep toggling state until we're not + // being touched again. + var colliding = false; + + // Button is "OFF" by default. + var state = false; + + Events.OnCollide(function(e) { + if (colliding) { + return false; + } + + // Only trigger for mobile characters. + if (e.Actor.IsMobile()) { + console.log("Mobile actor %s touched the on/off button!", e.Actor.Actor.Filename); + + // Only activate if touched from the bottom or sides. + if (e.Overlap.Y === 0) { + console.log("... but touched the top!"); + return false; + } + + colliding = true; + console.log(" -> emit state change"); + state = !state; + Message.Broadcast("broadcast:state-change", state); + + if (state) { + Self.ShowLayer(1); + } else { + Self.ShowLayer(0); + } + } + + // Always a solid button. + return false; + }); + + Events.OnLeave(function(e) { + colliding = false; + }) +} diff --git a/docs/Shell.md b/docs/Shell.md index 1e92b2f..334eab4 100644 --- a/docs/Shell.md +++ b/docs/Shell.md @@ -6,6 +6,9 @@ * `don't edit and drive` - enable editing while playing a level. * `scroll scroll scroll your boat` - enable scrolling the level with arrow keys while playing a level. +* `import antigravity` - during Play Mode, disables gravity for the player + character and allows free movement in all directions with the arrow keys. + Enter the cheat again to restore gravity to normal. ## Bool Props diff --git a/pkg/collision/actors_test.go b/pkg/collision/actors_test.go index d7e71f2..1493786 100644 --- a/pkg/collision/actors_test.go +++ b/pkg/collision/actors_test.go @@ -3,8 +3,8 @@ package collision_test import ( "testing" - "git.kirsle.net/go/render" "git.kirsle.net/apps/doodle/pkg/collision" + "git.kirsle.net/go/render" ) func TestActorCollision(t *testing.T) { diff --git a/pkg/commands.go b/pkg/commands.go index 4a134cd..57242a2 100644 --- a/pkg/commands.go +++ b/pkg/commands.go @@ -49,6 +49,20 @@ func (c Command) Run(d *Doodle) error { d.Flash("Use this cheat in Play Mode to make the level scrollable.") } return nil + } else if c.Raw == "import antigravity" { + if playScene, ok := d.Scene.(*PlayScene); ok { + playScene.antigravity = !playScene.antigravity + playScene.Player.SetGravity(!playScene.antigravity) + + if playScene.antigravity { + d.Flash("Gravity disabled for player character.") + } else { + d.Flash("Gravity restored for player character.") + } + } else { + d.Flash("Use this cheat in Play Mode to disable gravity for the player character.") + } + return nil } switch c.Command { diff --git a/pkg/doodads/actor.go b/pkg/doodads/actor.go index d970db2..8e8891e 100644 --- a/pkg/doodads/actor.go +++ b/pkg/doodads/actor.go @@ -25,8 +25,11 @@ type Actor interface { MoveTo(render.Point) // Set current Position to {X,Y}. } -// GetBoundingRect computes the full pairs of points for the collision box -// around a doodad actor. +// GetBoundingRect computes the full pairs of points for the bounding box of +// the actor. +// +// The X,Y coordinates are the position in the level of the actor, +// The W,H are the size of the actor's drawn box. func GetBoundingRect(d Actor) render.Rect { var ( P = d.Position() @@ -40,6 +43,27 @@ func GetBoundingRect(d Actor) render.Rect { } } +// GetBoundingRectHitbox returns the bounding rect of the Actor taking into +// account their self-declared collision hitbox. +// +// The rect returned has the X,Y coordinate set to the actor's position, plus +// the X,Y of their hitbox, if any. +// +// The W,H of the rect is the W,H of their declared hitbox. +// +// If the actor has NOT declared its hitbox, this function returns exactly the +// same way as GetBoundingRect() does. +func GetBoundingRectHitbox(d Actor, hitbox render.Rect) render.Rect { + rect := GetBoundingRect(d) + if !hitbox.IsZero() { + rect.X += hitbox.X + rect.Y += hitbox.Y + rect.W = hitbox.W + rect.H = hitbox.H + } + return rect +} + // GetBoundingRectWithHitbox is like GetBoundingRect but adjusts it for the // relative hitbox of the actor. // func GetBoundingRectWithHitbox(d Actor, hitbox render.Rect) render.Rect { diff --git a/pkg/doodads/drawing.go b/pkg/doodads/drawing.go index 43515c2..e576926 100644 --- a/pkg/doodads/drawing.go +++ b/pkg/doodads/drawing.go @@ -76,21 +76,6 @@ func (d *Drawing) SetGrounded(v bool) { d.grounded = v } -// // SetHitbox sets the actor's elected hitbox. -// func (d *Drawing) SetHitbox(x, y, w, h int) { -// d.hitbox = render.Rect{ -// X: int32(x), -// Y: int32(y), -// W: int32(w), -// H: int32(h), -// } -// } -// -// // Hitbox returns the actor's elected hitbox. -// func (d *Drawing) Hitbox() render.Rect { -// return d.hitbox -// } - // MoveBy a relative value. func (d *Drawing) MoveBy(by render.Point) { d.point.Add(by) diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 640a80e..7fcfc35 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -513,7 +513,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame { u.Supervisor.Add(w) frame.Pack(w, ui.Pack{ Side: ui.W, - PadX: 1, + PadX: 1, }) } @@ -547,7 +547,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame { label.Compute(d.Engine) frame.Pack(label, ui.Pack{ Side: ui.W, - PadX: 1, + PadX: 1, }) if labelHeight == 0 { diff --git a/pkg/editor_ui_doodad.go b/pkg/editor_ui_doodad.go index fa80908..6b4046b 100644 --- a/pkg/editor_ui_doodad.go +++ b/pkg/editor_ui_doodad.go @@ -111,8 +111,8 @@ func (u *EditorUI) setupDoodadFrame(e render.Engine, window *ui.Window) (*ui.Fra u.doodadPager = pager frame.Pack(pager, ui.Pack{ Side: ui.N, - Fill: true, - PadY: 5, + Fill: true, + PadY: 5, }) doodadsAvailable, err := doodads.ListDoodads() @@ -161,7 +161,7 @@ func (u *EditorUI) setupDoodadFrame(e render.Engine, window *ui.Window) (*ui.Fra btnRows = append(btnRows, row) frame.Pack(row, ui.Pack{ Side: ui.N, - Fill: true, + Fill: true, }) } diff --git a/pkg/editor_ui_palette.go b/pkg/editor_ui_palette.go index 3f29080..5fb0a87 100644 --- a/pkg/editor_ui_palette.go +++ b/pkg/editor_ui_palette.go @@ -3,10 +3,10 @@ package doodle import ( "fmt" - "git.kirsle.net/go/render" - "git.kirsle.net/go/ui" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" ) // SetupPalette sets up the palette panel. @@ -33,7 +33,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { u.DoodadTab.Hide() window.Pack(u.DoodadTab, ui.Pack{ Side: ui.N, - Fill: true, + Fill: true, }) } @@ -41,7 +41,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { u.PaletteTab = u.setupPaletteFrame(window) window.Pack(u.PaletteTab, ui.Pack{ Side: ui.N, - Fill: true, + Fill: true, }) return window @@ -106,8 +106,8 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame { frame.Pack(btn, ui.Pack{ Side: ui.N, - Fill: true, - PadY: 4, + Fill: true, + PadY: 4, }) } } diff --git a/pkg/editor_ui_toolbar.go b/pkg/editor_ui_toolbar.go index 7402ae7..de279f2 100644 --- a/pkg/editor_ui_toolbar.go +++ b/pkg/editor_ui_toolbar.go @@ -147,14 +147,14 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { btnFrame.Pack(btn, ui.Pack{ Side: ui.N, - PadY: 2, + PadY: 2, }) } // Spacer frame. frame.Pack(ui.NewFrame("spacer"), ui.Pack{ Side: ui.N, - PadY: 8, + PadY: 8, }) // "Brush Size" label @@ -171,7 +171,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { sizeFrame := ui.NewFrame("Brush Size Frame") frame.Pack(sizeFrame, ui.Pack{ Side: ui.N, - PadY: 0, + PadY: 0, }) sizeLabel := ui.NewLabel(ui.Label{ @@ -184,15 +184,15 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame { Background: render.Grey, }) sizeFrame.Pack(sizeLabel, ui.Pack{ - Side: ui.N, - FillX: true, - PadY: 2, + Side: ui.N, + FillX: true, + PadY: 2, }) sizeBtnFrame := ui.NewFrame("Size Increment Button Frame") sizeFrame.Pack(sizeBtnFrame, ui.Pack{ - Side: ui.N, - FillX: true, + Side: ui.N, + FillX: true, }) var incButtons = []struct { diff --git a/pkg/guitest_scene.go b/pkg/guitest_scene.go index da07136..ff1827d 100644 --- a/pkg/guitest_scene.go +++ b/pkg/guitest_scene.go @@ -54,7 +54,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { }) window.Pack(titleBar, ui.Pack{ Side: ui.N, - Fill: true, + Fill: true, }) // Window Body @@ -63,7 +63,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { Background: render.Yellow, }) window.Pack(body, ui.Pack{ - Side: ui.N, + Side: ui.N, Expand: true, }) s.body = body @@ -77,8 +77,8 @@ func (s *GUITestScene) Setup(d *Doodle) error { Width: 100, }) body.Pack(leftFrame, ui.Pack{ - Side: ui.W, - FillY: true, + Side: ui.W, + FillY: true, }) // Some left frame buttons. @@ -92,9 +92,9 @@ func (s *GUITestScene) Setup(d *Doodle) error { }) s.Supervisor.Add(btn) leftFrame.Pack(btn, ui.Pack{ - Side: ui.N, - FillX: true, - PadY: 2, + Side: ui.N, + FillX: true, + PadY: 2, }) } @@ -105,7 +105,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { BorderSize: 0, }) body.Pack(frame, ui.Pack{ - Side: ui.W, + Side: ui.W, Expand: true, Fill: true, }) @@ -120,7 +120,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { }) body.Pack(rightFrame, ui.Pack{ Side: ui.W, - Fill: true, + Fill: true, }) // A grid of buttons. @@ -142,7 +142,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { d.Flash("%s clicked", btn) }) rowFrame.Pack(btn, ui.Pack{ - Side: ui.W, + Side: ui.W, Expand: true, FillX: true, }) @@ -151,7 +151,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { } rightFrame.Pack(rowFrame, ui.Pack{ Side: ui.N, - Fill: true, + Fill: true, }) } @@ -163,7 +163,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { Color: render.Black, }, }), ui.Pack{ - Side: ui.NW, + Side: ui.NW, Padding: 2, }) @@ -175,7 +175,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { }), ) frame.Pack(cb, ui.Pack{ - Side: ui.NW, + Side: ui.NW, Padding: 4, }) cb.Supervise(s.Supervisor) @@ -187,7 +187,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { Color: render.Red, }, }), ui.Pack{ - Side: ui.SE, + Side: ui.SE, Padding: 8, }) frame.Pack(ui.NewLabel(ui.Label{ @@ -197,7 +197,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { Color: render.Blue, }, }), ui.Pack{ - Side: ui.SE, + Side: ui.SE, Padding: 8, }) @@ -233,11 +233,11 @@ func (s *GUITestScene) Setup(d *Doodle) error { var align = ui.W btnFrame.Pack(button1, ui.Pack{ - Side: align, + Side: align, Padding: 20, }) btnFrame.Pack(button2, ui.Pack{ - Side: align, + Side: align, Padding: 20, }) diff --git a/pkg/level/chunk_test.go b/pkg/level/chunk_test.go index c08b0c2..ab7b147 100644 --- a/pkg/level/chunk_test.go +++ b/pkg/level/chunk_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - "git.kirsle.net/go/render" "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/go/render" ) // Test the high level Chunker. diff --git a/pkg/main_scene.go b/pkg/main_scene.go index 9ba096e..607c4b8 100644 --- a/pkg/main_scene.go +++ b/pkg/main_scene.go @@ -68,7 +68,7 @@ func (s *MainScene) Setup(d *Doodle) error { s.Supervisor.Add(btn) frame.Pack(btn, ui.Pack{ Side: ui.N, - PadY: 8, + PadY: 8, // Fill: true, FillX: true, }) diff --git a/pkg/play_scene.go b/pkg/play_scene.go index 76c220c..bd29807 100644 --- a/pkg/play_scene.go +++ b/pkg/play_scene.go @@ -50,7 +50,8 @@ type PlayScene struct { // Player character Player *uix.Actor - playerJumpCounter int // limit jump length + antigravity bool // Cheat: disable player gravity + playerJumpCounter int // limit jump length } // Name of the scene. @@ -154,21 +155,7 @@ func (s *PlayScene) Setup(d *Doodle) error { } // Load in the player character. - player, err := doodads.LoadFile("azu-blu.doodad") - if err != nil { - log.Error("PlayScene.Setup: failed to load player doodad: %s", err) - player = doodads.NewDummy(32) - } - - s.Player = uix.NewActor("PLAYER", &level.Actor{}, player) - s.Player.MoveTo(render.NewPoint(128, 128)) - s.drawing.AddActor(s.Player) - s.drawing.FollowActor = s.Player.ID() - - // Set up the player character's script in the VM. - if err := s.scripting.AddLevelScript(s.Player.ID()); err != nil { - log.Error("PlayScene.Setup: scripting.InstallActor(player) failed: %s", err) - } + s.setupPlayer() // Run all the actor scripts' main() functions. if err := s.drawing.InstallScripts(); err != nil { @@ -186,6 +173,60 @@ func (s *PlayScene) Setup(d *Doodle) error { return nil } +// setupPlayer creates and configures the Player Character in the level. +func (s *PlayScene) setupPlayer() { + // Load in the player character. + player, err := doodads.LoadFile("azu-blu.doodad") + if err != nil { + log.Error("PlayScene.Setup: failed to load player doodad: %s", err) + player = doodads.NewDummy(32) + } + + // Find the spawn point of the player. Search the level for the + // "start-flag.doodad" + var ( + spawn render.Point + flagCount int + ) + for actorID, actor := range s.Level.Actors { + if actor.Filename == "start-flag.doodad" { + if flagCount > 1 { + break + } + + // TODO: start-flag.doodad is 86x86 pixels but we can't tell that + // from right here. + size := render.NewRect(86, 86) + log.Info("Found start-flag.doodad at %s (ID %s)", actor.Point, actorID) + spawn = render.NewPoint( + // X: centered inside the flag. + actor.Point.X+(size.W/2)-(player.Layers[0].Chunker.Size/2), + + // Y: the bottom of the flag, 4 pixels from the floor. + actor.Point.Y+size.H-4-(player.Layers[0].Chunker.Size), + ) + flagCount++ + } + } + + // Surface warnings around the spawn flag. + if flagCount == 0 { + s.d.Flash("Warning: this level contained no Start Flag.") + } else if flagCount > 1 { + s.d.Flash("Warning: this level contains multiple Start Flags. Player spawn point is ambiguous.") + } + + s.Player = uix.NewActor("PLAYER", &level.Actor{}, player) + s.Player.MoveTo(spawn) + s.drawing.AddActor(s.Player) + s.drawing.FollowActor = s.Player.ID() + + // Set up the player character's script in the VM. + if err := s.scripting.AddLevelScript(s.Player.ID()); err != nil { + log.Error("PlayScene.Setup: scripting.InstallActor(player) failed: %s", err) + } +} + // SetupAlertbox configures the alert box UI. func (s *PlayScene) SetupAlertbox() { window := ui.NewWindow("Level Completed") @@ -393,31 +434,24 @@ func (s *PlayScene) movePlayer(ev *event.State) { if ev.Right { velocity.X = playerSpeed } - if ev.Up && (s.Player.Grounded() || s.playerJumpCounter >= 0) { + if ev.Up && (s.Player.Grounded() || s.playerJumpCounter >= 0 || s.antigravity) { velocity.Y = -playerSpeed if s.Player.Grounded() { s.playerJumpCounter = 20 } } + if ev.Down && s.antigravity { + velocity.Y = playerSpeed + } if !s.Player.Grounded() { s.playerJumpCounter-- } - // // Apply gravity if not grounded. - // if !s.Player.Grounded() { - // // Gravity has to pipe through the collision checker, too, so it - // // can't give us a cheated downward boost. - // velocity.Y += gravity - // } - s.Player.SetVelocity(velocity) - // TODO: invoke the player OnKeypress for animation testing - // if velocity != render.Origin { s.scripting.To(s.Player.ID()).Events.RunKeypress(ev) - // } } // Drawing returns the private world drawing, for debugging with the console. diff --git a/pkg/scene.go b/pkg/scene.go index 4353a65..45cb452 100644 --- a/pkg/scene.go +++ b/pkg/scene.go @@ -1,8 +1,8 @@ package doodle import ( - "git.kirsle.net/go/render/event" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/go/render/event" ) // Scene is an abstraction for a game mode in Doodle. The app points to one diff --git a/pkg/scripting/pubsub.go b/pkg/scripting/pubsub.go index 8cabf4d..407f22e 100644 --- a/pkg/scripting/pubsub.go +++ b/pkg/scripting/pubsub.go @@ -18,7 +18,7 @@ RegisterPublishHooks adds the pub/sub hooks to a JavaScript VM. This adds the global methods `Message.Subscribe(name, func)` and `Message.Publish(name, args)` to the JavaScript VM's scope. */ -func RegisterPublishHooks(vm *VM) { +func RegisterPublishHooks(s *Supervisor, vm *VM) { // Goroutine to watch the VM's inbound channel and invoke Subscribe handlers // for any matching messages received. go func() { @@ -47,7 +47,6 @@ func RegisterPublishHooks(vm *VM) { }, "Publish": func(name string, v ...interface{}) { - log.Error("PUBLISH: %s %+v", name, v) for _, channel := range vm.Outbound { channel <- Message{ Name: name, @@ -55,5 +54,15 @@ func RegisterPublishHooks(vm *VM) { } } }, + + "Broadcast": func(name string, v ...interface{}) { + // Send the message to all actor VMs. + for _, vm := range s.scripts { + vm.Inbound <- Message{ + Name: name, + Args: v, + } + } + }, }) } diff --git a/pkg/scripting/scripting.go b/pkg/scripting/scripting.go index 3bad626..bb91697 100644 --- a/pkg/scripting/scripting.go +++ b/pkg/scripting/scripting.go @@ -77,7 +77,7 @@ func (s *Supervisor) AddLevelScript(id string) error { } s.scripts[id] = NewVM(id) - RegisterPublishHooks(s.scripts[id]) + RegisterPublishHooks(s, s.scripts[id]) RegisterEventHooks(s, s.scripts[id]) if err := s.scripts[id].RegisterLevelHooks(); err != nil { return err diff --git a/pkg/scripting/vm.go b/pkg/scripting/vm.go index 57cf67f..21f78f5 100644 --- a/pkg/scripting/vm.go +++ b/pkg/scripting/vm.go @@ -5,9 +5,9 @@ import ( "reflect" "time" - "git.kirsle.net/go/render" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/shmem" + "git.kirsle.net/go/render" "github.com/robertkrimen/otto" ) diff --git a/pkg/sprites/sprites.go b/pkg/sprites/sprites.go index f8c70ad..cbf40b9 100644 --- a/pkg/sprites/sprites.go +++ b/pkg/sprites/sprites.go @@ -8,11 +8,11 @@ import ( "os" "runtime" - "git.kirsle.net/go/render" - "git.kirsle.net/go/ui" "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/wasm" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" ) // LoadImage loads a sprite as a ui.Image object. It checks Doodle's embedded diff --git a/pkg/uix/actor.go b/pkg/uix/actor.go index 23d56d4..28ddc2a 100644 --- a/pkg/uix/actor.go +++ b/pkg/uix/actor.go @@ -6,6 +6,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/go/render" "github.com/google/uuid" "github.com/robertkrimen/otto" @@ -29,6 +30,7 @@ type Actor struct { // Actor runtime variables. hasGravity bool + isMobile bool // Mobile character, such as the player or an enemy hitbox render.Rect data map[string]string @@ -75,6 +77,18 @@ func (a *Actor) SetGravity(v bool) { a.hasGravity = v } +// SetMobile configures whether the actor is a mobile character (i.e. is the +// player or a mobile enemy). Mobile characters can set off certain traps when +// touched but non-mobile actors don't set each other off if touching. +func (a *Actor) SetMobile(v bool) { + a.isMobile = v +} + +// IsMobile returns whether the actor is a mobile character. +func (a *Actor) IsMobile() bool { + return a.isMobile +} + // GetBoundingRect gets the bounding box of the actor's doodad. func (a *Actor) GetBoundingRect() render.Rect { return doodads.GetBoundingRect(a) @@ -133,6 +147,21 @@ func (a *Actor) ShowLayer(index int) error { return nil } +// ShowLayerNamed sets the actor's ActiveLayer to the one named. +func (a *Actor) ShowLayerNamed(name string) error { + // Find the layer. + for i, layer := range a.Doodad.Layers { + if layer.Name == name { + return a.ShowLayer(i) + } + } + log.Warn("Actor(%s) ShowLayerNamed(%s): layer not found", + a.Actor.Filename, + name, + ) + return fmt.Errorf("the layer named %s was not found", name) +} + // Destroy deletes the actor from the running level. func (a *Actor) Destroy() { a.flagDestroy = true diff --git a/pkg/uix/actor_collision.go b/pkg/uix/actor_collision.go index 31c38d1..a2fda38 100644 --- a/pkg/uix/actor_collision.go +++ b/pkg/uix/actor_collision.go @@ -46,13 +46,18 @@ func (w *Canvas) loopActorCollision() error { // Advance any animations for this actor. if a.activeAnimation != nil && a.activeAnimation.nextFrameAt.Before(now) { if done := a.TickAnimation(a.activeAnimation); done { - // Animation has finished, run the callback script. - if a.animationCallback.IsFunction() { - a.animationCallback.Call(otto.NullValue()) + // Animation has finished, get the callback function. + callback := a.animationCallback + + // Clean up the animation state, in case the callback wants + // to immediately play another animation. + a.StopAnimation() + + // Call the callback function. + if callback.IsFunction() { + callback.Call(otto.NullValue()) } - // Clean up the animation state. - a.StopAnimation() } } @@ -104,7 +109,13 @@ func (w *Canvas) loopActorCollision() error { if w.scripting != nil { var ( rect = doodads.GetBoundingRect(b) - lastGoodBox = boxes[tuple.B] // worst case scenario we get blocked right away + lastGoodBox = render.Rect{ + X: originalPositions[b.ID()].X, + Y: originalPositions[b.ID()].Y, + W: boxes[tuple.B].W, + H: boxes[tuple.B].H, + } + // lastGoodBox = originalPositions[b.ID()] // boxes[tuple.B] // worst case scenario we get blocked right away ) // Firstly we want to make sure B isn't able to clip through A's @@ -121,7 +132,12 @@ func (w *Canvas) loopActorCollision() error { // B's movement. The next time A does NOT protest, that is to be // B's new position. - var firstPoint = true + // Special case for when a mobile actor lands ON TOP OF a solid + // actor. We want to stop their Y movement downwards, but allow + // horizontal movement on the X axis. + // Touching the solid actor from the side is already fine. + var onTop = false + for point := range render.IterLine( origPoint, b.Position(), @@ -133,6 +149,11 @@ func (w *Canvas) loopActorCollision() error { H: rect.H, } + var ( + lockX bool + lockY bool + ) + if info, err := collision.CompareBoxes(boxes[tuple.A], test); err == nil { // B is overlapping A's box, call its OnCollide handler // with Settled=false and see if it protests the overlap. @@ -145,27 +166,41 @@ func (w *Canvas) loopActorCollision() error { // Did A protest? if err == scripting.ErrReturnFalse { - break + // Are they on top? + if render.AbsInt(lastGoodBox.Y+lastGoodBox.H-boxes[tuple.A].Y) <= 2 { + onTop = true + } + + // What direction were we moving? + if test.Y != lastGoodBox.Y { + lockY = true + b.SetGrounded(true) + } + if test.X != lastGoodBox.X { + if !onTop { + lockX = true + } + } + + // Move them back to the last good box, locking the + // axis they were moving from being able to enter + // this box. + tmp := lastGoodBox + lastGoodBox = test + if lockY { + lastGoodBox.Y = tmp.Y + } + if lockX { + lastGoodBox.X = tmp.X + break + } } else { lastGoodBox = test } } - - firstPoint = false } - // Were we stopped before we even began? - if firstPoint { - // TODO: undo the effect of gravity this tick. Use case: - // the player lands on top of a solid door, and their - // movement is blocked the first step by the door. Originally - // he'd continue falling, so I had to move him up to stop it, - // turns out moving up by the -gravity is exactly the distance - // to go. Don't know why. - b.MoveBy(render.NewPoint(0, -balance.Gravity)) - } else { - b.MoveTo(lastGoodBox.Point()) - } + b.MoveTo(lastGoodBox.Point()) } else { log.Error( "ERROR: Actors %s and %s overlap and the script returned false,"+ diff --git a/pkg/uix/canvas_actors.go b/pkg/uix/canvas_actors.go index 351984e..6ff0ec7 100644 --- a/pkg/uix/canvas_actors.go +++ b/pkg/uix/canvas_actors.go @@ -4,11 +4,11 @@ import ( "errors" "fmt" - "git.kirsle.net/go/render" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/scripting" + "git.kirsle.net/go/render" ) // InstallActors adds external Actors to the canvas to be superimposed on top diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index b345137..bb12858 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -303,7 +303,7 @@ func (w *Canvas) loopEditable(ev *event.State) error { w.OnDragStart(actor.Actor) } break - } else if ev.Button2 { + } else if ev.Button3 { // Right click to delete an actor. deleteActors = append(deleteActors, actor.Actor) } diff --git a/pkg/uix/canvas_strokes.go b/pkg/uix/canvas_strokes.go index 70937ad..aedc6c5 100644 --- a/pkg/uix/canvas_strokes.go +++ b/pkg/uix/canvas_strokes.go @@ -1,13 +1,13 @@ package uix import ( - "git.kirsle.net/go/render" - "git.kirsle.net/go/ui" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/shmem" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" ) // canvas_strokes.go: functions related to drawtool.Stroke and the Canvas. diff --git a/pkg/wallpaper/texture.go b/pkg/wallpaper/texture.go index 9102135..32706a4 100644 --- a/pkg/wallpaper/texture.go +++ b/pkg/wallpaper/texture.go @@ -6,9 +6,9 @@ import ( "errors" "image" - "git.kirsle.net/go/render" "git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/userdir" + "git.kirsle.net/go/render" ) // CornerTexture returns the Texture. diff --git a/pkg/wallpaper/wallpaper.go b/pkg/wallpaper/wallpaper.go index 3f89d46..5f00bd7 100644 --- a/pkg/wallpaper/wallpaper.go +++ b/pkg/wallpaper/wallpaper.go @@ -9,8 +9,8 @@ import ( "runtime" "strings" - "git.kirsle.net/go/render" "git.kirsle.net/apps/doodle/pkg/bindata" + "git.kirsle.net/go/render" ) // Wallpaper is a repeatable background image to go behind levels. diff --git a/wasm/main_wasm.go b/wasm/main_wasm.go index 93eb136..2406e6c 100644 --- a/wasm/main_wasm.go +++ b/wasm/main_wasm.go @@ -7,12 +7,12 @@ import ( "syscall/js" - "git.kirsle.net/go/render" - "git.kirsle.net/go/render/canvas" doodle "git.kirsle.net/apps/doodle/pkg" "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/go/render" + "git.kirsle.net/go/render/canvas" ) func main() {