Skip to content

Commit a922927

Browse files
author
NBL Agent
committed
feat(product): Rarity V2 显式倍率表 + Dark Analytical Arena UI 重构
Rarity V2(数值): - 废弃纯指数曲线,改用显式倍率表:C=1.00 → XS Collector=13.20(12 档,Collector 跳升明显) - 产品关系更新:Lv55 XS Collector vs Lv100 C 有悬念(实测 XS-C 57.9%,目标 45-65%) - Lv100 C vs Lv40 XS Collector 仍压倒性(100%);同级 C vs XS-C 压倒性(100%) - 卡牌文案清理:9 张卡的 role/desc 去除机制暗示(灼烧/连击/侵蚀/神罚/龙威等),忠于真实 stats - 验收更新:Lv55 替换 Lv70 gate;新增相邻档梯度、Collector 跳升、S+ 明显优势检查(均跑真实 simulate) - BP 审计扩展到 Lv25/55/100 多等级循环赛 + 跨 Level/Rarity 抽查(Spearman 0.9930/0.9939/0.9939 ≥ 0.90) UI/UX(Dark Analytical Arena,展示层,战斗/数值零改动): - 页面架构:Header → BLUE|VS|RED Player Cards → Battle HUD → Control Bar → Battle Log → Result - Player Card:阵营 accent、角色名 19px、Lv 显眼、Battle Power 28px + presentation-only BP 差值(±x.x%) - Battle HUD:HP current/max + 百分比 + 阵营色 HP bar + 中央 ROUND 双位(34px) - Control Bar:开始战斗/再战 + SPEED segmented control(慢/快/瞬) - Battle Log:结构化事件行(R | actor → target | −DMG/+HP),260px 内滚动,语义着色克制 - Result:VICTORY 面板(胜者/稀有度·等级/剩余 HP/百分比/再战),非 error-alert 风格 - 容器 min(1680px, 100vw-48px),tabular-nums,移动端单列堆叠 - 浏览器验证:1440/1920/2560/3840/390 无溢出;慢/快/瞬 同 seed 日志/胜者/HP 逐字一致 文档:README/AGENTS 更新新 Rarity 表与 Lv55 规则;repo description 更新为产品定位。 本轮未 push(等待明确指令)。
1 parent 99d43fa commit a922927

11 files changed

Lines changed: 1055 additions & 315 deletions

File tree

AGENTS.md

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,25 @@
1010
## 三套核心实力系统
1111

1212
1. **Level** — 第一维度,超指数成长:`g(L) = exp(0.02143L + 0.0002253L²)`
13-
2. **Rarity** — 第二维度,12 档(C…XS Collector),纯指数:`rarityMul(t) = exp(ln6/11·t)`,C→XS Collector = 6 倍
13+
2. **Rarity** — 第二维度,12 档(C…XS Collector),**显式倍率表**`src/power.js``RARITY_MULT`):
14+
C=1.00, C+=1.15, B=1.35, B+=1.60, A=1.90, A+=2.30, S=2.90, SS=3.75, SSS=4.90,
15+
SSS Collector=6.60, XS=9.10, XS Collector=13.20。Rarity 越高相邻档差距越明显,Collector 档跳升明显。
1416
3. **Battle Power** — 只读最终属性的透明线性加权,**不参与战斗**,只是给玩家的综合实力数字
1517

1618
最终属性 = 基准 × 卡牌形状 × `g(L)×rarityMul(稀有度)`
1719

20+
产品关系:`g(55) × 13.20 ≈ g(100)`(p ≈ 84.8 vs 81.1)→ **Lv55 XS Collector vs Lv100 C 有悬念**(XS-C 总胜率约 45%–65%)。
21+
1822
## 关键设计约定(改动前必读)
1923

2024
- **卡牌只读**`src/cards.js` 固定 24 张卡(12 档 × 2 张),玩家不生成/导入/编辑卡。
2125
- **形状归一化**:每张卡的“战斗强度” `hp·atk²/(atk+def)` 与百分比属性都收窄在窄带内,
22-
稀有度差距**只由 rarityMul 表达**。因此:
23-
- 同级 C vs XS Collector = 6 倍 → 压倒性;
24-
- Lv70 XS Collector vs Lv100 C(p 相等 ≈81.1)→ 悬念;
25-
- 同档对抗互有胜负。
26-
调平衡时改某张卡的 base/百分比即可,**不要**改曲线或加系统。
26+
稀有度差距**只由 Rarity 倍率表表达**。因此:
27+
- 同级 C vs XS Collector = 13.2 倍 → 压倒性;
28+
- Lv55 XS Collector vs Lv100 C(p ≈ 84.8 vs 81.1)→ 悬念;
29+
- 同档对抗互有胜负;S 以上相邻档高档明显占优,Collector 跳升明显。
30+
调平衡时改某张卡的 base/百分比即可,**不要**加新系统;若战斗结果与产品目标有偏差,
31+
可微调 `RARITY_MULT` 表(保持显式表,不做公式拟合)。
2732
- **确定性**:战斗内所有随机必须走 `src/battle.js` 的 Mulberry32 PRNG。禁止 `Math.random()`
2833
同 Match Seed → 完全相同的对局。
2934
- **先模拟后播放**`simulate()` 生成完整事件列表,UI 只按 慢/快/瞬 的延迟回放。
@@ -47,5 +52,5 @@ npm run serve # 本地静态服务器 http://127.0.0.1:8774
4752
## 修改卡牌/数值后的检查清单
4853

