auctiontab.tsx
/**
- @license
- SPDX-License-Identifier: Apache-2.0 */
import React, { useState, useEffect, useRef } from 'react'; import { Staff, StaffId, EquippableItem } from '../types'; import { Gavel, Lock, Sparkles, Calculator, Eye, TrendingUp, Coins, HelpCircle, ScrollText, Hourglass, Compass, Gift, ChevronRight, Maximize2, FileSpreadsheet, AlertCircle, Clock, UserCheck } from 'lucide-react'; import { playCoinSound, playAbacusSound, playClickFeedback, playSweepSound } from '../utils/audio'; import { generateRandomItem } from '../utils/itemGenerator';
// Types of Chest Binders interface ConfiscatedBox { id: string; name: string; desc: string; apparentCost: number; // Starting bid guide secretValue: number; // Actual worth if sold or appraised clues: { lu: string; // Estimated baseline range bai: string; // Visual details of corner tong: string; // Secret probability of epic/legendary item guo: string; // Weight shaking clues li: string; // Culinary smell and touch }; contents: { isPrank: boolean; prankType?: 'tanghulu' | 'tofu' | 'ruler'; // Stuns, debuffs item?: EquippableItem; goldRefund?: number; description: string; label: string; }; revealedClues: Record<StaffId, boolean>; winnerName: string; finalPrice: number; }
interface Participant { name: string; avatar: string; isAI: boolean; isLocal: boolean; isPeer: boolean; coins: number; aiBidChance: number; // multiplier of bid courage aiAggressiveRatio: number; // likelihood of bidding higher cryingTimeLeft: number; // stun state (cannot bid) budgetModifier: number; // e.g. smelly tofu debuff unboxedRewardsCount: number; }
interface BiddingLog { timeStr: string; sender: string; message: string; colorClass: string; }
interface BotTemplate { id: string; name: string; avatar: string; defaultCoins: number; bidChance: number; aggRatio: number; desc: string; }
export const ALL_POTENTIAL_AI_BOTS: BotTemplate[] = [ { id: 'tong', name: '佟湘玉', avatar: '💃', defaultCoins: 600, bidChance: 0.65, aggRatio: 1.0, desc: '预算丰富,注重品质,出价稳健。' }, { id: 'bai', name: '白展堂', avatar: '🥋', defaultCoins: 450, bidChance: 0.8, aggRatio: 1.15, desc: '眼光毒辣,喜爱刀剑重装,出价果断。' }, { id: 'guo', name: '郭芙蓉', avatar: '🥊', defaultCoins: 350, bidChance: 0.9, aggRatio: 1.35, desc: '性格泼辣,行侠仗义,容易冲动加价。' }, { id: 'li', name: '李大嘴', avatar: '🧑🍳', defaultCoins: 300, bidChance: 0.75, aggRatio: 1.1, desc: '重情重义,偏爱食材和神铁,出价淳实。' }, { id: 'lu', name: '吕秀才', avatar: '🎓', defaultCoins: 400, bidChance: 0.5, aggRatio: 0.9, desc: '满腹经纶,极其吝啬算账,极少盲目加价。' }, { id: 'xing', name: '邢育森', avatar: '👮♂️', defaultCoins: 500, bidChance: 0.7, aggRatio: 1.2, desc: '官差捕头,威风八面,喜爱展现威仪。' } ];
interface AuctionTabProps { coins: number; staff: Record<StaffId, Staff>; bagItems: EquippableItem[]; addInnLog: (text: string) => void; playClickFeedback: () => void; peerId: string; conn: any; // Peer connection instance isHost: boolean; onModifyCoins: (delta: number) => void; onAddBaggageItem: (item: EquippableItem) => void; onModifyReputation: (delta: number) => void; lastAuctionMsg: { sender: string; payload: any; timestamp: number } | null; }
export const AuctionTab: React.FC
// Game variables
const [boxes, setBoxes] = useState<ConfiscatedBox[]>([]);
const [activeBoxIndex, setActiveBoxIndex] = useState
// Local interface states
const [skillUseCost, setSkillUseCost] = useState
// Sound triggers const triggerGavelSound = () => { try { playAbacusSound(); setTimeout(playAbacusSound, 150); } catch (e) {} };
const showToast = (msg: string) => { setToastMsg(msg); setTimeout(() => setToastMsg(''), 3000); };
// Check if player can afford entrance fee const canAffordEntrance = coins >= 100;
// --- Multi-player connection check --- const hasPeerConnected = conn && conn.open;
// --- Initialize Participants & Boxes (Host Authoritative) --- const initAuctionData = () => { // 1. Boxes Procedurally Generated const titles = [ { name: '官府缉私生铁箱', desc: '衙门从两广盐道查缴的陈年重铁箱,锁眼已锈死,贴着朱砂红泥官封。' }, { name: '江南名门没收织锦箱', desc: '江南织造案被官兵抄没的丝质木箱,散发着微弱的熏香及樟木气息。' }, { name: '关外走私西域铁漆匣', desc: '关外黑店查充的波斯特制铁盒,金锁雕刻着诡异的萨满咒文,不知藏着何药。' }, { name: '盗圣藏宝地遗留暗箱', desc: '传言白展堂当年遗忘在翠微山洞底的铁疙瘩,表面布满爪印和青苔。' }, { name: '御膳房流落秘炼砂瓮', desc: '宫廷大内失盗并被京兆衙门截获的泥封御膳砂瓮,不知藏着绝世好菜还是破烂。' } ]; const shuffledTitles = [...titles].sort(() => Math.random() - 0.5).slice(0, 4);
const proceduredBoxes = shuffledTitles.map((t, idx) => {
const isRareItem = Math.random() < 0.45;
const isPrank = !isRareItem && Math.random() < 0.4;
const baseStartingCost = 60 + Math.floor(Math.random() * 50);
let boxPrankType = null;
let secretValue = 10; // default trash
let contents;
if (isRareItem) {
// High worth legendary/epic treasures
const rollRarity = Math.random();
const rarity = rollRarity < 0.15 ? 'legendary' : (rollRarity < 0.5 ? 'epic' : 'rare');
const generatedLoot = generateRandomItem(rarity);
// Value computation relative to rarity
secretValue = rarity === 'legendary' ? 450 + Math.floor(Math.random() * 150) :
rarity === 'epic' ? 240 + Math.floor(Math.random() * 80) :
120 + Math.floor(Math.random() * 40);
contents = {
isPrank: false,
item: generatedLoot,
goldRefund: Math.floor(secretValue * 0.15),
description: `【${generatedLoot.name}】(${generatedLoot.rarity === 'legendary' ? '金色圣留' : (generatedLoot.rarity === 'epic' ? '紫色传世' : '蓝色精致')}):属性为 ${generatedLoot.stat} (+${generatedLoot.value})。`,
label: `${generatedLoot.name}`
};
} else if (isPrank) {
// Prank items adding chaotic mechanics
const prankRoll = Math.random();
const prankType = prankRoll < 0.35 ? 'tanghulu' : (prankRoll < 0.70 ? 'tofu' : 'ruler');
boxPrankType = prankType;
secretValue = 25 + Math.floor(Math.random() * 20);
if (prankType === 'tanghulu') {
contents = {
isPrank: true,
prankType,
description: `莫小贝吃剩的冰糖葫芦(整蛊:能让任意一名大掌柜嚎啕大哭5秒,无法参与下一次封箱叫价!)。`,
label: `哭哭糖葫芦`
};
} else if (prankType === 'tofu') {
contents = {
isPrank: true,
prankType,
description: `大嘴秘制存放了三年的臭豆腐(整蛊:恶臭熏晕所有人,使得他们下一轮叫价预算直接折减15%!)。`,
label: `陈年臭豆腐`
};
} else {
contents = {
isPrank: true,
prankType,
description: `捕头邢育森抽打小毛贼的惩戒戒尺(整蛊:具有莫大的江湖威严,能强制使任意大掌柜的出价冻结,无条件扣罚30文铜板!)。`,
label: `捕头戒尺`
};
}
} else {
// Ordinary garbage
const trashes = [
{ name: '李大嘴烤焦的麦饼', desc: '硬如黑铁,丢到街上能砸破流浪狗的脑袋。别期望它能吃。', val: 5 },
{ name: '假的葵花宝典抄本', desc: '打开第一页写着“欲练此功,端坐静思”...全是秀才抄的书生字帖!', val: 12 },
{ name: '同福客栈破旧榆木大门板契', desc: '早已换掉的库房破木板,一文不值,大概只能拿去当废柴烧。', val: 2 },
{ name: '捕头邢育森遗落的官帽穗', desc: '破旧褪色的红色流苏,有一股宿醉的烧白干酒气。', val: 8 },
{ name: '莫小贝泥捏的泥人张手办', desc: '捏得歪瓜裂枣,勉强能看出是个拿糖葫芦的野丫头。', val: 15 }
];
const trash = trashes[Math.floor(Math.random() * trashes.length)];
secretValue = trash.val;
contents = {
isPrank: false,
description: `${trash.desc} (价值: 🪙${trash.val}文)`,
label: trash.name
};
}
// 4. Formulate Speeches & Hints
const baseEstimateLuMin = Math.max(10, Math.floor(secretValue * 0.75 - Math.random() * 15));
const baseEstimateLuMax = Math.floor(secretValue * 1.25 + 10 + Math.random() * 20);
const luHint = `吕秀才扒拉着纸墨折算半日,神情凝重道:“子曾经曰过,凡事预则立。以学生之见,除去官税和损耗,底价值底线绝对在 🪙${baseEstimateLuMin} 到 🪙${baseEstimateLuMax} 文上下!”`;
let baiHint = `白展堂凑到缝隙,吹了吹浮灰:“啧啧,凭我当年盗圣的双眼,`;
if (secretValue > 200) {
baiHint += `这里面隐隐折射出阵阵奇珍神光!锁扣也带有贵金气味,必是重宝!”`;
} else if (isPrank) {
baiHint += `这里面锁扣有些松动,气味古怪得很,像是什么江湖把戏的整蛊道具,小心得不偿失!”`;
} else if (secretValue < 20) {
baiHint += `里面悄无声息,且木屑发潮霉烂,像是一堆毫无鸟用的厨房破木柴烂铁,千万别上当!”`;
} else {
baiHint += `分量一般般,似乎有些普通吃食或散银,买入不至于亏本,但想大发一笔就甭指望喽!”`;
}
let tongHint = `佟掌柜用鼻尖在封锁贴上浅笑微嗅:“额这大半辈子见到的宝贝,比吃过的盐还多。额敢说,这只箱子里能开出绝世金牌武具的概率,`;
if (isRareItem) {
const pct = 70 + Math.floor(Math.random() * 25);
tongHint += `有将近 ${pct}% 这样高!买下它断然是大吉利呀,掌柜买额的账准没错!”`;
} else {
const pct = 5 + Math.floor(Math.random() * 25);
tongHint += `只有微末的 ${pct}%。依额看,不如存着铜板去买下旁边的神武木匣!”`;
}
let guoHint = `郭芙蓉冷哼一声,使了一招轻柔掌风一震,箱体微微晃荡,拍掌喝道:“排山倒海!看我听声辨位!`;
if (secretValue > 150) {
guoHint += `箱体一震之后有沉闷的金石回响,感觉里头安放得严严实实,必是玄铁重器,沉甸甸的很有嚼头!”`;
} else if (isPrank) {
guoHint += `回音十分清脆活泼,甚至有少许沙沙滑落声,怕是什么糖果铁签、或者捕快的碎竹条,不算太重。”`;
} else {
guoHint += `轻飘飘、软绵绵,啪叽一下像是个馊饼,里头绝对没藏什么沉甸甸的武学秘籍,不值当的大力去抢。”`;
}
let liHint = `李大嘴上前舔了舔封条边缘,大口擤声:“掌柜的,你听大嘴的!`;
const pType = boxPrankType;
if (isPrank && pType === 'tofu') {
liHint += `哎呀,这箱缝有一丝陈醋臭豆腐的馊气!这恶臭,指定是我失踪了三年的臭气瓮,酸香里带着咸,大补!”`;
} else if (isRareItem) {
liHint += `封嘴飘出来一缕极其高贵的玄铁油亮之香,闻起来有股传世武器、金算盘的高贵气味!”`;
} else if (secretValue < 30) {
liHint += `有一股灶台烧焦的苦咸,活像是我李大嘴平日里不小心烤焦的麦粉黑面疙瘩,猪油大火都没法救的那种!”`;
} else {
liHint += `气味淡淡的,没有上等海产干货的香味。应该是一般般的铁盒子器物吧。”`;
}
return {
id: `box-${idx}-${Date.now()}`,
name: t.name,
desc: t.desc,
apparentCost: baseStartingCost,
secretValue,
clues: {
lu: luHint,
bai: baiHint,
tong: tongHint,
guo: guoHint,
li: liHint
},
contents,
revealedClues: {
lu: false,
bai: false,
tong: false,
guo: false,
li: false
},
winnerName: '',
finalPrice: 0
};
});
// 2. Setup standard & custom dynamic Seats
const baseParticipants: Participant[] = [
{ name: '大掌柜 (我)', avatar: '🧑💼', isAI: false, isLocal: true, isPeer: false, coins, aiBidChance: 0, aiAggressiveRatio: 0, cryingTimeLeft: 0, budgetModifier: 1.0, unboxedRewardsCount: 0 }
];
// Read selectedBotIds to construct AI bots on the fly!
selectedBotIds.forEach(botId => {
const template = ALL_POTENTIAL_AI_BOTS.find(b => b.id === botId);
if (template) {
baseParticipants.push({
name: template.name,
avatar: template.avatar,
isAI: true,
isLocal: false,
isPeer: false,
coins: template.defaultCoins + Math.floor(Math.random() * 200),
aiBidChance: template.bidChance,
aiAggressiveRatio: template.aggRatio,
cryingTimeLeft: 0,
budgetModifier: 1.0,
unboxedRewardsCount: 0
});
}
});
// If P2P friend is connected, they always replace the 2nd seat (or are inserted) as a Player!
if (hasPeerConnected) {
const peerShortName = conn.peer.substring(0, 4).toUpperCase();
const peerParticipant: Participant = {
name: `盟友掌柜 (${peerShortName})`,
avatar: '🤝',
isAI: false,
isLocal: false,
isPeer: true,
coins: 500 + Math.floor(Math.random() * 300), // starting sync budget or dynamic
aiBidChance: 0,
aiAggressiveRatio: 0,
cryingTimeLeft: 0,
budgetModifier: 1.0,
unboxedRewardsCount: 0
};
if (baseParticipants.length > 1) {
baseParticipants[1] = peerParticipant;
} else {
baseParticipants.push(peerParticipant);
}
}
setBoxes(proceduredBoxes);
setParticipants(baseParticipants);
setSelectedBoxIndex(0);
setActiveBoxIndex(0);
return baseParticipants;
};
// --- Start the game --- const handleStartAuction = () => { if (!canAffordEntrance) { showToast('⚠️ 铜钱不足 100 文,无法支付衙门提缴入场费!'); return; }
playClickFeedback();
onModifyCoins(-100);
addInnLog('⚖️ 支出 100 铜币押金,同福合伙人鱼贯而入官府充公大库。');
// Build boxes & competitors
const generatedParticipants = initAuctionData();
setIsLobbyCreator(isHost);
setPhase('observe');
setCountdown(20); // 20s inspection timer
// P2P Synchronize
if (hasPeerConnected && isHost) {
// We pass the procedured list and participants to peer
sendPeerAuctionMsg('START_AUCTION', {
boxes: proceduredBoxesSample(),
participants: generatedParticipants,
isHost: true
});
}
};
// Safe boxes structures serialization for network transmission const proceduredBoxesSample = () => { // Generate new state to pass // 1. We must generate locally before syncing to guarantee matching state! const titles = [ { name: '官府缉私生铁箱', desc: '衙门从两广盐道查缴的陈年重铁箱,锁眼已锈死,贴着朱砂红泥官封。' }, { name: '江南名门没收织锦箱', desc: '江南织造案被官兵抄没的丝质木箱,散发着微弱的熏香及樟木气息。' }, { name: '关外走私西域铁漆匣', desc: '关外黑店查充的波斯特制铁盒,金锁雕刻着诡异的萨满咒文,不知藏着何药。' }, { name: '盗圣藏宝地遗留暗箱', desc: '传言白展堂当年遗忘在翠微山洞底的铁疙瘩,表面布满爪印和青苔。' }, { name: '御膳房流落秘炼砂瓮', desc: '宫廷大内失盗并被京兆衙门截获的泥封御膳砂瓮,不知藏着绝世好菜还是破烂。' } ]; const shuffledTitles = [...titles].sort(() => Math.random() - 0.5).slice(0, 4);
return shuffledTitles.map((t, idx) => {
const isRareItem = Math.random() < 0.45;
const isPrank = !isRareItem && Math.random() < 0.4;
const baseStartingCost = 60 + Math.floor(Math.random() * 50);
let secretValue = 10;
let contents: any;
if (isRareItem) {
const rollRarity = Math.random();
const rarity = rollRarity < 0.15 ? 'legendary' : (rollRarity < 0.5 ? 'epic' : 'rare');
const generatedLoot = generateRandomItem(rarity);
secretValue = rarity === 'legendary' ? 450 + Math.floor(Math.random() * 150) :
rarity === 'epic' ? 240 + Math.floor(Math.random() * 80) :
120 + Math.floor(Math.random() * 40);
contents = {
isPrank: false,
item: generatedLoot,
goldRefund: Math.floor(secretValue * 0.15),
description: `【${generatedLoot.name}】(${generatedLoot.rarity === 'legendary' ? '金色圣留' : (generatedLoot.rarity === 'epic' ? '紫色传世' : '蓝色精致')}):属性为 ${generatedLoot.stat} (+${generatedLoot.value})。`,
label: `${generatedLoot.name}`
};
} else if (isPrank) {
const prankRoll = Math.random();
const prankType = prankRoll < 0.35 ? 'tanghulu' : (prankRoll < 0.70 ? 'tofu' : 'ruler');
secretValue = 25 + Math.floor(Math.random() * 20);
contents = {
isPrank: true,
prankType,
description: prankType === 'tanghulu' ? `莫小贝吃剩的冰糖葫芦(整蛊防抢:嚎哭5秒)。` :
prankType === 'tofu' ? `大嘴存放三年的陈置臭豆腐(折减预算15%)。` :
`捕头邢育森的行刑硬竹板指(冻结出价+扣款30文)。`,
label: prankType === 'tanghulu' ? `哭哭糖葫芦` : prankType === 'tofu' ? `陈年臭豆腐` : `捕头戒尺`
};
} else {
secretValue = 15;
contents = {
isPrank: false,
description: `李大嘴烤焦的硬如玄铁的麦面锅灰 (价值: 🪙15文)`,
label: `烤焦麦焦炭饼`
};
}
const lMin = Math.max(10, Math.floor(secretValue * 0.75 - 10));
const lMax = Math.floor(secretValue * 1.25 + 25);
return {
id: `box-${idx}-${Date.now()}`,
name: t.name,
desc: t.desc,
apparentCost: baseStartingCost,
secretValue,
clues: {
lu: `吕秀才估摸道:“账上看来,官价起价约 ${lMin} 到 ${lMax} 铜钱。”`,
bai: `白展堂探头道:凭我阅宝无数,里头有股珍宝神性光晕!`,
tong: `佟掌柜叹气道:里头有神品重装的概率有成以上!`,
guo: `郭芙蓉拍桌喝道:分量惊人,拍在掌上极沉!`,
li: `李大嘴大叫道:一股高贵的神铁美味扑鼻而来!`
},
contents,
revealedClues: { lu: false, bai: false, tong: false, guo: false, li: false },
winnerName: '',
finalPrice: 0
};
});
};
// Setup PeerJS sender
const sendPeerAuctionMsg = (subType: string, body?: any) => {
if (!conn) return;
conn.send({
type: 'AUCTION_MSG',
senderName: peerId ? 联盟大掌柜 : '老合伙人',
payload: {
subType,
...body
}
});
};
// --- Listen to Peer Messages --- useEffect(() => { if (!lastAuctionMsg) return; const { payload } = lastAuctionMsg; if (!payload || !payload.subType) return;
switch (payload.subType) {
case 'START_AUCTION': {
// Client receives host-generated layout
setBoxes(payload.boxes);
const peerShort = lastAuctionMsg.sender.substring(0, 4).toUpperCase();
let syncedParticipants = payload.participants;
if (syncedParticipants && syncedParticipants.length > 0) {
// Re-map host-side indices to client perspective
syncedParticipants = syncedParticipants.map((p: any) => {
if (p.isLocal) {
return { ...p, name: `盟友掌柜 (${peerShort})`, isLocal: false, isPeer: true };
}
if (p.isPeer) {
return { ...p, name: '大掌柜 (我)', isLocal: true, isPeer: false, coins: coins };
}
return p;
});
setParticipants(syncedParticipants);
} else {
// Fallback
const guestPartic: Participant[] = [
{ name: `大掌柜 (我)`, avatar: '🧑💼', isAI: false, isLocal: true, isPeer: false, coins, aiBidChance: 0, aiAggressiveRatio: 0, cryingTimeLeft: 0, budgetModifier: 1.0, unboxedRewardsCount: 0 },
{ name: `盟友掌柜 (${peerShort})`, avatar: '🤝', isAI: false, isLocal: false, isPeer: true, coins: payload.hostCoins || 500, aiBidChance: 0, aiAggressiveRatio: 0, cryingTimeLeft: 0, budgetModifier: 1.0, unboxedRewardsCount: 0 },
{ name: '白展堂', avatar: '🥋', isAI: true, isLocal: false, isPeer: false, coins: 400, aiBidChance: 0.8, aiAggressiveRatio: 1.15, cryingTimeLeft: 0, budgetModifier: 1.0, unboxedRewardsCount: 0 },
{ name: '郭芙蓉', avatar: '🥊', isAI: true, isLocal: false, isPeer: false, coins: 350, aiBidChance: 0.9, aiAggressiveRatio: 1.35, cryingTimeLeft: 0, budgetModifier: 1.0, unboxedRewardsCount: 0 }
];
setParticipants(guestPartic);
}
setPhase('observe');
setCountdown(20);
setSelectedBoxIndex(0);
setActiveBoxIndex(0);
showToast('⚖️ 密信:联盟客栈开启了库房起拍,共同进入观察期!');
break;
}
case 'SKILL_REVEALED': {
const { boxIdx, staffId, clueText } = payload;
setBoxes(prev => {
const updated = [...prev];
if (updated[boxIdx]) {
updated[boxIdx].revealedClues[staffId as StaffId] = true;
}
return updated;
});
showToast(`🔍 听闻联盟【${staff[staffId as StaffId]?.name || '密友'}】使用绝技探到新机密!`);
break;
}
case 'SYNC_BID_STATE': {
setCurrentBid(payload.currentBid);
setHighestBidder(payload.highestBidder);
setCountdown(payload.countdown);
if (payload.logs) {
setBiddingLogs(payload.logs);
}
break;
}
case 'CLIENT_PLACED_BID': {
// Only host has the authority to arbitrate and process client bid requests
if (isHost) {
processRawBid(payload.bidderName, payload.bidAmount);
}
break;
}
case 'NEXT_AUCTION_BOX': {
setActiveBoxIndex(payload.boxIndex);
setCurrentBid(payload.apparentCost);
setHighestBidder('官方法官');
setCountdown(15);
setBiddingLogs([]);
setPhase('bidding');
break;
}
case 'TRIGGER_UNBOXING_PHASE': {
setBoxes(payload.boxes);
setParticipants(payload.participants);
setPhase('unboxing');
setUnboxedIndex(0);
setUnboxedAnimation('closed');
break;
}
case 'NEXT_UNBOX_REVEAL': {
setUnboxedIndex(payload.index);
setUnboxedResult(payload.resultText);
setUnboxedAnimation('opened');
break;
}
case 'END_ALL_AUCTION': {
setPhase('ledger');
setBoxes(payload.finalBoxes);
setParticipants(payload.finalParticipants);
break;
}
}
}, [lastAuctionMsg]);
// --- Clock ticking interval (Host handles AI, timings, state transitions) --- useEffect(() => { let timerID: any = null; if (phase === 'observe' || phase === 'bidding') { timerID = setInterval(() => { setCountdown((prev) => { const next = prev - 1;
// A. Observation Phase Expired
if (phase === 'observe' && next <= 0) {
clearInterval(timerID);
// Move block to Bidding box 0
if (isHost) {
const firstBox = boxes[0];
setCurrentBid(firstBox ? firstBox.apparentCost : 70);
setHighestBidder('官方法官');
setCountdown(15);
setBiddingLogs([{
timeStr: nowTimeStr(),
sender: '⚖️ 衙门捕快',
message: `充公官兵宣读法谕:第一只没收宝箱 【${firstBox?.name || '神秘纸瓮'}】 宣布开拍!底价 ${firstBox?.apparentCost || 70} 铜钱!`,
colorClass: 'text-amber-800 font-bold'
}]);
setPhase('bidding');
setActiveBoxIndex(0);
if (hasPeerConnected) {
sendPeerAuctionMsg('NEXT_AUCTION_BOX', {
boxIndex: 0,
apparentCost: firstBox?.apparentCost || 70
});
}
} else {
// Wait for Host state sync
setCountdown(99);
}
return 0;
}
// B. Bidding Phase Expired (Hammer strikes!)
if (phase === 'bidding' && next <= 0) {
clearInterval(timerID);
if (isHost) {
triggerGavelSound();
handleBoxBiddingExpired();
}
return 0;
}
// C. AI Bitting Thinker (Ticks only for Host)
if (phase === 'bidding' && isHost) {
runAIParticipantsBiddingTurn(next);
}
return next;
});
// Decay Participant stun/cry times
setParticipants((prev) =>
prev.map((p) => ({
...p,
cryingTimeLeft: Math.max(0, p.cryingTimeLeft - 1)
}))
);
}, 1000);
}
return () => {
if (timerID) clearInterval(timerID);
};
}, [phase, boxes, activeBoxIndex, currentBid, highestBidder, isHost, hasPeerConnected]);
const nowTimeStr = () => { return new Date().toLocaleTimeString('zh-CN', { hour12: false, minute: '2-digit', second: '2-digit' }); };
// --- AI Bidding Core Heuristic (Autoritative) --- const runAIParticipantsBiddingTurn = (secondsLeft: number) => { const activeBox = boxes[activeBoxIndex]; if (!activeBox) return;
// AI only bids if they have interest and countdown is tight (< 12 seconds normally)
participants.forEach((p, pIdx) => {
if (!p.isAI || p.coins < currentBid + 10 || p.cryingTimeLeft > 0) return;
// Base formula of worthiness
const baseSecret = activeBox.secretValue;
// Some AI has unlocked character insights, some rely on random margin
let maxBudgetToPay = baseSecret * p.aiAggressiveRatio * p.budgetModifier;
// Let special character AI adjust their estimation
if (p.name === '白展堂' && activeBox.contents.item?.type === 'weapon') {
maxBudgetToPay *= 1.35; // Bai loves high power weapon
}
if (p.name === '郭芙蓉') {
maxBudgetToPay *= 1.4; // Guo is aggressive hot-headed
}
// Safe limit cap
if (maxBudgetToPay < activeBox.apparentCost * 0.8) {
maxBudgetToPay = activeBox.apparentCost * 1.1; // maintain minimum playability
}
// Check if price already exceeds AI's personal comfort budget
if (currentBid >= maxBudgetToPay) return;
// Bid triggers based on time density & chance
const baseBidThreshold = 0.08 + (secondsLeft < 5 ? 0.18 : 0.0);
const isWillingToRaise = Math.random() < (baseBidThreshold * p.aiBidChance);
if (isWillingToRaise && highestBidder !== p.name) {
// Raise with random step (+5, +10, +25)
const possibleSteps = [5, 10, 15, 25];
const step = possibleSteps[Math.floor(Math.random() * possibleSteps.length)];
const proposedBid = currentBid + step;
if (proposedBid <= p.coins && proposedBid <= maxBudgetToPay) {
processRawBid(p.name, proposedBid);
}
}
});
};
// --- Perform Bidding (Authoritative / Host controlled) --- const processRawBid = (bidderName: string, amount: number) => { if (amount <= currentBid) return;
// Trigger local and broadcast updates
setCurrentBid(amount);
setHighestBidder(bidderName);
// Dynamic bid extension: resets timer to 5s if bid made in final seconds
let finalTimer = countdown;
let timingLogMsg = '';
if (countdown <= 3) {
finalTimer = 4;
timingLogMsg = ' ⏰ (延迟钟声复位)';
}
setCountdown(finalTimer);
// Produce humorous dialogue from competitors
let speechQuote = '';
if (bidderName === '白展堂') {
const dialogue = [
'“葵花点穴手!这宝贝我点上了!出个重价!”',
'“我老白绝不退缩,这盒子定有玄机!”',
'“我出价,看在座的谁敢跟我争夺!”'
];
speechQuote = dialogue[Math.floor(Math.random() * dialogue.length)];
} else if (bidderName === '佟湘玉') {
const dialogue = [
'“额滴神呀,谁也别抢额的绝世神兵,额出!”',
'“加就加!大明律法规定价高者得,额不怕你!”',
'“为了这间客坊,额拼了!”'
];
speechQuote = dialogue[Math.floor(Math.random() * dialogue.length)];
} else if (bidderName === '郭芙蓉') {
const dialogue = [
'“排——山——倒——海!砸死你们,这个价格直接拿下!”',
'“本姑娘看准了!谁敢抬价?我要定了!”',
'“哼!本侠女不差钱,出个大数!”'
];
speechQuote = dialogue[Math.floor(Math.random() * dialogue.length)];
} else if (bidderName === '李大嘴') {
const dialogue = [
'“瞧一瞧来一大勺!好家伙,我大嘴也来开开荤,加钱!”',
'“这木匣份量沉甸甸的,里头指不定是口绝世玄铁大锅!”',
'“谁也别拦着我,我大嘴大碗喝酒、高价抢标!”'
];
speechQuote = dialogue[Math.floor(Math.random() * dialogue.length)];
} else if (bidderName === '吕秀才') {
const dialogue = [
'“子曾经曰过,名不正言不顺!在下就出一口价!”',
'“账目精细,我算清了,这底气充足,子曰:得加价!”',
'“圣贤书中有云:书中自有黄金屋,在下也出价一试!”'
];
speechQuote = dialogue[Math.floor(Math.random() * dialogue.length)];
} else if (bidderName === '邢育森') {
const dialogue = [
'“保境安民,官差办公!本捕头看中这宝物带兵镇邪了!”',
'“大明律例写得明白,价高者得公物,本捕头奉命加价!”',
'“我看谁敢退缩!邢某在此,正气凛然,亮牌跟上!”'
];
speechQuote = dialogue[Math.floor(Math.random() * dialogue.length)];
} else if (bidderName.startsWith('盟友掌柜')) {
speechQuote = '“诸位好汉承让了!联盟掌柜在此竞锋!”';
} else {
speechQuote = '“算盘一声响,同福大掌柜霸气竞标!”';
}
const newLog: BiddingLog = {
timeStr: nowTimeStr(),
sender: bidderName,
message: `${speechQuote} 🪙 加价至 【${amount}文】 铜板!${timingLogMsg}`,
colorClass: bidderName === '大掌柜 (我)' ? 'text-green-700 font-extrabold' :
bidderName.startsWith('盟友掌柜') ? 'text-indigo-700 font-semibold' : 'text-gray-800'
};
setBiddingLogs(prev => {
const list = [newLog, ...prev.slice(0, 18)];
// Update P2P peers
if (hasPeerConnected && isHost) {
sendPeerAuctionMsg('SYNC_BID_STATE', {
currentBid: amount,
highestBidder: bidderName,
countdown: finalTimer,
logs: list
});
}
return list;
});
try {
playCoinSound();
} catch (e) {}
};
// --- Player places a Bid (Host / Client Unified) --- const handlePlayerBid = (increment: number) => { if (phase !== 'bidding') return;
// Check crying debuff
const me = participants.find(p => p.isLocal);
if (me && me.cryingTimeLeft > 0) {
showToast('😭 莫小贝的糖葫芦威力太强,你正伤心痛哭,无法抢叫!');
return;
}
const targetPrice = currentBid + increment;
if (coins < targetPrice) {
showToast('⚠️ 客栈库房现银不足,钱不够了!');
return;
}
playClickFeedback();
if (isHost) {
processRawBid('大掌柜 (我)', targetPrice);
} else {
// Send Bid demand to host referee
const peerShort = peerId ? peerId.substring(0, 4).toUpperCase() : 'CLIENT';
sendPeerAuctionMsg('CLIENT_PLACED_BID', {
bidderName: `盟友掌柜 (${peerShort})`,
bidAmount: targetPrice
});
}
};
const handleAllInBid = () => { const me = participants.find(p => p.isLocal); if (me && me.cryingTimeLeft > 0) { showToast('😭 无法参与抢拍:哭啼中!'); return; }
const affordable = Math.min(coins, currentBid + 150);
if (affordable <= currentBid) {
showToast('⚠️ 钱没有别人多,没法梭哈了!');
return;
}
playClickFeedback();
if (isHost) {
processRawBid('大掌柜 (我)', affordable);
} else {
const peerShort = peerId ? peerId.substring(0, 4).toUpperCase() : 'CLIENT';
sendPeerAuctionMsg('CLIENT_PLACED_BID', {
bidderName: `盟友掌柜 (${peerShort})`,
bidAmount: affordable
});
}
};
// --- End Bidding on Selected Box (Host authoritative) --- const handleBoxBiddingExpired = () => { const winner = highestBidder; const finalPricePaid = currentBid;
// 1. Log completion in box state
setBoxes(prev => {
const updated = [...prev];
if (updated[activeBoxIndex]) {
updated[activeBoxIndex].winnerName = winner;
updated[activeBoxIndex].finalPrice = finalPricePaid;
}
// Deduct coins from winner immediately so ledger reports properly
if (winner === '大掌柜 (我)') {
onModifyCoins(-finalPricePaid);
addInnLog(`⚖️ 竞得封箱!你成功拍得官府箱子 【${updated[activeBoxIndex].name}】,共支付 🪙${finalPricePaid} 文!`);
} else {
// deduct from AI/Peer
setParticipants(prevPart =>
prevPart.map((p) => {
if (p.name === winner) {
return { ...p, coins: Math.max(0, p.coins - finalPricePaid) };
}
return p;
})
);
}
// Handle unboxing next step or continue
const nextIdx = activeBoxIndex + 1;
if (nextIdx < updated.length) {
// Move to bid next
setTimeout(() => {
setActiveBoxIndex(nextIdx);
setCurrentBid(updated[nextIdx].apparentCost);
setHighestBidder('官方法官');
setCountdown(15);
setBiddingLogs([{
timeStr: nowTimeStr(),
sender: '⚖️ 衙门捕快',
message: `啪!官槌落下!【${updated[activeBoxIndex]?.name || '上一个'}】由 【${winner}】 以 🪙${finalPricePaid}文 斩获!接下来开拍下一个封箱:宝箱 【${updated[nextIdx].name}】!`,
colorClass: 'text-rose-800 font-extrabold text-sm'
}]);
if (hasPeerConnected) {
sendPeerAuctionMsg('NEXT_AUCTION_BOX', {
boxIndex: nextIdx,
apparentCost: updated[nextIdx].apparentCost
});
}
}, 3500);
} else {
// Transition completely to UNBOXING
setTimeout(() => {
setPhase('unboxing');
setUnboxedIndex(0);
setUnboxedAnimation('closed');
if (hasPeerConnected) {
sendPeerAuctionMsg('TRIGGER_UNBOXING_PHASE', {
boxes: updated,
participants: participants
});
}
}, 3500);
}
return updated;
});
};
// --- Perform Character Inspection Skills --- const invokeStaffInspectionSkill = (staffId: StaffId) => { if (phase !== 'observe') return;
// Check lock
if (!staff[staffId]?.unlocked) {
showToast(`⚠️ 同福客栈内尚未招揽该伙伴,技能暂未解锁!`);
return;
}
if (coins < skillUseCost) {
showToast(`⚠️ 现银不足,无法资助伙伴使用特技!`);
return;
}
playClickFeedback();
onModifyCoins(-skillUseCost);
setBoxes(prev => {
const updated = [...prev];
const box = updated[selectedBoxIndex];
if (box) {
box.revealedClues[staffId] = true;
}
// Synchronize in P2P WebRTC
if (hasPeerConnected) {
sendPeerAuctionMsg('SKILL_REVEALED', {
boxIdx: selectedBoxIndex,
staffId,
clueText: box?.clues[staffId] || ''
});
}
return updated;
});
try {
playSweepSound();
} catch (e) {}
showToast(`🧙♂️ 【${staff[staffId].name}】施展绝技!点看下方机密获知详情!`);
};
// --- Unboxing Phase Click Trigger (Host/Client Synchronized) --- const triggerBoxUnseal = () => { if (phase !== 'unboxing') return; if (unboxedAnimation !== 'closed') return;
const currentBox = boxes[unboxedIndex];
if (!currentBox) return;
setUnboxedAnimation('opening');
playClickFeedback();
setTimeout(() => {
setUnboxedAnimation('opened');
let resultText = '';
const contents = currentBox.contents;
const winner = currentBox.winnerName;
// Handle item delivery or penalties
if (contents.isPrank) {
resultText = `【整蛊之灾】😭 内藏臭气熏天之物:${contents.label}!`;
// Trigger negative effects
if (contents.prankType === 'tanghulu') {
// Make other random bidders cry
setParticipants((prev) =>
prev.map((p) => {
if (p.name !== winner) {
return { ...p, cryingTimeLeft: 5 }; // Block from bidding 5 seconds
}
return p;
})
);
if (winner !== '大掌柜 (我)') {
onModifyReputation(-10); // Reputation drop
addInnLog(`😭 玩家拍到了莫小贝吃剩的糖葫芦,吃得大哭,客栈声望 -10!`);
} else {
addInnLog(`🤣 你拍得了小贝的哭哭糖葫芦,用它整到在座所有的商会同行!`);
}
} else if (contents.prankType === 'tofu') {
// Stun modifiers
setParticipants((prev) =>
prev.map((p) => {
if (p.name !== winner) {
return { ...p, budgetModifier: 0.85 }; // heavy budget drop
}
return p;
})
);
addInnLog(`😷 ${winner} 拍到了大嘴的陈年馊臭豆腐,熏倒了全场豪客!`);
} else if (contents.prankType === 'ruler') {
// Deduct from local or other
if (winner === '大掌柜 (我)') {
onModifyCoins(-30);
addInnLog(`⚖️ 衙门戒尺:由于你抢到戒尺,被衙门强征 30 文治安建设款!`);
}
}
} else if (contents.item) {
resultText = `【稀世宝剑】✨ 恭喜斩获:【${contents.item.name}】(${currentBox.secretValue}文)!`;
// If local player won, put directly in baggage
if (winner === '大掌柜 (我)') {
onAddBaggageItem(contents.item);
onModifyReputation(35); // Boost rep!
addInnLog(`🗡️ 开箱重宝!拍卖抢斩得一件【${contents.item.name}】并放进行囊。声望 +35!`);
} else {
addInnLog(`👏 豪客同行 【${winner}】 喜纳珍宝:【${contents.item.name}】!`);
}
} else {
// Garbage junk or raw refund
resultText = `【残铁腐木】🗑️ 回收底蕴:${contents.label}!`;
if (winner === '大掌柜 (我)') {
onModifyCoins(currentBox.secretValue);
onModifyReputation(5);
addInnLog(`🗑️ 变卖破烂:你将拍到的${contents.label}拉去废品回收,挽回损失 ${currentBox.secretValue} 铜钱。`);
}
}
setUnboxedResult(resultText);
// Synced Peer
if (hasPeerConnected && isHost) {
sendPeerAuctionMsg('NEXT_UNBOX_REVEAL', {
index: unboxedIndex,
resultText: resultText
});
}
}, 1800);
};
// Move to next unboxing reveal or wrap statistics const handleNextUnbox = () => { const nextIdx = unboxedIndex + 1; if (nextIdx < boxes.length) { setUnboxedIndex(nextIdx); setUnboxedAnimation('closed'); setUnboxedResult(''); } else { // Advance to final LEDGER Audting! setPhase('ledger');
if (hasPeerConnected && isHost) {
sendPeerAuctionMsg('END_ALL_AUCTION', {
finalBoxes: boxes,
finalParticipants: participants
});
}
}
};
// --- Exit Auction and return back to Lobby panel --- const handleExitAuction = () => { playClickFeedback(); setPhase('lobby'); setBoxes([]); };
// Get active item rarity color classes const getRarityClass = (rarity?: string) => { if (rarity === 'legendary') return 'border-amber-400 bg-amber-50 text-amber-700 font-extrabold animate-shimmer'; if (rarity === 'epic') return 'border-purple-400 bg-purple-50 text-purple-700 font-semibold'; if (rarity === 'rare') return 'border-blue-400 bg-blue-50 text-blue-700'; return 'border-gray-300 bg-gray-50 text-gray-700'; };
return (
{/* BACKGROUND GRAPHIC ACCENTS */}
<div className="absolute right-[-30px] bottom-[-30px] text-[#8d6e63]/10 text-9xl pointer-events-none font-brush transform -rotate-12">
官
</div>
{/* TOAST NOTIFICATION CONTAINER */}
{toastMsg && (
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 z-50 bg-[#801010] text-[#fffdfa] text-xs font-serif px-4 py-2.5 rounded-full border border-amber-300 shadow-2xl flex items-center gap-2 animate-bounce">
<AlertCircle className="w-4 h-4 text-amber-300 shrink-0" />
<span>{toastMsg}</span>
</div>
)}
{/* --- PHASE A: CHOOSE LOBBY & FEES --- */}
{phase === 'lobby' && (
<div className="flex-1 flex flex-col items-center justify-start md:justify-center p-4 max-w-xl mx-auto text-center gap-6 animate-fadeIn py-6">
<div className="relative">
<div className="w-20 h-20 bg-gradient-to-tr from-[#b71c1c] to-[#e65100] rounded-full flex items-center justify-center shadow-lg border-2 border-amber-300 mx-auto animate-pulse">
<Gavel className="w-10 h-10 text-amber-100" />
</div>
<span className="absolute bottom-[-6px] right-2 bg-amber-500 text-white text-[10px] font-black px-1.5 py-0.5 rounded-full border border-[#fdfaf2]">
NEW
</span>
</div>
<div className="space-y-2">
<h1 className="text-2xl font-serif font-black tracking-widest text-[#aa382c] border-b-2 border-dashed border-[#aa382c]/30 pb-2">
官府充公库房竞拍
</h1>
<p className="text-xs text-[#5d4037] font-medium max-w-sm mx-auto">
衙门抄缴库房对民间合伙商会限时拍卖!
看透线索、小心拆封、实时叫价博弈、抢拍江湖至高重宝!
</p>
</div>
{/* NETWORKING SYNC PANEL */}
<div className="w-full bg-[#efebe9]/60 border border-[#d7ccc8] rounded-xl p-3.5 space-y-2 text-left animate-fadeIn">
<h2 className="text-xs font-serif font-bold text-[#3e2723] flex items-center justify-between border-b pb-1.5">
<span>🎏 联机及局内状态</span>
<span className={`text-[10px] px-2 py-0.5 rounded-full ${hasPeerConnected ? 'bg-emerald-100 text-emerald-800' : 'bg-orange-100 text-orange-850'}`}>
{hasPeerConnected ? '🔴 密友直连已就绪' : '🧑💻 单人自律模拟模式'}
</span>
</h2>
<div className="text-[11px] text-[#5d4037] space-y-1 font-sans">
<p>• <b>自律玩法</b>:支持多名机器人对手陪玩。系统预设或由您随心点选!</p>
<p>• <b>P2P 联机</b>:好友连接后可自动抢占 <b>一号客位席</b> 并自动同步房主的机器人配置!</p>
</div>
</div>
{/* CUSTOM BOT SELECTION FOR DEBUG STAGE */}
<div className="w-full bg-[#efebe9]/60 border border-[#d7ccc8] rounded-xl p-3.5 space-y-3 text-left">
<h2 className="text-xs font-serif font-bold text-[#3e2723] flex items-center justify-between border-b pb-1.5">
<span>🤖 调试席位:配置出价对手 ({selectedBotIds.length} 名)</span>
<span className="text-[10px] text-[#aa382c] font-sans font-bold">点选即时增减</span>
</h2>
<div className="grid grid-cols-3 gap-2">
{ALL_POTENTIAL_AI_BOTS.map((bot) => {
const isSelected = selectedBotIds.includes(bot.id);
return (
<button
key={bot.id}
onClick={() => {
playClickFeedback();
if (isSelected) {
if (selectedBotIds.length <= 1) {
showToast('⚠️ 最少保留一名出价对手!');
return;
}
setSelectedBotIds(selectedBotIds.filter((id) => id !== bot.id));
} else {
if (selectedBotIds.length >= 4) {
showToast('⚠️ 席位已满,最多配置 4 名机器人对手!');
return;
}
setSelectedBotIds([...selectedBotIds, bot.id]);
}
}}
className={`p-1.5 rounded-lg border text-center transition-all flex flex-col items-center justify-between cursor-pointer ${
isSelected
? 'bg-[#fbe9e7] border-[#aa382c] text-[#aa382c] shadow-sm font-bold scale-102'
: 'bg-white/40 hover:bg-white border-[#d7ccc8] text-gray-600'
}`}
title={bot.desc}
>
<div className="text-lg mb-0.5">{bot.avatar}</div>
<div className="text-[10px] leading-tight">{bot.name}</div>
<div className="text-[8px] text-[#8d6e63] font-mono mt-0.5 text-center leading-none">
首资: {bot.defaultCoins}文
</div>
</button>
);
})}
</div>
<p className="text-[9.5px] text-gray-500 text-center leading-tight">
* 李大嘴、吕秀才、邢捕头、老白等,皆备有各自专属性格、竞标偏好与市井大白话碎碎念。
</p>
</div>
{/* ENTRY CHECK CARDS */}
<div className="grid grid-cols-2 gap-4 w-full">
<div className={`p-3 rounded-xl border border-dashed flex flex-col justify-between items-center bg-[#efebe9]/30 h-28 ${canAffordEntrance ? 'border-amber-500' : 'border-red-400'}`}>
<div className="flex items-center gap-1.5 text-xs text-[#5d4037] font-bold">
<Coins className="w-4 h-4 text-amber-600" />
<span>银票入场费</span>
</div>
<div className="my-1.5">
<p className="text-lg font-mono font-black text-[#8d6e63]">
100 <span className="text-xs font-serif text-[#aa382c]">文</span>
</p>
</div>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full ${canAffordEntrance ? 'bg-emerald-100 text-emerald-800' : 'bg-red-100 text-red-800'}`}>
{canAffordEntrance ? '额度充足' : '铜板不足'}
</span>
</div>
<div className="p-3 rounded-xl border border-dashed border-sky-400 flex flex-col justify-between items-center bg-[#efebe9]/30 h-28">
<div className="flex items-center gap-1.5 text-xs text-[#5d4037] font-bold">
<ScrollText className="w-4 h-4 text-sky-600" />
<span>衙门特许令</span>
</div>
<div className="my-1 text-[11px] text-sky-800 font-serif leading-tight">
<p>特许名帖通关卡</p>
<p className="text-[9px] text-[#8d6e63] font-sans mt-0.5">(当前未随身携,以100铜板入内)</p>
</div>
<span className="text-[10px] bg-slate-100 text-slate-800 font-bold px-2 py-0.5 rounded-full">
直接放行
</span>
</div>
</div>
{/* ACT BUTTONS */}
<div className="w-full space-y-2 mt-2">
<button
onClick={handleStartAuction}
disabled={!canAffordEntrance && !isLobbyCreator && hasPeerConnected}
className={`w-full py-3 rounded-xl font-serif font-black tracking-widest text-[#fffdfa] text-md cursor-pointer transition-all border shadow-lg flex items-center justify-center gap-2 ${
canAffordEntrance
? 'bg-gradient-to-r from-[#aa382c] to-[#801010] hover:scale-103 border-[#600000]'
: 'bg-gray-400 border-gray-500 cursor-not-allowed opacity-50'
}`}
>
<Gavel className="w-5 h-5" />
{(!isHost && hasPeerConnected) ? '等待房主拉取席位...' : '花费 🪙100 文 充公库竞拍'}
</button>
<p className="text-[10px] text-gray-500 italic">
* 押起官契,一锤定音。库房所得,直接扣发发放到当栈行囊!
</p>
</div>
</div>
)}
{/* --- PHASE B: OBSERVATION TIME (线索探查) --- */}
{phase === 'observe' && (
<div className="flex-1 flex flex-col gap-3 animate-fadeIn">
{/* HEADER TIMING */}
<div className="flex items-center justify-between border-b pb-2">
<div className="flex items-center gap-2">
<Gavel className="w-5 h-5 text-[#aa382c] shrink-0" />
<span className="font-serif font-black tracking-wide text-[#aa382c] text-sm">
充公库房观察探视期
</span>
</div>
<div className="flex items-center gap-2 bg-amber-50 border border-amber-300 px-3 py-1 rounded-full shadow-inner">
<Clock className="w-3.5 h-3.5 text-amber-700 animate-spin" />
<span className="text-xs font-mono font-bold text-amber-800">
观察剩余: {countdown}s
</span>
</div>
</div>
{/* INSTRUCTION TEXT */}
<div className="bg-amber-100/40 p-2.5 rounded-xl border border-amber-200/60 text-xs flex gap-2 items-start text-[#5d4037]">
<Compass className="w-4 h-4 text-[#aa382c] shrink-0 mt-0.5" />
<p className="leading-tight text-[11px]">
库房中央摆放着四只<b>衙门没收封条箱</b>!每只箱子内藏奇珍或江湖整蛊道具。
现在可以<b>消耗 🪙15文 铜板</b>让同福客栈解锁的好汉使用绝活听音和算账,掌握多余线索估值,防止被忽悠!
</p>
</div>
{/* SEIZE BOXES MULTI RANGE */}
<div className="grid grid-cols-4 gap-2.5 my-1.5">
{boxes.map((box, idx) => (
<button
key={box.id}
onClick={() => { playClickFeedback(); setSelectedBoxIndex(idx); }}
className={`p-3 rounded-xl border-2 flex flex-col items-center gap-2 transition-all relative ${
selectedBoxIndex === idx
? 'border-[#aa382c] bg-amber-100/30 ring-2 ring-[#aa382c]/20 scale-103'
: 'border-dashed border-[#8d6e63]/40 hover:border-[#8d6e63]'
}`}
>
<div className="text-3xl filter drop-shadow">
📦
</div>
{/* SEAL TAG */}
<div className="absolute top-1 right-1 bg-red-650 text-white text-[8px] px-1 py-0.5 rounded leading-none border border-red-300">
封
</div>
<div className="text-center">
<p className="text-[10px] font-bold text-[#5d4037] truncate max-w-[70px]">
{box.name}
</p>
<p className="text-[9px] text-[#aa382c] font-mono mt-0.5 font-semibold">
底价:🪙{box.apparentCost}
</p>
</div>
</button>
))}
</div>
{/* CHOSEN BOX DETAIL CARD */}
{boxes[selectedBoxIndex] && (
<div className="flex-1 min-h-[140px] bg-[#efebe9]/50 border-2 border-[#d7ccc8] rounded-xl p-3 flex flex-col justify-between">
<div>
<h3 className="text-xs font-serif font-black text-[#3e2723] flex items-center gap-1.5 border-b pb-1.5">
<span>📬 【当前锁定】{boxes[selectedBoxIndex].name}</span>
<span className="text-[10px] bg-[#8d6e63] text-[#fdfaf2] px-2 py-0.5 rounded-full font-sans font-medium">
底契估价: ${boxes[selectedBoxIndex].apparentCost}文
</span>
</h3>
<p className="text-[10px] text-gray-500 italic mt-1.5 leading-normal">
{boxes[selectedBoxIndex].desc}
</p>
</div>
{/* REVEALED CLUES SPEECHES */}
<div className="my-2 bg-[#fdfaf2] border rounded-lg p-2 max-h-[110px] overflow-y-auto space-y-1.5">
<p className="text-[10px] font-bold text-[#aa382c] border-b pb-0.5 flex justify-between">
<span>🎭 江湖客机密情报</span>
<span className="text-[9px] font-normal text-gray-500">点选好汉技能揭秘情报</span>
</p>
{Object.keys(boxes[selectedBoxIndex].revealedClues).some(k => boxes[selectedBoxIndex].revealedClues[k as StaffId]) ? (
(Object.keys(boxes[selectedBoxIndex].revealedClues) as StaffId[]).map((staffKey) => {
if (!boxes[selectedBoxIndex].revealedClues[staffKey]) return null;
return (
<p key={staffKey} className="text-[10px] leading-relaxed text-[#5d4037] bg-amber-50/60 p-1 rounded border border-amber-150">
• <b>{staff[staffKey]?.name || '老帮工'}:</b> {boxes[selectedBoxIndex].clues[staffKey]}
</p>
);
})
) : (
<div className="h-14 flex items-center justify-center">
<p className="text-[10.5px] text-gray-400 italic">
🕵️♂️ 此木箱密封完好。右侧支出铜钱差遣好汉技能,窥析内部宝物成分!
</p>
</div>
)}
</div>
</div>
)}
{/* STAFF TECHNICAL REVEALS SKILL PANEL */}
<div className="bg-[#efebe9]/40 border rounded-xl p-2.5">
<h4 className="text-[11.5px] font-serif font-black text-[#5d4037] mb-2 flex items-center justify-between">
<span>🧙♂️ 差遣伙计用神力探知 (费用: 🪙15文/次)</span>
<span className="text-[10px] text-gray-550 font-sans">解锁伙伴越多,探查维度越广!</span>
</h4>
<div className="grid grid-cols-5 gap-1.5">
{/* LU RONG */}
<button
onClick={() => invokeStaffInspectionSkill('lu')}
className={`py-1 rounded-lg border text-center transition-all ${
staff.lu.unlocked
? 'bg-amber-50 hover:bg-amber-100 border-amber-300'
: 'bg-gray-100 opacity-60 cursor-not-allowed border-gray-200'
}`}
>
<div className="text-xs font-serif font-black text-[#aa382c] flex flex-col items-center gap-1">
<span>算账</span>
<span className="text-[9px] text-[#8d6e63] font-normal">{staff.lu.unlocked ? '秀才估底' : '🔏未解锁'}</span>
</div>
</button>
{/* BAI */}
<button
onClick={() => invokeStaffInspectionSkill('bai')}
className={`py-1 rounded-lg border text-center transition-all ${
staff.bai.unlocked
? 'bg-orange-50 hover:bg-orange-100 border-orange-300'
: 'bg-gray-100 opacity-60 cursor-not-allowed border-gray-200'
}`}
>
<div className="text-xs font-serif font-black text-[#aa382c] flex flex-col items-center gap-1">
<span>微眯</span>
<span className="text-[9px] text-[#8d6e63] font-normal">{staff.bai.unlocked ? '老白辨真' : '🔏未解锁'}</span>
</div>
</button>
{/* TONG */}
<button
onClick={() => invokeStaffInspectionSkill('tong')}
className={`py-1 rounded-lg border text-center transition-all ${
staff.tong.unlocked
? 'bg-red-50 hover:bg-red-100 border-red-300'
: 'bg-gray-100 opacity-60 cursor-not-allowed border-gray-200'
}`}
>
<div className="text-xs font-serif font-black text-[#aa382c] flex flex-col items-center gap-1">
<span>财嗅</span>
<span className="text-[9px] text-[#8d6e63] font-normal">{staff.tong.unlocked ? '掌柜概率' : '🔏未解锁'}</span>
</div>
</button>
{/* GUO */}
<button
onClick={() => invokeStaffInspectionSkill('guo')}
className={`py-1 rounded-lg border text-center transition-all ${
staff.guo.unlocked
? 'bg-purple-50 hover:bg-purple-100 border-purple-300'
: 'bg-gray-100 opacity-60 cursor-not-allowed border-gray-200'
}`}
>
<div className="text-xs font-serif font-black text-[#aa382c] flex flex-col items-center gap-1">
<span>试震</span>
<span className="text-[9px] text-[#8d6e63] font-normal">{staff.guo.unlocked ? '小芙试重' : '🔏未解锁'}</span>
</div>
</button>
{/* LI */}
<button
onClick={() => invokeStaffInspectionSkill('li')}
className={`py-1 rounded-lg border text-center transition-all ${
staff.li.unlocked
? 'bg-emerald-50 hover:bg-emerald-100 border-emerald-300'
: 'bg-gray-100 opacity-60 cursor-not-allowed border-gray-200'
}`}
>
<div className="text-xs font-serif font-black text-[#aa382c] flex flex-col items-center gap-1">
<span>尝咸</span>
<span className="text-[9px] text-[#8d6e63] font-normal">{staff.li.unlocked ? '大嘴尝味' : '🔏未解锁'}</span>
</div>
</button>
</div>
</div>
</div>
)}
{/* --- PHASE C: REAL-TIME BIDDING搏弈阶段 --- */}
{phase === 'bidding' && (
<div className="flex-1 flex flex-col gap-3 animate-fadeIn">
{/* MULTIPLAYER SCREEN FOR ALL SEATS */}
<div className="bg-[#efebe9] border border-[#d7ccc8] rounded-2xl p-2.5 flex flex-col gap-2">
<div className="flex items-center justify-between border-b pb-1.5 mb-1">
<span className="text-xs font-serif font-black text-rose-800 flex items-center gap-1">
<Gavel className="w-3.5 h-3.5" />
<span>实时叫价局(四人座)</span>
</span>
<div className="flex items-center gap-2 bg-rose-100 border border-rose-300 px-3 py-0.5 rounded-full shadow-sm">
<Hourglass className="w-3.5 h-3.5 text-rose-700 animate-pulse" />
<span className="text-xs font-mono font-black text-rose-800">
落指倒计时: {countdown}s
</span>
</div>
</div>
{/* SEATS LAYOUT */}
<div className="grid grid-cols-4 gap-2">
{participants.map((p, idx) => {
const isUnderCrying = p.cryingTimeLeft > 0;
const isHighest = highestBidder === p.name;
return (
<div
key={p.name}
className={`p-2 rounded-xl border flex flex-col items-center justify-between relative transition-all ${
isHighest
? 'bg-amber-100/75 border-amber-500 ring-2 ring-amber-400 scale-102 shadow-md'
: 'bg-white/80 border-gray-200 shadow-inner'
}`}
>
{/* CRITICAL INDICATOR */}
{isHighest && (
<span className="absolute top-[-8px] bg-amber-500 text-white text-[8px] font-black leading-none px-1.5 py-0.5 rounded-full border border-white">
领先持契
</span>
)}
{isUnderCrying && (
<span className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-red-800/80 text-white text-[9px] font-bold py-1 px-1.5 rounded text-center leading-tight z-10 shadow-lg">
😭 痛哭中 ({p.cryingTimeLeft}s)
</span>
)}
<div className="text-2xl mb-1">
{p.avatar}
</div>
<div className="text-center">
<p className="text-[10px] font-black text-gray-800 truncate max-w-[70px]">
{p.name}
</p>
<p className="text-[9.5px] font-mono text-gray-500 mt-0.5 leading-none">
携金: 🪙{Math.floor(p.coins)}
</p>
</div>
</div>
);
})}
</div>
</div>
{/* DYNAMIC MIDDLE AREA: THE BOX CURRENT ACTIVE AUCTION */}
{boxes[activeBoxIndex] && (
<div className="flex-1 min-h-[120px] bg-[#fffdfa] border-2 border-red-800/20 rounded-xl p-3 flex gap-3 relative overflow-hidden">
<div className="flex flex-col items-center justify-center p-2 rounded-lg bg-red-50/40 border border-red-150 shrink-0 w-24">
<div className="text-4xl filter drop-shadow animate-bounce">
📦
</div>
<p className="text-[9.5px] text-red-850 font-serif font-black mt-1 text-center bg-red-100 px-1 py-0.5 rounded leading-tight w-full truncate">
{boxes[activeBoxIndex].name}
</p>
</div>
<div className="flex-1 flex flex-col justify-between">
<div>
<div className="flex justify-between items-center border-b pb-1">
<span className="text-[11px] text-gray-550 italic font-medium">没收原底案价值: 🪙{boxes[activeBoxIndex].apparentCost}文</span>
<span className="text-[10px] bg-red-100 text-red-805 px-1.5 py-0.2 rounded font-bold">4个没收箱中第{activeBoxIndex + 1}只</span>
</div>
{/* REALTIME COIN COUNTERS */}
<div className="my-1.5 flex items-baseline gap-2">
<span className="text-xs font-serif text-[#aa382c]">当前叫价:</span>
<span className="text-2xl font-mono font-black text-[#aa382c] tracking-tight">
🪙 {currentBid}
</span>
<span className="text-[10.5px] text-gray-500 font-sans">
(领先者: <b className="text-yellow-750 font-black">{highestBidder}</b>)
</span>
</div>
</div>
{/* LOGS LIST WITH GORGEOUS SCROLLER */}
<div
id="bid-log-container"
className="bg-[#efebe9]/20 border rounded-lg p-1.5 h-[80px] overflow-y-auto space-y-1 font-mono text-[9px]"
>
{biddingLogs.length === 0 ? (
<div className="h-full flex items-center justify-center italic text-gray-400">
⚖️ 拍卖倒计时启动中,静等商会伙伴揭标首喊!
</div>
) : (
biddingLogs.map((log, idx) => (
<p key={idx} className={`leading-snug border-b border-gray-50 pb-0.5 last:border-0 ${log.colorClass}`}>
[{log.timeStr}] <b>{log.sender}</b>: {log.message}
</p>
))
)}
</div>
</div>
</div>
)}
{/* PLAYER CONTROLS ACTIONS */}
<div className="bg-[#efebe9]/60 border border-[#d7ccc8] rounded-xl p-2.5 flex flex-col gap-2">
<div className="flex items-center justify-between text-[11px]">
<span className="font-serif font-black text-[#5d4037] flex items-center gap-1">
<Coins className="w-3.5 h-3.5 text-amber-600" />
<span>你当前余款: <b className="text-[#aa382c] text-xs font-mono">{coins}文</b></span>
</span>
<span className="text-[10px] text-gray-500">落锤成交后直接从余款结算</span>
</div>
<div className="grid grid-cols-4 gap-1.5">
<button
onClick={() => handlePlayerBid(5)}
disabled={coins < currentBid + 5}
className="py-2.5 bg-gradient-to-b from-[#efebe9] to-[#d7ccc8] hover:scale-102 rounded-lg border border-[#c6b0aa] text-[#4e342e] text-xs font-serif font-black shadow-sm"
>
+5文 竞逐
</button>
<button
onClick={() => handlePlayerBid(20)}
disabled={coins < currentBid + 20}
className="py-2.5 bg-gradient-to-b from-[#efebe9] to-[#d7ccc8] hover:scale-102 rounded-lg border border-[#c6b0aa] text-[#4e342e] text-xs font-serif font-black shadow-sm"
>
+20文 逼抢
</button>
<button
onClick={() => handlePlayerBid(50)}
disabled={coins < currentBid + 50}
className="py-2.5 bg-gradient-to-b from-[#ffd54f] to-[#ffb300] hover:scale-102 rounded-lg border border-yellow-600 text-amber-950 text-xs font-[#3e2723] font-black shadow-sm"
>
+50文 压制
</button>
<button
onClick={handleAllInBid}
disabled={coins <= currentBid}
className="py-2.5 bg-gradient-to-b from-[#aa382c] to-[#7f0000] hover:scale-102 rounded-lg border border-red-950 text-white text-xs font-serif font-black shadow-lg"
>
强拍砸场💣
</button>
</div>
<p className="text-[9px] text-[#aa382c] font-medium leading-none text-center italic">
* 延时叫价规则:最后 3 秒内若发生竞逐叫价,倒数时自动复增为 4 秒,不战不快!
</p>
</div>
</div>
)}
{/* --- PHASE D: UNBOXING (开箱反转,现场打脸) --- */}
{phase === 'unboxing' && (
<div className="flex-1 flex flex-col gap-3 animate-fadeIn">
<div className="border-b pb-2 flex items-center justify-between">
<span className="font-serif font-black text-[#aa382c] flex items-center gap-1 text-sm">
<Sparkles className="w-5 h-5 text-amber-500 animate-spin" />
<span>官府缉私库房开箱见底</span>
</span>
<span className="text-xs bg-[#efebe9] text-[#5d4037] px-2.5 py-0.5 rounded-full font-mono font-bold">
第 {unboxedIndex + 1} 只 / 共 {boxes.length} 只箱
</span>
</div>
<div className="flex-1 flex flex-col items-center justify-center text-center gap-4 py-3">
{/* LARGE BOX VISUAL WITH THREE STAGES OF ANIMATION */}
<div className="relative my-4">
<div className={`text-8xl filter drop-shadow-2xl transition-all ${
unboxedAnimation === 'opening' ? 'animate-bounce scale-110 rotate-12' :
unboxedAnimation === 'opened' ? 'scale-100 opacity-90' : 'animate-pulse hover:scale-105'
}`}>
{unboxedAnimation === 'opened' ? '🔓' : '📦'}
</div>
{/* paper seal */}
{unboxedAnimation === 'closed' && (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-[#aa382c] text-[#fffdfa] border border-amber-300 text-[10px] font-serif font-extrabold px-1.5 py-0.5 rounded shadow-lg select-none pointer-events-none transform -rotate-12 z-20">
朱砂封印
</div>
)}
</div>
{/* CHOSEN BOX SUMMARY DETAIL */}
{boxes[unboxedIndex] && (
<div className="space-y-1 max-w-sm">
<h3 className="text-sm font-serif font-black text-[#3e2723]">
【{boxes[unboxedIndex].name}】
</h3>
<div className="text-[11px] text-gray-500 font-sans">
<span>拍得赢家: <b className="text-rose-800 font-black">{boxes[unboxedIndex].winnerName}</b></span>
<span className="mx-2">|</span>
<span>成交价格Paid: <b className="text-amber-800 font-mono font-bold">🪙{boxes[unboxedIndex].finalPrice}文</b></span>
</div>
</div>
)}
{/* MAIN ACTION REVEAL BUTTON */}
<div className="w-full max-w-xs space-y-3 my-2">
{unboxedAnimation === 'closed' && (
<button
onClick={triggerBoxUnseal}
className="w-full py-3 bg-gradient-to-r from-red-850 to-amber-700 hover:scale-103 text-[#fffdfa] rounded-xl font-serif font-black tracking-widest text-sm shadow-xl border border-red-950 flex items-center justify-center gap-2"
>
<Gavel className="w-5 h-5" />
撕开大印 揭底开箱!
</button>
)}
{unboxedAnimation === 'opening' && (
<div className="py-3 bg-gray-100 text-[#8d6e63] font-serif font-black text-sm rounded-xl border animate-pulse flex items-center justify-center gap-2">
<Hourglass className="w-4 h-4 animate-spin text-[#aa382c]" />
商会掌柜屏息凝视中...
</div>
)}
{unboxedAnimation === 'opened' && (
<div className="space-y-4 w-full animate-fadeIn">
{/* RESULT NOTIFICATION BOX */}
<div className="bg-[#fffdfa] shadow-inner border border-amber-300/60 p-4 rounded-xl space-y-2">
<p className="text-xs font-serif font-bold text-[#aa382c] border-b pb-1">
👑 官府库书登记簿
</p>
<p className="text-xs text-[#5d4037] leading-relaxed font-sans font-bold">
{unboxedResult}
</p>
<p className="text-[10px] text-gray-450 mt-1 leading-snug">
{boxes[unboxedIndex].contents.description}
</p>
</div>
<button
onClick={handleNextUnbox}
className="w-full py-2.5 bg-gradient-to-r from-[#efebe9] to-[#d7ccc8] hover:scale-102 rounded-xl text-[#3e2723] border border-[#c6b0aa] font-serif font-black text-xs shadow-md flex items-center justify-center gap-1 cursor-pointer"
>
<span>{unboxedIndex + 1 < boxes.length ? '下一只封箱' : '核算商会总账簿 ⚖️'}</span>
<ChevronRight className="w-4 h-4 inline" />
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* --- PHASE E: TOTAL LEDGER (商会审计账簿) --- */}
{phase === 'ledger' && (
<div className="flex-1 flex flex-col gap-3 animate-fadeIn">
<div className="border-b pb-2 flex items-center justify-between">
<span className="font-serif font-black text-[#aa382c] flex items-center gap-1.5 text-sm">
<FileSpreadsheet className="w-5 h-5 text-emerald-700" />
<span>官府缉私行署:联合商会审计总账</span>
</span>
<span className="text-xs bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded-full font-bold">
⚖️ 账目明晰
</span>
</div>
<p className="text-[10.5px] text-gray-550 leading-tight">
合伙商会官印盖章,本次官库没收物品竞拍正式结束。
以下为本次被抄家物品名录的<b>成交价</b>(支付官银)与<b>黑市回收实重价值</b>(评估市值)明细:
</p>
{/* LEDGER GRID TABLE */}
<div className="flex-1 min-h-[220px] bg-white border border-gray-200 rounded-xl overflow-x-auto shadow-inner p-1">
<table className="w-full text-left text-[10px] font-sans">
<thead>
<tr className="bg-[#efebe9]/55 font-serif font-black text-[#5d4037] border-b text-[11px] leading-relaxed">
<th className="p-2 truncate max-w-[80px]">箱体名称</th>
<th className="p-2">最终买主</th>
<th className="p-2 text-right">买入单价</th>
<th className="p-2 text-right">评估市值</th>
<th className="p-2 text-right">盈亏核实</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100 font-mono">
{boxes.map((box) => {
const gain = box.secretValue - box.finalPrice;
const isLose = gain < 0;
return (
<tr key={box.id} className="hover:bg-gray-50 text-[10px]">
<td className="p-2 font-serif font-semibold truncate max-w-[90px]">{box.name}</td>
<td className="p-2 truncate text-gray-700">{box.winnerName || '流拍/官方'}</td>
<td className="p-2 text-right font-bold text-gray-800">🪙{box.finalPrice}</td>
<td className="p-2 text-right text-[#aa382c] font-black">🪙{box.secretValue}</td>
<td className={`p-2 text-right font-black ${isLose ? 'text-red-650' : 'text-emerald-700'}`}>
{gain >= 0 ? '+' : ''}{gain}文
</td>
</tr>
);
})}
</tbody>
</table>
{boxes.length === 0 && (
<div className="h-full flex items-center justify-center italic text-gray-400">
⚠️ 本次尚无开标账本。
</div>
)}
</div>
{/* EVALUATION BANNER SUMMARY AND COMICAL PUNCTUATION */}
{(() => {
const myWins = boxes.filter(b => b.winnerName === '大掌柜 (我)');
const totalSpent = myWins.reduce((sum, b) => sum + b.finalPrice, 0);
const totalValue = myWins.reduce((sum, b) => sum + b.secretValue, 0);
const netProfit = totalValue - totalSpent;
return (
<div className={`p-3 rounded-xl border flex flex-col justify-between ${
netProfit > 200 ? 'bg-amber-50 border-amber-400 text-amber-950' :
netProfit > 0 ? 'bg-emerald-50 border-emerald-300 text-emerald-950' :
'bg-red-50 border-red-300 text-red-950'
}`}>
<div className="flex justify-between items-center text-xs">
<span className="font-serif font-black flex items-center gap-1">
<UserCheck className="w-4 h-4 text-[#aa382c]" />
<span>大掌柜的买入总结算</span>
</span>
<span className="font-mono font-bold leading-none">
拍得: <b className="text-[#aa382c]">{myWins.length}</b> 只
</span>
</div>
<div className="mt-2 text-[11px] space-y-1">
<p>• 你此次拍耗铜板共计:<b>{totalSpent} 文</b></p>
<p>• 获得武备和回收市值共:<b>{totalValue} 文</b></p>
<p className="font-serif font-black flex items-center gap-1.5 mt-1 text-xs">
<span>💡 合计数盈亏结余:</span>
<span className={netProfit >= 0 ? 'text-emerald-700' : 'text-red-700'}>
{netProfit >= 0 ? '大赚一笔!' : '不幸遭割!'}{netProfit} 文
</span>
</p>
</div>
</div>
);
})()}
{/* BOTTOM CLOSING EXIT ACTION */}
<div className="w-full mt-1.5">
<button
onClick={handleExitAuction}
className="w-full py-3 bg-gradient-to-r from-[#aa382c] to-[#7f0000] text-white rounded-xl font-serif font-black tracking-widest text-xs shadow-md border cursor-pointer hover:scale-102 flex items-center justify-center gap-2"
>
<Gavel className="w-4 h-4 inline" />
收拢账目 离开库房大院
</button>
</div>
</div>
)}
</div>
); };