Split up the actor loop, so actors make decisions in a separate pass

This fixes a lot of subtle issues: creatures hitting you when you push a
block past them, blocks moving jerkily while you push them (not even
sure why on that one), probably implementation of "the stupid glitch"...
This commit is contained in:
Eevee (Evelyn Woods) 2020-09-10 12:39:18 -06:00
parent 549b34ad30
commit 1453f68de5
3 changed files with 133 additions and 60 deletions

View File

@ -56,6 +56,9 @@ export class Tile {
if (other.type.is_block && this.type.blocks_blocks)
return true;
if (this.type.blocks)
return this.type.blocks(this, other);
return false;
}
@ -78,10 +81,11 @@ export class Tile {
}
// Inventory stuff
give_item(name) {
this.inventory[name] = (this.inventory[name] ?? 0) + 1;
has_item(name) {
return this.inventory[name] ?? 0 > 0;
}
// TODO remove, not undoable
take_item(name, amount = null) {
if (this.inventory[name] && this.inventory[name] >= 1) {
if (amount == null && this.type.infinite_items && this.type.infinite_items[name]) {
@ -128,6 +132,26 @@ export class Cell extends Array {
tile.cell = null;
return index;
}
blocks_leaving(actor, direction) {
for (let tile of this) {
if (tile !== actor &&
! tile.type.is_swivel && tile.type.thin_walls &&
tile.type.thin_walls.has(direction))
{
return true;
}
}
return false;
}
blocks_entering(actor, direction) {
for (let tile of this) {
if (tile.blocks(actor, direction) && ! actor.ignores(tile.type.name))
return true;
}
return false;
}
}
export class Level {
@ -311,6 +335,7 @@ export class Level {
// XXX this entire turn order is rather different in ms rules
// FIXME OK, do a pass to make everyone decide their movement, and then actually do it. the question iiis, where does that fit in with animation
// First pass: tick cooldowns and animations; have actors arrive in their cells
for (let actor of this.actors) {
// Actors with no cell were destroyed
if (! actor.cell)
@ -336,12 +361,19 @@ export class Level {
actor.animation_speed = null;
if (! this.compat.tiles_react_instantly) {
this.step_on_cell(actor);
// May have been destroyed here, too!
}
}
}
}
// Second pass: actors decide their upcoming movement simultaneously
for (let actor of this.actors) {
// Note that this prop is only used internally within a single iteration of this loop,
// so it doesn't need to be undoable
actor.decision = null;
if (! actor.cell)
continue;
}
}
}
if (actor.movement_cooldown > 0)
continue;
@ -373,7 +405,7 @@ export class Level {
player_direction &&
actor.last_move_was_force)
{
direction_preference = [player_direction, actor.direction];
direction_preference = [player_direction];
this._set_prop(actor, 'last_move_was_force', false);
}
else {
@ -463,24 +495,61 @@ export class Level {
direction_preference = [['north', 'south', 'east', 'west'][Math.floor(Math.random() * 4)]];
}
if (! direction_preference)
// Check which of those directions we *can*, probably, move in
// TODO i think player on force floor will still have some issues here
if (direction_preference) {
// Players always move the way they want, even if blocked
if (actor.type.is_player) {
actor.decision = direction_preference[0];
continue;
}
for (let direction of direction_preference) {
let dest_cell = this.cell_with_offset(actor.cell, direction);
if (! dest_cell)
continue;
let moved = false;
for (let direction of direction_preference) {
this.set_actor_direction(actor, direction);
if (this.attempt_step(actor, direction)) {
moved = true;
if (! actor.cell.blocks_leaving(actor, direction) &&
! dest_cell.blocks_entering(actor, direction))
{
// We found a good direction! Stop here
actor.decision = direction;
break;
}
}
}
}
// Third pass: everyone actually moves
for (let actor of this.actors) {
if (! actor.cell)
continue;
if (! actor.decision)
continue;
this.set_actor_direction(actor, actor.decision);
this.attempt_step(actor, actor.decision);
// TODO do i need to do this more aggressively?
if (this.state === 'success' || this.state === 'failure')
break;
}
// Pass time
// Strip out any destroyed actors from the acting order
let p = 0;
for (let i = 0, l = this.actors.length; i < l; i++) {
let actor = this.actors[i];
if (actor.cell) {
if (p !== i) {
this.actors[p] = actor;
}
p++;
}
}
this.actors.length = p;
// Advance the clock
let tic_counter = this.tic_counter;
let time_remaining = this.time_remaining;
this.tic_counter++;
@ -502,19 +571,6 @@ export class Level {
});
}
// Strip out any destroyed actors from the acting order
let p = 0;
for (let i = 0, l = this.actors.length; i < l; i++) {
let actor = this.actors[i];
if (actor.cell) {
if (p !== i) {
this.actors[p] = actor;
}
p++;
}
}
this.actors.length = p;
// Commit the undo state at the end of each tic
this.commit();
}
@ -535,22 +591,15 @@ export class Level {
}
let move = DIRECTIONS[direction].movement;
let original_cell = actor.cell;
if (!original_cell) console.error(actor);
let goal_x = original_cell.x + move[0];
let goal_y = original_cell.y + move[1];
if (!actor.cell) console.error(actor);
let goal_cell = this.cell_with_offset(actor.cell, direction);
// TODO this could be a lot simpler if i could early-return! should ice bumping be
// somewhere else?
let blocked;
if (goal_x >= 0 && goal_x < this.width && goal_y >= 0 && goal_y < this.height) {
// Check for a thin wall in our current cell first
for (let tile of original_cell) {
if (tile !== actor &&
! tile.type.is_swivel && tile.type.thin_walls &&
tile.type.thin_walls.has(direction))
{
if (goal_cell) {
if (actor.cell.blocks_leaving(actor, direction)) {
blocked = true;
break;
}
}
// Only bother touching the goal cell if we're not already trapped
@ -561,7 +610,7 @@ export class Level {
// mid-iteration.)
// FIXME actually, this prevents flicking!
if (! blocked) {
let goal_cell = this.cells[goal_y][goal_x];
// This is similar to Cell.blocks_entering, but we have to do a little more work
// FIXME splashes should block you (e.g. pushing a block off a
// turtle) but currently do not because of this copy; we don't
// notice a new thing was added to the tile :(
@ -604,7 +653,7 @@ export class Level {
this.set_actor_direction(actor, DIRECTIONS[direction].opposite);
// Somewhat clumsy hack: step on the ice tile again, so if it's
// a corner, it'll turn us in the correct direction
for (let tile of original_cell) {
for (let tile of actor.cell) {
if (tile.type.slide_mode === 'ice' && tile.type.on_arrive) {
tile.type.on_arrive(tile, this, actor);
}
@ -614,7 +663,7 @@ export class Level {
}
// We're clear!
this.move_to(actor, goal_x, goal_y, speed);
this.move_to(actor, goal_cell, speed);
// Set movement cooldown since we just moved
this._set_prop(actor, 'movement_cooldown', speed);
@ -624,16 +673,15 @@ export class Level {
// Move the given actor to the given position and perform any appropriate
// tile interactions. Does NOT check for whether the move is actually
// legal; use attempt_step for that!
move_to(actor, x, y, speed) {
let original_cell = actor.cell;
if (x === original_cell.x && y === original_cell.y)
move_to(actor, goal_cell, speed) {
if (actor.cell === goal_cell)
return;
actor.previous_cell = actor.cell;
actor.animation_speed = speed;
actor.animation_progress = 0;
let goal_cell = this.cells[y][x];
let original_cell = actor.cell;
this.remove_tile(actor);
this.make_slide(actor, null);
this.add_tile(actor, goal_cell);
@ -731,6 +779,18 @@ export class Level {
}
}
cell_with_offset(cell, direction) {
let move = DIRECTIONS[direction].movement;
let goal_x = cell.x + move[0];
let goal_y = cell.y + move[1];
if (goal_x >= 0 && goal_x < this.width && goal_y >= 0 && goal_y < this.height) {
return this.cells[goal_y][goal_x];
}
else {
return null;
}
}
// -------------------------------------------------------------------------
// Undo handling

View File

@ -241,7 +241,7 @@ class Player extends PrimaryView {
return;
let [x, y] = this.renderer.cell_coords_from_event(ev);
this.level.move_to(this.level.player, x, y);
this.level.move_to(this.level.player, this.level.cells[y][x], 1);
});
let last_key;

View File

@ -169,8 +169,11 @@ const TILE_TYPES = {
// Locked doors
door_red: {
draw_layer: LAYER_TERRAIN,
blocks_all: true,
on_bump(me, level, other) {
blocks(me, other) {
// TODO not quite sure if this one is right; there are complex interactions with monsters, e.g. most monsters can eat blue keys but can't actually use them
return ! (other.type.has_inventory && other.has_item('key_red'));
},
on_arrive(me, level, other) {
if (other.type.has_inventory && other.take_item('key_red')) {
level.transmute_tile(me, 'floor');
}
@ -178,8 +181,10 @@ const TILE_TYPES = {
},
door_blue: {
draw_layer: LAYER_TERRAIN,
blocks_all: true,
on_bump(me, level, other) {
blocks(me, other) {
return ! (other.type.has_inventory && other.has_item('key_blue'));
},
on_arrive(me, level, other) {
if (other.type.has_inventory && other.take_item('key_blue')) {
level.transmute_tile(me, 'floor');
}
@ -187,8 +192,10 @@ const TILE_TYPES = {
},
door_yellow: {
draw_layer: LAYER_TERRAIN,
blocks_all: true,
on_bump(me, level, other) {
blocks(me, other) {
return ! (other.type.has_inventory && other.has_item('key_yellow'));
},
on_arrive(me, level, other) {
if (other.type.has_inventory && other.take_item('key_yellow')) {
level.transmute_tile(me, 'floor');
}
@ -196,8 +203,10 @@ const TILE_TYPES = {
},
door_green: {
draw_layer: LAYER_TERRAIN,
blocks_all: true,
on_bump(me, level, other) {
blocks(me, other) {
return ! (other.type.has_inventory && other.has_item('key_green'));
},
on_arrive(me, level, other) {
if (other.type.has_inventory && other.take_item('key_green')) {
level.transmute_tile(me, 'floor');
}
@ -795,8 +804,12 @@ const TILE_TYPES = {
},
socket: {
draw_layer: LAYER_TERRAIN,
blocks_all: true,
on_bump(me, level, other) {
blocks_monsters: true,
blocks_blocks: true,
blocks(me, other) {
return (level.chips_remaining > 0);
},
on_arrive(me, level, other) {
if (other.type.is_player && level.chips_remaining === 0) {
level.transmute_tile(me, 'floor');
}