2020-05-10 22:47:03 +00:00
|
|
|
import { KeyName } from "../applet/Keyboard";
|
|
|
|
import { DrawSet, Layer } from "../applet/Render";
|
2020-12-31 19:55:36 +00:00
|
|
|
import { ComponentSchema, Data as EcsData, FixedPoint } from "../ecs/Components";
|
2020-05-10 22:47:03 +00:00
|
|
|
import { Component, copySparse, Join, StateForSchema, Store } from "../ecs/Data";
|
|
|
|
import { DumbMotion } from "../ecs/Location";
|
2020-06-07 05:56:06 +00:00
|
|
|
import { LockstepProcessor, TICK_LENGTH } from "../ecs/Lockstep";
|
2020-02-16 05:00:26 +00:00
|
|
|
import { Buttons } from "./Input";
|
2019-12-14 23:11:00 +00:00
|
|
|
|
|
|
|
export enum GamePhase {
|
|
|
|
TITLE,
|
|
|
|
PLAYING,
|
|
|
|
LOST,
|
|
|
|
WON
|
|
|
|
}
|
|
|
|
export type RGB = [number, number, number];
|
|
|
|
export class World {
|
2020-12-31 19:55:36 +00:00
|
|
|
width = 500 as FixedPoint;
|
|
|
|
height = 400 as FixedPoint;
|
2019-12-14 23:11:00 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Core Game Status
|
|
|
|
*/
|
|
|
|
phase = GamePhase.TITLE;
|
|
|
|
score = 0;
|
|
|
|
|
2020-05-02 01:45:31 +00:00
|
|
|
constructor() { }
|
2019-12-14 23:11:00 +00:00
|
|
|
|
|
|
|
/*
|
|
|
|
* 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<string, any> = {};
|
|
|
|
|
|
|
|
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";
|
2020-05-02 01:45:31 +00:00
|
|
|
cx.fillText(score, this.width / 3 + 1, this.height - 18 + 1, this.width / 4);
|
2019-12-14 23:11:00 +00:00
|
|
|
cx.fillStyle = "#0ff";
|
2020-05-02 01:45:31 +00:00
|
|
|
cx.fillText(score, this.width / 3, this.height - 18, this.width / 4);
|
2019-12-14 23:11:00 +00:00
|
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-04 22:52:25 +00:00
|
|
|
interface GameSchema extends ComponentSchema {
|
|
|
|
boss: Boss;
|
|
|
|
bullet: Bullet;
|
|
|
|
hp: Hp;
|
|
|
|
lifetime: Lifetime;
|
|
|
|
message: Message;
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Data extends EcsData implements StateForSchema<GameSchema> {
|
|
|
|
boss: Store<Boss>;
|
|
|
|
bullet: Store<Bullet>;
|
|
|
|
hp: Store<Hp>;
|
|
|
|
lifetime: Store<Lifetime>;
|
|
|
|
message: Store<Message>;
|
2020-05-02 01:45:31 +00:00
|
|
|
playerControl: Store<PlayerControl>;
|
2020-04-04 22:52:25 +00:00
|
|
|
|
|
|
|
// globals
|
|
|
|
debugLayer = new Layer(2);
|
|
|
|
|
|
|
|
constructor(from: Partial<Data>) {
|
|
|
|
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);
|
2020-05-02 01:45:31 +00:00
|
|
|
this.playerControl = copySparse(from.playerControl);
|
2020-04-04 22:52:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
clone() {
|
|
|
|
return new Data(this);
|
2020-02-16 04:17:13 +00:00
|
|
|
}
|
2019-12-14 23:11:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export enum Teams {
|
|
|
|
PLAYER,
|
|
|
|
ENEMY
|
|
|
|
}
|
2020-04-04 22:52:25 +00:00
|
|
|
export class Bullet extends Component<Bullet> {
|
|
|
|
hit: boolean;
|
|
|
|
team: Teams;
|
|
|
|
attack: number;
|
|
|
|
constructor(from: Partial<Bullet>) {
|
|
|
|
super(from);
|
|
|
|
this.hit = from.hit ?? false;
|
|
|
|
this.team = from.team ?? Teams.ENEMY;
|
|
|
|
this.attack = from.attack ?? 1;
|
|
|
|
}
|
|
|
|
clone(): Bullet {
|
|
|
|
return new Bullet(this);
|
|
|
|
}
|
2019-12-14 23:11:00 +00:00
|
|
|
}
|
2020-04-04 22:52:25 +00:00
|
|
|
export class Hp extends Component<Hp> {
|
|
|
|
receivedDamage: number;
|
|
|
|
team: Teams;
|
|
|
|
hp: number;
|
|
|
|
constructor(from: Partial<Hp>) {
|
|
|
|
super(from);
|
|
|
|
this.receivedDamage = from.receivedDamage ?? 0;
|
|
|
|
this.team = from.team ?? Teams.ENEMY;
|
|
|
|
this.hp = from.hp ?? 10;
|
|
|
|
}
|
|
|
|
clone(): Hp {
|
|
|
|
return new Hp(this);
|
|
|
|
}
|
2019-12-14 23:11:00 +00:00
|
|
|
}
|
|
|
|
|
2020-04-04 22:52:25 +00:00
|
|
|
export class Lifetime extends Component<Lifetime> {
|
|
|
|
time: number;
|
2020-05-02 01:45:31 +00:00
|
|
|
constructor(from: Partial<Lifetime> & { time: number }) {
|
2020-04-04 22:52:25 +00:00
|
|
|
super(from);
|
|
|
|
this.time = from.time;
|
|
|
|
}
|
|
|
|
clone(): Lifetime {
|
|
|
|
return new Lifetime(this);
|
|
|
|
}
|
2019-12-14 23:11:00 +00:00
|
|
|
}
|
|
|
|
|
2020-04-04 22:52:25 +00:00
|
|
|
export class Boss extends Component<Boss> {
|
|
|
|
name: string;
|
|
|
|
constructor(from: Partial<Boss>) {
|
|
|
|
super(from);
|
|
|
|
this.name = from.name ?? "";
|
|
|
|
}
|
|
|
|
clone(): Boss {
|
|
|
|
return new Boss(this);
|
|
|
|
}
|
2019-12-14 23:11:00 +00:00
|
|
|
}
|
|
|
|
|
2020-04-04 22:52:25 +00:00
|
|
|
export class Message extends Component<Message> {
|
2020-12-31 19:55:36 +00:00
|
|
|
targetY = 0 as FixedPoint;
|
2020-04-04 22:52:25 +00:00
|
|
|
layer: number;
|
|
|
|
color: string;
|
|
|
|
message: string;
|
|
|
|
timeout = 3;
|
|
|
|
constructor(from: Partial<Message>) {
|
|
|
|
super(from);
|
2020-12-31 19:55:36 +00:00
|
|
|
this.targetY = from.targetY ?? (0 as FixedPoint);
|
2020-04-04 22:52:25 +00:00
|
|
|
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);
|
|
|
|
}
|
2019-12-14 23:11:00 +00:00
|
|
|
}
|
2020-02-16 05:00:26 +00:00
|
|
|
|
2020-05-02 01:45:31 +00:00
|
|
|
export class PlayerControl extends Component<PlayerControl> {
|
|
|
|
playerNumber: number;
|
|
|
|
constructor(from: Partial<PlayerControl> & {playerNumber: number}) {
|
|
|
|
super(from);
|
|
|
|
this.playerNumber = from.playerNumber;
|
|
|
|
}
|
|
|
|
clone(): PlayerControl {
|
|
|
|
return new PlayerControl(this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-02 02:51:21 +00:00
|
|
|
export class Engine implements LockstepProcessor<KeyName[], KeyName[][], Data> {
|
2020-02-16 05:00:26 +00:00
|
|
|
cloneState(old: Data) {
|
|
|
|
return new Data(old);
|
|
|
|
}
|
|
|
|
|
2020-05-18 23:32:56 +00:00
|
|
|
predictInput(prev: KeyName[][] = [], localPlayer: number, localInput: KeyName[]): KeyName[][] {
|
|
|
|
const prediction = prev.slice();
|
|
|
|
for(let i = 0; i <= localPlayer; i++) {
|
|
|
|
if(!prediction[i]) {
|
|
|
|
prediction[i] = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
prediction[localPlayer] = localInput;
|
|
|
|
return prediction;
|
2020-05-02 01:45:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
compareInput(a: KeyName[][], b: KeyName[][]): boolean {
|
2020-02-16 05:00:26 +00:00
|
|
|
if (a.length != b.length) return false;
|
|
|
|
|
2020-05-02 01:45:31 +00:00
|
|
|
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;
|
|
|
|
}
|
2020-02-16 05:00:26 +00:00
|
|
|
}
|
2020-05-02 01:45:31 +00:00
|
|
|
}
|
2020-02-16 05:00:26 +00:00
|
|
|
|
2020-05-02 01:45:31 +00:00
|
|
|
return true;
|
2020-02-16 05:00:26 +00:00
|
|
|
}
|
|
|
|
|
2020-05-02 01:45:31 +00:00
|
|
|
advanceState(state: Data, input: KeyName[][]) {
|
2020-06-07 05:56:06 +00:00
|
|
|
DumbMotion(state, TICK_LENGTH);
|
2020-05-02 01:45:31 +00:00
|
|
|
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;
|
|
|
|
}
|
2020-12-31 19:55:36 +00:00
|
|
|
location.VAngle = (dir * 0.01) as FixedPoint;
|
2020-03-26 04:29:24 +00:00
|
|
|
}
|
|
|
|
});
|
2020-02-16 05:00:26 +00:00
|
|
|
}
|
|
|
|
}
|