Clean up turn-based code

Mostly style nits, but also:

- Renamed some stuff in anticipation of removing GameEnded.

- Actor decisions are independent, so there's no need to do most of them
  in the first part of a tic and the player in the second part; they can
  all happen together in the second part.

- waiting_for_input was merged into turn_based, which I think makes it
  easier to follow what's going on between tics.  Although I just
  realized it introduces a bug, so, better fix that next.

- The canvas didn't need to know if we were waiting or not if we just
  force the tic offset to 1 while waiting.  This also fixed some slight
  jitter with force floors.
This commit is contained in:
Eevee (Evelyn Woods) 2020-11-03 09:50:37 -07:00
parent 83a1dd23ff
commit e7e02281a2
4 changed files with 266 additions and 275 deletions

View File

@ -121,7 +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> <label><input class="control-turn-based" type="checkbox"> Turn-based mode</label>
</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>

View File

@ -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,9 +364,14 @@ export class Level {
} }
} }
can_accept_input() {
player_awaiting_input() { // We can accept input anytime the player can move, i.e. when they're not already moving and
return this.player.movement_cooldown === 0 && (this.player.slide_mode === null || (this.player.slide_mode === 'force' && this.player.last_move_was_force)) // not in an un-overrideable slide.
// Note that this only makes sense in the middle of a tic; at the beginning of one, the
// player's movement cooldown may very well be 1, but it'll be decremented before they
// attempt to move
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
@ -412,25 +417,25 @@ export class Level {
return mod; return mod;
} }
// Move the game state forwards by one tic // Move the game state forwards by one tic.
// 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 // For turn-based mode, this is split into two parts: advance_tic_finish_movement completes any
// ongoing movement started in the previous tic, and advance_tic_act allows actors to make new
// decisions. The player makes decisions between these two parts.
advance_tic(p1_primary_direction, p1_secondary_direction, pass) { 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;
} }
// TODO rip out this try/catch, it's not how the game actually works
try { try {
if (pass == 1) if (pass == 1) {
{ this.advance_tic_finish_movement(p1_primary_direction, p1_secondary_direction);
this._advance_tic_part1(p1_primary_direction, p1_secondary_direction);
} }
else if (pass == 2) else if (pass == 2) {
{ this.advance_tic_act(p1_primary_direction, p1_secondary_direction);
this._advance_tic_part2(p1_primary_direction, p1_secondary_direction);
} }
else else {
{
console.warn(`What pass is this?`); console.warn(`What pass is this?`);
} }
} }
@ -449,7 +454,7 @@ export class Level {
} }
} }
_advance_tic_part1(p1_primary_direction, p1_secondary_direction) { advance_tic_finish_movement(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);
@ -522,23 +527,203 @@ export class Level {
if (this.player.movement_cooldown <= 0) { if (this.player.movement_cooldown <= 0) {
this.player.is_pushing = false; this.player.is_pushing = false;
} }
}
advance_tic_act(p1_primary_direction, p1_secondary_direction) {
// 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 != this.player) if (! actor.cell)
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)
{ {
this.actor_decision(actor, p1_primary_direction); continue;
}
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;
}
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];
@ -626,198 +811,6 @@ 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.
@ -1339,14 +1332,14 @@ export class Level {
} }
undo() { undo() {
//reverse the pending_undo too this.aid = Math.max(1, this.aid);
// In turn-based mode, we might still be in mid-tic with a partial undo stack; do that first
this.pending_undo.reverse(); this.pending_undo.reverse();
for (let undo of this.pending_undo) { for (let undo of this.pending_undo) {
undo(); undo();
} }
this.pending_undo = []; this.pending_undo = [];
this.aid = Math.max(1, this.aid);
let entry = this.undo_stack.pop(); let entry = this.undo_stack.pop();
// Undo in reverse order! There's no redo, so it's okay to destroy this // Undo in reverse order! There's no redo, so it's okay to destroy this

View File

@ -307,10 +307,24 @@ class Player extends PrimaryView {
} }
}); });
this.turn_based = false; // 0: normal realtime mode
this.turn_based_checkbox = this.root.querySelector('.controls .turn-based'); // 1: turn-based mode, and the next tic starts at the beginning
// 2: turn-based mode, and we're in mid-tic waiting for input
this.turn_based = 0;
this.turn_based_checkbox = this.root.querySelector('.controls .control-turn-based');
this.turn_based_checkbox.checked = false;
this.turn_based_checkbox.addEventListener('change', ev => { this.turn_based_checkbox.addEventListener('change', ev => {
this.turn_based = !this.turn_based; if (this.turn_based_checkbox.checked) {
// If we're leaving real-time mode then we're between tics
this.turn_based = 1;
}
else {
if (this.turn_based === 2) {
// Finish up the tic
this.advance_by(1);
}
this.turn_based = 0;
}
}); });
// Bind buttons // Bind buttons
@ -329,8 +343,9 @@ class Player extends PrimaryView {
this.undo_button = this.root.querySelector('.controls .control-undo'); this.undo_button = this.root.querySelector('.controls .control-undo');
this.undo_button.addEventListener('click', ev => { this.undo_button.addEventListener('click', ev => {
let player_cell = this.level.player.cell; let player_cell = this.level.player.cell;
// Keep undoing until (a) we're on another cell and (b) we're not // Keep undoing until (a) we're on another cell and (b) we're not sliding, i.e. we're
// sliding, i.e. we're about to make a conscious move // about to make a conscious move. Note that this means undoing all the way through
// force floors, even if you could override them!
let moved = false; let moved = false;
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))
@ -417,7 +432,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 this.current_keys_new = new Set; // keys that were pressed since input was last read
// 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') {
@ -659,7 +674,7 @@ class Player extends PrimaryView {
_clear_state() { _clear_state() {
this.set_state('waiting'); this.set_state('waiting');
this.waiting_for_input = false; this.turn_based = this.turn_based_checkbox.checked ? 1 : 0;
this.tic_offset = 0; this.tic_offset = 0;
this.last_advance = 0; this.last_advance = 0;
this.demo_faucet = null; this.demo_faucet = null;
@ -709,7 +724,7 @@ class Player extends PrimaryView {
for (let key of this.current_keys_new) { for (let key of this.current_keys_new) {
input.add(this.key_mapping[key]); input.add(this.key_mapping[key]);
} }
this.current_keys_new = new Set; this.current_keys_new.clear();
for (let action of Object.values(this.current_touches)) { for (let action of Object.values(this.current_touches)) {
input.add(action); input.add(action);
} }
@ -717,8 +732,6 @@ class Player extends PrimaryView {
} }
} }
waiting_for_input = false;
advance_by(tics) { advance_by(tics) {
for (let i = 0; i < tics; i++) { for (let i = 0; i < tics; i++) {
let input = this.get_input(); let input = this.get_input();
@ -785,37 +798,26 @@ class Player extends PrimaryView {
var primary_dir = this.primary_action ? ACTION_DIRECTIONS[this.primary_action] : null; var primary_dir = this.primary_action ? ACTION_DIRECTIONS[this.primary_action] : null;
var secondary_dir = this.secondary_action ? ACTION_DIRECTIONS[this.secondary_action] : null; var secondary_dir = this.secondary_action ? ACTION_DIRECTIONS[this.secondary_action] : null;
//turn based logic let has_input = primary_dir !== null || input.has('wait');
//first, handle a part 2 we just got input for // Turn-based mode complicates this slightly
if (this.waiting_for_input) // TODO advance_by(1) no longer advances by 1 tic necessarily...
{ if (this.turn_based === 2) {
if (!this.turn_based || primary_dir != null || input.has('wait')) if (has_input) {
{ this.level.advance_tic(primary_dir, secondary_dir, 2);
this.waiting_for_input = false; // TODO what if we just do the next tic part now? but then we can never realign to a tic boundary.
this.level.advance_tic( this.turn_based = 1;
primary_dir,
secondary_dir,
2);
} }
} }
else else {
{ // Start from a tic boundary
this.level.advance_tic( this.level.advance_tic(primary_dir, secondary_dir, 1);
primary_dir, if (this.turn_based > 0 && this.level.can_accept_input() && ! has_input) {
secondary_dir, // If we're in turn-based mode and could provide input here, but don't have any,
1); // then wait until we do
//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 this.turn_based = 2;
if (this.turn_based && this.level.player_awaiting_input() && !(primary_dir != null || input.has('wait')))
{
this.waiting_for_input = true;
} }
else else {
{ this.level.advance_tic(primary_dir, secondary_dir, 2);
this.level.advance_tic(
primary_dir,
secondary_dir,
2);
} }
} }
@ -855,12 +857,8 @@ class Player extends PrimaryView {
this.update_ui(); this.update_ui();
} }
} }
if (this.waiting_for_input) // XXX tic_offset = 0 was here, what does that change
{
//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') {
@ -871,9 +869,11 @@ class Player extends PrimaryView {
} }
undo() { undo() {
//if we were waiting for input and undo, well, now we're not
this.waiting_for_input = false;
this.level.undo(); this.level.undo();
// Undo always returns to the start of a tic
if (this.turn_based === 2) {
this.turn_based = 1;
}
} }
// Redraws every frame, unless the game isn't running // Redraws every frame, unless the game isn't running
@ -883,11 +883,12 @@ 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!
if (this.waiting_for_input) { if (this.turn_based === 2) {
//freeze tic_offset in time // We're frozen in mid-tic, so the clock hasn't advanced yet, but everything has already
// finished moving; pretend we're already on the next tic
this.tic_offset = 1;
} }
else else {
{
this.tic_offset = Math.min(0.9999, (performance.now() - this.last_advance) / 1000 / (1 / TICS_PER_SECOND)); this.tic_offset = Math.min(0.9999, (performance.now() - this.last_advance) / 1000 / (1 / TICS_PER_SECOND));
if (this.state === 'rewinding') { if (this.state === 'rewinding') {
this.tic_offset = 1 - this.tic_offset; this.tic_offset = 1 - this.tic_offset;
@ -909,7 +910,6 @@ 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);
} }

View File

@ -61,15 +61,13 @@ export class CanvasRenderer {
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) {
console.warn("CanvasRenderer.draw: No level to render"); console.warn("CanvasRenderer.draw: No level to render");
return; return;
} }
let tic = (this.level.tic_counter ?? 0) + tic_offset + (this.waiting_for_input ? 1 : 0); let tic = (this.level.tic_counter ?? 0) + tic_offset;
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);