From 675f71369d2f7adc4baae3ed08841ed38d638417 Mon Sep 17 00:00:00 2001 From: Sean LeBlanc Date: Sat, 5 Sep 2020 11:24:37 -0700 Subject: [PATCH] feat: add support for multiple followers (with option for stack/chain) (#170) adds support for multiple followers, with an option for whether to stack (keep all followers one step behind player) or chain (keep followers one step behind the follower ahead of them in the list of active followers) also fixes an issue in the hack options injection regex in the test code BREAKING CHANGE: this changes the `follower` export to `followers`, since it's now a list --- .../follower-test-js-follower-1-snap.png | Bin 1228 -> 1241 bytes ...est-js-multiple-followers-chain-1-snap.png | Bin 0 -> 1234 bytes ...est-js-multiple-followers-chain-2-snap.png | Bin 0 -> 1234 bytes ...est-js-multiple-followers-stack-1-snap.png | Bin 0 -> 1208 bytes ...est-js-multiple-followers-stack-2-snap.png | Bin 0 -> 1206 bytes src/follower.js | 80 +++++++++---- src/follower.test.js | 110 ++++++++++++++++++ src/test/bitsy.js | 4 +- 8 files changed, 168 insertions(+), 26 deletions(-) create mode 100644 src/__image_snapshots__/follower-test-js-multiple-followers-chain-1-snap.png create mode 100644 src/__image_snapshots__/follower-test-js-multiple-followers-chain-2-snap.png create mode 100644 src/__image_snapshots__/follower-test-js-multiple-followers-stack-1-snap.png create mode 100644 src/__image_snapshots__/follower-test-js-multiple-followers-stack-2-snap.png diff --git a/src/__image_snapshots__/follower-test-js-follower-1-snap.png b/src/__image_snapshots__/follower-test-js-follower-1-snap.png index 11254057eee9d58c17b7dda17184289057e58ebe..3059ab51c6b7d1ad4acb87431fe330aeaa88affe 100644 GIT binary patch delta 22 ecmX@Zd6RR3GB*Qbage(c!@6@aFE=W#W&r?ILkGtI delta 10 Rcmcb~d4_X>@HY3$#R?g1-M=TJ2}?_?yMd-?z^Hc>MM0 z?e*Wq6<%Jx7|&23!`NUz9NKVxdtKA^_3`gMmNJA_{QmiS+2=e4fwIrM4-WsI_TtO4 z2YWuBi?9C|1r$^Rqq6Pg49l0*_CMM4dFkirMa)o(Hk7_+$on(zXyx;{@#lFN4zYky zT>jtNjepG)WRB-EGlfTg3vTd!TXmaLShlvgjJaWhVKqZQ8b8AgKL!OK28EnAatu>$ zGBZpQX5auK>D}xMsoxkHo@$s0K`k-XNZ6p zhsi@#iOL(*2a1BhAD($P{Tchd{d@d=(Z0Ix!JHOP*%%VPF(fclGaM-M|IPOx&hDP- zo4WP~vTW8n7#(h0e#i8dg;rzK?{oBjqzO|eD{{MZ) zKVto#6#u`=pT}}%vNS`>O=gS7E7aGj*G#r$NZ8$8&tCBAQaMwDG{cgc%ngih7#VJJ zGPD4Z?Oizr8)l$ei-xmc1$60fR l2ZcKs!FEAxBt+hjFP0GWoq15P3|L4pc)I$ztaD0e0ssIqlAZtn literal 0 HcmV?d00001 diff --git a/src/__image_snapshots__/follower-test-js-multiple-followers-chain-2-snap.png b/src/__image_snapshots__/follower-test-js-multiple-followers-chain-2-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..e17ec602f7fd2283be6f60c2d9cae00390d7e5a6 GIT binary patch literal 1234 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G!U;i$lZxy-8q?;3=Awio-U3d z6?5L+H7pZxmv9R_*)Cft>-VAUhQP$_jn`G&xg_RWZmC+fs{Z3U-bNvnJ$hR&9DjZK zx_rLA!zKCf|BMZ)1+a>*}j!Mur_nPvgP}lmwy8D7zEV7Xxls41I_a{TfE=%`SnglhZzhY zbmR5n|JEDcv$eebk>~JWOKLo;Mb`D(eluRYlHLL|u=gf&gHbiZfifnBL?BB3&amK& z9K(h=5WP2S88kMtGw7&;&HuLNJ?n@3-w(H}{`YSO%={U)3=#)`I_83PfOse1`Z0N^ zDp7f(`oK{(=)?2wru9rVuipMX?^^TscDU$+n|Bx#fN6#&fuG?<-W%(N^ve3_A1;=2 z+;a?gz{ZgH_!~ps{5{+4>%Mr_ynifXTmJj^oV}h0B)>5(lJ5W873!bnU$-uWpCRJir*ekx^EQ096aRm8XZ8MnFNFWz z+qrnZevKM11AM#vj&V+HQ1zi=R#0M|zMpwV>6f_-A1urnIDiS5`35t?Hem)1Ad=qB z&H&_@34nRKYxo&{*aNi`LDK*Jw{i?MYCxyG)w|DJP~7kun9gqilkxMp<^TTt+zj-o z4;baVk!$#T&bs~|QuZ?|<3CXOd~W=CUWP*~U=$a>?_Wjr6XqXzc00~3e$NHe4$RdI jA1dLw6Ou>qlQ-&pl>~ibJ>@Ncg%pFQtDnm{r-UW|Ctk$k literal 0 HcmV?d00001 diff --git a/src/__image_snapshots__/follower-test-js-multiple-followers-stack-1-snap.png b/src/__image_snapshots__/follower-test-js-multiple-followers-stack-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..9ded9e3126a17c8f019a91c99ac821615250bcd1 GIT binary patch literal 1208 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G!U;i$lZxy-8q?;3=Awqo-U3d z6?5L+HO!N77jX@|^r3GLpZce^EsE~<+V-ef1wCM6lkwfW>i-#Ab&&`b;k$ZfN0)!! zo^Nk=uVKmkb^n+eUUM+Ca1w|9)_oJM`Tx0p{ST`K_L~3C?*~7>%b@V)3-5#F@wyj3 zK6|j|^Evyve}Q=n0%~CNE%iM^-?G~NCwo3G{rtU%xxs)Dgf^7EXYi{#{|Kmj{c|>k zL?$qLpZ>o*VLva&^^dn34ji8LSL8w5?t5j2T*B`!0NRpsMvh_1O=gB|!VDZhB)yxR zA@v(W!&4T9LqK%w9izicTZRNni23V($1?oiCNEcZ^}f70SUph7W_AYV8*l?aJfLyY zAjVhez#g~UlTAb?VMi8Uhw*4{p-hhwkK_WY8C=jF24^{ zxzK($r;Po(Ym@Izp05o`3)R2M8TS9$`zQT*pZxRt%pXqOVNf_Dcc4+-CVT@sL(cav zz+CXIR-Iu7qr)s)28jc53>)S$ILu&hxN-U|;{k1;8Xa{=e)&?(@S*DsBg1J(j=ag- zzzF2sj@`%p;2FbpkP8f}8O~Y1|MU2{{8M<&N&e0-r~KZ}eIg6XOb)WfFab8kN<=~NMPsQ`c+m}zj z&aUwC^38aL0y)M81LDwz^X~QsKVR2>|FM*zyyD--&lf7q891zKn18&D_b#a{F1T0u z{Jnj>K2T5?jHub$R+*$g1Za~IcMY;rrcy^*ak6KdN(^m>Nkdlrz{MI zfauseMu(ZU3<;KCk8JqNW z>buqL3$u>CNm9fH~)W z{lB<81_3oNGArXhQ2BiB{{Mg0-T~_bYR-8h*YNqA_5OK4A98|`t)0!@J#s%;>Sptw tEB+GO2$YcB$H*W*9j*eR4nO&q$)}n*&#E%N09X()c)I$ztaD0e0svkmY`_2j literal 0 HcmV?d00001 diff --git a/src/follower.js b/src/follower.js index b4ecd031..50164710 100644 --- a/src/follower.js +++ b/src/follower.js @@ -57,15 +57,25 @@ import { export var hackOptions = { allowFollowerCollision: false, // if true, the player can walk into the follower and talk to them (possible to get stuck this way) - follower: 'a', // id or name of sprite to be the follower; use '' to start without a follower + followers: ['a'], // ids or names of sprites to be followers; use [] to start without a follower delay: 200, // delay between each follower step (0 is immediate, 400 is twice as slow as normal) + stack: false, // if true, followers stack on top of each other; otherwise, they will form a chain }; -export var follower; +export var followers = []; var paths = {}; function setFollower(followerName) { - follower = followerName && getImage(followerName, bitsy.sprite); + var follower = followerName && getImage(followerName, bitsy.sprite); + if (!follower) { + throw new Error('Failed to find sprite with id/name "' + followerName + '"'); + } + var idx = followers.indexOf(follower); + if (idx > 0) { + followers.splice(idx, 1); + } else { + followers.push(follower); + } paths[follower.id] = paths[follower.id] || []; takeStep(); } @@ -78,22 +88,28 @@ function takeStep() { } walking = true; setTimeout(() => { - var path = paths[follower.id]; - var point = path.shift(); - if (point) { - follower.x = point.x; - follower.y = point.y; - follower.room = point.room; - } - walking = false; - if (path.length) { + let takeAnother = false; + followers.forEach(function (follower) { + var path = paths[follower.id]; + var point = path.shift(); + if (point) { + follower.x = point.x; + follower.y = point.y; + follower.room = point.room; + } + walking = false; + if (path.length) { + takeAnother = true; + } + }); + if (takeAnother) { takeStep(); } }, hackOptions.delay); } after('startExportedGame', function () { - setFollower(hackOptions.follower); + hackOptions.followers.forEach(setFollower); // remove + add player to sprite list to force rendering them on top of follower var p = bitsy.sprite[bitsy.playerId]; @@ -119,7 +135,7 @@ after('update', function () { return; } - if (!follower) { + if (!followers.length) { return; } @@ -146,25 +162,41 @@ after('update', function () { default: break; } - paths[follower.id].push(step); + followers.forEach(function (follower, idx) { + if (idx === 0 || hackOptions.stack) { + paths[follower.id].push(step); + } else { + var prevFollower = followers[idx - 1]; + var prev = paths[prevFollower.id]; + paths[follower.id].push(prev[prev.length - 2] || { + x: prevFollower.x, + y: prevFollower.y, + room: prevFollower.room, + }); + } + }); takeStep(); }); // make follower walk "through" exits before('movePlayerThroughExit', function (exit) { - if (follower) { + if (followers.length) { movedFollower = true; - paths[follower.id].push({ - x: exit.dest.x, - y: exit.dest.y, - room: exit.dest.room, + followers.forEach(function (follower) { + paths[follower.id].push({ + x: exit.dest.x, + y: exit.dest.y, + room: exit.dest.room, + }); }); takeStep(); } }); function filterFollowing(id) { - return follower === bitsy.sprite[id] ? null : id; + return followers.some(function (follower) { + return follower.id === id; + }) ? null : id; } var originalGetSpriteLeft; @@ -221,13 +253,13 @@ addDualDialogTag('followerDelay', function (environment, parameters) { hackOptions.delay = parseInt(parameters[0], 10); }); addDualDialogTag('followerSync', function () { - if (follower) { - var player = bitsy.player(); + var player = bitsy.player(); + followers.forEach(function (follower) { follower.room = player.room; follower.x = player.x; follower.y = player.y; paths[follower.id].length = 0; - } + }); }); before('moveSprites', function () { diff --git a/src/follower.test.js b/src/follower.test.js index e4a7f7e4..ac22e3ca 100644 --- a/src/follower.test.js +++ b/src/follower.test.js @@ -6,6 +6,83 @@ import { delay, } from './test/bitsy'; + snapshot, start + } from './test/bitsy'; + +const multiple = ` + +# BITSY VERSION 7.2 + +! ROOM_FORMAT 1 + +PAL 0 +0,82,204 +128,159,255 +255,255,255 + +ROOM 0 +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +0,a,a,a,a,a,a,a,a,a,a,a,a,a,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 +0,a,a,a,a,a,a,a,a,a,a,a,a,a,a,0 +0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +PAL 0 + +TIL a +11111111 +10000001 +10000001 +10011001 +10011001 +10000001 +10000001 +11111111 + +SPR A +00011000 +00011000 +00011000 +00111100 +01111110 +10111101 +00100100 +00100100 +POS 0 4,12 + +SPR a +00000000 +00000000 +00001000 +00011000 +00001000 +00001000 +00011100 +00000000 +POS 0 6,12 + +SPR b +00000000 +00000000 +00011000 +00100100 +00001000 +00010000 +00111100 +00000000 +POS 0 8,12 +`; + test('follower', async () => { await start({ hacks: ['follower'], @@ -19,3 +96,36 @@ test('follower', async () => { await snapshot(); await end(); }); + +test('multiple followers (chain)', async () => { + await start({ + hacks: [['follower', { + allowFollowerCollision: false, + followers: ['a', 'b'], + delay: 1, + }]], + gamedata: multiple, + }); + await press('ArrowLeft'); + await snapshot(); + await press('ArrowLeft'); + await snapshot(); + await end(); +}); + +test('multiple followers (stack)', async () => { + await start({ + hacks: [['follower', { + allowFollowerCollision: false, + followers: ['a', 'b'], + delay: 1, + stack: true, + }]], + gamedata: multiple, + }); + await press('ArrowLeft'); + await snapshot(); + await press('ArrowLeft'); + await snapshot(); + await end(); +}); diff --git a/src/test/bitsy.js b/src/test/bitsy.js index c5e9b511..8eef963d 100644 --- a/src/test/bitsy.js +++ b/src/test/bitsy.js @@ -48,7 +48,7 @@ export function delay(ms) { return new Promise((r) => setTimeout(r, ms)); } -// start pupeteer +// start puppeteer // and configure it for testing a bitsy game export async function start({ gamedata = '', @@ -80,7 +80,7 @@ export async function start({ return ``; } const [hackStr, options] = hack; - return ``; + return ``; }).join('\n') }$2`); }