From b6863ed27eafda8880ad9eeeaf11c3b9649230da Mon Sep 17 00:00:00 2001 From: Ole Herman Schumacher Elgesem Date: Wed, 21 Aug 2024 02:34:36 +0200 Subject: [PATCH] Animated pickaxe Signed-off-by: Ole Herman Schumacher Elgesem --- src/frontend/painter.ts | 138 +++++++++++++++++++++++++++++++++++----- src/libtrpg/game.ts | 98 ++++++++++++++++++++++------ src/libtrpg/keyboard.ts | 1 - src/libtrpg/rooms.ts | 22 +++++-- src/todo_utils.ts | 12 +++- 5 files changed, 229 insertions(+), 42 deletions(-) diff --git a/src/frontend/painter.ts b/src/frontend/painter.ts index 02f9a3c..db262bb 100644 --- a/src/frontend/painter.ts +++ b/src/frontend/painter.ts @@ -1,4 +1,4 @@ -import { Drawer } from "../todo_utils.ts"; +import { Drawer, xy_copy } from "../todo_utils.ts"; import { Application } from "./application.ts"; // For access to width, height, game object import { Choice, @@ -11,29 +11,121 @@ import { import { CR, XY } from "@olehermanse/utils"; import { cr, wh, xy } from "@olehermanse/utils/funcs.js"; -class SpriteLocation { +export class SpriteMetadata { cr: CR; frames: number; - constructor(r: number, c: number, frames?: number) { + constructor( + r: number, + c: number, + frames?: number, + public animation_data?: AnimationData, + ) { this.cr = cr(c, r); this.frames = frames ?? 1; } } +export interface AnimationFrame { + index: number; + time: number; +} + +export function frame(index: number, time: number): AnimationFrame { + return { index: index, time: time }; +} + +export class AnimationData { + constructor( + public frames: AnimationFrame[], + public loop?: boolean, + ) { + } + + get_animator() { + return new Animation(this.frames, this.loop); + } +} + +export class Animation { + current_ms: number = 0; + current_frame_index: number = 0; + max_time: number; + done: boolean = false; + constructor( + public frames: AnimationFrame[], + public loop?: boolean, + ) { + this.max_time = this.frames[this.frames.length - 1].time; + } + + restart() { + this.current_frame_index = 0; + this.current_ms = 0; + this.done = false; + } + + get_current_frame(): number { + return this.frames[this.current_frame_index].index; + } + + get_next_time(): number { + return this.frames[this.current_frame_index].time; + } + + tick(ms: number) { + if (this.done) { + return; + } + this.current_ms += ms; + const beyond_end = this.current_ms >= this.max_time; + if (beyond_end) { + if (!this.loop) { + this.done = true; + this.current_frame_index = this.frames.length - 1; + return; + } + // Go back to start if necessary + this.current_frame_index = 0; + this.current_ms -= this.max_time; + while (this.current_ms > this.max_time) { + this.current_ms -= this.max_time; + } + } + // Advance frame if necessary + while ( + this.current_frame_index < this.frames.length - 1 && + this.current_ms > this.get_next_time() + ) { + this.current_frame_index += 1; + } + } +} + const SPRITESHEET = { - player: new SpriteLocation(0, 0, 2), - sword: new SpriteLocation(1, 0, 2), - pickaxe: new SpriteLocation(1, 2, 2), - axe: new SpriteLocation(1, 4, 2), - staff: new SpriteLocation(1, 4, 2), - selector: new SpriteLocation(2, 0, 2), - chest: new SpriteLocation(3, 0), - rock: new SpriteLocation(3, 1, 3), - crystal: new SpriteLocation(3, 4), - skeleton: new SpriteLocation(4, 0, 4), - fog: new SpriteLocation(5, 0, 5), + player: new SpriteMetadata(0, 0, 2), + sword: new SpriteMetadata(1, 0, 2), + pickaxe: new SpriteMetadata( + 1, + 2, + 2, + new AnimationData([frame(0, 250), frame(1, 500)], false), + ), + axe: new SpriteMetadata(1, 4, 2), + staff: new SpriteMetadata(1, 4, 2), + selector: new SpriteMetadata(2, 0, 2), + chest: new SpriteMetadata(3, 0), + rock: new SpriteMetadata(3, 1, 3), + crystal: new SpriteMetadata(3, 4), + skeleton: new SpriteMetadata(4, 0, 4), + fog: new SpriteMetadata(5, 0, 5), }; +export type SpriteName = keyof typeof SPRITESHEET; + +export function get_sprite_metadata(name: SpriteName) { + return SPRITESHEET[name]; +} + type SpriteCallback = { (spritesheet: ImageBitmap[][]): void; }; @@ -48,10 +140,10 @@ function load_sprites( ) { const image = new Image(); const sprites: ImageBitmap[] = []; - const frames: SpriteLocation[] = []; + const frames: SpriteMetadata[] = []; for (let r = 0; r < rows; r++) { for (let c = 0; c < columns; c++) { - frames.push(new SpriteLocation(r, c)); + frames.push(new SpriteMetadata(r, c)); } } image.onload = () => { @@ -269,6 +361,20 @@ export class Painter { player.xy, player.reversed, ); + for (const item of player.inventory) { + if (item.animation === undefined || item.animation.done) { + continue; + } + const sprite = + this.sprites[item.name][item.animation.get_current_frame()]; + const pos = xy_copy(player.xy); + if (player.reversed) { + pos.x -= 4; + } else { + pos.x += 4; + } + this.offscreen_drawer.sprite(sprite, pos, player.reversed); + } } draw_card(choice: Choice) { diff --git a/src/libtrpg/game.ts b/src/libtrpg/game.ts index 0ec3487..5a79ff6 100644 --- a/src/libtrpg/game.ts +++ b/src/libtrpg/game.ts @@ -20,30 +20,31 @@ import { import { generate_room, RoomType } from "./rooms.ts"; import { Keyboard } from "./keyboard.ts"; import { cr_4_neighbors } from "../todo_utils.ts"; +import { + Animation, + get_sprite_metadata, + SpriteMetadata, + SpriteName, +} from "../frontend/painter.ts"; const DIAG = 1.42; const BASE_SPEED = 16.0; export class Entity { - name: string; - zone: Zone; xy: XY; fxy: XY | null; // can hold floating point xy values, so players can move half pixels. cr: CR; wh: WH; variant: number; - reversed: boolean; + animation?: Animation; constructor( - name: string, + public name: SpriteName, pos: CR, - zone: Zone, + public zone: Zone, variant?: number, - reversed?: boolean, + public reversed?: boolean, ) { - this.reversed = reversed === true; - this.name = name; - this.zone = zone; this.cr = cr(pos.c, pos.r); this.wh = wh(zone.cell_width, zone.cell_height); this.xy = cr_to_xy(this.cr, zone); @@ -51,6 +52,26 @@ export class Entity { this.fxy = null; } + tick(ms: number) { + if (this.animation === undefined) { + return; + } + this.animation.tick(ms); + } + + start_animation() { + if (this.animation !== undefined) { + this.animation.restart(); + return; + } + const metadata: SpriteMetadata = get_sprite_metadata(this.name); + if (metadata.animation_data === undefined) { + console.log("No animation found for " + this.name); + return; + } + this.animation = metadata.animation_data.get_animator(); + } + get center(): XY { const r = xy(this.xy.x, this.xy.y); r.x += this.wh.width / 2; @@ -74,7 +95,7 @@ export class Target { constructor( public cr: CR, grid: Grid, - public draw: boolean + public draw: boolean, ) { this.xy = cr_to_xy(cr, grid); } @@ -89,6 +110,7 @@ export class Player extends Entity { xp = 0; stats: Stats; upgrades: NamedUpgrade[]; + inventory: Entity[] = []; speed = BASE_SPEED; walk_counter = 0; target: Target | null = null; @@ -238,6 +260,9 @@ export class Player extends Entity { } tick(ms: number) { + for (const item of this.inventory) { + item.tick(ms); + } if (this.target === null) { return; } @@ -280,6 +305,15 @@ export class Player extends Entity { this.cr.c = new_pos.c; this.cr.r = new_pos.r; this.defog(); + const tile = this.game.current_zone.get_tile(this.cr); + if (tile.is_interactable()) { + this.interact(tile); + } + } + interact(tile: Tile) { + const item = tile.pickup(); + item.start_animation(); + this.inventory.push(item); } } @@ -332,6 +366,24 @@ export class Tile { is_empty() { return this.entities.length === 0; } + + is_interactable() { + if (this.is_empty() || this.is_rock()) { + return false; + } + if (this.entities[0].name === "pickaxe") { + return true; + } + return false; + } + + pickup() { + console.assert(this.entities.length > 0); + console.assert(this.entities[0].name === "pickaxe"); + const item = this.entities[0]; + array_remove(this.entities, item); + return item; + } } export class Zone extends Grid { @@ -604,7 +656,7 @@ export class Game { constructor(public grid: Grid) { this.current_zone = new Zone(grid, this, cr(0, 0)); this.put_zone(this.current_zone); - this.player = new Player(cr(1, 1), this.current_zone, this); + this.player = new Player(cr(7, 3), this.current_zone, this); this.choices = []; this.choices.push(new Choice("Vision", "light +1", 0, grid)); this.choices.push(new Choice("Haste", "Speed x2", 1, grid)); @@ -768,6 +820,12 @@ export class Game { this.choices[2].set(upgrades[2]); } + attempt_move_or_interact(pos: CR, mouse: boolean) { + const tile = this.current_zone.get_tile(pos); + console.assert(tile.is_empty() || tile.is_interactable()); + this.player.target = new Target(pos, this.grid, mouse); + } + zone_click(position: XY) { const pos = xy_to_cr(position, this.grid); if ( @@ -777,11 +835,11 @@ export class Game { ) { return; } - const tile = this.current_zone.tiles[pos.c][pos.r]; + const tile = this.current_zone.get_tile(pos); if (tile.light !== 5 || !tile.is_empty()) { return; } - this.player.target = new Target(pos, this.grid, true); + this.attempt_move_or_interact(pos, true); } level_up_click(position: XY) { @@ -807,8 +865,8 @@ export class Game { let up = this.keyboard.pressed("w") || this.keyboard.pressed("ArrowUp"); let down = this.keyboard.pressed("s") || this.keyboard.pressed("ArrowDown"); let left = this.keyboard.pressed("a") || this.keyboard.pressed("ArrowLeft"); - let right = - this.keyboard.pressed("d") || this.keyboard.pressed("ArrowRight"); + let right = this.keyboard.pressed("d") || + this.keyboard.pressed("ArrowRight"); if (up === down && left === right) { return; @@ -821,7 +879,7 @@ export class Game { (down && this.player.cr.r === this.current_zone.rows - 1) || (right && this.player.cr.c === this.current_zone.columns - 1) ) { - this.player.target = new Target(this.player.cr, this.grid, false); + this.attempt_move_or_interact(this.player.cr, false); return; } } @@ -854,8 +912,8 @@ export class Game { if (tile.light !== 5) { return; } - if (tile.is_empty()) { - this.player.target = new Target(pos, this.grid, false); + if (tile.is_empty() || tile.is_interactable()) { + this.attempt_move_or_interact(pos, false); return; } const alternatives: CR[] = cr_4_neighbors( @@ -880,7 +938,7 @@ export class Game { neighbor.c === this.current_zone.columns - 1 || neighbor.r === this.current_zone.rows - 1 ) { - this.player.target = new Target(neighbor, this.grid, false); + this.attempt_move_or_interact(neighbor, false); return; } second_choice = neighbor; @@ -888,7 +946,7 @@ export class Game { if (second_choice === null) { return; } - this.player.target = new Target(second_choice, this.grid, false); + this.attempt_move_or_interact(second_choice, false); } click(position: XY) { diff --git a/src/libtrpg/keyboard.ts b/src/libtrpg/keyboard.ts index 1ee456c..81b2089 100644 --- a/src/libtrpg/keyboard.ts +++ b/src/libtrpg/keyboard.ts @@ -2,7 +2,6 @@ export class Keyboard { state: Set = new Set(); press(key: string) { this.state.add(key); - console.log(key); } release(key: string) { this.state.delete(key); diff --git a/src/libtrpg/rooms.ts b/src/libtrpg/rooms.ts index de4c72b..d7436fe 100644 --- a/src/libtrpg/rooms.ts +++ b/src/libtrpg/rooms.ts @@ -1,8 +1,8 @@ import { cr, randint } from "@olehermanse/utils/funcs.js"; import { Entity, Zone } from "./game.ts"; -import { inside_rectangle, randpercent } from "../todo_utils"; +import { inside_rectangle } from "../todo_utils"; -export type RoomType = "generic" | "chest" | "empty"; +export type RoomType = "generic" | "chest" | "empty" | "spawn"; function _chest_room(zone: Zone) { // room is 16 columns and 12 rows @@ -39,10 +39,19 @@ function _generic_room(zone: Zone) { } } +function _spawn_room(zone: Zone) { + const pos = cr(8, 9); + const entity = new Entity("pickaxe", pos, zone); + zone.append(entity); +} + function _generate_entities(zone: Zone) { if (zone.room_type === "empty") { return; } + if (zone.room_type === "spawn") { + return _spawn_room(zone); + } if (zone.room_type === "chest") { return _chest_room(zone); } @@ -54,14 +63,19 @@ function _generate_entities(zone: Zone) { function _select_room_type(zone: Zone) { if (zone.pos.c === 0 && zone.pos.r === 0) { - zone.room_type = "empty"; + zone.room_type = "spawn"; return; } if (inside_rectangle(zone.pos.c, zone.pos.r, -1, -1, 1, 1)) { zone.room_type = "generic"; return; } - if (randpercent(10)) { + const percentage = randint(1, 100); + if (percentage <= 1) { // 1% + zone.room_type = "empty"; + return; + } + if (percentage <= 1 + 10) { // 10% zone.room_type = "chest"; return; } diff --git a/src/todo_utils.ts b/src/todo_utils.ts index 35cc806..c430b05 100644 --- a/src/todo_utils.ts +++ b/src/todo_utils.ts @@ -181,7 +181,13 @@ export function randpercent(threshold: number): boolean { return false; } -export function cr_4_neighbors(pos: CR, up?: boolean, down?: boolean, left?:boolean, right?:boolean): CR[] { +export function cr_4_neighbors( + pos: CR, + up?: boolean, + down?: boolean, + left?: boolean, + right?: boolean, +): CR[] { const c = pos.c; const r = pos.r; const results = []; @@ -199,3 +205,7 @@ export function cr_4_neighbors(pos: CR, up?: boolean, down?: boolean, left?:bool } return results; } + +export function xy_copy(pos: XY) { + return xy(pos.x, pos.y); +}