Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5555b5b
Moved ConeIndicator to a separate file
bananu7 Jun 25, 2024
7e45ea3
First experimental display of a projectile
bananu7 Jun 25, 2024
83e0ed9
Refactored Projectile into a separate component
bananu7 Jun 26, 2024
612ae14
Added parabola equation for projectile paths
bananu7 Jun 26, 2024
7301ca1
Read the attack rate from unit's attacker component
bananu7 Jun 26, 2024
b14603c
Make Catapult a proper unit and add Trooper with the peasant model fo…
bananu7 Jun 26, 2024
e6281f8
Added lerp to target but that doesn't work since the unit doesn't kno…
bananu7 Jun 27, 2024
a05e97b
Moved projectiles to be displayed by the board and not individual unit
bananu7 Jun 27, 2024
5a96b44
WIP on projectiles
bananu7 Jul 2, 2024
5034da5
Added a Projectile type
bananu7 Jul 2, 2024
5b9bdfd
Fixed build errors
bananu7 Jul 2, 2024
c332a10
Merge branch 'main' into projectiles
bananu7 Jul 2, 2024
b371fe3
Added projectile creation code
bananu7 Jul 2, 2024
070ac91
Projectiles are now created, sent and displayed
bananu7 Jul 2, 2024
c6106e5
Revert two small leftovers
bananu7 Jul 3, 2024
0ee7d76
Introduce dual projectile target
bananu7 Jul 3, 2024
3c19e66
Changed the projectile calculation to be ftime/ftime left
bananu7 Jul 3, 2024
94c7632
Hide projectiles that finished flight
bananu7 Jul 3, 2024
522ff00
Bumped three to 0.166.1
bananu7 Jul 3, 2024
05d45ff
Change default projectile to be a ball
bananu7 Jul 3, 2024
d479915
Implemented damage but doesn't compile becasue of presence cache
bananu7 Nov 21, 2024
947308c
Fix game presence cache passing
bananu7 Nov 21, 2024
5ec3962
Fix client build errors after three 0.166 bump
bananu7 Nov 22, 2024
df27ecb
Fix fireprojectile for unit targets
bananu7 Nov 22, 2024
e748b4e
Bump tsc to 5.6.3
bananu7 Nov 22, 2024
a688f6a
Fix build error with builder id
bananu7 Nov 22, 2024
b2e721b
Split tests into separate files
bananu7 Nov 22, 2024
03d68a3
Add a projectile test and make all projectiles target units
bananu7 Nov 22, 2024
ddd0c3b
Fix map border size and weird monolith
bananu7 Nov 22, 2024
6dc98ab
Use getUnitReferencePosition on the frontend for projectile display
bananu7 Nov 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@
},
"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",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.1",
"typescript": "^5.4.5",
"typescript": "^5.6.3",
"vite": "^3.0.7"
}
}
2 changes: 1 addition & 1 deletion packages/client/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ export function CommandPalette(props: Props) {
Produce {up.unitType}
<span className="tooltip">
<strong>{up.unitType}</strong>
<span style={{float:"right", color: canAfford?"white":"red"}}>{cost}💰</span>
<span style={{float:"right"}}>{time}🕑</span>
<span style={{float:"right", color: canAfford?"white":"red"}}>{cost}💰</span>
<br /><br/>
This excellent unit will serve you well, and I
would tell you how but the tooltip data isn't
Expand Down
17 changes: 10 additions & 7 deletions packages/client/src/components/MatchController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<UpdatePacket | null>(null);

const [messages, setMessages] = useState<string[]>([]);
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -186,7 +188,7 @@ export function MatchController(props: MatchControllerProps) {
}
else {
units.add(targetId);
}
}
return units;
});
} else {
Expand Down Expand Up @@ -278,12 +280,12 @@ export function MatchController(props: MatchControllerProps) {
}

{ /* TODO move to Lobby */ }
{ lastUpdatePacket &&
{ lastUpdatePacket &&
lastUpdatePacket.state.id === 'Precount' &&
<PrecountCounter count={lastUpdatePacket.state.count} />
}

{ lastUpdatePacket &&
{ lastUpdatePacket &&
lastUpdatePacket.state.id === 'Lobby' &&
matchMetadata &&
<Lobby
Expand All @@ -292,7 +294,7 @@ export function MatchController(props: MatchControllerProps) {
/>
}

{ lastUpdatePacket &&
{ lastUpdatePacket &&
lastUpdatePacket.state.id === 'Paused' &&
<div className="card">
<span>Game paused</span>
Expand Down Expand Up @@ -350,6 +352,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}
Expand All @@ -367,4 +370,4 @@ export function MatchController(props: MatchControllerProps) {
leaveMatch={leaveMatch}
/>
</div>);
}
}
1 change: 1 addition & 0 deletions packages/client/src/components/SpectateController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
29 changes: 29 additions & 0 deletions packages/client/src/debug/ConeIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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);
export function ConeIndicator(props: {action: UnitAction, smoothing: boolean}) {
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 (
<mesh
position={[0, 5, 0]}
rotation={[0, 0, -1.57]}
geometry={coneGeometry}
material={cache.getBasicMaterial(indicatorColor)}
/>
);
}
44 changes: 43 additions & 1 deletion packages/client/src/gfx/Board3D.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ 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, 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'
import { Map3D, Box } from './Map3D'
import { Unit3D } from './Unit3D'
import { Building3D } from './Building3D'
import { BuildPreview } from './BuildPreview'
import { Projectile3D } from './Projectile3D'
import { UNIT_DISPLAY_CATALOG, BuildingDisplayEntry } from './UnitDisplayCatalog'

import { SelectedCommand } from '../game/SelectedCommand'
Expand All @@ -25,6 +29,7 @@ export interface Props {
board: Board;
playerIndex: number;
units: Unit[];
projectiles: Projectile[],
selectedUnits: Set<UnitId>;
selectedCommand: SelectedCommand | undefined;

Expand Down Expand Up @@ -114,8 +119,45 @@ export function Board3D(props: Props) {
selectInBox={selectInBox}
pointerMove={setPointer}
/>
<Projectiles projectiles={props.projectiles} units={props.units} />
{ units }
{ buildPreview }
</group>
);
}

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 (
<Projectile3D
key={projectile.id}
origin={projectile.origin}
target={target}
flightTime={projectile.flightTime}
flightTimeLeft={projectile.flightTimeLeft}
/>
)

}).filter(notEmpty);

