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:
parent
83a1dd23ff
commit
e7e02281a2
@ -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>
|
||||||
|
|||||||
245
js/game.js
245
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,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,125 +527,25 @@ 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--) {
|
|
||||||
let actor = this.actors[i];
|
|
||||||
if (actor != this.player)
|
|
||||||
{
|
|
||||||
this.actor_decision(actor, p1_primary_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
|
|
||||||
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.cell)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Check this again, because one actor's movement might caused a later actor to move
|
|
||||||
// (e.g. by pressing a red or brown button)
|
|
||||||
if (actor.movement_cooldown > 0)
|
if (actor.movement_cooldown > 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (! actor.decision)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
let old_cell = actor.cell;
|
|
||||||
let success = this.attempt_step(actor, actor.decision);
|
|
||||||
|
|
||||||
// Track whether the player is blocked, for visual effect
|
|
||||||
if (actor === this.player && p1_primary_direction && ! success) {
|
|
||||||
this.sfx.play_once('blocked');
|
|
||||||
actor.is_blocked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Players can also bump the tiles in the cell next to the one they're leaving
|
|
||||||
let dir2 = actor.secondary_direction;
|
|
||||||
if (actor.type.is_player && dir2 &&
|
|
||||||
! old_cell.blocks_leaving(actor, dir2))
|
|
||||||
{
|
|
||||||
let neighbor = this.get_neighboring_cell(old_cell, dir2);
|
|
||||||
if (neighbor) {
|
|
||||||
let could_push = ! neighbor.blocks_entering(actor, dir2, this, true);
|
|
||||||
for (let tile of Array.from(neighbor)) {
|
|
||||||
if (tile.type.on_bump) {
|
|
||||||
tile.type.on_bump(tile, this, actor);
|
|
||||||
}
|
|
||||||
if (could_push && actor.can_push(tile, dir2)) {
|
|
||||||
// Block slapping: you can shove a block by walking past it sideways
|
|
||||||
// TODO i think cc2 uses the push pose and possibly even turns you here?
|
|
||||||
this.attempt_step(tile, dir2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip out any destroyed actors from the acting order
|
|
||||||
// FIXME this is O(n), where n is /usually/ small, but i still don't love it
|
|
||||||
let p = 0;
|
|
||||||
for (let i = 0, l = this.actors.length; i < l; i++) {
|
|
||||||
let actor = this.actors[i];
|
|
||||||
if (actor.cell) {
|
|
||||||
if (p !== i) {
|
|
||||||
this.actors[p] = actor;
|
|
||||||
}
|
|
||||||
p++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let local_p = p;
|
|
||||||
this.pending_undo.push(() => this.actors.splice(local_p, 0, actor));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.actors.length = p;
|
|
||||||
|
|
||||||
// Advance the clock
|
|
||||||
let tic_counter = this.tic_counter;
|
|
||||||
this.tic_counter += 1;
|
|
||||||
if (this.time_remaining !== null && ! this.timer_paused) {
|
|
||||||
let time_remaining = this.time_remaining;
|
|
||||||
this.pending_undo.push(() => {
|
|
||||||
this.tic_counter = tic_counter;
|
|
||||||
this.time_remaining = time_remaining;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.time_remaining -= 1;
|
|
||||||
if (this.time_remaining <= 0) {
|
|
||||||
this.fail('time');
|
|
||||||
}
|
|
||||||
else if (this.time_remaining % 20 === 0 && this.time_remaining < 30 * 20) {
|
|
||||||
this.sfx.play_once('tick');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.pending_undo.push(() => {
|
|
||||||
this.tic_counter = tic_counter;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
// Teeth can only move the first 4 of every 8 tics, though "first"
|
||||||
// can be adjusted
|
// can be adjusted
|
||||||
if (actor.slide_mode === null &&
|
if (actor.slide_mode === null &&
|
||||||
actor.type.uses_teeth_hesitation &&
|
actor.type.uses_teeth_hesitation &&
|
||||||
(this.tic_counter + this.step_parity) % 8 >= 4)
|
(this.tic_counter + this.step_parity) % 8 >= 4)
|
||||||
{
|
{
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let direction_preference;
|
let direction_preference;
|
||||||
@ -654,12 +559,12 @@ export class Level {
|
|||||||
if (actor.pending_push) {
|
if (actor.pending_push) {
|
||||||
actor.decision = actor.pending_push;
|
actor.decision = actor.pending_push;
|
||||||
this._set_prop(actor, 'pending_push', null);
|
this._set_prop(actor, 'pending_push', null);
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
if (actor.slide_mode === 'ice') {
|
if (actor.slide_mode === 'ice') {
|
||||||
// Actors can't make voluntary moves on ice; they just slide
|
// Actors can't make voluntary moves on ice; they just slide
|
||||||
actor.decision = actor.direction;
|
actor.decision = actor.direction;
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
else if (actor.slide_mode === 'force') {
|
else if (actor.slide_mode === 'force') {
|
||||||
// Only the player can make voluntary moves on a force floor,
|
// Only the player can make voluntary moves on a force floor,
|
||||||
@ -681,14 +586,14 @@ export class Level {
|
|||||||
this._set_prop(actor, 'last_move_was_force', true);
|
this._set_prop(actor, 'last_move_was_force', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
else if (actor === this.player) {
|
else if (actor === this.player) {
|
||||||
if (p1_primary_direction) {
|
if (p1_primary_direction) {
|
||||||
actor.decision = p1_primary_direction;
|
actor.decision = p1_primary_direction;
|
||||||
this._set_prop(actor, 'last_move_was_force', false);
|
this._set_prop(actor, 'last_move_was_force', false);
|
||||||
}
|
}
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
else if (actor.type.movement_mode === 'forward') {
|
else if (actor.type.movement_mode === 'forward') {
|
||||||
// blue tank behavior: keep moving forward, reverse if the flag is set
|
// blue tank behavior: keep moving forward, reverse if the flag is set
|
||||||
@ -703,7 +608,7 @@ export class Level {
|
|||||||
if (! actor.cell.some(tile => tile.type.name === 'cloner')) {
|
if (! actor.cell.some(tile => tile.type.name === 'cloner')) {
|
||||||
actor.decision = direction;
|
actor.decision = direction;
|
||||||
}
|
}
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
else if (actor.type.movement_mode === 'follow-left') {
|
else if (actor.type.movement_mode === 'follow-left') {
|
||||||
// bug behavior: always try turning as left as possible, and
|
// bug behavior: always try turning as left as possible, and
|
||||||
@ -819,6 +724,94 @@ export class Level {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Third pass: everyone actually moves
|
||||||
|
for (let i = this.actors.length - 1; i >= 0; i--) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
if (! actor.cell)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check this again, because one actor's movement might caused a later actor to move
|
||||||
|
// (e.g. by pressing a red or brown button)
|
||||||
|
if (actor.movement_cooldown > 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (! actor.decision)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
let old_cell = actor.cell;
|
||||||
|
let success = this.attempt_step(actor, actor.decision);
|
||||||
|
|
||||||
|
// Track whether the player is blocked, for visual effect
|
||||||
|
if (actor === this.player && p1_primary_direction && ! success) {
|
||||||
|
this.sfx.play_once('blocked');
|
||||||
|
actor.is_blocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Players can also bump the tiles in the cell next to the one they're leaving
|
||||||
|
let dir2 = actor.secondary_direction;
|
||||||
|
if (actor.type.is_player && dir2 &&
|
||||||
|
! old_cell.blocks_leaving(actor, dir2))
|
||||||
|
{
|
||||||
|
let neighbor = this.get_neighboring_cell(old_cell, dir2);
|
||||||
|
if (neighbor) {
|
||||||
|
let could_push = ! neighbor.blocks_entering(actor, dir2, this, true);
|
||||||
|
for (let tile of Array.from(neighbor)) {
|
||||||
|
if (tile.type.on_bump) {
|
||||||
|
tile.type.on_bump(tile, this, actor);
|
||||||
|
}
|
||||||
|
if (could_push && actor.can_push(tile, dir2)) {
|
||||||
|
// Block slapping: you can shove a block by walking past it sideways
|
||||||
|
// TODO i think cc2 uses the push pose and possibly even turns you here?
|
||||||
|
this.attempt_step(tile, dir2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip out any destroyed actors from the acting order
|
||||||
|
// FIXME this is O(n), where n is /usually/ small, but i still don't love it
|
||||||
|
let p = 0;
|
||||||
|
for (let i = 0, l = this.actors.length; i < l; i++) {
|
||||||
|
let actor = this.actors[i];
|
||||||
|
if (actor.cell) {
|
||||||
|
if (p !== i) {
|
||||||
|
this.actors[p] = actor;
|
||||||
|
}
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let local_p = p;
|
||||||
|
this.pending_undo.push(() => this.actors.splice(local_p, 0, actor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.actors.length = p;
|
||||||
|
|
||||||
|
// Advance the clock
|
||||||
|
let tic_counter = this.tic_counter;
|
||||||
|
this.tic_counter += 1;
|
||||||
|
if (this.time_remaining !== null && ! this.timer_paused) {
|
||||||
|
let time_remaining = this.time_remaining;
|
||||||
|
this.pending_undo.push(() => {
|
||||||
|
this.tic_counter = tic_counter;
|
||||||
|
this.time_remaining = time_remaining;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.time_remaining -= 1;
|
||||||
|
if (this.time_remaining <= 0) {
|
||||||
|
this.fail('time');
|
||||||
|
}
|
||||||
|
else if (this.time_remaining % 20 === 0 && this.time_remaining < 30 * 20) {
|
||||||
|
this.sfx.play_once('tick');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.pending_undo.push(() => {
|
||||||
|
this.tic_counter = tic_counter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -1339,15 +1332,15 @@ 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
|
||||||
entry.reverse();
|
entry.reverse();
|
||||||
|
|||||||
100
js/main.js
100
js/main.js
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -856,11 +858,7 @@ class Player extends PrimaryView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user