From d0c4ae91e39578808fd607296a6859bb39f100c4 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Mon, 3 Oct 2022 21:33:35 +0200 Subject: [PATCH 01/17] Unit cards are now clickable in bottom unit view --- packages/client/src/App.css | 22 +++++++++++++++---- packages/client/src/App.tsx | 1 + packages/client/src/Minimap.tsx | 4 ++-- .../client/src/components/BottomUnitView.tsx | 20 ++++++++++++----- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/packages/client/src/App.css b/packages/client/src/App.css index bda4334..2ad0654 100644 --- a/packages/client/src/App.css +++ b/packages/client/src/App.css @@ -38,20 +38,21 @@ } .CommandPalette { + z-index: 2; position: absolute; right: 0; bottom: 0; width: 350px; height: 200px; - z-index: 2; - padding: 10px; display: grid; grid-template-columns: 25% 25% 25% 25%; grid-column-gap: 5px; + padding: 10px; gap: 5px; background-color: #555; + color: white; } .CommandPalette > button { @@ -72,12 +73,15 @@ } .MainMenu { + z-index: 2; width: 300px; position: absolute; top: 5%; right: 10px; - z-index: 2; + background-color: #555; + color: white; + display: flex; flex-direction: column; gap: 10px; @@ -92,13 +96,16 @@ } .BottomUnitView { + z-index: 2; position: absolute; bottom: 0; left: 320px; width: 400px; height: 200px; + background-color: #555; - z-index: 2; + color: white; + padding: 8px; overflow: hidden; } @@ -115,6 +122,10 @@ border: 1px solid black; } +.BottomUnitView .UnitIcon:hover { + background-color: lime; +} + .ResourceView { position: absolute; z-index: 2; @@ -123,12 +134,15 @@ width: 150px; height: 40px; padding: 10px; + background-color: #555; + color: white; } @media (prefers-color-scheme: light) { .card, .CommandPalette, .MainMenu, .BottomUnitView, .ResourceView { background-color: #eee; + color: black; } } diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index dce538b..0189daa 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -149,6 +149,7 @@ function App() { /> diff --git a/packages/client/src/Minimap.tsx b/packages/client/src/Minimap.tsx index f74f047..0f2e6a1 100644 --- a/packages/client/src/Minimap.tsx +++ b/packages/client/src/Minimap.tsx @@ -10,12 +10,12 @@ export function Minimap(props: Props) { const style : CSSProperties = { position: 'absolute', left: 0, - bottom: 5, - border: '5px solid black', + bottom: 0, width: '300px', height: '300px', backgroundColor: '#11cc11', boxSizing: 'border-box', + overflow: 'hidden', }; const unitStyle = { diff --git a/packages/client/src/components/BottomUnitView.tsx b/packages/client/src/components/BottomUnitView.tsx index f16b150..4e75e7d 100644 --- a/packages/client/src/components/BottomUnitView.tsx +++ b/packages/client/src/components/BottomUnitView.tsx @@ -3,6 +3,7 @@ import { Multiplayer } from '../Multiplayer' type Props = { selectedUnits: Set, + setSelectedUnits: (us: Set) => void, units: UnitState[], } @@ -28,7 +29,7 @@ export function BottomUnitView (props: Props) { return unit; }); - return (); + return ( props.setSelectedUnits(new Set([id]))} />); } })(); @@ -75,24 +76,33 @@ function SingleUnitView(props: {unit: UnitState}) { } // TODO select on click -function UnitIcon(props: {unit: UnitState}) { +function UnitIcon(props: {unit: UnitState, onClick: () => void}) { const u = props.unit; // TODO component getters to shared code const health = u.components.find(c => c.type === "Hp") as Hp | undefined; return ( -
+
{u.kind} { health && }
); } -function MultiUnitView(props: {units: UnitState[]}) { +function MultiUnitView(props: {units: UnitState[], select: (id: UnitId) => void }) { return (
- { props.units.map(u => ) } + { props.units.map(u => + props.select(u.id)} + />) + }
); } From aa26f9e4ecd506cffd7efdc5deef193922c7611b Mon Sep 17 00:00:00 2001 From: bananu7 Date: Mon, 3 Oct 2022 21:37:32 +0200 Subject: [PATCH 02/17] Selection box can now properly extend past the map edge --- packages/client/src/gfx/Map3D.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/gfx/Map3D.tsx b/packages/client/src/gfx/Map3D.tsx index 6564cc9..643c9c1 100644 --- a/packages/client/src/gfx/Map3D.tsx +++ b/packages/client/src/gfx/Map3D.tsx @@ -95,7 +95,7 @@ export function Map3D(props: Map3DProps) { onPointerMove={pointerMove} position={[0.5*w, 0, ySize*0.5*h]} > - + From 78d191780d540a0d7e0950806b50dd06a2ac65ff Mon Sep 17 00:00:00 2001 From: bananu7 Date: Mon, 3 Oct 2022 21:57:11 +0200 Subject: [PATCH 03/17] Moved Unit3D to a separate file, added larger click meshes for all units --- packages/client/src/gfx/Board3D.tsx | 113 +-------------------- packages/client/src/gfx/Unit3D.tsx | 146 ++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 112 deletions(-) create mode 100644 packages/client/src/gfx/Unit3D.tsx diff --git a/packages/client/src/gfx/Board3D.tsx b/packages/client/src/gfx/Board3D.tsx index 47bf9c1..0ffb432 100644 --- a/packages/client/src/gfx/Board3D.tsx +++ b/packages/client/src/gfx/Board3D.tsx @@ -9,122 +9,11 @@ import { import * as THREE from 'three'; -//import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' -//import { SkeletonUtils } from "three/examples/jsm/utils/SkeletonUtils" - import { Board, Unit, GameMap, UnitId, Position, UnitState } from 'server/types' import { SelectionCircle } from './SelectionCircle' import { Line3D } from './Line3D' import { Map3D, Box } from './Map3D' - -type Unit3DProps = { - unit: UnitState, - selected: boolean, - click?: (id: UnitId, button: number) => void, - enemy: boolean, -} -export function Unit3D(props: Unit3DProps) { - //const [catalog] = useState(() => require('../../assets/catalog.json')); - //const clone = useMemo(() => SkeletonUtils.clone(gltf.scene), [gltf]); - - const onClick = (e: ThreeEvent) => { - e.stopPropagation(); - - if (props.click) - props.click(props.unit.id, e.nativeEvent.button); - } - - // TODO better color choices - const ownerToColor = (owner: number) => { - switch(owner) { - case 0: return 0xdddddd; - case 1: return 0x1111ee; - case 2: return 0xee1111; - } - }; - - const color = ownerToColor(props.unit.owner); - - // TODO proper unit catalog - const isBuilding = props.unit.kind === 'Base' || props.unit.kind === 'Barracks'; - const unitSize = isBuilding ? 5 : 1; - - /* TODO - debug path view - const path = props.unit.actionQueue.map(a => { - return new THREE.Vector3(a.target.x, 1, a.target.y); - })*/ - - // smoothing - const unitGroupRef = useRef(null); - const softSnapVelocity = - unitGroupRef.current ? - { x: props.unit.position.x - unitGroupRef.current.position.x, - y: props.unit.position.y - unitGroupRef.current.position.z - } - : - { x: 0, y: 0 }; - - const SMOOTHING_TIME = 100; - const SMOOTHING_SCALE = 1000 / SMOOTHING_TIME; - - const smoothingVelocity = { - x: props.unit.velocity.x + softSnapVelocity.x * SMOOTHING_SCALE, - y: props.unit.velocity.y + softSnapVelocity.y * SMOOTHING_SCALE - } - - // TODO - this will be replaced with animations etc - let indicatorColor = 0xeeeeee; - if (props.unit.status === 'Moving') - indicatorColor = 0x55ff55; - else if (props.unit.status === 'Attacking') - indicatorColor = 0xff5555; - else if (props.unit.status === 'Harvesting') - indicatorColor = 0x5555ff; - // indicate discrepancy between server and us - else if (smoothingVelocity.x > 0 || smoothingVelocity.y > 0) - indicatorColor = 0xffff55; - - useFrame((s, dt) => { - if(!unitGroupRef.current) - return; - - // TODO - temporary fix to bring units where they're needed quickly - if (softSnapVelocity.x > 5 || softSnapVelocity.y > 5) { - unitGroupRef.current.position.x = props.unit.position.x; - unitGroupRef.current.position.z = props.unit.position.y; - return; - } - - unitGroupRef.current.position.x += smoothingVelocity.x * dt; - unitGroupRef.current.position.z += smoothingVelocity.y * dt; - }); - - return ( - - {/**/} - - - - - - - - - - { props.selected && } - - - ); -} +import { Unit3D } from './Unit3D' export interface Props { board: Board; diff --git a/packages/client/src/gfx/Unit3D.tsx b/packages/client/src/gfx/Unit3D.tsx new file mode 100644 index 0000000..4eeec1d --- /dev/null +++ b/packages/client/src/gfx/Unit3D.tsx @@ -0,0 +1,146 @@ +import { useEffect, useState, useRef, Suspense, useLayoutEffect, useMemo } from 'react' + +import { + useLoader, Canvas, useFrame, + useThree, + ReactThreeFiber, + ThreeEvent +} from '@react-three/fiber' + +import * as THREE from 'three'; + +//import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' +//import { SkeletonUtils } from "three/examples/jsm/utils/SkeletonUtils" + +import { Board, Unit, GameMap, UnitId, Position, UnitState } from 'server/types' +import { SelectionCircle } from './SelectionCircle' +import { Line3D } from './Line3D' +import { Map3D, Box } from './Map3D' + +function ConeIndicator(props: {unit: UnitState, smoothing: boolean}) { + // TODO - this will be replaced with animations etc + let indicatorColor = 0xeeeeee; + if (props.unit.status === 'Moving') + indicatorColor = 0x55ff55; + else if (props.unit.status === 'Attacking') + indicatorColor = 0xff5555; + else if (props.unit.status === 'Harvesting') + indicatorColor = 0x5555ff; + // indicate discrepancy between server and us + else if (props.smoothing) + indicatorColor = 0xffff55; + + return ( + + + + + ); +} + +type Unit3DProps = { + unit: UnitState, + selected: boolean, + click?: (id: UnitId, button: number) => void, + enemy: boolean, +} +export function Unit3D(props: Unit3DProps) { + //const [catalog] = useState(() => require('../../assets/catalog.json')); + //const clone = useMemo(() => SkeletonUtils.clone(gltf.scene), [gltf]); + + const onClick = (e: ThreeEvent) => { + e.stopPropagation(); + + if (props.click) + props.click(props.unit.id, e.nativeEvent.button); + } + + // TODO better color choices + const ownerToColor = (owner: number) => { + switch(owner) { + case 0: return 0xdddddd; + case 1: return 0x1111ee; + case 2: return 0xee1111; + } + }; + + const color = ownerToColor(props.unit.owner); + + // TODO proper unit catalog + const isBuilding = props.unit.kind === 'Base' || props.unit.kind === 'Barracks'; + const unitSize = isBuilding ? 5 : 1; + + /* TODO - debug path view + const path = props.unit.actionQueue.map(a => { + return new THREE.Vector3(a.target.x, 1, a.target.y); + })*/ + + // smoothing + const unitGroupRef = useRef(null); + const softSnapVelocity = + unitGroupRef.current ? + { x: props.unit.position.x - unitGroupRef.current.position.x, + y: props.unit.position.y - unitGroupRef.current.position.z + } + : + { x: 0, y: 0 }; + + const SMOOTHING_TIME = 100; + const SMOOTHING_SCALE = 1000 / SMOOTHING_TIME; + + const smoothingVelocity = { + x: props.unit.velocity.x + softSnapVelocity.x * SMOOTHING_SCALE, + y: props.unit.velocity.y + softSnapVelocity.y * SMOOTHING_SCALE + } + + useFrame((s, dt) => { + if(!unitGroupRef.current) + return; + + // TODO - temporary fix to bring units where they're needed quickly + if (softSnapVelocity.x > 5 || softSnapVelocity.y > 5) { + unitGroupRef.current.position.x = props.unit.position.x; + unitGroupRef.current.position.z = props.unit.position.y; + return; + } + + unitGroupRef.current.position.x += smoothingVelocity.x * dt; + unitGroupRef.current.position.z += smoothingVelocity.y * dt; + }); + + return ( + + {/**/} + + 0.01 || smoothingVelocity.y > 0.01} /> + + { /* Click mesh */ } + + + + + + { props.selected && + + } + + + + + + + + ); +} From 550ae0d8eb087485c68937a2ad3d04a0e1e69000 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Mon, 3 Oct 2022 23:09:34 +0200 Subject: [PATCH 04/17] Added command palette hints --- packages/client/src/App.css | 19 ++++++++++-- packages/client/src/App.tsx | 1 + .../client/src/components/CommandPalette.tsx | 31 +++++++++++++++++-- packages/client/src/index.css | 4 +-- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/packages/client/src/App.css b/packages/client/src/App.css index 2ad0654..55a956b 100644 --- a/packages/client/src/App.css +++ b/packages/client/src/App.css @@ -55,6 +55,19 @@ color: white; } +.CommandPaletteHint { + position: absolute; + top: -80px; + left: 0px; + width: 320px; + height: 50px; + border: 1px solid transparent; + border-radius: 20px; + padding: 10px; + backdrop-filter: blur(10px); + background-color: rgba(150, 150, 150, 0.3); +} + .CommandPalette > button { width: 75px; height: 75px; @@ -62,7 +75,7 @@ } .CommandPalette > button.active { - background-color: #33cc33; + background-color: #339933; } .chat { @@ -120,10 +133,12 @@ padding: 5px; border-radius: 3px; border: 1px solid black; + transition: 0.2s border-color; + cursor: pointer; } .BottomUnitView .UnitIcon:hover { - background-color: lime; + border-color: lime; } .ResourceView { diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 0189daa..8d8387e 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -159,6 +159,7 @@ function App() { playerIndex={multiplayer.getPlayerIndex() || 0} // TODO really need a match class to fix this undefined unitStates={lastUpdatePacket ? lastUpdatePacket.units : []} selectedUnits={selectedUnits} + selectedAction={selectedAction} select={boardSelectUnits} mapClick={mapClick} unitRightClick={unitRightClick} diff --git a/packages/client/src/components/CommandPalette.tsx b/packages/client/src/components/CommandPalette.tsx index 1140bd5..0854e2e 100644 --- a/packages/client/src/components/CommandPalette.tsx +++ b/packages/client/src/components/CommandPalette.tsx @@ -21,7 +21,7 @@ export type SelectedAction = type Props = { selectedUnits: Set, selectedAction: SelectedAction | undefined, - setSelectedAction: (a: SelectedAction) => void, + setSelectedAction: (a: SelectedAction | undefined) => void, units: UnitState[], multiplayer: Multiplayer, } @@ -45,8 +45,10 @@ export function CommandPalette(props: Props) { const canMove = Boolean(units[0].components.find(c => c.type === 'Mover')); const canAttack = Boolean(units[0].components.find(c => c.type === 'Attacker')); - const stop = () => + const stop = () => { props.multiplayer.stopCommand(Array.from(props.selectedUnits)); + props.setSelectedAction(undefined); + } const productionUnits = (() => { if (props.selectedUnits.size === 0) @@ -95,18 +97,43 @@ export function CommandPalette(props: Props) { // TODO - second click to determine position const buildButtons = availableBuildings.map(bp => { const b = bp.buildingType; + const active = + props.selectedAction && + props.selectedAction.action === 'Build' && + props.selectedAction.building === b; return ( ); }) + let hint = ""; + if (props.selectedAction) { + switch (props.selectedAction.action) { + case 'Build': + hint = `Right-click on the map to build a ${props.selectedAction.building}.`; + break; + case 'Move': + hint = "Right-click on the map to move, or on a unit to follow it."; + break; + case 'Attack': + hint = "Right-click on an enemy unit to attack it, or on the map to move-attack there."; + break; + case 'Harvest': + hint = "Right-click on a resource node to start harvesting it automatically."; + break; + } + } + + return (
+ { hint && {hint} } { canMove &&
} - { serverState && lastUpdatePacket && + { lastUpdatePacket && + lastUpdatePacket.state.id === 'Precount' && + + } + + { lastUpdatePacket && + lastUpdatePacket.state.id === 'Lobby' && +
+ Waiting for the other player to join +
+ } + + { lastUpdatePacket && + lastUpdatePacket.state.id === 'Paused' && +
+ Game paused +
+ } + + { serverState && + lastUpdatePacket && + (lastUpdatePacket.state.id === 'Precount' || lastUpdatePacket.state.id === 'Play' || lastUpdatePacket.state.id === 'Paused') + && <> + {Math.floor(props.count / 1000)} +
+ ); +} diff --git a/packages/server/game.ts b/packages/server/game.ts index dfcc642..744928d 100644 --- a/packages/server/game.ts +++ b/packages/server/game.ts @@ -116,6 +116,7 @@ export function tick(dt: Milliseconds, g: Game): UpdatePacket[] { tickNumber: g.tickNumber, units: unitUpdates, player: p, + state: g.state, } }); } diff --git a/packages/server/types.ts b/packages/server/types.ts index 6636d6a..5675dde 100644 --- a/packages/server/types.ts +++ b/packages/server/types.ts @@ -62,6 +62,7 @@ export type CommandPacket = { } export type UpdatePacket = { + state: GameState, tickNumber: number, units: UnitState[], player: PlayerState, From 794062a1f645cec2adb2e048946554bdc0fa98cf Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 4 Oct 2022 11:51:49 +0200 Subject: [PATCH 09/17] A small visual tweak with shadows to make the UI stand out a bit --- packages/client/src/App.css | 12 ++++++++++++ packages/client/src/Minimap.tsx | 1 + 2 files changed, 13 insertions(+) diff --git a/packages/client/src/App.css b/packages/client/src/App.css index 55a956b..9179a1c 100644 --- a/packages/client/src/App.css +++ b/packages/client/src/App.css @@ -53,6 +53,8 @@ gap: 5px; background-color: #555; color: white; + + box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); } .CommandPaletteHint { @@ -66,6 +68,8 @@ padding: 10px; backdrop-filter: blur(10px); background-color: rgba(150, 150, 150, 0.3); + + box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); } .CommandPalette > button { @@ -99,6 +103,8 @@ flex-direction: column; gap: 10px; padding: 10px; + + box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); } .MainMenuButton { @@ -106,6 +112,8 @@ top: 10px; right: 10px; z-index: 10; + + box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); } .BottomUnitView { @@ -121,6 +129,8 @@ padding: 8px; overflow: hidden; + + box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); } .BottomUnitView .MultiUnitView { @@ -152,6 +162,8 @@ background-color: #555; color: white; + + box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); } @media (prefers-color-scheme: light) { diff --git a/packages/client/src/Minimap.tsx b/packages/client/src/Minimap.tsx index 0f2e6a1..3dc179b 100644 --- a/packages/client/src/Minimap.tsx +++ b/packages/client/src/Minimap.tsx @@ -16,6 +16,7 @@ export function Minimap(props: Props) { backgroundColor: '#11cc11', boxSizing: 'border-box', overflow: 'hidden', + boxShadow: '0px 0px 37px 0px rgba(0,0,0,0.5)', }; const unitStyle = { From a47a94d5aae57093a36f7614f51d446e018db45a Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 4 Oct 2022 12:46:02 +0200 Subject: [PATCH 10/17] Don't allow producing units without appropriate resources --- packages/server/game.ts | 13 +++++++++---- packages/server/units.ts | 10 ++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/server/game.ts b/packages/server/game.ts index 744928d..12b3c31 100644 --- a/packages/server/game.ts +++ b/packages/server/game.ts @@ -17,8 +17,7 @@ export function newGame(map: GameMap): Game { state: {id: 'Lobby'}, tickNumber: 0, // TODO factor number of players in creation - // TODO making it 5000 for now until resources arrive - players: [{resources: 5000}, {resources: 5000}], + players: [{resources: 50}, {resources: 50}], board: { map: map, }, @@ -418,11 +417,17 @@ function updateUnit(dt: Milliseconds, g: Game, unit: Unit, presence: PresenceMap if (!p.productionState) { const utp = p.unitsProduced.find(up => up.unitType == cmd.unitToProduce); - const time = utp.productionTime; const cost = utp.productionCost; + if (cost > owner.resources) { + console.info("[game] Unit ordered to produce but player doesn't have enough resources"); + clearCurrentAction(); + break; + } + owner.resources -= cost; + const time = utp.productionTime; p.productionState = { unitType: cmd.unitToProduce, timeLeft: time @@ -442,7 +447,7 @@ function updateUnit(dt: Milliseconds, g: Game, unit: Unit, presence: PresenceMap )); // TODO - build queue - unit.actionQueue.shift(); + clearCurrentAction(); p.productionState = undefined; } diff --git a/packages/server/units.ts b/packages/server/units.ts index 3f0d0b5..828aa5c 100644 --- a/packages/server/units.ts +++ b/packages/server/units.ts @@ -77,5 +77,15 @@ export function createStartingUnits(): Unit[] { startingUnits.push(createUnit(lastUnitId++, 2, 'Base', {x:80, y:85})); startingUnits.push(createUnit(lastUnitId++, 2, 'Harvester', {x:64, y:90})); + // left expo + startingUnits.push(createUnit(lastUnitId++, 0, 'ResourceNode', {x:6, y:50})); + startingUnits.push(createUnit(lastUnitId++, 0, 'ResourceNode', {x:6, y:54})); + startingUnits.push(createUnit(lastUnitId++, 0, 'ResourceNode', {x:6, y:58})); + + // right expo + startingUnits.push(createUnit(lastUnitId++, 0, 'ResourceNode', {x:86, y:40})); + startingUnits.push(createUnit(lastUnitId++, 0, 'ResourceNode', {x:86, y:44})); + startingUnits.push(createUnit(lastUnitId++, 0, 'ResourceNode', {x:86, y:48})); + return startingUnits; } From d6e7dcc2cdd7178cb9ecf7e4b34fe1b1816c00c9 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 4 Oct 2022 13:38:26 +0200 Subject: [PATCH 11/17] Added a welcome message! --- packages/client/src/App.css | 6 ++++++ packages/client/src/App.tsx | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/client/src/App.css b/packages/client/src/App.css index 9179a1c..9438e7a 100644 --- a/packages/client/src/App.css +++ b/packages/client/src/App.css @@ -29,9 +29,14 @@ .card { padding: 2em; + padding-left: 12em; + padding-right: 12em; background-color: #555; } +.card p { + font-size: 1.2em; +} .read-the-docs { color: #888; @@ -68,6 +73,7 @@ padding: 10px; backdrop-filter: blur(10px); background-color: rgba(150, 150, 150, 0.3); + color: white; box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); } diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 2901109..a347ca4 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -133,6 +133,16 @@ function App() { { !serverState &&
+

Welcome to (for the lack of a better name) BartekRTS

+

To play, either join an existing match, or create a new one. You will + need two people to play; the game won't start until two people join. You can + only join matches in the "lobby" state, you can't join matches that have already started +

+

The game is designed to be able to be refreshed at any time. If you experience any + weird behavior or crashes, refreshing the page should help and will reconnect you + back to your game.

+

GLHF!

+
multiplayer.joinMatch(matchId)} />
From d4f9be883641b0bdedcbfb5ff40cb193e4bdeba5 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 4 Oct 2022 19:11:34 +0200 Subject: [PATCH 12/17] Command palette now checks whether the player can afford the action --- packages/client/src/App.tsx | 17 ++++++---- packages/client/src/components/Chat.tsx | 31 +++++++++++++++++++ .../client/src/components/CommandPalette.tsx | 30 +++++++++++++----- 3 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 packages/client/src/components/Chat.tsx diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index a347ca4..7d663c3 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -7,6 +7,7 @@ import { CommandPalette, SelectedAction } from './components/CommandPalette'; import { BottomUnitView } from './components/BottomUnitView'; import { ResourceView } from './components/ResourceView'; import { PrecountCounter } from './components/PrecountCounter' +import { Chat } from './components/Chat'; import { View3D } from './gfx/View3D'; import { Board3D } from './gfx/Board3D'; @@ -32,6 +33,8 @@ function App() { const [serverState, setServerState] = useState(null); const [lastUpdatePacket, setLastUpdatePacket] = useState(null); + + const [messages, setMessages] = useState([]); const updateMatchState = useCallback(() => { multiplayer.getMatchState() @@ -124,12 +127,12 @@ function App() { } - {/*
-
    - {lines} -
- -
*/} + { + multiplayer.sendChatMessage("lol")} + messages={messages} + /> + } { !serverState &&
@@ -174,11 +177,13 @@ function App() { <> setMessages(m => [...m, msg]) } /> void; +} + +export function Chat(props: Props) { + const [lastIndex, setLastIndex] = useState(0); + + // TODO - make chat disappear after some time + const messages = props.messages.slice(props.messages.length-1).map((m, i) => + {m} + ); + + const style = { + position: "absolute", + top: "70%", + left: "40%", + zIndex: "2", + display: "flex", + flexDirection: "column", + overflow: "hidden", + } as React.CSSProperties; + + return ( +
+ { messages } +
+ ); +} diff --git a/packages/client/src/components/CommandPalette.tsx b/packages/client/src/components/CommandPalette.tsx index 0854e2e..af4ad0c 100644 --- a/packages/client/src/components/CommandPalette.tsx +++ b/packages/client/src/components/CommandPalette.tsx @@ -19,11 +19,13 @@ export type SelectedAction = | { action: 'Harvest' }; type Props = { + resources: number, // used to check if the player can afford stuff selectedUnits: Set, selectedAction: SelectedAction | undefined, setSelectedAction: (a: SelectedAction | undefined) => void, units: UnitState[], multiplayer: Multiplayer, + notify: (text: string) => void, } export function CommandPalette(props: Props) { if (props.selectedUnits.size === 0) { @@ -61,20 +63,26 @@ export function CommandPalette(props: Props) { if (!productionComponent) return []; - return productionComponent.unitsProduced.map(up => up.unitType); + return productionComponent.unitsProduced; })(); // TODO: produce just one unit from the set like SC? const produce = (utype: string) => props.multiplayer.produceCommand(Array.from(props.selectedUnits), utype); - const productionButtons = productionUnits.map(ut => - - ); + onClick={click} + >Produce {up.unitType}); + }); const availableBuildings = (() => { // TODO if no selected units then doesn't make sense to repeat this check @@ -97,17 +105,23 @@ export function CommandPalette(props: Props) { // TODO - second click to determine position const buildButtons = availableBuildings.map(bp => { const b = bp.buildingType; + const cost = bp.buildCost; + const active = props.selectedAction && props.selectedAction.action === 'Build' && props.selectedAction.building === b; + const click = props.resources >= cost ? + () => build(b, {x: 50, y: 50}) : + () => props.notify("Not enough resources."); + return ( ); }) From 978d301e73d6ff185367480e8fc10dd9d2419c26 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 4 Oct 2022 20:15:46 +0200 Subject: [PATCH 13/17] Added command palette tooltips to all buttons - still need actual text for units --- packages/client/src/App.css | 27 +++++ packages/client/src/Minimap.tsx | 3 +- .../client/src/components/CommandPalette.tsx | 104 ++++++++++++++---- packages/client/src/index.css | 2 +- 4 files changed, 112 insertions(+), 24 deletions(-) diff --git a/packages/client/src/App.css b/packages/client/src/App.css index 9438e7a..51674e8 100644 --- a/packages/client/src/App.css +++ b/packages/client/src/App.css @@ -59,6 +59,7 @@ background-color: #555; color: white; + border-radius: 10px 0 0 0; box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); } @@ -88,6 +89,31 @@ background-color: #339933; } +.CommandPalette > button .tooltip { + opacity: 0; + width: 96%; + box-sizing: border-box; + background-color: #1a1a1a; + color: #fff; + text-align: center; + border-radius: 6px; + padding: 10px; + + box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); + transition: 0.2s opacity; + + /* Position the tooltip */ + position: absolute; + z-index: 1; + left: 50%; + bottom: 110%; + margin-left: -48%; +} + +.CommandPalette > button:hover .tooltip { + opacity: 1; +} + .chat { position: absolute; top: 60%; @@ -136,6 +162,7 @@ padding: 8px; overflow: hidden; + border-radius: 10px 10px 0 0; box-shadow: 0px 0px 37px 0px rgba(0,0,0,0.5); } diff --git a/packages/client/src/Minimap.tsx b/packages/client/src/Minimap.tsx index 3dc179b..f25f4eb 100644 --- a/packages/client/src/Minimap.tsx +++ b/packages/client/src/Minimap.tsx @@ -13,10 +13,11 @@ export function Minimap(props: Props) { bottom: 0, width: '300px', height: '300px', - backgroundColor: '#11cc11', + backgroundColor: '#11aa11', boxSizing: 'border-box', overflow: 'hidden', boxShadow: '0px 0px 37px 0px rgba(0,0,0,0.5)', + borderRadius: '0 10px 0 0', }; const unitStyle = { diff --git a/packages/client/src/components/CommandPalette.tsx b/packages/client/src/components/CommandPalette.tsx index af4ad0c..db185ee 100644 --- a/packages/client/src/components/CommandPalette.tsx +++ b/packages/client/src/components/CommandPalette.tsx @@ -18,6 +18,32 @@ export type SelectedAction = | { action: 'Build', building: string } | { action: 'Harvest' }; +type ButtonProps = { + x?: number, + y: number, + active: boolean, + onClick:() => void, + children: React.ReactNode, +} + +function Button(props: ButtonProps) { + const style = { + gridRow: `${props.y} / span 1`, + }; + + if (props.x) { + (style as any)["gridColumn"] = `${props.x} / span 1`; + } + + return ( + + ); +} + type Props = { resources: number, // used to check if the player can afford stuff selectedUnits: Set, @@ -77,11 +103,24 @@ export function CommandPalette(props: Props) { () => produce(up.unitType) : () => props.notify("Not enough resources."); - return (); + return ( + + ); }); const availableBuildings = (() => { @@ -110,19 +149,30 @@ export function CommandPalette(props: Props) { const active = props.selectedAction && props.selectedAction.action === 'Build' && - props.selectedAction.building === b; + props.selectedAction.building === b || false; const click = props.resources >= cost ? () => build(b, {x: 50, y: 50}) : () => props.notify("Not enough resources."); return ( - + > + Build {b} + + {b} + {cost}💰 +

+ This building probably does something, but that + information would need to be stored in a dictionary + somewhere and pulled in during button/tooltip creation. +
+ + ); }) @@ -150,28 +200,38 @@ export function CommandPalette(props: Props) { { hint && {hint} } { canMove && - + > + ➜ + Move a unit to a specific location or order it to follow a unit. + } - + > + ✖ + Stop the current action and all the queued ones. + { canAttack && - + > + 🪓 + Attack an enemy unit or move towards a point and attack any enemy units on the way + } { productionButtons } diff --git a/packages/client/src/index.css b/packages/client/src/index.css index 3c84c43..8530c11 100644 --- a/packages/client/src/index.css +++ b/packages/client/src/index.css @@ -47,7 +47,7 @@ button { transition: border-color 0.2s; } button:hover { - border-color: #64ff6c; + border-color: #ccc; } @media (prefers-color-scheme: light) { From f652113428b51c2e0b391508d7cd8fce9b93295c Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 4 Oct 2022 20:20:22 +0200 Subject: [PATCH 14/17] Tooltip will now show price in red when the player can't afford it --- packages/client/src/components/CommandPalette.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/CommandPalette.tsx b/packages/client/src/components/CommandPalette.tsx index db185ee..61ca085 100644 --- a/packages/client/src/components/CommandPalette.tsx +++ b/packages/client/src/components/CommandPalette.tsx @@ -99,7 +99,8 @@ export function CommandPalette(props: Props) { const productionButtons = productionUnits.map(up => { const cost = up.productionCost; - const click = props.resources >= cost ? + const canAfford = props.resources >= cost; + const click = canAfford ? () => produce(up.unitType) : () => props.notify("Not enough resources."); @@ -113,7 +114,7 @@ export function CommandPalette(props: Props) { Produce {up.unitType} {up.unitType} - {cost}💰 + {cost}💰

This excellent unit will serve you well, and I would tell you how but the tooltip data isn't @@ -151,7 +152,9 @@ export function CommandPalette(props: Props) { props.selectedAction.action === 'Build' && props.selectedAction.building === b || false; - const click = props.resources >= cost ? + const canAfford = props.resources >= cost; + + const click = canAfford ? () => build(b, {x: 50, y: 50}) : () => props.notify("Not enough resources."); @@ -165,7 +168,7 @@ export function CommandPalette(props: Props) { Build {b} {b} - {cost}💰 + {cost}💰

This building probably does something, but that information would need to be stored in a dictionary From b89d42b6b8880c2411243bf29b759eb68a7f40d6 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 4 Oct 2022 20:37:47 +0200 Subject: [PATCH 15/17] The server now tracks each player's channel individually and sends individual updates (about resources at least) --- packages/server/index.ts | 28 +++++++++++++++++++++++----- packages/server/types.ts | 5 ----- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/server/index.ts b/packages/server/index.ts index d26da25..ab127e8 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -1,11 +1,11 @@ -import geckos, { Data, iceServers } from '@geckos.io/server' +import geckos, { Data, iceServers, ServerChannel } from '@geckos.io/server' import http from 'http' import express from 'express' import cors from 'cors' import bodyParser from 'body-parser' import {newGame, startGame, tick, command} from './game.js'; -import {Game, MatchInfo, IdentificationPacket, CommandPacket, UpdatePacket, PlayerEntry } from './types.js'; +import {Game, MatchInfo, IdentificationPacket, CommandPacket, UpdatePacket, UserId } from './types.js'; import {getMap} from './map.js'; import {readFileSync} from 'fs'; @@ -17,6 +17,12 @@ catch (err) { } console.log(`Starting RTS server - ${version}`); +type PlayerEntry = { + index: number, + user: UserId, + channel?: ServerChannel, +} + type Match = { game: Game, matchId: string, @@ -71,9 +77,19 @@ app.post('/create', async (req, res) => { const TICK_MS = 50; setInterval(() => { const updatePackets = tick(TICK_MS, game); - // TODO - those updates can't be broadcasted, need a way - // to address players individually - io.room(matchId).emit('tick', updatePackets[0]); + const match = matches.find(m => m.matchId === matchId); + + if (!match) + throw new Error("Match scheduled for update doesn't exist"); + + match.players.forEach((p, i) => { + // TODO: handle players without channels better? + if (!p.channel) + return; + + p.channel.emit('tick', updatePackets[i]); + }); + // io.room(matchId).emit('tick', updatePackets[0]); }, TICK_MS); console.log(`Match ${matchId} created`); @@ -181,6 +197,8 @@ io.onConnection(channel => { return; } + playerEntry.channel = channel; + channel.join(String(packet.matchId)); channel.userData = { diff --git a/packages/server/types.ts b/packages/server/types.ts index 5675dde..a76101a 100644 --- a/packages/server/types.ts +++ b/packages/server/types.ts @@ -145,11 +145,6 @@ export type TilePos = { x: number, y: number } export type PlayerIndex = number export type UserId = string -export type PlayerEntry = { - index: number, - user: UserId, -} - export type Unit = { id: number, actionQueue: Action[], From cced2196873f95fbf33a5c2a638b6bafa50061b7 Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 4 Oct 2022 21:18:06 +0200 Subject: [PATCH 16/17] Added production progress bar and proper unit production cancelling. --- .../client/src/components/BottomUnitView.tsx | 37 +++++++++++++++++-- packages/server/game.ts | 19 +++++++++- packages/server/types.ts | 5 +++ 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/BottomUnitView.tsx b/packages/client/src/components/BottomUnitView.tsx index 4e75e7d..093112c 100644 --- a/packages/client/src/components/BottomUnitView.tsx +++ b/packages/client/src/components/BottomUnitView.tsx @@ -42,7 +42,7 @@ export function BottomUnitView (props: Props) { function HealthBar(props: {hp: number, maxHp: number}) { const outer = { - height: "1px", + height: "3px", width: "100%", backgroundColor: "black", }; @@ -58,19 +58,50 @@ function HealthBar(props: {hp: number, maxHp: number}) { ); } +function ProductionProgressBar(props: {percent: number}) { + const outer = { + height: "15px", + width: "100%", + backgroundColor: "transparent", + border: "1px solid #333", + borderRadius: "3px", + }; + const bar = { + width: `${props.percent}%`, + height: '100%', + backgroundColor: '#00ee00', + }; + return ( +
+
+
+ ); +} + + function SingleUnitView(props: {unit: UnitState}) { const health = props.unit.components.find(c => c.type === "Hp") as Hp | undefined; const productionComponent = props.unit.components.find(c => c.type === "ProductionFacility") as ProductionFacility; - const productionProgress = productionComponent?.productionState?.timeLeft; + const productionProgress = (() => { + if (!productionComponent) + return; + if (!productionComponent.productionState) + return; + + const left = productionComponent.productionState.timeLeft; + const full = productionComponent.productionState.originalTimeToProduce; + const percent = ((full-left)/full) * 100; + return percent; + })(); return (

{props.unit.kind}

{ health && } { health &&

{health.hp}/{health.maxHp}

} - { productionProgress &&

{productionProgress}

} {props.unit.status} + { productionProgress && }
); } diff --git a/packages/server/game.ts b/packages/server/game.ts index 12b3c31..2e6016c 100644 --- a/packages/server/game.ts +++ b/packages/server/game.ts @@ -176,6 +176,18 @@ function updateUnit(dt: Milliseconds, g: Game, unit: Unit, presence: PresenceMap unit.pathToNext = null; } + const cancelProduction = () => { + if (unit.actionQueue[0].typ === "Produce") { + unit.actionQueue.shift(); + const p = unit.components.find(c => c.type === "ProductionFacility") as ProductionFacility | undefined; + if (p) { + // refund + owner.resources += p.productionState.originalCost; + p.productionState = undefined; + } + } + } + const clearCurrentAction = () => { stopMoving(); unit.actionQueue.shift(); @@ -296,6 +308,9 @@ function updateUnit(dt: Milliseconds, g: Game, unit: Unit, presence: PresenceMap case 'Stop': { stopMoving(); + // TODO dedicated cancel action + cancelProduction(); + unit.actionQueue = []; break; } @@ -430,7 +445,9 @@ function updateUnit(dt: Milliseconds, g: Game, unit: Unit, presence: PresenceMap const time = utp.productionTime; p.productionState = { unitType: cmd.unitToProduce, - timeLeft: time + timeLeft: time, + originalCost: cost, + originalTimeToProduce: time, }; } diff --git a/packages/server/types.ts b/packages/server/types.ts index a76101a..76a9f28 100644 --- a/packages/server/types.ts +++ b/packages/server/types.ts @@ -120,6 +120,11 @@ export type UnitProductionCapability = { export type CurrentProductionState = { unitType: string, timeLeft: number, + + // TODO - im not sure about those two, but it was convenient to do it like that + // when in doubt - remove and pull that info from the unit blueprint + originalTimeToProduce: number, + originalCost: number, } export type ProductionFacility = { type: 'ProductionFacility', From 8615cdb9644ebd1acc862bceedbaee10361ce42f Mon Sep 17 00:00:00 2001 From: bananu7 Date: Tue, 4 Oct 2022 21:39:42 +0200 Subject: [PATCH 17/17] Reject invalid actions --- packages/server/game.ts | 51 +++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/server/game.ts b/packages/server/game.ts index 2e6016c..2917474 100644 --- a/packages/server/game.ts +++ b/packages/server/game.ts @@ -2,7 +2,7 @@ import { Milliseconds, GameMap, Game, PlayerIndex, Unit, UnitId, Component, CommandPacket, UpdatePacket, Position, TilePos, UnitState, Hp, Mover, Attacker, Harvester, ProductionFacility, Builder, - ActionFollow, ActionAttack, + Action, ActionFollow, ActionAttack, PlayerState, } from './types'; @@ -49,6 +49,13 @@ export function command(c: CommandPacket, g: Game, playerIndex: number) { return; } + // Don't even add/set actions that the unit won't accept + const accept = willAcceptAction(u, c.action); + if (!accept) { + console.info(`[game] Rejecting action ${c.action.typ} for unit ${u.id}`); + return; + } + console.log(`[game] Adding action ${c.action.typ} for unit ${u.id}`); if (c.shift) @@ -58,6 +65,34 @@ export function command(c: CommandPacket, g: Game, playerIndex: number) { }); } +function willAcceptAction(unit: Unit, action: Action) { + // TODO maybe this should be better streamlined, like in a dictionary + // of required components for each action? + switch(action.typ) { + case 'Move': + if (!getMoveComponent(unit)) + return false; + break; + case 'Attack': + if (!getAttackerComponent(unit)) + return false; + break; + case 'Harvest': + if (!getHarvesterComponent(unit)) + return false; + break; + case 'Build': + if (!getBuilderComponent(unit)) + return false; + break; + case 'Produce': + if (!getProducerComponent(unit)) + return false; + break; + } + return true; +} + // Returns a list of update packets, one for each player export function tick(dt: Milliseconds, g: Game): UpdatePacket[] { switch (g.state.id) { @@ -177,14 +212,12 @@ function updateUnit(dt: Milliseconds, g: Game, unit: Unit, presence: PresenceMap } const cancelProduction = () => { - if (unit.actionQueue[0].typ === "Produce") { - unit.actionQueue.shift(); - const p = unit.components.find(c => c.type === "ProductionFacility") as ProductionFacility | undefined; - if (p) { - // refund - owner.resources += p.productionState.originalCost; - p.productionState = undefined; - } + const p = unit.components.find(c => c.type === "ProductionFacility") as ProductionFacility | undefined; + if (p) { + // refund + console.log(`[game] Refunding production cost from unit ${unit.id}`); + owner.resources += p.productionState.originalCost; + p.productionState = undefined; } }