import { BrickDisplay } from "@common/display/brick"; import { isPressed, updateKeys } from "@common/input"; import spritesheetImage from './assets/spritesheet.png'; import { ITEMS, Items, MONSTERS, MONSTERS_ORDER, loadData, type Item, type Monster } from "./data"; import { randBool, weightedChoice } from "@common/utils"; let display: BrickDisplay; const spritesheet = BrickDisplay.convertImage(spritesheetImage); const background = BrickDisplay.extractSprite(spritesheet, 12, 12, 10, 20); const winScreen = BrickDisplay.extractSprite(spritesheet, 24, 12, 6, 20); const playerSprite = BrickDisplay.extractSprite(spritesheet, 0, 4, 4, 4); const playerDeadSprite = BrickDisplay.extractSprite(spritesheet, 4, 4, 4, 4); const playerY = 2; let y = 0; let targetY = 0; let playerHealth = 10; let playerBlink = 0; let missBlink = 0; let win = false; let winBlink = false; let playerTurn = true; let lootToConfirm: Items | null = null; let lootBlink = false; let selectedSlot = 0; let inventory: Items[] = [Items.STICK]; let monsterAlive = true; let currentMonster = -1; let monsterY = 12; let monsterTargetY = 12; let monsterBlink = 0; let bulletY = -1; let bulletBlink = false; let bulletTargetY = -1; let bulletItem: Item | null = null; let lootY = -1; let lootItem: Items | null = null; let secondLootY = -1; let secondLootItem: Items | null = null; let frames = 0; let prevFrameTime: number = 0; async function loop(time: number) { frames++; const dt = time - prevFrameTime; prevFrameTime = time; if (frames % 8 == 0) { lootBlink = !lootBlink; } if (win && frames % 16 == 0) { winBlink = !winBlink; } if (playerHealth <= 0) { playerHealth = 0; playerTurn = false; } let lootConfirmed = false; const item = ITEMS[inventory[selectedSlot]]; const monster = MONSTERS[MONSTERS_ORDER[currentMonster]]; if (playerBlink) { if (frames % 4 == 0) { playerBlink--; } } else if (monsterBlink) { if (frames % 4 == 0) { monsterBlink--; } } else if (missBlink) { if (frames % 4 == 0) { missBlink--; } } else if (bulletTargetY !== bulletY) { console.log('Bullet animation'); if (frames % 2 == 0) { bulletY += Math.sign(bulletTargetY - bulletY); } bulletBlink = !bulletBlink; } else if (monsterTargetY !== monsterY) { console.log('Monster animation'); if (frames % 3 == 0) { monsterY += Math.sign(monsterTargetY - monsterY); } } else if (targetY !== y) { console.log('Player animation'); if (frames % 3 == 0) { y += Math.sign(targetY - y); } } else if (playerTurn) { lootToConfirm = null; if (y === lootY) { lootToConfirm = lootItem; playerTurn = false; } else if (y === secondLootY) { lootToConfirm = secondLootItem; playerTurn = false; } else if (isPressed('ArrowLeft')) { selectedSlot = (selectedSlot + inventory.length - 1) % inventory.length; } else if (isPressed('ArrowRight')) { selectedSlot = (selectedSlot + 1) % inventory.length; } else if (isPressed('ArrowUp') && (!monsterAlive || y < monsterY - 5)) { targetY = y + 4; playerTurn = false; } else if (isPressed('ArrowDown')) { targetY = y - 4; playerTurn = false; } else if (isPressed('Space') && (monsterAlive && (y >= monsterY - 5 || item.ranged || item.heal))) { if (item.consumable) { inventory.splice(selectedSlot, 1); selectedSlot = (selectedSlot + inventory.length - 1) % inventory.length; } if (item.ranged) { console.log('Ranged attack') bulletY = y + playerY + playerSprite.height; bulletTargetY = monsterY + monster.sprite.height; bulletItem = item; } else if (item.heal) { console.log('Heal' + item.heal); playerHealth += item.heal; playerTurn = false; } else { console.log('Melee attack'); damageMonster(item); playerTurn = false; } } } else if (lootToConfirm) { console.log('Loot confirm'); if (isPressed('Space')) { const i = ITEMS[lootToConfirm]; if (i.instantUse && i.heal) { playerHealth += i.heal; } else { inventory.push(lootToConfirm); if (!i.heal) { selectedSlot = inventory.length - 1; } } lootConfirmed = true; } else if (isPressed('ArrowUp')) { targetY = y + 4; playerTurn = true; } else if (isPressed('ArrowDown')) { targetY = y - 4; playerTurn = true; } } else if (monsterAlive) { // Monster turn console.log('Monster turn'); if (monster.health <= 0) { if (monsterBlink === 0) { // wait blink animation finish monsterAlive = false; const lootTable = { ...monster.lootTable }; for (const item of inventory) { lootTable[item] = 0; } const drop = weightedChoice(lootTable); lootY = monsterY - 1; lootItem = drop; if (drop == null) { spawnNextMonster(); } else { lootTable[drop] = 0; const rnd = Math.random(); if (rnd < monster.secondLootChance) { secondLootY = monsterY + 7; secondLootItem = weightedChoice(lootTable); } } playerTurn = true; } } else if (monster.ranged) { const retreat = randBool() && monsterY - y < 12; if (retreat) { monsterTargetY = monsterY + 4; playerTurn = true; } else { bulletY = monsterY + monster.sprite.height; bulletTargetY = y + playerY + playerSprite.height; } } else if (y < monsterY - 5) { monsterTargetY = monsterY - 4; playerTurn = true; } else { damagePlayer(monster); playerTurn = true; } } else { // return control to player playerTurn = true; } if (y === targetY && (lootItem || secondLootItem) && lootY < y - 10) { lootConfirmed = true; } if (lootConfirmed) { lootY = -1; secondLootY = -1; lootItem = null; secondLootItem = null; spawnNextMonster(); playerTurn = true; } if (bulletY > 0 && bulletTargetY === bulletY) { bulletTargetY = bulletY = -1; bulletBlink = false; if (playerTurn) { damageMonster(bulletItem ?? item); playerTurn = false; } else { damagePlayer(monster); playerTurn = true; } } display.clear(); display.clear(true); if (lootToConfirm) { const i = ITEMS[lootToConfirm]; display.speed = i.heal ?? i.damage; } else { display.speed = item.heal ?? item.damage; } display.score = playerHealth; display.gameOver = playerHealth <= 0; const bgY = y % display.height; if (missBlink % 2 == 0 || playerHealth <= 0) { display.drawImage(background, 0, bgY); display.drawImage(background, 0, bgY - display.height); } if (playerBlink % 2 == 0) { display.drawImage(playerHealth > 0 ? playerSprite : playerDeadSprite, 3, display.height - playerSprite.height - playerY); } if (monsterAlive && monsterBlink % 2 == 0 && monster) { display.drawImage(monster.sprite, 3, display.height + (y - monsterY) - monster.sprite.height - playerY); } if (lootItem && lootItem !== lootToConfirm) { display.drawImage(ITEMS[lootItem].sprite, 3, display.height + (y - lootY) - ITEMS[lootItem].sprite.height - playerY); } if (secondLootItem && secondLootItem !== lootToConfirm) { display.drawImage(ITEMS[secondLootItem].sprite, 3, display.height + (y - secondLootY) - ITEMS[secondLootItem].sprite.height - playerY); } if (bulletY > 0) { display.setPixel(display.width >> 1, display.height + (y - bulletY), bulletBlink); } if (lootToConfirm) { if (lootBlink) { display.drawImage(ITEMS[lootToConfirm].sprite, 0, 0, true); } } else { display.drawImage(item.sprite, 0, 0, true); } if (winBlink) { display.fillRect(2, 0, winScreen.width, winScreen.height, false); display.drawImage(winScreen, 2, 0); } display.update(); updateKeys(); requestAnimationFrame(loop); } function spawnNextMonster() { currentMonster++; monsterAlive = MONSTERS[MONSTERS_ORDER[currentMonster]] != null; if (monsterAlive) { MONSTERS[MONSTERS_ORDER[currentMonster]].health = MONSTERS[MONSTERS_ORDER[currentMonster]].maxHealth; monsterY = y + 20; monsterTargetY = y + 13; } else { win = true; } } function damageMonster(item: Item) { if (!monsterAlive) return; const rnd = Math.random(); console.log(`Attack for ${item.damage}, success: ${rnd.toFixed(1)} < ${item.accuracy}`); if (rnd < item.accuracy) { MONSTERS[MONSTERS_ORDER[currentMonster]].health -= item.damage; monsterBlink = 6; console.log(`Monster HP: ${MONSTERS[MONSTERS_ORDER[currentMonster]].health}`); } else { missBlink = 6; } } function damagePlayer(monster: Monster) { const rnd = Math.random(); console.log(`Monster attack for ${monster.damage}, success: ${rnd.toFixed(1)} < ${monster.accuracy}`); if (rnd < monster.accuracy) { playerHealth -= monster.damage; playerBlink = 6; } else { missBlink = 6; } } export default function main() { display = new BrickDisplay(); display.init(); loadData(spritesheet); spawnNextMonster(); requestAnimationFrame(loop); }