Styled the whole page; reimplemented pausing; implemented success, score, and time

This commit is contained in:
Eevee (Evelyn Woods) 2020-08-31 10:27:29 -06:00
parent b871181bf4
commit 0390d54909
4 changed files with 312 additions and 76 deletions

BIN
button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

View File

@ -11,7 +11,7 @@ function mk(tag_selector, ...children) {
let el = document.createElement(tag); let el = document.createElement(tag);
el.classList = classes.join(' '); el.classList = classes.join(' ');
if (children.length > 0) { if (children.length > 0) {
if (!(children[0] instanceof Node) && typeof(children[0]) !== "string" && typeof(children[0]) !== "number") { if (!(children[0] instanceof Node) && children[0] !== undefined && typeof(children[0]) !== "string" && typeof(children[0]) !== "number") {
let [attrs] = children.splice(0, 1); let [attrs] = children.splice(0, 1);
for (let [key, value] of Object.entries(attrs)) { for (let [key, value] of Object.entries(attrs)) {
el.setAttribute(key, value); el.setAttribute(key, value);
@ -235,7 +235,8 @@ class Level {
// playing: normal play // playing: normal play
// success: has been won // success: has been won
// failure: died // failure: died
// paused: paused // note that pausing is NOT handled here, but by whatever's driving our
// event loop!
this.state = 'playing'; this.state = 'playing';
} }
@ -244,6 +245,14 @@ class Level {
this.player = null; this.player = null;
this.actors = []; this.actors = [];
this.chips_remaining = this.stored_level.chips_required; this.chips_remaining = this.stored_level.chips_required;
if (this.stored_level.time_limit === 0) {
this.time_remaining = null;
}
else {
this.time_remaining = this.stored_level.time_limit;
}
this.bonus_points = 0;
this.tic_counter = 0;
this.hint_shown = null; this.hint_shown = null;
@ -282,9 +291,8 @@ class Level {
advance_tic(player_direction) { advance_tic(player_direction) {
if (this.state !== 'playing') { if (this.state !== 'playing') {
// FIXME this breaks the step buttons; maybe pausing should be in game only console.warn(`Level.advance_tic() called when state is ${this.state}`);
//console.warn(`Level.advance_tic() called when state is ${this.state}`); return;
//return;
} }
// XXX this entire turn order is rather different in ms rules // XXX this entire turn order is rather different in ms rules
@ -388,6 +396,17 @@ class Level {
if (this.state === 'success' || this.state === 'failure') if (this.state === 'success' || this.state === 'failure')
break; break;
} }
if (this.time_remaining !== null) {
this.tic_counter++;
while (this.tic_counter > 20) {
this.tic_counter -= 20;
this.time_remaining -= 1;
if (this.time_remaining <= 0) {
this.fail("Time's up!");
}
}
}
} }
fail(message) { fail(message) {
@ -395,6 +414,10 @@ class Level {
this.fail_message = message; this.fail_message = message;
} }
win() {
this.state = 'success';
}
// 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) {
@ -526,34 +549,58 @@ class Level {
// TODO: // TODO:
// - some kinda visual theme i guess lol // - some kinda visual theme i guess lol
// - level /number/
// - level password, if any // - level password, if any
// - set name // - timer!!!!!
// - timer! // - bonus points (cc2 only, or maybe only if got any so far this level)
// - intro splash with list of available levels // - intro splash with list of available level packs
// - button: quit to splash // - button: quit to splash
// - button: options // - button: options
// - implement winning and show score for this level // - implement winning and show score for this level
// - show current score so far // - show current score so far
// - about, help
const GAME_UI_HTML = ` const GAME_UI_HTML = `
<header>
<h1>Lexy's Labyrinth</h1>
<nav>
<button class="nav-about" type="button" disabled>about</button>
<button class="nav-help" type="button" disabled>help</button>
<button class="nav-options" type="button" disabled>options</button>
</nav>
</header>
<main> <main>
<header>
<h1 class="level-set">Chip's Challenge Level Pack 1</h1>
<nav>
<button class="set-nav-return" type="button" disabled>Change pack</button>
</nav>
<h2 class="level-name">Level 1 Key Pyramid</h2>
<nav class="nav">
<button class="nav-prev" type="button">\ufe0e</button>
<button class="nav-browse" type="button" disabled>Level select</button>
<button class="nav-next" type="button">\ufe0e</button>
</nav>
</header>
<div class="level"><!-- level canvas and any overlays go here --></div> <div class="level"><!-- level canvas and any overlays go here --></div>
<div class="bummer"></div> <div class="bummer"></div>
<div class="meta"></div> <div class="message"></div>
<div class="nav"> <div class="chips">
<button class="nav-prev" type="button">«</button> <h3>Chips</h3>
<button class="nav-browse" type="button">Level select</button> <output></output>
<button class="nav-next" type="button">»</button> </div>
<div class="time">
<h3>Time</h3>
<output></output>
</div>
<div class="bonus">
<h3>Bonus</h3>
<output></output>
</div> </div>
<div class="hint"></div>
<div class="chips"></div>
<div class="time"></div>
<div class="inventory"></div> <div class="inventory"></div>
<div class="controls"> <div class="controls">
<button class="control-pause" type="button">Pause</button> <button class="control-pause" type="button">Pause</button>
<button class="control-restart" type="button">Restart</button> <button class="control-restart" type="button" disabled>Restart</button>
<button class="control-undo" type="button">Undo</button> <button class="control-undo" type="button" disabled>Undo</button>
<button class="control-rewind" type="button">Rewind</button> <button class="control-rewind" type="button" disabled>Rewind</button>
</div> </div>
<div class="demo"> <div class="demo">
<h2>Solution demo available</h2> <h2>Solution demo available</h2>
@ -610,19 +657,20 @@ class Game {
this.container.style.setProperty('--tile-width', `${this.tileset.size_x}px`); this.container.style.setProperty('--tile-width', `${this.tileset.size_x}px`);
this.container.style.setProperty('--tile-height', `${this.tileset.size_y}px`); this.container.style.setProperty('--tile-height', `${this.tileset.size_y}px`);
this.level_el = this.container.querySelector('.level'); this.level_el = this.container.querySelector('.level');
this.meta_el = this.container.querySelector('.meta'); this.level_name_el = this.container.querySelector('.level-name');
this.nav_el = this.container.querySelector('.nav'); this.message_el = this.container.querySelector('.message');
this.hint_el = this.container.querySelector('.hint'); this.chips_el = this.container.querySelector('.chips output');
this.chips_el = this.container.querySelector('.chips'); this.time_el = this.container.querySelector('.time output');
this.time_el = this.container.querySelector('.time'); this.bonus_el = this.container.querySelector('.bonus output');
this.inventory_el = this.container.querySelector('.inventory'); this.inventory_el = this.container.querySelector('.inventory');
this.bummer_el = this.container.querySelector('.bummer'); this.bummer_el = this.container.querySelector('.bummer');
this.input_el = this.container.querySelector('.input'); this.input_el = this.container.querySelector('.input');
this.demo_el = this.container.querySelector('.demo'); this.demo_el = this.container.querySelector('.demo');
// Populate navigation // Populate navigation
this.nav_prev_button = this.nav_el.querySelector('.nav-prev'); let nav_el = this.container.querySelector('.nav');
this.nav_next_button = this.nav_el.querySelector('.nav-next'); this.nav_prev_button = nav_el.querySelector('.nav-prev');
this.nav_next_button = nav_el.querySelector('.nav-next');
this.nav_prev_button.addEventListener('click', ev => { this.nav_prev_button.addEventListener('click', ev => {
// TODO confirm // TODO confirm
if (this.level_index > 0) { if (this.level_index > 0) {
@ -637,13 +685,9 @@ class Game {
}); });
// Bind buttons // Bind buttons
this.container.querySelector('.controls .control-pause').addEventListener('click', ev => { this.pause_button = this.container.querySelector('.controls .control-pause');
if (this.level.state === 'playing') { this.pause_button.addEventListener('click', ev => {
this.level.state = 'paused'; this.toggle_pause();
}
else if (this.level.state === 'paused') {
this.level.state = 'playing';
}
ev.target.blur(); ev.target.blur();
}); });
// Demo playback // Demo playback
@ -685,10 +729,19 @@ class Game {
this.current_keys = new Set; // keys that are currently held this.current_keys = new Set; // keys that are currently held
// 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') {
this.toggle_pause();
return;
}
if (this.key_mapping[ev.key]) { if (this.key_mapping[ev.key]) {
this.current_keys.add(ev.key); this.current_keys.add(ev.key);
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
if (this.state === 'waiting') {
this.set_state('playing');
}
} }
}); });
key_target.addEventListener('keyup', ev => { key_target.addEventListener('keyup', ev => {
@ -721,7 +774,6 @@ class Game {
// Done with UI, now we can load a level // Done with UI, now we can load a level
this.load_level(0); this.load_level(0);
this.redraw();
// Fill in the scrubber // Fill in the scrubber
if (false && this.level.stored_level.demo) { if (false && this.level.stored_level.demo) {
@ -763,14 +815,22 @@ class Game {
load_level(level_index) { load_level(level_index) {
this.level_index = level_index; this.level_index = level_index;
this.level = new Level(this.stored_game.levels[level_index]); this.level = new Level(this.stored_game.levels[level_index]);
// waiting: haven't yet pressed a key so the timer isn't going
// playing: playing normally
// paused: um, paused
// rewinding: playing backwards
// stopped: level has ended one way or another
this.set_state('waiting');
// FIXME do better // FIXME do better
this.meta_el.textContent = this.level.stored_level.title; this.level_name_el.textContent = `Level ${level_index + 1}${this.level.stored_level.title}`;
document.title = `${PAGE_TITLE} - ${this.level.stored_level.title}`; document.title = `${PAGE_TITLE} - ${this.level.stored_level.title}`;
this.nav_prev_button.disabled = level_index <= 0; this.nav_prev_button.disabled = level_index <= 0;
this.nav_next_button.disabled = level_index >= this.stored_game.levels.length; this.nav_next_button.disabled = level_index >= this.stored_game.levels.length;
this.update_ui(); this.update_ui();
this.redraw();
} }
get_input() { get_input() {
@ -835,13 +895,19 @@ class Game {
this.level.advance_tic(player_move); this.level.advance_tic(player_move);
this.tic++; this.tic++;
if (this.level.state !== 'playing') {
// We either won or lost!
this.set_state('stopped');
break;
}
} }
this.redraw(); this.redraw();
this.update_ui(); this.update_ui();
} }
do_frame() { do_frame() {
if (this.level.state === 'playing') { if (this.state === 'playing') {
this.frame++; this.frame++;
if (this.frame % 3 === 0) { if (this.frame % 3 === 0) {
this.advance_by(1); this.advance_by(1);
@ -863,16 +929,18 @@ class Game {
} }
update_ui() { update_ui() {
this.pause_button.disabled = !(this.state === 'playing' || this.state === 'paused');
// TODO can we do this only if they actually changed? // TODO can we do this only if they actually changed?
this.chips_el.textContent = this.level.chips_remaining; this.chips_el.textContent = this.level.chips_remaining;
this.hint_el.textContent = this.level.hint_shown ?? ''; if (this.level.time_remaining === null) {
this.time_el.textContent = '---';
if (this.level.state === 'failure') {
this.bummer_el.textContent = this.level.fail_message;
} }
else { else {
this.bummer_el.textContent = ''; this.time_el.textContent = this.level.time_remaining;
} }
this.bonus_el.textContent = this.level.bonus_points;
this.message_el.textContent = this.level.hint_shown ?? "";
this.inventory_el.textContent = ''; this.inventory_el.textContent = '';
for (let [name, count] of Object.entries(this.level.player.inventory)) { for (let [name, count] of Object.entries(this.level.player.inventory)) {
@ -891,6 +959,59 @@ class Game {
} }
} }
toggle_pause() {
if (this.state === 'paused') {
this.set_state('playing');
}
else if (this.state === 'playing') {
this.set_state('paused');
}
}
set_state(new_state) {
if (new_state === this.state)
return;
this.state = new_state;
if (this.state === 'waiting') {
this.bummer_el.textContent = "Ready!";
}
else if (this.state === 'playing' || this.state === 'rewinding') {
this.bummer_el.textContent = "";
}
else if (this.state === 'paused') {
this.bummer_el.textContent = "/// paused ///";
}
else if (this.state === 'stopped') {
if (this.level.state === 'failure') {
this.bummer_el.textContent = this.level.fail_message;
}
else {
this.bummer_el.textContent = "";
let base = (this.level_index + 1) * 500;
let time = (this.level.time_remaining || 0) * 10;
this.bummer_el.append(
mk('p', "go bit buster!"),
mk('dl.score-chart',
mk('dt', "base score"),
mk('dd', base),
mk('dt', "time bonus"),
mk('dd', `+ ${time}`),
mk('dt', "score bonus"),
mk('dd', `+ ${this.level.bonus_points}`),
mk('dt.-sum', "level score"),
mk('dd.-sum', base + time + this.level.bonus_points),
mk('dt', "improvement"),
mk('dd', "(TODO)"),
mk('dt', "total score"),
mk('dd', "(TODO)"),
),
);
}
}
}
redraw() { redraw() {
let ctx = this.level_canvas.getContext('2d'); let ctx = this.level_canvas.getContext('2d');
ctx.clearRect(0, 0, this.level_canvas.width, this.level_canvas.height); ctx.clearRect(0, 0, this.level_canvas.width, this.level_canvas.height);

View File

@ -332,7 +332,7 @@ const TILE_TYPES = {
is_object: true, is_object: true,
is_item: true, is_item: true,
is_tool: true, is_tool: true,
item_ignores: new Set(['ice']), item_ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se']),
}, },
suction_boots: { suction_boots: {
is_object: true, is_object: true,
@ -419,6 +419,11 @@ const TILE_TYPES = {
} }
}, },
exit: { exit: {
on_arrive(me, level, other) {
if (other.type.is_player) {
level.win();
}
}
}, },
}; };

176
style.css
View File

@ -1,31 +1,67 @@
html { html {
font-size: 24px;
height: 100%; height: 100%;
} }
body { body {
font-size: 24px;
min-height: 100%; min-height: 100%;
margin: 0; margin: 0;
display: flex; display: flex;
justify-content: center; flex-direction: column;
align-items: center;
background: #404040; background: #101214;
color: white;
} }
/* Generic element styling */
button {
font-size: inherit;
padding: 0.125em 0.5em;
border: 1px solid black;
background: #909090;
border-image: url(button.png) 33.333% fill / auto repeat;
text-transform: lowercase;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
margin: 0;
}
/* Main page structure */
body > header {
display: flex;
align-items: center;
padding: 0.5em;
background: #00080c;
}
body > header > h1 {
flex: 1;
font-size: 1.25rem;
}
body > header > nav {
display: flex;
gap: 0.5em;
}
main { main {
flex: 0;
margin: auto; /* center in both directions baby */
display: grid; display: grid;
align-items: center;
grid: grid:
"level meta" min-content "header header"
"level nav" min-content "level chips"
"level chips" min-content "level time"
"level time" min-content "level bonus"
"level hint" 1fr "level message" 1fr
"level inventory" min-content "level inventory"
"controls controls" "level controls"
"demo demo" "demo demo"
/ min-content 12em /* Need explicit min-content to force the hint to wrap */
/ min-content min-content
; ;
gap: 1em; column-gap: 1em;
row-gap: 0.5em;
image-rendering: optimizeSpeed; image-rendering: optimizeSpeed;
@ -34,8 +70,23 @@ main {
--scale: 2; --scale: 2;
} }
button { main > header {
font-size: inherit; grid-area: header;
display: grid;
grid-auto-columns: 1fr auto;
align-items: center;
gap: 0.25em;
}
main > header > h1,
main > header > h2 {
grid-column: 1;
line-height: 1;
}
main > header > nav {
grid-column: 2;
justify-self: end;
display: flex;
gap: 0.25em;
} }
.level { .level {
@ -49,14 +100,16 @@ button {
} }
.bummer { .bummer {
grid-area: level; grid-area: level;
place-self: stretch;
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
z-index: 99; z-index: 99;
font-size: 48px; font-size: 48px;
padding: 25%; padding: 10%;
background: #0009; background: #0009;
color: white; color: white;
text-align: center; text-align: center;
@ -66,6 +119,29 @@ button {
.bummer:empty { .bummer:empty {
display: none; display: none;
} }
.bummer p {
margin: 0;
}
dl.score-chart {
display: grid;
grid-auto-columns: 1fr 1fr;
font-weight: normal;
}
dl.score-chart dt {
grid-column: 1;
text-align: left;
}
dl.score-chart dd {
grid-column: 2;
margin: 0;
text-align: right;
}
dl.score-chart .-sum {
margin-bottom: 0.5em;
border-top: 1px solid white;
color: hsl(40, 75%, 80%);
}
.meta { .meta {
grid-area: meta; grid-area: meta;
@ -73,30 +149,59 @@ button {
background: black; background: black;
text-align: center; text-align: center;
} }
.nav {
grid-area: nav;
display: flex;
gap: 1em;
}
.nav .nav-browse {
flex: 1;
}
.chips { .chips {
grid-area: chips; grid-area: chips;
padding: 0 0.5em;
color: yellow;
background: black;
}
.chips::before {
content: "chips left: ";
} }
.time { .time {
grid-area: time; grid-area: time;
} }
.hint { .bonus {
grid-area: hint; grid-area: bonus;
} }
.chips,
.time,
.bonus {
display: flex;
align-items: center;
}
.chips h3,
.time h3,
.bonus h3 {
flex: 1;
font-size: 1.25rem;
line-height: 1;
}
.chips output,
.time output,
.bonus output {
flex: 0;
font-size: 2em;
padding: 0.125em;
min-width: 2em;
min-height: 1em;
line-height: 1;
text-align: right;
font-family: monospace;
color: hsl(45, 100%, 60%);
background: #080808;
border: 1px inset #202020;
}
.message {
grid-area: message;
align-self: stretch;
padding: 0.5em;
font-family: serif;
font-style: italic;
color: hsl(45, 100%, 60%);
background: #080808;
border: 1px inset #202020;
}
.message:empty {
display: none;
}
.inventory { .inventory {
grid-area: inventory; grid-area: inventory;
display: flex; display: flex;
@ -112,6 +217,11 @@ button {
} }
.controls { .controls {
grid-area: controls; grid-area: controls;
display: flex;
gap: 0.25em;
}
.controls > button {
flex: 1;
} }
.demo { .demo {
grid-area: demo; grid-area: demo;