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:
parent
b375f431af
commit
08c86c6129
@ -112,7 +112,7 @@ export const PICKUP_PRIORITIES = {
|
|||||||
always: 3, // all actors; blue keys, yellow teleporters (everything picks up except cc2 blocks)
|
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
|
// 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
|
// 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)
|
player: 1, // players and doppelgangers; red keys (ignored by everything else)
|
||||||
real_player: 0,
|
real_player: 0,
|
||||||
};
|
};
|
||||||
|
|||||||
321
js/game.js
321
js/game.js
@ -11,11 +11,6 @@ export class Tile {
|
|||||||
}
|
}
|
||||||
this.cell = null;
|
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
|
// Pre-seed actors who are expected to have inventories, with one
|
||||||
// TODO do i need this at all?
|
// TODO do i need this at all?
|
||||||
if (type.item_pickup_priority <= PICKUP_PRIORITIES.normal) {
|
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
|
// CC2 strikes again: blocks cannot push sliding blocks, except that frame blocks can push
|
||||||
// sliding dirt blocks!
|
// 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'))
|
this.type.name === 'frame_block' && tile.type.name === 'dirt_block'))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@ -173,10 +168,19 @@ export class Tile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tile.prototype.emitting_edges = 0;
|
Object.assign(Tile.prototype, {
|
||||||
Tile.prototype.powered_edges = 0;
|
// Wire stuff, to avoid a lot of boring checks in circuit code
|
||||||
Tile.prototype.wire_directions = 0;
|
emitting_edges: 0,
|
||||||
Tile.prototype.wire_tunnel_directions = 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 {
|
export class Cell extends Array {
|
||||||
constructor(x, y) {
|
constructor(x, y) {
|
||||||
@ -661,9 +665,17 @@ export class Level extends LevelInterface {
|
|||||||
can_accept_input() {
|
can_accept_input() {
|
||||||
// We can accept input anytime the player can move, i.e. when they're not already moving and
|
// We can accept input anytime the player can move, i.e. when they're not already moving and
|
||||||
// not in an un-overrideable slide
|
// not in an un-overrideable slide
|
||||||
return this.player.movement_cooldown === 0 &&
|
if (this.player.movement_cooldown > 0)
|
||||||
(this.player.slide_mode === null || (
|
return false;
|
||||||
this.player.slide_mode === 'force' && this.player.can_override_slide));
|
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 -------------------------------------------------------------------------------------
|
// Randomness -------------------------------------------------------------------------------------
|
||||||
@ -748,6 +760,8 @@ export class Level extends LevelInterface {
|
|||||||
_advance_tic_lexy() {
|
_advance_tic_lexy() {
|
||||||
// Under CC2 rules, there are two wire updates at the very beginning of the game before the
|
// 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.
|
// 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) {
|
if (this.tic_counter === 0) {
|
||||||
this._do_wire_phase();
|
this._do_wire_phase();
|
||||||
this._do_wire_phase();
|
this._do_wire_phase();
|
||||||
@ -973,6 +987,11 @@ export class Level extends LevelInterface {
|
|||||||
else {
|
else {
|
||||||
this.make_actor_decision(actor, forced_only);
|
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) {
|
if (actor.pending_push) {
|
||||||
this._set_tile_prop(actor, 'pending_push', null);
|
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
|
// Actor is allowed to move, so do so
|
||||||
let success = this.attempt_step(actor, direction);
|
let success = this.attempt_step(actor, direction);
|
||||||
@ -1031,29 +1046,33 @@ export class Level extends LevelInterface {
|
|||||||
(terrain.type.slide_mode === 'ice' && (
|
(terrain.type.slide_mode === 'ice' && (
|
||||||
! actor.ignores(terrain.type.name) || actor.type.name === 'ghost')) ||
|
! actor.ignores(terrain.type.name) || actor.type.name === 'ghost')) ||
|
||||||
// But they only bonk on a force floor if it affects them
|
// But they only bonk on a force floor if it affects them
|
||||||
(terrain.type.slide_mode === 'force' &&
|
(terrain.type.slide_mode === 'force' && ! actor.ignores(terrain.type.name))))
|
||||||
actor.slide_mode && ! actor.ignores(terrain.type.name))))
|
|
||||||
{
|
{
|
||||||
// Turn the actor around so ice corners bonk correctly
|
// Turn the actor around so ice corners bonk correctly
|
||||||
|
// XXX this is jank as hell
|
||||||
if (terrain.type.slide_mode === 'ice') {
|
if (terrain.type.slide_mode === 'ice') {
|
||||||
this.set_actor_direction(actor, DIRECTIONS[direction].opposite);
|
this.set_actor_direction(actor, DIRECTIONS[direction].opposite);
|
||||||
}
|
}
|
||||||
// Pretend they stepped on the tile again
|
// Pretend they stepped on the cell again -- this is what allows item bestowal to
|
||||||
// Note that ghosts bonk even on ice corners, which they can otherwise pass through,
|
// function, as a bonking monster will notice the item now and take it.
|
||||||
// argh!
|
this.step_on_cell(actor, actor.cell);
|
||||||
if (terrain.type.on_arrive && actor.type.name !== 'ghost') {
|
|
||||||
terrain.type.on_arrive(terrain, this, actor);
|
// 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 we got a new direction, try moving again
|
||||||
if (direction !== actor.direction && ! this.compat.bonking_isnt_instant) {
|
// FIXME in compat case, i guess we just set direction?
|
||||||
success = this.attempt_step(actor, actor.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') {
|
else if (terrain.type.name === 'teleport_red' && ! terrain.is_active) {
|
||||||
// Failed teleport slides only last for a single attempt. (Successful teleports
|
// Curious special-case red teleporter behavior: if you pass through a wired but
|
||||||
// continue the slide until landing on a new tile, as normal; otherwise you couldn't
|
// inactive one, you keep sliding indefinitely. Players can override out of it, but
|
||||||
// push a block coming out of a teleporter.)
|
// other actors are just stuck. So, set this again.
|
||||||
this.make_slide(actor, null);
|
this._set_tile_prop(actor, 'is_pending_slide', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1087,6 +1106,42 @@ export class Level extends LevelInterface {
|
|||||||
return;
|
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) {
|
if (! this.compat.tiles_react_instantly) {
|
||||||
this.step_on_cell(actor, actor.cell);
|
this.step_on_cell(actor, actor.cell);
|
||||||
}
|
}
|
||||||
@ -1269,6 +1324,22 @@ export class Level extends LevelInterface {
|
|||||||
return [dir1, dir2];
|
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) {
|
make_player_decision(actor, input, forced_only = false) {
|
||||||
// Only reset the player's is_pushing between movement, so it lasts for the whole push
|
// Only reset the player's is_pushing between movement, so it lasts for the whole push
|
||||||
this._set_tile_prop(actor, 'is_pushing', false);
|
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
|
// succeed, even if overriding in the same direction we're already moving, that does count
|
||||||
// as an override.
|
// as an override.
|
||||||
let terrain = actor.cell.get_terrain();
|
let terrain = actor.cell.get_terrain();
|
||||||
|
let forced_move = this.get_forced_move(actor, terrain);
|
||||||
let may_move = ! forced_only && (
|
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);
|
let [dir1, dir2] = this._extract_player_directions(input);
|
||||||
|
|
||||||
// Check for special player actions, which can only happen at decision time. Dropping can
|
// 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
|
// 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);
|
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.
|
// 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
|
// Note that if this is an override, then the forced direction is still used to
|
||||||
// interpret our input!
|
// interpret our input!
|
||||||
if (dir1 === actor.direction || dir2 === actor.direction) {
|
let current_direction = forced_move ?? actor.direction;
|
||||||
let other_direction = dir1 === actor.direction ? dir2 : dir1;
|
if (dir1 === current_direction || dir2 === current_direction) {
|
||||||
let curr_open = try_direction(actor.direction, push_mode);
|
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);
|
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;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
actor.decision = actor.direction;
|
actor.decision = current_direction;
|
||||||
open = curr_open;
|
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
|
// If we're overriding a force floor but the direction we're moving in is blocked, we
|
||||||
// counts as a forced move (but only under the CC2 behavior of instant bonking)
|
// keep our override power (but only under the CC2 behavior of instant bonking).
|
||||||
if (actor.slide_mode === 'force' && ! open && ! this.compat.bonking_isnt_instant) {
|
// 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);
|
this._set_tile_prop(actor, 'can_override_slide', true);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Otherwise this is 100% a conscious move so we lose our override power next tic
|
// Otherwise this is 100% a conscious move, so we lose override
|
||||||
// TODO how does this interact with teleports
|
|
||||||
this._set_tile_prop(actor, 'can_override_slide', false);
|
this._set_tile_prop(actor, 'can_override_slide', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1429,12 +1504,15 @@ export class Level extends LevelInterface {
|
|||||||
|
|
||||||
let direction_preference;
|
let direction_preference;
|
||||||
let terrain = actor.cell.get_terrain();
|
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
|
// 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 (?!)
|
// 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;
|
actor.decision = actor.direction;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1640,8 +1718,8 @@ export class Level extends LevelInterface {
|
|||||||
// FIXME this is clumsy and creates behavior dependent on actor order. my
|
// FIXME this is clumsy and creates behavior dependent on actor order. my
|
||||||
// original implementation only did this if the push /failed/; is that worth
|
// 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?
|
// a compat option? also, how does any of this work under lynx rules?
|
||||||
if (tile.slide_mode === 'force' ||
|
if (tile.is_sliding && ! tile.is_pulled && (tile.movement_cooldown > 0 ||
|
||||||
(tile.slide_mode !== null && tile.movement_cooldown > 0))
|
tile.cell.get_terrain().type.slide_mode === 'force'))
|
||||||
{
|
{
|
||||||
this._set_tile_prop(tile, 'pending_push', direction);
|
this._set_tile_prop(tile, 'pending_push', direction);
|
||||||
// FIXME if the block has already made a decision then this is necessary
|
// 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) {
|
check_movement(actor, orig_cell, direction, push_mode) {
|
||||||
// Lynx: Players can't override backwards on force floors, and it functions like blocking,
|
// Lynx: Nothing can move backwards on force floors, and it functions like blocking, but
|
||||||
// but does NOT act like a bonk (hence why it's here)
|
// does NOT act like a bonk (hence why it's here)
|
||||||
if (this.compat.no_backwards_override && actor === this.player &&
|
if (this.compat.no_backwards_override) {
|
||||||
actor.slide_mode === 'force' && direction === DIRECTIONS[actor.direction].opposite)
|
let terrain = orig_cell.get_terrain()
|
||||||
{
|
if (terrain.type.slide_mode === 'force' && ! actor.ignores(terrain.type.name) &&
|
||||||
return false;
|
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);
|
||||||
@ -1818,6 +1899,11 @@ export class Level extends LevelInterface {
|
|||||||
this._set_tile_prop(actor, 'movement_speed', duration);
|
this._set_tile_prop(actor, 'movement_speed', duration);
|
||||||
this.move_to(actor, goal_cell);
|
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
|
// 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
|
||||||
// a thing, and also make them a real move rather than a weird pending thing
|
// 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) {
|
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
|
// 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;
|
// 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
|
// 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
|
// Announce we're approaching
|
||||||
// moving towards and needs to last through our next decision
|
|
||||||
this.make_slide(actor, null);
|
|
||||||
for (let tile of goal_cell) {
|
for (let tile of goal_cell) {
|
||||||
if (! tile)
|
if (! tile)
|
||||||
continue;
|
continue;
|
||||||
@ -1909,9 +1996,6 @@ export class Level extends LevelInterface {
|
|||||||
if (tile.type.on_approach) {
|
if (tile.type.on_approach) {
|
||||||
tile.type.on_approach(tile, this, actor);
|
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
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (tile.type.on_arrive) {
|
else if (tile.type.on_arrive) {
|
||||||
tile.type.on_arrive(tile, this, actor);
|
tile.type.on_arrive(tile, this, actor);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Play step sound
|
if (tile.type.slide_automatically) {
|
||||||
if (actor === this.player) {
|
// This keeps a player on force floor consistently using their sliding pose, even if
|
||||||
let terrain = cell.get_terrain();
|
// drawn between moves. It also simplifies checks elsewhere, so that's nice
|
||||||
if (actor.slide_mode === 'ice') {
|
this._set_tile_prop(actor, 'is_pending_slide', true);
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2024,22 +2077,7 @@ export class Level extends LevelInterface {
|
|||||||
// movement towards the teleporter it just stepped on, not the teleporter it's moved to
|
// movement towards the teleporter it just stepped on, not the teleporter it's moved to
|
||||||
this._set_tile_prop(actor, 'destination_cell', actor.cell);
|
this._set_tile_prop(actor, 'destination_cell', actor.cell);
|
||||||
|
|
||||||
if (teleporter.type.name === 'teleport_red' && ! teleporter.is_active) {
|
let dest, direction, success;
|
||||||
// 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;
|
|
||||||
for ([dest, direction] of teleporter.type.teleport_dest_order(teleporter, this, actor)) {
|
for ([dest, direction] of teleporter.type.teleport_dest_order(teleporter, this, actor)) {
|
||||||
// Teleporters already containing an actor are blocked and unusable
|
// Teleporters already containing an actor are blocked and unusable
|
||||||
if (dest !== teleporter && dest.cell.get_actor())
|
if (dest !== teleporter && dest.cell.get_actor())
|
||||||
@ -2053,7 +2091,6 @@ export class Level extends LevelInterface {
|
|||||||
this.allow_taking_yellow_teleporters)
|
this.allow_taking_yellow_teleporters)
|
||||||
{
|
{
|
||||||
// Super duper special yellow teleporter behavior: you pick it the fuck up
|
// Super duper special yellow teleporter behavior: you pick it the fuck up
|
||||||
this.make_slide(actor, null);
|
|
||||||
this.attempt_take(actor, teleporter);
|
this.attempt_take(actor, teleporter);
|
||||||
if (actor === this.player) {
|
if (actor === this.player) {
|
||||||
this.sfx.play_once('get-tool', teleporter.cell);
|
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
|
// 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
|
// down and before the next actor even gets a chance to act
|
||||||
if (this.check_movement(actor, dest.cell, direction, 'bump')) {
|
if (this.check_movement(actor, dest.cell, direction, 'bump')) {
|
||||||
// Sound plays from the origin cell simply because that's where the sfx player
|
success = true;
|
||||||
// thinks the player is currently; position isn't updated til next turn
|
|
||||||
this.sfx.play_once('teleport', teleporter.cell);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly set us as teleport sliding, since in some very obscure cases (auto-dropping a
|
// Teleport slides happen when coming out of a teleporter, but not other times, so need to
|
||||||
// yellow teleporter because you picked up an item with a full inventory and immediately
|
// be noted explicitly
|
||||||
// teleporting through it) it may not have been applied
|
this._set_tile_prop(actor, 'is_pending_slide', true);
|
||||||
this.make_slide(actor, 'teleport');
|
|
||||||
// Real players might be able to immediately override the resulting slide
|
// Real players might be able to immediately override the resulting slide
|
||||||
if (actor.type.is_real_player && teleporter.type.allow_player_override) {
|
if (actor.type.is_real_player && teleporter.type.allow_player_override) {
|
||||||
this._set_tile_prop(actor, 'can_override_slide', true);
|
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.set_actor_direction(actor, direction);
|
||||||
|
|
||||||
this.spawn_animation(actor.cell, 'teleport_flash');
|
if (success) {
|
||||||
if (dest.cell !== actor.cell) {
|
// Sound plays from the origin cell simply because that's where the sfx player thinks
|
||||||
this.spawn_animation(dest.cell, 'teleport_flash');
|
// 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.spawn_animation(actor.cell, 'teleport_flash');
|
||||||
this.remove_tile(actor, true);
|
if (dest.cell !== actor.cell) {
|
||||||
this.add_tile(actor, dest.cell);
|
this.spawn_animation(dest.cell, 'teleport_flash');
|
||||||
// Erase this to prevent tail-biting through a teleport
|
}
|
||||||
this._set_tile_prop(actor, 'previous_cell', null);
|
|
||||||
|
// 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) {
|
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_cooldown', null);
|
||||||
this._set_tile_prop(actor, 'movement_speed', 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.move_to(actor, ankh_cell);
|
||||||
|
|
||||||
this.transmute_tile(this.ankh_tile, 'floor');
|
this.transmute_tile(this.ankh_tile, 'floor');
|
||||||
@ -2816,7 +2857,8 @@ export class Level extends LevelInterface {
|
|||||||
}
|
}
|
||||||
this._init_animation(tile);
|
this._init_animation(tile);
|
||||||
this._set_tile_prop(tile, 'previous_cell', null);
|
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);
|
actor.toolbelt.push(name);
|
||||||
this._push_pending_undo(() => actor.toolbelt.pop());
|
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;
|
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
|
// Change an actor's direction
|
||||||
set_actor_direction(actor, direction) {
|
set_actor_direction(actor, direction) {
|
||||||
this._set_tile_prop(actor, 'direction', direction);
|
this._set_tile_prop(actor, 'direction', direction);
|
||||||
|
|||||||
@ -1074,7 +1074,10 @@ class Player extends PrimaryView {
|
|||||||
let header = mk('h3');
|
let header = mk('h3');
|
||||||
let dl = mk('dl');
|
let dl = mk('dl');
|
||||||
let props = {};
|
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');
|
let dd = mk('dd');
|
||||||
props[key] = dd;
|
props[key] = dd;
|
||||||
dl.append(mk('dt', key), dd);
|
dl.append(mk('dt', key), dd);
|
||||||
@ -1494,7 +1497,7 @@ class Player extends PrimaryView {
|
|||||||
// force floors, even if you could override them!
|
// force floors, even if you could override them!
|
||||||
let moved = false;
|
let moved = false;
|
||||||
while (this.level.has_undo() &&
|
while (this.level.has_undo() &&
|
||||||
! (moved && this.level.player.slide_mode === null))
|
! (moved && ! this.level.player.is_pending_slide))
|
||||||
{
|
{
|
||||||
this.undo();
|
this.undo();
|
||||||
if (player_cell !== this.level.player.cell) {
|
if (player_cell !== this.level.player.cell) {
|
||||||
|
|||||||
299
js/tiletypes.js
299
js/tiletypes.js
@ -1,5 +1,4 @@
|
|||||||
import { COLLISION, DIRECTIONS, DIRECTION_ORDER, LAYERS, TICS_PER_SECOND, PICKUP_PRIORITIES } from './defs.js';
|
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
|
// TODO factor out some repeated stuff: common monster bits, common item bits, repeated collision
|
||||||
// masks
|
// masks
|
||||||
@ -7,43 +6,6 @@ function activate_me(me, level) {
|
|||||||
me.type.activate(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) {
|
function blocks_leaving_thin_walls(me, actor, direction) {
|
||||||
return me.type.thin_walls.has(direction) && actor.type.name !== 'ghost';
|
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) {
|
function update_wireable(me, level) {
|
||||||
if (me.is_wired === undefined) {
|
if (me.is_wired === undefined) {
|
||||||
@ -125,53 +104,54 @@ function player_visual_state(me) {
|
|||||||
if (me.fail_reason === 'drowned') {
|
if (me.fail_reason === 'drowned') {
|
||||||
return 'drowned';
|
return 'drowned';
|
||||||
}
|
}
|
||||||
else if (me.fail_reason === 'burned') {
|
if (me.fail_reason === 'burned') {
|
||||||
return 'burned';
|
return 'burned';
|
||||||
}
|
}
|
||||||
else if (me.fail_reason === 'exploded') {
|
if (me.fail_reason === 'exploded') {
|
||||||
return 'exploded';
|
return 'exploded';
|
||||||
}
|
}
|
||||||
else if (me.fail_reason === 'slimed') {
|
if (me.fail_reason === 'slimed') {
|
||||||
return 'slimed';
|
return 'slimed';
|
||||||
}
|
}
|
||||||
else if (me.fail_reason === 'electrocuted') {
|
if (me.fail_reason === 'electrocuted') {
|
||||||
return 'burned'; //same gfx for now
|
return 'burned'; //same gfx for now
|
||||||
}
|
}
|
||||||
else if (me.fail_reason === 'fell') {
|
if (me.fail_reason === 'fell') {
|
||||||
return 'fell';
|
return 'fell';
|
||||||
}
|
}
|
||||||
else if (me.fail_reason) {
|
if (me.fail_reason) {
|
||||||
return 'failed';
|
return 'failed';
|
||||||
}
|
}
|
||||||
else if (me.exited) {
|
if (me.exited) {
|
||||||
return 'exited';
|
return 'exited';
|
||||||
}
|
}
|
||||||
// This is slightly complicated. We should show a swimming pose while still in water, or moving
|
// 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
|
// 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)
|
// 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'))
|
! me.not_swimming && me.has_item('flippers'))
|
||||||
{
|
{
|
||||||
return 'swimming';
|
return 'swimming';
|
||||||
}
|
}
|
||||||
else if (me.slide_mode === 'ice') {
|
if (me.is_sliding || me.is_pending_slide) {
|
||||||
return 'skating';
|
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') {
|
if (me.is_blocked) {
|
||||||
return 'forced';
|
|
||||||
}
|
|
||||||
else if (me.is_blocked) {
|
|
||||||
return 'blocked';
|
return 'blocked';
|
||||||
}
|
}
|
||||||
else if (me.is_pushing) {
|
if (me.is_pushing) {
|
||||||
return 'pushing';
|
return 'pushing';
|
||||||
}
|
}
|
||||||
else if (me.movement_speed) {
|
if (me.movement_speed) {
|
||||||
return 'moving';
|
return 'moving';
|
||||||
}
|
}
|
||||||
else {
|
return 'normal';
|
||||||
return 'normal';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function button_visual_state(me) {
|
function button_visual_state(me) {
|
||||||
@ -750,12 +730,16 @@ const TILE_TYPES = {
|
|||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
contains_wire: true,
|
contains_wire: true,
|
||||||
wire_propagation_mode: 'all',
|
wire_propagation_mode: 'all',
|
||||||
|
slide_mode: 'turntable',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return other.direction;
|
||||||
|
},
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
|
level._set_tile_prop(other, 'is_pending_slide', true);
|
||||||
level.set_actor_direction(other, DIRECTIONS[other.direction].right);
|
level.set_actor_direction(other, DIRECTIONS[other.direction].right);
|
||||||
if (other.type.on_rotate) {
|
if (other.type.on_rotate) {
|
||||||
other.type.on_rotate(other, level, 'right');
|
other.type.on_rotate(other, level, 'right');
|
||||||
}
|
}
|
||||||
level.make_slide(other, 'turntable');
|
|
||||||
},
|
},
|
||||||
activate(me, level) {
|
activate(me, level) {
|
||||||
level.transmute_tile(me, 'turntable_ccw');
|
level.transmute_tile(me, 'turntable_ccw');
|
||||||
@ -767,12 +751,16 @@ const TILE_TYPES = {
|
|||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
contains_wire: true,
|
contains_wire: true,
|
||||||
wire_propagation_mode: 'all',
|
wire_propagation_mode: 'all',
|
||||||
|
slide_mode: 'turntable',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return other.direction;
|
||||||
|
},
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
|
level._set_tile_prop(other, 'is_pending_slide', true);
|
||||||
level.set_actor_direction(other, DIRECTIONS[other.direction].left);
|
level.set_actor_direction(other, DIRECTIONS[other.direction].left);
|
||||||
if (other.type.on_rotate) {
|
if (other.type.on_rotate) {
|
||||||
other.type.on_rotate(other, level, 'left');
|
other.type.on_rotate(other, level, 'left');
|
||||||
}
|
}
|
||||||
level.make_slide(other, 'turntable');
|
|
||||||
},
|
},
|
||||||
activate(me, level) {
|
activate(me, level) {
|
||||||
level.transmute_tile(me, 'turntable_cw');
|
level.transmute_tile(me, 'turntable_cw');
|
||||||
@ -883,7 +871,13 @@ const TILE_TYPES = {
|
|||||||
cracked_ice: {
|
cracked_ice: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
slide_mode: 'ice',
|
slide_mode: 'ice',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return other.direction;
|
||||||
|
},
|
||||||
speed_factor: 2,
|
speed_factor: 2,
|
||||||
|
on_arrive(me, level, other) {
|
||||||
|
level._set_tile_prop(other, 'is_pending_slide', true);
|
||||||
|
},
|
||||||
on_depart(me, level, other) {
|
on_depart(me, level, other) {
|
||||||
level.transmute_tile(me, 'water');
|
level.transmute_tile(me, 'water');
|
||||||
level.spawn_animation(me.cell, 'splash');
|
level.spawn_animation(me.cell, 'splash');
|
||||||
@ -893,172 +887,103 @@ const TILE_TYPES = {
|
|||||||
ice: {
|
ice: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
slide_mode: 'ice',
|
slide_mode: 'ice',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return other.direction;
|
||||||
|
},
|
||||||
speed_factor: 2,
|
speed_factor: 2,
|
||||||
|
on_arrive(me, level, other) {
|
||||||
|
level._set_tile_prop(other, 'is_pending_slide', true);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ice_sw: {
|
ice_sw: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
thin_walls: new Set(['south', 'west']),
|
thin_walls: new Set(['south', 'west']),
|
||||||
slide_mode: 'ice',
|
slide_mode: 'ice',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return {
|
||||||
|
north: 'north',
|
||||||
|
south: 'east',
|
||||||
|
east: 'east',
|
||||||
|
west: 'north',
|
||||||
|
}[other.direction];
|
||||||
|
},
|
||||||
speed_factor: 2,
|
speed_factor: 2,
|
||||||
blocks_leaving: blocks_leaving_thin_walls,
|
blocks_leaving: blocks_leaving_thin_walls,
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
if (other.direction === 'south') {
|
level._set_tile_prop(other, 'is_pending_slide', true);
|
||||||
level.set_actor_direction(other, 'east');
|
|
||||||
}
|
|
||||||
else if (other.direction === 'west') {
|
|
||||||
level.set_actor_direction(other, 'north');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ice_nw: {
|
ice_nw: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
thin_walls: new Set(['north', 'west']),
|
thin_walls: new Set(['north', 'west']),
|
||||||
slide_mode: 'ice',
|
slide_mode: 'ice',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return {
|
||||||
|
north: 'east',
|
||||||
|
south: 'south',
|
||||||
|
east: 'east',
|
||||||
|
west: 'south',
|
||||||
|
}[other.direction];
|
||||||
|
},
|
||||||
speed_factor: 2,
|
speed_factor: 2,
|
||||||
blocks_leaving: blocks_leaving_thin_walls,
|
blocks_leaving: blocks_leaving_thin_walls,
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
if (other.direction === 'north') {
|
level._set_tile_prop(other, 'is_pending_slide', true);
|
||||||
level.set_actor_direction(other, 'east');
|
|
||||||
}
|
|
||||||
else if (other.direction === 'west') {
|
|
||||||
level.set_actor_direction(other, 'south');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ice_ne: {
|
ice_ne: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
thin_walls: new Set(['north', 'east']),
|
thin_walls: new Set(['north', 'east']),
|
||||||
slide_mode: 'ice',
|
slide_mode: 'ice',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return {
|
||||||
|
north: 'west',
|
||||||
|
south: 'south',
|
||||||
|
east: 'south',
|
||||||
|
west: 'west',
|
||||||
|
}[other.direction];
|
||||||
|
},
|
||||||
speed_factor: 2,
|
speed_factor: 2,
|
||||||
blocks_leaving: blocks_leaving_thin_walls,
|
blocks_leaving: blocks_leaving_thin_walls,
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
if (other.direction === 'north') {
|
level._set_tile_prop(other, 'is_pending_slide', true);
|
||||||
level.set_actor_direction(other, 'west');
|
|
||||||
}
|
|
||||||
else if (other.direction === 'east') {
|
|
||||||
level.set_actor_direction(other, 'south');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ice_se: {
|
ice_se: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
thin_walls: new Set(['south', 'east']),
|
thin_walls: new Set(['south', 'east']),
|
||||||
slide_mode: 'ice',
|
slide_mode: 'ice',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return {
|
||||||
|
north: 'north',
|
||||||
|
south: 'west',
|
||||||
|
east: 'north',
|
||||||
|
west: 'west',
|
||||||
|
}[other.direction];
|
||||||
|
},
|
||||||
speed_factor: 2,
|
speed_factor: 2,
|
||||||
blocks_leaving: blocks_leaving_thin_walls,
|
blocks_leaving: blocks_leaving_thin_walls,
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
if (other.direction === 'south') {
|
level._set_tile_prop(other, 'is_pending_slide', true);
|
||||||
level.set_actor_direction(other, 'west');
|
|
||||||
}
|
|
||||||
else if (other.direction === 'east') {
|
|
||||||
level.set_actor_direction(other, 'north');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
force_floor_n: {
|
force_floor_n: _define_force_floor('north', 'force_floor_s'),
|
||||||
layer: LAYERS.terrain,
|
force_floor_s: _define_force_floor('south', 'force_floor_n'),
|
||||||
slide_mode: 'force',
|
force_floor_e: _define_force_floor('east', 'force_floor_w'),
|
||||||
speed_factor: 2,
|
force_floor_w: _define_force_floor('west', 'force_floor_e'),
|
||||||
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_all: {
|
force_floor_all: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
slide_mode: 'force',
|
slide_mode: 'force',
|
||||||
|
slide_automatically: true,
|
||||||
speed_factor: 2,
|
speed_factor: 2,
|
||||||
allow_player_override: true,
|
allow_player_override: true,
|
||||||
on_begin: on_begin_force_floor,
|
get_slide_direction(me, level, _other) {
|
||||||
// TODO ms: this is random, and an acting wall to monsters (!)
|
return level.get_force_floor_direction();
|
||||||
|
},
|
||||||
blocks(me, level, other) {
|
blocks(me, level, other) {
|
||||||
return (level.compat.rff_blocks_monsters &&
|
return (level.compat.rff_blocks_monsters &&
|
||||||
(other.type.collision_mask & COLLISION.monster_typical));
|
(other.type.collision_mask & COLLISION.monster_typical));
|
||||||
},
|
},
|
||||||
on_arrive(me, level, other) {
|
|
||||||
level.set_actor_direction(other, level.get_force_floor_direction());
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
slime: {
|
slime: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
@ -1758,6 +1683,9 @@ const TILE_TYPES = {
|
|||||||
teleport_blue: {
|
teleport_blue: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
slide_mode: 'teleport',
|
slide_mode: 'teleport',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return other.direction;
|
||||||
|
},
|
||||||
contains_wire: true,
|
contains_wire: true,
|
||||||
wire_propagation_mode: 'all',
|
wire_propagation_mode: 'all',
|
||||||
*teleport_dest_order(me, level, other) {
|
*teleport_dest_order(me, level, other) {
|
||||||
@ -1845,6 +1773,9 @@ const TILE_TYPES = {
|
|||||||
teleport_red: {
|
teleport_red: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
slide_mode: 'teleport',
|
slide_mode: 'teleport',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return other.direction;
|
||||||
|
},
|
||||||
contains_wire: true,
|
contains_wire: true,
|
||||||
wire_propagation_mode: 'none',
|
wire_propagation_mode: 'none',
|
||||||
allow_player_override: true,
|
allow_player_override: true,
|
||||||
@ -1899,6 +1830,9 @@ const TILE_TYPES = {
|
|||||||
teleport_green: {
|
teleport_green: {
|
||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
slide_mode: 'teleport',
|
slide_mode: 'teleport',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return other.direction;
|
||||||
|
},
|
||||||
*teleport_dest_order(me, level, other) {
|
*teleport_dest_order(me, level, other) {
|
||||||
// The CC2 green teleporter scheme is:
|
// The CC2 green teleporter scheme is:
|
||||||
// 1. Use the PRNG to pick another green teleporter
|
// 1. Use the PRNG to pick another green teleporter
|
||||||
@ -1970,6 +1904,9 @@ const TILE_TYPES = {
|
|||||||
layer: LAYERS.terrain,
|
layer: LAYERS.terrain,
|
||||||
item_priority: PICKUP_PRIORITIES.always,
|
item_priority: PICKUP_PRIORITIES.always,
|
||||||
slide_mode: 'teleport',
|
slide_mode: 'teleport',
|
||||||
|
get_slide_direction(me, level, other) {
|
||||||
|
return other.direction;
|
||||||
|
},
|
||||||
allow_player_override: true,
|
allow_player_override: true,
|
||||||
*teleport_dest_order(me, level, other) {
|
*teleport_dest_order(me, level, other) {
|
||||||
let exit_direction = other.direction;
|
let exit_direction = other.direction;
|
||||||
@ -2810,10 +2747,6 @@ const TILE_TYPES = {
|
|||||||
if (terrain && terrain.type.on_arrive && ! me.ignores(terrain.type.name)) {
|
if (terrain && terrain.type.on_arrive && ! me.ignores(terrain.type.name)) {
|
||||||
terrain.type.on_arrive(terrain, level, me);
|
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) {
|
if (obstacle && obstacle.type.is_actor) {
|
||||||
level.kill_actor(obstacle, me, 'explosion');
|
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
|
// 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
|
// bowling balls in the process of being released from a cloner
|
||||||
return;
|
return;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user