feedback7 UIBACK: upgrading the 调度中心 opens the new shop slot EMPTY — the next refresh or round start fills it; 0.2.0's fill-at-once (464a823, item 19) reverted (GitHub #332 and PR #333 by @2321Robin, official recording BV1AXwuzdEys 1:39 — authored by them: the economy.js change, the rewritten level-slots test and the docs hunks applied as reviewed, without the PR's UPGRADE_FILLS_NEW_SLOTS flag and LOCAL RULING comment: the empty slot is the rule): the shop bar draws a null operator slot as a bare dashed frame (EmptyCard, inert scard--sold) instead of the SOLD OUT 「已招募」 frame, one new UI string in the four packs; META.md §3 and §7 and PLAYING.md say so; the dev mock opens the slot empty and survives null slots; test/ui/shop-empty-slot.e2e.test.js; golden: only matches moves, all 18 (the bots' shop RNG stream) — end results changed in solo-FUNNY-1 defeat→victory, solo-NORMAL-1 and solo-HARD-1 victory→defeat, solo-HARD-2 and solo-ABYSS-1 defeat→eliminated, coop2-NORMAL-14-standins defeat→victory (2 distinct stand-ins fielded: golden-standins.test.js asks ≥ 2); roster, bonds, fields, standins, diy unchanged
This commit is contained in:
1 parent
b4bc89ad4e
commit
4bb0c11e42
15 files changed
+691
-542
No files matched your search
+6
-4
@@ -508,7 +508,7 @@ Any `choice:` handler whose EffectRef reuses its own key must guard like this (o
|
|||||||
* **Shop**: `shopSlots[level]` chess slots + item slot(s), copy-weighted rolls over remaining pool copies of unbanned,
|
* **Shop**: `shopSlots[level]` chess slots + item slot(s), copy-weighted rolls over remaining pool copies of unbanned,
|
||||||
visible chess with tier ≤ level; item slot: tier by the same shares, uniform item within the tier. Level-up price =
|
visible chess with tier ≤ level; item slot: tier by the same shares, uniform item within the tier. Level-up price =
|
||||||
base per mode, −1 each round start (floor 0), reset to the next base after upgrading; an upgrade opens the new level's
|
base per mode, −1 each round start (floor 0), reset to the next base after upgrading; an upgrade opens the new level's
|
||||||
extra slots at once with new cards (the cards shown stay); `MAX_LEVEL` at 6. One freeze
|
extra slots at once, empty (the cards shown stay; the empty slots fill on the next roll); `MAX_LEVEL` at 6. One freeze
|
||||||
toggle freezes every unsold slot until the next round start; a manual refresh rerolls everything (new slots stay
|
toggle freezes every unsold slot until the next round start; a manual refresh rerolls everything (new slots stay
|
||||||
frozen). Unfrozen slots are cleared at combat start. Slot positions are stable (frozen slots keep their index).
|
frozen). Unfrozen slots are cleared at combat start. Slot positions are stable (frozen slots keep their index).
|
||||||
* **Pool**: copies 12/14/18/16/8/5 (缪尔赛思 4); a normal piece holds 1 copy, an elite 3; displays never reserve copies;
|
* **Pool**: copies 12/14/18/16/8/5 (缪尔赛思 4); a normal piece holds 1 copy, an elite 3; displays never reserve copies;
|
||||||
@@ -826,9 +826,11 @@ round was over. The official 1 s `broadcastBeginDelay` is not modelled.
|
|||||||
units > active bond > Σ active layers > standing > seat; the ranks marked 存疑 in PRTS). Unite enemies re-enter with
|
units > active bond > Σ active layers > standing > seat; the ranks marked 存疑 in PRTS). Unite enemies re-enter with
|
||||||
their original stats on the official 联防 spawn timing (waves.js `buildUniteWave`, DATA.md §15 #22).
|
their original stats on the official 联防 spawn timing (waves.js `buildUniteWave`, DATA.md §15 #22).
|
||||||
* A manual refresh while frozen keeps the new slots frozen until the next round start.
|
* A manual refresh while frozen keeps the new slots frozen until the next round start.
|
||||||
* A level-up opens the new level's extra slots at once, each with a new card drawn at the new level, and keeps the
|
* A level-up opens the new level's extra slots at once — empty, with no card drawn into them (official footage, GitHub
|
||||||
cards shown (0.2.0, community report item 19; the official tutorial's 「升级后将出现更多的商品栏位」); the new card
|
#332: bilibili BV1AXwuzdEys 1:39, a 1→2 upgrade with the new slot visible and empty; the official texts only ever say
|
||||||
follows the freeze toggle like a manual refresh's cards.
|
「升级后将出现更多的商品栏位」/「增加刷新栏位」) — and keeps the cards shown. The empty slots fill on the next roll (a manual
|
||||||
|
refresh or the round start), like every unfrozen slot; the shop draws an empty slot as a bare frame, never 已招募. 0.2.0
|
||||||
|
drew a card into each new slot (community report item 19, one uncorroborated remark): reverted in 0.2.2.
|
||||||
* Buying a second copy of an equipped normal item merges into the golden item in the hand (not equipped).
|
* Buying a second copy of an equipped normal item merges into the golden item in the hand (not equipped).
|
||||||
* Promotions by effects (升华, 博士投影) keep the equipment; merges return it; 突变细胞's transformation returns it (the
|
* Promotions by effects (升华, 博士投影) keep the equipment; merges return it; 突变细胞's transformation returns it (the
|
||||||
cell included) before its new operator is gained into the 整备区 — the carrier's tile is left empty (official footage,
|
cell included) before its new operator is gained into the 整备区 — the carrier's tile is left empty (official footage,
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@
|
|||||||
| 出售干员 | 任何干员(含精锐)都只返还 **1** 资金 |
|
| 出售干员 | 任何干员(含精锐)都只返还 **1** 资金 |
|
||||||
| 装备 / 法术 | 调度中心出售除特殊装备外的所有装备:4 把特殊维式重锤(战栗 / 坚固 / 加速 / 灼燃)只来自维多利亚盟约(每 25 层)和洛洛的定制品,突变细胞只来自昆图斯的策略。装备不能出售,整备区里的只能「销毁」(不返还)。已配发的装备锁定在干员身上:只能被第三件装备替换(弹出替换窗口,选择要替换的一件,它会被销毁;盟约之币、博士投影等装备即销毁的道具也一样,先销毁选中的那件再生效,干员留下一个空栏位,精锐干员不能使用博士投影,此时什么都不销毁),或在干员出售 / 合成精锐时退回整备区。**突变细胞**不会用完:战斗结束后装备者被销毁(部署在场上的话,原来的格子空出来,剩余可放置角色加 1),突变细胞和它的其他装备先退回整备区,可以再次配发给任意干员;随后获得一名高一阶(最高 VI)的随机普通干员(装备者是精锐也一样),和其他新获得的干员一样进入整备区(满了进入临时整备区),需要自己重新部署;如果它凑齐了合成所需的同名干员,照常合成精锐。**坚固维式重锤**首次受到致命伤害时锁血 8 秒,**每次部署一次**:被击倒再部署(或突袭跳跃再部署、被 M3茧甲 / 埃芒加德复活)后又能锁一次,娜仁图亚策略借来的锤子也一样;借用到期时正在锁血的会锁满 8 秒;站在阿戈尔干员身前被开场吞噬、自带源石溶剂掉血时也会用掉这一次 |
|
| 装备 / 法术 | 调度中心出售除特殊装备外的所有装备:4 把特殊维式重锤(战栗 / 坚固 / 加速 / 灼燃)只来自维多利亚盟约(每 25 层)和洛洛的定制品,突变细胞只来自昆图斯的策略。装备不能出售,整备区里的只能「销毁」(不返还)。已配发的装备锁定在干员身上:只能被第三件装备替换(弹出替换窗口,选择要替换的一件,它会被销毁;盟约之币、博士投影等装备即销毁的道具也一样,先销毁选中的那件再生效,干员留下一个空栏位,精锐干员不能使用博士投影,此时什么都不销毁),或在干员出售 / 合成精锐时退回整备区。**突变细胞**不会用完:战斗结束后装备者被销毁(部署在场上的话,原来的格子空出来,剩余可放置角色加 1),突变细胞和它的其他装备先退回整备区,可以再次配发给任意干员;随后获得一名高一阶(最高 VI)的随机普通干员(装备者是精锐也一样),和其他新获得的干员一样进入整备区(满了进入临时整备区),需要自己重新部署;如果它凑齐了合成所需的同名干员,照常合成精锐。**坚固维式重锤**首次受到致命伤害时锁血 8 秒,**每次部署一次**:被击倒再部署(或突袭跳跃再部署、被 M3茧甲 / 埃芒加德复活)后又能锁一次,娜仁图亚策略借来的锤子也一样;借用到期时正在锁血的会锁满 8 秒;站在阿戈尔干员身前被开场吞噬、自带源石溶剂掉血时也会用掉这一次 |
|
||||||
| 冻结 | 免费,保留当前所有未买的格子到下一回合;冻结状态下仍可主动刷新 |
|
| 冻结 | 免费,保留当前所有未买的格子到下一回合;冻结状态下仍可主动刷新 |
|
||||||
| 升级调度中心(1→2 … 5→6) | 险境及以上 5 / 8 / 11 / 12 / 13;同盟标准 5 / 8 / 10 / 11 / 11;独立标准 1 / 1 / 5 / 8 / 10。**每过一回合价格 −1**(最低 0),升级后按下一级的价格重新计算。升级到干员栏位更多的等级(险境及以上 2 级 4 个、4 级 5 个;标准 2 级 4 个、6 级 5 个)时,**新栏位立刻刷出一名新干员**,已有的卡片不变 |
|
| 升级调度中心(1→2 … 5→6) | 险境及以上 5 / 8 / 11 / 12 / 13;同盟标准 5 / 8 / 10 / 11 / 11;独立标准 1 / 1 / 5 / 8 / 10。**每过一回合价格 −1**(最低 0),升级后按下一级的价格重新计算。升级到干员栏位更多的等级(险境及以上 2 级 4 个、4 级 5 个;标准 2 级 4 个、6 级 5 个)时,**新栏位立刻出现,但空着不补干员**(官方如此:升级只开位,新位在下一次刷新——手动刷新或回合开始的自动刷新——时才会出现干员;空栏位只画一个空框,不写「已招募」),已有的卡片不变 |
|
||||||
| 其他收入 | 悬赏敌人、盟约「远见」「奇迹」、装备「骑士储蓄罐」、部分策略和特质 |
|
| 其他收入 | 悬赏敌人、盟约「远见」「奇迹」、装备「骑士储蓄罐」、部分策略和特质 |
|
||||||
|
|
||||||
调度中心等级决定商店能出现的**最高阶级**和格子数(险境及以上:干员 3 / 4 / 4 / 5 / 5 / 5 格 + 装备 1 格)。
|
调度中心等级决定商店能出现的**最高阶级**和格子数(险境及以上:干员 3 / 4 / 4 / 5 / 5 / 5 格 + 装备 1 格)。
|
||||||
|
|||||||
@@ -156,6 +156,9 @@
|
|||||||
.scard__idesc { font-size: .12rem; line-height: 1.45; color: var(--text-lo); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
.scard__idesc { font-size: .12rem; line-height: 1.45; color: var(--text-lo); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
.scard--sold { background: repeating-linear-gradient(135deg, rgba(255, 255, 255, .02) 0 .08rem, transparent .08rem .16rem), rgba(8, 11, 10, .75); border: 1px dashed var(--line-2); justify-content: center; align-items: center; }
|
.scard--sold { background: repeating-linear-gradient(135deg, rgba(255, 255, 255, .02) 0 .08rem, transparent .08rem .16rem), rgba(8, 11, 10, .75); border: 1px dashed var(--line-2); justify-content: center; align-items: center; }
|
||||||
.scard__soldtxt { display: flex; flex-direction: column; align-items: center; gap: .02rem; font-size: .16rem; color: var(--text-dim); }
|
.scard__soldtxt { display: flex; flex-direction: column; align-items: center; gap: .02rem; font-size: .16rem; color: var(--text-dim); }
|
||||||
|
/* a slot the 调度中心's upgrade opened and nothing filled yet (ui/shopBar.js EmptyCard, also .scard--sold): the bare frame — no hatching,
|
||||||
|
no SOLD OUT text */
|
||||||
|
.scard--sold.scard--empty { background: rgba(8, 11, 10, .4); border-color: var(--line); }
|
||||||
|
|
||||||
/* ---- funds card ---- */
|
/* ---- funds card ---- */
|
||||||
.funds { width: 1.18rem; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .08rem; padding: .06rem; border-left: 1px solid var(--line); }
|
.funds { width: 1.18rem; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .08rem; padding: .06rem; border-left: 1px solid var(--line); }
|
||||||
|
|||||||
@@ -593,7 +593,7 @@ async function mockRequest(t, f = {}) {
|
|||||||
prepOnly();
|
prepOnly();
|
||||||
if (p.shop.refreshPrice > p.funds) fail('NO_FUNDS');
|
if (p.shop.refreshPrice > p.funds) fail('NO_FUNDS');
|
||||||
if (p.shop.freeRefreshes > 0) p.shop.freeRefreshes--; else p.funds -= 1;
|
if (p.shop.freeRefreshes > 0) p.shop.freeRefreshes--; else p.funds -= 1;
|
||||||
const n = p.shop.slots.filter((s) => s.kind !== 'item').length;
|
const n = p.shop.slots.filter((s) => !s || s.kind !== 'item').length;
|
||||||
p.shop.slots = [...Array.from({ length: n }, () => makeSlot(p.shop.level)), makeItemSlot()];
|
p.shop.slots = [...Array.from({ length: n }, () => makeSlot(p.shop.level)), makeItemSlot()];
|
||||||
refreshPrivate(); return {};
|
refreshPrivate(); return {};
|
||||||
}
|
}
|
||||||
@@ -604,7 +604,8 @@ async function mockRequest(t, f = {}) {
|
|||||||
if (p.shop.upgradePrice > p.funds) fail('NO_FUNDS');
|
if (p.shop.upgradePrice > p.funds) fail('NO_FUNDS');
|
||||||
p.funds -= p.shop.upgradePrice; p.shop.level++; p.shop.upgradePrice = [5, 8, 11, 12, 13][p.shop.level - 1] ?? 0;
|
p.funds -= p.shop.upgradePrice; p.shop.level++; p.shop.upgradePrice = [5, 8, 11, 12, 13][p.shop.level - 1] ?? 0;
|
||||||
const n = p.shop.level >= 4 ? 5 : 4;
|
const n = p.shop.level >= 4 ? 5 : 4;
|
||||||
while (p.shop.slots.filter((s) => s.kind !== 'item').length < n) p.shop.slots.splice(p.shop.slots.length - 1, 0, makeSlot(p.shop.level));
|
// the upgrade opens the new slot EMPTY (server/match/player/economy.js _openLevelSlots: a null slot; a refresh fills it)
|
||||||
|
while (p.shop.slots.filter((s) => !s || s.kind !== 'item').length < n) p.shop.slots.splice(p.shop.slots.length - 1, 0, null);
|
||||||
toast(`调度中心等级提升至 ${p.shop.level}`, 'success');
|
toast(`调度中心等级提升至 ${p.shop.level}`, 'success');
|
||||||
refreshPrivate(); return {};
|
refreshPrivate(); return {};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
" · 未装备模组": " · No Module equipped",
|
" · 未装备模组": " · No Module equipped",
|
||||||
"已购买": "Purchased",
|
"已购买": "Purchased",
|
||||||
"已招募": "Recruited",
|
"已招募": "Recruited",
|
||||||
|
"空栏位:刷新或下回合开始时补满": "Empty slot: filled by the next refresh or at the start of the next round",
|
||||||
"再次点击确认升级({price} 资金)": "Tap again to confirm upgrade ({price} Funds)",
|
"再次点击确认升级({price} 资金)": "Tap again to confirm upgrade ({price} Funds)",
|
||||||
"升级调度中心({price} 资金) · {key}": "Upgrade Dispatch Center ({price} Funds) · {key}",
|
"升级调度中心({price} 资金) · {key}": "Upgrade Dispatch Center ({price} Funds) · {key}",
|
||||||
"已满级": "Max Level",
|
"已满级": "Max Level",
|
||||||
|
|||||||
@@ -682,6 +682,7 @@
|
|||||||
" · 未装备模组": " · モジュール未装備",
|
" · 未装备模组": " · モジュール未装備",
|
||||||
"已购买": "購入済み",
|
"已购买": "購入済み",
|
||||||
"已招募": "獲得済み",
|
"已招募": "獲得済み",
|
||||||
|
"空栏位:刷新或下回合开始时补满": "空きスロット:更新するか次のラウンド開始時に補充されます",
|
||||||
"再次点击确认升级({price} 资金)": "もう一度タップでレベルアップ(資金{price})",
|
"再次点击确认升级({price} 资金)": "もう一度タップでレベルアップ(資金{price})",
|
||||||
"升级调度中心({price} 资金) · {key}": "管理センターをレベルアップ(資金{price}) · {key}",
|
"升级调度中心({price} 资金) · {key}": "管理センターをレベルアップ(資金{price}) · {key}",
|
||||||
"已满级": "最大レベル",
|
"已满级": "最大レベル",
|
||||||
|
|||||||
@@ -678,6 +678,7 @@
|
|||||||
" · 未装备模组": " · 모듈 미장착",
|
" · 未装备模组": " · 모듈 미장착",
|
||||||
"已购买": "구매 완료",
|
"已购买": "구매 완료",
|
||||||
"已招募": "구매 완료",
|
"已招募": "구매 완료",
|
||||||
|
"空栏位:刷新或下回合开始时补满": "빈 슬롯: 갱신하거나 다음 라운드가 시작되면 채워집니다",
|
||||||
"再次点击确认升级({price} 资金)": "한 번 더 탭해 승급 확인 (자금 {price})",
|
"再次点击确认升级({price} 资金)": "한 번 더 탭해 승급 확인 (자금 {price})",
|
||||||
"升级调度中心({price} 资金) · {key}": "보급센터 승급 (자금 {price}) · {key}",
|
"升级调度中心({price} 资金) · {key}": "보급센터 승급 (자금 {price}) · {key}",
|
||||||
"已满级": "최대 레벨",
|
"已满级": "최대 레벨",
|
||||||
|
|||||||
@@ -680,6 +680,7 @@
|
|||||||
" · 未装备模组": " · 未裝備模組",
|
" · 未装备模组": " · 未裝備模組",
|
||||||
"已购买": "已購買",
|
"已购买": "已購買",
|
||||||
"已招募": "已招募",
|
"已招募": "已招募",
|
||||||
|
"空栏位:刷新或下回合开始时补满": "空欄位:更新或下回合開始時補滿",
|
||||||
"再次点击确认升级({price} 资金)": "再次點擊確認升級({price} 資金)",
|
"再次点击确认升级({price} 资金)": "再次點擊確認升級({price} 資金)",
|
||||||
"升级调度中心({price} 资金) · {key}": "升級調度中心({price} 資金) · {key}",
|
"升级调度中心({price} 资金) · {key}": "升級調度中心({price} 資金) · {key}",
|
||||||
"已满级": "已滿級",
|
"已满级": "已滿級",
|
||||||
|
|||||||
+13
-1
@@ -175,6 +175,17 @@ function SoldCard({ item = false }) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A slot with no card in it: the one a 调度中心 upgrade has just opened (a `null` slot — server/match/player/economy.js
|
||||||
|
* _openLevelSlots; GitHub #332 / PR #333: the official shop shows the new slot, empty, until the next refresh or round
|
||||||
|
* start fills it). Never 已招募 / SOLD OUT: nothing was bought there.
|
||||||
|
*/
|
||||||
|
function EmptyCard({ item = false }) {
|
||||||
|
// `scard--sold` too: the same inert frame (no hover lift, not a buyable card for every `:not(.scard--sold)` rule); `scard--empty` only restyles it
|
||||||
|
return html`<div class=${cx('scard', 'scard--sold', 'scard--empty', item && 'scard--item')} role="img" aria-label=${t('空栏位:刷新或下回合开始时补满')}
|
||||||
|
title=${t('空栏位:刷新或下回合开始时补满')}></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function LevelCard({ shop, reason, armed = false, onTap }) {
|
function LevelCard({ shop, reason, armed = false, onTap }) {
|
||||||
const lv = shop?.level ?? 1;
|
const lv = shop?.level ?? 1;
|
||||||
const max = lv >= (shop?.maxLevel ?? 6);
|
const max = lv >= (shop?.maxLevel ?? 6);
|
||||||
@@ -346,7 +357,8 @@ export function ShopBar({ priv, editable, collapsed, onCollapse, onBuy, onLevel,
|
|||||||
armed=${armed} onTap=${tapCard} offBonds=${offBonds} />`
|
armed=${armed} onTap=${tapCard} offBonds=${offBonds} />`
|
||||||
: html`<div class="shopbar__cards">
|
: html`<div class="shopbar__cards">
|
||||||
${chessSlots.map(({ s, i }) => {
|
${chessSlots.map(({ s, i }) => {
|
||||||
if (!s || s.sold) return html`<${SoldCard} key=${`s${i}`} />`;
|
if (!s) return html`<${EmptyCard} key=${`e${i}`} />`;
|
||||||
|
if (s.sold) return html`<${SoldCard} key=${`s${i}`} />`;
|
||||||
const reason = shopBlockReason('buy', { priv, editable, slot: s, ...LOOKUPS });
|
const reason = shopBlockReason('buy', { priv, editable, slot: s, ...LOOKUPS });
|
||||||
return html`<${ChessCard} key=${`c${i}:${s.id}`} slot=${s} idx=${i} priv=${priv} frozen=${frozen} onBuy=${onBuy} onDetail=${onDetail} offBonds=${offBonds}
|
return html`<${ChessCard} key=${`c${i}:${s.id}`} slot=${s} idx=${i} priv=${priv} frozen=${frozen} onBuy=${onBuy} onDetail=${onDetail} offBonds=${offBonds}
|
||||||
reason=${reason} armed=${armed === armKey('c', i, s)} onTap=${editable ? (idx) => tapCard('c', idx, s, 'chess', reason, onBuy) : null} />`;
|
reason=${reason} armed=${armed === armKey('c', i, s)} onTap=${editable ? (idx) => tapCard('c', idx, s, 'chess', reason, onBuy) : null} />`;
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export function attachAudit(m, { invariants = true, limit = 200 } = {}) {
|
|||||||
if (f0 - ps.funds !== price && !fx && !hasSpendEffects(m, ps)) fail(`${ps.playerId}: level-up paid ${f0 - ps.funds}, price ${price}`);
|
if (f0 - ps.funds !== price && !fx && !hasSpendEffects(m, ps)) fail(`${ps.playerId}: level-up paid ${f0 - ps.funds}, price ${price}`);
|
||||||
const next = gd.upgradeBase(ps.shop.level) ?? 0;
|
const next = gd.upgradeBase(ps.shop.level) ?? 0;
|
||||||
if (ps.shop.upgradePrice !== next) fail(`${ps.playerId}: upgrade price after level-up ${ps.shop.upgradePrice}, expected ${next}`);
|
if (ps.shop.upgradePrice !== next) fail(`${ps.playerId}: upgrade price after level-up ${ps.shop.upgradePrice}, expected ${next}`);
|
||||||
// the new level's extra slots open at once (item 19 of 2026-10-06); the cards shown before stay in place
|
// the new level's extra slots open at once, empty (GitHub #332 / PR #333); the cards shown before stay in place
|
||||||
const { chess, item } = gd.shopSlots(ps.shop.level);
|
const { chess, item } = gd.shopSlots(ps.shop.level);
|
||||||
const want = Math.max(chess, layout0.chess) + Math.max(item, layout0.item);
|
const want = Math.max(chess, layout0.chess) + Math.max(item, layout0.item);
|
||||||
if (ps.shop.slots.length !== want) fail(`${ps.playerId}: ${ps.shop.slots.length} shop slots after the level-up to ${ps.shop.level}, expected ${want}`);
|
if (ps.shop.slots.length !== want) fail(`${ps.playerId}: ${ps.shop.slots.length} shop slots after the level-up to ${ps.shop.level}, expected ${want}`);
|
||||||
|
|||||||
@@ -93,11 +93,14 @@ export class PlayerEconomy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 调度中心's upgrade opens the new level's extra slots at once, each with a new card drawn at the new level; the
|
* The 调度中心's upgrade opens the new level's extra slots at once — EMPTY: no card is drawn into them (a `null` slot).
|
||||||
* cards already shown stay where they are (chess slots keep their index, the item slot stays after them). Official: the
|
* The next roll (a manual refresh or the round start) fills them with the other unfrozen slots; the cards already shown
|
||||||
* tutorial's 休整期 page 「升级:…升级后将出现更多的商品栏位、可调度干员以及新装备」; the community report of 2026-10-06
|
* stay where they are (chess slots keep their index, the item slot stays after them). Official: the footage of a 1→2
|
||||||
* (item 19) 「升级商店获得新的商店位时用新卡补上,而不是空着」. Until 0.2.0 the extra slots waited for the next roll (a
|
* upgrade (bilibili BV1AXwuzdEys 1:39, GitHub #332 / PR #333 by @2321Robin) shows the new slot appear and stay empty, and
|
||||||
* refresh or the round start). [ASSUMED] the new card follows the freeze toggle, as a manual refresh's cards do.
|
* the texts only name the slots — the tutorial's 休整期 page 「升级后将出现更多的商品栏位」, PRTS 帮助 「增加刷新栏位」 —,
|
||||||
|
* never a card that comes with them. 0.2.0 drew a card into each new slot instead (item 19 of the community report of
|
||||||
|
* 2026-10-06, 「升级商店获得新的商店位时用新卡补上,而不是空着」, one uncorroborated remark; its follow-on, that the new card
|
||||||
|
* follows the freeze toggle, was [ASSUMED]); until then the extra slots waited for the next roll as well.
|
||||||
*/
|
*/
|
||||||
_openLevelSlots() {
|
_openLevelSlots() {
|
||||||
const { chess: nChess, item: nItem } = this.gd.shopSlots(this.shop.level);
|
const { chess: nChess, item: nItem } = this.gd.shopSlots(this.shop.level);
|
||||||
@@ -106,9 +109,8 @@ export class PlayerEconomy {
|
|||||||
if (nChess <= layout.chess && nItem <= layout.item) return;
|
if (nChess <= layout.chess && nItem <= layout.item) return;
|
||||||
const chess = old.slice(0, layout.chess);
|
const chess = old.slice(0, layout.chess);
|
||||||
const items = old.slice(layout.chess);
|
const items = old.slice(layout.chess);
|
||||||
const fresh = (s) => { if (s) s.frozen = this.shop.frozen; return s; };
|
while (chess.length < nChess) chess.push(null);
|
||||||
while (chess.length < nChess) chess.push(fresh(this._rollChessSlot()));
|
while (items.length < nItem) items.push(null);
|
||||||
while (items.length < nItem) items.push(fresh(this._rollItemSlot()));
|
|
||||||
this.shop.slots = [...chess, ...items];
|
this.shop.slots = [...chess, ...items];
|
||||||
this.shop.layout = { chess: chess.length, item: items.length };
|
this.shop.layout = { chess: chess.length, item: items.length };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ test('matches: the 补位 match\'s human seat fields stand-ins (listed per round
|
|||||||
assert.ok(dg, 'scenario present');
|
assert.ok(dg, 'scenario present');
|
||||||
assert.deepEqual(dg.errors, { engine: 0, logged: 0, sim: 0, dispatcher: 0 });
|
assert.deepEqual(dg.errors, { engine: 0, logged: 0, sim: 0, dispatcher: 0 });
|
||||||
const fielded = dg.standIns.rounds.filter(([, s]) => s).flatMap(([, s]) => s.split(' '));
|
const fielded = dg.standIns.rounds.filter(([, s]) => s).flatMap(([, s]) => s.split(' '));
|
||||||
assert.ok(new Set(fielded).size >= 3, `stand-ins fielded: ${[...new Set(fielded)].join(', ')}`);
|
// (≥ 2 since a level-up leaves its new slot empty — GitHub #332, PR #333 by @2321Robin: this match's shop rolls, and with
|
||||||
|
// them which stand-ins the human seat fields, part from the stream in which the upgrade drew a card into the new slot)
|
||||||
|
assert.ok(new Set(fielded).size >= 2, `stand-ins fielded: ${[...new Set(fielded)].join(', ')}`);
|
||||||
for (const f of fielded) {
|
for (const f of fielded) {
|
||||||
const [id, charId] = f.split('→');
|
const [id, charId] = f.split('→');
|
||||||
const c = CHESS[`chess_char_${id}`];
|
const c = CHESS[`chess_char_${id}`];
|
||||||
|
|||||||
+493
-499
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,9 @@
|
|||||||
// Community report of 2026-10-06, item 19: 「升级商店获得新的商店位时用新卡补上,而不是空着」. The official tutorial's 休整期
|
// GitHub #332 / PR #333 by @2321Robin (the footage of an official 1→2 upgrade: bilibili BV1AXwuzdEys 1:39): a 调度中心
|
||||||
// page says 「升级:消耗资金升级调度中心。升级后将出现更多的商品栏位、可调度干员以及新装备」: the upgrade itself opens the
|
// upgrade opens the new level's extra slots at once but EMPTY — the official shop shows the new slot, draws no card into
|
||||||
// new level's extra slot. Until 0.2.0 the shop kept its old slot count until the next roll (a refresh or the round start),
|
// it, and fills it with the next roll (a manual refresh or the round start). The official texts only name the slots: the
|
||||||
// so a level-up from 1 to 2 (险境: 3 → 4 operator slots) or 3 to 4 (4 → 5) showed no new card. Now the upgrade adds the
|
// tutorial's 休整期 page 「升级后将出现更多的商品栏位」, PRTS 帮助 「增加刷新栏位」. 0.2.0 drew a card into each new slot instead
|
||||||
// extra slots at once, each with a new card drawn at the new level, and leaves the cards already shown where they are.
|
// (item 19 of the community report of 2026-10-06, 「升级商店获得新的商店位时用新卡补上,而不是空着」, one uncorroborated remark);
|
||||||
|
// before that the shop kept its old slot count until the next roll. Either way the upgrade leaves the cards shown where they are.
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { makeMatch, DATA } from './harness.js';
|
import { makeMatch, DATA } from './harness.js';
|
||||||
@@ -11,7 +12,7 @@ import { GameData } from '../../server/match/gamedata.js';
|
|||||||
const ids = (slots) => slots.map((s) => (s ? `${s.kind}:${s.id}${s.sold ? '(sold)' : ''}` : null));
|
const ids = (slots) => slots.map((s) => (s ? `${s.kind}:${s.id}${s.sold ? '(sold)' : ''}` : null));
|
||||||
const tierOf = (id) => DATA.chess[id]?.tier ?? 0;
|
const tierOf = (id) => DATA.chess[id]?.tier ?? 0;
|
||||||
|
|
||||||
test('a level-up opens the new level\'s operator slot at once with a new card; the cards shown stay in place', () => {
|
test('a level-up opens the new level\'s operator slot EMPTY; the cards shown stay in place; a refresh fills it', () => {
|
||||||
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed: 7, fake: true }).start();
|
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed: 7, fake: true }).start();
|
||||||
h.toPrep(1);
|
h.toPrep(1);
|
||||||
const ps = h.ps('p_0');
|
const ps = h.ps('p_0');
|
||||||
@@ -20,35 +21,37 @@ test('a level-up opens the new level\'s operator slot at once with a new card; t
|
|||||||
const s1 = ps.shop.slots.slice();
|
const s1 = ps.shop.slots.slice();
|
||||||
assert.equal(s1.length, 4);
|
assert.equal(s1.length, 4);
|
||||||
assert.deepEqual(h.m.handle('p_0', { t: 'g.levelUp' }), { ok: true });
|
assert.deepEqual(h.m.handle('p_0', { t: 'g.levelUp' }), { ok: true });
|
||||||
// level 2: 4 operator slots + the item slot — the three cards and the item kept (the same objects), one new card
|
// level 2: 4 operator slots + the item slot — the three cards and the item kept (the same objects), the new slot empty
|
||||||
assert.equal(ps.shop.level, 2);
|
assert.equal(ps.shop.level, 2);
|
||||||
assert.deepEqual(ps.shop.layout, { chess: 4, item: 1 });
|
assert.deepEqual(ps.shop.layout, { chess: 4, item: 1 });
|
||||||
assert.equal(ps.shop.slots.length, 5);
|
assert.equal(ps.shop.slots.length, 5);
|
||||||
for (let i = 0; i < 3; i++) assert.equal(ps.shop.slots[i], s1[i], `operator card ${i} kept`);
|
for (let i = 0; i < 3; i++) assert.equal(ps.shop.slots[i], s1[i], `operator card ${i} kept`);
|
||||||
assert.equal(ps.shop.slots[4], s1[3], 'the item card stays after the operator cards');
|
assert.equal(ps.shop.slots[4], s1[3], 'the item card stays after the operator cards');
|
||||||
const added = ps.shop.slots[3];
|
assert.equal(ps.shop.slots[3], null, 'the new slot is open and empty');
|
||||||
assert.ok(added && added.kind === 'chess' && !added.sold, `a new operator card: ${JSON.stringify(added)}`);
|
|
||||||
assert.ok(tierOf(added.id) >= 1 && tierOf(added.id) <= 2, `drawn at the new level (tier ${tierOf(added.id)})`);
|
|
||||||
assert.equal(added.frozen, false);
|
|
||||||
// the player sees it right away (m.private shop)
|
// the player sees it right away (m.private shop)
|
||||||
assert.deepEqual(ids(ps.privateView().shop.slots), ids(ps.shop.slots));
|
assert.deepEqual(ids(ps.privateView().shop.slots), ids(ps.shop.slots));
|
||||||
// 2 → 3 keeps 4 operator slots: nothing rerolled
|
// a manual refresh fills every slot at the new level (the empty one included)
|
||||||
|
assert.deepEqual(h.m.handle('p_0', { t: 'g.refresh' }), { ok: true });
|
||||||
|
assert.equal(ps.shop.slots.length, 5);
|
||||||
|
for (const s of ps.shop.slots) assert.ok(s && !s.sold, 'every slot carries a fresh card after the refresh');
|
||||||
|
for (const s of ps.shop.slots) if (s.kind === 'chess') assert.ok(tierOf(s.id) >= 1 && tierOf(s.id) <= 2, `drawn at level 2 (tier ${tierOf(s.id)})`);
|
||||||
|
// 2 → 3 keeps 4 operator slots: nothing rerolled, nothing added
|
||||||
const s2 = ps.shop.slots.slice();
|
const s2 = ps.shop.slots.slice();
|
||||||
h.m.handle('p_0', { t: 'g.levelUp' });
|
h.m.handle('p_0', { t: 'g.levelUp' });
|
||||||
assert.equal(ps.shop.level, 3);
|
assert.equal(ps.shop.level, 3);
|
||||||
assert.deepEqual(ps.shop.slots, s2, 'no extra slot at level 3, no reroll');
|
assert.deepEqual(ps.shop.slots, s2, 'no extra slot at level 3, no reroll');
|
||||||
// 3 → 4: the fifth operator slot
|
// 3 → 4: the fifth operator slot, empty again
|
||||||
h.m.handle('p_0', { t: 'g.levelUp' });
|
h.m.handle('p_0', { t: 'g.levelUp' });
|
||||||
assert.equal(ps.shop.level, 4);
|
assert.equal(ps.shop.level, 4);
|
||||||
assert.deepEqual(ps.shop.layout, { chess: 5, item: 1 });
|
assert.deepEqual(ps.shop.layout, { chess: 5, item: 1 });
|
||||||
for (let i = 0; i < 4; i++) assert.equal(ps.shop.slots[i], s2[i]);
|
for (let i = 0; i < 4; i++) assert.equal(ps.shop.slots[i], s2[i]);
|
||||||
assert.equal(ps.shop.slots[5], s2[4]);
|
assert.equal(ps.shop.slots[5], s2[4]);
|
||||||
assert.ok(ps.shop.slots[4]?.kind === 'chess' && tierOf(ps.shop.slots[4].id) <= 4);
|
assert.equal(ps.shop.slots[4], null, 'the new slot is open and empty');
|
||||||
h.invariants();
|
h.invariants();
|
||||||
h.m.dispose();
|
h.m.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a bought card stays sold; a frozen shop freezes the new card too [ASSUMED], and the round start keeps it', () => {
|
test('a bought card stays sold; the empty new slot has nothing to freeze; the round start fills the shop to the new level', () => {
|
||||||
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed: 11, fake: true }).start();
|
const h = makeMatch({ mode: 'solo', difficulty: 'NORMAL', seed: 11, fake: true }).start();
|
||||||
h.toPrep(1);
|
h.toPrep(1);
|
||||||
const ps = h.ps('p_0');
|
const ps = h.ps('p_0');
|
||||||
@@ -58,35 +61,43 @@ test('a bought card stays sold; a frozen shop freezes the new card too [ASSUMED]
|
|||||||
assert.deepEqual(h.m.handle('p_0', { t: 'g.freeze' }), { ok: true });
|
assert.deepEqual(h.m.handle('p_0', { t: 'g.freeze' }), { ok: true });
|
||||||
h.m.handle('p_0', { t: 'g.levelUp' });
|
h.m.handle('p_0', { t: 'g.levelUp' });
|
||||||
assert.equal(ps.shop.slots[0].sold, true, 'the bought slot is not refilled');
|
assert.equal(ps.shop.slots[0].sold, true, 'the bought slot is not refilled');
|
||||||
const added = ps.shop.slots[3];
|
assert.equal(ps.shop.slots[3], null, 'the new slot is open and empty — nothing to freeze');
|
||||||
assert.ok(added && !added.sold && added.frozen === true, 'the new card follows the freeze toggle');
|
|
||||||
const keep = ps.shop.slots.filter((s) => s && !s.sold && s.frozen).map((s) => s.id);
|
const keep = ps.shop.slots.filter((s) => s && !s.sold && s.frozen).map((s) => s.id);
|
||||||
assert.equal(keep.length, 4, 'two operator cards, the new one and the item');
|
assert.equal(keep.length, 3, 'two operator cards and the item carry the freeze toggle');
|
||||||
// the next round start keeps every frozen card (in place) and rolls only the sold slot
|
// the round start fills every slot of the new level: frozen cards kept in place, the sold and the empty slot rolled
|
||||||
h.drive(() => h.m.phase === 'PREP' && h.m.round === 2);
|
h.drive(() => h.m.phase === 'PREP' && h.m.round === 2);
|
||||||
assert.deepEqual(ps.shop.slots.slice(1).map((s) => s.id), keep);
|
|
||||||
assert.equal(ps.shop.slots.length, 5);
|
assert.equal(ps.shop.slots.length, 5);
|
||||||
|
for (const s of ps.shop.slots) assert.ok(s, 'the round start leaves no empty slot');
|
||||||
|
assert.deepEqual(ps.shop.slots.slice(1).map((s) => s.id).slice(0, 2), keep.slice(0, 2), 'the frozen operator cards kept');
|
||||||
|
assert.equal(ps.shop.slots[4].id, keep[2], 'the frozen item kept after the operator slots');
|
||||||
|
assert.notEqual(ps.shop.slots[0].id, undefined, 'the sold slot rerolled');
|
||||||
h.invariants();
|
h.invariants();
|
||||||
h.m.dispose();
|
h.m.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('every mode\'s slot table: a level-up adds exactly the slots the new level has more of (标准: 3,4,4,4,4,5)', () => {
|
test('every mode\'s slot table: a level-up opens exactly the new positions, empty (标准: 3,4,4,4,4,5)', () => {
|
||||||
for (const [mode, difficulty] of [['solo', 'FUNNY'], ['solo', 'NORMAL'], ['coop', 'FUNNY'], ['coop', 'ABYSS']]) {
|
for (const [mode, difficulty] of [['solo', 'FUNNY'], ['solo', 'NORMAL'], ['coop', 'FUNNY'], ['coop', 'ABYSS']]) {
|
||||||
const h = makeMatch({ mode, difficulty, seed: 3, fake: true, humans: 1, bots: mode === 'coop' ? 1 : 0 }).start();
|
const h = makeMatch({ mode, difficulty, seed: 3, fake: true, humans: 1, bots: mode === 'coop' ? 1 : 0 }).start();
|
||||||
h.toPrep(1);
|
h.toPrep(1);
|
||||||
const ps = h.ps('p_0');
|
const ps = h.ps('p_0');
|
||||||
const gd = h.m.gd;
|
const gd = h.m.gd;
|
||||||
ps.funds = 500;
|
ps.funds = 500;
|
||||||
|
let empty = 0;
|
||||||
for (let lv = 2; lv <= gd.maxShopLevel; lv++) {
|
for (let lv = 2; lv <= gd.maxShopLevel; lv++) {
|
||||||
const before = ps.shop.slots.slice();
|
const before = ps.shop.slots.slice();
|
||||||
assert.deepEqual(h.m.handle('p_0', { t: 'g.levelUp' }), { ok: true });
|
assert.deepEqual(h.m.handle('p_0', { t: 'g.levelUp' }), { ok: true });
|
||||||
const { chess, item } = gd.shopSlots(lv);
|
const { chess, item } = gd.shopSlots(lv);
|
||||||
assert.equal(ps.shop.slots.length, chess + item, `${mode} ${difficulty} L${lv}`);
|
assert.equal(ps.shop.slots.length, chess + item, `${mode} ${difficulty} L${lv}`);
|
||||||
const grew = chess - gd.shopSlots(lv - 1).chess;
|
const grew = (chess - gd.shopSlots(lv - 1).chess) + (item - gd.shopSlots(lv - 1).item);
|
||||||
const kept = ps.shop.slots.filter((s) => before.includes(s)).length;
|
const kept = ps.shop.slots.filter((s) => s && before.includes(s)).length;
|
||||||
assert.equal(kept, before.length, `${mode} ${difficulty} L${lv}: every shown card kept`);
|
assert.equal(kept, before.filter((s) => s).length, `${mode} ${difficulty} L${lv}: every shown card kept`);
|
||||||
assert.equal(ps.shop.slots.length - before.length, grew + (item - gd.shopSlots(lv - 1).item));
|
// the empty positions accumulate: nothing fills them between the upgrades (no refresh / round start here)
|
||||||
|
empty += grew;
|
||||||
|
assert.equal(ps.shop.slots.filter((s) => !s).length, empty, `${mode} ${difficulty} L${lv}: the new positions are open and empty`);
|
||||||
}
|
}
|
||||||
|
// one refresh fills everything at the final level
|
||||||
|
assert.deepEqual(h.m.handle('p_0', { t: 'g.refresh' }), { ok: true });
|
||||||
|
for (const s of ps.shop.slots) assert.ok(s, `${mode} ${difficulty}: the refresh leaves no empty slot`);
|
||||||
h.invariants();
|
h.invariants();
|
||||||
h.m.dispose();
|
h.m.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
// Browser check of the shop bar's empty slot (GitHub #332 / PR #333; opt-in, ~8 s) on the in-match mock harness:
|
||||||
|
// SP_E2E=1 CHROME_PATH=… node --test test/ui/shop-empty-slot.e2e.test.js
|
||||||
|
//
|
||||||
|
// A 调度中心 upgrade opens the new level's extra slot EMPTY (server/match/player/economy.js _openLevelSlots: a null slot
|
||||||
|
// in m.private shop.slots) and the next refresh or round start fills it. The bar used to draw every null slot like a
|
||||||
|
// bought one — the SOLD OUT frame with 「已招募」 — which on this slot says that somebody was recruited where nothing was.
|
||||||
|
// Asserted: the null slot is a bare frame (no text, a tooltip saying it fills at the next refresh), a bought slot still
|
||||||
|
// says 已招募, a real card is still a card, the empty frame is not a buyable card (`.scard:not(.scard--sold)` skips it)
|
||||||
|
// and a tap on it opens nothing; the refresh the mock answers fills it.
|
||||||
|
|
||||||
|
import { test, describe, before, after } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
|
||||||
|
const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||||
|
const ENABLED = process.env.SP_E2E === '1' && existsSync(CHROME);
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
describe('the empty slot of the shop bar', { skip: !ENABLED && 'set SP_E2E=1 (and have Chrome) to run' }, () => {
|
||||||
|
let srv;
|
||||||
|
let browser;
|
||||||
|
let base;
|
||||||
|
before(async () => {
|
||||||
|
const { startServer } = await import('../../server/index.js');
|
||||||
|
const puppeteer = (await import('puppeteer-core')).default;
|
||||||
|
srv = await startServer({ port: 0, host: '127.0.0.1', quiet: true });
|
||||||
|
base = `http://127.0.0.1:${srv.port}`;
|
||||||
|
browser = await puppeteer.launch({ executablePath: CHROME, headless: true, args: ['--no-sandbox', '--force-device-scale-factor=1'] });
|
||||||
|
});
|
||||||
|
after(async () => {
|
||||||
|
await browser?.close();
|
||||||
|
await srv?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
const cards = (page) => page.evaluate(() => [...document.querySelectorAll('.shopbar__cards .scard')].map((el) => ({
|
||||||
|
empty: el.classList.contains('scard--empty'), sold: el.classList.contains('scard--sold'), text: el.textContent.replace(/\s+/g, ' ').trim(),
|
||||||
|
title: el.getAttribute('title') || '',
|
||||||
|
})));
|
||||||
|
|
||||||
|
test('a null slot is a bare frame, a bought one still says 已招募, and a refresh fills the empty one', async () => {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1600, height: 900 });
|
||||||
|
const problems = [];
|
||||||
|
page.on('console', (m) => { if (m.type() === 'error') problems.push(`console: ${m.text()}`); });
|
||||||
|
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
||||||
|
await page.goto(`${base}/dev/game-mock.html?phase=PREP&render=fallback`, { waitUntil: 'networkidle0', timeout: 60000 });
|
||||||
|
await page.waitForSelector('.shopbar__cards .scard', { timeout: 15000 });
|
||||||
|
await sleep(400);
|
||||||
|
const before = await cards(page);
|
||||||
|
const n = before.length;
|
||||||
|
assert.ok(n >= 4 && !before[0].sold, `the mock's shop has ${n} operator cards, the first one for sale`);
|
||||||
|
|
||||||
|
// the upgrade's new slot: a null at the end of the operator slots; one more slot bought
|
||||||
|
await page.evaluate(() => globalThis.__MOCK__.mutate((S) => {
|
||||||
|
const slots = S.priv.shop.slots;
|
||||||
|
const chess = slots.filter((s) => !s || s.kind !== 'item');
|
||||||
|
const item = slots.filter((s) => s && s.kind === 'item');
|
||||||
|
chess[0] = { ...chess[0], sold: true };
|
||||||
|
S.priv.shop.slots = [...chess, null, ...item];
|
||||||
|
}));
|
||||||
|
await sleep(300);
|
||||||
|
const after = await cards(page);
|
||||||
|
assert.equal(after.length, n + 1);
|
||||||
|
const empty = after.filter((c) => c.empty);
|
||||||
|
assert.equal(empty.length, 1, 'one empty frame');
|
||||||
|
assert.equal(empty[0].text, '', 'no SOLD OUT, no 已招募 on it');
|
||||||
|
assert.match(empty[0].title, /空栏位/);
|
||||||
|
assert.ok(empty[0].sold, 'it carries the inert frame class every `:not(.scard--sold)` rule skips (no hover lift, not a buyable card)');
|
||||||
|
assert.equal(after[0].empty, false);
|
||||||
|
assert.match(after[0].text, /已招募/, 'a bought slot still says so');
|
||||||
|
assert.equal(after.filter((c) => !c.sold).length, before.filter((c) => !c.sold).length - 1, 'the rest are real cards');
|
||||||
|
assert.equal(after[after.length - 1].empty, true, 'the new slot is the last operator slot');
|
||||||
|
|
||||||
|
// a tap on the empty frame opens nothing
|
||||||
|
await page.evaluate(() => document.querySelector('.shopbar__cards .scard--empty').click());
|
||||||
|
await sleep(250);
|
||||||
|
assert.equal(await page.$('.dpanel'), null);
|
||||||
|
|
||||||
|
// a refresh fills it: the mock rolls every slot
|
||||||
|
await page.evaluate(() => [...document.querySelectorAll('.toolbtn')].find((b) => /刷新/.test(b.textContent))?.click());
|
||||||
|
await page.waitForFunction(() => document.querySelectorAll('.shopbar__cards .scard--empty').length === 0, { timeout: 5000 });
|
||||||
|
assert.equal((await cards(page)).length, n + 1);
|
||||||
|
assert.deepEqual(problems, []);
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the upgrade itself: tapping the level card twice opens the fifth slot empty, the cards shown stay', async () => {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setViewport({ width: 1600, height: 900 });
|
||||||
|
const problems = [];
|
||||||
|
page.on('console', (m) => { if (m.type() === 'error') problems.push(`console: ${m.text()}`); });
|
||||||
|
page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
|
||||||
|
await page.goto(`${base}/dev/game-mock.html?phase=PREP&render=fallback`, { waitUntil: 'networkidle0', timeout: 60000 });
|
||||||
|
await page.waitForSelector('.shopbar__cards .scard', { timeout: 15000 });
|
||||||
|
// level 3 with its four operator slots, funds for the upgrade (the mock answers like the server: level 4 has five)
|
||||||
|
await page.evaluate(() => globalThis.__MOCK__.mutate((S) => {
|
||||||
|
const shop = S.priv.shop;
|
||||||
|
const chess = shop.slots.filter((s) => s && s.kind === 'chess' && !s.sold).slice(0, 4);
|
||||||
|
shop.level = 3; shop.upgradePrice = 11; S.priv.funds = 50;
|
||||||
|
shop.slots = [...chess, shop.slots.find((s) => s && s.kind === 'item')];
|
||||||
|
}));
|
||||||
|
await sleep(300);
|
||||||
|
const before = await cards(page);
|
||||||
|
assert.equal(before.length, 4);
|
||||||
|
assert.ok(before.every((c) => !c.sold), 'four real cards');
|
||||||
|
await page.click('.lvcard'); // two taps: select, confirm
|
||||||
|
await sleep(200);
|
||||||
|
await page.click('.lvcard');
|
||||||
|
await page.waitForFunction(() => document.querySelectorAll('.shopbar__cards .scard--empty').length === 1, { timeout: 5000 });
|
||||||
|
const after = await cards(page);
|
||||||
|
assert.equal(after.length, 5);
|
||||||
|
assert.deepEqual(after.map((c) => c.empty), [false, false, false, false, true]);
|
||||||
|
assert.deepEqual(after.slice(0, 4).map((c) => c.text), before.map((c) => c.text), 'the cards shown stay as they were');
|
||||||
|
assert.ok(after.slice(0, 4).every((c) => !/已招募/.test(c.text)));
|
||||||
|
assert.deepEqual(problems, []);
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user