Files
mini-zmc/services/game-service.js
2026-07-29 10:50:33 +08:00

813 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const {
DEFAULT_SETTINGS,
BOT_NAMES,
AREA_CLUES
} = require("../utils/constants");
const { randomCode, clone } = require("../utils/format");
const ROOMS_KEY = "zmc_rooms";
const CURRENT_ROOM_KEY = "zmc_current_room";
const USER_ID = "player-me";
const ROOM_SCHEMA_VERSION = 2;
const ROOM_STATUSES = ["waiting", "preparing", "playing", "paused", "finished", "cancelled"];
const PLAYER_ROLES = ["", "seeker", "hider"];
const PLAYER_STATUSES = ["idle", "ready", "hiding", "seeking", "caught", "spectating", "offline", "quit"];
const DEFAULT_RUNTIME = {
demoAccelerated: true,
prepareSecondsPerMinute: 1,
maxGameSeconds: 180,
demoClueIntervalSeconds: 15
};
function isRecord(value) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function asNumber(value, fallback) {
const number = Number(value);
return Number.isFinite(number) ? number : fallback;
}
function assertIntegerInRange(value, min, max, message) {
if (!Number.isInteger(value) || value < min || value > max) {
throw new Error(message);
}
}
function validateRoomPayload(payload) {
const source = isRecord(payload) ? payload : {};
const name = String(source.name || "").trim();
if (!name) throw new Error("请填写房间名称");
const settings = normalizeSettings(source);
assertIntegerInRange(settings.durationMinutes, 15, 90, "游戏时长应为 1590 分钟");
assertIntegerInRange(settings.prepareMinutes, 1, 10, "准备时长应为 110 分钟");
assertIntegerInRange(settings.maxPlayers, 4, 30, "房间人数应为 430 人");
assertIntegerInRange(settings.seekerCount, 1, 29, "初始寻找者人数应为 129 人");
assertIntegerInRange(settings.clueIntervalMinutes, 3, 10, "线索间隔应为 310 分钟");
assertIntegerInRange(settings.radiusMeters, 200, 1500, "活动半径应为 2001500 米");
if (settings.seekerCount >= settings.maxPlayers) {
throw new Error("初始寻找者必须少于房间人数上限");
}
return {
...settings,
name: name.slice(0, 16)
};
}
function normalizeSettings(settings) {
const source = isRecord(settings) ? settings : {};
const merged = { ...DEFAULT_SETTINGS, ...source };
return {
mode: merged.mode === "classic" ? "classic" : "infection",
durationMinutes: asNumber(merged.durationMinutes, DEFAULT_SETTINGS.durationMinutes),
prepareMinutes: asNumber(merged.prepareMinutes, DEFAULT_SETTINGS.prepareMinutes),
maxPlayers: asNumber(merged.maxPlayers, DEFAULT_SETTINGS.maxPlayers),
seekerCount: asNumber(merged.seekerCount, DEFAULT_SETTINGS.seekerCount),
clueIntervalMinutes: asNumber(merged.clueIntervalMinutes, DEFAULT_SETTINGS.clueIntervalMinutes),
radiusMeters: asNumber(merged.radiusMeters, DEFAULT_SETTINGS.radiusMeters),
skillsEnabled: merged.skillsEnabled !== false
};
}
function normalizeRuntime(runtime) {
const source = isRecord(runtime) ? runtime : {};
return {
demoAccelerated: source.demoAccelerated !== false,
prepareSecondsPerMinute: Math.max(1, asNumber(
source.prepareSecondsPerMinute,
DEFAULT_RUNTIME.prepareSecondsPerMinute
)),
maxGameSeconds: Math.max(1, asNumber(
source.maxGameSeconds,
DEFAULT_RUNTIME.maxGameSeconds
)),
demoClueIntervalSeconds: Math.max(1, asNumber(
source.demoClueIntervalSeconds,
DEFAULT_RUNTIME.demoClueIntervalSeconds
))
};
}
function getPrepareDurationSeconds(room) {
if (room.runtime.demoAccelerated) {
return room.settings.prepareMinutes * room.runtime.prepareSecondsPerMinute;
}
return room.settings.prepareMinutes * 60;
}
function getGameDurationSeconds(room) {
const configured = room.settings.durationMinutes * 60;
return room.runtime.demoAccelerated
? Math.min(configured, room.runtime.maxGameSeconds)
: configured;
}
function getClueIntervalSeconds(room) {
return room.runtime.demoAccelerated
? room.runtime.demoClueIntervalSeconds
: room.settings.clueIntervalMinutes * 60;
}
function normalizePlayer(player, index, roomCreatedAt) {
const source = isRecord(player) ? player : {};
const fallbackName = `玩家${index + 1}`;
const name = String(source.name || fallbackName).slice(0, 10);
const ready = Boolean(source.ready);
return {
...source,
id: String(source.id || `legacy-player-${index}`),
name,
avatar: String(source.avatar || name.slice(0, 1) || "?"),
isHost: Boolean(source.isHost),
isBot: Boolean(source.isBot),
ready,
role: PLAYER_ROLES.includes(source.role) ? source.role : "",
status: PLAYER_STATUSES.includes(source.status)
? source.status
: ready ? "ready" : "idle",
caughtAt: source.caughtAt || null,
caughtBy: source.caughtBy || null,
captures: Math.max(0, asNumber(source.captures, 0)),
initialRole: PLAYER_ROLES.includes(source.initialRole) ? source.initialRole : source.role || "",
skillsUsed: {
radar: Math.max(0, asNumber(source.skillsUsed && source.skillsUsed.radar, 0)),
silent: Math.max(0, asNumber(source.skillsUsed && source.skillsUsed.silent, 0))
},
silentPending: Boolean(source.silentPending),
joinedAt: asNumber(source.joinedAt, roomCreatedAt)
};
}
function normalizeEvent(event, index, roomCreatedAt) {
const source = isRecord(event) ? event : {};
return {
...source,
id: String(source.id || `legacy-event-${roomCreatedAt}-${index}`),
type: String(source.type || "system"),
message: String(source.message || "游戏状态已更新"),
createdAt: asNumber(source.createdAt, roomCreatedAt)
};
}
function normalizeRoom(room, codeHint = "") {
if (!isRecord(room)) return null;
const code = String(room.code || codeHint || "");
if (!/^\d{6}$/.test(code)) return null;
const createdAt = asNumber(room.createdAt, Date.now());
const players = Array.isArray(room.players)
? room.players.map((player, index) => normalizePlayer(player, index, createdAt))
: [];
const events = Array.isArray(room.events)
? room.events.slice(0, 20).map((event, index) => normalizeEvent(event, index, createdAt))
: [];
return {
...room,
schemaVersion: ROOM_SCHEMA_VERSION,
id: String(room.id || `room-${code}`),
code,
name: String(room.name || "未命名游戏").slice(0, 16),
hostId: String(room.hostId || (players[0] && players[0].id) || ""),
status: ROOM_STATUSES.includes(room.status) ? room.status : "waiting",
settings: normalizeSettings(room.settings),
runtime: normalizeRuntime(room.runtime),
players,
events,
captureTokensUsed: Array.isArray(room.captureTokensUsed)
? room.captureTokensUsed.slice(-50).map(String)
: [],
winner: room.winner === "hiders" || room.winner === "seekers" ? room.winner : "",
phaseStartedAt: room.phaseStartedAt || null,
phaseEndsAt: room.phaseEndsAt || null,
nextClueAt: room.nextClueAt || null,
finishedAt: room.finishedAt || null,
pausedAt: room.pausedAt || null,
pausedRemainingMs: Math.max(0, asNumber(room.pausedRemainingMs, 0)),
pausedClueRemainingMs: Math.max(0, asNumber(room.pausedClueRemainingMs, 0)),
finishReason: String(room.finishReason || ""),
resultSnapshot: isRecord(room.resultSnapshot) ? room.resultSnapshot : null,
createdAt,
updatedAt: asNumber(room.updatedAt, createdAt)
};
}
function readRooms() {
try {
const rooms = wx.getStorageSync(ROOMS_KEY);
return isRecord(rooms) ? rooms : {};
} catch (error) {
return {};
}
}
function writeRooms(rooms) {
wx.setStorageSync(ROOMS_KEY, rooms);
}
function saveRoom(room) {
const rooms = readRooms();
const normalized = normalizeRoom(room);
if (!normalized) throw new Error("房间数据无效");
normalized.updatedAt = Date.now();
rooms[normalized.code] = normalized;
writeRooms(rooms);
return clone(normalized);
}
function makePlayer(id, name, isHost = false, isBot = false) {
return {
id,
name,
avatar: name.slice(0, 1),
isHost,
isBot,
ready: isHost,
role: "",
initialRole: "",
status: "idle",
caughtAt: null,
caughtBy: null,
captures: 0,
skillsUsed: { radar: 0, silent: 0 },
silentPending: false,
joinedAt: Date.now()
};
}
function makeDemoRoom() {
const room = {
schemaVersion: ROOM_SCHEMA_VERSION,
id: "room-demo",
code: "731204",
name: "周末公园追逃局",
hostId: "demo-host",
status: "waiting",
settings: {
...DEFAULT_SETTINGS,
durationMinutes: 20,
prepareMinutes: 1
},
runtime: { ...DEFAULT_RUNTIME },
players: [
makePlayer("demo-host", "阿树", true, true),
makePlayer("demo-2", "小满", false, true),
makePlayer("demo-3", "木木", false, true)
],
events: [],
createdAt: Date.now()
};
room.players.forEach((player) => {
player.ready = true;
});
return room;
}
function ensureSeedData() {
const rooms = readRooms();
let changed = false;
Object.keys(rooms).forEach((code) => {
const normalized = normalizeRoom(rooms[code], code);
if (normalized) {
rooms[code] = normalized;
} else {
delete rooms[code];
}
changed = true;
});
if (!rooms["731204"]) {
rooms["731204"] = makeDemoRoom();
changed = true;
}
if (changed) {
writeRooms(rooms);
}
}
function createRoom(payload) {
const validated = validateRoomPayload(payload);
const rooms = readRooms();
let code = randomCode();
while (rooms[code]) code = randomCode();
const room = {
schemaVersion: ROOM_SCHEMA_VERSION,
id: `room-${Date.now()}`,
code,
name: validated.name,
hostId: USER_ID,
status: "waiting",
settings: {
...DEFAULT_SETTINGS,
...validated
},
runtime: { ...DEFAULT_RUNTIME },
players: [makePlayer(USER_ID, "我", true, false)],
events: [
{
id: `event-${Date.now()}`,
type: "system",
message: "房间已创建,邀请好友加入吧",
createdAt: Date.now()
}
],
createdAt: Date.now()
};
saveRoom(room);
wx.setStorageSync(CURRENT_ROOM_KEY, code);
return clone(room);
}
function getRoom(code) {
const room = normalizeRoom(readRooms()[code], code);
return room ? clone(room) : null;
}
function getCurrentRoom() {
let code = "";
try {
code = wx.getStorageSync(CURRENT_ROOM_KEY);
} catch (error) {
return null;
}
return code ? getRoom(code) : null;
}
function setCurrentRoom(code) {
wx.setStorageSync(CURRENT_ROOM_KEY, code);
}
function joinRoom(code, displayName = "我") {
const normalizedCode = String(code || "").trim();
if (!/^\d{6}$/.test(normalizedCode)) {
throw new Error("请输入正确的六位房间码");
}
const normalizedName = String(displayName || "").trim().slice(0, 10);
if (!normalizedName) throw new Error("请输入游戏昵称");
const rooms = readRooms();
const room = normalizeRoom(rooms[normalizedCode], normalizedCode);
if (!room) {
throw new Error("没有找到这个房间,请检查房间码");
}
if (room.status === "finished") throw new Error("这场游戏已经结束");
if (room.status === "cancelled") throw new Error("房间已被房主取消");
if (room.status !== "waiting") throw new Error("游戏已经开始,暂时无法加入");
const existing = room.players.find((player) => player.id === USER_ID);
if (!existing) {
if (room.players.length >= room.settings.maxPlayers) {
throw new Error("房间人数已满");
}
const player = makePlayer(USER_ID, normalizedName, false, false);
if (room.code === "731204") {
player.ready = true;
player.status = "ready";
}
room.players.push(player);
} else {
existing.name = normalizedName;
existing.avatar = normalizedName.slice(0, 1);
}
saveRoom(room);
setCurrentRoom(normalizedCode);
return clone(room);
}
function addDemoPlayers(code, targetCount = 6) {
const room = getRoom(code);
if (!room || room.status !== "waiting") return room;
const usedNames = room.players.map((player) => player.name);
const available = BOT_NAMES.filter((name) => !usedNames.includes(name));
while (
room.players.length < targetCount &&
room.players.length < room.settings.maxPlayers &&
available.length
) {
const name = available.shift();
const player = makePlayer(`bot-${Date.now()}-${room.players.length}`, name, false, true);
player.ready = true;
room.players.push(player);
}
return saveRoom(room);
}
function toggleReady(code, playerId = USER_ID) {
const room = getRoom(code);
if (!room) throw new Error("房间不存在");
if (room.status !== "waiting") throw new Error("游戏已开始,不能修改准备状态");
const player = room && room.players.find((item) => item.id === playerId);
if (!player) throw new Error("你不在当前房间中");
player.ready = !player.ready;
player.status = player.ready ? "ready" : "idle";
return saveRoom(room);
}
function startGame(code, operatorId = USER_ID) {
const room = getRoom(code);
if (!room) throw new Error("房间不存在");
if (room.status !== "waiting") throw new Error("当前房间不能重复开始");
const canControlDemo = room.code === "731204" && operatorId === USER_ID;
if (room.hostId !== operatorId && !canControlDemo) {
throw new Error("只有房主可以开始游戏");
}
if (room.players.length < 4) throw new Error("至少需要 4 名玩家");
if (room.settings.seekerCount >= room.players.length) {
throw new Error("寻找者人数必须少于当前玩家人数");
}
if (room.players.some((player) => !player.ready)) {
throw new Error("还有玩家尚未准备好");
}
const seekerCount = room.settings.seekerCount;
const sorted = [...room.players].sort((a, b) => {
if (a.id === USER_ID) return -1;
if (b.id === USER_ID) return 1;
return a.joinedAt - b.joinedAt;
});
sorted.forEach((player, index) => {
player.role = index < seekerCount ? "seeker" : "hider";
player.initialRole = player.role;
player.status = player.role === "seeker" ? "seeking" : "hiding";
player.ready = true;
player.skillsUsed = { radar: 0, silent: 0 };
player.silentPending = false;
});
room.players = sorted;
room.status = "preparing";
room.phaseStartedAt = Date.now();
room.phaseEndsAt = room.phaseStartedAt + getPrepareDurationSeconds(room) * 1000;
room.gameDurationSeconds = getGameDurationSeconds(room);
room.captureTokensUsed = [];
room.events.unshift({
id: `event-${Date.now()}`,
type: "system",
message: "身份已分配,躲藏者请立即出发",
createdAt: Date.now()
});
return saveRoom(room);
}
function beginPlaying(code) {
const room = getRoom(code);
if (!room) throw new Error("房间不存在");
if (room.status !== "preparing") throw new Error("当前房间不能进入追逃阶段");
const scheduledStartAt = room.phaseEndsAt || Date.now();
room.status = "playing";
room.phaseStartedAt = scheduledStartAt;
room.gameDurationSeconds = room.gameDurationSeconds || getGameDurationSeconds(room);
room.phaseEndsAt = scheduledStartAt + room.gameDurationSeconds * 1000;
room.nextClueAt = scheduledStartAt + getClueIntervalSeconds(room) * 1000;
room.events.unshift({
id: `event-${Date.now()}`,
type: "system",
message: "追逃开始!请留意安全边界",
createdAt: Date.now()
});
return saveRoom(room);
}
function addEvent(code, message, type = "system") {
const room = getRoom(code);
if (!room) return null;
room.events.unshift({
id: `event-${Date.now()}-${Math.random()}`,
type,
message,
createdAt: Date.now()
});
room.events = room.events.slice(0, 20);
return saveRoom(room);
}
function revealClue(code) {
const room = getRoom(code);
if (!room) throw new Error("房间不存在");
if (room.status !== "playing") throw new Error("当前阶段不能广播线索");
const silentPlayers = room.players.filter(
(player) => player.role === "hider" && player.status === "hiding" && player.silentPending
);
silentPlayers.forEach((player) => {
player.silentPending = false;
});
const area = AREA_CLUES[Math.floor(Math.random() * AREA_CLUES.length)];
const message = silentPlayers.length
? `${silentPlayers.length} 名目标的静默已生效;${area}`
: area;
room.events.unshift({
id: `event-${Date.now()}-${Math.random()}`,
type: "clue",
message,
createdAt: Date.now()
});
room.events = room.events.slice(0, 20);
room.nextClueAt = Date.now() + getClueIntervalSeconds(room) * 1000;
return saveRoom(room);
}
function useSkill(code, skill, playerId = USER_ID) {
const room = getRoom(code);
if (!room) throw new Error("房间不存在");
if (room.status !== "playing") throw new Error("当前阶段不能使用技能");
if (!room.settings.skillsEnabled) throw new Error("本局未启用角色技能");
const player = room.players.find((item) => item.id === playerId);
if (!player) throw new Error("你不在当前房间中");
const expectedSkill = player.role === "seeker" ? "radar" : "silent";
if (skill !== expectedSkill) throw new Error("当前身份不能使用这个技能");
if (player.status !== (player.role === "seeker" ? "seeking" : "hiding")) {
throw new Error("当前玩家状态不能使用技能");
}
if (player.skillsUsed[skill] >= 1) throw new Error("本局技能已经使用过");
player.skillsUsed[skill] += 1;
if (skill === "radar") {
const nearby = Math.random() > 0.45;
room.events.unshift({
id: `event-${Date.now()}-${Math.random()}`,
type: "skill",
message: nearby ? "雷达回应:附近 150 米内可能有人" : "雷达回应:附近暂未检测到目标",
createdAt: Date.now()
});
} else {
player.silentPending = true;
room.events.unshift({
id: `event-${Date.now()}-${Math.random()}`,
type: "skill",
message: "有目标已开启静默信号",
createdAt: Date.now()
});
}
room.events = room.events.slice(0, 20);
return saveRoom(room);
}
function captureTokenKey(targetId, timeWindow) {
return `${targetId}:${timeWindow}`;
}
function hashCaptureToken(value) {
let hash = 2166136261;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return String(Math.abs(hash) % 10000).padStart(4, "0");
}
function getCaptureCode(code, targetId = USER_ID, now = Date.now()) {
const room = getRoom(code);
if (!room) return "";
const target = room.players.find(
(player) => player.id === targetId && player.role === "hider" && player.status === "hiding"
);
if (!target) return "";
const timeWindow = Math.floor(now / 30000);
return hashCaptureToken(`${room.code}:${target.id}:${timeWindow}`);
}
function capture(code, targetId, submittedCode, seekerId = USER_ID, now = Date.now()) {
const room = getRoom(code);
if (!room || room.status !== "playing") {
throw new Error("当前不能提交抓捕");
}
const seeker = room.players.find((player) => player.id === seekerId);
if (!seeker || seeker.role !== "seeker" || seeker.status !== "seeking") {
throw new Error("只有正在游戏的寻找者可以提交抓捕");
}
const target = room.players.find((player) => player.id === targetId);
if (!target) throw new Error("没有找到抓捕目标");
const timeWindow = Math.floor(now / 30000);
const tokenKey = captureTokenKey(target.id, timeWindow);
if (room.captureTokensUsed.includes(tokenKey)) throw new Error("口令已经使用过");
if (target.role !== "hider" || target.status !== "hiding") {
throw new Error("目标已不在躲藏状态");
}
if (String(submittedCode) !== getCaptureCode(code, target.id, now)) {
throw new Error("口令不正确或已经失效");
}
room.captureTokensUsed.push(tokenKey);
room.captureTokensUsed = room.captureTokensUsed.slice(-50);
target.caughtAt = now;
target.caughtBy = seeker.id;
seeker.captures += 1;
if (room.settings.mode === "infection") {
target.role = "seeker";
target.status = "seeking";
} else {
target.status = "caught";
}
room.events.unshift({
id: `event-${Date.now()}`,
type: "capture",
message: `${seeker.name} 抓到了 ${target.name}`,
createdAt: Date.now()
});
const survivors = room.players.filter(
(player) => player.role === "hider" && player.status === "hiding"
);
if (!survivors.length) {
finishGameObject(room, "seekers");
}
return saveRoom(room);
}
function makeResultSnapshot(room, winner, finishedAt, reason) {
const gameStartedAt = room.phaseStartedAt || room.createdAt;
const elapsedSeconds = Math.max(0, Math.floor((finishedAt - gameStartedAt) / 1000));
const players = room.players.map((player) => {
const survivedUntil = player.caughtAt || finishedAt;
return {
id: player.id,
name: player.name,
avatar: player.avatar,
initialRole: player.initialRole || player.role,
finalRole: player.role,
finalStatus: player.status,
captures: player.captures,
caughtAt: player.caughtAt,
survivedSeconds: player.initialRole === "hider"
? Math.max(0, Math.floor((survivedUntil - gameStartedAt) / 1000))
: 0,
joinedAt: player.joinedAt
};
});
players.sort((a, b) => {
if (b.captures !== a.captures) return b.captures - a.captures;
if (b.survivedSeconds !== a.survivedSeconds) return b.survivedSeconds - a.survivedSeconds;
if (a.joinedAt !== b.joinedAt) return a.joinedAt - b.joinedAt;
return a.id.localeCompare(b.id);
});
return {
winner,
reason,
finishedAt,
elapsedSeconds,
eventCount: room.events.length,
players
};
}
function finishGameObject(room, winner, reason = "completed") {
const finishedAt = Date.now();
room.status = "finished";
room.finishedAt = finishedAt;
room.winner = winner;
room.finishReason = reason;
room.phaseEndsAt = finishedAt;
room.resultSnapshot = makeResultSnapshot(room, winner, finishedAt, reason);
}
function finishGame(code, winner, operatorId = USER_ID, reason = "manual") {
const room = getRoom(code);
if (!room) throw new Error("房间不存在");
if (!["preparing", "playing", "paused"].includes(room.status)) {
throw new Error("当前房间不能结束游戏");
}
const canControlDemo = room.code === "731204" && operatorId === USER_ID;
if (operatorId && room.hostId !== operatorId && !canControlDemo) {
throw new Error("只有房主可以提前结束游戏");
}
const survivors = room.players.filter(
(player) => player.role === "hider" && player.status === "hiding"
);
finishGameObject(room, winner || (survivors.length ? "hiders" : "seekers"), reason);
return saveRoom(room);
}
function pauseGame(code, operatorId = USER_ID) {
const room = getRoom(code);
if (!room) throw new Error("房间不存在");
if (room.status !== "playing") throw new Error("只有追逃阶段可以暂停");
const canControlDemo = room.code === "731204" && operatorId === USER_ID;
if (room.hostId !== operatorId && !canControlDemo) throw new Error("只有房主可以暂停游戏");
const now = Date.now();
room.status = "paused";
room.pausedAt = now;
room.pausedRemainingMs = Math.max(0, room.phaseEndsAt - now);
room.pausedClueRemainingMs = room.nextClueAt
? Math.max(0, room.nextClueAt - now)
: 0;
room.events.unshift({
id: `event-${now}`,
type: "system",
message: "房主暂停了游戏,请所有玩家留在安全位置",
createdAt: now
});
return saveRoom(room);
}
function resumeGame(code, operatorId = USER_ID) {
const room = getRoom(code);
if (!room) throw new Error("房间不存在");
if (room.status !== "paused") throw new Error("当前游戏没有暂停");
const canControlDemo = room.code === "731204" && operatorId === USER_ID;
if (room.hostId !== operatorId && !canControlDemo) throw new Error("只有房主可以继续游戏");
const now = Date.now();
room.status = "playing";
room.phaseEndsAt = now + room.pausedRemainingMs;
room.nextClueAt = room.pausedClueRemainingMs
? now + room.pausedClueRemainingMs
: now + getClueIntervalSeconds(room) * 1000;
room.pausedAt = null;
room.pausedRemainingMs = 0;
room.pausedClueRemainingMs = 0;
room.events.unshift({
id: `event-${now}`,
type: "system",
message: "房主继续了游戏",
createdAt: now
});
return saveRoom(room);
}
function quitGame(code, playerId = USER_ID) {
const room = getRoom(code);
if (!room) throw new Error("房间不存在");
if (!["preparing", "playing", "paused"].includes(room.status)) {
throw new Error("当前不能退出游戏");
}
const player = room.players.find((item) => item.id === playerId);
if (!player) throw new Error("你不在当前房间中");
if (player.status === "quit") throw new Error("玩家已经退出游戏");
player.status = "quit";
player.ready = false;
room.events.unshift({
id: `event-${Date.now()}`,
type: "safety",
message: `${player.name} 已安全退出并前往集合点`,
createdAt: Date.now()
});
const survivors = room.players.filter(
(item) => item.role === "hider" && item.status === "hiding"
);
if (!survivors.length) finishGameObject(room, "seekers", "all_hiders_left");
return saveRoom(room);
}
function resetRoom(code) {
const room = getRoom(code);
if (!room) return null;
room.status = "waiting";
room.winner = "";
room.finishReason = "";
room.phaseEndsAt = null;
room.phaseStartedAt = null;
room.nextClueAt = null;
room.gameDurationSeconds = null;
room.finishedAt = null;
room.pausedAt = null;
room.pausedRemainingMs = 0;
room.pausedClueRemainingMs = 0;
room.resultSnapshot = null;
room.events = [];
room.players.forEach((player) => {
player.role = "";
player.initialRole = "";
player.status = "ready";
player.ready = true;
player.caughtAt = null;
player.caughtBy = null;
player.captures = 0;
player.skillsUsed = { radar: 0, silent: 0 };
player.silentPending = false;
});
room.captureTokensUsed = [];
return saveRoom(room);
}
function clearCurrentRoom() {
wx.removeStorageSync(CURRENT_ROOM_KEY);
}
module.exports = {
USER_ID,
ensureSeedData,
createRoom,
getRoom,
getCurrentRoom,
setCurrentRoom,
joinRoom,
addDemoPlayers,
toggleReady,
startGame,
beginPlaying,
revealClue,
useSkill,
getCaptureCode,
capture,
finishGame,
pauseGame,
resumeGame,
quitGame,
resetRoom,
clearCurrentRoom
};