Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0b9a10ae9 | ||
|
|
874fe81039 |
No files matched your search
@@ -32,3 +32,7 @@ Thumbs.db
|
|||||||
# 发版 token
|
# 发版 token
|
||||||
maipulse/tools/.release-token
|
maipulse/tools/.release-token
|
||||||
tools/.release-token
|
tools/.release-token
|
||||||
|
# 打包时从 maipulse/ 复制进去的运行时副本,不入库
|
||||||
|
android/app/src/main/assets/
|
||||||
|
android/.gradle/
|
||||||
|
android/build/
|
||||||
@@ -12,8 +12,8 @@ android {
|
|||||||
minSdk 24
|
minSdk 24
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
// 和网页里的 APP_VERSION 对齐,升级时一起改
|
// 和网页里的 APP_VERSION 对齐,升级时一起改
|
||||||
versionCode 102
|
versionCode 103
|
||||||
versionName "1.0.2"
|
versionName "1.0.3"
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "arm64-v8a"
|
abiFilters "arm64-v8a"
|
||||||
|
|||||||
@@ -736,6 +736,12 @@ class PlayData:
|
|||||||
default_factory=lambda: config.PLAY_MUSIC_PLAY_COUNT_GAIN)
|
default_factory=lambda: config.PLAY_MUSIC_PLAY_COUNT_GAIN)
|
||||||
map_distance_gain: int = field(default_factory=lambda: config.PLAY_MAP_DISTANCE_GAIN)
|
map_distance_gain: int = field(default_factory=lambda: config.PLAY_MAP_DISTANCE_GAIN)
|
||||||
character_exp_gain: int = field(default_factory=lambda: config.PLAY_CHARACTER_EXP_GAIN)
|
character_exp_gain: int = field(default_factory=lambda: config.PLAY_CHARACTER_EXP_GAIN)
|
||||||
|
#: ★ 目标地图:0 = 不切换(沿用服务器下发的 selectMapId,只推进当前选中的那张)。
|
||||||
|
#: 非 0 = 先把 userData.selectMapId 切到该 mapId,再把 distance 加在它身上。
|
||||||
|
#: 注意:服务端是否接受客户端改 selectMapId 尚未在真机验证 —— 真机样本里
|
||||||
|
#: selectMapId 只出现在 Get 侧,Upsert 侧从没被改过。若服务端忽略该字段,
|
||||||
|
#: 推进仍会落在「服务器当前选中的地图」上,而不是这里指定的那张。
|
||||||
|
select_map_id: int = 0
|
||||||
|
|
||||||
# ---- 里程 / 登录奖励 ----
|
# ---- 里程 / 登录奖励 ----
|
||||||
# ★ 这两个是**登录奖励发放**才产生的,普通游玩结算不回传(留空)。
|
# ★ 这两个是**登录奖励发放**才产生的,普通游玩结算不回传(留空)。
|
||||||
@@ -1757,6 +1763,11 @@ def apply_play_effects(
|
|||||||
ud["dateTime"] = int(auth_date_time)
|
ud["dateTime"] = int(auth_date_time)
|
||||||
if ud.get("banState") is None:
|
if ud.get("banState") is None:
|
||||||
ud["banState"] = 0
|
ud["banState"] = 0
|
||||||
|
# ★ 切区域:只有显式给了目标 mapId 才写入;否则保持服务器下发的 selectMapId。
|
||||||
|
# 放在这里(而不是差分段)是为了让下面 userMapList 能读到"切换后"的值。
|
||||||
|
_want_map = int(getattr(play, "select_map_id", 0) or 0)
|
||||||
|
if _want_map:
|
||||||
|
ud["selectMapId"] = _want_map
|
||||||
|
|
||||||
# ---------------- userExtend ----------------
|
# ---------------- userExtend ----------------
|
||||||
ext_list = user_all.get("userExtend") or []
|
ext_list = user_all.get("userExtend") or []
|
||||||
@@ -1824,17 +1835,31 @@ def apply_play_effects(
|
|||||||
if "userMapList" in user_all:
|
if "userMapList" in user_all:
|
||||||
if play.map_distance_gain:
|
if play.map_distance_gain:
|
||||||
baseline = user_all.get("_baseline_map") or []
|
baseline = user_all.get("_baseline_map") or []
|
||||||
map_id = None
|
|
||||||
ud = (user_all.get("userData") or [{}])[0]
|
ud = (user_all.get("userData") or [{}])[0]
|
||||||
if isinstance(ud, dict):
|
want = int(getattr(play, "select_map_id", 0) or 0)
|
||||||
map_id = ud.get("selectMapId")
|
map_id = want or (ud.get("selectMapId") if isinstance(ud, dict) else None)
|
||||||
target = None
|
target = None
|
||||||
for item in baseline:
|
for item in baseline:
|
||||||
if isinstance(item, dict) and item.get("mapId") == map_id:
|
if isinstance(item, dict) and item.get("mapId") == map_id:
|
||||||
target = copy.deepcopy(item)
|
target = copy.deepcopy(item)
|
||||||
break
|
break
|
||||||
if target is None and baseline and isinstance(baseline[0], dict):
|
if target is None:
|
||||||
target = copy.deepcopy(baseline[0])
|
if map_id:
|
||||||
|
# ★ 目标地图不在玩家基线里(没进过这个区域):
|
||||||
|
# 按 GetUserMap 返回的字段形状造一条全新记录,distance 从 0 起算。
|
||||||
|
# 这条会因「新状态有、基线没有」在差分环节被保留,
|
||||||
|
# isNewMapList 自动打 "1"(新增)。
|
||||||
|
target = {
|
||||||
|
"mapId": int(map_id),
|
||||||
|
"distance": 0,
|
||||||
|
"isLock": False,
|
||||||
|
"isClear": False,
|
||||||
|
"isComplete": False,
|
||||||
|
"unlockFlag": 0,
|
||||||
|
}
|
||||||
|
log_info("userMapList:目标地图 " + str(map_id) + " 不在基线中,按新记录构造")
|
||||||
|
elif baseline and isinstance(baseline[0], dict):
|
||||||
|
target = copy.deepcopy(baseline[0])
|
||||||
if target is not None:
|
if target is not None:
|
||||||
target["distance"] = int(target.get("distance", 0) or 0) + play.map_distance_gain
|
target["distance"] = int(target.get("distance", 0) or 0) + play.map_distance_gain
|
||||||
user_all["userMapList"] = [target]
|
user_all["userMapList"] = [target]
|
||||||
|
|||||||
+59
-9
@@ -37,6 +37,7 @@ from flask import Flask, jsonify, request, send_file # noqa: E402
|
|||||||
import api # noqa: E402
|
import api # noqa: E402
|
||||||
from core import accounts, charge, items, repair, session, shop, unlock, b50, profile # noqa: E402
|
from core import accounts, charge, items, repair, session, shop, unlock, b50, profile # noqa: E402
|
||||||
from core import characters, mileage, history, recommend as recommend_mod # noqa: E402
|
from core import characters, mileage, history, recommend as recommend_mod # noqa: E402
|
||||||
|
from core import runmap # noqa: E402
|
||||||
app = Flask(__name__, static_folder=None)
|
app = Flask(__name__, static_folder=None)
|
||||||
|
|
||||||
LOCK = threading.Lock()
|
LOCK = threading.Lock()
|
||||||
@@ -370,9 +371,13 @@ def api_ds():
|
|||||||
@app.post("/api/logout")
|
@app.post("/api/logout")
|
||||||
@guarded
|
@guarded
|
||||||
def api_logout():
|
def api_logout():
|
||||||
if not S["logged_in"]:
|
# ★ 只要还有 token 就去登出:热重启/异常后 logged_in 可能已经是 False,
|
||||||
|
# 但服务端会话还活着,这时候不踢掉就会卡 isLogin=True,再扫码 returnCode=100。
|
||||||
|
if not S.get("logged_in") and not S.get("token"):
|
||||||
return fail("未登录")
|
return fail("未登录")
|
||||||
resp = session.logout(S)
|
resp = session.logout(S)
|
||||||
|
S.update({"logged_in": False, "is_login": False, "token": "",
|
||||||
|
"login_id": "", "login_at": 0})
|
||||||
return ok(returnCode=getattr(resp, "returnCode", None))
|
return ok(returnCode=getattr(resp, "returnCode", None))
|
||||||
|
|
||||||
|
|
||||||
@@ -387,11 +392,11 @@ def api_shutdown():
|
|||||||
"""
|
"""
|
||||||
info = {}
|
info = {}
|
||||||
try:
|
try:
|
||||||
if S.get("logged_in"):
|
if S.get("logged_in") or S.get("token"):
|
||||||
resp = session.logout(S)
|
resp = session.logout(S)
|
||||||
info["logout"] = getattr(resp, "returnCode", None)
|
info["logout"] = getattr(resp, "returnCode", None)
|
||||||
else:
|
else:
|
||||||
info["logout"] = "未登录,跳过"
|
info["logout"] = "没有会话,跳过"
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
info["logoutError"] = str(exc)
|
info["logoutError"] = str(exc)
|
||||||
threading.Timer(0.8, lambda: os._exit(0)).start()
|
threading.Timer(0.8, lambda: os._exit(0)).start()
|
||||||
@@ -408,20 +413,24 @@ def api_reload():
|
|||||||
登录态和会话都保住。只重载 core 包内的纯逻辑模块,不碰 api / Flask。
|
登录态和会话都保住。只重载 core 包内的纯逻辑模块,不碰 api / Flask。
|
||||||
"""
|
"""
|
||||||
import importlib
|
import importlib
|
||||||
names = ["core.settle", "core.play", "core.items", "core.unlock",
|
import pkgutil
|
||||||
"core.charge", "core.repair", "core.session", "core.shop",
|
import core as _core_pkg
|
||||||
"core.b50", "core.profile", "core.characters", "core.mileage"]
|
# ★ 自动发现 core 包下所有已加载的模块,别再手写列表:
|
||||||
|
# 漏一个(比如后加的 runmap)就会出现「改了代码、reload 报成功、实际没生效」。
|
||||||
|
names = sorted("core." + m.name for m in pkgutil.iter_modules(_core_pkg.__path__))
|
||||||
done = []
|
done = []
|
||||||
|
skipped = []
|
||||||
for name in names:
|
for name in names:
|
||||||
mod = sys.modules.get(name)
|
mod = sys.modules.get(name)
|
||||||
if mod is None:
|
if mod is None:
|
||||||
|
skipped.append(name) # 没被 import 过的不用管
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
importlib.reload(mod)
|
importlib.reload(mod)
|
||||||
done.append(name)
|
done.append(name)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
return fail("重载 %s 失败:%s" % (name, exc))
|
return fail("重载 %s 失败:%s" % (name, exc))
|
||||||
return ok(reloaded=done, logged=bool(S.get("logged_in")))
|
return ok(reloaded=done, skipped=skipped, logged=bool(S.get("logged_in")))
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/state")
|
@app.get("/api/state")
|
||||||
@@ -655,6 +664,47 @@ def api_unlock():
|
|||||||
return ok(**result)
|
return ok(**result)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- 一键跑图
|
||||||
|
|
||||||
|
@app.get("/api/runmap")
|
||||||
|
@guarded
|
||||||
|
def api_runmap_list():
|
||||||
|
"""区域列表 + 玩家当前进度(只读)。"""
|
||||||
|
need_login()
|
||||||
|
try:
|
||||||
|
return ok(**runmap.overview(S))
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return fail(str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/runmap")
|
||||||
|
@guarded
|
||||||
|
def api_runmap_write():
|
||||||
|
"""一键跑图:把选中区域推到终点,或推指定距离。默认 dry-run。"""
|
||||||
|
need_login()
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
map_id = int(body.get("mapId") or 0)
|
||||||
|
distance = int(body.get("distance") or 0)
|
||||||
|
step = int(body.get("step") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return fail("mapId / distance / step 必须都是数字")
|
||||||
|
if not map_id:
|
||||||
|
return fail("没有选区区域")
|
||||||
|
mode = str(body.get("mode") or "fill")
|
||||||
|
if mode not in ("fill", "delta"):
|
||||||
|
return fail("mode 只能是 fill 或 delta")
|
||||||
|
if mode == "delta" and distance <= 0:
|
||||||
|
return fail("delta 模式下 distance 必须大于 0")
|
||||||
|
try:
|
||||||
|
result = runmap.run(S, map_id, mode=mode, distance=distance, step=step,
|
||||||
|
dry_run=bool(body.get("dryRun", True)),
|
||||||
|
cooldown=float(body.get("cooldown", 0)))
|
||||||
|
return ok(**result)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
return fail(str(exc))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- 小黑屋
|
# ---------------------------------------------------------------- 小黑屋
|
||||||
|
|
||||||
@app.post("/api/unstick")
|
@app.post("/api/unstick")
|
||||||
@@ -874,8 +924,8 @@ def api_master():
|
|||||||
|
|
||||||
# ---------------------------------------------------------------- 更新
|
# ---------------------------------------------------------------- 更新
|
||||||
|
|
||||||
APP_VERSION = "1.0.2"
|
APP_VERSION = "1.0.3"
|
||||||
APP_BUILD = "2026-10-30"
|
APP_BUILD = "2026-11-04"
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/version")
|
@app.get("/api/version")
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""一键跑图:把指定区域的地图进度推到终点。
|
||||||
|
|
||||||
|
机制(不是独立接口,走 UpsertUserAll 的差分):
|
||||||
|
userData.selectMapId 指向本次推进的地图
|
||||||
|
userMapList 只回传「distance 变了」的那一条
|
||||||
|
PlayData.map_distance_gain 每局推进的距离
|
||||||
|
|
||||||
|
地图表来自游戏包 StreamingAssets/<版本>/map/MapXXXXXX/Map.xml:
|
||||||
|
TreasureExDatas 里每条奖励带一个 Distance,
|
||||||
|
Flag=End 那条就是跑完这张图的终点距离。
|
||||||
|
|
||||||
|
★ 两个现场才看得出来的不确定点(代码已按最乐观实现,需要真实账号验证):
|
||||||
|
1. 服务端是否接受客户端改写 selectMapId。真机样本里 selectMapId 只出现在
|
||||||
|
Get 侧,Upsert 侧从没被改过。若不接受,推进仍会落在服务器当前选中的图上。
|
||||||
|
2. 一局能走多远。真机一局 11000,跑完 Phigros 要 68 局;一次性 +750000
|
||||||
|
可能被拒绝或截断。所以默认分段(step)推进,而不是一把梭。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import api
|
||||||
|
|
||||||
|
from . import settle, session
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
MAP_JSON = os.path.join(BASE_DIR, "data", "master", "map.json")
|
||||||
|
|
||||||
|
#: 真机一局走过的距离(机台实测值)
|
||||||
|
STEP_DISTANCE = 11000
|
||||||
|
|
||||||
|
_CATALOG = None
|
||||||
|
|
||||||
|
|
||||||
|
def catalog() -> list:
|
||||||
|
"""全部区域(含终点距离)。按 mapId 升序。"""
|
||||||
|
global _CATALOG
|
||||||
|
if _CATALOG is None:
|
||||||
|
if not os.path.isfile(MAP_JSON):
|
||||||
|
_CATALOG = []
|
||||||
|
else:
|
||||||
|
with open(MAP_JSON, encoding="utf-8") as fp:
|
||||||
|
_CATALOG = json.load(fp)
|
||||||
|
return _CATALOG
|
||||||
|
|
||||||
|
|
||||||
|
def find_area(map_id: int) -> dict:
|
||||||
|
mid = int(map_id)
|
||||||
|
for row in catalog():
|
||||||
|
try:
|
||||||
|
if int(row.get("mapId") or 0) == mid:
|
||||||
|
return row
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def progress(snapshot: dict) -> dict:
|
||||||
|
"""从快照取玩家各地图进度:mapId -> {distance, isClear, isComplete, isLock}。
|
||||||
|
|
||||||
|
★ 节点可能是「Get 响应包装」{"userMapList": [...]},要先拆一层。
|
||||||
|
"""
|
||||||
|
node = (snapshot or {}).get("userMapList")
|
||||||
|
if isinstance(node, dict):
|
||||||
|
node = node.get("userMapList") or []
|
||||||
|
out = {}
|
||||||
|
for row in node or []:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
mid = int(row.get("mapId") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if not mid:
|
||||||
|
continue
|
||||||
|
out[mid] = {
|
||||||
|
"distance": int(row.get("distance") or 0),
|
||||||
|
"isClear": bool(row.get("isClear")),
|
||||||
|
"isComplete": bool(row.get("isComplete")),
|
||||||
|
"isLock": bool(row.get("isLock")),
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def overview(state: dict) -> dict:
|
||||||
|
"""区域列表 + 玩家进度,给前端渲染。"""
|
||||||
|
user_id = int(state["user_id"])
|
||||||
|
snapshot = api.GetNeedUserData(user_id, token=state["token"], save=False)
|
||||||
|
have = progress(snapshot)
|
||||||
|
select = None
|
||||||
|
node = snapshot.get("userData")
|
||||||
|
# ★ Get 侧 userData 是两层嵌套:{"userData": {"userId":…, "userData": {...}, "banState":0}}
|
||||||
|
# 内层是**单个 dict**,不是 list;Upsert 侧才可能是 list。两种都要兼容。
|
||||||
|
if isinstance(node, dict):
|
||||||
|
inner = node.get("userData")
|
||||||
|
if isinstance(inner, dict):
|
||||||
|
node = [inner]
|
||||||
|
elif isinstance(inner, list):
|
||||||
|
node = inner
|
||||||
|
elif isinstance(node, list):
|
||||||
|
node = node
|
||||||
|
if isinstance(node, dict):
|
||||||
|
node = [node]
|
||||||
|
if isinstance(node, list) and node and isinstance(node[0], dict):
|
||||||
|
try:
|
||||||
|
select = int(node[0].get("selectMapId") or 0) or None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
select = None
|
||||||
|
|
||||||
|
areas = []
|
||||||
|
for row in catalog():
|
||||||
|
mid = int(row.get("mapId") or 0)
|
||||||
|
cur = have.get(mid) or {}
|
||||||
|
end = int(row.get("endDistance") or 0)
|
||||||
|
dist = int(cur.get("distance") or 0)
|
||||||
|
area = dict(row)
|
||||||
|
area.update({
|
||||||
|
"current": dist,
|
||||||
|
"cleared": bool(cur.get("isClear") or cur.get("isComplete")),
|
||||||
|
"entered": mid in have,
|
||||||
|
"selected": mid == select,
|
||||||
|
"remain": max(0, end - dist) if end else None,
|
||||||
|
"percent": (round(dist * 100.0 / end, 1) if end else None),
|
||||||
|
})
|
||||||
|
areas.append(area)
|
||||||
|
return {"areas": areas, "selectMapId": select, "entered": len(have)}
|
||||||
|
|
||||||
|
|
||||||
|
def plan(map_id: int, mode: str = "fill", distance: int = 0,
|
||||||
|
current: int = 0, step: int = 0) -> list:
|
||||||
|
"""算出要分几段推,每段推多少。返回 [gain, gain, ...]。"""
|
||||||
|
area = find_area(map_id)
|
||||||
|
end = int(area.get("endDistance") or 0)
|
||||||
|
|
||||||
|
if mode == "fill":
|
||||||
|
total = max(0, end - int(current or 0))
|
||||||
|
else:
|
||||||
|
total = max(0, int(distance or 0))
|
||||||
|
if total <= 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
step = int(step or 0)
|
||||||
|
if step <= 0:
|
||||||
|
# 没指定就按「不超过终点的 1/8、也不超过 10 万」切,避免单包过猛
|
||||||
|
cap = max(STEP_DISTANCE, min(100000, (end or total) // 8 or STEP_DISTANCE))
|
||||||
|
step = max(STEP_DISTANCE, cap)
|
||||||
|
|
||||||
|
gains, left = [], total
|
||||||
|
while left > 0:
|
||||||
|
take = min(step, left)
|
||||||
|
gains.append(take)
|
||||||
|
left -= take
|
||||||
|
return gains
|
||||||
|
|
||||||
|
|
||||||
|
def run(state: dict, map_id: int, *, mode: str = "fill", distance: int = 0,
|
||||||
|
step: int = 0, dry_run: bool = False, cooldown: float = 0.0,
|
||||||
|
verify: bool = False) -> dict:
|
||||||
|
"""把指定区域推进到终点(mode=fill)或推指定距离(mode=delta)。"""
|
||||||
|
mid = int(map_id)
|
||||||
|
area = find_area(mid)
|
||||||
|
if not area:
|
||||||
|
raise RuntimeError("没有这个区域:%s" % mid)
|
||||||
|
|
||||||
|
user_id = int(state["user_id"])
|
||||||
|
probe = api.GetNeedUserData(user_id, token=state["token"], save=False)
|
||||||
|
cur = (progress(probe).get(mid) or {}).get("distance") or 0
|
||||||
|
gains = plan(mid, mode=mode, distance=distance, current=cur, step=step)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"mode": mode,
|
||||||
|
"mapId": mid,
|
||||||
|
"name": area.get("name"),
|
||||||
|
"endDistance": int(area.get("endDistance") or 0),
|
||||||
|
"before": cur,
|
||||||
|
"total": sum(gains),
|
||||||
|
"batches": len(gains),
|
||||||
|
"done": 0,
|
||||||
|
"failed": 0,
|
||||||
|
"dryRun": bool(dry_run),
|
||||||
|
"returnCode": None,
|
||||||
|
"errors": [],
|
||||||
|
"steps": [],
|
||||||
|
}
|
||||||
|
if not gains:
|
||||||
|
result["note"] = "已经是终点,不用推"
|
||||||
|
return result
|
||||||
|
|
||||||
|
for index, gain in enumerate(gains, 1):
|
||||||
|
one = _run_once(state, mid, gain, dry_run, cooldown, verify)
|
||||||
|
result["returnCode"] = one.get("returnCode")
|
||||||
|
if one.get("error"):
|
||||||
|
result["failed"] += gain
|
||||||
|
result["errors"].append("第 %d 段:%s" % (index, one["error"]))
|
||||||
|
break
|
||||||
|
result["done"] += gain
|
||||||
|
result["steps"].append({"seq": index, "gain": gain,
|
||||||
|
"after": one.get("after"),
|
||||||
|
"returnCode": one.get("returnCode")})
|
||||||
|
if dry_run:
|
||||||
|
break
|
||||||
|
result["after"] = result["before"] + result["done"]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _run_once(state: dict, map_id: int, gain: int,
|
||||||
|
dry_run: bool, cooldown: float, verify: bool = False) -> dict:
|
||||||
|
"""发一局,推进一小段。
|
||||||
|
|
||||||
|
★ verify=False(默认):发完不回头读。回读要再拉一次全量快照,
|
||||||
|
一段就多几秒,跑几十段时几乎全是等回读。进度靠本地累加,
|
||||||
|
真要看落地效果,跑完点一次「读进度」就行。
|
||||||
|
"""
|
||||||
|
user_id = int(state["user_id"])
|
||||||
|
session.fresh(state)
|
||||||
|
who = session.ids()
|
||||||
|
snapshot = api.GetNeedUserData(user_id, token=state["token"], save=False)
|
||||||
|
|
||||||
|
play_data, playlog = settle.prepare(state, snapshot)
|
||||||
|
play_data.map_distance_gain = int(gain)
|
||||||
|
play_data.select_map_id = int(map_id)
|
||||||
|
|
||||||
|
settle.ensure_play_window(state)
|
||||||
|
settle.upload(state, playlog)
|
||||||
|
|
||||||
|
patch = settle.patch(state, snapshot, play_data)
|
||||||
|
resp = api.UpsertUserAll(
|
||||||
|
user_id, state["login_id"],
|
||||||
|
login_date_time=state["login_date_time"],
|
||||||
|
auth_date_time=state["date_time"],
|
||||||
|
token=state["token"],
|
||||||
|
snapshot=snapshot,
|
||||||
|
play=play_data,
|
||||||
|
user_playlog_list=[playlog],
|
||||||
|
patch=patch,
|
||||||
|
verify_landing=bool(verify),
|
||||||
|
auto_characters=False,
|
||||||
|
place_id=who["place_id"], place_name=who["place_name"],
|
||||||
|
region_id=who["region_id"], region_name=who["region_name"],
|
||||||
|
client_id=who["client_id"],
|
||||||
|
cooldown_sec=float(cooldown),
|
||||||
|
dry_run=bool(dry_run),
|
||||||
|
confirm=not dry_run,
|
||||||
|
)
|
||||||
|
one = {
|
||||||
|
"returnCode": getattr(resp, "returnCode", None),
|
||||||
|
"apiName": getattr(resp, "apiName", ""),
|
||||||
|
}
|
||||||
|
if verify and not dry_run:
|
||||||
|
after = api.GetNeedUserData(user_id, token=state["token"], save=False)
|
||||||
|
one["after"] = (progress(after).get(int(map_id)) or {}).get("distance")
|
||||||
|
return one
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,19 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="icon icon-tabler icons-tabler-outline icon-tabler-route"
|
||||||
|
>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||||
|
<path d="M6 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M18 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||||
|
<path d="M7.5 17.5l6.5 -6.5" />
|
||||||
|
<path d="M9 6l2 2" />
|
||||||
|
<path d="M13 12l2 2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 521 B |
+211
-12
@@ -316,6 +316,7 @@
|
|||||||
.card[data-tab]{display:none;grid-column:1/-1}
|
.card[data-tab]{display:none;grid-column:1/-1}
|
||||||
body[data-tab="home"] .card[data-tab="home"],
|
body[data-tab="home"] .card[data-tab="home"],
|
||||||
body[data-tab="ticket"] .card[data-tab="ticket"],
|
body[data-tab="ticket"] .card[data-tab="ticket"],
|
||||||
|
body[data-tab="map"] .card[data-tab="map"],
|
||||||
body[data-tab="b50"] .card[data-tab="b50"],
|
body[data-tab="b50"] .card[data-tab="b50"],
|
||||||
body[data-tab="risk"] .card[data-tab="risk"],
|
body[data-tab="risk"] .card[data-tab="risk"],
|
||||||
body[data-tab="settings"] .card[data-tab="settings"]{display:block}
|
body[data-tab="settings"] .card[data-tab="settings"]{display:block}
|
||||||
@@ -556,6 +557,50 @@
|
|||||||
<div id="chargeList" class="picked"></div>
|
<div id="chargeList" class="picked"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card" data-tab="map">
|
||||||
|
<h2>一键跑图</h2>
|
||||||
|
<p class="desc">推进区域地图进度。走 UpsertUserAll 的 userMapList 差分 ——
|
||||||
|
只回传 distance 变化的那一条。</p>
|
||||||
|
<div class="row">
|
||||||
|
<div style="flex:1 1 100%">
|
||||||
|
<label>搜区域(名字或 mapId)</label>
|
||||||
|
<input id="mapSearch" placeholder="比如 Phigros / Arcaea / 550060" oninput="debouncedRenderAreas()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div style="flex:1 1 100%">
|
||||||
|
<label id="mapAreaLabel">选区域</label>
|
||||||
|
<select id="mapArea" onchange="onPickArea()">
|
||||||
|
<option value="">— 点「读进度」载入区域表 —</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="mapInfo" class="picked"></div>
|
||||||
|
<div class="row">
|
||||||
|
<div style="flex:1 1 100%"><label>推进方式</label>
|
||||||
|
<select id="mapMode" onchange="onPickArea();syncMapInputs()">
|
||||||
|
<option value="fill" selected>推到终点(自动算还差多少)</option>
|
||||||
|
<option value="delta">推指定距离(自己填)</option>
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div style="flex:1 1 100%"><label id="mapDistanceLabel">距离(推指定距离时才用)</label>
|
||||||
|
<input id="mapDistance" type="number" value="50000" min="1000" step="1000"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div style="flex:1 1 100%"><label>每段距离(0=自动)</label>
|
||||||
|
<input id="mapStep" type="number" value="0" min="0" step="11000"></div>
|
||||||
|
</div>
|
||||||
|
<div class="check">
|
||||||
|
<label><input id="mapDry" type="checkbox" checked> 只预检不发包</label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button onclick="doRunmap()">开始跑图</button>
|
||||||
|
<button class="ghost" onclick="loadRunmap()">读进度</button>
|
||||||
|
</div>
|
||||||
|
<div id="mapOut" class="picked"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card" data-tab="risk" style="border:0;background:transparent;padding:0">
|
<section class="card" data-tab="risk" style="border:0;background:transparent;padding:0">
|
||||||
<div class="warncard">
|
<div class="warncard">
|
||||||
<b>⚠️ 高风险区</b>
|
<b>⚠️ 高风险区</b>
|
||||||
@@ -724,6 +769,8 @@
|
|||||||
<input id="dimRange" type="range" min="0" max="70" step="1" value="12"
|
<input id="dimRange" type="range" min="0" max="70" step="1" value="12"
|
||||||
oninput="onLookInput('dim', this.value)">
|
oninput="onLookInput('dim', this.value)">
|
||||||
<div class="check"><label><input id="rainOn" type="checkbox" onchange="toggleRain(this.checked)"> 落雨效果(「漏的是给」往下飘)</label></div>
|
<div class="check"><label><input id="rainOn" type="checkbox" onchange="toggleRain(this.checked)"> 落雨效果(「漏的是给」往下飘)</label></div>
|
||||||
|
<div class="check"><label><input id="giveImgOn" type="checkbox" onchange="toggleGive('img', this.checked)"> 发包成功弹「给」图</label></div>
|
||||||
|
<div class="check"><label><input id="giveSndOn" type="checkbox" onchange="toggleGive('snd', this.checked)"> 发包成功播「给」音效</label></div>
|
||||||
<button class="ghost" onclick="resetLook()">恢复默认外观</button>
|
<button class="ghost" onclick="resetLook()">恢复默认外观</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -774,6 +821,7 @@
|
|||||||
<nav id="tabs">
|
<nav id="tabs">
|
||||||
<button data-go="home" class="on" onclick="go('home')"><i class="ico" style="--m:url(assets/icons/home.svg)"></i>主页</button>
|
<button data-go="home" class="on" onclick="go('home')"><i class="ico" style="--m:url(assets/icons/home.svg)"></i>主页</button>
|
||||||
<button data-go="ticket" onclick="go('ticket')"><i class="ico" style="--m:url(assets/icons/ticket.svg)"></i>票据</button>
|
<button data-go="ticket" onclick="go('ticket')"><i class="ico" style="--m:url(assets/icons/ticket.svg)"></i>票据</button>
|
||||||
|
<button data-go="map" onclick="go('map')"><i class="ico" style="--m:url(assets/icons/map.svg)"></i>跑图</button>
|
||||||
<button data-go="b50" onclick="go('b50')"><i class="ico" style="--m:url(assets/icons/b50.svg)"></i>B50</button>
|
<button data-go="b50" onclick="go('b50')"><i class="ico" style="--m:url(assets/icons/b50.svg)"></i>B50</button>
|
||||||
<button data-go="risk" onclick="go('risk')"><i class="ico" style="--m:url(assets/icons/risk.svg)"></i>风险</button>
|
<button data-go="risk" onclick="go('risk')"><i class="ico" style="--m:url(assets/icons/risk.svg)"></i>风险</button>
|
||||||
<button data-go="settings" onclick="go('settings')"><i class="ico" style="--m:url(assets/icons/settings.svg)"></i>设置</button>
|
<button data-go="settings" onclick="go('settings')"><i class="ico" style="--m:url(assets/icons/settings.svg)"></i>设置</button>
|
||||||
@@ -836,7 +884,7 @@ async function post(url, body){
|
|||||||
async function get(url){ return await (await fetch(url)).json(); }
|
async function get(url){ return await (await fetch(url)).json(); }
|
||||||
|
|
||||||
/* ---------- 移动端外壳:分页 / 轻提示 ---------- */
|
/* ---------- 移动端外壳:分页 / 轻提示 ---------- */
|
||||||
const TABS = ['home', 'ticket', 'b50', 'risk', 'settings'];
|
const TABS = ['home', 'ticket', 'map', 'b50', 'risk', 'settings'];
|
||||||
let tickerTimer = null;
|
let tickerTimer = null;
|
||||||
function ticker(msg, cls){
|
function ticker(msg, cls){
|
||||||
const el = $('ticker');
|
const el = $('ticker');
|
||||||
@@ -855,6 +903,7 @@ function go(tab){
|
|||||||
try { history.replaceState(null, '', '#' + tab); } catch(e) {}
|
try { history.replaceState(null, '', '#' + tab); } catch(e) {}
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
if(tab === 'home') loadProfile();
|
if(tab === 'home') loadProfile();
|
||||||
|
if(tab === 'map') syncMapInputs();
|
||||||
if(tab === 'settings') syncSettings();
|
if(tab === 'settings') syncSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1149,6 +1198,9 @@ function applyLook(o){
|
|||||||
// 落雨:默认开;只有显式存了 rain:false 才关(彩虹标题不在这个开关里)
|
// 落雨:默认开;只有显式存了 rain:false 才关(彩虹标题不在这个开关里)
|
||||||
applyRain(o.rain !== false);
|
applyRain(o.rain !== false);
|
||||||
if($('rainOn')) $('rainOn').checked = (o.rain !== false);
|
if($('rainOn')) $('rainOn').checked = (o.rain !== false);
|
||||||
|
// 「给」的图与音效:默认都开,显式 false 才关
|
||||||
|
if($('giveImgOn')) $('giveImgOn').checked = (o.giveImg !== false);
|
||||||
|
if($('giveSndOn')) $('giveSndOn').checked = (o.giveSnd !== false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const RAIN_CHARS = ['漏', '的', '是', '给'];
|
const RAIN_CHARS = ['漏', '的', '是', '给'];
|
||||||
@@ -1185,6 +1237,12 @@ function toggleRain(on){
|
|||||||
applyLook(o);
|
applyLook(o);
|
||||||
ticker(on ? '落雨开了' : '落雨关了', 'ok');
|
ticker(on ? '落雨开了' : '落雨关了', 'ok');
|
||||||
}
|
}
|
||||||
|
function toggleGive(kind, on){
|
||||||
|
const o = readLook();
|
||||||
|
if(kind === 'img') o.giveImg = !!on; else o.giveSnd = !!on;
|
||||||
|
saveLook(o);
|
||||||
|
ticker((kind === 'img' ? '弹图' : '音效') + (on ? '开了' : '关了'), 'ok');
|
||||||
|
}
|
||||||
function onLookInput(field, value){
|
function onLookInput(field, value){
|
||||||
const o = readLook();
|
const o = readLook();
|
||||||
const v = parseInt(value, 10) || 0;
|
const v = parseInt(value, 10) || 0;
|
||||||
@@ -1564,20 +1622,27 @@ function endWait(){
|
|||||||
$('wbar').style.width = '0%';
|
$('wbar').style.width = '0%';
|
||||||
$('wnum').textContent = '';
|
$('wnum').textContent = '';
|
||||||
}
|
}
|
||||||
/* 发包成功:弹梗图 + 喊一声。图片和音效都在 html/assets 下 */
|
/* 发包成功:弹梗图 + 喊一声。图片和音效都在 html/assets 下,两个都能在设置里单独关 */
|
||||||
let giveAudio = null;
|
let giveAudio = null;
|
||||||
function showGive(){
|
function showGive(){
|
||||||
|
const o = readLook();
|
||||||
|
const wantImg = (o.giveImg !== false);
|
||||||
|
const wantSnd = (o.giveSnd !== false);
|
||||||
|
if(!wantImg && !wantSnd) return;
|
||||||
const box = $('giveBox');
|
const box = $('giveBox');
|
||||||
if(!box) return;
|
if(wantImg && box){
|
||||||
box.classList.add('on');
|
box.classList.add('on');
|
||||||
try {
|
clearTimeout(showGive._t);
|
||||||
if(!giveAudio){ giveAudio = new Audio('/assets/give.mp3'); giveAudio.volume = 1; }
|
showGive._t = setTimeout(() => box.classList.remove('on'), 1600);
|
||||||
giveAudio.currentTime = 0;
|
}
|
||||||
const pr = giveAudio.play();
|
if(wantSnd){
|
||||||
if(pr && pr.catch) pr.catch(() => {}); // 自动播放被拦就算了,图照弹
|
try {
|
||||||
} catch(e){}
|
if(!giveAudio){ giveAudio = new Audio('/assets/give.mp3'); giveAudio.volume = 1; }
|
||||||
clearTimeout(showGive._t);
|
giveAudio.currentTime = 0;
|
||||||
showGive._t = setTimeout(() => box.classList.remove('on'), 1600);
|
const pr = giveAudio.play();
|
||||||
|
if(pr && pr.catch) pr.catch(() => {}); // 自动播放被拦就算了,图照弹
|
||||||
|
} catch(e){}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function postLong(url, body, title, sub, seconds){
|
async function postLong(url, body, title, sub, seconds){
|
||||||
@@ -2066,6 +2131,140 @@ async function doBuyTicket(){
|
|||||||
if(!dry) loadCharge();
|
if(!dry) loadCharge();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- 跑图 ---------- */
|
||||||
|
let RUNMAP = null;
|
||||||
|
async function loadRunmap(){
|
||||||
|
const lab0 = $('mapAreaLabel');
|
||||||
|
if(lab0) lab0.textContent = '选区域(正在读取进度…)';
|
||||||
|
log('正在读进度,要拉一次完整快照,稍等…');
|
||||||
|
const r = await get('/api/runmap');
|
||||||
|
if(!r.ok){ log('读区域表失败:'+r.error, 'err'); if(lab0) lab0.textContent = '选区域(读取失败)'; return; }
|
||||||
|
RUNMAP = r;
|
||||||
|
renderAreas();
|
||||||
|
const areas = r.areas || [];
|
||||||
|
log(`区域表已载入:${areas.length} 个区域,已进过 ${r.entered} 个`, 'ok');
|
||||||
|
if(r.selectMapId){
|
||||||
|
const cur = areas.find(a => a.mapId === r.selectMapId);
|
||||||
|
$('mapInfo').textContent = `机台当前选中:${cur ? cur.name : r.selectMapId}(${r.selectMapId})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function renderAreas(){
|
||||||
|
if(!RUNMAP) return;
|
||||||
|
const kw = ($('mapSearch').value || '').trim().toLowerCase();
|
||||||
|
const all = RUNMAP.areas || [];
|
||||||
|
const areas = all.filter(a => {
|
||||||
|
if(!kw) return true;
|
||||||
|
if(String(a.mapId).indexOf(kw) >= 0) return true;
|
||||||
|
if((a.name || '').toLowerCase().indexOf(kw) >= 0) return true;
|
||||||
|
if((a.island || '').toLowerCase().indexOf(kw) >= 0) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
const keep = $('mapArea').value;
|
||||||
|
const byVer = {};
|
||||||
|
areas.forEach(a => { const v = a.version || '其他'; (byVer[v] = byVer[v] || []).push(a); });
|
||||||
|
let html = '<option value="">— 选一个区域 —</option>';
|
||||||
|
Object.keys(byVer).sort().forEach(v => {
|
||||||
|
html += `<optgroup label="${v} · ${byVer[v].length} 个">`;
|
||||||
|
byVer[v].forEach(a => {
|
||||||
|
let mark;
|
||||||
|
if(a.cleared){
|
||||||
|
mark = '✅ 已通关 ';
|
||||||
|
} else if((a.current || 0) > 0){
|
||||||
|
mark = '▶ 进行中 ';
|
||||||
|
} else {
|
||||||
|
mark = ' '; // 全角空格占位,让未开始的排在后面视觉上更整齐
|
||||||
|
}
|
||||||
|
const tag = a.collabo ? '〔联动〕' : '';
|
||||||
|
html += `<option value="${a.mapId}">${mark}${a.name}${tag} · ${a.mapId}</option>`;
|
||||||
|
});
|
||||||
|
html += '</optgroup>';
|
||||||
|
});
|
||||||
|
$('mapArea').innerHTML = html;
|
||||||
|
if(keep && areas.some(a => String(a.mapId) === keep)) $('mapArea').value = keep;
|
||||||
|
// ★ select 被 nsUpgrade 包装成了自定义控件,改完 options 必须同步一次显示层
|
||||||
|
nsSync('mapArea');
|
||||||
|
const lab = $('mapAreaLabel');
|
||||||
|
if(lab) lab.textContent = kw ? `选区域(筛出 ${areas.length} 个)` : `选区域(共 ${areas.length} 个)`;
|
||||||
|
if(kw && areas.length === 1){
|
||||||
|
$('mapArea').value = String(areas[0].mapId);
|
||||||
|
nsSync('mapArea');
|
||||||
|
onPickArea();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _mapTimer = null;
|
||||||
|
function debouncedRenderAreas(){
|
||||||
|
clearTimeout(_mapTimer);
|
||||||
|
_mapTimer = setTimeout(renderAreas, 220);
|
||||||
|
}
|
||||||
|
function currentArea(){
|
||||||
|
const id = parseInt($('mapArea').value, 10);
|
||||||
|
if(!id || !RUNMAP) return null;
|
||||||
|
return (RUNMAP.areas || []).find(a => a.mapId === id) || null;
|
||||||
|
}
|
||||||
|
function syncMapInputs(){
|
||||||
|
const fill = $('mapMode').value === 'fill';
|
||||||
|
const dist = $('mapDistance');
|
||||||
|
const lab = $('mapDistanceLabel');
|
||||||
|
if(dist){
|
||||||
|
dist.disabled = fill;
|
||||||
|
dist.style.opacity = fill ? '.45' : '1';
|
||||||
|
dist.style.background = fill ? '#f2f4f7' : '';
|
||||||
|
}
|
||||||
|
if(lab) lab.textContent = fill ? '距离(推到终点时不用填)' : '距离(推指定距离时必填)';
|
||||||
|
}
|
||||||
|
function onPickArea(){
|
||||||
|
syncMapInputs();
|
||||||
|
const a = currentArea();
|
||||||
|
if(!a){ $('mapInfo').textContent = ''; return; }
|
||||||
|
const end = a.endDistance || 0, cur = a.current || 0;
|
||||||
|
let txt = `${a.name} · mapId ${a.mapId} · ${a.version}`;
|
||||||
|
if(a.collabo) txt += ' · 联动';
|
||||||
|
if(a.infinity) txt += ' · 无限区域';
|
||||||
|
if(a.cleared) txt += '\n✅ 这个区域已经通关了';
|
||||||
|
if(end){
|
||||||
|
txt += `\n终点 ${end.toLocaleString()} · 当前 ${cur.toLocaleString()} · ${a.percent}%`;
|
||||||
|
if(a.remain != null) txt += ` · 还差 ${a.remain.toLocaleString()}`;
|
||||||
|
} else {
|
||||||
|
txt += `\n无终点(无限区域)· 当前 ${cur.toLocaleString()}`;
|
||||||
|
}
|
||||||
|
if(a.selected) txt += '\n★ 机台当前选中的就是它';
|
||||||
|
$('mapInfo').textContent = txt;
|
||||||
|
if(!end && $('mapMode').value === 'fill'){
|
||||||
|
$('mapMode').value = 'delta';
|
||||||
|
log('这个区域没有终点,已切到「推指定距离」', 'warn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function doRunmap(){
|
||||||
|
const a = currentArea();
|
||||||
|
if(!a) return log('先选区域', 'err');
|
||||||
|
const mode = $('mapMode').value;
|
||||||
|
const distance = parseInt($('mapDistance').value || '0', 10);
|
||||||
|
const step = parseInt($('mapStep').value || '0', 10);
|
||||||
|
const dry = $('mapDry').checked;
|
||||||
|
const end = a.endDistance || 0, cur = a.current || 0;
|
||||||
|
if(mode === 'fill' && !end) return log('这个区域没有终点,改用「推指定距离」', 'err');
|
||||||
|
if(mode === 'fill' && end && cur >= end) return log('已经是终点,不用跑', 'warn');
|
||||||
|
if(mode === 'delta' && distance <= 0) return log('distance 要大于 0', 'err');
|
||||||
|
const total = mode === 'fill' ? (end - cur) : distance;
|
||||||
|
const seg = step > 0 ? step : Math.max(11000, Math.min(100000, Math.round(total / 8)));
|
||||||
|
const batches = Math.ceil(total / seg);
|
||||||
|
log(`跑图 ${a.name}(${a.mapId})· 推 ${total.toLocaleString()} · 分 ${batches} 段${dry?'(预检)':''}`);
|
||||||
|
if(!dry && batches > 1) log(`要换 ${batches} 局,每局至少 60 秒,别关页面`, 'warn');
|
||||||
|
const r = await postLong('/api/runmap',
|
||||||
|
{mapId:a.mapId, mode, distance, step, dryRun:dry},
|
||||||
|
'正在跑图', `推进 ${total.toLocaleString()},分 ${batches} 段。`, Math.max(60, batches * 9));
|
||||||
|
if(!r.ok){ log('失败:'+r.error, 'err'); if(r.trace) log(r.trace,'dim'); return; }
|
||||||
|
if(r.note){ log(r.note, 'warn'); return; }
|
||||||
|
log(`回码 ${r.returnCode} · ${(r.before||0).toLocaleString()} → ${(r.after||0).toLocaleString()}`
|
||||||
|
+ `(+${(r.done||0).toLocaleString()})· ${r.batches} 段`,
|
||||||
|
r.failed ? 'warn' : 'ok');
|
||||||
|
(r.errors || []).forEach(e => log(e, 'warn'));
|
||||||
|
if(!dry && r.steps && r.steps.length){
|
||||||
|
log(r.steps.map(s => `#${s.seq} +${s.gain} → ${s.after == null ? '?' : s.after}`).join(' '), 'dim');
|
||||||
|
}
|
||||||
|
if(!dry) loadRunmap();
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- 道具 ---------- */
|
/* ---------- 道具 ---------- */
|
||||||
const WAIT_SUB = '服务端要求一局至少 60 秒,每次发包都要等满这一分钟,别关页面。';
|
const WAIT_SUB = '服务端要求一局至少 60 秒,每次发包都要等满这一分钟,别关页面。';
|
||||||
async function doGrantPicked(kind, target){
|
async function doGrantPicked(kind, target){
|
||||||
|
|||||||
Reference in new issue
Block a user