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 20 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
Copy link
Owner Author

Choose a reason for hiding this comment

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

leftover

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