Modernise & make fun #3
27 changed files with 275 additions and 226 deletions
|
|
@ -46,5 +46,5 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
CMD node -e "require('http').get('http://localhost:3000/state',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
|
CMD node -e "require('http').get('http://localhost:3000/state',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
|
||||||
|
|
||||||
ENTRYPOINT ["node", "dist/main.js"]
|
ENTRYPOINT ["node", "dist/main.js"]
|
||||||
# Override these to tune the server, e.g. `--name`, `--playerLimit`, `--worldSize`.
|
# Override these to tune the server, e.g. `--name`, `--playerLimit`, `--scoreLimit`.
|
||||||
CMD ["--port", "3000"]
|
CMD ["--port", "3000"]
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ import { PhysicalContainer } from './physics/containers/physical-container';
|
||||||
import { evaluateSdf } from './physics/functions/evaluate-sdf';
|
import { evaluateSdf } from './physics/functions/evaluate-sdf';
|
||||||
import { Physical } from './physics/physicals/physical';
|
import { Physical } from './physics/physicals/physical';
|
||||||
|
|
||||||
export const createWorld = (objectContainer: PhysicalContainer, worldRadius: number) => {
|
export const createWorld = (objectContainer: PhysicalContainer) => {
|
||||||
const objects: Array<Physical> = [];
|
const objects: Array<Physical> = [];
|
||||||
const lights: Array<Physical> = [];
|
const lights: Array<Physical> = [];
|
||||||
|
|
||||||
for (let r = 0; r < worldRadius; r += settings.radiusSteps) {
|
for (let r = 0; r < settings.worldRadius; r += settings.radiusSteps) {
|
||||||
const circumference = 2 * Math.PI * r;
|
const circumference = 2 * Math.PI * r;
|
||||||
const stepCount = circumference * settings.objectsOnCircleLength;
|
const stepCount = circumference * settings.objectsOnCircleLength;
|
||||||
for (let rad = 0; rad < 2 * Math.PI; rad += (2 * Math.PI) / stepCount) {
|
for (let rad = 0; rad < 2 * Math.PI; rad += (2 * Math.PI) / stepCount) {
|
||||||
|
|
@ -67,8 +67,29 @@ export const createWorld = (objectContainer: PhysicalContainer, worldRadius: num
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.info('Generated planet count', objects.length);
|
console.info(`Generated ${objects.length} planets`);
|
||||||
console.info('Generated light count', lights.length);
|
console.info(`Generated ${lights.length} light`);
|
||||||
|
|
||||||
|
// Associate each lamp with its NEAREST planet, so a planet can repaint "its"
|
||||||
|
// lamps to the owning team's colour when it flips. Lamps are already placed by
|
||||||
|
// proximity during world-gen, so the nearest planet is the one whose capture
|
||||||
|
// they should advertise. Distances use the planet SDF (negative inside), which
|
||||||
|
// is exactly the "closest planet" metric we want.
|
||||||
|
const planets = objects.filter((o): o is PlanetPhysical => o instanceof PlanetPhysical);
|
||||||
|
lights
|
||||||
|
.filter((l): l is LampPhysical => l instanceof LampPhysical)
|
||||||
|
.forEach((lamp) => {
|
||||||
|
let nearest: PlanetPhysical | undefined;
|
||||||
|
let nearestDistance = Infinity;
|
||||||
|
planets.forEach((planet) => {
|
||||||
|
const distance = planet.distance(lamp.center);
|
||||||
|
if (distance < nearestDistance) {
|
||||||
|
nearestDistance = distance;
|
||||||
|
nearest = planet;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
nearest?.addLamp(lamp);
|
||||||
|
});
|
||||||
|
|
||||||
[...objects, ...lights].forEach((o) => objectContainer.addObject(o));
|
[...objects, ...lights].forEach((o) => objectContainer.addObject(o));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ export const defaultOptions: Options = {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
name: 'Test server',
|
name: 'Test server',
|
||||||
playerLimit: 16,
|
playerLimit: 16,
|
||||||
npcCount: 16,
|
npcCount: 8,
|
||||||
seed: Math.random(),
|
seed: Math.random(),
|
||||||
scoreLimit: 1000,
|
scoreLimit: 2500,
|
||||||
worldSize: 8000,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export class GameServer extends CommandReceiver {
|
||||||
private initialize() {
|
private initialize() {
|
||||||
const previousPlayers = this.players;
|
const previousPlayers = this.players;
|
||||||
this.objects = new PhysicalContainer();
|
this.objects = new PhysicalContainer();
|
||||||
createWorld(this.objects, this.options.worldSize);
|
createWorld(this.objects);
|
||||||
this.objects.initialize();
|
this.objects.initialize();
|
||||||
this.players = new PlayerContainer(
|
this.players = new PlayerContainer(
|
||||||
this.objects,
|
this.objects,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ export class LampPhysical extends LampBase implements StaticPhysical {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public queueSetLight(color: vec3, lightness: number) {
|
||||||
|
this.remoteCall('setLight', color, lightness);
|
||||||
|
}
|
||||||
|
|
||||||
public distance(target: vec2): number {
|
public distance(target: vec2): number {
|
||||||
return vec2.distance(this.center, target);
|
return vec2.distance(this.center, target);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,13 @@ export class ProjectilePhysical extends ProjectileBase implements DynamicPhysica
|
||||||
return !this.isDestroyed;
|
return !this.isDestroyed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get direction(): vec2 {
|
||||||
|
const direction = vec2.clone(this.velocity);
|
||||||
|
return vec2.length(direction) > 0
|
||||||
|
? vec2.normalize(direction, direction)
|
||||||
|
: vec2.fromValues(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
private moveOutsideOfObject() {
|
private moveOutsideOfObject() {
|
||||||
let wasCollision = true;
|
let wasCollision = true;
|
||||||
const delta = vec2.scale(
|
const delta = vec2.scale(
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,4 @@ export interface Options {
|
||||||
scoreLimit: number;
|
scoreLimit: number;
|
||||||
npcCount: number;
|
npcCount: number;
|
||||||
seed: number;
|
seed: number;
|
||||||
worldSize: number;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
import { vec2 } from 'gl-matrix';
|
||||||
import { CommandReceiver, Circle, PlayerInformation, CharacterTeam } from 'shared';
|
import {
|
||||||
|
CommandReceiver,
|
||||||
|
Circle,
|
||||||
|
PlayerInformation,
|
||||||
|
CharacterTeam,
|
||||||
|
Random,
|
||||||
|
settings,
|
||||||
|
} from 'shared';
|
||||||
import { PhysicalContainer } from '../physics/containers/physical-container';
|
import { PhysicalContainer } from '../physics/containers/physical-container';
|
||||||
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
import { getBoundingBoxOfCircle } from '../physics/functions/get-bounding-box-of-circle';
|
||||||
import { isCircleIntersecting } from '../physics/functions/is-circle-intersecting';
|
import { isCircleIntersecting } from '../physics/functions/is-circle-intersecting';
|
||||||
import { CharacterPhysical } from '../objects/character-physical';
|
import { CharacterPhysical } from '../objects/character-physical';
|
||||||
|
import { PlanetPhysical } from '../objects/planet-physical';
|
||||||
import { PlayerContainer } from './player-container';
|
import { PlayerContainer } from './player-container';
|
||||||
|
|
||||||
export abstract class PlayerBase extends CommandReceiver {
|
export abstract class PlayerBase extends CommandReceiver {
|
||||||
|
|
@ -22,14 +30,14 @@ export abstract class PlayerBase extends CommandReceiver {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createCharacter(preferredCenter: vec2) {
|
protected createCharacter() {
|
||||||
this.character = new CharacterPhysical(
|
this.character = new CharacterPhysical(
|
||||||
this.playerInfo.name.slice(0, 20),
|
this.playerInfo.name.slice(0, 20),
|
||||||
this.sumKills,
|
this.sumKills,
|
||||||
this.sumDeaths,
|
this.sumDeaths,
|
||||||
this.team,
|
this.team,
|
||||||
this.objectContainer,
|
this.objectContainer,
|
||||||
this.findEmptyPositionForPlayer(preferredCenter),
|
this.findEmptyPositionForPlayer(this.findSpawnCenter()),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.objectContainer.addObject(this.character);
|
this.objectContainer.addObject(this.character);
|
||||||
|
|
@ -37,11 +45,34 @@ export abstract class PlayerBase extends CommandReceiver {
|
||||||
|
|
||||||
public abstract step(deltaTimeInSeconds: number): void;
|
public abstract step(deltaTimeInSeconds: number): void;
|
||||||
|
|
||||||
protected findEmptyPositionForPlayer(preferredCenter: vec2): vec2 {
|
private findSpawnCenter(): vec2 {
|
||||||
if (!preferredCenter) {
|
const planets = this.objectContainer
|
||||||
preferredCenter = vec2.create();
|
.findIntersecting(
|
||||||
|
getBoundingBoxOfCircle(new Circle(vec2.create(), settings.worldRadius * 2)),
|
||||||
|
)
|
||||||
|
.filter((o): o is PlanetPhysical => o instanceof PlanetPhysical);
|
||||||
|
|
||||||
|
const friendly = planets.filter((p) => p.team === this.team);
|
||||||
|
const neutral = planets.filter((p) => p.team === CharacterTeam.neutral);
|
||||||
|
const candidates = friendly.length ? friendly : neutral.length ? neutral : planets;
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
return vec2.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isContested = (planet: PlanetPhysical) =>
|
||||||
|
this.playerContainer.players.some(
|
||||||
|
(p) =>
|
||||||
|
p.team !== this.team &&
|
||||||
|
p.character?.isAlive &&
|
||||||
|
vec2.distance(p.center, planet.center) < settings.spawnSafetyDistance,
|
||||||
|
);
|
||||||
|
const safe = candidates.filter((p) => !isContested(p));
|
||||||
|
|
||||||
|
// candidates is non-empty here, so choose() always returns a planet.
|
||||||
|
return vec2.clone(Random.choose(safe.length ? safe : candidates)!.center);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected findEmptyPositionForPlayer(preferredCenter: vec2): vec2 {
|
||||||
let rotation = 0;
|
let rotation = 0;
|
||||||
let radius = 0;
|
let radius = 0;
|
||||||
for (; ;) {
|
for (; ;) {
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,8 @@ export class Player extends PlayerBase {
|
||||||
(this.aspectRatio = v.aspectRatio),
|
(this.aspectRatio = v.aspectRatio),
|
||||||
[MoveActionCommand.type]: (c: MoveActionCommand) =>
|
[MoveActionCommand.type]: (c: MoveActionCommand) =>
|
||||||
this.character?.handleMovementAction(c),
|
this.character?.handleMovementAction(c),
|
||||||
[PrimaryActionCommand.type]: (c: PrimaryActionCommand) => {
|
[PrimaryActionCommand.type]: (c: PrimaryActionCommand) =>
|
||||||
this.character?.shootTowards(c.position);
|
this.character?.shootTowards(c.position, c.charge),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -60,11 +59,7 @@ export class Player extends PlayerBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createCharacter() {
|
protected createCharacter() {
|
||||||
const preferredCenter = this.playerContainer.players.find(
|
super.createCharacter();
|
||||||
(p) => p.character?.isAlive && p.team === this.team,
|
|
||||||
)?.center;
|
|
||||||
|
|
||||||
super.createCharacter(preferredCenter ?? vec2.create());
|
|
||||||
|
|
||||||
this.objectsPreviouslyInViewArea.push(this.character!);
|
this.objectsPreviouslyInViewArea.push(this.character!);
|
||||||
this.queueCommandSend(new CreatePlayerCommand(this.character!));
|
this.queueCommandSend(new CreatePlayerCommand(this.character!));
|
||||||
|
|
@ -206,8 +201,4 @@ export class Player extends PlayerBase {
|
||||||
this.queueCommandSend(new ServerAnnouncement(announcement));
|
this.queueCommandSend(new ServerAnnouncement(announcement));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
|
||||||
super.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { Command, GameObject } from 'shared';
|
|
||||||
|
|
||||||
export class ReactToCollisionCommand extends Command {
|
|
||||||
public constructor(public readonly other: GameObject) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
47
frontend/package-lock.json
generated
47
frontend/package-lock.json
generated
|
|
@ -23,7 +23,7 @@
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sass": "^1.100.0",
|
"sass": "^1.100.0",
|
||||||
"sass-loader": "^17.0.0",
|
"sass-loader": "^17.0.0",
|
||||||
"sdf-2d": "^0.7.6",
|
"sdf-2d": "file:../../sdf-2d",
|
||||||
"shared": "file:../shared",
|
"shared": "file:../shared",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"socket.io-msgpack-parser": "^3.0.2",
|
"socket.io-msgpack-parser": "^3.0.2",
|
||||||
|
|
@ -36,6 +36,40 @@
|
||||||
"webpack-dev-server": "^5.2.4"
|
"webpack-dev-server": "^5.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../sdf-2d": {
|
||||||
|
"version": "0.7.6",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"gl-matrix": "^3.4.4",
|
||||||
|
"resize-observer-polyfill": "^1.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
||||||
|
"@typescript-eslint/parser": "^8.60.1",
|
||||||
|
"eslint": "^10.4.1",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
|
"eslint-plugin-unused-imports": "^4.4.1",
|
||||||
|
"globals": "^17.6.0",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
|
"terser-webpack-plugin": "^5.6.1",
|
||||||
|
"ts-loader": "^9.6.0",
|
||||||
|
"typedoc": "^0.28.19",
|
||||||
|
"typedoc-plugin-extras": "^4.0.1",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
"webpack": "^5.107.2",
|
||||||
|
"webpack-cli": "^7.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../sdf-2d/dist": {
|
||||||
|
"extraneous": true
|
||||||
|
},
|
||||||
|
"../../sdf-2d/lib": {
|
||||||
|
"extraneous": true
|
||||||
|
},
|
||||||
"../shared": {
|
"../shared": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
@ -5409,15 +5443,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sdf-2d": {
|
"node_modules/sdf-2d": {
|
||||||
"version": "0.7.6",
|
"resolved": "../../sdf-2d",
|
||||||
"resolved": "https://registry.npmjs.org/sdf-2d/-/sdf-2d-0.7.6.tgz",
|
"link": true
|
||||||
"integrity": "sha512-aBJUjwYjWP+/fSvLHH6vhpFyWeSJNCSEHpE0XBX69s2bQlo/NOUr3nq/KuhRZ6A3FeXrGU4lzyurHJ1N8f4rFg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"gl-matrix": "^3.3.0",
|
|
||||||
"resize-observer-polyfill": "^1.5.1"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/select-hose": {
|
"node_modules/select-hose": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sass": "^1.100.0",
|
"sass": "^1.100.0",
|
||||||
"sass-loader": "^17.0.0",
|
"sass-loader": "^17.0.0",
|
||||||
"sdf-2d": "^0.7.6",
|
"sdf-2d": "^0.8.0",
|
||||||
"shared": "file:../shared",
|
"shared": "file:../shared",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"socket.io-msgpack-parser": "^3.0.2",
|
"socket.io-msgpack-parser": "^3.0.2",
|
||||||
|
|
|
||||||
58
frontend/src/scripts/charge-indicator.ts
Normal file
58
frontend/src/scripts/charge-indicator.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { holdDurationToCharge } from 'shared';
|
||||||
|
import { Pointer } from './helper/pointer';
|
||||||
|
|
||||||
|
|
||||||
|
export abstract class ChargeIndicator {
|
||||||
|
private static element?: HTMLElement;
|
||||||
|
private static heldSince = 0;
|
||||||
|
private static raf = 0;
|
||||||
|
private static followPointer = false;
|
||||||
|
|
||||||
|
public static begin(x: number, y: number, followPointer = false) {
|
||||||
|
this.end();
|
||||||
|
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.className = 'charge-ring';
|
||||||
|
element.style.left = `${x}px`;
|
||||||
|
element.style.top = `${y}px`;
|
||||||
|
document.body.appendChild(element);
|
||||||
|
|
||||||
|
this.element = element;
|
||||||
|
this.heldSince = performance.now();
|
||||||
|
this.followPointer = followPointer;
|
||||||
|
this.raf = requestAnimationFrame(this.update);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static end() {
|
||||||
|
if (this.element) {
|
||||||
|
cancelAnimationFrame(this.raf);
|
||||||
|
this.element.parentElement?.removeChild(this.element);
|
||||||
|
this.element = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static update = () => {
|
||||||
|
const element = ChargeIndicator.element;
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const charge = holdDurationToCharge(
|
||||||
|
(performance.now() - ChargeIndicator.heldSince) / 1000,
|
||||||
|
);
|
||||||
|
element.style.opacity = charge < 0.12 ? '0' : '1';
|
||||||
|
element.style.background = `conic-gradient(rgba(255, 255, 255, 0.85) ${charge * 360
|
||||||
|
}deg, rgba(255, 255, 255, 0.15) 0deg)`;
|
||||||
|
element.classList.toggle('full', charge >= 1);
|
||||||
|
|
||||||
|
if (ChargeIndicator.followPointer) {
|
||||||
|
const cursor = Pointer.getDisplayPosition();
|
||||||
|
if (cursor) {
|
||||||
|
element.style.left = `${cursor.x}px`;
|
||||||
|
element.style.top = `${cursor.y}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChargeIndicator.raf = requestAnimationFrame(ChargeIndicator.update);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -30,10 +30,12 @@ import { CommandSocket } from './commands/command-socket';
|
||||||
import { PlayerDecision } from './join-form-handler';
|
import { PlayerDecision } from './join-form-handler';
|
||||||
import { GameObjectContainer } from './objects/game-object-container';
|
import { GameObjectContainer } from './objects/game-object-container';
|
||||||
import parser from 'socket.io-msgpack-parser';
|
import parser from 'socket.io-msgpack-parser';
|
||||||
import { BlobShape } from './shapes/blob-shape';
|
import { CharacterShape } from './shapes/character-shape';
|
||||||
import { PlanetShape } from './shapes/planet-shape';
|
import { PlanetShape } from './shapes/planet-shape';
|
||||||
import { RenderCommand } from './commands/types/render';
|
import { RenderCommand } from './commands/types/render';
|
||||||
import { StepCommand } from './commands/types/step';
|
import { StepCommand } from './commands/types/step';
|
||||||
|
import { Tutorial } from './tutorial';
|
||||||
|
import { Scoreboard } from './scoreboard';
|
||||||
|
|
||||||
export class Game extends CommandReceiver {
|
export class Game extends CommandReceiver {
|
||||||
public gameObjects = new GameObjectContainer(this);
|
public gameObjects = new GameObjectContainer(this);
|
||||||
|
|
@ -48,12 +50,11 @@ export class Game extends CommandReceiver {
|
||||||
private mouseListener: MouseListener;
|
private mouseListener: MouseListener;
|
||||||
private touchListener: TouchListener;
|
private touchListener: TouchListener;
|
||||||
|
|
||||||
private declaPlanetCountElement = document.createElement('div');
|
private scoreboard = new Scoreboard();
|
||||||
private redPlanetCountElement = document.createElement('div');
|
|
||||||
private announcementText = document.createElement('h2');
|
private announcementText = document.createElement('h2');
|
||||||
private progressBar = document.createElement('div');
|
|
||||||
private arrows: { [id: number]: HTMLElement } = {};
|
private arrows: { [id: number]: HTMLElement } = {};
|
||||||
private socketReceiver!: CommandSocket;
|
private socketReceiver!: CommandSocket;
|
||||||
|
private tutorial!: Tutorial;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly playerDecision: PlayerDecision,
|
private readonly playerDecision: PlayerDecision,
|
||||||
|
|
@ -63,9 +64,6 @@ export class Game extends CommandReceiver {
|
||||||
super();
|
super();
|
||||||
this.started = new Promise((r) => (this.resolveStarted = r));
|
this.started = new Promise((r) => (this.resolveStarted = r));
|
||||||
this.announcementText.className = 'announcement';
|
this.announcementText.className = 'announcement';
|
||||||
this.progressBar.className = 'planet-progress';
|
|
||||||
this.progressBar.appendChild(this.declaPlanetCountElement);
|
|
||||||
this.progressBar.appendChild(this.redPlanetCountElement);
|
|
||||||
|
|
||||||
this.keyboardListener = new KeyboardListener();
|
this.keyboardListener = new KeyboardListener();
|
||||||
this.mouseListener = new MouseListener(this.canvas, this);
|
this.mouseListener = new MouseListener(this.canvas, this);
|
||||||
|
|
@ -78,12 +76,14 @@ export class Game extends CommandReceiver {
|
||||||
this.socket?.close();
|
this.socket?.close();
|
||||||
this.gameObjects = new GameObjectContainer(this);
|
this.gameObjects = new GameObjectContainer(this);
|
||||||
this.overlay.innerHTML = '';
|
this.overlay.innerHTML = '';
|
||||||
|
this.arrows = {};
|
||||||
this.isEnding = false;
|
this.isEnding = false;
|
||||||
this.lastAnnouncementText = '';
|
this.lastAnnouncementText = '';
|
||||||
this.overlay.appendChild(this.progressBar);
|
this.overlay.appendChild(this.scoreboard.element);
|
||||||
this.announcementText.innerText = '';
|
this.announcementText.innerText = '';
|
||||||
this.timeScaling = 1;
|
this.timeScaling = 1;
|
||||||
this.overlay.appendChild(this.announcementText);
|
this.overlay.appendChild(this.announcementText);
|
||||||
|
this.tutorial = new Tutorial(this.overlay);
|
||||||
|
|
||||||
this.socket = io(this.playerDecision.server, {
|
this.socket = io(this.playerDecision.server, {
|
||||||
reconnectionDelayMax: 10000,
|
reconnectionDelayMax: 10000,
|
||||||
|
|
@ -114,12 +114,17 @@ export class Game extends CommandReceiver {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socketReceiver = new CommandSocket(this.socket);
|
this.socketReceiver = new CommandSocket(this.socket);
|
||||||
|
// The tutorial listens to the same input streams as the socket, so its
|
||||||
|
// stages clear off the player's own commands without any server involvement.
|
||||||
this.keyboardListener.clearSubscribers();
|
this.keyboardListener.clearSubscribers();
|
||||||
this.keyboardListener.subscribe(this.socketReceiver);
|
this.keyboardListener.subscribe(this.socketReceiver);
|
||||||
|
this.keyboardListener.subscribe(this.tutorial);
|
||||||
this.mouseListener.clearSubscribers();
|
this.mouseListener.clearSubscribers();
|
||||||
this.mouseListener.subscribe(this.socketReceiver);
|
this.mouseListener.subscribe(this.socketReceiver);
|
||||||
|
this.mouseListener.subscribe(this.tutorial);
|
||||||
this.touchListener.clearSubscribers();
|
this.touchListener.clearSubscribers();
|
||||||
this.touchListener.subscribe(this.socketReceiver);
|
this.touchListener.subscribe(this.socketReceiver);
|
||||||
|
this.touchListener.subscribe(this.tutorial);
|
||||||
|
|
||||||
this.isBetweenGames = false;
|
this.isBetweenGames = false;
|
||||||
|
|
||||||
|
|
@ -189,8 +194,7 @@ export class Game extends CommandReceiver {
|
||||||
clamp(p.x, arrowPadding, width - arrowPadding),
|
clamp(p.x, arrowPadding, width - arrowPadding),
|
||||||
clamp(height - p.y, arrowPadding, height - arrowPadding),
|
clamp(height - p.y, arrowPadding, height - arrowPadding),
|
||||||
);
|
);
|
||||||
e.style.transform = `translateX(${p.x}px) translateY(${
|
e.style.transform = `translateX(${p.x}px) translateY(${p.y
|
||||||
p.y
|
|
||||||
}px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
|
}px) translateX(-50%) translateY(-50%) rotate(${-angle + Math.PI / 2}rad) `;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -214,7 +218,7 @@ export class Game extends CommandReceiver {
|
||||||
this.canvas,
|
this.canvas,
|
||||||
[
|
[
|
||||||
PlanetShape.descriptor,
|
PlanetShape.descriptor,
|
||||||
BlobShape.descriptor,
|
CharacterShape.descriptor,
|
||||||
{
|
{
|
||||||
...CircleLight.descriptor,
|
...CircleLight.descriptor,
|
||||||
shaderCombinationSteps: [0, 1, 2, 4, 8, 16],
|
shaderCombinationSteps: [0, 1, 2, 4, 8, 16],
|
||||||
|
|
@ -223,10 +227,11 @@ export class Game extends CommandReceiver {
|
||||||
this.gameLoop.bind(this),
|
this.gameLoop.bind(this),
|
||||||
{
|
{
|
||||||
shadowTraceCount: 16,
|
shadowTraceCount: 16,
|
||||||
paletteSize: settings.palette.length,
|
paletteSize: settings.paletteDim.length,
|
||||||
colorPalette: settings.palette,
|
colorPalette: settings.paletteDim,
|
||||||
enableHighDpiRendering: true,
|
enableHighDpiRendering: true,
|
||||||
lightCutoffDistance: settings.lightCutoffDistance,
|
lightCutoffDistance: settings.lightCutoffDistance,
|
||||||
|
lightOverlapReduction: settings.lightOverlapReduction,
|
||||||
textures: {
|
textures: {
|
||||||
noiseTexture: {
|
noiseTexture: {
|
||||||
source: noiseTexture,
|
source: noiseTexture,
|
||||||
|
|
@ -276,7 +281,9 @@ export class Game extends CommandReceiver {
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((this.timeSinceLastAnnouncement += deltaTime) > 0.5) {
|
if (
|
||||||
|
(this.timeSinceLastAnnouncement += deltaTime) > settings.announcementVisibleSeconds
|
||||||
|
) {
|
||||||
this.lastAnnouncementText = '';
|
this.lastAnnouncementText = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,6 +299,10 @@ export class Game extends CommandReceiver {
|
||||||
new RenderCommand(this.renderer, this.overlay, shouldChangeLayout),
|
new RenderCommand(this.renderer, this.overlay, shouldChangeLayout),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.touchListener.update(deltaTime);
|
||||||
|
|
||||||
|
this.tutorial.step(this.gameObjects);
|
||||||
|
|
||||||
this.socketReceiver.sendQueuedCommands();
|
this.socketReceiver.sendQueuedCommands();
|
||||||
|
|
||||||
return this.isActive;
|
return this.isActive;
|
||||||
|
|
@ -299,10 +310,8 @@ export class Game extends CommandReceiver {
|
||||||
|
|
||||||
private draw() {
|
private draw() {
|
||||||
if (this.lastGameState) {
|
if (this.lastGameState) {
|
||||||
this.declaPlanetCountElement.style.width =
|
// The local player's team is read off the main character once it exists.
|
||||||
(this.lastGameState.declaCount / this.lastGameState.limit) * 50 + '%';
|
this.scoreboard.update(this.lastGameState, this.gameObjects.player?.team);
|
||||||
this.redPlanetCountElement.style.width =
|
|
||||||
(this.lastGameState.redCount / this.lastGameState.limit) * 50 + '%';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.lastOtherPlayerDirections) {
|
if (this.lastOtherPlayerDirections) {
|
||||||
|
|
|
||||||
13
frontend/src/scripts/helper/pointer.ts
Normal file
13
frontend/src/scripts/helper/pointer.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { vec2 } from 'gl-matrix';
|
||||||
|
|
||||||
|
export class Pointer {
|
||||||
|
private static displayPosition: vec2 | null = null;
|
||||||
|
|
||||||
|
public static setDisplayPosition(x: number, y: number): void {
|
||||||
|
Pointer.displayPosition = vec2.fromValues(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getDisplayPosition(): vec2 | null {
|
||||||
|
return Pointer.displayPosition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -102,7 +102,7 @@ export class JoinFormHandler {
|
||||||
|
|
||||||
private removeServer(server: ServerChooserOption) {
|
private removeServer(server: ServerChooserOption) {
|
||||||
this.servers = this.servers.filter((s) => s !== server);
|
this.servers = this.servers.filter((s) => s !== server);
|
||||||
if (this.servers.length) {
|
if (!this.servers.length) {
|
||||||
this.joinButton.disabled = true;
|
this.joinButton.disabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,16 +143,17 @@ class ServerChooserOption {
|
||||||
this.setServerInfoLabelText();
|
this.setServerInfoLabelText();
|
||||||
|
|
||||||
this.socket = io(url, {
|
this.socket = io(url, {
|
||||||
reconnection: false,
|
reconnection: true,
|
||||||
|
reconnectionAttempts: 5,
|
||||||
timeout: 4000,
|
timeout: 4000,
|
||||||
parser,
|
parser,
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
// `connect_timeout` was removed in socket.io-client v3+; connection
|
this.socket.io.on('reconnect_failed', this.destroy.bind(this));
|
||||||
// timeouts now surface through `connect_error` (reconnection is disabled).
|
|
||||||
this.socket.on('connect_error', this.destroy.bind(this));
|
this.socket.on('connect', () =>
|
||||||
this.socket.on('disconnect', this.destroy.bind(this));
|
this.socket.emit(TransportEvents.SubscribeForServerInfoUpdates),
|
||||||
this.socket.emit(TransportEvents.SubscribeForServerInfoUpdates);
|
);
|
||||||
this.socket.on(
|
this.socket.on(
|
||||||
TransportEvents.ServerInfoUpdate,
|
TransportEvents.ServerInfoUpdate,
|
||||||
([playerCount, gameState]: [number, number]) => {
|
([playerCount, gameState]: [number, number]) => {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,36 @@
|
||||||
import { vec2, vec3 } from 'gl-matrix';
|
import { vec2, vec3 } from 'gl-matrix';
|
||||||
import { CircleLight } from 'sdf-2d';
|
import { CircleLight } from 'sdf-2d';
|
||||||
import { CommandExecutors, Id, LampBase } from 'shared';
|
import { CommandExecutors, Id, LampBase, mixRgb, settings } from 'shared';
|
||||||
import { RenderCommand } from '../../commands/types/render';
|
import { RenderCommand } from '../../commands/types/render';
|
||||||
|
import { StepCommand } from '../../commands/types/step';
|
||||||
|
|
||||||
export class LampView extends LampBase {
|
export class LampView extends LampBase {
|
||||||
private light: CircleLight;
|
private light: CircleLight;
|
||||||
|
|
||||||
|
private targetColor: vec3;
|
||||||
|
private targetLightness: number;
|
||||||
|
|
||||||
protected commandExecutors: CommandExecutors = {
|
protected commandExecutors: CommandExecutors = {
|
||||||
[RenderCommand.type]: this.draw.bind(this),
|
[RenderCommand.type]: this.draw.bind(this),
|
||||||
|
[StepCommand.type]: this.step.bind(this),
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(id: Id, center: vec2, color: vec3, lightness: number) {
|
constructor(id: Id, center: vec2, color: vec3, lightness: number) {
|
||||||
super(id, center, color, lightness);
|
super(id, center, color, lightness);
|
||||||
this.light = new CircleLight(center, color, lightness);
|
this.light = new CircleLight(vec2.clone(center), vec3.clone(color), lightness);
|
||||||
|
this.targetColor = vec3.clone(color);
|
||||||
|
this.targetLightness = lightness;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLight(color: vec3, lightness: number) {
|
||||||
|
this.targetColor = vec3.clone(color);
|
||||||
|
this.targetLightness = lightness;
|
||||||
|
}
|
||||||
|
|
||||||
|
private step({ deltaTimeInSeconds }: StepCommand): void {
|
||||||
|
const t = 1 - Math.exp(-deltaTimeInSeconds / settings.lampLerpSeconds);
|
||||||
|
this.light.color = mixRgb(this.light.color, this.targetColor, t);
|
||||||
|
this.light.intensity += (this.targetLightness - this.light.intensity) * t;
|
||||||
}
|
}
|
||||||
|
|
||||||
private draw({ renderer }: RenderCommand): void {
|
private draw({ renderer }: RenderCommand): void {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,10 @@ export class ProjectileView extends ProjectileBase {
|
||||||
|
|
||||||
this.center = this.centerExtrapolator.getValue(deltaTimeInSeconds);
|
this.center = this.centerExtrapolator.getValue(deltaTimeInSeconds);
|
||||||
this.light.center = this.center;
|
this.light.center = this.center;
|
||||||
this.light.intensity = (0.15 * this.strength) / settings.projectileMaxStrength;
|
this.light.intensity = Math.min(
|
||||||
|
0.1,
|
||||||
|
(0.15 * this.strength) / settings.projectileMaxStrength,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private draw({ renderer }: RenderCommand): void {
|
private draw({ renderer }: RenderCommand): void {
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import { mat2d, vec2 } from 'gl-matrix';
|
|
||||||
import { Drawable, DrawableDescriptor } from 'sdf-2d';
|
|
||||||
import { Circle } from 'shared';
|
|
||||||
|
|
||||||
export class BlobShape extends Drawable {
|
|
||||||
public static descriptor: DrawableDescriptor = {
|
|
||||||
sdf: {
|
|
||||||
shader: `
|
|
||||||
uniform vec2 headCenters[BLOB_COUNT];
|
|
||||||
uniform vec2 leftFootCenters[BLOB_COUNT];
|
|
||||||
uniform vec2 rightFootCenters[BLOB_COUNT];
|
|
||||||
uniform float headRadii[BLOB_COUNT];
|
|
||||||
uniform float footRadii[BLOB_COUNT];
|
|
||||||
uniform int blobColors[BLOB_COUNT];
|
|
||||||
|
|
||||||
float blobSmoothMin(float a, float b)
|
|
||||||
{
|
|
||||||
const highp float k = 300.0;
|
|
||||||
highp float res = exp2(-k * a) + exp2(-k * b);
|
|
||||||
return -log2(res) / k;
|
|
||||||
}
|
|
||||||
|
|
||||||
float circleDistance(vec2 circleCenter, float radius, vec2 target) {
|
|
||||||
return distance(target, circleCenter) - radius;
|
|
||||||
}
|
|
||||||
|
|
||||||
float blobMinDistance(vec2 target, out vec4 color) {
|
|
||||||
float minDistance = 1000.0;
|
|
||||||
|
|
||||||
for (int i = 0; i < BLOB_COUNT; i++) {
|
|
||||||
float headDistance = circleDistance(headCenters[i], headRadii[i], target);
|
|
||||||
float leftFootDistance = circleDistance(leftFootCenters[i], footRadii[i], target);
|
|
||||||
float rightFootDistance = circleDistance(rightFootCenters[i], footRadii[i], target);
|
|
||||||
|
|
||||||
float res = min(
|
|
||||||
blobSmoothMin(headDistance, leftFootDistance),
|
|
||||||
blobSmoothMin(headDistance, rightFootDistance)
|
|
||||||
);
|
|
||||||
|
|
||||||
vec2 leftEyeOffset = vec2(-headRadii[i] * 0.35, headRadii[i] * 0.2);
|
|
||||||
vec2 rightEyeOffset = vec2(headRadii[i] * 0.35, headRadii[i] * 0.2);
|
|
||||||
|
|
||||||
float eyeDistance = min(
|
|
||||||
circleDistance(headCenters[i] + rightEyeOffset, headRadii[i] * 0.25, target),
|
|
||||||
circleDistance(headCenters[i] + leftEyeOffset, headRadii[i] * 0.25, target)
|
|
||||||
);
|
|
||||||
|
|
||||||
eyeDistance = max(
|
|
||||||
eyeDistance,
|
|
||||||
-circleDistance(headCenters[i] + leftEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.2, target)
|
|
||||||
);
|
|
||||||
|
|
||||||
eyeDistance = max(
|
|
||||||
eyeDistance,
|
|
||||||
-circleDistance(headCenters[i] + rightEyeOffset + vec2(0, -headRadii[i] * 0.175), headRadii[i] * 0.2, target)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (res < minDistance) {
|
|
||||||
minDistance = res;
|
|
||||||
color = eyeDistance < 0.0 ? vec4(1.0) : readFromPalette(blobColors[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return minDistance;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
distanceFunctionName: 'blobMinDistance',
|
|
||||||
},
|
|
||||||
propertyUniformMapping: {
|
|
||||||
footRadius: 'footRadii',
|
|
||||||
headRadius: 'headRadii',
|
|
||||||
rightFootCenter: 'rightFootCenters',
|
|
||||||
leftFootCenter: 'leftFootCenters',
|
|
||||||
headCenter: 'headCenters',
|
|
||||||
color: 'blobColors',
|
|
||||||
},
|
|
||||||
uniformCountMacroName: 'BLOB_COUNT',
|
|
||||||
shaderCombinationSteps: [0, 1, 2, 8],
|
|
||||||
empty: new BlobShape(0),
|
|
||||||
};
|
|
||||||
|
|
||||||
protected head!: Circle;
|
|
||||||
protected leftFoot!: Circle;
|
|
||||||
protected rightFoot!: Circle;
|
|
||||||
|
|
||||||
public constructor(private readonly color: number) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
const circle = new Circle(vec2.create(), 200);
|
|
||||||
this.setCircles([circle, circle, circle]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setCircles([head, leftFoot, rightFoot]: [Circle, Circle, Circle]) {
|
|
||||||
this.head = head;
|
|
||||||
this.leftFoot = leftFoot;
|
|
||||||
this.rightFoot = rightFoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public minDistance(target: vec2): number {
|
|
||||||
return Math.min(
|
|
||||||
this.head.distance(target),
|
|
||||||
this.leftFoot.distance(target),
|
|
||||||
this.rightFoot.distance(target),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any {
|
|
||||||
return {
|
|
||||||
headCenter: vec2.transformMat2d(vec2.create(), this.head.center, transform2d),
|
|
||||||
leftFootCenter: vec2.transformMat2d(
|
|
||||||
vec2.create(),
|
|
||||||
this.leftFoot.center,
|
|
||||||
transform2d,
|
|
||||||
),
|
|
||||||
rightFootCenter: vec2.transformMat2d(
|
|
||||||
vec2.create(),
|
|
||||||
this.rightFoot.center,
|
|
||||||
transform2d,
|
|
||||||
),
|
|
||||||
headRadius: this.head.radius * transform1d,
|
|
||||||
footRadius: this.leftFoot.radius * transform1d,
|
|
||||||
color: this.color,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -137,12 +137,6 @@ module.exports = (env, argv) => {
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.ts', '.js', '.json'],
|
extensions: ['.ts', '.js', '.json'],
|
||||||
alias: {
|
|
||||||
// sdf-2d's package.json `exports` only declares an `import` condition,
|
|
||||||
// which webpack 5 cannot resolve for the production (require) build.
|
|
||||||
// Point straight at its entry to bypass package exports resolution.
|
|
||||||
'sdf-2d$': path.resolve(__dirname, 'node_modules/sdf-2d/lib/main.js'),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,14 @@ import { Command } from '../../command';
|
||||||
|
|
||||||
@serializable
|
@serializable
|
||||||
export class PrimaryActionCommand extends Command {
|
export class PrimaryActionCommand extends Command {
|
||||||
public constructor(public readonly position: vec2) {
|
public constructor(
|
||||||
|
public readonly position: vec2,
|
||||||
|
public readonly charge: number = 0,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
return [this.position];
|
return [this.position, this.charge];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { vec2 } from 'gl-matrix';
|
|
||||||
import { serializable } from '../../../serialization/serializable';
|
|
||||||
import { Command } from '../../command';
|
|
||||||
|
|
||||||
@serializable
|
|
||||||
export class SecondaryActionCommand extends Command {
|
|
||||||
public constructor(public readonly position: vec2) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
|
||||||
return [this.position];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
shared/src/helper/charge.ts
Normal file
5
shared/src/helper/charge.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { clamp01 } from './clamp';
|
||||||
|
import { settings } from '../settings';
|
||||||
|
|
||||||
|
export const holdDurationToCharge = (heldSeconds: number): number =>
|
||||||
|
clamp01(heldSeconds / settings.chargeShotFullHoldSeconds);
|
||||||
|
|
@ -16,7 +16,6 @@ export * from './commands/command-generator';
|
||||||
export * from './commands/types/actions/move-action';
|
export * from './commands/types/actions/move-action';
|
||||||
export * from './commands/types/actions/primary-action';
|
export * from './commands/types/actions/primary-action';
|
||||||
export * from './commands/types/actions/set-aspect-ratio-action';
|
export * from './commands/types/actions/set-aspect-ratio-action';
|
||||||
export * from './commands/types/actions/secondary-action';
|
|
||||||
export * from './helper/array';
|
export * from './helper/array';
|
||||||
export * from './helper/last';
|
export * from './helper/last';
|
||||||
export * from './helper/rgb';
|
export * from './helper/rgb';
|
||||||
|
|
@ -24,6 +23,7 @@ export * from './helper/hsl';
|
||||||
export * from './helper/rgb255';
|
export * from './helper/rgb255';
|
||||||
export * from './helper/mix-rgb';
|
export * from './helper/mix-rgb';
|
||||||
export * from './helper/clamp';
|
export * from './helper/clamp';
|
||||||
|
export * from './helper/charge';
|
||||||
export * from './helper/calculate-view-area';
|
export * from './helper/calculate-view-area';
|
||||||
export * from './helper/circle';
|
export * from './helper/circle';
|
||||||
export * from './helper/rectangle';
|
export * from './helper/rectangle';
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ export class CharacterBase extends GameObject {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public onShoot(strength: number) { }
|
public onShoot(strength: number) { }
|
||||||
|
|
||||||
|
public onHitConfirmed() { }
|
||||||
|
|
||||||
|
public onKillConfirmed(victimName?: string, streak?: number) { }
|
||||||
|
|
||||||
public setHealth(health: number) {
|
public setHealth(health: number) {
|
||||||
this.health = health;
|
this.health = health;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ export class LampBase extends GameObject {
|
||||||
super(id);
|
super(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overridden by LampView (which lerps toward the new colour/brightness); a
|
||||||
|
// no-op on the wire base.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public setLight(color: vec3, lightness: number) {}
|
||||||
|
|
||||||
public toArray(): Array<any> {
|
public toArray(): Array<any> {
|
||||||
const { id, center, color, lightness } = this;
|
const { id, center, color, lightness } = this;
|
||||||
return [id, center, color, lightness];
|
return [id, center, color, lightness];
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { settings } from '../../settings';
|
||||||
import { serializable } from '../../serialization/serializable';
|
import { serializable } from '../../serialization/serializable';
|
||||||
import { GameObject } from '../game-object';
|
import { GameObject } from '../game-object';
|
||||||
import { Id } from '../../communication/id';
|
import { Id } from '../../communication/id';
|
||||||
|
import { CharacterTeam } from './character-base';
|
||||||
|
|
||||||
@serializable
|
@serializable
|
||||||
export class PlanetBase extends GameObject {
|
export class PlanetBase extends GameObject {
|
||||||
|
|
@ -25,6 +26,8 @@ export class PlanetBase extends GameObject {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
public generatedPoints(value: number) {}
|
public generatedPoints(value: number) {}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
public onFlipped(team: CharacterTeam) {}
|
||||||
|
|
||||||
public static createPlanetVertices(
|
public static createPlanetVertices(
|
||||||
center: vec2,
|
center: vec2,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue