Add support for demos, terrible UI for it, and a clumsy pause button
This commit is contained in:
parent
101b68c017
commit
b871181bf4
@ -1,6 +1,91 @@
|
|||||||
import * as util from './format-util.js';
|
import * as util from './format-util.js';
|
||||||
import TILE_TYPES from './tiletypes.js';
|
import TILE_TYPES from './tiletypes.js';
|
||||||
|
|
||||||
|
const CC2_DEMO_INPUT_MASK = {
|
||||||
|
drop: 0x01,
|
||||||
|
down: 0x02,
|
||||||
|
left: 0x04,
|
||||||
|
right: 0x08,
|
||||||
|
up: 0x10,
|
||||||
|
swap: 0x20,
|
||||||
|
cycle: 0x40,
|
||||||
|
};
|
||||||
|
|
||||||
|
class CC2Demo {
|
||||||
|
constructor(buf) {
|
||||||
|
this.buf = buf;
|
||||||
|
this.bytes = new Uint8Array(buf);
|
||||||
|
|
||||||
|
// byte 0 is unknown, always 0?
|
||||||
|
this.force_floor_seed = this.bytes[1];
|
||||||
|
this.blob_seed = this.bytes[2];
|
||||||
|
|
||||||
|
|
||||||
|
let l = this.bytes.length;
|
||||||
|
if (l % 2 === 0) {
|
||||||
|
l--;
|
||||||
|
}
|
||||||
|
for (let p = 3; p < l; p += 2) {
|
||||||
|
let delay = this.bytes[p];
|
||||||
|
|
||||||
|
let input_mask = this.bytes[p + 1];
|
||||||
|
let input = new Set;
|
||||||
|
if ((input_mask & 0x80) !== 0) {
|
||||||
|
input.add('p2');
|
||||||
|
}
|
||||||
|
for (let [action, bit] of Object.entries(CC2_DEMO_INPUT_MASK)) {
|
||||||
|
if ((input_mask & bit) !== 0) {
|
||||||
|
input.add(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('demo step', delay, input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*[Symbol.iterator]() {
|
||||||
|
let l = this.bytes.length;
|
||||||
|
if (l % 2 === 0) {
|
||||||
|
l--;
|
||||||
|
// TODO assert last byte is terminating 0xff
|
||||||
|
}
|
||||||
|
let input = new Set;
|
||||||
|
let t = 0;
|
||||||
|
// 47 left 33 down means
|
||||||
|
// LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
|
||||||
|
// | * * * | * * * | * * * | * * * | * * * | * * * | * *
|
||||||
|
// | * * * | * * * | * * * | * * * | * * * | * * * | * *
|
||||||
|
for (let p = 3; p < l; p += 2) {
|
||||||
|
// The first byte measures how long the /previous/ input remains
|
||||||
|
// valid, so yield that first. Note that this is measured in 60Hz
|
||||||
|
// frames, so we need to convert to 20Hz tics by subtracting 3
|
||||||
|
// frames at a time.
|
||||||
|
t += this.bytes[p];
|
||||||
|
// t >= 4: almost right, desyncs just before yellow door
|
||||||
|
// t >= 3: skips a move, then desyncs trying to leave red door room
|
||||||
|
// t >= 2: skips a move, also desyncs before yellow door
|
||||||
|
// t >= 1: same as 2
|
||||||
|
// t >= 0: same as 3
|
||||||
|
while (t > 0) {
|
||||||
|
t -= 3;
|
||||||
|
console.log(t, input);
|
||||||
|
yield input;
|
||||||
|
}
|
||||||
|
|
||||||
|
let input_mask = this.bytes[p + 1];
|
||||||
|
let is_player_2 = ((input_mask & 0x80) !== 0);
|
||||||
|
// TODO handle player 2
|
||||||
|
for (let [action, bit] of Object.entries(CC2_DEMO_INPUT_MASK)) {
|
||||||
|
if ((input_mask & bit) === 0) {
|
||||||
|
input.delete(action);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
input.add(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO assert that direction + next match the tile types
|
// TODO assert that direction + next match the tile types
|
||||||
const TILE_ENCODING = {
|
const TILE_ENCODING = {
|
||||||
0x01: 'floor',
|
0x01: 'floor',
|
||||||
@ -405,9 +490,13 @@ export function parse_level(buf) {
|
|||||||
}
|
}
|
||||||
else if (section_type === 'KEY ') {
|
else if (section_type === 'KEY ') {
|
||||||
}
|
}
|
||||||
else if (section_type === 'REPL') {
|
else if (section_type === 'REPL' || section_type === 'PRPL') {
|
||||||
}
|
// "Replay", i.e. demo solution
|
||||||
else if (section_type === 'PRPL') {
|
let data = section_buf;
|
||||||
|
if (section_type === 'PRPL') {
|
||||||
|
data = decompress(data);
|
||||||
|
}
|
||||||
|
level.demo = new CC2Demo(data);
|
||||||
}
|
}
|
||||||
else if (section_type === 'RDNY') {
|
else if (section_type === 'RDNY') {
|
||||||
}
|
}
|
||||||
|
|||||||
271
js/main.js
271
js/main.js
@ -257,7 +257,7 @@ class Level {
|
|||||||
|
|
||||||
let stored_cell = this.stored_level.linear_cells[n];
|
let stored_cell = this.stored_level.linear_cells[n];
|
||||||
n++;
|
n++;
|
||||||
|
|
||||||
for (let template_tile of stored_cell) {
|
for (let template_tile of stored_cell) {
|
||||||
let tile = Tile.from_template(template_tile, x, y);
|
let tile = Tile.from_template(template_tile, x, y);
|
||||||
if (tile.type.is_player) {
|
if (tile.type.is_player) {
|
||||||
@ -282,8 +282,9 @@ class Level {
|
|||||||
|
|
||||||
advance_tic(player_direction) {
|
advance_tic(player_direction) {
|
||||||
if (this.state !== 'playing') {
|
if (this.state !== 'playing') {
|
||||||
console.warn(`Level.advance_tic() called when state is ${this.state}`);
|
// FIXME this breaks the step buttons; maybe pausing should be in game only
|
||||||
return;
|
//console.warn(`Level.advance_tic() called when state is ${this.state}`);
|
||||||
|
//return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX this entire turn order is rather different in ms rules
|
// XXX this entire turn order is rather different in ms rules
|
||||||
@ -327,6 +328,7 @@ class Level {
|
|||||||
}
|
}
|
||||||
else if (actor === this.player) {
|
else if (actor === this.player) {
|
||||||
if (player_direction) {
|
if (player_direction) {
|
||||||
|
console.log('--- player moving', player_direction);
|
||||||
direction_preference = [player_direction];
|
direction_preference = [player_direction];
|
||||||
actor.last_move_was_force = false;
|
actor.last_move_was_force = false;
|
||||||
}
|
}
|
||||||
@ -412,7 +414,7 @@ class Level {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only bother touching the goal cell if we're not already trapped in this one
|
// Only bother touching the goal cell if we're not already trapped in this one
|
||||||
// FIXME actually, this prevents flicking!
|
// FIXME actually, this prevents flicking!
|
||||||
if (! blocked) {
|
if (! blocked) {
|
||||||
@ -522,26 +524,82 @@ 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
|
||||||
|
// - button: quit to splash
|
||||||
|
// - button: options
|
||||||
|
// - implement winning and show score for this level
|
||||||
|
// - show current score so far
|
||||||
const GAME_UI_HTML = `
|
const GAME_UI_HTML = `
|
||||||
<main>
|
<main>
|
||||||
<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="meta"></div>
|
<div class="meta"></div>
|
||||||
<div class="nav">
|
<div class="nav">
|
||||||
<button class="nav-prev" type="button">«</button>
|
<button class="nav-prev" type="button">«</button>
|
||||||
<button class="nav-browse" type="button">Choose level...</button>
|
<button class="nav-browse" type="button">Level select</button>
|
||||||
<button class="nav-next" type="button">»</button>
|
<button class="nav-next" type="button">»</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="hint"></div>
|
<div class="hint"></div>
|
||||||
<div class="chips"></div>
|
<div class="chips"></div>
|
||||||
<div class="time"></div>
|
<div class="time"></div>
|
||||||
<div class="inventory"></div>
|
<div class="inventory"></div>
|
||||||
<div class="bummer"></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>
|
||||||
|
</div>
|
||||||
|
<div class="demo">
|
||||||
|
<h2>Solution demo available</h2>
|
||||||
|
<div class="demo-controls">
|
||||||
|
<button class="demo-play" type="button">Restart and play</button>
|
||||||
|
<button class="demo-step-1" type="button">Step 1 tic</button>
|
||||||
|
<button class="demo-step-4" type="button">Step 1 move</button>
|
||||||
|
<button class="demo-step-20" type="button">Step 1 second</button>
|
||||||
|
</div>
|
||||||
|
<div class="demo-scrubber"></div>
|
||||||
|
<div class="input"></div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
`;
|
`;
|
||||||
|
const ACTION_LABELS = {
|
||||||
|
up: '⬆️\ufe0f',
|
||||||
|
down: '⬇️\ufe0f',
|
||||||
|
left: '⬅️\ufe0f',
|
||||||
|
right: '➡️\ufe0f',
|
||||||
|
drop: '🚮',
|
||||||
|
cycle: '🔄',
|
||||||
|
swap: '👫',
|
||||||
|
};
|
||||||
|
const ACTION_DIRECTIONS = {
|
||||||
|
up: 'north',
|
||||||
|
down: 'south',
|
||||||
|
left: 'west',
|
||||||
|
right: 'east',
|
||||||
|
};
|
||||||
class Game {
|
class Game {
|
||||||
constructor(stored_game, tileset) {
|
constructor(stored_game, tileset) {
|
||||||
this.stored_game = stored_game;
|
this.stored_game = stored_game;
|
||||||
this.tileset = tileset;
|
this.tileset = tileset;
|
||||||
|
this.key_mapping = {
|
||||||
|
ArrowLeft: 'left',
|
||||||
|
ArrowRight: 'right',
|
||||||
|
ArrowUp: 'up',
|
||||||
|
ArrowDown: 'down',
|
||||||
|
w: 'up',
|
||||||
|
a: 'left',
|
||||||
|
s: 'down',
|
||||||
|
d: 'right',
|
||||||
|
q: 'drop',
|
||||||
|
e: 'cycle',
|
||||||
|
c: 'swap',
|
||||||
|
};
|
||||||
|
|
||||||
// TODO obey level options; allow overriding
|
// TODO obey level options; allow overriding
|
||||||
this.viewport_size_x = 9;
|
this.viewport_size_x = 9;
|
||||||
@ -559,6 +617,8 @@ class Game {
|
|||||||
this.time_el = this.container.querySelector('.time');
|
this.time_el = this.container.querySelector('.time');
|
||||||
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.demo_el = this.container.querySelector('.demo');
|
||||||
|
|
||||||
// Populate navigation
|
// Populate navigation
|
||||||
this.nav_prev_button = this.nav_el.querySelector('.nav-prev');
|
this.nav_prev_button = this.nav_el.querySelector('.nav-prev');
|
||||||
@ -576,6 +636,27 @@ 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';
|
||||||
|
}
|
||||||
|
ev.target.blur();
|
||||||
|
});
|
||||||
|
// Demo playback
|
||||||
|
this.container.querySelector('.demo .demo-step-1').addEventListener('click', ev => {
|
||||||
|
this.advance_by(1);
|
||||||
|
});
|
||||||
|
this.container.querySelector('.demo .demo-step-4').addEventListener('click', ev => {
|
||||||
|
this.advance_by(4);
|
||||||
|
});
|
||||||
|
this.container.querySelector('.demo .demo-step-20').addEventListener('click', ev => {
|
||||||
|
this.advance_by(20);
|
||||||
|
});
|
||||||
|
|
||||||
// Populate inventory
|
// Populate inventory
|
||||||
this._inventory_tiles = {};
|
this._inventory_tiles = {};
|
||||||
let floor_tile = this.render_inventory_tile('floor');
|
let floor_tile = this.render_inventory_tile('floor');
|
||||||
@ -599,48 +680,83 @@ class Game {
|
|||||||
this.next_player_move = null;
|
this.next_player_move = null;
|
||||||
this.player_used_move = false;
|
this.player_used_move = false;
|
||||||
let key_target = document.body;
|
let key_target = document.body;
|
||||||
|
this.previous_input = new Set; // actions that were held last tic
|
||||||
|
this.previous_action = null; // last direction we were moving, if any
|
||||||
|
this.current_keys = new Set; // keys that are currently held
|
||||||
// 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 => {
|
||||||
let direction;
|
if (this.key_mapping[ev.key]) {
|
||||||
if (ev.key === 'ArrowDown') {
|
this.current_keys.add(ev.key);
|
||||||
direction = 'south';
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
else if (ev.key === 'ArrowUp') {
|
|
||||||
direction = 'north';
|
|
||||||
}
|
|
||||||
else if (ev.key === 'ArrowLeft') {
|
|
||||||
direction = 'west';
|
|
||||||
}
|
|
||||||
else if (ev.key === 'ArrowRight') {
|
|
||||||
direction = 'east';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (! direction)
|
|
||||||
return;
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
last_key = ev.key;
|
|
||||||
this.pending_player_move = direction;
|
|
||||||
this.next_player_move = direction;
|
|
||||||
this.player_used_move = false;
|
|
||||||
});
|
});
|
||||||
key_target.addEventListener('keyup', ev => {
|
key_target.addEventListener('keyup', ev => {
|
||||||
if (ev.key === last_key) {
|
if (this.key_mapping[ev.key]) {
|
||||||
last_key = null;
|
this.current_keys.delete(ev.key);
|
||||||
this.pending_player_move = null;
|
ev.stopPropagation();
|
||||||
if (this.player_used_move) {
|
ev.preventDefault();
|
||||||
this.next_player_move = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Populate demo scrubber
|
||||||
|
let scrubber_el = this.container.querySelector('.demo-scrubber');
|
||||||
|
let scrubber_elements = {};
|
||||||
|
for (let [action, label] of Object.entries(ACTION_LABELS)) {
|
||||||
|
let el = mk('li');
|
||||||
|
scrubber_el.append(el);
|
||||||
|
scrubber_elements[action] = el;
|
||||||
|
}
|
||||||
|
this.demo_scrubber_marker = mk('div.demo-scrubber-marker');
|
||||||
|
scrubber_el.append(this.demo_scrubber_marker);
|
||||||
|
|
||||||
|
// Populate input debugger
|
||||||
|
this.input_el = this.container.querySelector('.input');
|
||||||
|
this.input_action_elements = {};
|
||||||
|
for (let [action, label] of Object.entries(ACTION_LABELS)) {
|
||||||
|
let el = mk('span.input-action', {'data-action': action}, label);
|
||||||
|
this.input_el.append(el);
|
||||||
|
this.input_action_elements[action] = el;
|
||||||
|
}
|
||||||
|
|
||||||
// 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();
|
this.redraw();
|
||||||
|
|
||||||
|
// Fill in the scrubber
|
||||||
|
if (false && this.level.stored_level.demo) {
|
||||||
|
let input_starts = {};
|
||||||
|
for (let action of Object.keys(ACTION_LABELS)) {
|
||||||
|
input_starts[action] = null;
|
||||||
|
}
|
||||||
|
let t = 0;
|
||||||
|
for (let input of this.level.stored_level.demo) {
|
||||||
|
for (let [action, t0] of Object.entries(input_starts)) {
|
||||||
|
if (input.has(action)) {
|
||||||
|
if (t0 === null) {
|
||||||
|
input_starts[action] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (t0 !== null) {
|
||||||
|
let bar = mk('span.demo-scrubber-bar');
|
||||||
|
bar.style.setProperty('--start-time', t0);
|
||||||
|
bar.style.setProperty('--end-time', t);
|
||||||
|
scrubber_elements[action].append(bar);
|
||||||
|
input_starts[action] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t += 1;
|
||||||
|
}
|
||||||
|
this.demo = this.level.stored_level.demo[Symbol.iterator]();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// TODO update these, as appropriate, when loading a level
|
||||||
|
this.input_el.style.display = 'none';
|
||||||
|
this.demo_el.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
this.frame = 0;
|
this.frame = 0;
|
||||||
this.tick++;
|
this.tic = 0;
|
||||||
requestAnimationFrame(this.do_frame.bind(this));
|
requestAnimationFrame(this.do_frame.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,18 +773,80 @@ class Game {
|
|||||||
this.update_ui();
|
this.update_ui();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_input() {
|
||||||
|
if (this.demo) {
|
||||||
|
let step = this.demo.next();
|
||||||
|
if (step.done) {
|
||||||
|
return new Set;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return step.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Convert input keys to actions. This is only done now
|
||||||
|
// because there might be multiple keys bound to one
|
||||||
|
// action, and it still counts as pressed as long as at
|
||||||
|
// least one key is held
|
||||||
|
let input = new Set;
|
||||||
|
for (let key of this.current_keys) {
|
||||||
|
input.add(this.key_mapping[key]);
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
advance_by(tics) {
|
||||||
|
for (let i = 0; i < tics; i++) {
|
||||||
|
let input = this.get_input();
|
||||||
|
let current_input = input;
|
||||||
|
if (! input.has('up') && ! input.has('down') && ! input.has('left') && ! input.has('right')) {
|
||||||
|
//input = this.previous_input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose the movement direction based on the held keys. A
|
||||||
|
// newly pressed action takes priority; in the case of a tie,
|
||||||
|
// um, XXX ????
|
||||||
|
let chosen_action = null;
|
||||||
|
let any_action = null;
|
||||||
|
for (let action of ['up', 'down', 'left', 'right']) {
|
||||||
|
if (input.has(action)) {
|
||||||
|
if (this.previous_input.has(action)) {
|
||||||
|
chosen_action = action;
|
||||||
|
}
|
||||||
|
any_action = action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (! chosen_action) {
|
||||||
|
// No keys are new, so check whether we were previously
|
||||||
|
// holding a key and are still doing it
|
||||||
|
if (this.previous_action && input.has(this.previous_action)) {
|
||||||
|
chosen_action = this.previous_action;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// No dice, so use an arbitrary action
|
||||||
|
chosen_action = any_action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let player_move = chosen_action ? ACTION_DIRECTIONS[chosen_action] : null;
|
||||||
|
this.previous_action = chosen_action;
|
||||||
|
this.previous_input = current_input;
|
||||||
|
|
||||||
|
this.level.advance_tic(player_move);
|
||||||
|
this.tic++;
|
||||||
|
}
|
||||||
|
this.redraw();
|
||||||
|
this.update_ui();
|
||||||
|
}
|
||||||
|
|
||||||
do_frame() {
|
do_frame() {
|
||||||
if (this.level.state === 'playing') {
|
if (this.level.state === 'playing') {
|
||||||
this.frame++;
|
this.frame++;
|
||||||
if (this.frame % 3 === 0) {
|
if (this.frame % 3 === 0) {
|
||||||
this.level.advance_tic(this.next_player_move);
|
this.advance_by(1);
|
||||||
this.next_player_move = this.pending_player_move;
|
|
||||||
this.player_used_move = true;
|
|
||||||
this.redraw();
|
|
||||||
}
|
}
|
||||||
this.frame %= 60;
|
this.frame %= 60;
|
||||||
|
|
||||||
this.update_ui();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(this.do_frame.bind(this));
|
requestAnimationFrame(this.do_frame.bind(this));
|
||||||
@ -702,8 +880,17 @@ class Game {
|
|||||||
this.inventory_el.append(mk('img', {src: this.render_inventory_tile(name)}));
|
this.inventory_el.append(mk('img', {src: this.render_inventory_tile(name)}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.demo) {
|
||||||
|
this.demo_scrubber_marker.style.setProperty('--time', this.tic);
|
||||||
|
this.demo_scrubber_marker.scrollIntoView({inline: 'center'});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let action of Object.keys(ACTION_LABELS)) {
|
||||||
|
this.input_action_elements[action].classList.toggle('--pressed', this.previous_input.has(action));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
104
style.css
104
style.css
@ -10,7 +10,7 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
background: #606060;
|
background: #404040;
|
||||||
}
|
}
|
||||||
main {
|
main {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -21,6 +21,8 @@ main {
|
|||||||
"level time" min-content
|
"level time" min-content
|
||||||
"level hint" 1fr
|
"level hint" 1fr
|
||||||
"level inventory" min-content
|
"level inventory" min-content
|
||||||
|
"controls controls"
|
||||||
|
"demo demo"
|
||||||
/ min-content 12em
|
/ min-content 12em
|
||||||
;
|
;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
@ -32,6 +34,10 @@ main {
|
|||||||
--scale: 2;
|
--scale: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.level {
|
.level {
|
||||||
grid-area: level;
|
grid-area: level;
|
||||||
|
|
||||||
@ -41,6 +47,25 @@ main {
|
|||||||
display: block;
|
display: block;
|
||||||
width: calc(9 * var(--tile-width) * var(--scale));
|
width: calc(9 * var(--tile-width) * var(--scale));
|
||||||
}
|
}
|
||||||
|
.bummer {
|
||||||
|
grid-area: level;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
z-index: 99;
|
||||||
|
font-size: 48px;
|
||||||
|
padding: 25%;
|
||||||
|
background: #0009;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 2px 1px black;
|
||||||
|
}
|
||||||
|
.bummer:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.meta {
|
.meta {
|
||||||
grid-area: meta;
|
grid-area: meta;
|
||||||
|
|
||||||
@ -85,22 +110,69 @@ main {
|
|||||||
.inventory img {
|
.inventory img {
|
||||||
width: calc(2 * var(--tile-width));
|
width: calc(2 * var(--tile-width));
|
||||||
}
|
}
|
||||||
.bummer {
|
.controls {
|
||||||
grid-area: level;
|
grid-area: controls;
|
||||||
|
}
|
||||||
|
.demo {
|
||||||
|
grid-area: demo;
|
||||||
|
}
|
||||||
|
|
||||||
display: flex;
|
.demo-scrubber {
|
||||||
justify-content: center;
|
position: relative;
|
||||||
align-items: center;
|
overflow-x: auto;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1em 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.demo-scrubber li {
|
||||||
|
position: relative;
|
||||||
|
height: 1em;
|
||||||
|
margin: 2px 0;
|
||||||
|
background: #303030;
|
||||||
|
}
|
||||||
|
.demo-scrubber li .demo-scrubber-bar {
|
||||||
|
position: absolute;
|
||||||
|
height: 1em;
|
||||||
|
left: calc(var(--start-time) * 3px);
|
||||||
|
width: calc((var(--end-time) - var(--start-time)) * 3px);
|
||||||
|
background: #606060;
|
||||||
|
}
|
||||||
|
.demo-scrubber .demo-scrubber-marker {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: calc(var(--time) * 3px);
|
||||||
|
width: 2px;
|
||||||
|
margin-left: -1px;
|
||||||
|
background: darkred;
|
||||||
|
--time: 0;
|
||||||
|
}
|
||||||
|
|
||||||
z-index: 99;
|
/* Debug stuff */
|
||||||
font-size: 48px;
|
.input {
|
||||||
padding: 25%;
|
display: grid;
|
||||||
background: #0009;
|
grid:
|
||||||
|
"drop up cycle" 1.5em
|
||||||
|
"left swap right" 1.5em
|
||||||
|
". down . " 1.5em
|
||||||
|
/ 1.5em 1.5em 1.5em
|
||||||
|
;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
|
.input-action {
|
||||||
|
padding: 0.25em;
|
||||||
|
line-height: 1;
|
||||||
|
color: #fff4;
|
||||||
|
background: #202020;
|
||||||
|
}
|
||||||
|
.input-action[data-action=up] { grid-area: up; }
|
||||||
|
.input-action[data-action=down] { grid-area: down; }
|
||||||
|
.input-action[data-action=left] { grid-area: left; }
|
||||||
|
.input-action[data-action=right] { grid-area: right; }
|
||||||
|
.input-action[data-action=swap] { grid-area: swap; }
|
||||||
|
.input-action[data-action=cycle] { grid-area: cycle; }
|
||||||
|
.input-action[data-action=drop] { grid-area: drop; }
|
||||||
|
.input-action.--pressed {
|
||||||
color: white;
|
color: white;
|
||||||
text-align: center;
|
background: hsl(215, 75%, 25%);
|
||||||
font-weight: bold;
|
|
||||||
text-shadow: 0 2px 1px black;
|
|
||||||
}
|
|
||||||
.bummer:empty {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user