return (<group name="Projectiles">
{ projectiles }
</group>)
}

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 ? getUnitReferencePosition(targetUnit) : undefined;
}
}

1 change: 1 addition & 0 deletions packages/client/src/gfx/BuildPreview.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
8 changes: 4 additions & 4 deletions packages/client/src/gfx/Line3D.tsx
Original file line number Diff line number Diff line change
@@ -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 <line_> in a <Canvas>, 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, typeof Line>,
'line_': ReactThreeFiber.Object3DNode<THREE.Line, typeof THREE.Line>,
}
}
}
Expand All @@ -33,4 +33,4 @@ export function Line3D(props: Line3DProps) {
<lineBasicMaterial color="yellow" />
</line_>
)
}
}
4 changes: 2 additions & 2 deletions packages/client/src/gfx/MapBorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ export function MapBorder(props: MapBorderProps) {
<instancedMesh
name="Map border mesh"
ref={ref}
args={[undefined, undefined, w*h]}
args={[undefined, undefined, borderTilesCount]}
receiveShadow
>
<boxGeometry args={[1, 20, 1]} />
<meshStandardMaterial />
</instancedMesh>
);
}
}
7 changes: 4 additions & 3 deletions packages/client/src/gfx/MapLight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function MapLight(props: MapLightProps) {

return (
<group>
<directionalLight
<directionalLight
ref={lightRef}
// TODO time of day
position={[400, 180, 90]}
Expand Down Expand Up @@ -60,8 +60,9 @@ export default function useShadowHelper(

useEffect(() => {
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);
}
Expand All @@ -78,4 +79,4 @@ export default function useShadowHelper(
helper.current.update();
}
});
}
}
62 changes: 62 additions & 0 deletions packages/client/src/gfx/Projectile3D.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three';

import { Position, Milliseconds } from '@bananu7-rts/server/src/types'
import { ThreeCache } from './ThreeCache'

const cache = new ThreeCache();

export type ProjectileProps = {
origin: Position,
target: Position,
flightTime: Milliseconds,
flightTimeLeft: Milliseconds,
}

export function Projectile3D(props: ProjectileProps) {
const projectilePosition = new THREE.Vector3(props.origin.x, 5, props.origin.y);
const projectileRef = useRef<THREE.Mesh>(null);

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<number>(props.flightTimeLeft);

// if(time_since_fire * projectile_speed > distance(target, shot_location)) hit(target, projectile);

useFrame((s, dt) => {
if(!projectileRef.current)
return;

flightTimeLeft.current -= dt * 1000;
if (flightTimeLeft.current <= 0) {
projectileRef.current.visible = false;
return;
}

const range = 20;
const e = 1 - (flightTimeLeft.current / props.flightTime);
const y = parabolaHeight(range, 10, e);

projectileRef.current.position.lerpVectors(startPos, targetPos, e);
projectileRef.current.position.y = y;
});

return (
<mesh
ref={projectileRef}
material={cache.getStandardMaterial(0xaaaaaa)}
geometry={cache.getSphereGeometry(1.0)}
/>
);
}

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));
}
16 changes: 15 additions & 1 deletion packages/client/src/gfx/ThreeCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,21 @@ export class ThreeCache {
return geometry;
}
}


spheres: Map<number, THREE.SphereGeometry> = 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<number, THREE.MeshStandardMaterial> = new Map();
getStandardMaterial(color: number) {
const cached = this.standardMaterials.get(color);
Expand Down
Loading