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,11 +2,23 @@
|
||||||
import { Layer, SpriteSheet } from "../applet/Render";
|
import { Layer, SpriteSheet } from "../applet/Render";
|
||||||
import { Component, copyDense, copySparse, EntityState, StateForSchema, Store } from "./Data";
|
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 {
|
export class Box {
|
||||||
constructor(
|
constructor(
|
||||||
public x: number, public y: number,
|
public x: FixedPoint, public y: FixedPoint,
|
||||||
public w: number, public h: number
|
public w: FixedPoint, public h: FixedPoint
|
||||||
) {};
|
) { };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,7 +26,7 @@ export class Box {
|
||||||
*/
|
*/
|
||||||
export function Approach(source: number, target: number, speed: number): number {
|
export function Approach(source: number, target: number, speed: number): number {
|
||||||
const delta = target - source;
|
const delta = target - source;
|
||||||
if(Math.abs(delta) <= speed) {
|
if (Math.abs(delta) <= speed) {
|
||||||
return target;
|
return target;
|
||||||
} else {
|
} else {
|
||||||
return source + Math.sign(delta) * speed;
|
return source + Math.sign(delta) * speed;
|
||||||
|
@ -25,10 +37,10 @@ export function Approach(source: number, target: number, speed: number): number
|
||||||
* pairs of vertex coordinates in ccw winding order
|
* pairs of vertex coordinates in ccw winding order
|
||||||
*/
|
*/
|
||||||
export interface Polygon {
|
export interface Polygon {
|
||||||
points: number[];
|
points: FixedPoint[];
|
||||||
}
|
}
|
||||||
export class PolygonComponent extends Component<PolygonComponent> {
|
export class PolygonComponent extends Component<PolygonComponent> {
|
||||||
points: number[];
|
points: FixedPoint[];
|
||||||
constructor(from: Partial<PolygonComponent>) {
|
constructor(from: Partial<PolygonComponent>) {
|
||||||
super(from);
|
super(from);
|
||||||
this.points = from.points?.slice() ?? []
|
this.points = from.points?.slice() ?? []
|
||||||
|
@ -39,16 +51,16 @@ export class PolygonComponent extends Component<PolygonComponent> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Location extends Component<Location> {
|
export class Location extends Component<Location> {
|
||||||
X: number;
|
X: FixedPoint;
|
||||||
Y: number;
|
Y: FixedPoint;
|
||||||
Angle: number;
|
Angle: number;
|
||||||
VX: number;
|
VX: number;
|
||||||
VY: number;
|
VY: number;
|
||||||
VAngle: number;
|
VAngle: number;
|
||||||
constructor(from: Partial<Location>) {
|
constructor(from: Partial<Location>) {
|
||||||
super(from);
|
super(from);
|
||||||
this.X = from.X ?? 0;
|
this.X = from.X ?? (0 as FixedPoint);
|
||||||
this.Y = from.Y ?? 0;
|
this.Y = from.Y ?? (0 as FixedPoint);
|
||||||
this.Angle = from.Angle ?? 0;
|
this.Angle = from.Angle ?? 0;
|
||||||
this.VX = from.VX ?? 0;
|
this.VX = from.VX ?? 0;
|
||||||
this.VY = from.VY ?? 0;
|
this.VY = from.VY ?? 0;
|
||||||
|
@ -90,7 +102,7 @@ export class RenderSprite extends Component<RenderSprite> {
|
||||||
public index: number;
|
public index: number;
|
||||||
public offsetX: number;
|
public offsetX: number;
|
||||||
public offsetY: number;
|
public offsetY: number;
|
||||||
constructor(from: Partial<RenderSprite> & {sheet: SpriteSheet}) {
|
constructor(from: Partial<RenderSprite> & { sheet: SpriteSheet }) {
|
||||||
super(from);
|
super(from);
|
||||||
this.sheet = from.sheet;
|
this.sheet = from.sheet;
|
||||||
this.layer = from.layer ?? 1;
|
this.layer = from.layer ?? 1;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Data, Location, Polygon } from "./Components";
|
import { Data, Floor, Location, Polygon } from "./Components";
|
||||||
import { Join } from "./Data";
|
import { Join } from "./Data";
|
||||||
|
|
||||||
export function TransformCx(cx: CanvasRenderingContext2D, location: Location, dt = 0) {
|
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 {
|
export function TfPolygon({points}: Polygon, {X, Y, Angle}: Location): Polygon {
|
||||||
const sin = Math.sin(Angle);
|
const sin = Math.sin(Angle);
|
||||||
const cos = Math.cos(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) {
|
for(let i = 0; i < points.length; i += 2) {
|
||||||
const x = points[i];
|
const x = points[i];
|
||||||
const y = points[i+1];
|
const y = points[i+1];
|
||||||
result.points[i] = x*cos - y*sin + X;
|
result.points[i] = Floor(x*cos - y*sin + X);
|
||||||
result.points[i+1] = x*sin + y*cos + Y;
|
result.points[i+1] = Floor(x*sin + y*cos + Y);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DumbMotion(data: Data, interval: number) {
|
export function DumbMotion(data: Data, interval: number) {
|
||||||
Join(data, "location").forEach(([location]) => {
|
Join(data, "location").forEach(([location]) => {
|
||||||
location.X += location.VX * interval;
|
location.X = Floor(location.X + location.VX * interval);
|
||||||
location.Y += location.VY * interval;
|
location.Y = Floor(location.Y + location.VY * interval);
|
||||||
location.Angle += location.VAngle * interval;
|
location.Angle += location.VAngle * interval;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
CollisionClass,
|
CollisionClass,
|
||||||
ComponentSchema,
|
ComponentSchema,
|
||||||
Data,
|
Data,
|
||||||
|
FixedPoint,
|
||||||
Location,
|
Location,
|
||||||
PolygonComponent,
|
PolygonComponent,
|
||||||
RenderBounds,
|
RenderBounds,
|
||||||
|
@ -164,11 +165,11 @@ class LoopTest {
|
||||||
// spinner box
|
// spinner box
|
||||||
Create(this.data, {
|
Create(this.data, {
|
||||||
location: new Location({
|
location: new Location({
|
||||||
X: 200,
|
X: 200 as FixedPoint,
|
||||||
Y: 200,
|
Y: 200 as FixedPoint,
|
||||||
VAngle: Math.PI
|
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"}),
|
collisionTargetClass: new CollisionClass({ name: "block"}),
|
||||||
renderBounds: new RenderBounds({color: "#0a0", layer: 0}),
|
renderBounds: new RenderBounds({color: "#0a0", layer: 0}),
|
||||||
});
|
});
|
||||||
|
@ -176,12 +177,12 @@ class LoopTest {
|
||||||
// triangles
|
// triangles
|
||||||
[0, 1, 2, 3, 4, 5].forEach(angle => Create(this.data, {
|
[0, 1, 2, 3, 4, 5].forEach(angle => Create(this.data, {
|
||||||
location: new Location({
|
location: new Location({
|
||||||
X: 200,
|
X: 200 as FixedPoint,
|
||||||
Y: 200,
|
Y: 200 as FixedPoint,
|
||||||
Angle: angle,
|
Angle: angle,
|
||||||
VAngle: -Math.PI/10
|
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"}),
|
collisionSourceClass: new CollisionClass({ name: "tri"}),
|
||||||
renderBounds: new RenderBounds({
|
renderBounds: new RenderBounds({
|
||||||
color: "#d40",
|
color: "#d40",
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import { PlaySfx } from "../applet/Audio";
|
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 { Create, Id, Join, Lookup, Remove } from "../ecs/Data";
|
||||||
import { Data, Lifetime, Teams, World } from "./GameComponents";
|
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) {
|
export function SmokeDamage(data: Data, world: World) {
|
||||||
Join(data, "hp", "location").forEach(([hp, {X, Y}]) => {
|
Join(data, "hp", "location").forEach(([hp, {X, Y}]) => {
|
||||||
// convert dealt damage to particles
|
// convert dealt damage to particles
|
||||||
const puffs = Math.floor(hp.receivedDamage / 3);
|
const puffs = Floor(hp.receivedDamage / 3);
|
||||||
SpawnBlast(data, world, X, Y, 2, "#000", puffs);
|
SpawnBlast(data, world, X, Y, 2 as FixedPoint, "#000", puffs);
|
||||||
hp.receivedDamage = Math.floor(hp.receivedDamage % 3);
|
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++) {
|
for(let puff = 0; puff < count; puff++) {
|
||||||
const angle = Math.PI * 2 * puff / count;
|
const angle = Math.PI * 2 * puff / count;
|
||||||
SpawnPuff(data, world, x, y, size, color, angle);
|
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, {
|
return Create(data, {
|
||||||
location: new Location({
|
location: new Location({
|
||||||
X: x,
|
X: x,
|
||||||
|
@ -65,7 +72,7 @@ function SpawnPuff(data: Data, world: World, x: number, y: number, size: number,
|
||||||
-size, size,
|
-size, size,
|
||||||
size, size,
|
size, size,
|
||||||
size, -size
|
size, -size
|
||||||
]}),
|
] as FixedPoint[]}),
|
||||||
renderBounds: new RenderBounds({
|
renderBounds: new RenderBounds({
|
||||||
color,
|
color,
|
||||||
// TODO: work out standard layers
|
// TODO: work out standard layers
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { KeyName } from "../applet/Keyboard";
|
import { KeyName } from "../applet/Keyboard";
|
||||||
import { DrawSet, Layer } from "../applet/Render";
|
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 { Component, copySparse, Join, StateForSchema, Store } from "../ecs/Data";
|
||||||
import { DumbMotion } from "../ecs/Location";
|
import { DumbMotion } from "../ecs/Location";
|
||||||
import { LockstepProcessor, TICK_LENGTH } from "../ecs/Lockstep";
|
import { LockstepProcessor, TICK_LENGTH } from "../ecs/Lockstep";
|
||||||
|
@ -14,8 +14,8 @@ export enum GamePhase {
|
||||||
}
|
}
|
||||||
export type RGB = [number, number, number];
|
export type RGB = [number, number, number];
|
||||||
export class World {
|
export class World {
|
||||||
width = 500;
|
width = 500 as FixedPoint;
|
||||||
height = 400;
|
height = 400 as FixedPoint;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Core Game Status
|
* Core Game Status
|
||||||
|
@ -147,14 +147,14 @@ export class Boss extends Component<Boss> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Message extends Component<Message> {
|
export class Message extends Component<Message> {
|
||||||
targetY = 0;
|
targetY = 0 as FixedPoint;
|
||||||
layer: number;
|
layer: number;
|
||||||
color: string;
|
color: string;
|
||||||
message: string;
|
message: string;
|
||||||
timeout = 3;
|
timeout = 3;
|
||||||
constructor(from: Partial<Message>) {
|
constructor(from: Partial<Message>) {
|
||||||
super(from);
|
super(from);
|
||||||
this.targetY = from.targetY ?? 0;
|
this.targetY = from.targetY ?? (0 as FixedPoint);
|
||||||
this.layer = from.layer ?? 1;
|
this.layer = from.layer ?? 1;
|
||||||
this.color = from.color ?? "#000";
|
this.color = from.color ?? "#000";
|
||||||
this.message = from.message ?? "";
|
this.message = from.message ?? "";
|
||||||
|
@ -219,7 +219,7 @@ export class Engine implements LockstepProcessor<KeyName[], KeyName[][], Data> {
|
||||||
if (playerInput.indexOf("right") != -1) {
|
if (playerInput.indexOf("right") != -1) {
|
||||||
dir += 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 { KeyControl, KeyName } from "../applet/Keyboard";
|
||||||
import { DrawSet } from "../applet/Render";
|
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 { Create } from "../ecs/Data";
|
||||||
import { RunRenderBounds } from "../ecs/Renderers";
|
import { RunRenderBounds } from "../ecs/Renderers";
|
||||||
import { LockstepClient, Server } from "../net/LockstepClient";
|
import { LockstepClient, Server } from "../net/LockstepClient";
|
||||||
|
@ -46,10 +46,10 @@ export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
|
||||||
playerNumber: 0
|
playerNumber: 0
|
||||||
}),
|
}),
|
||||||
location: new Location({
|
location: new Location({
|
||||||
X: 100,
|
X: 100 as FixedPoint,
|
||||||
Y: 200,
|
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({
|
renderBounds: new RenderBounds({
|
||||||
color: "#a0f",
|
color: "#a0f",
|
||||||
layer: 0
|
layer: 0
|
||||||
|
@ -61,10 +61,10 @@ export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
|
||||||
playerNumber: 1
|
playerNumber: 1
|
||||||
}),
|
}),
|
||||||
location: new Location({
|
location: new Location({
|
||||||
X: 400,
|
X: 400 as FixedPoint,
|
||||||
Y: 200,
|
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({
|
renderBounds: new RenderBounds({
|
||||||
color: "#f0a",
|
color: "#f0a",
|
||||||
layer: 0
|
layer: 0
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { DrawSet } from "../applet/Render";
|
import { DrawSet } from "../applet/Render";
|
||||||
|
import { Floor } from "../ecs/Components";
|
||||||
import { Join, Remove } from "../ecs/Data";
|
import { Join, Remove } from "../ecs/Data";
|
||||||
import { TransformCx } from "../ecs/Location";
|
import { TransformCx } from "../ecs/Location";
|
||||||
import { Data, GamePhase, World } from "./GameComponents";
|
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");
|
const messages = Join(data, "message", "location");
|
||||||
messages.sort(([{timeout: timeoutA}, {}], [{timeout: timeoutB}, {}]) => timeoutA - timeoutB);
|
messages.sort(([{timeout: timeoutA}, {}], [{timeout: timeoutB}, {}]) => timeoutA - timeoutB);
|
||||||
|
|
||||||
let y = world.height / 3;
|
let y = Floor(world.height / 3);
|
||||||
messages.forEach(([message, location]) => {
|
messages.forEach(([message, location]) => {
|
||||||
message.targetY = y;
|
message.targetY = y;
|
||||||
y += ADVANCE;
|
y = Floor(y + ADVANCE);
|
||||||
|
|
||||||
const delta = message.targetY - location.Y;
|
const delta = message.targetY - location.Y;
|
||||||
if(Math.abs(delta) < 100 * interval) {
|
if(Math.abs(delta) < 100 * interval) {
|
||||||
location.Y = message.targetY;
|
location.Y = message.targetY;
|
||||||
location.VY = 0;
|
location.VY = Floor(0);
|
||||||
} else {
|
} else {
|
||||||
location.VY = Math.sign(delta) * 100;
|
location.VY = Floor(Math.sign(delta) * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(location.X >= world.width / 2 && message.timeout >= 0) {
|
if(location.X >= world.width / 2 && message.timeout >= 0) {
|
||||||
location.X = world.width / 2;
|
location.X = Floor(world.width / 2);
|
||||||
message.timeout -= interval;
|
message.timeout -= interval;
|
||||||
location.VX = 0;
|
location.VX = Floor(0);
|
||||||
} else if(world.phase == GamePhase.PLAYING) {
|
} else if(world.phase == GamePhase.PLAYING) {
|
||||||
location.VX = world.width;
|
location.VX = world.width;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue