- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting. - Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn): Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes. - Formal partners for all ten pets in the arena and portraits, including new VineSprite, ThunderLeopard and TideWhale. - Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands, configurable Unity run_script timeout, companion static stage. - Also includes concurrent work from another tool in this working tree (FlameLion, enemy combat FX, challenge mode, home explorer, server updates). - Resources registered in Doc/ResDesc_*.md; verified on isolated :18081. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
8.8 KiB
Python
203 lines
8.8 KiB
Python
#!/usr/bin/env python3
|
||
"""G3 economy samples from Config/Gameplay/greenfield.json.
|
||
|
||
Three trajectories from GameDesign 8.2: first day, seven logins, and stuck before a boss.
|
||
Ignores natural marble recovery. Pinball candy/coins use an equal chance across the five bins.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
CONFIG = ROOT / "Config/Gameplay/greenfield.json"
|
||
OUT_MD = ROOT / "Doc/Design/G3/EconomySample.md"
|
||
OUT_JSON = ROOT / "Doc/Design/G3/economy_sample.json"
|
||
|
||
# GameDesign 8.2, aligned to the live first-five clear and pinball means.
|
||
DESIGN_DAY1_BALLS = 67
|
||
DESIGN_TEN_SHOTS = {"candy": 200, "coins": 520, "tickets": 1}
|
||
|
||
|
||
def reward_of(node: dict) -> dict:
|
||
src = node.get("reward") or node.get("contents") or {}
|
||
return {k: int(src.get(k, 0) or 0) for k in ("coins", "candy", "balls", "tickets")}
|
||
|
||
|
||
def add(total: dict, part: dict, times: int = 1) -> None:
|
||
for k in total:
|
||
total[k] += part.get(k, 0) * times
|
||
|
||
|
||
def line(name: str, part: dict, times: int = 1) -> str:
|
||
shown = {k: part.get(k, 0) * times for k in ("coins", "candy", "balls", "tickets") if part.get(k, 0)}
|
||
body = "、".join(f"{label}{shown[key]}" for key, label in (
|
||
("balls", "弹珠"), ("candy", "糖果"), ("coins", "金币"), ("tickets", "券")
|
||
) if key in shown) or "无资源"
|
||
suffix = f" ×{times}" if times > 1 else ""
|
||
return f"| {name}{suffix} | {body} |"
|
||
|
||
|
||
def main() -> None:
|
||
cfg = json.loads(CONFIG.read_text(encoding="utf-8"))
|
||
economy = cfg["economy"]
|
||
stages = cfg["stages"]
|
||
pinball = cfg["pinball"]
|
||
candy_mean = sum(pinball["candy"]) / len(pinball["candy"])
|
||
coin_mean = sum(pinball["coins"]) / len(pinball["coins"])
|
||
ten = {
|
||
"candy": int(round(candy_mean * 10)),
|
||
"coins": int(round(coin_mean * 10)),
|
||
"tickets": 10 // economy["ticketEvery"],
|
||
}
|
||
train_to = lambda level: economy["trainCost"] * sum(range(1, level))
|
||
|
||
zero = {"coins": 0, "candy": 0, "balls": 0, "tickets": 0}
|
||
day1 = dict(zero)
|
||
rows = []
|
||
|
||
def take(name: str, part: dict, times: int = 1) -> None:
|
||
add(day1, part, times)
|
||
rows.append(line(name, part, times))
|
||
|
||
take("初始弹珠", {"balls": economy["initialBalls"]})
|
||
take("欢迎邮件", reward_of(cfg["mail"]["initial"][0]))
|
||
take("七日签到第1天", reward_of({"reward": cfg["daily"]["signin"][0]}))
|
||
for task in cfg["daily"]["tasks"]:
|
||
take(task["title"], reward_of(task))
|
||
take(cfg["shop"]["goods"][0]["name"], reward_of(cfg["shop"]["goods"][0]))
|
||
take("教学目标(通过1-1)", {"balls": economy["goalBalls"]})
|
||
first5 = [s for s in stages if s["id"] <= 5]
|
||
for stage in first5:
|
||
take(f"首通 {stage['code']}", reward_of(stage))
|
||
chest_by_stage = {d["stageId"]: cfg["rewards"]["chests"][d["chest"]] for d in cfg["rewards"]["stageDrops"]}
|
||
for stage_id in (1, 3):
|
||
chest = chest_by_stage[stage_id]
|
||
take(f"打开{chest['name']}({stages[stage_id - 1]['code']}掉落)", reward_of(chest))
|
||
|
||
design_stage_balls = 17
|
||
config_stage_balls = sum(s["reward"]["balls"] for s in first5)
|
||
design_matched = (
|
||
economy["initialBalls"] + cfg["daily"]["signin"][0]["balls"]
|
||
+ sum(t["reward"]["balls"] for t in cfg["daily"]["tasks"])
|
||
+ cfg["shop"]["goods"][0]["reward"]["balls"] + design_stage_balls
|
||
)
|
||
|
||
week = dict(zero)
|
||
week_rows = []
|
||
signin = {"balls": sum(d["balls"] for d in cfg["daily"]["signin"])}
|
||
tasks = {"balls": 7 * sum(t["reward"]["balls"] for t in cfg["daily"]["tasks"])}
|
||
shop = {"balls": 7 * cfg["shop"]["goods"][0]["reward"]["balls"]}
|
||
for name, part in (("七日签到全部领取", signin), ("七日每日任务全部领取", tasks), ("七日免费补给", shop)):
|
||
add(week, part)
|
||
week_rows.append(line(name, part))
|
||
|
||
stuck = dict(zero)
|
||
stuck_rows = []
|
||
|
||
def stuck_take(name: str, part: dict, times: int = 1) -> None:
|
||
add(stuck, part, times)
|
||
stuck_rows.append(line(name, part, times))
|
||
|
||
stuck_take("初始弹珠", {"balls": economy["initialBalls"]})
|
||
stuck_take("欢迎邮件", reward_of(cfg["mail"]["initial"][0]))
|
||
stuck_take("签到第1天", reward_of({"reward": cfg["daily"]["signin"][0]}))
|
||
for task in cfg["daily"]["tasks"]:
|
||
stuck_take(task["title"], reward_of(task))
|
||
stuck_take("免费补给", reward_of(cfg["shop"]["goods"][0]))
|
||
stuck_take("教学目标", {"balls": economy["goalBalls"]})
|
||
for stage in stages:
|
||
if stage["id"] >= 5:
|
||
break
|
||
stuck_take(f"首通 {stage['code']}", reward_of(stage))
|
||
stuck_take("打开绿野补给箱", reward_of(chest_by_stage[1]))
|
||
need_candy = train_to(5)
|
||
need_coins = economy["evolveCoins"]
|
||
candy_gap = max(0, need_candy - stuck["candy"])
|
||
coin_gap = max(0, need_coins - stuck["coins"])
|
||
shots_for_candy = (candy_gap + candy_mean - 1) // candy_mean if candy_mean else 0
|
||
shots_for_coins = (coin_gap + coin_mean - 1) // coin_mean if coin_mean else 0
|
||
shard_from_chest = 10
|
||
shard_gap = economy["evolveShards"] - shard_from_chest
|
||
|
||
report = {
|
||
"config": str(CONFIG.relative_to(ROOT)).replace("\\", "/"),
|
||
"ignored": ["natural marble recovery", "pinball path variance beyond bin average"],
|
||
"pinballAverage": {"candy": candy_mean, "coins": coin_mean, "perTenShots": ten},
|
||
"designDay1Balls": DESIGN_DAY1_BALLS,
|
||
"designFormulaWithLiveNonStageSources": design_matched,
|
||
"configFirstFiveStageBalls": config_stage_balls,
|
||
"day1": day1,
|
||
"sevenDaysWithoutClears": week,
|
||
"stuckBefore1_5": {
|
||
"walletBeforePinball": stuck,
|
||
"trainToLevel5Candy": need_candy,
|
||
"candyGap": candy_gap,
|
||
"pinballShotsToCloseCandy": int(shots_for_candy),
|
||
"evolveCoins": need_coins,
|
||
"coinGap": coin_gap,
|
||
"pinballShotsToCloseCoins": int(shots_for_coins),
|
||
"evolveShards": economy["evolveShards"],
|
||
"shardsFromChoiceChest": shard_from_chest,
|
||
"shardGap": shard_gap,
|
||
},
|
||
}
|
||
OUT_JSON.parent.mkdir(parents=True, exist_ok=True)
|
||
OUT_JSON.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
||
def wallet(w: dict) -> str:
|
||
return f"弹珠 {w['balls']}、糖果 {w['candy']}、金币 {w['coins']}、捕捉券 {w['tickets']}"
|
||
|
||
md = f"""# G3 经济样例
|
||
|
||
由 `Tools/economy_sample.py` 读取 `Config/Gameplay/greenfield.json` 生成。不计弹珠自然恢复。弹珠台按五个落区等概率取均值:糖果 {candy_mean:g}、金币 {coin_mean:g}。实际单次受路径影响。
|
||
|
||
## 对照设计稿
|
||
|
||
`GameDesign` 8.2 的首日上限是 **{DESIGN_DAY1_BALLS}** 枚弹珠:初始 20、签到 10、每日任务 15、免费补给 5、前五关首通 {design_stage_balls}。按同一口径合计得到 **{design_matched}**。现行 1-1~1-5 首通弹珠合计是 **{config_stage_balls}**,与稿一致。
|
||
|
||
稿中「10 发预计糖果 200、金币 520、券 1」。现行均值为糖果 {ten['candy']}、金币 {ten['coins']}、券 {ten['tickets']},与稿一致。
|
||
|
||
## 首日获取上限
|
||
|
||
假设当天领完邮件、签到、三个每日任务、免费补给和教学目标,打通 1-1~1-5 并打开已掉落的绿野补给箱与碎片自选箱。自选箱只给碎片,不进下表钱包。溪口大礼箱要先通过 1-5,打开后另有金币 150、糖果 80、弹珠 5、券 1,未计入本表。
|
||
|
||
| 来源 | 到账 |
|
||
| --- | --- |
|
||
{chr(10).join(rows)}
|
||
|
||
合计:**{wallet(day1)}**。这是获取上限,不是要求当天花完。
|
||
|
||
## 七日回访
|
||
|
||
不打新关、不发射。每日未领奖励过期,下表按七天都当天领完。签到不循环。
|
||
|
||
| 来源 | 到账 |
|
||
| --- | --- |
|
||
{chr(10).join(week_rows)}
|
||
|
||
合计:**{wallet(week)}**。
|
||
|
||
## 卡在 1-5 之前
|
||
|
||
已通过 1-1~1-4,打不过溪口大角王。当天可领来源如下,不含为了拿糖果而发射的弹珠台产出。
|
||
|
||
| 来源 | 到账 |
|
||
| --- | --- |
|
||
{chr(10).join(stuck_rows)}
|
||
|
||
合计:**{wallet(stuck)}**。
|
||
|
||
- 训练到建议 Lv.5 需要糖果 {need_candy}。账上差 {candy_gap},按均值再发射 {int(shots_for_candy)} 次可补上。主线不另扣体力。
|
||
- 进化需要金币 {need_coins}。账上差 {coin_gap},按均值再发射 {int(shots_for_coins)} 次可补上。通过 1-1 后商店每天还能用金币换糖果,那是把金币换成糖果,不增加总资源。
|
||
- 进化需要碎片 {economy['evolveShards']}。1-3 自选箱提供 {shard_from_chest},还差 {shard_gap}。重复伙伴每次 {economy['duplicateShards']} 片,不能把碎片换回捕捉券。
|
||
|
||
训练、进化和再抽一只伙伴都能从免费来源走到,碎片比糖果和金币更慢。
|
||
"""
|
||
OUT_MD.write_text(md, encoding="utf-8")
|
||
print(json.dumps({"day1": day1, "week": week, "stuck": stuck, "tenShots": ten}, ensure_ascii=False))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|