Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Projectiles #103

Draft
wants to merge 30 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 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
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
1 change: 1 addition & 0 deletions packages/client/src/components/MatchController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
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
30 changes: 30 additions & 0 deletions packages/client/src/debug/ConeIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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}) {
// TODO - this will be replaced with animations etc
bananu7 marked this conversation as resolved.
Show resolved Hide resolved
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)}
/>
);
}
25 changes: 24 additions & 1 deletion packages/client/src/gfx/Board3D.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ 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'
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 +28,7 @@ export interface Props {
board: Board;
playerIndex: number;
units: Unit[];
projectiles: Projectile[],
selectedUnits: Set<UnitId>;
selectedCommand: SelectedCommand | undefined;

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

function Projectiles(props: { projectiles: Projectile[] }) {
const projectiles = props.projectiles.map(projectile => {
return (
<Projectile3D
position={projectile.origin}
target={projectile.target}
attackRate={500}
/>
)

}).filter(notEmpty);

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

64 changes: 64 additions & 0 deletions packages/client/src/gfx/Projectile3D.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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,
target: Position,
attackRate: number, // TODO this is just flight time?
}

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<THREE.Mesh>(null);

const tRef = useRef<number>(0);

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

const attackRate = props.attackRate;
const range = 20;
const e = tRef.current * (1000/attackRate);
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 (
<mesh
ref={projectileRef}
material={cache.getBasicMaterial(0xeeeeee)}
geometry={cache.getCylinderGeometry(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));
}
26 changes: 2 additions & 24 deletions packages/client/src/gfx/Unit3D.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
import * as THREE from 'three';

import { Board, Unit, GameMap, UnitId, Position, UnitAction } from '@bananu7-rts/server/src/types'

import { SelectionCircle } from './SelectionCircle'
import { Line3D } from './Line3D'
import { Map3D, Box } from './Map3D'
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();
Expand All @@ -28,30 +30,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 (
<mesh
position={[0, 5, 0]}
rotation={[0, 0, -1.57]}
geometry={coneGeometry}
material={cache.getBasicMaterial(indicatorColor)}
/>
);
}

type Unit3DProps = {
unit: Unit,
displayEntry: UnitDisplayEntry,
Expand Down
5 changes: 5 additions & 0 deletions packages/client/src/gfx/UnitDisplayCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion packages/server/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,19 @@ import { buildPresenceAndBuildingMaps } from './game/presence.js'

export function newGame(matchId: string, board: Board): Game {
const units = createStartingUnits(2, board);
const startingResources = 1500;
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

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,
projectiles: [],
lastUnitId: units.length,
lastProjectileId: 1,
winCondition: 'BuildingElimination',
}
}
Expand Down Expand Up @@ -235,6 +238,7 @@ export function tick(dt: Milliseconds, g: Game): UpdatePacket[] {
units: unitUpdates,
player: p,
state: g.state,
projectiles: g.projectiles,
}
});
}
Expand Down
29 changes: 23 additions & 6 deletions packages/server/src/game/unit/unit.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -64,17 +64,33 @@ 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;
}
}

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: {x: origin.x, y: origin.y },
})
}

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;
Expand All @@ -83,7 +99,8 @@ 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);

attemptDamage(gm, unit.position, ac, target);
}
// in any other case we can't do much else
}
Expand Down
15 changes: 11 additions & 4 deletions packages/server/src/game/units.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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 },
],
Expand All @@ -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, 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, kind: 'projectile' },
{ type: 'Vision', range: 10 },
]
};
Expand Down
Loading
Loading