Add SkillProgressionAnomalyAnalyser
Some checks failed
Tests / Run tests on PHP v8.4 (push) Failing after 1s
Tests / Run tests on PHP v8.5 (push) Failing after 0s

Compares consecutive perks-snapshot rows per Steam ID and emits a
SkillProgressionAnomalyProblem for any single skill whose level gained
more than THRESHOLD_DELTA between two snapshots. Login/Logout/LevelUp
event rows are skipped via a perk-pair regex check on the bracketed
event field.

Threshold of 3 reflects PZ's slow leveling pace: typical session bridges
should not produce four-or-more level jumps in a single skill. The
constant is documented inline so operators can tune for modded XP
servers without touching analysis logic.

Synthetic fixture extended with a PlayerSuspect Steam ID carrying two
snapshots: Strength jumps 2 -> 10 (delta +8, triggers), Fitness jumps
2 -> 8 (+6, triggers), Maintenance jumps 0 -> 3 (+3, exactly at
threshold, does NOT trigger). The existing single-snapshot players
remain noise-free.
This commit is contained in:
2026-04-30 22:43:44 +00:00
parent ba3fae8736
commit 0c90e40a28
6 changed files with 267 additions and 3 deletions

View File

@@ -0,0 +1,107 @@
<?php
namespace IndifferentKetchup\Codex\Analysis\ProjectZomboid;
use IndifferentKetchup\Codex\Analysis\InsightInterface;
use IndifferentKetchup\Codex\Analysis\Problem;
/**
* Problem emitted by SkillProgressionAnomalyAnalyser when a single skill
* gained more than the configured threshold between two consecutive
* snapshots of the same player. Coalesced by (Steam ID, skill).
*/
class SkillProgressionAnomalyProblem extends Problem
{
private string $steamId = '';
private string $player = '';
private string $skill = '';
private int $fromLevel = 0;
private int $toLevel = 0;
private int $delta = 0;
public function setSteamId(string $steamId): static
{
$this->steamId = $steamId;
return $this;
}
public function setPlayer(string $player): static
{
$this->player = $player;
return $this;
}
public function setSkill(string $skill): static
{
$this->skill = $skill;
return $this;
}
public function setFromLevel(int $level): static
{
$this->fromLevel = $level;
return $this;
}
public function setToLevel(int $level): static
{
$this->toLevel = $level;
return $this;
}
public function setDelta(int $delta): static
{
$this->delta = $delta;
return $this;
}
public function getSteamId(): string
{
return $this->steamId;
}
public function getPlayer(): string
{
return $this->player;
}
public function getSkill(): string
{
return $this->skill;
}
public function getFromLevel(): int
{
return $this->fromLevel;
}
public function getToLevel(): int
{
return $this->toLevel;
}
public function getDelta(): int
{
return $this->delta;
}
public function getMessage(): string
{
return sprintf(
'Player %s (%s) gained %d levels of %s between snapshots (%d to %d).',
$this->player,
$this->steamId,
$this->delta,
$this->skill,
$this->fromLevel,
$this->toLevel
);
}
public function isEqual(InsightInterface $insight): bool
{
return $insight instanceof self
&& $insight->getSteamId() === $this->steamId
&& $insight->getSkill() === $this->skill;
}
}