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 {
|
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.initial_force_floor_direction = initial_force_floor_direction;
|
||||||
this.blob_seed = blob_seed;
|
this.blob_seed = blob_seed;
|
||||||
|
this.step_parity = step_parity;
|
||||||
|
this.tw_seed = tw_seed;
|
||||||
this.inputs = inputs ?? new Uint8Array;
|
this.inputs = inputs ?? new Uint8Array;
|
||||||
this.duration = this.inputs.length;
|
this.duration = this.inputs.length;
|
||||||
this.cursor = 0;
|
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) {
|
get(t) {
|
||||||
if (this.duration <= 0) {
|
if (this.duration <= 0) {
|
||||||
return 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;
|
||||||
|
}
|
||||||
215
js/game.js
215
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
|
// 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
|
// item modifier (i.e. "no" sign) or a real player
|
||||||
// TODO would love to get this outta here
|
// 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();
|
let item_mod = this.cell.get_item_mod();
|
||||||
if (item_mod && item_mod.type.item_modifier)
|
if (item_mod && item_mod.type.item_modifier)
|
||||||
return false;
|
return false;
|
||||||
@ -286,6 +286,7 @@ export class Cell extends Array {
|
|||||||
// - null: Default. Do not impact game state. Treat pushable objects as blocking.
|
// - 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/
|
// - 'bump': Fire bump triggers. Don't move pushable objects, but do check whether they /could/
|
||||||
// be pushed, recursively if necessary.
|
// 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.
|
// - 'push': Fire bump triggers. Attempt to move pushable objects out of the way immediately.
|
||||||
try_entering(actor, direction, level, push_mode = null) {
|
try_entering(actor, direction, level, push_mode = null) {
|
||||||
let pushable_tiles = [];
|
let pushable_tiles = [];
|
||||||
@ -352,7 +353,7 @@ export class Cell extends Array {
|
|||||||
for (let tile of pushable_tiles) {
|
for (let tile of pushable_tiles) {
|
||||||
if (tile._trying_to_push)
|
if (tile._trying_to_push)
|
||||||
return false;
|
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
|
// FIXME this doesn't take railroad curves into account, e.g. it thinks a
|
||||||
// rover can't push a block through a curve
|
// rover can't push a block through a curve
|
||||||
if (tile.movement_cooldown > 0 ||
|
if (tile.movement_cooldown > 0 ||
|
||||||
@ -360,6 +361,13 @@ export class Cell extends Array {
|
|||||||
{
|
{
|
||||||
return false;
|
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') {
|
else if (push_mode === 'push') {
|
||||||
if (actor === level.player) {
|
if (actor === level.player) {
|
||||||
@ -485,6 +493,7 @@ export class Level extends LevelInterface {
|
|||||||
// PRNG is initialized to zero
|
// PRNG is initialized to zero
|
||||||
this._rng1 = 0;
|
this._rng1 = 0;
|
||||||
this._rng2 = 0;
|
this._rng2 = 0;
|
||||||
|
this._tw_rng = Math.floor(Math.random() * 0x80000000);
|
||||||
if (this.stored_level.blob_behavior === 0) {
|
if (this.stored_level.blob_behavior === 0) {
|
||||||
this._blob_modifier = 0x55;
|
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
|
// TODO complain if no player
|
||||||
// Used for doppelgängers
|
// Used for doppelgängers
|
||||||
this.player1_move = null;
|
this.player1_move = null;
|
||||||
@ -822,6 +837,16 @@ export class Level extends LevelInterface {
|
|||||||
return this._rng1 ^ this._rng2;
|
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
|
// Weird thing done by CC2 to make blobs... more... random
|
||||||
get_blob_modifier() {
|
get_blob_modifier() {
|
||||||
let mod = this._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
|
// 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.)
|
// they only take a few bytes each so that's fine.)
|
||||||
for (let key of [
|
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',
|
'tic_counter', 'time_remaining', 'timer_paused',
|
||||||
'chips_remaining', 'bonus_points', 'state',
|
'chips_remaining', 'bonus_points', 'state',
|
||||||
'player1_move', 'player2_move', 'remaining_players', 'player',
|
'player1_move', 'player2_move', 'remaining_players', 'player',
|
||||||
@ -966,15 +991,7 @@ export class Level extends LevelInterface {
|
|||||||
if (actor.type.ttl)
|
if (actor.type.ttl)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (actor.movement_cooldown <= 0) {
|
this._do_actor_idle(actor);
|
||||||
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._swap_players();
|
this._swap_players();
|
||||||
@ -1098,15 +1115,7 @@ export class Level extends LevelInterface {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
this._do_actor_cooldown(actor, cooldown);
|
this._do_actor_cooldown(actor, cooldown);
|
||||||
if (actor.movement_cooldown <= 0) {
|
this._do_actor_idle(actor);
|
||||||
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._swap_players();
|
this._swap_players();
|
||||||
@ -1159,7 +1168,7 @@ export class Level extends LevelInterface {
|
|||||||
terrain.type.on_arrive(terrain, this, actor);
|
terrain.type.on_arrive(terrain, this, actor);
|
||||||
}
|
}
|
||||||
// If we got a new direction, try moving again
|
// 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);
|
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() {
|
_swap_players() {
|
||||||
if (this.remaining_players <= 0) {
|
if (this.remaining_players <= 0) {
|
||||||
this.win();
|
this.win();
|
||||||
@ -1250,6 +1271,9 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
if (actor.type.is_real_player) {
|
if (actor.type.is_real_player) {
|
||||||
this.player = actor;
|
this.player = actor;
|
||||||
|
if (this.compat.player_moves_last && i !== 0) {
|
||||||
|
[this.actors[0], this.actors[i]] = [this.actors[i], this.actors[0]];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1257,24 +1281,37 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_do_cleanup_phase() {
|
_do_cleanup_phase() {
|
||||||
// Strip out any destroyed actors from the acting order
|
// Lynx compat: Any blue tank that still has the reversal flag set here, but is in motion,
|
||||||
// FIXME this is O(n), where n is /usually/ small, but i still don't love it. not strictly
|
// should ignore it. Unfortunately this has to be done as its own pass (as it is in Lynx!)
|
||||||
// necessary, either; maybe only do it every few tics?
|
// because of acting order issues
|
||||||
let p = 0;
|
if (this.compat.tanks_ignore_button_while_moving) {
|
||||||
for (let i = 0, l = this.actors.length; i < l; i++) {
|
for (let actor of this.actors) {
|
||||||
let actor = this.actors[i];
|
if (actor.cell && actor.pending_reverse && actor.movement_cooldown > 0) {
|
||||||
if (actor.cell) {
|
this._set_tile_prop(actor, 'pending_reverse', false);
|
||||||
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;
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
@ -1399,11 +1436,11 @@ export class Level extends LevelInterface {
|
|||||||
// At this point, we have exactly 1 or 2 directions, and deciding between them requires
|
// 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
|
// 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!
|
// 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;
|
let open;
|
||||||
if (dir2 === null) {
|
if (dir2 === null) {
|
||||||
// Only one direction is held, but for consistency, "check" it anyway
|
// 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;
|
actor.decision = dir1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -1413,8 +1450,8 @@ export class Level extends LevelInterface {
|
|||||||
// interpret our input!
|
// interpret our input!
|
||||||
if (dir1 === actor.direction || dir2 === actor.direction) {
|
if (dir1 === actor.direction || dir2 === actor.direction) {
|
||||||
let other_direction = dir1 === actor.direction ? dir2 : dir1;
|
let other_direction = dir1 === actor.direction ? dir2 : dir1;
|
||||||
let curr_open = try_direction(actor.direction, 'push');
|
let curr_open = try_direction(actor.direction, push_mode);
|
||||||
let other_open = try_direction(other_direction, 'push');
|
let other_open = try_direction(other_direction, push_mode);
|
||||||
if (! curr_open && other_open) {
|
if (! curr_open && other_open) {
|
||||||
actor.decision = other_direction;
|
actor.decision = other_direction;
|
||||||
open = true;
|
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 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
|
// FIXME lynx only checks horizontal, what about cc2? it must check both
|
||||||
// because of the behavior where pushing into a corner always pushes horizontal
|
// because of the behavior where pushing into a corner always pushes horizontal
|
||||||
let open1 = try_direction(dir1, 'push');
|
let open1 = try_direction(dir1, push_mode);
|
||||||
let open2 = try_direction(dir2, 'push');
|
let open2 = try_direction(dir2, push_mode);
|
||||||
if (open1 && ! open2) {
|
if (open1 && ! open2) {
|
||||||
actor.decision = dir1;
|
actor.decision = dir1;
|
||||||
open = true;
|
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
|
// If we're overriding a force floor but the direction we're moving in is blocked, this
|
||||||
// counts as a forced move
|
// counts as a forced move (but only under the CC2 behavior of instant bonking)
|
||||||
if (actor.slide_mode === 'force' && ! open) {
|
if (actor.slide_mode === 'force' && ! open && ! this.compat.bonking_isnt_instant) {
|
||||||
this._set_tile_prop(actor, 'last_move_was_force', true);
|
this._set_tile_prop(actor, 'last_move_was_force', true);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -1536,6 +1573,14 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_movement(actor, orig_cell, direction, push_mode) {
|
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);
|
let dest_cell = this.get_neighboring_cell(orig_cell, direction);
|
||||||
if (! dest_cell) {
|
if (! dest_cell) {
|
||||||
if (push_mode === 'push') {
|
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.
|
// Try to move the given actor one tile in the given direction and update their cooldown.
|
||||||
// Return true if successful.
|
// 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!
|
// In mid-movement, we can't even change direction!
|
||||||
if (actor.movement_cooldown > 0)
|
if (actor.movement_cooldown > 0)
|
||||||
return false;
|
return false;
|
||||||
@ -1594,15 +1641,17 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
direction = redirected_direction;
|
direction = redirected_direction;
|
||||||
}
|
}
|
||||||
this.set_actor_direction(actor, direction);
|
|
||||||
|
|
||||||
// Grab speed /first/, in case the movement or on_blocked turns us into an animation
|
// Grab speed /first/, in case the movement or on_blocked turns us into an animation
|
||||||
// immediately (and then we won't have a speed!)
|
// immediately (and then we won't have a speed!)
|
||||||
// FIXME that's a weird case actually since the explosion ends up still moving
|
// FIXME that's a weird case actually since the explosion ends up still moving
|
||||||
let speed = actor.type.movement_speed;
|
let speed = actor.type.movement_speed;
|
||||||
|
|
||||||
let move = DIRECTIONS[direction].movement;
|
let success = this.check_movement(actor, actor.cell, direction, 'push');
|
||||||
if (! 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;
|
return false;
|
||||||
|
|
||||||
// We're clear! Compute our speed and move us
|
// We're clear! Compute our speed and move us
|
||||||
@ -1621,9 +1670,10 @@ 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);
|
||||||
this._set_tile_prop(actor, 'movement_cooldown', speed * 3);
|
let duration = Math.max(3, speed * 3 - frameskip);
|
||||||
this._set_tile_prop(actor, 'movement_speed', speed * 3);
|
this._set_tile_prop(actor, 'movement_cooldown', duration);
|
||||||
this.move_to(actor, goal_cell, speed);
|
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
|
// 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
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
attempt_out_of_turn_step(actor, direction) {
|
attempt_out_of_turn_step(actor, direction, frameskip = 0) {
|
||||||
let success = this.attempt_step(actor, direction);
|
let success = this.attempt_step(actor, direction, frameskip);
|
||||||
if (success) {
|
if (success) {
|
||||||
this._do_extra_cooldown(actor);
|
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
|
// Move the given actor to the given position and perform any appropriate
|
||||||
// tile interactions. Does NOT check for whether the move is actually
|
// tile interactions. Does NOT check for whether the move is actually
|
||||||
// legal; use attempt_step for that!
|
// legal; use attempt_step for that!
|
||||||
move_to(actor, goal_cell, speed) {
|
move_to(actor, goal_cell) {
|
||||||
if (actor.cell === goal_cell)
|
if (actor.cell === goal_cell)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -2524,13 +2574,42 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
add_actor(actor) {
|
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.actors.push(actor);
|
||||||
this._push_pending_undo(() => this.actors.pop());
|
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) {
|
spawn_animation(cell, name) {
|
||||||
let type = TILE_TYPES[name];
|
let type = TILE_TYPES[name];
|
||||||
// Spawned VFX erase any existing VFX
|
// Spawned VFX silently erase any existing VFX
|
||||||
if (type.layer === LAYERS.vfx) {
|
if (type.layer === LAYERS.vfx) {
|
||||||
let vfx = cell[type.layer];
|
let vfx = cell[type.layer];
|
||||||
if (vfx) {
|
if (vfx) {
|
||||||
@ -2538,19 +2617,9 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tile = new Tile(type);
|
let tile = new Tile(type);
|
||||||
// Co-opt movement_cooldown/speed for these despite that they aren't moving, since those
|
this._init_animation(tile);
|
||||||
// properties are also used to animate everything else anyway. Decrement the cooldown
|
this.add_tile(tile, cell);
|
||||||
// immediately, as Lynx does; note that Lynx also ticks /and destroys/ animations early in
|
this.add_actor(tile);
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transmute_tile(tile, name) {
|
transmute_tile(tile, name) {
|
||||||
@ -2585,15 +2654,13 @@ export class Level extends LevelInterface {
|
|||||||
if (! old_type.is_actor) {
|
if (! old_type.is_actor) {
|
||||||
console.warn("Transmuting a non-actor into an animation!");
|
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 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) {
|
if (tile.last_extra_cooldown_tic) {
|
||||||
this._set_tile_prop(tile, 'last_extra_cooldown_tic', null);
|
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) {
|
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 c2g from './format-c2g.js';
|
||||||
import * as dat from './format-dat.js';
|
import * as dat from './format-dat.js';
|
||||||
import * as format_base from './format-base.js';
|
import * as format_base from './format-base.js';
|
||||||
|
import * as format_tws from './format-tws.js';
|
||||||
import { Level } from './game.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 { PrimaryView, Overlay, DialogOverlay, ConfirmOverlay, flash_button, load_json_from_storage, save_json_to_storage } from './main-base.js';
|
||||||
import { Editor } from './main-editor.js';
|
import { Editor } from './main-editor.js';
|
||||||
@ -1122,7 +1123,7 @@ class Player extends PrimaryView {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
let [x, y] = this.renderer.cell_coords_from_event(ev);
|
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
|
// 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
|
// think because we're still claiming a speed of 1 so time has to pass before the move
|
||||||
// actually "happens"
|
// actually "happens"
|
||||||
@ -1341,8 +1342,7 @@ class Player extends PrimaryView {
|
|||||||
this.debug.replay_duration_el.textContent = format_replay_duration(t);
|
this.debug.replay_duration_el.textContent = format_replay_duration(t);
|
||||||
|
|
||||||
if (! record) {
|
if (! record) {
|
||||||
this.level.force_floor_direction = replay.initial_force_floor_direction;
|
replay.configure_level(this.level);
|
||||||
this.level._blob_modifier = replay.blob_seed;
|
|
||||||
// FIXME should probably start playback on first real input
|
// FIXME should probably start playback on first real input
|
||||||
this.set_state('playing');
|
this.set_state('playing');
|
||||||
}
|
}
|
||||||
@ -2979,10 +2979,22 @@ const COMPAT_FLAGS = [
|
|||||||
key: 'use_lynx_loop',
|
key: 'use_lynx_loop',
|
||||||
label: "Game uses the Lynx-style update loop",
|
label: "Game uses the Lynx-style update loop",
|
||||||
rulesets: new Set(['steam', 'steam-strict', 'lynx', 'ms']),
|
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',
|
key: 'emulate_60fps',
|
||||||
label: "Game runs at 60 FPS",
|
label: "Game runs at 60 FPS",
|
||||||
rulesets: new Set(['steam', 'steam-strict']),
|
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
|
// Tiles
|
||||||
@ -2995,6 +3007,14 @@ const COMPAT_FLAGS = [
|
|||||||
key: 'rff_actually_random',
|
key: 'rff_actually_random',
|
||||||
label: "Random force floors are actually random",
|
label: "Random force floors are actually random",
|
||||||
rulesets: new Set(['ms']),
|
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
|
// Items
|
||||||
@ -3010,6 +3030,10 @@ const COMPAT_FLAGS = [
|
|||||||
key: 'monsters_ignore_keys',
|
key: 'monsters_ignore_keys',
|
||||||
label: "Monsters completely ignore keys",
|
label: "Monsters completely ignore keys",
|
||||||
rulesets: new Set(['ms']),
|
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
|
// Blocks
|
||||||
@ -3047,10 +3071,26 @@ const COMPAT_FLAGS = [
|
|||||||
key: 'tanks_always_obey_button',
|
key: 'tanks_always_obey_button',
|
||||||
label: "Blue tanks always obey blue buttons",
|
label: "Blue tanks always obey blue buttons",
|
||||||
rulesets: new Set(['steam-strict']),
|
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',
|
key: 'rff_blocks_monsters',
|
||||||
label: "Random force floors block monsters",
|
label: "Random force floors block monsters",
|
||||||
rulesets: new Set(['ms']),
|
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',
|
key: 'fire_allows_monsters',
|
||||||
label: "Fire doesn't block monsters",
|
label: "Fire doesn't block monsters",
|
||||||
@ -3355,9 +3395,8 @@ class PackTestDialog extends DialogOverlay {
|
|||||||
let replay = stored_level.replay;
|
let replay = stored_level.replay;
|
||||||
level = new Level(stored_level, compat);
|
level = new Level(stored_level, compat);
|
||||||
level.sfx = dummy_sfx;
|
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
|
level.undo_enabled = false; // slight performance boost
|
||||||
|
replay.configure_level(level);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
let input = replay.get(level.tic_counter);
|
let input = replay.get(level.tic_counter);
|
||||||
|
|||||||
@ -191,7 +191,14 @@ function pursue_player(me, level) {
|
|||||||
let player = level.player;
|
let player = level.player;
|
||||||
// CC2 behavior (not Lynx (TODO compat?)): pursue the player's apparent position, not just the
|
// CC2 behavior (not Lynx (TODO compat?)): pursue the player's apparent position, not just the
|
||||||
// cell they're in
|
// 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 dx = me.cell.x - px;
|
||||||
let dy = me.cell.y - py;
|
let dy = me.cell.y - py;
|
||||||
@ -1495,11 +1502,17 @@ const TILE_TYPES = {
|
|||||||
level._set_tile_prop(me, 'presses', 0);
|
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) {
|
add_press_ready(me, level, other) {
|
||||||
// Same as below, but without ejection
|
// Same as below, but without ejection
|
||||||
level._set_tile_prop(me, 'presses', (me.presses ?? 0) + 1);
|
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) {
|
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
|
// 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) {
|
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(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,
|
movement_speed: 8,
|
||||||
decide_movement(me, level) {
|
decide_movement(me, level) {
|
||||||
// move completely at random
|
// move completely at random
|
||||||
let modifier = level.get_blob_modifier();
|
let d;
|
||||||
return [DIRECTION_ORDER[(level.prng() + modifier) % 4]];
|
if (level.compat.blobs_use_tw_prng) {
|
||||||
|
d = level.tw_prng_random4();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let modifier = level.get_blob_modifier();
|
||||||
|
d = (level.prng() + modifier) % 4;
|
||||||
|
}
|
||||||
|
return [DIRECTION_ORDER[d]];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
teeth: {
|
teeth: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user