Rework mobile layout to be more compact, et al.

- On small screens, the top two headers (with the pack + level names)
  are now removed; instead the pack and level name are shown when
  starting each level, and the buttons from those headers are moved into
  a pause menu.

- The options, compat, and level browser dialogs were all reworked to
  fit better on narrow screens.

- The level overlay has a more consistent layout and tries harder to not
  draw in the middle, where the player generally is (except that the
  mobile pause menu goes there, but oh well).

- The score tally at the end of a level is now less of a small table and
  more of...  more numbers, I guess?

- Links to the music source and author now open in a new window to
  reduce risk of accidentally clicking them and losing your progress.

- A few obituaries were shortened, and several more were added.

- The game ending screen is now accessible on a touchscreen (oops).

- The pause and rewind buttons visually indicate when you're in that
  mode, suggesting you can hit them again to switch to normal play.

- Touch controls are now relative to the player and only apply within
  the game viewport.

- Disabled buttons look a bit less janky.

Still some work to do on this, but it's a pretty solid start.
This commit is contained in:
Eevee (Evelyn Woods) 2021-05-21 21:10:44 -06:00
parent 8b03d09c78
commit 41e5b5f9b8
6 changed files with 760 additions and 341 deletions

View File

@ -5,6 +5,7 @@
<title>Lexy's Labyrinth</title> <title>Lexy's Labyrinth</title>
<link rel="stylesheet" type="text/css" href="style.css"> <link rel="stylesheet" type="text/css" href="style.css">
<link rel="shortcut icon" type="image/png" href="icon.png"> <link rel="shortcut icon" type="image/png" href="icon.png">
<link rel="manifest" type="application/json" href="manifest.json">
<script> <script>
"use strict"; "use strict";
{ {
@ -73,6 +74,12 @@
<g id="svg-icon-menu-chevron"> <g id="svg-icon-menu-chevron">
<path d="M2,4 l6,6 l6,-6 v3 l-6,6 l-6,-6 z"></path> <path d="M2,4 l6,6 l6,-6 v3 l-6,6 l-6,-6 z"></path>
</g> </g>
<g id="svg-icon-prev">
<path d="M14,1 2,8 14,14 z">
</g>
<g id="svg-icon-next">
<path d="M2,1 14,8 2,14 z">
</g>
<!-- Actions --> <!-- Actions -->
<g id="svg-icon-up"> <g id="svg-icon-up">
<path d="M0,12 l8,-8 l8,8 z"></path> <path d="M0,12 l8,-8 l8,8 z"></path>
@ -116,8 +123,8 @@
<h1><a href="https://github.com/eevee/lexys-labyrinth">Lexy's Labyrinth</a></h1> <h1><a href="https://github.com/eevee/lexys-labyrinth">Lexy's Labyrinth</a></h1>
<p>— an <a href="https://github.com/eevee/lexys-labyrinth">open source</a> game by <a href="https://eev.ee/">eevee</a></p> <p>— an <a href="https://github.com/eevee/lexys-labyrinth">open source</a> game by <a href="https://eev.ee/">eevee</a></p>
<nav> <nav>
<button id="main-compat" type="button">mode: <output>lexy</output></button>
<button id="main-options" type="button">options</button> <button id="main-options" type="button">options</button>
<button id="main-compat" type="button">compat mode: <output>lexy</output></button>
</nav> </nav>
</header> </header>
<header id="header-pack"> <header id="header-pack">
@ -133,11 +140,11 @@
<h3 id="level-name">Level 1 — Key Pyramid</h3> <h3 id="level-name">Level 1 — Key Pyramid</h3>
<nav> <nav>
<button id="main-prev-level" type="button"> <button id="main-prev-level" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="previous"><path d="M14,1 2,8 14,14 z"></svg> <svg class="svg-icon" viewBox="0 0 16 16" title="previous"><use href="#svg-icon-prev"></svg>
</button> </button>
<button id="main-choose-level" type="button">Level select</button> <button id="main-choose-level" type="button">Level select</button>
<button id="main-next-level" type="button"> <button id="main-next-level" type="button">
<svg class="svg-icon" viewBox="0 0 16 16" title="next"><path d="M2,1 14,8 2,14 z"></svg> <svg class="svg-icon" viewBox="0 0 16 16" title="next"><use href="#svg-icon-next"></svg>
</button> </button>
</nav> </nav>
</header> </header>
@ -229,7 +236,7 @@
<svg class="svg-icon" viewBox="0 0 16 16"><path d="M1,8 7,2 7,14 z M9,8 15,2 15,14 z"></path></svg> <svg class="svg-icon" viewBox="0 0 16 16"><path d="M1,8 7,2 7,14 z M9,8 15,2 15,14 z"></path></svg>
<span class="-optional-label">rewind</span> <span class="keyhint"><kbd>z</kbd></span></button> <span class="-optional-label">rewind</span> <span class="keyhint"><kbd>z</kbd></span></button>
<div class="radio-faux-button-set"> <div class="radio-faux-button-set">
<label><input class="control-turn-based" type="checkbox"> <span>Turn <br>based <br>mode</span></label> <label><input class="control-turn-based" type="checkbox"> <span>Step <br>mode</span></label>
</div> </div>
</div> </div>
<div id="player-actions"> <div id="player-actions">
@ -245,16 +252,15 @@
</div> </div>
<section id="player-game-area"> <section id="player-game-area">
<div class="level"><!-- level canvas and any overlays go here --></div> <div class="level"><!-- level canvas and any overlays go here --></div>
<div class="overlay-message"> <div class="player-overlay-message"></div>
<h1 class="-top"></h1>
<div class="-middle"></div>
<p class="-bottom"></p>
<p class="-keyhint"></p>
</div>
<div class="player-hint-wrapper"> <div class="player-hint-wrapper">
<div class="player-hint"></div> <div class="player-hint"></div>
<svg class="player-hint-bg-icon svg-icon" viewBox="0 0 16 16"><use href="#svg-icon-hint"></use></svg> <svg class="player-hint-bg-icon svg-icon" viewBox="0 0 16 16"><use href="#svg-icon-hint"></use></svg>
</div> </div>
<div class="player-level-number">
Level
<output></output>
</div>
<div class="chips"> <div class="chips">
<h3> <h3>
<svg class="svg-icon" viewBox="0 0 16 16" title="Hearts"> <svg class="svg-icon" viewBox="0 0 16 16" title="Hearts">
@ -304,7 +310,7 @@
<div class="inventory"></div> <div class="inventory"></div>
</section> </section>
<div id="player-music"> <div id="player-music">
🎵 <a id="player-music-title">title</a> by <a id="player-music-author">author</a> 🎵 <a id="player-music-title" target="_blank">title</a> by <a id="player-music-author" target="_blank">author</a>
<audio loop preload="auto"> <audio loop preload="auto">
</div> </div>
</div> </div>

View File

@ -127,8 +127,8 @@ export const PICKUP_PRIORITIES = {
export const COMPAT_RULESET_LABELS = { export const COMPAT_RULESET_LABELS = {
lexy: "Lexy", lexy: "Lexy",
steam: "Steam/CC2", steam: "Steam",
'steam-strict': "Steam/CC2 (strict)", 'steam-strict': "Steam (strict)",
lynx: "Lynx", lynx: "Lynx",
ms: "Microsoft", ms: "Microsoft",
custom: "Custom", custom: "Custom",

View File

@ -1,4 +1,4 @@
import { mk } from './util.js'; import { mk, mk_svg } from './util.js';
// Superclass for the main display modes: the player, the editor, and the splash screen // Superclass for the main display modes: the player, the editor, and the splash screen
export class PrimaryView { export class PrimaryView {
@ -285,6 +285,13 @@ export function flash_button(button) {
}, 500); }, 500);
} }
export function svg_icon(name) {
return mk_svg(
'svg.svg-icon',
{viewBox: '0 0 16 16'},
mk_svg('use', {href: `#svg-icon-${name}`}));
}
export function load_json_from_storage(key) { export function load_json_from_storage(key) {
return JSON.parse(window.localStorage.getItem(key)); return JSON.parse(window.localStorage.getItem(key));
} }

View File

@ -8,7 +8,7 @@ import * as dat from './format-dat.js';
import * as format_base from './format-base.js'; import * as format_base from './format-base.js';
import * as format_tws from './format-tws.js'; import * as format_tws from './format-tws.js';
import { Level } from './game.js'; import { Level } from './game.js';
import { PrimaryView, DialogOverlay, ConfirmOverlay, flash_button, load_json_from_storage, save_json_to_storage } from './main-base.js'; import { PrimaryView, DialogOverlay, ConfirmOverlay, flash_button, svg_icon, load_json_from_storage, save_json_to_storage } from './main-base.js';
import { Editor } from './editor/main.js'; import { Editor } from './editor/main.js';
import CanvasRenderer from './renderer-canvas.js'; import CanvasRenderer from './renderer-canvas.js';
import SOUNDTRACK from './soundtrack.js'; import SOUNDTRACK from './soundtrack.js';
@ -59,44 +59,44 @@ const OBITUARIES = {
drowned: [ drowned: [
"you tried out water cooling", "you tried out water cooling",
"you fell into the c", "you fell into the c",
"water disaster", "water disaster!",
"you sank like a rock", "you sank like a rock",
"your stack overflowed", "your stack overflowed",
], ],
burned: [ burned: [
"your core temp got too high", "your core temp got too high",
"your plans went up in smoke", "your plans went up in smoke",
"you got roasted", "you held your feet to the fire",
"you really blazed through that one", "you really blazed through that one",
"you turned up the heat", "you turned up the heat",
], ],
slimed: [ slimed: [
"what an oozefest", "you mutated",
"quite a sticky situation", "quite a sticky situation",
"you got dunked in the gunk", "you were garbage collected",
"that'll leave a stain", "that'll leave a stain",
"what a waste", "what a waste",
], ],
exploded: [ exploded: [
//" "you blew it",
"looks like you're having a blast", "you're having a blast",
"you tripped over something of mine", "you became 64 bits",
"you were blown to bits",
"you will surely be mist", "you will surely be mist",
"try not to trip",
], ],
squished: [ squished: [
"that block of ram was too much for you", "you encountered a block of ram",
"you became two-dimensional", "you became two-dimensional",
"you're a little flat, not too sharp", "your hit box collided",
"nice compression ratio", "nice compression ratio",
//" "you took a cube route",
], ],
time: [ time: [
"you tried to overclock", "you tried to overclock",
"you lost track of time", "you lost track of time",
"your speedrun went badly", "your speedrun went badly",
"you're feeling quite alarmed", "you overslept",
//" "you got ticked off",
], ],
electrocuted: [ electrocuted: [
"a shocking revelation", "a shocking revelation",
@ -119,48 +119,48 @@ const OBITUARIES = {
"you're having a ball", "you're having a ball",
"you'll bounce back from this", "you'll bounce back from this",
"should've gone the other way", "should've gone the other way",
//" "ping? pong!",
//" //"",
], ],
walker: [ walker: [
"you let it walk all over you", "you let it walk all over you",
"step into, step over, step out", "step into, step over, step out",
"don't just wander around at random", "you wandered around at random",
//" //"",
//" //"",
], ],
fireball: [ fireball: [
"you had a meltdown", "you had a meltdown",
"you haven't been flamed like that since usenet", "watch your core temp",
//" "you got roasted",
//" "you lost the flamewar",
//" "goodness gracious",
], ],
glider: [ glider: [
"your ship came in", "your ship came in",
"don't worry, everything's fin now", "everything turned out fin",
"should've given it a wider berth", "should've given it a wider berth",
"watch out for that skipper", "watch out for that skipper",
//" "don't harbor any resentment",
], ],
tank_blue: [ tank_blue: [
"you didn't watch where they tread", "watch where you tread",
"please and tank blue", "well, tanks for trying",
"should've reversed course", "should've reversed course",
"you strayed from the straight and narrow", "strayed from the straight and narrow",
//" "you charged in blindly",
], ],
tank_yellow: [ tank_yellow: [
"you let things get out of control", "things got out of control",
"you need more direction in your life", "you lost all direction",
"your chances of surviving that were remote", "your chances of survival were remote",
//" //"
//" //"
], ],
bug: [ bug: [
"you got ants in your pants", "you got ants in your pants",
"time for some debugging", "you need to debug",
//" "all the pest to you",
//" //"
//" //"
], ],
@ -176,72 +176,72 @@ const OBITUARIES = {
"you got a little nybble", "you got a little nybble",
"you're quite a mouthful", "you're quite a mouthful",
"you passed the taste test", "you passed the taste test",
//" "you ate it",
], ],
teeth_timid: [ teeth_timid: [
"you got a killer byte", "you got a killer byte",
//" "you were nibbled to bits",
"you got a tongue-lashing", "you got a tongue-lashing",
//" "how unvoretunate",
//" "you had an acci-dent",
], ],
blob: [ blob: [
"your luck ran out",
"gooed job on that one", "gooed job on that one",
"the rng manipulated you", "try gooing another way",
"goo another way next time", "what're the odds",
//" "ooze laughing now",
//"
], ],
doppelganger1: [ doppelganger1: [
"you were outfoxed", "you were outfoxed",
"sometimes a copy beats the original", "you need some vixen up",
"better reflect on what went wrong", "take some time to reflect",
"you've been duped", "you've been duped",
//" "stop hitting yourself",
], ],
doppelganger2: [ doppelganger2: [
"your plans just didn't gel", "your plans just didn't gel",
"bet that makes you hopping mad", "you got hopping mad",
//" "hare today, gone tomorrow",
//" "she left quite an impression",
//" "you were gänged up on",
], ],
rover: [ rover: [
"should've given it more roomba", "try giving it more roomba",
"exterminate. exterminate.", "exterminate. exterminate.",
"your space was invaded", "your space was invaded",
"red rover, red rover, this playthrough is over", "the robots have taken over",
"defeated by a confused frisbee", "defeated by a confused frisbee",
], ],
ghost: [ ghost: [
"you were scared to death", "you were scared to death",
"that wasn't very friendly", "that wasn't very friendly",
"now you're both ghosts", "now you're both ghosts",
//" "you were haunted down",
//" "what did you ex-specter",
], ],
floor_mimic: [ floor_mimic: [
"you never saw that coming", "you never saw that coming",
"you were absolutely floored", "you were absolutely floored",
"this seems fu-tile", "this seems fu-tile",
"watch your step", "watch your step",
//" "you put your foot in its mouth",
], ],
// Misc // Misc
dynamite_lit: [ dynamite_lit: [
"you've got a short fuse", "you've got a short fuse",
"you failed to put the pin back in", "you failed to put the pin back in",
//" "it had a hair trigger",
//" "no take-backs",
//" "you ran the wrong way",
], ],
rolling_ball: [ rolling_ball: [
"you were bowled over", "you were bowled over",
"you found some head cannon", "you found some head cannon",
"strike one!", "strike one!",
"down for the ten-count", "down for the ten-count",
"watch out, pinhead", "you really dropped the ball",
], ],
}; };
// Helper class used to let the game play sounds without knowing too much about the Player // Helper class used to let the game play sounds without knowing too much about the Player
@ -426,8 +426,9 @@ class Player extends PrimaryView {
this.play_speed = 1; this.play_speed = 1;
this.level_el = this.root.querySelector('.level'); this.level_el = this.root.querySelector('.level');
this.overlay_message_el = this.root.querySelector('.overlay-message'); this.overlay_message_el = this.root.querySelector('.player-overlay-message');
this.hint_el = this.root.querySelector('.player-hint'); this.hint_el = this.root.querySelector('.player-hint');
this.number_el = this.root.querySelector('.player-level-number output');
this.chips_el = this.root.querySelector('.chips output'); this.chips_el = this.root.querySelector('.chips output');
this.time_el = this.root.querySelector('.time output'); this.time_el = this.root.querySelector('.time output');
this.bonus_el = this.root.querySelector('.bonus output'); this.bonus_el = this.root.querySelector('.bonus output');
@ -491,6 +492,71 @@ class Player extends PrimaryView {
ev.target.blur(); ev.target.blur();
}); });
// Create the mobile pause menu, which consolidates buttons from around the desktop UI
// TODO i really need to, uh, consolidate this
let btn = (...args) => {
let onclick = args.pop();
let props = {};
let last = args[args.length - 1];
if (typeof last === 'object' && last.constructor === Object) {
props = args.pop();
}
let button = mk('button', props, ...args);
button.addEventListener('click', onclick);
return button;
};
this.mobile_pause_menu = mk('div.mobile-pause-menu',
// waiting
btn("Play", {'class': 'button-bright -only-waiting'}, () => {
this.set_state('playing');
}),
// paused
mk('p.-only-paused',
btn("Resume", {'class': 'button-bright'}, () => {
this.set_state('playing');
}),
btn("Retry", () => {
this.confirm_game_interruption("Abandon this attempt and try again?", () => {
this.restart_level();
});
}),
),
// failure
btn("Retry", {'class': 'button-bright -only-failure -only-ended'}, () => {
this.restart_level();
}),
// success
btn("Onwards!", {'class': 'button-bright -only-success'}, () => {
this.conductor.maybe_change_level(this.conductor.level_index + 1);
}),
mk('p',
this.mobile_prev_button = btn(svg_icon('prev'), {'class': '-narrow'}, () => {
this.confirm_game_interruption("Abandon this attempt and return to the previous level?", () => {
this.conductor.maybe_change_level(this.conductor.level_index - 1);
});
}),
btn("Level select", () => {
// TODO this should really be in the level browser itself since you can check
// scores without losing a game
this.confirm_game_interruption("Abandon this attempt?", () => {
this.open_level_browser();
});
}),
this.mobile_next_button = btn(svg_icon('next'), {'class': '-narrow'}, () => {
this.confirm_game_interruption("Abandon this attempt and proceed to the next level?", () => {
this.conductor.maybe_change_level(this.conductor.level_index + 1);
});
}),
),
btn("Quit to pack list", () => {
this.confirm_game_interruption("Abandon this attempt and return to the pack list?", () => {
this.conductor.switch_to_splash();
});
}),
);
this.use_interpolation = true; this.use_interpolation = true;
// Default to the LL tileset for safety, but change when we load a level // Default to the LL tileset for safety, but change when we load a level
// (Note also that this must be created in the constructor so the CC2 timing option can be // (Note also that this must be created in the constructor so the CC2 timing option can be
@ -605,15 +671,7 @@ class Player extends PrimaryView {
} }
else if (this.state === 'stopped') { else if (this.state === 'stopped') {
if (this.level.state === 'success') { if (this.level.state === 'success') {
// Advance to the next level, if any this.proceed_to_next_level();
if (this.conductor.level_index < this.conductor.stored_game.level_metadata.length - 1) {
this.conductor.change_level(this.conductor.level_index + 1);
}
else {
// TODO for CCLs, by default, this is also at level 144
this.set_state('ended');
this.update_ui();
}
} }
else { else {
// Restart // Restart
@ -672,48 +730,28 @@ class Player extends PrimaryView {
// Similarly, grab touch events and translate them to directions // Similarly, grab touch events and translate them to directions
this.current_touches = {}; // ident => action this.current_touches = {}; // ident => action
this.touch_restart_delay = new util.DelayTimer; this.touch_restart_delay = new util.DelayTimer;
let touch_target = this.root.querySelector('#player-game-area'); // FIXME should be .level but the message overlay blocks touching, whoops! let touch_target = this.root.querySelector('#player-game-area .level');
let collect_touches = ev => { let collect_touches = ev => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
this.using_touch = true; this.using_touch = true;
// If state is anything other than playing/waiting, probably switch to playing, similar // Figure out where these touches are, relative to the player
// to pressing spacebar
if (ev.type === 'touchstart') {
if (this.state === 'paused') {
this.toggle_pause();
return;
}
else if (this.state === 'stopped') {
if (this.touch_restart_delay.active) {
// If it's only been a very short time since the level ended, ignore taps
// here, so you don't accidentally mash restart and lose the chance to undo
}
else if (this.level.state === 'success') {
// Advance to the next level
// TODO game ending?
this.conductor.change_level(this.conductor.level_index + 1);
}
else {
// Restart
this.restart_level();
}
return;
}
}
// Figure out where these touches are, relative to the game area
// TODO allow starting a level without moving? // TODO allow starting a level without moving?
let rect = this.level_el.getBoundingClientRect(); // TODO if you don't move the touch, the player can pass it and will keep going in that
// direction?
let [px, py] = this.level.player.visual_position();
px += 0.5;
py += 0.5;
for (let touch of ev.changedTouches) { for (let touch of ev.changedTouches) {
// Normalize touch coordinates to [-1, 1] let [x, y] = this.renderer.point_to_real_cell_coords(touch.clientX, touch.clientY);
let rx = (touch.clientX - rect.left) / rect.width * 2 - 1; let dx = x - px;
let ry = (touch.clientY - rect.top) / rect.height * 2 - 1; let dy = y - py;
console.log(dx, dy);
// Divine a direction from the results // Divine a direction from the results
let action; let action;
if (Math.abs(rx) > Math.abs(ry)) { if (Math.abs(dx) > Math.abs(dy)) {
if (rx < 0) { if (dx < 0) {
action = 'left'; action = 'left';
} }
else { else {
@ -721,7 +759,7 @@ class Player extends PrimaryView {
} }
} }
else { else {
if (ry < 0) { if (dy < 0) {
action = 'up'; action = 'up';
} }
else { else {
@ -745,9 +783,29 @@ class Player extends PrimaryView {
}; };
touch_target.addEventListener('touchend', dismiss_touches); touch_target.addEventListener('touchend', dismiss_touches);
touch_target.addEventListener('touchcancel', dismiss_touches); touch_target.addEventListener('touchcancel', dismiss_touches);
// Also grab taps on the overlay, for the specific case that tapping on the end of level
// tally advances to the next level
this.overlay_message_el.addEventListener('touchstart', ev => {
if (this.state === 'stopped') {
if (this.touch_restart_delay.active) {
// If it's only been a very short time since the level ended, ignore taps
// here, so you don't accidentally mash restart and lose the chance to undo
}
else if (this.level.state === 'success') {
// Advance to the next level
this.proceed_to_next_level();
}
else {
// Restart
this.restart_level();
}
ev.stopPropagation();
ev.preventDefault();
}
});
// When we lose focus, act as though every key was released, and pause the game // When we lose focus, act as though every key was released, and pause the game
window.addEventListener('blur', ev => { window.addEventListener('blur', () => {
this.current_keys.clear(); this.current_keys.clear();
this.current_touches = {}; this.current_touches = {};
@ -1253,10 +1311,14 @@ class Player extends PrimaryView {
this.update_tileset(); this.update_tileset();
this.renderer.set_level(this.level); this.renderer.set_level(this.level);
this.update_viewport_size(); this.update_viewport_size();
this.number_el.textContent = stored_level.number;
// TODO base this on a hash of the UA + some identifier for the pack + the level index. StoredLevel doesn't know its own index atm... // TODO base this on a hash of the UA + some identifier for the pack + the level index. StoredLevel doesn't know its own index atm...
this.change_music(this.conductor.level_index % SOUNDTRACK.length); this.change_music(this.conductor.level_index % SOUNDTRACK.length);
this._clear_state(); this._clear_state();
this.mobile_prev_button.disabled = ! (this.conductor.level_index - 1 >= 0);
this.mobile_next_button.disabled = ! (this.conductor.level_index + 1 < this.conductor.stored_game.level_metadata.length);
this._update_replay_ui(); this._update_replay_ui();
if (this.debug.enabled) { if (this.debug.enabled) {
this.debug.replay_level_label.textContent = this.level.stored_level.has_replay ? "available" : "none"; this.debug.replay_level_label.textContent = this.level.stored_level.has_replay ? "available" : "none";
@ -1298,6 +1360,7 @@ class Player extends PrimaryView {
this.current_keyring = {}; this.current_keyring = {};
this.current_toolbelt = []; this.current_toolbelt = [];
this.previous_hint_tile = null; this.previous_hint_tile = null;
this.current_touches = {};
this.chips_el.classList.remove('--done'); this.chips_el.classList.remove('--done');
this.time_el.classList.remove('--frozen'); this.time_el.classList.remove('--frozen');
@ -1328,6 +1391,15 @@ class Player extends PrimaryView {
this._redraw(); this._redraw();
} }
proceed_to_next_level() {
// Advance to the next level, if any
if (! this.conductor.maybe_change_level(this.conductor.level_index + 1)) {
// TODO for CCLs, by default, this is also at level 144
this.set_state('ended');
this.update_ui();
}
}
open_level_browser() { open_level_browser() {
new LevelBrowserOverlay(this.conductor).open(); new LevelBrowserOverlay(this.conductor).open();
} }
@ -1732,46 +1804,52 @@ class Player extends PrimaryView {
this.current_keys_new.clear(); this.current_keys_new.clear();
} }
// TODO wonder if some other update_ui stuff could move here
this.pause_button.classList.toggle('--pressed', this.state === 'paused');
this.rewind_button.classList.toggle('--pressed', this.state === 'rewinding');
// Populate the overlay // Populate the overlay
let overlay_reason = ''; let overlay = this.overlay_message_el;
let overlay_top = ''; overlay.setAttribute('data-reason', this.state);
let overlay_middle = null; this.overlay_message_el.textContent = '';
let overlay_bottom = '';
let overlay_keyhint = '';
if (this.state === 'waiting') { if (this.state === 'waiting') {
overlay_reason = 'waiting';
let stored_level = this.level.stored_level; let stored_level = this.level.stored_level;
overlay_top = `#${stored_level.number} ${stored_level.title}`; overlay.append(
overlay_middle = "Ready!"; mk('h1', this.conductor.stored_game.title),
if (stored_level.author) { mk('h2', `#${stored_level.number} ${stored_level.title}`),
overlay_bottom = `by ${stored_level.author}`; mk('h3', stored_level.author ? `by ${stored_level.author}` : "\u200b"),
} this.mobile_pause_menu,
mk('p.-controls-hint', "WASD/↑←↓→ to move · space to start without moving"),
);
} }
else if (this.state === 'paused') { else if (this.state === 'paused') {
overlay_reason = 'paused'; overlay.append(mk('h2', "/// paused ///"));
overlay_bottom = "/// paused ///";
if (this.using_touch) { if (this.using_touch) {
overlay_keyhint = "tap to resume"; overlay.append(mk('p.-controls-hint', "tap to resume"));
} }
else { else {
overlay_keyhint = "press P to resume"; overlay.append(mk('p.-controls-hint', "press space to resume"));
} }
overlay.append(this.mobile_pause_menu);
} }
else if (this.state === 'stopped') { else if (this.state === 'stopped') {
// Set a timer before tapping the overlay will restart/advance // Set a timer before tapping the overlay will restart/advance
this.touch_restart_delay.set(2000); this.touch_restart_delay.set(2000);
if (this.level.state === 'failure') { if (this.level.state === 'failure') {
overlay_reason = 'failure'; overlay.setAttribute('data-reason', 'failure');
overlay_top = "whoops";
let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic']; let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic'];
overlay_bottom = random_choice(obits); overlay.append(
mk('h2', "whoops" + random_choice(["", "!", "?", "..."])),
mk('h3', random_choice(obits)),
this.mobile_pause_menu,
);
if (this.using_touch) { if (this.using_touch) {
// TODO touch gesture to rewind? // TODO touch gesture to rewind?
overlay_keyhint = "tap to try again, or use undo/rewind above"; overlay.append(mk('p.-controls-hint', "tap to try again, or use undo/rewind above"));
} }
else { else {
overlay_keyhint = "press space to try again, or Z to rewind"; overlay.append(mk('p.-controls-hint', "press space to try again, or Z to rewind"));
} }
} }
else { else {
@ -1831,7 +1909,7 @@ class Player extends PrimaryView {
this.conductor.save_savefile(); this.conductor.save_savefile();
} }
overlay_reason = 'success'; overlay.setAttribute('data-reason', 'success');
let base = level_number * 500; let base = level_number * 500;
let time = scorecard.time * 10; let time = scorecard.time * 10;
// Pick a success message // Pick a success message
@ -1841,89 +1919,110 @@ class Player extends PrimaryView {
time_left_fraction = this.level.time_remaining / TICS_PER_SECOND / this.level.stored_level.time_limit; time_left_fraction = this.level.time_remaining / TICS_PER_SECOND / this.level.stored_level.time_limit;
} }
let quip;
if (this.level.chips_remaining > 0) { if (this.level.chips_remaining > 0) {
overlay_top = random_choice([ quip = random_choice([
"socket to em!", "go bug blaster!", "socket to em!", "go bug blaster!",
]); ]);
} }
else if (this.level.time_remaining && this.level.time_remaining < 200) { else if (this.level.time_remaining && this.level.time_remaining < 200) {
overlay_top = random_choice([ quip = random_choice([
"in the nick of time!", "cutting it close!", "in the nick of time!", "cutting it close!",
]); ]);
} }
else if (time_left_fraction !== null && time_left_fraction > 1) { else if (time_left_fraction !== null && time_left_fraction > 1) {
overlay_top = random_choice([ quip = random_choice([
"faster than light!", "impossible speed!", "pipelined!", "faster than light!", "impossible speed!", "pipelined!",
]); ]);
} }
else if (time_left_fraction !== null && time_left_fraction > 0.75) { else if (time_left_fraction !== null && time_left_fraction > 0.75) {
overlay_top = random_choice([ quip = random_choice([
"lightning quick!", "nice speedrun!", "eagerly evaluated!", "lightning quick!", "nice speedrun!", "eagerly evaluated!",
]); ]);
} }
else { else {
overlay_top = random_choice([ quip = random_choice([
"you did it!", "nice going!", "great job!", "good work!", "you did it!", "nice going!", "great job!", "good work!",
"onwards!", "tubular!", "yeehaw!", "hot damn!", "onwards!", "tubular!", "yeehaw!", "hot damn!",
"alphanumeric!", "nice dynamic typing!", "alphanumeric!", "nice dynamic typing!",
]); ]);
} }
overlay.append(mk('h2', quip));
let bonus = this.level.bonus_points;
let score_improvement = mk('div.-improvement');
let time_improvement = mk('div.-improvement');
if (! old_scorecard) {
score_improvement.classList.add('--new');
score_improvement.append(mk('h3', "first time!"));
// leave time improvement empty since we already say it's first time once
}
else {
let diff = scorecard.score - old_scorecard.score;
let diffstr = Math.abs(diff).toLocaleString();
if (diff > 0) {
score_improvement.classList.add('--better');
score_improvement.append(mk('h4', "new record!"), mk('p', `+ ${diffstr}`));
}
else if (diff === 0) {
score_improvement.classList.add('--same');
score_improvement.append(mk('h4', "tied your best!"), mk('p', `+ ${diffstr}`));
}
else {
score_improvement.classList.add('--worse');
score_improvement.append(mk('h4', "vs your best:"), mk('p', ` ${diffstr}`));
}
diff = scorecard.abstime - old_scorecard.abstime;
diffstr = util.format_duration(Math.abs(diff) / TICS_PER_SECOND, 2);
if (diff < 0) {
time_improvement.classList.add('--better');
time_improvement.append(mk('h4', "new record!"), mk('p', ` ${diffstr}`));
}
else if (diff === 0) {
time_improvement.classList.add('--same');
time_improvement.append(mk('h4', "tied your best!"), mk('p', ` ${diffstr}`));
}
else {
time_improvement.classList.add('--worse');
time_improvement.append(mk('h4', "vs your best:"), mk('p', `+ ${diffstr}`));
}
}
overlay.append(mk('div.scoreboard',
// base score + time bonus + score bonus
mk('div.-subscore', mk('h4', "base score"), mk('p', base.toLocaleString())),
mk('div.-subscore',
mk('h4', "time bonus"),
mk('p', time ? `+ ${time.toLocaleString()}` : "—")),
mk('div.-subscore',
mk('h4', "score bonus"),
mk('p', bonus ? `+ ${bonus.toLocaleString()}` : "—")),
// level score ... first time OR new record OR x short
mk('div.-level-score',
mk('h4', "level score"),
mk('p', scorecard.score.toLocaleString(), scorecard.aid === 0 ? "★" : "")),
score_improvement,
mk('div.-level-score',
mk('h4', "real time"),
mk('p', util.format_duration(scorecard.abstime / TICS_PER_SECOND, 2))),
time_improvement,
// TODO show your level time, time improvement...? not quite enough room...
mk('div.-total-score',
mk('h4', "total score"),
mk('p', savefile.total_score.toLocaleString())),
mk('div.-total-score',
mk('h4', "total real time"),
mk('p', util.format_duration(savefile.total_abstime / TICS_PER_SECOND, 2))),
));
if (this.using_touch) { if (this.using_touch) {
overlay_keyhint = "tap to move on"; overlay.append(mk('p.-controls-hint', "tap to move on"));
} }
else { else {
overlay_keyhint = "press space to move on"; overlay.append(mk('p.-controls-hint', "press space to move on"));
}
overlay_middle = mk('dl.score-chart',
mk('dt.-component', "base score"),
mk('dd.-component', base.toLocaleString()),
mk('dt.-component', "time bonus"),
mk('dd.-component', `+ ${time.toLocaleString()}`),
);
if (this.level.bonus_points) {
overlay_middle.append(
mk('dt.-component', "score bonus"),
mk('dd.-component', `+ ${this.level.bonus_points.toLocaleString()}`),
);
}
// TODO show your time, time improvement...?
let score_dd = mk('dd.-sum', scorecard.score.toLocaleString());
if (scorecard.aid === 0) {
score_dd.append(mk('span.-star', "★"));
}
overlay_middle.append(mk('dt.-sum', "level score"), score_dd);
overlay_middle.append(
mk('dt.-total', "total score"),
mk('dd.-total', savefile.total_score.toLocaleString()),
);
if (old_scorecard && old_scorecard.score < scorecard.score) {
overlay_middle.append(
mk('dd.-total', `(+ ${(scorecard.score - old_scorecard.score).toLocaleString()})`),
);
}
else {
overlay_middle.append(mk('dd', ""));
}
overlay_middle.append(
mk('dd', ""),
mk('dt', "real time"),
mk('dd', util.format_duration(scorecard.abstime / TICS_PER_SECOND, 2)),
mk('dt.-total', "total time"),
mk('dd.-total', util.format_duration(savefile.total_abstime / TICS_PER_SECOND, 2)),
);
if (old_scorecard && old_scorecard.abstime > scorecard.abstime) {
overlay_middle.append(
mk('dd.-total', `( ${util.format_duration((old_scorecard.abstime - scorecard.abstime) / TICS_PER_SECOND, 2)})`),
);
}
else {
overlay_middle.append(mk('dd', ""));
} }
} }
} }
@ -1932,20 +2031,17 @@ class Player extends PrimaryView {
// long and clunky? final score is not interesting. could show other stats, total // long and clunky? final score is not interesting. could show other stats, total
// time, say something if you skipped levels... // time, say something if you skipped levels...
// TODO disable most of the ui here? probably?? // TODO disable most of the ui here? probably??
overlay_reason = 'ended';
overlay_middle = "Congratulations! You solved a whole set of funny escape rooms. But is that the best score you can manage...?";
let savefile = this.conductor.current_pack_savefile; let savefile = this.conductor.current_pack_savefile;
overlay_bottom = `FINAL SCORE: ${savefile.total_score.toLocaleString()}`; overlay.append(
mk('p.-score', "FINAL SCORE", mk('output', savefile.total_score.toLocaleString())),
this.mobile_pause_menu,
mk('p.-congrats', "Congratulations! You beat some funny escape rooms. Now improve your score!"),
);
// TODO press spacebar to... restart from level 1?? or what // TODO press spacebar to... restart from level 1?? or what
} }
this.overlay_message_el.setAttribute('data-reason', overlay_reason); else {
this.overlay_message_el.querySelector('.-top').textContent = overlay_top; // 'playing', or bogus
this.overlay_message_el.querySelector('.-bottom').textContent = overlay_bottom; overlay.setAttribute('data-reason', '');
this.overlay_message_el.querySelector('.-keyhint').textContent = overlay_keyhint;
let middle = this.overlay_message_el.querySelector('.-middle');
middle.textContent = '';
if (overlay_middle) {
middle.append(overlay_middle);
} }
// Ask the renderer to apply a rewind effect only when rewinding, or when paused from // Ask the renderer to apply a rewind effect only when rewinding, or when paused from
@ -2041,6 +2137,7 @@ class Player extends PrimaryView {
if (style['display'] === 'none') if (style['display'] === 'none')
return; return;
let tolerable_fraction = 1;
let is_portrait = window.matchMedia('(orientation: portrait)').matches; let is_portrait = window.matchMedia('(orientation: portrait)').matches;
// The base size is the size of the canvas, i.e. the viewport size times the tile size -- // The base size is the size of the canvas, i.e. the viewport size times the tile size --
// but note that we have 2x4 extra tiles for the inventory depending on layout, plus half a // but note that we have 2x4 extra tiles for the inventory depending on layout, plus half a
@ -2061,8 +2158,16 @@ class Player extends PrimaryView {
// between the player container and the game area // between the player container and the game area
let player = this.root.querySelector('#player-main'); let player = this.root.querySelector('#player-main');
let game_area = this.root.querySelector('#player-game-area'); let game_area = this.root.querySelector('#player-game-area');
let avail_x = this.root.offsetWidth - (player.offsetWidth - game_area.offsetWidth); let avail_x = this.root.offsetWidth;
let avail_y = this.root.offsetHeight - (player.offsetHeight - game_area.offsetHeight); let avail_y = this.root.offsetHeight;
if (is_portrait) {
// Controls are only on top and bottom; anything to the sides is empty space
avail_y -= (player.offsetHeight - game_area.offsetHeight);
}
else {
// Other way around
avail_x -= (player.offsetWidth - game_area.offsetWidth);
}
// ...minus the width of the debug panel, if visible // ...minus the width of the debug panel, if visible
if (this.debug.enabled) { if (this.debug.enabled) {
avail_x -= this.root.querySelector('#player-debug').getBoundingClientRect().width; avail_x -= this.root.querySelector('#player-debug').getBoundingClientRect().width;
@ -2072,6 +2177,7 @@ class Player extends PrimaryView {
avail_y -= Math.max(0, document.body.scrollHeight - document.body.clientHeight); avail_y -= Math.max(0, document.body.scrollHeight - document.body.clientHeight);
let dpr = window.devicePixelRatio || 1.0; let dpr = window.devicePixelRatio || 1.0;
dpr *= tolerable_fraction;
// Divide to find the biggest scale that still fits. Leave a LITTLE wiggle room for pixel // Divide to find the biggest scale that still fits. Leave a LITTLE wiggle room for pixel
// rounding and breathing (except on small screens, where being too small REALLY hurts), but // rounding and breathing (except on small screens, where being too small REALLY hurts), but
// not too much since there's already a flex gap between the game and header/footer // not too much since there's already a flex gap between the game and header/footer
@ -2411,7 +2517,7 @@ class Splash extends PrimaryView {
_create_pack_element(ident, packdef = null) { _create_pack_element(ident, packdef = null) {
let title = packdef ? packdef.title : ident; let title = packdef ? packdef.title : ident;
let button = mk('button.button-big', {type: 'button'}, title); let button = mk('button.button-big.button-bright', {type: 'button'}, title);
if (packdef) { if (packdef) {
button.addEventListener('click', ev => { button.addEventListener('click', ev => {
this.conductor.fetch_pack(packdef.path, packdef.title); this.conductor.fetch_pack(packdef.path, packdef.title);
@ -2959,26 +3065,6 @@ class OptionsOverlay extends DialogOverlay {
); );
} }
_add_options(root, options) {
let ul = mk('ul');
root.append(ul);
for (let optdef of options) {
let li = mk('li');
let label = mk('label.option');
label.append(mk('input', {type: 'checkbox', name: optdef.key}));
label.append(mk('span.option-label', optdef.label));
let help_icon = mk('img.-help', {src: 'icons/help.png'});
label.append(help_icon);
let help_text = mk('p.option-help', optdef.note);
li.append(label);
li.append(help_text);
ul.append(li);
help_icon.addEventListener('click', ev => {
help_text.classList.toggle('--visible');
});
}
}
close() { close() {
// Ensure the player's music is set back how we left it // Ensure the player's music is set back how we left it
this.conductor.player.update_music_playback_state(); this.conductor.player.update_music_playback_state();
@ -3387,7 +3473,7 @@ class LevelBrowserOverlay extends DialogOverlay {
this.set_title("choose a level"); this.set_title("choose a level");
let thead = mk('thead', mk('tr', let thead = mk('thead', mk('tr',
mk('th', ""), mk('th', ""),
mk('th', "Level"), mk('th.-title', "Level"),
mk('th.-time', mk('abbr', { mk('th.-time', mk('abbr', {
title: "Time left on the clock when you finished; doesn't exist for untimed levels", title: "Time left on the clock when you finished; doesn't exist for untimed levels",
}, "Best clock")), }, "Best clock")),
@ -3414,6 +3500,7 @@ class LevelBrowserOverlay extends DialogOverlay {
} }
// 0 means untimed level // 0 means untimed level
// FIXME wait, not necessarily! shouldn't untimed be null?
if (scorecard.time !== 0) { if (scorecard.time !== 0) {
time = String(scorecard.time); time = String(scorecard.time);
} }
@ -3504,7 +3591,7 @@ class LevelBrowserOverlay extends DialogOverlay {
table.append(mk('tfoot', mk('tr', table.append(mk('tfoot', mk('tr',
mk('th'), mk('th'),
mk('th', "Total"), mk('th.-title', "Total"),
mk('th'), mk('th'),
mk('th.-time', util.format_duration(total_abstime / TICS_PER_SECOND, 2)), mk('th.-time', util.format_duration(total_abstime / TICS_PER_SECOND, 2)),
mk('th.-score', total_score.toLocaleString()), mk('th.-score', total_score.toLocaleString()),
@ -3836,6 +3923,14 @@ class Conductor {
return this.change_level(level_index ?? (this.current_pack_savefile.current_level ?? 1) - 1); return this.change_level(level_index ?? (this.current_pack_savefile.current_level ?? 1) - 1);
} }
// Attempt to change level, but silently return false if the given level number doesn't exist
maybe_change_level(level_index) {
if (level_index < 0 || level_index >= this.stored_game.level_metadata.length)
return false;
return this.change_level(level_index);
}
change_level(level_index) { change_level(level_index) {
// FIXME handle errors here // FIXME handle errors here
try { try {

View File

@ -112,6 +112,15 @@ export class CanvasRenderer {
return [x, y]; return [x, y];
} }
point_to_cell_coords(client_x, client_y) {
let rect = this.canvas.getBoundingClientRect();
let scale_x = rect.width / this.canvas.width;
let scale_y = rect.height / this.canvas.height;
let x = Math.floor((client_x - rect.x) / scale_x / this.tileset.size_x + this.viewport_x);
let y = Math.floor((client_y - rect.y) / scale_y / this.tileset.size_y + this.viewport_y);
return [x, y];
}
real_cell_coords_from_event(ev) { real_cell_coords_from_event(ev) {
let rect = this.canvas.getBoundingClientRect(); let rect = this.canvas.getBoundingClientRect();
let scale_x = rect.width / this.canvas.width; let scale_x = rect.width / this.canvas.width;
@ -121,6 +130,15 @@ export class CanvasRenderer {
return [x, y]; return [x, y];
} }
point_to_real_cell_coords(client_x, client_y) {
let rect = this.canvas.getBoundingClientRect();
let scale_x = rect.width / this.canvas.width;
let scale_y = rect.height / this.canvas.height;
let x = (client_x - rect.x) / scale_x / this.tileset.size_x + this.viewport_x;
let y = (client_y - rect.y) / scale_y / this.tileset.size_y + this.viewport_y;
return [x, y];
}
// Draw to a canvas using tile coordinates // Draw to a canvas using tile coordinates
blit(ctx, sx, sy, dx, dy, w = 1, h = w) { blit(ctx, sx, sy, dx, dy, w = 1, h = w) {
let tw = this.tileset.size_x; let tw = this.tileset.size_x;

511
style.css
View File

@ -18,9 +18,9 @@ body {
color: #ececec; color: #ececec;
--panel-bg-color: hsl(220, 10%, 15%); --panel-bg-color: hsl(220, 10%, 15%);
--button-bg-color: hsl(220, 10%, 25%); --button-bg-color: hsl(220, 20%, 25%);
--button-bg-shadow-color: #fff1; --button-bg-shadow-color: #fff1;
--button-bg-hover-color: hsl(220, 15%, 30%); --button-bg-hover-color: hsl(220, 30%, 30%);
--generic-bg-hover-on-white: hsl(220, 60%, 90%); --generic-bg-hover-on-white: hsl(220, 60%, 90%);
--generic-bg-selected-on-white: hsl(220, 60%, 85%); --generic-bg-selected-on-white: hsl(220, 60%, 85%);
--generic-border-selected-on-white: hsl(220, 60%, 75%); --generic-border-selected-on-white: hsl(220, 60%, 75%);
@ -50,10 +50,10 @@ button,
color: white; color: white;
background-color: var(--button-bg-color); background-color: var(--button-bg-color);
background-image: linear-gradient(to bottom, var(--button-bg-shadow-color), transparent 75%); background-image: linear-gradient(to bottom, var(--button-bg-shadow-color), transparent 75%);
border: 1px solid hsl(220, 10%, 10%); border: 1px solid hsl(220, 10%, 7.5%);
box-shadow: box-shadow:
inset 0 0 0 1px hsl(220, 10%, 33%), inset 0 0 1px 1px #fff2,
0 1px 1px hsl(220, 10%, 10%); 0 1px 1px hsl(220, 10%, 7.5%);
border-radius: 0.25em; border-radius: 0.25em;
text-transform: lowercase; text-transform: lowercase;
cursor: pointer; cursor: pointer;
@ -68,12 +68,15 @@ button:active,
/* Need this for the editor's tool help things and i'm not questioning it */ /* Need this for the editor's tool help things and i'm not questioning it */
z-index: 1; z-index: 1;
} }
button:enabled.button-bright {
background-color: hsl(220, 50%, 25%);
}
button:enabled.button-bright:hover {
background-color: hsl(220, 70%, 30%);
}
button:disabled { button:disabled {
color: #606060; color: #606060;
background-color: #202020; background-color: #202020;
box-shadow:
inset 0 0 2px 1px hsl(220, 0%, 10%),
0 1px 0 hsl(220, 10%, 10%);
cursor: auto; cursor: auto;
} }
button.button-big { button.button-big {
@ -202,12 +205,13 @@ svg.svg-icon {
border-top-right-radius: 0.25em; border-top-right-radius: 0.25em;
border-bottom-right-radius: 0.25em; border-bottom-right-radius: 0.25em;
} }
button.--pressed,
.radio-faux-button-set > label > input:checked + span { .radio-faux-button-set > label > input:checked + span {
background: hsl(220, 80%, 50%); background: hsl(220, 80%, 50%);
box-shadow: box-shadow:
inset 0 0 1px 1px hsl(220, 50%, 40%), inset 0 1px 3px 1px hsl(220, 50%, 15%),
inset 0 -0.125em 0.5em 0.25em hsl(220, 50%, 30%), inset 0 0.25em 1em 0.5em hsl(220, 50%, 30%),
0 1px 1px hsl(220, 10%, 10%); 0 1px 1px hsl(220, 10%, 10%)
} }
.button-row { .button-row {
@ -411,6 +415,84 @@ table.level-browser tbody tr:hover {
table.level-browser tbody tr:nth-child(10n) td { table.level-browser tbody tr:nth-child(10n) td {
border-bottom: 2px solid hsl(220, 20%, 80%); border-bottom: 2px solid hsl(220, 20%, 80%);
} }
@media (max-width: 600px) {
/* Unique media query: this is only necessary for VERY narrow screens */
/* In order to wrap the rows, turn the table markup into a stack of grids */
table.level-browser {
display: block;
}
table.level-browser tr {
display: grid;
grid:
"number star name name forget"
"number . clock time score"
/ 3em 1em 1fr 1fr 1fr
;
}
table.level-browser td,
table.level-browser th {
display: block;
}
table.level-browser thead .-title,
table.level-browser tfoot .-title {
/* "Level" and "Total" column headers are not useful and eat a lot of space */
display: none;
}
table.level-browser thead th:empty,
table.level-browser tfoot th:empty {
/* These are filler for table layout purposes */
display: none;
}
table.level-browser .-number {
grid-area: number;
}
table.level-browser .-title {
grid-area: name;
}
table.level-browser .-time {
grid-area: clock;
}
table.level-browser .-time + .-time {
grid-area: time;
}
table.level-browser .-score {
grid-area: score;
}
table.level-browser .-aid {
grid-area: star;
/* We overflow a bit because of our padding, so just leak into it */
justify-self: center;
}
table.level-browser .-button {
grid-area: forget;
justify-self: end;
}
/* Move borders off cells and onto rows */
table.level-browser thead tr th {
border: none;
}
table.level-browser tfoot tr th {
border: none;
}
table.level-browser tbody tr:nth-child(10n) td {
border: none;
}
table.level-browser thead tr {
border-bottom: 2px solid hsl(220, 20%, 60%);
}
table.level-browser tfoot tr {
border-top: 2px solid hsl(220, 20%, 60%);
}
table.level-browser tbody tr {
border-bottom: 1px solid #ddd;
}
table.level-browser tbody tr.--current {
border: none;
}
table.level-browser tbody tr:nth-child(10n) {
border-bottom: 2px solid hsl(220, 20%, 80%);
}
}
/* Compat dialog */ /* Compat dialog */
.dialog-compat { .dialog-compat {
@ -441,6 +523,46 @@ img.compat-icon,
vertical-align: middle; vertical-align: middle;
} }
@media (max-width: 800px) {
/* Stack the formgrid, it doesn't fit very well as columns */
.dialog dl.formgrid {
display: block;
}
.dialog dl.formgrid > dt {
margin: 0.5em 0;
text-align: left;
}
.dialog dl.formgrid > dt:first-child {
margin-top: 0;
}
.dialog dl.formgrid > * + dt {
border-top: 1px solid #ccc;
padding-top: 0.5em;
}
.dialog dl.formgrid > * + dt:empty {
padding-top: 0;
}
.dialog dl.formgrid > dd {
margin: 0.5em 0;
}
.dialog-compat .radio-faux-button-set {
font-size: 0.83em;
flex-wrap: wrap;
}
.dialog-compat .radio-faux-button-set > * {
flex: 1 0 30%;
}
.dialog-compat .radio-faux-button-set .-button {
border-radius: 0;
}
ul.compat-flags img.compat-icon,
ul.compat-flags span.compat-icon-gap {
width: 16px;
height: 16px;
}
}
/* Options dialog */ /* Options dialog */
.dialog-options { .dialog-options {
@ -481,6 +603,8 @@ label.option .option-label {
max-width: 90%; max-width: 90%;
max-height: 90%; max-height: 90%;
} }
.dialog-options {
}
} }
@ -511,7 +635,6 @@ body > header > nav {
gap: 0.5em; gap: 0.5em;
} }
body > header button { body > header button {
font-size: 0.75em;
white-space: nowrap; white-space: nowrap;
} }
body > header h1 a { body > header h1 a {
@ -587,14 +710,18 @@ pre.stack-trace {
order: 3; order: 3;
color: #606060; color: #606060;
} }
#header-icon {
image-rendering: crisp-edges;
image-rendering: pixelated;
}
@media (max-width: 800px) { @media ((orientation: portrait) and (max-width: 800px)) or ((orientation: landscape) and (max-height: 600px)) {
body > header { body > header {
padding: 0.125em 0.25em; padding: 1px;
} }
/* All these headings are way too big on phones */ /* All these headings are way too big on phones */
body > header h1 { body > header h1 {
font-size: 1.25em; font-size: 1.125em;
} }
body > header h2 { body > header h2 {
font-size: 1.125em; font-size: 1.125em;
@ -606,6 +733,12 @@ pre.stack-trace {
/* "a game by eevee" takes up too much space :( */ /* "a game by eevee" takes up too much space :( */
display: none; display: none;
} }
/* Hide the top/bottom nav while playing entirely */
body[data-mode=player] #header-pack,
body[data-mode=player] #header-level {
display: none;
}
} }
/**************************************************************************************************/ /**************************************************************************************************/
@ -813,15 +946,6 @@ pre.stack-trace {
/* this also forces the button to be 1 line of text high even for empty title */ /* this also forces the button to be 1 line of text high even for empty title */
white-space: pre-wrap; white-space: pre-wrap;
} }
.played-pack-list > li > button:enabled {
background-color: hsl(220, 30%, 25%);
box-shadow:
inset 0 0 0 1px hsl(220, 30%, 33%),
0 1px 1px hsl(220, 10%, 10%);
}
.played-pack-list > li > button:enabled:hover {
background-color: hsl(220, 40%, 30%);
}
.played-pack-list p { .played-pack-list p {
color: #c0c0c0; color: #c0c0c0;
font-style: italic; font-style: italic;
@ -1128,9 +1252,6 @@ ol.packtest-summary > li {
#player-actions button svg { #player-actions button svg {
font-size: 1em; font-size: 1em;
} }
#player-controls .-optional-label {
display: none;
}
#player-controls button { #player-controls button {
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
line-height: 1.33; line-height: 1.33;
@ -1159,28 +1280,48 @@ ol.packtest-summary > li {
"buttons" "buttons"
"game" "game"
"actions" "actions"
"music"
; ;
} }
#player-controls, #player-controls,
#player-actions { #player-actions {
/* Not much in these rows so make them a bit bigger for hittability */
font-size: 1.33em;
justify-content: center; justify-content: center;
} }
#player-controls button, #player-controls button,
#player-actions button { #player-actions button {
flex: auto; flex: auto;
padding: 0.25em 0.5em;
}
#player-controls .control-restart {
/* This is a dedicated pause-menu button */
display: none;
} }
#player .keyhint { #player .keyhint {
/* Hide key hints; there's nowhere to put them and they take up surprisingly a lot of space */ /* Hide key hints; there's nowhere to put them and they take up surprisingly a lot of space */
display: none; display: none;
} }
} }
@media (max-width: 800px) { @media (orientation: landscape) and (max-height: 600px) {
/* On a small landscape screen, remove the music row (it matters!) */
#player-main {
grid:
"buttons game actions"
"buttons game actions"
/ 1fr auto 1fr
;
}
}
@media ((orientation: portrait) and (max-width: 800px)) or ((orientation: landscape) and (max-height: 600px)) {
#player-controls .-optional-label {
display: none;
}
#splash-fullscreen { #splash-fullscreen {
display: revert; display: revert;
} }
#player-music { #player-music {
font-size: 0.875em; /* TODO :( */
display: none;
} }
} }
@ -1206,7 +1347,7 @@ ol.packtest-summary > li {
row-gap: calc(var(--tile-height) * var(--scale) / 4); row-gap: calc(var(--tile-height) * var(--scale) / 4);
padding: calc(var(--tile-height) * var(--scale) / 4) calc(var(--tile-width) * var(--scale) / 4); padding: calc(var(--tile-height) * var(--scale) / 4) calc(var(--tile-width) * var(--scale) / 4);
background: hsl(220, 10%, 20%); background: hsl(220, 10%, 15%);
box-shadow: 0 0.25em 1em black; box-shadow: 0 0.25em 1em black;
} }
@ -1214,7 +1355,7 @@ ol.packtest-summary > li {
grid-area: level; grid-area: level;
position: relative; position: relative;
outline: 2px solid black; outline: 1px solid hsl(220, 10%, 5%);
} }
.level canvas { .level canvas {
display: block; display: block;
@ -1223,78 +1364,172 @@ ol.packtest-summary > li {
--viewport-width: 9; --viewport-width: 9;
--viewport-height: 9; --viewport-height: 9;
} }
#player .overlay-message {
.player-overlay-message {
grid-area: level; grid-area: level;
place-self: stretch; place-self: stretch;
position: relative; position: relative;
display: grid; display: grid;
grid-template-rows: 2fr 6fr 2fr 1fr; grid:
justify-content: stretch; "pack" calc(1.25em * 1.25 * 1)
"level" calc(1.333em * 1.25 * 2)
"author" calc(1em * 1.25 * 1)
"space" 1fr
"controls" 1.5em
;
align-items: center; align-items: center;
/* Prevent blowout; force using the canvas's height */ gap: 0.25em;
/* Prevent blowout; force using the canvas's size */
height: 0; height: 0;
min-height: 100%; min-height: 100%;
width: 0;
min-width: 100%;
box-sizing: border-box; box-sizing: border-box;
z-index: 2; z-index: 2;
font-size: calc(0.5 * var(--tile-width) * var(--scale)); font-size: calc(0.5 * var(--tile-width) * var(--scale));
line-height: 1.25;
background: #0009; background: #0009;
color: white; color: white;
text-align: center; text-align: center;
text-shadow: 0 2px 1px black; text-shadow: 0 1px 1px black;
}
#player .overlay-message > * {
padding: 0 5%;
} }
/* Allow clicking through the overlay in debug mode */ /* Allow clicking through the overlay in debug mode */
body.--debug .overlay-message { body.--debug .player-overlay-message {
pointer-events: none; pointer-events: none;
} }
#player .overlay-message p { .player-overlay-message > * {
margin: 0; padding: 0 0.25em;
} }
#player .overlay-message .-top { .player-overlay-message h1 {
font-size: 1.5em; /* Pack title, doesn't need to be too big */
grid-area: pack;
font-size: 1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: hsl(220, 25%, 60%);
} }
#player .overlay-message .-middle { .player-overlay-message > h2 {
grid-area: level;
font-size: 2em;
} }
#player .overlay-message .-bottom { .player-overlay-message[data-reason='waiting'] > h2 {
/* For 'waiting' this is a level name, so make it two lines of smaller text */
font-size: 1.333em;
} }
#player .overlay-message .-keyhint { .player-overlay-message > h3 {
align-self: end; grid-area: author;
font-size: 0.5em; font-size: 1em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: hsl(220, 10%, 90%);
}
.player-overlay-message > .scoreboard {
grid-row: author / space;
}
.player-overlay-message .-controls-hint {
grid-area: controls;
font-size: 0.75em;
color: #c0c0c0; color: #c0c0c0;
} }
#player .overlay-message[data-reason=""] { .player-overlay-message .mobile-pause-menu {
grid-area: space;
}
.mobile-pause-menu {
font-size: 1.25em;
display: none; /* flex */
flex-direction: column;
align-items: stretch;
gap: 0.33em;
width: 80%;
margin: auto;
}
.mobile-pause-menu button {
padding: 0.33em;
}
.mobile-pause-menu > p {
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: stretch;
margin: 0;
gap: 0.33em;
}
.mobile-pause-menu > p > button {
flex: 1;
line-height: 1;
}
.mobile-pause-menu > p > button.-narrow {
flex: initial;
}
.mobile-pause-menu .-only-waiting,
.mobile-pause-menu .-only-paused,
.mobile-pause-menu .-only-failure,
.mobile-pause-menu .-only-success,
.mobile-pause-menu .-only-ended {
display: none; display: none;
} }
#player .overlay-message[data-reason=failure] { .player-overlay-message[data-reason=waiting] .mobile-pause-menu .-only-waiting,
box-shadow: inset 0 0 calc(4 * var(--tile-width)) var(--tile-width) black; .player-overlay-message[data-reason=paused] .mobile-pause-menu .-only-paused,
.player-overlay-message[data-reason=failure] .mobile-pause-menu .-only-failure,
.player-overlay-message[data-reason=success] .mobile-pause-menu .-only-success,
.player-overlay-message[data-reason=ended] .mobile-pause-menu .-only-ended {
display: initial;
} }
#player .overlay-message[data-reason=success] { .player-overlay-message[data-reason=paused] .mobile-pause-menu p.-only-paused {
background: hsla(220, 50%, 25%, 0.5); display: flex;
box-shadow: inset 0 0 calc(4 * var(--tile-width)) hsl(220, 50%, 25%);
} }
#player .overlay-message[data-reason=ended] { .player-overlay-message[data-reason=""] {
/* Shove the middle + bottom parts down so they don't overlay the busiest part of the ending image */ display: none;
grid-template-rows: 8fr 4fr 2fr 0; }
.player-overlay-message[data-reason=waiting] {
background: linear-gradient(to bottom, #000d, #0008 40%, #0008 60%, #000d);
}
.player-overlay-message[data-reason=failure] {
background: hsla(330, 20%, 10%, 0.5);
background: radial-gradient(#0004, hsla(330, 10%, 10%, 0.5) 40%, hsl(330, 20%, 10%));
}
.player-overlay-message[data-reason=success] {
background: radial-gradient(hsla(220, 60%, 5%, 0.75), 60%, hsla(220, 60%, 25%, 0.75));
}
.player-overlay-message[data-reason=ended] {
/* Rearrange this entirely, to fit the ending image in */
grid:
"congrats" min-content
"." 0.5em
"menu" 1fr
"." 0.5em
"score" min-content
;
overflow: hidden; overflow: hidden;
x-color: black;
background: url(ending.png) no-repeat center center / cover; background: url(ending.png) no-repeat center center / cover;
box-shadow: inset 0 0 calc(4 * var(--tile-width)) hsl(220, 50%, 25%); box-shadow: inset 0 0 calc(4 * var(--tile-width)) hsl(220, 50%, 25%);
x-text-shadow: 0 0 2px white, 0 2px 2px white;
} }
#player .overlay-message[data-reason=ended] .-middle { .player-overlay-message[data-reason=ended] .mobile-pause-menu {
grid-area: menu;
}
.player-overlay-message[data-reason=ended] > .-congrats {
grid-area: congrats;
margin: 0;
padding: 0.25em;
background: #0009; background: #0009;
} }
#player .overlay-message[data-reason=ended] .-bottom { .player-overlay-message[data-reason=ended] > .-score {
font-size: 1.5em; grid-area: score;
margin: 0;
padding: 0.25em;
background: #0006; background: #0006;
} }
.player-overlay-message[data-reason=ended] > .-score output {
font-size: 2em;
display: block;
}
@supports (mask-image: none) or (-webkit-mask-image: none) { @supports (mask-image: none) or (-webkit-mask-image: none) {
/* Do this complicated rotating sunburst thing only if masks work */ /* Do this complicated rotating sunburst thing only if masks work */
#player .overlay-message[data-reason=ended]::before { .player-overlay-message[data-reason=ended]::before {
content: ''; content: '';
position: absolute; position: absolute;
z-index: -1; z-index: -1;
@ -1317,35 +1552,68 @@ body.--debug .overlay-message {
transform: rotate(1turn); transform: rotate(1turn);
} }
} }
dl.score-chart { .scoreboard {
display: grid; display: grid;
grid-auto-columns: 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
grid-auto-rows: 1.33em; grid-auto-rows: min-content;
margin: auto 10%; align-items: center;
row-gap: 0.75em;
margin: auto 5%;
font-weight: normal; font-weight: normal;
text-align: center;
} }
dl.score-chart dt { .scoreboard .-subscore {
grid-column: 1; grid-column: span 2;
text-align: left; color: #f4f4f4;
} }
dl.score-chart dd { .scoreboard .-level-score {
grid-column: 2; grid-column: span 3;
}
.scoreboard .-improvement {
grid-column: span 3;
}
.scoreboard .-improvement.--same h4 {
color: hsl(240, 50%, 60%);
}
.scoreboard .-improvement.--same p {
color: hsl(240, 50%, 80%);
}
.scoreboard .-improvement.--worse h4 {
color: hsl(330, 50%, 60%);
}
.scoreboard .-improvement.--worse p {
color: hsl(330, 50%, 80%);
}
.scoreboard .-improvement.--better h4 {
color: hsl(210, 50%, 60%);
}
.scoreboard .-improvement.--better p {
color: hsl(210, 50%, 80%);
}
.scoreboard .-total-score {
grid-column: span 3;
color: hsl(45, 50%, 75%);
}
.scoreboard h4 {
font-size: 0.833em;
color: hsl(220, 10%, 80%);
}
.scoreboard .-total-score h4 {
color: hsl(30, 50%, 60%);
}
.scoreboard p {
margin: 0; margin: 0;
text-align: right;
} }
dl.score-chart .-component { .scoreboard .-total-score p {
color: #d8d8d8; font-size: 1.333em;
}
dl.score-chart .-sum {
border-top: 1px solid white;
}
dl.score-chart .-total {
color: hsl(40, 75%, 80%);
}
dl.score-chart .-star {
position: absolute;
} }
.player-level-number {
grid-area: number;
/* This is only for portrait, and mostly to fill space */
display: none;
line-height: 1;
}
.chips { .chips {
grid-area: chips; grid-area: chips;
} }
@ -1358,17 +1626,17 @@ dl.score-chart .-star {
.chips, .chips,
.time, .time,
.bonus { .bonus {
font-size: calc(var(--tile-height) * var(--scale) / 3); font-size: calc(var(--tile-height) * var(--scale) * 3/4);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5em; gap: 0.25em;
} }
.chips h3, .chips h3,
.time h3, .time h3,
.bonus h3 { .bonus h3 {
flex: 0; flex: 0;
order: 2; order: 2;
font-size: 1.5em; font-size: 0.75em;
line-height: 1; line-height: 1;
color: hsl(220, 20%, 80%); color: hsl(220, 20%, 80%);
} }
@ -1376,7 +1644,6 @@ dl.score-chart .-star {
.time output, .time output,
.bonus output { .bonus output {
flex: 1; flex: 1;
font-size: 2em;
min-width: 2em; min-width: 2em;
min-height: 1em; min-height: 1em;
line-height: 1; line-height: 1;
@ -1408,28 +1675,25 @@ dl.score-chart .-star {
} }
} }
.chips output.--done, .chips output.--done,
.time output.--frozen { .time output.--frozen,
.bonus output {
color: hsl(220, 10%, 30%); color: hsl(220, 10%, 30%);
} }
.bonus output { #player.--bonus-visible .bonus output {
color: #e2c9ff; color: #e2c9ff;
} }
#player .bonus {
visibility: hidden;
display: flex;
}
#player.--bonus-visible .bonus {
visibility: initial;
}
.player-rules { .player-rules {
font-size: calc(var(--tile-height) * var(--scale) / 4); font-size: calc(var(--tile-height) * var(--scale) / 4);
grid-area: rules; grid-area: rules;
align-self: end; align-self: end;
display: flex;
flex-direction: column;
gap: 0.5em;
color: hsl(220, 20%, 80%); color: hsl(220, 20%, 80%);
} }
.player-rules p { .player-rules p {
display: none; display: none;
margin: 0.25em 0; margin: 0;
} }
#player.--hide-logic .player-rules #player-rule-logic-hidden { #player.--hide-logic .player-rules #player-rule-logic-hidden {
display: revert; display: revert;
@ -1516,17 +1780,25 @@ dl.score-chart .-star {
#player-game-area { #player-game-area {
/* Rearrange the grid to be vertical */ /* Rearrange the grid to be vertical */
grid: grid:
"level level level" "inventory chips chips time" min-content
"inventory rules chips" calc((var(--tile-height) * var(--scale) * (2 - 1/6)) / 3) "inventory rules bonus bonus" min-content
"inventory rules time" calc((var(--tile-height) * var(--scale) * (2 - 1/6)) / 3) "level level level level" min-content
"inventory rules bonus" calc((var(--tile-height) * var(--scale) * (2 - 1/6)) / 3) / min-content 1fr 1fr 2fr
/ min-content min-content 1fr
; ;
row-gap: calc(var(--tile-height) * var(--scale) / 6); }
.player-level-number {
/* TODO this makes us too big on my phone, damn */
/*display: initial;*/
}
.chips,
.time,
.bonus {
/* These numbers need to be sliiightly smaller */
font-size: calc(var(--tile-height) * var(--scale) * 2/3);
} }
#player .inventory { #player .inventory {
/* stick me in the center right */ /* stick me in the center left */
place-self: center end; place-self: center start;
} }
#player-game-area > .player-hint-wrapper { #player-game-area > .player-hint-wrapper {
/* Overlay hints on the inventory area */ /* Overlay hints on the inventory area */
@ -1536,17 +1808,38 @@ dl.score-chart .-star {
font-size: calc(var(--tile-height) * var(--scale) / 2.5); font-size: calc(var(--tile-height) * var(--scale) / 2.5);
} }
#player-game-area > .player-hint-wrapper > .player-hint { #player-game-area > .player-hint-wrapper > .player-hint {
padding: 0.25em 0.33em; padding: 0.33em 0.5em;
line-height: 1.33; line-height: 1.33;
} }
.player-rules { .player-rules {
align-self: center; align-self: center;
flex-direction: row;
} }
.player-rules p span { .player-rules p span {
/* There's only room for the icons, since there's no dedicated hint space */ /* There's only room for the icons, since there's no dedicated hint space */
display: none; display: none;
} }
} }
@media ((orientation: portrait) and (max-width: 800px)) or ((orientation: landscape) and (max-height: 600px)) {
/* Overlay is a bit different on what I assume is a touchscreen */
.player-overlay-message[data-reason='waiting'] > p {
/* Hide the "Ready!" and controls, since there's a menu */
display: none;
}
.mobile-pause-menu {
display: flex;
}
}
@media (orientation: portrait) and (max-width: 800px) {
#player-game-area {
padding: 0;
background: none;
box-shadow: none;
}
.level {
outline: 1px solid black;
}
}
/* Debug stuff */ /* Debug stuff */
body.--debug #player-debug { body.--debug #player-debug {