Try to keep XY locations integral
This commit is contained in:
parent
1ef5565be1
commit
76cde242bf
7 changed files with 70 additions and 49 deletions
|
@ -2,10 +2,22 @@
|
|||
import { Layer, SpriteSheet } from "../applet/Render";
|
||||
import { Component, copyDense, copySparse, EntityState, StateForSchema, Store } from "./Data";
|
||||
|
||||
/**
|
||||
* Nominal type used to remind users to enforce some level
|
||||
* of quantization on numbers kept in game state. This reduces
|
||||
* physics precision in theory, but lets us feel more comfortable
|
||||
* about determinism for rollback physics.
|
||||
*/
|
||||
export type FixedPoint = number & {_type: "fixed point"};
|
||||
|
||||
export function Floor(x: number): FixedPoint {
|
||||
return Math.floor(x) as FixedPoint;
|
||||
}
|
||||
|
||||
export class Box {
|
||||
constructor(
|
||||
public x: number, public y: number,
|
||||
public w: number, public h: number
|
||||
public x: FixedPoint, public y: FixedPoint,
|
||||
public w: FixedPoint, public h: FixedPoint
|
||||
) { };
|
||||
};
|
||||
|
||||
|
@ -25,10 +37,10 @@ export function Approach(source: number, target: number, speed: number): number
|
|||
* pairs of vertex coordinates in ccw winding order
|
||||
*/
|
||||
export interface Polygon {
|
||||
points: number[];
|
||||
points: FixedPoint[];
|
||||
}
|
||||
export class PolygonComponent extends Component<PolygonComponent> {
|
||||
points: number[];
|
||||
points: FixedPoint[];
|
||||
constructor(from: Partial<PolygonComponent>) {
|
||||
super(from);
|
||||
this.points = from.points?.slice() ?? []
|
||||
|
@ -39,16 +51,16 @@ export class PolygonComponent extends Component<PolygonComponent> {
|
|||
}
|
||||
|
||||
export class Location extends Component<Location> {
|
||||
X: number;
|
||||
Y: number;
|
||||
X: FixedPoint;
|
||||
Y: FixedPoint;
|
||||
Angle: number;
|
||||
VX: number;
|
||||
VY: number;
|
||||
VAngle: number;
|
||||
constructor(from: Partial<Location>) {
|
||||
super(from);
|
||||
this.X = from.X ?? 0;
|
||||
this.Y = from.Y ?? 0;
|
||||
this.X = from.X ?? (0 as FixedPoint);
|
||||
this.Y = from.Y ?? (0 as FixedPoint);
|
||||
this.Angle = from.Angle ?? 0;
|
||||
this.VX = from.VX ?? 0;
|
||||
this.VY = from.VY ?? 0;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Data, Location, Polygon } from "./Components";
|
||||
import { Data, Floor, Location, Polygon } from "./Components";
|
||||
import { Join } from "./Data";
|
||||
|
||||
export function TransformCx(cx: CanvasRenderingContext2D, location: Location, dt = 0) {
|
||||
|
@ -9,20 +9,20 @@ export function TransformCx(cx: CanvasRenderingContext2D, location: Location, dt
|
|||
export function TfPolygon({points}: Polygon, {X, Y, Angle}: Location): Polygon {
|
||||
const sin = Math.sin(Angle);
|
||||
const cos = Math.cos(Angle);
|
||||
const result = {points: new Array(points.length)};
|
||||
const result: Polygon = {points: new Array(points.length)};
|
||||
for(let i = 0; i < points.length; i += 2) {
|
||||
const x = points[i];
|
||||
const y = points[i+1];
|
||||
result.points[i] = x*cos - y*sin + X;
|
||||
result.points[i+1] = x*sin + y*cos + Y;
|
||||
result.points[i] = Floor(x*cos - y*sin + X);
|
||||
result.points[i+1] = Floor(x*sin + y*cos + Y);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function DumbMotion(data: Data, interval: number) {
|
||||
Join(data, "location").forEach(([location]) => {
|
||||
location.X += location.VX * interval;
|
||||
location.Y += location.VY * interval;
|
||||
location.X = Floor(location.X + location.VX * interval);
|
||||
location.Y = Floor(location.Y + location.VY * interval);
|
||||
location.Angle += location.VAngle * interval;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
CollisionClass,
|
||||
ComponentSchema,
|
||||
Data,
|
||||
FixedPoint,
|
||||
Location,
|
||||
PolygonComponent,
|
||||
RenderBounds,
|
||||
|
@ -164,11 +165,11 @@ class LoopTest {
|
|||
// spinner box
|
||||
Create(this.data, {
|
||||
location: new Location({
|
||||
X: 200,
|
||||
Y: 200,
|
||||
X: 200 as FixedPoint,
|
||||
Y: 200 as FixedPoint,
|
||||
VAngle: Math.PI
|
||||
}),
|
||||
bounds: new PolygonComponent({points: [-50, 50, -60, 250, 60, 250, 50, 50]}),
|
||||
bounds: new PolygonComponent({points: [-50, 50, -60, 250, 60, 250, 50, 50] as FixedPoint[]}),
|
||||
collisionTargetClass: new CollisionClass({ name: "block"}),
|
||||
renderBounds: new RenderBounds({color: "#0a0", layer: 0}),
|
||||
});
|
||||
|
@ -176,12 +177,12 @@ class LoopTest {
|
|||
// triangles
|
||||
[0, 1, 2, 3, 4, 5].forEach(angle => Create(this.data, {
|
||||
location: new Location({
|
||||
X: 200,
|
||||
Y: 200,
|
||||
X: 200 as FixedPoint,
|
||||
Y: 200 as FixedPoint,
|
||||
Angle: angle,
|
||||
VAngle: -Math.PI/10
|
||||
}),
|
||||
bounds: new PolygonComponent({points: [70, 0, 55, 40, 85, 40]}),
|
||||
bounds: new PolygonComponent({points: [70, 0, 55, 40, 85, 40] as FixedPoint[]}),
|
||||
collisionSourceClass: new CollisionClass({ name: "tri"}),
|
||||
renderBounds: new RenderBounds({
|
||||
color: "#d40",
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import { PlaySfx } from "../applet/Audio";
|
||||
import { Location, Polygon, PolygonComponent, RenderBounds } from "../ecs/Components";
|
||||
import {
|
||||
FixedPoint,
|
||||
Floor,
|
||||
Location,
|
||||
Polygon,
|
||||
PolygonComponent,
|
||||
RenderBounds,
|
||||
} from "../ecs/Components";
|
||||
import { Create, Id, Join, Lookup, Remove } from "../ecs/Data";
|
||||
import { Data, Lifetime, Teams, World } from "./GameComponents";
|
||||
|
||||
|
@ -39,20 +46,20 @@ export function CheckLifetime(data: Data, world: World, interval: number) {
|
|||
export function SmokeDamage(data: Data, world: World) {
|
||||
Join(data, "hp", "location").forEach(([hp, {X, Y}]) => {
|
||||
// convert dealt damage to particles
|
||||
const puffs = Math.floor(hp.receivedDamage / 3);
|
||||
SpawnBlast(data, world, X, Y, 2, "#000", puffs);
|
||||
hp.receivedDamage = Math.floor(hp.receivedDamage % 3);
|
||||
const puffs = Floor(hp.receivedDamage / 3);
|
||||
SpawnBlast(data, world, X, Y, 2 as FixedPoint, "#000", puffs);
|
||||
hp.receivedDamage = Floor(hp.receivedDamage % 3);
|
||||
});
|
||||
}
|
||||
|
||||
function SpawnBlast(data: Data, world: World, x: number, y: number, size: number, color: string, count: number) {
|
||||
function SpawnBlast(data: Data, world: World, x: FixedPoint, y: FixedPoint, size: FixedPoint, color: string, count: FixedPoint) {
|
||||
for(let puff = 0; puff < count; puff++) {
|
||||
const angle = Math.PI * 2 * puff / count;
|
||||
SpawnPuff(data, world, x, y, size, color, angle);
|
||||
}
|
||||
}
|
||||
|
||||
function SpawnPuff(data: Data, world: World, x: number, y: number, size: number, color: string, angle: number): Id {
|
||||
function SpawnPuff(data: Data, world: World, x: FixedPoint, y: FixedPoint, size: FixedPoint, color: string, angle: number): Id {
|
||||
return Create(data, {
|
||||
location: new Location({
|
||||
X: x,
|
||||
|
@ -65,7 +72,7 @@ function SpawnPuff(data: Data, world: World, x: number, y: number, size: number,
|
|||
-size, size,
|
||||
size, size,
|
||||
size, -size
|
||||
]}),
|
||||
] as FixedPoint[]}),
|
||||
renderBounds: new RenderBounds({
|
||||
color,
|
||||
// TODO: work out standard layers
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { KeyName } from "../applet/Keyboard";
|
||||
import { DrawSet, Layer } from "../applet/Render";
|
||||
import { ComponentSchema, Data as EcsData } from "../ecs/Components";
|
||||
import { ComponentSchema, Data as EcsData, FixedPoint } from "../ecs/Components";
|
||||
import { Component, copySparse, Join, StateForSchema, Store } from "../ecs/Data";
|
||||
import { DumbMotion } from "../ecs/Location";
|
||||
import { LockstepProcessor, TICK_LENGTH } from "../ecs/Lockstep";
|
||||
|
@ -14,8 +14,8 @@ export enum GamePhase {
|
|||
}
|
||||
export type RGB = [number, number, number];
|
||||
export class World {
|
||||
width = 500;
|
||||
height = 400;
|
||||
width = 500 as FixedPoint;
|
||||
height = 400 as FixedPoint;
|
||||
|
||||
/*
|
||||
* Core Game Status
|
||||
|
@ -147,14 +147,14 @@ export class Boss extends Component<Boss> {
|
|||
}
|
||||
|
||||
export class Message extends Component<Message> {
|
||||
targetY = 0;
|
||||
targetY = 0 as FixedPoint;
|
||||
layer: number;
|
||||
color: string;
|
||||
message: string;
|
||||
timeout = 3;
|
||||
constructor(from: Partial<Message>) {
|
||||
super(from);
|
||||
this.targetY = from.targetY ?? 0;
|
||||
this.targetY = from.targetY ?? (0 as FixedPoint);
|
||||
this.layer = from.layer ?? 1;
|
||||
this.color = from.color ?? "#000";
|
||||
this.message = from.message ?? "";
|
||||
|
@ -219,7 +219,7 @@ export class Engine implements LockstepProcessor<KeyName[], KeyName[][], Data> {
|
|||
if (playerInput.indexOf("right") != -1) {
|
||||
dir += 1;
|
||||
}
|
||||
location.VAngle = dir * 0.01;
|
||||
location.VAngle = (dir * 0.01) as FixedPoint;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import subscribe from "callbag-subscribe";
|
|||
|
||||
import { KeyControl, KeyName } from "../applet/Keyboard";
|
||||
import { DrawSet } from "../applet/Render";
|
||||
import { Location, PolygonComponent, RenderBounds } from "../ecs/Components";
|
||||
import { FixedPoint, Location, PolygonComponent, RenderBounds } from "../ecs/Components";
|
||||
import { Create } from "../ecs/Data";
|
||||
import { RunRenderBounds } from "../ecs/Renderers";
|
||||
import { LockstepClient, Server } from "../net/LockstepClient";
|
||||
|
@ -46,10 +46,10 @@ export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
|
|||
playerNumber: 0
|
||||
}),
|
||||
location: new Location({
|
||||
X: 100,
|
||||
Y: 200,
|
||||
X: 100 as FixedPoint,
|
||||
Y: 200 as FixedPoint,
|
||||
}),
|
||||
bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}),
|
||||
bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40] as FixedPoint[]}),
|
||||
renderBounds: new RenderBounds({
|
||||
color: "#a0f",
|
||||
layer: 0
|
||||
|
@ -61,10 +61,10 @@ export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
|
|||
playerNumber: 1
|
||||
}),
|
||||
location: new Location({
|
||||
X: 400,
|
||||
Y: 200,
|
||||
X: 400 as FixedPoint,
|
||||
Y: 200 as FixedPoint,
|
||||
}),
|
||||
bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40]}),
|
||||
bounds: new PolygonComponent({points: [-30, 0, 30, 0, 0, 40] as FixedPoint[]}),
|
||||
renderBounds: new RenderBounds({
|
||||
color: "#f0a",
|
||||
layer: 0
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { DrawSet } from "../applet/Render";
|
||||
import { Floor } from "../ecs/Components";
|
||||
import { Join, Remove } from "../ecs/Data";
|
||||
import { TransformCx } from "../ecs/Location";
|
||||
import { Data, GamePhase, World } from "./GameComponents";
|
||||
|
@ -22,23 +23,23 @@ export function ArrangeMessages(data: Data, world: World, interval: number) {
|
|||
const messages = Join(data, "message", "location");
|
||||
messages.sort(([{timeout: timeoutA}, {}], [{timeout: timeoutB}, {}]) => timeoutA - timeoutB);
|
||||
|
||||
let y = world.height / 3;
|
||||
let y = Floor(world.height / 3);
|
||||
messages.forEach(([message, location]) => {
|
||||
message.targetY = y;
|
||||
y += ADVANCE;
|
||||
y = Floor(y + ADVANCE);
|
||||
|
||||
const delta = message.targetY - location.Y;
|
||||
if(Math.abs(delta) < 100 * interval) {
|
||||
location.Y = message.targetY;
|
||||
location.VY = 0;
|
||||
location.VY = Floor(0);
|
||||
} else {
|
||||
location.VY = Math.sign(delta) * 100;
|
||||
location.VY = Floor(Math.sign(delta) * 100);
|
||||
}
|
||||
|
||||
if(location.X >= world.width / 2 && message.timeout >= 0) {
|
||||
location.X = world.width / 2;
|
||||
location.X = Floor(world.width / 2);
|
||||
message.timeout -= interval;
|
||||
location.VX = 0;
|
||||
location.VX = Floor(0);
|
||||
} else if(world.phase == GamePhase.PLAYING) {
|
||||
location.VX = world.width;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue