Fix a few more Lynx compat issues
This commit is contained in:
parent
2dcd73d44a
commit
63609ba77e
@ -178,6 +178,10 @@ export const COMPAT_FLAGS = [
|
|||||||
key: 'traps_like_lynx',
|
key: 'traps_like_lynx',
|
||||||
label: "Traps eject faster, and even when already open",
|
label: "Traps eject faster, and even when already open",
|
||||||
rulesets: new Set(['lynx']),
|
rulesets: new Set(['lynx']),
|
||||||
|
}, {
|
||||||
|
key: 'blue_floors_vanish_on_arrive',
|
||||||
|
label: "Fake blue walls vanish on arrival",
|
||||||
|
rulesets: new Set(['lynx']),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Items
|
// Items
|
||||||
@ -217,6 +221,10 @@ export const COMPAT_FLAGS = [
|
|||||||
key: 'tanks_teeth_push_ice_blocks',
|
key: 'tanks_teeth_push_ice_blocks',
|
||||||
label: "Ice blocks emulate pgchip rules",
|
label: "Ice blocks emulate pgchip rules",
|
||||||
rulesets: new Set(['ms']),
|
rulesets: new Set(['ms']),
|
||||||
|
}, {
|
||||||
|
key: 'allow_pushing_blocks_off_faux_walls',
|
||||||
|
label: "Blocks may be pushed off of blue (fake), invisible, and revealing walls",
|
||||||
|
rulesets: new Set(['lynx']),
|
||||||
}, {
|
}, {
|
||||||
key: 'emulate_spring_mining',
|
key: 'emulate_spring_mining',
|
||||||
label: "Spring mining is possible",
|
label: "Spring mining is possible",
|
||||||
|
|||||||
87
js/game.js
87
js/game.js
@ -134,7 +134,7 @@ export class Tile {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
can_push(tile, direction) {
|
can_push(tile, direction, level) {
|
||||||
// This tile already has a push queued, sorry
|
// This tile already has a push queued, sorry
|
||||||
if (tile.pending_push)
|
if (tile.pending_push)
|
||||||
return false;
|
return false;
|
||||||
@ -158,7 +158,7 @@ export class Tile {
|
|||||||
// Need to explicitly check this here, otherwise you could /attempt/ to push a block,
|
// Need to explicitly check this here, otherwise you could /attempt/ to push a block,
|
||||||
// which would fail, but it would still change the block's direction
|
// which would fail, but it would still change the block's direction
|
||||||
// XXX this expects to take a level but it only matters with push_mode === 'push'
|
// XXX this expects to take a level but it only matters with push_mode === 'push'
|
||||||
return tile.cell.try_leaving(tile, direction);
|
return tile.cell.try_leaving(tile, direction, level);
|
||||||
}
|
}
|
||||||
|
|
||||||
can_pull(tile, direction) {
|
can_pull(tile, direction) {
|
||||||
@ -261,7 +261,7 @@ export class Cell extends Array {
|
|||||||
if (thin_walls && thin_walls.type.blocks_leaving && thin_walls.type.blocks_leaving(thin_walls, actor, direction)) {
|
if (thin_walls && thin_walls.type.blocks_leaving && thin_walls.type.blocks_leaving(thin_walls, actor, direction)) {
|
||||||
blocker = thin_walls;
|
blocker = thin_walls;
|
||||||
}
|
}
|
||||||
else if (terrain.type.traps && terrain.type.traps(terrain, actor)) {
|
else if (terrain.type.traps && terrain.type.traps(terrain, level, actor)) {
|
||||||
blocker = terrain;
|
blocker = terrain;
|
||||||
}
|
}
|
||||||
else if (terrain.type.blocks_leaving && terrain.type.blocks_leaving(terrain, actor, direction)) {
|
else if (terrain.type.blocks_leaving && terrain.type.blocks_leaving(terrain, actor, direction)) {
|
||||||
@ -302,6 +302,7 @@ export class Cell extends Array {
|
|||||||
// It seems the order is thus: canopy + thin wall; terrain; actor; item. Which is the usual
|
// It seems the order is thus: canopy + thin wall; terrain; actor; item. Which is the usual
|
||||||
// ordering from the top down, except that terrain is checked before actors. Really, the
|
// ordering from the top down, except that terrain is checked before actors. Really, the
|
||||||
// ordering is from "outermost" to "innermost", which makes physical sense.
|
// ordering is from "outermost" to "innermost", which makes physical sense.
|
||||||
|
let still_blocked = false;
|
||||||
for (let layer of [
|
for (let layer of [
|
||||||
LAYERS.canopy, LAYERS.thin_wall, LAYERS.terrain, LAYERS.swivel,
|
LAYERS.canopy, LAYERS.thin_wall, LAYERS.terrain, LAYERS.swivel,
|
||||||
LAYERS.actor, LAYERS.item_mod, LAYERS.item])
|
LAYERS.actor, LAYERS.item_mod, LAYERS.item])
|
||||||
@ -310,6 +311,7 @@ export class Cell extends Array {
|
|||||||
if (! tile)
|
if (! tile)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
let original_name = tile.type.name;
|
||||||
// TODO check ignores here?
|
// TODO check ignores here?
|
||||||
if (tile.type.on_bumped) {
|
if (tile.type.on_bumped) {
|
||||||
tile.type.on_bumped(tile, level, actor);
|
tile.type.on_bumped(tile, level, actor);
|
||||||
@ -325,7 +327,7 @@ export class Cell extends Array {
|
|||||||
if (push_mode === null)
|
if (push_mode === null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (actor.can_push(tile, direction) || (
|
if (actor.can_push(tile, direction, level) || (
|
||||||
level.compat.tanks_teeth_push_ice_blocks && tile.type.name === 'ice_block' &&
|
level.compat.tanks_teeth_push_ice_blocks && tile.type.name === 'ice_block' &&
|
||||||
(actor.type.name === 'teeth' || actor.type.name === 'teeth_timid' || actor.type.name === 'tank_blue')
|
(actor.type.name === 'teeth' || actor.type.name === 'teeth_timid' || actor.type.name === 'tank_blue')
|
||||||
)) {
|
)) {
|
||||||
@ -338,6 +340,13 @@ export class Cell extends Array {
|
|||||||
if (actor.type.on_blocked) {
|
if (actor.type.on_blocked) {
|
||||||
actor.type.on_blocked(actor, level, direction, tile);
|
actor.type.on_blocked(actor, level, direction, tile);
|
||||||
}
|
}
|
||||||
|
// Lynx (or at least TW?) allows pushing blocks off of particular wall types
|
||||||
|
if (level.compat.allow_pushing_blocks_off_faux_walls &&
|
||||||
|
['fake_wall', 'wall_invisible', 'wall_appearing'].includes(original_name))
|
||||||
|
{
|
||||||
|
still_blocked = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -417,7 +426,7 @@ export class Cell extends Array {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return ! still_blocked;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special railroad ability: change the direction we attempt to leave
|
// Special railroad ability: change the direction we attempt to leave
|
||||||
@ -1271,6 +1280,14 @@ export class Level extends LevelInterface {
|
|||||||
terrain.type.on_stand(terrain, this, actor);
|
terrain.type.on_stand(terrain, this, actor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Lynx gives everything in an open trap an extra cooldown, which makes things walk into
|
||||||
|
// open traps at double speed and does weird things to the ejection timing
|
||||||
|
if (this.compat.traps_like_lynx) {
|
||||||
|
let terrain = actor.cell.get_terrain();
|
||||||
|
if (terrain && terrain.type.name === 'trap' && terrain.presses > 0) {
|
||||||
|
this._do_extra_cooldown(actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (actor.just_stepped_on_teleporter) {
|
if (actor.just_stepped_on_teleporter) {
|
||||||
this.attempt_teleport(actor);
|
this.attempt_teleport(actor);
|
||||||
}
|
}
|
||||||
@ -1336,25 +1353,27 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Strip out any destroyed actors from the acting order
|
// Strip out any destroyed actors from the acting order
|
||||||
if (! this.compat.reuse_actor_slots) {
|
// 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?
|
let p = 0;
|
||||||
let p = 0;
|
for (let i = 0, l = this.actors.length; i < l; i++) {
|
||||||
for (let i = 0, l = this.actors.length; i < l; i++) {
|
let actor = this.actors[i];
|
||||||
let actor = this.actors[i];
|
if (actor.cell || (
|
||||||
if (actor.cell) {
|
// Don't strip out actors under Lynx, where slots were reused -- unless they're VFX,
|
||||||
if (p !== i) {
|
// which aren't in the original game and thus are exempt
|
||||||
this.actors[p] = actor;
|
this.compat.reuse_actor_slots && actor.type.layer !== LAYERS.vfx))
|
||||||
}
|
{
|
||||||
p++;
|
if (p !== i) {
|
||||||
}
|
this.actors[p] = actor;
|
||||||
else {
|
|
||||||
let local_p = p;
|
|
||||||
this._push_pending_undo(() => this.actors.splice(local_p, 0, actor));
|
|
||||||
}
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let local_p = p;
|
||||||
|
this._push_pending_undo(() => this.actors.splice(local_p, 0, actor));
|
||||||
}
|
}
|
||||||
this.actors.length = p;
|
|
||||||
}
|
}
|
||||||
|
this.actors.length = p;
|
||||||
|
|
||||||
// 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
|
||||||
@ -1574,7 +1593,7 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
if (forced_only)
|
if (forced_only)
|
||||||
return;
|
return;
|
||||||
if (terrain.type.traps && terrain.type.traps(terrain, actor)) {
|
if (terrain.type.traps && terrain.type.traps(terrain, this, 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
|
||||||
// when released, it will make one move out of the trap and /then/ turn around and
|
// when released, it will make one move out of the trap and /then/ turn around and
|
||||||
@ -1665,9 +1684,7 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
// Try to move the given actor one tile in the given direction and update their cooldown.
|
// Try to move the given actor one tile in the given direction and update their cooldown.
|
||||||
// Return true if successful.
|
// Return true if successful.
|
||||||
// ('frameskip' is an absolute number of frames subtracted from the normal speed, only used for
|
attempt_step(actor, direction) {
|
||||||
// Lynx's odd trap ejection behavior.)
|
|
||||||
attempt_step(actor, direction, frameskip = 0) {
|
|
||||||
// In mid-movement, we can't even change direction!
|
// In mid-movement, we can't even change direction!
|
||||||
if (actor.movement_cooldown > 0)
|
if (actor.movement_cooldown > 0)
|
||||||
return false;
|
return false;
|
||||||
@ -1713,7 +1730,7 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
let orig_cell = actor.cell;
|
let orig_cell = actor.cell;
|
||||||
this._set_tile_prop(actor, 'previous_cell', orig_cell);
|
this._set_tile_prop(actor, 'previous_cell', orig_cell);
|
||||||
let duration = Math.max(3, speed * 3 - frameskip);
|
let duration = speed * 3;
|
||||||
this._set_tile_prop(actor, 'movement_cooldown', duration);
|
this._set_tile_prop(actor, 'movement_cooldown', duration);
|
||||||
this._set_tile_prop(actor, 'movement_speed', duration);
|
this._set_tile_prop(actor, 'movement_speed', duration);
|
||||||
this.move_to(actor, goal_cell);
|
this.move_to(actor, goal_cell);
|
||||||
@ -1739,14 +1756,14 @@ export class Level extends LevelInterface {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
attempt_out_of_turn_step(actor, direction, frameskip = 0) {
|
attempt_out_of_turn_step(actor, direction) {
|
||||||
if (actor.slide_mode === 'turntable') {
|
if (actor.slide_mode === 'turntable') {
|
||||||
// Something is (e.g.) pushing a block that just landed on a turntable and is waiting to
|
// Something is (e.g.) pushing a block that just landed on a turntable and is waiting to
|
||||||
// slide out of it. Ignore the push direction and move in its current direction;
|
// slide out of it. Ignore the push direction and move in its current direction;
|
||||||
// otherwise a player will push a block straight through, then turn, which sucks
|
// otherwise a player will push a block straight through, then turn, which sucks
|
||||||
direction = actor.direction;
|
direction = actor.direction;
|
||||||
}
|
}
|
||||||
let success = this.attempt_step(actor, direction, frameskip);
|
let success = this.attempt_step(actor, direction);
|
||||||
if (success) {
|
if (success) {
|
||||||
this._do_extra_cooldown(actor);
|
this._do_extra_cooldown(actor);
|
||||||
}
|
}
|
||||||
@ -2653,8 +2670,9 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add_actor(actor) {
|
add_actor(actor) {
|
||||||
if (this.compat.reuse_actor_slots) {
|
if (this.compat.reuse_actor_slots && actor.type.layer !== LAYERS.vfx) {
|
||||||
// Place the new actor in the first slot taken up by a nonexistent one
|
// Place the new actor in the first slot taken up by a nonexistent one, but not VFX
|
||||||
|
// which aren't supposed to impact gameplay
|
||||||
for (let i = 0, l = this.actors.length; i < l; i++) {
|
for (let i = 0, l = this.actors.length; i < l; i++) {
|
||||||
let old_actor = this.actors[i];
|
let old_actor = this.actors[i];
|
||||||
if (old_actor !== this.player && ! old_actor.cell) {
|
if (old_actor !== this.player && ! old_actor.cell) {
|
||||||
@ -2677,9 +2695,12 @@ export class Level extends LevelInterface {
|
|||||||
let duration = tile.type.ttl;
|
let duration = tile.type.ttl;
|
||||||
if (this.compat.force_lynx_animation_lengths) {
|
if (this.compat.force_lynx_animation_lengths) {
|
||||||
// Lynx animation duration is 12 tics, but it drops one if necessary to make the
|
// Lynx animation duration is 12 tics, but it drops one if necessary to make the
|
||||||
// animation end on an even tic (???) and that takes step parity into account
|
// animation end on an odd tic (???) and that takes step parity into account
|
||||||
// because I guess it uses the global clock (?????????????????)
|
// because I guess it uses the global clock (?????????????????). Also, unlike CC2, Lynx
|
||||||
duration = (12 - (this.tic_counter + this.step_parity) % 1) * 3;
|
// animations are removed once their cooldown goes BELOW zero, so to simulate that we
|
||||||
|
// make the animation one tic longer.
|
||||||
|
// XXX wait am i sure that cc2 doesn't work that way too?
|
||||||
|
duration = (12 + (this.tic_counter + this.step_parity) % 2) * 3;
|
||||||
}
|
}
|
||||||
this._set_tile_prop(tile, 'movement_speed', duration);
|
this._set_tile_prop(tile, 'movement_speed', duration);
|
||||||
this._set_tile_prop(tile, 'movement_cooldown', duration);
|
this._set_tile_prop(tile, 'movement_cooldown', duration);
|
||||||
|
|||||||
@ -407,13 +407,22 @@ const TILE_TYPES = {
|
|||||||
fake_floor: {
|
fake_floor: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
blocks_collision: COLLISION.block_cc1 | COLLISION.monster_general,
|
blocks_collision: COLLISION.block_cc1 | COLLISION.monster_general,
|
||||||
|
reveal(me, level, other) {
|
||||||
|
level.spawn_animation(me.cell, 'puff');
|
||||||
|
level.transmute_tile(me, 'floor');
|
||||||
|
if (other === level.player) {
|
||||||
|
level.sfx.play_once('fake-floor', me.cell);
|
||||||
|
}
|
||||||
|
},
|
||||||
on_bumped(me, level, other) {
|
on_bumped(me, level, other) {
|
||||||
if (other.type.can_reveal_walls) {
|
if (other.type.can_reveal_walls && ! level.compat.blue_floors_vanish_on_arrive) {
|
||||||
level.spawn_animation(me.cell, 'puff');
|
this.reveal(me, level, other);
|
||||||
level.transmute_tile(me, 'floor');
|
}
|
||||||
if (other === level.player) {
|
},
|
||||||
level.sfx.play_once('fake-floor', me.cell);
|
on_arrive(me, level, other) {
|
||||||
}
|
// In Lynx, these disappear only when you step on them
|
||||||
|
if (level.compat.blue_floors_vanish_on_arrive) {
|
||||||
|
this.reveal(me, level, other);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1465,8 +1474,8 @@ const TILE_TYPES = {
|
|||||||
on_ready(me, level) {
|
on_ready(me, level) {
|
||||||
me.arrows = me.arrows ?? 0;
|
me.arrows = me.arrows ?? 0;
|
||||||
},
|
},
|
||||||
traps(me, actor) {
|
traps(me, level, other) {
|
||||||
return ! actor._clone_release;
|
return ! other._clone_release;
|
||||||
},
|
},
|
||||||
activate(me, level, aggressive = false) {
|
activate(me, level, aggressive = false) {
|
||||||
let actor = me.cell.get_actor();
|
let actor = me.cell.get_actor();
|
||||||
@ -1532,10 +1541,9 @@ const TILE_TYPES = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
// Lynx (not cc2): open traps immediately eject their contents on arrival, if possible,
|
// Lynx (not cc2): open traps immediately eject their contents on arrival, if possible
|
||||||
// and also do it slightly faster
|
|
||||||
if (level.compat.traps_like_lynx) {
|
if (level.compat.traps_like_lynx) {
|
||||||
level.attempt_out_of_turn_step(other, other.direction, 3);
|
level.attempt_out_of_turn_step(other, other.direction);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
add_press_ready(me, level, other) {
|
add_press_ready(me, level, other) {
|
||||||
@ -1544,17 +1552,13 @@ const TILE_TYPES = {
|
|||||||
},
|
},
|
||||||
add_press(me, level, is_wire = false) {
|
add_press(me, level, is_wire = false) {
|
||||||
level._set_tile_prop(me, 'presses', me.presses + 1);
|
level._set_tile_prop(me, 'presses', me.presses + 1);
|
||||||
// TODO weird cc2 case that may or may not be a bug: actors aren't ejected if the trap
|
|
||||||
// opened because of wiring
|
|
||||||
if (me.presses === 1 && ! is_wire) {
|
if (me.presses === 1 && ! is_wire) {
|
||||||
// Free any actor on us, if we went from 0 to 1 presses (i.e. closed to open)
|
// Free any actor on us, if we went from 0 to 1 presses (i.e. closed to open)
|
||||||
let actor = me.cell.get_actor();
|
let actor = me.cell.get_actor();
|
||||||
if (actor) {
|
if (actor) {
|
||||||
// Forcibly move anything released from a trap, which keeps it in sync with
|
// Forcibly move anything released from a trap, which keeps it in sync with
|
||||||
// whatever pushed the button
|
// whatever pushed the button
|
||||||
level.attempt_out_of_turn_step(
|
level.attempt_out_of_turn_step(actor, actor.direction);
|
||||||
actor, actor.direction,
|
|
||||||
level.compat.traps_like_lynx ? 3 : 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1565,8 +1569,19 @@ const TILE_TYPES = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// FIXME also doesn't trap ghosts, is that a special case???
|
// FIXME also doesn't trap ghosts, is that a special case???
|
||||||
traps(me, actor) {
|
traps(me, level, other) {
|
||||||
return ! me.presses && ! me._initially_open && actor.type.name !== 'ghost';
|
if (level.compat.traps_like_lynx) {
|
||||||
|
// Lynx traps don't actually track open vs closed; actors are just ejected by force
|
||||||
|
// by a separate pass at the end of the tic that checks what's on a brown button.
|
||||||
|
// That means a trap held open by a button at level start won't effectively be open
|
||||||
|
// if whatever's on the button moves within the first tic, a quirk that CCLXP2 #17
|
||||||
|
// Double Trouble critically relies on!
|
||||||
|
// To fix this, assume that a trap can never be released on the first turn.
|
||||||
|
// FIXME that's not right since a block or immobile mob might be on a button...
|
||||||
|
if (level.tic_counter === 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return ! me.presses && ! me._initially_open && other.type.name !== 'ghost';
|
||||||
},
|
},
|
||||||
on_power(me, level) {
|
on_power(me, level) {
|
||||||
// Treat being powered or not as an extra kind of brown button press
|
// Treat being powered or not as an extra kind of brown button press
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user