Skip to content

Commit

Permalink
Merge pull request #49 from bananu7/buildings-on-grid
Browse files Browse the repository at this point in the history
Buildings on grid
  • Loading branch information
bananu7 authored Jul 26, 2023
2 parents 448e8bc + 9f66c75 commit cb3d18c
Show file tree
Hide file tree
Showing 26 changed files with 1,118 additions and 689 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# RTS

An OpenSource RTS game.
An OpenSource RTS game.

## Important information about building on Windows

Unfortunately, the project doesn't build on Windows straight off NPM. Because of the native `node-datachannel` dependency, it requires CMake and OpenSSL to be manually installed and present.

I personally opt to add CMake to `PATH` (both for CMD and MSYS2 shells), and to enable CMake to find OpenSSL, the easiest way is to set `OPENSSL_ROOT_DIR` before running `npm install`. The "root" directory is the one containing the `bin` folder.
9 changes: 4 additions & 5 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "client",
"name": "@bananu7-rts/client",
"private": true,
"version": "0.0.0",
"type": "module",
Expand All @@ -11,13 +11,12 @@
},
"dependencies": {
"@geckos.io/client": "^2.2.3",
"@react-three/fiber": "^8.8.9",
"@types/three": "^0.144.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@react-three/fiber": "^8.8.9",
"three": "^0.144.0",

"server": "*"
"@bananu7-rts/server": "*",
"three": "^0.144.0"
},
"devDependencies": {
"@types/react": "^18.0.17",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/Multiplayer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import geckos, { Data, ClientChannel } from '@geckos.io/client'
import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Position } from 'server/src/types'
import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Position } from '@bananu7-rts/server/src/types'
import { HTTP_API_URL, GECKOS_URL, GECKOS_PORT } from './config'

export type OnChatMessage = (msg: string) => void;
Expand Down
5 changes: 4 additions & 1 deletion packages/client/src/components/BottomUnitView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Unit, UnitState, Position, ProductionFacility, Hp, Builder } from 'server/src/types'
import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Unit, UnitState, Position, ProductionFacility, Hp, Builder } from '@bananu7-rts/server/src/types'
import { Multiplayer } from '../Multiplayer'

type Props = {
Expand Down Expand Up @@ -110,6 +110,9 @@ function SingleUnitView(props: {unit: UnitState, owned: boolean}) {
<h2>{props.unit.kind}</h2>
{ health && <HealthBar hp={health.hp} maxHp={health.maxHp} /> }
{ health && <h3>{health.hp}/{health.maxHp}</h3> }
<span>{props.unit.id}</span><br/>
<span>{props.unit.position.x}</span><br/>
<span>{props.unit.position.y}</span>
{ props.owned && <span>{props.unit.status}</span> }
{ props.owned && productionProgress && <ProductionProgressBar percent={productionProgress} /> }
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Position,
ProductionFacility,
Builder,
} from 'server/src/types'
} from '@bananu7-rts/server/src/types'
import { MatchControl } from '../Multiplayer'
import { SelectedAction } from '../game/SelectedAction'

Expand Down
39 changes: 31 additions & 8 deletions packages/client/src/components/MatchController.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useState, useEffect, useCallback } from 'react'
import { ThreeEvent } from '@react-three/fiber'

import { SelectedAction } from '../game/SelectedAction';
import { canPerformSelectedAction } from '../game/UnitQuery';
import { clampToGrid } from '../game/Grid';

import { Minimap } from './Minimap';
import { CommandPalette } from './CommandPalette';
Expand All @@ -15,7 +17,8 @@ import { Board3D } from '../gfx/Board3D';

import { MatchControl } from '../Multiplayer';

import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Position } from 'server/src/types'
import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Position } from '@bananu7-rts/server/src/types'
import { mapEmptyForBuilding } from '@bananu7-rts/server/src/shared'

