diff --git a/js/defs.js b/js/defs.js index 33e797d..6fb0e7b 100644 --- a/js/defs.js +++ b/js/defs.js @@ -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, }; diff --git a/js/game.js b/js/game.js index 7622c45..191d6ff 100644 --- a/js/game.js +++ b/js/game.js @@ -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); diff --git a/js/main.js b/js/main.js index ec090ba..57da8d3 100644 --- a/js/main.js +++ b/js/main.js @@ -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) { diff --git a/js/tiletypes.js b/js/tiletypes.js index f9f54f2..4791122 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -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;