import React, { useEffect, useMemo, useState } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Slider } from "@/components/ui/slider"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select"; import { Shuffle, Copy, Link as LinkIcon, Lock, Unlock, Trash2, Filter, Plus, RefreshCw, } from "lucide-react"; // --- Minimal data model (you can extend easily) --- const ELEMENTS = ["Glacio", "Fusion", "Electro", "Aero", "Havoc", "Spectro"] as const; const ROLES = ["主C", "副C/拐", "治疗"] as const; /** * A very small starter roster. You can add more characters anytime. * id must be stable; name is display; element+role drive filters. */ const STARTER_ROSTER = [ { id: "rover-s", name: "旅者·光(男)", element: "Spectro", role: "主C", rarity: 5 }, { id: "rover-h", name: "旅者·冥(女)", element: "Havoc", role: "主C", rarity: 5 }, { id: "jiyan", name: "忌炎", element: "Aero", role: "主C", rarity: 5 }, { id: "lingyang", name: "凌阳", element: "Glacio", role: "主C", rarity: 5 }, { id: "verina", name: "维里奈", element: "Spectro", role: "治疗", rarity: 5 }, { id: "yinlin", name: "吟霖", element: "Electro", role: "副C/拐", rarity: 5 }, { id: "calcharo", name: "卡卡罗", element: "Electro", role: "主C", rarity: 5 }, { id: "jianxin", name: "鉴心", element: "Aero", role: "副C/拐", rarity: 4 }, { id: "baizhi", name: "白芷", element: "Glacio", role: "治疗", rarity: 4 }, { id: "danjin", name: "丹瑾", element: "Fusion", role: "副C/拐", rarity: 4 }, { id: "taoqi", name: "桃祈", element: "Glacio", role: "治疗", rarity: 4 }, { id: "chixia", name: "炽霞", element: "Fusion", role: "主C", rarity: 4 }, { id: "mortefi", name: "莫特菲", element: "Fusion", role: "副C/拐", rarity: 4 }, { id: "yangyang", name: "杨瑶", element: "Aero", role: "副C/拐", rarity: 4 }, ] as const; // --- Utilities --- function mulberry32(seed: number) { return function () { let t = (seed += 0x6d2b79f5); t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } function pickRandom(rng: () => number, arr: T[]): T | undefined { if (!arr.length) return undefined; const idx = Math.floor(rng() * arr.length); return arr[idx]; } function parseOwned(): Record { try { const raw = localStorage.getItem("wuwa_owned"); return raw ? JSON.parse(raw) : {}; } catch { return {}; } } function saveOwned(obj: Record) { localStorage.setItem("wuwa_owned", JSON.stringify(obj)); } function encodeTeam(ids: string[]): string { return encodeURIComponent(ids.join(",")); } function decodeTeam(s: string | null): string[] { if (!s) return []; return decodeURIComponent(s).split(",").filter(Boolean); } // --- Component --- export default function App() { const [roster, setRoster] = useState([...STARTER_ROSTER]); const [owned, setOwned] = useState>(() => parseOwned()); const [teamSize, setTeamSize] = useState(3); const [allowSameElement, setAllowSameElement] = useState(false); const [rarity, setRarity] = useState<"all" | 4 | 5>("all"); const [elementFilter, setElementFilter] = useState<"all" | (typeof ELEMENTS)[number]>("all"); const [roleWeights, setRoleWeights] = useState>({ 主C: 2, "副C/拐": 2, 治疗: 2, }); const [seedStr, setSeedStr] = useState(""); const [lockedSlots, setLockedSlots] = useState<(string | null)[]>([null, null, null, null]); const [result, setResult] = useState([]); const [search, setSearch] = useState(""); // Load from URL if present useEffect(() => { const params = new URLSearchParams(location.search); const team = decodeTeam(params.get("team")); if (team.length) setResult(team); const seedQ = params.get("seed"); if (seedQ) setSeedStr(seedQ); }, []); // Derived lists const filteredPool = useMemo(() => { const base = roster.filter((c) => (rarity === "all" ? true : c.rarity === rarity)); const byEl = base.filter((c) => (elementFilter === "all" ? true : c.element === elementFilter)); const bySearch = byEl.filter((c) => c.name.toLowerCase().includes(search.toLowerCase())); const byOwned = bySearch.filter((c) => ownedOnly ? owned[c.id] : true); return byOwned; }, [roster, rarity, elementFilter, search, owned]); const ownedOnly = false; // you can expose a toggle later if you like function toggleOwned(id: string) { setOwned((prev) => { const next = { ...prev, [id]: !prev[id] }; saveOwned(next); return next; }); } function addCustomCharacter() { const name = prompt("自定义角色名称"); if (!name) return; const element = prompt(`元素 (${ELEMENTS.join("/")})`, "Spectro") as any; const role = prompt(`定位 (${ROLES.join("/")})`, "副C/拐") as any; const rarityStr = prompt("稀有度 4 或 5", "4"); const rarityNum = rarityStr === "5" ? 5 : 4; const id = name.trim().toLowerCase().replace(/\s+/g, "-") + "-custom"; setRoster((r) => [...r, { id, name, element, role, rarity: rarityNum }]); } function shareURL(ids: string[]) { const params = new URLSearchParams(); if (ids.length) params.set("team", encodeTeam(ids)); if (seedStr) params.set("seed", seedStr); const url = `${location.origin}${location.pathname}?${params.toString()}`; navigator.clipboard.writeText(url); alert("已复制分享链接到剪贴板!"); } function lockSlot(i: number, id?: string) { setLockedSlots((prev) => { const next = [...prev]; next[i] = id ?? (prev[i] ? null : result[i] ?? null); return next; }); } function removeFromTeam(i: number) { setResult((prev) => prev.filter((_, idx) => idx !== i)); setLockedSlots((prev) => prev.map((v, idx) => (idx === i ? null : v))); } function randomize() { const seed = seedStr ? hash(seedStr) : Date.now(); const rng = mulberry32(seed); // Start with locked picks const team: string[] = []; const usedElements = new Set(); for (let i = 0; i < teamSize; i++) { const lockedId = lockedSlots[i]; if (lockedId) { team[i] = lockedId; const el = roster.find((c) => c.id === lockedId)?.element; if (el) usedElements.add(el); } } // Build candidate pools by role preference const rolePool: { role: typeof ROLES[number]; weight: number }[] = []; ROLES.forEach((r) => rolePool.push({ role: r, weight: roleWeights[r] })); const pool = filteredPool.slice(); const chosen = new Set(team.filter(Boolean)); for (let i = 0; i < teamSize; i++) { if (team[i]) continue; // slot already locked // Spin a role by weight const targetRole = spinWeighted(rng, rolePool); const candidates = pool.filter((c) => !chosen.has(c.id) && c.role === targetRole); const viable = candidates.filter((c) => allowSameElement || !usedElements.has(c.element)); const pickFrom = viable.length ? viable : pool.filter((c) => !chosen.has(c.id)); const pick = pickRandom(rng, pickFrom); if (!pick) break; team[i] = pick.id; chosen.add(pick.id); if (!allowSameElement) usedElements.add(pick.element); } setResult(team); } function clearTeam() { setResult([]); setLockedSlots([null, null, null, null]); } const displayTeam = useMemo(() => result.map((id) => roster.find((c) => c.id === id)).filter(Boolean), [result, roster]); return (

