331 lines
10 KiB
TypeScript
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);
|
|
} |