Add teleport overriding and seriously clean up teleport code

This commit is contained in:
Eevee (Evelyn Woods) 2020-12-15 16:44:37 -07:00
parent 7c82a4cdf9
commit 25b4b32f94
2 changed files with 120 additions and 80 deletions

View File

@ -631,7 +631,7 @@ export class Level {
continue; continue;
if (actor.just_stepped_on_teleporter) { if (actor.just_stepped_on_teleporter) {
this.attempt_teleport(actor); this.attempt_teleport(actor, actor === this.player ? p1_input : null);
} }
} }
@ -678,6 +678,11 @@ export class Level {
continue; continue;
// Check for special player actions, which can only happen when not moving // Check for special player actions, which can only happen when not moving
// FIXME if you press a key while moving it should happen as soon as you stop (assuming
// the key is still held down)
// FIXME cc2 seems to rely on key repeat for this; the above is true, but also, if you
// have four bowling balls and hold Q, you'll throw the first, wait a second or so, then
// release the rest rapid-fire. absurd
if (actor === this.player) { if (actor === this.player) {
let new_input = p1_input & ~this.previous_input; let new_input = p1_input & ~this.previous_input;
if (new_input & INPUT_BITS.cycle) { if (new_input & INPUT_BITS.cycle) {
@ -768,13 +773,8 @@ export class Level {
this.commit(); this.commit();
} }
make_player_decision(actor, input) { _extract_player_directions(input) {
// Only reset the player's is_pushing between movement, so it lasts for the whole push // Extract directions from an input mask
this._set_tile_prop(actor, 'is_pushing', false);
// TODO player in a cloner can't move (but player in a trap can still turn)
// Extract directions from the input mask
let dir1 = null, dir2 = null; let dir1 = null, dir2 = null;
if (((input & INPUT_BITS['up']) && (input & INPUT_BITS['down'])) || if (((input & INPUT_BITS['up']) && (input & INPUT_BITS['down'])) ||
((input & INPUT_BITS['left']) && (input & INPUT_BITS['right']))) ((input & INPUT_BITS['left']) && (input & INPUT_BITS['right'])))
@ -795,15 +795,21 @@ export class Level {
} }
} }
} }
return [dir1, dir2];
}
make_player_decision(actor, input) {
// Only reset the player's is_pushing between movement, so it lasts for the whole push
this._set_tile_prop(actor, 'is_pushing', false);
// TODO player in a cloner can't move (but player in a trap can still turn)
let [dir1, dir2] = this._extract_player_directions(input);
let try_direction = (direction, push_mode) => { let try_direction = (direction, push_mode) => {
direction = actor.cell.redirect_exit(actor, direction); direction = actor.cell.redirect_exit(actor, direction);
let dest_cell = this.get_neighboring_cell(actor.cell, direction); // FIXME if the player steps into a monster cell here, they die instantly! but only
return (dest_cell && // if the cell doesn't block them??
! actor.cell.blocks_leaving(actor, direction) && return this.check_movement(actor, actor.cell, direction, push_mode);
// FIXME if the player steps into a monster cell here, they die instantly! but only
// if the cell doesn't block them??
! dest_cell.blocks_entering(actor, direction, this, push_mode));
}; };
// The player is unusual in several ways. // The player is unusual in several ways.
@ -957,7 +963,7 @@ export class Level {
direction = actor.cell.redirect_exit(actor, direction); direction = actor.cell.redirect_exit(actor, direction);
if (this.is_move_allowed(actor, direction, 'trace')) { if (this.check_movement(actor, actor.cell, direction, 'trace')) {
// We found a good direction! Stop here // We found a good direction! Stop here
actor.decision = direction; actor.decision = direction;
all_blocked = false; all_blocked = false;
@ -999,10 +1005,10 @@ export class Level {
} }
// FIXME make this interact with railroads correctly and use it for players and in attempt_step // FIXME make this interact with railroads correctly and use it for players and in attempt_step
is_move_allowed(actor, direction, push_mode) { check_movement(actor, orig_cell, direction, push_mode) {
let dest_cell = this.get_neighboring_cell(actor.cell, direction); let dest_cell = this.get_neighboring_cell(orig_cell, direction);
return (dest_cell && return (dest_cell &&
! actor.cell.blocks_leaving(actor, direction) && ! orig_cell.blocks_leaving(actor, direction) &&
! dest_cell.blocks_entering(actor, direction, this, push_mode)); ! dest_cell.blocks_entering(actor, direction, this, push_mode));
} }
@ -1263,70 +1269,82 @@ export class Level {
} }
} }
attempt_teleport(actor) { attempt_teleport(actor, input) {
let teleporter = actor.just_stepped_on_teleporter; let teleporter = actor.just_stepped_on_teleporter;
actor.just_stepped_on_teleporter = null; actor.just_stepped_on_teleporter = null;
// Handle teleporting, now that the dust has cleared let push_mode = actor === this.player ? 'move' : 'trace';
// FIXME something funny happening here, your input isn't ignored while walking out of it?
let original_direction = actor.direction; let original_direction = actor.direction;
let success = false; let success = false;
for (let dest of teleporter.type.teleport_dest_order(teleporter, this, actor)) { let dest, direction;
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
// FIXME should check collision? // FIXME should check collision?
if (dest.cell.some(tile => tile.type.is_actor && tile !== actor)) if (dest.cell.some(tile => tile.type.is_actor && tile !== actor))
continue; continue;
// Physically move the actor to the new teleporter
// XXX lynx treats this as a slide and does it in a pass in the main loop // XXX lynx treats this as a slide and does it in a pass in the main loop
// XXX not especially undo-efficient
// FIXME the new aggressive move checker could help here!
this.remove_tile(actor);
this.add_tile(actor, dest.cell);
// FIXME teleport overrides... also allow block slapping... seems like horizontal
// wins...
// Red and green teleporters attempt to spit you out in every direction before
// giving up on a destination (but not if you return to the original).
// Note that we use actor.direction here (rather than original_direction) because
// green teleporters modify it in teleport_dest_order, to randomize the exit
// direction
let direction = actor.direction;
let num_directions = 1;
if (teleporter.type.teleport_try_all_directions && dest !== teleporter) {
num_directions = 4;
}
// FIXME bleugh hardcode // FIXME bleugh hardcode
if (dest === teleporter && teleporter.type.name === 'teleport_yellow') { if (dest === teleporter && teleporter.type.name === 'teleport_yellow') {
break; break;
} }
for (let i = 0; i < num_directions; i++) { if (this.check_movement(actor, dest.cell, direction, push_mode)) {
if (this.attempt_out_of_turn_step(actor, direction)) { success = true;
success = true; // Sound plays from the origin cell simply because that's where the sfx player
// 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
// thinks the player is currently; position isn't updated til next turn this.sfx.play_once('teleport', teleporter.cell);
this.sfx.play_once('teleport', teleporter.cell);
break;
}
else {
direction = DIRECTIONS[direction].right;
}
}
if (success) {
break; break;
} }
else if (num_directions === 4) {
// Restore our original facing before continuing
// (For red teleports, we try every possible destination in our original
// movement direction, so this is correct. For green teleports, we only try one
// destination and then fall back to walking through the source in our original
// movement direction, so this is still correct.)
this.set_actor_direction(actor, original_direction);
}
} }
if (success) {
if (teleporter.type.teleport_allow_override && actor === this.player) {
// Red and yellow teleporters allow players to override the exit direction. This
// can only happen after we've found a suitable destination. As with normal player
// decisions, we aggressively check each direction first (meaning we might bump the
// same cell twice here!), and then figure out what to do afterwards.
// Note that it's possible to bump a direction multiple times during this process,
// and also possible to perform a three-way block slap: the direction she leaves,
// the other direction she was holding, and the original exit direction we found.
let [dir1, dir2] = this._extract_player_directions(input);
let open1 = false, open2 = false;
if (dir1) {
open1 = this.check_movement(actor, dest.cell, dir1, push_mode);
}
if (dir2) {
open2 = this.check_movement(actor, dest.cell, dir2, push_mode);
}
// If the player didn't even try to override, do nothing
if (! dir1 && ! dir2) {
}
// If only one direction is available, whether because she only held one direction
// or because one of them was blocked, use that one
else if ((open1 && ! open2) || (dir1 && ! dir2)) {
direction = dir1;
}
else if (! open1 && open2) {
direction = dir2;
}
// Otherwise, we have a tie. If either direction is the exit we found (which
// can only happen if both are open), prefer that one...
else if (dir1 === direction || dir2 === direction) {
}
// ...otherwise, prefer the horizontal one.
else if (dir1 === 'west' || dir1 === 'east') {
direction = dir1;
}
else {
direction = dir2;
}
}
// Now physically move the actor and have them take a turn
this.remove_tile(actor);
this.add_tile(actor, dest.cell);
this.attempt_out_of_turn_step(actor, direction);
}
if (! success && actor.type.has_inventory && teleporter.type.name === 'teleport_yellow') { if (! success && actor.type.has_inventory && teleporter.type.name === 'teleport_yellow') {
// Super duper special yellow teleporter behavior: you pick it the fuck up // Super duper special yellow teleporter behavior: you pick it the fuck up
// FIXME not if there's only one in the level? // FIXME not if there's only one in the level?

View File

@ -1,4 +1,4 @@
import { COLLISION, DIRECTIONS, DRAW_LAYERS } from './defs.js'; import { COLLISION, DIRECTIONS, DIRECTION_ORDER, DRAW_LAYERS } from './defs.js';
import { random_choice } from './util.js'; import { random_choice } from './util.js';
function activate_me(me, level) { function activate_me(me, level) {
@ -1141,12 +1141,15 @@ const TILE_TYPES = {
teleport_blue: { teleport_blue: {
draw_layer: DRAW_LAYERS.terrain, draw_layer: DRAW_LAYERS.terrain,
wire_propagation_mode: 'all', wire_propagation_mode: 'all',
teleport_dest_order(me, level, other) { *teleport_dest_order(me, level, other) {
let exit_direction = other.direction;
if (! me.wire_directions) { if (! me.wire_directions) {
// TODO cc2 has a bug where, once it wraps around to the bottom right, it seems to // TODO cc2 has a bug where, once it wraps around to the bottom right, it seems to
// forget that it was ever looking for an unwired teleport and will just grab the // forget that it was ever looking for an unwired teleport and will just grab the
// first one it sees // first one it sees
return level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true); for (let dest of level.iter_tiles_in_reading_order(me.cell, 'teleport_blue', true)) {
yield [dest, exit_direction];
}
} }
// Wired blue teleports form a network, which means we have to walk all wires from this // Wired blue teleports form a network, which means we have to walk all wires from this
@ -1225,13 +1228,14 @@ const TILE_TYPES = {
) % level_size); ) % level_size);
} }
found.sort((a, b) => dest_indices.get(b) - dest_indices.get(a)); found.sort((a, b) => dest_indices.get(b) - dest_indices.get(a));
return found; for (let dest of found) {
yield [dest, exit_direction];
}
}, },
}, },
teleport_red: { teleport_red: {
draw_layer: DRAW_LAYERS.terrain, draw_layer: DRAW_LAYERS.terrain,
wire_propagation_mode: 'none', wire_propagation_mode: 'none',
teleport_try_all_directions: true,
teleport_allow_override: true, teleport_allow_override: true,
_is_active(me) { _is_active(me) {
return ! (me.wire_directions && (me.cell.powered_edges & me.wire_directions) === 0); return ! (me.wire_directions && (me.cell.powered_edges & me.wire_directions) === 0);
@ -1243,13 +1247,21 @@ const TILE_TYPES = {
// has the bizarre behavior of NOT considering a red teleporter wired if none of its // has the bizarre behavior of NOT considering a red teleporter wired if none of its
// wires are directly connected to another neighboring wire. // wires are directly connected to another neighboring wire.
// FIXME implement that, merge current code with is_cell_wired // FIXME implement that, merge current code with is_cell_wired
if (! this._is_active(me)) { let iterable;
yield me; if (this._is_active(me)) {
return; iterable = level.iter_tiles_in_reading_order(me.cell, 'teleport_red');
} }
for (let tile of level.iter_tiles_in_reading_order(me.cell, 'teleport_red')) { else {
iterable = [me];
}
let exit_direction = other.direction;
for (let tile of iterable) {
if (tile === me || this._is_active(tile)) { if (tile === me || this._is_active(tile)) {
yield tile; // Red teleporters allow exiting in any direction, searching clockwise
yield [tile, exit_direction];
yield [tile, DIRECTIONS[exit_direction].right];
yield [tile, DIRECTIONS[exit_direction].opposite];
yield [tile, DIRECTIONS[exit_direction].left];
} }
} }
}, },
@ -1262,28 +1274,38 @@ const TILE_TYPES = {
}, },
teleport_green: { teleport_green: {
draw_layer: DRAW_LAYERS.terrain, draw_layer: DRAW_LAYERS.terrain,
teleport_try_all_directions: true,
teleport_dest_order(me, level, other) { teleport_dest_order(me, level, other) {
let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green')); let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green'));
if (all.length <= 1) { if (all.length <= 1) {
// If this is the only teleporter, just walk out the other side — and, crucially, do // If this is the only teleporter, just walk out the other side — and, crucially, do
// NOT advance the PRNG // NOT advance the PRNG
return [me]; return [[me, other.direction]];
} }
// Note the iterator starts on the /next/ teleporter, so there's an implicit +1 here. // Note the iterator starts on the /next/ teleporter, so there's an implicit +1 here.
// The -1 is to avoid spitting us back out of the same teleporter, which will be last in // The -1 is to avoid spitting us back out of the same teleporter, which will be last in
// the list // the list
let target = all[level.prng() % (all.length - 1)]; let target = all[level.prng() % (all.length - 1)];
// Also set the actor's (initial) exit direction // Also pick the actor's exit direction
level.set_actor_direction(other, ['north', 'east', 'south', 'west'][level.prng() % 4]); let exit_direction = DIRECTION_ORDER[level.prng() % 4];
return [target, me]; return [
// Green teleporters allow exiting in any direction, similar to red, but only on the
// one they found; if that fails, you walk straight across the one you entered
[target, exit_direction],
[target, DIRECTIONS[exit_direction].right],
[target, DIRECTIONS[exit_direction].opposite],
[target, DIRECTIONS[exit_direction].left],
[me, other.direction],
];
}, },
}, },
teleport_yellow: { teleport_yellow: {
draw_layer: DRAW_LAYERS.terrain, draw_layer: DRAW_LAYERS.terrain,
teleport_allow_override: true, teleport_allow_override: true,
teleport_dest_order(me, level, other) { *teleport_dest_order(me, level, other) {
return level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true); let exit_direction = other.direction;
for (let dest of level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true)) {
yield [dest, exit_direction];
}
}, },
}, },
// Flame jet rules: // Flame jet rules:
@ -1363,7 +1385,7 @@ const TILE_TYPES = {
for (let i = level.actors.length - 1; i >= 0; i--) { for (let i = level.actors.length - 1; i >= 0; i--) {
let actor = level.actors[i]; let actor = level.actors[i];
// TODO generify somehow?? // TODO generify somehow??
if (actor.type.name === 'tank_yellow' && level.is_move_allowed(actor, other.direction, 'trace')) { if (actor.type.name === 'tank_yellow' && level.check_movement(actor, actor.cell, other.direction, 'trace')) {
unblocked_tanks.push(actor); unblocked_tanks.push(actor);
} }
} }