Fix a few more Lynx compat issues

This commit is contained in:
Eevee (Evelyn Woods) 2021-03-08 23:53:52 -07:00
parent 2dcd73d44a
commit 63609ba77e
3 changed files with 95 additions and 51 deletions

View File

@ -178,6 +178,10 @@ export const COMPAT_FLAGS = [
key: 'traps_like_lynx',
label: "Traps eject faster, and even when already open",
rulesets: new Set(['lynx']),
}, {
key: 'blue_floors_vanish_on_arrive',
label: "Fake blue walls vanish on arrival",
rulesets: new Set(['lynx']),
},
// Items
@ -217,6 +221,10 @@ export const COMPAT_FLAGS = [
key: 'tanks_teeth_push_ice_blocks',
label: "Ice blocks emulate pgchip rules",
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',
label: "Spring mining is possible",

View File

@ -134,7 +134,7 @@ export class Tile {
return false;
}
can_push(tile, direction) {
can_push(tile, direction, level) {
// This tile already has a push queued, sorry
if (tile.pending_push)
return false;
@ -158,7 +158,7 @@ export class Tile {
// 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
// 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) {
@ -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)) {
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;
}
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
// ordering from the top down, except that terrain is checked before actors. Really, the
// ordering is from "outermost" to "innermost", which makes physical sense.
let still_blocked = false;
for (let layer of [
LAYERS.canopy, LAYERS.thin_wall, LAYERS.terrain, LAYERS.swivel,
LAYERS.actor, LAYERS.item_mod, LAYERS.item])
@ -310,6 +311,7 @@ export class Cell extends Array {
if (! tile)
continue;
let original_name = tile.type.name;
// TODO check ignores here?
if (tile.type.on_bumped) {
tile.type.on_bumped(tile, level, actor);
@ -325,7 +327,7 @@ export class Cell extends Array {
if (push_mode === null)
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' &&
(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) {
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;
}
@ -417,7 +426,7 @@ export class Cell extends Array {
return false;
}
return true;
return ! still_blocked;
}
// 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);
}
}
// 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) {
this.attempt_teleport(actor);
}
@ -1336,25 +1353,27 @@ export class Level extends LevelInterface {
}
// 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
// necessary, either; maybe only do it every few tics?
let p = 0;
for (let i = 0, l = this.actors.length; i < l; i++) {
let actor = this.actors[i];
if (actor.cell) {
if (p !== i) {
this.actors[p] = actor;
}
p++;
}
else {
let local_p = p;
this._push_pending_undo(() => this.actors.splice(local_p, 0, actor));
// 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?
let p = 0;
for (let i = 0, l = this.actors.length; i < l; i++) {
let actor = this.actors[i];
if (actor.cell || (
// Don't strip out actors under Lynx, where slots were reused -- unless they're VFX,
// which aren't in the original game and thus are exempt
this.compat.reuse_actor_slots && actor.type.layer !== LAYERS.vfx))
{
if (p !== i) {
this.actors[p] = 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
// 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)
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
// 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
@ -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.
// Return true if successful.
// ('frameskip' is an absolute number of frames subtracted from the normal speed, only used for
// Lynx's odd trap ejection behavior.)
attempt_step(actor, direction, frameskip = 0) {
attempt_step(actor, direction) {
// In mid-movement, we can't even change direction!
if (actor.movement_cooldown > 0)
return false;
@ -1713,7 +1730,7 @@ export class Level extends LevelInterface {
let orig_cell = actor.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_speed', duration);
this.move_to(actor, goal_cell);
@ -1739,14 +1756,14 @@ export class Level extends LevelInterface {
return true;
}
attempt_out_of_turn_step(actor, direction, frameskip = 0) {
attempt_out_of_turn_step(actor, direction) {
if (actor.slide_mode === 'turntable') {
// 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;
// otherwise a player will push a block straight through, then turn, which sucks
direction = actor.direction;
}
let success = this.attempt_step(actor, direction, frameskip);
let success = this.attempt_step(actor, direction);
if (success) {
this._do_extra_cooldown(actor);
}
@ -2653,8 +2670,9 @@ export class Level extends LevelInterface {
}
add_actor(actor) {
if (this.compat.reuse_actor_slots) {
// Place the new actor in the first slot taken up by a nonexistent one
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, but not VFX
// which aren't supposed to impact gameplay
for (let i = 0, l = this.actors.length; i < l; i++) {
let old_actor = this.actors[i];
if (old_actor !== this.player && ! old_actor.cell) {
@ -2677,9 +2695,12 @@ export class Level extends LevelInterface {
let duration = tile.type.ttl;
if (this.compat.force_lynx_animation_lengths) {
// 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
// because I guess it uses the global clock (?????????????????)
duration = (12 - (this.tic_counter + this.step_parity) % 1) * 3;
// animation end on an odd tic (???) and that takes step parity into account
// because I guess it uses the global clock (?????????????????). Also, unlike CC2, Lynx
// 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_cooldown', duration);

View File

@ -407,13 +407,22 @@ const TILE_TYPES = {
fake_floor: {
layer: LAYERS.terrain,
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) {
if (other.type.can_reveal_walls) {
level.spawn_animation(me.cell, 'puff');
level.transmute_tile(me, 'floor');
if (other === level.player) {
level.sfx.play_once('fake-floor', me.cell);
}
if (other.type.can_reveal_walls && ! level.compat.blue_floors_vanish_on_arrive) {
this.reveal(me, level, other);
}
},
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) {
me.arrows = me.arrows ?? 0;
},
traps(me, actor) {
return ! actor._clone_release;
traps(me, level, other) {
return ! other._clone_release;
},
activate(me, level, aggressive = false) {
let actor = me.cell.get_actor();
@ -1532,10 +1541,9 @@ const TILE_TYPES = {
}
},
on_arrive(me, level, other) {
// Lynx (not cc2): open traps immediately eject their contents on arrival, if possible,
// and also do it slightly faster
// Lynx (not cc2): open traps immediately eject their contents on arrival, if possible
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) {
@ -1544,17 +1552,13 @@ const TILE_TYPES = {
},
add_press(me, level, is_wire = false) {
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) {
// Free any actor on us, if we went from 0 to 1 presses (i.e. closed to open)
let actor = me.cell.get_actor();
if (actor) {
// Forcibly move anything released from a trap, which keeps it in sync with
// whatever pushed the button
level.attempt_out_of_turn_step(
actor, actor.direction,
level.compat.traps_like_lynx ? 3 : 0);
level.attempt_out_of_turn_step(actor, actor.direction);
}
}
},
@ -1565,8 +1569,19 @@ const TILE_TYPES = {
}
},
// FIXME also doesn't trap ghosts, is that a special case???
traps(me, actor) {
return ! me.presses && ! me._initially_open && actor.type.name !== 'ghost';
traps(me, level, other) {
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) {
// Treat being powered or not as an extra kind of brown button press