From 5c6cd01b3942aef4ee99859c279aea8d4b4a5a27 Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Sat, 26 Sep 2020 22:10:42 +1000 Subject: [PATCH 01/19] Implement turn based mode Seems to work mechanically though I haven't extensively stress tested it yet. Force floors work the way you'd want them to though (you're given control whenever you can make an input and not otherwise). There are some graphical bugs with rewinding, but there were some without turn based mode anyway... --- index.html | 1 + js/game.js | 442 +++++++++++++++++++++++++++++------------------------ js/main.js | 19 ++- 3 files changed, 257 insertions(+), 205 deletions(-) diff --git a/index.html b/index.html index ea5bf21..b805381 100644 --- a/index.html +++ b/index.html @@ -120,6 +120,7 @@ + Turn-Based
diff --git a/js/game.js b/js/game.js index df2f551..88a97e9 100644 --- a/js/game.js +++ b/js/game.js @@ -158,6 +158,7 @@ export class Level { this.height = stored_level.size_y; this.size_x = stored_level.size_x; this.size_y = stored_level.size_y; + this.turn_based = false; this.restart(compat); } @@ -185,7 +186,8 @@ export class Level { else { this.time_remaining = this.stored_level.time_limit * 20; } - this.timer_paused = false; + this.timer_paused = false + this.waiting_for_input = false // Note that this clock counts *up*, even on untimed levels, and is unaffected by CC2's // clock alteration shenanigans this.tic_counter = 0; @@ -349,10 +351,25 @@ export class Level { } // Commit the undo state at the end of each tic - this.commit(); + if (!this.waiting_for_input) { + this.commit(); + } } _advance_tic(p1_primary_direction, p1_secondary_direction) { + var skip_to_third_pass = false; + + //if we're waiting for input, then we want to skip straight to phase 3 with a player decision filled out when they have one ready + if (this.waiting_for_input) { + this.actor_decision(this.player, p1_primary_direction); + if (this.player.decision != null) { + skip_to_third_pass = true; + } + else { + return; + } + } + // 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 this._set_prop(this.player, 'secondary_direction', p1_secondary_direction); @@ -370,212 +387,69 @@ export class Level { // arrival as its own mini pass, for one reason: if the player dies (which will end the game // immediately), we still want every time's animation to finish, or it'll look like some // objects move backwards when the death screen appears! - let cell_steppers = []; - for (let actor of this.actors) { - // Actors with no cell were destroyed - if (! actor.cell) - continue; - - // Clear any old decisions ASAP. Note that this prop is only used internally within a - // single tic, so it doesn't need to be undoable - actor.decision = null; - - // Decrement the cooldown here, but don't check it quite yet, - // because stepping on cells in the next block might reset it - if (actor.movement_cooldown > 0) { - this._set_prop(actor, 'movement_cooldown', actor.movement_cooldown - 1); - } - - if (actor.animation_speed) { - // Deal with movement animation - this._set_prop(actor, 'animation_progress', actor.animation_progress + 1); - if (actor.animation_progress >= actor.animation_speed) { - if (actor.type.ttl) { - // This is purely an animation so it disappears once it's played - this.remove_tile(actor); - continue; - } - this._set_prop(actor, 'previous_cell', null); - this._set_prop(actor, 'animation_progress', null); - this._set_prop(actor, 'animation_speed', null); - if (! this.compat.tiles_react_instantly) { - // We need to track the actor AND the cell explicitly, because it's possible - // that one actor's step will cause another actor to start another move, and - // then they'd end up stepping on the new cell they're moving to instead of - // the one they just landed on! - cell_steppers.push([actor, actor.cell]); - } - } - } - } - for (let [actor, cell] of cell_steppers) { - this.step_on_cell(actor, cell); - } - - // Only reset the player's is_pushing between movement, so it lasts for the whole push - if (this.player.movement_cooldown <= 0) { - this.player.is_pushing = false; - } - - // Second pass: actors decide their upcoming movement simultaneously - for (let actor of this.actors) { - if (! actor.cell) - continue; - - if (actor.movement_cooldown > 0) - continue; - - // XXX does the cooldown drop while in a trap? is this even right? - if (actor.stuck && ! actor.type.is_player) - 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; - } - - let direction_preference; - // Actors can't make voluntary moves on ice, so they're stuck with - // whatever they've got - if (actor.slide_mode === 'ice') { - direction_preference = [actor.direction]; - } - 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) - { - direction_preference = [p1_primary_direction]; - this._set_prop(actor, 'last_move_was_force', false); - } - else { - direction_preference = [actor.direction]; - if (actor === this.player) { - this._set_prop(actor, 'last_move_was_force', true); - } - } - } - else if (actor === this.player) { - if (p1_primary_direction) { - direction_preference = [p1_primary_direction]; - this._set_prop(actor, 'last_move_was_force', false); - } - } - else if (actor.type.movement_mode === 'forward') { - // blue tank behavior: keep moving forward - direction_preference = [actor.direction]; - } - 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 - // TODO unclear if this is right in cc2 as well. definitely not in ms, which chooses a legal move - direction_preference = [actor.direction, ['north', 'south', 'east', 'west'][Math.floor(Math.random() * 4)]]; - } - 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 - // TODO cc2 has twiddles for how this works per-level, as well as the initial seed for demo playback - direction_preference = [['north', 'south', 'east', 'west'][Math.floor(Math.random() * 4)]]; - } - - // Check which of those directions we *can*, probably, move in - // TODO i think player on force floor will still have some issues here - if (direction_preference) { - // Players and sliding actors always move the way they want, even if blocked - if (actor.type.is_player || actor.slide_mode) { - actor.decision = direction_preference[0]; + if (!skip_to_third_pass) { + let cell_steppers = []; + for (let actor of this.actors) { + // Actors with no cell were destroyed + if (! actor.cell) continue; + + // Clear any old decisions ASAP. Note that this prop is only used internally within a + // single tic, so it doesn't need to be undoable + actor.decision = null; + + // Decrement the cooldown here, but don't check it quite yet, + // because stepping on cells in the next block might reset it + if (actor.movement_cooldown > 0) { + this._set_prop(actor, 'movement_cooldown', actor.movement_cooldown - 1); } - for (let direction of direction_preference) { - let dest_cell = this.cell_with_offset(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 (actor.animation_speed) { + // Deal with movement animation + this._set_prop(actor, 'animation_progress', actor.animation_progress + 1); + if (actor.animation_progress >= actor.animation_speed) { + if (actor.type.ttl) { + // This is purely an animation so it disappears once it's played + this.remove_tile(actor); + continue; + } + this._set_prop(actor, 'previous_cell', null); + this._set_prop(actor, 'animation_progress', null); + this._set_prop(actor, 'animation_speed', null); + if (! this.compat.tiles_react_instantly) { + // We need to track the actor AND the cell explicitly, because it's possible + // that one actor's step will cause another actor to start another move, and + // then they'd end up stepping on the new cell they're moving to instead of + // the one they just landed on! + cell_steppers.push([actor, actor.cell]); + } } } } + for (let [actor, cell] of cell_steppers) { + this.step_on_cell(actor, cell); + } + + // Only reset the player's is_pushing between movement, so it lasts for the whole push + if (this.player.movement_cooldown <= 0) { + this.player.is_pushing = false; + } + + // Second pass: actors decide their upcoming movement simultaneously + for (let actor of this.actors) { + this.actor_decision(actor, p1_primary_direction); + } + } + + //in Turn-Based mode, wait for input if the player can voluntarily move on tic_counter % 4 == 0 and isn't + if (this.turn_based && this.player.movement_cooldown == 0 && this.player.decision == null && this.tic_counter % 4 == 0) + { + this.waiting_for_input = true; + return; + } + else + { + this.waiting_for_input = false; } // Third pass: everyone actually moves @@ -664,6 +538,169 @@ export class Level { }); } } + + actor_decision(actor, p1_primary_direction) { + if (! actor.cell) + return; + + if (actor.movement_cooldown > 0) + return; + + // XXX does the cooldown drop while in a trap? is this even right? + if (actor.stuck && ! actor.type.is_player) + 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; + // Actors can't make voluntary moves on ice, so they're stuck with + // whatever they've got + if (actor.slide_mode === 'ice') { + direction_preference = [actor.direction]; + } + 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 || this.turn_based) && + actor.last_move_was_force) + { + if (p1_primary_direction != null) + { + direction_preference = [p1_primary_direction]; + this._set_prop(actor, 'last_move_was_force', false); + } + } + else { + direction_preference = [actor.direction]; + if (actor === this.player) { + this._set_prop(actor, 'last_move_was_force', true); + } + } + } + else if (actor === this.player) { + if (p1_primary_direction) { + direction_preference = [p1_primary_direction]; + this._set_prop(actor, 'last_move_was_force', false); + } + } + else if (actor.type.movement_mode === 'forward') { + // blue tank behavior: keep moving forward + direction_preference = [actor.direction]; + } + 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 + // TODO unclear if this is right in cc2 as well. definitely not in ms, which chooses a legal move + direction_preference = [actor.direction, ['north', 'south', 'east', 'west'][Math.floor(Math.random() * 4)]]; + } + 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 + // TODO cc2 has twiddles for how this works per-level, as well as the initial seed for demo playback + direction_preference = [['north', 'south', 'east', 'west'][Math.floor(Math.random() * 4)]]; + } + + // Check which of those directions we *can*, probably, move in + // TODO i think player on force floor will still have some issues here + if (direction_preference) { + // Players and sliding actors always move the way they want, even if blocked + if (actor.type.is_player || actor.slide_mode) { + actor.decision = direction_preference[0]; + return; + } + + for (let direction of direction_preference) { + let dest_cell = this.cell_with_offset(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; + } + } + } + } // Try to move the given actor one tile in the given direction and update // their cooldown. Return true if successful. @@ -945,6 +982,7 @@ export class Level { } undo() { + this.waiting_for_input = false; this.aid = Math.max(1, this.aid); let entry = this.undo_stack.pop(); diff --git a/js/main.js b/js/main.js index 8c865e2..773a2a8 100644 --- a/js/main.js +++ b/js/main.js @@ -397,6 +397,13 @@ class Player extends PrimaryView { 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; + this.level.turn_based = this.turn_based; + }); // Bind buttons this.pause_button = this.root.querySelector('.controls .control-pause'); @@ -809,9 +816,15 @@ class Player extends PrimaryView { // TODO this is not gonna be right while pausing lol // TODO i'm not sure it'll be right when rewinding either // 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.state === 'rewinding') { - this.tic_offset = 1 - this.tic_offset; + if (this.level.waiting_for_input) { + this.last_advance = performance.now(); + } + 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(); From 30a145599c353865f7d41520b644462bd7f33f9d Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Sat, 26 Sep 2020 22:18:19 +1000 Subject: [PATCH 02/19] fix a graphical undoing bug in Turn-Based Mode turns out we were smuggling pending_undo to previous moves. aha! --- js/game.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/game.js b/js/game.js index 88a97e9..45e7754 100644 --- a/js/game.js +++ b/js/game.js @@ -982,7 +982,9 @@ export class Level { } undo() { + //ok, actually we're not doing an in-progress move after all. this.waiting_for_input = false; + this.pending_undo = []; this.aid = Math.max(1, this.aid); let entry = this.undo_stack.pop(); From d2e900dc3a55ae4569014a59137b45d2024dc3f4 Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Sat, 26 Sep 2020 22:31:18 +1000 Subject: [PATCH 03/19] fix a rewind bug in non turn based mode Was caused by not making these setters undoable. --- js/game.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/js/game.js b/js/game.js index 45e7754..9f82422 100644 --- a/js/game.js +++ b/js/game.js @@ -832,10 +832,9 @@ export class Level { if (actor.cell === goal_cell) return; - // TODO undo this stuff? - actor.previous_cell = actor.cell; - actor.animation_speed = speed; - actor.animation_progress = 0; + this._set_prop(actor, 'previous_cell', actor.cell); + this._set_prop(actor, 'animation_speed', speed); + this._set_prop(actor, 'animation_progress', 0); let original_cell = actor.cell; this.remove_tile(actor); From e6a4e8893558cea88cca9eeae28b8061c2e982b3 Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Sat, 26 Sep 2020 22:32:01 +1000 Subject: [PATCH 04/19] spaceify previous commit --- js/game.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/game.js b/js/game.js index 9f82422..3697033 100644 --- a/js/game.js +++ b/js/game.js @@ -832,9 +832,9 @@ export class Level { if (actor.cell === goal_cell) return; - this._set_prop(actor, 'previous_cell', actor.cell); - this._set_prop(actor, 'animation_speed', speed); - this._set_prop(actor, 'animation_progress', 0); + this._set_prop(actor, 'previous_cell', actor.cell); + this._set_prop(actor, 'animation_speed', speed); + this._set_prop(actor, 'animation_progress', 0); let original_cell = actor.cell; this.remove_tile(actor); From 5b7273e9d9948faacaf621f8c19c0cfe620d2e4e Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Sat, 26 Sep 2020 22:40:38 +1000 Subject: [PATCH 05/19] Turn-Based: add space to wait --- js/game.js | 10 +++++----- js/main.js | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/js/game.js b/js/game.js index 3697033..216a9e9 100644 --- a/js/game.js +++ b/js/game.js @@ -332,14 +332,14 @@ export class Level { } // Move the game state forwards by one tic - advance_tic(p1_primary_direction, p1_secondary_direction) { + advance_tic(p1_primary_direction, p1_secondary_direction, force_wait) { if (this.state !== 'playing') { console.warn(`Level.advance_tic() called when state is ${this.state}`); return; } try { - this._advance_tic(p1_primary_direction, p1_secondary_direction); + this._advance_tic(p1_primary_direction, p1_secondary_direction, force_wait); } catch (e) { if (e instanceof GameEnded) { @@ -356,13 +356,13 @@ export class Level { } } - _advance_tic(p1_primary_direction, p1_secondary_direction) { + _advance_tic(p1_primary_direction, p1_secondary_direction, force_wait) { var skip_to_third_pass = false; //if we're waiting for input, then we want to skip straight to phase 3 with a player decision filled out when they have one ready if (this.waiting_for_input) { this.actor_decision(this.player, p1_primary_direction); - if (this.player.decision != null) { + if (this.player.decision != null || force_wait) { skip_to_third_pass = true; } else { @@ -442,7 +442,7 @@ export class Level { } //in Turn-Based mode, wait for input if the player can voluntarily move on tic_counter % 4 == 0 and isn't - if (this.turn_based && this.player.movement_cooldown == 0 && this.player.decision == null && this.tic_counter % 4 == 0) + if (this.turn_based && this.player.movement_cooldown == 0 && this.player.decision == null && (this.tic_counter % 4 == 0) && !force_wait) { this.waiting_for_input = true; return; diff --git a/js/main.js b/js/main.js index 773a2a8..d20d983 100644 --- a/js/main.js +++ b/js/main.js @@ -341,6 +341,8 @@ class Player extends PrimaryView { ArrowRight: 'right', ArrowUp: 'up', ArrowDown: 'down', + Spacebar: 'wait', + " ": 'wait', w: 'up', a: 'left', s: 'down', @@ -764,6 +766,7 @@ class Player extends PrimaryView { this.level.advance_tic( this.primary_action ? ACTION_DIRECTIONS[this.primary_action] : null, this.secondary_action ? ACTION_DIRECTIONS[this.secondary_action] : null, + input.has('wait') ); if (this.level.state !== 'playing') { From e908434a20798883a0b23b4e762e8507f2af360f Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Sat, 26 Sep 2020 23:01:42 +1000 Subject: [PATCH 06/19] only restart on fresh press of spacebar --- js/main.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/main.js b/js/main.js index d20d983..430cfe0 100644 --- a/js/main.js +++ b/js/main.js @@ -530,7 +530,9 @@ class Player extends PrimaryView { } else { // Restart - this.restart_level(); + if (!this.current_keys.has(ev.key)) { + this.restart_level(); + } } return; } From ccfd5c30ce234b4cd0d8858d2ca556c6e21db05a Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Sat, 26 Sep 2020 23:45:53 +1000 Subject: [PATCH 07/19] fix animation ugliness in turn-based mode Uguhughugh it looks SO SMOOTH NOW. I can go to bed happy. --- js/main.js | 12 ++++++++++-- js/renderer-canvas.js | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/js/main.js b/js/main.js index 430cfe0..a430b17 100644 --- a/js/main.js +++ b/js/main.js @@ -787,8 +787,9 @@ class Player extends PrimaryView { this._advance_handle = null; return; } - + this.last_advance = performance.now(); + if (this.state === 'playing') { this.advance_by(1); } @@ -806,6 +807,13 @@ class Player extends PrimaryView { this.update_ui(); } } + + if (this.level.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; if (this.state === 'rewinding') { // Rewind faster than normal time @@ -822,7 +830,7 @@ class Player extends PrimaryView { // TODO i'm not sure it'll be right when rewinding either // TODO or if the game's speed changes. wow! if (this.level.waiting_for_input) { - this.last_advance = performance.now(); + //freeze tic_offset in time } else { diff --git a/js/renderer-canvas.js b/js/renderer-canvas.js index 575bda3..6bcd447 100644 --- a/js/renderer-canvas.js +++ b/js/renderer-canvas.js @@ -58,7 +58,7 @@ export class CanvasRenderer { return; } - let tic = (this.level.tic_counter ?? 0) + tic_offset; + let tic = (this.level.tic_counter ?? 0) + tic_offset + (this.level.waiting_for_input ? 1 : 0); let tw = this.tileset.size_x; let th = this.tileset.size_y; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); From c8d80dfc63a25e8c7dfa85cca1d3199bf27ec6b6 Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Sat, 26 Sep 2020 23:48:47 +1000 Subject: [PATCH 08/19] another spaceify (oops) I literally tried to change the setting for this in notepad++ but it crashes every time I do ha ha --- js/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/main.js b/js/main.js index a430b17..0a64077 100644 --- a/js/main.js +++ b/js/main.js @@ -810,7 +810,7 @@ class Player extends PrimaryView { if (this.level.waiting_for_input) { - //freeze tic_offset in time so we don't try to interpolate to the next frame too soon + //freeze tic_offset in time so we don't try to interpolate to the next frame too soon this.tic_offset = 0; } From 2e1a87199a507cd777cdfbf50b6f4b1628ef3b9c Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 21:24:46 +1100 Subject: [PATCH 09/19] code refactor part 1: advance_tic is now two parts seems to work so far --- js/game.js | 156 ++++++++++++++++++++++++++--------------------------- js/main.js | 42 +++++++++++++-- 2 files changed, 112 insertions(+), 86 deletions(-) diff --git a/js/game.js b/js/game.js index 216a9e9..5276b7b 100644 --- a/js/game.js +++ b/js/game.js @@ -187,7 +187,6 @@ export class Level { this.time_remaining = this.stored_level.time_limit * 20; } this.timer_paused = false - this.waiting_for_input = false // Note that this clock counts *up*, even on untimed levels, and is unaffected by CC2's // clock alteration shenanigans this.tic_counter = 0; @@ -331,15 +330,32 @@ export class Level { } } + player_awaiting_input() { + //todo: the tic_counter part feels kludgey. maybe there's some other way a wait/wall nudge can wait a certain amount of time per tap? + return this.player.movement_cooldown === 0 && this.player.slide_mode === null && this.tic_counter % 2 == 0 + } + // Move the game state forwards by one tic - advance_tic(p1_primary_direction, p1_secondary_direction, force_wait) { + // 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') { console.warn(`Level.advance_tic() called when state is ${this.state}`); return; } try { - this._advance_tic(p1_primary_direction, p1_secondary_direction, force_wait); + 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) { if (e instanceof GameEnded) { @@ -350,26 +366,13 @@ export class Level { } } - // Commit the undo state at the end of each tic - if (!this.waiting_for_input) { + // Commit the undo state at the end of each tic (pass 2) + if (pass == 2) { this.commit(); } } - _advance_tic(p1_primary_direction, p1_secondary_direction, force_wait) { - var skip_to_third_pass = false; - - //if we're waiting for input, then we want to skip straight to phase 3 with a player decision filled out when they have one ready - if (this.waiting_for_input) { - this.actor_decision(this.player, p1_primary_direction); - if (this.player.decision != null || force_wait) { - skip_to_third_pass = true; - } - else { - return; - } - } - + _advance_tic_part1(p1_primary_direction, p1_secondary_direction) { // 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 this._set_prop(this.player, 'secondary_direction', p1_secondary_direction); @@ -387,71 +390,64 @@ export class Level { // arrival as its own mini pass, for one reason: if the player dies (which will end the game // immediately), we still want every time's animation to finish, or it'll look like some // objects move backwards when the death screen appears! - if (!skip_to_third_pass) { - let cell_steppers = []; - for (let actor of this.actors) { - // Actors with no cell were destroyed - if (! actor.cell) - continue; + let cell_steppers = []; + for (let actor of this.actors) { + // Actors with no cell were destroyed + if (! actor.cell) + continue; - // Clear any old decisions ASAP. Note that this prop is only used internally within a - // single tic, so it doesn't need to be undoable - actor.decision = null; + // Clear any old decisions ASAP. Note that this prop is only used internally within a + // single tic, so it doesn't need to be undoable + actor.decision = null; - // Decrement the cooldown here, but don't check it quite yet, - // because stepping on cells in the next block might reset it - if (actor.movement_cooldown > 0) { - this._set_prop(actor, 'movement_cooldown', actor.movement_cooldown - 1); - } + // Decrement the cooldown here, but don't check it quite yet, + // because stepping on cells in the next block might reset it + if (actor.movement_cooldown > 0) { + this._set_prop(actor, 'movement_cooldown', actor.movement_cooldown - 1); + } - if (actor.animation_speed) { - // Deal with movement animation - this._set_prop(actor, 'animation_progress', actor.animation_progress + 1); - if (actor.animation_progress >= actor.animation_speed) { - if (actor.type.ttl) { - // This is purely an animation so it disappears once it's played - this.remove_tile(actor); - continue; - } - this._set_prop(actor, 'previous_cell', null); - this._set_prop(actor, 'animation_progress', null); - this._set_prop(actor, 'animation_speed', null); - if (! this.compat.tiles_react_instantly) { - // We need to track the actor AND the cell explicitly, because it's possible - // that one actor's step will cause another actor to start another move, and - // then they'd end up stepping on the new cell they're moving to instead of - // the one they just landed on! - cell_steppers.push([actor, actor.cell]); - } - } - } - } - for (let [actor, cell] of cell_steppers) { - this.step_on_cell(actor, cell); - } + if (actor.animation_speed) { + // Deal with movement animation + this._set_prop(actor, 'animation_progress', actor.animation_progress + 1); + if (actor.animation_progress >= actor.animation_speed) { + if (actor.type.ttl) { + // This is purely an animation so it disappears once it's played + this.remove_tile(actor); + continue; + } + this._set_prop(actor, 'previous_cell', null); + this._set_prop(actor, 'animation_progress', null); + this._set_prop(actor, 'animation_speed', null); + if (! this.compat.tiles_react_instantly) { + // We need to track the actor AND the cell explicitly, because it's possible + // that one actor's step will cause another actor to start another move, and + // then they'd end up stepping on the new cell they're moving to instead of + // the one they just landed on! + cell_steppers.push([actor, actor.cell]); + } + } + } + } + for (let [actor, cell] of cell_steppers) { + this.step_on_cell(actor, cell); + } - // Only reset the player's is_pushing between movement, so it lasts for the whole push - if (this.player.movement_cooldown <= 0) { - this.player.is_pushing = false; - } - - // Second pass: actors decide their upcoming movement simultaneously - for (let actor of this.actors) { - this.actor_decision(actor, p1_primary_direction); - } - } - - //in Turn-Based mode, wait for input if the player can voluntarily move on tic_counter % 4 == 0 and isn't - if (this.turn_based && this.player.movement_cooldown == 0 && this.player.decision == null && (this.tic_counter % 4 == 0) && !force_wait) - { - this.waiting_for_input = true; - return; - } - else - { - this.waiting_for_input = false; - } + // Only reset the player's is_pushing between movement, so it lasts for the whole push + if (this.player.movement_cooldown <= 0) { + this.player.is_pushing = false; + } + // Second pass: actors decide their upcoming movement simultaneously + for (let actor of this.actors) { + 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 actor of this.actors) { if (! actor.cell) @@ -981,8 +977,6 @@ export class Level { } undo() { - //ok, actually we're not doing an in-progress move after all. - this.waiting_for_input = false; this.pending_undo = []; this.aid = Math.max(1, this.aid); diff --git a/js/main.js b/js/main.js index 0a64077..7c3aa8d 100644 --- a/js/main.js +++ b/js/main.js @@ -702,6 +702,8 @@ class Player extends PrimaryView { return input; } } + + waiting_for_input = false; advance_by(tics) { for (let i = 0; i < tics; i++) { @@ -765,11 +767,41 @@ class Player extends PrimaryView { this.previous_input = input; this.sfx_player.advance_tic(); - this.level.advance_tic( - this.primary_action ? ACTION_DIRECTIONS[this.primary_action] : null, - this.secondary_action ? ACTION_DIRECTIONS[this.secondary_action] : null, - input.has('wait') - ); + + var primary_dir = this.primary_action ? ACTION_DIRECTIONS[this.primary_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.level.advance_tic( + primary_dir, + secondary_dir, + 2); + } + } + else //TODO: or `if (!this.waiting_for_input)` to be snappier + { + 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') { // We either won or lost! From a8ce3bca115c4311d05c17d52e43e56ecdef2ece Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 21:42:51 +1100 Subject: [PATCH 10/19] fix bugs we're back at parity now, it looks like --- js/main.js | 8 ++++++-- js/renderer-canvas.js | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/js/main.js b/js/main.js index 7c3aa8d..77651e9 100644 --- a/js/main.js +++ b/js/main.js @@ -655,6 +655,7 @@ class Player extends PrimaryView { _clear_state() { this.set_state('waiting'); + this.waiting_for_input = false; this.tic_offset = 0; this.last_advance = 0; this.demo_faucet = null; @@ -777,10 +778,12 @@ class Player extends PrimaryView { { 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 //TODO: or `if (!this.waiting_for_input)` to be snappier @@ -840,7 +843,7 @@ class Player extends PrimaryView { } } - if (this.level.waiting_for_input) + 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; @@ -861,7 +864,7 @@ class Player extends PrimaryView { // TODO this is not gonna be right while pausing lol // TODO i'm not sure it'll be right when rewinding either // TODO or if the game's speed changes. wow! - if (this.level.waiting_for_input) { + if (this.waiting_for_input) { //freeze tic_offset in time } else @@ -887,6 +890,7 @@ class Player extends PrimaryView { // Actually redraw. Used to force drawing outside of normal play _redraw() { + this.renderer.waiting_for_input = this.waiting_for_input; this.renderer.draw(this.tic_offset); } diff --git a/js/renderer-canvas.js b/js/renderer-canvas.js index 6bcd447..0339c7b 100644 --- a/js/renderer-canvas.js +++ b/js/renderer-canvas.js @@ -51,6 +51,8 @@ export class CanvasRenderer { sx * tw, sy * th, w * tw, h * th, dx * tw, dy * th, w * tw, h * th); } + + waiting_for_input = false; draw(tic_offset = 0) { if (! this.level) { @@ -58,7 +60,7 @@ export class CanvasRenderer { return; } - let tic = (this.level.tic_counter ?? 0) + tic_offset + (this.level.waiting_for_input ? 1 : 0); + let tic = (this.level.tic_counter ?? 0) + tic_offset + (this.waiting_for_input ? 1 : 0); let tw = this.tileset.size_x; let th = this.tileset.size_y; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); From e53f00a432781ffc2e3ce3e059ea1de21616cdc7 Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 21:54:13 +1100 Subject: [PATCH 11/19] I broke force arrows, in fact! so much for tooting my own horn --- js/game.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/js/game.js b/js/game.js index 5276b7b..b30c149 100644 --- a/js/game.js +++ b/js/game.js @@ -332,7 +332,7 @@ export class Level { player_awaiting_input() { //todo: the tic_counter part feels kludgey. maybe there's some other way a wait/wall nudge can wait a certain amount of time per tap? - return this.player.movement_cooldown === 0 && this.player.slide_mode === null && this.tic_counter % 2 == 0 + return this.player.movement_cooldown === 0 && (this.player.slide_mode === null || (this.player.slide_mode === 'force' && this.player.last_move_was_force)) && this.tic_counter % 2 == 0 } // Move the game state forwards by one tic @@ -438,8 +438,12 @@ export class Level { } // Second pass: actors decide their upcoming movement simultaneously + // (we'll do the player's decision in part 2!) for (let actor of this.actors) { - this.actor_decision(actor, p1_primary_direction); + if (actor != this.player) + { + this.actor_decision(actor, p1_primary_direction); + } } } From bb168d7e1e655e4070d3beae35a65de99d357747 Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 22:07:29 +1100 Subject: [PATCH 12/19] fix force arrows in turn based mode if you got misaligned in tic_counter then you'd force arrow forever. also, turn_based is no longer passed in! yay! --- js/game.js | 6 ++---- js/main.js | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/js/game.js b/js/game.js index b30c149..ce68a15 100644 --- a/js/game.js +++ b/js/game.js @@ -158,7 +158,6 @@ export class Level { this.height = stored_level.size_y; this.size_x = stored_level.size_x; this.size_y = stored_level.size_y; - this.turn_based = false; this.restart(compat); } @@ -331,8 +330,7 @@ export class Level { } player_awaiting_input() { - //todo: the tic_counter part feels kludgey. maybe there's some other way a wait/wall nudge can wait a certain amount of time per tap? - return this.player.movement_cooldown === 0 && (this.player.slide_mode === null || (this.player.slide_mode === 'force' && this.player.last_move_was_force)) && this.tic_counter % 2 == 0 + return this.player.movement_cooldown === 0 && (this.player.slide_mode === null || (this.player.slide_mode === 'force' && this.player.last_move_was_force)) } // Move the game state forwards by one tic @@ -573,7 +571,7 @@ export class Level { // can override forwards??) and DEFINITELY all kinds of stuff // in ms if (actor === this.player && - (p1_primary_direction || this.turn_based) && + p1_primary_direction && actor.last_move_was_force) { if (p1_primary_direction != null) diff --git a/js/main.js b/js/main.js index 77651e9..076498f 100644 --- a/js/main.js +++ b/js/main.js @@ -404,7 +404,6 @@ class Player extends PrimaryView { this.turn_based_checkbox = this.root.querySelector('.controls .turn-based'); this.turn_based_checkbox.addEventListener('change', ev => { this.turn_based = !this.turn_based; - this.level.turn_based = this.turn_based; }); // Bind buttons From a7c38ae0af83159ac98373b39521a276a1804a51 Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 22:17:07 +1100 Subject: [PATCH 13/19] fix 'keys held for less than a frame are ignored' bug this was annoying me! --- js/main.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/js/main.js b/js/main.js index 076498f..da6caea 100644 --- a/js/main.js +++ b/js/main.js @@ -509,6 +509,7 @@ class Player extends PrimaryView { this.previous_input = new Set; // actions that were held last tic this.previous_action = null; // last direction we were moving, if any 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 key_target.addEventListener('keydown', ev => { if (ev.key === 'p' || ev.key === 'Pause') { @@ -549,6 +550,7 @@ class Player extends PrimaryView { if (this.key_mapping[ev.key]) { this.current_keys.add(ev.key); + this.current_keys_new.add(ev.key); ev.stopPropagation(); ev.preventDefault(); @@ -699,6 +701,10 @@ class Player extends PrimaryView { for (let key of this.current_keys) { 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; return input; } } From e9d542f4385adf118b5c86ad1ef255c84af03e12 Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 22:22:27 +1100 Subject: [PATCH 14/19] fix a bug when undoing while waiting for input need to unset it (we used to do this but it got lost in the refactor) --- js/main.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/js/main.js b/js/main.js index da6caea..427f17b 100644 --- a/js/main.js +++ b/js/main.js @@ -428,7 +428,7 @@ class Player extends PrimaryView { while (this.level.undo_stack.length > 0 && ! (moved && this.level.player.slide_mode === null)) { - this.level.undo(); + this.undo(); if (player_cell !== this.level.player.cell) { moved = true; } @@ -843,7 +843,7 @@ class Player extends PrimaryView { } else { // Rewind by undoing one tic every tic - this.level.undo(); + this.undo(); this.update_ui(); } } @@ -862,6 +862,12 @@ class Player extends PrimaryView { 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 redraw() { // Calculate this here, not in _redraw, because that's called at weird From be5cc7f97fef67bfe024ad49dd8fdf9b4e9ca462 Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 22:23:13 +1100 Subject: [PATCH 15/19] spaceify --- js/game.js | 146 +++++++++++++++++++++--------------------- js/main.js | 104 +++++++++++++++--------------- js/renderer-canvas.js | 4 +- 3 files changed, 127 insertions(+), 127 deletions(-) diff --git a/js/game.js b/js/game.js index ce68a15..c9dfd1b 100644 --- a/js/game.js +++ b/js/game.js @@ -329,12 +329,12 @@ 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)) - } + 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)) + } // 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 + // 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') { console.warn(`Level.advance_tic() called when state is ${this.state}`); @@ -342,18 +342,18 @@ export class Level { } try { - 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?`); - } + 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) { if (e instanceof GameEnded) { @@ -388,68 +388,68 @@ export class Level { // arrival as its own mini pass, for one reason: if the player dies (which will end the game // immediately), we still want every time's animation to finish, or it'll look like some // objects move backwards when the death screen appears! - let cell_steppers = []; - for (let actor of this.actors) { - // Actors with no cell were destroyed - if (! actor.cell) - continue; + let cell_steppers = []; + for (let actor of this.actors) { + // Actors with no cell were destroyed + if (! actor.cell) + continue; - // Clear any old decisions ASAP. Note that this prop is only used internally within a - // single tic, so it doesn't need to be undoable - actor.decision = null; + // Clear any old decisions ASAP. Note that this prop is only used internally within a + // single tic, so it doesn't need to be undoable + actor.decision = null; - // Decrement the cooldown here, but don't check it quite yet, - // because stepping on cells in the next block might reset it - if (actor.movement_cooldown > 0) { - this._set_prop(actor, 'movement_cooldown', actor.movement_cooldown - 1); - } + // Decrement the cooldown here, but don't check it quite yet, + // because stepping on cells in the next block might reset it + if (actor.movement_cooldown > 0) { + this._set_prop(actor, 'movement_cooldown', actor.movement_cooldown - 1); + } - if (actor.animation_speed) { - // Deal with movement animation - this._set_prop(actor, 'animation_progress', actor.animation_progress + 1); - if (actor.animation_progress >= actor.animation_speed) { - if (actor.type.ttl) { - // This is purely an animation so it disappears once it's played - this.remove_tile(actor); - continue; - } - this._set_prop(actor, 'previous_cell', null); - this._set_prop(actor, 'animation_progress', null); - this._set_prop(actor, 'animation_speed', null); - if (! this.compat.tiles_react_instantly) { - // We need to track the actor AND the cell explicitly, because it's possible - // that one actor's step will cause another actor to start another move, and - // then they'd end up stepping on the new cell they're moving to instead of - // the one they just landed on! - cell_steppers.push([actor, actor.cell]); - } - } - } - } - for (let [actor, cell] of cell_steppers) { - this.step_on_cell(actor, cell); - } + if (actor.animation_speed) { + // Deal with movement animation + this._set_prop(actor, 'animation_progress', actor.animation_progress + 1); + if (actor.animation_progress >= actor.animation_speed) { + if (actor.type.ttl) { + // This is purely an animation so it disappears once it's played + this.remove_tile(actor); + continue; + } + this._set_prop(actor, 'previous_cell', null); + this._set_prop(actor, 'animation_progress', null); + this._set_prop(actor, 'animation_speed', null); + if (! this.compat.tiles_react_instantly) { + // We need to track the actor AND the cell explicitly, because it's possible + // that one actor's step will cause another actor to start another move, and + // then they'd end up stepping on the new cell they're moving to instead of + // the one they just landed on! + cell_steppers.push([actor, actor.cell]); + } + } + } + } + for (let [actor, cell] of cell_steppers) { + this.step_on_cell(actor, cell); + } - // Only reset the player's is_pushing between movement, so it lasts for the whole push - if (this.player.movement_cooldown <= 0) { - this.player.is_pushing = false; - } + // Only reset the player's is_pushing between movement, so it lasts for the whole push + if (this.player.movement_cooldown <= 0) { + this.player.is_pushing = false; + } - // Second pass: actors decide their upcoming movement simultaneously - // (we'll do the player's decision in part 2!) - for (let actor of this.actors) { - 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); - + // Second pass: actors decide their upcoming movement simultaneously + // (we'll do the player's decision in part 2!) + for (let actor of this.actors) { + 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 actor of this.actors) { if (! actor.cell) diff --git a/js/main.js b/js/main.js index 427f17b..192d463 100644 --- a/js/main.js +++ b/js/main.js @@ -428,7 +428,7 @@ class Player extends PrimaryView { while (this.level.undo_stack.length > 0 && ! (moved && this.level.player.slide_mode === null)) { - this.undo(); + this.undo(); if (player_cell !== this.level.player.cell) { moved = true; } @@ -509,7 +509,7 @@ class Player extends PrimaryView { this.previous_input = new Set; // actions that were held last tic this.previous_action = null; // last direction we were moving, if any 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; //for keys that have only been held a frame // TODO this could all probably be more rigorous but it's fine for now key_target.addEventListener('keydown', ev => { if (ev.key === 'p' || ev.key === 'Pause') { @@ -550,7 +550,7 @@ class Player extends PrimaryView { if (this.key_mapping[ev.key]) { this.current_keys.add(ev.key); - this.current_keys_new.add(ev.key); + this.current_keys_new.add(ev.key); ev.stopPropagation(); ev.preventDefault(); @@ -656,7 +656,7 @@ class Player extends PrimaryView { _clear_state() { this.set_state('waiting'); - this.waiting_for_input = false; + this.waiting_for_input = false; this.tic_offset = 0; this.last_advance = 0; this.demo_faucet = null; @@ -701,15 +701,15 @@ class Player extends PrimaryView { for (let key of this.current_keys) { input.add(this.key_mapping[key]); } - for (let key of this.current_keys_new) { + for (let key of this.current_keys_new) { input.add(this.key_mapping[key]); } - this.current_keys_new = new Set; + this.current_keys_new = new Set; return input; } } - - waiting_for_input = false; + + waiting_for_input = false; advance_by(tics) { for (let i = 0; i < tics; i++) { @@ -773,43 +773,43 @@ class Player extends PrimaryView { this.previous_input = input; this.sfx_player.advance_tic(); - - var primary_dir = this.primary_action ? ACTION_DIRECTIONS[this.primary_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 //TODO: or `if (!this.waiting_for_input)` to be snappier - { - 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); - } - } + + var primary_dir = this.primary_action ? ACTION_DIRECTIONS[this.primary_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 //TODO: or `if (!this.waiting_for_input)` to be snappier + { + 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') { // We either won or lost! @@ -843,7 +843,7 @@ class Player extends PrimaryView { } else { // Rewind by undoing one tic every tic - this.undo(); + this.undo(); this.update_ui(); } } @@ -862,11 +862,11 @@ class Player extends PrimaryView { 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(); - } + 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 redraw() { @@ -901,7 +901,7 @@ class Player extends PrimaryView { // Actually redraw. Used to force drawing outside of normal play _redraw() { - this.renderer.waiting_for_input = this.waiting_for_input; + this.renderer.waiting_for_input = this.waiting_for_input; this.renderer.draw(this.tic_offset); } diff --git a/js/renderer-canvas.js b/js/renderer-canvas.js index 0339c7b..c353945 100644 --- a/js/renderer-canvas.js +++ b/js/renderer-canvas.js @@ -51,8 +51,8 @@ export class CanvasRenderer { sx * tw, sy * th, w * tw, h * th, dx * tw, dy * th, w * tw, h * th); } - - waiting_for_input = false; + + waiting_for_input = false; draw(tic_offset = 0) { if (! this.level) { From f7e83342a013c541d8a48115e5987e4cfc3d5e3b Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 22:33:45 +1100 Subject: [PATCH 16/19] fix a turn based rewinding visual bug --- js/game.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/js/game.js b/js/game.js index c9dfd1b..1959541 100644 --- a/js/game.js +++ b/js/game.js @@ -979,7 +979,13 @@ export class Level { } 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); let entry = this.undo_stack.pop(); From e3de4d59c7f197bc2a864fcc96d6cabf4ff8c4ce Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 22:34:00 +1100 Subject: [PATCH 17/19] spaceify --- js/game.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/game.js b/js/game.js index 1959541..778896c 100644 --- a/js/game.js +++ b/js/game.js @@ -979,13 +979,13 @@ export class Level { } undo() { - //reverse the pending_undo too + //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); let entry = this.undo_stack.pop(); From 9e53aa75a0ccd73904dbac5a447a0bda5ad545bf Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Wed, 14 Oct 2020 23:13:38 +1100 Subject: [PATCH 18/19] I tried 'snappier' and it was a bad idea, so now you don't have to it basically skips the first frame of each turn-based movement. it's not TERRIBLE but the smooth movement is already really nice, we don't need to change it --- js/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/main.js b/js/main.js index 090b8e2..adf0453 100644 --- a/js/main.js +++ b/js/main.js @@ -796,7 +796,7 @@ class Player extends PrimaryView { } } - else //TODO: or `if (!this.waiting_for_input)` to be snappier + else { this.level.advance_tic( primary_dir, From f670224460c5febbec677b6cfa69169580f8010d Mon Sep 17 00:00:00 2001 From: Timothy Stiles Date: Mon, 26 Oct 2020 16:08:29 +1100 Subject: [PATCH 19/19] forgot to change a continue to a return --- js/game.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/game.js b/js/game.js index 70d7e1a..40448fa 100644 --- a/js/game.js +++ b/js/game.js @@ -650,7 +650,7 @@ export class Level { if (actor.pending_push) { actor.decision = actor.pending_push; this._set_prop(actor, 'pending_push', null); - continue; + return; } if (actor.slide_mode === 'ice') { // Actors can't make voluntary moves on ice; they just slide