diff --git a/src/common/utils.ts b/src/common/utils.ts index 0ed2ecf..f0abd04 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -4,7 +4,10 @@ export const nextFrame = async (): Promise => new Promise((resolve) => r export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random()); export const randBool = () => Math.random() < 0.5; export const choice = (array: any[]) => array[randInt(0, array.length)]; -export const weightedChoice = (options: [T, number][]): T => { +export const weightedChoice = (options: [T, number][] | Partial>): T | null => { + if (!Array.isArray(options)) { + options = Object.entries(options) as [T, number][]; + } const sum = options.reduce((acc, o) => acc + o[1], 0); const rnd = Math.random() * sum; @@ -17,7 +20,7 @@ export const weightedChoice = (options: [T, number][]): T => { } } - return options[0][0]; + return null; } export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k); diff --git a/src/games/brick-dungeon/assets/spritesheet.png b/src/games/brick-dungeon/assets/spritesheet.png index e4c7fc4..da94f29 100644 Binary files a/src/games/brick-dungeon/assets/spritesheet.png and b/src/games/brick-dungeon/assets/spritesheet.png differ diff --git a/src/games/brick-dungeon/data.ts b/src/games/brick-dungeon/data.ts index 3c59fc7..5d665ed 100644 --- a/src/games/brick-dungeon/data.ts +++ b/src/games/brick-dungeon/data.ts @@ -5,18 +5,21 @@ const emptySprite: BrickDisplayImage = { image: [], width: 0, height: 0 }; export interface Item { sprite: BrickDisplayImage; damage: number; - heal?: number; accuracy: number; + heal?: number; + instantUse?: boolean; + consumable?: boolean; ranged: boolean; - consumable: boolean; } export interface Monster { sprite: BrickDisplayImage; damage: number; + accuracy: number; health: number; + maxHealth: number; ranged: boolean; - lootTable: Record; // + lootTable: Partial>; // secondLootChance: number; } @@ -48,28 +51,24 @@ export const ITEMS: Item[] = [ damage: 1, accuracy: 1, ranged: false, - consumable: false, }, { // SWORD sprite: emptySprite, accuracy: 1, damage: 5, ranged: false, - consumable: false, }, { // BIG_SWORD sprite: emptySprite, accuracy: 0.9, damage: 10, ranged: false, - consumable: false, }, { // BOW sprite: emptySprite, accuracy: 0.7, damage: 5, ranged: true, - consumable: false, }, { // GRENADE sprite: emptySprite, @@ -92,14 +91,16 @@ export const ITEMS: Item[] = [ damage: 0, heal: 5, ranged: false, - consumable: true, + instantUse: true, }, ]; export const MONSTERS: Monster[] = [ { // SMALL_SLIME sprite: emptySprite, health: 1, + maxHealth: 1, damage: 1, + accuracy: 0.5, lootTable: { [Items.HEAL]: 0.5, [Items.SWORD]: 0.3, @@ -112,7 +113,9 @@ export const MONSTERS: Monster[] = [ { // SMALL_DEMON sprite: emptySprite, health: 2, + maxHealth: 2, damage: 1, + accuracy: 0.7, lootTable: { [Items.HEAL]: 0.5, [Items.SWORD]: 0.3, @@ -126,7 +129,9 @@ export const MONSTERS: Monster[] = [ { // SNAKE sprite: emptySprite, health: 3, + maxHealth: 3, damage: 2, + accuracy: 0.8, lootTable: { [Items.HEAL]: 0.5, [Items.POTION]: 0.3, @@ -139,13 +144,15 @@ export const MONSTERS: Monster[] = [ { // SMALL_ELEMENTAL sprite: emptySprite, health: 4, + maxHealth: 4, damage: 2, + accuracy: 0.5, lootTable: { [Items.HEAL]: 0.5, [Items.BOW]: 0.3, [Items.GRENADE]: 0.1, }, - ranged: false, + ranged: true, secondLootChance: 0, }, ]; diff --git a/src/games/brick-dungeon/index.ts b/src/games/brick-dungeon/index.ts index c067bb9..05ebbd3 100644 --- a/src/games/brick-dungeon/index.ts +++ b/src/games/brick-dungeon/index.ts @@ -3,7 +3,8 @@ import { isPressed, updateKeys } from "@common/input"; import backgroundImage from './assets/background.png'; import spritesheetImage from './assets/spritesheet.png'; -import { ITEMS, Items, MONSTERS, loadData, type Item } from "./data"; +import { ITEMS, Items, MONSTERS, loadData, type Item, type Monster } from "./data"; +import { delay, randBool, weightedChoice } from "@common/utils"; let display: BrickDisplay; let background: BrickDisplayImage; @@ -13,21 +14,31 @@ const playerY = 2; let y = 0; let targetY = 0; +let playerHealth = 10; +let playerBlink = 0; + let playerTurn = true; let lootToConfirm: Items | null = null; +let lootBlink = false; let selectedSlot = 0; -let inventory: Items[] = [Items.STICK, Items.BOW]; +let inventory: Items[] = [Items.STICK]; let monsterAlive = true; -let monster = -1; +let currentMonster = -1; let monsterY = 12; let monsterTargetY = 12; +let monsterBlink = 0; let bulletY = -1; let bulletBlink = false; let bulletTargetY = -1; +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) { @@ -35,8 +46,22 @@ async function loop(time: number) { const dt = time - prevFrameTime; prevFrameTime = time; + if (frames % 8 == 0) { + lootBlink = !lootBlink; + } + + let lootConfirmed = false; const item = ITEMS[inventory[selectedSlot]]; - if (bulletTargetY !== bulletY) { + const monster = MONSTERS[currentMonster]; + if (playerBlink) { + if (frames % 4 == 0) { + playerBlink--; + } + } else if (monsterBlink) { + if (frames % 4 == 0) { + monsterBlink--; + } + } else if (bulletTargetY !== bulletY) { console.log('Bullet animation'); if (frames % 2 == 0) { bulletY += Math.sign(bulletTargetY - bulletY); @@ -52,18 +77,25 @@ async function loop(time: number) { if (frames % 3 == 0) { y += Math.sign(targetY - y); } - } else if (playerTurn) { - // console.log('Player turn'); - if (isPressed('ArrowLeft')) { + } else if (playerTurn && playerHealth > 0) { + 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; - } - if (isPressed('ArrowUp') && (!monsterAlive || y < monsterY - 5)) { + } else if (isPressed('ArrowUp') && (!monsterAlive || y < monsterY - 5)) { targetY = y + 4; playerTurn = false; - } - if (isPressed('Space') && (monsterAlive && (y >= monsterY - 5 || item.ranged))) { + } 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; @@ -71,49 +103,142 @@ async function loop(time: number) { if (item.ranged) { console.log('Ranged attack') bulletY = y + playerY + playerSprite.height; - bulletTargetY = monsterY + MONSTERS[monster].sprite.height; + bulletTargetY = monsterY + monster.sprite.height; + } else if (item.heal) { + console.log('Heal' + item.heal); + playerHealth += item.heal; + playerTurn = false; } else { console.log('Melee attack'); damageMonster(item); + playerTurn = false; } - playerTurn = false; } } else if (lootToConfirm) { - console.log('lootConfirm'); - // TODO + console.log('Loot confirm'); + + if (isPressed('Space')) { + const i = ITEMS[lootToConfirm]; + if (i.instantUse && i.heal) { + playerHealth += i.heal; + } else { + inventory.push(lootToConfirm); + } + + 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'); - // TODO - playerTurn = true; + 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) { + 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(); + 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; - damageMonster(item); + if (playerTurn) { + damageMonster(item); + playerTurn = false; + } else { + damagePlayer(monster); + playerTurn = true; + } } display.clear(); display.clear(true); display.speed = item.damage; + display.score = playerHealth; + display.gameOver = playerHealth === 0; const bgY = y % display.height; display.drawImage(background, 0, bgY); display.drawImage(background, 0, bgY - display.height); - display.drawImage(playerSprite, 3, display.height - playerSprite.height - playerY); - if (monsterAlive && MONSTERS[monster]) { - display.drawImage(MONSTERS[monster].sprite, 3, display.height + (y - monsterY) - MONSTERS[monster].sprite.height - playerY); + if (playerBlink % 2 == 0) { + display.drawImage(playerSprite, 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); } - display.drawImage(ITEMS[inventory[selectedSlot]].sprite, 0, 0, true); + if (lootToConfirm) { + if (lootBlink) { + display.drawImage(ITEMS[lootToConfirm].sprite, 0, 0, true); + } + } else { + display.drawImage(item.sprite, 0, 0, true); + } display.update(); @@ -122,22 +247,34 @@ async function loop(time: number) { } function spawnNextMonster() { - monster++; - monsterAlive = MONSTERS[monster] != null; - monsterTargetY = monsterY = y + 13; + currentMonster++; + monsterAlive = MONSTERS[currentMonster] != null; + if (monsterAlive) { + MONSTERS[currentMonster].health = MONSTERS[currentMonster].maxHealth; + } + monsterY = y + 20; + monsterTargetY = y + 13; } function damageMonster(item: Item) { const rnd = Math.random(); - console.log(`Attack for ${item.damage}, success: ${rnd.toFixed(1)} < ${item.accuracy}`) + console.log(`Attack for ${item.damage}, success: ${rnd.toFixed(1)} < ${item.accuracy}`); if (monsterAlive && rnd < item.accuracy) { - MONSTERS[monster].health -= item.damage; - console.log(`Monster HP: ${MONSTERS[monster].health}`); - if (MONSTERS[monster].health <= 0) { - monsterAlive = false; - // TODO spawn loot + MONSTERS[currentMonster].health -= item.damage; + monsterBlink = 10; + console.log(`Monster HP: ${MONSTERS[currentMonster].health}`); + } +} - spawnNextMonster(); +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 = 10; + if (playerHealth <= 0) { + playerHealth = 0; } } }