Refactor sliding handling

Eliminates a number of annoying little hacks by getting rid of
`slide_mode` and instead trusting the terrain, live, like CC2 seems to
do (and Lynx definitely does).
This commit is contained in:
Eevee (Evelyn Woods) 2021-05-10 20:23:02 -06:00
parent b375f431af
commit 08c86c6129
4 changed files with 297 additions and 332 deletions

View File

@ -112,7 +112,7 @@ export const PICKUP_PRIORITIES = {
always: 3, // all actors; blue keys, yellow teleporters (everything picks up except cc2 blocks)
// TODO is this even necessary? in cc2 the general rule seems to be that anything stepping on
// an item picks it up, and collision is used to avoid that most of the time
normal: 2, // actors with inventories; most items
normal: 3, // actors with inventories; most items
player: 1, // players and doppelgangers; red keys (ignored by everything else)
real_player: 0,
};

View File

@ -11,11 +11,6 @@ export class Tile {
}
this.cell = null;
if (type.is_actor) {
this.slide_mode = null;
this.movement_cooldown = 0;
}
// Pre-seed actors who are expected to have inventories, with one
// TODO do i need this at all?
if (type.item_pickup_priority <= PICKUP_PRIORITIES.normal) {
@ -138,7 +133,7 @@ export class Tile {
// CC2 strikes again: blocks cannot push sliding blocks, except that frame blocks can push
// sliding dirt blocks!
if (this.type.is_block && tile.slide_mode && ! (
if (this.type.is_block && tile.is_sliding && ! (
this.type.name === 'frame_block' && tile.type.name === 'dirt_block'))
{
return false;
@ -173,10 +168,19 @@ export class Tile {
}
}
}
Tile.prototype.emitting_edges = 0;
Tile.prototype.powered_edges = 0;
Tile.prototype.wire_directions = 0;
Tile.prototype.wire_tunnel_directions = 0;
Object.assign(Tile.prototype, {
// Wire stuff, to avoid a lot of boring checks in circuit code
emitting_edges: 0,
powered_edges: 0,
wire_directions: 0,
wire_tunnel_directions: 0,
// Actor defaults
movement_cooldown: 0,
is_sliding: false,
is_pending_slide: false,
can_override_slide: false,
});
export class Cell extends Array {
constructor(x, y) {
@ -661,9 +665,17 @@ export class Level extends LevelInterface {
can_accept_input() {
// We can accept input anytime the player can move, i.e. when they're not already moving and
// not in an un-overrideable slide
return this.player.movement_cooldown === 0 &&
(this.player.slide_mode === null || (
this.player.slide_mode === 'force' && this.player.can_override_slide));
if (this.player.movement_cooldown > 0)
return false;
if (! this.player.pending_slide)
return true;
if (! this.player.can_override_slide)
return false;
let terrain = this.player.cell.get_terrain();
if (terrain.type.allow_player_override)
return true;
return false;
}
// Randomness -------------------------------------------------------------------------------------
@ -748,6 +760,8 @@ export class Level extends LevelInterface {
_advance_tic_lexy() {
// Under CC2 rules, there are two wire updates at the very beginning of the game before the
// player can actually move. That means the first tic has five wire phases total.
// FIXME this breaks item bestowal contraptions that immediately flip a force floor, since
// the critters on the force floors don't get a bonk before this happens
if (this.tic_counter === 0) {
this._do_wire_phase();
this._do_wire_phase();
@ -973,6 +987,11 @@ export class Level extends LevelInterface {
else {
this.make_actor_decision(actor, forced_only);
}
// This only persists until the next decision
if (actor.is_pending_slide) {
this._set_tile_prop(actor, 'is_pending_slide', false);
}
}
}
@ -1010,10 +1029,6 @@ export class Level extends LevelInterface {
if (actor.pending_push) {
this._set_tile_prop(actor, 'pending_push', null);
}
// Turntable slide wears off after a single /attempted/ move
if (actor.slide_mode === 'turntable') {
this.make_slide(actor, null);
}
// Actor is allowed to move, so do so
let success = this.attempt_step(actor, direction);
@ -1031,29 +1046,33 @@ export class Level extends LevelInterface {
(terrain.type.slide_mode === 'ice' && (
! actor.ignores(terrain.type.name) || actor.type.name === 'ghost')) ||
// But they only bonk on a force floor if it affects them
(terrain.type.slide_mode === 'force' &&
actor.slide_mode && ! actor.ignores(terrain.type.name))))
(terrain.type.slide_mode === 'force' && ! actor.ignores(terrain.type.name))))
{
// Turn the actor around so ice corners bonk correctly
// XXX this is jank as hell
if (terrain.type.slide_mode === 'ice') {
this.set_actor_direction(actor, DIRECTIONS[direction].opposite);
}
// Pretend they stepped on the tile again
// Note that ghosts bonk even on ice corners, which they can otherwise pass through,
// argh!
if (terrain.type.on_arrive && actor.type.name !== 'ghost') {
terrain.type.on_arrive(terrain, this, actor);
// Pretend they stepped on the cell again -- this is what allows item bestowal to
// function, as a bonking monster will notice the item now and take it.
this.step_on_cell(actor, actor.cell);
// Note that ghosts bonk even on ice corners, which they can otherwise pass through!
let forced_move = this.get_forced_move(actor, terrain);
if (actor.type.name === 'ghost') {
forced_move = actor.direction;
}
// If we got a new direction, try moving again
if (direction !== actor.direction && ! this.compat.bonking_isnt_instant) {
success = this.attempt_step(actor, actor.direction);
// FIXME in compat case, i guess we just set direction?
if (forced_move && direction !== forced_move && ! this.compat.bonking_isnt_instant) {
success = this.attempt_step(actor, forced_move);
}
}
else if (actor.slide_mode === 'teleport') {
// Failed teleport slides only last for a single attempt. (Successful teleports
// continue the slide until landing on a new tile, as normal; otherwise you couldn't
// push a block coming out of a teleporter.)
this.make_slide(actor, null);
else if (terrain.type.name === 'teleport_red' && ! terrain.is_active) {
// Curious special-case red teleporter behavior: if you pass through a wired but
// inactive one, you keep sliding indefinitely. Players can override out of it, but
// other actors are just stuck. So, set this again.
this._set_tile_prop(actor, 'is_pending_slide', true);
}
}
@ -1087,6 +1106,42 @@ export class Level extends LevelInterface {
return;
}
// Play step sound when the player completes a move
if (actor === this.player) {
let terrain = actor.cell.get_terrain();
if (actor.is_sliding && terrain.type.slide_mode === 'ice') {
this.sfx.play_once('slide-ice');
}
else if (actor.is_sliding && terrain.type.slide_mode === 'force') {
this.sfx.play_once('slide-force');
}
else if (terrain.type.name === 'popdown_floor') {
this.sfx.play_once('step-popdown');
}
else if (terrain.type.name === 'gravel' || terrain.type.name === 'railroad') {
this.sfx.play_once('step-gravel');
}
else if (terrain.type.name === 'water') {
if (actor.ignores(terrain.type.name)) {
this.sfx.play_once('step-water');
}
}
else if (terrain.type.name === 'fire') {
if (actor.has_item('fire_boots')) {
this.sfx.play_once('step-fire');
}
}
else if (terrain.type.slide_mode === 'force') {
this.sfx.play_once('step-force');
}
else if (terrain.type.slide_mode === 'ice') {
this.sfx.play_once('step-ice');
}
else {
this.sfx.play_once('step-floor');
}
}
if (! this.compat.tiles_react_instantly) {
this.step_on_cell(actor, actor.cell);
}
@ -1269,6 +1324,22 @@ export class Level extends LevelInterface {
return [dir1, dir2];
}
get_forced_move(actor, terrain = null) {
if (! terrain) {
terrain = actor.cell.get_terrain();
}
if (! terrain.type.slide_mode)
return null;
if (! terrain.type.get_slide_direction)
return null;
if (! (actor.is_pending_slide || terrain.type.slide_automatically))
return null;
if (actor.ignores(terrain.type.name))
return null;
return terrain.type.get_slide_direction(terrain, this, actor);
}
make_player_decision(actor, input, forced_only = false) {
// Only reset the player's is_pushing between movement, so it lasts for the whole push
this._set_tile_prop(actor, 'is_pushing', false);
@ -1301,8 +1372,9 @@ export class Level extends LevelInterface {
// succeed, even if overriding in the same direction we're already moving, that does count
// as an override.
let terrain = actor.cell.get_terrain();
let forced_move = this.get_forced_move(actor, terrain);
let may_move = ! forced_only && (
! actor.slide_mode || (actor.can_override_slide && terrain.type.allow_player_override));
! forced_move || (actor.can_override_slide && terrain.type.allow_player_override));
let [dir1, dir2] = this._extract_player_directions(input);
// Check for special player actions, which can only happen at decision time. Dropping can
@ -1330,11 +1402,11 @@ export class Level extends LevelInterface {
}
}
if (actor.slide_mode && ! (may_move && dir1)) {
if (forced_move && ! (may_move && dir1)) {
// This is a forced move and we're not overriding it, so we're done
actor.decision = actor.direction;
actor.decision = forced_move;
if (actor.slide_mode === 'force') {
if (terrain.type.slide_mode === 'force') {
this._set_tile_prop(actor, 'can_override_slide', true);
}
}
@ -1357,16 +1429,17 @@ export class Level extends LevelInterface {
// one, UNLESS it's blocked AND the other isn't.
// Note that if this is an override, then the forced direction is still used to
// 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_mode);
let current_direction = forced_move ?? actor.direction;
if (dir1 === current_direction || dir2 === current_direction) {
let other_direction = dir1 === current_direction ? dir2 : dir1;
let curr_open = try_direction(current_direction, push_mode);
let other_open = try_direction(other_direction, push_mode);
if (! curr_open && other_open) {
actor.decision = other_direction;
open = true;
}
else {
actor.decision = actor.direction;
actor.decision = current_direction;
open = curr_open;
}
}
@ -1396,14 +1469,16 @@ 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 (but only under the CC2 behavior of instant bonking)
if (actor.slide_mode === 'force' && ! open && ! this.compat.bonking_isnt_instant) {
// If we're overriding a force floor but the direction we're moving in is blocked, we
// keep our override power (but only under the CC2 behavior of instant bonking).
// Notably, this happens even if we do end up able to move!
if (forced_move && terrain.type.slide_mode === 'force' && ! open &&
! this.compat.bonking_isnt_instant)
{
this._set_tile_prop(actor, 'can_override_slide', true);
}
else {
// Otherwise this is 100% a conscious move so we lose our override power next tic
// TODO how does this interact with teleports
// Otherwise this is 100% a conscious move, so we lose override
this._set_tile_prop(actor, 'can_override_slide', false);
}
}
@ -1429,12 +1504,15 @@ export class Level extends LevelInterface {
let direction_preference;
let terrain = actor.cell.get_terrain();
if (actor.slide_mode ||
let forced_move = this.get_forced_move(actor, terrain);
if (forced_move) {
// Actors can't make voluntary moves while sliding; they just, ah, slide.
actor.decision = forced_move;
return;
}
else if (actor.type.name === 'ghost' && terrain.type.slide_mode === 'ice') {
// TODO weird cc2 quirk/bug: ghosts bonk on ice even though they don't slide on it
// FIXME and if they have cleats, they get stuck instead (?!)
(actor.type.name === 'ghost' && terrain.type.slide_mode === 'ice'))
{
// Actors can't make voluntary moves while sliding; they just, ah, slide.
actor.decision = actor.direction;
return;
}
@ -1640,8 +1718,8 @@ export class Level extends LevelInterface {
// FIXME this is clumsy and creates behavior dependent on actor order. my
// original implementation only did this if the push /failed/; is that worth
// a compat option? also, how does any of this work under lynx rules?
if (tile.slide_mode === 'force' ||
(tile.slide_mode !== null && tile.movement_cooldown > 0))
if (tile.is_sliding && ! tile.is_pulled && (tile.movement_cooldown > 0 ||
tile.cell.get_terrain().type.slide_mode === 'force'))
{
this._set_tile_prop(tile, 'pending_push', direction);
// FIXME if the block has already made a decision then this is necessary
@ -1710,12 +1788,15 @@ 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;
// Lynx: Nothing can move 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) {
let terrain = orig_cell.get_terrain()
if (terrain.type.slide_mode === 'force' && ! actor.ignores(terrain.type.name) &&
direction === DIRECTIONS[actor.direction].opposite)
{
return false;
}
}
let dest_cell = this.get_neighboring_cell(orig_cell, direction);
@ -1818,6 +1899,11 @@ export class Level extends LevelInterface {
this._set_tile_prop(actor, 'movement_speed', duration);
this.move_to(actor, goal_cell);
// Whether we're sliding is determined entirely by whether we most recently moved onto a
// sliding tile that we don't ignore. This could /almost/ be computed on the fly, except
// that an actor that starts on e.g. ice or a teleporter is not considered sliding.
this._set_tile_prop(actor, 'is_sliding', terrain.type.slide_mode && ! actor.ignores(terrain.type.name));
// 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
// a thing, and also make them a real move rather than a weird pending thing
@ -1840,7 +1926,10 @@ export class Level extends LevelInterface {
}
attempt_out_of_turn_step(actor, direction) {
if (actor.slide_mode === 'turntable') {
if (actor.is_sliding && actor.cell.get_terrain().type.slide_mode === 'turntable') {
// FIXME where should this be? should a block on a turntable ignore pushes? but then
// if it gets blocked it's stuck, right?
// FIXME ok that is already the case, oops
// 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
@ -1893,9 +1982,7 @@ export class Level extends LevelInterface {
}
}
// Announce we're approaching. Slide mode is set here, since it's about the tile we're
// moving towards and needs to last through our next decision
this.make_slide(actor, null);
// Announce we're approaching
for (let tile of goal_cell) {
if (! tile)
continue;
@ -1909,9 +1996,6 @@ export class Level extends LevelInterface {
if (tile.type.on_approach) {
tile.type.on_approach(tile, this, actor);
}
if (tile.type.slide_mode) {
this.make_slide(actor, tile.type.slide_mode);
}
}
// Now add the actor back; we have to wait this long because e.g. monsters erase splashes
@ -1973,45 +2057,14 @@ export class Level extends LevelInterface {
continue;
}
}
else if (tile.type.on_arrive) {
tile.type.on_arrive(tile, this, actor);
}
}
// Play step sound
if (actor === this.player) {
let terrain = cell.get_terrain();
if (actor.slide_mode === 'ice') {
this.sfx.play_once('slide-ice');
}
else if (actor.slide_mode === 'force') {
this.sfx.play_once('slide-force');
}
else if (terrain.type.name === 'popdown_floor') {
this.sfx.play_once('step-popdown');
}
else if (terrain.type.name === 'gravel' || terrain.type.name === 'railroad') {
this.sfx.play_once('step-gravel');
}
else if (terrain.type.name === 'water') {
if (actor.ignores(terrain.type.name)) {
this.sfx.play_once('step-water');
}
}
else if (terrain.type.name === 'fire') {
if (actor.has_item('fire_boots')) {
this.sfx.play_once('step-fire');
}
}
else if (terrain.type.slide_mode === 'force') {
this.sfx.play_once('step-force');
}
else if (terrain.type.slide_mode === 'ice') {
this.sfx.play_once('step-ice');
}
else {
this.sfx.play_once('step-floor');
if (tile.type.slide_automatically) {
// This keeps a player on force floor consistently using their sliding pose, even if
// drawn between moves. It also simplifies checks elsewhere, so that's nice
this._set_tile_prop(actor, 'is_pending_slide', true);
}
}
}
@ -2024,22 +2077,7 @@ export class Level extends LevelInterface {
// movement towards the teleporter it just stepped on, not the teleporter it's moved to
this._set_tile_prop(actor, 'destination_cell', actor.cell);
if (teleporter.type.name === 'teleport_red' && ! teleporter.is_active) {
// Curious special-case red teleporter behavior: if you pass through a wired but
// inactive one, you keep sliding indefinitely. Players can override out of it, but
// other actors cannot. (Normally, a teleport slide ends after one decision phase.)
// XXX this is useful when the exit is briefly blocked, but it can also get monsters
// stuck forever :(
// XXX kind of repeating myself here, there must be a more natural approach
this.make_slide(actor, 'teleport-forever');
if (actor.type.is_real_player && teleporter.type.allow_player_override) {
this._set_tile_prop(actor, 'can_override_slide', true);
}
// Also, there's no sound and whatnot, so everything else is skipped outright.
return;
}
let dest, direction;
let dest, direction, success;
for ([dest, direction] of teleporter.type.teleport_dest_order(teleporter, this, actor)) {
// Teleporters already containing an actor are blocked and unusable
if (dest !== teleporter && dest.cell.get_actor())
@ -2053,7 +2091,6 @@ export class Level extends LevelInterface {
this.allow_taking_yellow_teleporters)
{
// Super duper special yellow teleporter behavior: you pick it the fuck up
this.make_slide(actor, null);
this.attempt_take(actor, teleporter);
if (actor === this.player) {
this.sfx.play_once('get-tool', teleporter.cell);
@ -2070,17 +2107,14 @@ export class Level extends LevelInterface {
// it can even come up under cc2 rules, since teleporting is done after an actor cools
// down and before the next actor even gets a chance to act
if (this.check_movement(actor, dest.cell, direction, 'bump')) {
// Sound plays from the origin cell simply because that's where the sfx player
// thinks the player is currently; position isn't updated til next turn
this.sfx.play_once('teleport', teleporter.cell);
success = true;
break;
}
}
// Explicitly set us as teleport sliding, since in some very obscure cases (auto-dropping a
// yellow teleporter because you picked up an item with a full inventory and immediately
// teleporting through it) it may not have been applied
this.make_slide(actor, 'teleport');
// Teleport slides happen when coming out of a teleporter, but not other times, so need to
// be noted explicitly
this._set_tile_prop(actor, 'is_pending_slide', true);
// Real players might be able to immediately override the resulting slide
if (actor.type.is_real_player && teleporter.type.allow_player_override) {
this._set_tile_prop(actor, 'can_override_slide', true);
@ -2088,16 +2122,22 @@ export class Level extends LevelInterface {
this.set_actor_direction(actor, direction);
this.spawn_animation(actor.cell, 'teleport_flash');
if (dest.cell !== actor.cell) {
this.spawn_animation(dest.cell, 'teleport_flash');
}
if (success) {
// Sound plays from the origin cell simply because that's where the sfx player thinks
// the player is currently; position isn't updated til next turn
this.sfx.play_once('teleport', teleporter.cell);
// Now physically move the actor, but their movement waits until next decision phase
this.remove_tile(actor, true);
this.add_tile(actor, dest.cell);
// Erase this to prevent tail-biting through a teleport
this._set_tile_prop(actor, 'previous_cell', null);
this.spawn_animation(actor.cell, 'teleport_flash');
if (dest.cell !== actor.cell) {
this.spawn_animation(dest.cell, 'teleport_flash');
}
// Now physically move the actor, but their movement waits until next decision phase
this.remove_tile(actor, true);
this.add_tile(actor, dest.cell);
// Erase this to prevent tail-biting through a teleport
this._set_tile_prop(actor, 'previous_cell', null);
}
}
remember_player_move(direction) {
@ -2598,7 +2638,8 @@ export class Level extends LevelInterface {
this._set_tile_prop(actor, 'movement_cooldown', null);
this._set_tile_prop(actor, 'movement_speed', null);
this.make_slide(actor, null);
this._set_tile_prop(actor, 'is_sliding', false);
this._set_tile_prop(actor, 'is_pending_slide', false);
this.move_to(actor, ankh_cell);
this.transmute_tile(this.ankh_tile, 'floor');
@ -2816,7 +2857,8 @@ export class Level extends LevelInterface {
}
this._init_animation(tile);
this._set_tile_prop(tile, 'previous_cell', null);
this.make_slide(tile, null);
this._set_tile_prop(tile, 'is_sliding', false);
this._set_tile_prop(tile, 'is_pending_slide', false);
}
}
@ -2929,14 +2971,6 @@ export class Level extends LevelInterface {
actor.toolbelt.push(name);
this._push_pending_undo(() => actor.toolbelt.pop());
}
// FIXME hardcodey, but, this doesn't seem to fit anywhere else
if (name === 'cleats' && actor.slide_mode === 'ice') {
this.make_slide(actor, null);
}
else if (name === 'suction_boots' && actor.slide_mode === 'force') {
this.make_slide(actor, null);
}
}
return true;
}
@ -2988,11 +3022,6 @@ export class Level extends LevelInterface {
}
}
// Mark an actor as sliding
make_slide(actor, mode) {
this._set_tile_prop(actor, 'slide_mode', mode);
}
// Change an actor's direction
set_actor_direction(actor, direction) {
this._set_tile_prop(actor, 'direction', direction);

View File

@ -1074,7 +1074,10 @@ class Player extends PrimaryView {
let header = mk('h3');
let dl = mk('dl');
let props = {};
for (let key of ['direction', 'movement_speed', 'movement_cooldown', 'slide_mode']) {
for (let key of [
'direction', 'movement_speed', 'movement_cooldown',
'is_sliding', 'is_pending_slide', 'can_override_slide',
]) {
let dd = mk('dd');
props[key] = dd;
dl.append(mk('dt', key), dd);
@ -1494,7 +1497,7 @@ class Player extends PrimaryView {
// force floors, even if you could override them!
let moved = false;
while (this.level.has_undo() &&
! (moved && this.level.player.slide_mode === null))
! (moved && ! this.level.player.is_pending_slide))
{
this.undo();
if (player_cell !== this.level.player.cell) {

View File

@ -1,5 +1,4 @@
import { COLLISION, DIRECTIONS, DIRECTION_ORDER, LAYERS, TICS_PER_SECOND, PICKUP_PRIORITIES } from './defs.js';
import { random_choice } from './util.js';
// TODO factor out some repeated stuff: common monster bits, common item bits, repeated collision
// masks
@ -7,43 +6,6 @@ function activate_me(me, level) {
me.type.activate(me, level);
}
function on_begin_force_floor(me, level) {
// At the start of the level, if there's an actor on a force floor:
// - use on_arrive to set the actor's direction
// - set the slide_mode (normally done by the main game loop)
// - item bestowal: if they're being pushed into a wall and standing on an item, pick up the
// item, even if they couldn't normally pick items up
// FIXME get rid of this
let actor = me.cell.get_actor();
if (! actor)
return;
me.type.on_arrive(me, level, actor);
if (me.type.slide_mode) {
level._set_tile_prop(actor, 'slide_mode', me.type.slide_mode);
}
// Item bestowal
// TODO seemingly lynx/cc2 only pick RFF direction at decision time, but that's in conflict with
// doing this here; decision time hasn't happened yet, but we need to know what direction we're
// moving to know whether bestowal happens? so what IS the cause of item bestowal?
let neighbor = level.get_neighboring_cell(me.cell, actor.direction);
if (neighbor && level.can_actor_enter_cell(actor, neighbor, actor.direction))
return;
let item = me.cell.get_item();
if (! item)
return;
if (item.type.item_priority < actor.type.item_pickup_priority)
return;
if (! level.attempt_take(actor, item))
return;
if (actor.ignores(me.type.name)) {
// If they just picked up suction boots, they're no longer sliding
// TODO this feels hacky, shouldn't the slide mode be erased some other way?
level._set_tile_prop(actor, 'slide_mode', null);
}
}
function blocks_leaving_thin_walls(me, actor, direction) {
return me.type.thin_walls.has(direction) && actor.type.name !== 'ghost';
}
@ -90,6 +52,23 @@ function _define_gate(key) {
},
};
}
function _define_force_floor(direction, opposite_type) {
return {
layer: LAYERS.terrain,
slide_mode: 'force',
speed_factor: 2,
slide_automatically: true,
allow_player_override: true,
get_slide_direction(me, level, other) {
return direction;
},
activate(me, level) {
level.transmute_tile(me, opposite_type);
},
on_gray_button: activate_me,
on_power: activate_me,
};
}
function update_wireable(me, level) {
if (me.is_wired === undefined) {
@ -125,53 +104,54 @@ function player_visual_state(me) {
if (me.fail_reason === 'drowned') {
return 'drowned';
}
else if (me.fail_reason === 'burned') {
if (me.fail_reason === 'burned') {
return 'burned';
}
else if (me.fail_reason === 'exploded') {
if (me.fail_reason === 'exploded') {
return 'exploded';
}
else if (me.fail_reason === 'slimed') {
if (me.fail_reason === 'slimed') {
return 'slimed';
}
else if (me.fail_reason === 'electrocuted') {
if (me.fail_reason === 'electrocuted') {
return 'burned'; //same gfx for now
}
else if (me.fail_reason === 'fell') {
if (me.fail_reason === 'fell') {
return 'fell';
}
else if (me.fail_reason) {
if (me.fail_reason) {
return 'failed';
}
else if (me.exited) {
if (me.exited) {
return 'exited';
}
// This is slightly complicated. We should show a swimming pose while still in water, or moving
// away from water (as CC2 does), but NOT when stepping off a lilypad (which will already have
// been turned into water), and NOT without flippers (which can happen if we start on water)
else if (me.cell && (me.previous_cell || me.cell).has('water') &&
if (me.cell && (me.previous_cell || me.cell).has('water') &&
! me.not_swimming && me.has_item('flippers'))
{
return 'swimming';
}
else if (me.slide_mode === 'ice') {
return 'skating';
if (me.is_sliding || me.is_pending_slide) {
let terrain = me.cell.get_terrain();
if (terrain.type.slide_mode === 'ice') {
return 'skating';
}
else if (terrain.type.slide_mode === 'force') {
return 'forced';
}
}
else if (me.slide_mode === 'force') {
return 'forced';
}
else if (me.is_blocked) {
if (me.is_blocked) {
return 'blocked';
}
else if (me.is_pushing) {
if (me.is_pushing) {
return 'pushing';
}
else if (me.movement_speed) {
if (me.movement_speed) {
return 'moving';
}
else {
return 'normal';
}
return 'normal';
}
function button_visual_state(me) {
@ -750,12 +730,16 @@ const TILE_TYPES = {
layer: LAYERS.terrain,
contains_wire: true,
wire_propagation_mode: 'all',
slide_mode: 'turntable',
get_slide_direction(me, level, other) {
return other.direction;
},
on_arrive(me, level, other) {
level._set_tile_prop(other, 'is_pending_slide', true);
level.set_actor_direction(other, DIRECTIONS[other.direction].right);
if (other.type.on_rotate) {
other.type.on_rotate(other, level, 'right');
}
level.make_slide(other, 'turntable');
},
activate(me, level) {
level.transmute_tile(me, 'turntable_ccw');
@ -767,12 +751,16 @@ const TILE_TYPES = {
layer: LAYERS.terrain,
contains_wire: true,
wire_propagation_mode: 'all',
slide_mode: 'turntable',
get_slide_direction(me, level, other) {
return other.direction;
},
on_arrive(me, level, other) {
level._set_tile_prop(other, 'is_pending_slide', true);
level.set_actor_direction(other, DIRECTIONS[other.direction].left);
if (other.type.on_rotate) {
other.type.on_rotate(other, level, 'left');
}
level.make_slide(other, 'turntable');
},
activate(me, level) {
level.transmute_tile(me, 'turntable_cw');
@ -883,7 +871,13 @@ const TILE_TYPES = {
cracked_ice: {
layer: LAYERS.terrain,
slide_mode: 'ice',
get_slide_direction(me, level, other) {
return other.direction;
},
speed_factor: 2,
on_arrive(me, level, other) {
level._set_tile_prop(other, 'is_pending_slide', true);
},
on_depart(me, level, other) {
level.transmute_tile(me, 'water');
level.spawn_animation(me.cell, 'splash');
@ -893,172 +887,103 @@ const TILE_TYPES = {
ice: {
layer: LAYERS.terrain,
slide_mode: 'ice',
get_slide_direction(me, level, other) {
return other.direction;
},
speed_factor: 2,
on_arrive(me, level, other) {
level._set_tile_prop(other, 'is_pending_slide', true);
},
},
ice_sw: {
layer: LAYERS.terrain,
thin_walls: new Set(['south', 'west']),
slide_mode: 'ice',
get_slide_direction(me, level, other) {
return {
north: 'north',
south: 'east',
east: 'east',
west: 'north',
}[other.direction];
},
speed_factor: 2,
blocks_leaving: blocks_leaving_thin_walls,
on_arrive(me, level, other) {
if (other.direction === 'south') {
level.set_actor_direction(other, 'east');
}
else if (other.direction === 'west') {
level.set_actor_direction(other, 'north');
}
level._set_tile_prop(other, 'is_pending_slide', true);
},
},
ice_nw: {
layer: LAYERS.terrain,
thin_walls: new Set(['north', 'west']),
slide_mode: 'ice',
get_slide_direction(me, level, other) {
return {
north: 'east',
south: 'south',
east: 'east',
west: 'south',
}[other.direction];
},
speed_factor: 2,
blocks_leaving: blocks_leaving_thin_walls,
on_arrive(me, level, other) {
if (other.direction === 'north') {
level.set_actor_direction(other, 'east');
}
else if (other.direction === 'west') {
level.set_actor_direction(other, 'south');
}
level._set_tile_prop(other, 'is_pending_slide', true);
},
},
ice_ne: {
layer: LAYERS.terrain,
thin_walls: new Set(['north', 'east']),
slide_mode: 'ice',
get_slide_direction(me, level, other) {
return {
north: 'west',
south: 'south',
east: 'south',
west: 'west',
}[other.direction];
},
speed_factor: 2,
blocks_leaving: blocks_leaving_thin_walls,
on_arrive(me, level, other) {
if (other.direction === 'north') {
level.set_actor_direction(other, 'west');
}
else if (other.direction === 'east') {
level.set_actor_direction(other, 'south');
}
level._set_tile_prop(other, 'is_pending_slide', true);
},
},
ice_se: {
layer: LAYERS.terrain,
thin_walls: new Set(['south', 'east']),
slide_mode: 'ice',
get_slide_direction(me, level, other) {
return {
north: 'north',
south: 'west',
east: 'north',
west: 'west',
}[other.direction];
},
speed_factor: 2,
blocks_leaving: blocks_leaving_thin_walls,
on_arrive(me, level, other) {
if (other.direction === 'south') {
level.set_actor_direction(other, 'west');
}
else if (other.direction === 'east') {
level.set_actor_direction(other, 'north');
}
level._set_tile_prop(other, 'is_pending_slide', true);
},
},
force_floor_n: {
layer: LAYERS.terrain,
slide_mode: 'force',
speed_factor: 2,
allow_player_override: true,
on_begin: on_begin_force_floor,
on_arrive(me, level, other) {
level.set_actor_direction(other, 'north');
},
activate(me, level) {
level.transmute_tile(me, 'force_floor_s');
let actor = me.cell.get_actor();
if (actor && actor.movement_cooldown <= 0) {
level.set_actor_direction(actor, 'south');
// If we're using the Lynx loop, then decisions have already happened, and the new
// direction will be overwritten if this actor has yet to move
if (actor.decision && ! actor.ignores(me.type.name)) {
actor.decision = actor.direction;
}
}
},
on_gray_button: activate_me,
on_power: activate_me,
},
force_floor_e: {
layer: LAYERS.terrain,
slide_mode: 'force',
speed_factor: 2,
allow_player_override: true,
on_begin: on_begin_force_floor,
on_arrive(me, level, other) {
level.set_actor_direction(other, 'east');
},
activate(me, level) {
level.transmute_tile(me, 'force_floor_w');
let actor = me.cell.get_actor();
if (actor && actor.movement_cooldown <= 0) {
level.set_actor_direction(actor, 'west');
if (actor.decision && ! actor.ignores(me.type.name)) {
actor.decision = actor.direction;
}
}
},
on_gray_button: activate_me,
on_power: activate_me,
},
force_floor_s: {
layer: LAYERS.terrain,
slide_mode: 'force',
speed_factor: 2,
allow_player_override: true,
on_begin: on_begin_force_floor,
on_arrive(me, level, other) {
level.set_actor_direction(other, 'south');
},
activate(me, level) {
level.transmute_tile(me, 'force_floor_n');
let actor = me.cell.get_actor();
if (actor && actor.movement_cooldown <= 0) {
level.set_actor_direction(actor, 'north');
if (actor.decision && ! actor.ignores(me.type.name)) {
actor.decision = actor.direction;
}
}
},
on_gray_button: activate_me,
on_power: activate_me,
},
force_floor_w: {
layer: LAYERS.terrain,
slide_mode: 'force',
speed_factor: 2,
allow_player_override: true,
on_begin: on_begin_force_floor,
on_arrive(me, level, other) {
level.set_actor_direction(other, 'west');
},
activate(me, level) {
level.transmute_tile(me, 'force_floor_e');
let actor = me.cell.get_actor();
if (actor && actor.movement_cooldown <= 0) {
level.set_actor_direction(actor, 'east');
if (actor.decision && ! actor.ignores(me.type.name)) {
actor.decision = actor.direction;
}
}
},
on_gray_button: activate_me,
on_power: activate_me,
},
force_floor_n: _define_force_floor('north', 'force_floor_s'),
force_floor_s: _define_force_floor('south', 'force_floor_n'),
force_floor_e: _define_force_floor('east', 'force_floor_w'),
force_floor_w: _define_force_floor('west', 'force_floor_e'),
force_floor_all: {
layer: LAYERS.terrain,
slide_mode: 'force',
slide_automatically: true,
speed_factor: 2,
allow_player_override: true,
on_begin: on_begin_force_floor,
// TODO ms: this is random, and an acting wall to monsters (!)
get_slide_direction(me, level, _other) {
return level.get_force_floor_direction();
},
blocks(me, level, other) {
return (level.compat.rff_blocks_monsters &&
(other.type.collision_mask & COLLISION.monster_typical));
},
on_arrive(me, level, other) {
level.set_actor_direction(other, level.get_force_floor_direction());
},
},
slime: {
layer: LAYERS.terrain,
@ -1758,6 +1683,9 @@ const TILE_TYPES = {
teleport_blue: {
layer: LAYERS.terrain,
slide_mode: 'teleport',
get_slide_direction(me, level, other) {
return other.direction;
},
contains_wire: true,
wire_propagation_mode: 'all',
*teleport_dest_order(me, level, other) {
@ -1845,6 +1773,9 @@ const TILE_TYPES = {
teleport_red: {
layer: LAYERS.terrain,
slide_mode: 'teleport',
get_slide_direction(me, level, other) {
return other.direction;
},
contains_wire: true,
wire_propagation_mode: 'none',
allow_player_override: true,
@ -1899,6 +1830,9 @@ const TILE_TYPES = {
teleport_green: {
layer: LAYERS.terrain,
slide_mode: 'teleport',
get_slide_direction(me, level, other) {
return other.direction;
},
*teleport_dest_order(me, level, other) {
// The CC2 green teleporter scheme is:
// 1. Use the PRNG to pick another green teleporter
@ -1970,6 +1904,9 @@ const TILE_TYPES = {
layer: LAYERS.terrain,
item_priority: PICKUP_PRIORITIES.always,
slide_mode: 'teleport',
get_slide_direction(me, level, other) {
return other.direction;
},
allow_player_override: true,
*teleport_dest_order(me, level, other) {
let exit_direction = other.direction;
@ -2810,10 +2747,6 @@ const TILE_TYPES = {
if (terrain && terrain.type.on_arrive && ! me.ignores(terrain.type.name)) {
terrain.type.on_arrive(terrain, level, me);
}
// FIXME Ugh should this just be step_on or what? but it doesn't slide on ice
if (terrain && terrain.type.slide_mode === 'force') {
level.make_slide(me, terrain.type.slide_mode);
}
}
},
},
@ -2949,7 +2882,7 @@ const TILE_TYPES = {
if (obstacle && obstacle.type.is_actor) {
level.kill_actor(obstacle, me, 'explosion');
}
else if (me.slide_mode || me._clone_release) {
else if (me.is_sliding || me._clone_release) {
// Sliding bowling balls don't blow up if they hit a regular wall, and neither do
// bowling balls in the process of being released from a cloner
return;