update renderer and web viewer to correctly cut map and show caps (#1261)

This commit is contained in:
NotSoToothless
2026-05-19 15:37:19 -07:00
committed by GitHub
parent 899dfbb9e5
commit 2c9e89eee2
3 changed files with 1448 additions and 236 deletions
+890 -156
View File
File diff suppressed because it is too large Load Diff
+399
View File
@@ -0,0 +1,399 @@
#!/usr/bin/env python3
"""
Build map/mode info JSON for tank missions with planes + capture zones.
Output shape:
{
"<map_name>": {
"<mode>": [
{
"info": { ... }
}
]
}
}
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
BOTS_DIR = SCRIPT_DIR.parent.parent
DATAMINE_LEVELS_DIR = BOTS_DIR / "War-Thunder-Datamine" / "aces.vromfs.bin_u" / "levels"
DATAMINE_MISSIONS_DIR = BOTS_DIR / "War-Thunder-Datamine" / "mis.vromfs.bin_u"
MISSION_ROOT = DATAMINE_MISSIONS_DIR / "gamedata" / "missions" / "cta" / "tanks"
SHARED_LEVELS_DIR = BOTS_DIR / "SHARED" / "MAPS" / "LEVELS"
SHARED_MISSIONS_DIR = SHARED_LEVELS_DIR / "MISSIONS"
SKIP_PARTS = {"mainareas", "battleareas", "spawnareas"}
def _clean_map_key(raw: str) -> str:
key = Path(raw).name.strip().rstrip("*")
if key.endswith(".png"):
key = key[:-4]
return key
def _normalize_rel(path: str) -> str:
rel = str(path or "").strip().replace("\\", "/").lstrip("/")
if rel[:9].lower() == "gamedata/":
rel = "gamedata/" + rel[9:]
return rel.lower()
def _load_json(path: Path, cache: dict[Path, Any]) -> Any:
if path in cache:
return cache[path]
try:
cache[path] = json.loads(path.read_text())
except Exception:
cache[path] = None
return cache[path]
def _repo_rel(path: Path) -> str:
try:
return str(path.resolve().relative_to(BOTS_DIR.resolve())).replace("\\", "/")
except Exception:
return str(path)
def _datamine_level_path(level_path: str) -> str:
rel = str(level_path or "").replace("\\", "/").lstrip("/")
if rel.lower().startswith("levels/"):
return f"War-Thunder-Datamine/aces.vromfs.bin_u/{rel}"
return f"War-Thunder-Datamine/{rel}" if rel else rel
def _resolve_mission_file(rel: str) -> Path | None:
norm = _normalize_rel(rel)
if not norm:
return None
for root in (SHARED_MISSIONS_DIR, DATAMINE_MISSIONS_DIR):
raw = root / norm
if raw.suffix.lower() == ".blk":
cands = [raw.with_suffix(".blkx"), raw]
elif raw.suffix.lower() == ".blkx":
cands = [raw, raw.with_suffix(".blk")]
else:
cands = [raw, raw.with_suffix(".blkx"), raw.with_suffix(".blk")]
for p in cands:
if p.exists():
return p
return None
def _import_paths(mission_def: dict) -> list[str]:
imports = mission_def.get("imports")
if not isinstance(imports, dict):
return []
import_record = imports.get("import_record")
records = import_record if isinstance(import_record, list) else [import_record]
out: list[str] = []
for rec in records:
if not isinstance(rec, dict):
continue
f = rec.get("file")
if isinstance(f, str) and f.strip():
out.append(f)
return out
def _collect_defs(root_file: Path, cache: dict[Path, Any]) -> tuple[dict[str, dict], list[str], list[str]]:
root_def = _load_json(root_file, cache)
if not isinstance(root_def, dict):
return {}, [], []
areas: dict[str, dict] = {}
files_used: list[str] = []
imports_used: list[str] = []
queue: list[Path] = [root_file]
seen: set[Path] = set()
while queue:
cur_file = queue.pop(0).resolve()
if cur_file in seen:
continue
seen.add(cur_file)
files_used.append(str(cur_file))
cur_def = _load_json(cur_file, cache)
if not isinstance(cur_def, dict):
continue
cur_areas = cur_def.get("areas")
if isinstance(cur_areas, dict):
for name, area in cur_areas.items():
if isinstance(name, str) and isinstance(area, dict) and name not in areas:
areas[name] = area
for rel in _import_paths(cur_def):
imports_used.append(_normalize_rel(rel))
p = _resolve_mission_file(rel)
if p is not None:
queue.append(p)
return areas, files_used, sorted(set(imports_used))
def _radius_from_tm(tm: list) -> float:
try:
ax = float(tm[0][0])
az = float(tm[0][2])
bx = float(tm[2][0])
bz = float(tm[2][2])
ra = (ax * ax + az * az) ** 0.5
rb = (bx * bx + bz * bz) ** 0.5
return max(ra, rb)
except Exception:
return 0.0
def _capture_prefixes(mission_type: str, mode_key: str) -> list[str]:
m = (mission_type or "").lower()
k = (mode_key or "").lower()
if "conq" in m or "conq" in k:
return ["conq_capture_area_"]
if "dom" in m or "dom" in k:
return ["dom_capture_area_"]
if "bttl" in m or "bttl" in k:
return ["bttl_t1_capture_area_", "bttl_t2_capture_area_"]
return ["capture_area_"]
def _capture_sort(name: str) -> tuple[int, str]:
digits = "".join(ch for ch in name if ch.isdigit())
return (int(digits) if digits else 999, name)
def _pick_capture_areas(areas: dict[str, dict], mission_type: str, mode_key: str) -> list[dict]:
prefixes = _capture_prefixes(mission_type, mode_key)
names: list[str] = []
for name, area in areas.items():
if not isinstance(area, dict):
continue
lname = name.lower()
if "capture_area" not in lname or lname.startswith("briefing_"):
continue
if not any(lname.startswith(p) for p in prefixes):
continue
names.append(name)
if not names:
return []
suffix_prio = ["_arcade", "_realistic", "_hardcore", "_simulator", ""]
groups: dict[str, dict[str, str]] = {}
for name in names:
lname = name.lower()
suffix = ""
base = lname
for sfx in suffix_prio[:-1]:
if lname.endswith(sfx):
suffix = sfx
base = lname[:-len(sfx)]
break
groups.setdefault(base, {})[suffix] = name
chosen: list[str] = []
for base in sorted(groups.keys(), key=_capture_sort):
variants = groups[base]
for sfx in suffix_prio:
if sfx in variants:
chosen.append(variants[sfx])
break
out: list[dict] = []
for name in chosen:
area = areas.get(name)
if not isinstance(area, dict):
continue
tm = area.get("tm")
if not isinstance(tm, list) or len(tm) < 4 or not isinstance(tm[3], list):
continue
try:
cx = float(tm[3][0])
cz = float(tm[3][2])
except Exception:
continue
out.append({
"name": name,
"type": str(area.get("type", "")),
"x": cx,
"z": cz,
"radius": _radius_from_tm(tm),
})
return out
def _battle_areas(areas: dict[str, dict]) -> list[dict]:
out: list[dict] = []
for name, area in areas.items():
if not isinstance(area, dict):
continue
lname = name.lower()
if "battle_area" not in lname or lname.startswith("briefing_"):
continue
tm = area.get("tm")
if not isinstance(tm, list) or len(tm) < 4 or not isinstance(tm[3], list):
continue
try:
cx = float(tm[3][0])
cz = float(tm[3][2])
except Exception:
continue
out.append({
"name": name,
"type": str(area.get("type", "")),
"x": cx,
"z": cz,
"radius": _radius_from_tm(tm),
})
out.sort(key=lambda x: x["name"])
return out
def _level_info(level_stem: str, cache: dict[Path, Any]) -> dict:
cands = [
DATAMINE_LEVELS_DIR / f"{level_stem}.blkx",
SHARED_LEVELS_DIR / f"{level_stem}.blkx",
SHARED_LEVELS_DIR / "DATAMINE" / f"{level_stem}.blkx",
]
for p in cands:
if not p.exists():
continue
data = _load_json(p, cache)
if not isinstance(data, dict):
continue
return {
"file": _repo_rel(p),
"tankMapCoord0": data.get("tankMapCoord0"),
"tankMapCoord1": data.get("tankMapCoord1"),
"aiTanksMapCoord0": data.get("aiTanksMapCoord0"),
"aiTanksMapCoord1": data.get("aiTanksMapCoord1"),
"customLevelTankMap": data.get("customLevelTankMap"),
"customLevelMap": data.get("customLevelMap"),
}
return {}
def _has_planes(allowed: dict | None, imports_used: list[str]) -> bool:
if isinstance(allowed, dict):
val = allowed.get("isAirplanesAllowed")
if isinstance(val, bool):
return val
return any("air_spawns" in p for p in imports_used)
def _has_tanks(allowed: dict | None) -> bool:
if isinstance(allowed, dict):
val = allowed.get("isTanksAllowed")
if isinstance(val, bool):
return val
return True
def _mode_key(mission_cfg: dict, mission_file: Path) -> str:
postfix = mission_cfg.get("postfix")
if isinstance(postfix, str) and postfix.strip():
return postfix.lstrip("_")
mtype = mission_cfg.get("type")
if isinstance(mtype, str) and mtype.strip():
return mtype
return mission_file.stem
def should_skip_mission_file(path: Path) -> bool:
if path.name.startswith("template_"):
return True
parts = {p.lower() for p in path.parts}
if SKIP_PARTS & parts:
return True
return False
def build_index() -> dict[str, dict[str, list[dict[str, Any]]]]:
cache: dict[Path, Any] = {}
output: dict[str, dict[str, list[dict[str, Any]]]] = {}
for mission_file in sorted(MISSION_ROOT.rglob("*.blkx")):
if should_skip_mission_file(mission_file):
continue
mission_def = _load_json(mission_file, cache)
if not isinstance(mission_def, dict):
continue
mission_settings = mission_def.get("mission_settings")
if not isinstance(mission_settings, dict):
continue
mission_cfg = mission_settings.get("mission")
if not isinstance(mission_cfg, dict):
continue
level_path = mission_cfg.get("level", "")
if not isinstance(level_path, str) or not level_path.strip():
continue
level_stem = Path(level_path).stem
mode = _mode_key(mission_cfg, mission_file)
mission_type = str(mission_cfg.get("type", ""))
allowed = mission_cfg.get("allowedUnitTypes")
areas, files_used, imports_used = _collect_defs(mission_file, cache)
captures = _pick_capture_areas(areas, mission_type, mode)
if not captures:
continue
has_tanks = _has_tanks(allowed if isinstance(allowed, dict) else None)
has_planes = _has_planes(allowed if isinstance(allowed, dict) else None, imports_used)
if not (has_tanks and has_planes):
continue
battle_areas = _battle_areas(areas)
info = {
"mission_file": _repo_rel(mission_file),
"level_path": _datamine_level_path(level_path),
"mission_type": mission_type,
"mode_postfix": mission_cfg.get("postfix", ""),
"allowed_unit_types": allowed if isinstance(allowed, dict) else {},
"useAlternativeMapCoord": mission_cfg.get("useAlternativeMapCoord"),
"capture_areas": captures,
"battle_areas": battle_areas,
"level_def": _level_info(level_stem, cache),
"imports_count": len(imports_used),
"files_used_count": len(files_used),
}
output.setdefault(level_stem, {}).setdefault(mode, []).append({"info": info})
return output
def main() -> None:
parser = argparse.ArgumentParser(description="Build map_name -> mode -> [info] JSON")
parser.add_argument(
"--out",
type=Path,
default=SCRIPT_DIR.parent / "map_mode_info.json",
help="Output JSON path",
)
args = parser.parse_args()
data = build_index()
args.out.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
maps = len(data)
modes = sum(len(v) for v in data.values())
entries = sum(len(items) for v in data.values() for items in v.values())
print(f"Wrote {args.out} (maps={maps}, modes={modes}, entries={entries})")
if __name__ == "__main__":
main()
+149 -70
View File
@@ -12,6 +12,9 @@ const RC = {
WIN_TRAIL: 'rgba(0,120,0,', LOSE_TRAIL: 'rgba(132,18,18,', WIN_TRAIL: 'rgba(0,120,0,', LOSE_TRAIL: 'rgba(132,18,18,',
DOT_R: 5, AIR_R: 4, DRONE_R: 3, DOT_R: 5, AIR_R: 4, DRONE_R: 3,
}; };
const RC_CAP_STROKE_PX = 3;
const RC_CAP_ICON_ALPHA = 0.35;
const RC_CAP_ICON_MIN_SIZE = 10;
function replayT(key) { function replayT(key) {
return (window.__t && window.__t(key)) || key; return (window.__t && window.__t(key)) || key;
@@ -39,6 +42,7 @@ class ReplayCanvas {
this._groundCoords = data.levelCoords; this._groundCoords = data.levelCoords;
this._airCoords = data.mapCoords || null; this._airCoords = data.mapCoords || null;
this._fullMapLevel = data.fullMapLevel || null; this._fullMapLevel = data.fullMapLevel || null;
this.captureAreas = Array.isArray(data.captureAreas) ? data.captureAreas : [];
this._mode = 'ground'; this._mode = 'ground';
const hasAircraft = data.entities.some(e => e.type === 'aircraft'); const hasAircraft = data.entities.some(e => e.type === 'aircraft');
this.hasAirMode = !!(this._airCoords && this._fullMapLevel && hasAircraft); this.hasAirMode = !!(this._airCoords && this._fullMapLevel && hasAircraft);
@@ -47,7 +51,7 @@ class ReplayCanvas {
this.z0 = data.levelCoords.z0; this.z0 = data.levelCoords.z0;
this.xRange = data.levelCoords.x1 - data.levelCoords.x0; this.xRange = data.levelCoords.x1 - data.levelCoords.x0;
this.zRange = data.levelCoords.z1 - data.levelCoords.z0; this.zRange = data.levelCoords.z1 - data.levelCoords.z0;
// Default map source rect (full image) — overwritten by _computeAutocrop // Source rect for minimap image (full image by default)
this._mapSrc = { u: 0, v: 0, w: 1, h: 1 }; this._mapSrc = { u: 0, v: 0, w: 1, h: 1 };
this.players = {}; this.players = {};
@@ -74,9 +78,6 @@ class ReplayCanvas {
}); });
} }
// Zoom into the area players actually use (like the video maker autocrop)
this._computeAutocrop();
// Pre-compute deaths // Pre-compute deaths
this._computeDeaths(); this._computeDeaths();
this.currentTime = this.tStart; this.currentTime = this.tStart;
@@ -110,70 +111,6 @@ class ReplayCanvas {
]; ];
} }
/** Recompute x0/z0/xRange/zRange to zoom into entity activity.
* In 'ground' mode crops to ground entities, in 'air' mode crops to aircraft+drones. */
_computeAutocrop() {
const origX0 = this.x0, origZ0 = this.z0;
const origXR = this.xRange, origZR = this.zRange;
const airMode = this._mode === 'air';
let minX = Infinity, maxX = -Infinity;
let minZ = Infinity, maxZ = -Infinity;
for (const ent of this.entities) {
if (airMode) {
if (ent.type !== 'aircraft' && ent.type !== 'drone') continue;
} else {
if (ent.type !== 'ground') continue;
}
const { positions } = ent;
for (let i = 0; i < positions.length; i += 2) {
const x = positions[i], z = positions[i + 1];
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (z < minZ) minZ = z;
if (z > maxZ) maxZ = z;
}
}
if (!isFinite(minX)) return; // no relevant positions — keep full map
// Padding: 10% of span or 50 world units, whichever is larger
const span = Math.max(maxX - minX, maxZ - minZ);
const pad = Math.max(50, span * 0.10);
minX -= pad; maxX += pad;
minZ -= pad; maxZ += pad;
// Expand to square, with a minimum of 15% of the map range
const minSide = Math.max(origXR, origZR) * 0.15;
let side = Math.max(maxX - minX, maxZ - minZ, minSide);
const midX = (minX + maxX) / 2, midZ = (minZ + maxZ) / 2;
minX = midX - side / 2; maxX = midX + side / 2;
minZ = midZ - side / 2; maxZ = midZ + side / 2;
// Clamp to LevelDef bounds (shift first, then hard-clamp)
const x1 = origX0 + origXR, z1 = origZ0 + origZR;
if (minX < origX0) { maxX += origX0 - minX; minX = origX0; }
if (maxX > x1) { minX -= maxX - x1; maxX = x1; }
if (minZ < origZ0) { maxZ += origZ0 - minZ; minZ = origZ0; }
if (maxZ > z1) { minZ -= maxZ - z1; maxZ = z1; }
minX = Math.max(minX, origX0); maxX = Math.min(maxX, x1);
minZ = Math.max(minZ, origZ0); maxZ = Math.min(maxZ, z1);
// Fractional source rect for the minimap image
// Image top-left = world (origX0, origZ0+origZR), bottom-right = (origX0+origXR, origZ0)
this._mapSrc = {
u: (minX - origX0) / origXR,
v: (z1 - maxZ) / origZR,
w: (maxX - minX) / origXR,
h: (maxZ - minZ) / origZR,
};
// Apply new bounds — worldToPixel will now map this sub-region to the full canvas
this.x0 = minX;
this.z0 = minZ;
this.xRange = maxX - minX;
this.zRange = maxZ - minZ;
}
getPositionAtTime(entity, time) { getPositionAtTime(entity, time) {
const { times, positions } = entity; const { times, positions } = entity;
if (time < times[0] || time > times[times.length - 1]) return null; if (time < times[0] || time > times[times.length - 1]) return null;
@@ -533,6 +470,140 @@ class ReplayCanvas {
ctx.drawImage(img, x - size / 2 + dx, y - size / 2 + dy, dw, dh); ctx.drawImage(img, x - size / 2 + dx, y - size / 2 + dy, dw, dh);
} }
_capOutlinePoints(cap) {
const tm = cap?.tm;
if (!tm || !Array.isArray(tm.a0) || !Array.isArray(tm.a2) || !Array.isArray(tm.center)) return [];
const ax0 = Number(tm.a0[0]), az0 = Number(tm.a0[2]);
const ax2 = Number(tm.a2[0]), az2 = Number(tm.a2[2]);
const cx = Number(tm.center[0]), cz = Number(tm.center[2]);
if (!Number.isFinite(ax0) || !Number.isFinite(az0) || !Number.isFinite(ax2) || !Number.isFinite(az2)
|| !Number.isFinite(cx) || !Number.isFinite(cz)) return [];
const capType = String(cap?.type || '').toLowerCase();
const points = [];
if (capType === 'sphere' || capType === 'cylinder') {
const steps = 64;
for (let i = 0; i < steps; i++) {
const t = (2 * Math.PI * i) / steps;
const wx = cx + Math.cos(t) * ax0 + Math.sin(t) * ax2;
const wz = cz + Math.cos(t) * az0 + Math.sin(t) * az2;
points.push(this.worldToPixel(wx, wz));
}
return points;
}
if (capType === 'box') {
const corners = [[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5], [-0.5, 0.5]];
for (const [sx, sz] of corners) {
const wx = cx + sx * ax0 + sz * ax2;
const wz = cz + sx * az0 + sz * az2;
points.push(this.worldToPixel(wx, wz));
}
return points;
}
return [];
}
_pickContrastOutlineColor(points) {
if (!points || points.length === 0) return '#fff';
const W = this.canvasSize;
const H = this.canvasSize;
let lumaSum = 0;
let n = 0;
for (const [px, py] of points) {
const x = Math.max(0, Math.min(W - 1, Math.round(px)));
const y = Math.max(0, Math.min(H - 1, Math.round(py)));
try {
const d = this.mapCtx.getImageData(x, y, 1, 1).data;
const luma = 0.2126 * d[0] + 0.7152 * d[1] + 0.0722 * d[2];
lumaSum += luma;
n++;
} catch (_) {
continue;
}
}
if (n === 0) return '#fff';
return (lumaSum / n) < 128 ? '#fff' : '#000';
}
_capIconForLabel(label) {
const key = String(label || '').toLowerCase();
if (!this._capIconCache) return null;
return this._capIconCache[`capture_${key}`] || this._capIconCache.cap_icon || null;
}
_drawCaptureIcon(label, px, py, size) {
const img = this._capIconForLabel(label);
if (!img || !img.naturalWidth) return;
const s = Math.max(RC_CAP_ICON_MIN_SIZE, Math.round(size));
this.mapCtx.save();
this.mapCtx.globalAlpha = RC_CAP_ICON_ALPHA;
this.mapCtx.drawImage(img, px - s / 2, py - s / 2, s, s);
this.mapCtx.restore();
}
_drawCaptureAreasOnMap() {
if (this._mode !== 'ground') return;
if (!this.captureAreas || this.captureAreas.length === 0) return;
const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
for (let i = 0; i < this.captureAreas.length; i++) {
const cap = this.captureAreas[i];
const label = labels[i] || String(i + 1);
const cx = Number(cap?.x || 0);
const cz = Number(cap?.z || 0);
const [px, py] = this.worldToPixel(cx, cz);
const outlinePoints = this._capOutlinePoints(cap);
let iconSize = 18;
if (outlinePoints.length >= 3) {
const outline = this._pickContrastOutlineColor(outlinePoints);
this.mapCtx.strokeStyle = outline;
this.mapCtx.lineWidth = RC_CAP_STROKE_PX;
this.mapCtx.beginPath();
this.mapCtx.moveTo(outlinePoints[0][0], outlinePoints[0][1]);
for (let p = 1; p < outlinePoints.length; p++) {
this.mapCtx.lineTo(outlinePoints[p][0], outlinePoints[p][1]);
}
this.mapCtx.closePath();
this.mapCtx.stroke();
let area2 = 0;
const nPts = outlinePoints.length;
for (let p = 0; p < nPts; p++) {
const a = outlinePoints[p];
const b = outlinePoints[(p + 1) % nPts];
area2 += a[0] * b[1] - b[0] * a[1];
}
const capArea = Math.abs(area2) * 0.5;
if (capArea > 0) {
iconSize = Math.max(RC_CAP_ICON_MIN_SIZE, Math.min(this.canvasSize, Math.round(Math.sqrt(capArea / 2))));
}
} else {
const rr = Math.max(0, Number(cap?.radius || 0));
let rp = Math.round(rr * ((this.canvasSize / this.xRange + this.canvasSize / this.zRange) * 0.5));
rp = Math.max(8, rp);
if (px < -rp || py < -rp || px > this.canvasSize + rp || py > this.canvasSize + rp) continue;
const ringSamples = [
[px + rp, py], [px - rp, py], [px, py + rp], [px, py - rp],
[px + Math.round(rp * 0.707), py + Math.round(rp * 0.707)],
[px - Math.round(rp * 0.707), py + Math.round(rp * 0.707)],
[px + Math.round(rp * 0.707), py - Math.round(rp * 0.707)],
[px - Math.round(rp * 0.707), py - Math.round(rp * 0.707)],
];
const outline = this._pickContrastOutlineColor(ringSamples);
this.mapCtx.strokeStyle = outline;
this.mapCtx.lineWidth = RC_CAP_STROKE_PX;
this.mapCtx.beginPath();
this.mapCtx.arc(px, py, rp, 0, Math.PI * 2);
this.mapCtx.stroke();
const capArea = Math.PI * rp * rp;
iconSize = Math.max(RC_CAP_ICON_MIN_SIZE, Math.min(this.canvasSize, Math.round(Math.sqrt(capArea / 2))));
}
this._drawCaptureIcon(label, px, py, iconSize);
}
}
async _loadEntityIcons() { async _loadEntityIcons() {
this._iconCache = {}; this._iconCache = {};
this._iconDebug = {}; this._iconDebug = {};
@@ -587,12 +658,21 @@ class ReplayCanvas {
} else { } else {
this._airMapImg = null; this._airMapImg = null;
} }
this._capIconCache = {
cap_icon: await loadImg('/api/icons/type/cap_icon'),
};
const capLetters = new Set();
for (let i = 0; i < this.captureAreas.length && i < 26; i++) capLetters.add(String.fromCharCode(97 + i));
for (const letter of capLetters) {
this._capIconCache[`capture_${letter}`] = await loadImg(`/api/icons/type/capture_${letter}`);
}
this._drawMapToCanvas(); this._drawMapToCanvas();
} }
_drawMapToCanvas() { _drawMapToCanvas() {
const img = this._mode === 'air' ? (this._airMapImg || this._groundMapImg) : this._groundMapImg; const img = this._mode === 'air' ? (this._airMapImg || this._groundMapImg) : this._groundMapImg;
this.mapCtx.clearRect(0, 0, this.canvasSize, this.canvasSize);
if (!img) { if (!img) {
this.mapCtx.fillStyle = '#111'; this.mapCtx.fillStyle = '#111';
this.mapCtx.fillRect(0, 0, this.canvasSize, this.canvasSize); this.mapCtx.fillRect(0, 0, this.canvasSize, this.canvasSize);
@@ -602,6 +682,7 @@ class ReplayCanvas {
const sx = u * img.naturalWidth, sy = v * img.naturalHeight; const sx = u * img.naturalWidth, sy = v * img.naturalHeight;
const sw = w * img.naturalWidth, sh = h * img.naturalHeight; const sw = w * img.naturalWidth, sh = h * img.naturalHeight;
this.mapCtx.drawImage(img, sx, sy, sw, sh, 0, 0, this.canvasSize, this.canvasSize); this.mapCtx.drawImage(img, sx, sy, sw, sh, 0, 0, this.canvasSize, this.canvasSize);
this._drawCaptureAreasOnMap();
} }
setMode(mode) { setMode(mode) {
@@ -617,8 +698,6 @@ class ReplayCanvas {
this.zRange = coords.z1 - coords.z0; this.zRange = coords.z1 - coords.z0;
this._mapSrc = { u: 0, v: 0, w: 1, h: 1 }; this._mapSrc = { u: 0, v: 0, w: 1, h: 1 };
// Recompute autocrop for the new entity filter
this._computeAutocrop();
// Redraw map background with new crop region // Redraw map background with new crop region
this._drawMapToCanvas(); this._drawMapToCanvas();
// Recompute death positions in new coordinate space // Recompute death positions in new coordinate space