Fix rovers once and for all; make helmet work more often; rename some stuff; simplify attempt_step

This commit is contained in:
Eevee (Evelyn Woods) 2020-12-16 20:05:36 -07:00
parent 7cf92f7841
commit d4da572940
3 changed files with 181 additions and 186 deletions

View File

@ -1128,7 +1128,7 @@ export function parse_level(buf, number = 1) {
}
}
else {
console.warn(`Unrecognized section type '${type}' at offset ${bytes.byteOffset}`);
console.warn(`Unrecognized section type '${type}' at offset ${bytes.byteOffset}`, view);
// TODO save it, persist when editing level
}
}

View File

@ -52,8 +52,6 @@ export class Tile {
// Extremely awkward special case: items don't block monsters if the cell also contains an
// item modifier (i.e. "no" sign) or a real player
// TODO would love to get this outta here
// FIXME i think this can be removed if monster/player interaction stops being a literal
// collision and starts being a result of even attempting to move
if (this.type.is_item &&
this.cell.some(tile => tile.type.item_modifier || tile.type.is_real_player))
return false;
@ -104,7 +102,7 @@ export class Tile {
direction = tile.cell.redirect_exit(tile, direction);
// Need to explicitly check this here, otherwise you could /attempt/ to push a block,
// which would fail, but it would still change the block's direction
return ! tile.cell.blocks_leaving(tile, direction);
return tile.cell.try_leaving(tile, direction);
}
// Inventory stuff
@ -195,57 +193,55 @@ export class Cell extends Array {
return this.some(tile => tile.name === name);
}
blocks_leaving(actor, direction) {
try_leaving(actor, direction) {
for (let tile of this) {
if (tile === actor)
continue;
if (tile.type.traps && tile.type.traps(tile, actor))
return true;
return false;
if (tile.type.blocks_leaving && tile.type.blocks_leaving(tile, actor, direction))
return true;
}
return false;
}
return true;
}
// Check if this actor can move this direction into this cell. May have side effects, depending
// on the value of push_mode:
// - null: Default. Treat pushable objects as blocking.
// - 'ignore': Treat pushable objects as nonblocking.
// - 'trace': Don't try to move pushable objects, but do check whether they could be pushed,
// recursively if necessary.
// - 'move': Attempt to move pushable objects out of the way immediately.
blocks_entering(actor, direction, level, push_mode = null) {
// Check if this actor can move this direction into this cell. Returns true on success. May
// have side effects, depending on the value of push_mode:
// - null: Default. Do not impact game state. Treat pushable objects as blocking.
// - 'bump': Fire bump triggers. Don't move pushable objects, but do check whether they /could/
// be pushed, recursively if necessary.
// - 'push': Fire bump triggers. Attempt to move pushable objects out of the way immediately.
try_entering(actor, direction, level, push_mode = null) {
let pushable_tiles = [];
let blocked = false;
for (let tile of this) {
// (Note that here, and anywhere else that has any chance of altering the cell's contents,
// we iterate over a copy of the cell to insulate ourselves from tiles appearing or
// disappearing mid-iteration.)
for (let tile of Array.from(this)) {
// TODO check ignores here?
if (actor.type.on_bump) {
actor.type.on_bump(actor, level, tile, direction);
}
if (tile.type.on_bumped) {
tile.type.on_bumped(tile, level, actor);
}
if (! tile.blocks(actor, direction, level))
continue;
if (push_mode === null)
return true;
return false;
if (! actor.can_push(tile, direction)) {
if (push_mode === 'move') {
// Track this instead of returning immediately, because 'move' mode also bumps
if (push_mode === 'push') {
// Track this instead of returning immediately, because 'push' mode also bumps
// every tile in the cell
blocked = true;
}
else {
return true;
}
}
if (push_mode === 'ignore')
continue;
if (push_mode === 'move') {
if (actor.type.on_bump) {
actor.type.on_bump(actor, level, tile);
}
if (tile.type.on_bumped) {
tile.type.on_bumped(tile, level, actor);
return false;
}
}
@ -254,27 +250,37 @@ export class Cell extends Array {
}
if (blocked)
return true;
return false;
// If we got this far, all that's left is to deal with pushables
if (pushable_tiles.length > 0) {
let neighbor_cell = level.get_neighboring_cell(this, direction);
if (! neighbor_cell)
return true;
return false;
for (let tile of pushable_tiles) {
if (push_mode === 'trace') {
if (neighbor_cell.blocks_entering(tile, direction, level, push_mode))
return true;
if (push_mode === 'bump') {
// FIXME and leaving!
if (! neighbor_cell.try_entering(tile, direction, level, push_mode))
return false;
}
else if (push_mode === 'push') {
if (actor === this.player) {
this._set_tile_prop(actor, 'is_pushing', true);
}
if (! level.attempt_out_of_turn_step(tile, direction)) {
if (tile.slide_mode !== null && tile.movement_cooldown !== 0) {
// If the push failed and the obstacle is in the middle of a slide,
// remember this as the next move it'll make
level._set_tile_prop(tile, 'pending_push', direction);
}
return false;
}
else if (push_mode === 'move') {
if (! level.attempt_step(tile, direction))
return true;
}
}
}
return false;
return true;
}
// Special railroad ability: change the direction we attempt to leave
@ -729,6 +735,10 @@ export class Level {
success = this.attempt_step(actor, actor.decision);
}
if (! success && actor.type.on_blocked) {
actor.type.on_blocked(actor, this, actor.decision);
}
// Track whether the player is blocked, for visual effect
if (actor === this.player && actor.decision && ! success) {
this.sfx.play_once('blocked');
@ -848,7 +858,7 @@ export class Level {
else if (actor.slide_mode === 'ice') {
// A sliding player that bonks into a wall still needs to turn around, but in this
// case they do NOT start pushing blocks early
if (! try_direction(actor.direction, 'trace')) {
if (! try_direction(actor.direction, 'bump')) {
this._handle_slide_bonk(actor);
}
}
@ -863,7 +873,7 @@ export class Level {
let open;
if (dir2 === null) {
// Only one direction is held, but for consistency, "check" it anyway
open = try_direction(dir1, 'move');
open = try_direction(dir1, 'push');
actor.decision = dir1;
}
else {
@ -871,8 +881,8 @@ export class Level {
// one, UNLESS it's blocked AND the other isn't
if (dir1 === actor.direction || dir2 === actor.direction) {
let other_direction = dir1 === actor.direction ? dir2 : dir1;
let curr_open = try_direction(actor.direction, 'move');
let other_open = try_direction(other_direction, 'move');
let curr_open = try_direction(actor.direction, 'push');
let other_open = try_direction(other_direction, 'push');
if (! curr_open && other_open) {
actor.decision = other_direction;
open = true;
@ -887,8 +897,8 @@ export class Level {
// FIXME i'm told cc2 prefers orthogonal actually, but need to check on that
// FIXME lynx only checks horizontal, what about cc2? it must check both
// because of the behavior where pushing into a corner always pushes horizontal
let open1 = try_direction(dir1, 'move');
let open2 = try_direction(dir2, 'move');
let open1 = try_direction(dir1, 'push');
let open2 = try_direction(dir2, 'push');
if (open1 && ! open2) {
actor.decision = dir1;
open = true;
@ -978,7 +988,7 @@ export class Level {
direction = actor.cell.redirect_exit(actor, direction);
if (this.check_movement(actor, actor.cell, direction, 'trace')) {
if (this.check_movement(actor, actor.cell, direction, 'bump')) {
// We found a good direction! Stop here
actor.decision = direction;
all_blocked = false;
@ -1023,8 +1033,8 @@ export class Level {
check_movement(actor, orig_cell, direction, push_mode) {
let dest_cell = this.get_neighboring_cell(orig_cell, direction);
return (dest_cell &&
! orig_cell.blocks_leaving(actor, direction) &&
! dest_cell.blocks_entering(actor, direction, this, push_mode));
orig_cell.try_leaving(actor, direction) &&
dest_cell.try_entering(actor, direction, this, push_mode));
}
// Try to move the given actor one tile in the given direction and update their cooldown.
@ -1043,27 +1053,28 @@ export class Level {
let move = DIRECTIONS[direction].movement;
let goal_cell = this.get_neighboring_cell(actor.cell, direction);
if (! goal_cell)
return false;
// TODO this could be a lot simpler if i could early-return! should ice bumping be
// somewhere else?
let blocked;
if (goal_cell) {
// Only bother touching the goal cell if we're not already trapped in this one
if (actor.cell.blocks_leaving(actor, direction)) {
blocked = true;
if (! actor.cell.try_leaving(actor, direction))
return false;
if (! goal_cell.try_entering(actor, direction, this, 'push'))
return false;
// FIXME this feels clunky
let terrain = goal_cell.get_terrain();
if (terrain && terrain.type.slide_mode && ! actor.ignores(terrain.type.name)) {
double_speed = true;
}
// (Note that here, and anywhere else that has any chance of
// altering the cell's contents, we iterate over a copy of the cell
// to insulate ourselves from tiles appearing or disappearing
// mid-iteration.)
// FIXME actually, this prevents flicking!
if (! blocked) {
// FIXME this can probably reuse blocks_entering now
// FIXME this can probably reuse try_entering now
// Try to move into the cell. This is usually a simple check of whether we can
// enter it (similar to Cell.blocks_entering), but if the only thing blocking us is
// enter it (similar to Cell.try_entering), but if the only thing blocking us is
// a pushable object, we have to do two more passes: one to push anything pushable,
// then one to check whether we're blocked again.
/*
let blocked_by_pushable = false;
for (let tile of goal_cell) {
if (tile.blocks(actor, direction, this)) {
@ -1115,18 +1126,9 @@ export class Level {
}
// Now check if we're still blocked
blocked = goal_cell.blocks_entering(actor, direction, this);
}
}
}
else {
// Hit the edge
blocked = true;
}
if (blocked) {
return false;
blocked = goal_cell.try_entering(actor, direction, this);
}
*/
// We're clear!
if (double_speed || actor.has_item('speed_boots')) {
@ -1200,33 +1202,12 @@ export class Level {
this.hint_shown = null;
}
for (let tile of goal_cell) {
if (actor.type.is_real_player && tile.type.is_monster) {
this.fail(tile.type.name);
}
else if (actor.type.is_monster && tile.type.is_real_player) {
this.fail(actor.type.name);
}
else if (actor.type.is_block && tile.type.is_real_player) {
this.fail('squished');
}
// FIXME this could go in on_approach now
if (actor === this.player && tile.type.is_hint) {
this.hint_shown = tile.hint_text ?? this.stored_level.hint;
}
}
// If we're stepping directly on the player, that kills them too; the player and a monster
// must be at least 5 tics apart
// TODO the rules in lynx might be slightly different?
if (actor.type.is_monster && goal_cell === this.player.previous_cell &&
// Player has decided to leave their cell, but hasn't actually taken a step yet
this.player.movement_cooldown === this.player.movement_speed &&
// See the extensive comment in attempt_out_of_turn_step
this.player.cooldown_delay_hack !== 2)
{
this.fail(actor.type.name);
}
if (actor === this.player && goal_cell[0].type.name === 'floor') {
this.sfx.play_once('step-floor');
}
@ -1240,6 +1221,22 @@ export class Level {
if (actor.ignores(tile.type.name))
continue;
// Possibly kill a player
if (actor.has_item('helmet') || tile.has_item('helmet')) {
// Helmet disables this, do nothing
}
else if (actor.type.is_real_player && tile.type.is_monster) {
this.fail(tile.type.name);
}
else if (actor.type.is_monster && tile.type.is_real_player) {
this.fail(actor.type.name);
}
else if (actor.type.is_block && tile.type.is_real_player) {
// Note that blocks squish players if they move for ANY reason, even if pushed by
// another player!
this.fail('squished');
}
if (tile.type.on_approach) {
tile.type.on_approach(tile, this, actor);
}
@ -1248,6 +1245,21 @@ export class Level {
}
}
// If we're a monster stepping on the player's tail, that also kills her immediately; the
// player and a monster must be strictly more than 4 tics apart
// FIXME this only works for the /current/ player but presumably applies to all of them,
// though i'm having trouble coming up with a test
// TODO the rules in lynx might be slightly different?
if (actor.type.is_monster && goal_cell === this.player.previous_cell &&
// Player has decided to leave their cell, but hasn't actually taken a step yet
this.player.movement_cooldown === this.player.movement_speed &&
! actor.has_item('helmet') && ! this.player.has_item('helmet') &&
// See the extensive comment in attempt_out_of_turn_step
this.player.cooldown_delay_hack !== 2)
{
this.fail(actor.type.name);
}
if (this.compat.tiles_react_instantly) {
this.step_on_cell(actor, actor.cell);
}
@ -1294,7 +1306,7 @@ export class Level {
let teleporter = actor.just_stepped_on_teleporter;
actor.just_stepped_on_teleporter = null;
let push_mode = actor === this.player ? 'move' : 'trace';
let push_mode = actor === this.player ? 'push' : 'bump';
let original_direction = actor.direction;
let success = false;
let dest, direction;

View File

@ -27,7 +27,7 @@ function on_ready_force_floor(me, level) {
let neighbor = level.get_neighboring_cell(me.cell, actor.direction);
if (! neighbor)
return;
if (! neighbor.blocks_entering(actor, actor.direction, level, 'trace'))
if (neighbor.try_entering(actor, actor.direction, level))
return;
let item = me.cell.get_item();
if (! item)
@ -975,7 +975,7 @@ const TILE_TYPES = {
draw_layer: DRAW_LAYERS.item,
is_chip: true,
is_required_chip: true,
blocks_collision: COLLISION.block_cc1 | COLLISION.monster_solid,
blocks_collision: COLLISION.block_cc1 | (COLLISION.monster_solid & ~COLLISION.rover),
on_arrive(me, level, other) {
if (other.type.is_real_player) {
level.collect_chip();
@ -1991,34 +1991,14 @@ const TILE_TYPES = {
},
_emulatees: ['teeth', 'glider', 'bug', 'ball', 'teeth_timid', 'fireball', 'paramecium', 'walker'],
decide_movement(me, level) {
level._set_tile_prop(me, 'attempted_moves', me.attempted_moves + 1);
if (me.attempted_moves >= 32) {
level._set_tile_prop(me, 'attempted_moves', me.attempted_moves - 32); // XXX unsure if this is 0 or -32
level._set_tile_prop(me, 'attempted_moves', 0);
level._set_tile_prop(me, 'current_emulatee', (me.current_emulatee + 1) % me.type._emulatees.length);
}
let emulatee = me.type._emulatees[me.current_emulatee];
let preference = TILE_TYPES[emulatee].decide_movement(me, level);
// Rig up the preference so a failure counts as two moves
if (preference) {
level._set_tile_prop(me, 'attempted_moves', me.attempted_moves + 1);
let last_direction = preference[preference.length - 1];
if (typeof last_direction === 'function') {
// This is tricky! We want this function to only be called ONCE max, since the
// walker uses it to carefully tune the PRNG, so replace it with one that lazily
// overwrites 'last_direction'
preference.pop();
let lazy = last_direction;
preference.push(function() {
last_direction = lazy();
return last_direction;
});
}
preference.push(function() {
level._set_tile_prop(me, 'attempted_moves', me.attempted_moves + 1);
return last_direction;
});
}
return preference;
return TILE_TYPES[emulatee].decide_movement(me, level);
},
visual_state(me) {
if (me && me.current_emulatee !== undefined) {
@ -2226,6 +2206,7 @@ const TILE_TYPES = {
draw_layer: DRAW_LAYERS.actor,
is_actor: true,
has_inventory: true,
can_reveal_walls: true,
// FIXME ???????
collision_mask: COLLISION.block_cc2,
// FIXME do i start moving immediately when dropped, or next turn?
@ -2233,20 +2214,22 @@ const TILE_TYPES = {
decide_movement(me, level) {
return [me.direction];
},
// FIXME feel like this should be on_blocked?
// FIXME specific case for player!
on_bump(me, level, other) {
on_blocked(me, level, direction) {
if (me.slide_mode)
return;
if (other.type.is_actor) {
level.transmute_tile(me, 'explosion');
let cell = level.get_neighboring_cell(me.cell, direction);
if (cell) {
let other = cell.get_actor();
if (other) {
if (other.is_real_player) {
level.fail(me.type.name);
}
else {
level.transmute_tile(other, 'explosion');
return false;
}
else if (other.blocks(me, me.direction, level)) {
}
}
level.transmute_tile(me, 'explosion');
return false;
}
},
},
xray_eye: {
@ -2424,7 +2407,7 @@ const TILE_TYPES = {
draw_layer: DRAW_LAYERS.item,
is_chip: true,
is_required_chip: true,
blocks_collision: COLLISION.block_cc1 | COLLISION.monster_solid,
blocks_collision: COLLISION.block_cc1 | (COLLISION.monster_solid & ~COLLISION.rover),
on_arrive(me, level, other) {
if (other.type.is_real_player) {
level.collect_chip();
@ -2435,7 +2418,7 @@ const TILE_TYPES = {
chip_extra: {
draw_layer: DRAW_LAYERS.item,
is_chip: true,
blocks_collision: COLLISION.block_cc1 | COLLISION.monster_solid,
blocks_collision: COLLISION.block_cc1 | (COLLISION.monster_solid & ~COLLISION.rover),
on_arrive(me, level, other) {
if (other.type.is_real_player) {
level.collect_chip();