parent
d91f2b34e3
commit
fa07df8452
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.5 KiB |
@ -0,0 +1,100 @@
|
||||
import { createEffect, createSignal, onCleanup } from "solid-js";
|
||||
import { applyMovesGenerator, type State } from "../../game.ts";
|
||||
import { unwrap } from "solid-js/store";
|
||||
import clone from "../../clone.ts";
|
||||
|
||||
export type AnimationState<T> = {
|
||||
running: true,
|
||||
state: T
|
||||
} | {
|
||||
running: false,
|
||||
state: undefined
|
||||
};
|
||||
|
||||
export function useAnimationQueue<T>(
|
||||
delay: number = 1000,
|
||||
callback: (animationState: AnimationState<T>) => void,
|
||||
) {
|
||||
const generatorQueue: Generator<T, T | undefined, unknown>[] = [];
|
||||
const [running, setRunning] = createSignal(false);
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | undefined = undefined;
|
||||
|
||||
function scheduler() {
|
||||
if (generatorQueue.length === 0) {
|
||||
clearInterval(interval);
|
||||
interval = undefined;
|
||||
setRunning(false);
|
||||
callback({
|
||||
running: false,
|
||||
state: undefined,
|
||||
});
|
||||
} else {
|
||||
const generator = generatorQueue[0]!;
|
||||
const next = generator.next();
|
||||
|
||||
if (next.done) {
|
||||
generatorQueue.shift();
|
||||
}
|
||||
|
||||
if (next.value !== undefined) {
|
||||
callback({
|
||||
running: true,
|
||||
state: next.value
|
||||
});
|
||||
} else {
|
||||
setImmediate(scheduler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
interval = undefined;
|
||||
setRunning(false);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
start(generator: Generator<T, T | undefined, unknown>) {
|
||||
generatorQueue.push(generator);
|
||||
|
||||
if (!interval) {
|
||||
interval = setInterval(scheduler, delay);
|
||||
setRunning(true);
|
||||
}
|
||||
},
|
||||
running,
|
||||
};
|
||||
}
|
||||
|
||||
export default function useAnimation(
|
||||
state: State,
|
||||
delay: number,
|
||||
) {
|
||||
const [temporaryState, setTemporaryState] = createSignal<State>();
|
||||
const {
|
||||
start,
|
||||
running,
|
||||
} = useAnimationQueue<State>(delay, (animationState) => {
|
||||
setTemporaryState(clone(animationState.state));
|
||||
});
|
||||
|
||||
return {
|
||||
start() {
|
||||
const stateClone = clone(unwrap(state));
|
||||
const generator: Generator<State, State, unknown> = applyMovesGenerator(stateClone, true);
|
||||
setTemporaryState(clone(stateClone));
|
||||
start(generator);
|
||||
},
|
||||
running,
|
||||
state: temporaryState,
|
||||
get(): AnimationState<State> {
|
||||
return {
|
||||
running: running(),
|
||||
state: temporaryState()
|
||||
} as AnimationState<State>;
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import { createEffect, type Accessor, onCleanup } from "solid-js";
|
||||
import * as draw from "./draw.ts";
|
||||
import * as game from "../../game.ts";
|
||||
|
||||
export type UseBoardMouse = {
|
||||
canvas: Accessor<HTMLCanvasElement | undefined>,
|
||||
state: game.State,
|
||||
getTransform: Accessor<draw.Transform>,
|
||||
setHoveredCell: (cell: [x: number, y: number] | null) => void,
|
||||
clickTile: (x: number, y: number) => void,
|
||||
};
|
||||
|
||||
export default function useBoardMouse(options: UseBoardMouse) {
|
||||
createEffect(() => {
|
||||
const c = options.canvas();
|
||||
|
||||
if (!c) return;
|
||||
|
||||
function getTileBelowMouse(event: MouseEvent): [number, number] | null {
|
||||
const transform = options.getTransform();
|
||||
const bounds = c!.getBoundingClientRect();
|
||||
|
||||
const x = ((event.clientX - bounds.left) / window.devicePixelRatio - transform.sx) / transform.scale;
|
||||
const y = ((event.clientY - bounds.top) / window.devicePixelRatio - transform.sy) / transform.scale;
|
||||
|
||||
const [hexX, hexY] = draw.inverseHexPosition(x, y);
|
||||
if (game.cellIn(options.state.cells, hexX, hexY)) {
|
||||
return [hexX, hexY];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function mouseDown(event: MouseEvent) {
|
||||
const tile = getTileBelowMouse(event);
|
||||
if (tile) {
|
||||
options.clickTile(tile[0], tile[1]);
|
||||
}
|
||||
}
|
||||
|
||||
function mouseMove(event: MouseEvent) {
|
||||
const tile = getTileBelowMouse(event);
|
||||
options.setHoveredCell(tile);
|
||||
}
|
||||
|
||||
c.addEventListener("mousedown", mouseDown);
|
||||
c.addEventListener("mousemove", mouseMove);
|
||||
c.addEventListener("mouseleave", () => options.setHoveredCell(null));
|
||||
onCleanup(() => {
|
||||
c.removeEventListener("mousedown", mouseDown);
|
||||
c.removeEventListener("mousemove", mouseMove);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import { createMemo, type Accessor, type Resource, createResource } from "solid-js";
|
||||
import * as game from "../../game.js";
|
||||
import { loadTileImages, type TileImages } from "../tiles.js";
|
||||
import * as draw from "./draw.js";
|
||||
import type { Tool } from "../Toolbar.js";
|
||||
import type { AnimationState } from "./useAnimation.ts";
|
||||
|
||||
export type UseDrawBoardOptions = {
|
||||
canvas: Accessor<HTMLCanvasElement | undefined>,
|
||||
state: game.State,
|
||||
getTransform: Accessor<draw.Transform>,
|
||||
hoveredCell: Accessor<[x: number, y: number] | null>,
|
||||
selectedTool: Accessor<Tool>,
|
||||
getAnimationState: Accessor<AnimationState<game.State>>,
|
||||
playerID?: string | undefined,
|
||||
targetTiles: Accessor<[x: number, y: number][]>,
|
||||
};
|
||||
|
||||
export default function useDrawBoard(options: UseDrawBoardOptions) {
|
||||
const {
|
||||
canvas,
|
||||
state,
|
||||
getTransform,
|
||||
hoveredCell,
|
||||
selectedTool,
|
||||
getAnimationState,
|
||||
playerID,
|
||||
targetTiles
|
||||
} = options;
|
||||
|
||||
const [tileImages] = createResource(loadTileImages);
|
||||
|
||||
const cells = createMemo(() => {
|
||||
const animationState = getAnimationState();
|
||||
if (animationState.running) {
|
||||
return animationState.state.cells;
|
||||
} else if (playerID) {
|
||||
return game.getLocalState(state, playerID);
|
||||
} else {
|
||||
return state.cells;
|
||||
}
|
||||
});
|
||||
|
||||
const animationRunning = () => getAnimationState().running;
|
||||
|
||||
return function drawBoard() {
|
||||
const ctx = canvas()?.getContext("2d");
|
||||
const images = tileImages();
|
||||
if (!ctx || !("cells" in state) || !images) return;
|
||||
|
||||
ctx.imageSmoothingQuality = "high";
|
||||
|
||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const transform = getTransform();
|
||||
const transformedCtx = new draw.TransformedCanvas2DCtx(ctx, transform);
|
||||
|
||||
if (hoveredCell() && !animationRunning()) {
|
||||
const [x, y] = hoveredCell()!;
|
||||
const tool = selectedTool();
|
||||
let valid = true;
|
||||
|
||||
if (playerID) {
|
||||
// TODO: pass this logic as a parameter
|
||||
if (tool.type === "placeBuilding") {
|
||||
valid = game.canPlaceBuilding(
|
||||
state.cells,
|
||||
game.remainingResources(state.cells, state.resources, state.moves, playerID),
|
||||
playerID,
|
||||
x,
|
||||
y,
|
||||
tool.building
|
||||
);
|
||||
} else if (tool.type === "attack" && tool.selected) {
|
||||
valid = !!targetTiles().find(([x2, y2]) => x2 === x && y2 === y);
|
||||
} else if (tool.type === "attack" && !tool.selected) {
|
||||
valid = game.canAttackFrom(
|
||||
state.cells,
|
||||
game.remainingResources(state.cells, state.resources, state.moves, playerID),
|
||||
playerID,
|
||||
x,
|
||||
y
|
||||
);
|
||||
}
|
||||
}
|
||||
draw.drawCellOutline(transformedCtx, x, y, valid);
|
||||
}
|
||||
|
||||
for (const [x, y, cell] of game.iterateCells(cells())) {
|
||||
draw.drawCell(transformedCtx, cell, x, y, images);
|
||||
if (cell.active) {
|
||||
draw.drawCellOutline(transformedCtx, x, y, true);
|
||||
}
|
||||
if (cell.target) {
|
||||
draw.drawCellTarget(transformedCtx, x, y, images);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [x, y] of targetTiles()) {
|
||||
draw.drawCellTarget(transformedCtx, x, y, images);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue