/* BeatEnemy.jsx — 軍法處置 v2 (Public Enemy Wall + Slap Mechanic) - Admin-curated queue of up to 10 enemies (each has hits + maxHP 500) - Front-end shows ONLY the top 3 active enemies for selection - Click an enemy card to select; slap them with 「軍法伺候·掌摑面門」 - Avatar filter intensifies past 35% / 75% damage - 10% crit chance produces giant gold floating text + bumps CRIT counter - Real-time battle log scrolls to bottom - When an enemy's hits == maxHP they cycle out, #4 slides in - All state in localStorage for demo. Real backend would use Firebase/Supabase. */ const MAX_HP = 500; const LS_QUEUE_KEY = "sw_martial_queue_v2"; const LS_LOG_KEY = "sw_martial_log_v2"; const LS_STATS_KEY = "sw_martial_stats_v2"; // Mock queue (in production, this is populated by admin via Firebase/Supabase) const SEED_QUEUE = [ { id: "SW-66666", nickname: "曹賊·孟德", avatar: null, hits: 120 }, { id: "SW-88888", nickname: "袁紹·本初", avatar: null, hits: 380 }, { id: "SW-44444", nickname: "呂奉先", avatar: null, hits: 240 }, { id: "SW-22222", nickname: "董卓·仲穎", avatar: null, hits: 0 }, { id: "SW-33333", nickname: "司馬懿·仲達", avatar: null, hits: 0 }, { id: "SW-55555", nickname: "夏侯惇", avatar: null, hits: 0 }, { id: "SW-77777", nickname: "張遼·文遠", avatar: null, hits: 0 }, ]; const ATTACK_LINES = [ "吃我一拳!", "叫你夜襲!", "戰犯納命來!", "敢犯吾盟?", "看招!", "受死吧!", ]; const BEG_LINES = [ "將軍饒命!", "再也不敢了!", "主公救我!", "饒了我吧!", "罪該萬死!", "求求您…", ]; // Attack methods (frontend-selectable) const ATTACK_KINDS = { slap: { cn: "掌 摑 面 門", en: "SLAP", eyebrow: "軍 法 伺 候", logVerb: "掴掌", floaters: ["掴!", "啦!", "昋!", "拍!", "損!"], }, spit: { cn: "吞 口 水", en: "SPIT", eyebrow: "變 隸 之 辱", logVerb: "吞液", floaters: ["吞!", "峨峨!", "不潔!", "呆走!", "商!"], }, egg: { cn: "丟 雞 蛋", en: "EGG", eyebrow: "群 愁 炸 面", logVerb: "雞蛋砸面", floaters: ["砸!", "象漾!", "滾!", "蛋黃滾滾!", "潝!"], }, }; // ─── Storage helpers ─────────────────────────────────────── const readJSON = (key, fallback) => { try { return JSON.parse(localStorage.getItem(key) || "null") ?? fallback; } catch { return fallback; } }; const writeJSON = (key, val) => { try { localStorage.setItem(key, JSON.stringify(val)); } catch {} }; // ─── Mock real-time backend (replace with Firebase/Supabase) ── // // async function syncQueue() { // // const snap = await firebase.firestore().collection("martial_queue").get(); // // return snap.docs.map(d => d.data()); // } // // async function pushHit(enemyId) { // // await firebase.firestore().collection("martial_queue").doc(enemyId) // // .update({ hits: firebase.firestore.FieldValue.increment(1) }); // } // ─── Palm SVG (slap overlay) ─────────────────────────────── const PalmSVG = () => ( {/* Stylised hand/palm in red-orange — simplified silhouette */} {/* palm */} {/* fingers — 5 columns */} {/* impact streaks */} ); // ─── Spit SVG (saliva globs + spray streaks) ─────────────── const SpitSVG = () => ( {/* central glob */} {/* drips */} {/* satellite droplets */} {/* spray streaks */} ); // ─── Egg SVG (yolk splat + cracked shell) ────────────────── const EggSVG = () => ( {/* egg-white splat — irregular blob */} {/* yolk */} {/* shell shards */} {/* yolk drips */} ); // ─── Component ───────────────────────────────────────────── const BeatEnemy = () => { // Queue state — seeded from localStorage or SEED_QUEUE const [queue, setQueue] = React.useState(() => readJSON(LS_QUEUE_KEY, SEED_QUEUE).map((e) => ({ ...e, hits: Math.min(Number(e.hits) || 0, MAX_HP) }))); const [selectedId, setSelectedId] = React.useState(null); const [isHit, setIsHit] = React.useState(false); const [floaters, setFloaters] = React.useState([]); // [{id, x, y, text, kind}] const [slaps, setSlaps] = React.useState([]); // [{id, x, y, rot}] const [log, setLog] = React.useState(() => readJSON(LS_LOG_KEY, [])); const [stats, setStats] = React.useState(() => readJSON(LS_STATS_KEY, { totalHits: 0, crit: 0, wrath: 0 })); const nextId = React.useRef(0); const logEndRef = React.useRef(null); // Persist on change React.useEffect(() => writeJSON(LS_QUEUE_KEY, queue), [queue]); React.useEffect(() => writeJSON(LS_LOG_KEY, log.slice(-50)), [log]); React.useEffect(() => writeJSON(LS_STATS_KEY, stats), [stats]); // Active enemies — only show top 3 alive (hits < MAX_HP), in queue order const activeQueue = queue.filter((e) => e.hits < MAX_HP).slice(0, 3); // Default selection: first active enemy React.useEffect(() => { if (!selectedId && activeQueue.length > 0) { setSelectedId(activeQueue[0].id); } else if (selectedId && !activeQueue.find((e) => e.id === selectedId)) { setSelectedId(activeQueue[0]?.id || null); } }, [queue, selectedId, activeQueue]); const selected = activeQueue.find((e) => e.id === selectedId) || activeQueue[0]; // Auto-scroll log React.useEffect(() => { if (logEndRef.current) { logEndRef.current.scrollTop = logEndRef.current.scrollHeight; } }, [log]); const pushLog = (text, kind) => { const t = new Date().toLocaleTimeString("zh-Hant", { hour12: false }); setLog((l) => [...l, { t, text, kind: kind || "" }].slice(-50)); }; const onStrike = (kind = "slap") => { if (!selected) return; setIsHit(true); setTimeout(() => setIsHit(false), 400); const meta = ATTACK_KINDS[kind] || ATTACK_KINDS.slap; const isCrit = Math.random() < 0.1; const fid = nextId.current++; // Floating text if (isCrit) { setFloaters((arr) => [...arr, { id: fid, x: 0, y: 0, text: "暴擊 !!! -999", kind: "crit", }]); } else { // 50% kind-specific yell, 30% generic taunt, 20% beg const r = Math.random(); let pool, sub; if (r < 0.5) { pool = meta.floaters; sub = kind; } else if (r < 0.8) { pool = ATTACK_LINES; sub = "attack"; } else { pool = BEG_LINES; sub = "beg"; } setFloaters((arr) => [...arr, { id: fid, x: (Math.random() * 160 - 80), y: (Math.random() * 30 - 15), text: pool[Math.floor(Math.random() * pool.length)], kind: sub, }]); } setTimeout(() => setFloaters((arr) => arr.filter((f) => f.id !== fid)), 1300); // Attack overlay — random position const sid = nextId.current++; setSlaps((arr) => [...arr, { id: sid, kind, x: 20 + Math.random() * 60, y: 20 + Math.random() * 60, rot: Math.random() * 80 - 40, scale: 0.8 + Math.random() * 0.4, }]); setTimeout(() => setSlaps((arr) => arr.filter((s) => s.id !== sid)), 700); // Increment hits + stats const newHits = selected.hits + 1; const reachedMax = newHits >= MAX_HP; setQueue((q) => q.map((e) => e.id === selected.id ? { ...e, hits: newHits } : e)); setStats((s) => ({ totalHits: s.totalHits + 1, crit: s.crit + (isCrit ? 1 : 0), wrath: Math.min(9999, s.wrath + 1), })); // Log if (isCrit) { pushLog(`「${selected.nickname}」面門中暴擊 ── 金光迸現,三軍喝采!`, "crit"); } else if (newHits % 25 === 0 || newHits < 5) { pushLog(`「${selected.nickname}」遵受 ${meta.logVerb} · 受刑進度 ${newHits.toLocaleString()} / ${MAX_HP.toLocaleString()}。`); } if (reachedMax) { pushLog(`敵將「${selected.nickname}」不堪羞辱,痛哭流涕,已被打暈抬走!`, "evict"); } }; // Damage progress for selected const progress = selected ? selected.hits / MAX_HP : 0; const tilt = progress >= 0.75 ? -8 : progress >= 0.35 ? -3 : 0; return (
PUBLIC ENEMY WALL · 軍法處置

