From 0d2d8cd897e94bd94e0b3ae7c6ce9aa580a44a0a Mon Sep 17 00:00:00 2001 From: LLLL Colonq Date: Wed, 26 Aug 2026 23:47:59 -0400 Subject: Add a_tension_span --- 2026/games_submissions/the0x539/.gitignore | 1 + .../the0x539/MinecraftStandard.otf | Bin 0 -> 159732 bytes 2026/games_submissions/the0x539/crafting.js | 98 ++++ 2026/games_submissions/the0x539/data.js | 101 ++++ 2026/games_submissions/the0x539/difficulty.js | 100 ++++ 2026/games_submissions/the0x539/index.html | 26 + 2026/games_submissions/the0x539/interaction.js | 581 +++++++++++++++++++++ 2026/games_submissions/the0x539/items/boots.png | Bin 0 -> 142 bytes 2026/games_submissions/the0x539/items/bow.png | Bin 0 -> 160 bytes 2026/games_submissions/the0x539/items/chest.png | Bin 0 -> 775 bytes .../the0x539/items/chestplate.png | Bin 0 -> 389 bytes 2026/games_submissions/the0x539/items/clock.gif | Bin 0 -> 2153 bytes 2026/games_submissions/the0x539/items/cobble.png | Bin 0 -> 518 bytes 2026/games_submissions/the0x539/items/dust.png | Bin 0 -> 179 bytes 2026/games_submissions/the0x539/items/gold.png | Bin 0 -> 399 bytes 2026/games_submissions/the0x539/items/helmet.png | Bin 0 -> 134 bytes 2026/games_submissions/the0x539/items/hopper.png | Bin 0 -> 404 bytes 2026/games_submissions/the0x539/items/ingot.png | Bin 0 -> 156 bytes 2026/games_submissions/the0x539/items/leggings.png | Bin 0 -> 146 bytes 2026/games_submissions/the0x539/items/log.png | Bin 0 -> 3913 bytes 2026/games_submissions/the0x539/items/pickaxe.png | Bin 0 -> 174 bytes 2026/games_submissions/the0x539/items/piston.png | Bin 0 -> 635 bytes 2026/games_submissions/the0x539/items/plank.png | Bin 0 -> 4974 bytes 2026/games_submissions/the0x539/items/rod.png | Bin 0 -> 183 bytes 2026/games_submissions/the0x539/items/stick.png | Bin 0 -> 388 bytes 2026/games_submissions/the0x539/items/string.png | Bin 0 -> 162 bytes .../the0x539/items/tooltip-backdrop.png | Bin 0 -> 768 bytes 2026/games_submissions/the0x539/main.js | 197 +++++++ 2026/games_submissions/the0x539/screen.css | 168 ++++++ 2026/games_submissions/the0x539/sfx/big-ding.ogg | Bin 0 -> 15799 bytes 2026/games_submissions/the0x539/sfx/oof.ogg | Bin 0 -> 12546 bytes 2026/games_submissions/the0x539/sfx/small-ding.ogg | Bin 0 -> 7139 bytes .../the0x539/tooltip-backdrop.png | Bin 0 -> 710 bytes 2026/games_submissions/the0x539/ui.png | Bin 0 -> 764 bytes 34 files changed, 1272 insertions(+) create mode 100644 2026/games_submissions/the0x539/.gitignore create mode 100755 2026/games_submissions/the0x539/MinecraftStandard.otf create mode 100644 2026/games_submissions/the0x539/crafting.js create mode 100644 2026/games_submissions/the0x539/data.js create mode 100644 2026/games_submissions/the0x539/difficulty.js create mode 100644 2026/games_submissions/the0x539/index.html create mode 100644 2026/games_submissions/the0x539/interaction.js create mode 100755 2026/games_submissions/the0x539/items/boots.png create mode 100755 2026/games_submissions/the0x539/items/bow.png create mode 100755 2026/games_submissions/the0x539/items/chest.png create mode 100644 2026/games_submissions/the0x539/items/chestplate.png create mode 100755 2026/games_submissions/the0x539/items/clock.gif create mode 100755 2026/games_submissions/the0x539/items/cobble.png create mode 100755 2026/games_submissions/the0x539/items/dust.png create mode 100644 2026/games_submissions/the0x539/items/gold.png create mode 100755 2026/games_submissions/the0x539/items/helmet.png create mode 100644 2026/games_submissions/the0x539/items/hopper.png create mode 100755 2026/games_submissions/the0x539/items/ingot.png create mode 100755 2026/games_submissions/the0x539/items/leggings.png create mode 100755 2026/games_submissions/the0x539/items/log.png create mode 100755 2026/games_submissions/the0x539/items/pickaxe.png create mode 100755 2026/games_submissions/the0x539/items/piston.png create mode 100644 2026/games_submissions/the0x539/items/plank.png create mode 100755 2026/games_submissions/the0x539/items/rod.png create mode 100755 2026/games_submissions/the0x539/items/stick.png create mode 100755 2026/games_submissions/the0x539/items/string.png create mode 100755 2026/games_submissions/the0x539/items/tooltip-backdrop.png create mode 100644 2026/games_submissions/the0x539/main.js create mode 100644 2026/games_submissions/the0x539/screen.css create mode 100755 2026/games_submissions/the0x539/sfx/big-ding.ogg create mode 100755 2026/games_submissions/the0x539/sfx/oof.ogg create mode 100755 2026/games_submissions/the0x539/sfx/small-ding.ogg create mode 100755 2026/games_submissions/the0x539/tooltip-backdrop.png create mode 100644 2026/games_submissions/the0x539/ui.png (limited to '2026/games_submissions/the0x539') diff --git a/2026/games_submissions/the0x539/.gitignore b/2026/games_submissions/the0x539/.gitignore new file mode 100644 index 0000000..845270b --- /dev/null +++ b/2026/games_submissions/the0x539/.gitignore @@ -0,0 +1 @@ +harness diff --git a/2026/games_submissions/the0x539/MinecraftStandard.otf b/2026/games_submissions/the0x539/MinecraftStandard.otf new file mode 100755 index 0000000..8fa0b3e Binary files /dev/null and b/2026/games_submissions/the0x539/MinecraftStandard.otf differ diff --git a/2026/games_submissions/the0x539/crafting.js b/2026/games_submissions/the0x539/crafting.js new file mode 100644 index 0000000..7a61cfc --- /dev/null +++ b/2026/games_submissions/the0x539/crafting.js @@ -0,0 +1,98 @@ +import { recipes, amounts } from './data.js'; + +export function getRecipeOutput() { + const input = readGrid(); + if (input.length === 0) return null; + + const mirroredInput = input.map(row => row.toReversed()); + + for (const [item, recipe] of Object.entries(recipes)) { + const isMatch = compare(input, recipe) || compare(mirroredInput, recipe); + if (!isMatch) continue; + + const amount = amounts[item] ?? 1; + return [item, amount]; + } + + return null; +} + +function last(arr) { + return arr[arr.length - 1]; +} + +function isSymmetric(grid) { + return grid.every(row => row[0] === last(row)); +} + +function readGrid() { + const cells = document.querySelectorAll('crafting-grid > inventory-cell'); + + const rows = []; + for (let y = 0; y < 3; y++) { + const row = []; + for (let x = 0; x < 3; x++) { + let item = null; + const stack = cells[3 * y + x].firstElementChild; + if (!!stack) { + item = stack.getAttribute('data-item'); + } + row.push(item); + } + rows.push(row); + } + + trimGrid(rows); + + return rows; +} + +function trimGrid(grid) { + if (grid.flat().every(v => v === null)) { + while (grid.length > 0) { + grid.pop(); + } + return; + } + + // it is now established that the grid is non-empty, + // so there will always be at least one row+col + + // trim blank rows from bottom + while (last(grid).every(v => v === null)) { + grid.pop(); + } + + // trim blank rows from top + while (grid[0].every(v => v === null)) { + grid.shift(); + } + + // trim blank columns from right + while (grid.every(row => last(row) === null)) { + for (const row of grid) { + row.pop(); + } + } + + // trim blank columns from left + while (grid.every(row => row[0] === null)) { + for (const row of grid) { + row.shift(); + } + } +} + +function compare(a, b) { + if (a.length !== b.length) return false; + + for (let y = 0; y < a.length; y++) { + if (a[y].length !== b[y].length) return false; + + for (let x = 0; x < a[y].length; x++) { + if (a[y][x] !== b[y][x]) return false; + } + } + + return true; +} diff --git a/2026/games_submissions/the0x539/data.js b/2026/games_submissions/the0x539/data.js new file mode 100644 index 0000000..977c557 --- /dev/null +++ b/2026/games_submissions/the0x539/data.js @@ -0,0 +1,101 @@ +const ingredients = { + i: 'ingot', + l: 'log', + p: 'plank', + s: 'stick', + d: 'dust', + c: 'cobble', + C: 'chest', + S: 'string', + g: 'gold', +} + +function recipe(...shape) { + return shape.map(line => Array.from(line).map(ch => { + if (ch === ' ') { + return null; + } else { + const item = ingredients[ch]; + console.assert(!!item); + return item; + } + })); +} + +export const recipes = { + helmet: recipe( + 'iii', + 'i i', + ), + chestplate: recipe( + 'i i', + 'iii', + 'iii', + ), + leggings: recipe( + 'iii', + 'i i', + 'i i', + ), + boots: recipe( + 'i i', + 'i i', + ), + plank: recipe( + 'l', + ), + stick: recipe( + 'p', + 'p', + ), + pickaxe: recipe( + 'ppp', + ' s ', + ' s ', + ), + piston: recipe( + 'ppp', + 'cic', + 'cdc', + ), + chest: recipe( + 'ppp', + 'p p', + 'ppp', + ), + hopper: recipe( + 'i i', + 'iCi', + ' i ', + ), + rod: recipe( + ' s', + ' sS', + 's S', + ), + bow: recipe( + ' sS', + 's S', + ' sS', + ), + clock: recipe( + ' g ', + 'gdg', + ' g ', + ), +}; + +export const amounts = { + plank: 4, + stick: 4, +} + +export const stackSizes = { + helmet: 1, + chestplate: 1, + leggings: 1, + boots: 1, + pickaxe: 1, + rod: 1, + bow: 1, +} diff --git a/2026/games_submissions/the0x539/difficulty.js b/2026/games_submissions/the0x539/difficulty.js new file mode 100644 index 0000000..87a86b9 --- /dev/null +++ b/2026/games_submissions/the0x539/difficulty.js @@ -0,0 +1,100 @@ +export const scenarios = [ + { + level: 0, + items: { + log: 2, + }, + goal: ['chest'], + time: _ => 10, + }, + { + level: 5, + items: { + plank: 7, + cobble: 6, + dust: 8, + ingot: 4, + }, + goal: ['piston'], + time: d => 15 - d, + }, + { + level: 10, + items: { + log: 2, + cobble: 4, + dust: 2, + ingot: 1, + }, + goal: ['piston'], + time: d => 15 - d, + }, + { + level: 13, + items: { + log: 8, + }, + goal: ['pickaxe'], + time: d => 15 - d, + }, + { + level: 18, + items: { + ingot: 8, + plank: 8, + }, + goal: ['hopper'], + time: d => 20 - d, + }, + { + level: 21, + items: { + ingot: 12, + log: 3, + }, + goal: ['hopper'], + time: d => 20 - d, + }, + { + level: 25, + items: { + ingot: 24, + }, + goal: ['helmet', 'chestplate', 'leggings', 'boots'], + time: d => 18 - d, + }, + { + level: 30, + items: { + ingot: 28, + log: 1, + string: 3, + }, + goal: ['helmet', 'chestplate', 'leggings', 'boots', 'bow'], + time: d => 30 - d, + }, + { + level: 35, + items: { + ingot: 24, + log: 1, + string: 3, + gold: 64, + dust: 17, + }, + goal: ['helmet', 'chestplate', 'leggings', 'boots', 'bow'], + time: _ => 15, + }, + { + level: 40, + items: { + ingot: 24, + log: 1, + string: 3, + gold: 63, + dust: 15, + }, + goal: ['helmet', 'chestplate', 'leggings', 'boots', 'bow'], + time: _ => 15, + }, +]; diff --git a/2026/games_submissions/the0x539/index.html b/2026/games_submissions/the0x539/index.html new file mode 100644 index 0000000..5a073ed --- /dev/null +++ b/2026/games_submissions/the0x539/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/2026/games_submissions/the0x539/interaction.js b/2026/games_submissions/the0x539/interaction.js new file mode 100644 index 0000000..d45d5c9 --- /dev/null +++ b/2026/games_submissions/the0x539/interaction.js @@ -0,0 +1,581 @@ +import { stackSizes } from './data.js'; +import { getRecipeOutput } from './crafting.js'; + +class CraftEvent extends Event { + item; + constructor(item) { + super('craft'); + this.item = item; + } +} + +const grabbedStack = document.querySelector('grabbed-stack'); +const craftingGrid = document.querySelector('crafting-grid'); +const inventoryGrid = document.querySelector('inventory-grid'); +const outputCell = document.querySelector('crafting-output'); + +let state = 'idle'; + +const splitTargets = new Set(); + +export function resetInventory() { + for (const stack of document.querySelectorAll('item-stack')) { + stack.remove(); + } + splitTargets.clear(); + state = 'idle'; +} + +export function createItem(item, count) { + const elem = document.createElement('item-stack'); + elem.setAttribute('data-item', item); + elem.setAttribute('data-count', count); + const countElem = document.createElement('data'); + countElem.textContent = count.toString(); + elem.appendChild(countElem); + return elem; +} + +export function mouseDown(event) { + if (state !== 'idle') return; + if (event.button !== 0 && event.button !== 2) return; + + const cell = event.currentTarget; + + // Shift click: Transfer a full stack to the other place. + if (event.shiftKey) { + const sourceStack = cell.firstElementChild; + if (sourceStack) { + const targetGrid = cell.parentElement === inventoryGrid ? craftingGrid : inventoryGrid; + shiftClickTransfer(sourceStack, targetGrid); + } + state = 'shift-drag'; + return; + } + + const heldStack = grabbedStack.firstElementChild; + + // Click with empty cursor: pick up items + if (!heldStack) { + const sourceStack = cell.firstElementChild; + if (!sourceStack) { + return; + } + + if (event.button === 2) { + // Right click: take half the stack, rounded up + const sourceCount = getCount(sourceStack); + if (sourceCount === 1) { + grabbedStack.appendChild(sourceStack); + updateRecipeOutput(); + return; + } + + const takenCount = Math.ceil(sourceCount / 2); + const newCount = sourceCount - takenCount; + const item = sourceStack.getAttribute('data-item'); + setCount(sourceStack, newCount); + grabbedStack.appendChild(createItem(item, takenCount)); + } else { + // Left click: take the whole stack. Drag to pick up other stacks + grabbedStack.appendChild(sourceStack); + state = 'pickup-drag'; + updateRecipeOutput(); + } + } else { + // Click with items held: deposit items + + if (event.button === 2) { + // Right click: deposit one item. Drag to deposit one item in each cell + state = 'split-one'; + depositOne(cell); + } else { + // Left click: split stack evenly across all empty/compatible cells. + // If no compatible cells are dragged over, swap stack with the cell the mouse was released on. + // (In the common case, this is just what a "click" looks like on a cell with incompatible contents.) + heldStack.setAttribute('data-original-count', heldStack.getAttribute('data-count')); + state = 'split-evenly'; + addSplitTarget(cell); + } + } +} + +export function mouseEnter(event) { + const cell = event.currentTarget; + + switch (state) { + case 'shift-drag': { + const sourceStack = cell.firstElementChild; + if (!sourceStack) break; + const targetGrid = cell.parentElement === inventoryGrid ? craftingGrid : inventoryGrid; + shiftClickTransfer(sourceStack, targetGrid); + updateRecipeOutput(); + break; + } + + case 'pickup-drag': { + const sourceStack = cell.firstElementChild; + if (!sourceStack) break; + const heldStack = grabbedStack.firstElementChild; + const item = sourceStack.getAttribute('data-item'); + if (item !== heldStack.getAttribute('data-item')) return; + + const stackSize = stackSizes[item] ?? 64; + + const newCount = getCount(heldStack) + getCount(sourceStack); + if (newCount > stackSize) return; + + setCount(heldStack, newCount); + sourceStack.remove(); + updateRecipeOutput(); + break; + } + + case 'split-one': + depositOne(cell); + break; + + case 'split-evenly': + addSplitTarget(cell); + break; + + default: + break; + } +} + +export function mouseUp(event) { + switch (state) { + case 'split-evenly': + case 'split-exhausted': { + if (event.button !== 0) return; + + if (splitTargets.size <= 1) { + const targetCell = event.target.closest('inventory-cell'); + if (targetCell) { + depositAll(targetCell); + } + } else { + commitSplit(); + } + break; + } + + case 'pickup-drag': + if (event.button !== 0) return; + state = 'idle'; + break; + + case 'split-one': + if (event.button !== 2) return; + state = 'idle'; + break; + + case 'shift-drag': + // this could be either button. whatever. + state = 'idle'; + break; + + default: + break; + } +} + +export function craftClick(event) { + if (event.shiftKey) { + craftAll(); + } else { + craftOne(); + } +} + +function craftOne() { + const previewStack = outputCell.firstElementChild; + if (!previewStack) return; + + const item = previewStack.getAttribute('data-item'); + const craftCount = getCount(previewStack); + const stackSize = stackSizes[item] ?? 64; + + const heldStack = grabbedStack.firstElementChild; + if (heldStack !== null) { + if (heldStack.getAttribute('data-item') !== item) { + return; + } + + const heldCount = getCount(heldStack); + if (heldCount + craftCount > stackSize) { + return; + } + + setCount(heldStack, heldCount + craftCount); + } else { + grabbedStack.appendChild(createItem(item, craftCount)); + } + consumeIngredients(); + document.dispatchEvent(new CraftEvent(item)); +} + +function craftAll() { + const previewStack = outputCell.firstElementChild; + if (!previewStack) return; + + const item = previewStack.getAttribute('data-item'); + const craftCount = getCount(previewStack); + const stackSize = stackSizes[item] ?? 64; + + let success = false; + + do { + let unallocated = craftCount; + + const nonEmptyTargets = []; + let emptyTarget = null; + + for (const cell of inventoryGrid.children) { + const stack = cell.firstElementChild; + if (stack === null) { + // This slot is empty. + // All crafting outputs that don't fit into existing stacks + // will be deposited into the first empty slot. + emptyTarget ??= cell; + continue; + } + + if (stack.getAttribute('data-item') !== item) { + // This slot already contains a different item, + // so we can't add crafting outputs to it. + continue; + } + + const spareCapacity = stackSize - getCount(stack); + if (spareCapacity <= 0) { + // This slot contains the correct item, but is already full. + continue; + } + + const amountToAdd = Math.min(unallocated, spareCapacity); + unallocated -= amountToAdd; + nonEmptyTargets.push([stack, amountToAdd]); + + if (unallocated === 0) { + // All crafting outputs can fit into existing stacks of the item. + break; + } + } + + if (unallocated > 0 && emptyTarget === null) { + // The inventory is full: no slots are empty and existing stacks don't have enough room. + // Abort this attempt and conclude the loop. + break; + } + + // Add crafting output to existing stacks, to whatever extent possible. + for (const [stack, amountToAdd] of nonEmptyTargets) { + setCount(stack, getCount(stack) + amountToAdd); + } + + // Put any remaining items into the first empty slot. + if (unallocated > 0) { + emptyTarget.appendChild(createItem(item, unallocated)); + } + + success = true; + + // Consume one set of ingredients. + // Cease crafting if this causes the recipe output to change. + } while (!consumeIngredients()); + + if (success) { + document.dispatchEvent(new CraftEvent(item)); + } +} + +function consumeIngredients() { + let anyExhausted = false; + + for (const cell of craftingGrid.children) { + const stack = cell.firstElementChild; + if (!stack) continue; + + const count = getCount(stack); + if (count === 1) { + stack.remove(); + anyExhausted = true; + } else { + setCount(stack, count - 1); + } + } + + return anyExhausted && updateRecipeOutput(); +} + +// Returns whether the output actually changed +function updateRecipeOutput() { + const outputStack = outputCell.firstElementChild; + + const output = getRecipeOutput(); + if (output === null) { + if (outputStack === null) { + return false; + } else { + outputStack.remove(); + return true; + } + } + + const [item, count] = output; + if (outputStack === null) { + outputCell.appendChild(createItem(item, count)); + return true; + } + + if (outputStack.getAttribute('data-item') === item && getCount(outputStack) === count) { + return false; + } + + outputStack.setAttribute('data-item', item); + setCount(outputStack, count); + return true; +} + +export function consumeClock() { + if (state === 'split-exhausted') { + commitSplit(); + } + + if (state === 'split-evenly' && splitTargets.size > 1) { + // oh dear. this is a complicated situation + // first, let's try to search for uninvolved clocks + const uninvolved = document.querySelector('inventory-cell > item-stack[data-item="clock"]:not([data-original-count]):not(crafting-output item-stack)'); + if (!!uninvolved) { + const count = getCount(uninvolved); + if (count === 1) { + uninvolved.remove(); + } else { + setCount(uninvolved, count - 1); + } + // phew + return; + } + + // okay so we will actually need to remove a clock from the items being split, ugh + // not a lot of code, but low confidence that it works properly + const heldStack = grabbedStack.firstElementChild; + const count = +heldStack.getAttribute('data-original-count'); + heldStack.setAttribute('data-original-count', count - 1); + updateSplitPreview(); + return; + } + + const stack = document.querySelector('item-stack[data-item="clock"][data-count]:not(crafting-output item-stack)') + const count = getCount(stack); + if (count === 1) { + if (stack.parentElement === grabbedStack) { + // conclude drag operations if they're done using a stack of clocks that gets deleted + state = 'idle'; + } + stack.remove(); + } else { + setCount(stack, count - 1); + } +} + +function getCount(itemStack) { + return +itemStack.getAttribute('data-count'); +} + +function setCount(itemStack, value) { + itemStack.setAttribute('data-count', value); + itemStack.firstElementChild.textContent = value.toString(); +} + +function depositOne(targetCell) { + const heldStack = grabbedStack.firstElementChild; + const item = heldStack.getAttribute('data-item'); + const heldCount = getCount(heldStack); + + const stackSize = stackSizes[item] ?? 64; + + const targetStack = targetCell.firstElementChild; + if (targetStack) { + if (targetStack.getAttribute('data-item') !== item) { + return; + } + + const targetCount = getCount(targetStack); + if (targetCount >= stackSize) return; + + setCount(targetStack, targetCount + 1); + } else { + targetCell.appendChild(createItem(item, 1)); + } + + if (heldCount === 1) { + heldStack.remove(); + state = 'idle'; + } else { + setCount(heldStack, heldCount - 1); + } + + updateRecipeOutput(); +} + +function shiftClickTransfer(sourceStack, targetGrid) { + const item = sourceStack.getAttribute('data-item'); + const stackSize = stackSizes[item] ?? 64; + let count = getCount(sourceStack); + + // First, search for existing stacks to add to + for (const cell of targetGrid.children) { + const targetStack = cell.firstElementChild; + if (!targetStack) continue; + if (targetStack.getAttribute('data-item') !== item) continue; + const curCount = getCount(targetStack); + const available = stackSize - curCount; + if (available === 0) continue; + + const transferSize = Math.min(count, available); + const newCount = curCount + transferSize; + setCount(targetStack, newCount); + + count -= transferSize; + if (count === 0) { + sourceStack.remove(); + updateRecipeOutput(); + return; + } else { + setCount(sourceStack, count); + } + } + + // Now, look for any empty slots to move whatever's left to + for (const cell of targetGrid.children) { + if (!cell.firstElementChild) { + cell.appendChild(sourceStack); + break; + } + } + + updateRecipeOutput(); +} + +function addSplitTarget(targetCell) { + const heldStack = grabbedStack.firstElementChild; + const item = heldStack.getAttribute('data-item'); + + const targetStack = targetCell.firstElementChild; + if (targetStack) { + if (targetStack.getAttribute('data-item') !== item) { + return; + } + } + + splitTargets.add(targetCell); + + if (splitTargets.size > 1) { + updateSplitPreview(); + } +} + +function updateSplitPreview() { + const heldStack = grabbedStack.firstElementChild; + const item = heldStack.getAttribute('data-item'); + const heldCount = +heldStack.getAttribute('data-original-count'); + + const stackSize = stackSizes[item] ?? 64; + + const splitCount = Math.max(1, Math.floor(heldCount / splitTargets.size)); + let remainder = heldCount; + + for (const targetCell of splitTargets) { + let targetStack = targetCell.firstElementChild; + if (!targetStack) { + targetStack = createItem(item, 0); + targetCell.appendChild(targetStack); + } + if (!targetStack.hasAttribute('data-original-count')) { + targetStack.setAttribute('data-original-count', targetStack.getAttribute('data-count')); + } + const targetCount = +targetStack.getAttribute('data-original-count'); + const available = stackSize - targetCount; + const transferSize = Math.min(splitCount, available); + setCount(targetStack, targetCount + transferSize); + remainder -= transferSize; + // TODO: give stacks yellow text if they're too full to accomodate their full share of the split + + if (remainder === 0) break; + } + + setCount(heldStack, remainder); + + if (splitCount === 1 && remainder === 0) { + state = 'split-exhausted'; + } +} + +function commitSplit() { + const heldStack = grabbedStack.firstElementChild; + const heldCount = getCount(heldStack); + + if (heldCount === 0) { + heldStack.remove(); + } else { + heldStack.removeAttribute('data-original-count'); + } + + for (const target of splitTargets) { + target.firstElementChild?.removeAttribute('data-original-count'); + } + + splitTargets.clear(); + + state = 'idle'; + + updateRecipeOutput(); +} + +function depositAll(targetCell) { + const heldStack = grabbedStack.firstElementChild; + const item = heldStack.getAttribute('data-item'); + const heldCount = getCount(heldStack); + + const stackSize = stackSizes[item] ?? 64; + + const targetStack = targetCell.firstElementChild; + if (!!targetStack && targetStack.getAttribute('data-item') !== item) { + // swap held and target stacks because they don't match + targetCell.appendChild(heldStack); + grabbedStack.appendChild(targetStack); + } else { + if (!targetStack) { + // deposit the full stack to the slot + targetCell.appendChild(heldStack); + } else { + // deposit as much as possible + const targetCount = getCount(targetStack); + const available = stackSize - targetCount; + const transferSize = Math.min(heldCount, available); + const newCount = targetCount + transferSize; + setCount(targetStack, newCount); + + if (transferSize < heldCount) { + setCount(heldStack, heldCount - transferSize); + } else { + heldStack.remove(); + } + } + } + + heldStack.removeAttribute('data-original-count'); + + for (const target of splitTargets) { + target.firstElementChild?.removeAttribute('data-original-count'); + } + + splitTargets.clear(); + + state = 'idle'; + + updateRecipeOutput(); +} diff --git a/2026/games_submissions/the0x539/items/boots.png b/2026/games_submissions/the0x539/items/boots.png new file mode 100755 index 0000000..ca6c33b Binary files /dev/null and b/2026/games_submissions/the0x539/items/boots.png differ diff --git a/2026/games_submissions/the0x539/items/bow.png b/2026/games_submissions/the0x539/items/bow.png new file mode 100755 index 0000000..6922e69 Binary files /dev/null and b/2026/games_submissions/the0x539/items/bow.png differ diff --git a/2026/games_submissions/the0x539/items/chest.png b/2026/games_submissions/the0x539/items/chest.png new file mode 100755 index 0000000..eb7f766 Binary files /dev/null and b/2026/games_submissions/the0x539/items/chest.png differ diff --git a/2026/games_submissions/the0x539/items/chestplate.png b/2026/games_submissions/the0x539/items/chestplate.png new file mode 100644 index 0000000..0cc9a63 Binary files /dev/null and b/2026/games_submissions/the0x539/items/chestplate.png differ diff --git a/2026/games_submissions/the0x539/items/clock.gif b/2026/games_submissions/the0x539/items/clock.gif new file mode 100755 index 0000000..d4a7698 Binary files /dev/null and b/2026/games_submissions/the0x539/items/clock.gif differ diff --git a/2026/games_submissions/the0x539/items/cobble.png b/2026/games_submissions/the0x539/items/cobble.png new file mode 100755 index 0000000..55dbc4f Binary files /dev/null and b/2026/games_submissions/the0x539/items/cobble.png differ diff --git a/2026/games_submissions/the0x539/items/dust.png b/2026/games_submissions/the0x539/items/dust.png new file mode 100755 index 0000000..9d17c14 Binary files /dev/null and b/2026/games_submissions/the0x539/items/dust.png differ diff --git a/2026/games_submissions/the0x539/items/gold.png b/2026/games_submissions/the0x539/items/gold.png new file mode 100644 index 0000000..99b1a46 Binary files /dev/null and b/2026/games_submissions/the0x539/items/gold.png differ diff --git a/2026/games_submissions/the0x539/items/helmet.png b/2026/games_submissions/the0x539/items/helmet.png new file mode 100755 index 0000000..850dac6 Binary files /dev/null and b/2026/games_submissions/the0x539/items/helmet.png differ diff --git a/2026/games_submissions/the0x539/items/hopper.png b/2026/games_submissions/the0x539/items/hopper.png new file mode 100644 index 0000000..9655818 Binary files /dev/null and b/2026/games_submissions/the0x539/items/hopper.png differ diff --git a/2026/games_submissions/the0x539/items/ingot.png b/2026/games_submissions/the0x539/items/ingot.png new file mode 100755 index 0000000..17e8aa8 Binary files /dev/null and b/2026/games_submissions/the0x539/items/ingot.png differ diff --git a/2026/games_submissions/the0x539/items/leggings.png b/2026/games_submissions/the0x539/items/leggings.png new file mode 100755 index 0000000..a28a10f Binary files /dev/null and b/2026/games_submissions/the0x539/items/leggings.png differ diff --git a/2026/games_submissions/the0x539/items/log.png b/2026/games_submissions/the0x539/items/log.png new file mode 100755 index 0000000..56d24f0 Binary files /dev/null and b/2026/games_submissions/the0x539/items/log.png differ diff --git a/2026/games_submissions/the0x539/items/pickaxe.png b/2026/games_submissions/the0x539/items/pickaxe.png new file mode 100755 index 0000000..9dea9e2 Binary files /dev/null and b/2026/games_submissions/the0x539/items/pickaxe.png differ diff --git a/2026/games_submissions/the0x539/items/piston.png b/2026/games_submissions/the0x539/items/piston.png new file mode 100755 index 0000000..f08618a Binary files /dev/null and b/2026/games_submissions/the0x539/items/piston.png differ diff --git a/2026/games_submissions/the0x539/items/plank.png b/2026/games_submissions/the0x539/items/plank.png new file mode 100644 index 0000000..863206a Binary files /dev/null and b/2026/games_submissions/the0x539/items/plank.png differ diff --git a/2026/games_submissions/the0x539/items/rod.png b/2026/games_submissions/the0x539/items/rod.png new file mode 100755 index 0000000..6610384 Binary files /dev/null and b/2026/games_submissions/the0x539/items/rod.png differ diff --git a/2026/games_submissions/the0x539/items/stick.png b/2026/games_submissions/the0x539/items/stick.png new file mode 100755 index 0000000..d32381d Binary files /dev/null and b/2026/games_submissions/the0x539/items/stick.png differ diff --git a/2026/games_submissions/the0x539/items/string.png b/2026/games_submissions/the0x539/items/string.png new file mode 100755 index 0000000..ad78eb4 Binary files /dev/null and b/2026/games_submissions/the0x539/items/string.png differ diff --git a/2026/games_submissions/the0x539/items/tooltip-backdrop.png b/2026/games_submissions/the0x539/items/tooltip-backdrop.png new file mode 100755 index 0000000..05c02cd Binary files /dev/null and b/2026/games_submissions/the0x539/items/tooltip-backdrop.png differ diff --git a/2026/games_submissions/the0x539/main.js b/2026/games_submissions/the0x539/main.js new file mode 100644 index 0000000..14a2b45 --- /dev/null +++ b/2026/games_submissions/the0x539/main.js @@ -0,0 +1,197 @@ +import { recipes, amounts } from './data.js'; +import { + createItem, + mouseDown, + mouseEnter, + mouseUp, + craftClick, + consumeClock, + resetInventory, +} from './interaction.js'; +import { scenarios } from './difficulty.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"]:not(crafting-output item-stack)')) { + // time's up! + endGame(false); + } else { + consumeClock(); + } +} + +function startGame(difficulty) { + document.body.classList.remove('failed'); + + resetInventory(); + + const scenario = scenarios.toReversed().find(s => difficulty >= s.level); + + desiredCrafts.clear(); + for (const item of scenario.goal) { + desiredCrafts.add(item); + } + + 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); + } + + for (const [item, amount] of Object.entries(scenario.items)) { + giveItem(item, amount); + } + giveItem('clock', scenario.time(difficulty - scenario.level)); + + 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' }); diff --git a/2026/games_submissions/the0x539/screen.css b/2026/games_submissions/the0x539/screen.css new file mode 100644 index 0000000..d9a6f72 --- /dev/null +++ b/2026/games_submissions/the0x539/screen.css @@ -0,0 +1,168 @@ +@font-face { + font-family: "Minecraft"; + src: url("./MinecraftStandard.otf") format("opentype"); +} + +* { + box-sizing: border-box; + user-select: none; + font-family: Minecraft; + font-smooth: never; + font-size: 6px; + text-rendering: geometricPrecision; + line-height: 1em; +} + +body { + margin: 0; +} + +crafting-grid, +inventory-grid { + position: absolute; + display: grid; + align-items: center; + justify-items: center; +} + +follow-cursor { + position: absolute; + pointer-events: none; +} + +grabbed-stack { + display: block; + translate: -50% -50%; + z-index: 10; + pointer-events: none; +} + +:root:has(grabbed-stack > item-stack) { + cursor: grabbing; +} + +inventory-cell, +crafting-output { + width: 16px; + height: 16px; + + &:hover, + &:has(> item-stack[data-original-count]) { + background-color: rgba(255, 255, 255, 0.5); + } + + &:has(> item-stack) { + cursor: grab; + } +} + +crafting-grid { + left: 62px; + top: 22px; + grid-template: repeat(3, 18px) / repeat(3, 18px); +} + +crafting-output { + position: absolute; + left: 157px; + top: 41px; +} + +inventory-grid { + left: 12px; + top: 94px; + grid-template: repeat(3, 18px) / repeat(12, 18px); +} + +item-stack { + display: block; + position: relative; + + & > data { + position: absolute; + color: #fcfcfc; + text-shadow: #3e3e3e 1px 1px; + bottom: 0; + right: 0; + translate: 1px -1px; + } + + &[data-count="0"] { + display: none; + } + + &[data-count="1"] > data { + display: none; + } + + &[data-item="clock"]::before { + background-image: url("./items/clock.gif"); + } + + &::before { + display: block; + width: 16px; + height: 16px; + content: ""; + background-size: 16px 16px; + } +} + +img { + pointer-events: none; +} + +item-tooltip { + display: block; + color: white; + border-style: solid; + border-width: 4px; + border-image-source: url("./tooltip-backdrop.png"); + border-image-slice: 4 fill; + text-shadow: gray 1px 1px; + height: 16px; + line-height: 1em; + color: #fcfcfc; + text-shadow: #3e3e3e 1px 1px; + translate: 8px -100%; + + &:empty, + &:has(+ * > item-stack) { + display: none; + } +} + +body.failed { + filter: brightness(0.8) sepia(75%) hue-rotate(-45deg); + cursor: not-allowed; + & > * { + pointer-events: none; + } +} + +label[for="todo-list"] { + position: absolute; + top: 12px; + left: 12px; + color: #3f3f3f; + + &:has(+ #todo-list:empty) { + display: none; + } +} + +#todo-list { + position: absolute; + left: 12px; + top: 24px; + margin: 0; + padding: 0; + display: flex; + width: 32px; + flex-wrap: wrap; + justify-content: center; + + & > li { + list-style: none; + } +} diff --git a/2026/games_submissions/the0x539/sfx/big-ding.ogg b/2026/games_submissions/the0x539/sfx/big-ding.ogg new file mode 100755 index 0000000..a3f4c0b Binary files /dev/null and b/2026/games_submissions/the0x539/sfx/big-ding.ogg differ diff --git a/2026/games_submissions/the0x539/sfx/oof.ogg b/2026/games_submissions/the0x539/sfx/oof.ogg new file mode 100755 index 0000000..10448dc Binary files /dev/null and b/2026/games_submissions/the0x539/sfx/oof.ogg differ diff --git a/2026/games_submissions/the0x539/sfx/small-ding.ogg b/2026/games_submissions/the0x539/sfx/small-ding.ogg new file mode 100755 index 0000000..a38b2cc Binary files /dev/null and b/2026/games_submissions/the0x539/sfx/small-ding.ogg differ diff --git a/2026/games_submissions/the0x539/tooltip-backdrop.png b/2026/games_submissions/the0x539/tooltip-backdrop.png new file mode 100755 index 0000000..893b468 Binary files /dev/null and b/2026/games_submissions/the0x539/tooltip-backdrop.png differ diff --git a/2026/games_submissions/the0x539/ui.png b/2026/games_submissions/the0x539/ui.png new file mode 100644 index 0000000..ab1873c Binary files /dev/null and b/2026/games_submissions/the0x539/ui.png differ -- cgit v1.3.1