Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions server/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def parse_positions(parser, classified, target_steamid):
sample_ticks.append(t); tick_round[t] = r["official_num"]
if not sample_ticks:
return []
df = parser.parse_ticks(["X", "Y", "steamid"], ticks=sample_ticks)
df = parser.parse_ticks(["X", "Y", "yaw", "steamid"], ticks=sample_ticks)
if not isinstance(df, pd.DataFrame):
df = pd.DataFrame(df)
required = {"tick", "steamid", "X", "Y"}
Expand All @@ -131,6 +131,7 @@ def parse_positions(parser, classified, target_steamid):
df = df[df["steamid"] == sid].copy()
df["X"] = pd.to_numeric(df["X"], errors="coerce")
df["Y"] = pd.to_numeric(df["Y"], errors="coerce")
df["yaw"] = pd.to_numeric(df["yaw"], errors="coerce") if "yaw" in df.columns else np.nan
df = df[np.isfinite(df["X"]) & np.isfinite(df["Y"])].copy()
if df.empty:
return []
Expand All @@ -141,8 +142,12 @@ def parse_positions(parser, classified, target_steamid):
for num, grp in df.groupby("official_num"):
grp = grp.sort_values("tick")
fe = fe_by_num[num]
path = [[round((int(t) - fe) / config.TICK_RATE, 3), float(x), float(y)]
for t, x, y in zip(grp["tick"], grp["X"], grp["Y"])]
path = []
for t, x, y, yaw in zip(grp["tick"], grp["X"], grp["Y"], grp["yaw"]):
entry = [round((int(t) - fe) / config.TICK_RATE, 3), float(x), float(y)]
if pd.notna(yaw):
entry.append(float(yaw))
path.append(entry)
m = meta_by_num[num]
out.append({"official_num": int(num), "side": m["side"],
"rtype": m["rtype"], "path": path})
Expand Down
43 changes: 30 additions & 13 deletions server/static/replay.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,9 @@ class ReplayPlayer {
else this.disabled.add(roundId);
}

// Interpolate a [[t,x,y], ...] series. `holdLast` is useful for death
// markers and grenade heads, while live players disappear after their final
// position sample.
// Interpolate a [[t,x,y(,yaw)], ...] series. Returns [x, y] (or [x, y, yaw]
// when the series carries yaw). `holdLast` is useful for death markers and
// grenade heads, while live players disappear after their final position.
_interp(series, gameTime, holdLast = false) {
if (!Array.isArray(series) || !finiteNumber(gameTime)) return null;
let previous = null;
Expand All @@ -140,7 +140,7 @@ class ReplayPlayer {
if (previous === null) {
previous = sample;
if (gameTime < sample[0]) return null;
if (gameTime === sample[0]) return [sample[1], sample[2]];
if (gameTime === sample[0]) return sample.slice(1);
continue;
}
if (sample[0] <= previous[0]) {
Expand All @@ -150,14 +150,23 @@ class ReplayPlayer {
if (gameTime <= sample[0]) {
const fraction = Math.max(0, Math.min(1,
(gameTime - previous[0]) / (sample[0] - previous[0])));
return [
const result = [
previous[1] + (sample[1] - previous[1]) * fraction,
previous[2] + (sample[2] - previous[2]) * fraction
];
// Yaw is circular; don't interpolate across wrap-around.
if (previous.length >= 4 && sample.length >= 4 &&
finiteNumber(previous[3]) && finiteNumber(sample[3]) &&
Math.abs(sample[3] - previous[3]) < 180) {
result.push(previous[3] + (sample[3] - previous[3]) * fraction);
} else if (previous.length >= 4 && finiteNumber(previous[3])) {
result.push(previous[3]);
}
return result;
}
previous = sample;
}
if (previous && (holdLast || gameTime === previous[0])) return [previous[1], previous[2]];
if (previous && (holdLast || gameTime === previous[0])) return previous.slice(1);
return null;
}

Expand Down Expand Up @@ -221,15 +230,23 @@ class ReplayPlayer {
const position = this._interp(path, gameTime);
const pixel = position && this.g2p(position[0], position[1]);
if (!pixel) continue;
const velocity = this._velocityAt(path, gameTime);
if (velocity) {
const scale = this.transform.scale;
const vx = velocity[0] / scale;
const vy = -velocity[1] / scale;
if (finiteNumber(vx) && finiteNumber(vy) && Math.hypot(vx, vy) > 0.5) {
this._drawArrow(pixel[0], pixel[1], Math.atan2(vy, vx), color);
// Yaw ≈ atan2(dy, dx) in game coords (measured from demo data). Radar
// flips Y, so canvas_angle = -atan2(dy, dx) = -yaw.
let arrowAngle = null;
if (position.length >= 3 && finiteNumber(position[2])) {
arrowAngle = -position[2] * Math.PI / 180;
} else {
const velocity = this._velocityAt(path, gameTime);
if (velocity) {
const scale = this.transform.scale;
const vx = velocity[0] / scale;
const vy = -velocity[1] / scale;
if (finiteNumber(vx) && finiteNumber(vy) && Math.hypot(vx, vy) > 0.5) {
arrowAngle = Math.atan2(vy, vx);
}
}
}
if (arrowAngle !== null) this._drawArrow(pixel[0], pixel[1], arrowAngle, color);
ctx.save();
ctx.globalAlpha = 0.86;
ctx.fillStyle = color;
Expand Down