Merge branch 'master' of github.com:eevee/lexys-labyrinth
This commit is contained in:
commit
83a1dd23ff
@ -121,6 +121,7 @@
|
|||||||
<button class="control-restart" type="button">Restart</button>
|
<button class="control-restart" type="button">Restart</button>
|
||||||
<button class="control-undo" type="button">Undo</button>
|
<button class="control-undo" type="button">Undo</button>
|
||||||
<button class="control-rewind" type="button">Rewind <span class="keyhint">(z)</span></button>
|
<button class="control-rewind" type="button">Rewind <span class="keyhint">(z)</span></button>
|
||||||
|
<input class="turn-based" type="checkbox">Turn-Based</input>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-controls">
|
<div class="demo-controls">
|
||||||
<button class="demo-play" type="button">View replay</button>
|
<button class="demo-play" type="button">View replay</button>
|
||||||
|
|||||||
428
js/game.js
428
js/game.js
@ -199,7 +199,7 @@ export class Level {
|
|||||||
else {
|
else {
|
||||||
this.time_remaining = this.stored_level.time_limit * 20;
|
this.time_remaining = this.stored_level.time_limit * 20;
|
||||||
}
|
}
|
||||||
this.timer_paused = false;
|
this.timer_paused = false
|
||||||
// Note that this clock counts *up*, even on untimed levels, and is unaffected by CC2's
|
// Note that this clock counts *up*, even on untimed levels, and is unaffected by CC2's
|
||||||
// clock alteration shenanigans
|
// clock alteration shenanigans
|
||||||
this.tic_counter = 0;
|
this.tic_counter = 0;
|
||||||
@ -364,6 +364,11 @@ export class Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
player_awaiting_input() {
|
||||||
|
return this.player.movement_cooldown === 0 && (this.player.slide_mode === null || (this.player.slide_mode === 'force' && this.player.last_move_was_force))
|
||||||
|
}
|
||||||
|
|
||||||
// Lynx PRNG, used unchanged in CC2
|
// Lynx PRNG, used unchanged in CC2
|
||||||
prng() {
|
prng() {
|
||||||
// TODO what if we just saved this stuff, as well as the RFF direction, at the beginning of
|
// TODO what if we just saved this stuff, as well as the RFF direction, at the beginning of
|
||||||
@ -408,14 +413,26 @@ export class Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Move the game state forwards by one tic
|
// Move the game state forwards by one tic
|
||||||
advance_tic(p1_primary_direction, p1_secondary_direction) {
|
// split into two parts for turn-based mode: first part is the consequences of the previous tick, second part depends on the player's input
|
||||||
|
advance_tic(p1_primary_direction, p1_secondary_direction, pass) {
|
||||||
if (this.state !== 'playing') {
|
if (this.state !== 'playing') {
|
||||||
console.warn(`Level.advance_tic() called when state is ${this.state}`);
|
console.warn(`Level.advance_tic() called when state is ${this.state}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this._advance_tic(p1_primary_direction, p1_secondary_direction);
|
if (pass == 1)
|
||||||
|
{
|
||||||
|
this._advance_tic_part1(p1_primary_direction, p1_secondary_direction);
|
||||||
|
}
|
||||||
|
else if (pass == 2)
|
||||||
|
{
|
||||||
|
this._advance_tic_part2(p1_primary_direction, p1_secondary_direction);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.warn(`What pass is this?`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
if (e instanceof GameEnded) {
|
if (e instanceof GameEnded) {
|
||||||
@ -426,11 +443,13 @@ export class Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit the undo state at the end of each tic
|
// Commit the undo state at the end of each tic (pass 2)
|
||||||
this.commit();
|
if (pass == 2) {
|
||||||
|
this.commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_advance_tic(p1_primary_direction, p1_secondary_direction) {
|
_advance_tic_part1(p1_primary_direction, p1_secondary_direction) {
|
||||||
// Player's secondary direction is set immediately; it applies on arrival to cells even if
|
// Player's secondary direction is set immediately; it applies on arrival to cells even if
|
||||||
// it wasn't held the last time the player started moving
|
// it wasn't held the last time the player started moving
|
||||||
this._set_prop(this.player, 'secondary_direction', p1_secondary_direction);
|
this._set_prop(this.player, 'secondary_direction', p1_secondary_direction);
|
||||||
@ -505,199 +524,21 @@ export class Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: actors decide their upcoming movement simultaneously
|
// Second pass: actors decide their upcoming movement simultaneously
|
||||||
|
// (we'll do the player's decision in part 2!)
|
||||||
for (let i = this.actors.length - 1; i >= 0; i--) {
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
let actor = this.actors[i];
|
let actor = this.actors[i];
|
||||||
if (! actor.cell)
|
if (actor != this.player)
|
||||||
continue;
|
|
||||||
|
|
||||||
if (actor.movement_cooldown > 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Teeth can only move the first 4 of every 8 tics, though "first"
|
|
||||||
// can be adjusted
|
|
||||||
if (actor.slide_mode === null &&
|
|
||||||
actor.type.uses_teeth_hesitation &&
|
|
||||||
(this.tic_counter + this.step_parity) % 8 >= 4)
|
|
||||||
{
|
{
|
||||||
continue;
|
this.actor_decision(actor, p1_primary_direction);
|
||||||
}
|
|
||||||
|
|
||||||
let direction_preference;
|
|
||||||
if (this.compat.sliding_tanks_ignore_button &&
|
|
||||||
actor.slide_mode && actor.pending_reverse)
|
|
||||||
{
|
|
||||||
this._set_prop(actor, 'pending_reverse', false);
|
|
||||||
}
|
|
||||||
// Blocks that were pushed while sliding will move in the push direction as soon as they
|
|
||||||
// stop sliding, regardless of what they landed on
|
|
||||||
if (actor.pending_push) {
|
|
||||||
actor.decision = actor.pending_push;
|
|
||||||
this._set_prop(actor, 'pending_push', null);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (actor.slide_mode === 'ice') {
|
|
||||||
// Actors can't make voluntary moves on ice; they just slide
|
|
||||||
actor.decision = actor.direction;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (actor.slide_mode === 'force') {
|
|
||||||
// Only the player can make voluntary moves on a force floor,
|
|
||||||
// and only if their previous move was an /involuntary/ move on
|
|
||||||
// a force floor. If they do, it overrides the forced move
|
|
||||||
// XXX this in particular has some subtleties in lynx (e.g. you
|
|
||||||
// can override forwards??) and DEFINITELY all kinds of stuff
|
|
||||||
// in ms
|
|
||||||
if (actor === this.player &&
|
|
||||||
p1_primary_direction &&
|
|
||||||
actor.last_move_was_force)
|
|
||||||
{
|
|
||||||
actor.decision = p1_primary_direction;
|
|
||||||
this._set_prop(actor, 'last_move_was_force', false);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
actor.decision = actor.direction;
|
|
||||||
if (actor === this.player) {
|
|
||||||
this._set_prop(actor, 'last_move_was_force', true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (actor === this.player) {
|
|
||||||
if (p1_primary_direction) {
|
|
||||||
actor.decision = p1_primary_direction;
|
|
||||||
this._set_prop(actor, 'last_move_was_force', false);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (actor.type.movement_mode === 'forward') {
|
|
||||||
// blue tank behavior: keep moving forward, reverse if the flag is set
|
|
||||||
let direction = actor.direction;
|
|
||||||
if (actor.pending_reverse) {
|
|
||||||
direction = DIRECTIONS[actor.direction].opposite;
|
|
||||||
this._set_prop(actor, 'pending_reverse', false);
|
|
||||||
}
|
|
||||||
// Tanks are controlled explicitly so they don't check if they're blocked
|
|
||||||
// TODO tanks in traps turn around, but tanks on cloners do not, and i use the same
|
|
||||||
// prop for both
|
|
||||||
if (! actor.cell.some(tile => tile.type.name === 'cloner')) {
|
|
||||||
actor.decision = direction;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (actor.type.movement_mode === 'follow-left') {
|
|
||||||
// bug behavior: always try turning as left as possible, and
|
|
||||||
// fall back to less-left turns when that fails
|
|
||||||
let d = DIRECTIONS[actor.direction];
|
|
||||||
direction_preference = [d.left, actor.direction, d.right, d.opposite];
|
|
||||||
}
|
|
||||||
else if (actor.type.movement_mode === 'follow-right') {
|
|
||||||
// paramecium behavior: always try turning as right as
|
|
||||||
// possible, and fall back to less-right turns when that fails
|
|
||||||
let d = DIRECTIONS[actor.direction];
|
|
||||||
direction_preference = [d.right, actor.direction, d.left, d.opposite];
|
|
||||||
}
|
|
||||||
else if (actor.type.movement_mode === 'turn-left') {
|
|
||||||
// glider behavior: preserve current direction; if that doesn't
|
|
||||||
// work, turn left, then right, then back the way we came
|
|
||||||
let d = DIRECTIONS[actor.direction];
|
|
||||||
direction_preference = [actor.direction, d.left, d.right, d.opposite];
|
|
||||||
}
|
|
||||||
else if (actor.type.movement_mode === 'turn-right') {
|
|
||||||
// fireball behavior: preserve current direction; if that doesn't
|
|
||||||
// work, turn right, then left, then back the way we came
|
|
||||||
let d = DIRECTIONS[actor.direction];
|
|
||||||
direction_preference = [actor.direction, d.right, d.left, d.opposite];
|
|
||||||
}
|
|
||||||
else if (actor.type.movement_mode === 'bounce') {
|
|
||||||
// bouncy ball behavior: preserve current direction; if that
|
|
||||||
// doesn't work, bounce back the way we came
|
|
||||||
let d = DIRECTIONS[actor.direction];
|
|
||||||
direction_preference = [actor.direction, d.opposite];
|
|
||||||
}
|
|
||||||
else if (actor.type.movement_mode === 'bounce-random') {
|
|
||||||
// walker behavior: preserve current direction; if that doesn't work, pick a random
|
|
||||||
// direction, even the one we failed to move in (but ONLY then)
|
|
||||||
direction_preference = [actor.direction, 'WALKER'];
|
|
||||||
}
|
|
||||||
else if (actor.type.movement_mode === 'pursue') {
|
|
||||||
// teeth behavior: always move towards the player
|
|
||||||
let target_cell = this.player.cell;
|
|
||||||
// CC2 behavior (not Lynx (TODO compat?)): pursue the cell the player is leaving, if
|
|
||||||
// they're still mostly in it
|
|
||||||
if (this.player.previous_cell && this.player.animation_speed &&
|
|
||||||
this.player.animation_progress <= this.player.animation_speed / 2)
|
|
||||||
{
|
|
||||||
target_cell = this.player.previous_cell;
|
|
||||||
}
|
|
||||||
let dx = actor.cell.x - target_cell.x;
|
|
||||||
let dy = actor.cell.y - target_cell.y;
|
|
||||||
let preferred_horizontal, preferred_vertical;
|
|
||||||
if (dx > 0) {
|
|
||||||
preferred_horizontal = 'west';
|
|
||||||
}
|
|
||||||
else if (dx < 0) {
|
|
||||||
preferred_horizontal = 'east';
|
|
||||||
}
|
|
||||||
if (dy > 0) {
|
|
||||||
preferred_vertical = 'north';
|
|
||||||
}
|
|
||||||
else if (dy < 0) {
|
|
||||||
preferred_vertical = 'south';
|
|
||||||
}
|
|
||||||
// Chooses the furthest direction, vertical wins ties
|
|
||||||
if (Math.abs(dx) > Math.abs(dy)) {
|
|
||||||
// Horizontal first
|
|
||||||
direction_preference = [preferred_horizontal, preferred_vertical].filter(x => x);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Vertical first
|
|
||||||
direction_preference = [preferred_vertical, preferred_horizontal].filter(x => x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (actor.type.movement_mode === 'random') {
|
|
||||||
// blob behavior: move completely at random
|
|
||||||
let modifier = this.get_blob_modifier();
|
|
||||||
direction_preference = [['north', 'east', 'south', 'west'][(this.prng() + modifier) % 4]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check which of those directions we *can*, probably, move in
|
|
||||||
// TODO i think player on force floor will still have some issues here
|
|
||||||
// FIXME probably bail earlier for stuck actors so the prng isn't advanced? what is the
|
|
||||||
// lynx behavior? also i hear something about blobs on cloners??
|
|
||||||
if (direction_preference && ! actor.stuck) {
|
|
||||||
let fallback_direction;
|
|
||||||
for (let direction of direction_preference) {
|
|
||||||
if (direction === 'WALKER') {
|
|
||||||
// Walkers roll a random direction ONLY if their first attempt was blocked
|
|
||||||
direction = actor.direction;
|
|
||||||
let num_turns = this.prng() % 4;
|
|
||||||
for (let i = 0; i < num_turns; i++) {
|
|
||||||
direction = DIRECTIONS[direction].right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fallback_direction = direction;
|
|
||||||
|
|
||||||
let dest_cell = this.get_neighboring_cell(actor.cell, direction);
|
|
||||||
if (! dest_cell)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (! actor.cell.blocks_leaving(actor, direction) &&
|
|
||||||
! dest_cell.blocks_entering(actor, direction, this, true))
|
|
||||||
{
|
|
||||||
// We found a good direction! Stop here
|
|
||||||
actor.decision = direction;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If all the decisions are blocked, actors still try the last one (and might even
|
|
||||||
// be able to move that way by the time their turn comes around!)
|
|
||||||
if (actor.decision === null) {
|
|
||||||
actor.decision = fallback_direction;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_advance_tic_part2(p1_primary_direction, p1_secondary_direction) {
|
||||||
|
//player now makes a decision based on input
|
||||||
|
this.actor_decision(this.player, p1_primary_direction);
|
||||||
|
|
||||||
// Third pass: everyone actually moves
|
// Third pass: everyone actually moves
|
||||||
for (let i = this.actors.length - 1; i >= 0; i--) {
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
let actor = this.actors[i];
|
let actor = this.actors[i];
|
||||||
@ -785,6 +626,198 @@ export class Level {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actor_decision(actor, p1_primary_direction) {
|
||||||
|
if (! actor.cell)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (actor.movement_cooldown > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Teeth can only move the first 4 of every 8 tics, though "first"
|
||||||
|
// can be adjusted
|
||||||
|
if (actor.slide_mode === null &&
|
||||||
|
actor.type.uses_teeth_hesitation &&
|
||||||
|
(this.tic_counter + this.step_parity) % 8 >= 4)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction_preference;
|
||||||
|
if (this.compat.sliding_tanks_ignore_button &&
|
||||||
|
actor.slide_mode && actor.pending_reverse)
|
||||||
|
{
|
||||||
|
this._set_prop(actor, 'pending_reverse', false);
|
||||||
|
}
|
||||||
|
// Blocks that were pushed while sliding will move in the push direction as soon as they
|
||||||
|
// stop sliding, regardless of what they landed on
|
||||||
|
if (actor.pending_push) {
|
||||||
|
actor.decision = actor.pending_push;
|
||||||
|
this._set_prop(actor, 'pending_push', null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (actor.slide_mode === 'ice') {
|
||||||
|
// Actors can't make voluntary moves on ice; they just slide
|
||||||
|
actor.decision = actor.direction;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (actor.slide_mode === 'force') {
|
||||||
|
// Only the player can make voluntary moves on a force floor,
|
||||||
|
// and only if their previous move was an /involuntary/ move on
|
||||||
|
// a force floor. If they do, it overrides the forced move
|
||||||
|
// XXX this in particular has some subtleties in lynx (e.g. you
|
||||||
|
// can override forwards??) and DEFINITELY all kinds of stuff
|
||||||
|
// in ms
|
||||||
|
if (actor === this.player &&
|
||||||
|
p1_primary_direction &&
|
||||||
|
actor.last_move_was_force)
|
||||||
|
{
|
||||||
|
actor.decision = p1_primary_direction;
|
||||||
|
this._set_prop(actor, 'last_move_was_force', false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
actor.decision = actor.direction;
|
||||||
|
if (actor === this.player) {
|
||||||
|
this._set_prop(actor, 'last_move_was_force', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (actor === this.player) {
|
||||||
|
if (p1_primary_direction) {
|
||||||
|
actor.decision = p1_primary_direction;
|
||||||
|
this._set_prop(actor, 'last_move_was_force', false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (actor.type.movement_mode === 'forward') {
|
||||||
|
// blue tank behavior: keep moving forward, reverse if the flag is set
|
||||||
|
let direction = actor.direction;
|
||||||
|
if (actor.pending_reverse) {
|
||||||
|
direction = DIRECTIONS[actor.direction].opposite;
|
||||||
|
this._set_prop(actor, 'pending_reverse', false);
|
||||||
|
}
|
||||||
|
// Tanks are controlled explicitly so they don't check if they're blocked
|
||||||
|
// TODO tanks in traps turn around, but tanks on cloners do not, and i use the same
|
||||||
|
// prop for both
|
||||||
|
if (! actor.cell.some(tile => tile.type.name === 'cloner')) {
|
||||||
|
actor.decision = direction;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (actor.type.movement_mode === 'follow-left') {
|
||||||
|
// bug behavior: always try turning as left as possible, and
|
||||||
|
// fall back to less-left turns when that fails
|
||||||
|
let d = DIRECTIONS[actor.direction];
|
||||||
|
direction_preference = [d.left, actor.direction, d.right, d.opposite];
|
||||||
|
}
|
||||||
|
else if (actor.type.movement_mode === 'follow-right') {
|
||||||
|
// paramecium behavior: always try turning as right as
|
||||||
|
// possible, and fall back to less-right turns when that fails
|
||||||
|
let d = DIRECTIONS[actor.direction];
|
||||||
|
direction_preference = [d.right, actor.direction, d.left, d.opposite];
|
||||||
|
}
|
||||||
|
else if (actor.type.movement_mode === 'turn-left') {
|
||||||
|
// glider behavior: preserve current direction; if that doesn't
|
||||||
|
// work, turn left, then right, then back the way we came
|
||||||
|
let d = DIRECTIONS[actor.direction];
|
||||||
|
direction_preference = [actor.direction, d.left, d.right, d.opposite];
|
||||||
|
}
|
||||||
|
else if (actor.type.movement_mode === 'turn-right') {
|
||||||
|
// fireball behavior: preserve current direction; if that doesn't
|
||||||
|
// work, turn right, then left, then back the way we came
|
||||||
|
let d = DIRECTIONS[actor.direction];
|
||||||
|
direction_preference = [actor.direction, d.right, d.left, d.opposite];
|
||||||
|
}
|
||||||
|
else if (actor.type.movement_mode === 'bounce') {
|
||||||
|
// bouncy ball behavior: preserve current direction; if that
|
||||||
|
// doesn't work, bounce back the way we came
|
||||||
|
let d = DIRECTIONS[actor.direction];
|
||||||
|
direction_preference = [actor.direction, d.opposite];
|
||||||
|
}
|
||||||
|
else if (actor.type.movement_mode === 'bounce-random') {
|
||||||
|
// walker behavior: preserve current direction; if that doesn't work, pick a random
|
||||||
|
// direction, even the one we failed to move in (but ONLY then)
|
||||||
|
direction_preference = [actor.direction, 'WALKER'];
|
||||||
|
}
|
||||||
|
else if (actor.type.movement_mode === 'pursue') {
|
||||||
|
// teeth behavior: always move towards the player
|
||||||
|
let target_cell = this.player.cell;
|
||||||
|
// CC2 behavior (not Lynx (TODO compat?)): pursue the cell the player is leaving, if
|
||||||
|
// they're still mostly in it
|
||||||
|
if (this.player.previous_cell && this.player.animation_speed &&
|
||||||
|
this.player.animation_progress <= this.player.animation_speed / 2)
|
||||||
|
{
|
||||||
|
target_cell = this.player.previous_cell;
|
||||||
|
}
|
||||||
|
let dx = actor.cell.x - target_cell.x;
|
||||||
|
let dy = actor.cell.y - target_cell.y;
|
||||||
|
let preferred_horizontal, preferred_vertical;
|
||||||
|
if (dx > 0) {
|
||||||
|
preferred_horizontal = 'west';
|
||||||
|
}
|
||||||
|
else if (dx < 0) {
|
||||||
|
preferred_horizontal = 'east';
|
||||||
|
}
|
||||||
|
if (dy > 0) {
|
||||||
|
preferred_vertical = 'north';
|
||||||
|
}
|
||||||
|
else if (dy < 0) {
|
||||||
|
preferred_vertical = 'south';
|
||||||
|
}
|
||||||
|
// Chooses the furthest direction, vertical wins ties
|
||||||
|
if (Math.abs(dx) > Math.abs(dy)) {
|
||||||
|
// Horizontal first
|
||||||
|
direction_preference = [preferred_horizontal, preferred_vertical].filter(x => x);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Vertical first
|
||||||
|
direction_preference = [preferred_vertical, preferred_horizontal].filter(x => x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (actor.type.movement_mode === 'random') {
|
||||||
|
// blob behavior: move completely at random
|
||||||
|
let modifier = this.get_blob_modifier();
|
||||||
|
direction_preference = [['north', 'east', 'south', 'west'][(this.prng() + modifier) % 4]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check which of those directions we *can*, probably, move in
|
||||||
|
// TODO i think player on force floor will still have some issues here
|
||||||
|
// FIXME probably bail earlier for stuck actors so the prng isn't advanced? what is the
|
||||||
|
// lynx behavior? also i hear something about blobs on cloners??
|
||||||
|
if (direction_preference && ! actor.stuck) {
|
||||||
|
let fallback_direction;
|
||||||
|
for (let direction of direction_preference) {
|
||||||
|
if (direction === 'WALKER') {
|
||||||
|
// Walkers roll a random direction ONLY if their first attempt was blocked
|
||||||
|
direction = actor.direction;
|
||||||
|
let num_turns = this.prng() % 4;
|
||||||
|
for (let i = 0; i < num_turns; i++) {
|
||||||
|
direction = DIRECTIONS[direction].right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fallback_direction = direction;
|
||||||
|
|
||||||
|
let dest_cell = this.get_neighboring_cell(actor.cell, direction);
|
||||||
|
if (! dest_cell)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (! actor.cell.blocks_leaving(actor, direction) &&
|
||||||
|
! dest_cell.blocks_entering(actor, direction, this, true))
|
||||||
|
{
|
||||||
|
// We found a good direction! Stop here
|
||||||
|
actor.decision = direction;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all the decisions are blocked, actors still try the last one (and might even
|
||||||
|
// be able to move that way by the time their turn comes around!)
|
||||||
|
if (actor.decision === null) {
|
||||||
|
actor.decision = fallback_direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
@ -1306,6 +1339,13 @@ export class Level {
|
|||||||
}
|
}
|
||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
|
//reverse the pending_undo too
|
||||||
|
this.pending_undo.reverse();
|
||||||
|
for (let undo of this.pending_undo) {
|
||||||
|
undo();
|
||||||
|
}
|
||||||
|
this.pending_undo = [];
|
||||||
|
|
||||||
this.aid = Math.max(1, this.aid);
|
this.aid = Math.max(1, this.aid);
|
||||||
|
|
||||||
let entry = this.undo_stack.pop();
|
let entry = this.undo_stack.pop();
|
||||||
|
|||||||
95
js/main.js
95
js/main.js
@ -243,6 +243,8 @@ class Player extends PrimaryView {
|
|||||||
ArrowRight: 'right',
|
ArrowRight: 'right',
|
||||||
ArrowUp: 'up',
|
ArrowUp: 'up',
|
||||||
ArrowDown: 'down',
|
ArrowDown: 'down',
|
||||||
|
Spacebar: 'wait',
|
||||||
|
" ": 'wait',
|
||||||
w: 'up',
|
w: 'up',
|
||||||
a: 'left',
|
a: 'left',
|
||||||
s: 'down',
|
s: 'down',
|
||||||
@ -304,6 +306,12 @@ class Player extends PrimaryView {
|
|||||||
this.music_audio_el.pause();
|
this.music_audio_el.pause();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.turn_based = false;
|
||||||
|
this.turn_based_checkbox = this.root.querySelector('.controls .turn-based');
|
||||||
|
this.turn_based_checkbox.addEventListener('change', ev => {
|
||||||
|
this.turn_based = !this.turn_based;
|
||||||
|
});
|
||||||
|
|
||||||
// Bind buttons
|
// Bind buttons
|
||||||
this.pause_button = this.root.querySelector('.controls .control-pause');
|
this.pause_button = this.root.querySelector('.controls .control-pause');
|
||||||
@ -327,7 +335,7 @@ class Player extends PrimaryView {
|
|||||||
while (this.level.undo_stack.length > 0 &&
|
while (this.level.undo_stack.length > 0 &&
|
||||||
! (moved && this.level.player.slide_mode === null))
|
! (moved && this.level.player.slide_mode === null))
|
||||||
{
|
{
|
||||||
this.level.undo();
|
this.undo();
|
||||||
if (player_cell !== this.level.player.cell) {
|
if (player_cell !== this.level.player.cell) {
|
||||||
moved = true;
|
moved = true;
|
||||||
}
|
}
|
||||||
@ -409,6 +417,7 @@ class Player extends PrimaryView {
|
|||||||
this.previous_action = null; // last direction we were moving, if any
|
this.previous_action = null; // last direction we were moving, if any
|
||||||
this.using_touch = false; // true if using touch controls
|
this.using_touch = false; // true if using touch controls
|
||||||
this.current_keys = new Set; // keys that are currently held
|
this.current_keys = new Set; // keys that are currently held
|
||||||
|
this.current_keys_new = new Set; //for keys that have only been held a frame
|
||||||
// TODO this could all probably be more rigorous but it's fine for now
|
// TODO this could all probably be more rigorous but it's fine for now
|
||||||
key_target.addEventListener('keydown', ev => {
|
key_target.addEventListener('keydown', ev => {
|
||||||
if (ev.key === 'p' || ev.key === 'Pause') {
|
if (ev.key === 'p' || ev.key === 'Pause') {
|
||||||
@ -429,7 +438,9 @@ class Player extends PrimaryView {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Restart
|
// Restart
|
||||||
this.restart_level();
|
if (!this.current_keys.has(ev.key)) {
|
||||||
|
this.restart_level();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -447,6 +458,7 @@ class Player extends PrimaryView {
|
|||||||
|
|
||||||
if (this.key_mapping[ev.key]) {
|
if (this.key_mapping[ev.key]) {
|
||||||
this.current_keys.add(ev.key);
|
this.current_keys.add(ev.key);
|
||||||
|
this.current_keys_new.add(ev.key);
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
@ -647,6 +659,7 @@ class Player extends PrimaryView {
|
|||||||
_clear_state() {
|
_clear_state() {
|
||||||
this.set_state('waiting');
|
this.set_state('waiting');
|
||||||
|
|
||||||
|
this.waiting_for_input = false;
|
||||||
this.tic_offset = 0;
|
this.tic_offset = 0;
|
||||||
this.last_advance = 0;
|
this.last_advance = 0;
|
||||||
this.demo_faucet = null;
|
this.demo_faucet = null;
|
||||||
@ -693,12 +706,18 @@ class Player extends PrimaryView {
|
|||||||
for (let key of this.current_keys) {
|
for (let key of this.current_keys) {
|
||||||
input.add(this.key_mapping[key]);
|
input.add(this.key_mapping[key]);
|
||||||
}
|
}
|
||||||
|
for (let key of this.current_keys_new) {
|
||||||
|
input.add(this.key_mapping[key]);
|
||||||
|
}
|
||||||
|
this.current_keys_new = new Set;
|
||||||
for (let action of Object.values(this.current_touches)) {
|
for (let action of Object.values(this.current_touches)) {
|
||||||
input.add(action);
|
input.add(action);
|
||||||
}
|
}
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waiting_for_input = false;
|
||||||
|
|
||||||
advance_by(tics) {
|
advance_by(tics) {
|
||||||
for (let i = 0; i < tics; i++) {
|
for (let i = 0; i < tics; i++) {
|
||||||
@ -762,10 +781,43 @@ class Player extends PrimaryView {
|
|||||||
this.previous_input = input;
|
this.previous_input = input;
|
||||||
|
|
||||||
this.sfx_player.advance_tic();
|
this.sfx_player.advance_tic();
|
||||||
this.level.advance_tic(
|
|
||||||
this.primary_action ? ACTION_DIRECTIONS[this.primary_action] : null,
|
var primary_dir = this.primary_action ? ACTION_DIRECTIONS[this.primary_action] : null;
|
||||||
this.secondary_action ? ACTION_DIRECTIONS[this.secondary_action] : null,
|
var secondary_dir = this.secondary_action ? ACTION_DIRECTIONS[this.secondary_action] : null;
|
||||||
);
|
|
||||||
|
//turn based logic
|
||||||
|
//first, handle a part 2 we just got input for
|
||||||
|
if (this.waiting_for_input)
|
||||||
|
{
|
||||||
|
if (!this.turn_based || primary_dir != null || input.has('wait'))
|
||||||
|
{
|
||||||
|
this.waiting_for_input = false;
|
||||||
|
this.level.advance_tic(
|
||||||
|
primary_dir,
|
||||||
|
secondary_dir,
|
||||||
|
2);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.level.advance_tic(
|
||||||
|
primary_dir,
|
||||||
|
secondary_dir,
|
||||||
|
1);
|
||||||
|
//then if we should wait for input, the player needs input and we don't have input, we set waiting_for_input, else we run part 2
|
||||||
|
if (this.turn_based && this.level.player_awaiting_input() && !(primary_dir != null || input.has('wait')))
|
||||||
|
{
|
||||||
|
this.waiting_for_input = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.level.advance_tic(
|
||||||
|
primary_dir,
|
||||||
|
secondary_dir,
|
||||||
|
2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.level.state !== 'playing') {
|
if (this.level.state !== 'playing') {
|
||||||
// We either won or lost!
|
// We either won or lost!
|
||||||
@ -783,8 +835,9 @@ class Player extends PrimaryView {
|
|||||||
this._advance_handle = null;
|
this._advance_handle = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.last_advance = performance.now();
|
this.last_advance = performance.now();
|
||||||
|
|
||||||
if (this.state === 'playing') {
|
if (this.state === 'playing') {
|
||||||
this.advance_by(1);
|
this.advance_by(1);
|
||||||
}
|
}
|
||||||
@ -798,10 +851,17 @@ class Player extends PrimaryView {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Rewind by undoing one tic every tic
|
// Rewind by undoing one tic every tic
|
||||||
this.level.undo();
|
this.undo();
|
||||||
this.update_ui();
|
this.update_ui();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.waiting_for_input)
|
||||||
|
{
|
||||||
|
//freeze tic_offset in time so we don't try to interpolate to the next frame too soon
|
||||||
|
this.tic_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
let dt = 1000 / TICS_PER_SECOND;
|
let dt = 1000 / TICS_PER_SECOND;
|
||||||
if (this.state === 'rewinding') {
|
if (this.state === 'rewinding') {
|
||||||
// Rewind faster than normal time
|
// Rewind faster than normal time
|
||||||
@ -810,6 +870,12 @@ class Player extends PrimaryView {
|
|||||||
this._advance_handle = window.setTimeout(this._advance_bound, dt);
|
this._advance_handle = window.setTimeout(this._advance_bound, dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
//if we were waiting for input and undo, well, now we're not
|
||||||
|
this.waiting_for_input = false;
|
||||||
|
this.level.undo();
|
||||||
|
}
|
||||||
|
|
||||||
// Redraws every frame, unless the game isn't running
|
// Redraws every frame, unless the game isn't running
|
||||||
redraw() {
|
redraw() {
|
||||||
// Calculate this here, not in _redraw, because that's called at weird
|
// Calculate this here, not in _redraw, because that's called at weird
|
||||||
@ -817,9 +883,15 @@ class Player extends PrimaryView {
|
|||||||
// TODO this is not gonna be right while pausing lol
|
// TODO this is not gonna be right while pausing lol
|
||||||
// TODO i'm not sure it'll be right when rewinding either
|
// TODO i'm not sure it'll be right when rewinding either
|
||||||
// TODO or if the game's speed changes. wow!
|
// TODO or if the game's speed changes. wow!
|
||||||
this.tic_offset = Math.min(0.9999, (performance.now() - this.last_advance) / 1000 / (1 / TICS_PER_SECOND));
|
if (this.waiting_for_input) {
|
||||||
if (this.state === 'rewinding') {
|
//freeze tic_offset in time
|
||||||
this.tic_offset = 1 - this.tic_offset;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.tic_offset = Math.min(0.9999, (performance.now() - this.last_advance) / 1000 / (1 / TICS_PER_SECOND));
|
||||||
|
if (this.state === 'rewinding') {
|
||||||
|
this.tic_offset = 1 - this.tic_offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._redraw();
|
this._redraw();
|
||||||
@ -837,6 +909,7 @@ class Player extends PrimaryView {
|
|||||||
|
|
||||||
// Actually redraw. Used to force drawing outside of normal play
|
// Actually redraw. Used to force drawing outside of normal play
|
||||||
_redraw() {
|
_redraw() {
|
||||||
|
this.renderer.waiting_for_input = this.waiting_for_input;
|
||||||
this.renderer.draw(this.tic_offset);
|
this.renderer.draw(this.tic_offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,8 @@ export class CanvasRenderer {
|
|||||||
sx * tw, sy * th, w * tw, h * th,
|
sx * tw, sy * th, w * tw, h * th,
|
||||||
dx * tw, dy * th, w * tw, h * th);
|
dx * tw, dy * th, w * tw, h * th);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waiting_for_input = false;
|
||||||
|
|
||||||
draw(tic_offset = 0) {
|
draw(tic_offset = 0) {
|
||||||
if (! this.level) {
|
if (! this.level) {
|
||||||
@ -67,7 +69,7 @@ export class CanvasRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tic = (this.level.tic_counter ?? 0) + tic_offset;
|
let tic = (this.level.tic_counter ?? 0) + tic_offset + (this.waiting_for_input ? 1 : 0);
|
||||||
let tw = this.tileset.size_x;
|
let tw = this.tileset.size_x;
|
||||||
let th = this.tileset.size_y;
|
let th = this.tileset.size_y;
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user