mirror of
https://github.com/morten-olsen/shipped.git
synced 2026-02-07 23:26:23 +01:00
init
This commit is contained in:
42
packages/fleet-map/package.json
Normal file
42
packages/fleet-map/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@shipped/fleet-map",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build:cjs": "tsc -p tsconfig.json",
|
||||
"build:esm": "tsc -p tsconfig.esm.json",
|
||||
"build": "pnpm build:cjs && pnpm build:esm"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"types": "./dist/cjs/types/index.d.ts",
|
||||
"main": "./dist/esm/index.js",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"_exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/esm/types/index.d.ts",
|
||||
"default": "./dist/esm/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/cjs/types/index.d.ts",
|
||||
"default": "./dist/cjs/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@shipped/config": "workspace:^",
|
||||
"@types/react": "^18.0.28",
|
||||
"react": "^18.2.0",
|
||||
"react-native-svg": "^13.9.0",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@shipped/engine": "workspace:^",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"styled-components": "6.0.0-rc.1"
|
||||
}
|
||||
}
|
||||
67
packages/fleet-map/src/bridge/bridge.ts
Normal file
67
packages/fleet-map/src/bridge/bridge.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { MapTile, Vessel, Port } from '@shipped/engine';
|
||||
|
||||
type Update = {
|
||||
delta: State;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
type ConnectionEvents = {
|
||||
sync: (input: Sync) => void;
|
||||
update: (input: Update) => void;
|
||||
}
|
||||
|
||||
abstract class Connection extends EventEmitter<ConnectionEvents> {}
|
||||
|
||||
type BridgeEvents = {
|
||||
update: () => void;
|
||||
}
|
||||
|
||||
type World = {
|
||||
map: MapTile[][];
|
||||
size: { width: number, height: number };
|
||||
}
|
||||
|
||||
type State = {
|
||||
vessels: Vessel[];
|
||||
ports: Port[];
|
||||
}
|
||||
|
||||
type Sync = {
|
||||
world: World;
|
||||
state: State;
|
||||
}
|
||||
|
||||
class Bridge extends EventEmitter<BridgeEvents> {
|
||||
#connection: Connection
|
||||
#state?: State;
|
||||
#world?: World;
|
||||
|
||||
constructor(connection: Connection) {
|
||||
super();
|
||||
this.#connection = connection;
|
||||
this.#connection.on('sync', this.#onSync);
|
||||
this.#connection.on('update', this.#onUpdate);
|
||||
}
|
||||
|
||||
public get state() {
|
||||
return this.#state;
|
||||
}
|
||||
|
||||
public get world() {
|
||||
return this.#world;
|
||||
}
|
||||
|
||||
#onSync = (input: Sync) => {
|
||||
this.#state = input.state;
|
||||
this.#world = input.world;
|
||||
this.emit('update');
|
||||
}
|
||||
|
||||
#onUpdate = (input: Update) => {
|
||||
this.#state = input.delta;
|
||||
this.emit('update');
|
||||
}
|
||||
}
|
||||
|
||||
export { Bridge, Connection };
|
||||
23
packages/fleet-map/src/bridge/context.tsx
Normal file
23
packages/fleet-map/src/bridge/context.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createContext } from "react";
|
||||
import { Bridge } from "."
|
||||
|
||||
type BridgeContextValue = {
|
||||
bridge?: Bridge;
|
||||
}
|
||||
|
||||
type BridgeContextProviderProps = {
|
||||
bridge?: Bridge;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const BridgeContext = createContext<BridgeContextValue>(undefined as any);
|
||||
|
||||
const BridgeProvider: React.FC<BridgeContextProviderProps> = ({ bridge, children }) => {
|
||||
return (
|
||||
<BridgeContext.Provider value={{ bridge }}>
|
||||
{children}
|
||||
</BridgeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export { BridgeContext, BridgeProvider };
|
||||
41
packages/fleet-map/src/bridge/hooks.ts
Normal file
41
packages/fleet-map/src/bridge/hooks.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useContext, useEffect, useState } from "react"
|
||||
import { BridgeContext } from "./context"
|
||||
|
||||
const useBridgeState = () => {
|
||||
const { bridge } = useContext(BridgeContext);
|
||||
const [state, setState] = useState(bridge?.state);
|
||||
useEffect(() => {
|
||||
if (!bridge) return;
|
||||
const update = () => {
|
||||
setState(bridge.state);
|
||||
}
|
||||
bridge.on('update', update);
|
||||
return () => {
|
||||
bridge.off('update', update);
|
||||
};
|
||||
}, [bridge]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
const useBridgeWorld = () => {
|
||||
const { bridge } = useContext(BridgeContext);
|
||||
const [state, setState] = useState(bridge?.world);
|
||||
useEffect(() => {
|
||||
if (!bridge) return;
|
||||
const update = () => {
|
||||
setState(bridge.world);
|
||||
}
|
||||
bridge.on('update', update);
|
||||
return () => {
|
||||
bridge.off('update', update);
|
||||
};
|
||||
}, [bridge]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export {
|
||||
useBridgeState,
|
||||
useBridgeWorld,
|
||||
};
|
||||
3
packages/fleet-map/src/bridge/index.ts
Normal file
3
packages/fleet-map/src/bridge/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { BridgeProvider } from './context';
|
||||
export { useBridgeState, useBridgeWorld } from './hooks';
|
||||
export { Bridge, Connection } from './bridge';
|
||||
13
packages/fleet-map/src/index.ts
Normal file
13
packages/fleet-map/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export {
|
||||
FleetMap,
|
||||
} from './ui';
|
||||
export {
|
||||
VesselInfo,
|
||||
} from './ui/vessel';
|
||||
export {
|
||||
Bridge,
|
||||
Connection,
|
||||
BridgeProvider,
|
||||
useBridgeState,
|
||||
useBridgeWorld,
|
||||
} from './bridge';
|
||||
19
packages/fleet-map/src/ui/index.tsx
Normal file
19
packages/fleet-map/src/ui/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { FC } from "react";
|
||||
import { FleetMapWorld } from "./world";
|
||||
import { FleetMapState } from "./state";
|
||||
import { useBridgeWorld } from "../bridge";
|
||||
|
||||
const FleetMap: FC = () => {
|
||||
const world = useBridgeWorld();
|
||||
|
||||
if (!world) return null;
|
||||
|
||||
return (
|
||||
<svg viewBox={`0 0 ${world.size.width} ${world.size.height}`}>
|
||||
<FleetMapWorld />
|
||||
<FleetMapState />
|
||||
</svg>
|
||||
)
|
||||
};
|
||||
|
||||
export { FleetMap }
|
||||
71
packages/fleet-map/src/ui/state.tsx
Normal file
71
packages/fleet-map/src/ui/state.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
|
||||
import { FC, Fragment } from "react";
|
||||
import { Vessel, getVesselStats } from "@shipped/engine";
|
||||
import { useBridgeState, useBridgeWorld } from "../bridge";
|
||||
|
||||
const getStatusColor = (vessel: Vessel) => {
|
||||
if (vessel.fuel.current <= 0) return "#e74c3c";
|
||||
if (vessel.fuel.current / vessel.fuel.capacity <= 0.2) return "#f39c12";
|
||||
return "#fefefe";
|
||||
}
|
||||
|
||||
const FleetMapState: FC = () => {
|
||||
const world = useBridgeWorld();
|
||||
const state = useBridgeState();
|
||||
|
||||
if (!world || !state) return null;
|
||||
|
||||
const cellWidth = world.size.width / world.map[0].length;
|
||||
const cellHeight = world.size.height / world.map.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
{state.vessels.map((vessel, i) => (
|
||||
<g key={i}>
|
||||
{vessel.plan && vessel.plan.length > 0 && (
|
||||
<>
|
||||
<path
|
||||
d={`M${vessel.position.x * cellWidth + (cellWidth / 2)},${vessel.position.y * cellHeight + (cellHeight / 2)}L${vessel.plan.map(({ x, y }) => `${x * cellWidth + (cellWidth / 2)},${y * cellHeight + (cellHeight / 2)}`).join("L")}`}
|
||||
style={{ opacity: 0.3, transition: "transform 1s" }}
|
||||
stroke="#0a3d62"
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray="0.4 0.4"
|
||||
strokeLinecap="round"
|
||||
strokeWidth={0.2}
|
||||
fill="none"
|
||||
/>
|
||||
{vessel.plan.map(({ x, y }, i) => (
|
||||
<circle
|
||||
opacity={0.6}
|
||||
key={i}
|
||||
cx={x * cellWidth + (cellWidth / 2)}
|
||||
cy={y * cellHeight + (cellHeight / 2)}
|
||||
r={0.15}
|
||||
fill="#0a3d62"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</g>
|
||||
))}
|
||||
{state.vessels.map((vessel, i) => (
|
||||
<g key={i}>
|
||||
<g
|
||||
onMouseEnter={() => console.log(vessel)}
|
||||
transform={`translate(${vessel.position.x * cellWidth + (cellWidth / 2)},${vessel.position.y * cellHeight + (cellHeight / 2)}) rotate(${(vessel.direction || 0) * (180/Math.PI)})`}
|
||||
style={{ transition: "transform 0.3s" }}
|
||||
>
|
||||
<path
|
||||
d="M-1.5,-.5 L1.0,-.5 L1.5,0 L1.0,.5, L-1.5,.5 Z"
|
||||
stroke="#666"
|
||||
strokeWidth={0.1}
|
||||
fill={getStatusColor(vessel)}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export { FleetMapState }
|
||||
96
packages/fleet-map/src/ui/vessel/index.tsx
Normal file
96
packages/fleet-map/src/ui/vessel/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Vessel } from "@shipped/engine"
|
||||
import styled from "styled-components";
|
||||
|
||||
type Props = {
|
||||
vessel: Vessel;
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const Card = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
border-bottom: 0.5px solid #fff;
|
||||
`;
|
||||
|
||||
const Value = styled.div`
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
`;
|
||||
|
||||
const Label = styled.div`
|
||||
font-size: 0.5rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Unit = styled.span`
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-top: 1rem;
|
||||
`;
|
||||
|
||||
function numberWithCommas(x: number) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
const VesselInfo: React.FC<Props> = ({ vessel }) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Header>Stats</Header>
|
||||
<Card>
|
||||
<Label>Fuel</Label>
|
||||
<Value>{(vessel.fuel.current / vessel.fuel.capacity * 100).toFixed(2)}<Unit>%</Unit></Value>
|
||||
</Card>
|
||||
<Card>
|
||||
<Label>Cash</Label>
|
||||
<Value>{numberWithCommas(vessel.cash)}<Unit>$</Unit></Value>
|
||||
</Card>
|
||||
<Card>
|
||||
<Label>Goods</Label>
|
||||
<Value>{vessel.goods}<Unit>kg</Unit></Value>
|
||||
</Card>
|
||||
<Card>
|
||||
<Label>Power</Label>
|
||||
<Value>{(vessel.power * 100).toFixed(1)}<Unit>%</Unit></Value>
|
||||
</Card>
|
||||
<Card>
|
||||
<Label>Score</Label>
|
||||
<Value>{(vessel.cash / (vessel.score.fuelUsed / vessel.score.rounds)).toFixed(0)}<Unit>$/F/R</Unit></Value>
|
||||
</Card>
|
||||
|
||||
<Header>History</Header>
|
||||
<Card>
|
||||
<Label>Distance travelled</Label>
|
||||
<Value>{(vessel.score.distanceTravelled).toFixed(1)}</Value>
|
||||
</Card>
|
||||
<Card>
|
||||
<Label>Fuel used</Label>
|
||||
<Value>{(vessel.score.fuelUsed).toFixed(1)}</Value>
|
||||
</Card>
|
||||
<Card>
|
||||
<Label>Rounds</Label>
|
||||
<Value>{vessel.score.rounds}</Value>
|
||||
</Card>
|
||||
</Wrapper>
|
||||
)
|
||||
};
|
||||
|
||||
export { VesselInfo }
|
||||
39
packages/fleet-map/src/ui/world.tsx
Normal file
39
packages/fleet-map/src/ui/world.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { FC } from "react";
|
||||
import { useBridgeWorld } from "../bridge";
|
||||
|
||||
const getTileColor = (type: string) => {
|
||||
if (type === "land") return "#78e08f";
|
||||
return "#e58e26";
|
||||
}
|
||||
|
||||
const inset = 0.07;
|
||||
|
||||
const FleetMapWorld: FC = () => {
|
||||
const world = useBridgeWorld();
|
||||
|
||||
if (!world) return null;
|
||||
|
||||
const cellWidth = world.size.width / world.map[0].length;
|
||||
const cellHeight = world.size.height / world.map.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<rect width={world.size.width * cellWidth} height={world.size.height * cellHeight} fill="#6a89cc" />
|
||||
{world.map.map((row, y) => row.map((cell, x) => ( cell.type === 'water' ? null :
|
||||
<rect
|
||||
key={`${x},${y}`}
|
||||
x={x * cellWidth + inset}
|
||||
y={y * cellHeight + inset}
|
||||
rx={0.2}
|
||||
width={cellWidth - inset * 2}
|
||||
height={cellHeight - inset * 2}
|
||||
stroke="#666"
|
||||
strokeWidth={0.1}
|
||||
fill={getTileColor(cell.type)}
|
||||
/>
|
||||
))).flat()}
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
export { FleetMapWorld }
|
||||
10
packages/fleet-map/tsconfig.esm.json
Normal file
10
packages/fleet-map/tsconfig.esm.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "@shipped/config/esm",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/esm",
|
||||
"declarationDir": "./dist/esm/types"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
8
packages/fleet-map/tsconfig.json
Normal file
8
packages/fleet-map/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@shipped/config/cjs",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist/cjs",
|
||||
"declarationDir": "./dist/cjs/types"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user