敵 將 名 冊

・ 同盟公敵 · 衆怒所向 · 三人並列 · 打盡再續 ・
{/* === Top 3 enemy cards === */}
{activeQueue.length === 0 && (
名冊空缺 · 暫無敵將── 待管理員押解。
)} {activeQueue.map((e) => { const p = e.hits / MAX_HP; const isSelected = e.id === selectedId; return ( ); })}
{/* === Selected enemy — large slap arena + actions === */} {selected && (
CURRENT TARGET · 當前受刑
{selected.nickname}
= 0.75 ? "slap-face--severe" : progress >= 0.35 ? "slap-face--red" : ""}`} style={{ transform: `rotate(${tilt}deg)`, backgroundImage: selected.avatar ? `url(${selected.avatar})` : "none", }} > {!selected.avatar && {selected.nickname[0]}}
{/* Attack overlays (slap / spit / egg) */} {slaps.map((s) => (
{(s.kind === "spit") ? : (s.kind === "egg") ? : }
))} {/* Floating text */} {floaters.map((f) => ( {f.text} ))}
{Object.entries(ATTACK_KINDS).map(([k, meta]) => ( ))}
TOTAL LASHES
當將累積
{selected.hits.toLocaleString()}
剩餘 {(MAX_HP - selected.hits).toLocaleString()} 掌 · 即可打退
CRIT
{stats.crit.toLocaleString()}
暴 擊 次 數
WRATH
{stats.wrath.toLocaleString()}
同 盟 怒 氣
BATTLE LOG · 戰 報 LIVE
{log.length === 0 ? (
尚無軍法紀錄 …
) : log.map((l, i) => (
{l.t} {l.text}
))}
)}
); }; window.BeatEnemy = BeatEnemy;