diff --git a/index.html b/index.html index ca1a0f9..04e3b97 100644 --- a/index.html +++ b/index.html @@ -52,7 +52,11 @@
-
+
+

+
+

+

Chips

diff --git a/js/game.js b/js/game.js index 853fd08..7974ab3 100644 --- a/js/game.js +++ b/js/game.js @@ -622,7 +622,7 @@ export class Level { this.time_remaining -= 1; if (this.time_remaining <= 0) { - this.fail("Time's up!"); + this.fail('time'); } } else { @@ -767,31 +767,28 @@ export class Level { } // Check for a couple effects that always apply immediately - // TODO i guess this covers blocks too // TODO do blocks smash monsters? for (let tile of goal_cell) { + if (actor.type.is_player && tile.type.is_monster) { + this.fail(tile.type.name); + } + else if (actor.type.is_monster && tile.type.is_player) { + this.fail(actor.type.name); + } + else if (actor.type.is_block && tile.type.is_player) { + this.fail('squished'); + } + if (tile.type.slide_mode && ! actor.ignores(tile.type.name)) { this.make_slide(actor, tile.type.slide_mode); } - if ((actor.type.is_player && tile.type.is_monster) || - (actor.type.is_monster && tile.type.is_player)) - { - // TODO ooh, obituaries - this.fail("Oops! Watch out for creatures!"); - return; - } - if (actor.type.is_block && tile.type.is_player) { - // TODO ooh, obituaries - this.fail("squish"); - return; - } } // If we're stepping directly on the player, that kills them too // TODO this only works because i have the player move first; in lynx the check is the other // way around if (actor.type.is_monster && goal_cell === this.player_leaving_cell) { - this.fail("Oops! Watch out for creatures!"); + this.fail(actor.type.name); } if (this.compat.tiles_react_instantly) { @@ -931,7 +928,7 @@ export class Level { this.time_remaining = 1; } else { - this.fail("Time's up!"); + this.fail('time'); } } } @@ -939,10 +936,10 @@ export class Level { fail(message) { this.pending_undo.push(() => { this.state = 'playing'; - this.fail_message = null; + this.fail_reason = null; }); this.state = 'failure'; - this.fail_message = message; + this.fail_reason = message; throw new GameEnded; } diff --git a/js/main.js b/js/main.js index 58c82d9..309d735 100644 --- a/js/main.js +++ b/js/main.js @@ -8,7 +8,7 @@ import { Level } from './game.js'; import CanvasRenderer from './renderer-canvas.js'; import { Tileset, CC2_TILESET_LAYOUT, TILE_WORLD_TILESET_LAYOUT } from './tileset.js'; import TILE_TYPES from './tiletypes.js'; -import { mk, promise_event, fetch, walk_grid } from './util.js'; +import { random_choice, mk, promise_event, fetch, walk_grid } from './util.js'; const PAGE_TITLE = "Lexy's Labyrinth"; // Stackable modal overlay of some kind, usually a dialog @@ -136,6 +136,65 @@ const ACTION_DIRECTIONS = { left: 'west', right: 'east', }; +const OBITUARIES = { + drowned: [ + "player tried out water cooling", + "player fell into the c", + ], + burned: [ + "player's core temp got too high", + "player's plans went up in smoke", + ], + exploded: [ + "player didn't watch where they step", + "player is having a blast", + "player tripped over something of mine", + ], + squished: [ + "player was overwhelmed by a block of ram", + "player became two-dimensional", + ], + time: [ + "player tried to overclock", + "player's time ran out", + "player's speedrun went badly", + ], + generic: [ + "player had a bad time", + ], + + // Specific creatures + ball: [ + "player is having a ball", + ], + walker: [ + "player let it walk all over them", + ], + fireball: [ + "player had a meltdown", + ], + glider: [ + "player's ship came in", + ], + tank_blue: [ + "player didn't watch where they tread", + ], + tank_yellow: [ + "player let things get out of control", + ], + bug: [ + "player has ants in their pants", + ], + paramecium: [ + "player has the creepy crawlies", + ], + teeth: [ + "player got a mega bite", + ], + blob: [ + "player didn't do a gooed job", + ], +}; class Player extends PrimaryView { constructor(conductor) { super(conductor, document.body.querySelector('main#player')); @@ -163,12 +222,12 @@ class Player extends PrimaryView { this.root.style.setProperty('--tile-width', `${this.conductor.tileset.size_x}px`); this.root.style.setProperty('--tile-height', `${this.conductor.tileset.size_y}px`); this.level_el = this.root.querySelector('.level'); + this.overlay_message_el = this.root.querySelector('.overlay-message'); this.message_el = this.root.querySelector('.message'); this.chips_el = this.root.querySelector('.chips output'); this.time_el = this.root.querySelector('.time output'); this.bonus_el = this.root.querySelector('.bonus output'); this.inventory_el = this.root.querySelector('.inventory'); - this.bummer_el = this.root.querySelector('.bummer'); this.input_el = this.root.querySelector('.input'); this.demo_el = this.root.querySelector('.demo'); @@ -589,42 +648,90 @@ class Player extends PrimaryView { this.state = new_state; + // Populate the overlay + let overlay_reason = ''; + let overlay_top = ''; + let overlay_middle = null; + let overlay_bottom = ''; if (this.state === 'waiting') { - this.bummer_el.textContent = "Ready!"; - } - else if (this.state === 'playing' || this.state === 'rewinding') { - this.bummer_el.textContent = ""; + overlay_reason = 'waiting'; + overlay_middle = "Ready!"; } else if (this.state === 'paused') { - this.bummer_el.textContent = "/// paused ///"; + overlay_reason = 'paused'; + overlay_bottom = "/// paused ///"; } else if (this.state === 'stopped') { if (this.level.state === 'failure') { - this.bummer_el.textContent = this.level.fail_message; + overlay_reason = 'failure'; + overlay_top = "whoops"; + let obits = OBITUARIES[this.level.fail_reason] ?? OBITUARIES['generic']; + overlay_bottom = random_choice(obits); } else { - this.bummer_el.textContent = ""; + overlay_reason = 'success'; let base = (this.conductor.level_index + 1) * 500; let time = Math.ceil((this.level.time_remaining ?? 0) / 20) * 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)"), - ), + // Pick a success message + // TODO done on first try; took many tries + let time_left_fraction = null; + if (this.level.time_remaining !== null && this.level.stored_level.time_limit !== null) { + time_left_fraction = this.level.time_remaining / 20 / this.level.stored_level.time_limit; + } + + if (this.level.chips_remaining > 0) { + overlay_top = random_choice([ + "socket to em!", "go bug blaster!", + ]); + } + else if (this.level.time_remaining && this.level.time_remaining < 200) { + overlay_top = random_choice([ + "in the nick of time!", "cutting it close!", + ]); + } + else if (time_left_fraction !== null && time_left_fraction > 1) { + overlay_top = random_choice([ + "faster than light!", "impossible speed!", "pipelined!", + ]); + } + else if (time_left_fraction !== null && time_left_fraction > 0.75) { + overlay_top = random_choice([ + "lightning quick!", "nice speedrun!", "eagerly evaluated!", + ]); + } + else { + overlay_top = random_choice([ + "you did it!", "nice going!", "great job!", "good work!", + "onwards!", "tubular!", "yeehaw!", "hot damn!", + "alphanumeric!", "nice dynamic typing!", + ]); + } + overlay_bottom = "press spacebar to continue"; + + overlay_middle = 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)"), ); } } + this.overlay_message_el.setAttribute('data-reason', overlay_reason); + this.overlay_message_el.querySelector('.-top').textContent = overlay_top; + this.overlay_message_el.querySelector('.-bottom').textContent = overlay_bottom; + let middle = this.overlay_message_el.querySelector('.-middle'); + middle.textContent = ''; + if (overlay_middle) { + middle.append(overlay_middle); + } // The advance and redraw methods run in a loop, but they cancel // themselves if the game isn't running, so restart them here diff --git a/js/tiletypes.js b/js/tiletypes.js index 4fc04cf..bc73fbb 100644 --- a/js/tiletypes.js +++ b/js/tiletypes.js @@ -291,7 +291,7 @@ const TILE_TYPES = { } else if (other.type.is_player) { level.transmute_tile(other, 'player_burned'); - level.fail("Oops! You can't walk on fire without fire boots!"); + level.fail('burned'); } else { level.remove_tile(other); @@ -312,7 +312,7 @@ const TILE_TYPES = { } else if (other.type.is_player) { level.transmute_tile(other, 'splash'); - level.fail("swimming with the fishes"); + level.fail('drowned'); } else { level.transmute_tile(other, 'splash'); @@ -436,7 +436,7 @@ const TILE_TYPES = { let was_player = other.type.is_player; level.transmute_tile(other, 'explosion'); if (was_player) { - level.fail("watch where you step"); + level.fail('exploded'); } }, }, @@ -555,7 +555,7 @@ const TILE_TYPES = { let was_player = other.type.is_player; level.transmute_tile(other, 'explosion'); if (was_player) { - level.fail("watch where you step"); + level.fail('exploded'); } }, }, diff --git a/js/util.js b/js/util.js index e41027e..9ca5486 100644 --- a/js/util.js +++ b/js/util.js @@ -1,3 +1,7 @@ +export function random_choice(list) { + return list[Math.floor(Math.random() * list.length)]; +} + export function mk(tag_selector, ...children) { let [tag, ...classes] = tag_selector.split('.'); let el = document.createElement(tag); diff --git a/style.css b/style.css index 41ba6f6..5bea114 100644 --- a/style.css +++ b/style.css @@ -323,34 +323,46 @@ body[data-mode=player] #editor-play { --viewport-width: 9; --viewport-height: 9; } -.bummer { +#player .overlay-message { grid-area: level; place-self: stretch; - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: 1fr 3fr 1fr; justify-content: center; align-items: center; z-index: 1; - font-size: 48px; - padding: 10%; + font-size: calc(0.5 * var(--tile-width) * var(--scale)); + padding: 6.25%; background: #0009; color: white; text-align: center; - font-weight: bold; text-shadow: 0 2px 1px black; } -.bummer:empty { +#player .overlay-message p { + margin: 0; +} +#player .overlay-message .-top { + font-size: 1.5em; +} +#player .overlay-message .-middle { +} +#player .overlay-message .-bottom { +} +#player .overlay-message[data-reason=""] { display: none; } -.bummer p { - margin: 0; +#player .overlay-message[data-reason=failure] { + box-shadow: inset 0 0 calc(4 * var(--tile-width)) var(--tile-width) black; +} +#player .overlay-message[data-reason=success] { + box-shadow: inset 0 0 var(--tile-width) white; } dl.score-chart { display: grid; grid-auto-columns: 1fr 1fr; - font-size: 1.25rem; + margin: auto; font-weight: normal; } dl.score-chart dt {