Brick-dungeon: damaging monsters
This commit is contained in:
parent
a26e069504
commit
0bb010190d
|
|
@ -37,6 +37,7 @@ body {
|
||||||
border-width: calc(var(--pixel-size) * 0.05);
|
border-width: calc(var(--pixel-size) * 0.05);
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
transition: color 100ms ease-out;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
|
@ -49,6 +50,7 @@ body {
|
||||||
width: calc(var(--pixel-size) * 0.4);
|
width: calc(var(--pixel-size) * 0.4);
|
||||||
height: calc(var(--pixel-size) * 0.4);
|
height: calc(var(--pixel-size) * 0.4);
|
||||||
background-color: var(--color);
|
background-color: var(--color);
|
||||||
|
transition: background-color 100ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
|
|
@ -123,6 +125,7 @@ body {
|
||||||
.text {
|
.text {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
transition: color 100ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ interface IKeyState {
|
||||||
const KEYS: Record<string, IKeyState> = {};
|
const KEYS: Record<string, IKeyState> = {};
|
||||||
|
|
||||||
document.body.addEventListener('keydown', (e) => {
|
document.body.addEventListener('keydown', (e) => {
|
||||||
const keyId = e.code.toLowerCase();
|
const keyId = e.code;
|
||||||
|
console.debug(`[Input] Pressed ${keyId}`);
|
||||||
if (KEYS[keyId]) {
|
if (KEYS[keyId]) {
|
||||||
KEYS[keyId].state = true;
|
KEYS[keyId].state = true;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -18,15 +19,16 @@ document.body.addEventListener('keydown', (e) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
document.body.addEventListener('keyup', (e) => {
|
document.body.addEventListener('keyup', (e) => {
|
||||||
const keyId = e.code.toLowerCase();
|
const keyId = e.code;
|
||||||
|
console.debug(`[Input] Released ${keyId}`);
|
||||||
if (KEYS[keyId]) {
|
if (KEYS[keyId]) {
|
||||||
KEYS[keyId].state = false;
|
KEYS[keyId].state = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const isPressed = (key: string): boolean => KEYS[key.toLowerCase()]?.pressed ?? false;
|
export const isPressed = (key: string): boolean => KEYS[key]?.pressed ?? false;
|
||||||
export const isReleased = (key: string): boolean => KEYS[key.toLowerCase()]?.released ?? false;
|
export const isReleased = (key: string): boolean => KEYS[key]?.released ?? false;
|
||||||
export const isHeld = (key: string): boolean => KEYS[key.toLowerCase()]?.held ?? false;
|
export const isHeld = (key: string): boolean => KEYS[key]?.held ?? false;
|
||||||
|
|
||||||
export function updateKeys() {
|
export function updateKeys() {
|
||||||
for (const key of Object.values(KEYS)) {
|
for (const key of Object.values(KEYS)) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,21 @@ export const nextFrame = async (): Promise<number> => new Promise((resolve) => r
|
||||||
export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random());
|
export const randInt = (min: number, max: number) => Math.round(min + (max - min - 1) * Math.random());
|
||||||
export const randBool = () => Math.random() < 0.5;
|
export const randBool = () => Math.random() < 0.5;
|
||||||
export const choice = (array: any[]) => array[randInt(0, array.length)];
|
export const choice = (array: any[]) => array[randInt(0, array.length)];
|
||||||
|
export const weightedChoice = <T>(options: [T, number][]): T => {
|
||||||
|
const sum = options.reduce((acc, o) => acc + o[1], 0);
|
||||||
|
|
||||||
|
const rnd = Math.random() * sum;
|
||||||
|
|
||||||
|
let weight = 0;
|
||||||
|
for (const [item, probability] of options) {
|
||||||
|
weight += probability;
|
||||||
|
if (rnd < weight) {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options[0][0];
|
||||||
|
}
|
||||||
|
|
||||||
export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k);
|
export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k);
|
||||||
export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
export const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 203 B After Width: | Height: | Size: 198 B |
|
|
@ -0,0 +1,163 @@
|
||||||
|
import { BrickDisplay, type BrickDisplayImage } from "@common/display";
|
||||||
|
|
||||||
|
const emptySprite: BrickDisplayImage = { image: [], width: 0, height: 0 };
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
sprite: BrickDisplayImage;
|
||||||
|
damage: number;
|
||||||
|
heal?: number;
|
||||||
|
accuracy: number;
|
||||||
|
ranged: boolean;
|
||||||
|
consumable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Monster {
|
||||||
|
sprite: BrickDisplayImage;
|
||||||
|
damage: number;
|
||||||
|
health: number;
|
||||||
|
ranged: boolean;
|
||||||
|
lootTable: Record<string, number>; // <itemId, probability of drop>
|
||||||
|
secondLootChance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Items {
|
||||||
|
STICK,
|
||||||
|
SWORD,
|
||||||
|
HEAVY_SWORD,
|
||||||
|
BOW,
|
||||||
|
GRENADE,
|
||||||
|
POTION,
|
||||||
|
HEAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Monsters {
|
||||||
|
SMALL_SLIME,
|
||||||
|
SMALL_DEMON,
|
||||||
|
SNAKE,
|
||||||
|
SMALL_ELEMENTAL,
|
||||||
|
SLIME,
|
||||||
|
DEMON,
|
||||||
|
ELEMENTAL,
|
||||||
|
BIG_SLIME,
|
||||||
|
BIG_ELEMENTAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ITEMS: Item[] = [
|
||||||
|
{ // STICK
|
||||||
|
sprite: emptySprite,
|
||||||
|
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,
|
||||||
|
accuracy: 0.8,
|
||||||
|
damage: 10,
|
||||||
|
ranged: true,
|
||||||
|
consumable: true,
|
||||||
|
},
|
||||||
|
{ // POTION
|
||||||
|
sprite: emptySprite,
|
||||||
|
accuracy: 1,
|
||||||
|
damage: 0,
|
||||||
|
heal: 10,
|
||||||
|
ranged: false,
|
||||||
|
consumable: true,
|
||||||
|
},
|
||||||
|
{ // HEAL
|
||||||
|
sprite: emptySprite,
|
||||||
|
accuracy: 1,
|
||||||
|
damage: 0,
|
||||||
|
heal: 5,
|
||||||
|
ranged: false,
|
||||||
|
consumable: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
export const MONSTERS: Monster[] = [
|
||||||
|
{ // SMALL_SLIME
|
||||||
|
sprite: emptySprite,
|
||||||
|
health: 1,
|
||||||
|
damage: 1,
|
||||||
|
lootTable: {
|
||||||
|
[Items.HEAL]: 0.5,
|
||||||
|
[Items.SWORD]: 0.3,
|
||||||
|
[Items.POTION]: 0.15,
|
||||||
|
[Items.BOW]: 0.05,
|
||||||
|
},
|
||||||
|
ranged: false,
|
||||||
|
secondLootChance: 0,
|
||||||
|
},
|
||||||
|
{ // SMALL_DEMON
|
||||||
|
sprite: emptySprite,
|
||||||
|
health: 2,
|
||||||
|
damage: 1,
|
||||||
|
lootTable: {
|
||||||
|
[Items.HEAL]: 0.5,
|
||||||
|
[Items.SWORD]: 0.3,
|
||||||
|
[Items.POTION]: 0.15,
|
||||||
|
[Items.BOW]: 0.04,
|
||||||
|
[Items.HEAVY_SWORD]: 0.01,
|
||||||
|
},
|
||||||
|
ranged: false,
|
||||||
|
secondLootChance: 0,
|
||||||
|
},
|
||||||
|
{ // SNAKE
|
||||||
|
sprite: emptySprite,
|
||||||
|
health: 3,
|
||||||
|
damage: 2,
|
||||||
|
lootTable: {
|
||||||
|
[Items.HEAL]: 0.5,
|
||||||
|
[Items.POTION]: 0.3,
|
||||||
|
[Items.BOW]: 0.1,
|
||||||
|
[Items.GRENADE]: 0.01,
|
||||||
|
},
|
||||||
|
ranged: false,
|
||||||
|
secondLootChance: 0,
|
||||||
|
},
|
||||||
|
{ // SMALL_ELEMENTAL
|
||||||
|
sprite: emptySprite,
|
||||||
|
health: 4,
|
||||||
|
damage: 2,
|
||||||
|
lootTable: {
|
||||||
|
[Items.HEAL]: 0.5,
|
||||||
|
[Items.BOW]: 0.3,
|
||||||
|
[Items.GRENADE]: 0.1,
|
||||||
|
},
|
||||||
|
ranged: false,
|
||||||
|
secondLootChance: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function loadData(spritesheet: BrickDisplayImage) {
|
||||||
|
for (let i = 0; i < ITEMS.length; i++) {
|
||||||
|
ITEMS[i].sprite = BrickDisplay.extractSprite(spritesheet, i * 4, 0, 4, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < MONSTERS.length; i++) {
|
||||||
|
const x = (i % 8) << 2;
|
||||||
|
const y = (3 + i / 8) << 2;
|
||||||
|
MONSTERS[i].sprite = BrickDisplay.extractSprite(spritesheet, x, y, 4, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,26 +1,32 @@
|
||||||
import { BrickDisplay, type BrickDisplayImage } from "@common/display";
|
import { BrickDisplay, type BrickDisplayImage } from "@common/display";
|
||||||
import { delay } from "@common/utils";
|
|
||||||
import { isPressed, updateKeys } from "@common/input";
|
import { isPressed, updateKeys } from "@common/input";
|
||||||
|
|
||||||
import backgroundImage from './assets/background.png';
|
import backgroundImage from './assets/background.png';
|
||||||
import spritesheetImage from './assets/spritesheet.png';
|
import spritesheetImage from './assets/spritesheet.png';
|
||||||
|
import { ITEMS, Items, MONSTERS, loadData, type Item } from "./data";
|
||||||
|
|
||||||
let display: BrickDisplay;
|
let display: BrickDisplay;
|
||||||
let background: BrickDisplayImage;
|
let background: BrickDisplayImage;
|
||||||
let player: BrickDisplayImage;
|
let playerSprite: BrickDisplayImage;
|
||||||
const weapons: BrickDisplayImage[] = [];
|
|
||||||
const monsters: BrickDisplayImage[] = [];
|
|
||||||
|
|
||||||
let bgY = 0;
|
const playerY = 2;
|
||||||
let weapon = 0;
|
|
||||||
let monster = 0;
|
|
||||||
|
|
||||||
function moveBackground() {
|
let y = 0;
|
||||||
bgY++;
|
let targetY = 0;
|
||||||
if (bgY >= 0) {
|
let playerTurn = true;
|
||||||
bgY -= display.height;
|
let lootToConfirm: Items | null = null;
|
||||||
}
|
|
||||||
}
|
let selectedSlot = 0;
|
||||||
|
let inventory: Items[] = [Items.STICK, Items.BOW];
|
||||||
|
|
||||||
|
let monsterAlive = true;
|
||||||
|
let monster = -1;
|
||||||
|
let monsterY = 12;
|
||||||
|
let monsterTargetY = 12;
|
||||||
|
|
||||||
|
let bulletY = -1;
|
||||||
|
let bulletBlink = false;
|
||||||
|
let bulletTargetY = -1;
|
||||||
|
|
||||||
let frames = 0;
|
let frames = 0;
|
||||||
let prevFrameTime: number = 0;
|
let prevFrameTime: number = 0;
|
||||||
|
|
@ -29,56 +35,123 @@ async function loop(time: number) {
|
||||||
const dt = time - prevFrameTime;
|
const dt = time - prevFrameTime;
|
||||||
prevFrameTime = time;
|
prevFrameTime = time;
|
||||||
|
|
||||||
if (frames > 10) {
|
const item = ITEMS[inventory[selectedSlot]];
|
||||||
moveBackground();
|
if (bulletTargetY !== bulletY) {
|
||||||
frames = 0;
|
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) {
|
||||||
|
// console.log('Player turn');
|
||||||
|
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)) {
|
||||||
|
targetY = y + 4;
|
||||||
|
playerTurn = false;
|
||||||
|
}
|
||||||
|
if (isPressed('Space') && (monsterAlive && (y >= monsterY - 5 || item.ranged))) {
|
||||||
|
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 + MONSTERS[monster].sprite.height;
|
||||||
|
} else {
|
||||||
|
console.log('Melee attack');
|
||||||
|
damageMonster(item);
|
||||||
|
}
|
||||||
|
playerTurn = false;
|
||||||
|
}
|
||||||
|
} else if (lootToConfirm) {
|
||||||
|
console.log('lootConfirm');
|
||||||
|
// TODO
|
||||||
|
} else if (monsterAlive) { // Monster turn
|
||||||
|
console.log('Monster turn');
|
||||||
|
// TODO
|
||||||
|
playerTurn = true;
|
||||||
|
} else { // return control to player
|
||||||
|
playerTurn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulletY > 0 && bulletTargetY === bulletY) {
|
||||||
|
bulletTargetY = bulletY = -1;
|
||||||
|
bulletBlink = false;
|
||||||
|
damageMonster(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
display.clear();
|
display.clear();
|
||||||
display.clear(true);
|
display.clear(true);
|
||||||
|
|
||||||
|
display.speed = item.damage;
|
||||||
|
|
||||||
|
const bgY = y % display.height;
|
||||||
display.drawImage(background, 0, bgY);
|
display.drawImage(background, 0, bgY);
|
||||||
display.drawImage(background, 0, bgY + display.height);
|
display.drawImage(background, 0, bgY - display.height);
|
||||||
|
|
||||||
display.drawImage(player, 3, 14);
|
display.drawImage(playerSprite, 3, display.height - playerSprite.height - playerY);
|
||||||
display.drawImage(monsters[monster], 3, 2);
|
if (monsterAlive && MONSTERS[monster]) {
|
||||||
|
display.drawImage(MONSTERS[monster].sprite, 3, display.height + (y - monsterY) - MONSTERS[monster].sprite.height - playerY);
|
||||||
|
}
|
||||||
|
|
||||||
display.drawImage(weapons[weapon], 0, 0, true);
|
if (bulletY > 0) {
|
||||||
|
display.setPixel(display.width >> 1, display.height + (y - bulletY), bulletBlink);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.drawImage(ITEMS[inventory[selectedSlot]].sprite, 0, 0, true);
|
||||||
|
|
||||||
display.update();
|
display.update();
|
||||||
|
|
||||||
if (isPressed('arrowleft')) {
|
|
||||||
weapon = (weapon + weapons.length - 1) % weapons.length;
|
|
||||||
} else if (isPressed('arrowright')) {
|
|
||||||
weapon = (weapon + 1) % weapons.length;
|
|
||||||
}
|
|
||||||
if (isPressed('arrowup')) {
|
|
||||||
monster = (monster + monsters.length - 1) % monsters.length;
|
|
||||||
} else if (isPressed('arrowdown')) {
|
|
||||||
monster = (monster + 1) % monsters.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateKeys();
|
updateKeys();
|
||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function spawnNextMonster() {
|
||||||
|
monster++;
|
||||||
|
monsterAlive = MONSTERS[monster] != null;
|
||||||
|
monsterTargetY = monsterY = y + 13;
|
||||||
|
}
|
||||||
|
|
||||||
|
function damageMonster(item: Item) {
|
||||||
|
const rnd = Math.random();
|
||||||
|
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
|
||||||
|
|
||||||
|
spawnNextMonster();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function main() {
|
export default function main() {
|
||||||
display = new BrickDisplay();
|
display = new BrickDisplay();
|
||||||
display.init();
|
display.init();
|
||||||
|
|
||||||
background = BrickDisplay.convertImage(backgroundImage);
|
background = BrickDisplay.convertImage(backgroundImage);
|
||||||
const spritesheet = BrickDisplay.convertImage(spritesheetImage);
|
const spritesheet = BrickDisplay.convertImage(spritesheetImage);
|
||||||
|
loadData(spritesheet);
|
||||||
|
|
||||||
for (let i = 0; i < 4; i++) {
|
playerSprite = BrickDisplay.extractSprite(spritesheet, 0, 8, 4, 4);
|
||||||
weapons.push(BrickDisplay.extractSprite(spritesheet, i * 4, 0, 4, 4));
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
monsters.push(BrickDisplay.extractSprite(spritesheet, i * 4, 12, 4, 4));
|
|
||||||
}
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
monsters.push(BrickDisplay.extractSprite(spritesheet, i * 4, 16, 4, 4));
|
|
||||||
}
|
|
||||||
player = BrickDisplay.extractSprite(spritesheet, 0, 8, 4, 4)
|
|
||||||
|
|
||||||
|
spawnNextMonster();
|
||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue