345 lines
9.5 KiB
TypeScript
345 lines
9.5 KiB
TypeScript
import type { Entity, World } from "../../src/index";
|
|
import { query } from "../../src/query";
|
|
import type { ToolEffectRecorder } from "./stats";
|
|
import {
|
|
ActionCard,
|
|
CARRYING_TOOLS,
|
|
GameState,
|
|
Player,
|
|
Table,
|
|
Tool,
|
|
WoodenFishMarker,
|
|
allPlayersSelected,
|
|
getActionCard,
|
|
getLeftPlayer,
|
|
getPlayersFromMarker,
|
|
getPlayersInSeatOrder,
|
|
getRightPlayer,
|
|
getSelectedAction,
|
|
getToolOf,
|
|
type ActionKind,
|
|
} from "./components";
|
|
|
|
export interface RoundProposals {
|
|
fetchWater: boolean;
|
|
exchange: boolean;
|
|
drink: boolean;
|
|
chant: boolean;
|
|
}
|
|
|
|
export function beginSelectionPhase(world: World): void {
|
|
const state = world.getSingleton(GameState);
|
|
if (state.phase !== "gameOver") {
|
|
state.phase = "selecting";
|
|
state.message = `Round ${world.getSingleton(Table).round}: choose an action card.`;
|
|
}
|
|
}
|
|
|
|
export function selectAction(
|
|
world: World,
|
|
player: Entity,
|
|
kind: ActionKind,
|
|
): void {
|
|
const state = world.getSingleton(GameState);
|
|
if (state.phase !== "selecting") return;
|
|
if (!world.has(player, Player)) throw new Error("Invalid player");
|
|
|
|
const card = getActionCard(world, player, kind);
|
|
if (!card) throw new Error("Player does not have that action card");
|
|
|
|
const cardData = world.get(card, ActionCard);
|
|
if (cardData.zone === "cooldown") {
|
|
throw new Error("Cannot select the action card played last round");
|
|
}
|
|
|
|
for (const other of world.query(query(ActionCard))) {
|
|
const data = world.get(other, ActionCard);
|
|
if (data.owner === player && data.zone === "selected") {
|
|
data.zone = "hand";
|
|
}
|
|
}
|
|
|
|
cardData.zone = "selected";
|
|
|
|
if (allPlayersSelected(world)) {
|
|
state.message = "All players selected. Resolving round.";
|
|
}
|
|
}
|
|
|
|
export function collectProposals(world: World): RoundProposals {
|
|
const proposals: RoundProposals = {
|
|
fetchWater: false,
|
|
exchange: false,
|
|
drink: false,
|
|
chant: false,
|
|
};
|
|
|
|
for (const player of getPlayersInSeatOrder(world)) {
|
|
const action = getSelectedAction(world, player);
|
|
if (!action || action === "rest") continue;
|
|
proposals[action] = true;
|
|
}
|
|
|
|
return proposals;
|
|
}
|
|
|
|
export function resolveRound(world: World, stats?: ToolEffectRecorder): void {
|
|
const state = world.getSingleton(GameState);
|
|
if (state.phase === "gameOver") return;
|
|
|
|
state.phase = "resolving";
|
|
const proposals = collectProposals(world);
|
|
|
|
if (proposals.chant) resolveChant(world, stats);
|
|
if (proposals.fetchWater) resolveFetchWater(world, stats);
|
|
if (proposals.exchange) resolveExchange(world, stats);
|
|
if (proposals.drink) resolveDrink(world, stats);
|
|
if (world.getSingleton(GameState).phase === "gameOver") return;
|
|
|
|
resolveEndOfRoundTools(world, stats);
|
|
checkWinner(world);
|
|
}
|
|
|
|
export function prepareNextRound(world: World): void {
|
|
const state = world.getSingleton(GameState);
|
|
if (state.phase === "gameOver") return;
|
|
|
|
for (const card of world.query(query(ActionCard))) {
|
|
const data = world.get(card, ActionCard);
|
|
if (data.zone === "cooldown") data.zone = "hand";
|
|
}
|
|
|
|
for (const card of world.query(query(ActionCard))) {
|
|
const data = world.get(card, ActionCard);
|
|
if (data.zone === "selected") data.zone = "cooldown";
|
|
}
|
|
|
|
world.getSingleton(Table).round++;
|
|
state.phase = "selecting";
|
|
state.message = `Round ${world.getSingleton(Table).round}: choose an action card.`;
|
|
}
|
|
|
|
export function resolveFetchWater(
|
|
world: World,
|
|
stats?: ToolEffectRecorder,
|
|
): void {
|
|
const table = world.getSingleton(Table);
|
|
const players = getPlayersInSeatOrder(world);
|
|
const participants = players.filter((player) => {
|
|
const action = getSelectedAction(world, player);
|
|
const tool = world.get(getToolOf(world, player), Tool);
|
|
return (
|
|
action !== null && action !== "rest" && CARRYING_TOOLS.has(tool.kind)
|
|
);
|
|
});
|
|
|
|
for (const player of participants) {
|
|
const toolEntity = getToolOf(world, player);
|
|
const tool = world.get(toolEntity, Tool);
|
|
let placed = 2;
|
|
|
|
if (tool.kind === "bucket") placed = 1;
|
|
if (tool.kind === "woodenBucket" && participants.length > 1) placed = 0;
|
|
|
|
const playerData = world.get(player, Player);
|
|
if (playerData.water >= placed) {
|
|
playerData.water -= placed;
|
|
tool.water += placed;
|
|
stats?.record(tool.kind, "挑水", {
|
|
playerWaterDelta: -placed,
|
|
ownToolWaterDelta: placed,
|
|
});
|
|
} else {
|
|
stats?.record(tool.kind, "挑水", {}, "水不足,未放水");
|
|
}
|
|
}
|
|
|
|
table.centralWater += players.length - participants.length;
|
|
|
|
for (const player of participants) {
|
|
const tool = world.get(getToolOf(world, player), Tool);
|
|
if (tool.kind === "ladle" && table.centralWater > 0) {
|
|
table.centralWater--;
|
|
tool.water++;
|
|
stats?.record(tool.kind, "挑水", {
|
|
ownToolWaterDelta: 1,
|
|
centralWaterDelta: -1,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export function resolveExchange(
|
|
world: World,
|
|
stats?: ToolEffectRecorder,
|
|
): void {
|
|
const participants = getPlayersInSeatOrder(world).filter(
|
|
(player) => getSelectedAction(world, player) !== "chant",
|
|
);
|
|
if (participants.length <= 1) return;
|
|
|
|
const table = world.getSingleton(Table);
|
|
const tools = participants.map((player) => getToolOf(world, player));
|
|
|
|
for (const toolEntity of tools) {
|
|
const tool = world.get(toolEntity, Tool);
|
|
if (tool.kind === "shoulderPole" && tool.water > 0) {
|
|
const dumped = tool.water;
|
|
table.centralWater += dumped;
|
|
tool.water = 0;
|
|
stats?.record(tool.kind, "交换", {
|
|
ownToolWaterDelta: -dumped,
|
|
centralWaterDelta: dumped,
|
|
});
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < participants.length; i++) {
|
|
const giverTool = tools[i];
|
|
const receiver = participants[(i + 1) % participants.length];
|
|
world.get(giverTool, Tool).owner = receiver;
|
|
}
|
|
}
|
|
|
|
export function resolveDrink(world: World, stats?: ToolEffectRecorder): void {
|
|
for (const player of getPlayersFromMarker(world)) {
|
|
if (!canParticipateInDrink(world, player)) continue;
|
|
|
|
const table = world.getSingleton(Table);
|
|
const playerData = world.get(player, Player);
|
|
const tool = world.get(getToolOf(world, player), Tool);
|
|
let drankFromTool = 0;
|
|
|
|
if (table.centralWater > 0) {
|
|
table.centralWater--;
|
|
playerData.water++;
|
|
}
|
|
|
|
const maxFromTool =
|
|
tool.kind === "bigBucket" ? tool.water : Math.min(2, tool.water);
|
|
if (maxFromTool > 0) {
|
|
tool.water -= maxFromTool;
|
|
playerData.water += maxFromTool;
|
|
drankFromTool = maxFromTool;
|
|
stats?.record(tool.kind, "喝水", {
|
|
playerWaterDelta: maxFromTool,
|
|
ownToolWaterDelta: -maxFromTool,
|
|
});
|
|
}
|
|
|
|
if (tool.kind === "waterJar") {
|
|
playerData.water++;
|
|
stats?.record(tool.kind, "喝水", {
|
|
playerWaterDelta: 1,
|
|
});
|
|
}
|
|
|
|
if (
|
|
tool.kind === "bigBowl" &&
|
|
drankFromTool === 0 &&
|
|
table.centralWater > 0
|
|
) {
|
|
table.centralWater--;
|
|
tool.water++;
|
|
stats?.record(tool.kind, "喝水", {
|
|
ownToolWaterDelta: 1,
|
|
centralWaterDelta: -1,
|
|
});
|
|
}
|
|
|
|
if (playerData.water >= 10) {
|
|
setWinner(world, player);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function resolveChant(world: World, stats?: ToolEffectRecorder): void {
|
|
const chanters = getPlayersFromMarker(world).filter(
|
|
(player) => getSelectedAction(world, player) === "chant",
|
|
);
|
|
if (chanters.length === 0) return;
|
|
|
|
world.getSingleton(WoodenFishMarker).holder = chanters[chanters.length - 1];
|
|
|
|
for (const player of chanters) {
|
|
const toolEntity = getToolOf(world, player);
|
|
const tool = world.get(toolEntity, Tool);
|
|
|
|
if (tool.kind === "bottle") {
|
|
tool.water++;
|
|
stats?.record(tool.kind, "念经", {
|
|
ownToolWaterDelta: 1,
|
|
});
|
|
} else if (tool.kind === "mouse") {
|
|
const moved = moveOneNeighborWaterTo(world, player, toolEntity);
|
|
if (moved) {
|
|
stats?.record(tool.kind, "念经", {
|
|
ownToolWaterDelta: 1,
|
|
otherToolWaterDelta: -1,
|
|
});
|
|
} else {
|
|
stats?.record(tool.kind, "念经", {}, "相邻道具无水可偷");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function resolveEndOfRoundTools(
|
|
world: World,
|
|
stats?: ToolEffectRecorder,
|
|
): void {
|
|
for (const toolEntity of world.query(query(Tool))) {
|
|
const tool = world.get(toolEntity, Tool);
|
|
if (tool.kind === "woodenFish") {
|
|
world.getSingleton(WoodenFishMarker).holder = tool.owner;
|
|
stats?.record(tool.kind, "轮末", {}, "获得木鱼标记");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function checkWinner(world: World): Entity | null {
|
|
for (const player of getPlayersFromMarker(world)) {
|
|
if (world.get(player, Player).water >= 10) {
|
|
setWinner(world, player);
|
|
return player;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function canParticipateInDrink(world: World, player: Entity): boolean {
|
|
const tool = world.get(getToolOf(world, player), Tool);
|
|
if (tool.kind !== "waterJar") return true;
|
|
return getSelectedAction(world, player) === "drink";
|
|
}
|
|
|
|
function moveOneNeighborWaterTo(
|
|
world: World,
|
|
player: Entity,
|
|
destinationTool: Entity,
|
|
): boolean {
|
|
const neighbors = [
|
|
getLeftPlayer(world, player),
|
|
getRightPlayer(world, player),
|
|
];
|
|
for (const neighbor of neighbors) {
|
|
const neighborToolEntity = getToolOf(world, neighbor);
|
|
const neighborTool = world.get(neighborToolEntity, Tool);
|
|
if (neighborTool.water > 0) {
|
|
neighborTool.water--;
|
|
world.get(destinationTool, Tool).water++;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function setWinner(world: World, player: Entity): void {
|
|
const state = world.getSingleton(GameState);
|
|
state.phase = "gameOver";
|
|
state.winner = player;
|
|
state.message = `${world.get(player, Player).name} wins!`;
|
|
}
|