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 { 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 // 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 // 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 // item modifier (i.e. "no" sign) or a real player
// TODO would love to get this outta here // 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 && if (this.type.is_item &&
this.cell.some(tile => tile.type.item_modifier || tile.type.is_real_player)) this.cell.some(tile => tile.type.item_modifier || tile.type.is_real_player))
return false; return false;
@ -104,7 +102,7 @@ export class Tile {
direction = tile.cell.redirect_exit(tile, direction); direction = tile.cell.redirect_exit(tile, direction);
// Need to explicitly check this here, otherwise you could /attempt/ to push a block, // 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 // 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 // Inventory stuff
@ -195,57 +193,55 @@ export class Cell extends Array {
return this.some(tile => tile.name === name); return this.some(tile => tile.name === name);
} }
blocks_leaving(actor, direction) { try_leaving(actor, direction) {
for (let tile of this) { for (let tile of this) {
if (tile === actor) if (tile === actor)
continue; continue;
if (tile.type.traps && tile.type.traps(tile, actor)) 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)) if (tile.type.blocks_leaving && tile.type.blocks_leaving(tile, actor, direction))
return true;
}
return false; return false;
} }
return true;
}
// Check if this actor can move this direction into this cell. May have side effects, depending // Check if this actor can move this direction into this cell. Returns true on success. May
// on the value of push_mode: // have side effects, depending on the value of push_mode:
// - null: Default. Treat pushable objects as blocking. // - null: Default. Do not impact game state. Treat pushable objects as blocking.
// - 'ignore': Treat pushable objects as nonblocking. // - 'bump': Fire bump triggers. Don't move pushable objects, but do check whether they /could/
// - 'trace': Don't try to move pushable objects, but do check whether they could be pushed, // be pushed, recursively if necessary.
// recursively if necessary. // - 'push': Fire bump triggers. Attempt to move pushable objects out of the way immediately.
// - 'move': Attempt to move pushable objects out of the way immediately. try_entering(actor, direction, level, push_mode = null) {
blocks_entering(actor, direction, level, push_mode = null) {
let pushable_tiles = []; let pushable_tiles = [];
let blocked = false; 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)) if (! tile.blocks(actor, direction, level))
continue; continue;
if (push_mode === null) if (push_mode === null)
return true; return false;
if (! actor.can_push(tile, direction)) { if (! actor.can_push(tile, direction)) {
if (push_mode === 'move') { if (push_mode === 'push') {
// Track this instead of returning immediately, because 'move' mode also bumps // Track this instead of returning immediately, because 'push' mode also bumps
// every tile in the cell // every tile in the cell
blocked = true; blocked = true;
} }
else { else {
return true; return false;
}
}
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);
} }
} }
@ -254,27 +250,37 @@ export class Cell extends Array {
} }
if (blocked) if (blocked)
return true; return false;
// If we got this far, all that's left is to deal with pushables // If we got this far, all that's left is to deal with pushables
if (pushable_tiles.length > 0) { if (pushable_tiles.length > 0) {
let neighbor_cell = level.get_neighboring_cell(this, direction); let neighbor_cell = level.get_neighboring_cell(this, direction);
if (! neighbor_cell) if (! neighbor_cell)
return true; return false;
for (let tile of pushable_tiles) { for (let tile of pushable_tiles) {
if (push_mode === 'trace') { if (push_mode === 'bump') {
if (neighbor_cell.blocks_entering(tile, direction, level, push_mode)) // FIXME and leaving!
return true; 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 // Special railroad ability: change the direction we attempt to leave
@ -729,6 +735,10 @@ export class Level {
success = this.attempt_step(actor, actor.decision); 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 // Track whether the player is blocked, for visual effect
if (actor === this.player && actor.decision && ! success) { if (actor === this.player && actor.decision && ! success) {
this.sfx.play_once('blocked'); this.sfx.play_once('blocked');
@ -848,7 +858,7 @@ export class Level {
else if (actor.slide_mode === 'ice') { else if (actor.slide_mode === 'ice') {
// A sliding player that bonks into a wall still needs to turn around, but in this // A sliding player that bonks into a wall still needs to turn around, but in this
// case they do NOT start pushing blocks early // 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); this._handle_slide_bonk(actor);
} }
} }
@ -863,7 +873,7 @@ export class Level {
let open; let open;
if (dir2 === null) { if (dir2 === null) {
// Only one direction is held, but for consistency, "check" it anyway // Only one direction is held, but for consistency, "check" it anyway
open = try_direction(dir1, 'move'); open = try_direction(dir1, 'push');
actor.decision = dir1; actor.decision = dir1;
} }
else { else {
@ -871,8 +881,8 @@ export class Level {
// one, UNLESS it's blocked AND the other isn't // one, UNLESS it's blocked AND the other isn't
if (dir1 === actor.direction || dir2 === actor.direction) { if (dir1 === actor.direction || dir2 === actor.direction) {
let other_direction = dir1 === actor.direction ? dir2 : dir1; let other_direction = dir1 === actor.direction ? dir2 : dir1;
let curr_open = try_direction(actor.direction, 'move'); let curr_open = try_direction(actor.direction, 'push');
let other_open = try_direction(other_direction, 'move'); let other_open = try_direction(other_direction, 'push');
if (! curr_open && other_open) { if (! curr_open && other_open) {
actor.decision = other_direction; actor.decision = other_direction;
open = true; open = true;
@ -887,8 +897,8 @@ export class Level {
// FIXME i'm told cc2 prefers orthogonal actually, but need to check on that // 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 // FIXME lynx only checks horizontal, what about cc2? it must check both
// because of the behavior where pushing into a corner always pushes horizontal // because of the behavior where pushing into a corner always pushes horizontal
let open1 = try_direction(dir1, 'move'); let open1 = try_direction(dir1, 'push');
let open2 = try_direction(dir2, 'move'); let open2 = try_direction(dir2, 'push');
if (open1 && ! open2) { if (open1 && ! open2) {
actor.decision = dir1; actor.decision = dir1;
open = true; open = true;
@ -978,7 +988,7 @@ export class Level {
direction = actor.cell.redirect_exit(actor, direction); 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 // We found a good direction! Stop here
actor.decision = direction; actor.decision = direction;
all_blocked = false; all_blocked = false;
@ -1023,8 +1033,8 @@ export class Level {
check_movement(actor, orig_cell, direction, push_mode) { check_movement(actor, orig_cell, direction, push_mode) {
let dest_cell = this.get_neighboring_cell(orig_cell, direction); let dest_cell = this.get_neighboring_cell(orig_cell, direction);
return (dest_cell && return (dest_cell &&
! orig_cell.blocks_leaving(actor, direction) && orig_cell.try_leaving(actor, direction) &&
! dest_cell.blocks_entering(actor, direction, this, push_mode)); 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. // 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 move = DIRECTIONS[direction].movement;
let goal_cell = this.get_neighboring_cell(actor.cell, direction); 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 // Only bother touching the goal cell if we're not already trapped in this one
if (actor.cell.blocks_leaving(actor, direction)) { if (! actor.cell.try_leaving(actor, direction))
blocked = true; 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 // FIXME this can probably reuse try_entering now
// 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
// Try to move into the cell. This is usually a simple check of whether we can // 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, // a pushable object, we have to do two more passes: one to push anything pushable,
// then one to check whether we're blocked again. // then one to check whether we're blocked again.
/*
let blocked_by_pushable = false; let blocked_by_pushable = false;
for (let tile of goal_cell) { for (let tile of goal_cell) {
if (tile.blocks(actor, direction, this)) { if (tile.blocks(actor, direction, this)) {
@ -1115,18 +1126,9 @@ export class Level {
} }
// Now check if we're still blocked // Now check if we're still blocked
blocked = goal_cell.blocks_entering(actor, direction, this); blocked = goal_cell.try_entering(actor, direction, this);
}
}
}
else {
// Hit the edge
blocked = true;
}
if (blocked) {
return false;
} }
*/
// We're clear! // We're clear!
if (double_speed || actor.has_item('speed_boots')) { if (double_speed || actor.has_item('speed_boots')) {
@ -1200,33 +1202,12 @@ export class Level {
this.hint_shown = null; this.hint_shown = null;
} }
for (let tile of goal_cell) { for (let tile of goal_cell) {
if (actor.type.is_real_player && tile.type.is_monster) { // FIXME this could go in on_approach now
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');
}
if (actor === this.player && tile.type.is_hint) { if (actor === this.player && tile.type.is_hint) {
this.hint_shown = tile.hint_text ?? this.stored_level.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') { if (actor === this.player && goal_cell[0].type.name === 'floor') {
this.sfx.play_once('step-floor'); this.sfx.play_once('step-floor');
} }
@ -1240,6 +1221,22 @@ export class Level {
if (actor.ignores(tile.type.name)) if (actor.ignores(tile.type.name))
continue; 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) { if (tile.type.on_approach) {
tile.type.on_approach(tile, this, actor); 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) { if (this.compat.tiles_react_instantly) {
this.step_on_cell(actor, actor.cell); this.step_on_cell(actor, actor.cell);
} }
@ -1294,7 +1306,7 @@ export class Level {
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;
let push_mode = actor === this.player ? 'move' : 'trace'; let push_mode = actor === this.player ? 'push' : 'bump';
let original_direction = actor.direction; let original_direction = actor.direction;
let success = false; let success = false;
let dest, direction; 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); let neighbor = level.get_neighboring_cell(me.cell, actor.direction);
if (! neighbor) if (! neighbor)
return; return;
if (! neighbor.blocks_entering(actor, actor.direction, level, 'trace')) if (neighbor.try_entering(actor, actor.direction, level))
return; return;
let item = me.cell.get_item(); let item = me.cell.get_item();
if (! item) if (! item)
@ -975,7 +975,7 @@ const TILE_TYPES = {
draw_layer: DRAW_LAYERS.item, draw_layer: DRAW_LAYERS.item,
is_chip: true, is_chip: true,
is_required_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) { on_arrive(me, level, other) {
if (other.type.is_real_player) { if (other.type.is_real_player) {
level.collect_chip(); level.collect_chip();
@ -1991,34 +1991,14 @@ const TILE_TYPES = {
}, },
_emulatees: ['teeth', 'glider', 'bug', 'ball', 'teeth_timid', 'fireball', 'paramecium', 'walker'], _emulatees: ['teeth', 'glider', 'bug', 'ball', 'teeth_timid', 'fireball', 'paramecium', 'walker'],
decide_movement(me, level) { decide_movement(me, level) {
level._set_tile_prop(me, 'attempted_moves', me.attempted_moves + 1);
if (me.attempted_moves >= 32) { 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); level._set_tile_prop(me, 'current_emulatee', (me.current_emulatee + 1) % me.type._emulatees.length);
} }
let emulatee = me.type._emulatees[me.current_emulatee]; let emulatee = me.type._emulatees[me.current_emulatee];
let preference = TILE_TYPES[emulatee].decide_movement(me, level); return 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;
}, },
visual_state(me) { visual_state(me) {
if (me && me.current_emulatee !== undefined) { if (me && me.current_emulatee !== undefined) {
@ -2226,6 +2206,7 @@ const TILE_TYPES = {
draw_layer: DRAW_LAYERS.actor, draw_layer: DRAW_LAYERS.actor,
is_actor: true, is_actor: true,
has_inventory: true, has_inventory: true,
can_reveal_walls: true,
// FIXME ??????? // FIXME ???????
collision_mask: COLLISION.block_cc2, collision_mask: COLLISION.block_cc2,
// FIXME do i start moving immediately when dropped, or next turn? // FIXME do i start moving immediately when dropped, or next turn?
@ -2233,20 +2214,22 @@ const TILE_TYPES = {
decide_movement(me, level) { decide_movement(me, level) {
return [me.direction]; return [me.direction];
}, },
// FIXME feel like this should be on_blocked? on_blocked(me, level, direction) {
// FIXME specific case for player!
on_bump(me, level, other) {
if (me.slide_mode) if (me.slide_mode)
return; return;
if (other.type.is_actor) { let cell = level.get_neighboring_cell(me.cell, direction);
level.transmute_tile(me, 'explosion'); 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'); level.transmute_tile(other, 'explosion');
return false;
} }
else if (other.blocks(me, me.direction, level)) { }
}
level.transmute_tile(me, 'explosion'); level.transmute_tile(me, 'explosion');
return false;
}
}, },
}, },
xray_eye: { xray_eye: {
@ -2424,7 +2407,7 @@ const TILE_TYPES = {
draw_layer: DRAW_LAYERS.item, draw_layer: DRAW_LAYERS.item,
is_chip: true, is_chip: true,
is_required_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) { on_arrive(me, level, other) {
if (other.type.is_real_player) { if (other.type.is_real_player) {
level.collect_chip(); level.collect_chip();
@ -2435,7 +2418,7 @@ const TILE_TYPES = {
chip_extra: { chip_extra: {
draw_layer: DRAW_LAYERS.item, draw_layer: DRAW_LAYERS.item,
is_chip: true, 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) { on_arrive(me, level, other) {
if (other.type.is_real_player) { if (other.type.is_real_player) {
level.collect_chip(); level.collect_chip();