parent
fce9712698
commit
59c6fed541
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,61 @@
|
||||
.toolbar {
|
||||
padding-inline: 2em;
|
||||
}
|
||||
|
||||
.toolbar > ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
/* display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
max-width: calc(3 * 100px); */
|
||||
gap: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tool {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tool:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tool:not(.selected):hover, .tool:not(.selected):focus {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.tool.selected {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tool .background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.tool .background > svg, .tool .only-background > svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tool.selected:focus .background, .tool.selected:focus .only-background {
|
||||
filter: drop-shadow(0px 0px 4px white);
|
||||
}
|
||||
|
||||
.tool .icon, .tool .only-background {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.description {
|
||||
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
import { PLAYER_COLORS } from "../consts.js";
|
||||
import classes from "./Toolbar.module.css";
|
||||
import PlayerTile from "../tile-player-any.svg?raw";
|
||||
import { Buildings } from "../game.js";
|
||||
import { For, Match, Switch } from "solid-js";
|
||||
|
||||
const WIDTH = 173.2;
|
||||
const HEIGHT = 200;
|
||||
|
||||
export type Tool = {
|
||||
type: "placeBuilding",
|
||||
building: keyof typeof Buildings,
|
||||
};
|
||||
|
||||
export type ToolbarProps = {
|
||||
playerID: string,
|
||||
selectedTool: () => Tool,
|
||||
setSelectedTool: (tool: Tool) => void,
|
||||
resources: () => number,
|
||||
resourceGain: () => number,
|
||||
setReady: (ready: boolean) => void,
|
||||
ready: () => boolean,
|
||||
};
|
||||
|
||||
export default function Toolbar(props: ToolbarProps) {
|
||||
function isPlacingBuilding(building: keyof typeof Buildings) {
|
||||
const tool = props.selectedTool();
|
||||
if (tool.type !== "placeBuilding") return false;
|
||||
return tool.building === building;
|
||||
}
|
||||
|
||||
function selectBuilding(building: keyof typeof Buildings) {
|
||||
props.setSelectedTool({
|
||||
type: "placeBuilding",
|
||||
building,
|
||||
});
|
||||
}
|
||||
|
||||
return <nav class={classes.toolbar}>
|
||||
<ul>
|
||||
<ToolbarItem
|
||||
playerID={props.playerID}
|
||||
type="road"
|
||||
selected={() => isPlacingBuilding("road")}
|
||||
onSelect={() => selectBuilding("road")}
|
||||
/>
|
||||
<ToolbarItem
|
||||
playerID={props.playerID}
|
||||
type="pawn"
|
||||
selected={() => isPlacingBuilding("pawn")}
|
||||
onSelect={() => selectBuilding("pawn")}
|
||||
/>
|
||||
<ToolbarItem
|
||||
playerID={props.playerID}
|
||||
type="factory"
|
||||
selected={() => isPlacingBuilding("factory")}
|
||||
onSelect={() => selectBuilding("factory")}
|
||||
/>
|
||||
<li>
|
||||
Resources: {props.resources()} (+{props.resourceGain()})
|
||||
<br />
|
||||
<button onClick={() => props.setReady(!props.ready())}>
|
||||
{props.ready() ? "Waiting..." : "Ready"}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<Description selectedTool={props.selectedTool} />
|
||||
</nav>;
|
||||
}
|
||||
|
||||
|
||||
function ToolbarItem(props: {
|
||||
playerID: string,
|
||||
type: keyof typeof Buildings,
|
||||
selected: () => boolean,
|
||||
onSelect: () => void,
|
||||
}) {
|
||||
const color = PLAYER_COLORS[+props.playerID % PLAYER_COLORS.length];
|
||||
|
||||
return <li
|
||||
class={[classes.tool, props.selected() ? classes.selected : ""].filter(Boolean).join(" ")}
|
||||
tabindex={0}
|
||||
onClick={props.onSelect}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") props.onSelect();
|
||||
}}
|
||||
role="button"
|
||||
>
|
||||
<div
|
||||
innerHTML={PlayerTile}
|
||||
class={props.type !== "road" ? classes.background : classes["only-background"]}
|
||||
style={{
|
||||
"--player-color": color
|
||||
}}
|
||||
></div>
|
||||
{
|
||||
props.type !== "road" &&
|
||||
<img class={classes.icon} src={`/tile-${props.type}.svg`} width={WIDTH} height={HEIGHT} />
|
||||
}
|
||||
</li>;
|
||||
}
|
||||
|
||||
const TILE_DESCRIPTIONS = {
|
||||
road: "The most basic tile, can be placed on any empty tile adjacent to one of your tiles. Allows buildings to be constructed on it.",
|
||||
base: "Your HQ, if you lose it, you lose the game!",
|
||||
factory: "Gives a steady income of resources, but will explode the adjacent tiles if destroyed.",
|
||||
pawn: "Your first line of defense: has good HP and can attack a single adjacent tile to prevent enemy expansion to the border of your territory."
|
||||
} satisfies Record<keyof typeof Buildings, string>;
|
||||
|
||||
const TILE_NAMES = {
|
||||
road: "Road",
|
||||
base: "HQ",
|
||||
factory: "Factory",
|
||||
pawn: "Defender"
|
||||
} satisfies Record<keyof typeof Buildings, string>;
|
||||
|
||||
type KeyOf<T> = T extends object ? keyof T : never;
|
||||
type TryIndex<T, K extends string> = T extends Record<K, unknown> ? T[K] : never;
|
||||
|
||||
type Stats = {
|
||||
[K in KeyOf<typeof Buildings[keyof typeof Buildings]>]:
|
||||
(value: TryIndex<typeof Buildings[keyof typeof Buildings], K>) => string
|
||||
}
|
||||
|
||||
const STATS = {
|
||||
cost: (cost: number) => `Cost: ${cost}`,
|
||||
gain: (gain: number) => `Resource gain: ${gain}/turn`,
|
||||
placedOn: (tiles: readonly (keyof typeof Buildings)[]) => {
|
||||
const tileNames = tiles.map(tile => TILE_NAMES[tile]);
|
||||
if (tileNames.length <= 2) {
|
||||
return `Must be placed on ${tileNames.join(" or ")}`;
|
||||
} else {
|
||||
return `Must be placed on ${tileNames.slice(0, -1).join(", ")} or ${tileNames[tiles.length - 1]}`;
|
||||
}
|
||||
},
|
||||
} satisfies Stats;
|
||||
|
||||
function Description(props: {
|
||||
selectedTool: () => Tool
|
||||
}) {
|
||||
const stats = (building: keyof typeof Buildings) => {
|
||||
const keys = Object.keys(STATS) as (keyof typeof STATS)[];
|
||||
const buildingRules = Buildings[building];
|
||||
|
||||
return keys.flatMap((key) => {
|
||||
if (key in buildingRules) {
|
||||
// SAFETY: can be trivially proven by applying decidability on `key`.
|
||||
return [STATS[key]((buildingRules as Record<string, any>)[key])]
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
return <div class={classes.description}>
|
||||
<Switch fallback={<i>No tool selected</i>}>
|
||||
<Match when={props.selectedTool().type === "placeBuilding"}>
|
||||
{(_) => {
|
||||
const building = () => (props.selectedTool() as (Tool & { type: "placeBuilding" })).building;
|
||||
return (<>
|
||||
<h3>{TILE_NAMES[building()]}</h3>
|
||||
<p>{TILE_DESCRIPTIONS[building()]}</p>
|
||||
<ul class={classes.stats}>
|
||||
<For each={stats(building())}>
|
||||
{(item, index) => {
|
||||
return <li data-index={index()}>{item}</li>;
|
||||
}}
|
||||
</For>
|
||||
</ul>
|
||||
</>);
|
||||
}}
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import { BASE_URL } from "../consts.js";
|
||||
import { Buildings } from "../game.js";
|
||||
|
||||
export type NonRoads = Exclude<keyof typeof Buildings, "road">;
|
||||
export type TileImages = Record<NonRoads, HTMLImageElement>;
|
||||
|
||||
export async function loadTileImages(): Promise<TileImages> {
|
||||
const tiles = await Promise.all(
|
||||
(Object.keys(Buildings) as (keyof typeof Buildings)[])
|
||||
.flatMap((building): Promise<[NonRoads, HTMLImageElement]>[] => {
|
||||
if (building === "road") return [];
|
||||
return [new Promise((resolve, reject) => {
|
||||
const svg = document.createElement("img");
|
||||
svg.src = BASE_URL + `./tile-${building}.svg`;
|
||||
svg.addEventListener("load", () => {
|
||||
resolve([building, svg]);
|
||||
});
|
||||
svg.addEventListener("error", (err) => {
|
||||
console.error(`Error while loading image ${svg.src}: ${err}`);
|
||||
reject(err);
|
||||
});
|
||||
})];
|
||||
})
|
||||
);
|
||||
|
||||
return Object.fromEntries(tiles) as TileImages;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
|
||||
export const PLAYER_COLORS = ["#f06040", "#4050f0"];
|
||||
export const BASE_URL = "/";
|
After Width: | Height: | Size: 2.0 KiB |
Loading…
Reference in new issue