@axiapps/bridge-metrics 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +271 -0
- package/dist/index.d.cts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +268 -0
- package/package.json +2 -1
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(src_exports, {
|
|
|
23
23
|
DEFAULT_DISRUPTION_METHOD: () => DEFAULT_DISRUPTION_METHOD,
|
|
24
24
|
METRICS_SPEC: () => METRICS_SPEC,
|
|
25
25
|
NON_DAMAGING_CONDITIONS: () => NON_DAMAGING_CONDITIONS,
|
|
26
|
+
OUT_OF_POSITION: () => OUT_OF_POSITION,
|
|
26
27
|
PROFESSION_COLORS: () => PROFESSION_COLORS,
|
|
27
28
|
ROLLUP_SOURCES_VERSION: () => ROLLUP_SOURCES_VERSION,
|
|
28
29
|
ReportSchemaError: () => ReportSchemaError,
|
|
@@ -30,12 +31,14 @@ __export(src_exports, {
|
|
|
30
31
|
applySquadStabilityGeneration: () => applySquadStabilityGeneration,
|
|
31
32
|
buildConditionIconMap: () => buildConditionIconMap,
|
|
32
33
|
buildRollupData: () => buildRollupData,
|
|
34
|
+
classifyDegree: () => classifyDegree,
|
|
33
35
|
compareRunSets: () => compareRunSets,
|
|
34
36
|
computeDownContribution: () => computeDownContribution,
|
|
35
37
|
computeIncomingDisruptions: () => computeIncomingDisruptions,
|
|
36
38
|
computeOutgoingConditions: () => computeOutgoingConditions,
|
|
37
39
|
computeOutgoingCrowdControl: () => computeOutgoingCrowdControl,
|
|
38
40
|
computePlayerAggregation: () => computePlayerAggregation,
|
|
41
|
+
computePositioning: () => computePositioning,
|
|
39
42
|
computeSquadBarrier: () => computeSquadBarrier,
|
|
40
43
|
computeSquadHealing: () => computeSquadHealing,
|
|
41
44
|
createPlayerAggregationAccumulators: () => createPlayerAggregationAccumulators,
|
|
@@ -2835,11 +2838,277 @@ var compareRunSets = (a, b) => {
|
|
|
2835
2838
|
})
|
|
2836
2839
|
};
|
|
2837
2840
|
};
|
|
2841
|
+
|
|
2842
|
+
// src/positioning.ts
|
|
2843
|
+
var OUT_OF_POSITION = 1200;
|
|
2844
|
+
var clamp = (val, min, max) => Math.max(min, Math.min(max, val));
|
|
2845
|
+
var squadOf = (r) => (r.details?.players ?? []).filter((p) => !p?.notInSquad);
|
|
2846
|
+
function linkedDeathHits(player, tagPositions, pollingRate) {
|
|
2847
|
+
const replay = player?.combatReplayData;
|
|
2848
|
+
if (!replay || !Array.isArray(replay.dead) || !Array.isArray(replay.down)) return [];
|
|
2849
|
+
const playerPositions = replay.positions;
|
|
2850
|
+
if (!Array.isArray(playerPositions) || playerPositions.length === 0) return [];
|
|
2851
|
+
const playerStart = Number(replay.start ?? 0);
|
|
2852
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
2853
|
+
const deadSet = /* @__PURE__ */ new Set();
|
|
2854
|
+
for (const entry of replay.dead) {
|
|
2855
|
+
if (Array.isArray(entry) && Number.isFinite(Number(entry[0])) && Number(entry[0]) > 0) {
|
|
2856
|
+
deadSet.add(Number(entry[0]));
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
const hits = [];
|
|
2860
|
+
for (const entry of replay.down) {
|
|
2861
|
+
if (!Array.isArray(entry)) continue;
|
|
2862
|
+
const downStartMs = Number(entry[0]);
|
|
2863
|
+
const linkedDeathMs = Number(entry[1]);
|
|
2864
|
+
if (!Number.isFinite(downStartMs) || downStartMs < 0) continue;
|
|
2865
|
+
if (!deadSet.has(linkedDeathMs)) continue;
|
|
2866
|
+
const pollIndex = Math.floor(downStartMs / pollingRate);
|
|
2867
|
+
const playerIdx = clamp(pollIndex - playerOffset, 0, playerPositions.length - 1);
|
|
2868
|
+
const tagIdx = clamp(pollIndex, 0, tagPositions.length - 1);
|
|
2869
|
+
const [px, py] = playerPositions[playerIdx];
|
|
2870
|
+
const [tx, ty] = tagPositions[tagIdx];
|
|
2871
|
+
hits.push({ px, py, tx, ty, downStartMs });
|
|
2872
|
+
}
|
|
2873
|
+
return hits;
|
|
2874
|
+
}
|
|
2875
|
+
function classifyDegree(report) {
|
|
2876
|
+
const squad = squadOf(report);
|
|
2877
|
+
const meta = report.details?.combatReplayMetaData ?? {};
|
|
2878
|
+
const commander = squad.find((p) => p?.hasCommanderTag);
|
|
2879
|
+
const tagPositions = commander?.combatReplayData?.positions ?? [];
|
|
2880
|
+
if (commander && tagPositions.length > 0 && (meta.pollingRate ?? 0) > 0 && (meta.inchToPixel ?? 0) > 0) return "full";
|
|
2881
|
+
if (squad.some((p) => typeof p?.statsAll?.[0]?.distToCom === "number")) return "coarse";
|
|
2882
|
+
return "none";
|
|
2883
|
+
}
|
|
2884
|
+
function computePositioning(report) {
|
|
2885
|
+
const degree = classifyDegree(report);
|
|
2886
|
+
const squad = squadOf(report);
|
|
2887
|
+
const meta = report.details?.combatReplayMetaData ?? {};
|
|
2888
|
+
const pollingRate = (meta.pollingRate ?? 0) > 0 ? meta.pollingRate : 0;
|
|
2889
|
+
const inchToPixel = (meta.inchToPixel ?? 0) > 0 ? meta.inchToPixel : 0;
|
|
2890
|
+
const commander = squad.find((p) => p?.hasCommanderTag);
|
|
2891
|
+
const tagPositions = commander?.combatReplayData?.positions ?? [];
|
|
2892
|
+
const replayUsable = !!commander && tagPositions.length > 0 && pollingRate > 0 && inchToPixel > 0;
|
|
2893
|
+
const perPlayerMap = /* @__PURE__ */ new Map();
|
|
2894
|
+
const outOfPositionDeaths = [];
|
|
2895
|
+
if (replayUsable) {
|
|
2896
|
+
for (const player of squad) {
|
|
2897
|
+
const account = player?.account ?? "Unknown";
|
|
2898
|
+
const isCommanderPlayer = !!player?.hasCommanderTag;
|
|
2899
|
+
const playerPositions = player?.combatReplayData?.positions;
|
|
2900
|
+
if (!Array.isArray(playerPositions) || playerPositions.length === 0) continue;
|
|
2901
|
+
const playerStart = Number(player?.combatReplayData?.start ?? 0);
|
|
2902
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
2903
|
+
const samples = [];
|
|
2904
|
+
for (let i = 0; i < playerPositions.length; i++) {
|
|
2905
|
+
const tagIdx = clamp(i + playerOffset, 0, tagPositions.length - 1);
|
|
2906
|
+
const [px, py] = playerPositions[i];
|
|
2907
|
+
const [tx, ty] = tagPositions[tagIdx];
|
|
2908
|
+
const dist = isCommanderPlayer ? 0 : Math.hypot(px - tx, py - ty) / inchToPixel;
|
|
2909
|
+
samples.push(dist);
|
|
2910
|
+
}
|
|
2911
|
+
if (!isCommanderPlayer) {
|
|
2912
|
+
const existing = perPlayerMap.get(account);
|
|
2913
|
+
if (existing) {
|
|
2914
|
+
for (const s of samples) existing.push(s);
|
|
2915
|
+
} else {
|
|
2916
|
+
perPlayerMap.set(account, samples);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (isCommanderPlayer) continue;
|
|
2920
|
+
for (const { px, py, tx, ty, downStartMs } of linkedDeathHits(player, tagPositions, pollingRate)) {
|
|
2921
|
+
const distAtDown = Math.round(Math.hypot(px - tx, py - ty) / inchToPixel);
|
|
2922
|
+
if (distAtDown > OUT_OF_POSITION) {
|
|
2923
|
+
outOfPositionDeaths.push({ account, distAtDown, atSec: Math.round(downStartMs / 1e3) });
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
const perPlayer = [];
|
|
2929
|
+
if (replayUsable) {
|
|
2930
|
+
for (const [account, samples] of perPlayerMap) {
|
|
2931
|
+
if (samples.length === 0) continue;
|
|
2932
|
+
const avg = samples.reduce((s, v) => s + v, 0) / samples.length;
|
|
2933
|
+
const peak = Math.max(...samples);
|
|
2934
|
+
perPlayer.push({ account, avgDistToTag: Math.round(avg), peakDistToTag: Math.round(peak) });
|
|
2935
|
+
}
|
|
2936
|
+
perPlayer.sort((a, b) => b.peakDistToTag - a.peakDistToTag);
|
|
2937
|
+
} else if (degree === "coarse") {
|
|
2938
|
+
for (const p of squad) {
|
|
2939
|
+
const account = p?.account;
|
|
2940
|
+
if (!account) continue;
|
|
2941
|
+
const distToCom = p?.statsAll?.[0]?.distToCom;
|
|
2942
|
+
if (typeof distToCom !== "number") continue;
|
|
2943
|
+
perPlayer.push({ account, avgDistToTag: distToCom, peakDistToTag: distToCom });
|
|
2944
|
+
}
|
|
2945
|
+
perPlayer.sort((a, b) => b.peakDistToTag - a.peakDistToTag);
|
|
2946
|
+
}
|
|
2947
|
+
if (degree !== "full" || !replayUsable) {
|
|
2948
|
+
return { degree, perPlayer, outOfPositionDeaths, squad: null, commander: null, deathClusters: [], figure: void 0 };
|
|
2949
|
+
}
|
|
2950
|
+
const squadNonCmdr = squad.filter((p) => !p?.hasCommanderTag && Array.isArray(p?.combatReplayData?.positions) && (p.combatReplayData.positions.length ?? 0) > 0);
|
|
2951
|
+
const numTicks = tagPositions.length;
|
|
2952
|
+
let spreadSum = 0;
|
|
2953
|
+
let peakSpreadValue = 0;
|
|
2954
|
+
let peakSpreadAtSec = 0;
|
|
2955
|
+
let tagToCentroidSum = 0;
|
|
2956
|
+
let peakLeadValue = 0;
|
|
2957
|
+
let peakLeadAtSec = 0;
|
|
2958
|
+
const deathCoords = [];
|
|
2959
|
+
for (const player of squad) {
|
|
2960
|
+
for (const { px, py } of linkedDeathHits(player, tagPositions, pollingRate)) {
|
|
2961
|
+
deathCoords.push([px, py]);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
for (let i = 0; i < numTicks; i++) {
|
|
2965
|
+
const atSec = i * pollingRate / 1e3;
|
|
2966
|
+
const [tx, ty] = tagPositions[i];
|
|
2967
|
+
if (squadNonCmdr.length > 0) {
|
|
2968
|
+
let spreadAtTick = 0;
|
|
2969
|
+
let centroidX = 0;
|
|
2970
|
+
let centroidY = 0;
|
|
2971
|
+
for (const player of squadNonCmdr) {
|
|
2972
|
+
const positions = player.combatReplayData.positions;
|
|
2973
|
+
const playerStart = Number(player.combatReplayData?.start ?? 0);
|
|
2974
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
2975
|
+
const pIdx = clamp(i - playerOffset, 0, positions.length - 1);
|
|
2976
|
+
const [px, py] = positions[pIdx];
|
|
2977
|
+
spreadAtTick += Math.hypot(px - tx, py - ty) / inchToPixel;
|
|
2978
|
+
centroidX += px;
|
|
2979
|
+
centroidY += py;
|
|
2980
|
+
}
|
|
2981
|
+
const avgSpreadAtTick = spreadAtTick / squadNonCmdr.length;
|
|
2982
|
+
spreadSum += avgSpreadAtTick;
|
|
2983
|
+
if (avgSpreadAtTick > peakSpreadValue) {
|
|
2984
|
+
peakSpreadValue = avgSpreadAtTick;
|
|
2985
|
+
peakSpreadAtSec = atSec;
|
|
2986
|
+
}
|
|
2987
|
+
centroidX /= squadNonCmdr.length;
|
|
2988
|
+
centroidY /= squadNonCmdr.length;
|
|
2989
|
+
const tagToCentroidDist = Math.hypot(tx - centroidX, ty - centroidY) / inchToPixel;
|
|
2990
|
+
tagToCentroidSum += tagToCentroidDist;
|
|
2991
|
+
if (tagToCentroidDist > peakLeadValue) {
|
|
2992
|
+
peakLeadValue = tagToCentroidDist;
|
|
2993
|
+
peakLeadAtSec = atSec;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
const avgSpread = numTicks > 0 ? spreadSum / numTicks : 0;
|
|
2998
|
+
const peakAvgRatio = avgSpread > 0 ? peakSpreadValue / avgSpread : 1;
|
|
2999
|
+
const cohesionNote = peakAvgRatio > 2.5 ? "tight then scattered" : "held together";
|
|
3000
|
+
const squadFollowLag = numTicks > 0 ? tagToCentroidSum / numTicks : 0;
|
|
3001
|
+
const CLUSTER_CELL = 150;
|
|
3002
|
+
const cellMap = /* @__PURE__ */ new Map();
|
|
3003
|
+
for (const [x, y] of deathCoords) {
|
|
3004
|
+
const cellX = Math.floor(x / CLUSTER_CELL);
|
|
3005
|
+
const cellY = Math.floor(y / CLUSTER_CELL);
|
|
3006
|
+
const key = `${cellX},${cellY}`;
|
|
3007
|
+
const existing = cellMap.get(key);
|
|
3008
|
+
if (existing) {
|
|
3009
|
+
existing.sumX += x;
|
|
3010
|
+
existing.sumY += y;
|
|
3011
|
+
existing.count++;
|
|
3012
|
+
} else {
|
|
3013
|
+
cellMap.set(key, { sumX: x, sumY: y, count: 1 });
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
const deathClusters = Array.from(cellMap.values()).map(({ sumX, sumY, count }) => ({ x: sumX / count, y: sumY / count, count })).sort((a, b) => b.count - a.count).slice(0, 6);
|
|
3017
|
+
const stride = Math.ceil(1e3 / pollingRate);
|
|
3018
|
+
const tagPath = [];
|
|
3019
|
+
for (let i = 0; i < tagPositions.length; i += stride) {
|
|
3020
|
+
tagPath.push(tagPositions[i]);
|
|
3021
|
+
}
|
|
3022
|
+
const downCoords = [];
|
|
3023
|
+
for (const player of squad) {
|
|
3024
|
+
const replay = player?.combatReplayData;
|
|
3025
|
+
if (!replay || !Array.isArray(replay.down) || !Array.isArray(replay.positions)) continue;
|
|
3026
|
+
const playerStart = Number(replay.start ?? 0);
|
|
3027
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
3028
|
+
for (const entry of replay.down) {
|
|
3029
|
+
if (!Array.isArray(entry)) continue;
|
|
3030
|
+
const downStartMs = Number(entry[0]);
|
|
3031
|
+
if (!Number.isFinite(downStartMs) || downStartMs < 0) continue;
|
|
3032
|
+
const pollIndex = Math.floor(downStartMs / pollingRate);
|
|
3033
|
+
const playerIdx = clamp(pollIndex - playerOffset, 0, replay.positions.length - 1);
|
|
3034
|
+
const [px, py] = replay.positions[playerIdx];
|
|
3035
|
+
downCoords.push([px, py]);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
const spreadTimeline = [];
|
|
3039
|
+
let sumMassX = 0;
|
|
3040
|
+
let sumMassY = 0;
|
|
3041
|
+
let massCount = 0;
|
|
3042
|
+
let minX = Infinity;
|
|
3043
|
+
let maxX = -Infinity;
|
|
3044
|
+
let minY = Infinity;
|
|
3045
|
+
let maxY = -Infinity;
|
|
3046
|
+
for (let i = 0; i < numTicks; i += stride) {
|
|
3047
|
+
const atSec = i * pollingRate / 1e3;
|
|
3048
|
+
const [tx, ty] = tagPositions[i];
|
|
3049
|
+
let spreadAtTick = 0;
|
|
3050
|
+
let centroidX = 0;
|
|
3051
|
+
let centroidY = 0;
|
|
3052
|
+
for (const player of squadNonCmdr) {
|
|
3053
|
+
const positions = player.combatReplayData.positions;
|
|
3054
|
+
const playerStart = Number(player.combatReplayData?.start ?? 0);
|
|
3055
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
3056
|
+
const pIdx = clamp(i - playerOffset, 0, positions.length - 1);
|
|
3057
|
+
const [px, py] = positions[pIdx];
|
|
3058
|
+
spreadAtTick += Math.hypot(px - tx, py - ty) / inchToPixel;
|
|
3059
|
+
centroidX += px;
|
|
3060
|
+
centroidY += py;
|
|
3061
|
+
}
|
|
3062
|
+
if (squadNonCmdr.length > 0) {
|
|
3063
|
+
centroidX /= squadNonCmdr.length;
|
|
3064
|
+
centroidY /= squadNonCmdr.length;
|
|
3065
|
+
spreadTimeline.push([atSec, Math.round(spreadAtTick / squadNonCmdr.length)]);
|
|
3066
|
+
sumMassX += centroidX;
|
|
3067
|
+
sumMassY += centroidY;
|
|
3068
|
+
minX = Math.min(minX, centroidX);
|
|
3069
|
+
maxX = Math.max(maxX, centroidX);
|
|
3070
|
+
minY = Math.min(minY, centroidY);
|
|
3071
|
+
maxY = Math.max(maxY, centroidY);
|
|
3072
|
+
massCount++;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
const massX = massCount > 0 ? sumMassX / massCount : 0;
|
|
3076
|
+
const massY = massCount > 0 ? sumMassY / massCount : 0;
|
|
3077
|
+
const massR = massCount > 0 ? Math.hypot(maxX - minX, maxY - minY) / 2 : 0;
|
|
3078
|
+
const sizes = meta.sizes ?? [0, 0];
|
|
3079
|
+
const figure = {
|
|
3080
|
+
map: { sizes, inchToPixel },
|
|
3081
|
+
tagPath,
|
|
3082
|
+
squadMass: { x: Math.round(massX), y: Math.round(massY), r: Math.round(massR) },
|
|
3083
|
+
deaths: deathCoords,
|
|
3084
|
+
downs: downCoords,
|
|
3085
|
+
spread: spreadTimeline,
|
|
3086
|
+
peakSpread: Math.round(peakSpreadValue)
|
|
3087
|
+
};
|
|
3088
|
+
return {
|
|
3089
|
+
degree,
|
|
3090
|
+
perPlayer,
|
|
3091
|
+
outOfPositionDeaths,
|
|
3092
|
+
squad: {
|
|
3093
|
+
avgSpread: Math.round(avgSpread),
|
|
3094
|
+
peakSpread: { value: Math.round(peakSpreadValue), atSec: peakSpreadAtSec },
|
|
3095
|
+
cohesionNote
|
|
3096
|
+
},
|
|
3097
|
+
commander: {
|
|
3098
|
+
account: commander?.account ?? "Unknown",
|
|
3099
|
+
peakLeadFromSquad: { value: Math.round(peakLeadValue), atSec: peakLeadAtSec },
|
|
3100
|
+
squadFollowLag: Math.round(squadFollowLag)
|
|
3101
|
+
},
|
|
3102
|
+
deathClusters,
|
|
3103
|
+
figure
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
2838
3106
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2839
3107
|
0 && (module.exports = {
|
|
2840
3108
|
DEFAULT_DISRUPTION_METHOD,
|
|
2841
3109
|
METRICS_SPEC,
|
|
2842
3110
|
NON_DAMAGING_CONDITIONS,
|
|
3111
|
+
OUT_OF_POSITION,
|
|
2843
3112
|
PROFESSION_COLORS,
|
|
2844
3113
|
ROLLUP_SOURCES_VERSION,
|
|
2845
3114
|
ReportSchemaError,
|
|
@@ -2847,12 +3116,14 @@ var compareRunSets = (a, b) => {
|
|
|
2847
3116
|
applySquadStabilityGeneration,
|
|
2848
3117
|
buildConditionIconMap,
|
|
2849
3118
|
buildRollupData,
|
|
3119
|
+
classifyDegree,
|
|
2850
3120
|
compareRunSets,
|
|
2851
3121
|
computeDownContribution,
|
|
2852
3122
|
computeIncomingDisruptions,
|
|
2853
3123
|
computeOutgoingConditions,
|
|
2854
3124
|
computeOutgoingCrowdControl,
|
|
2855
3125
|
computePlayerAggregation,
|
|
3126
|
+
computePositioning,
|
|
2856
3127
|
computeSquadBarrier,
|
|
2857
3128
|
computeSquadHealing,
|
|
2858
3129
|
createPlayerAggregationAccumulators,
|
package/dist/index.d.cts
CHANGED
|
@@ -11,3 +11,99 @@ export { PlayerRoleClassification, RoleClassificationFactor } from './roles.cjs'
|
|
|
11
11
|
export { isResUtilitySkill } from './resUtility.cjs';
|
|
12
12
|
export { parseTimestamp as parseFightTimestamp, resolveFightTimestamp } from './timestampUtils.cjs';
|
|
13
13
|
export { PlayerAggregate, ReportSchemaError, RunPlayerSummary, RunSetComparison, RunSummary, aggregatePlayers, compareRunSets, extractRunSummary } from './reportMetrics.cjs';
|
|
14
|
+
|
|
15
|
+
type ReplayDegree = 'full' | 'coarse' | 'none';
|
|
16
|
+
type AnyPlayer = {
|
|
17
|
+
notInSquad?: boolean;
|
|
18
|
+
hasCommanderTag?: boolean;
|
|
19
|
+
account?: string;
|
|
20
|
+
profession?: string;
|
|
21
|
+
statsAll?: Array<{
|
|
22
|
+
distToCom?: number;
|
|
23
|
+
stackDist?: number;
|
|
24
|
+
}>;
|
|
25
|
+
combatReplayData?: {
|
|
26
|
+
positions?: Array<[number, number]>;
|
|
27
|
+
dead?: Array<[number, number]>;
|
|
28
|
+
down?: Array<[number, number]>;
|
|
29
|
+
start?: number;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
type ParsedReport = {
|
|
33
|
+
details?: {
|
|
34
|
+
players?: AnyPlayer[];
|
|
35
|
+
combatReplayMetaData?: {
|
|
36
|
+
pollingRate?: number;
|
|
37
|
+
inchToPixel?: number;
|
|
38
|
+
sizes?: [number, number];
|
|
39
|
+
};
|
|
40
|
+
durationMS?: number;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
type PerPlayerDistance = {
|
|
44
|
+
account: string;
|
|
45
|
+
avgDistToTag: number;
|
|
46
|
+
peakDistToTag: number;
|
|
47
|
+
};
|
|
48
|
+
type OutOfPositionDeath = {
|
|
49
|
+
account: string;
|
|
50
|
+
distAtDown: number;
|
|
51
|
+
atSec: number;
|
|
52
|
+
};
|
|
53
|
+
type SquadCohesion = {
|
|
54
|
+
avgSpread: number;
|
|
55
|
+
peakSpread: {
|
|
56
|
+
value: number;
|
|
57
|
+
atSec: number;
|
|
58
|
+
};
|
|
59
|
+
cohesionNote: string;
|
|
60
|
+
};
|
|
61
|
+
type CommanderOverextension = {
|
|
62
|
+
account: string;
|
|
63
|
+
peakLeadFromSquad: {
|
|
64
|
+
value: number;
|
|
65
|
+
atSec: number;
|
|
66
|
+
};
|
|
67
|
+
/** v1 definition: mean per-tick distance from commander to squad centroid */
|
|
68
|
+
squadFollowLag: number;
|
|
69
|
+
};
|
|
70
|
+
type DeathCluster = {
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
count: number;
|
|
74
|
+
};
|
|
75
|
+
type PositioningFigure = {
|
|
76
|
+
map: {
|
|
77
|
+
sizes: [number, number];
|
|
78
|
+
inchToPixel: number;
|
|
79
|
+
};
|
|
80
|
+
/** Down-sampled tag path: ~1 point/sec (stride = Math.ceil(1000/pollingRate)) */
|
|
81
|
+
tagPath: Array<[number, number]>;
|
|
82
|
+
/** Approximate bounding circle of the squad centroid over time */
|
|
83
|
+
squadMass: {
|
|
84
|
+
x: number;
|
|
85
|
+
y: number;
|
|
86
|
+
r: number;
|
|
87
|
+
};
|
|
88
|
+
/** Death locations (reused from deathClusters source) */
|
|
89
|
+
deaths: Array<[number, number]>;
|
|
90
|
+
/** Down positions */
|
|
91
|
+
downs: Array<[number, number]>;
|
|
92
|
+
/** [[sec, spreadValue]] down-sampled to ~1 point/sec */
|
|
93
|
+
spread: Array<[number, number]>;
|
|
94
|
+
peakSpread: number;
|
|
95
|
+
};
|
|
96
|
+
type PositioningSummary = {
|
|
97
|
+
degree: ReplayDegree;
|
|
98
|
+
perPlayer: PerPlayerDistance[];
|
|
99
|
+
outOfPositionDeaths: OutOfPositionDeath[];
|
|
100
|
+
squad: SquadCohesion | null;
|
|
101
|
+
commander: CommanderOverextension | null;
|
|
102
|
+
deathClusters: DeathCluster[];
|
|
103
|
+
figure: PositioningFigure | undefined;
|
|
104
|
+
};
|
|
105
|
+
declare const OUT_OF_POSITION = 1200;
|
|
106
|
+
declare function classifyDegree(report: ParsedReport): ReplayDegree;
|
|
107
|
+
declare function computePositioning(report: ParsedReport): PositioningSummary;
|
|
108
|
+
|
|
109
|
+
export { type CommanderOverextension, type DeathCluster, OUT_OF_POSITION, type OutOfPositionDeath, type ParsedReport, type PerPlayerDistance, type PositioningFigure, type PositioningSummary, type ReplayDegree, type SquadCohesion, classifyDegree, computePositioning };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,3 +11,99 @@ export { PlayerRoleClassification, RoleClassificationFactor } from './roles.js';
|
|
|
11
11
|
export { isResUtilitySkill } from './resUtility.js';
|
|
12
12
|
export { parseTimestamp as parseFightTimestamp, resolveFightTimestamp } from './timestampUtils.js';
|
|
13
13
|
export { PlayerAggregate, ReportSchemaError, RunPlayerSummary, RunSetComparison, RunSummary, aggregatePlayers, compareRunSets, extractRunSummary } from './reportMetrics.js';
|
|
14
|
+
|
|
15
|
+
type ReplayDegree = 'full' | 'coarse' | 'none';
|
|
16
|
+
type AnyPlayer = {
|
|
17
|
+
notInSquad?: boolean;
|
|
18
|
+
hasCommanderTag?: boolean;
|
|
19
|
+
account?: string;
|
|
20
|
+
profession?: string;
|
|
21
|
+
statsAll?: Array<{
|
|
22
|
+
distToCom?: number;
|
|
23
|
+
stackDist?: number;
|
|
24
|
+
}>;
|
|
25
|
+
combatReplayData?: {
|
|
26
|
+
positions?: Array<[number, number]>;
|
|
27
|
+
dead?: Array<[number, number]>;
|
|
28
|
+
down?: Array<[number, number]>;
|
|
29
|
+
start?: number;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
type ParsedReport = {
|
|
33
|
+
details?: {
|
|
34
|
+
players?: AnyPlayer[];
|
|
35
|
+
combatReplayMetaData?: {
|
|
36
|
+
pollingRate?: number;
|
|
37
|
+
inchToPixel?: number;
|
|
38
|
+
sizes?: [number, number];
|
|
39
|
+
};
|
|
40
|
+
durationMS?: number;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
type PerPlayerDistance = {
|
|
44
|
+
account: string;
|
|
45
|
+
avgDistToTag: number;
|
|
46
|
+
peakDistToTag: number;
|
|
47
|
+
};
|
|
48
|
+
type OutOfPositionDeath = {
|
|
49
|
+
account: string;
|
|
50
|
+
distAtDown: number;
|
|
51
|
+
atSec: number;
|
|
52
|
+
};
|
|
53
|
+
type SquadCohesion = {
|
|
54
|
+
avgSpread: number;
|
|
55
|
+
peakSpread: {
|
|
56
|
+
value: number;
|
|
57
|
+
atSec: number;
|
|
58
|
+
};
|
|
59
|
+
cohesionNote: string;
|
|
60
|
+
};
|
|
61
|
+
type CommanderOverextension = {
|
|
62
|
+
account: string;
|
|
63
|
+
peakLeadFromSquad: {
|
|
64
|
+
value: number;
|
|
65
|
+
atSec: number;
|
|
66
|
+
};
|
|
67
|
+
/** v1 definition: mean per-tick distance from commander to squad centroid */
|
|
68
|
+
squadFollowLag: number;
|
|
69
|
+
};
|
|
70
|
+
type DeathCluster = {
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
count: number;
|
|
74
|
+
};
|
|
75
|
+
type PositioningFigure = {
|
|
76
|
+
map: {
|
|
77
|
+
sizes: [number, number];
|
|
78
|
+
inchToPixel: number;
|
|
79
|
+
};
|
|
80
|
+
/** Down-sampled tag path: ~1 point/sec (stride = Math.ceil(1000/pollingRate)) */
|
|
81
|
+
tagPath: Array<[number, number]>;
|
|
82
|
+
/** Approximate bounding circle of the squad centroid over time */
|
|
83
|
+
squadMass: {
|
|
84
|
+
x: number;
|
|
85
|
+
y: number;
|
|
86
|
+
r: number;
|
|
87
|
+
};
|
|
88
|
+
/** Death locations (reused from deathClusters source) */
|
|
89
|
+
deaths: Array<[number, number]>;
|
|
90
|
+
/** Down positions */
|
|
91
|
+
downs: Array<[number, number]>;
|
|
92
|
+
/** [[sec, spreadValue]] down-sampled to ~1 point/sec */
|
|
93
|
+
spread: Array<[number, number]>;
|
|
94
|
+
peakSpread: number;
|
|
95
|
+
};
|
|
96
|
+
type PositioningSummary = {
|
|
97
|
+
degree: ReplayDegree;
|
|
98
|
+
perPlayer: PerPlayerDistance[];
|
|
99
|
+
outOfPositionDeaths: OutOfPositionDeath[];
|
|
100
|
+
squad: SquadCohesion | null;
|
|
101
|
+
commander: CommanderOverextension | null;
|
|
102
|
+
deathClusters: DeathCluster[];
|
|
103
|
+
figure: PositioningFigure | undefined;
|
|
104
|
+
};
|
|
105
|
+
declare const OUT_OF_POSITION = 1200;
|
|
106
|
+
declare function classifyDegree(report: ParsedReport): ReplayDegree;
|
|
107
|
+
declare function computePositioning(report: ParsedReport): PositioningSummary;
|
|
108
|
+
|
|
109
|
+
export { type CommanderOverextension, type DeathCluster, OUT_OF_POSITION, type OutOfPositionDeath, type ParsedReport, type PerPlayerDistance, type PositioningFigure, type PositioningSummary, type ReplayDegree, type SquadCohesion, classifyDegree, computePositioning };
|
package/dist/index.js
CHANGED
|
@@ -86,10 +86,276 @@ import {
|
|
|
86
86
|
hexToRgba,
|
|
87
87
|
toSuperscript
|
|
88
88
|
} from "./chunk-WW5XFXGC.js";
|
|
89
|
+
|
|
90
|
+
// src/positioning.ts
|
|
91
|
+
var OUT_OF_POSITION = 1200;
|
|
92
|
+
var clamp = (val, min, max) => Math.max(min, Math.min(max, val));
|
|
93
|
+
var squadOf = (r) => (r.details?.players ?? []).filter((p) => !p?.notInSquad);
|
|
94
|
+
function linkedDeathHits(player, tagPositions, pollingRate) {
|
|
95
|
+
const replay = player?.combatReplayData;
|
|
96
|
+
if (!replay || !Array.isArray(replay.dead) || !Array.isArray(replay.down)) return [];
|
|
97
|
+
const playerPositions = replay.positions;
|
|
98
|
+
if (!Array.isArray(playerPositions) || playerPositions.length === 0) return [];
|
|
99
|
+
const playerStart = Number(replay.start ?? 0);
|
|
100
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
101
|
+
const deadSet = /* @__PURE__ */ new Set();
|
|
102
|
+
for (const entry of replay.dead) {
|
|
103
|
+
if (Array.isArray(entry) && Number.isFinite(Number(entry[0])) && Number(entry[0]) > 0) {
|
|
104
|
+
deadSet.add(Number(entry[0]));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const hits = [];
|
|
108
|
+
for (const entry of replay.down) {
|
|
109
|
+
if (!Array.isArray(entry)) continue;
|
|
110
|
+
const downStartMs = Number(entry[0]);
|
|
111
|
+
const linkedDeathMs = Number(entry[1]);
|
|
112
|
+
if (!Number.isFinite(downStartMs) || downStartMs < 0) continue;
|
|
113
|
+
if (!deadSet.has(linkedDeathMs)) continue;
|
|
114
|
+
const pollIndex = Math.floor(downStartMs / pollingRate);
|
|
115
|
+
const playerIdx = clamp(pollIndex - playerOffset, 0, playerPositions.length - 1);
|
|
116
|
+
const tagIdx = clamp(pollIndex, 0, tagPositions.length - 1);
|
|
117
|
+
const [px, py] = playerPositions[playerIdx];
|
|
118
|
+
const [tx, ty] = tagPositions[tagIdx];
|
|
119
|
+
hits.push({ px, py, tx, ty, downStartMs });
|
|
120
|
+
}
|
|
121
|
+
return hits;
|
|
122
|
+
}
|
|
123
|
+
function classifyDegree(report) {
|
|
124
|
+
const squad = squadOf(report);
|
|
125
|
+
const meta = report.details?.combatReplayMetaData ?? {};
|
|
126
|
+
const commander = squad.find((p) => p?.hasCommanderTag);
|
|
127
|
+
const tagPositions = commander?.combatReplayData?.positions ?? [];
|
|
128
|
+
if (commander && tagPositions.length > 0 && (meta.pollingRate ?? 0) > 0 && (meta.inchToPixel ?? 0) > 0) return "full";
|
|
129
|
+
if (squad.some((p) => typeof p?.statsAll?.[0]?.distToCom === "number")) return "coarse";
|
|
130
|
+
return "none";
|
|
131
|
+
}
|
|
132
|
+
function computePositioning(report) {
|
|
133
|
+
const degree = classifyDegree(report);
|
|
134
|
+
const squad = squadOf(report);
|
|
135
|
+
const meta = report.details?.combatReplayMetaData ?? {};
|
|
136
|
+
const pollingRate = (meta.pollingRate ?? 0) > 0 ? meta.pollingRate : 0;
|
|
137
|
+
const inchToPixel = (meta.inchToPixel ?? 0) > 0 ? meta.inchToPixel : 0;
|
|
138
|
+
const commander = squad.find((p) => p?.hasCommanderTag);
|
|
139
|
+
const tagPositions = commander?.combatReplayData?.positions ?? [];
|
|
140
|
+
const replayUsable = !!commander && tagPositions.length > 0 && pollingRate > 0 && inchToPixel > 0;
|
|
141
|
+
const perPlayerMap = /* @__PURE__ */ new Map();
|
|
142
|
+
const outOfPositionDeaths = [];
|
|
143
|
+
if (replayUsable) {
|
|
144
|
+
for (const player of squad) {
|
|
145
|
+
const account = player?.account ?? "Unknown";
|
|
146
|
+
const isCommanderPlayer = !!player?.hasCommanderTag;
|
|
147
|
+
const playerPositions = player?.combatReplayData?.positions;
|
|
148
|
+
if (!Array.isArray(playerPositions) || playerPositions.length === 0) continue;
|
|
149
|
+
const playerStart = Number(player?.combatReplayData?.start ?? 0);
|
|
150
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
151
|
+
const samples = [];
|
|
152
|
+
for (let i = 0; i < playerPositions.length; i++) {
|
|
153
|
+
const tagIdx = clamp(i + playerOffset, 0, tagPositions.length - 1);
|
|
154
|
+
const [px, py] = playerPositions[i];
|
|
155
|
+
const [tx, ty] = tagPositions[tagIdx];
|
|
156
|
+
const dist = isCommanderPlayer ? 0 : Math.hypot(px - tx, py - ty) / inchToPixel;
|
|
157
|
+
samples.push(dist);
|
|
158
|
+
}
|
|
159
|
+
if (!isCommanderPlayer) {
|
|
160
|
+
const existing = perPlayerMap.get(account);
|
|
161
|
+
if (existing) {
|
|
162
|
+
for (const s of samples) existing.push(s);
|
|
163
|
+
} else {
|
|
164
|
+
perPlayerMap.set(account, samples);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (isCommanderPlayer) continue;
|
|
168
|
+
for (const { px, py, tx, ty, downStartMs } of linkedDeathHits(player, tagPositions, pollingRate)) {
|
|
169
|
+
const distAtDown = Math.round(Math.hypot(px - tx, py - ty) / inchToPixel);
|
|
170
|
+
if (distAtDown > OUT_OF_POSITION) {
|
|
171
|
+
outOfPositionDeaths.push({ account, distAtDown, atSec: Math.round(downStartMs / 1e3) });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const perPlayer = [];
|
|
177
|
+
if (replayUsable) {
|
|
178
|
+
for (const [account, samples] of perPlayerMap) {
|
|
179
|
+
if (samples.length === 0) continue;
|
|
180
|
+
const avg = samples.reduce((s, v) => s + v, 0) / samples.length;
|
|
181
|
+
const peak = Math.max(...samples);
|
|
182
|
+
perPlayer.push({ account, avgDistToTag: Math.round(avg), peakDistToTag: Math.round(peak) });
|
|
183
|
+
}
|
|
184
|
+
perPlayer.sort((a, b) => b.peakDistToTag - a.peakDistToTag);
|
|
185
|
+
} else if (degree === "coarse") {
|
|
186
|
+
for (const p of squad) {
|
|
187
|
+
const account = p?.account;
|
|
188
|
+
if (!account) continue;
|
|
189
|
+
const distToCom = p?.statsAll?.[0]?.distToCom;
|
|
190
|
+
if (typeof distToCom !== "number") continue;
|
|
191
|
+
perPlayer.push({ account, avgDistToTag: distToCom, peakDistToTag: distToCom });
|
|
192
|
+
}
|
|
193
|
+
perPlayer.sort((a, b) => b.peakDistToTag - a.peakDistToTag);
|
|
194
|
+
}
|
|
195
|
+
if (degree !== "full" || !replayUsable) {
|
|
196
|
+
return { degree, perPlayer, outOfPositionDeaths, squad: null, commander: null, deathClusters: [], figure: void 0 };
|
|
197
|
+
}
|
|
198
|
+
const squadNonCmdr = squad.filter((p) => !p?.hasCommanderTag && Array.isArray(p?.combatReplayData?.positions) && (p.combatReplayData.positions.length ?? 0) > 0);
|
|
199
|
+
const numTicks = tagPositions.length;
|
|
200
|
+
let spreadSum = 0;
|
|
201
|
+
let peakSpreadValue = 0;
|
|
202
|
+
let peakSpreadAtSec = 0;
|
|
203
|
+
let tagToCentroidSum = 0;
|
|
204
|
+
let peakLeadValue = 0;
|
|
205
|
+
let peakLeadAtSec = 0;
|
|
206
|
+
const deathCoords = [];
|
|
207
|
+
for (const player of squad) {
|
|
208
|
+
for (const { px, py } of linkedDeathHits(player, tagPositions, pollingRate)) {
|
|
209
|
+
deathCoords.push([px, py]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (let i = 0; i < numTicks; i++) {
|
|
213
|
+
const atSec = i * pollingRate / 1e3;
|
|
214
|
+
const [tx, ty] = tagPositions[i];
|
|
215
|
+
if (squadNonCmdr.length > 0) {
|
|
216
|
+
let spreadAtTick = 0;
|
|
217
|
+
let centroidX = 0;
|
|
218
|
+
let centroidY = 0;
|
|
219
|
+
for (const player of squadNonCmdr) {
|
|
220
|
+
const positions = player.combatReplayData.positions;
|
|
221
|
+
const playerStart = Number(player.combatReplayData?.start ?? 0);
|
|
222
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
223
|
+
const pIdx = clamp(i - playerOffset, 0, positions.length - 1);
|
|
224
|
+
const [px, py] = positions[pIdx];
|
|
225
|
+
spreadAtTick += Math.hypot(px - tx, py - ty) / inchToPixel;
|
|
226
|
+
centroidX += px;
|
|
227
|
+
centroidY += py;
|
|
228
|
+
}
|
|
229
|
+
const avgSpreadAtTick = spreadAtTick / squadNonCmdr.length;
|
|
230
|
+
spreadSum += avgSpreadAtTick;
|
|
231
|
+
if (avgSpreadAtTick > peakSpreadValue) {
|
|
232
|
+
peakSpreadValue = avgSpreadAtTick;
|
|
233
|
+
peakSpreadAtSec = atSec;
|
|
234
|
+
}
|
|
235
|
+
centroidX /= squadNonCmdr.length;
|
|
236
|
+
centroidY /= squadNonCmdr.length;
|
|
237
|
+
const tagToCentroidDist = Math.hypot(tx - centroidX, ty - centroidY) / inchToPixel;
|
|
238
|
+
tagToCentroidSum += tagToCentroidDist;
|
|
239
|
+
if (tagToCentroidDist > peakLeadValue) {
|
|
240
|
+
peakLeadValue = tagToCentroidDist;
|
|
241
|
+
peakLeadAtSec = atSec;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const avgSpread = numTicks > 0 ? spreadSum / numTicks : 0;
|
|
246
|
+
const peakAvgRatio = avgSpread > 0 ? peakSpreadValue / avgSpread : 1;
|
|
247
|
+
const cohesionNote = peakAvgRatio > 2.5 ? "tight then scattered" : "held together";
|
|
248
|
+
const squadFollowLag = numTicks > 0 ? tagToCentroidSum / numTicks : 0;
|
|
249
|
+
const CLUSTER_CELL = 150;
|
|
250
|
+
const cellMap = /* @__PURE__ */ new Map();
|
|
251
|
+
for (const [x, y] of deathCoords) {
|
|
252
|
+
const cellX = Math.floor(x / CLUSTER_CELL);
|
|
253
|
+
const cellY = Math.floor(y / CLUSTER_CELL);
|
|
254
|
+
const key = `${cellX},${cellY}`;
|
|
255
|
+
const existing = cellMap.get(key);
|
|
256
|
+
if (existing) {
|
|
257
|
+
existing.sumX += x;
|
|
258
|
+
existing.sumY += y;
|
|
259
|
+
existing.count++;
|
|
260
|
+
} else {
|
|
261
|
+
cellMap.set(key, { sumX: x, sumY: y, count: 1 });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const deathClusters = Array.from(cellMap.values()).map(({ sumX, sumY, count }) => ({ x: sumX / count, y: sumY / count, count })).sort((a, b) => b.count - a.count).slice(0, 6);
|
|
265
|
+
const stride = Math.ceil(1e3 / pollingRate);
|
|
266
|
+
const tagPath = [];
|
|
267
|
+
for (let i = 0; i < tagPositions.length; i += stride) {
|
|
268
|
+
tagPath.push(tagPositions[i]);
|
|
269
|
+
}
|
|
270
|
+
const downCoords = [];
|
|
271
|
+
for (const player of squad) {
|
|
272
|
+
const replay = player?.combatReplayData;
|
|
273
|
+
if (!replay || !Array.isArray(replay.down) || !Array.isArray(replay.positions)) continue;
|
|
274
|
+
const playerStart = Number(replay.start ?? 0);
|
|
275
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
276
|
+
for (const entry of replay.down) {
|
|
277
|
+
if (!Array.isArray(entry)) continue;
|
|
278
|
+
const downStartMs = Number(entry[0]);
|
|
279
|
+
if (!Number.isFinite(downStartMs) || downStartMs < 0) continue;
|
|
280
|
+
const pollIndex = Math.floor(downStartMs / pollingRate);
|
|
281
|
+
const playerIdx = clamp(pollIndex - playerOffset, 0, replay.positions.length - 1);
|
|
282
|
+
const [px, py] = replay.positions[playerIdx];
|
|
283
|
+
downCoords.push([px, py]);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const spreadTimeline = [];
|
|
287
|
+
let sumMassX = 0;
|
|
288
|
+
let sumMassY = 0;
|
|
289
|
+
let massCount = 0;
|
|
290
|
+
let minX = Infinity;
|
|
291
|
+
let maxX = -Infinity;
|
|
292
|
+
let minY = Infinity;
|
|
293
|
+
let maxY = -Infinity;
|
|
294
|
+
for (let i = 0; i < numTicks; i += stride) {
|
|
295
|
+
const atSec = i * pollingRate / 1e3;
|
|
296
|
+
const [tx, ty] = tagPositions[i];
|
|
297
|
+
let spreadAtTick = 0;
|
|
298
|
+
let centroidX = 0;
|
|
299
|
+
let centroidY = 0;
|
|
300
|
+
for (const player of squadNonCmdr) {
|
|
301
|
+
const positions = player.combatReplayData.positions;
|
|
302
|
+
const playerStart = Number(player.combatReplayData?.start ?? 0);
|
|
303
|
+
const playerOffset = Math.floor(playerStart / pollingRate);
|
|
304
|
+
const pIdx = clamp(i - playerOffset, 0, positions.length - 1);
|
|
305
|
+
const [px, py] = positions[pIdx];
|
|
306
|
+
spreadAtTick += Math.hypot(px - tx, py - ty) / inchToPixel;
|
|
307
|
+
centroidX += px;
|
|
308
|
+
centroidY += py;
|
|
309
|
+
}
|
|
310
|
+
if (squadNonCmdr.length > 0) {
|
|
311
|
+
centroidX /= squadNonCmdr.length;
|
|
312
|
+
centroidY /= squadNonCmdr.length;
|
|
313
|
+
spreadTimeline.push([atSec, Math.round(spreadAtTick / squadNonCmdr.length)]);
|
|
314
|
+
sumMassX += centroidX;
|
|
315
|
+
sumMassY += centroidY;
|
|
316
|
+
minX = Math.min(minX, centroidX);
|
|
317
|
+
maxX = Math.max(maxX, centroidX);
|
|
318
|
+
minY = Math.min(minY, centroidY);
|
|
319
|
+
maxY = Math.max(maxY, centroidY);
|
|
320
|
+
massCount++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const massX = massCount > 0 ? sumMassX / massCount : 0;
|
|
324
|
+
const massY = massCount > 0 ? sumMassY / massCount : 0;
|
|
325
|
+
const massR = massCount > 0 ? Math.hypot(maxX - minX, maxY - minY) / 2 : 0;
|
|
326
|
+
const sizes = meta.sizes ?? [0, 0];
|
|
327
|
+
const figure = {
|
|
328
|
+
map: { sizes, inchToPixel },
|
|
329
|
+
tagPath,
|
|
330
|
+
squadMass: { x: Math.round(massX), y: Math.round(massY), r: Math.round(massR) },
|
|
331
|
+
deaths: deathCoords,
|
|
332
|
+
downs: downCoords,
|
|
333
|
+
spread: spreadTimeline,
|
|
334
|
+
peakSpread: Math.round(peakSpreadValue)
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
degree,
|
|
338
|
+
perPlayer,
|
|
339
|
+
outOfPositionDeaths,
|
|
340
|
+
squad: {
|
|
341
|
+
avgSpread: Math.round(avgSpread),
|
|
342
|
+
peakSpread: { value: Math.round(peakSpreadValue), atSec: peakSpreadAtSec },
|
|
343
|
+
cohesionNote
|
|
344
|
+
},
|
|
345
|
+
commander: {
|
|
346
|
+
account: commander?.account ?? "Unknown",
|
|
347
|
+
peakLeadFromSquad: { value: Math.round(peakLeadValue), atSec: peakLeadAtSec },
|
|
348
|
+
squadFollowLag: Math.round(squadFollowLag)
|
|
349
|
+
},
|
|
350
|
+
deathClusters,
|
|
351
|
+
figure
|
|
352
|
+
};
|
|
353
|
+
}
|
|
89
354
|
export {
|
|
90
355
|
DEFAULT_DISRUPTION_METHOD,
|
|
91
356
|
METRICS_SPEC,
|
|
92
357
|
NON_DAMAGING_CONDITIONS,
|
|
358
|
+
OUT_OF_POSITION,
|
|
93
359
|
PROFESSION_COLORS,
|
|
94
360
|
ROLLUP_SOURCES_VERSION,
|
|
95
361
|
ReportSchemaError,
|
|
@@ -97,12 +363,14 @@ export {
|
|
|
97
363
|
applySquadStabilityGeneration,
|
|
98
364
|
buildConditionIconMap,
|
|
99
365
|
buildRollupData,
|
|
366
|
+
classifyDegree,
|
|
100
367
|
compareRunSets,
|
|
101
368
|
computeDownContribution,
|
|
102
369
|
computeIncomingDisruptions,
|
|
103
370
|
computeOutgoingConditions,
|
|
104
371
|
computeOutgoingCrowdControl,
|
|
105
372
|
computePlayerAggregation,
|
|
373
|
+
computePositioning,
|
|
106
374
|
computeSquadBarrier,
|
|
107
375
|
computeSquadHealing,
|
|
108
376
|
createPlayerAggregationAccumulators,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiapps/bridge-metrics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AxiBridge report aggregation core: player aggregation, rollup builder, metric extractors",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"publishConfig": { "access": "public" },
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "tsup",
|
|
36
|
+
"prepare": "tsup",
|
|
36
37
|
"prepublishOnly": "tsup",
|
|
37
38
|
"test": "vitest run --maxWorkers=2"
|
|
38
39
|
},
|