4954
1. `npm test`(BP 排序、确定性、验收匹配全部要绿)
50-
2. `npm run verify`(含 `scripts/battlepower-audit.js`Lv50 循环赛胜率与 BP 排名 Spearman 相关 ≥ 0.85
55+
2. `npm run verify`(含 `scripts/battlepower-audit.js`Lv25/55/100 循环赛胜率与 BP 排名 Spearman 相关 ≥ 0.90 + 跨 Level/Rarity 抽查
5156
3. 如果改了 `index.html` 的 script 标签或新增/删除文件,同步更新 `scripts/static-check.js` 的期望清单

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
输入 —— 决定战斗表现:
1313

1414
- **Level** — 第一实力维度。Lv100 与 Lv40 之间存在巨大差距(超指数成长)。
15-
- **Rarity** — 第二实力维度。C / C+ / B / B+ / A / A+ / S / SS / SSS / SSS Collector / XS / XS Collector,共 12 档,C→XS Collector 为 6 倍数值差
15+
- **Rarity** — 第二实力维度。C / C+ / B / B+ / A / A+ / S / SS / SSS / SSS Collector / XS / XS Collector,共 12 档,使用显式倍率表(C=1.00 → XS Collector=13.20)。Rarity 越高,相邻档之间实力提升越明显;Collector 档对前一普通档有明显跳升
1616
- **12 个战斗属性** — 生命 / 攻击 / 防御 / 速度 / 命中 / 闪避 / 暴击率 / 暴击伤害 / 穿透 / 吸血 / 每回合回复 / 波动,是实际战斗内容。
1717

1818
评价 —— 不参与战斗:
@@ -41,9 +41,15 @@
4141

4242
## 数值公式
4343

44-
- 等级曲线:`g(L) = exp(0.02143·L + 0.0002253·L²)`,g(40)=3.38,g(70)=13.52,g(100)=81.12
45-
- 稀有度曲线:`rarityMul(t) = exp(ln6/11·t)`,C=1.00 → XS Collector≈6.00
44+
- 等级曲线:`g(L) = exp(0.02143·L + 0.0002253·L²)`,g(40)=3.38,g(55)=6.42,g(100)=81.12
45+
- 稀有度表(显式倍率,无公式拟合):
46+
47+
| Rarity | C | C+ | B | B+ | A | A+ | S | SS | SSS | SSS Collector | XS | XS Collector |
48+
|---|---|---|---|---|---|---|---|---|---|---|---|---|
49+
| 倍率 | 1.00 | 1.15 | 1.35 | 1.60 | 1.90 | 2.30 | 2.90 | 3.75 | 4.90 | 6.60 | 9.10 | 13.20 |
50+
4651
- 最终属性:`p = g(L) × rarityMul(稀有度)`,HP/ATK/DEF/SPD = 基准 × 卡牌形状 × p
52+
- 产品关系:`Lv55 XS Collector × g(55) ≈ Lv100 C × g(100)`(p ≈ 84.8 vs 81.1)→ 同级对抗有悬念
4753
- 基础伤害:`ATK² / (ATK + DEF×(1−PEN))`,再乘暴击与波动
4854
- Battle Power:12 项最终属性的透明线性加权
4955

@@ -55,15 +61,15 @@ npm run verify # 静态检查 + 全量测试 + 产品实战验收
5561
npm run serve # 本地静态服务器
5662
```
5763

58-
测试覆盖:Level 单调性、Rarity 层级、Battle Power 排序、同 Match Seed 确定性、播放速度独立、战斗终止、无 NaN、HP 合法、近战力双方都能赢、大差距不翻盘。
64+
测试覆盖:Level 单调性、Rarity 层级、Battle Power 排序、同 Match Seed 确定性、播放速度独立、战斗终止、无 NaN、HP 合法、近战力双方都能赢、大差距不翻盘、相邻档梯度与 Collector 跳升;`npm run verify` 还包含多等级 BP 审计(Lv25/55/100 循环赛,Spearman ≥ 0.90)
5965

6066
## 仓库结构
6167

6268
```text
6369
index.html 单页入口(只加载 4 个模块)
6470
styles.css
6571
src/cards.js 24 张内置卡 + 12 档稀有度
66-
src/power.js Level/Rarity 曲线 + 属性计算 + Battle Power
72+
src/power.js Level 曲线 + Rarity 倍率表 + 属性计算 + Battle Power
6773
src/battle.js 确定性自动战斗引擎(先模拟后播放)
6874
src/app.js 页面交互与回放
6975
tests/ node:test 测试

index.html

Lines changed: 119 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,61 +7,134 @@
77
<link rel="stylesheet" href="styles.css">
88
</head>
99
<body>
10+
<div class="app">
1011

11-
<h1>数值卡牌 · 自动 PK</h1>
12-
<div class="sub">选两张卡 → 调等级 → 开始战斗 → 看谁赢 · 等级、稀有度与属性决定战斗表现;Battle Power 评价综合实力</div>
12+
<!-- ===================== HEADER ===================== -->
13+
<header class="site-header">
14+
<h1>数值卡牌 · 自动 PK</h1>
15+
<p class="sub">选两张卡 → 调等级 → 开始战斗 → 看谁赢 · 等级、稀有度与属性决定战斗表现;Battle Power 评价综合实力</p>
16+
</header>
1317

14-
<div class="setup">
15-
<div class="panel" id="panelA">
16-
<h2>◆ 蓝方</h2>
17-
<select id="selA"></select>
18-
<div class="lvlrow">
19-
<label>等级</label>
20-
<input type="range" id="lvlA" min="1" max="100" value="50">
21-
<div class="lvlval" id="lvlAVal">50</div>
22-
</div>
23-
<div id="prevA"></div>
18+
<!-- ===================== PLAYER CARDS ===================== -->
19+
<div class="setup" id="setup">
20+
<section class="pcard blue" id="panelA">
21+
<div class="pcard-top">
22+
<span class="team">TEAM&nbsp;BLUE</span>
23+
<select id="selA" aria-label="蓝方卡牌"></select>
24+
</div>
25+
<div class="identity">
26+
<div class="name-row">
27+
<span class="cname" id="nameA"></span>
28+
<span class="rar-badge" id="rarA"></span>
29+
<span class="role-badge" id="roleA"></span>
30+
</div>
31+
<div class="cdesc" id="descA"></div>
32+
<div class="lvl-row">
33+
<label for="lvlA">LEVEL</label>
34+
<input type="range" id="lvlA" min="1" max="100" value="50">
35+
<span class="lvl-val" id="lvlAVal">Lv.50</span>
36+
</div>
37+
</div>
38+
<div class="bp-block">
39+
<span class="bp-label">BATTLE POWER</span>
40+
<span class="bp-value" id="bpA"></span>
41+
<span class="bp-delta" id="bpDeltaA"></span>
42+
</div>
43+
<div class="stats-grid" id="statsA"></div>
44+
</section>
45+
46+
<div class="vs-col"><div class="vs">VS</div></div>
47+
48+
<section class="pcard red" id="panelB">
49+
<div class="pcard-top">
50+
<span class="team">TEAM&nbsp;RED</span>
51+
<select id="selB" aria-label="红方卡牌"></select>
52+
</div>
53+
<div class="identity">
54+
<div class="name-row">
55+
<span class="cname" id="nameB"></span>
56+
<span class="rar-badge" id="rarB"></span>
57+
<span class="role-badge" id="roleB"></span>
58+
</div>
59+
<div class="cdesc" id="descB"></div>
60+
<div class="lvl-row">
61+
<label for="lvlB">LEVEL</label>
62+
<input type="range" id="lvlB" min="1" max="100" value="50">
63+
<span class="lvl-val" id="lvlBVal">Lv.50</span>
64+
</div>
65+
</div>
66+
<div class="bp-block">
67+
<span class="bp-label">BATTLE POWER</span>
68+
<span class="bp-value" id="bpB"></span>
69+
<span class="bp-delta" id="bpDeltaB"></span>
70+
</div>
71+
<div class="stats-grid" id="statsB"></div>
72+
</section>
2473
</div>
2574

26-
<div class="vscol"><div class="vs">VS</div></div>
75+
<!-- ===================== BATTLE ARENA / HP HUD ===================== -->
76+
<section class="battle-hud" id="battleHud">
77+
<div class="hud-side blue">
78+
<div class="hud-head">
79+
<span class="hud-name" id="sNameA"></span>
80+
<span class="hud-meta" id="sMetaA"></span>
81+
</div>
82+
<div class="hud-hp" id="hpTA">— / —</div>
83+
<div class="hpbar-wrap"><div class="hpbar" id="hpbarA" style="width:100%"></div></div>
84+
<div class="hud-pct" id="pctA">100%</div>
85+
</div>
2786

28-
<div class="panel" id="panelB">
29-
<h2>◆ 红方</h2>
30-
<select id="selB"></select>
31-
<div class="lvlrow">
32-
<label>等级</label>
33-
<input type="range" id="lvlB" min="1" max="100" value="50">
34-
<div class="lvlval" id="lvlBVal">50</div>
87+
<div class="round-center">
88+
<div class="round-label">ROUND</div>
89+
<div class="round-value" id="roundTxt"></div>
3590
</div>
36-
<div id="prevB"></div>
37-
</div>
38-
</div>
3991

40-
<div class="stage">
41-
<div class="fighter">
42-
<div class="fname"><span class="n" id="sNameA"></span><span class="rar" id="sRarA"></span></div>
43-
<div class="hpwrap"><div class="hpbar" id="hpbarA" style="width:100%"></div><div class="hptext" id="hpTA">— / —</div></div>
44-
</div>
45-
<div class="vscol"><div class="vs" id="roundTxt"></div></div>
46-
<div class="fighter">
47-
<div class="fname"><span class="n" id="sNameB"></span><span class="rar" id="sRarB"></span></div>
48-
<div class="hpwrap"><div class="hpbar" id="hpbarB" style="width:100%"></div><div class="hptext" id="hpTB">— / —</div></div>
49-
</div>
50-
</div>
92+
<div class="hud-side red">
93+
<div class="hud-head">
94+
<span class="hud-name" id="sNameB"></span>
95+
<span class="hud-meta" id="sMetaB"></span>
96+
</div>
97+
<div class="hud-hp" id="hpTB">— / —</div>
98+
<div class="hpbar-wrap"><div class="hpbar" id="hpbarB" style="width:100%"></div></div>
99+
<div class="hud-pct" id="pctB">100%</div>
100+
</div>
101+
</section>
51102

52-
<div class="bar">
53-
<button id="startBtn">开始战斗</button>
54-
<button id="againBtn" class="ghost">再战</button>
55-
<div class="speedsel">
56-
<span>速度</span>
57-
<div class="spdbtn on" data-speed="slow"></div>
58-
<div class="spdbtn" data-speed="fast"></div>
59-
<div class="spdbtn" data-speed="instant"></div>
60-
</div>
61-
</div>
103+
<!-- ===================== BATTLE CONTROL BAR ===================== -->
104+
<section class="control-bar">
105+
<div class="ctrl-btns">
106+
<button id="startBtn" class="btn primary">▶ 开始战斗</button>
107+
<button id="againBtn" class="btn secondary">↻ 再战</button>
108+
</div>
109+
<div class="speed-seg" role="group" aria-label="播放速度">
110+
<span class="speed-label">SPEED</span>
111+
<button class="spdbtn on" data-speed="slow"></button>
112+
<button class="spdbtn" data-speed="fast"></button>
113+
<button class="spdbtn" data-speed="instant"></button>
114+
</div>
115+
</section>
62116

63-
<div class="logbox" id="logbox"></div>
64-
<div class="result" id="resultBox" style="display:none"></div>
117+
<!-- ===================== BATTLE LOG ===================== -->
118+
<section class="log-panel">
119+
<div class="log-head">
120+
<span>BATTLE&nbsp;LOG</span>
121+
<span class="log-count" id="logCount">0 EVENTS</span>
122+
</div>
123+
<div class="logbox" id="logbox"></div>
124+
</section>
125+
126+
<!-- ===================== BATTLE RESULT ===================== -->
127+
<section class="result-panel" id="resultBox" style="display:none">
128+
<div class="result-title" id="resultTitle">🏆 VICTORY</div>
129+
<div class="result-name" id="resultName"></div>
130+
<div class="result-meta" id="resultMeta"></div>
131+
<div class="result-hp-label">剩余生命</div>
132+
<div class="result-hp" id="resultHp">— / —</div>
133+
<div class="result-pct" id="resultPct"></div>
134+
<button id="resultAgainBtn" class="btn primary">↻ 再战</button>
135+
</section>
136+
137+
</div>
65138

66139
<script src="src/cards.js"></script>
67140
<script src="src/power.js"></script>

scripts/acceptance.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ console.log('【核心匹配(关键验收)】');
4747
{
4848
let aWins = 0, bWins = 0, total = 0;
4949
for (const x of xscs) for (const c of cs) {
50-
const r = winStats(x, 70, c, 100, 200);
50+
const r = winStats(x, 55, c, 100, 200);
5151
aWins += r.a; bWins += r.b; total += r.seeds;
5252
}
5353
const xRate = aWins / (aWins + bWins) * 100;
54-
report('Lv70 XS Collector vs Lv100 C → 有悬念(双方都能赢)',
55-
bWins > 0 && xRate >= 30 && xRate <= 70,
54+
report('Lv55 XS Collector vs Lv100 C → 悬念(XS-C 约 45%–65%)',
55+
bWins > 0 && xRate >= 45 && xRate <= 65,
5656
`XS-C ${pct(aWins, total)} / C ${pct(bWins, total)}(综合 ${xRate.toFixed(1)}%)`);
5757
}
5858
{
@@ -75,6 +75,30 @@ for (let t = 0; t < RARITY_LIST.length; t++) {
7575
`${pct(r.a, r.seeds)} / ${pct(r.b, r.seeds)}`);
7676
}
7777

78+
// ---- 同等级相邻档 ----
79+
console.log('\n【同等级相邻档(Lv50 × 120 场,高档在前)】');
80+
{
81+
const tier = (t) => CARDS.filter(c => c.rarity === t);
82+
const pairs = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10], [10, 11]];
83+
const rates = [];
84+
for (const [lo, hi] of pairs) {
85+
let a = 0, b = 0, tot = 0;
86+
for (const h of tier(hi)) for (const l of tier(lo)) {
87+
const r = winStats(h, 50, l, 50, 60);
88+
a += r.a; b += r.b; tot += r.seeds;
89+
}
90+
const hiPct = a / (a + b) * 100;
91+
rates.push(hiPct);
92+
const ok = lo < 5 ? hiPct >= 55 : hiPct >= 90;
93+
report(`[${RARITY_LIST[hi]} vs ${RARITY_LIST[lo]}] 高档总体更强`, ok,
94+
`高档 ${pct(a, tot)} / 低档 ${pct(b, tot)}`);
95+
}
96+
const sPlus = rates.slice(5); // S 及以上相邻档
97+
let increasing = true;
98+
for (let i = 1; i < sPlus.length; i++) if (sPlus[i] < sPlus[i - 1] - 0.5) increasing = false;
99+
report('S 以上相邻档优势随档位逐步更明显(单调不减)', increasing, sPlus.map(r => r.toFixed(1) + '%').join(' → '));
100+
}
101+
78102
// ---- 近战力随机性 ----
79103
console.log('\n【近 Battle Power 卡对 × 大量 Match Seed(双方都能赢)】');
80104
const bps = CARDS.map(c => ({ card: c, bp: battlePower(buildUnit(c, 50)) }));

0 commit comments

Comments
 (0)