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;
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;
// 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) {
let new_input = p1_input & ~this.previous_input;
if (new_input & INPUT_BITS.cycle) {
@ -768,13 +773,8 @@ export class Level {
this.commit();
}
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)
// Extract directions from the input mask
_extract_player_directions(input) {
// Extract directions from an input mask
let dir1 = null, dir2 = null;
if (((input & INPUT_BITS['up']) && (input & INPUT_BITS['down'])) ||
((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) => {
direction = actor.cell.redirect_exit(actor, direction);
let dest_cell = this.get_neighboring_cell(actor.cell, direction);
return (dest_cell &&
! actor.cell.blocks_leaving(actor, direction) &&
// 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));
// FIXME if the player steps into a monster cell here, they die instantly! but only
// if the cell doesn't block them??
return this.check_movement(actor, actor.cell, direction, push_mode);
};
// The player is unusual in several ways.
@ -957,7 +963,7 @@ export class Level {
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
actor.decision = direction;
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
is_move_allowed(actor, direction, push_mode) {
let dest_cell = this.get_neighboring_cell(actor.cell, direction);
check_movement(actor, orig_cell, direction, push_mode) {
let dest_cell = this.get_neighboring_cell(orig_cell, direction);
return (dest_cell &&
! actor.cell.blocks_leaving(actor, direction) &&
! orig_cell.blocks_leaving(actor, direction) &&
! 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;
actor.just_stepped_on_teleporter = null;
// Handle teleporting, now that the dust has cleared
// FIXME something funny happening here, your input isn't ignored while walking out of it?
let push_mode = actor === this.player ? 'move' : 'trace';
let original_direction = actor.direction;
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
// FIXME should check collision?
if (dest.cell.some(tile => tile.type.is_actor && tile !== actor))
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 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
if (dest === teleporter && teleporter.type.name === 'teleport_yellow') {
break;
}
for (let i = 0; i < num_directions; i++) {
if (this.attempt_out_of_turn_step(actor, direction)) {
success = true;
// 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);
break;
}
else {
direction = DIRECTIONS[direction].right;
}
}
if (success) {
if (this.check_movement(actor, dest.cell, direction, push_mode)) {
success = true;
// 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);
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') {
// Super duper special yellow teleporter behavior: you pick it the fuck up
// 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';
function activate_me(me, level) {
@ -1141,12 +1141,15 @@ const TILE_TYPES = {
teleport_blue: {
draw_layer: DRAW_LAYERS.terrain,
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) {
// 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
// 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
@ -1225,13 +1228,14 @@ const TILE_TYPES = {
) % level_size);
}
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: {
draw_layer: DRAW_LAYERS.terrain,
wire_propagation_mode: 'none',
teleport_try_all_directions: true,
teleport_allow_override: true,
_is_active(me) {
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
// wires are directly connected to another neighboring wire.
// FIXME implement that, merge current code with is_cell_wired
if (! this._is_active(me)) {
yield me;
return;
let iterable;
if (this._is_active(me)) {
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)) {
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: {
draw_layer: DRAW_LAYERS.terrain,
teleport_try_all_directions: true,
teleport_dest_order(me, level, other) {
let all = Array.from(level.iter_tiles_in_reading_order(me.cell, 'teleport_green'));
if (all.length <= 1) {
// If this is the only teleporter, just walk out the other side — and, crucially, do
// 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.
// The -1 is to avoid spitting us back out of the same teleporter, which will be last in
// the list
let target = all[level.prng() % (all.length - 1)];
// Also set the actor's (initial) exit direction
level.set_actor_direction(other, ['north', 'east', 'south', 'west'][level.prng() % 4]);
return [target, me];
// Also pick the actor's exit direction
let exit_direction = DIRECTION_ORDER[level.prng() % 4];
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: {
draw_layer: DRAW_LAYERS.terrain,
teleport_allow_override: true,
teleport_dest_order(me, level, other) {
return level.iter_tiles_in_reading_order(me.cell, 'teleport_yellow', true);
*teleport_dest_order(me, level, other) {
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:
@ -1363,7 +1385,7 @@ const TILE_TYPES = {
for (let i = level.actors.length - 1; i >= 0; i--) {
let actor = level.actors[i];
// 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);
}
}