1
0
Fork 0
tsgames/src/games/brick-dungeon/index.ts

331 lines
10 KiB
TypeScript

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);
}