新建项目

This commit is contained in:
2026-07-29 10:50:33 +08:00
parent bfd3841cbd
commit 4f69b859a8
49 changed files with 5565 additions and 0 deletions

812
services/game-service.js Normal file
View File

@@ -0,0 +1,812 @@
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
};