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...
This commit is contained in:
Timothy Stiles 2020-09-26 22:10:42 +10:00
parent 12066072ec
commit 5c6cd01b39
3 changed files with 257 additions and 205 deletions

View File

@ -120,6 +120,7 @@
<button class="control-restart" type="button">Restart</button>
<button class="control-undo" type="button">Undo</button>
<button class="control-rewind" type="button">Rewind (z)</button>
<input class="turn-based" type="checkbox">Turn-Based</input>
</div>
<div class="demo-controls">
<button class="demo-play" type="button">View replay</button>

View File

@ -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
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,6 +387,7 @@ 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
@ -418,16 +436,119 @@ export class Level {
}
// 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
for (let actor of this.actors) {
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.cell_with_offset(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)) {
// 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;
// XXX does the cooldown drop while in a trap? is this even right?
if (actor.stuck && ! actor.type.is_player)
continue;
return;
// Teeth can only move the first 4 of every 8 tics, though "first"
// can be adjusted
@ -435,7 +556,7 @@ export class Level {
actor.type.uses_teeth_hesitation &&
(this.tic_counter + this.step_parity) % 8 >= 4)
{
continue;
return;
}
let direction_preference;
@ -452,12 +573,15 @@ export class Level {
// can override forwards??) and DEFINITELY all kinds of stuff
// in ms
if (actor === this.player &&
p1_primary_direction &&
(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) {
@ -559,7 +683,7 @@ export class Level {
// 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];
continue;
return;
}
for (let direction of direction_preference) {
@ -578,93 +702,6 @@ export class Level {
}
}
// Third pass: everyone actually moves
for (let actor of this.actors) {
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.cell_with_offset(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)) {
// 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
// their cooldown. Return true if successful.
attempt_step(actor, direction) {
@ -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();

View File

@ -398,6 +398,13 @@ class Player extends PrimaryView {
}
});
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');
this.pause_button.addEventListener('click', ev => {
@ -809,10 +816,16 @@ 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) {
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();