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);
el.classList = classes.join(' ');
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);
for (let [key, value] of Object.entries(attrs)) {
el.setAttribute(key, value);
@ -235,7 +235,8 @@ class Level {
// playing: normal play
// success: has been won
// failure: died
// paused: paused
// note that pausing is NOT handled here, but by whatever's driving our
// event loop!
this.state = 'playing';
}
@ -244,6 +245,14 @@ class Level {
this.player = null;
this.actors = [];
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;
@ -282,9 +291,8 @@ class Level {
advance_tic(player_direction) {
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}`);
//return;
console.warn(`Level.advance_tic() called when state is ${this.state}`);
return;
}
// XXX this entire turn order is rather different in ms rules
@ -388,6 +396,17 @@ class Level {
if (this.state === 'success' || this.state === 'failure')
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) {
@ -395,6 +414,10 @@ class Level {
this.fail_message = message;
}
win() {
this.state = 'success';
}
// Try to move the given actor one tile in the given direction and update
// their cooldown. Return true if successful.
attempt_step(actor, direction) {
@ -526,34 +549,58 @@ class Level {
// TODO:
// - some kinda visual theme i guess lol
// - level /number/
// - level password, if any
// - set name
// - timer!
// - intro splash with list of available levels
// - timer!!!!!
// - bonus points (cc2 only, or maybe only if got any so far this level)
// - intro splash with list of available level packs
// - button: quit to splash
// - button: options
// - implement winning and show score for this level
// - show current score so far
// - about, help
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>
<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="bummer"></div>
<div class="meta"></div>
<div class="nav">
<button class="nav-prev" type="button">«</button>
<button class="nav-browse" type="button">Level select</button>
<button class="nav-next" type="button">»</button>
<div class="message"></div>
<div class="chips">
<h3>Chips</h3>
<output></output>
</div>
<div class="time">
<h3>Time</h3>
<output></output>
</div>
<div class="bonus">
<h3>Bonus</h3>
<output></output>
</div>
<div class="hint"></div>
<div class="chips"></div>
<div class="time"></div>
<div class="inventory"></div>
<div class="controls">
<button class="control-pause" type="button">Pause</button>
<button class="control-restart" type="button">Restart</button>
<button class="control-undo" type="button">Undo</button>
<button class="control-rewind" type="button">Rewind</button>
<button class="control-restart" type="button" disabled>Restart</button>
<button class="control-undo" type="button" disabled>Undo</button>
<button class="control-rewind" type="button" disabled>Rewind</button>
</div>
<div class="demo">
<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-height', `${this.tileset.size_y}px`);
this.level_el = this.container.querySelector('.level');
this.meta_el = this.container.querySelector('.meta');
this.nav_el = this.container.querySelector('.nav');
this.hint_el = this.container.querySelector('.hint');
this.chips_el = this.container.querySelector('.chips');
this.time_el = this.container.querySelector('.time');
this.level_name_el = this.container.querySelector('.level-name');
this.message_el = this.container.querySelector('.message');
this.chips_el = this.container.querySelector('.chips output');
this.time_el = this.container.querySelector('.time output');
this.bonus_el = this.container.querySelector('.bonus output');
this.inventory_el = this.container.querySelector('.inventory');
this.bummer_el = this.container.querySelector('.bummer');
this.input_el = this.container.querySelector('.input');
this.demo_el = this.container.querySelector('.demo');
// Populate navigation
this.nav_prev_button = this.nav_el.querySelector('.nav-prev');
this.nav_next_button = this.nav_el.querySelector('.nav-next');
let nav_el = this.container.querySelector('.nav');
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 => {
// TODO confirm
if (this.level_index > 0) {
@ -637,13 +685,9 @@ class Game {
});
// Bind buttons
this.container.querySelector('.controls .control-pause').addEventListener('click', ev => {
if (this.level.state === 'playing') {
this.level.state = 'paused';
}
else if (this.level.state === 'paused') {
this.level.state = 'playing';
}
this.pause_button = this.container.querySelector('.controls .control-pause');
this.pause_button.addEventListener('click', ev => {
this.toggle_pause();
ev.target.blur();
});
// Demo playback
@ -685,10 +729,19 @@ class Game {
this.current_keys = new Set; // keys that are currently held
// 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') {
this.toggle_pause();
return;
}
if (this.key_mapping[ev.key]) {
this.current_keys.add(ev.key);
ev.stopPropagation();
ev.preventDefault();
if (this.state === 'waiting') {
this.set_state('playing');
}
}
});
key_target.addEventListener('keyup', ev => {
@ -721,7 +774,6 @@ class Game {
// Done with UI, now we can load a level
this.load_level(0);
this.redraw();
// Fill in the scrubber
if (false && this.level.stored_level.demo) {
@ -763,14 +815,22 @@ class Game {
load_level(level_index) {
this.level_index = 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
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}`;
this.nav_prev_button.disabled = level_index <= 0;
this.nav_next_button.disabled = level_index >= this.stored_game.levels.length;
this.update_ui();
this.redraw();
}
get_input() {
@ -835,13 +895,19 @@ class Game {
this.level.advance_tic(player_move);
this.tic++;
if (this.level.state !== 'playing') {
// We either won or lost!
this.set_state('stopped');
break;
}
}
this.redraw();
this.update_ui();
}
do_frame() {
if (this.level.state === 'playing') {
if (this.state === 'playing') {
this.frame++;
if (this.frame % 3 === 0) {
this.advance_by(1);
@ -863,16 +929,18 @@ class Game {
}
update_ui() {
this.pause_button.disabled = !(this.state === 'playing' || this.state === 'paused');
// TODO can we do this only if they actually changed?
this.chips_el.textContent = this.level.chips_remaining;
this.hint_el.textContent = this.level.hint_shown ?? '';
if (this.level.state === 'failure') {
this.bummer_el.textContent = this.level.fail_message;
if (this.level.time_remaining === null) {
this.time_el.textContent = '---';
}
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 = '';
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() {
let ctx = this.level_canvas.getContext('2d');
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_item: true,
is_tool: true,
item_ignores: new Set(['ice']),
item_ignores: new Set(['ice', 'ice_nw', 'ice_ne', 'ice_sw', 'ice_se']),
},
suction_boots: {
is_object: true,
@ -419,6 +419,11 @@ const TILE_TYPES = {
}
},
exit: {
on_arrive(me, level, other) {
if (other.type.is_player) {
level.win();
}
}
},
};

176
style.css
View File

@ -1,31 +1,67 @@
html {
font-size: 24px;
height: 100%;
}
body {
font-size: 24px;
min-height: 100%;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
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 {
flex: 0;
margin: auto; /* center in both directions baby */
display: grid;
align-items: center;
grid:
"level meta" min-content
"level nav" min-content
"level chips" min-content
"level time" min-content
"level hint" 1fr
"level inventory" min-content
"controls controls"
"header header"
"level chips"
"level time"
"level bonus"
"level message" 1fr
"level inventory"
"level controls"
"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;
@ -34,8 +70,23 @@ main {
--scale: 2;
}
button {
font-size: inherit;
main > header {
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 {
@ -49,14 +100,16 @@ button {
}
.bummer {
grid-area: level;
place-self: stretch;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 99;
font-size: 48px;
padding: 25%;
padding: 10%;
background: #0009;
color: white;
text-align: center;
@ -66,6 +119,29 @@ button {
.bummer:empty {
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 {
grid-area: meta;
@ -73,30 +149,59 @@ button {
background: black;
text-align: center;
}
.nav {
grid-area: nav;
display: flex;
gap: 1em;
}
.nav .nav-browse {
flex: 1;
}
.chips {
grid-area: chips;
padding: 0 0.5em;
color: yellow;
background: black;
}
.chips::before {
content: "chips left: ";
}
.time {
grid-area: time;
}
.hint {
grid-area: hint;
.bonus {
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 {
grid-area: inventory;
display: flex;
@ -112,6 +217,11 @@ button {
}
.controls {
grid-area: controls;
display: flex;
gap: 0.25em;
}
.controls > button {
flex: 1;
}
.demo {
grid-area: demo;