import { load_texture } from "./assets"; import { btn, Button } from "./input"; import { Vec2, new_v2, vadd, vsub, vnorm, vmul } from "./vector"; import { spr, use_texture, Texture } from "./graphics"; import { has_collision, Level } from "./level"; export interface ActorProps { id: string; cx: number; cy: number; xr: number; yr: number; position: Vec2; velocity: Vec2; friction: number; collide_radius: number; } export function new_actor_props( id: string, position: Vec2, collide_radius: number ): ActorProps { const cx = Math.trunc(position.x / 16); const cy = Math.trunc(position.y / 16); const xr = (position.x - cx * 16) / 16; const yr = (position.y - cy * 16) / 16; return { cx, cy, xr, yr, velocity: new_v2(0), friction: 0.7, id, position, collide_radius, }; } export class Actor { type: ActorType; props: ActorProps; constructor(type: ActorType, props: ActorProps) { this.type = type; this.props = props; } update() {} // IDEAS: Make gameplay logic at 30fps and render updates at 60fps? Does // this mean we do some "update" in render? // // TODO: Update physics in a better kind of way rather than an object call. update_physics(level: Level) { // This is very nice: https://deepnight.net/tutorial/a-simple-platformer-engine-part-1-basics/ // Apply friction to velocity and zero if we're close enough to zero. const props = this.props; const velocity = vmul(props.velocity, props.friction); if (Math.abs(velocity.x) < 0.01) { velocity.x = 0; } if (Math.abs(velocity.y) < 0.01) { velocity.y = 0; } let cx = props.cx; let cy = props.cy; // Adjust xr with velocity, check for collisions, etc. // TODO: This code is somewhat wrong because it kinda assumes that we're // the same size as our cell size, which is incorrect. let xr = props.xr + velocity.x; do { // TODO: Cap velocity to 1 tile/frame? Then we wouldn't need this loop... if (xr >= 0.7 && has_collision(level, cx + 1, cy)) { xr = 0.7; velocity.x = 0; } if (xr <= 0.3 && has_collision(level, cx - 1, cy)) { xr = 0.3; velocity.x = 0; } if (xr > 1) { cx += 1; xr -= 1; } if (xr < 0) { cx -= 1; xr += 1; } } while (xr > 1 || xr < 0); let yr = props.yr + velocity.y; do { if (yr >= 0.4 && has_collision(level, cx, cy + 1)) { yr = 0.4; velocity.y = 0; } if (yr <= 0.1 && has_collision(level, cx, cy - 1)) { yr = 0.1; velocity.y = 0; } if (yr > 1) { cy += 1; yr -= 1; } if (yr < 0) { cy -= 1; yr += 1; } } while (yr > 1 || yr < 0); // TODO: Entity collision detection const new_position = new_v2((cx + xr) * 16, (cy + yr) * 16); props.cx = cx; props.cy = cy; props.xr = xr; props.yr = yr; props.velocity = velocity; props.position = new_position; } draw(_clock: number) {} bonk(_other: Actor) {} } const robo_info = { anchor: new_v2(16, 16), // Distance from upper-left of sprite. bounds: new_v2(32), // Width/height of sprite. sprite: "./bot.png", animations: [ { start: 0, length: 1, speed: 20 }, { start: 1, length: 4, speed: 8 }, ], }; export class Robo extends Actor { bot_sprite: Texture | undefined = undefined; constructor(props: ActorProps) { super("robo", props); load_texture(robo_info.sprite).then((texture) => { this.bot_sprite = texture; }); } update() { // Acceleration from input let a = new_v2(0); if (btn(Button.Up)) { a.y -= 1; } if (btn(Button.Down)) { a.y += 1; } if (btn(Button.Left)) { a.x -= 1; } if (btn(Button.Right)) { a.x += 1; } vnorm(a); this.props.velocity = vadd(this.props.velocity, vmul(a, 0.07)); } draw(clock: number) { if (this.bot_sprite != undefined) { use_texture(this.bot_sprite); const vel = this.props.velocity; const moving = vel.x != 0 || vel.y != 0; const anim = robo_info.animations[moving ? 1 : 0]; const { x: w, y: h } = robo_info.bounds; const { x, y } = vsub(this.props.position, robo_info.anchor); const frame = (anim.start + ((clock / anim.speed) % anim.length)) >> 0; spr(x, y, w, h, frame * w, 0, 32, 32); } } } const ACTOR_TABLE = { robo: (s: ActorProps) => new Robo(s), }; export type ActorType = keyof typeof ACTOR_TABLE; export function is_actor_type(type: string): type is ActorType { return ACTOR_TABLE.hasOwnProperty(type); } export function spawn_actor(type: ActorType, props: ActorProps): Actor { return ACTOR_TABLE[type](props); }