Add compat switches for using the CC2 timing and update order
Other gameplay changes/fixes that crept in: - Ghosts no longer pick up red keys - Doppelgangers now read their movement directly from players, so no intermediate variables are necessary - Spring mining is no longer possible - Push recursion is detected and prevented - Bowling balls will also blow up anything that runs into them
This commit is contained in:
parent
1aa406fc7b
commit
2381bd38b9
400
js/game.js
400
js/game.js
@ -259,14 +259,17 @@ export class Cell extends Array {
|
|||||||
|
|
||||||
// If we got this far, all that's left is to deal with pushables
|
// If we got this far, all that's left is to deal with pushables
|
||||||
if (pushable_tiles.length > 0) {
|
if (pushable_tiles.length > 0) {
|
||||||
let neighbor_cell = level.get_neighboring_cell(this, direction);
|
// This ends recursive push attempts, which can happen with a row of ice clogged by ice
|
||||||
if (! neighbor_cell)
|
// blocks that are trying to slide
|
||||||
return false;
|
actor._trying_to_push = true;
|
||||||
|
try {
|
||||||
for (let tile of pushable_tiles) {
|
for (let tile of pushable_tiles) {
|
||||||
|
if (tile._trying_to_push)
|
||||||
|
return false;
|
||||||
if (push_mode === 'bump') {
|
if (push_mode === 'bump') {
|
||||||
// FIXME and leaving!
|
// FIXME this doesn't take railroad curves into account, e.g. it thinks a
|
||||||
if (! neighbor_cell.try_entering(tile, direction, level, push_mode))
|
// rover can't push a block through a curve
|
||||||
|
if (! level.check_movement(tile, tile.cell, direction, push_mode))
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (push_mode === 'push') {
|
else if (push_mode === 'push') {
|
||||||
@ -284,6 +287,18 @@ export class Cell extends Array {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally {
|
||||||
|
delete actor._trying_to_push;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In push mode, check one last time for being blocked, in case we e.g. pushed a block
|
||||||
|
// off of a recessed wall
|
||||||
|
// TODO deleting this allows spring mining, though i ended up causing it in a more
|
||||||
|
// aggressive form; try deleting this and running the 163 BLOX replay, it happens with
|
||||||
|
// ice blocks near the end
|
||||||
|
if (push_mode === 'push' && this.some(tile => tile.blocks(actor, direction, level)))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -420,9 +435,6 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO complain if no player
|
// TODO complain if no player
|
||||||
// Used for doppelgangers
|
|
||||||
this.player1_move = null;
|
|
||||||
this.player2_move = null;
|
|
||||||
|
|
||||||
// Connect buttons and teleporters
|
// Connect buttons and teleporters
|
||||||
let num_cells = this.width * this.height;
|
let num_cells = this.width * this.height;
|
||||||
@ -657,9 +669,10 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.begin_tic(p1_input);
|
this.begin_tic(p1_input);
|
||||||
this.finish_tic(p1_input);
|
this.finish_tic();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME a whole bunch of these comments are gonna be wrong or confusing now
|
||||||
begin_tic(p1_input) {
|
begin_tic(p1_input) {
|
||||||
if (this.undo_enabled) {
|
if (this.undo_enabled) {
|
||||||
// Store some current level state in the undo entry. (These will often not be modified, but
|
// Store some current level state in the undo entry. (These will often not be modified, but
|
||||||
@ -668,7 +681,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', 'remaining_players', 'player',
|
'remaining_players', 'player',
|
||||||
]) {
|
]) {
|
||||||
this.pending_undo.level_props[key] = this[key];
|
this.pending_undo.level_props[key] = this[key];
|
||||||
}
|
}
|
||||||
@ -677,11 +690,49 @@ export class Level extends LevelInterface {
|
|||||||
this.p1_released |= ~p1_input; // Action keys released since we last checked them
|
this.p1_released |= ~p1_input; // Action keys released since we last checked them
|
||||||
this.swap_player1 = false;
|
this.swap_player1 = false;
|
||||||
|
|
||||||
// This effect only lasts one tic, after which we can move again
|
|
||||||
this._set_tile_prop(this.player, 'is_blocked', false);
|
|
||||||
|
|
||||||
this.sfx.set_player_position(this.player.cell);
|
this.sfx.set_player_position(this.player.cell);
|
||||||
|
|
||||||
|
if (this.compat.use_lynx_loop) {
|
||||||
|
if (this.compat.emulate_60fps) {
|
||||||
|
this._begin_tic_lynx60();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._begin_tic_lynx();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this._begin_tic_lexy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the Lexy-style loop has a notion of "finishing" a tic, since (unlike the Lynx loop) the
|
||||||
|
// decision phase happens in the /middle/
|
||||||
|
finish_tic() {
|
||||||
|
if (this.compat.use_lynx_loop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._do_decision_phase();
|
||||||
|
|
||||||
|
// Lexy's separate movement loop
|
||||||
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
if (! actor.cell)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this._do_actor_movement(actor, actor.decision);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._do_cleanup_phase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lexy-style loop, the one I stumbled upon accidentally. Here, cooldowns happen /first/ as
|
||||||
|
// their own phase, then decisions are made, then movement happens. This approach has several
|
||||||
|
// advantages: there's no cooldown on the same tic that movement begins, which lets the renderer
|
||||||
|
// interpolate between tics more easily; there's no jitter when pushing a block, as is seen in
|
||||||
|
// CC2; and generally more things happen in parallel, which improves the illusion that all the
|
||||||
|
// game objects are acting simultaneously.
|
||||||
|
_begin_tic_lexy() {
|
||||||
// CC2 wiring runs every frame, not every tic, so we need to do it three times, but dealing
|
// CC2 wiring runs every frame, not every tic, so we need to do it three times, but dealing
|
||||||
// with it is delicate. We want the result of a button press to draw, but not last longer
|
// with it is delicate. We want the result of a button press to draw, but not last longer
|
||||||
// than intended, so we only want one update between the end of the cooldown pass and the
|
// than intended, so we only want one update between the end of the cooldown pass and the
|
||||||
@ -693,12 +744,8 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
this.update_wiring();
|
this.update_wiring();
|
||||||
|
|
||||||
// FIRST PASS: actors tick their cooldowns, finish their movement, and possibly step on
|
// Advance everyone's cooldowns
|
||||||
// cells they were moving into. This has a few advantages: it makes rendering interpolation
|
// Note that we iterate in reverse order, DESPITE keeping dead actors around with null
|
||||||
// much easier, and doing it as a separate pass from /starting/ movement (unlike Lynx)
|
|
||||||
// improves the illusion that everything is happening simultaneously.
|
|
||||||
// Note that, as far as I can tell, CC2 actually runs this pass every /frame/. We do not!
|
|
||||||
// Also Note that we iterate in reverse order, DESPITE keeping dead actors around with null
|
|
||||||
// cells, to match the Lynx and CC2 behavior. This is actually important in some cases;
|
// cells, to match the Lynx and CC2 behavior. This is actually important in some cases;
|
||||||
// check out the start of CCLP3 #54, where the gliders will eat the blue key immediately if
|
// check out the start of CCLP3 #54, where the gliders will eat the blue key immediately if
|
||||||
// they act in forward order! (More subtly, even the decision pass does things like
|
// they act in forward order! (More subtly, even the decision pass does things like
|
||||||
@ -709,22 +756,159 @@ export class Level extends LevelInterface {
|
|||||||
if (! actor.cell)
|
if (! actor.cell)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (actor.movement_cooldown <= 0)
|
this._do_actor_cooldown(actor, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mini extra pass: deal with teleporting separately. Otherwise, actors may have been in
|
||||||
|
// the way of the teleporter but finished moving away during the above loop; this is
|
||||||
|
// particularly bad when it happens with a block you're pushing. (CC2 doesn't need to do
|
||||||
|
// this because blocks you're pushing are always a frame ahead of you anyway.)
|
||||||
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
if (! actor.cell)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (actor.just_stepped_on_teleporter) {
|
||||||
|
this.attempt_teleport(actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update_wiring();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lynx-style loop: everyone decides, then everyone moves/cools.
|
||||||
|
_begin_tic_lynx() {
|
||||||
|
this._do_decision_phase();
|
||||||
|
this._do_combined_action_phase(3);
|
||||||
|
this.update_wiring();
|
||||||
|
|
||||||
|
this._do_cleanup_phase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as above, but split up to run at 60fps, where only every third frame allows for
|
||||||
|
// decisions. This is how CC2 works.
|
||||||
|
_begin_tic_lynx60() {
|
||||||
|
this._do_decision_phase(true);
|
||||||
|
this._do_combined_action_phase(1, true);
|
||||||
|
this.update_wiring();
|
||||||
|
|
||||||
|
this._do_decision_phase(true);
|
||||||
|
this._do_combined_action_phase(1, true);
|
||||||
|
this.update_wiring();
|
||||||
|
|
||||||
|
this._do_decision_phase();
|
||||||
|
this._do_combined_action_phase(1);
|
||||||
|
this.update_wiring();
|
||||||
|
|
||||||
|
this._do_cleanup_phase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decision phase: all actors decide on their movement "simultaneously"
|
||||||
|
_do_decision_phase(forced_only = false) {
|
||||||
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
|
||||||
|
// Clear any old decisions ASAP. Note that this prop is only used internally within a
|
||||||
|
// single tic, so it doesn't need to be undoable
|
||||||
|
actor.decision = null;
|
||||||
|
|
||||||
|
if (! actor.cell)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (actor.movement_cooldown > 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (! forced_only && actor.type.on_tic) {
|
||||||
|
actor.type.on_tic(actor, this);
|
||||||
|
if (! actor.cell)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actor === this.player) {
|
||||||
|
this.make_player_decision(actor, this.p1_input, forced_only);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.make_actor_decision(actor, forced_only);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This only persists for a single decision phase
|
||||||
|
if (! forced_only) {
|
||||||
|
this.yellow_tank_decision = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lynx's combined action phase: each actor attempts to move, then cools down, in order
|
||||||
|
_do_combined_action_phase(cooldown, forced_only = false) {
|
||||||
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
if (! actor.cell)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
this._do_actor_movement(actor, actor.decision);
|
||||||
|
this._do_actor_cooldown(actor, cooldown);
|
||||||
|
if (actor.just_stepped_on_teleporter) {
|
||||||
|
this.attempt_teleport(actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Have an actor attempt to move
|
||||||
|
_do_actor_movement(actor, direction) {
|
||||||
|
// Check this again, since an earlier pass may have caused us to start moving
|
||||||
|
if (actor.movement_cooldown > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (! direction)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Actor is allowed to move, so do so
|
||||||
|
let success = this.attempt_step(actor, direction);
|
||||||
|
|
||||||
|
// FIXME not convinced that ice bonking should actually go here. in cc2 it appears to
|
||||||
|
// happen every frame, fwiw, but i'm not sure if that includes frames with forced moves
|
||||||
|
// (though i guess that's impossible)
|
||||||
|
if (! success) {
|
||||||
|
let terrain = actor.cell.get_terrain();
|
||||||
|
if (terrain.type.slide_mode === 'ice' && (! actor.ignores(terrain.type.name) ||
|
||||||
|
// TODO weird cc2 quirk/bug: ghosts bonk on ice even though they don't slide on it
|
||||||
|
// FIXME and if they have cleats, they get stuck instead (?!)
|
||||||
|
(actor.type.name === 'ghost' && actor.cell.get_terrain().type.slide_mode === 'ice')))
|
||||||
|
{
|
||||||
|
// Bonk on ice: turn the actor around, consult the tile in case it's an ice
|
||||||
|
// corner, and try again
|
||||||
|
actor.direction = DIRECTIONS[direction].opposite;
|
||||||
|
direction = terrain.type.get_slide_direction(terrain, this, actor);
|
||||||
|
success = this.attempt_step(actor, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track whether the player is blocked, both for visual effect and for doppelgangers
|
||||||
|
if (actor === this.player && ! success) {
|
||||||
|
this.sfx.play_once('blocked');
|
||||||
|
this._set_tile_prop(actor, 'is_blocked', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
_do_actor_cooldown(actor, cooldown = 3) {
|
||||||
|
if (actor.movement_cooldown <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
if (actor.cooldown_delay_hack) {
|
if (actor.cooldown_delay_hack) {
|
||||||
// See the extensive comment in attempt_out_of_turn_step
|
// See the extensive comment in attempt_out_of_turn_step
|
||||||
actor.cooldown_delay_hack += 1;
|
actor.cooldown_delay_hack += 1;
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._set_tile_prop(actor, 'movement_cooldown', Math.max(0, actor.movement_cooldown - 1));
|
this._set_tile_prop(actor, 'movement_cooldown', Math.max(0, actor.movement_cooldown - cooldown));
|
||||||
|
|
||||||
if (actor.movement_cooldown <= 0) {
|
if (actor.movement_cooldown <= 0) {
|
||||||
if (actor.type.ttl) {
|
if (actor.type.ttl) {
|
||||||
// This is an animation that just finished, so destroy it
|
// This is an animation that just finished, so destroy it
|
||||||
this.remove_tile(actor);
|
this.remove_tile(actor);
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! this.compat.tiles_react_instantly) {
|
if (! this.compat.tiles_react_instantly) {
|
||||||
@ -744,104 +928,7 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mini extra pass: deal with teleporting separately. Otherwise, actors may have been in
|
_do_cleanup_phase() {
|
||||||
// the way of the teleporter but finished moving away during the above loop; this is
|
|
||||||
// particularly bad when it happens with a block you're pushing.
|
|
||||||
for (let i = this.actors.length - 1; i >= 0; i--) {
|
|
||||||
let actor = this.actors[i];
|
|
||||||
if (! actor.cell)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (actor.just_stepped_on_teleporter) {
|
|
||||||
this.attempt_teleport(actor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here's the third.
|
|
||||||
this.update_wiring();
|
|
||||||
}
|
|
||||||
|
|
||||||
finish_tic() {
|
|
||||||
// After cooldowns but before the decision phase, remember the player's /current/ direction,
|
|
||||||
// which may be affected by sliding. This will affect the behavior of doppelgangers earlier
|
|
||||||
// in the actor order than the player.
|
|
||||||
if (this.player.movement_cooldown > 0) {
|
|
||||||
this.remember_player_move(this.player.direction);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.remember_player_move(this.player.decision);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SECOND PASS: actors decide their upcoming movement simultaneously
|
|
||||||
for (let i = this.actors.length - 1; i >= 0; i--) {
|
|
||||||
let actor = this.actors[i];
|
|
||||||
|
|
||||||
// Clear any old decisions ASAP. Note that this prop is only used internally within a
|
|
||||||
// single tic, so it doesn't need to be undoable
|
|
||||||
actor.decision = null;
|
|
||||||
|
|
||||||
if (! actor.cell)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (actor.movement_cooldown > 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (actor === this.player) {
|
|
||||||
this.make_player_decision(actor, this.p1_input);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.make_actor_decision(actor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This only persists for a single decision phase
|
|
||||||
this.yellow_tank_decision = null;
|
|
||||||
|
|
||||||
// THIRD PASS: everyone actually moves
|
|
||||||
for (let i = this.actors.length - 1; i >= 0; i--) {
|
|
||||||
let actor = this.actors[i];
|
|
||||||
if (! actor.cell)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (actor.type.on_tic) {
|
|
||||||
actor.type.on_tic(actor, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check this again, since an earlier pass may have caused us to start moving
|
|
||||||
if (actor.movement_cooldown > 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (! actor.decision)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Actor is allowed to move, so do so
|
|
||||||
let success = this.attempt_step(actor, actor.decision);
|
|
||||||
|
|
||||||
// FIXME not convinced that ice bonking should actually go here. in cc2 it appears to
|
|
||||||
// happen every frame, fwiw, but i'm not sure if that includes frames with forced moves
|
|
||||||
// (though i guess that's impossible)
|
|
||||||
if (! success) {
|
|
||||||
let terrain = actor.cell.get_terrain();
|
|
||||||
if (terrain.type.slide_mode === 'ice' && (! actor.ignores(terrain.type.name) ||
|
|
||||||
// TODO weird cc2 quirk/bug: ghosts bonk on ice even though they don't slide on it
|
|
||||||
// FIXME and if they have cleats, they get stuck instead (?!)
|
|
||||||
(actor.type.name === 'ghost' && actor.cell.get_terrain().type.slide_mode === 'ice')))
|
|
||||||
{
|
|
||||||
// Bonk on ice: turn the actor around, consult the tile in case it's an ice
|
|
||||||
// corner, and try again
|
|
||||||
actor.direction = DIRECTIONS[actor.decision].opposite;
|
|
||||||
actor.decision = terrain.type.get_slide_direction(terrain, this, actor);
|
|
||||||
success = this.attempt_step(actor, actor.decision);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track whether the player is blocked, for visual effect
|
|
||||||
if (actor === this.player && actor.decision && ! success) {
|
|
||||||
this.sfx.play_once('blocked');
|
|
||||||
this._set_tile_prop(actor, 'is_blocked', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip out any destroyed actors from the acting order
|
// Strip out any destroyed actors from the acting order
|
||||||
// FIXME this is O(n), where n is /usually/ small, but i still don't love it. not strictly
|
// FIXME this is O(n), where n is /usually/ small, but i still don't love it. not strictly
|
||||||
// necessary, either; maybe only do it every few tics?
|
// necessary, either; maybe only do it every few tics?
|
||||||
@ -902,7 +989,8 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
// Advance the clock
|
// Advance the clock
|
||||||
// TODO i suspect cc2 does this at the beginning of the tic, but even if you've won? if you
|
// TODO i suspect cc2 does this at the beginning of the tic, but even if you've won? if you
|
||||||
// step on a penalty + exit you win, but you see the clock flicker 1 for a single frame
|
// step on a penalty + exit you win, but you see the clock flicker 1 for a single frame.
|
||||||
|
// maybe the win check happens at the start of the frame too?
|
||||||
this.tic_counter += 1;
|
this.tic_counter += 1;
|
||||||
if (this.time_remaining !== null && ! this.timer_paused) {
|
if (this.time_remaining !== null && ! this.timer_paused) {
|
||||||
this.time_remaining -= 1;
|
this.time_remaining -= 1;
|
||||||
@ -942,9 +1030,15 @@ export class Level extends LevelInterface {
|
|||||||
return [dir1, dir2];
|
return [dir1, dir2];
|
||||||
}
|
}
|
||||||
|
|
||||||
make_player_decision(actor, input) {
|
make_player_decision(actor, input, forced_only = false) {
|
||||||
// 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);
|
||||||
|
// This effect only lasts one tic, after which we can move again. Note that this one has
|
||||||
|
// gameplay impact -- doppelgangers use it to know if they should copy your facing direction
|
||||||
|
// even if you're not moving
|
||||||
|
if (! forced_only) {
|
||||||
|
this._set_tile_prop(actor, 'is_blocked', false);
|
||||||
|
}
|
||||||
|
|
||||||
// If the game has already been won (or lost), don't bother with a move; it'll misalign the
|
// 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.
|
// player from their actual position and not accomplish anything gameplay-wise.
|
||||||
@ -978,7 +1072,7 @@ export class Level extends LevelInterface {
|
|||||||
if (actor.slide_mode) {
|
if (actor.slide_mode) {
|
||||||
forced_decision = terrain.type.get_slide_direction(terrain, this, actor);
|
forced_decision = terrain.type.get_slide_direction(terrain, this, actor);
|
||||||
}
|
}
|
||||||
let may_move = (! actor.slide_mode || (actor.slide_mode === 'force' && actor.last_move_was_force));
|
let may_move = ! forced_only && (! actor.slide_mode || (actor.slide_mode === 'force' && actor.last_move_was_force));
|
||||||
let [dir1, dir2] = this._extract_player_directions(input);
|
let [dir1, dir2] = this._extract_player_directions(input);
|
||||||
|
|
||||||
// Check for special player actions, which can only happen at decision time. Dropping can
|
// Check for special player actions, which can only happen at decision time. Dropping can
|
||||||
@ -986,6 +1080,7 @@ export class Level extends LevelInterface {
|
|||||||
// can be done freely while sliding.
|
// can be done freely while sliding.
|
||||||
// FIXME cc2 seems to rely on key repeat for this; if you have four bowling balls and hold
|
// FIXME cc2 seems to rely on key repeat for this; if you have four bowling balls and hold
|
||||||
// Q, you'll throw the first, wait a second or so, then release the rest rapid-fire. absurd
|
// Q, you'll throw the first, wait a second or so, then release the rest rapid-fire. absurd
|
||||||
|
if (! forced_only) {
|
||||||
let new_input = input & this.p1_released;
|
let new_input = input & this.p1_released;
|
||||||
this.p1_released = 0xff;
|
this.p1_released = 0xff;
|
||||||
if (new_input & INPUT_BITS.cycle) {
|
if (new_input & INPUT_BITS.cycle) {
|
||||||
@ -1002,6 +1097,7 @@ export class Level extends LevelInterface {
|
|||||||
this.swap_player1 = true;
|
this.swap_player1 = true;
|
||||||
this.p1_released &= ~INPUT_BITS.swap;
|
this.p1_released &= ~INPUT_BITS.swap;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (actor.slide_mode && ! (may_move && dir1)) {
|
if (actor.slide_mode && ! (may_move && dir1)) {
|
||||||
// This is a forced move and we're not overriding it, so we're done
|
// This is a forced move and we're not overriding it, so we're done
|
||||||
@ -1011,7 +1107,7 @@ export class Level extends LevelInterface {
|
|||||||
this._set_tile_prop(actor, 'last_move_was_force', true);
|
this._set_tile_prop(actor, 'last_move_was_force', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (dir1 === null) {
|
else if (dir1 === null || forced_only) {
|
||||||
// Not attempting to move, so do nothing
|
// Not attempting to move, so do nothing
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -1080,14 +1176,9 @@ export class Level extends LevelInterface {
|
|||||||
this._set_tile_prop(actor, 'last_move_was_force', false);
|
this._set_tile_prop(actor, 'last_move_was_force', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remember our choice for the sake of doppelgangers
|
|
||||||
// FIXME still a bit unclear on how they handle secondary direction, but i'm not sure that's
|
|
||||||
// even a real concept in lynx, so maybe this is right??
|
|
||||||
this.remember_player_move(actor.decision);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
make_actor_decision(actor) {
|
make_actor_decision(actor, forced_only = false) {
|
||||||
// Compat flag for blue tanks
|
// Compat flag for blue tanks
|
||||||
if (this.compat.sliding_tanks_ignore_button &&
|
if (this.compat.sliding_tanks_ignore_button &&
|
||||||
actor.slide_mode && actor.pending_reverse)
|
actor.slide_mode && actor.pending_reverse)
|
||||||
@ -1114,6 +1205,8 @@ export class Level extends LevelInterface {
|
|||||||
actor.decision = terrain.type.get_slide_direction(terrain, this, actor);
|
actor.decision = terrain.type.get_slide_direction(terrain, this, actor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (forced_only)
|
||||||
|
return;
|
||||||
if (actor.cell.some(tile => tile.type.traps && tile.type.traps(tile, actor))) {
|
if (actor.cell.some(tile => tile.type.traps && tile.type.traps(tile, actor))) {
|
||||||
// An actor in a cloner or a closed trap can't turn
|
// An actor in a cloner or a closed trap can't turn
|
||||||
// TODO because of this, if a tank is trapped when a blue button is pressed, then
|
// TODO because of this, if a tank is trapped when a blue button is pressed, then
|
||||||
@ -1130,9 +1223,9 @@ export class Level extends LevelInterface {
|
|||||||
return;
|
return;
|
||||||
let all_blocked = true;
|
let all_blocked = true;
|
||||||
for (let [i, direction] of direction_preference.entries()) {
|
for (let [i, direction] of direction_preference.entries()) {
|
||||||
if (direction === null) {
|
if (! direction) {
|
||||||
// This actor is giving up! Alas.
|
// This actor is giving up! Alas.
|
||||||
actor.decision = direction;
|
actor.decision = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (typeof direction === 'function') {
|
if (typeof direction === 'function') {
|
||||||
@ -1224,14 +1317,20 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._set_tile_prop(actor, 'previous_cell', actor.cell);
|
this._set_tile_prop(actor, 'previous_cell', actor.cell);
|
||||||
this._set_tile_prop(actor, 'movement_cooldown', speed);
|
this._set_tile_prop(actor, 'movement_cooldown', speed * 3);
|
||||||
this._set_tile_prop(actor, 'movement_speed', speed);
|
this._set_tile_prop(actor, 'movement_speed', speed * 3);
|
||||||
this.move_to(actor, goal_cell, speed);
|
this.move_to(actor, goal_cell, speed);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
attempt_out_of_turn_step(actor, direction) {
|
attempt_out_of_turn_step(actor, direction) {
|
||||||
|
if (this.compat.use_lynx_loop) {
|
||||||
|
let success = this._do_actor_movement(actor, direction);
|
||||||
|
this._do_actor_cooldown(actor, this.compat.emulate_60fps ? 1 : 3);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.attempt_step(actor, direction)) {
|
if (this.attempt_step(actor, direction)) {
|
||||||
// Here's the problem.
|
// Here's the problem.
|
||||||
// In CC2, cooldown is measured in frames, not tics, and it decrements every frame, not
|
// In CC2, cooldown is measured in frames, not tics, and it decrements every frame, not
|
||||||
@ -1373,7 +1472,8 @@ export class Level extends LevelInterface {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (tile.type.is_item &&
|
if (tile.type.is_item &&
|
||||||
(actor.type.has_inventory ||
|
// FIXME implement item priority i'm begging you
|
||||||
|
((actor.type.has_inventory && ! (tile.type.name === 'key_red' && ! actor.type.is_player)) ||
|
||||||
cell.some(t => t.type.item_modifier === 'pickup')) &&
|
cell.some(t => t.type.item_modifier === 'pickup')) &&
|
||||||
this.attempt_take(actor, tile))
|
this.attempt_take(actor, tile))
|
||||||
{
|
{
|
||||||
@ -1471,7 +1571,13 @@ export class Level extends LevelInterface {
|
|||||||
// Now physically move the actor and have them take a turn
|
// Now physically move the actor and have them take a turn
|
||||||
this.remove_tile(actor);
|
this.remove_tile(actor);
|
||||||
this.add_tile(actor, dest.cell);
|
this.add_tile(actor, dest.cell);
|
||||||
|
// FIXME i think the cc2 approach might be to handle this at decision time, hence why
|
||||||
|
// overriding works at all; it happens to work for me because this happens immediately
|
||||||
|
// before decision time as a separate pass! for now, simulate by undoing the cooldown
|
||||||
this.attempt_out_of_turn_step(actor, direction);
|
this.attempt_out_of_turn_step(actor, direction);
|
||||||
|
if (this.compat.use_lynx_loop && actor.movement_cooldown) {
|
||||||
|
this._set_tile_prop(actor, 'movement_cooldown', actor.movement_cooldown + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (! success && actor.type.has_inventory && teleporter.type.name === 'teleport_yellow') {
|
if (! success && actor.type.has_inventory && teleporter.type.name === 'teleport_yellow') {
|
||||||
// Super duper special yellow teleporter behavior: you pick it the fuck up
|
// Super duper special yellow teleporter behavior: you pick it the fuck up
|
||||||
@ -1483,17 +1589,6 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remember_player_move(direction) {
|
|
||||||
if (this.player.type.name === 'player') {
|
|
||||||
this.player1_move = direction;
|
|
||||||
this.player2_move = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.player1_move = null;
|
|
||||||
this.player2_move = direction;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cycle_inventory(actor) {
|
cycle_inventory(actor) {
|
||||||
if (this.stored_level.use_cc1_boots)
|
if (this.stored_level.use_cc1_boots)
|
||||||
return;
|
return;
|
||||||
@ -1966,11 +2061,10 @@ export class Level extends LevelInterface {
|
|||||||
spawn_animation(cell, name) {
|
spawn_animation(cell, name) {
|
||||||
let type = TILE_TYPES[name];
|
let type = TILE_TYPES[name];
|
||||||
let tile = new Tile(type);
|
let tile = new Tile(type);
|
||||||
// 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 those
|
||||||
// also used to animate everything else. Decrement the cooldown immediately, to match the
|
// properties are also used to animate everything else anyway. Decrement the cooldown
|
||||||
// normal actor behavior of decrementing one's own cooldown at the end of one's turn
|
// immediately, as Lynx does; note that Lynx also ticks /and destroys/ animations early in
|
||||||
// FIXME this replicates cc2 behavior, but it also means the animation is actually visible
|
// the decision phase, but this seems to work out just as well
|
||||||
// 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);
|
||||||
|
|||||||
13
js/main.js
13
js/main.js
@ -1006,6 +1006,12 @@ class Player extends PrimaryView {
|
|||||||
ret[key] = value;
|
ret[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XXX do not commit this
|
||||||
|
/*
|
||||||
|
ret.use_lynx_loop = true;
|
||||||
|
ret.emulate_60fps = true;
|
||||||
|
*/
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2171,7 +2177,12 @@ class PackTestDialog extends DialogOverlay {
|
|||||||
|
|
||||||
// TODO compat options here??
|
// TODO compat options here??
|
||||||
let replay = stored_level.replay;
|
let replay = stored_level.replay;
|
||||||
level = new Level(stored_level, {});
|
level = new Level(stored_level, {
|
||||||
|
/*
|
||||||
|
use_lynx_loop: true,
|
||||||
|
emulate_60fps: true,
|
||||||
|
*/
|
||||||
|
});
|
||||||
level.sfx = dummy_sfx;
|
level.sfx = dummy_sfx;
|
||||||
level.force_floor_direction = replay.initial_force_floor_direction;
|
level.force_floor_direction = replay.initial_force_floor_direction;
|
||||||
level._blob_modifier = replay.blob_seed;
|
level._blob_modifier = replay.blob_seed;
|
||||||
|
|||||||
@ -2242,7 +2242,22 @@ const TILE_TYPES = {
|
|||||||
decide_movement(me, level) {
|
decide_movement(me, level) {
|
||||||
return [me.direction];
|
return [me.direction];
|
||||||
},
|
},
|
||||||
|
on_bumped(me, level, other) {
|
||||||
|
// Blow up anything that runs into us... unless we're on a cloner
|
||||||
|
// FIXME there are other cases where this won't be right; this shouldn't happen if the
|
||||||
|
// cell blocks the actor, but i don't have a callback for that?
|
||||||
|
if (me.cell.has('cloner'))
|
||||||
|
return;
|
||||||
|
if (other.type.is_real_player) {
|
||||||
|
level.fail(me.type.name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
level.transmute_tile(other, 'explosion');
|
||||||
|
}
|
||||||
|
level.transmute_tile(me, 'explosion');
|
||||||
|
},
|
||||||
on_blocked(me, level, direction) {
|
on_blocked(me, level, direction) {
|
||||||
|
// Blow up anything we run into
|
||||||
let cell = level.get_neighboring_cell(me.cell, direction);
|
let cell = level.get_neighboring_cell(me.cell, direction);
|
||||||
let other;
|
let other;
|
||||||
if (cell) {
|
if (cell) {
|
||||||
@ -2392,8 +2407,13 @@ const TILE_TYPES = {
|
|||||||
key_green: true,
|
key_green: true,
|
||||||
},
|
},
|
||||||
decide_movement(me, level) {
|
decide_movement(me, level) {
|
||||||
if (level.player1_move) {
|
if (level.player.type.name === 'player') {
|
||||||
return [level.player1_move];
|
if (level.player.movement_cooldown || level.player.is_blocked) {
|
||||||
|
return [level.player.direction];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return [level.player.decision];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return null;
|
return null;
|
||||||
@ -2427,8 +2447,13 @@ const TILE_TYPES = {
|
|||||||
key_yellow: true,
|
key_yellow: true,
|
||||||
},
|
},
|
||||||
decide_movement(me, level) {
|
decide_movement(me, level) {
|
||||||
if (level.player2_move) {
|
if (level.player.type.name === 'player2') {
|
||||||
return [level.player2_move];
|
if (level.player.movement_cooldown || level.player.is_blocked) {
|
||||||
|
return [level.player.direction];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return [level.player.decision];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return null;
|
return null;
|
||||||
@ -2550,7 +2575,7 @@ const TILE_TYPES = {
|
|||||||
is_actor: true,
|
is_actor: true,
|
||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
blocks_collision: COLLISION.player,
|
blocks_collision: COLLISION.player,
|
||||||
ttl: 6,
|
ttl: 16,
|
||||||
// If anything else even begins to step on an animation, it's erased
|
// If anything else even begins to step on an animation, it's erased
|
||||||
// FIXME possibly erased too fast; cc2 shows it briefly? could i get away with on_arrive here?
|
// FIXME possibly erased too fast; cc2 shows it briefly? could i get away with on_arrive here?
|
||||||
on_approach(me, level, other) {
|
on_approach(me, level, other) {
|
||||||
@ -2562,7 +2587,7 @@ const TILE_TYPES = {
|
|||||||
is_actor: true,
|
is_actor: true,
|
||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
blocks_collision: COLLISION.player,
|
blocks_collision: COLLISION.player,
|
||||||
ttl: 6,
|
ttl: 16,
|
||||||
on_approach(me, level, other) {
|
on_approach(me, level, other) {
|
||||||
level.remove_tile(me);
|
level.remove_tile(me);
|
||||||
},
|
},
|
||||||
@ -2574,7 +2599,7 @@ const TILE_TYPES = {
|
|||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
blocks_collision: 0,
|
blocks_collision: 0,
|
||||||
// determined experimentally
|
// determined experimentally
|
||||||
ttl: 12,
|
ttl: 36,
|
||||||
},
|
},
|
||||||
// Custom VFX (identical function, but different aesthetic)
|
// Custom VFX (identical function, but different aesthetic)
|
||||||
splash_slime: {
|
splash_slime: {
|
||||||
@ -2582,7 +2607,7 @@ const TILE_TYPES = {
|
|||||||
is_actor: true,
|
is_actor: true,
|
||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
blocks_collision: COLLISION.player,
|
blocks_collision: COLLISION.player,
|
||||||
ttl: 6,
|
ttl: 16,
|
||||||
on_approach(me, level, other) {
|
on_approach(me, level, other) {
|
||||||
level.remove_tile(me);
|
level.remove_tile(me);
|
||||||
},
|
},
|
||||||
@ -2593,13 +2618,13 @@ const TILE_TYPES = {
|
|||||||
draw_layer: DRAW_LAYERS.actor,
|
draw_layer: DRAW_LAYERS.actor,
|
||||||
is_actor: true,
|
is_actor: true,
|
||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
ttl: 8,
|
ttl: 8 * 3,
|
||||||
},
|
},
|
||||||
player2_exit: {
|
player2_exit: {
|
||||||
draw_layer: DRAW_LAYERS.actor,
|
draw_layer: DRAW_LAYERS.actor,
|
||||||
is_actor: true,
|
is_actor: true,
|
||||||
collision_mask: 0,
|
collision_mask: 0,
|
||||||
ttl: 8,
|
ttl: 8 * 3,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Invalid tiles that appear in some CCL levels because community level
|
// Invalid tiles that appear in some CCL levels because community level
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user