From 5555b5bee99009abb5c0969d20b6b88147322d18 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 26 Jun 2024 01:04:33 +0200 Subject: [PATCH 01/29] Moved ConeIndicator to a separate file --- packages/client/src/debug/ConeIndicator.tsx | 27 +++++++++++++++++++++ packages/client/src/gfx/Unit3D.tsx | 25 +------------------ 2 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 packages/client/src/debug/ConeIndicator.tsx diff --git a/packages/client/src/debug/ConeIndicator.tsx b/packages/client/src/debug/ConeIndicator.tsx new file mode 100644 index 0000000..9dd6ede --- /dev/null +++ b/packages/client/src/debug/ConeIndicator.tsx @@ -0,0 +1,27 @@ +import { ThreeCache } from '../gfx/ThreeCache' +import * as THREE from 'three'; +const cache = new ThreeCache(); + +const coneGeometry = new THREE.ConeGeometry(0.5, 2, 8); +export function ConeIndicator(props: {action: UnitAction, smoothing: boolean}) { + // TODO - this will be replaced with animations etc + let indicatorColor = 0xeeeeee; + if (props.action === 'Moving') + indicatorColor = 0x55ff55; + else if (props.action === 'Attacking') + indicatorColor = 0xff5555; + else if (props.action === 'Harvesting') + indicatorColor = 0x5555ff; + // indicate discrepancy between server and us + else if (props.smoothing) + indicatorColor = 0xffff55; + + return ( + + ); +} diff --git a/packages/client/src/gfx/Unit3D.tsx b/packages/client/src/gfx/Unit3D.tsx index c772b55..f9c0861 100644 --- a/packages/client/src/gfx/Unit3D.tsx +++ b/packages/client/src/gfx/Unit3D.tsx @@ -17,6 +17,7 @@ import { ThreeCache } from './ThreeCache' import { FileModel } from './FileModel' import { UnitDisplayEntry } from './UnitDisplayCatalog' import { Horizon } from '../debug/Horizon' +import { ConeIndicator } from '../debug/ConeIndicator' import { debugFlags } from '../debug/flags' const cache = new ThreeCache(); @@ -28,30 +29,6 @@ const invisibleMaterial = new THREE.MeshBasicMaterial({ opacity:0, }); -const coneGeometry = new THREE.ConeGeometry(0.5, 2, 8); -function ConeIndicator(props: {action: UnitAction, smoothing: boolean}) { - // TODO - this will be replaced with animations etc - let indicatorColor = 0xeeeeee; - if (props.action === 'Moving') - indicatorColor = 0x55ff55; - else if (props.action === 'Attacking') - indicatorColor = 0xff5555; - else if (props.action === 'Harvesting') - indicatorColor = 0x5555ff; - // indicate discrepancy between server and us - else if (props.smoothing) - indicatorColor = 0xffff55; - - return ( - - ); -} - type Unit3DProps = { unit: Unit, displayEntry: UnitDisplayEntry, From 7e45ea3fde1416da2e63aaee63cd5e101542afa3 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 26 Jun 2024 01:04:57 +0200 Subject: [PATCH 02/29] First experimental display of a projectile --- packages/client/src/gfx/Unit3D.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/client/src/gfx/Unit3D.tsx b/packages/client/src/gfx/Unit3D.tsx index f9c0861..d881084 100644 --- a/packages/client/src/gfx/Unit3D.tsx +++ b/packages/client/src/gfx/Unit3D.tsx @@ -126,12 +126,33 @@ export function Unit3D(props: Unit3DProps) { const action = props.unit.state.action; + const projectileTarget = new THREE.Vector3(50, 0, 50); + const projectilePosition = new THREE.Vector3(props.unit.position.x, 5, props.unit.position.y); + const projectileRef = useRef(null); + useFrame((s, dt) => { + if(!projectileRef.current) + return; + + if (projectileRef.current.position.y > 10) { + projectileRef.current.position.x = props.unit.position.x + projectileRef.current.position.y = 0; + projectileRef.current.position.z = props.unit.position.y; + } else { + projectileRef.current.position.y += dt * 5; + } + }); + return ( { debugFlags.showPaths && props.selected && debugPath && } + Date: Wed, 26 Jun 2024 19:59:26 +0200 Subject: [PATCH 03/29] Refactored Projectile into a separate component --- packages/client/src/debug/ConeIndicator.tsx | 3 ++ packages/client/src/gfx/Projectile.tsx | 38 +++++++++++++++++++++ packages/client/src/gfx/Unit3D.tsx | 23 ++----------- 3 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 packages/client/src/gfx/Projectile.tsx diff --git a/packages/client/src/debug/ConeIndicator.tsx b/packages/client/src/debug/ConeIndicator.tsx index 9dd6ede..0a4249d 100644 --- a/packages/client/src/debug/ConeIndicator.tsx +++ b/packages/client/src/debug/ConeIndicator.tsx @@ -1,5 +1,8 @@ import { ThreeCache } from '../gfx/ThreeCache' import * as THREE from 'three'; + +import { UnitAction } from '@bananu7-rts/server/src/types' + const cache = new ThreeCache(); const coneGeometry = new THREE.ConeGeometry(0.5, 2, 8); diff --git a/packages/client/src/gfx/Projectile.tsx b/packages/client/src/gfx/Projectile.tsx new file mode 100644 index 0000000..c20b4a1 --- /dev/null +++ b/packages/client/src/gfx/Projectile.tsx @@ -0,0 +1,38 @@ +import { useRef } from 'react' +import { useFrame } from '@react-three/fiber' +import * as THREE from 'three'; + +import { Position } from '@bananu7-rts/server/src/types' +import { ThreeCache } from './ThreeCache' + +const cache = new ThreeCache(); + +export type ProjectileProps = { + position: Position +} + +export function Projectile(props: ProjectileProps) { + const projectileTarget = new THREE.Vector3(50, 0, 50); + const projectilePosition = new THREE.Vector3(props.position.x, 5, props.position.y); + const projectileRef = useRef(null); + useFrame((s, dt) => { + if(!projectileRef.current) + return; + + if (projectileRef.current.position.y > 10) { + projectileRef.current.position.x = props.position.x + projectileRef.current.position.y = 0; + projectileRef.current.position.z = props.position.y; + } else { + projectileRef.current.position.y += dt * 5; + } + }); + + return ( + + ); +} diff --git a/packages/client/src/gfx/Unit3D.tsx b/packages/client/src/gfx/Unit3D.tsx index d881084..7a3fa71 100644 --- a/packages/client/src/gfx/Unit3D.tsx +++ b/packages/client/src/gfx/Unit3D.tsx @@ -16,6 +16,7 @@ import { Map3D, Box } from './Map3D' import { ThreeCache } from './ThreeCache' import { FileModel } from './FileModel' import { UnitDisplayEntry } from './UnitDisplayCatalog' +import { Projectile } from './Projectile' import { Horizon } from '../debug/Horizon' import { ConeIndicator } from '../debug/ConeIndicator' import { debugFlags } from '../debug/flags' @@ -126,33 +127,13 @@ export function Unit3D(props: Unit3DProps) { const action = props.unit.state.action; - const projectileTarget = new THREE.Vector3(50, 0, 50); - const projectilePosition = new THREE.Vector3(props.unit.position.x, 5, props.unit.position.y); - const projectileRef = useRef(null); - useFrame((s, dt) => { - if(!projectileRef.current) - return; - - if (projectileRef.current.position.y > 10) { - projectileRef.current.position.x = props.unit.position.x - projectileRef.current.position.y = 0; - projectileRef.current.position.z = props.unit.position.y; - } else { - projectileRef.current.position.y += dt * 5; - } - }); - return ( { debugFlags.showPaths && props.selected && debugPath && } - + Date: Wed, 26 Jun 2024 20:35:58 +0200 Subject: [PATCH 04/29] Added parabola equation for projectile paths --- packages/client/src/gfx/Projectile.tsx | 27 ++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/client/src/gfx/Projectile.tsx b/packages/client/src/gfx/Projectile.tsx index c20b4a1..532ccd2 100644 --- a/packages/client/src/gfx/Projectile.tsx +++ b/packages/client/src/gfx/Projectile.tsx @@ -15,17 +15,27 @@ export function Projectile(props: ProjectileProps) { const projectileTarget = new THREE.Vector3(50, 0, 50); const projectilePosition = new THREE.Vector3(props.position.x, 5, props.position.y); const projectileRef = useRef(null); + + const tRef = useRef(0); + useFrame((s, dt) => { if(!projectileRef.current) return; - if (projectileRef.current.position.y > 10) { + const range = 20; + const y = parabolaHeight(range, 10, tRef.current); + + if (tRef.current === 0) { projectileRef.current.position.x = props.position.x - projectileRef.current.position.y = 0; projectileRef.current.position.z = props.position.y; - } else { - projectileRef.current.position.y += dt * 5; } + + projectileRef.current.position.y = y; + projectileRef.current.position.x = props.position.x + tRef.current * range; + + tRef.current += dt; + if (tRef.current > 1) + tRef.current = 0; }); return ( @@ -36,3 +46,12 @@ export function Projectile(props: ProjectileProps) { /> ); } + +function parabolaHeight(length: number, height: number, epsilon: number) { + const k = length; + const h = height; + + const x = epsilon * k; + + return 4*h * (x/k - (x*x)/(k*k)); +} From 7301ca1c3d63c3aefdf6159d746d3afd1d0073c3 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 26 Jun 2024 21:01:27 +0200 Subject: [PATCH 05/29] Read the attack rate from unit's attacker component --- packages/client/src/gfx/Projectile.tsx | 11 +++++++---- packages/client/src/gfx/Unit3D.tsx | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/client/src/gfx/Projectile.tsx b/packages/client/src/gfx/Projectile.tsx index 532ccd2..f645bce 100644 --- a/packages/client/src/gfx/Projectile.tsx +++ b/packages/client/src/gfx/Projectile.tsx @@ -8,7 +8,8 @@ import { ThreeCache } from './ThreeCache' const cache = new ThreeCache(); export type ProjectileProps = { - position: Position + position: Position, + attackRate: number, // TODO this is just flight time? } export function Projectile(props: ProjectileProps) { @@ -22,8 +23,10 @@ export function Projectile(props: ProjectileProps) { if(!projectileRef.current) return; + const attackRate = props.attackRate; const range = 20; - const y = parabolaHeight(range, 10, tRef.current); + const e = tRef.current * (1000/attackRate); + const y = parabolaHeight(range, 10, e); if (tRef.current === 0) { projectileRef.current.position.x = props.position.x @@ -31,10 +34,10 @@ export function Projectile(props: ProjectileProps) { } projectileRef.current.position.y = y; - projectileRef.current.position.x = props.position.x + tRef.current * range; + projectileRef.current.position.x = props.position.x + e * range; tRef.current += dt; - if (tRef.current > 1) + if (tRef.current > attackRate / 1000) tRef.current = 0; }); diff --git a/packages/client/src/gfx/Unit3D.tsx b/packages/client/src/gfx/Unit3D.tsx index 7a3fa71..a4af649 100644 --- a/packages/client/src/gfx/Unit3D.tsx +++ b/packages/client/src/gfx/Unit3D.tsx @@ -10,6 +10,8 @@ import { import * as THREE from 'three'; import { Board, Unit, GameMap, UnitId, Position, UnitAction } from '@bananu7-rts/server/src/types' +import { getAttackerComponent } from '@bananu7-rts/server/src/game/components' + import { SelectionCircle } from './SelectionCircle' import { Line3D } from './Line3D' import { Map3D, Box } from './Map3D' @@ -133,7 +135,9 @@ export function Unit3D(props: Unit3DProps) { props.selected && debugPath && } - + { action === "Attacking" && + + } Date: Wed, 26 Jun 2024 21:02:47 +0200 Subject: [PATCH 06/29] Make Catapult a proper unit and add Trooper with the peasant model for now --- packages/client/src/gfx/UnitDisplayCatalog.ts | 5 +++++ packages/server/src/game/units.ts | 13 ++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/client/src/gfx/UnitDisplayCatalog.ts b/packages/client/src/gfx/UnitDisplayCatalog.ts index cdc0ec4..b0d65c0 100644 --- a/packages/client/src/gfx/UnitDisplayCatalog.ts +++ b/packages/client/src/gfx/UnitDisplayCatalog.ts @@ -36,6 +36,11 @@ export const UNIT_DISPLAY_CATALOG : UnitDisplayCatalog = { selectorSize: 6, }), 'Trooper': () => ({ + isBuilding: false, + modelPath: 'peasant_1.glb', + selectorSize: 1, + }), + 'Catapult': () => ({ isBuilding: false, modelPath: 'catapult.glb', selectorSize: 2.5, diff --git a/packages/server/src/game/units.ts b/packages/server/src/game/units.ts index 9556e1b..937dc59 100644 --- a/packages/server/src/game/units.ts +++ b/packages/server/src/game/units.ts @@ -37,7 +37,8 @@ const UNIT_CATALOG : Catalog = { { type: 'Hp', maxHp: 600, hp: 600 }, { type: 'Building', size: 6 }, { type: 'ProductionFacility', unitsProduced: [ - {unitType: 'Trooper', productionTime: 5000, productionCost: 50} + {unitType: 'Trooper', productionTime: 5000, productionCost: 50}, + {unitType: 'Catapult', productionTime: 15000, productionCost: 150} ]}, { type: 'Vision', range: 5 }, ], @@ -48,8 +49,14 @@ const UNIT_CATALOG : Catalog = { ], 'Trooper': () => [ { type: 'Hp', maxHp: 50, hp: 50 }, - { type: 'Mover', speed: 10 }, - { type: 'Attacker', damage: 10, attackRate: 500, range: 6, cooldown: 0 }, + { type: 'Mover', speed: 12 }, + { type: 'Attacker', damage: 8, attackRate: 600, range: 2, cooldown: 0 }, + { type: 'Vision', range: 10 }, + ], + 'Catapult': () => [ + { type: 'Hp', maxHp: 80, hp: 80 }, + { type: 'Mover', speed: 8 }, + { type: 'Attacker', damage: 10, attackRate: 2000, range: 10, cooldown: 0 }, { type: 'Vision', range: 10 }, ] }; From e6281f845433c7387601236be4880b7ac57c65ad Mon Sep 17 00:00:00 2001 From: bananu7 Date: Thu, 27 Jun 2024 13:20:01 +0200 Subject: [PATCH 07/29] Added lerp to target but that doesn't work since the unit doesn't know about its target --- packages/client/src/gfx/Projectile.tsx | 6 +++++- packages/client/src/gfx/Unit3D.tsx | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/client/src/gfx/Projectile.tsx b/packages/client/src/gfx/Projectile.tsx index f645bce..a871136 100644 --- a/packages/client/src/gfx/Projectile.tsx +++ b/packages/client/src/gfx/Projectile.tsx @@ -9,6 +9,7 @@ const cache = new ThreeCache(); export type ProjectileProps = { position: Position, + target: Position, attackRate: number, // TODO this is just flight time? } @@ -33,8 +34,11 @@ export function Projectile(props: ProjectileProps) { projectileRef.current.position.z = props.position.y; } + const startPos = new THREE.Vector3(props.position.x, 0, props.position.y); + const targetPos = new THREE.Vector3(props.target.x, 0, props.target.y); + + projectileRef.current.position.lerpVectors(startPos, targetPos, e); projectileRef.current.position.y = y; - projectileRef.current.position.x = props.position.x + e * range; tRef.current += dt; if (tRef.current > attackRate / 1000) diff --git a/packages/client/src/gfx/Unit3D.tsx b/packages/client/src/gfx/Unit3D.tsx index a4af649..fe9a8de 100644 --- a/packages/client/src/gfx/Unit3D.tsx +++ b/packages/client/src/gfx/Unit3D.tsx @@ -128,6 +128,13 @@ export function Unit3D(props: Unit3DProps) { }); const action = props.unit.state.action; + const ac = getAttackerComponent(props.unit); + const targetPos = {x:0, y:0}; + if (props.unit.state.state === "active" && props.unit.state.current.typ === "Attack") { + const target = units.find(u => u.id === props.unit.state.current.target); + targetPos.x = target.position.x; + targetPos.y = target.position.y; + } return ( @@ -135,8 +142,12 @@ export function Unit3D(props: Unit3DProps) { props.selected && debugPath && } - { action === "Attacking" && - + { ac && action === "Attacking" && + } Date: Thu, 27 Jun 2024 15:07:16 +0200 Subject: [PATCH 08/29] Moved projectiles to be displayed by the board and not individual unit --- packages/client/src/gfx/Board3D.tsx | 36 +++++++++++++++++++++++++++++ packages/client/src/gfx/Unit3D.tsx | 16 ------------- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/packages/client/src/gfx/Board3D.tsx b/packages/client/src/gfx/Board3D.tsx index b7be36f..a92403b 100644 --- a/packages/client/src/gfx/Board3D.tsx +++ b/packages/client/src/gfx/Board3D.tsx @@ -10,12 +10,15 @@ import { import * as THREE from 'three'; import { Board, Unit, GameMap, UnitId, Position, TilePos, Building } from '@bananu7-rts/server/src/types' +import { getAttackerComponent } from '@bananu7-rts/server/src/game/components' +import { notEmpty } from '@bananu7-rts/server/src/tsutil' import { SelectionCircle } from './SelectionCircle' import { Line3D } from './Line3D' import { Map3D, Box } from './Map3D' import { Unit3D } from './Unit3D' import { Building3D } from './Building3D' import { BuildPreview } from './BuildPreview' +import { Projectile } from './Projectile' import { UNIT_DISPLAY_CATALOG, BuildingDisplayEntry } from './UnitDisplayCatalog' import { SelectedCommand } from '../game/SelectedCommand' @@ -114,8 +117,41 @@ export function Board3D(props: Props) { selectInBox={selectInBox} pointerMove={setPointer} /> + { units } { buildPreview } ); } + +function Projectiles(props: { units: Unit[] }) { + const projectiles = props.units.map(unit => { + const ac = getAttackerComponent(unit); + if (unit.state.state !== "active" + || unit.state.current.typ !== "Attack" + || unit.state.action !== "Attacking" + ) { + return undefined; + } + + const targetId = unit.state.current.target; + const target = props.units.find(u => u.id === targetId); + + if (!target) + return undefined; + + return ( + + ) + + }).filter(notEmpty); + + return ( + { projectiles } + ) +} + diff --git a/packages/client/src/gfx/Unit3D.tsx b/packages/client/src/gfx/Unit3D.tsx index fe9a8de..a6e10ee 100644 --- a/packages/client/src/gfx/Unit3D.tsx +++ b/packages/client/src/gfx/Unit3D.tsx @@ -10,7 +10,6 @@ import { import * as THREE from 'three'; import { Board, Unit, GameMap, UnitId, Position, UnitAction } from '@bananu7-rts/server/src/types' -import { getAttackerComponent } from '@bananu7-rts/server/src/game/components' import { SelectionCircle } from './SelectionCircle' import { Line3D } from './Line3D' @@ -18,7 +17,6 @@ import { Map3D, Box } from './Map3D' import { ThreeCache } from './ThreeCache' import { FileModel } from './FileModel' import { UnitDisplayEntry } from './UnitDisplayCatalog' -import { Projectile } from './Projectile' import { Horizon } from '../debug/Horizon' import { ConeIndicator } from '../debug/ConeIndicator' import { debugFlags } from '../debug/flags' @@ -128,13 +126,6 @@ export function Unit3D(props: Unit3DProps) { }); const action = props.unit.state.action; - const ac = getAttackerComponent(props.unit); - const targetPos = {x:0, y:0}; - if (props.unit.state.state === "active" && props.unit.state.current.typ === "Attack") { - const target = units.find(u => u.id === props.unit.state.current.target); - targetPos.x = target.position.x; - targetPos.y = target.position.y; - } return ( @@ -142,13 +133,6 @@ export function Unit3D(props: Unit3DProps) { props.selected && debugPath && } - { ac && action === "Attacking" && - - } Date: Tue, 2 Jul 2024 11:26:00 +0200 Subject: [PATCH 09/29] WIP on projectiles --- packages/server/src/game.ts | 3 ++- packages/server/src/game/unit/unit.ts | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/server/src/game.ts b/packages/server/src/game.ts index 97a35df..f852203 100644 --- a/packages/server/src/game.ts +++ b/packages/server/src/game.ts @@ -21,13 +21,14 @@ import { buildPresenceAndBuildingMaps } from './game/presence.js' export function newGame(matchId: string, board: Board): Game { const units = createStartingUnits(2, board); + const startingResources = 150; return { matchId, state: {id: 'Lobby'}, tickNumber: 0, // TODO factor number of players in creation // TODO handle disconnect separately from elimination - players: [{resources: 50, stillInGame: true}, {resources: 50, stillInGame: true}], + players: [{resources: startingResources, stillInGame: true}, {resources: startingResources, stillInGame: true}], board, units, lastUnitId: units.length, diff --git a/packages/server/src/game/unit/unit.ts b/packages/server/src/game/unit/unit.ts index 7a93b8c..925ee49 100644 --- a/packages/server/src/game/unit/unit.ts +++ b/packages/server/src/game/unit/unit.ts @@ -75,6 +75,12 @@ const attemptDamage = (ac: Attacker, target: Unit) => { } } +function fireProjectile(gm: GameWithPresenceCache, ac: Attacker, target: Unit | Position) { + gm.projectiles.push({ + id: ++gm.lastProjectileId, + }) +} + export const aggro = (unit: Unit, gm: GameWithPresenceCache, ac: Attacker, target: Unit, dt: Milliseconds) => { // first let the movement system do its thing const movementTolerance = ac.range - ATTACK_RANGE_COMPENSATION; From 5034da56243a37e4a5d2a8f1cf2d9f52480fd173 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 2 Jul 2024 23:32:46 +0200 Subject: [PATCH 10/29] Added a Projectile type --- packages/server/src/types.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 47cd36d..d174edf 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -169,9 +169,9 @@ export type Vision = { // Internal Game stuff export type TilePos = { x: number, y: number } -export type PlayerIndex = number -export type UserId = string - +export type PlayerIndex = number; +export type UserId = string; +export type ProjectileId = number; export type UnitAction = 'Moving'|'Attacking'|'Harvesting'|'Idle'|'Producing'|'Building'; @@ -220,6 +220,14 @@ export type PlayerState = { export type WinCondition = 'BuildingElimination'|'OneLeft'; + +export type Projectile = { + id: ProjectileId, + damage: number, + target: Position, // TODO - projectiles targeting units + origin: Position, +} + export type Game = { // uuid: UUID, TODO readonly matchId: MatchId, From 5b9bdfd68c1edd2d4e6163f2bf2f56a079d0c89b Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 2 Jul 2024 23:38:44 +0200 Subject: [PATCH 11/29] Fixed build errors --- packages/server/src/game.ts | 2 ++ packages/server/src/game/unit/unit.ts | 11 +++++++---- packages/server/src/types.ts | 2 ++ packages/server/test/util.ts | 2 ++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/server/src/game.ts b/packages/server/src/game.ts index f852203..ad1e77d 100644 --- a/packages/server/src/game.ts +++ b/packages/server/src/game.ts @@ -31,7 +31,9 @@ export function newGame(matchId: string, board: Board): Game { players: [{resources: startingResources, stillInGame: true}, {resources: startingResources, stillInGame: true}], board, units, + projectiles: [], lastUnitId: units.length, + lastProjectileId: 1, winCondition: 'BuildingElimination', } } diff --git a/packages/server/src/game/unit/unit.ts b/packages/server/src/game/unit/unit.ts index 925ee49..44dc30e 100644 --- a/packages/server/src/game/unit/unit.ts +++ b/packages/server/src/game/unit/unit.ts @@ -1,6 +1,6 @@ import { Unit, UnitId, Milliseconds, PlayerState, GameWithPresenceCache, - Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Component + Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Component, Position } from '../../types' import * as V from '../../vector.js' @@ -75,9 +75,12 @@ const attemptDamage = (ac: Attacker, target: Unit) => { } } -function fireProjectile(gm: GameWithPresenceCache, ac: Attacker, target: Unit | Position) { - gm.projectiles.push({ - id: ++gm.lastProjectileId, +function fireProjectile(gm: GameWithPresenceCache, origin: Position, ac: Attacker, target: Unit | Position) { + gm.game.projectiles.push({ + id: ++gm.game.lastProjectileId, + damage: ac.damage, + target: "position" in target ? target.position : target, + origin, }) } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index d174edf..6c9d8c1 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -238,7 +238,9 @@ export type Game = { players: PlayerState[], tickNumber: number, units: Unit[], + projectiles: Projectile[], lastUnitId: number, + lastProjectileId: number, } export type GameMap = { diff --git a/packages/server/test/util.ts b/packages/server/test/util.ts index a34fc11..1f07472 100644 --- a/packages/server/test/util.ts +++ b/packages/server/test/util.ts @@ -33,7 +33,9 @@ export function createBasicGame(override: Partial, mapSize?: number): Game players: [createOnePlayerState(), createOnePlayerState()], board, units, + projectiles: [], lastUnitId: units.length, + lastProjectileId: 1, winCondition: 'OneLeft', }; From b371fe3fb7c9b7b5dd6edc4de89bd64cc97fa452 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 2 Jul 2024 23:47:48 +0200 Subject: [PATCH 12/29] Added projectile creation code --- packages/server/src/game/unit/unit.ts | 9 ++++++++- packages/server/src/game/units.ts | 6 +++--- packages/server/src/types.ts | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/server/src/game/unit/unit.ts b/packages/server/src/game/unit/unit.ts index 44dc30e..0fecff8 100644 --- a/packages/server/src/game/unit/unit.ts +++ b/packages/server/src/game/unit/unit.ts @@ -92,7 +92,14 @@ export const aggro = (unit: Unit, gm: GameWithPresenceCache, ac: Attacker, targe unit.state.action = 'Attacking'; const targetPos = getUnitReferencePosition(target); unit.direction = V.angleFromTo(unit.position, targetPos); - attemptDamage(ac, target); + + // depending on the attacker type, either fire a projectile or deal direct damage + // TODO: windup + if (ac.kind === "projectile") { + fireProjectile(gm, unit.position, ac, target); + } else { + attemptDamage(ac, target); + } } // in any other case we can't do much else } diff --git a/packages/server/src/game/units.ts b/packages/server/src/game/units.ts index 937dc59..226fb4c 100644 --- a/packages/server/src/game/units.ts +++ b/packages/server/src/game/units.ts @@ -13,7 +13,7 @@ const UNIT_CATALOG : Catalog = { 'Harvester': () => [ { type: 'Hp', maxHp: 50, hp: 50 }, { type: 'Mover', speed: 10 }, - { type: 'Attacker', damage: 5, attackRate: 1000, range: 2, cooldown: 0 }, + { type: 'Attacker', damage: 5, attackRate: 1000, range: 2, cooldown: 0, kind: 'direct' }, { type: 'Harvester', harvestingTime: 2000, harvestingValue: 8, harvestingProgress: 0 }, { type: 'Builder', buildingsProduced: [ { buildingType: 'Base', buildTime: 5000, buildCost: 400 }, @@ -50,13 +50,13 @@ const UNIT_CATALOG : Catalog = { 'Trooper': () => [ { type: 'Hp', maxHp: 50, hp: 50 }, { type: 'Mover', speed: 12 }, - { type: 'Attacker', damage: 8, attackRate: 600, range: 2, cooldown: 0 }, + { type: 'Attacker', damage: 8, attackRate: 600, range: 2, cooldown: 0, kind: 'direct' }, { type: 'Vision', range: 10 }, ], 'Catapult': () => [ { type: 'Hp', maxHp: 80, hp: 80 }, { type: 'Mover', speed: 8 }, - { type: 'Attacker', damage: 10, attackRate: 2000, range: 10, cooldown: 0 }, + { type: 'Attacker', damage: 10, attackRate: 2000, range: 10, cooldown: 0, kind: 'projectile' }, { type: 'Vision', range: 10 }, ] }; diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 6c9d8c1..127143d 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -106,6 +106,7 @@ export type Attacker = { attackRate: Milliseconds, range: number, cooldown: Milliseconds, + kind: 'direct' | 'projectile', } export type Mover = { type: 'Mover', From 070ac9172bcfcd38ce540f43d5d70a883793bca5 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 3 Jul 2024 00:05:28 +0200 Subject: [PATCH 13/29] Projectiles are now created, sent and displayed --- .../client/src/components/CommandPalette.tsx | 2 +- .../client/src/components/MatchController.tsx | 1 + .../src/components/SpectateController.tsx | 1 + packages/client/src/gfx/Board3D.tsx | 33 ++++++------------- .../gfx/{Projectile.tsx => Projectile3D.tsx} | 2 +- packages/server/src/game.ts | 3 +- packages/server/src/game/unit/unit.ts | 25 +++++++------- packages/server/src/types.ts | 1 + 8 files changed, 30 insertions(+), 38 deletions(-) rename packages/client/src/gfx/{Projectile.tsx => Projectile3D.tsx} (97%) diff --git a/packages/client/src/components/CommandPalette.tsx b/packages/client/src/components/CommandPalette.tsx index 61abb1b..e65c9ec 100644 --- a/packages/client/src/components/CommandPalette.tsx +++ b/packages/client/src/components/CommandPalette.tsx @@ -116,8 +116,8 @@ export function CommandPalette(props: Props) { Produce {up.unitType} {up.unitType} - {cost}💰 {time}🕑 + {cost}💰

This excellent unit will serve you well, and I would tell you how but the tooltip data isn't diff --git a/packages/client/src/components/MatchController.tsx b/packages/client/src/components/MatchController.tsx index a41a971..a03307c 100644 --- a/packages/client/src/components/MatchController.tsx +++ b/packages/client/src/components/MatchController.tsx @@ -350,6 +350,7 @@ export function MatchController(props: MatchControllerProps) { board={matchMetadata.board} playerIndex={props.ctrl.getPlayerIndex()} units={lastUpdatePacket ? lastUpdatePacket.units : []} + projectiles={lastUpdatePacket ? lastUpdatePacket.projectiles : []} selectedUnits={selectedUnits} selectedCommand={selectedCommand} select={boardSelectUnits} diff --git a/packages/client/src/components/SpectateController.tsx b/packages/client/src/components/SpectateController.tsx index 854da8d..3325c57 100644 --- a/packages/client/src/components/SpectateController.tsx +++ b/packages/client/src/components/SpectateController.tsx @@ -191,6 +191,7 @@ export function SpectateController(props: SpectateControllerProps) { board={matchMetadata.board} playerIndex={0} // TODO - spectator has no player index units={lastUpdatePacket ? lastUpdatePacket.units : []} + projectiles={lastUpdatePacket ? lastUpdatePacket.projectiles : []} selectedUnits={selectedUnits} selectedCommand={undefined} // the board needs selected command to show e.g. build preview select={boardSelectUnits} diff --git a/packages/client/src/gfx/Board3D.tsx b/packages/client/src/gfx/Board3D.tsx index a92403b..65dbad8 100644 --- a/packages/client/src/gfx/Board3D.tsx +++ b/packages/client/src/gfx/Board3D.tsx @@ -9,7 +9,7 @@ import { import * as THREE from 'three'; -import { Board, Unit, GameMap, UnitId, Position, TilePos, Building } from '@bananu7-rts/server/src/types' +import { Board, Unit, GameMap, UnitId, Position, TilePos, Building, Projectile } from '@bananu7-rts/server/src/types' import { getAttackerComponent } from '@bananu7-rts/server/src/game/components' import { notEmpty } from '@bananu7-rts/server/src/tsutil' import { SelectionCircle } from './SelectionCircle' @@ -18,7 +18,7 @@ import { Map3D, Box } from './Map3D' import { Unit3D } from './Unit3D' import { Building3D } from './Building3D' import { BuildPreview } from './BuildPreview' -import { Projectile } from './Projectile' +import { Projectile3D } from './Projectile3D' import { UNIT_DISPLAY_CATALOG, BuildingDisplayEntry } from './UnitDisplayCatalog' import { SelectedCommand } from '../game/SelectedCommand' @@ -28,6 +28,7 @@ export interface Props { board: Board; playerIndex: number; units: Unit[]; + projectiles: Projectile[], selectedUnits: Set; selectedCommand: SelectedCommand | undefined; @@ -117,34 +118,20 @@ export function Board3D(props: Props) { selectInBox={selectInBox} pointerMove={setPointer} /> - + { units } { buildPreview }
); } -function Projectiles(props: { units: Unit[] }) { - const projectiles = props.units.map(unit => { - const ac = getAttackerComponent(unit); - if (unit.state.state !== "active" - || unit.state.current.typ !== "Attack" - || unit.state.action !== "Attacking" - ) { - return undefined; - } - - const targetId = unit.state.current.target; - const target = props.units.find(u => u.id === targetId); - - if (!target) - return undefined; - +function Projectiles(props: { projectiles: Projectile[] }) { + const projectiles = props.projectiles.map(projectile => { return ( - ) diff --git a/packages/client/src/gfx/Projectile.tsx b/packages/client/src/gfx/Projectile3D.tsx similarity index 97% rename from packages/client/src/gfx/Projectile.tsx rename to packages/client/src/gfx/Projectile3D.tsx index a871136..f50f449 100644 --- a/packages/client/src/gfx/Projectile.tsx +++ b/packages/client/src/gfx/Projectile3D.tsx @@ -13,7 +13,7 @@ export type ProjectileProps = { attackRate: number, // TODO this is just flight time? } -export function Projectile(props: ProjectileProps) { +export function Projectile3D(props: ProjectileProps) { const projectileTarget = new THREE.Vector3(50, 0, 50); const projectilePosition = new THREE.Vector3(props.position.x, 5, props.position.y); const projectileRef = useRef(null); diff --git a/packages/server/src/game.ts b/packages/server/src/game.ts index ad1e77d..197f941 100644 --- a/packages/server/src/game.ts +++ b/packages/server/src/game.ts @@ -21,7 +21,7 @@ import { buildPresenceAndBuildingMaps } from './game/presence.js' export function newGame(matchId: string, board: Board): Game { const units = createStartingUnits(2, board); - const startingResources = 150; + const startingResources = 1500; return { matchId, state: {id: 'Lobby'}, @@ -238,6 +238,7 @@ export function tick(dt: Milliseconds, g: Game): UpdatePacket[] { units: unitUpdates, player: p, state: g.state, + projectiles: g.projectiles, } }); } diff --git a/packages/server/src/game/unit/unit.ts b/packages/server/src/game/unit/unit.ts index 0fecff8..1fa281d 100644 --- a/packages/server/src/game/unit/unit.ts +++ b/packages/server/src/game/unit/unit.ts @@ -64,14 +64,21 @@ export const detectNearbyEnemy = (unit: Unit, units: Unit[]) => { return target; } -const attemptDamage = (ac: Attacker, target: Unit) => { - if (ac.cooldown === 0) { - // TODO - attack cooldown +const attemptDamage = (gm: GameWithPresenceCache, origin: Position, ac: Attacker, target: Unit) => { + if (ac.cooldown !== 0) + return; + + ac.cooldown = ac.attackRate; + + // depending on the attacker type, either fire a projectile or deal direct damage + // TODO: windup + if (ac.kind === "projectile") { + fireProjectile(gm, origin, ac, target); + } else { const hp = getHpComponent(target); if (hp) { hp.hp -= ac.damage; } - ac.cooldown = ac.attackRate; } } @@ -80,7 +87,7 @@ function fireProjectile(gm: GameWithPresenceCache, origin: Position, ac: Attacke id: ++gm.game.lastProjectileId, damage: ac.damage, target: "position" in target ? target.position : target, - origin, + origin: {x: origin.x, y: origin.y }, }) } @@ -93,13 +100,7 @@ export const aggro = (unit: Unit, gm: GameWithPresenceCache, ac: Attacker, targe const targetPos = getUnitReferencePosition(target); unit.direction = V.angleFromTo(unit.position, targetPos); - // depending on the attacker type, either fire a projectile or deal direct damage - // TODO: windup - if (ac.kind === "projectile") { - fireProjectile(gm, unit.position, ac, target); - } else { - attemptDamage(ac, target); - } + attemptDamage(gm, unit.position, ac, target); } // in any other case we can't do much else } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 127143d..bceb7af 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -91,6 +91,7 @@ export type UpdatePacket = { tickNumber: number, units: Unit[], player: PlayerState, + projectiles: Projectile[], } // Components From c6106e5c5a76cbe7f705439b684f2ab7538b7ecb Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 3 Jul 2024 14:08:35 +0200 Subject: [PATCH 14/29] Revert two small leftovers --- packages/client/src/debug/ConeIndicator.tsx | 1 - packages/server/src/game.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/client/src/debug/ConeIndicator.tsx b/packages/client/src/debug/ConeIndicator.tsx index 0a4249d..1a4ca54 100644 --- a/packages/client/src/debug/ConeIndicator.tsx +++ b/packages/client/src/debug/ConeIndicator.tsx @@ -7,7 +7,6 @@ const cache = new ThreeCache(); const coneGeometry = new THREE.ConeGeometry(0.5, 2, 8); export function ConeIndicator(props: {action: UnitAction, smoothing: boolean}) { - // TODO - this will be replaced with animations etc let indicatorColor = 0xeeeeee; if (props.action === 'Moving') indicatorColor = 0x55ff55; diff --git a/packages/server/src/game.ts b/packages/server/src/game.ts index 197f941..75bc757 100644 --- a/packages/server/src/game.ts +++ b/packages/server/src/game.ts @@ -21,7 +21,7 @@ import { buildPresenceAndBuildingMaps } from './game/presence.js' export function newGame(matchId: string, board: Board): Game { const units = createStartingUnits(2, board); - const startingResources = 1500; + const startingResources = 150; return { matchId, state: {id: 'Lobby'}, From 0ee7d763787958b542a0e329e6247abb6a1d5134 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 3 Jul 2024 15:06:48 +0200 Subject: [PATCH 15/29] Introduce dual projectile target --- packages/client/src/gfx/Board3D.tsx | 24 ++++++++++++++++++++---- packages/server/src/game.ts | 2 +- packages/server/src/game/unit/unit.ts | 14 ++++++++++---- packages/server/src/types.ts | 11 ++++++++++- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/client/src/gfx/Board3D.tsx b/packages/client/src/gfx/Board3D.tsx index 65dbad8..a02c145 100644 --- a/packages/client/src/gfx/Board3D.tsx +++ b/packages/client/src/gfx/Board3D.tsx @@ -9,7 +9,7 @@ import { import * as THREE from 'three'; -import { Board, Unit, GameMap, UnitId, Position, TilePos, Building, Projectile } from '@bananu7-rts/server/src/types' +import { Board, Unit, GameMap, UnitId, Position, TilePos, Building, Projectile, ProjectileTarget } from '@bananu7-rts/server/src/types' import { getAttackerComponent } from '@bananu7-rts/server/src/game/components' import { notEmpty } from '@bananu7-rts/server/src/tsutil' import { SelectionCircle } from './SelectionCircle' @@ -118,19 +118,26 @@ export function Board3D(props: Props) { selectInBox={selectInBox} pointerMove={setPointer} /> - + { units } { buildPreview }
); } -function Projectiles(props: { projectiles: Projectile[] }) { +function Projectiles(props: { projectiles: Projectile[], units: Unit[] }) { const projectiles = props.projectiles.map(projectile => { + // TODO how to display projectiles trying to reach units that don't exist anymore? + + const target = getPositionFromProjectileTarget(projectile.target, props.units); + if (!target) { + return null; + } + return ( ) @@ -142,3 +149,12 @@ function Projectiles(props: { projectiles: Projectile[] }) {
) } +function getPositionFromProjectileTarget(target: ProjectileTarget, units: Unit[]): Position | undefined { + if (target.type === "positionTarget") { + return target.position; + } else { + const targetUnit = units.find(u => u.id === target.unitId); + return targetUnit ? targetUnit.position : undefined; + } +} + \ No newline at end of file diff --git a/packages/server/src/game.ts b/packages/server/src/game.ts index 75bc757..197f941 100644 --- a/packages/server/src/game.ts +++ b/packages/server/src/game.ts @@ -21,7 +21,7 @@ import { buildPresenceAndBuildingMaps } from './game/presence.js' export function newGame(matchId: string, board: Board): Game { const units = createStartingUnits(2, board); - const startingResources = 150; + const startingResources = 1500; return { matchId, state: {id: 'Lobby'}, diff --git a/packages/server/src/game/unit/unit.ts b/packages/server/src/game/unit/unit.ts index 1fa281d..3eaecba 100644 --- a/packages/server/src/game/unit/unit.ts +++ b/packages/server/src/game/unit/unit.ts @@ -1,6 +1,6 @@ import { Unit, UnitId, Milliseconds, PlayerState, GameWithPresenceCache, - Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Component, Position + Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Component, Position, ProjectileTarget } from '../../types' import * as V from '../../vector.js' @@ -73,7 +73,12 @@ const attemptDamage = (gm: GameWithPresenceCache, origin: Position, ac: Attacker // depending on the attacker type, either fire a projectile or deal direct damage // TODO: windup if (ac.kind === "projectile") { - fireProjectile(gm, origin, ac, target); + const projectileTarget: ProjectileTarget = { + type: "positionTarget", + position: target.position, + }; + + fireProjectile(gm, origin, ac, projectileTarget); } else { const hp = getHpComponent(target); if (hp) { @@ -82,12 +87,13 @@ const attemptDamage = (gm: GameWithPresenceCache, origin: Position, ac: Attacker } } -function fireProjectile(gm: GameWithPresenceCache, origin: Position, ac: Attacker, target: Unit | Position) { +function fireProjectile(gm: GameWithPresenceCache, origin: Position, ac: Attacker, target: ProjectileTarget) { gm.game.projectiles.push({ id: ++gm.game.lastProjectileId, damage: ac.damage, - target: "position" in target ? target.position : target, + target, origin: {x: origin.x, y: origin.y }, + speed: 100, // TODO ac.projectileSpeed, but that'd require a separate RangedAttacker component }) } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index bceb7af..9d595e5 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -223,11 +223,20 @@ export type PlayerState = { export type WinCondition = 'BuildingElimination'|'OneLeft'; +export type ProjectileTarget = { + type: "unitTarget", + unitId: UnitId, +} | { + type: "positionTarget", + position: Position, +} + export type Projectile = { id: ProjectileId, damage: number, - target: Position, // TODO - projectiles targeting units + target: ProjectileTarget, origin: Position, + speed: number, } export type Game = { From 3c19e66bef6ef6618b77e5989a5c4d4ba1f7159f Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 3 Jul 2024 17:04:52 +0200 Subject: [PATCH 16/29] Changed the projectile calculation to be ftime/ftime left --- packages/client/src/gfx/Board3D.tsx | 6 ++-- packages/client/src/gfx/Projectile3D.tsx | 36 +++++++++++------------- packages/server/src/game.ts | 7 +++++ packages/server/src/game/unit/unit.ts | 21 +++++++++----- packages/server/src/types.ts | 3 +- 5 files changed, 43 insertions(+), 30 deletions(-) diff --git a/packages/client/src/gfx/Board3D.tsx b/packages/client/src/gfx/Board3D.tsx index a02c145..0ecef9d 100644 --- a/packages/client/src/gfx/Board3D.tsx +++ b/packages/client/src/gfx/Board3D.tsx @@ -136,9 +136,11 @@ function Projectiles(props: { projectiles: Projectile[], units: Unit[] }) { return ( ) diff --git a/packages/client/src/gfx/Projectile3D.tsx b/packages/client/src/gfx/Projectile3D.tsx index f50f449..b4ca719 100644 --- a/packages/client/src/gfx/Projectile3D.tsx +++ b/packages/client/src/gfx/Projectile3D.tsx @@ -2,47 +2,43 @@ import { useRef } from 'react' import { useFrame } from '@react-three/fiber' import * as THREE from 'three'; -import { Position } from '@bananu7-rts/server/src/types' +import { Position, Milliseconds } from '@bananu7-rts/server/src/types' import { ThreeCache } from './ThreeCache' const cache = new ThreeCache(); export type ProjectileProps = { - position: Position, + origin: Position, target: Position, - attackRate: number, // TODO this is just flight time? + flightTime: Milliseconds, + flightTimeLeft: Milliseconds, } export function Projectile3D(props: ProjectileProps) { - const projectileTarget = new THREE.Vector3(50, 0, 50); - const projectilePosition = new THREE.Vector3(props.position.x, 5, props.position.y); + const projectilePosition = new THREE.Vector3(props.origin.x, 5, props.origin.y); const projectileRef = useRef(null); - const tRef = useRef(0); + const startPos = new THREE.Vector3(props.origin.x, 0, props.origin.y); + const targetPos = new THREE.Vector3(props.target.x, 0, props.target.y); + + const flightTimeLeft = useRef(props.flightTimeLeft); + + // if(time_since_fire * projectile_speed > distance(target, shot_location)) hit(target, projectile); useFrame((s, dt) => { if(!projectileRef.current) return; - const attackRate = props.attackRate; + flightTimeLeft.current -= dt * 1000; + if (flightTimeLeft.current <= 0) + return; + const range = 20; - const e = tRef.current * (1000/attackRate); + const e = 1 - (flightTimeLeft.current / props.flightTime); const y = parabolaHeight(range, 10, e); - if (tRef.current === 0) { - projectileRef.current.position.x = props.position.x - projectileRef.current.position.z = props.position.y; - } - - const startPos = new THREE.Vector3(props.position.x, 0, props.position.y); - const targetPos = new THREE.Vector3(props.target.x, 0, props.target.y); - projectileRef.current.position.lerpVectors(startPos, targetPos, e); projectileRef.current.position.y = y; - - tRef.current += dt; - if (tRef.current > attackRate / 1000) - tRef.current = 0; }); return ( diff --git a/packages/server/src/game.ts b/packages/server/src/game.ts index 197f941..0eaa45a 100644 --- a/packages/server/src/game.ts +++ b/packages/server/src/game.ts @@ -211,6 +211,7 @@ export function tick(dt: Milliseconds, g: Game): UpdatePacket[] { } g.tickNumber += 1; + updateProjectiles(dt, g); updateUnits(dt, g); break; } @@ -279,6 +280,12 @@ function updateUnits(dt: Milliseconds, g: Game) { }); } +function updateProjectiles(dt: Milliseconds, g: Game) { + for (const projectile of g.projectiles) { + projectile.flightTimeLeft -= dt; + } +} + function eliminated(g: Game): PlayerIndex[] { const isBuilding = (u: Unit) => !!u.components.find(c => c.type === 'Building'); diff --git a/packages/server/src/game/unit/unit.ts b/packages/server/src/game/unit/unit.ts index 3eaecba..ba9e9fc 100644 --- a/packages/server/src/game/unit/unit.ts +++ b/packages/server/src/game/unit/unit.ts @@ -64,7 +64,7 @@ export const detectNearbyEnemy = (unit: Unit, units: Unit[]) => { return target; } -const attemptDamage = (gm: GameWithPresenceCache, origin: Position, ac: Attacker, target: Unit) => { +const attemptDamage = (gm: GameWithPresenceCache, unit: Unit, ac: Attacker, target: Unit) => { if (ac.cooldown !== 0) return; @@ -75,10 +75,10 @@ const attemptDamage = (gm: GameWithPresenceCache, origin: Position, ac: Attacker if (ac.kind === "projectile") { const projectileTarget: ProjectileTarget = { type: "positionTarget", - position: target.position, + position: getUnitReferencePosition(target), }; - fireProjectile(gm, origin, ac, projectileTarget); + fireProjectile(gm, unit, ac, projectileTarget); } else { const hp = getHpComponent(target); if (hp) { @@ -87,13 +87,20 @@ const attemptDamage = (gm: GameWithPresenceCache, origin: Position, ac: Attacker } } -function fireProjectile(gm: GameWithPresenceCache, origin: Position, ac: Attacker, target: ProjectileTarget) { +function fireProjectile(gm: GameWithPresenceCache, unit: Unit, ac: Attacker, target: ProjectileTarget) { + const projectileSpeed = 10; // units per s // TODO ac.projectileSpeed, but that'd require a separate RangedAttacker component + const distanceToTarget = target.type === "positionTarget" + ? V.distance(getUnitReferencePosition(unit), target.position) + : 0 // TODO target units// unitInteractionDistance(unit, ); + const flightTime: Milliseconds = (distanceToTarget / projectileSpeed) * 1000; + gm.game.projectiles.push({ id: ++gm.game.lastProjectileId, damage: ac.damage, target, - origin: {x: origin.x, y: origin.y }, - speed: 100, // TODO ac.projectileSpeed, but that'd require a separate RangedAttacker component + origin: {x: unit.position.x, y: unit.position.y }, + flightTime: flightTime, + flightTimeLeft: flightTime, }) } @@ -106,7 +113,7 @@ export const aggro = (unit: Unit, gm: GameWithPresenceCache, ac: Attacker, targe const targetPos = getUnitReferencePosition(target); unit.direction = V.angleFromTo(unit.position, targetPos); - attemptDamage(gm, unit.position, ac, target); + attemptDamage(gm, unit, ac, target); } // in any other case we can't do much else } diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 9d595e5..7cdc918 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -236,7 +236,8 @@ export type Projectile = { damage: number, target: ProjectileTarget, origin: Position, - speed: number, + flightTime: Milliseconds, + flightTimeLeft: Milliseconds, } export type Game = { From 94c763269a4c8f42e71a4f83c733372428423226 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 3 Jul 2024 17:07:31 +0200 Subject: [PATCH 17/29] Hide projectiles that finished flight --- packages/client/src/gfx/Projectile3D.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/client/src/gfx/Projectile3D.tsx b/packages/client/src/gfx/Projectile3D.tsx index b4ca719..e47ced0 100644 --- a/packages/client/src/gfx/Projectile3D.tsx +++ b/packages/client/src/gfx/Projectile3D.tsx @@ -30,8 +30,10 @@ export function Projectile3D(props: ProjectileProps) { return; flightTimeLeft.current -= dt * 1000; - if (flightTimeLeft.current <= 0) + if (flightTimeLeft.current <= 0) { + projectileRef.current.visible = false; return; + } const range = 20; const e = 1 - (flightTimeLeft.current / props.flightTime); From 522ff000d581e73a1fe5cadf30c19f4b67381cd3 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 3 Jul 2024 17:13:55 +0200 Subject: [PATCH 18/29] Bumped three to 0.166.1 --- packages/client/package.json | 6 +-- yarn.lock | 73 ++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/packages/client/package.json b/packages/client/package.json index 354193f..5ae9fc7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -11,12 +11,12 @@ }, "dependencies": { "@geckos.io/client": "^3.0.0", - "@react-three/fiber": "^8.8.9", - "@types/three": "^0.144.0", + "@react-three/fiber": "^8.16.8", + "@types/three": "^0.166.0", "react": "^18.2.0", "react-dom": "^18.2.0", "@bananu7-rts/server": "*", - "three": "^0.144.0" + "three": "^0.166.1" }, "devDependencies": { "@types/react": "^18.0.17", diff --git a/yarn.lock b/yarn.lock index a893a1c..bdbdb4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1332,18 +1332,21 @@ dependencies: "@octokit/openapi-types" "^12.11.0" -"@react-three/fiber@^8.8.9": - version "8.12.0" - resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-8.12.0.tgz#80241992d780d436dfba59d90d9b61e52371fa3e" - integrity sha512-o6DkNtNHqcOFRbxaiY5xayelE/9+Z0z9wWu5awqjVbc0c/1QM6AttJH6rKW0U/O6LWxSfxDTARXVzknZqpDJ7A== +"@react-three/fiber@^8.16.8": + version "8.16.8" + resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-8.16.8.tgz#4d2fecda7b38f534de6bdac49ca37c815cf9a4ef" + integrity sha512-Lc8fjATtvQEfSd8d5iKdbpHtRm/aPMeFj7jQvp6TNHfpo8IQTW3wwcE1ZMrGGoUH+w2mnyS+0MK1NLPLnuzGkQ== dependencies: "@babel/runtime" "^7.17.8" "@types/react-reconciler" "^0.26.7" + "@types/webxr" "*" + base64-js "^1.5.1" + buffer "^6.0.3" its-fine "^1.0.6" react-reconciler "^0.27.0" react-use-measure "^2.1.1" scheduler "^0.21.0" - suspend-react "^0.0.8" + suspend-react "^0.1.3" zustand "^3.7.1" "@sinclair/typebox@^0.27.8": @@ -1376,6 +1379,11 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@tweenjs/tween.js@~23.1.2": + version "23.1.2" + resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.2.tgz#4e5357fd6742f5aa50447d3fa808aed4cda93ed7" + integrity sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ== + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -1524,12 +1532,21 @@ "@types/mime" "*" "@types/node" "*" -"@types/three@^0.144.0": - version "0.144.0" - resolved "https://registry.yarnpkg.com/@types/three/-/three-0.144.0.tgz#a154f40122dbc3668c5424a5373f3965c6564557" - integrity sha512-psvEs6q5rLN50jUYZ3D4pZMfxTbdt3A243blt0my7/NcL6chaCZpHe2csbCtx0SOD9fI/XnF3wnVUAYZGqCSYg== +"@types/stats.js@*": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.3.tgz#705446e12ce0fad618557dd88236f51148b7a935" + integrity sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ== + +"@types/three@^0.166.0": + version "0.166.0" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.166.0.tgz#a220b9ffecb7b650e2fdc6ef982285e651ec5dbe" + integrity sha512-FHMnpcdhdbdOOIYbfkTkUVpYMW53odxbTRwd0/xJpYnTzEsjnVnondGAvHZb4z06UW0vo6WPVuvH0/9qrxKx7g== dependencies: + "@tweenjs/tween.js" "~23.1.2" + "@types/stats.js" "*" "@types/webxr" "*" + fflate "~0.8.2" + meshoptimizer "~0.18.1" "@types/webxr@*": version "0.5.1" @@ -1832,7 +1849,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -1932,6 +1949,14 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" @@ -2961,6 +2986,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fflate@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + figures@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" @@ -3441,7 +3471,7 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -4135,6 +4165,11 @@ merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +meshoptimizer@~0.18.1: + version "0.18.1" + resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz#cdb90907f30a7b5b1190facd3b7ee6b7087797d8" + integrity sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -5890,10 +5925,10 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -suspend-react@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/suspend-react/-/suspend-react-0.0.8.tgz#b0740c1386b4eb652f17affe4339915ee268bd31" - integrity sha512-ZC3r8Hu1y0dIThzsGw0RLZplnX9yXwfItcvaIzJc2VQVi8TGyGDlu92syMB5ulybfvGLHAI5Ghzlk23UBPF8xg== +suspend-react@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/suspend-react/-/suspend-react-0.1.3.tgz#a52f49d21cfae9a2fb70bd0c68413d3f9d90768e" + integrity sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ== tar-fs@^2.0.0: version "2.1.1" @@ -5962,10 +5997,10 @@ text-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== -three@^0.144.0: - version "0.144.0" - resolved "https://registry.yarnpkg.com/three/-/three-0.144.0.tgz#2818517169f8ff94eea5f664f6ff1fcdcd436cc8" - integrity sha512-R8AXPuqfjfRJKkYoTQcTK7A6i3AdO9++2n8ubya/GTU+fEHhYKu1ZooRSCPkx69jbnzT7dD/xEo6eROQTt2lJw== +three@^0.166.1: + version "0.166.1" + resolved "https://registry.yarnpkg.com/three/-/three-0.166.1.tgz#322cfc48fff4e751cd47d61fd1558c387d098d7c" + integrity sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg== through2@^2.0.0: version "2.0.5" From 05d45ff4b54641efad97ef21daecdaa2c33115a0 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Wed, 3 Jul 2024 17:22:12 +0200 Subject: [PATCH 19/29] Change default projectile to be a ball --- packages/client/src/gfx/Projectile3D.tsx | 4 ++-- packages/client/src/gfx/ThreeCache.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/client/src/gfx/Projectile3D.tsx b/packages/client/src/gfx/Projectile3D.tsx index e47ced0..caa3657 100644 --- a/packages/client/src/gfx/Projectile3D.tsx +++ b/packages/client/src/gfx/Projectile3D.tsx @@ -46,8 +46,8 @@ export function Projectile3D(props: ProjectileProps) { return ( ); } diff --git a/packages/client/src/gfx/ThreeCache.ts b/packages/client/src/gfx/ThreeCache.ts index 4db006c..da1d49c 100644 --- a/packages/client/src/gfx/ThreeCache.ts +++ b/packages/client/src/gfx/ThreeCache.ts @@ -29,6 +29,20 @@ export class ThreeCache { return geometry; } } + + spheres: Map = new Map(); + getSphereGeometry(radius: number) { + const cached = this.spheres.get(radius); + if (cached) { + return cached; + } else { + const widthSegments = 24; + const heightSegments = 8; + const geometry = new THREE.SphereGeometry(radius, widthSegments, heightSegments); + this.spheres.set(radius, geometry); + return geometry; + } + } standardMaterials: Map = new Map(); getStandardMaterial(color: number) { From d479915db5e428224606d3c42ba6ca81958161ce Mon Sep 17 00:00:00 2001 From: bananu7 Date: Thu, 21 Nov 2024 22:36:10 +0100 Subject: [PATCH 20/29] Implemented damage but doesn't compile becasue of presence cache --- packages/server/src/game.ts | 8 ++++- packages/server/src/game/unit/unit.ts | 42 +++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/server/src/game.ts b/packages/server/src/game.ts index 0eaa45a..357360d 100644 --- a/packages/server/src/game.ts +++ b/packages/server/src/game.ts @@ -18,6 +18,7 @@ import { findPositionForProducedUnit } from './game/produce.js' import { spiral, willAcceptCommand, getUnitReferencePosition } from './game/util.js' import { updateUnit } from './game/unit_update.js' import { buildPresenceAndBuildingMaps } from './game/presence.js' +import { resolveProjectile } from './game/unit/unit.js' export function newGame(matchId: string, board: Board): Game { const units = createStartingUnits(2, board); @@ -283,9 +284,14 @@ function updateUnits(dt: Milliseconds, g: Game) { function updateProjectiles(dt: Milliseconds, g: Game) { for (const projectile of g.projectiles) { projectile.flightTimeLeft -= dt; + + if (projectile.flightTimeLeft <= 0) { + resolveProjectile(g, projectile); + } } -} + g.projectiles = g.projectiles.filter(p => p.flightTimeLeft > 0); +} function eliminated(g: Game): PlayerIndex[] { const isBuilding = (u: Unit) => !!u.components.find(c => c.type === 'Building'); diff --git a/packages/server/src/game/unit/unit.ts b/packages/server/src/game/unit/unit.ts index ba9e9fc..fd554b0 100644 --- a/packages/server/src/game/unit/unit.ts +++ b/packages/server/src/game/unit/unit.ts @@ -1,6 +1,6 @@ import { Unit, UnitId, Milliseconds, PlayerState, GameWithPresenceCache, - Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Component, Position, ProjectileTarget + Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Component, Position, ProjectileTarget, Projectile } from '../../types' import * as V from '../../vector.js' @@ -80,10 +80,14 @@ const attemptDamage = (gm: GameWithPresenceCache, unit: Unit, ac: Attacker, targ fireProjectile(gm, unit, ac, projectileTarget); } else { - const hp = getHpComponent(target); - if (hp) { - hp.hp -= ac.damage; - } + applyDamage(target, ac.damage); + } +} + +const applyDamage = (target: Unit, damage: number) => { + const hp = getHpComponent(target); + if (hp) { + hp.hp -= damage; } } @@ -157,3 +161,31 @@ export const idle = (unit: Unit, gm: GameWithPresenceCache, dt: Milliseconds): b return true; } + +export const resolveProjectile = (gm: GameWithPresenceCache, p: Projectile) => { + switch (p.target.type) { + case 'unitTarget': + const tid = p.target.unitId + const targetUnit = gm.game.units.find(u => u.id === tid); + if (!targetUnit) { + break; // the unit might have died/decomposed already, it's fine + } + + applyDamage(targetUnit, p.damage); + + break; + case 'positionTarget': + const position = p.target.position; + // TODO: splash radius configure + const PROJECTILE_SPLASH_RADIUS = 5.0; + // TODO use presence cache for query + // TODO I have no helper for unit-area queries + /* + const unitsHit = gm.game.units.filter(u => unitInteractionDistance(position, u) < PROJECTILE_SPLASH_RADIUS); + + for (const u of unitsHit) { + applyDamage(u, p.damage); + }*/ + break; + } +} From 947308c37bb1cc97a5a901fe26ee13ac9bf76d8e Mon Sep 17 00:00:00 2001 From: bananu7 Date: Thu, 21 Nov 2024 22:42:45 +0100 Subject: [PATCH 21/29] Fix game presence cache passing --- packages/server/src/game.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/server/src/game.ts b/packages/server/src/game.ts index 357360d..1c124e6 100644 --- a/packages/server/src/game.ts +++ b/packages/server/src/game.ts @@ -1,7 +1,7 @@ import { Milliseconds, Position, Board, - GameMap, Game, PlayerIndex, Unit, UnitId, Component, CommandPacket, UpdatePacket, PresenceMap, BuildingMap, TilePos, + GameMap, Game, GameWithPresenceCache, PlayerIndex, Unit, UnitId, Component, CommandPacket, UpdatePacket, PresenceMap, BuildingMap, TilePos, Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Command, CommandFollow, CommandAttack, CommandMove, CommandAttackMove, CommandStop, CommandHarvest, CommandProduce, CommandBuild, PlayerState, UnitProductionCapability, BuildCapability @@ -212,8 +212,11 @@ export function tick(dt: Milliseconds, g: Game): UpdatePacket[] { } g.tickNumber += 1; - updateProjectiles(dt, g); - updateUnits(dt, g); + + const [presence, buildings] = buildPresenceAndBuildingMaps(g.units, g.board); + const gm = {game: g, presence, buildings}; + updateProjectiles(dt, gm); + updateUnits(dt, gm); break; } } @@ -257,22 +260,19 @@ export function endGame(g: Game) { }); } -function updateUnits(dt: Milliseconds, g: Game) { - // Build a unit presence map - const [presence, buildings] = buildPresenceAndBuildingMaps(g.units, g.board); - +function updateUnits(dt: Milliseconds, gm: GameWithPresenceCache) { // calculate updates and velocities - for (const unit of g.units) { - updateUnit(dt, { game: g, presence, buildings }, unit); + for (const unit of gm.game.units) { + updateUnit(dt, gm, unit); } // move everything at once - for (const unit of g.units) { + for (const unit of gm.game.units) { V.vecAdd(unit.position, unit.velocity); unit.velocity.x = 0; unit.velocity.y = 0; } - g.units = g.units.filter(u => { + gm.game.units = gm.game.units.filter(u => { const hp = getHpComponent(u); if (!hp) return true; // units with no HP live forever @@ -281,16 +281,16 @@ function updateUnits(dt: Milliseconds, g: Game) { }); } -function updateProjectiles(dt: Milliseconds, g: Game) { - for (const projectile of g.projectiles) { +function updateProjectiles(dt: Milliseconds, gm: GameWithPresenceCache) { + for (const projectile of gm.game.projectiles) { projectile.flightTimeLeft -= dt; if (projectile.flightTimeLeft <= 0) { - resolveProjectile(g, projectile); + resolveProjectile(gm, projectile); } } - g.projectiles = g.projectiles.filter(p => p.flightTimeLeft > 0); + gm.game.projectiles = gm.game.projectiles.filter(p => p.flightTimeLeft > 0); } function eliminated(g: Game): PlayerIndex[] { From 5ec39628e78ee16256b291d58b1d5bd430362a98 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Fri, 22 Nov 2024 10:51:05 +0100 Subject: [PATCH 22/29] Fix client build errors after three 0.166 bump --- packages/client/src/gfx/BuildPreview.tsx | 1 + packages/client/src/gfx/Line3D.tsx | 8 ++++---- packages/client/src/gfx/MapLight.tsx | 7 ++++--- packages/client/src/gfx/ThreeCache.ts | 4 ++-- packages/client/src/gfx/View3D.tsx | 1 - 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/client/src/gfx/BuildPreview.tsx b/packages/client/src/gfx/BuildPreview.tsx index 6b33412..59d4b66 100644 --- a/packages/client/src/gfx/BuildPreview.tsx +++ b/packages/client/src/gfx/BuildPreview.tsx @@ -1,5 +1,6 @@ import { useRef, RefObject } from 'react' import { useFrame } from '@react-three/fiber' +import * as THREE from 'three'; import { Board, Unit, GameMap, UnitId, Position, TilePos } from '@bananu7-rts/server/src/types' import { isBuildPlacementOk } from '@bananu7-rts/server/src/shared' import { clampToGrid } from '../game/Grid' diff --git a/packages/client/src/gfx/Line3D.tsx b/packages/client/src/gfx/Line3D.tsx index 455f31f..3d329f0 100644 --- a/packages/client/src/gfx/Line3D.tsx +++ b/packages/client/src/gfx/Line3D.tsx @@ -1,17 +1,17 @@ import { useRef, useLayoutEffect } from 'react' import { extend, ReactThreeFiber } from '@react-three/fiber'; -import { Line } from 'three'; +import * as THREE from 'three'; // Add class `Line` as `Line_` to react-three-fiber's extend function. This // makes it so that when you use in a , the three reconciler // will use the class `Line` -extend({ Line_: Line }); +extend({ Line_: THREE.Line }); // declare `line_` as a JSX element so that typescript doesn't complain declare global { namespace JSX { interface IntrinsicElements { - 'line_': ReactThreeFiber.Object3DNode, + 'line_': ReactThreeFiber.Object3DNode, } } } @@ -33,4 +33,4 @@ export function Line3D(props: Line3DProps) { ) -} \ No newline at end of file +} diff --git a/packages/client/src/gfx/MapLight.tsx b/packages/client/src/gfx/MapLight.tsx index 7f17dd4..32d0101 100644 --- a/packages/client/src/gfx/MapLight.tsx +++ b/packages/client/src/gfx/MapLight.tsx @@ -27,7 +27,7 @@ export function MapLight(props: MapLightProps) { return ( - { if (!ref.current) return; + if (!ref.current.shadow) return; - helper.current = new THREE.CameraHelper(ref.current?.shadow.camera); + helper.current = new THREE.CameraHelper(ref.current.shadow.camera); if (helper.current) { scene.add(helper.current); } @@ -78,4 +79,4 @@ export default function useShadowHelper( helper.current.update(); } }); -} \ No newline at end of file +} diff --git a/packages/client/src/gfx/ThreeCache.ts b/packages/client/src/gfx/ThreeCache.ts index da1d49c..1bee79b 100644 --- a/packages/client/src/gfx/ThreeCache.ts +++ b/packages/client/src/gfx/ThreeCache.ts @@ -30,7 +30,7 @@ export class ThreeCache { } } - spheres: Map = new Map(); + spheres: Map = new Map(); getSphereGeometry(radius: number) { const cached = this.spheres.get(radius); if (cached) { @@ -43,7 +43,7 @@ export class ThreeCache { return geometry; } } - + standardMaterials: Map = new Map(); getStandardMaterial(color: number) { const cached = this.standardMaterials.get(color); diff --git a/packages/client/src/gfx/View3D.tsx b/packages/client/src/gfx/View3D.tsx index 7361a7b..9a94bec 100644 --- a/packages/client/src/gfx/View3D.tsx +++ b/packages/client/src/gfx/View3D.tsx @@ -89,7 +89,6 @@ export function View3D(props: Props) { position: startCameraPosition, }} gl={{ - physicallyCorrectLights: true, pixelRatio: window.devicePixelRatio, }} linear={true} From df27ecba6f13327ab7393886e7f1b6fd41e65d8e Mon Sep 17 00:00:00 2001 From: bananu7 Date: Fri, 22 Nov 2024 12:44:33 +0100 Subject: [PATCH 23/29] Fix fireprojectile for unit targets --- packages/server/src/game/unit/unit.ts | 39 +++++++++++++++------------ 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/server/src/game/unit/unit.ts b/packages/server/src/game/unit/unit.ts index fd554b0..4e64b05 100644 --- a/packages/server/src/game/unit/unit.ts +++ b/packages/server/src/game/unit/unit.ts @@ -1,4 +1,4 @@ -import { +import { Unit, UnitId, Milliseconds, PlayerState, GameWithPresenceCache, Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, Vision, Building, Component, Position, ProjectileTarget, Projectile } from '../../types' @@ -22,14 +22,6 @@ export const cancelProduction = (unit: Unit, owner: PlayerState) => { } } -const getUnitReferencePositionById = (unit: Unit, units: Unit[], targetId: UnitId) => { - const target = units.find(u => u.id === targetId); // TODO Map - if (target) - return getUnitReferencePosition(target); - else - return; -} - // TODO - presence cache export const findClosestUnitBy = (unit: Unit, units: Unit[], p: (u: Unit) => boolean) => { const unitsFiltered = units.filter(p); @@ -39,7 +31,7 @@ export const findClosestUnitBy = (unit: Unit, units: Unit[], p: (u: Unit) => boo } unitsFiltered.sort((a: Unit, b: Unit) => unitInteractionDistance(unit, a) - unitInteractionDistance(unit, b)); - + return unitsFiltered[0]; } @@ -50,7 +42,7 @@ export const detectNearbyEnemy = (unit: Unit, units: Unit[]) => { } // TODO query range for optimizations - const target = findClosestUnitBy(unit, units, u => + const target = findClosestUnitBy(unit, units, u => u.owner !== unit.owner && u.owner !== 0 ); @@ -65,9 +57,9 @@ export const detectNearbyEnemy = (unit: Unit, units: Unit[]) => { } const attemptDamage = (gm: GameWithPresenceCache, unit: Unit, ac: Attacker, target: Unit) => { - if (ac.cooldown !== 0) + if (ac.cooldown !== 0) return; - + ac.cooldown = ac.attackRate; // depending on the attacker type, either fire a projectile or deal direct damage @@ -91,11 +83,24 @@ const applyDamage = (target: Unit, damage: number) => { } } +const computeProjectileDistance = (gm: GameWithPresenceCache, unit: Unit, target: ProjectileTarget): number | undefined => { + if (target.type === "positionTarget") { + return V.distance(getUnitReferencePosition(unit), target.position) + } else { + const targetUnit = gm.game.units.find(u => u.id == target.unitId); + if (!targetUnit) + return undefined; + return V.distance(getUnitReferencePosition(unit), getUnitReferencePosition(targetUnit)); + } +} + function fireProjectile(gm: GameWithPresenceCache, unit: Unit, ac: Attacker, target: ProjectileTarget) { const projectileSpeed = 10; // units per s // TODO ac.projectileSpeed, but that'd require a separate RangedAttacker component - const distanceToTarget = target.type === "positionTarget" - ? V.distance(getUnitReferencePosition(unit), target.position) - : 0 // TODO target units// unitInteractionDistance(unit, ); + const distanceToTarget = computeProjectileDistance(gm, unit, target); + if (!distanceToTarget){ + console.warn("[game] Trying to fire a projectile at a target that doesn't exist."); + return; + } const flightTime: Milliseconds = (distanceToTarget / projectileSpeed) * 1000; gm.game.projectiles.push({ @@ -143,7 +148,7 @@ export const idle = (unit: Unit, gm: GameWithPresenceCache, dt: Milliseconds): b return true; } - const target = detectNearbyEnemy(unit, gm.game.units); + const target = detectNearbyEnemy(unit, gm.game.units); if (!target) { // try to return to the idle position; // if it's close enough, it shouldn't start moving at all From e748b4ef79566fc3ab1b5df0d9d65cebd30324fd Mon Sep 17 00:00:00 2001 From: bananu7 Date: Fri, 22 Nov 2024 13:02:28 +0100 Subject: [PATCH 24/29] Bump tsc to 5.6.3 --- packages/client/package.json | 2 +- packages/server/package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/client/package.json b/packages/client/package.json index 5ae9fc7..cea456b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -22,7 +22,7 @@ "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", "@vitejs/plugin-react": "^2.0.1", - "typescript": "^5.4.5", + "typescript": "^5.6.3", "vite": "^3.0.7" } } diff --git a/packages/server/package.json b/packages/server/package.json index ff91f44..ff9130d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -28,6 +28,7 @@ }, "type": "module", "devDependencies": { + "typescript": "^5.6.3", "vitest": "^0.34.2" } } diff --git a/yarn.lock b/yarn.lock index bdbdb4a..6005374 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6186,6 +6186,11 @@ typescript@^5.4.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@^5.6.3: + version "5.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.3.tgz#5f3449e31c9d94febb17de03cc081dd56d81db5b" + integrity sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw== + ufo@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.2.0.tgz#28d127a087a46729133fdc89cb1358508b3f80ba" From a688f6a0e835a93fbaf063dbf8d73ce80fc118a6 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Fri, 22 Nov 2024 13:07:18 +0100 Subject: [PATCH 25/29] Fix build error with builder id --- .../client/src/components/MatchController.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/MatchController.tsx b/packages/client/src/components/MatchController.tsx index a03307c..51e093b 100644 --- a/packages/client/src/components/MatchController.tsx +++ b/packages/client/src/components/MatchController.tsx @@ -51,7 +51,7 @@ type MatchControllerProps = { export function MatchController(props: MatchControllerProps) { const [showMainMenu, setShowMainMenu] = useState(false); const [msgs, setMsgs] = useState([] as string[]); - + const [lastUpdatePacket, setLastUpdatePacket] = useState(null); const [messages, setMessages] = useState([]); @@ -137,12 +137,14 @@ export function MatchController(props: MatchControllerProps) { } else if (selectedCommand.command === 'Build') { // Only send one harvester to build // TODO send the closest one + // TODO frontend shouldn't be making this decision!!! + const chosenBuilderId = selectedUnits.keys().next().value!; // there's always at least one basing on the check above const gridPos = clampToGrid(p); const buildingSize = getBuildingSizeFromBuildingName(selectedCommand.building); const emptyForBuilding = mapEmptyForBuilding(matchMetadata.board.map, {size: buildingSize, type: 'Building'}, gridPos); if (emptyForBuilding) { - props.ctrl.buildCommand([selectedUnits.keys().next().value], selectedCommand.building, gridPos, shift); + props.ctrl.buildCommand([chosenBuilderId], selectedCommand.building, gridPos, shift); } else { console.log("[MatchController] trying to build in an invalid location") } @@ -186,7 +188,7 @@ export function MatchController(props: MatchControllerProps) { } else { units.add(targetId); - } + } return units; }); } else { @@ -278,12 +280,12 @@ export function MatchController(props: MatchControllerProps) { } { /* TODO move to Lobby */ } - { lastUpdatePacket && + { lastUpdatePacket && lastUpdatePacket.state.id === 'Precount' && } - { lastUpdatePacket && + { lastUpdatePacket && lastUpdatePacket.state.id === 'Lobby' && matchMetadata && } - { lastUpdatePacket && + { lastUpdatePacket && lastUpdatePacket.state.id === 'Paused' &&
Game paused @@ -368,4 +370,4 @@ export function MatchController(props: MatchControllerProps) { leaveMatch={leaveMatch} />
); -} \ No newline at end of file +} From b2e721bd7f9299c800344bf25d2650dfd30e338d Mon Sep 17 00:00:00 2001 From: bananu7 Date: Fri, 22 Nov 2024 14:37:12 +0100 Subject: [PATCH 26/29] Split tests into separate files --- packages/server/test/behaviour.test.ts | 383 ------------------ packages/server/test/behaviour/attack.test.ts | 98 +++++ packages/server/test/behaviour/build.test.ts | 68 ++++ .../server/test/behaviour/harvest.test.ts | 83 ++++ .../server/test/behaviour/movement.test.ts | 64 +++ .../server/test/behaviour/produce.test.ts | 77 ++++ .../test/behaviour/win_condition.test.ts | 28 ++ 7 files changed, 418 insertions(+), 383 deletions(-) delete mode 100644 packages/server/test/behaviour.test.ts create mode 100644 packages/server/test/behaviour/attack.test.ts create mode 100644 packages/server/test/behaviour/build.test.ts create mode 100644 packages/server/test/behaviour/harvest.test.ts create mode 100644 packages/server/test/behaviour/movement.test.ts create mode 100644 packages/server/test/behaviour/produce.test.ts create mode 100644 packages/server/test/behaviour/win_condition.test.ts diff --git a/packages/server/test/behaviour.test.ts b/packages/server/test/behaviour.test.ts deleted file mode 100644 index 63f40c2..0000000 --- a/packages/server/test/behaviour.test.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { tick, command } from '../src/game.js' -import { Game, PlayerState, Unit, GameMap, Position, TilePos, Harvester } from '../src/types' -import { expect, test, describe } from 'vitest' - -import { createBasicGame, createOnePlayerState, spawnUnit, markRectangle } from './util.js' - -const TICK_MS = 50; - -describe('win condition', () => { - test('BuildingElimination', () => { - const game = createBasicGame({ winCondition: 'BuildingElimination'}); - - tick(TICK_MS, game); - - expect(game.state.id).toBe('GameEnded'); - }); - - test('OneLeft', () => { - const game = createBasicGame({ - winCondition: 'OneLeft', - players: [createOnePlayerState(), createOnePlayerState()], - }); - - tick(TICK_MS, game); - - expect(game.state.id).toBe('Play'); - }); -}); - -describe('movement', () => { - test('move to map', () => { - const game = createBasicGame({}); - - spawnUnit(game, 1, "Harvester", {x: 5, y: 5}); - tick(TICK_MS, game); - command({ - command: { typ: 'Move', target: { x: 15, y: 15 }}, - unitIds: [1], - shift: false, - }, - game, - 1 - ); - - tick(TICK_MS, game); - - expect(game.units[0].state.state).toBe('active'); - expect(game.units[0].state.action).toBe('Moving'); - - for (let i = 0; i < 20 * 10; i++) - tick(TICK_MS, game); - - expect(game.units[0].state.state).toBe('idle'); - expect(game.units[0].pathToNext).toBeUndefined(); - expect(game.units[0].position.x).toBeCloseTo(15, 0); - expect(game.units[0].position.y).toBeCloseTo(15, 0); - - expect(game.state.id).toBe('Play'); - }); - - test('follow-move to building', () => { - const game = createBasicGame({}, 30); - spawnUnit(game, 1, "Harvester", {x: 2, y: 2}); - spawnUnit(game, 1, "Base", {x: 20, y: 5}); - - tick(TICK_MS, game); - - command({ - command: { typ: 'Follow', target: 2 }, - unitIds: [1], - shift: false, - }, - game, - 1 - ); - - for (let i = 0; i < 10 * 10; i++) { - tick(TICK_MS, game); - } - - expect(game.units[0].position.x).toBeGreaterThan(15); - expect(game.units[0].state.state).toBe('idle'); - expect(game.units[0].state.action).toBe('Idle'); - }); -}); - -test('attack action on building', () => { - const game = createBasicGame({}, 30); - - // It can't be exact like {x: 4, y: 10} because then it barely caught in range - spawnUnit(game, 1, "Trooper", {x: 3.5, y: 9}); - spawnUnit(game, 2, "Base", {x: 18, y: 10}); - - tick(TICK_MS, game); - expect(game.units[0].state.state).toBe('idle'); - expect(game.units[0].state.action).toBe('Idle'); - - command({ - command: { typ: 'Attack', target: 2 }, - unitIds: [1], - shift: false, - }, - game, - 1 - ); - - tick(TICK_MS, game); - - expect(game.units[0].state.state).toBe('active'); - expect(game.units[0].state.action).toBe('Moving'); - - for (let i = 0; i < 3 * 10; i++) { - tick(TICK_MS, game); - } - - expect(game.units[0].state.state).toBe('active'); - expect(game.units[0].state.action).toBe('Attacking'); -}); - -test('attack-move action', () => { - const game = createBasicGame({}, 30); - - // spawn two troopers, one to the left, one in the middle - // belonging to the other player - spawnUnit(game, 1, "Trooper", {x: 4, y: 10}); - spawnUnit(game, 2, "Trooper", {x: 15, y: 8}); - tick(TICK_MS, game); - - console.log("[test] Checking if the units are idle") - expect(game.units[0].state.state).toBe('idle'); - expect(game.units[0].state.action).toBe('Idle'); - expect(game.units[1].state.state).toBe('idle'); - expect(game.units[1].state.action).toBe('Idle'); - - console.log("[test] Giving AttackMove command"); - command({ - command: { typ: 'AttackMove', target: { x: 25, y: 10 }}, - unitIds: [1], - shift: false, - }, - game, - 1 - ); - tick(TICK_MS, game); - - expect(game.units[0].state.state).toBe('active'); - expect(game.units[0].state.action).toBe('Moving'); - - console.log("[test] Checking if the unit aggroes on opponent"); - for (let i = 0; i < 2 * 10; i++) { - tick(TICK_MS, game); - } - - expect(game.units[0].state.state).toBe('active'); - expect(game.units[0].state.action).toBe('Attacking'); -}); - -describe('attack action', () => { - test("don't allow targetting self", () => { - const game = createBasicGame({}); - spawnUnit(game, 1, "Trooper", {x: 4, y: 10}); - - command({ - command: { typ: 'Attack', target: 1, }, - unitIds: [1], - shift: false, - }, - game, - 1 - ); - tick(TICK_MS, game); - - expect(game.units[0].state.state).toBe('idle'); - expect(game.units[0].state.action).toBe('Idle'); - }); -}) - -describe('produce action', () => { - test('ensure resources', () => { - const game = createBasicGame({}); - - spawnUnit(game, 1, "Barracks", {x: 5, y: 5}); - - command({ - command: { typ: 'Produce', unitToProduce: "Trooper" }, - unitIds: [1], - shift: false, - }, - game, - 1 - ); - - tick(TICK_MS, game); - - expect(game.players[0].resources).toBe(0); - expect(game.units[0].state.state).toBe('idle'); - }); - - describe('find appropriate location for the unit', () => { - test.each([ - { name: "empty map", f: () => {} }, - { name: "no space below", f: (game: Game) => { - markRectangle(game.board.map, {x: 4, y: 10}, {x: 10, y: 12}); - }}, - ])('$name', ({f}) => { - const game = createBasicGame({}); - spawnUnit(game, 1, "Barracks", {x: 4, y: 4}); - - game.players[0].resources += 1000; - - f(game); - - command({ - command: { typ: 'Produce', unitToProduce: "Trooper" }, - unitIds: [1], - shift: false, - }, - game, - 1 - ); - - for (let i = 0; i < 15 * 10; i++) - tick(TICK_MS, game); - - expect(game.units.length).toBe(2); - - // after the unit has been produced, it's hard to tell what a "valid" - // location is, but at the very least it should be able to move - command({ - command: { typ: 'Move', target: { x: 15, y: 15 }}, - unitIds: [2], - shift: false, - }, - game, - 1 - ); - - debugger; - - tick(TICK_MS, game); - - expect(game.units[1].state.state).toBe('active'); - expect(game.units[1].state.action).toBe('Moving'); - }); - }); -}); - -describe('harvest action', () => { - test('all harvest phases', () => { - const game = createBasicGame({}, 50); - - spawnUnit(game, 0, "ResourceNode", {x: 6, y: 6}); - spawnUnit(game, 1, "Harvester", {x: 10, y: 8 }); - spawnUnit(game, 1, "Base", {x: 30, y: 10 }); - - command({ - command: { typ: 'Harvest', target: 1 }, - unitIds: [2], - shift: false, - }, - game, - 1 - ); - - console.log("[test] phase 1 - move to resource"); - tick(TICK_MS, game); - expect(game.units[1].state.state).toBe('active'); - expect(game.units[1].state.action).toBe('Moving'); - - console.log("[test] phase 2 - harvesting"); - for (let i = 0; i < 10; i++) - tick(TICK_MS, game); - expect(game.units[1].state.action).toBe('Harvesting'); - - console.log("[test] phase 3 - pickup and move"); - for (let i = 0; i < 5 * 10; i++) - tick(TICK_MS, game); - const hc = game.units[1].components.filter(c => c.type == "Harvester")[0] as Harvester; - expect(hc.resourcesCarried).toBeTruthy(); - expect(game.units[1].state.action).toBe('Moving'); - - console.log("[test] phase 4 - dropoff"); - for (let i = 0; i < 4.5 * 10; i++) - tick(TICK_MS, game); - - expect(hc.resourcesCarried).toBeUndefined(); - expect(game.units[1].state.action).toBe('Moving'); - expect(game.players[0].resources).toBe(8); - }); - - test('compete for one resource', () => { - const game = createBasicGame({}, 40); - - spawnUnit(game, 0, "ResourceNode", {x: 15, y: 15}); - // the first one is closer - spawnUnit(game, 1, "Harvester", {x: 10, y: 14 }); // id 2 - spawnUnit(game, 1, "Harvester", {x: 25, y: 16 }); // id 3 - - command({ - command: { typ: 'Harvest', target: 1 }, - unitIds: [2, 3], - shift: false, - }, - game, - 1 - ); - - for (let i = 0; i < 2 * 10; i++) { - tick(TICK_MS, game); - } - - expect(game.units[1].state.action).toBe('Harvesting'); - expect(game.units[2].state.action).toBe('Idle'); - - for (let i = 0; i < 4 * 10; i++) { - tick(TICK_MS, game); - } - - expect(game.units[2].state.action).toBe('Harvesting'); - expect(game.units[1].state.action).toBe('Idle'); - }); -}); - -describe('build action', () => { - test('ensure resources', () => { - const game = createBasicGame({}); - spawnUnit(game, 1, "Harvester", {x: 5, y: 5}); - - command({ - command: { typ: 'Build', building: "Barracks", position: { x: 4, y: 4 }}, - unitIds: [1], - shift: true, - }, - game, - 1 - ); - - tick(TICK_MS, game); - - expect(game.players[0].resources).toBe(0); - expect(game.units[0].state.state).toBe('idle'); - }); - - // Checks if the unit can move after placing a building on top of itself - test('build on top', () => { - const game = createBasicGame({}); - - game.players[0].resources += 1000; - - spawnUnit(game, 1, "Harvester", {x: 5, y: 5}); - - tick(TICK_MS, game); - - command({ - command: { typ: 'Build', building: "Barracks", position: { x: 4, y: 4 }}, - unitIds: [1], - shift: true, - }, - game, - 1 - ); - - for (let i = 0; i < 20 * 10; i++) - tick(TICK_MS, game); - - expect(game.units.length).toBe(2); - - console.log("[test] telling the unit to move") - command({ - command: { typ: 'Move', target: { x: 15, y: 15 }}, - unitIds: [1], - shift: false, - }, - game, - 1 - ); - - tick(TICK_MS, game); - - expect(game.units[0].state.state).toBe('active'); - expect(game.units[0].state.action).toBe('Moving'); - }); -}); diff --git a/packages/server/test/behaviour/attack.test.ts b/packages/server/test/behaviour/attack.test.ts new file mode 100644 index 0000000..e636071 --- /dev/null +++ b/packages/server/test/behaviour/attack.test.ts @@ -0,0 +1,98 @@ +import { tick, command } from '../../src/game.js' +import { Game, PlayerState, Unit, GameMap, Position, TilePos, Harvester } from '../../src/types' +import { expect, test, describe } from 'vitest' + +import { createBasicGame, createOnePlayerState, spawnUnit, markRectangle } from '../util.js' + +const TICK_MS = 50; + +test('attack action on building', () => { + const game = createBasicGame({}, 30); + + // It can't be exact like {x: 4, y: 10} because then it barely caught in range + spawnUnit(game, 1, "Trooper", {x: 3.5, y: 9}); + spawnUnit(game, 2, "Base", {x: 18, y: 10}); + + tick(TICK_MS, game); + expect(game.units[0].state.state).toBe('idle'); + expect(game.units[0].state.action).toBe('Idle'); + + command({ + command: { typ: 'Attack', target: 2 }, + unitIds: [1], + shift: false, + }, + game, + 1 + ); + + tick(TICK_MS, game); + + expect(game.units[0].state.state).toBe('active'); + expect(game.units[0].state.action).toBe('Moving'); + + for (let i = 0; i < 3 * 10; i++) { + tick(TICK_MS, game); + } + + expect(game.units[0].state.state).toBe('active'); + expect(game.units[0].state.action).toBe('Attacking'); +}); + +test('attack-move action', () => { + const game = createBasicGame({}, 30); + + // spawn two troopers, one to the left, one in the middle + // belonging to the other player + spawnUnit(game, 1, "Trooper", {x: 4, y: 10}); + spawnUnit(game, 2, "Trooper", {x: 15, y: 8}); + tick(TICK_MS, game); + + console.log("[test] Checking if the units are idle") + expect(game.units[0].state.state).toBe('idle'); + expect(game.units[0].state.action).toBe('Idle'); + expect(game.units[1].state.state).toBe('idle'); + expect(game.units[1].state.action).toBe('Idle'); + + console.log("[test] Giving AttackMove command"); + command({ + command: { typ: 'AttackMove', target: { x: 25, y: 10 }}, + unitIds: [1], + shift: false, + }, + game, + 1 + ); + tick(TICK_MS, game); + + expect(game.units[0].state.state).toBe('active'); + expect(game.units[0].state.action).toBe('Moving'); + + console.log("[test] Checking if the unit aggroes on opponent"); + for (let i = 0; i < 2 * 10; i++) { + tick(TICK_MS, game); + } + + expect(game.units[0].state.state).toBe('active'); + expect(game.units[0].state.action).toBe('Attacking'); +}); + +describe('attack action', () => { + test("don't allow targetting self", () => { + const game = createBasicGame({}); + spawnUnit(game, 1, "Trooper", {x: 4, y: 10}); + + command({ + command: { typ: 'Attack', target: 1, }, + unitIds: [1], + shift: false, + }, + game, + 1 + ); + tick(TICK_MS, game); + + expect(game.units[0].state.state).toBe('idle'); + expect(game.units[0].state.action).toBe('Idle'); + }); +}) diff --git a/packages/server/test/behaviour/build.test.ts b/packages/server/test/behaviour/build.test.ts new file mode 100644 index 0000000..3f44813 --- /dev/null +++ b/packages/server/test/behaviour/build.test.ts @@ -0,0 +1,68 @@ +import { tick, command } from '../../src/game.js' +import { Game, PlayerState, Unit, GameMap, Position, TilePos, Harvester } from '../../src/types' +import { expect, test, describe } from 'vitest' + +import { createBasicGame, createOnePlayerState, spawnUnit, markRectangle } from '../util.js' + +const TICK_MS = 50; + +describe('build action', () => { + test('ensure resources', () => { + const game = createBasicGame({}); + spawnUnit(game, 1, "Harvester", {x: 5, y: 5}); + + command({ + command: { typ: 'Build', building: "Barracks", position: { x: 4, y: 4 }}, + unitIds: [1], + shift: true, + }, + game, + 1 + ); + + tick(TICK_MS, game); + + expect(game.players[0].resources).toBe(0); + expect(game.units[0].state.state).toBe('idle'); + }); + + // Checks if the unit can move after placing a building on top of itself + test('build on top', () => { + const game = createBasicGame({}); + + game.players[0].resources += 1000; + + spawnUnit(game, 1, "Harvester", {x: 5, y: 5}); + + tick(TICK_MS, game); + + command({ + command: { typ: 'Build', building: "Barracks", position: { x: 4, y: 4 }}, + unitIds: [1], + shift: true, + }, + game, + 1 + ); + + for (let i = 0; i < 20 * 10; i++) + tick(TICK_MS, game); + + expect(game.units.length).toBe(2); + + console.log("[test] telling the unit to move") + command({ + command: { typ: 'Move', target: { x: 15, y: 15 }}, + unitIds: [1], + shift: false, + }, + game, + 1 + ); + + tick(TICK_MS, game); + + expect(game.units[0].state.state).toBe('active'); + expect(game.units[0].state.action).toBe('Moving'); + }); +}); diff --git a/packages/server/test/behaviour/harvest.test.ts b/packages/server/test/behaviour/harvest.test.ts new file mode 100644 index 0000000..c7172cc --- /dev/null +++ b/packages/server/test/behaviour/harvest.test.ts @@ -0,0 +1,83 @@ +import { tick, command } from '../../src/game.js' +import { Game, PlayerState, Unit, GameMap, Position, TilePos, Harvester } from '../../src/types' +import { expect, test, describe } from 'vitest' + +import { createBasicGame, createOnePlayerState, spawnUnit, markRectangle } from '../util.js' + +const TICK_MS = 50; + +describe('harvest action', () => { + test('all harvest phases', () => { + const game = createBasicGame({}, 50); + + spawnUnit(game, 0, "ResourceNode", {x: 6, y: 6}); + spawnUnit(game, 1, "Harvester", {x: 10, y: 8 }); + spawnUnit(game, 1, "Base", {x: 30, y: 10 }); + + command({ + command: { typ: 'Harvest', target: 1 }, + unitIds: [2], + shift: false, + }, + game, + 1 + ); + + console.log("[test] phase 1 - move to resource"); + tick(TICK_MS, game); + expect(game.units[1].state.state).toBe('active'); + expect(game.units[1].state.action).toBe('Moving'); + + console.log("[test] phase 2 - harvesting"); + for (let i = 0; i < 10; i++) + tick(TICK_MS, game); + expect(game.units[1].state.action).toBe('Harvesting'); + + console.log("[test] phase 3 - pickup and move"); + for (let i = 0; i < 5 * 10; i++) + tick(TICK_MS, game); + const hc = game.units[1].components.filter(c => c.type == "Harvester")[0] as Harvester; + expect(hc.resourcesCarried).toBeTruthy(); + expect(game.units[1].state.action).toBe('Moving'); + + console.log("[test] phase 4 - dropoff"); + for (let i = 0; i < 4.5 * 10; i++) + tick(TICK_MS, game); + + expect(hc.resourcesCarried).toBeUndefined(); + expect(game.units[1].state.action).toBe('Moving'); + expect(game.players[0].resources).toBe(8); + }); + + test('compete for one resource', () => { + const game = createBasicGame({}, 40); + + spawnUnit(game, 0, "ResourceNode", {x: 15, y: 15}); + // the first one is closer + spawnUnit(game, 1, "Harvester", {x: 10, y: 14 }); // id 2 + spawnUnit(game, 1, "Harvester", {x: 25, y: 16 }); // id 3 + + command({ + command: { typ: 'Harvest', target: 1 }, + unitIds: [2, 3], + shift: false, + }, + game, + 1 + ); + + for (let i = 0; i < 2 * 10; i++) { + tick(TICK_MS, game); + } + + expect(game.units[1].state.action).toBe('Harvesting'); + expect(game.units[2].state.action).toBe('Idle'); + + for (let i = 0; i < 4 * 10; i++) { + tick(TICK_MS, game); + } + + expect(game.units[2].state.action).toBe('Harvesting'); + expect(game.units[1].state.action).toBe('Idle'); + }); +}); diff --git a/packages/server/test/behaviour/movement.test.ts b/packages/server/test/behaviour/movement.test.ts new file mode 100644 index 0000000..b57b1a4 --- /dev/null +++ b/packages/server/test/behaviour/movement.test.ts @@ -0,0 +1,64 @@ +import { tick, command } from '../../src/game.js' +import { Game, PlayerState, Unit, GameMap, Position, TilePos, Harvester } from '../../src/types' +import { expect, test, describe } from 'vitest' + +import { createBasicGame, createOnePlayerState, spawnUnit, markRectangle } from '../util.js' + +const TICK_MS = 50; + +describe('movement', () => { + test('move to map', () => { + const game = createBasicGame({}); + + spawnUnit(game, 1, "Harvester", {x: 5, y: 5}); + tick(TICK_MS, game); + command({ + command: { typ: 'Move', target: { x: 15, y: 15 }}, + unitIds: [1], + shift: false, + }, + game, + 1 + ); + + tick(TICK_MS, game); + + expect(game.units[0].state.state).toBe('active'); + expect(game.units[0].state.action).toBe('Moving'); + + for (let i = 0; i < 20 * 10; i++) + tick(TICK_MS, game); + + expect(game.units[0].state.state).toBe('idle'); + expect(game.units[0].pathToNext).toBeUndefined(); + expect(game.units[0].position.x).toBeCloseTo(15, 0); + expect(game.units[0].position.y).toBeCloseTo(15, 0); + + expect(game.state.id).toBe('Play'); + }); + + test('follow-move to building', () => { + const game = createBasicGame({}, 30); + spawnUnit(game, 1, "Harvester", {x: 2, y: 2}); + spawnUnit(game, 1, "Base", {x: 20, y: 5}); + + tick(TICK_MS, game); + + command({ + command: { typ: 'Follow', target: 2 }, + unitIds: [1], + shift: false, + }, + game, + 1 + ); + + for (let i = 0; i < 10 * 10; i++) { + tick(TICK_MS, game); + } + + expect(game.units[0].position.x).toBeGreaterThan(15); + expect(game.units[0].state.state).toBe('idle'); + expect(game.units[0].state.action).toBe('Idle'); + }); +}); diff --git a/packages/server/test/behaviour/produce.test.ts b/packages/server/test/behaviour/produce.test.ts new file mode 100644 index 0000000..536ebc8 --- /dev/null +++ b/packages/server/test/behaviour/produce.test.ts @@ -0,0 +1,77 @@ +import { tick, command } from '../../src/game.js' +import { Game, PlayerState, Unit, GameMap, Position, TilePos, Harvester } from '../../src/types' +import { expect, test, describe } from 'vitest' + +import { createBasicGame, createOnePlayerState, spawnUnit, markRectangle } from '../util.js' + +const TICK_MS = 50; + +describe('produce action', () => { + test('ensure resources', () => { + const game = createBasicGame({}); + + spawnUnit(game, 1, "Barracks", {x: 5, y: 5}); + + command({ + command: { typ: 'Produce', unitToProduce: "Trooper" }, + unitIds: [1], + shift: false, + }, + game, + 1 + ); + + tick(TICK_MS, game); + + expect(game.players[0].resources).toBe(0); + expect(game.units[0].state.state).toBe('idle'); + }); + + describe('find appropriate location for the unit', () => { + test.each([ + { name: "empty map", f: () => {} }, + { name: "no space below", f: (game: Game) => { + markRectangle(game.board.map, {x: 4, y: 10}, {x: 10, y: 12}); + }}, + ])('$name', ({f}) => { + const game = createBasicGame({}); + spawnUnit(game, 1, "Barracks", {x: 4, y: 4}); + + game.players[0].resources += 1000; + + f(game); + + command({ + command: { typ: 'Produce', unitToProduce: "Trooper" }, + unitIds: [1], + shift: false, + }, + game, + 1 + ); + + for (let i = 0; i < 15 * 10; i++) + tick(TICK_MS, game); + + expect(game.units.length).toBe(2); + + // after the unit has been produced, it's hard to tell what a "valid" + // location is, but at the very least it should be able to move + command({ + command: { typ: 'Move', target: { x: 15, y: 15 }}, + unitIds: [2], + shift: false, + }, + game, + 1 + ); + + debugger; + + tick(TICK_MS, game); + + expect(game.units[1].state.state).toBe('active'); + expect(game.units[1].state.action).toBe('Moving'); + }); + }); +}); diff --git a/packages/server/test/behaviour/win_condition.test.ts b/packages/server/test/behaviour/win_condition.test.ts new file mode 100644 index 0000000..848fa1f --- /dev/null +++ b/packages/server/test/behaviour/win_condition.test.ts @@ -0,0 +1,28 @@ +import { tick, command } from '../../src/game.js' +import { Game, PlayerState, Unit, GameMap, Position, TilePos, Harvester } from '../../src/types' +import { expect, test, describe } from 'vitest' + +import { createBasicGame, createOnePlayerState, spawnUnit, markRectangle } from '../util.js' + +const TICK_MS = 50; + +describe('win condition', () => { + test('BuildingElimination', () => { + const game = createBasicGame({ winCondition: 'BuildingElimination'}); + + tick(TICK_MS, game); + + expect(game.state.id).toBe('GameEnded'); + }); + + test('OneLeft', () => { + const game = createBasicGame({ + winCondition: 'OneLeft', + players: [createOnePlayerState(), createOnePlayerState()], + }); + + tick(TICK_MS, game); + + expect(game.state.id).toBe('Play'); + }); +}); From 03d68a3298eb8fb9f070254c6d55c74f82ae1cf6 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Fri, 22 Nov 2024 14:52:04 +0100 Subject: [PATCH 27/29] Add a projectile test and make all projectiles target units --- packages/server/package.json | 3 +-- packages/server/src/game/unit/unit.ts | 7 +++++ packages/server/test/behaviour/attack.test.ts | 27 ++++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index ff9130d..e82fe2f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -23,8 +23,7 @@ "express": "^4.18.1", "fastpriorityqueue": "^0.7.2", "pngjs": "^6.0.0", - "ts-node": "^10.9.1", - "typescript": "^5.4.5" + "ts-node": "^10.9.1" }, "type": "module", "devDependencies": { diff --git a/packages/server/src/game/unit/unit.ts b/packages/server/src/game/unit/unit.ts index 4e64b05..a5ed08f 100644 --- a/packages/server/src/game/unit/unit.ts +++ b/packages/server/src/game/unit/unit.ts @@ -65,10 +65,17 @@ const attemptDamage = (gm: GameWithPresenceCache, unit: Unit, ac: Attacker, targ // depending on the attacker type, either fire a projectile or deal direct damage // TODO: windup if (ac.kind === "projectile") { + // TODO allow both types of projectiles + /* const projectileTarget: ProjectileTarget = { type: "positionTarget", position: getUnitReferencePosition(target), }; + */ + const projectileTarget: ProjectileTarget = { + type: "unitTarget", + unitId: target.id, + }; fireProjectile(gm, unit, ac, projectileTarget); } else { diff --git a/packages/server/test/behaviour/attack.test.ts b/packages/server/test/behaviour/attack.test.ts index e636071..42eb45e 100644 --- a/packages/server/test/behaviour/attack.test.ts +++ b/packages/server/test/behaviour/attack.test.ts @@ -1,5 +1,5 @@ import { tick, command } from '../../src/game.js' -import { Game, PlayerState, Unit, GameMap, Position, TilePos, Harvester } from '../../src/types' +import { Game, PlayerState, Unit, GameMap, Position, TilePos, Hp } from '../../src/types' import { expect, test, describe } from 'vitest' import { createBasicGame, createOnePlayerState, spawnUnit, markRectangle } from '../util.js' @@ -95,4 +95,29 @@ describe('attack action', () => { expect(game.units[0].state.state).toBe('idle'); expect(game.units[0].state.action).toBe('Idle'); }); + + test("attack flow with a projectile unit", () => { + const game = createBasicGame({}); + spawnUnit(game, 1, "Catapult", {x: 4, y: 10}); + spawnUnit(game, 2, "Trooper", {x: 10, y: 10}); + + command({ + command: { typ: 'Attack', target: 2, }, + unitIds: [1], + shift: false, + }, + game, + 1 + ); + tick(TICK_MS, game); + + expect(game.units[0].state.state).toBe('active'); + + const hp = game.units[1].components.filter(c => c.type == "Hp")[0] as Hp; + expect(hp.hp).toBe(hp.maxHp); + for (let i = 0; i < 5 * 10; i++) { + tick(TICK_MS, game); + } + expect(hp.hp).toBeLessThan(hp.maxHp); + }); }) From ddd0c3b16406b12a8a8193ecbca39b04e8702523 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Fri, 22 Nov 2024 15:07:27 +0100 Subject: [PATCH 28/29] Fix map border size and weird monolith --- packages/client/src/gfx/MapBorder.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/gfx/MapBorder.tsx b/packages/client/src/gfx/MapBorder.tsx index 68ca5e3..69fbed2 100644 --- a/packages/client/src/gfx/MapBorder.tsx +++ b/packages/client/src/gfx/MapBorder.tsx @@ -127,11 +127,11 @@ export function MapBorder(props: MapBorderProps) { ); -} \ No newline at end of file +} From 6dc98abb158ef6117ba488063580f73abc73295c Mon Sep 17 00:00:00 2001 From: bananu7 Date: Fri, 22 Nov 2024 15:18:49 +0100 Subject: [PATCH 29/29] Use getUnitReferencePosition on the frontend for projectile display --- packages/client/src/gfx/Board3D.tsx | 7 ++++--- packages/server/src/game/util.ts | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/client/src/gfx/Board3D.tsx b/packages/client/src/gfx/Board3D.tsx index 0ecef9d..87537c2 100644 --- a/packages/client/src/gfx/Board3D.tsx +++ b/packages/client/src/gfx/Board3D.tsx @@ -11,6 +11,7 @@ import * as THREE from 'three'; import { Board, Unit, GameMap, UnitId, Position, TilePos, Building, Projectile, ProjectileTarget } from '@bananu7-rts/server/src/types' import { getAttackerComponent } from '@bananu7-rts/server/src/game/components' +import { getUnitReferencePosition } from '@bananu7-rts/server/src/game/util' import { notEmpty } from '@bananu7-rts/server/src/tsutil' import { SelectionCircle } from './SelectionCircle' import { Line3D } from './Line3D' @@ -125,7 +126,7 @@ export function Board3D(props: Props) { ); } -function Projectiles(props: { projectiles: Projectile[], units: Unit[] }) { +function Projectiles(props: { projectiles: Projectile[], units: Unit[] }) { const projectiles = props.projectiles.map(projectile => { // TODO how to display projectiles trying to reach units that don't exist anymore? @@ -156,7 +157,7 @@ function getPositionFromProjectileTarget(target: ProjectileTarget, units: Unit[] return target.position; } else { const targetUnit = units.find(u => u.id === target.unitId); - return targetUnit ? targetUnit.position : undefined; + return targetUnit ? getUnitReferencePosition(targetUnit) : undefined; } } - \ No newline at end of file + \ No newline at end of file diff --git a/packages/server/src/game/util.ts b/packages/server/src/game/util.ts index f702643..a695b6c 100644 --- a/packages/server/src/game/util.ts +++ b/packages/server/src/game/util.ts @@ -119,7 +119,7 @@ export function willAcceptCommand(unit: Unit, command: Command) { // TODO maybe this should be better streamlined, like in a dictionary // of required components for each command? switch(command.typ) { - case 'Move': + case 'Move': if (!getMoveComponent(unit)) return false; break; @@ -144,10 +144,9 @@ export function willAcceptCommand(unit: Unit, command: Command) { } export function getUnitReferencePosition(target: Unit): Position { - // For regular units, their position is in the middle - // For buildings, it's the top-left corner + // Effectively "middle" of the unit const bc = getBuildingComponent(target); - + if (!bc) { return { x: target.position.x, y: target.position.y }; } else {