diff --git a/js/format-c2g.js b/js/format-c2g.js index 6270ac3..4b8d2bc 100644 --- a/js/format-c2g.js +++ b/js/format-c2g.js @@ -79,7 +79,7 @@ export function encode_replay(replay, stored_level = null) { } let input = replay.inputs[i]; - if (input !== prev_input || count >= 252) { + if (input !== prev_input || count >= 252 - 2) { out[p] = count; out[p + 1] = input; p += 2; @@ -92,6 +92,8 @@ export function encode_replay(replay, stored_level = null) { } out[p] = 0xff; p += 1; + out[p] = 0x00; + p += 1; out = out.subarray(0, p); // TODO stick it on the level if given? return out; @@ -1301,7 +1303,9 @@ class C2M { export function synthesize_level(stored_level) { let c2m = new C2M; - c2m.add_section('CC2M', '133'); + c2m.add_section('CC2M', '7'); // latest version + // TODO add in a VERS (editor version) section? some other indication LL produced it? not for + // url sharing though, should make that as small as possible if (stored_level.title) { c2m.add_section('TITL', stored_level.title); diff --git a/js/game.js b/js/game.js index a148bd9..7adbb22 100644 --- a/js/game.js +++ b/js/game.js @@ -378,7 +378,7 @@ export class Level extends LevelInterface { let n = 0; let connectables = []; - this.players = []; + this.remaining_players = 0; // FIXME handle traps correctly: // - if an actor is in the cell, set the trap to open and unstick everything in it for (let y = 0; y < this.height; y++) { @@ -399,7 +399,10 @@ export class Level extends LevelInterface { } if (tile.type.is_real_player) { - this.players.push(tile); + this.remaining_players += 1; + if (this.player === null) { + this.player = tile; + } } if (tile.type.is_actor) { this.actors.push(tile); @@ -413,9 +416,6 @@ export class Level extends LevelInterface { } } // TODO complain if no player - // FIXME this is not how multiple players works - this.player = this.players[0]; - this.player_index = 0; // Used for doppelgangers this.player1_move = null; this.player2_move = null; @@ -663,7 +663,7 @@ export class Level extends LevelInterface { '_rng1', '_rng2', '_blob_modifier', 'force_floor_direction', 'tic_counter', 'time_remaining', 'timer_paused', 'chips_remaining', 'bonus_points', 'hint_shown', 'state', - 'player1_move', 'player2_move', + 'player1_move', 'player2_move', 'remaining_players', 'player', ]) { this.pending_undo.level_props[key] = this[key]; } @@ -848,11 +848,39 @@ export class Level extends LevelInterface { this.actors.length = p; // Possibly switch players - // FIXME not correct + // FIXME cc2 has very poor interactions between this feature and cloners; come up with some + // better rules as a default if (this.swap_player1) { - this.player_index += 1; - this.player_index %= this.players.length; - this.player = this.players[this.player_index]; + // Reset the set of keys released since last tic + this.p1_released = 0xff; + + // Iterate backwards over the actor list looking for a viable next player to control + let i0 = this.actors.indexOf(this.player); + if (i0 < 0) { + i0 = 0; + } + let i = i0; + while (true) { + i -= 1; + if (i < 0) { + i += this.actors.length; + } + if (i === i0) + break; + + let actor = this.actors[i]; + if (! actor.cell) + continue; + + if (actor.type.is_real_player) { + this.player = actor; + break; + } + } + } + + if (this.remaining_players <= 0) { + this.win(); } // Advance the clock @@ -901,6 +929,12 @@ export class Level extends LevelInterface { // Only reset the player's is_pushing between movement, so it lasts for the whole push this._set_tile_prop(actor, 'is_pushing', false); + // If the game has already been won (or lost), don't bother with a move; it'll misalign the + // player from their actual position and not accomplish anything gameplay-wise. + // (Note this is only necessary because our update order is inverted from CC2.) + if (this.state !== 'playing') + return null; + // TODO player in a cloner can't move (but player in a trap can still turn) let [dir1, dir2] = this._extract_player_directions(input); @@ -940,7 +974,7 @@ export class Level extends LevelInterface { if (new_input & INPUT_BITS.drop) { this.drop_item(this.player); } - if (new_input & INPUT_BITS.swap) { + if ((new_input & INPUT_BITS.swap) && this.remaining_players > 1) { // This is delayed until the end of the tic to avoid screwing up anything // checking this.player this.swap_player1 = true; @@ -1905,6 +1939,8 @@ export class Level extends LevelInterface { // Co-opt movement_cooldown/speed for these despite that they aren't moving, since they're // also used to animate everything else. Decrement the cooldown immediately, to match the // normal actor behavior of decrementing one's own cooldown at the end of one's turn + // FIXME this replicates cc2 behavior, but it also means the animation is actually visible + // for one less tic than expected this._set_tile_prop(tile, 'movement_speed', tile.type.ttl); this._set_tile_prop(tile, 'movement_cooldown', tile.type.ttl - 1); cell._add(tile); diff --git a/js/main.js b/js/main.js index fdb876c..b2b57d4 100644 --- a/js/main.js +++ b/js/main.js @@ -1266,7 +1266,7 @@ class Player extends PrimaryView { this.cycle_button.disabled = ! ( this.state === 'playing' && ! this.level.stored_level.use_cc1_boots && this.level.player.toolbelt && this.level.player.toolbelt.length > 1); - this.swap_button.disabled = ! (this.state === 'playing' && this.level.players.length > 1); + this.swap_button.disabled = ! (this.state === 'playing' && this.level.remaining_players > 1); // TODO can we do this only if they actually changed? this.chips_el.textContent = this.level.chips_remaining; diff --git a/js/tileset.js b/js/tileset.js index b777e28..58ae498 100644 --- a/js/tileset.js +++ b/js/tileset.js @@ -171,7 +171,7 @@ export const CC2_TILESET_LAYOUT = { suction_boots: [3, 6], hiking_boots: [4, 6], lightning_bolt: [5, 6], - // weird translucent spiral + // FIXME draw the current player background... more external state for the renderer though... // TODO dopps can push but i don't think they have any other visuals doppelganger1: { base: [7, 6], @@ -467,6 +467,8 @@ export const CC2_TILESET_LAYOUT = { exploded: [1, 5], failed: [1, 5], }, + // Do a quick spin I guess?? + player1_exit: [[0, 22], [8, 22], [0, 23], [8, 23]], bogus_player_win: { overlay: [0, 23], base: 'exit', @@ -591,6 +593,7 @@ export const CC2_TILESET_LAYOUT = { exploded: [1, 5], failed: [1, 5], }, + player2_exit: [[0, 27], [8, 27], [0, 28], [8, 28]], fire: [ [12, 29], [13, 29], @@ -879,6 +882,8 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, { // Custom VFX splash_slime: [[0, 38], [1, 38], [2, 38], [3, 38]], + player1_exit: [[8, 38], [9, 38], [10, 38], [11, 38]], + player2_exit: [[12, 38], [13, 38], [14, 38], [15, 38]], }); export class Tileset { diff --git a/js/tiletypes.js b/js/tiletypes.js index 64b65cf..b95675b 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -48,6 +48,8 @@ function player_visual_state(me) { return 'normal'; } + // FIXME fail reason gets attached to the wrong player if there's a swap at the same time as a + // player gets hit if (me.fail_reason === 'drowned') { return 'drowned'; } @@ -68,6 +70,7 @@ function player_visual_state(me) { } else if (me.cell && (me.previous_cell || me.cell).some(t => t.type.name === 'water')) { // CC2 shows a swimming pose while still in water, or moving away from water + // FIXME this also shows in some cases when we don't have flippers, e.g. when starting in water return 'swimming'; } else if (me.slide_mode === 'ice') { @@ -2492,9 +2495,12 @@ const TILE_TYPES = { draw_layer: DRAW_LAYERS.terrain, blocks_collision: COLLISION.block_cc1 | COLLISION.monster_solid & ~COLLISION.rover, on_arrive(me, level, other) { - // FIXME multiple players if (other.type.is_real_player) { - level.win(); + level.remaining_players -= 1; + if (level.remaining_players > 0) { + level.swap_player1 = true; + level.transmute_tile(other, other.type.name === 'player' ? 'player1_exit' : 'player2_exit'); + } } }, }, @@ -2542,6 +2548,20 @@ const TILE_TYPES = { level.remove_tile(me); }, }, + // New VFX (not in CC2, so they don't block to avoid altering gameplay) + // TODO would like these to play faster but the first frame is often skipped due to other bugs + player1_exit: { + draw_layer: DRAW_LAYERS.actor, + is_actor: true, + collision_mask: 0, + ttl: 8, + }, + player2_exit: { + draw_layer: DRAW_LAYERS.actor, + is_actor: true, + collision_mask: 0, + ttl: 8, + }, // Invalid tiles that appear in some CCL levels because community level // designers love to make nonsense diff --git a/tileset-lexy.png b/tileset-lexy.png index 2b83923..12ec338 100644 Binary files a/tileset-lexy.png and b/tileset-lexy.png differ diff --git a/tileset-src/tileset-lexy-player-exit.aseprite b/tileset-src/tileset-lexy-player-exit.aseprite new file mode 100644 index 0000000..b4a1e4b Binary files /dev/null and b/tileset-src/tileset-lexy-player-exit.aseprite differ diff --git a/tileset-src/tileset-lexy.aseprite b/tileset-src/tileset-lexy.aseprite index e5d1f43..480c568 100644 Binary files a/tileset-src/tileset-lexy.aseprite and b/tileset-src/tileset-lexy.aseprite differ