Add a bunch of Lynx compat options
This commit is contained in:
parent
a750a569ab
commit
be275d380d
@ -8,14 +8,25 @@ export class StoredCell extends Array {
|
||||
}
|
||||
|
||||
export class Replay {
|
||||
constructor(initial_force_floor_direction, blob_seed, inputs = null) {
|
||||
constructor(initial_force_floor_direction, blob_seed, inputs = null, step_parity = null, tw_seed = 0) {
|
||||
this.initial_force_floor_direction = initial_force_floor_direction;
|
||||
this.blob_seed = blob_seed;
|
||||
this.step_parity = step_parity;
|
||||
this.tw_seed = tw_seed;
|
||||
this.inputs = inputs ?? new Uint8Array;
|
||||
this.duration = this.inputs.length;
|
||||
this.cursor = 0;
|
||||
}
|
||||
|
||||
configure_level(level) {
|
||||
level.force_floor_direction = this.initial_force_floor_direction;
|
||||
level._blob_modifier = this.blob_seed;
|
||||
level._tw_rng = this.tw_seed;
|
||||
if (this.step_parity !== null) {
|
||||
level.step_parity = this.step_parity;
|
||||
}
|
||||
}
|
||||
|
||||
get(t) {
|
||||
if (this.duration <= 0) {
|
||||
return 0;
|
||||
|
||||
131
js/format-tws.js
Normal file
131
js/format-tws.js
Normal file
@ -0,0 +1,131 @@
|
||||
import { INPUT_BITS } from './defs.js';
|
||||
import * as format_base from './format-base.js';
|
||||
|
||||
|
||||
const TW_DIRECTION_TO_INPUT_BITS = [
|
||||
INPUT_BITS.up,
|
||||
INPUT_BITS.left,
|
||||
INPUT_BITS.down,
|
||||
INPUT_BITS.right,
|
||||
INPUT_BITS.up | INPUT_BITS.left,
|
||||
INPUT_BITS.down | INPUT_BITS.left,
|
||||
INPUT_BITS.up | INPUT_BITS.right,
|
||||
INPUT_BITS.down | INPUT_BITS.right,
|
||||
];
|
||||
|
||||
export function parse_solutions(bytes) {
|
||||
let buf;
|
||||
if (bytes.buffer) {
|
||||
buf = bytes.buffer;
|
||||
}
|
||||
else {
|
||||
buf = bytes;
|
||||
bytes = new Uint8Array(buf);
|
||||
}
|
||||
let view = new DataView(buf);
|
||||
let magic = view.getUint32(0, true);
|
||||
if (magic !== 0x999b3335)
|
||||
return;
|
||||
|
||||
// 1 for lynx, 2 for ms; also extended to 3 for cc2, 4 for ll
|
||||
let ruleset = bytes[4];
|
||||
let extra_bytes = bytes[7];
|
||||
|
||||
let ret = {
|
||||
ruleset: ruleset,
|
||||
levels: [],
|
||||
};
|
||||
|
||||
let p = 8 + extra_bytes;
|
||||
let is_first = true;
|
||||
while (p < buf.byteLength) {
|
||||
let len = view.getUint32(p, true);
|
||||
p += 4;
|
||||
if (len === 0xffffffff)
|
||||
break;
|
||||
|
||||
if (len === 0) {
|
||||
// Empty, do nothing
|
||||
}
|
||||
else if (len < 6) {
|
||||
// This should never happen
|
||||
// TODO gripe?
|
||||
}
|
||||
else if (bytes[p] === 0 && bytes[p + 1] === 0 && bytes[p + 2] === 0 &&
|
||||
bytes[p + 3] === 0 && bytes[p + 5] === 0 && bytes[p + 6] === 0)
|
||||
{
|
||||
// This record is special and contains the name of the set; it's optional but, if present, must be first
|
||||
if (! is_first) {
|
||||
// TODO gripe?
|
||||
}
|
||||
}
|
||||
else if (len === 6) {
|
||||
// Short record; password only, no replay
|
||||
}
|
||||
else {
|
||||
// Long record
|
||||
let number = view.getUint16(p, true);
|
||||
// Password is 2–5 but we don't care
|
||||
let flags = bytes[p + 6];
|
||||
let initial_state = bytes[p + 7];
|
||||
let step_parity = initial_state >> 3;
|
||||
let initial_rff = ['north', 'west', 'south', 'east'][initial_state & 0x7];
|
||||
let initial_rng = view.getUint32(p + 8, true); // FIXME how is this four bytes?? lynx rng doesn't even have four bytes of STATE
|
||||
console.log(number, initial_state.toString(16), initial_rng.toString(16));
|
||||
let total_duration = view.getUint32(p + 12, true);
|
||||
|
||||
// TODO split this off though
|
||||
let inputs = [];
|
||||
let q = p + 16;
|
||||
while (q < p + len) {
|
||||
// There are four formats for packing solutions, identified by the lowest two bits,
|
||||
// except that format 3 is actually two formats, don't ask
|
||||
let fmt = bytes[q] & 0x3;
|
||||
let fmt2 = (bytes[q] >> 4) & 0x1;
|
||||
if (fmt === 0) {
|
||||
let val = bytes[q];
|
||||
q += 1;
|
||||
let input1 = TW_DIRECTION_TO_INPUT_BITS[(val >> 2) & 0x3];
|
||||
let input2 = TW_DIRECTION_TO_INPUT_BITS[(val >> 4) & 0x3];
|
||||
let input3 = TW_DIRECTION_TO_INPUT_BITS[(val >> 6) & 0x3];
|
||||
inputs.push(
|
||||
0, 0, 0, input1,
|
||||
0, 0, 0, input2,
|
||||
0, 0, 0, input3,
|
||||
);
|
||||
}
|
||||
else if (fmt === 1 || fmt === 2 || (fmt === 3 && fmt2 === 0)) {
|
||||
let val;
|
||||
if (fmt === 1) {
|
||||
val = bytes[q];
|
||||
q += 1;
|
||||
}
|
||||
else if (fmt === 2) {
|
||||
val = view.getUint16(q, true);
|
||||
q += 2;
|
||||
}
|
||||
else {
|
||||
val = view.getUint32(q, true);
|
||||
q += 4;
|
||||
}
|
||||
let input = TW_DIRECTION_TO_INPUT_BITS[(val >> 2) & 0x7];
|
||||
let duration = val >> 5;
|
||||
for (let i = 0; i < duration; i++) {
|
||||
inputs.push(0);
|
||||
}
|
||||
inputs.push(input);
|
||||
}
|
||||
else { // 3-1
|
||||
// variable-size and only needed for ms so let's just hope not
|
||||
throw new Error;
|
||||
}
|
||||
}
|
||||
|
||||
ret.levels[number - 1] = new format_base.Replay(initial_rff, 0, inputs, step_parity, initial_rng);
|
||||
}
|
||||
|
||||
is_first = false;
|
||||
p += len;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
185
js/game.js
185
js/game.js
@ -59,7 +59,7 @@ export class Tile {
|
||||
// Extremely awkward special case: items don't block monsters if the cell also contains an
|
||||
// item modifier (i.e. "no" sign) or a real player
|
||||
// TODO would love to get this outta here
|
||||
if (this.type.is_item || this.type.is_chip) {
|
||||
if ((this.type.is_item || this.type.is_chip) && ! level.compat.monsters_blocked_by_items) {
|
||||
let item_mod = this.cell.get_item_mod();
|
||||
if (item_mod && item_mod.type.item_modifier)
|
||||
return false;
|
||||
@ -286,6 +286,7 @@ export class Cell extends Array {
|
||||
// - null: Default. Do not impact game state. Treat pushable objects as blocking.
|
||||
// - 'bump': Fire bump triggers. Don't move pushable objects, but do check whether they /could/
|
||||
// be pushed, recursively if necessary.
|
||||
// - 'slap': Like 'bump', but also sets the 'decision' of pushable objects.
|
||||
// - 'push': Fire bump triggers. Attempt to move pushable objects out of the way immediately.
|
||||
try_entering(actor, direction, level, push_mode = null) {
|
||||
let pushable_tiles = [];
|
||||
@ -352,7 +353,7 @@ export class Cell extends Array {
|
||||
for (let tile of pushable_tiles) {
|
||||
if (tile._trying_to_push)
|
||||
return false;
|
||||
if (push_mode === 'bump') {
|
||||
if (push_mode === 'bump' || push_mode === 'slap') {
|
||||
// FIXME this doesn't take railroad curves into account, e.g. it thinks a
|
||||
// rover can't push a block through a curve
|
||||
if (tile.movement_cooldown > 0 ||
|
||||
@ -360,6 +361,13 @@ export class Cell extends Array {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (push_mode === 'slap') {
|
||||
if (actor === level.player) {
|
||||
level._set_tile_prop(actor, 'is_pushing', true);
|
||||
level.sfx.play_once('push');
|
||||
}
|
||||
tile.decision = direction;
|
||||
}
|
||||
}
|
||||
else if (push_mode === 'push') {
|
||||
if (actor === level.player) {
|
||||
@ -485,6 +493,7 @@ export class Level extends LevelInterface {
|
||||
// PRNG is initialized to zero
|
||||
this._rng1 = 0;
|
||||
this._rng2 = 0;
|
||||
this._tw_rng = Math.floor(Math.random() * 0x80000000);
|
||||
if (this.stored_level.blob_behavior === 0) {
|
||||
this._blob_modifier = 0x55;
|
||||
}
|
||||
@ -553,6 +562,12 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.compat.player_moves_last) {
|
||||
let i = this.actors.indexOf(this.player);
|
||||
if (i > 0) {
|
||||
[this.actors[0], this.actors[i]] = [this.actors[i], this.actors[0]];
|
||||
}
|
||||
}
|
||||
// TODO complain if no player
|
||||
// Used for doppelgängers
|
||||
this.player1_move = null;
|
||||
@ -822,6 +837,16 @@ export class Level extends LevelInterface {
|
||||
return this._rng1 ^ this._rng2;
|
||||
}
|
||||
|
||||
// Tile World's PRNG, used for blobs in Tile World Lynx
|
||||
_advance_tw_prng() {
|
||||
this._tw_rng = ((Math.imul(this._tw_rng, 1103515245) & 0x7fffffff) + 12345) & 0x7fffffff;
|
||||
}
|
||||
tw_prng_random4() {
|
||||
let x = this._tw_rng;
|
||||
this._advance_tw_prng();
|
||||
return this._tw_rng >> 29;
|
||||
}
|
||||
|
||||
// Weird thing done by CC2 to make blobs... more... random
|
||||
get_blob_modifier() {
|
||||
let mod = this._blob_modifier;
|
||||
@ -883,7 +908,7 @@ export class Level extends LevelInterface {
|
||||
// Store some current level state in the undo entry. (These will often not be modified, but
|
||||
// they only take a few bytes each so that's fine.)
|
||||
for (let key of [
|
||||
'_rng1', '_rng2', '_blob_modifier', 'force_floor_direction',
|
||||
'_rng1', '_rng2', '_blob_modifier', '_tw_rng', 'force_floor_direction',
|
||||
'tic_counter', 'time_remaining', 'timer_paused',
|
||||
'chips_remaining', 'bonus_points', 'state',
|
||||
'player1_move', 'player2_move', 'remaining_players', 'player',
|
||||
@ -966,15 +991,7 @@ export class Level extends LevelInterface {
|
||||
if (actor.type.ttl)
|
||||
continue;
|
||||
|
||||
if (actor.movement_cooldown <= 0) {
|
||||
let terrain = actor.cell.get_terrain();
|
||||
if (terrain.type.on_stand && ! actor.ignores(terrain.type.name)) {
|
||||
terrain.type.on_stand(terrain, this, actor);
|
||||
}
|
||||
}
|
||||
if (actor.just_stepped_on_teleporter) {
|
||||
this.attempt_teleport(actor);
|
||||
}
|
||||
this._do_actor_idle(actor);
|
||||
}
|
||||
|
||||
this._swap_players();
|
||||
@ -1098,15 +1115,7 @@ export class Level extends LevelInterface {
|
||||
continue;
|
||||
|
||||
this._do_actor_cooldown(actor, cooldown);
|
||||
if (actor.movement_cooldown <= 0) {
|
||||
let terrain = actor.cell.get_terrain();
|
||||
if (terrain.type.on_stand && ! actor.ignores(terrain.type.name)) {
|
||||
terrain.type.on_stand(terrain, this, actor);
|
||||
}
|
||||
}
|
||||
if (actor.just_stepped_on_teleporter) {
|
||||
this.attempt_teleport(actor);
|
||||
}
|
||||
this._do_actor_idle(actor);
|
||||
}
|
||||
|
||||
this._swap_players();
|
||||
@ -1159,7 +1168,7 @@ export class Level extends LevelInterface {
|
||||
terrain.type.on_arrive(terrain, this, actor);
|
||||
}
|
||||
// If we got a new direction, try moving again
|
||||
if (direction !== actor.direction) {
|
||||
if (direction !== actor.direction && ! this.compat.bonking_isnt_instant) {
|
||||
success = this.attempt_step(actor, actor.direction);
|
||||
}
|
||||
}
|
||||
@ -1212,6 +1221,18 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
}
|
||||
|
||||
_do_actor_idle(actor) {
|
||||
if (actor.movement_cooldown <= 0) {
|
||||
let terrain = actor.cell.get_terrain();
|
||||
if (terrain.type.on_stand && ! actor.ignores(terrain.type.name)) {
|
||||
terrain.type.on_stand(terrain, this, actor);
|
||||
}
|
||||
}
|
||||
if (actor.just_stepped_on_teleporter) {
|
||||
this.attempt_teleport(actor);
|
||||
}
|
||||
}
|
||||
|
||||
_swap_players() {
|
||||
if (this.remaining_players <= 0) {
|
||||
this.win();
|
||||
@ -1250,6 +1271,9 @@ export class Level extends LevelInterface {
|
||||
|
||||
if (actor.type.is_real_player) {
|
||||
this.player = actor;
|
||||
if (this.compat.player_moves_last && i !== 0) {
|
||||
[this.actors[0], this.actors[i]] = [this.actors[i], this.actors[0]];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1257,7 +1281,19 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
|
||||
_do_cleanup_phase() {
|
||||
// Lynx compat: Any blue tank that still has the reversal flag set here, but is in motion,
|
||||
// should ignore it. Unfortunately this has to be done as its own pass (as it is in Lynx!)
|
||||
// because of acting order issues
|
||||
if (this.compat.tanks_ignore_button_while_moving) {
|
||||
for (let actor of this.actors) {
|
||||
if (actor.cell && actor.pending_reverse && actor.movement_cooldown > 0) {
|
||||
this._set_tile_prop(actor, 'pending_reverse', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
@ -1275,6 +1311,7 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
}
|
||||
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
|
||||
@ -1399,11 +1436,11 @@ export class Level extends LevelInterface {
|
||||
// At this point, we have exactly 1 or 2 directions, and deciding between them requires
|
||||
// checking which ones are blocked. Note that we do this even if only one direction is
|
||||
// requested, meaning that we get to push blocks before anything else has moved!
|
||||
let push_mode = this.compat.no_early_push ? 'bump' : 'push';
|
||||
let push_mode = this.compat.no_early_push ? 'slap' : 'push';
|
||||
let open;
|
||||
if (dir2 === null) {
|
||||
// Only one direction is held, but for consistency, "check" it anyway
|
||||
open = try_direction(dir1, 'push');
|
||||
open = try_direction(dir1, push_mode);
|
||||
actor.decision = dir1;
|
||||
}
|
||||
else {
|
||||
@ -1413,8 +1450,8 @@ export class Level extends LevelInterface {
|
||||
// interpret our input!
|
||||
if (dir1 === actor.direction || dir2 === actor.direction) {
|
||||
let other_direction = dir1 === actor.direction ? dir2 : dir1;
|
||||
let curr_open = try_direction(actor.direction, 'push');
|
||||
let other_open = try_direction(other_direction, 'push');
|
||||
let curr_open = try_direction(actor.direction, push_mode);
|
||||
let other_open = try_direction(other_direction, push_mode);
|
||||
if (! curr_open && other_open) {
|
||||
actor.decision = other_direction;
|
||||
open = true;
|
||||
@ -1429,8 +1466,8 @@ export class Level extends LevelInterface {
|
||||
// FIXME i'm told cc2 prefers orthogonal actually, but need to check on that
|
||||
// FIXME lynx only checks horizontal, what about cc2? it must check both
|
||||
// because of the behavior where pushing into a corner always pushes horizontal
|
||||
let open1 = try_direction(dir1, 'push');
|
||||
let open2 = try_direction(dir2, 'push');
|
||||
let open1 = try_direction(dir1, push_mode);
|
||||
let open2 = try_direction(dir2, push_mode);
|
||||
if (open1 && ! open2) {
|
||||
actor.decision = dir1;
|
||||
open = true;
|
||||
@ -1451,8 +1488,8 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
|
||||
// If we're overriding a force floor but the direction we're moving in is blocked, this
|
||||
// counts as a forced move
|
||||
if (actor.slide_mode === 'force' && ! open) {
|
||||
// counts as a forced move (but only under the CC2 behavior of instant bonking)
|
||||
if (actor.slide_mode === 'force' && ! open && ! this.compat.bonking_isnt_instant) {
|
||||
this._set_tile_prop(actor, 'last_move_was_force', true);
|
||||
}
|
||||
else {
|
||||
@ -1536,6 +1573,14 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
|
||||
check_movement(actor, orig_cell, direction, push_mode) {
|
||||
// Lynx: Players can't override backwards on force floors, and it functions like blocking,
|
||||
// but does NOT act like a bonk (hence why it's here)
|
||||
if (this.compat.no_backwards_override && actor === this.player &&
|
||||
actor.slide_mode === 'force' && direction === DIRECTIONS[actor.direction].opposite)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let dest_cell = this.get_neighboring_cell(orig_cell, direction);
|
||||
if (! dest_cell) {
|
||||
if (push_mode === 'push') {
|
||||
@ -1577,7 +1622,9 @@ 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.
|
||||
attempt_step(actor, direction) {
|
||||
// ('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) {
|
||||
// In mid-movement, we can't even change direction!
|
||||
if (actor.movement_cooldown > 0)
|
||||
return false;
|
||||
@ -1594,15 +1641,17 @@ export class Level extends LevelInterface {
|
||||
|
||||
direction = redirected_direction;
|
||||
}
|
||||
this.set_actor_direction(actor, direction);
|
||||
|
||||
// Grab speed /first/, in case the movement or on_blocked turns us into an animation
|
||||
// immediately (and then we won't have a speed!)
|
||||
// FIXME that's a weird case actually since the explosion ends up still moving
|
||||
let speed = actor.type.movement_speed;
|
||||
|
||||
let move = DIRECTIONS[direction].movement;
|
||||
if (! this.check_movement(actor, actor.cell, direction, 'push'))
|
||||
let success = this.check_movement(actor, actor.cell, direction, 'push');
|
||||
// Only set direction after checking movement; check_movement needs it for preventing
|
||||
// backwards overriding in Lynx
|
||||
this.set_actor_direction(actor, direction);
|
||||
if (! success)
|
||||
return false;
|
||||
|
||||
// We're clear! Compute our speed and move us
|
||||
@ -1621,9 +1670,10 @@ export class Level extends LevelInterface {
|
||||
|
||||
let orig_cell = actor.cell;
|
||||
this._set_tile_prop(actor, 'previous_cell', orig_cell);
|
||||
this._set_tile_prop(actor, 'movement_cooldown', speed * 3);
|
||||
this._set_tile_prop(actor, 'movement_speed', speed * 3);
|
||||
this.move_to(actor, goal_cell, speed);
|
||||
let duration = Math.max(3, speed * 3 - frameskip);
|
||||
this._set_tile_prop(actor, 'movement_cooldown', duration);
|
||||
this._set_tile_prop(actor, 'movement_speed', duration);
|
||||
this.move_to(actor, goal_cell);
|
||||
|
||||
// Do Lexy-style hooking here: only attempt to pull things just after we've actually moved
|
||||
// successfully, which means the hook can never stop us from moving and hook slapping is not
|
||||
@ -1646,8 +1696,8 @@ export class Level extends LevelInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
attempt_out_of_turn_step(actor, direction) {
|
||||
let success = this.attempt_step(actor, direction);
|
||||
attempt_out_of_turn_step(actor, direction, frameskip = 0) {
|
||||
let success = this.attempt_step(actor, direction, frameskip);
|
||||
if (success) {
|
||||
this._do_extra_cooldown(actor);
|
||||
}
|
||||
@ -1665,7 +1715,7 @@ export class Level extends LevelInterface {
|
||||
// Move the given actor to the given position and perform any appropriate
|
||||
// tile interactions. Does NOT check for whether the move is actually
|
||||
// legal; use attempt_step for that!
|
||||
move_to(actor, goal_cell, speed) {
|
||||
move_to(actor, goal_cell) {
|
||||
if (actor.cell === goal_cell)
|
||||
return;
|
||||
|
||||
@ -2524,13 +2574,42 @@ 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
|
||||
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) {
|
||||
this.actors[i] = actor;
|
||||
this._push_pending_undo(() => this.actors[i] = old_actor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.actors.push(actor);
|
||||
this._push_pending_undo(() => this.actors.pop());
|
||||
}
|
||||
|
||||
_init_animation(tile) {
|
||||
// Co-opt movement_cooldown/speed for these despite that they aren't moving, since those
|
||||
// properties are also used to animate everything else anyway. Decrement the cooldown
|
||||
// immediately, as Lynx does; note that Lynx also ticks /and destroys/ animations early in
|
||||
// the decision phase, but this seems to work out just as well
|
||||
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;
|
||||
}
|
||||
this._set_tile_prop(tile, 'movement_speed', duration);
|
||||
this._set_tile_prop(tile, 'movement_cooldown', duration);
|
||||
this._do_extra_cooldown(tile);
|
||||
}
|
||||
|
||||
spawn_animation(cell, name) {
|
||||
let type = TILE_TYPES[name];
|
||||
// Spawned VFX erase any existing VFX
|
||||
// Spawned VFX silently erase any existing VFX
|
||||
if (type.layer === LAYERS.vfx) {
|
||||
let vfx = cell[type.layer];
|
||||
if (vfx) {
|
||||
@ -2538,19 +2617,9 @@ export class Level extends LevelInterface {
|
||||
}
|
||||
}
|
||||
let tile = new Tile(type);
|
||||
// Co-opt movement_cooldown/speed for these despite that they aren't moving, since those
|
||||
// properties are also used to animate everything else anyway. Decrement the cooldown
|
||||
// immediately, as Lynx does; note that Lynx also ticks /and destroys/ animations early in
|
||||
// the decision phase, but this seems to work out just as well
|
||||
this._set_tile_prop(tile, 'movement_speed', tile.type.ttl);
|
||||
this._set_tile_prop(tile, 'movement_cooldown', tile.type.ttl);
|
||||
this._do_extra_cooldown(tile);
|
||||
cell._add(tile);
|
||||
this.actors.push(tile);
|
||||
this._push_pending_undo(() => {
|
||||
this.actors.pop();
|
||||
cell._remove(tile);
|
||||
});
|
||||
this._init_animation(tile);
|
||||
this.add_tile(tile, cell);
|
||||
this.add_actor(tile);
|
||||
}
|
||||
|
||||
transmute_tile(tile, name) {
|
||||
@ -2585,15 +2654,13 @@ export class Level extends LevelInterface {
|
||||
if (! old_type.is_actor) {
|
||||
console.warn("Transmuting a non-actor into an animation!");
|
||||
}
|
||||
this._set_tile_prop(tile, 'previous_cell', null);
|
||||
this._set_tile_prop(tile, 'movement_speed', tile.type.ttl);
|
||||
this._set_tile_prop(tile, 'movement_cooldown', tile.type.ttl);
|
||||
// This is effectively a completely new object, so remove double cooldown prevention;
|
||||
// this cooldown MUST happen, because the renderer can't handle cooldown == speed
|
||||
// the initial cooldown MUST happen, because the renderer can't handle cooldown == speed
|
||||
if (tile.last_extra_cooldown_tic) {
|
||||
this._set_tile_prop(tile, 'last_extra_cooldown_tic', null);
|
||||
}
|
||||
this._do_extra_cooldown(tile);
|
||||
this._init_animation(tile);
|
||||
this._set_tile_prop(tile, 'previous_cell', null);
|
||||
}
|
||||
|
||||
if (old_type.on_death) {
|
||||
|
||||
49
js/main.js
49
js/main.js
@ -6,6 +6,7 @@ import { DIRECTIONS, INPUT_BITS, TICS_PER_SECOND } from './defs.js';
|
||||
import * as c2g from './format-c2g.js';
|
||||
import * as dat from './format-dat.js';
|
||||
import * as format_base from './format-base.js';
|
||||
import * as format_tws from './format-tws.js';
|
||||
import { Level } from './game.js';
|
||||
import { PrimaryView, Overlay, DialogOverlay, ConfirmOverlay, flash_button, load_json_from_storage, save_json_to_storage } from './main-base.js';
|
||||
import { Editor } from './main-editor.js';
|
||||
@ -1122,7 +1123,7 @@ class Player extends PrimaryView {
|
||||
return;
|
||||
|
||||
let [x, y] = this.renderer.cell_coords_from_event(ev);
|
||||
this.level.move_to(this.level.player, this.level.cell(x, y), 1);
|
||||
this.level.move_to(this.level.player, this.level.cell(x, y));
|
||||
// TODO this behaves a bit weirdly when paused (doesn't redraw even with a force), i
|
||||
// think because we're still claiming a speed of 1 so time has to pass before the move
|
||||
// actually "happens"
|
||||
@ -1341,8 +1342,7 @@ class Player extends PrimaryView {
|
||||
this.debug.replay_duration_el.textContent = format_replay_duration(t);
|
||||
|
||||
if (! record) {
|
||||
this.level.force_floor_direction = replay.initial_force_floor_direction;
|
||||
this.level._blob_modifier = replay.blob_seed;
|
||||
replay.configure_level(this.level);
|
||||
// FIXME should probably start playback on first real input
|
||||
this.set_state('playing');
|
||||
}
|
||||
@ -2979,10 +2979,22 @@ const COMPAT_FLAGS = [
|
||||
key: 'use_lynx_loop',
|
||||
label: "Game uses the Lynx-style update loop",
|
||||
rulesets: new Set(['steam', 'steam-strict', 'lynx', 'ms']),
|
||||
}, {
|
||||
key: 'player_moves_last',
|
||||
label: "Player always moves last",
|
||||
rulesets: new Set(['lynx', 'ms']),
|
||||
}, {
|
||||
key: 'emulate_60fps',
|
||||
label: "Game runs at 60 FPS",
|
||||
rulesets: new Set(['steam', 'steam-strict']),
|
||||
}, {
|
||||
key: 'reuse_actor_slots',
|
||||
label: "Game reuses slots in the actor list",
|
||||
rulesets: new Set(['lynx']),
|
||||
}, {
|
||||
key: 'force_lynx_animation_lengths',
|
||||
label: "Animations use Lynx duration",
|
||||
rulesets: new Set(['lynx']),
|
||||
},
|
||||
|
||||
// Tiles
|
||||
@ -2995,6 +3007,14 @@ const COMPAT_FLAGS = [
|
||||
key: 'rff_actually_random',
|
||||
label: "Random force floors are actually random",
|
||||
rulesets: new Set(['ms']),
|
||||
}, {
|
||||
key: 'no_backwards_override',
|
||||
label: "Player can't override backwards on a force floor",
|
||||
rulesets: new Set(['lynx']),
|
||||
}, {
|
||||
key: 'traps_like_lynx',
|
||||
label: "Traps eject faster, and even when already open",
|
||||
rulesets: new Set(['lynx']),
|
||||
},
|
||||
|
||||
// Items
|
||||
@ -3010,6 +3030,10 @@ const COMPAT_FLAGS = [
|
||||
key: 'monsters_ignore_keys',
|
||||
label: "Monsters completely ignore keys",
|
||||
rulesets: new Set(['ms']),
|
||||
}, {
|
||||
key: 'monsters_blocked_by_items',
|
||||
label: "Monsters can't step on items to get the player",
|
||||
rulesets: new Set(['lynx']),
|
||||
},
|
||||
|
||||
// Blocks
|
||||
@ -3047,10 +3071,26 @@ const COMPAT_FLAGS = [
|
||||
key: 'tanks_always_obey_button',
|
||||
label: "Blue tanks always obey blue buttons",
|
||||
rulesets: new Set(['steam-strict']),
|
||||
}, {
|
||||
key: 'tanks_ignore_button_while_moving',
|
||||
label: "Blue tanks ignore blue buttons while moving",
|
||||
rulesets: new Set(['lynx']),
|
||||
}, {
|
||||
key: 'blobs_use_tw_prng',
|
||||
label: "Blobs use the Tile World RNG",
|
||||
rulesets: new Set(['lynx']),
|
||||
}, {
|
||||
key: 'teeth_target_internal_position',
|
||||
label: "Teeth target the player's internal position",
|
||||
rulesets: new Set(['lynx']),
|
||||
}, {
|
||||
key: 'rff_blocks_monsters',
|
||||
label: "Random force floors block monsters",
|
||||
rulesets: new Set(['ms']),
|
||||
}, {
|
||||
key: 'bonking_isnt_instant',
|
||||
label: "Bonking while sliding doesn't apply instantly",
|
||||
rulesets: new Set(['lynx', 'ms']),
|
||||
}, {
|
||||
key: 'fire_allows_monsters',
|
||||
label: "Fire doesn't block monsters",
|
||||
@ -3355,9 +3395,8 @@ class PackTestDialog extends DialogOverlay {
|
||||
let replay = stored_level.replay;
|
||||
level = new Level(stored_level, compat);
|
||||
level.sfx = dummy_sfx;
|
||||
level.force_floor_direction = replay.initial_force_floor_direction;
|
||||
level._blob_modifier = replay.blob_seed;
|
||||
level.undo_enabled = false; // slight performance boost
|
||||
replay.configure_level(level);
|
||||
|
||||
while (true) {
|
||||
let input = replay.get(level.tic_counter);
|
||||
|
||||
@ -191,7 +191,14 @@ function pursue_player(me, level) {
|
||||
let player = level.player;
|
||||
// CC2 behavior (not Lynx (TODO compat?)): pursue the player's apparent position, not just the
|
||||
// cell they're in
|
||||
let [px, py] = player.visual_position();
|
||||
let px, py;
|
||||
if (level.compat.teeth_target_internal_position) {
|
||||
px = player.cell.x;
|
||||
py = player.cell.y;
|
||||
}
|
||||
else {
|
||||
[px, py] = player.visual_position();
|
||||
}
|
||||
|
||||
let dx = me.cell.x - px;
|
||||
let dy = me.cell.y - py;
|
||||
@ -1495,11 +1502,17 @@ const TILE_TYPES = {
|
||||
level._set_tile_prop(me, 'presses', 0);
|
||||
}
|
||||
},
|
||||
on_arrive(me, level, other) {
|
||||
// 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) {
|
||||
level.attempt_out_of_turn_step(other, other.direction, 3);
|
||||
}
|
||||
},
|
||||
add_press_ready(me, level, other) {
|
||||
// Same as below, but without ejection
|
||||
level._set_tile_prop(me, 'presses', (me.presses ?? 0) + 1);
|
||||
},
|
||||
// Lynx (not cc2): open traps immediately eject their contents on arrival, if possible
|
||||
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
|
||||
@ -1510,7 +1523,9 @@ const TILE_TYPES = {
|
||||
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.attempt_out_of_turn_step(
|
||||
actor, actor.direction,
|
||||
level.compat.traps_like_lynx ? 3 : 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -2399,8 +2414,15 @@ const TILE_TYPES = {
|
||||
movement_speed: 8,
|
||||
decide_movement(me, level) {
|
||||
// move completely at random
|
||||
let d;
|
||||
if (level.compat.blobs_use_tw_prng) {
|
||||
d = level.tw_prng_random4();
|
||||
}
|
||||
else {
|
||||
let modifier = level.get_blob_modifier();
|
||||
return [DIRECTION_ORDER[(level.prng() + modifier) % 4]];
|
||||
d = (level.prng() + modifier) % 4;
|
||||
}
|
||||
return [DIRECTION_ORDER[d]];
|
||||
},
|
||||
},
|
||||
teeth: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user