import { load_texture } from "./assets"; import { btn, Button } from "./input"; import { Vec2, new_v2, vadd, vsub, vnorm, vmul } from "./vector"; import { color, stroke, circle, spr, use_texture, Texture } from "./graphics"; import { has_collision, Level, World } from "./level"; import { log } from "./log"; 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; } pre_update(_world: World) {} post_update(_world: World) {} update(world: World) { this.pre_update(world); this.update_physics(world.current_level); this.post_update(world); } // 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; let xr = props.xr; let yr = props.yr; const steps = Math.ceil( (Math.abs(velocity.x) + Math.abs(velocity.y)) / 0.33 ); if (steps > 0) { for (let n = 0; n < steps; n++) { xr += velocity.x / steps; if (velocity.x != 0) { // Physics and whatnot. 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; } } while (xr > 1) { xr--; cx++; } while (xr < 0) { xr++; cx--; } yr += velocity.y / steps; if (velocity.y != 0) { 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; } } while (yr > 1) { cy += 1; yr -= 1; } while (yr < 0) { cy -= 1; yr += 1; } } } // 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; }); } pre_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)); } post_update(world: World) { const level = world.current_level; const props = this.props; const w = level.cw - 1; const h = level.ch - 1; log("robo position", "cx", props.cx, "cy", props.cy, "w", w, "h", h); let next_level = null; if (props.cx > w || (props.cx == w && props.xr > 0.8)) { const iid = level.neighbors.e; next_level = iid && world.level_map.get(iid); if (next_level) { props.cx = 0; props.xr = 0.2; } } else if (props.cy > h || (props.cy == h && props.yr > 0.8)) { const iid = level.neighbors.s; next_level = iid && world.level_map.get(iid); if (next_level) { props.cy = 0; props.yr = 0.2; } } else if (props.cx < 0 || (props.cx == 0 && props.xr < 0.2)) { const iid = level.neighbors.w; next_level = iid && world.level_map.get(iid); if (next_level) { props.cx = next_level.cw - 1; props.xr = 0.8; } } else if (props.cy < 0 || (props.cy == 0 && props.yr < 0.2)) { const iid = level.neighbors.n; next_level = iid && world.level_map.get(iid); if (next_level) { props.cy = next_level.ch - 1; props.yr = 0.8; } } if (next_level) { log("vwoop", next_level.iid); world.current_level = next_level; // Yikes. } } 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); color(0, 0, 0, 0); stroke(1, 0, 0, 1); circle(this.props.position.x, this.props.position.y, 8, 1); stroke(0, 1, 0, 1); circle(this.props.position.x, this.props.position.y, 16, 1); } } } 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); }