Implement all-players-exit behavior; touch up locks, buttons, logic gates; fix demo saving

This commit is contained in:
Eevee (Evelyn Woods) 2020-12-19 17:16:50 -07:00
parent 78f59b38c1
commit 148beb7d74
8 changed files with 82 additions and 17 deletions

View File

@ -79,7 +79,7 @@ export function encode_replay(replay, stored_level = null) {
} }
let input = replay.inputs[i]; let input = replay.inputs[i];
if (input !== prev_input || count >= 252) { if (input !== prev_input || count >= 252 - 2) {
out[p] = count; out[p] = count;
out[p + 1] = input; out[p + 1] = input;
p += 2; p += 2;
@ -92,6 +92,8 @@ export function encode_replay(replay, stored_level = null) {
} }
out[p] = 0xff; out[p] = 0xff;
p += 1; p += 1;
out[p] = 0x00;
p += 1;
out = out.subarray(0, p); out = out.subarray(0, p);
// TODO stick it on the level if given? // TODO stick it on the level if given?
return out; return out;
@ -1301,7 +1303,9 @@ class C2M {
export function synthesize_level(stored_level) { export function synthesize_level(stored_level) {
let c2m = new C2M; 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) { if (stored_level.title) {
c2m.add_section('TITL', stored_level.title); c2m.add_section('TITL', stored_level.title);

View File

@ -378,7 +378,7 @@ export class Level extends LevelInterface {
let n = 0; let n = 0;
let connectables = []; let connectables = [];
this.players = []; this.remaining_players = 0;
// FIXME handle traps correctly: // FIXME handle traps correctly:
// - if an actor is in the cell, set the trap to open and unstick everything in it // - 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++) { for (let y = 0; y < this.height; y++) {
@ -399,7 +399,10 @@ export class Level extends LevelInterface {
} }
if (tile.type.is_real_player) { 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) { if (tile.type.is_actor) {
this.actors.push(tile); this.actors.push(tile);
@ -413,9 +416,6 @@ export class Level extends LevelInterface {
} }
} }
// TODO complain if no player // 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 // Used for doppelgangers
this.player1_move = null; this.player1_move = null;
this.player2_move = null; this.player2_move = null;
@ -663,7 +663,7 @@ export class Level extends LevelInterface {
'_rng1', '_rng2', '_blob_modifier', 'force_floor_direction', '_rng1', '_rng2', '_blob_modifier', 'force_floor_direction',
'tic_counter', 'time_remaining', 'timer_paused', 'tic_counter', 'time_remaining', 'timer_paused',
'chips_remaining', 'bonus_points', 'hint_shown', 'state', '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]; this.pending_undo.level_props[key] = this[key];
} }
@ -848,11 +848,39 @@ export class Level extends LevelInterface {
this.actors.length = p; this.actors.length = p;
// Possibly switch players // 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) { if (this.swap_player1) {
this.player_index += 1; // Reset the set of keys released since last tic
this.player_index %= this.players.length; this.p1_released = 0xff;
this.player = this.players[this.player_index];
// 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 // 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 // Only reset the player's is_pushing between movement, so it lasts for the whole push
this._set_tile_prop(actor, 'is_pushing', false); 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) // TODO player in a cloner can't move (but player in a trap can still turn)
let [dir1, dir2] = this._extract_player_directions(input); let [dir1, dir2] = this._extract_player_directions(input);
@ -940,7 +974,7 @@ export class Level extends LevelInterface {
if (new_input & INPUT_BITS.drop) { if (new_input & INPUT_BITS.drop) {
this.drop_item(this.player); 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 // This is delayed until the end of the tic to avoid screwing up anything
// checking this.player // checking this.player
this.swap_player1 = true; 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 // 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 // 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 // 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_speed', tile.type.ttl);
this._set_tile_prop(tile, 'movement_cooldown', tile.type.ttl - 1); this._set_tile_prop(tile, 'movement_cooldown', tile.type.ttl - 1);
cell._add(tile); cell._add(tile);

View File

@ -1266,7 +1266,7 @@ class Player extends PrimaryView {
this.cycle_button.disabled = ! ( this.cycle_button.disabled = ! (
this.state === 'playing' && ! this.level.stored_level.use_cc1_boots && this.state === 'playing' && ! this.level.stored_level.use_cc1_boots &&
this.level.player.toolbelt && this.level.player.toolbelt.length > 1); 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? // TODO can we do this only if they actually changed?
this.chips_el.textContent = this.level.chips_remaining; this.chips_el.textContent = this.level.chips_remaining;

View File

@ -171,7 +171,7 @@ export const CC2_TILESET_LAYOUT = {
suction_boots: [3, 6], suction_boots: [3, 6],
hiking_boots: [4, 6], hiking_boots: [4, 6],
lightning_bolt: [5, 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 // TODO dopps can push but i don't think they have any other visuals
doppelganger1: { doppelganger1: {
base: [7, 6], base: [7, 6],
@ -467,6 +467,8 @@ export const CC2_TILESET_LAYOUT = {
exploded: [1, 5], exploded: [1, 5],
failed: [1, 5], failed: [1, 5],
}, },
// Do a quick spin I guess??
player1_exit: [[0, 22], [8, 22], [0, 23], [8, 23]],
bogus_player_win: { bogus_player_win: {
overlay: [0, 23], overlay: [0, 23],
base: 'exit', base: 'exit',
@ -591,6 +593,7 @@ export const CC2_TILESET_LAYOUT = {
exploded: [1, 5], exploded: [1, 5],
failed: [1, 5], failed: [1, 5],
}, },
player2_exit: [[0, 27], [8, 27], [0, 28], [8, 28]],
fire: [ fire: [
[12, 29], [12, 29],
[13, 29], [13, 29],
@ -879,6 +882,8 @@ export const LL_TILESET_LAYOUT = Object.assign({}, CC2_TILESET_LAYOUT, {
// Custom VFX // Custom VFX
splash_slime: [[0, 38], [1, 38], [2, 38], [3, 38]], 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 { export class Tileset {

View File

@ -48,6 +48,8 @@ function player_visual_state(me) {
return 'normal'; 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') { if (me.fail_reason === 'drowned') {
return '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')) { 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 // 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'; return 'swimming';
} }
else if (me.slide_mode === 'ice') { else if (me.slide_mode === 'ice') {
@ -2492,9 +2495,12 @@ const TILE_TYPES = {
draw_layer: DRAW_LAYERS.terrain, draw_layer: DRAW_LAYERS.terrain,
blocks_collision: COLLISION.block_cc1 | COLLISION.monster_solid & ~COLLISION.rover, blocks_collision: COLLISION.block_cc1 | COLLISION.monster_solid & ~COLLISION.rover,
on_arrive(me, level, other) { on_arrive(me, level, other) {
// FIXME multiple players
if (other.type.is_real_player) { 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); 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 // Invalid tiles that appear in some CCL levels because community level
// designers love to make nonsense // designers love to make nonsense

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Binary file not shown.