鳴潮 | 队伍随机生成器

随机队伍 角色库 {/* --- Build Tab --- */}
{teamSize}
setTeamSize(v[0])} min={1} max={4} step={1} />
允许重复元素
{ROLES.map((r) => (
{r} setRoleWeights((s) => ({ ...s, [r]: v[0] }))} min={0} max={5} step={1} /> {roleWeights[r]}
))}
setSeedStr(e.target.value)} placeholder="留空=随机" />
{Array.from({ length: teamSize }).map((_, i) => { const char = displayTeam[i]; const locked = lockedSlots[i]; return ( {char ? ( ) : (
?
)}
{char ? char.name : "空位"}
{char ? `${char.element} · ${char.role} · ${char.rarity}★` : ""}
); })}
在角色库里点选一个角色可快速锁定某个槽位:选择一个槽位后点击角色卡片。
lockNextEmptySlot(id, lockedSlots, setLockedSlots, setResult)} />
{/* --- Roster Tab --- */}
setSearch(e.target.value)} />
{filteredPool.map((c) => (
{c.rarity}★
{c.name}
{c.element} · {c.role}
))}
提示:角色库仅内置少量示例,可随时用“自定义角色”添加,你也可以在代码中扩充 STARTER_ROSTER。

© {new Date().getFullYear()} Wuwa Team Randomizer (Lite). 非官方粉丝作品 | 支持分享链接、种子复现与自定义角色。

); } // --- Subcomponents & helpers --- function AvatarGlyph({ name, element, rarity, small = false }: { name: string; element: string; rarity: number; small?: boolean }) { const initials = name.slice(0, 2); return (
{initials}
{rarity}★
); } function gradientForElement(el: string) { const map: Record = { Glacio: "linear-gradient(135deg,#e0f2fe,#bae6fd)", Fusion: "linear-gradient(135deg,#fee2e2,#fecaca)", Electro: "linear-gradient(135deg,#ede9fe,#ddd6fe)", Aero: "linear-gradient(135deg,#dcfce7,#bbf7d0)", Havoc: "linear-gradient(135deg,#fef9c3,#fde68a)", Spectro: "linear-gradient(135deg,#fae8ff,#f5d0fe)", }; return map[el] ?? "linear-gradient(135deg,#f1f5f9,#e2e8f0)"; } function copyText(t: string) { navigator.clipboard.writeText(t); } function hash(s: string): number { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24); } return h >>> 0; } function spinWeighted(rng: () => number, arr: T[]): T["weight"] extends number ? T : T { const total = arr.reduce((a, b) => a + Math.max(0, b.weight), 0); let r = rng() * (total || 1); for (const item of arr) { r -= Math.max(0, item.weight); if (r <= 0) return item as any; } return arr[arr.length - 1] as any; } function QuickPickGrid({ roster, onPick }: { roster: readonly any[]; onPick: (id: string) => void }) { const [slot, setSlot] = useState(0); return (
把角色快速放入槽位:
{roster.map((c) => ( ))}
); } function onPickAtSlot(id: string, slot: number, onPick: (id: string) => void) { // Delegate: parent manages where to put it onPick(id); } function lockNextEmptySlot(id: string, lockedSlots: (string | null)[], setLockedSlots: any, setResult: any) { // Lock into the first empty slot; if none empty, replace last const idx = lockedSlots.findIndex((s) => s == null); if (idx >= 0) { setLockedSlots((prev: (string | null)[]) => { const next = [...prev]; next[idx] = id; return next; }); setResult((prev: string[]) => { const next = [...prev]; next[idx] = id; return next; }); } else { setLockedSlots((prev: (string | null)[]) => { const next = [...prev]; next[next.length - 1] = id; return next; }); setResult((prev: string[]) => { const next = [...prev]; next[next.length - 1] = id; return next; }); } }