diff options
Diffstat (limited to '2026/games_submissions/the0x539/main.js')
| -rw-r--r-- | 2026/games_submissions/the0x539/main.js | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/2026/games_submissions/the0x539/main.js b/2026/games_submissions/the0x539/main.js new file mode 100644 index 0000000..b89465e --- /dev/null +++ b/2026/games_submissions/the0x539/main.js @@ -0,0 +1,196 @@ +import { recipes, amounts } from './data.js'; +import { + createItem, + mouseDown, + mouseEnter, + mouseUp, + craftClick, + consumeClock, + resetInventory, +} from './interaction.js'; + +const followCursor = document.querySelector('follow-cursor'); +const tooltip = document.querySelector('item-tooltip'); +const craftingGrid = document.querySelector('crafting-grid'); +const inventoryGrid = document.querySelector('inventory-grid'); +const craftingOutput = document.querySelector('crafting-output'); +const todoList = document.getElementById('todo-list'); + +const desiredCrafts = new Set(); + +const playSound = { + playImpl(id, volume = 1.0, pitch = 1.0) { + const audio = document.getElementById(id); + + audio.currentTime = 0; + + const p = new Promise(resolve => { + const f = (event) => { + audio.removeEventListener('ended', f); + resolve(event); + }; + audio.addEventListener('ended', f); + }); + + audio.volume = volume; + audio.playbackRate = pitch; + audio.preservesPitch = false; + + audio.play(); + return p; + }, + + smallDing() { + return this.playImpl('small-ding', 0.1, 0.55 + 0.7 * Math.random()); + }, + + bigDing() { + return this.playImpl('big-ding', 0.75); + }, + + oof() { + return this.playImpl('oof', 0.75); + }, +}; + +function onCraft(event) { + const item = event.item; + if (desiredCrafts.has(item)) { + desiredCrafts.delete(item); + + document.querySelector(`#todo-list > li:has([data-item="${item}"])`)?.remove(); + + if (desiredCrafts.size > 0) { + playSound.smallDing(); + } else { + endGame(true); + } + } +} + +function handleMessage(msg) { + if (msg.op === 'start') { + startGame(msg.difficulty); + } +} + +function giveItem(item, count) { + const emptySlots = document.querySelectorAll('inventory-grid > inventory-cell:empty'); + const index = Math.floor(Math.random() * emptySlots.length); + emptySlots[index].appendChild(createItem(item, count)); +} + +let timerInterval = null; + +function timer() { + if (!document.querySelector('item-stack[data-item="clock"]')) { + // time's up! + endGame(false); + } else { + consumeClock(); + } +} + +function startGame(difficulty) { + document.body.classList.remove('failed'); + + resetInventory(); + + desiredCrafts.clear(); + desiredCrafts.add('helmet'); + desiredCrafts.add('chestplate'); + desiredCrafts.add('leggings'); + desiredCrafts.add('boots'); + + todoList.replaceChildren(); + for (const item of desiredCrafts) { + const listItem = document.createElement('li'); + + const stack = document.createElement('item-stack'); + stack.setAttribute('data-item', item); + listItem.append(stack); + + todoList.append(listItem); + } + + giveItem('log', 64); + giveItem('ingot', 64); + giveItem('clock', 12); + giveItem('dust', 16); + giveItem('cobble', 16); + + document.addEventListener('mousemove', onMouseMove); + timerInterval = setInterval(timer, 1000); + window.parent.postMessage({ op: 'started', verb: 'craft!' }); +} + +async function endGame(win) { + clearInterval(timerInterval); + timerInterval = null; + if (win) { + await playSound.bigDing(); + } else { + document.body.classList.add('failed'); + await playSound.oof(); + } + await new Promise(resolve => setTimeout(resolve, 500)); + document.removeEventListener('mousemove', onMouseMove); + window.parent.postMessage({ op: 'done', win }); +} + +function createCell() { + const elem = document.createElement('inventory-cell'); + elem.addEventListener('mousedown', mouseDown); + elem.addEventListener('mouseenter', mouseEnter); + return elem; +} + +function onMouseMove(event) { + followCursor.setAttribute('style', `left: ${event.clientX}px; top: ${event.clientY}px`); + + const hoverItem = document.querySelector('item-stack:hover'); + if (hoverItem !== null) { + const name = hoverItem.getAttribute('data-item'); + tooltip.textContent = name; + } else { + tooltip.textContent = ''; + } +} + +document.addEventListener('contextmenu', e => e.preventDefault()); + +const allItems = new Set(); +for (const [output, shape] of Object.entries(recipes)) { + allItems.add(output); + for (const item of shape.flat()) { + if (item) { + allItems.add(item); + } + } +} + +const stylesheet = document.styleSheets[0]; + +for (const item of allItems) { + stylesheet.insertRule(` + item-stack[data-item="${item}"]::before { + background-image: url("./items/${item}.png"); + } + `); +} + +for (let i = 0; i < 3 * 3; i++) { + craftingGrid.appendChild(createCell()); +} + +for (let i = 0; i < 12 * 3; i++) { + inventoryGrid.appendChild(createCell()); +} + +document.addEventListener('mouseup', mouseUp); +document.addEventListener('craft', onCraft); + +craftingOutput.addEventListener('click', craftClick); + +window.addEventListener('message', m => handleMessage(m.data)); +window.parent.postMessage({ op: 'ready' }); |