type MatchControllerProps = {
ctrl: MatchControl
Expand Down Expand Up @@ -97,10 +100,14 @@ export function MatchController(props: MatchControllerProps) {

const lines = msgs.map((m: string, i: number) => <li key={i}>{String(m)}</li>);

const mapClick = useCallback((p: Position, button: number, shift: boolean) => {
const mapClick = useCallback((originalEvent: ThreeEvent<MouseEvent>, p: Position, button: number, shift: boolean) => {
if (selectedUnits.size === 0)
return;

// TODO that's kinda annoying that server state is nullable here
if (!serverState)
return;

// TODO key being pressed and then RMB is attack move
switch (button) {
case 0:
Expand All @@ -113,7 +120,14 @@ export function MatchController(props: MatchControllerProps) {
} else if (selectedAction.action === 'Build') {
// Only send one harvester to build
// TODO send the closest one
props.ctrl.buildCommand([selectedUnits.keys().next().value], selectedAction.building, p, shift);
const gridPos = clampToGrid(p);
// TODO building size
const emptyForBuilding = mapEmptyForBuilding(serverState.board.map, 6, gridPos);
if (emptyForBuilding) {
props.ctrl.buildCommand([selectedUnits.keys().next().value], selectedAction.building, gridPos, shift);
} else {
console.log("[MatchController] trying to build in an invalid location")
}
}
break;
case 2:
Expand All @@ -123,15 +137,18 @@ export function MatchController(props: MatchControllerProps) {

setSelectedAction(undefined);

}, [selectedAction, selectedUnits]);
}, [serverState, selectedAction, selectedUnits]); // TODO will get recomputed on every new state, should it use ref?

const unitClick = useCallback((targetId: UnitId, button: number, shift: boolean) => {
if (!lastUpdatePacket)
const unitClick = useCallback((originalEvent: ThreeEvent<MouseEvent>, targetId: UnitId, button: number, shift: boolean) => {
if (!lastUpdatePacket) {
originalEvent.stopPropagation();
return;
}

const target = lastUpdatePacket.units.find(u => u.id === targetId);
if (!target) {
console.warn("A right click generated on a unit that does not exist");
console.warn("[MatchController] A right click generated on a unit that does not exist");
originalEvent.stopPropagation();
return;
}

Expand Down Expand Up @@ -164,7 +181,10 @@ export function MatchController(props: MatchControllerProps) {
break;
}

if (selectedAction.action === 'Move') {
if (selectedAction.action === 'Build') {
// propagate the event so that it hits the map instead
return;
} else if (selectedAction.action === 'Move') {
props.ctrl.followCommand(Array.from(selectedUnits), targetId, shift);
} else if (selectedAction.action === 'Attack') {
props.ctrl.attackCommand(Array.from(selectedUnits), targetId, shift);
Expand All @@ -186,6 +206,9 @@ export function MatchController(props: MatchControllerProps) {
}
break;
}

originalEvent.stopPropagation();

}, [lastUpdatePacket, selectedAction, selectedUnits]);

const boardSelectUnits = (newUnits: Set<UnitId>, shift: boolean) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/components/MatchList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { MatchInfo } from 'server/src/types'
import { MatchInfo } from '@bananu7-rts/server/src/types'
import { HTTP_API_URL } from '../config'

type Props = {
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/components/Minimap.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, CSSProperties } from 'react'
import { Board, UnitState } from 'server/src/types'
import { Board, UnitState } from '@bananu7-rts/server/src/types'

type Props = {
board: Board,
Expand Down
12 changes: 8 additions & 4 deletions packages/client/src/components/SpectateController.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
import { ThreeEvent } from '@react-three/fiber'

import { SelectedAction } from '../game/SelectedAction';
import { canPerformSelectedAction } from '../game/UnitQuery';
Expand All @@ -15,7 +16,7 @@ import { Board3D } from '../gfx/Board3D';

import { SpectatorControl } from '../Multiplayer';

import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Position } from 'server/src/types'
import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Position } from '@bananu7-rts/server/src/types'

type SpectateControllerProps = {
ctrl: SpectatorControl
Expand Down Expand Up @@ -61,13 +62,13 @@ export function SpectateController(props: SpectateControllerProps) {

const lines = msgs.map((m: string, i: number) => <li key={i}>{String(m)}</li>);

const mapClick = useCallback((p: Position, button: number, shift: boolean) => {
const mapClick = useCallback(() => {
if (selectedUnits.size === 0)
return;

setSelectedUnits(new Set());
}, [selectedUnits]);

const unitClick = useCallback((targetId: UnitId, button: number, shift: boolean) => {
const unitClick = useCallback((originalEvent: ThreeEvent<MouseEvent>, targetId: UnitId, button: number, shift: boolean) => {
if (!lastUpdatePacket)
return;

Expand Down Expand Up @@ -105,6 +106,9 @@ export function SpectateController(props: SpectateControllerProps) {
}
break;
}

originalEvent.stopPropagation();

}, [lastUpdatePacket, selectedUnits]);

const boardSelectUnits = (newUnits: Set<UnitId>, shift: boolean) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/client/src/debug/DebugApp.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useState, useEffect, useCallback, CSSProperties } from 'react'

import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Position } from 'server/src/types'
import { Game, CommandPacket, IdentificationPacket, UpdatePacket, UnitId, Position } from '@bananu7-rts/server/src/types'
import { Multiplayer, MatchControl, SpectatorControl } from '../Multiplayer';
import { Board, UnitState } from 'server/src/types'
import { Board, UnitState } from '@bananu7-rts/server/src/types'
import { MatchList } from '../components/MatchList';

const multiplayer = new Multiplayer("debug_user");
Expand Down
8 changes: 8 additions & 0 deletions packages/client/src/game/Grid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Position } from '@bananu7-rts/server/src/types'

export function clampToGrid(p: Position): Position{
return {
x: Math.floor(p.x/2)*2,
y: Math.floor(p.y/2)*2
}
}
2 changes: 1 addition & 1 deletion packages/client/src/game/UnitQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
UnitState,
ProductionFacility,
Builder,
} from 'server/src/types'
} from '@bananu7-rts/server/src/types'

import { SelectedAction } from './SelectedAction'

Expand Down
75 changes: 30 additions & 45 deletions packages/client/src/gfx/Board3D.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,47 +9,17 @@ import {

import * as THREE from 'three';

import { Board, Unit, GameMap, UnitId, Position, UnitState } from 'server/src/types'
import { Board, Unit, GameMap, UnitId, Position, UnitState, TilePos } from '@bananu7-rts/server/src/types'
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 { UNIT_DISPLAY_CATALOG, BuildingDisplayEntry } from './UnitDisplayCatalog'

import { SelectedAction } from '../game/SelectedAction'

function BuildPreview(props: {position: RefObject<Position>, building: string}) {
const unitSize = 5;

if (!props.position.current) {
return <></>;
}

const ref = useRef<THREE.Group>(null);
useFrame(() => {
if(!ref.current)
return;

if (!props.position.current)
return;

ref.current.position.x = props.position.current.x;
ref.current.position.z = props.position.current.y;
})

return (
<group ref={ref} position={[-100, 2, -100]}>
<mesh>
<boxGeometry args={[unitSize, 2, unitSize]} />
<meshBasicMaterial color={0x33cc33} transparent={true} opacity={0.5} />
</mesh>
<mesh>
<boxGeometry args={[unitSize, 2, unitSize]} />
<meshBasicMaterial color={0x00ff00} wireframe={true}/>
</mesh>
</group>
);
}

export interface Props {
board: Board;
playerIndex: number;
Expand All @@ -58,8 +28,8 @@ export interface Props {
selectedAction: SelectedAction | undefined;

select: (ids: Set<UnitId>, shift: boolean) => void;
mapClick: (p: Position, button: number, shift: boolean) => void;
unitClick: (u: UnitId, button: number, shift: boolean) => void;
mapClick: (originalEvent: ThreeEvent<MouseEvent>, p: Position, button: number, shift: boolean) => void;
unitClick: (originalEvent: ThreeEvent<MouseEvent>, u: UnitId, button: number, shift: boolean) => void;
}

export function Board3D(props: Props) {
Expand All @@ -69,14 +39,29 @@ export function Board3D(props: Props) {
pointer.current.y = p.y;
}, [pointer]);

const units = props.unitStates.map(u =>
(<Unit3D
key={u.id}
unit={u}
click={props.unitClick}
selected={props.selectedUnits.has(u.id)}
enemy={u.owner !== props.playerIndex}
/>));
const units = props.unitStates.map(u => {
const catalogEntryFn = UNIT_DISPLAY_CATALOG[u.kind];
if (!catalogEntryFn)
throw new Error("No display catalog entry for unit" + u.kind);
const catalogEntry = catalogEntryFn();

const unitProps = {
key: u.id,
unit: u,
click: props.unitClick,
selected: props.selectedUnits.has(u.id),
enemy: u.owner !== props.playerIndex
};

// this needs to be done separately because the if disambiguates the type
// of the retrieved catalogEntry
if (catalogEntry.isBuilding) {
return (<Building3D {...unitProps} displayEntry={catalogEntry} />);
} else {
return (<Unit3D {...unitProps} displayEntry={catalogEntry} />);
}
});


const groupRef = useRef<THREE.Group>(null);

Expand Down Expand Up @@ -113,7 +98,7 @@ export function Board3D(props: Props) {
{
props.selectedAction &&
props.selectedAction.action === 'Build' &&
<BuildPreview building={props.selectedAction.building} position={pointer}/>
<BuildPreview building={props.selectedAction.building} position={pointer} map={props.board.map}/>
}
</group>
);
Expand Down
Loading

0 comments on commit cb3d18c

Please sign in to comment.