import { KeyName } from "../applet/Keyboard"; import { DrawSet, Layer } from "../applet/Render"; import { ComponentSchema, Data as EcsData } from "../ecs/Components"; import { Component, copySparse, Join, StateForSchema, Store } from "../ecs/Data"; import { DumbMotion } from "../ecs/Location"; import { INPUT_FREQUENCY, LockstepProcessor } from "../ecs/Lockstep"; import { Buttons } from "./Input"; export enum GamePhase { TITLE, PLAYING, LOST, WON } export type RGB = [number, number, number]; export class World { width = 500; height = 400; /* * Core Game Status */ phase = GamePhase.TITLE; score = 0; constructor() { } /* * Drawing Layers */ groundLayer = new Layer(0); debugLayer = new Layer(2); bulletLayer = new Layer(10); playerLayer = new Layer(15); smokeLayer = new Layer(16); hudLayer = new Layer(20); bgColor: RGB = [255, 255, 255]; /** * Catch-all debug tool */ debug: Record = {}; drawHud(drawSet: DrawSet) { drawSet.queue(this.hudLayer.toRender((cx, dt) => { cx.font = "16px monospace"; cx.textAlign = "left"; cx.textBaseline = "middle"; const score = `Score: ${this.score}`; cx.fillStyle = "#000"; cx.fillText(score, this.width / 3 + 1, this.height - 18 + 1, this.width / 4); cx.fillStyle = "#0ff"; cx.fillText(score, this.width / 3, this.height - 18, this.width / 4); })); } } interface GameSchema extends ComponentSchema { boss: Boss; bullet: Bullet; hp: Hp; lifetime: Lifetime; message: Message; } export class Data extends EcsData implements StateForSchema { boss: Store; bullet: Store; hp: Store; lifetime: Store; message: Store; playerControl: Store; // globals debugLayer = new Layer(2); constructor(from: Partial) { super(from); this.boss = copySparse(from.boss); this.bullet = copySparse(from.bullet); this.hp = copySparse(from.hp); this.lifetime = copySparse(from.lifetime); this.message = copySparse(from.message); this.playerControl = copySparse(from.playerControl); } clone() { return new Data(this); } } export enum Teams { PLAYER, ENEMY } export class Bullet extends Component { hit: boolean; team: Teams; attack: number; constructor(from: Partial) { super(from); this.hit = from.hit ?? false; this.team = from.team ?? Teams.ENEMY; this.attack = from.attack ?? 1; } clone(): Bullet { return new Bullet(this); } } export class Hp extends Component { receivedDamage: number; team: Teams; hp: number; constructor(from: Partial) { super(from); this.receivedDamage = from.receivedDamage ?? 0; this.team = from.team ?? Teams.ENEMY; this.hp = from.hp ?? 10; } clone(): Hp { return new Hp(this); } } export class Lifetime extends Component { time: number; constructor(from: Partial & { time: number }) { super(from); this.time = from.time; } clone(): Lifetime { return new Lifetime(this); } } export class Boss extends Component { name: string; constructor(from: Partial) { super(from); this.name = from.name ?? ""; } clone(): Boss { return new Boss(this); } } export class Message extends Component { targetY = 0; layer: number; color: string; message: string; timeout = 3; constructor(from: Partial) { super(from); this.targetY = from.targetY ?? 0; this.layer = from.layer ?? 1; this.color = from.color ?? "#000"; this.message = from.message ?? ""; this.timeout = from.timeout ?? 3; } clone(): Message { return new Message(this); } } export class PlayerControl extends Component { playerNumber: number; constructor(from: Partial & {playerNumber: number}) { super(from); this.playerNumber = from.playerNumber; } clone(): PlayerControl { return new PlayerControl(this); } } export class Engine implements LockstepProcessor { cloneState(old: Data) { return new Data(old); } predictInput(prev: KeyName[][] | null, localPlayer: number, localInput: KeyName[]): KeyName[][] { return prev?.map((prevInput, player) => (player == localPlayer) ? localInput : prevInput) ?? []; } compareInput(a: KeyName[][], b: KeyName[][]): boolean { if (a.length != b.length) return false; for (let i = 0; i < a.length; i++) { if (a[i].length != b[i].length) return false; for (let j = 0; j < a[i].length; j++) { if (a[i][j] != b[i][j]) { return false; } } } return true; } advanceState(state: Data, input: KeyName[][]) { DumbMotion(state, INPUT_FREQUENCY); Join(state, "playerControl", "location").forEach(([player, location]) => { const playerInput = input[player.playerNumber]; if(playerInput) { let dir = 0; if (playerInput.indexOf("left") != -1) { dir -= 1; } if (playerInput.indexOf("right") != -1) { dir += 1; } location.VAngle = dir * 0.01; } }); } }