Implement (PARTIAL) undo; remove 'doomed'; fix a few small tile bugs
This commit is contained in:
parent
1cc631c27e
commit
0ba5ecc7e3
266
js/main.js
266
js/main.js
@ -69,11 +69,10 @@ async function fetch(url) {
|
|||||||
const PAGE_TITLE = "Lexy's Labyrinth";
|
const PAGE_TITLE = "Lexy's Labyrinth";
|
||||||
|
|
||||||
class Tile {
|
class Tile {
|
||||||
constructor(type, x, y, direction = 'south') {
|
constructor(type, direction = 'south') {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.direction = direction;
|
this.direction = direction;
|
||||||
|
this.cell = null;
|
||||||
|
|
||||||
this.slide_mode = null;
|
this.slide_mode = null;
|
||||||
this.movement_cooldown = 0;
|
this.movement_cooldown = 0;
|
||||||
@ -83,10 +82,10 @@ class Tile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static from_template(tile_template, x, y) {
|
static from_template(tile_template) {
|
||||||
let type = TILE_TYPES[tile_template.name];
|
let type = TILE_TYPES[tile_template.name];
|
||||||
if (! type) console.error(tile_template.name);
|
if (! type) console.error(tile_template.name);
|
||||||
let tile = new this(type, x, y, tile_template.direction);
|
let tile = new this(type, tile_template.direction);
|
||||||
if (type.load) {
|
if (type.load) {
|
||||||
type.load(tile, tile_template);
|
type.load(tile, tile_template);
|
||||||
}
|
}
|
||||||
@ -129,15 +128,6 @@ class Tile {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
become(name) {
|
|
||||||
this.type = TILE_TYPES[name];
|
|
||||||
// TODO adjust anything else?
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.doomed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inventory stuff
|
// Inventory stuff
|
||||||
give_item(name) {
|
give_item(name) {
|
||||||
this.inventory[name] = (this.inventory[name] ?? 0) + 1;
|
this.inventory[name] = (this.inventory[name] ?? 0) + 1;
|
||||||
@ -162,14 +152,21 @@ class Tile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Cell extends Array {
|
class Cell extends Array {
|
||||||
constructor() {
|
constructor(x, y) {
|
||||||
super();
|
super();
|
||||||
this.is_dirty = false;
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
}
|
}
|
||||||
|
|
||||||
_add(tile) {
|
_add(tile, index = null) {
|
||||||
|
if (index === null) {
|
||||||
this.push(tile);
|
this.push(tile);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
this.splice(index, 0, tile);
|
||||||
|
}
|
||||||
|
tile.cell = this;
|
||||||
|
}
|
||||||
|
|
||||||
// DO NOT use me to remove a tile permanently, only to move it!
|
// DO NOT use me to remove a tile permanently, only to move it!
|
||||||
// Should only be called from Level, which handles some bookkeeping!
|
// Should only be called from Level, which handles some bookkeeping!
|
||||||
@ -179,14 +176,15 @@ class Cell extends Array {
|
|||||||
throw new Error("Asked to remove tile that doesn't seem to exist");
|
throw new Error("Asked to remove tile that doesn't seem to exist");
|
||||||
|
|
||||||
this.splice(layer, 1);
|
this.splice(layer, 1);
|
||||||
|
return layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
each(f) {
|
each(f) {
|
||||||
for (let i = this.length - 1; i >= 0; i--) {
|
let copy = Array.from(this);
|
||||||
if (f(this[i]) === false)
|
for (let i = 0, l = copy.length; i < l; i++) {
|
||||||
|
if (f(copy[i]) === false)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this._gc();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_gc() {
|
_gc() {
|
||||||
@ -237,13 +235,16 @@ class Level {
|
|||||||
// TODO in lynx/steam, this carries over between levels; in tile world, you can set it manually
|
// TODO in lynx/steam, this carries over between levels; in tile world, you can set it manually
|
||||||
this.force_floor_direction = 'north';
|
this.force_floor_direction = 'north';
|
||||||
|
|
||||||
|
this.undo_stack = [];
|
||||||
|
this.pending_undo = [];
|
||||||
|
|
||||||
let n = 0;
|
let n = 0;
|
||||||
let connectables = [];
|
let connectables = [];
|
||||||
for (let y = 0; y < this.height; y++) {
|
for (let y = 0; y < this.height; y++) {
|
||||||
let row = [];
|
let row = [];
|
||||||
this.cells.push(row);
|
this.cells.push(row);
|
||||||
for (let x = 0; x < this.width; x++) {
|
for (let x = 0; x < this.width; x++) {
|
||||||
let cell = new Cell;
|
let cell = new Cell(x, y);
|
||||||
row.push(cell);
|
row.push(cell);
|
||||||
|
|
||||||
let stored_cell = this.stored_level.linear_cells[n];
|
let stored_cell = this.stored_level.linear_cells[n];
|
||||||
@ -251,7 +252,7 @@ class Level {
|
|||||||
let has_cloner, has_forbidden;
|
let has_cloner, has_forbidden;
|
||||||
|
|
||||||
for (let template_tile of stored_cell) {
|
for (let template_tile of stored_cell) {
|
||||||
let tile = Tile.from_template(template_tile, x, y);
|
let tile = Tile.from_template(template_tile);
|
||||||
if (tile.type.is_hint) {
|
if (tile.type.is_hint) {
|
||||||
// Copy over the tile-specific hint, if any
|
// Copy over the tile-specific hint, if any
|
||||||
tile.specific_hint = template_tile.specific_hint ?? null;
|
tile.specific_hint = template_tile.specific_hint ?? null;
|
||||||
@ -278,7 +279,7 @@ class Level {
|
|||||||
this.actors.push(tile);
|
this.actors.push(tile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cell.push(tile);
|
cell._add(tile);
|
||||||
|
|
||||||
if (tile.type.connects_to) {
|
if (tile.type.connects_to) {
|
||||||
connectables.push(tile);
|
connectables.push(tile);
|
||||||
@ -289,17 +290,15 @@ class Level {
|
|||||||
|
|
||||||
// Connect buttons and teleporters
|
// Connect buttons and teleporters
|
||||||
let num_cells = this.width * this.height;
|
let num_cells = this.width * this.height;
|
||||||
console.log(this.stored_level.custom_trap_wiring);
|
|
||||||
console.log(this.stored_level.custom_cloner_wiring);
|
|
||||||
for (let connectable of connectables) {
|
for (let connectable of connectables) {
|
||||||
let x = connectable.x;
|
let cell = connectable.cell;
|
||||||
let y = connectable.y;
|
let x = cell.x;
|
||||||
|
let y = cell.y;
|
||||||
let goal = connectable.type.connects_to;
|
let goal = connectable.type.connects_to;
|
||||||
let found = false;
|
let found = false;
|
||||||
|
|
||||||
// Check for custom wiring, for MSCC .DAT levels
|
// Check for custom wiring, for MSCC .DAT levels
|
||||||
let n = x + y * this.width;
|
let n = x + y * this.width;
|
||||||
console.log(x, y, n);
|
|
||||||
let target_cell_n = null;
|
let target_cell_n = null;
|
||||||
if (goal === 'trap') {
|
if (goal === 'trap') {
|
||||||
target_cell_n = this.stored_level.custom_trap_wiring[n] ?? null;
|
target_cell_n = this.stored_level.custom_trap_wiring[n] ?? null;
|
||||||
@ -367,7 +366,7 @@ class Level {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (actor.movement_cooldown > 0) {
|
if (actor.movement_cooldown > 0) {
|
||||||
actor.movement_cooldown -= 1;
|
this._set_prop(actor, 'movement_cooldown', actor.movement_cooldown - 1);
|
||||||
if (actor.movement_cooldown > 0)
|
if (actor.movement_cooldown > 0)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -394,19 +393,19 @@ class Level {
|
|||||||
actor.last_move_was_force)
|
actor.last_move_was_force)
|
||||||
{
|
{
|
||||||
direction_preference = [player_direction];
|
direction_preference = [player_direction];
|
||||||
actor.last_move_was_force = false;
|
this._set_prop(actor, 'last_move_was_force', false);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
direction_preference = [actor.direction];
|
direction_preference = [actor.direction];
|
||||||
if (actor === this.player) {
|
if (actor === this.player) {
|
||||||
actor.last_move_was_force = true;
|
this._set_prop(actor, 'last_move_was_force', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (actor === this.player) {
|
else if (actor === this.player) {
|
||||||
if (player_direction) {
|
if (player_direction) {
|
||||||
direction_preference = [player_direction];
|
direction_preference = [player_direction];
|
||||||
actor.last_move_was_force = false;
|
this._set_prop(actor, 'last_move_was_force', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (actor.type.movement_mode === 'forward') {
|
else if (actor.type.movement_mode === 'forward') {
|
||||||
@ -452,8 +451,8 @@ class Level {
|
|||||||
}
|
}
|
||||||
else if (actor.type.movement_mode === 'pursue') {
|
else if (actor.type.movement_mode === 'pursue') {
|
||||||
// teeth behavior: always move towards the player
|
// teeth behavior: always move towards the player
|
||||||
let dx = actor.x - this.player.x;
|
let dx = actor.cell.x - this.player.cell.x;
|
||||||
let dy = actor.y - this.player.y;
|
let dy = actor.cell.y - this.player.cell.y;
|
||||||
// Chooses the furthest direction, vertical wins ties
|
// Chooses the furthest direction, vertical wins ties
|
||||||
if (Math.abs(dx) > Math.abs(dy)) {
|
if (Math.abs(dx) > Math.abs(dy)) {
|
||||||
// Horizontal
|
// Horizontal
|
||||||
@ -485,7 +484,7 @@ class Level {
|
|||||||
|
|
||||||
let moved = false;
|
let moved = false;
|
||||||
for (let direction of direction_preference) {
|
for (let direction of direction_preference) {
|
||||||
actor.direction = direction;
|
this.set_actor_direction(actor, direction);
|
||||||
if (this.attempt_step(actor, direction)) {
|
if (this.attempt_step(actor, direction)) {
|
||||||
moved = true;
|
moved = true;
|
||||||
break;
|
break;
|
||||||
@ -499,7 +498,7 @@ class Level {
|
|||||||
if (actor.slide_mode !== null) {
|
if (actor.slide_mode !== null) {
|
||||||
speed_multiplier = 2;
|
speed_multiplier = 2;
|
||||||
}
|
}
|
||||||
actor.movement_cooldown = actor.type.movement_speed / speed_multiplier;
|
this._set_prop(actor, 'movement_cooldown', actor.type.movement_speed / speed_multiplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO do i need to do this more aggressively?
|
// TODO do i need to do this more aggressively?
|
||||||
@ -508,6 +507,13 @@ class Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.time_remaining !== null) {
|
if (this.time_remaining !== null) {
|
||||||
|
let tic_counter = this.tic_counter;
|
||||||
|
let time_remaining = this.time_remaining;
|
||||||
|
this.pending_undo.push(() => {
|
||||||
|
this.tic_counter = tic_counter;
|
||||||
|
this.time_remaining = time_remaining;
|
||||||
|
});
|
||||||
|
|
||||||
this.tic_counter++;
|
this.tic_counter++;
|
||||||
while (this.tic_counter > 20) {
|
while (this.tic_counter > 20) {
|
||||||
this.tic_counter -= 20;
|
this.tic_counter -= 20;
|
||||||
@ -517,28 +523,22 @@ class Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fail(message) {
|
// Commit the undo state at the end of each tic
|
||||||
this.state = 'failure';
|
this.commit();
|
||||||
this.fail_message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
win() {
|
|
||||||
this.state = 'success';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to move the given actor one tile in the given direction and update
|
// Try to move the given actor one tile in the given direction and update
|
||||||
// their cooldown. Return true if successful.
|
// their cooldown. Return true if successful.
|
||||||
attempt_step(actor, direction) {
|
attempt_step(actor, direction) {
|
||||||
let move = DIRECTIONS[direction].movement;
|
let move = DIRECTIONS[direction].movement;
|
||||||
let goal_x = actor.x + move[0];
|
let original_cell = actor.cell;
|
||||||
let goal_y = actor.y + move[1];
|
let goal_x = original_cell.x + move[0];
|
||||||
|
let goal_y = original_cell.y + move[1];
|
||||||
|
|
||||||
let blocked;
|
let blocked;
|
||||||
if (goal_x >= 0 && goal_x < this.width && goal_y >= 0 && goal_y < this.height) {
|
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
|
// Check for a thin wall in our current cell first
|
||||||
let original_cell = this.cells[actor.y][actor.x];
|
|
||||||
original_cell.each(tile => {
|
original_cell.each(tile => {
|
||||||
if (tile !== actor && tile.type.thin_walls &&
|
if (tile !== actor && tile.type.thin_walls &&
|
||||||
tile.type.thin_walls.has(direction))
|
tile.type.thin_walls.has(direction))
|
||||||
@ -594,19 +594,14 @@ class Level {
|
|||||||
// tile interactions. Does NOT check for whether the move is actually
|
// tile interactions. Does NOT check for whether the move is actually
|
||||||
// legal; use attempt_step for that!
|
// legal; use attempt_step for that!
|
||||||
move_to(actor, x, y) {
|
move_to(actor, x, y) {
|
||||||
if (x === actor.x && y === actor.y)
|
let original_cell = actor.cell;
|
||||||
|
if (x === original_cell.x && y === original_cell.y)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let goal_cell = this.cells[y][x];
|
let goal_cell = this.cells[y][x];
|
||||||
let original_cell = this.cells[actor.y][actor.x];
|
this.remove_tile(actor);
|
||||||
original_cell._remove(actor);
|
|
||||||
actor.slide_mode = null;
|
actor.slide_mode = null;
|
||||||
goal_cell._add(actor);
|
this.add_tile(actor, goal_cell);
|
||||||
actor.x = x;
|
|
||||||
actor.y = y;
|
|
||||||
|
|
||||||
original_cell.is_dirty = true;
|
|
||||||
goal_cell.is_dirty = true;
|
|
||||||
|
|
||||||
// Announce we're leaving, for the handful of tiles that care about it
|
// Announce we're leaving, for the handful of tiles that care about it
|
||||||
original_cell.each(tile => {
|
original_cell.each(tile => {
|
||||||
@ -622,7 +617,7 @@ class Level {
|
|||||||
|
|
||||||
// Step on all the tiles in the new cell
|
// Step on all the tiles in the new cell
|
||||||
if (actor === this.player) {
|
if (actor === this.player) {
|
||||||
this.hint_shown = null;
|
this._set_prop(this, 'hint_shown', null);
|
||||||
}
|
}
|
||||||
let teleporter;
|
let teleporter;
|
||||||
goal_cell.each(tile => {
|
goal_cell.each(tile => {
|
||||||
@ -632,12 +627,11 @@ class Level {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (actor === this.player && tile.type.is_hint) {
|
if (actor === this.player && tile.type.is_hint) {
|
||||||
this.hint_shown = tile.specific_hint ?? this.stored_level.hint;
|
this._set_prop(this, 'hint_shown', tile.specific_hint ?? this.stored_level.hint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tile.type.is_item && actor.type.has_inventory) {
|
if (tile.type.is_item && this.give_actor(actor, tile.type.name)) {
|
||||||
actor.give_item(tile.type.name);
|
this.remove_tile(tile);
|
||||||
tile.destroy();
|
|
||||||
}
|
}
|
||||||
else if (tile.type.is_teleporter) {
|
else if (tile.type.is_teleporter) {
|
||||||
teleporter = tile;
|
teleporter = tile;
|
||||||
@ -663,12 +657,10 @@ class Level {
|
|||||||
// Physically move the actor to the new teleporter
|
// Physically move the actor to the new teleporter
|
||||||
// XXX is this right, compare with tile world? i overhear it's actually implemented as a slide?
|
// XXX is this right, compare with tile world? i overhear it's actually implemented as a slide?
|
||||||
// XXX will probably play badly with undo lol
|
// XXX will probably play badly with undo lol
|
||||||
let tele_cell = this.cells[goal.y][goal.x];
|
let tele_cell = goal.cell;
|
||||||
current_cell._remove(actor);
|
current_cell._remove(actor);
|
||||||
tele_cell._add(actor);
|
tele_cell._add(actor);
|
||||||
current_cell = tele_cell;
|
current_cell = tele_cell;
|
||||||
actor.x = goal.x;
|
|
||||||
actor.y = goal.y;
|
|
||||||
if (this.attempt_step(actor, actor.direction))
|
if (this.attempt_step(actor, actor.direction))
|
||||||
// Success, teleportation complete
|
// Success, teleportation complete
|
||||||
break;
|
break;
|
||||||
@ -683,17 +675,56 @@ class Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Undo handling
|
||||||
|
|
||||||
|
commit() {
|
||||||
|
this.undo_stack.push(this.pending_undo);
|
||||||
|
this.pending_undo = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
let entry = this.undo_stack.pop();
|
||||||
|
// Undo in reverse order! There's no redo, so it's okay to destroy this
|
||||||
|
entry.reverse();
|
||||||
|
for (let undo of entry) {
|
||||||
|
undo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Level alteration methods. EVERYTHING that changes the state of a level,
|
// Level alteration methods. EVERYTHING that changes the state of a level,
|
||||||
// including the state of a single tile, should do it through one of these
|
// including the state of a single tile, should do it through one of these
|
||||||
// for undo/rewind purposes
|
// for undo/rewind purposes
|
||||||
|
|
||||||
|
_set_prop(obj, key, val) {
|
||||||
|
let old_val = obj[key];
|
||||||
|
this.pending_undo.push(() => obj[key] = old_val);
|
||||||
|
obj[key] = val;
|
||||||
|
}
|
||||||
|
|
||||||
collect_chip() {
|
collect_chip() {
|
||||||
if (this.chips_remaining > 0) {
|
let current = this.chips_remaining;
|
||||||
|
if (current > 0) {
|
||||||
|
this.pending_undo.push(() => this.chips_remaining = current);
|
||||||
this.chips_remaining--;
|
this.chips_remaining--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fail(message) {
|
||||||
|
this.pending_undo.push(() => {
|
||||||
|
this.state = 'playing';
|
||||||
|
this.fail_message = null;
|
||||||
|
});
|
||||||
|
this.state = 'failure';
|
||||||
|
this.fail_message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
win() {
|
||||||
|
this.pending_undo.push(() => this.state = 'playing');
|
||||||
|
this.state = 'success';
|
||||||
|
}
|
||||||
|
|
||||||
// Get the next direction a random force floor will use. They share global
|
// Get the next direction a random force floor will use. They share global
|
||||||
// state and cycle clockwise.
|
// state and cycle clockwise.
|
||||||
get_force_floor_direction() {
|
get_force_floor_direction() {
|
||||||
@ -702,11 +733,55 @@ class Level {
|
|||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO make a set of primitives for actually altering the level that also
|
// Tile stuff in particular
|
||||||
// record how to undo themselves
|
|
||||||
|
remove_tile(tile) {
|
||||||
|
let cell = tile.cell;
|
||||||
|
let layer = cell._remove(tile);
|
||||||
|
this.pending_undo.push(() => cell._add(tile, layer));
|
||||||
|
}
|
||||||
|
|
||||||
|
add_tile(tile, cell, layer = null) {
|
||||||
|
cell._add(tile, layer);
|
||||||
|
this.pending_undo.push(() => cell._remove(tile));
|
||||||
|
}
|
||||||
|
|
||||||
|
transmute_tile(tile, name) {
|
||||||
|
let current = tile.type.name;
|
||||||
|
this.pending_undo.push(() => tile.type = TILE_TYPES[current]);
|
||||||
|
tile.type = TILE_TYPES[name];
|
||||||
|
// TODO adjust anything else?
|
||||||
|
}
|
||||||
|
|
||||||
|
give_actor(actor, name) {
|
||||||
|
if (! actor.type.has_inventory)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
let current = actor.inventory[name];
|
||||||
|
this.pending_undo.push(() => actor.inventory[name] = current);
|
||||||
|
actor.inventory[name] = (current ?? 0) + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark an actor as sliding
|
||||||
make_slide(actor, mode) {
|
make_slide(actor, mode) {
|
||||||
actor.slide_mode = mode;
|
actor.slide_mode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change an actor's direction
|
||||||
|
set_actor_direction(actor, direction) {
|
||||||
|
let current = actor.direction;
|
||||||
|
this.pending_undo.push(() => actor.direction = current);
|
||||||
|
actor.direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_actor_stuck(actor, is_stuck) {
|
||||||
|
let current = actor.stuck;
|
||||||
|
if (current === is_stuck)
|
||||||
|
return;
|
||||||
|
this.pending_undo.push(() => actor.stuck = current);
|
||||||
|
actor.stuck = is_stuck;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -915,8 +990,8 @@ const GAME_UI_HTML = `
|
|||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="control-pause" type="button">Pause</button>
|
<button class="control-pause" type="button">Pause</button>
|
||||||
<button class="control-restart" type="button">Restart</button>
|
<button class="control-restart" type="button">Restart</button>
|
||||||
<button class="control-undo" type="button" disabled>Undo</button>
|
<button class="control-undo" type="button">Undo</button>
|
||||||
<button class="control-rewind" type="button" disabled>Rewind</button>
|
<button class="control-rewind" type="button">Rewind</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo">
|
<div class="demo">
|
||||||
<h2>Solution demo available</h2>
|
<h2>Solution demo available</h2>
|
||||||
@ -966,6 +1041,7 @@ class Game {
|
|||||||
// TODO obey level options; allow overriding
|
// TODO obey level options; allow overriding
|
||||||
this.viewport_size_x = 9;
|
this.viewport_size_x = 9;
|
||||||
this.viewport_size_y = 9;
|
this.viewport_size_y = 9;
|
||||||
|
this.scale = 1;
|
||||||
|
|
||||||
document.body.innerHTML = GAME_UI_HTML;
|
document.body.innerHTML = GAME_UI_HTML;
|
||||||
this.container = document.body.querySelector('main');
|
this.container = document.body.querySelector('main');
|
||||||
@ -1026,6 +1102,23 @@ class Game {
|
|||||||
}).open();
|
}).open();
|
||||||
ev.target.blur();
|
ev.target.blur();
|
||||||
});
|
});
|
||||||
|
this.undo_button = this.container.querySelector('.controls .control-undo');
|
||||||
|
this.undo_button.addEventListener('click', ev => {
|
||||||
|
let player_cell = this.level.player.cell;
|
||||||
|
while (player_cell === this.level.player.cell && this.level.undo_stack.length > 0) {
|
||||||
|
this.level.undo();
|
||||||
|
}
|
||||||
|
if (this.level.undo_stack.length === 0) {
|
||||||
|
this.set_state('waiting');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Be sure to undo any success or failure
|
||||||
|
this.set_state('playing');
|
||||||
|
}
|
||||||
|
this.update_ui();
|
||||||
|
this.redraw();
|
||||||
|
ev.target.blur();
|
||||||
|
});
|
||||||
// Demo playback
|
// Demo playback
|
||||||
this.container.querySelector('.demo .demo-step-1').addEventListener('click', ev => {
|
this.container.querySelector('.demo .demo-step-1').addEventListener('click', ev => {
|
||||||
this.advance_by(1);
|
this.advance_by(1);
|
||||||
@ -1050,8 +1143,8 @@ class Game {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
let rect = this.level_canvas.getBoundingClientRect();
|
let rect = this.level_canvas.getBoundingClientRect();
|
||||||
let x = Math.floor((ev.clientX - rect.x) / 2 / this.tileset.size_x + this.viewport_x);
|
let x = Math.floor((ev.clientX - rect.x) / this.scale / this.tileset.size_x + this.viewport_x);
|
||||||
let y = Math.floor((ev.clientY - rect.y) / 2 / this.tileset.size_y + this.viewport_y);
|
let y = Math.floor((ev.clientY - rect.y) / this.scale / this.tileset.size_y + this.viewport_y);
|
||||||
this.level.move_to(this.level.player, x, y);
|
this.level.move_to(this.level.player, x, y);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1261,7 +1354,7 @@ class Game {
|
|||||||
if (! this._inventory_tiles[name]) {
|
if (! this._inventory_tiles[name]) {
|
||||||
// TODO reuse the canvas
|
// TODO reuse the canvas
|
||||||
let canvas = mk('canvas', {width: this.tileset.size_x, height: this.tileset.size_y});
|
let canvas = mk('canvas', {width: this.tileset.size_x, height: this.tileset.size_y});
|
||||||
this.tileset.draw({type: TILE_TYPES[name]}, canvas.getContext('2d'), 0, 0);
|
this.tileset.draw({type: TILE_TYPES[name]}, null, canvas.getContext('2d'), 0, 0);
|
||||||
this._inventory_tiles[name] = canvas.toDataURL();
|
this._inventory_tiles[name] = canvas.toDataURL();
|
||||||
}
|
}
|
||||||
return this._inventory_tiles[name];
|
return this._inventory_tiles[name];
|
||||||
@ -1348,29 +1441,34 @@ class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redraw() {
|
redraw() {
|
||||||
|
// TODO split this out to a renderer, call it every frame, have the level flag itself as dirty
|
||||||
let ctx = this.level_canvas.getContext('2d');
|
let ctx = this.level_canvas.getContext('2d');
|
||||||
ctx.clearRect(0, 0, this.level_canvas.width, this.level_canvas.height);
|
ctx.clearRect(0, 0, this.level_canvas.width, this.level_canvas.height);
|
||||||
|
|
||||||
let xmargin = (this.viewport_size_x - 1) / 2;
|
let xmargin = (this.viewport_size_x - 1) / 2;
|
||||||
let ymargin = (this.viewport_size_y - 1) / 2;
|
let ymargin = (this.viewport_size_y - 1) / 2;
|
||||||
let x0 = this.level.player.x - xmargin;
|
let player_cell = this.level.player.cell;
|
||||||
let y0 = this.level.player.y - ymargin;
|
let x0 = player_cell.x - xmargin;
|
||||||
|
let y0 = player_cell.y - ymargin;
|
||||||
x0 = Math.max(0, Math.min(this.level.width - this.viewport_size_x, x0));
|
x0 = Math.max(0, Math.min(this.level.width - this.viewport_size_x, x0));
|
||||||
y0 = Math.max(0, Math.min(this.level.height - this.viewport_size_y, y0));
|
y0 = Math.max(0, Math.min(this.level.height - this.viewport_size_y, y0));
|
||||||
this.viewport_x = x0;
|
this.viewport_x = x0;
|
||||||
this.viewport_y = y0;
|
this.viewport_y = y0;
|
||||||
|
// Draw in layers, so animated objects aren't overdrawn by neighboring terrain
|
||||||
|
let any_drawn = true;
|
||||||
|
let i = -1;
|
||||||
|
while (any_drawn) {
|
||||||
|
i++;
|
||||||
|
any_drawn = false;
|
||||||
for (let dx = 0; dx < this.viewport_size_x; dx++) {
|
for (let dx = 0; dx < this.viewport_size_x; dx++) {
|
||||||
for (let dy = 0; dy < this.viewport_size_y; dy++) {
|
for (let dy = 0; dy < this.viewport_size_y; dy++) {
|
||||||
let cell = this.level.cells[dy + y0][dx + x0];
|
let cell = this.level.cells[dy + y0][dx + x0];
|
||||||
/*
|
let tile = cell[i];
|
||||||
if (! cell.is_dirty)
|
if (tile) {
|
||||||
continue;
|
any_drawn = true;
|
||||||
*/
|
|
||||||
cell.is_dirty = false;
|
|
||||||
|
|
||||||
for (let tile of cell) {
|
|
||||||
if (! tile.doomed) {
|
if (! tile.doomed) {
|
||||||
this.tileset.draw(tile, ctx, dx, dy);
|
this.tileset.draw(tile, this.level, ctx, dx, dy);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1399,7 +1497,9 @@ class Game {
|
|||||||
if (scale <= 0) {
|
if (scale <= 0) {
|
||||||
scale = 1;
|
scale = 1;
|
||||||
}
|
}
|
||||||
// FIXME this doesn't take into account the inventory, which is also affected by scale
|
|
||||||
|
// FIXME the above logic doesn't take into account the inventory, which is also affected by scale
|
||||||
|
this.scale = scale;
|
||||||
this.container.style.setProperty('--scale', scale);
|
this.container.style.setProperty('--scale', scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { DIRECTIONS } from './defs.js';
|
||||||
|
|
||||||
// TODO really need to specify this format more concretely, whoof
|
// TODO really need to specify this format more concretely, whoof
|
||||||
// XXX special kinds of drawing i know this has for a fact:
|
// XXX special kinds of drawing i know this has for a fact:
|
||||||
// - letter tiles draw from a block of half-tiles onto the center of the base
|
// - letter tiles draw from a block of half-tiles onto the center of the base
|
||||||
@ -129,8 +131,10 @@ export const CC2_TILESET_LAYOUT = {
|
|||||||
base: 'purple_floor',
|
base: 'purple_floor',
|
||||||
overlay: [8, 9],
|
overlay: [8, 9],
|
||||||
},
|
},
|
||||||
// TODO state (10 is closed)
|
trap: {
|
||||||
trap: [9, 9],
|
closed: [9, 9],
|
||||||
|
open: [10, 9],
|
||||||
|
},
|
||||||
button_gray: [11, 9],
|
button_gray: [11, 9],
|
||||||
fireball: [[12, 9], [13, 9], [14, 9], [15, 9]],
|
fireball: [[12, 9], [13, 9], [14, 9], [15, 9]],
|
||||||
|
|
||||||
@ -427,27 +431,35 @@ export class Tileset {
|
|||||||
dx * this.size_x, dy * this.size_y, w, h);
|
dx * this.size_x, dy * this.size_y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(tile, ctx, x, y) {
|
draw(tile, level, ctx, x, y) {
|
||||||
this.draw_type(tile.type.name, tile, ctx, x, y);
|
this.draw_type(tile.type.name, tile, level, ctx, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draws a tile type, given by name. Passing in a tile is optional, but
|
// Draws a tile type, given by name. Passing in a tile is optional, but
|
||||||
// without it you'll get defaults.
|
// without it you'll get defaults.
|
||||||
draw_type(name, tile, ctx, x, y) {
|
draw_type(name, tile, level, ctx, x, y) {
|
||||||
let drawspec = this.layout[name];
|
let drawspec = this.layout[name];
|
||||||
if (! drawspec) {
|
if (! drawspec) {
|
||||||
console.error(`Don't know how to draw tile type ${type.name}!`);
|
console.error(`Don't know how to draw tile type ${type.name}!`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (tile && tile.movement_cooldown) {
|
||||||
|
let offset = DIRECTIONS[tile.direction].movement;
|
||||||
|
x -= tile.movement_cooldown / tile.type.movement_speed * offset[0];
|
||||||
|
y -= tile.movement_cooldown / tile.type.movement_speed * offset[1];
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if (drawspec.overlay) {
|
if (drawspec.overlay) {
|
||||||
// Goofy overlay thing used for green/purple toggle tiles and
|
// Goofy overlay thing used for green/purple toggle tiles and
|
||||||
// southeast thin walls. Draw the base (a type name), then draw
|
// southeast thin walls. Draw the base (a type name), then draw
|
||||||
// the overlay (either a type name or a regular draw spec).
|
// the overlay (either a type name or a regular draw spec).
|
||||||
// TODO chance of infinite recursion here
|
// TODO chance of infinite recursion here
|
||||||
this.draw_type(drawspec.base, tile, ctx, x, y);
|
this.draw_type(drawspec.base, tile, level, ctx, x, y);
|
||||||
if (typeof drawspec.overlay === 'string') {
|
if (typeof drawspec.overlay === 'string') {
|
||||||
this.draw_type(drawspec.overlay, tile, ctx, x, y);
|
this.draw_type(drawspec.overlay, tile, level, ctx, x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -464,12 +476,27 @@ export class Tileset {
|
|||||||
|
|
||||||
// Unwrap animations etc.
|
// Unwrap animations etc.
|
||||||
if (!(coords instanceof Array)) {
|
if (!(coords instanceof Array)) {
|
||||||
// Must be an object of directions
|
// Must be an object of either tile-specific state, or directions
|
||||||
|
if (name === 'trap') {
|
||||||
|
if (tile && tile.open) {
|
||||||
|
coords = coords.open;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
coords = coords.closed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
coords = coords[(tile && tile.direction) ?? 'south'];
|
coords = coords[(tile && tile.direction) ?? 'south'];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (coords[0] instanceof Array) {
|
if (coords[0] instanceof Array) {
|
||||||
|
if (level) {
|
||||||
|
coords = coords[Math.floor(level.tic_counter % 5 / 5 * coords.length)];
|
||||||
|
}
|
||||||
|
else {
|
||||||
coords = coords[0];
|
coords = coords[0];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (drawspec.mask) {
|
if (drawspec.mask) {
|
||||||
// Continue on with masking
|
// Continue on with masking
|
||||||
|
|||||||
@ -19,12 +19,14 @@ const TILE_TYPES = {
|
|||||||
wall_appearing: {
|
wall_appearing: {
|
||||||
blocks: true,
|
blocks: true,
|
||||||
on_bump(me, level, other) {
|
on_bump(me, level, other) {
|
||||||
me.become('wall');
|
level.transmute_tile(me, 'wall');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
popwall: {
|
popwall: {
|
||||||
|
blocks_monsters: true,
|
||||||
|
blocks_blocks: true,
|
||||||
on_depart(me, level, other) {
|
on_depart(me, level, other) {
|
||||||
me.become('wall');
|
level.transmute_tile(me, 'wall');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
thinwall_n: {
|
thinwall_n: {
|
||||||
@ -45,13 +47,13 @@ const TILE_TYPES = {
|
|||||||
fake_wall: {
|
fake_wall: {
|
||||||
blocks: true,
|
blocks: true,
|
||||||
on_bump(me, level, other) {
|
on_bump(me, level, other) {
|
||||||
me.become('wall');
|
level.transmute_tile(me, 'wall');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fake_floor: {
|
fake_floor: {
|
||||||
blocks: true,
|
blocks: true,
|
||||||
on_bump(me, level, other) {
|
on_bump(me, level, other) {
|
||||||
me.become('floor');
|
level.transmute_tile(me, 'floor');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -110,7 +112,7 @@ const TILE_TYPES = {
|
|||||||
blocks_blocks: true,
|
blocks_blocks: true,
|
||||||
// TODO block melinda only without the hiking boots; can't use ignore because then she wouldn't step on it :S also ignore doesn't apply to blocks anyway.
|
// TODO block melinda only without the hiking boots; can't use ignore because then she wouldn't step on it :S also ignore doesn't apply to blocks anyway.
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
me.become('floor');
|
level.transmute_tile(me, 'floor');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
gravel: {
|
gravel: {
|
||||||
@ -122,10 +124,10 @@ const TILE_TYPES = {
|
|||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
if (other.type.is_player) {
|
if (other.type.is_player) {
|
||||||
level.fail("Oops! You can't walk on fire without fire boots!");
|
level.fail("Oops! You can't walk on fire without fire boots!");
|
||||||
other.become('player_burned');
|
level.transmute_tile(other, 'player_burned');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
other.destroy();
|
level.remove_tile(other);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -133,15 +135,15 @@ const TILE_TYPES = {
|
|||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
// TODO cc1 allows items under water, i think; water was on the upper layer
|
// TODO cc1 allows items under water, i think; water was on the upper layer
|
||||||
if (other.type.name == 'dirt_block' || other.type.name == 'clone_block') {
|
if (other.type.name == 'dirt_block' || other.type.name == 'clone_block') {
|
||||||
other.destroy();
|
level.remove_tile(other);
|
||||||
me.become('dirt');
|
level.transmute_tile(me, 'dirt');
|
||||||
}
|
}
|
||||||
else if (other.type.is_player) {
|
else if (other.type.is_player) {
|
||||||
level.fail("Oops! You can't swim without flippers!");
|
level.fail("swimming with the fishes");
|
||||||
other.become('player_drowned');
|
level.transmute_tile(other, 'player_drowned');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
other.destroy();
|
level.remove_tile(other);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -234,8 +236,11 @@ const TILE_TYPES = {
|
|||||||
bomb: {
|
bomb: {
|
||||||
// TODO explode
|
// TODO explode
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
me.destroy();
|
level.remove_tile(me);
|
||||||
other.destroy();
|
level.remove_tile(other);
|
||||||
|
if (other.type.is_player) {
|
||||||
|
level.fail("watch where you step");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
thief_tools: {
|
thief_tools: {
|
||||||
@ -267,6 +272,7 @@ const TILE_TYPES = {
|
|||||||
dirt_block: {
|
dirt_block: {
|
||||||
blocks: true,
|
blocks: true,
|
||||||
is_object: true,
|
is_object: true,
|
||||||
|
is_actor: true,
|
||||||
is_block: true,
|
is_block: true,
|
||||||
ignores: new Set(['fire']),
|
ignores: new Set(['fire']),
|
||||||
},
|
},
|
||||||
@ -274,6 +280,7 @@ const TILE_TYPES = {
|
|||||||
// TODO is this in any way distinct from dirt block
|
// TODO is this in any way distinct from dirt block
|
||||||
blocks: true,
|
blocks: true,
|
||||||
is_object: true,
|
is_object: true,
|
||||||
|
is_actor: true,
|
||||||
is_block: true,
|
is_block: true,
|
||||||
ignores: new Set(['fire']),
|
ignores: new Set(['fire']),
|
||||||
},
|
},
|
||||||
@ -284,10 +291,9 @@ const TILE_TYPES = {
|
|||||||
cloner: {
|
cloner: {
|
||||||
blocks: true,
|
blocks: true,
|
||||||
activate(me, level) {
|
activate(me, level) {
|
||||||
let cell = level.cells[me.y][me.x];
|
let cell = me.cell;
|
||||||
// Clone so we don't end up repeatedly cloning the same object
|
// Copy, so we don't end up repeatedly cloning the same object
|
||||||
let current_tiles = Array.from(cell);
|
for (let tile of Array.from(cell)) {
|
||||||
for (let tile of current_tiles) {
|
|
||||||
if (tile !== me && tile.type.is_actor) {
|
if (tile !== me && tile.type.is_actor) {
|
||||||
// Copy this stuff in case the movement changes it
|
// Copy this stuff in case the movement changes it
|
||||||
let type = tile.type;
|
let type = tile.type;
|
||||||
@ -295,16 +301,14 @@ const TILE_TYPES = {
|
|||||||
|
|
||||||
// Unstick and try to move the actor; if it's blocked,
|
// Unstick and try to move the actor; if it's blocked,
|
||||||
// abort the clone
|
// abort the clone
|
||||||
tile.stuck = false;
|
level.set_actor_stuck(tile, false);
|
||||||
if (level.attempt_step(tile, direction)) {
|
if (level.attempt_step(tile, direction)) {
|
||||||
level.actors.push(tile);
|
level.actors.push(tile);
|
||||||
// FIXME rearrange to make this possible lol
|
|
||||||
// FIXME go through level for this, and everything else of course
|
|
||||||
// FIXME add this underneath, just above the cloner
|
// FIXME add this underneath, just above the cloner
|
||||||
cell._add(new tile.constructor(type, me.x, me.y, direction));
|
level.add_tile(new tile.constructor(type, direction), cell);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
tile.stuck = true;
|
level.set_actor_stuck(tile, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -313,7 +317,7 @@ const TILE_TYPES = {
|
|||||||
trap: {
|
trap: {
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
if (! me.open) {
|
if (! me.open) {
|
||||||
other.stuck = true;
|
level.set_actor_stuck(other, true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -331,7 +335,7 @@ const TILE_TYPES = {
|
|||||||
for (let actor of level.actors) {
|
for (let actor of level.actors) {
|
||||||
// TODO generify somehow??
|
// TODO generify somehow??
|
||||||
if (actor.type.name === 'tank_blue') {
|
if (actor.type.name === 'tank_blue') {
|
||||||
actor.direction = DIRECTIONS[actor.direction].opposite;
|
level.set_actor_direction(actor, DIRECTIONS[actor.direction].opposite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -339,20 +343,21 @@ const TILE_TYPES = {
|
|||||||
button_green: {
|
button_green: {
|
||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
// Swap green floors and walls
|
// Swap green floors and walls
|
||||||
|
// TODO could probably make this more compact for undo purposes
|
||||||
for (let row of level.cells) {
|
for (let row of level.cells) {
|
||||||
for (let cell of row) {
|
for (let cell of row) {
|
||||||
for (let tile of cell) {
|
for (let tile of cell) {
|
||||||
if (tile.type.name === 'green_floor') {
|
if (tile.type.name === 'green_floor') {
|
||||||
tile.become('green_wall');
|
level.transmute_tile(tile, 'green_wall');
|
||||||
}
|
}
|
||||||
else if (tile.type.name === 'green_wall') {
|
else if (tile.type.name === 'green_wall') {
|
||||||
tile.become('green_floor');
|
level.transmute_tile(tile, 'green_floor');
|
||||||
}
|
}
|
||||||
else if (tile.type.name === 'green_chip') {
|
else if (tile.type.name === 'green_chip') {
|
||||||
tile.become('green_bomb');
|
level.transmute_tile(tile, 'green_bomb');
|
||||||
}
|
}
|
||||||
else if (tile.type.name === 'green_bomb') {
|
else if (tile.type.name === 'green_bomb') {
|
||||||
tile.become('green_chip');
|
level.transmute_tile(tile, 'green_chip');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -366,9 +371,9 @@ const TILE_TYPES = {
|
|||||||
if (me.connection && ! me.connection.doomed) {
|
if (me.connection && ! me.connection.doomed) {
|
||||||
let trap = me.connection;
|
let trap = me.connection;
|
||||||
trap.open = true;
|
trap.open = true;
|
||||||
for (let tile of level.cells[trap.y][trap.x]) {
|
for (let tile of trap.cell) {
|
||||||
if (tile.stuck) {
|
if (tile.stuck) {
|
||||||
tile.stuck = false;
|
level.set_actor_stuck(tile, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -377,9 +382,9 @@ const TILE_TYPES = {
|
|||||||
if (me.connection && ! me.connection.doomed) {
|
if (me.connection && ! me.connection.doomed) {
|
||||||
let trap = me.connection;
|
let trap = me.connection;
|
||||||
trap.open = false;
|
trap.open = false;
|
||||||
for (let tile of level.cells[trap.y][trap.x]) {
|
for (let tile of trap.cell) {
|
||||||
if (tile.is_actor) {
|
if (tile.is_actor) {
|
||||||
tile.stuck = false;
|
level.set_actor_stuck(tile, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -553,7 +558,7 @@ const TILE_TYPES = {
|
|||||||
on_arrive(me, level, other) {
|
on_arrive(me, level, other) {
|
||||||
if (other.type.is_player) {
|
if (other.type.is_player) {
|
||||||
level.collect_chip();
|
level.collect_chip();
|
||||||
me.destroy();
|
level.remove_tile(me);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user