Implement all-players-exit behavior; touch up locks, buttons, logic gates; fix demo saving
This commit is contained in:
parent
78f59b38c1
commit
148beb7d74
@ -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);
|
||||||
|
|||||||
58
js/game.js
58
js/game.js
@ -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);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
BIN
tileset-lexy.png
BIN
tileset-lexy.png
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
BIN
tileset-src/tileset-lexy-player-exit.aseprite
Normal file
BIN
tileset-src/tileset-lexy-player-exit.aseprite
Normal file
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user