NEO-138: reward router reputation grants, Bruno quest-flow, docs.
Apply completionRewardBundle.reputationGrants via ReputationOperations with compensating rollback; thread faction stores through quest wiring; add operator chain Bruno helper and grid-contract accept success; update README and E7.M3 docs.pull/178/head
parent
599d87bd0d
commit
3466f1ee32
|
|
@ -0,0 +1,41 @@
|
||||||
|
meta {
|
||||||
|
name: Accept grid contract after operator chain
|
||||||
|
type: http
|
||||||
|
seq: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
docs {
|
||||||
|
NEO-138: complete operator chain via HTTP quest flow (rep grant +15 Grid Operators), then accept prototype_quest_grid_contract succeeds without manual standing seed.
|
||||||
|
Pre-request runs operator-chain-quest-flow-helper (gather/combat/refine intros + chain steps). No standing GET until NEO-139.
|
||||||
|
}
|
||||||
|
|
||||||
|
script:pre-request {
|
||||||
|
const { completeOperatorChainQuestFlow } = require("./scripts/operator-chain-quest-flow-helper");
|
||||||
|
await completeOperatorChainQuestFlow(bru);
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{baseUrl}}/game/players/{{playerId}}/quests/prototype_quest_grid_contract/accept
|
||||||
|
body: json
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"schemaVersion": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests {
|
||||||
|
test("accepts grid contract when operator chain rep grant met minStanding", function () {
|
||||||
|
expect(res.getStatus()).to.equal(200);
|
||||||
|
const body = res.getBody();
|
||||||
|
expect(body.schemaVersion).to.equal(1);
|
||||||
|
expect(body.accepted).to.equal(true);
|
||||||
|
expect(body.reasonCode).to.equal(undefined);
|
||||||
|
expect(body.quest).to.be.an("object");
|
||||||
|
expect(body.quest.questId).to.equal("prototype_quest_grid_contract");
|
||||||
|
expect(body.quest.status).to.equal("active");
|
||||||
|
expect(body.quest.currentStepIndex).to.equal(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ meta {
|
||||||
docs {
|
docs {
|
||||||
NEO-137: prototype_quest_grid_contract requires completed operator chain and Grid Operators standing >= 15.
|
NEO-137: prototype_quest_grid_contract requires completed operator chain and Grid Operators standing >= 15.
|
||||||
Pre-request marks operator chain completed via POST …/__dev/quest-fixture (no rep delivery; standing stays 0).
|
Pre-request marks operator chain completed via POST …/__dev/quest-fixture (no rep delivery; standing stays 0).
|
||||||
Success Bruno deferred to NEO-138 when operator-chain rep grants land.
|
Success sibling: seq 11 Accept grid contract after operator chain (NEO-138 organic rep grant).
|
||||||
}
|
}
|
||||||
|
|
||||||
script:pre-request {
|
script:pre-request {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
const axios = require("axios");
|
||||||
|
const {
|
||||||
|
resetAllPrototypeQuestProgress,
|
||||||
|
resetPrototypeResourceNodes,
|
||||||
|
} = require("./bruno-dev-fixture-helper");
|
||||||
|
const { resetPrototypeCombatTargets } = require("./combat-targets-reset-helper");
|
||||||
|
const { clearInventory, sumItemQuantity, getInventory } = require("./inventory-api-helper");
|
||||||
|
const {
|
||||||
|
moveNearPrototypeNpc,
|
||||||
|
PULSE_COOLDOWN_MS,
|
||||||
|
MAX_PULSE_DEFEAT_ATTEMPTS,
|
||||||
|
sleep,
|
||||||
|
} = require("./prototype-npc-bruno-helper");
|
||||||
|
|
||||||
|
const GATHER_QUEST_ID = "prototype_quest_gather_intro";
|
||||||
|
const COMBAT_QUEST_ID = "prototype_quest_combat_intro";
|
||||||
|
const REFINE_QUEST_ID = "prototype_quest_refine_intro";
|
||||||
|
const CHAIN_QUEST_ID = "prototype_quest_operator_chain";
|
||||||
|
const DELTA_NODE_ID = "prototype_urban_bulk_delta";
|
||||||
|
const CAST_SLOT_INDEX = 3;
|
||||||
|
|
||||||
|
function resolveConfig(bru) {
|
||||||
|
return {
|
||||||
|
baseUrl: bru.getEnvVar("baseUrl") || bru.getVar("baseUrl"),
|
||||||
|
playerId: bru.getEnvVar("playerId") || bru.getVar("playerId"),
|
||||||
|
jsonHeaders: { headers: { "Content-Type": "application/json" } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getQuestProgress(bru) {
|
||||||
|
const { baseUrl, playerId } = resolveConfig(bru);
|
||||||
|
const response = await axios.get(`${baseUrl}/game/players/${playerId}/quest-progress`, {
|
||||||
|
validateStatus: () => true,
|
||||||
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`quest-progress GET failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function questRow(body, questId) {
|
||||||
|
const row = body.quests.find((x) => x.questId === questId);
|
||||||
|
if (!row) {
|
||||||
|
throw new Error(`missing quest row ${questId}: ${JSON.stringify(body.quests)}`);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acceptQuest(bru, questId) {
|
||||||
|
const { baseUrl, playerId, jsonHeaders } = resolveConfig(bru);
|
||||||
|
const response = await axios.post(
|
||||||
|
`${baseUrl}/game/players/${playerId}/quests/${questId}/accept`,
|
||||||
|
{ schemaVersion: 1 },
|
||||||
|
{ ...jsonHeaders, validateStatus: () => true },
|
||||||
|
);
|
||||||
|
if (response.status !== 200 || response.data?.accepted !== true) {
|
||||||
|
throw new Error(`accept ${questId} failed: ${response.status} ${JSON.stringify(response.data)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveNear(bru, x, z) {
|
||||||
|
const { baseUrl, playerId, jsonHeaders } = resolveConfig(bru);
|
||||||
|
await axios.post(
|
||||||
|
`${baseUrl}/game/players/${playerId}/move`,
|
||||||
|
{ schemaVersion: 1, target: { x, y: 0.9, z } },
|
||||||
|
jsonHeaders,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryGather(bru, interactableId) {
|
||||||
|
const { baseUrl, playerId, jsonHeaders } = resolveConfig(bru);
|
||||||
|
const response = await axios.post(
|
||||||
|
`${baseUrl}/game/players/${playerId}/interact`,
|
||||||
|
{ schemaVersion: 1, interactableId },
|
||||||
|
{ ...jsonHeaders, validateStatus: () => true },
|
||||||
|
);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`gather ${interactableId} HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
if (response.data?.allowed === true || response.data?.reasonCode === "node_depleted") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.data?.reasonCode === "out_of_range") {
|
||||||
|
throw new Error(`gather ${interactableId} out_of_range — moveNear before interact`);
|
||||||
|
}
|
||||||
|
throw new Error(`gather ${interactableId} denied: ${JSON.stringify(response.data)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function craft(bru, recipeId) {
|
||||||
|
const { baseUrl, playerId, jsonHeaders } = resolveConfig(bru);
|
||||||
|
const response = await axios.post(
|
||||||
|
`${baseUrl}/game/players/${playerId}/craft`,
|
||||||
|
{ schemaVersion: 1, recipeId },
|
||||||
|
{ ...jsonHeaders, validateStatus: () => true },
|
||||||
|
);
|
||||||
|
if (response.status !== 200 || response.data?.success !== true) {
|
||||||
|
throw new Error(`craft ${recipeId} failed: ${response.status} ${JSON.stringify(response.data)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function defeatNpc(bru, targetId) {
|
||||||
|
const { baseUrl, playerId, jsonHeaders } = resolveConfig(bru);
|
||||||
|
await moveNearPrototypeNpc(baseUrl, playerId, targetId, jsonHeaders);
|
||||||
|
await axios.post(
|
||||||
|
`${baseUrl}/game/players/${playerId}/target/select`,
|
||||||
|
{ schemaVersion: 1, targetId },
|
||||||
|
jsonHeaders,
|
||||||
|
);
|
||||||
|
await sleep(bru, PULSE_COOLDOWN_MS);
|
||||||
|
const castBody = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
slotIndex: CAST_SLOT_INDEX,
|
||||||
|
abilityId: "prototype_pulse",
|
||||||
|
targetId,
|
||||||
|
};
|
||||||
|
for (let i = 0; i < MAX_PULSE_DEFEAT_ATTEMPTS; i += 1) {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${baseUrl}/game/players/${playerId}/ability-cast`,
|
||||||
|
castBody,
|
||||||
|
jsonHeaders,
|
||||||
|
);
|
||||||
|
const body = response.data;
|
||||||
|
if (!body?.accepted || !body?.combatResolution) {
|
||||||
|
throw new Error(
|
||||||
|
`cast ${i + 1} vs ${targetId} failed (${body?.reasonCode ?? "no_reason"}): ${JSON.stringify(body)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (body.combatResolution.targetDefeated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (i < MAX_PULSE_DEFEAT_ATTEMPTS - 1) {
|
||||||
|
await sleep(bru, PULSE_COOLDOWN_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`did not defeat ${targetId} within ${MAX_PULSE_DEFEAT_ATTEMPTS} pulses`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeGatherIntro(bru) {
|
||||||
|
await acceptQuest(bru, GATHER_QUEST_ID);
|
||||||
|
await moveNear(bru, 10, -6);
|
||||||
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
await tryGather(bru, "prototype_resource_node_alpha");
|
||||||
|
}
|
||||||
|
const progress = await getQuestProgress(bru);
|
||||||
|
const row = questRow(progress, GATHER_QUEST_ID);
|
||||||
|
if (row.status !== "completed") {
|
||||||
|
throw new Error(`gather intro not completed: ${JSON.stringify(row)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeCombatIntro(bru) {
|
||||||
|
const { baseUrl, playerId, jsonHeaders } = resolveConfig(bru);
|
||||||
|
await acceptQuest(bru, COMBAT_QUEST_ID);
|
||||||
|
await axios.post(
|
||||||
|
`${baseUrl}/game/players/${playerId}/hotbar-loadout`,
|
||||||
|
{
|
||||||
|
schemaVersion: 1,
|
||||||
|
slots: [{ slotIndex: CAST_SLOT_INDEX, abilityId: "prototype_pulse" }],
|
||||||
|
},
|
||||||
|
jsonHeaders,
|
||||||
|
);
|
||||||
|
await defeatNpc(bru, "prototype_npc_melee");
|
||||||
|
await defeatNpc(bru, "prototype_npc_ranged");
|
||||||
|
await defeatNpc(bru, "prototype_npc_elite");
|
||||||
|
const progress = await getQuestProgress(bru);
|
||||||
|
const row = questRow(progress, COMBAT_QUEST_ID);
|
||||||
|
if (row.status !== "completed") {
|
||||||
|
throw new Error(`combat intro not completed: ${JSON.stringify(row)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeRefineIntro(bru) {
|
||||||
|
await acceptQuest(bru, REFINE_QUEST_ID);
|
||||||
|
const scrap = sumItemQuantity(await getInventory(bru), "scrap_metal_bulk");
|
||||||
|
if (scrap < 5) {
|
||||||
|
throw new Error(`refine intro needs >=5 scrap, have ${scrap}`);
|
||||||
|
}
|
||||||
|
await craft(bru, "refine_scrap_standard");
|
||||||
|
const progress = await getQuestProgress(bru);
|
||||||
|
const row = questRow(progress, REFINE_QUEST_ID);
|
||||||
|
if (row.status !== "completed") {
|
||||||
|
throw new Error(`refine intro not completed: ${JSON.stringify(row)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeOperatorChain(bru) {
|
||||||
|
await acceptQuest(bru, CHAIN_QUEST_ID);
|
||||||
|
await moveNear(bru, 16, -2);
|
||||||
|
await tryGather(bru, DELTA_NODE_ID);
|
||||||
|
let progress = await getQuestProgress(bru);
|
||||||
|
let row = questRow(progress, CHAIN_QUEST_ID);
|
||||||
|
if (row.status !== "active" || row.currentStepIndex !== 1) {
|
||||||
|
throw new Error(`chain step 1 not advanced: ${JSON.stringify(row)}`);
|
||||||
|
}
|
||||||
|
await craft(bru, "refine_scrap_standard");
|
||||||
|
progress = await getQuestProgress(bru);
|
||||||
|
row = questRow(progress, CHAIN_QUEST_ID);
|
||||||
|
if (row.status !== "active" || row.currentStepIndex !== 2) {
|
||||||
|
throw new Error(`chain step 2 not advanced: ${JSON.stringify(row)}`);
|
||||||
|
}
|
||||||
|
await craft(bru, "make_field_stim_mk0");
|
||||||
|
progress = await getQuestProgress(bru);
|
||||||
|
row = questRow(progress, CHAIN_QUEST_ID);
|
||||||
|
if (row.status !== "completed") {
|
||||||
|
throw new Error(`operator chain not completed: ${JSON.stringify(row)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEO-138: organic operator-chain completion via HTTP (gather/craft/encounter wiring).
|
||||||
|
* Applies +15 Grid Operators rep on chain completion reward delivery.
|
||||||
|
*/
|
||||||
|
async function completeOperatorChainQuestFlow(bru) {
|
||||||
|
await resetPrototypeResourceNodes(bru);
|
||||||
|
await clearInventory(bru);
|
||||||
|
await resetPrototypeCombatTargets(bru);
|
||||||
|
await resetAllPrototypeQuestProgress(bru);
|
||||||
|
await completeGatherIntro(bru);
|
||||||
|
await completeCombatIntro(bru);
|
||||||
|
await completeRefineIntro(bru);
|
||||||
|
await completeOperatorChain(bru);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
completeOperatorChainQuestFlow,
|
||||||
|
CHAIN_QUEST_ID,
|
||||||
|
};
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
| **Module ID** | E7.M3 |
|
| **Module ID** | E7.M3 |
|
||||||
| **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) |
|
| **Epic** | [Epic 7 — Quest / Faction](../epics/epic_07_quest_faction.md) |
|
||||||
| **Stage target** | Pre-production |
|
| **Stage target** | Pre-production |
|
||||||
| **Status** | In Progress — **E7M3-01 catalog landed** ([NEO-133](https://linear.app/neon-sprawl/issue/NEO-133)); **E7M3-02 server faction catalog load landed** ([NEO-134](https://linear.app/neon-sprawl/issue/NEO-134)): fail-fast `content/factions/*_factions.json`, `IFactionDefinitionRegistry`, quest faction cross-ref + E7M3 bundle/grid gates; **E7M3-03 faction standing + reputation delta stores landed** ([NEO-135](https://linear.app/neon-sprawl/issue/NEO-135)): `IFactionStandingStore`, `IReputationDeltaStore`, in-memory + Postgres `V009`/`V010`; **E7M3-04 `ReputationOperations` landed** ([NEO-136](https://linear.app/neon-sprawl/issue/NEO-136)): auditable `TryApplyDelta` orchestration ([NEO-136 plan](../../plans/NEO-136-implementation-plan.md)); **E7M3-05 `FactionGateOperations` landed** ([NEO-137](https://linear.app/neon-sprawl/issue/NEO-137)): quest accept gate eval + `faction_gate_blocked` ([NEO-137 plan](../../plans/NEO-137-implementation-plan.md)). Slice 3 backlog [E7M3-pre-production-backlog.md](../../plans/E7M3-pre-production-backlog.md): **E7M3-06** [NEO-138](https://linear.app/neon-sprawl/issue/NEO-138) → **E7M3-11** [NEO-143](https://linear.app/neon-sprawl/issue/NEO-143). Upstream: E7.M1 **Ready**, E7.M2 **Ready**. |
|
| **Status** | In Progress — **E7M3-01 catalog landed** ([NEO-133](https://linear.app/neon-sprawl/issue/NEO-133)); **E7M3-02 server faction catalog load landed** ([NEO-134](https://linear.app/neon-sprawl/issue/NEO-134)): fail-fast `content/factions/*_factions.json`, `IFactionDefinitionRegistry`, quest faction cross-ref + E7M3 bundle/grid gates; **E7M3-03 faction standing + reputation delta stores landed** ([NEO-135](https://linear.app/neon-sprawl/issue/NEO-135)): `IFactionStandingStore`, `IReputationDeltaStore`, in-memory + Postgres `V009`/`V010`; **E7M3-04 `ReputationOperations` landed** ([NEO-136](https://linear.app/neon-sprawl/issue/NEO-136)): auditable `TryApplyDelta` orchestration ([NEO-136 plan](../../plans/NEO-136-implementation-plan.md)); **E7M3-05 `FactionGateOperations` landed** ([NEO-137](https://linear.app/neon-sprawl/issue/NEO-137)): quest accept gate eval + `faction_gate_blocked` ([NEO-137 plan](../../plans/NEO-137-implementation-plan.md)); **E7M3-06 reward router rep grants landed** ([NEO-138](https://linear.app/neon-sprawl/issue/NEO-138)): `RewardRouterOperations` applies `reputationGrants` idempotently via `ReputationOperations` ([NEO-138 plan](../../plans/NEO-138-implementation-plan.md)). Slice 3 backlog [E7M3-pre-production-backlog.md](../../plans/E7M3-pre-production-backlog.md): **E7M3-07** [NEO-139](https://linear.app/neon-sprawl/issue/NEO-139) → **E7M3-11** [NEO-143](https://linear.app/neon-sprawl/issue/NEO-143). Upstream: E7.M1 **Ready**, E7.M2 **Ready**. |
|
||||||
| **Linear** | Label **`E7.M3`** · [E7M3-pre-production-backlog.md](../../plans/E7M3-pre-production-backlog.md) |
|
| **Linear** | Label **`E7.M3`** · [E7M3-pre-production-backlog.md](../../plans/E7M3-pre-production-backlog.md) |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -59,9 +59,9 @@ Other decisions settled by Linear AC, backlog kickoff defaults, and repo precede
|
||||||
|
|
||||||
## Acceptance criteria checklist
|
## Acceptance criteria checklist
|
||||||
|
|
||||||
- [ ] First-time quest completion applies rep grants exactly once per delivery key.
|
- [x] First-time quest completion applies rep grants exactly once per delivery key.
|
||||||
- [ ] Unknown faction id in bundle denies delivery before store record.
|
- [x] Unknown faction id in bundle denies delivery before store record.
|
||||||
- [ ] `dotnet test` covers rep happy path, idempotent replay, unknown-faction deny, and rollback paths.
|
- [x] `dotnet test` covers rep happy path, idempotent replay, unknown-faction deny, and rollback paths.
|
||||||
|
|
||||||
## Technical approach
|
## Technical approach
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Crafting;
|
using NeonSprawl.Server.Game.Crafting;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
|
|
@ -38,6 +39,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -84,6 +87,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -121,6 +126,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -160,6 +167,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -196,6 +205,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -233,6 +244,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -267,6 +280,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -297,6 +312,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -330,6 +347,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -365,6 +384,8 @@ public sealed class CraftOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -415,7 +436,9 @@ public sealed class CraftOperationsTests
|
||||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||||
services.GetRequiredService<IPlayerQuestStateStore>(),
|
services.GetRequiredService<IPlayerQuestStateStore>(),
|
||||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||||
services.GetRequiredService<IRewardDeliveryStore>());
|
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||||
|
services.GetRequiredService<IFactionStandingStore>(),
|
||||||
|
services.GetRequiredService<IReputationDeltaStore>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||||
|
|
@ -454,7 +477,9 @@ public sealed class CraftOperationsTests
|
||||||
IQuestDefinitionRegistry QuestRegistry,
|
IQuestDefinitionRegistry QuestRegistry,
|
||||||
IPlayerQuestStateStore ProgressStore,
|
IPlayerQuestStateStore ProgressStore,
|
||||||
IPlayerPerkStateStore PerkStore,
|
IPlayerPerkStateStore PerkStore,
|
||||||
IRewardDeliveryStore DeliveryStore);
|
IRewardDeliveryStore DeliveryStore,
|
||||||
|
IFactionStandingStore StandingStore,
|
||||||
|
IReputationDeltaStore AuditStore);
|
||||||
|
|
||||||
private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore
|
private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Encounters;
|
using NeonSprawl.Server.Game.Encounters;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
|
|
@ -46,6 +47,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -83,6 +86,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -124,6 +129,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -163,6 +170,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -195,6 +204,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var before);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var before);
|
||||||
|
|
||||||
|
|
@ -219,6 +230,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -257,6 +270,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -304,6 +319,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -348,6 +365,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -376,6 +395,8 @@ public sealed class EncounterCombatWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -432,7 +453,9 @@ public sealed class EncounterCombatWiringTests
|
||||||
services.GetRequiredService<ISkillLevelCurve>(),
|
services.GetRequiredService<ISkillLevelCurve>(),
|
||||||
services.GetRequiredService<PerkUnlockEngine>(),
|
services.GetRequiredService<PerkUnlockEngine>(),
|
||||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||||
services.GetRequiredService<IRewardDeliveryStore>());
|
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||||
|
services.GetRequiredService<IFactionStandingStore>(),
|
||||||
|
services.GetRequiredService<IReputationDeltaStore>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||||
|
|
@ -464,5 +487,7 @@ public sealed class EncounterCombatWiringTests
|
||||||
ISkillLevelCurve LevelCurve,
|
ISkillLevelCurve LevelCurve,
|
||||||
PerkUnlockEngine PerkUnlockEngine,
|
PerkUnlockEngine PerkUnlockEngine,
|
||||||
IPlayerPerkStateStore PerkStore,
|
IPlayerPerkStateStore PerkStore,
|
||||||
IRewardDeliveryStore DeliveryStore);
|
IRewardDeliveryStore DeliveryStore,
|
||||||
|
IFactionStandingStore StandingStore,
|
||||||
|
IReputationDeltaStore AuditStore);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Encounters;
|
using NeonSprawl.Server.Game.Encounters;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
|
|
@ -46,6 +47,8 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -95,6 +98,8 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterFirst);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterFirst);
|
||||||
|
|
||||||
|
|
@ -117,6 +122,8 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -181,6 +188,8 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -221,6 +230,8 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -263,6 +274,8 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -303,6 +316,8 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -341,6 +356,8 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -409,7 +426,9 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
services.GetRequiredService<ISkillLevelCurve>(),
|
services.GetRequiredService<ISkillLevelCurve>(),
|
||||||
services.GetRequiredService<PerkUnlockEngine>(),
|
services.GetRequiredService<PerkUnlockEngine>(),
|
||||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||||
services.GetRequiredService<IRewardDeliveryStore>());
|
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||||
|
services.GetRequiredService<IFactionStandingStore>(),
|
||||||
|
services.GetRequiredService<IReputationDeltaStore>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||||
|
|
@ -455,7 +474,9 @@ public sealed class EncounterCompletionOperationsTests
|
||||||
ISkillLevelCurve LevelCurve,
|
ISkillLevelCurve LevelCurve,
|
||||||
PerkUnlockEngine PerkUnlockEngine,
|
PerkUnlockEngine PerkUnlockEngine,
|
||||||
IPlayerPerkStateStore PerkStore,
|
IPlayerPerkStateStore PerkStore,
|
||||||
IRewardDeliveryStore DeliveryStore);
|
IRewardDeliveryStore DeliveryStore,
|
||||||
|
IFactionStandingStore StandingStore,
|
||||||
|
IReputationDeltaStore AuditStore);
|
||||||
|
|
||||||
private sealed class CompletionStoreDeniesMark(IEncounterCompletionStore inner) : IEncounterCompletionStore
|
private sealed class CompletionStoreDeniesMark(IEncounterCompletionStore inner) : IEncounterCompletionStore
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Gathering;
|
using NeonSprawl.Server.Game.Gathering;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
|
|
@ -39,6 +40,8 @@ public sealed class GatherOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||||
|
|
@ -95,6 +98,8 @@ public sealed class GatherOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||||
|
|
@ -133,6 +138,8 @@ public sealed class GatherOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
Assert.True(gather.Success);
|
Assert.True(gather.Success);
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +163,8 @@ public sealed class GatherOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||||
|
|
@ -193,6 +202,8 @@ public sealed class GatherOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
var rowExists = deps.InstanceStore.TryGetRemainingGathers(unknownId, out _);
|
var rowExists = deps.InstanceStore.TryGetRemainingGathers(unknownId, out _);
|
||||||
|
|
@ -228,6 +239,8 @@ public sealed class GatherOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||||
|
|
@ -264,6 +277,8 @@ public sealed class GatherOperationsTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||||
|
|
@ -290,7 +305,9 @@ public sealed class GatherOperationsTests
|
||||||
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
services.GetRequiredService<IQuestDefinitionRegistry>(),
|
||||||
services.GetRequiredService<IPlayerQuestStateStore>(),
|
services.GetRequiredService<IPlayerQuestStateStore>(),
|
||||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||||
services.GetRequiredService<IRewardDeliveryStore>());
|
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||||
|
services.GetRequiredService<IFactionStandingStore>(),
|
||||||
|
services.GetRequiredService<IReputationDeltaStore>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CountItem(PlayerInventorySnapshot snapshot, string itemId)
|
private static int CountItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||||
|
|
@ -330,7 +347,9 @@ public sealed class GatherOperationsTests
|
||||||
IQuestDefinitionRegistry QuestRegistry,
|
IQuestDefinitionRegistry QuestRegistry,
|
||||||
IPlayerQuestStateStore ProgressStore,
|
IPlayerQuestStateStore ProgressStore,
|
||||||
IPlayerPerkStateStore PerkStore,
|
IPlayerPerkStateStore PerkStore,
|
||||||
IRewardDeliveryStore DeliveryStore);
|
IRewardDeliveryStore DeliveryStore,
|
||||||
|
IFactionStandingStore StandingStore,
|
||||||
|
IReputationDeltaStore AuditStore);
|
||||||
|
|
||||||
private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore
|
private sealed class ProgressionStoreAlwaysMissing : IPlayerSkillProgressionStore
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using Microsoft.Extensions.Time.Testing;
|
||||||
using NeonSprawl.Server.Game.AbilityInput;
|
using NeonSprawl.Server.Game.AbilityInput;
|
||||||
using NeonSprawl.Server.Game.Combat;
|
using NeonSprawl.Server.Game.Combat;
|
||||||
using NeonSprawl.Server.Game.Crafting;
|
using NeonSprawl.Server.Game.Crafting;
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Gathering;
|
using NeonSprawl.Server.Game.Gathering;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
|
|
@ -71,6 +72,8 @@ public sealed class SalvageActivityDeniedRegistryWebApplicationFactory : WebAppl
|
||||||
d.ServiceType == typeof(IResourceNodeInstanceStore) ||
|
d.ServiceType == typeof(IResourceNodeInstanceStore) ||
|
||||||
d.ServiceType == typeof(IPlayerPerkStateStore) ||
|
d.ServiceType == typeof(IPlayerPerkStateStore) ||
|
||||||
d.ServiceType == typeof(IPlayerQuestStateStore) ||
|
d.ServiceType == typeof(IPlayerQuestStateStore) ||
|
||||||
|
d.ServiceType == typeof(IFactionStandingStore) ||
|
||||||
|
d.ServiceType == typeof(IReputationDeltaStore) ||
|
||||||
d.ServiceType == typeof(TimeProvider) ||
|
d.ServiceType == typeof(TimeProvider) ||
|
||||||
d.ServiceType == typeof(IPlayerAbilityCooldownStore) ||
|
d.ServiceType == typeof(IPlayerAbilityCooldownStore) ||
|
||||||
d.ServiceType == typeof(ISkillDefinitionRegistry) ||
|
d.ServiceType == typeof(ISkillDefinitionRegistry) ||
|
||||||
|
|
@ -92,6 +95,8 @@ public sealed class SalvageActivityDeniedRegistryWebApplicationFactory : WebAppl
|
||||||
services.AddSingleton<IResourceNodeInstanceStore, InMemoryResourceNodeInstanceStore>();
|
services.AddSingleton<IResourceNodeInstanceStore, InMemoryResourceNodeInstanceStore>();
|
||||||
services.AddSingleton<IPlayerPerkStateStore, InMemoryPlayerPerkStateStore>();
|
services.AddSingleton<IPlayerPerkStateStore, InMemoryPlayerPerkStateStore>();
|
||||||
services.AddSingleton<IPlayerQuestStateStore, InMemoryPlayerQuestStateStore>();
|
services.AddSingleton<IPlayerQuestStateStore, InMemoryPlayerQuestStateStore>();
|
||||||
|
services.AddSingleton<IFactionStandingStore, InMemoryFactionStandingStore>();
|
||||||
|
services.AddSingleton<IReputationDeltaStore, InMemoryReputationDeltaStore>();
|
||||||
services.AddSingleton<IPlayerAbilityCooldownStore, InMemoryPlayerAbilityCooldownStore>();
|
services.AddSingleton<IPlayerAbilityCooldownStore, InMemoryPlayerAbilityCooldownStore>();
|
||||||
services.AddSingleton<ISkillDefinitionRegistry, SalvageActivityDeniedSkillRegistry>();
|
services.AddSingleton<ISkillDefinitionRegistry, SalvageActivityDeniedSkillRegistry>();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,7 @@ public sealed class QuestAcceptApiTests
|
||||||
var perkStore = services.GetRequiredService<IPlayerPerkStateStore>();
|
var perkStore = services.GetRequiredService<IPlayerPerkStateStore>();
|
||||||
var deliveryStore = services.GetRequiredService<IRewardDeliveryStore>();
|
var deliveryStore = services.GetRequiredService<IRewardDeliveryStore>();
|
||||||
var standingStore = services.GetRequiredService<IFactionStandingStore>();
|
var standingStore = services.GetRequiredService<IFactionStandingStore>();
|
||||||
|
var auditStore = services.GetRequiredService<IReputationDeltaStore>();
|
||||||
var timeProvider = TimeProvider.System;
|
var timeProvider = TimeProvider.System;
|
||||||
Assert.True(QuestStateOperations.TryAccept(
|
Assert.True(QuestStateOperations.TryAccept(
|
||||||
PlayerId,
|
PlayerId,
|
||||||
|
|
@ -236,6 +237,7 @@ public sealed class QuestAcceptApiTests
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
standingStore,
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider).Success);
|
timeProvider).Success);
|
||||||
Assert.True(QuestStateOperations.TryMarkComplete(
|
Assert.True(QuestStateOperations.TryMarkComplete(
|
||||||
PlayerId,
|
PlayerId,
|
||||||
|
|
@ -250,6 +252,8 @@ public sealed class QuestAcceptApiTests
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider).Success);
|
timeProvider).Success);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,8 @@ public sealed class QuestObjectiveWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -266,6 +268,8 @@ public sealed class QuestObjectiveWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
Assert.True(result.Success);
|
Assert.True(result.Success);
|
||||||
}
|
}
|
||||||
|
|
@ -304,6 +308,7 @@ public sealed class QuestObjectiveWiringTests
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
deps.StandingStore,
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static QuestStateOperationResult TryAdvanceStep(QuestWiringTestDependencies deps, string questId, int step) =>
|
private static QuestStateOperationResult TryAdvanceStep(QuestWiringTestDependencies deps, string questId, int step) =>
|
||||||
|
|
@ -321,6 +326,8 @@ public sealed class QuestObjectiveWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static QuestStateOperationResult TryMarkComplete(QuestWiringTestDependencies deps, string questId) =>
|
private static QuestStateOperationResult TryMarkComplete(QuestWiringTestDependencies deps, string questId) =>
|
||||||
|
|
@ -337,6 +344,8 @@ public sealed class QuestObjectiveWiringTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static void AcceptAndMarkComplete(QuestWiringTestDependencies deps, string questId)
|
private static void AcceptAndMarkComplete(QuestWiringTestDependencies deps, string questId)
|
||||||
|
|
@ -361,6 +370,8 @@ public sealed class QuestObjectiveWiringTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static CraftResult TryCraft(QuestWiringTestDependencies deps, string recipeId) =>
|
private static CraftResult TryCraft(QuestWiringTestDependencies deps, string recipeId) =>
|
||||||
|
|
@ -379,6 +390,8 @@ public sealed class QuestObjectiveWiringTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||||
|
|
@ -445,6 +458,7 @@ public sealed class QuestObjectiveWiringTests
|
||||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||||
services.GetRequiredService<IRewardDeliveryStore>(),
|
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||||
services.GetRequiredService<IFactionStandingStore>(),
|
services.GetRequiredService<IFactionStandingStore>(),
|
||||||
|
services.GetRequiredService<IReputationDeltaStore>(),
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -468,5 +482,6 @@ public sealed class QuestObjectiveWiringTests
|
||||||
IPlayerPerkStateStore PerkStore,
|
IPlayerPerkStateStore PerkStore,
|
||||||
IRewardDeliveryStore DeliveryStore,
|
IRewardDeliveryStore DeliveryStore,
|
||||||
IFactionStandingStore StandingStore,
|
IFactionStandingStore StandingStore,
|
||||||
|
IReputationDeltaStore AuditStore,
|
||||||
TimeProvider TimeProvider);
|
TimeProvider TimeProvider);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,8 @@ public sealed class QuestProgressApiTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
Assert.True(result.Success);
|
Assert.True(result.Success);
|
||||||
}
|
}
|
||||||
|
|
@ -402,6 +404,7 @@ public sealed class QuestProgressApiTests
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
deps.StandingStore,
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static QuestStateOperationResult TryMarkComplete(QuestWiringTestDependencies deps, string questId) =>
|
private static QuestStateOperationResult TryMarkComplete(QuestWiringTestDependencies deps, string questId) =>
|
||||||
|
|
@ -418,6 +421,8 @@ public sealed class QuestProgressApiTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static void AcceptAndMarkComplete(QuestWiringTestDependencies deps, string questId)
|
private static void AcceptAndMarkComplete(QuestWiringTestDependencies deps, string questId)
|
||||||
|
|
@ -442,6 +447,8 @@ public sealed class QuestProgressApiTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static CraftResult TryCraft(QuestWiringTestDependencies deps, string recipeId) =>
|
private static CraftResult TryCraft(QuestWiringTestDependencies deps, string recipeId) =>
|
||||||
|
|
@ -460,6 +467,8 @@ public sealed class QuestProgressApiTests
|
||||||
deps.ProgressStore,
|
deps.ProgressStore,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static void SeedStack(QuestWiringTestDependencies deps, string itemId, int quantity)
|
private static void SeedStack(QuestWiringTestDependencies deps, string itemId, int quantity)
|
||||||
|
|
@ -496,6 +505,7 @@ public sealed class QuestProgressApiTests
|
||||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||||
services.GetRequiredService<IRewardDeliveryStore>(),
|
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||||
services.GetRequiredService<IFactionStandingStore>(),
|
services.GetRequiredService<IFactionStandingStore>(),
|
||||||
|
services.GetRequiredService<IReputationDeltaStore>(),
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -519,5 +529,6 @@ public sealed class QuestProgressApiTests
|
||||||
IPlayerPerkStateStore PerkStore,
|
IPlayerPerkStateStore PerkStore,
|
||||||
IRewardDeliveryStore DeliveryStore,
|
IRewardDeliveryStore DeliveryStore,
|
||||||
IFactionStandingStore StandingStore,
|
IFactionStandingStore StandingStore,
|
||||||
|
IReputationDeltaStore AuditStore,
|
||||||
TimeProvider TimeProvider);
|
TimeProvider TimeProvider);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -389,9 +389,40 @@ public sealed class QuestStateOperationsTests
|
||||||
Assert.Single(deliveryEvent.GrantedSkillXp);
|
Assert.Single(deliveryEvent.GrantedSkillXp);
|
||||||
Assert.Equal("salvage", deliveryEvent.GrantedSkillXp[0].SkillId);
|
Assert.Equal("salvage", deliveryEvent.GrantedSkillXp[0].SkillId);
|
||||||
Assert.Equal(50, deliveryEvent.GrantedSkillXp[0].Amount);
|
Assert.Equal(50, deliveryEvent.GrantedSkillXp[0].Amount);
|
||||||
|
Assert.Single(deliveryEvent.GrantedReputation);
|
||||||
|
Assert.Equal(GridFactionId, deliveryEvent.GrantedReputation[0].FactionId);
|
||||||
|
Assert.Equal(15, deliveryEvent.GrantedReputation[0].Amount);
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||||
Assert.Equal(CountBagItem(beforeInventory!, "survey_drone_kit") + 1, CountBagItem(afterInventory!, "survey_drone_kit"));
|
Assert.Equal(CountBagItem(beforeInventory!, "survey_drone_kit") + 1, CountBagItem(afterInventory!, "survey_drone_kit"));
|
||||||
Assert.Equal(100, deps.SkillProgressionStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"));
|
Assert.Equal(100, deps.SkillProgressionStore.GetXpTotals(PlayerId).GetValueOrDefault("salvage"));
|
||||||
|
Assert.Equal(
|
||||||
|
PrototypeE7M3QuestFactionRules.GridContractMinStanding,
|
||||||
|
deps.StandingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryAccept_ShouldAcceptGridContract_WhenOperatorChainCompletedWithRepGrant()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var deps = ResolveDependencies(factory);
|
||||||
|
AcceptAndComplete(deps, GatherQuestId);
|
||||||
|
AcceptAndComplete(deps, RefineQuestId);
|
||||||
|
AcceptAndComplete(deps, CombatQuestId);
|
||||||
|
Assert.True(TryAccept(deps, ChainQuestId).Success);
|
||||||
|
Assert.True(TryMarkComplete(deps, ChainQuestId).Success);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = TryAccept(deps, GridContractQuestId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.Null(result.ReasonCode);
|
||||||
|
Assert.NotNull(result.Snapshot);
|
||||||
|
Assert.Equal(QuestProgressStatus.Completed, result.Snapshot!.Status);
|
||||||
|
Assert.Equal(
|
||||||
|
PrototypeE7M3QuestFactionRules.GridContractMinStanding,
|
||||||
|
deps.StandingStore.TryGetStanding(PlayerId, GridFactionId).Standing);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -575,6 +606,8 @@ public sealed class QuestStateOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.ReputationDeltaStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static QuestStateOperationResult TryAccept(QuestTestDependencies deps, string questId) =>
|
private static QuestStateOperationResult TryAccept(QuestTestDependencies deps, string questId) =>
|
||||||
|
|
@ -595,6 +628,7 @@ public sealed class QuestStateOperationsTests
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
deps.StandingStore,
|
deps.StandingStore,
|
||||||
|
deps.ReputationDeltaStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static QuestStateOperationResult TryAdvanceStep(QuestTestDependencies deps, string questId, int newStepIndex) =>
|
private static QuestStateOperationResult TryAdvanceStep(QuestTestDependencies deps, string questId, int newStepIndex) =>
|
||||||
|
|
@ -612,6 +646,8 @@ public sealed class QuestStateOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.ReputationDeltaStore,
|
||||||
deps.TimeProvider);
|
deps.TimeProvider);
|
||||||
|
|
||||||
private static QuestTestDependencies ResolveDependencies(InMemoryWebApplicationFactory factory)
|
private static QuestTestDependencies ResolveDependencies(InMemoryWebApplicationFactory factory)
|
||||||
|
|
|
||||||
|
|
@ -106,5 +106,6 @@ public sealed class InMemoryRewardDeliveryStoreTests
|
||||||
DeliveredAt,
|
DeliveredAt,
|
||||||
[new RewardItemGrantApplied("survey_drone_kit", 1)],
|
[new RewardItemGrantApplied("survey_drone_kit", 1)],
|
||||||
[new RewardSkillXpGrantApplied("salvage", 50)],
|
[new RewardSkillXpGrantApplied("salvage", 50)],
|
||||||
|
[],
|
||||||
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, QuestId));
|
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, QuestId));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using NeonSprawl.Server.Game.Encounters;
|
using NeonSprawl.Server.Game.Encounters;
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
using NeonSprawl.Server.Game.Quests;
|
using NeonSprawl.Server.Game.Quests;
|
||||||
|
|
@ -15,6 +16,7 @@ public sealed class RewardRouterOperationsTests
|
||||||
private const string PlayerId = "dev-local-1";
|
private const string PlayerId = "dev-local-1";
|
||||||
private const string GatherQuestId = "prototype_quest_gather_intro";
|
private const string GatherQuestId = "prototype_quest_gather_intro";
|
||||||
private const string OperatorChainQuestId = "prototype_quest_operator_chain";
|
private const string OperatorChainQuestId = "prototype_quest_operator_chain";
|
||||||
|
private const string GridOperatorsFactionId = PrototypeE7M3QuestFactionRules.GridContractGateFactionId;
|
||||||
private const string SalvageScrapEfficiencyPerk = "salvage_scrap_efficiency_1";
|
private const string SalvageScrapEfficiencyPerk = "salvage_scrap_efficiency_1";
|
||||||
private static readonly DateTimeOffset DeliveredAt = new(2026, 6, 7, 14, 0, 0, TimeSpan.Zero);
|
private static readonly DateTimeOffset DeliveredAt = new(2026, 6, 7, 14, 0, 0, TimeSpan.Zero);
|
||||||
|
|
||||||
|
|
@ -39,7 +41,7 @@ public sealed class RewardRouterOperationsTests
|
||||||
Assert.Single(first.GrantedSkillXp);
|
Assert.Single(first.GrantedSkillXp);
|
||||||
Assert.Equal("salvage", first.GrantedSkillXp[0].SkillId);
|
Assert.Equal("salvage", first.GrantedSkillXp[0].SkillId);
|
||||||
Assert.Equal(25, first.GrantedSkillXp[0].Amount);
|
Assert.Equal(25, first.GrantedSkillXp[0].Amount);
|
||||||
Assert.NotNull(first.DeliveryEvent);
|
Assert.Empty(first.GrantedReputation);
|
||||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, GatherQuestId, out var stored));
|
Assert.True(deps.DeliveryStore.TryGet(PlayerId, GatherQuestId, out var stored));
|
||||||
Assert.Equal(first.DeliveryEvent!.Value.IdempotencyKey, stored.IdempotencyKey);
|
Assert.Equal(first.DeliveryEvent!.Value.IdempotencyKey, stored.IdempotencyKey);
|
||||||
var xpAfter = deps.SkillProgressionStore.GetXpTotals(PlayerId);
|
var xpAfter = deps.SkillProgressionStore.GetXpTotals(PlayerId);
|
||||||
|
|
@ -59,9 +61,7 @@ public sealed class RewardRouterOperationsTests
|
||||||
// Arrange
|
// Arrange
|
||||||
await using var factory = new InMemoryWebApplicationFactory();
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
var deps = ResolveDependencies(factory);
|
var deps = ResolveDependencies(factory);
|
||||||
var bundle = new QuestRewardBundleRow(
|
var bundle = PrototypeE7M3QuestFactionRules.ExpectedCompletionBundles[OperatorChainQuestId];
|
||||||
[new RewardGrantRow("survey_drone_kit", 1)],
|
|
||||||
[new QuestSkillXpGrantRow("salvage", 50)]);
|
|
||||||
var xpBefore = deps.SkillProgressionStore.GetXpTotals(PlayerId);
|
var xpBefore = deps.SkillProgressionStore.GetXpTotals(PlayerId);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -76,6 +76,9 @@ public sealed class RewardRouterOperationsTests
|
||||||
Assert.Single(result.GrantedSkillXp);
|
Assert.Single(result.GrantedSkillXp);
|
||||||
Assert.Equal("salvage", result.GrantedSkillXp[0].SkillId);
|
Assert.Equal("salvage", result.GrantedSkillXp[0].SkillId);
|
||||||
Assert.Equal(50, result.GrantedSkillXp[0].Amount);
|
Assert.Equal(50, result.GrantedSkillXp[0].Amount);
|
||||||
|
Assert.Single(result.GrantedReputation);
|
||||||
|
Assert.Equal(GridOperatorsFactionId, result.GrantedReputation[0].FactionId);
|
||||||
|
Assert.Equal(15, result.GrantedReputation[0].Amount);
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var inventory);
|
||||||
Assert.Equal(1, CountBagItem(inventory!, "survey_drone_kit"));
|
Assert.Equal(1, CountBagItem(inventory!, "survey_drone_kit"));
|
||||||
var xpAfter = deps.SkillProgressionStore.GetXpTotals(PlayerId);
|
var xpAfter = deps.SkillProgressionStore.GetXpTotals(PlayerId);
|
||||||
|
|
@ -83,6 +86,84 @@ public sealed class RewardRouterOperationsTests
|
||||||
Assert.True(xpAfter.TryGetValue("salvage", out var salvageXp));
|
Assert.True(xpAfter.TryGetValue("salvage", out var salvageXp));
|
||||||
Assert.Equal(salvageBefore + 50, salvageXp);
|
Assert.Equal(salvageBefore + 50, salvageXp);
|
||||||
Assert.True(deps.DeliveryStore.TryGet(PlayerId, OperatorChainQuestId, out _));
|
Assert.True(deps.DeliveryStore.TryGet(PlayerId, OperatorChainQuestId, out _));
|
||||||
|
Assert.Equal(
|
||||||
|
PrototypeE7M3QuestFactionRules.GridContractMinStanding,
|
||||||
|
deps.StandingStore.TryGetStanding(PlayerId, GridOperatorsFactionId).Standing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryDeliverQuestCompletion_ShouldNotDoubleApplyReputation_WhenAlreadyDelivered()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var deps = ResolveDependencies(factory);
|
||||||
|
var bundle = PrototypeE7M3QuestFactionRules.ExpectedCompletionBundles[OperatorChainQuestId];
|
||||||
|
var standingBefore = deps.StandingStore.TryGetStanding(PlayerId, GridOperatorsFactionId).Standing;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var first = Deliver(deps, OperatorChainQuestId, bundle, TimeProvider.System);
|
||||||
|
var second = Deliver(deps, OperatorChainQuestId, bundle, TimeProvider.System);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(first.Success);
|
||||||
|
Assert.Single(first.GrantedReputation);
|
||||||
|
Assert.Equal(15, first.GrantedReputation[0].Amount);
|
||||||
|
Assert.True(second.Success);
|
||||||
|
Assert.Equal(RewardDeliveryReasonCodes.AlreadyDelivered, second.ReasonCode);
|
||||||
|
Assert.Equal(
|
||||||
|
standingBefore + 15,
|
||||||
|
deps.StandingStore.TryGetStanding(PlayerId, GridOperatorsFactionId).Standing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryDeliverQuestCompletion_ShouldDenyUnknownFaction_WithoutStoreRecord()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var deps = ResolveDependencies(factory);
|
||||||
|
var bundle = new QuestRewardBundleRow(
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[new ReputationGrantRow("prototype_faction_unknown", 10)]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = Deliver(deps, GatherQuestId, bundle, TimeProvider.System);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.Equal(RewardDeliveryReasonCodes.UnknownFaction, result.ReasonCode);
|
||||||
|
Assert.False(deps.DeliveryStore.TryGet(PlayerId, GatherQuestId, out _));
|
||||||
|
Assert.Equal(0, deps.StandingStore.TryGetStanding(PlayerId, "prototype_faction_unknown").Standing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryDeliverQuestCompletion_ShouldRollbackItemsAndSkillXp_WhenReputationApplyDenied()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
await using var factory = new InMemoryWebApplicationFactory();
|
||||||
|
var deps = ResolveDependencies(factory);
|
||||||
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
|
||||||
|
var xpBefore = deps.SkillProgressionStore.GetXpTotals(PlayerId);
|
||||||
|
var salvageBefore = xpBefore.TryGetValue("salvage", out var beforeSalvage) ? beforeSalvage : 0;
|
||||||
|
var bundle = new QuestRewardBundleRow(
|
||||||
|
[new RewardGrantRow("survey_drone_kit", 1)],
|
||||||
|
[new QuestSkillXpGrantRow("salvage", 50)],
|
||||||
|
[new ReputationGrantRow("prototype_faction_unknown", 10)]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = Deliver(deps, OperatorChainQuestId, bundle, TimeProvider.System);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result.Success);
|
||||||
|
Assert.Equal(RewardDeliveryReasonCodes.UnknownFaction, result.ReasonCode);
|
||||||
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var afterInventory);
|
||||||
|
Assert.Equal(
|
||||||
|
CountBagItem(beforeInventory!, "survey_drone_kit"),
|
||||||
|
CountBagItem(afterInventory!, "survey_drone_kit"));
|
||||||
|
var xpAfter = deps.SkillProgressionStore.GetXpTotals(PlayerId);
|
||||||
|
var salvageAfter = xpAfter.TryGetValue("salvage", out var afterSalvage) ? afterSalvage : 0;
|
||||||
|
Assert.Equal(salvageBefore, salvageAfter);
|
||||||
|
Assert.False(deps.DeliveryStore.TryGet(PlayerId, OperatorChainQuestId, out _));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -144,6 +225,7 @@ public sealed class RewardRouterOperationsTests
|
||||||
DeliveredAt,
|
DeliveredAt,
|
||||||
[new RewardItemGrantApplied("survey_drone_kit", 1)],
|
[new RewardItemGrantApplied("survey_drone_kit", 1)],
|
||||||
[new RewardSkillXpGrantApplied("salvage", 50)],
|
[new RewardSkillXpGrantApplied("salvage", 50)],
|
||||||
|
[],
|
||||||
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, OperatorChainQuestId));
|
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, OperatorChainQuestId));
|
||||||
Assert.True(deps.DeliveryStore.TryRecord(existingEvent));
|
Assert.True(deps.DeliveryStore.TryRecord(existingEvent));
|
||||||
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
|
deps.InventoryStore.TryGetSnapshot(PlayerId, out var beforeInventory);
|
||||||
|
|
@ -250,6 +332,8 @@ public sealed class RewardRouterOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -279,6 +363,8 @@ public sealed class RewardRouterOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -299,6 +385,7 @@ public sealed class RewardRouterOperationsTests
|
||||||
DeliveredAt,
|
DeliveredAt,
|
||||||
[],
|
[],
|
||||||
[new RewardSkillXpGrantApplied("salvage", 25)],
|
[new RewardSkillXpGrantApplied("salvage", 25)],
|
||||||
|
[],
|
||||||
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, GatherQuestId));
|
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, GatherQuestId));
|
||||||
var store = new DeliveryStoreSimulatingRecordRaceLoss(winnerEvent);
|
var store = new DeliveryStoreSimulatingRecordRaceLoss(winnerEvent);
|
||||||
var bundle = new QuestRewardBundleRow([], [new QuestSkillXpGrantRow("salvage", 25)]);
|
var bundle = new QuestRewardBundleRow([], [new QuestSkillXpGrantRow("salvage", 25)]);
|
||||||
|
|
@ -318,6 +405,8 @@ public sealed class RewardRouterOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
store,
|
store,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -346,6 +435,7 @@ public sealed class RewardRouterOperationsTests
|
||||||
DeliveredAt,
|
DeliveredAt,
|
||||||
[],
|
[],
|
||||||
[new RewardSkillXpGrantApplied("salvage", 25)],
|
[new RewardSkillXpGrantApplied("salvage", 25)],
|
||||||
|
[],
|
||||||
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, GatherQuestId));
|
RewardDeliveryIds.MakeIdempotencyKey(PlayerId, GatherQuestId));
|
||||||
var store = new DeliveryStoreSimulatingRecordRaceLoss(winnerEvent);
|
var store = new DeliveryStoreSimulatingRecordRaceLoss(winnerEvent);
|
||||||
var bundle = new QuestRewardBundleRow([], [new QuestSkillXpGrantRow("salvage", 25)]);
|
var bundle = new QuestRewardBundleRow([], [new QuestSkillXpGrantRow("salvage", 25)]);
|
||||||
|
|
@ -363,6 +453,8 @@ public sealed class RewardRouterOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
store,
|
store,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
TimeProvider.System);
|
TimeProvider.System);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|
@ -471,6 +563,8 @@ public sealed class RewardRouterOperationsTests
|
||||||
deps.PerkUnlockEngine,
|
deps.PerkUnlockEngine,
|
||||||
deps.PerkStore,
|
deps.PerkStore,
|
||||||
deps.DeliveryStore,
|
deps.DeliveryStore,
|
||||||
|
deps.StandingStore,
|
||||||
|
deps.AuditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
private static void FillBag(RewardRouterTestDependencies deps)
|
private static void FillBag(RewardRouterTestDependencies deps)
|
||||||
|
|
@ -498,7 +592,9 @@ public sealed class RewardRouterOperationsTests
|
||||||
services.GetRequiredService<ISkillLevelCurve>(),
|
services.GetRequiredService<ISkillLevelCurve>(),
|
||||||
services.GetRequiredService<PerkUnlockEngine>(),
|
services.GetRequiredService<PerkUnlockEngine>(),
|
||||||
services.GetRequiredService<IPlayerPerkStateStore>(),
|
services.GetRequiredService<IPlayerPerkStateStore>(),
|
||||||
services.GetRequiredService<IRewardDeliveryStore>());
|
services.GetRequiredService<IRewardDeliveryStore>(),
|
||||||
|
services.GetRequiredService<IFactionStandingStore>(),
|
||||||
|
services.GetRequiredService<IReputationDeltaStore>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
private static int CountBagItem(PlayerInventorySnapshot snapshot, string itemId)
|
||||||
|
|
@ -537,7 +633,9 @@ public sealed class RewardRouterOperationsTests
|
||||||
ISkillLevelCurve LevelCurve,
|
ISkillLevelCurve LevelCurve,
|
||||||
PerkUnlockEngine PerkUnlockEngine,
|
PerkUnlockEngine PerkUnlockEngine,
|
||||||
IPlayerPerkStateStore PerkStore,
|
IPlayerPerkStateStore PerkStore,
|
||||||
IRewardDeliveryStore DeliveryStore);
|
IRewardDeliveryStore DeliveryStore,
|
||||||
|
IFactionStandingStore StandingStore,
|
||||||
|
IReputationDeltaStore AuditStore);
|
||||||
|
|
||||||
/// <summary>TryGet misses until after a failed <see cref="IRewardDeliveryStore.TryRecord"/> — simulates concurrent race loser.</summary>
|
/// <summary>TryGet misses until after a failed <see cref="IRewardDeliveryStore.TryRecord"/> — simulates concurrent race loser.</summary>
|
||||||
private sealed class DeliveryStoreSimulatingRecordRaceLoss(RewardDeliveryEvent winnerEvent) : IRewardDeliveryStore
|
private sealed class DeliveryStoreSimulatingRecordRaceLoss(RewardDeliveryEvent winnerEvent) : IRewardDeliveryStore
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using NeonSprawl.Server.Diagnostics;
|
using NeonSprawl.Server.Diagnostics;
|
||||||
using NeonSprawl.Server.Game.Combat;
|
using NeonSprawl.Server.Game.Combat;
|
||||||
using NeonSprawl.Server.Game.Encounters;
|
using NeonSprawl.Server.Game.Encounters;
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Gigs;
|
using NeonSprawl.Server.Game.Gigs;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
|
|
@ -90,6 +91,8 @@ public static class AbilityCastApi
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
TimeProvider clock) =>
|
TimeProvider clock) =>
|
||||||
{
|
{
|
||||||
|
|
@ -297,6 +300,8 @@ public static class AbilityCastApi
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
clock,
|
clock,
|
||||||
loggerFactory.CreateLogger(nameof(EncounterCombatWiring)));
|
loggerFactory.CreateLogger(nameof(EncounterCombatWiring)));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
using NeonSprawl.Server.Game.Quests;
|
using NeonSprawl.Server.Game.Quests;
|
||||||
|
|
@ -33,6 +34,8 @@ public static class CraftOperations
|
||||||
IPlayerQuestStateStore progressStore,
|
IPlayerQuestStateStore progressStore,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
if (quantity <= 0)
|
if (quantity <= 0)
|
||||||
|
|
@ -174,6 +177,8 @@ public static class CraftOperations
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
return new CraftResult(
|
return new CraftResult(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
using NeonSprawl.Server.Game.PositionState;
|
using NeonSprawl.Server.Game.PositionState;
|
||||||
|
|
@ -20,7 +21,8 @@ public static class PlayerCraftApi
|
||||||
IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve,
|
IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve,
|
||||||
PerkUnlockEngine perkUnlockEngine, IQuestDefinitionRegistry questRegistry,
|
PerkUnlockEngine perkUnlockEngine, IQuestDefinitionRegistry questRegistry,
|
||||||
IPlayerQuestStateStore progressStore, IPlayerPerkStateStore perkStore,
|
IPlayerQuestStateStore progressStore, IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore, TimeProvider timeProvider) =>
|
IRewardDeliveryStore deliveryStore, IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore, TimeProvider timeProvider) =>
|
||||||
{
|
{
|
||||||
if (body is null || body.SchemaVersion != PlayerCraftRequest.CurrentSchemaVersion)
|
if (body is null || body.SchemaVersion != PlayerCraftRequest.CurrentSchemaVersion)
|
||||||
{
|
{
|
||||||
|
|
@ -54,6 +56,8 @@ public static class PlayerCraftApi
|
||||||
progressStore,
|
progressStore,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
if (!result.Success &&
|
if (!result.Success &&
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
using NeonSprawl.Server.Game.Quests;
|
using NeonSprawl.Server.Game.Quests;
|
||||||
|
|
@ -35,6 +36,8 @@ public static class EncounterCombatWiring
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
ILogger? logger = null)
|
ILogger? logger = null)
|
||||||
{
|
{
|
||||||
|
|
@ -99,6 +102,8 @@ public static class EncounterCombatWiring
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
if (!completion.Success)
|
if (!completion.Success)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
using NeonSprawl.Server.Game.Quests;
|
using NeonSprawl.Server.Game.Quests;
|
||||||
|
|
@ -38,6 +39,8 @@ public static class EncounterCompletionOperations
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
if (!encounterRegistry.TryNormalizeKnown(encounterId, out var normalizedEncounterId) ||
|
if (!encounterRegistry.TryNormalizeKnown(encounterId, out var normalizedEncounterId) ||
|
||||||
|
|
@ -160,6 +163,8 @@ public static class EncounterCompletionOperations
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
return new EncounterCompletionResult(
|
return new EncounterCompletionResult(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
using NeonSprawl.Server.Game.Quests;
|
using NeonSprawl.Server.Game.Quests;
|
||||||
|
|
@ -34,6 +35,8 @@ public static class GatherOperations
|
||||||
IPlayerQuestStateStore progressStore,
|
IPlayerQuestStateStore progressStore,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
var key = ResourceNodeInstanceIds.Normalize(interactableId);
|
var key = ResourceNodeInstanceIds.Normalize(interactableId);
|
||||||
|
|
@ -149,6 +152,8 @@ public static class GatherOperations
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
return new GatherResult(
|
return new GatherResult(
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Gathering;
|
using NeonSprawl.Server.Game.Gathering;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
|
|
@ -25,7 +26,8 @@ public static class InteractionApi
|
||||||
IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve, PerkUnlockEngine perkUnlockEngine,
|
IPlayerSkillProgressionStore xpStore, ISkillLevelCurve levelCurve, PerkUnlockEngine perkUnlockEngine,
|
||||||
IResourceNodeInstanceStore nodeInstanceStore, IQuestDefinitionRegistry questRegistry,
|
IResourceNodeInstanceStore nodeInstanceStore, IQuestDefinitionRegistry questRegistry,
|
||||||
IPlayerQuestStateStore progressStore, IPlayerPerkStateStore perkStore,
|
IPlayerQuestStateStore progressStore, IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore, TimeProvider timeProvider) =>
|
IRewardDeliveryStore deliveryStore, IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore, TimeProvider timeProvider) =>
|
||||||
{
|
{
|
||||||
if (body is null || body.SchemaVersion != InteractionRequest.CurrentSchemaVersion)
|
if (body is null || body.SchemaVersion != InteractionRequest.CurrentSchemaVersion)
|
||||||
{
|
{
|
||||||
|
|
@ -95,6 +97,8 @@ public static class InteractionApi
|
||||||
progressStore,
|
progressStore,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
if (!gather.Success)
|
if (!gather.Success)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ public static class QuestAcceptApi
|
||||||
ISkillDefinitionRegistry skillRegistry, IPlayerSkillProgressionStore skillProgressionStore,
|
ISkillDefinitionRegistry skillRegistry, IPlayerSkillProgressionStore skillProgressionStore,
|
||||||
ISkillLevelCurve levelCurve, PerkUnlockEngine perkUnlockEngine, IPlayerPerkStateStore perkStore,
|
ISkillLevelCurve levelCurve, PerkUnlockEngine perkUnlockEngine, IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore, IFactionStandingStore standingStore,
|
IRewardDeliveryStore deliveryStore, IFactionStandingStore standingStore,
|
||||||
TimeProvider timeProvider) =>
|
IReputationDeltaStore auditStore, TimeProvider timeProvider) =>
|
||||||
{
|
{
|
||||||
if (body is not null &&
|
if (body is not null &&
|
||||||
body.SchemaVersion != 0 &&
|
body.SchemaVersion != 0 &&
|
||||||
|
|
@ -49,6 +49,7 @@ public static class QuestAcceptApi
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
standingStore,
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
return Results.Json(MapResponse(trimmedId, deliveryStore, result));
|
return Results.Json(MapResponse(trimmedId, deliveryStore, result));
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
using NeonSprawl.Server.Game.Rewards;
|
using NeonSprawl.Server.Game.Rewards;
|
||||||
|
|
@ -28,6 +29,8 @@ public static class QuestObjectiveWiring
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
ILogger? logger = null)
|
ILogger? logger = null)
|
||||||
{
|
{
|
||||||
|
|
@ -74,6 +77,8 @@ public static class QuestObjectiveWiring
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -97,6 +102,8 @@ public static class QuestObjectiveWiring
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
ILogger? logger = null)
|
ILogger? logger = null)
|
||||||
{
|
{
|
||||||
|
|
@ -143,6 +150,8 @@ public static class QuestObjectiveWiring
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -165,6 +174,8 @@ public static class QuestObjectiveWiring
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
ILogger? logger = null)
|
ILogger? logger = null)
|
||||||
{
|
{
|
||||||
|
|
@ -210,6 +221,8 @@ public static class QuestObjectiveWiring
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -254,6 +267,8 @@ public static class QuestObjectiveWiring
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
ILogger? logger = null)
|
ILogger? logger = null)
|
||||||
{
|
{
|
||||||
|
|
@ -278,6 +293,8 @@ public static class QuestObjectiveWiring
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
@ -343,6 +360,8 @@ public static class QuestObjectiveWiring
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
for (var iteration = 0; iteration < MaxStepCompletionIterations; iteration++)
|
for (var iteration = 0; iteration < MaxStepCompletionIterations; iteration++)
|
||||||
|
|
@ -381,6 +400,8 @@ public static class QuestObjectiveWiring
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -399,6 +420,8 @@ public static class QuestObjectiveWiring
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
if (!advance.Success)
|
if (!advance.Success)
|
||||||
|
|
@ -420,6 +443,8 @@ public static class QuestObjectiveWiring
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
|
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
|
||||||
|
|
@ -439,6 +464,8 @@ public static class QuestObjectiveWiring
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
using NeonSprawl.Server.Game.PositionState;
|
using NeonSprawl.Server.Game.PositionState;
|
||||||
|
|
@ -22,7 +23,8 @@ public static class QuestProgressApi
|
||||||
IItemDefinitionRegistry itemRegistry, ISkillDefinitionRegistry skillRegistry,
|
IItemDefinitionRegistry itemRegistry, ISkillDefinitionRegistry skillRegistry,
|
||||||
IPlayerSkillProgressionStore skillProgressionStore, ISkillLevelCurve levelCurve,
|
IPlayerSkillProgressionStore skillProgressionStore, ISkillLevelCurve levelCurve,
|
||||||
PerkUnlockEngine perkUnlockEngine, IPlayerPerkStateStore perkStore,
|
PerkUnlockEngine perkUnlockEngine, IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore, TimeProvider timeProvider, ILogger<Program>? logger) =>
|
IRewardDeliveryStore deliveryStore, IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore, TimeProvider timeProvider, ILogger<Program>? logger) =>
|
||||||
{
|
{
|
||||||
var trimmedId = id.Trim();
|
var trimmedId = id.Trim();
|
||||||
if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _))
|
if (trimmedId.Length == 0 || !positions.TryGetPosition(trimmedId, out _))
|
||||||
|
|
@ -42,6 +44,8 @@ public static class QuestProgressApi
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider,
|
timeProvider,
|
||||||
logger);
|
logger);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ public static class QuestStateOperations
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
IFactionStandingStore standingStore,
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
||||||
|
|
@ -100,6 +101,8 @@ public static class QuestStateOperations
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
|
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
|
||||||
|
|
@ -130,6 +133,8 @@ public static class QuestStateOperations
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
||||||
|
|
@ -185,6 +190,8 @@ public static class QuestStateOperations
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
|
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var refreshed))
|
||||||
|
|
@ -215,6 +222,8 @@ public static class QuestStateOperations
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
|
||||||
|
|
@ -250,6 +259,8 @@ public static class QuestStateOperations
|
||||||
perkUnlockEngine,
|
perkUnlockEngine,
|
||||||
perkStore,
|
perkStore,
|
||||||
deliveryStore,
|
deliveryStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
timeProvider);
|
timeProvider);
|
||||||
|
|
||||||
if (!delivery.Success)
|
if (!delivery.Success)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ public sealed class InMemoryRewardDeliveryStore : IRewardDeliveryStore
|
||||||
{
|
{
|
||||||
GrantedItems = deliveryEvent.GrantedItems.ToArray(),
|
GrantedItems = deliveryEvent.GrantedItems.ToArray(),
|
||||||
GrantedSkillXp = deliveryEvent.GrantedSkillXp.ToArray(),
|
GrantedSkillXp = deliveryEvent.GrantedSkillXp.ToArray(),
|
||||||
|
GrantedReputation = deliveryEvent.GrantedReputation.ToArray(),
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ public readonly record struct RewardDeliveryEvent(
|
||||||
DateTimeOffset DeliveredAt,
|
DateTimeOffset DeliveredAt,
|
||||||
IReadOnlyList<RewardItemGrantApplied> GrantedItems,
|
IReadOnlyList<RewardItemGrantApplied> GrantedItems,
|
||||||
IReadOnlyList<RewardSkillXpGrantApplied> GrantedSkillXp,
|
IReadOnlyList<RewardSkillXpGrantApplied> GrantedSkillXp,
|
||||||
|
IReadOnlyList<RewardReputationGrantApplied> GrantedReputation,
|
||||||
string IdempotencyKey);
|
string IdempotencyKey);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
|
|
||||||
namespace NeonSprawl.Server.Game.Rewards;
|
namespace NeonSprawl.Server.Game.Rewards;
|
||||||
|
|
||||||
/// <summary>Id normalization and composite keys for reward delivery stores (NEO-126).</summary>
|
/// <summary>Id normalization and composite keys for reward delivery stores (NEO-126).</summary>
|
||||||
|
|
@ -31,6 +33,22 @@ public static class RewardDeliveryIds
|
||||||
return $"{p}:quest_complete:{q}";
|
return $"{p}:quest_complete:{q}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Stable audit row id for one rep grant on a quest completion delivery (NEO-138).</summary>
|
||||||
|
public static string MakeReputationDeltaId(string idempotencyKey, string? factionId)
|
||||||
|
{
|
||||||
|
var faction = FactionStandingIds.NormalizeFactionId(factionId);
|
||||||
|
if (idempotencyKey.Length == 0 || faction.Length == 0)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{idempotencyKey}:rep:{faction}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Rollback audit row id for compensating rep revert (NEO-138).</summary>
|
||||||
|
public static string MakeReputationRollbackDeltaId(string reputationDeltaId) =>
|
||||||
|
reputationDeltaId.Length == 0 ? string.Empty : $"{reputationDeltaId}:rollback";
|
||||||
|
|
||||||
/// <summary>Composite store key for <paramref name="playerId"/> + <paramref name="questId"/>.</summary>
|
/// <summary>Composite store key for <paramref name="playerId"/> + <paramref name="questId"/>.</summary>
|
||||||
public static string MakeStoreKey(string? playerId, string? questId)
|
public static string MakeStoreKey(string? playerId, string? questId)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Skills;
|
using NeonSprawl.Server.Game.Skills;
|
||||||
|
|
||||||
|
|
@ -34,4 +35,19 @@ public static class RewardDeliveryReasonCodes
|
||||||
|
|
||||||
/// <summary>Passthrough from <see cref="SkillProgressionSnapshotApi.ReasonSourceKindNotAllowed"/>.</summary>
|
/// <summary>Passthrough from <see cref="SkillProgressionSnapshotApi.ReasonSourceKindNotAllowed"/>.</summary>
|
||||||
public const string SourceKindNotAllowed = SkillProgressionSnapshotApi.ReasonSourceKindNotAllowed;
|
public const string SourceKindNotAllowed = SkillProgressionSnapshotApi.ReasonSourceKindNotAllowed;
|
||||||
|
|
||||||
|
/// <summary>Passthrough from <see cref="FactionStandingReasonCodes.UnknownFaction"/> via rep apply (NEO-138).</summary>
|
||||||
|
public const string UnknownFaction = FactionStandingReasonCodes.UnknownFaction;
|
||||||
|
|
||||||
|
/// <summary>Passthrough from <see cref="ReputationApplyReasonCodes.InvalidDelta"/>.</summary>
|
||||||
|
public const string InvalidDelta = ReputationApplyReasonCodes.InvalidDelta;
|
||||||
|
|
||||||
|
/// <summary>Passthrough from <see cref="ReputationApplyReasonCodes.InvalidDeltaId"/>.</summary>
|
||||||
|
public const string InvalidDeltaId = ReputationApplyReasonCodes.InvalidDeltaId;
|
||||||
|
|
||||||
|
/// <summary>Passthrough from <see cref="ReputationApplyReasonCodes.InvalidSource"/>.</summary>
|
||||||
|
public const string InvalidSource = ReputationApplyReasonCodes.InvalidSource;
|
||||||
|
|
||||||
|
/// <summary>Passthrough from <see cref="ReputationApplyReasonCodes.AuditAppendFailed"/>.</summary>
|
||||||
|
public const string AuditAppendFailed = ReputationApplyReasonCodes.AuditAppendFailed;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@ namespace NeonSprawl.Server.Game.Rewards;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="GrantedItems">Item grants on success; empty when denied.</param>
|
/// <param name="GrantedItems">Item grants on success; empty when denied.</param>
|
||||||
/// <param name="GrantedSkillXp">Skill XP grants on success; empty when denied.</param>
|
/// <param name="GrantedSkillXp">Skill XP grants on success; empty when denied.</param>
|
||||||
|
/// <param name="GrantedReputation">Reputation grants on success; empty when denied.</param>
|
||||||
/// <param name="DeliveryEvent">Delivery payload on success; null when denied.</param>
|
/// <param name="DeliveryEvent">Delivery payload on success; null when denied.</param>
|
||||||
public readonly record struct RewardDeliveryResult(
|
public readonly record struct RewardDeliveryResult(
|
||||||
bool Success,
|
bool Success,
|
||||||
string? ReasonCode,
|
string? ReasonCode,
|
||||||
IReadOnlyList<RewardItemGrantApplied> GrantedItems,
|
IReadOnlyList<RewardItemGrantApplied> GrantedItems,
|
||||||
IReadOnlyList<RewardSkillXpGrantApplied> GrantedSkillXp,
|
IReadOnlyList<RewardSkillXpGrantApplied> GrantedSkillXp,
|
||||||
|
IReadOnlyList<RewardReputationGrantApplied> GrantedReputation,
|
||||||
RewardDeliveryEvent? DeliveryEvent,
|
RewardDeliveryEvent? DeliveryEvent,
|
||||||
DateTimeOffset? DeliveredAt);
|
DateTimeOffset? DeliveredAt);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
namespace NeonSprawl.Server.Game.Rewards;
|
||||||
|
|
||||||
|
/// <summary>One reputation grant applied on a successful quest completion reward delivery (NEO-138).</summary>
|
||||||
|
public readonly record struct RewardReputationGrantApplied(string FactionId, int Amount);
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using NeonSprawl.Server.Game.Encounters;
|
using NeonSprawl.Server.Game.Encounters;
|
||||||
|
using NeonSprawl.Server.Game.Factions;
|
||||||
using NeonSprawl.Server.Game.Items;
|
using NeonSprawl.Server.Game.Items;
|
||||||
using NeonSprawl.Server.Game.Mastery;
|
using NeonSprawl.Server.Game.Mastery;
|
||||||
using NeonSprawl.Server.Game.Quests;
|
using NeonSprawl.Server.Game.Quests;
|
||||||
|
|
@ -9,15 +10,17 @@ namespace NeonSprawl.Server.Game.Rewards;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies quest <see cref="QuestRewardBundleRow"/> completion grants idempotently (NEO-127).
|
/// Applies quest <see cref="QuestRewardBundleRow"/> completion grants idempotently (NEO-127).
|
||||||
/// Quest-state wiring: <see cref="QuestStateOperations"/> (NEO-128).
|
/// Quest-state wiring: <see cref="QuestStateOperations"/> (NEO-128).
|
||||||
|
/// Reputation grants: <see cref="ReputationOperations"/> (NEO-138).
|
||||||
/// E9.M1 telemetry hook sites: <see cref="TryDeliverQuestCompletion"/> (NEO-130).
|
/// E9.M1 telemetry hook sites: <see cref="TryDeliverQuestCompletion"/> (NEO-130).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RewardRouterOperations
|
public static class RewardRouterOperations
|
||||||
{
|
{
|
||||||
private static readonly RewardItemGrantApplied[] EmptyItemGrants = [];
|
private static readonly RewardItemGrantApplied[] EmptyItemGrants = [];
|
||||||
private static readonly RewardSkillXpGrantApplied[] EmptySkillXpGrants = [];
|
private static readonly RewardSkillXpGrantApplied[] EmptySkillXpGrants = [];
|
||||||
|
private static readonly RewardReputationGrantApplied[] EmptyReputationGrants = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies <paramref name="bundle"/> item + skill XP rows for a quest completion and records
|
/// Applies <paramref name="bundle"/> item, skill XP, and reputation rows for a quest completion and records
|
||||||
/// <see cref="RewardDeliveryEvent"/> when grants succeed.
|
/// <see cref="RewardDeliveryEvent"/> when grants succeed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static RewardDeliveryResult TryDeliverQuestCompletion(
|
public static RewardDeliveryResult TryDeliverQuestCompletion(
|
||||||
|
|
@ -32,6 +35,8 @@ public static class RewardRouterOperations
|
||||||
PerkUnlockEngine perkUnlockEngine,
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
IPlayerPerkStateStore perkStore,
|
IPlayerPerkStateStore perkStore,
|
||||||
IRewardDeliveryStore deliveryStore,
|
IRewardDeliveryStore deliveryStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
TimeProvider timeProvider)
|
TimeProvider timeProvider)
|
||||||
{
|
{
|
||||||
var normalizedPlayerId = RewardDeliveryIds.NormalizePlayerId(playerId);
|
var normalizedPlayerId = RewardDeliveryIds.NormalizePlayerId(playerId);
|
||||||
|
|
@ -53,6 +58,8 @@ public static class RewardRouterOperations
|
||||||
|
|
||||||
var itemGrants = bundle?.ItemGrants ?? Array.Empty<RewardGrantRow>();
|
var itemGrants = bundle?.ItemGrants ?? Array.Empty<RewardGrantRow>();
|
||||||
var skillGrants = bundle?.SkillXpGrants ?? Array.Empty<QuestSkillXpGrantRow>();
|
var skillGrants = bundle?.SkillXpGrants ?? Array.Empty<QuestSkillXpGrantRow>();
|
||||||
|
var reputationGrants = bundle?.ReputationGrants ?? Array.Empty<ReputationGrantRow>();
|
||||||
|
var idempotencyKey = RewardDeliveryIds.MakeIdempotencyKey(normalizedPlayerId, normalizedQuestId);
|
||||||
|
|
||||||
if (itemGrants.Count > 0)
|
if (itemGrants.Count > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -87,7 +94,6 @@ public static class RewardRouterOperations
|
||||||
|
|
||||||
if (addOutcome.Kind == PlayerInventoryMutationKind.StoreMissing)
|
if (addOutcome.Kind == PlayerInventoryMutationKind.StoreMissing)
|
||||||
{
|
{
|
||||||
CompensatingRemoveItems(playerId, appliedItems, itemRegistry, inventoryStore);
|
|
||||||
return Deny(RewardDeliveryReasonCodes.InventoryStoreMissing);
|
return Deny(RewardDeliveryReasonCodes.InventoryStoreMissing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,13 +123,45 @@ public static class RewardRouterOperations
|
||||||
|
|
||||||
if (skillOutcome.Kind == SkillProgressionGrantApplyKind.ProgressionStoreMissing)
|
if (skillOutcome.Kind == SkillProgressionGrantApplyKind.ProgressionStoreMissing)
|
||||||
{
|
{
|
||||||
CompensatingRemoveItems(playerId, appliedItems, itemRegistry, inventoryStore);
|
CompensatingRevertAll(
|
||||||
|
playerId,
|
||||||
|
normalizedQuestId,
|
||||||
|
idempotencyKey,
|
||||||
|
appliedItems,
|
||||||
|
appliedSkillXp,
|
||||||
|
perksUnlockedByThisCall,
|
||||||
|
[],
|
||||||
|
itemRegistry,
|
||||||
|
inventoryStore,
|
||||||
|
skillProgressionStore,
|
||||||
|
levelCurve,
|
||||||
|
perkUnlockEngine,
|
||||||
|
perkStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
|
timeProvider);
|
||||||
return Deny(RewardDeliveryReasonCodes.ProgressionStoreMissing);
|
return Deny(RewardDeliveryReasonCodes.ProgressionStoreMissing);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skillOutcome.Kind == SkillProgressionGrantApplyKind.Denied)
|
if (skillOutcome.Kind == SkillProgressionGrantApplyKind.Denied)
|
||||||
{
|
{
|
||||||
CompensatingRemoveItems(playerId, appliedItems, itemRegistry, inventoryStore);
|
CompensatingRevertAll(
|
||||||
|
playerId,
|
||||||
|
normalizedQuestId,
|
||||||
|
idempotencyKey,
|
||||||
|
appliedItems,
|
||||||
|
appliedSkillXp,
|
||||||
|
perksUnlockedByThisCall,
|
||||||
|
[],
|
||||||
|
itemRegistry,
|
||||||
|
inventoryStore,
|
||||||
|
skillProgressionStore,
|
||||||
|
levelCurve,
|
||||||
|
perkUnlockEngine,
|
||||||
|
perkStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
|
timeProvider);
|
||||||
var reasonCode = skillOutcome.Response?.ReasonCode ?? RewardDeliveryReasonCodes.UnknownSkill;
|
var reasonCode = skillOutcome.Response?.ReasonCode ?? RewardDeliveryReasonCodes.UnknownSkill;
|
||||||
return Deny(reasonCode);
|
return Deny(reasonCode);
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +181,50 @@ public static class RewardRouterOperations
|
||||||
// E9.M1 wiring: place per-row emits inside the apply loop and/or after TryRecord success only — not on
|
// E9.M1 wiring: place per-row emits inside the apply loop and/or after TryRecord success only — not on
|
||||||
// TryRecord race-rollback paths (grants applied then reverted below); mirror reward_delivery gating.
|
// TryRecord race-rollback paths (grants applied then reverted below); mirror reward_delivery gating.
|
||||||
|
|
||||||
|
var appliedReputation = new List<RewardReputationGrantApplied>(reputationGrants.Count);
|
||||||
|
foreach (var grant in reputationGrants)
|
||||||
|
{
|
||||||
|
var deltaId = RewardDeliveryIds.MakeReputationDeltaId(idempotencyKey, grant.FactionId);
|
||||||
|
var repOutcome = ReputationOperations.TryApplyDelta(
|
||||||
|
playerId,
|
||||||
|
grant.FactionId,
|
||||||
|
grant.Amount,
|
||||||
|
deltaId,
|
||||||
|
ReputationDeltaSourceKinds.QuestCompletion,
|
||||||
|
normalizedQuestId,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
|
timeProvider);
|
||||||
|
|
||||||
|
if (!repOutcome.Success)
|
||||||
|
{
|
||||||
|
CompensatingRevertAll(
|
||||||
|
playerId,
|
||||||
|
normalizedQuestId,
|
||||||
|
idempotencyKey,
|
||||||
|
appliedItems,
|
||||||
|
appliedSkillXp,
|
||||||
|
perksUnlockedByThisCall,
|
||||||
|
appliedReputation,
|
||||||
|
itemRegistry,
|
||||||
|
inventoryStore,
|
||||||
|
skillProgressionStore,
|
||||||
|
levelCurve,
|
||||||
|
perkUnlockEngine,
|
||||||
|
perkStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
|
timeProvider);
|
||||||
|
return Deny(MapReputationReason(repOutcome.ReasonCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
var appliedAmount = repOutcome.NewStanding - repOutcome.PreviousStanding;
|
||||||
|
appliedReputation.Add(new RewardReputationGrantApplied(grant.FactionId, appliedAmount));
|
||||||
|
|
||||||
|
// --- Telemetry hook site (NEO-141): future E9.M1 catalog event `reputation_delta` ---
|
||||||
|
// TODO(E9.M1): catalog emit — once per applied rep row on TryRecord success only (not rollback paths).
|
||||||
|
}
|
||||||
|
|
||||||
var deliveredAt = timeProvider.GetUtcNow();
|
var deliveredAt = timeProvider.GetUtcNow();
|
||||||
var deliveryEvent = new RewardDeliveryEvent(
|
var deliveryEvent = new RewardDeliveryEvent(
|
||||||
normalizedPlayerId,
|
normalizedPlayerId,
|
||||||
|
|
@ -150,7 +232,8 @@ public static class RewardRouterOperations
|
||||||
deliveredAt,
|
deliveredAt,
|
||||||
appliedItems,
|
appliedItems,
|
||||||
appliedSkillXp,
|
appliedSkillXp,
|
||||||
RewardDeliveryIds.MakeIdempotencyKey(normalizedPlayerId, normalizedQuestId));
|
appliedReputation,
|
||||||
|
idempotencyKey);
|
||||||
|
|
||||||
if (deliveryStore.TryRecord(deliveryEvent))
|
if (deliveryStore.TryRecord(deliveryEvent))
|
||||||
{
|
{
|
||||||
|
|
@ -158,7 +241,7 @@ public static class RewardRouterOperations
|
||||||
// TODO(E9.M1): catalog emit — once per first-time quest completion delivery (TryRecord success).
|
// TODO(E9.M1): catalog emit — once per first-time quest completion delivery (TryRecord success).
|
||||||
// Not on TryGet idempotent replay, deny paths, or TryRecord race-loser rollback paths.
|
// Not on TryGet idempotent replay, deny paths, or TryRecord race-loser rollback paths.
|
||||||
// Planned payload fields: playerId, questId, deliveredAt, grantedItems (itemId, quantity),
|
// Planned payload fields: playerId, questId, deliveredAt, grantedItems (itemId, quantity),
|
||||||
// grantedSkillXp (skillId, amount), idempotencyKey ({playerId}:quest_complete:{questId}).
|
// grantedSkillXp (skillId, amount), grantedReputation (factionId, amount), idempotencyKey.
|
||||||
// No ingest or ILogger here (comments-only).
|
// No ingest or ILogger here (comments-only).
|
||||||
|
|
||||||
return SuccessFromEvent(deliveryEvent, reasonCode: null);
|
return SuccessFromEvent(deliveryEvent, reasonCode: null);
|
||||||
|
|
@ -166,19 +249,23 @@ public static class RewardRouterOperations
|
||||||
|
|
||||||
// Lost TryRecord race after apply — rollback this call's grants (mirror
|
// Lost TryRecord race after apply — rollback this call's grants (mirror
|
||||||
// EncounterCompletionOperations when TryMarkCompleted returns false).
|
// EncounterCompletionOperations when TryMarkCompleted returns false).
|
||||||
CompensatingRemoveItems(playerId, appliedItems, itemRegistry, inventoryStore);
|
CompensatingRevertAll(
|
||||||
_ = SkillProgressionGrantOperations.TryRevertAppliedSkillGrants(
|
|
||||||
playerId,
|
playerId,
|
||||||
|
normalizedQuestId,
|
||||||
|
idempotencyKey,
|
||||||
|
appliedItems,
|
||||||
appliedSkillXp,
|
appliedSkillXp,
|
||||||
perksUnlockedByThisCall,
|
perksUnlockedByThisCall,
|
||||||
skillProgressionStore,
|
appliedReputation,
|
||||||
perkStore);
|
itemRegistry,
|
||||||
ResyncPathAutoPerksAfterRaceRollback(
|
inventoryStore,
|
||||||
playerId,
|
|
||||||
appliedSkillXp,
|
|
||||||
skillProgressionStore,
|
skillProgressionStore,
|
||||||
levelCurve,
|
levelCurve,
|
||||||
perkUnlockEngine);
|
perkUnlockEngine,
|
||||||
|
perkStore,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
|
timeProvider);
|
||||||
|
|
||||||
if (deliveryStore.TryGet(normalizedPlayerId, normalizedQuestId, out var racedEvent))
|
if (deliveryStore.TryGet(normalizedPlayerId, normalizedQuestId, out var racedEvent))
|
||||||
{
|
{
|
||||||
|
|
@ -188,6 +275,79 @@ public static class RewardRouterOperations
|
||||||
return Deny(RewardDeliveryReasonCodes.AlreadyDelivered);
|
return Deny(RewardDeliveryReasonCodes.AlreadyDelivered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void CompensatingRevertAll(
|
||||||
|
string playerId,
|
||||||
|
string normalizedQuestId,
|
||||||
|
string idempotencyKey,
|
||||||
|
IReadOnlyList<RewardItemGrantApplied> appliedItems,
|
||||||
|
IReadOnlyList<RewardSkillXpGrantApplied> appliedSkillXp,
|
||||||
|
HashSet<string> perksUnlockedByThisCall,
|
||||||
|
IReadOnlyList<RewardReputationGrantApplied> appliedReputation,
|
||||||
|
IItemDefinitionRegistry itemRegistry,
|
||||||
|
IPlayerInventoryStore inventoryStore,
|
||||||
|
IPlayerSkillProgressionStore skillProgressionStore,
|
||||||
|
ISkillLevelCurve levelCurve,
|
||||||
|
PerkUnlockEngine perkUnlockEngine,
|
||||||
|
IPlayerPerkStateStore perkStore,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
|
TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
CompensatingRevertReputation(
|
||||||
|
playerId,
|
||||||
|
normalizedQuestId,
|
||||||
|
idempotencyKey,
|
||||||
|
appliedReputation,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
|
timeProvider);
|
||||||
|
_ = SkillProgressionGrantOperations.TryRevertAppliedSkillGrants(
|
||||||
|
playerId,
|
||||||
|
appliedSkillXp,
|
||||||
|
perksUnlockedByThisCall,
|
||||||
|
skillProgressionStore,
|
||||||
|
perkStore);
|
||||||
|
CompensatingRemoveItems(playerId, appliedItems, itemRegistry, inventoryStore);
|
||||||
|
ResyncPathAutoPerksAfterRaceRollback(
|
||||||
|
playerId,
|
||||||
|
appliedSkillXp,
|
||||||
|
skillProgressionStore,
|
||||||
|
levelCurve,
|
||||||
|
perkUnlockEngine);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CompensatingRevertReputation(
|
||||||
|
string playerId,
|
||||||
|
string normalizedQuestId,
|
||||||
|
string idempotencyKey,
|
||||||
|
IReadOnlyList<RewardReputationGrantApplied> grants,
|
||||||
|
IFactionStandingStore standingStore,
|
||||||
|
IReputationDeltaStore auditStore,
|
||||||
|
TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
for (var i = grants.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var grant = grants[i];
|
||||||
|
if (grant.Amount == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var forwardDeltaId = RewardDeliveryIds.MakeReputationDeltaId(idempotencyKey, grant.FactionId);
|
||||||
|
var rollbackDeltaId = RewardDeliveryIds.MakeReputationRollbackDeltaId(forwardDeltaId);
|
||||||
|
_ = ReputationOperations.TryApplyDelta(
|
||||||
|
playerId,
|
||||||
|
grant.FactionId,
|
||||||
|
-grant.Amount,
|
||||||
|
rollbackDeltaId,
|
||||||
|
ReputationDeltaSourceKinds.QuestCompletion,
|
||||||
|
normalizedQuestId,
|
||||||
|
standingStore,
|
||||||
|
auditStore,
|
||||||
|
timeProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void ResyncPathAutoPerksAfterRaceRollback(
|
private static void ResyncPathAutoPerksAfterRaceRollback(
|
||||||
string playerId,
|
string playerId,
|
||||||
IReadOnlyList<RewardSkillXpGrantApplied> appliedSkillXp,
|
IReadOnlyList<RewardSkillXpGrantApplied> appliedSkillXp,
|
||||||
|
|
@ -219,6 +379,18 @@ public static class RewardRouterOperations
|
||||||
_ => reasonCode,
|
_ => reasonCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string MapReputationReason(string? reasonCode) =>
|
||||||
|
reasonCode switch
|
||||||
|
{
|
||||||
|
FactionStandingReasonCodes.UnknownFaction => RewardDeliveryReasonCodes.UnknownFaction,
|
||||||
|
ReputationApplyReasonCodes.InvalidDelta => RewardDeliveryReasonCodes.InvalidDelta,
|
||||||
|
ReputationApplyReasonCodes.InvalidDeltaId => RewardDeliveryReasonCodes.InvalidDeltaId,
|
||||||
|
ReputationApplyReasonCodes.InvalidSource => RewardDeliveryReasonCodes.InvalidSource,
|
||||||
|
ReputationApplyReasonCodes.AuditAppendFailed => RewardDeliveryReasonCodes.AuditAppendFailed,
|
||||||
|
null => RewardDeliveryReasonCodes.UnknownFaction,
|
||||||
|
_ => reasonCode,
|
||||||
|
};
|
||||||
|
|
||||||
private static void CompensatingRemoveItems(
|
private static void CompensatingRemoveItems(
|
||||||
string playerId,
|
string playerId,
|
||||||
IReadOnlyList<RewardItemGrantApplied> grants,
|
IReadOnlyList<RewardItemGrantApplied> grants,
|
||||||
|
|
@ -243,6 +415,7 @@ public static class RewardRouterOperations
|
||||||
ReasonCode: reasonCode,
|
ReasonCode: reasonCode,
|
||||||
GrantedItems: EmptyItemGrants,
|
GrantedItems: EmptyItemGrants,
|
||||||
GrantedSkillXp: EmptySkillXpGrants,
|
GrantedSkillXp: EmptySkillXpGrants,
|
||||||
|
GrantedReputation: EmptyReputationGrants,
|
||||||
DeliveryEvent: null,
|
DeliveryEvent: null,
|
||||||
DeliveredAt: null);
|
DeliveredAt: null);
|
||||||
|
|
||||||
|
|
@ -252,6 +425,7 @@ public static class RewardRouterOperations
|
||||||
ReasonCode: reasonCode,
|
ReasonCode: reasonCode,
|
||||||
GrantedItems: deliveryEvent.GrantedItems,
|
GrantedItems: deliveryEvent.GrantedItems,
|
||||||
GrantedSkillXp: deliveryEvent.GrantedSkillXp,
|
GrantedSkillXp: deliveryEvent.GrantedSkillXp,
|
||||||
|
GrantedReputation: deliveryEvent.GrantedReputation,
|
||||||
DeliveryEvent: deliveryEvent,
|
DeliveryEvent: deliveryEvent,
|
||||||
DeliveredAt: deliveryEvent.DeliveredAt);
|
DeliveredAt: deliveryEvent.DeliveredAt);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ Game code applies standing changes through **`ReputationOperations.TryApplyDelta
|
||||||
|
|
||||||
**Pre-flight denies (no store mutation):** `deltaAmount == 0` → **`invalid_delta`**; empty `deltaId` → **`invalid_delta_id`**; empty `sourceKind` or `sourceId` → **`invalid_source`**. Store boundary denies (**`unknown_faction`**, **`player_not_writable`**) pass through unchanged.
|
**Pre-flight denies (no store mutation):** `deltaAmount == 0` → **`invalid_delta`**; empty `deltaId` → **`invalid_delta_id`**; empty `sourceKind` or `sourceId` → **`invalid_source`**. Store boundary denies (**`unknown_faction`**, **`player_not_writable`**) pass through unchanged.
|
||||||
|
|
||||||
**Prototype source kind:** **`quest_completion`** (`ReputationDeltaSourceKinds.QuestCompletion`); quest id as `sourceId`. Quest bundle wiring lands in NEO-138 (`RewardRouterOperations`).
|
**Prototype source kind:** **`quest_completion`** (`ReputationDeltaSourceKinds.QuestCompletion`); quest id as `sourceId`. Quest bundle wiring: **`RewardRouterOperations.TryDeliverQuestCompletion`** (NEO-138).
|
||||||
|
|
||||||
Plan: [NEO-136 implementation plan](../../docs/plans/NEO-136-implementation-plan.md).
|
Plan: [NEO-136 implementation plan](../../docs/plans/NEO-136-implementation-plan.md).
|
||||||
|
|
||||||
|
|
@ -348,6 +348,7 @@ Manual QA: [`docs/manual-qa/NEO-121.md`](../../docs/manual-qa/NEO-121.md); plan:
|
||||||
| **`DeliveredAt`** | UTC timestamp at delivery record commit. |
|
| **`DeliveredAt`** | UTC timestamp at delivery record commit. |
|
||||||
| **`GrantedItems`** | Snapshot of applied item rows (`itemId`, `quantity`) at commit time. |
|
| **`GrantedItems`** | Snapshot of applied item rows (`itemId`, `quantity`) at commit time. |
|
||||||
| **`GrantedSkillXp`** | Snapshot of applied skill XP rows (`skillId`, `amount`) at commit time. |
|
| **`GrantedSkillXp`** | Snapshot of applied skill XP rows (`skillId`, `amount`) at commit time. |
|
||||||
|
| **`GrantedReputation`** | Snapshot of applied reputation rows (`factionId`, `amount`) at commit time (NEO-138). |
|
||||||
| **`IdempotencyKey`** | Stable **`{playerId}:quest_complete:{questId}`** — dedupe input for [E7.M2](../../docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md) quest completion delivery. |
|
| **`IdempotencyKey`** | Stable **`{playerId}:quest_complete:{questId}`** — dedupe input for [E7.M2](../../docs/decomposition/modules/E7_M2_RewardAndUnlockRouter.md) quest completion delivery. |
|
||||||
|
|
||||||
**Store interface:**
|
**Store interface:**
|
||||||
|
|
@ -359,7 +360,7 @@ Encounter loot remains on **`IEncounterCompleteEventStore`** (E5.M3); quest comp
|
||||||
|
|
||||||
## Reward router (NEO-127)
|
## Reward router (NEO-127)
|
||||||
|
|
||||||
**`RewardRouterOperations.TryDeliverQuestCompletion`** applies a quest **`completionRewardBundle`** via **`PlayerInventoryOperations`** (item rows) and **`SkillProgressionGrantOperations.TryApplyGrant`** with fixed **`mission_reward`** source kind (skill XP rows), then records **`RewardDeliveryEvent`** in **`IRewardDeliveryStore`**.
|
**`RewardRouterOperations.TryDeliverQuestCompletion`** applies a quest **`completionRewardBundle`** via **`PlayerInventoryOperations`** (item rows), **`SkillProgressionGrantOperations.TryApplyGrant`** with fixed **`mission_reward`** source kind (skill XP rows), and **`ReputationOperations.TryApplyDelta`** with **`ReputationDeltaSourceKinds.QuestCompletion`** (reputation rows — NEO-138), then records **`RewardDeliveryEvent`** in **`IRewardDeliveryStore`**.
|
||||||
|
|
||||||
| Reason code | When |
|
| Reason code | When |
|
||||||
|-------------|------|
|
|-------------|------|
|
||||||
|
|
@ -369,8 +370,9 @@ Encounter loot remains on **`IEncounterCompleteEventStore`** (E5.M3); quest comp
|
||||||
| **`inventory_full`**, **`invalid_item`**, **`invalid_quantity`** | Item grant pre-flight or apply failed. |
|
| **`inventory_full`**, **`invalid_item`**, **`invalid_quantity`** | Item grant pre-flight or apply failed. |
|
||||||
| **`unknown_skill`**, **`invalid_amount`**, **`source_kind_not_allowed`** | Skill XP grant denied. |
|
| **`unknown_skill`**, **`invalid_amount`**, **`source_kind_not_allowed`** | Skill XP grant denied. |
|
||||||
| **`progression_store_missing`** | Skill progression store could not apply XP delta. |
|
| **`progression_store_missing`** | Skill progression store could not apply XP delta. |
|
||||||
|
| **`unknown_faction`**, **`invalid_delta`**, **`audit_append_failed`**, **`invalid_delta_id`**, **`invalid_source`** | Reputation grant denied via **`ReputationOperations`** (NEO-138). |
|
||||||
|
|
||||||
Apply order: **items first**, then **skill XP**. Inventory deny or skill deny fails closed without store record (compensating item rollback on partial apply or skill failure after items). Idempotent replay checks **`TryGet`** before apply. If grants succeed but **`TryRecord`** returns `false` (concurrent race), this call **rolls back** its item grants and calls **`SkillProgressionGrantOperations.TryRevertAppliedSkillGrants`** — negative XP delta per applied skill row plus removal of perks unlocked during this call only — then **`ReevaluateAfterLevelUp`** at post-rollback levels so a concurrent winner's path-auto unlocks are preserved — then returns the existing store event (mirror **`EncounterCompletionOperations`** compensating rollback when **`TryMarkCompleted`** loses). **Quest-state wiring (NEO-128):** **`QuestStateOperations.TryMarkComplete`** and **`QuestObjectiveWiring`** terminal-step completion call this operation before **`IPlayerQuestStateStore.TryMarkComplete`**. Plans: [NEO-127](../../docs/plans/NEO-127-implementation-plan.md), [NEO-128](../../docs/plans/NEO-128-implementation-plan.md).
|
Apply order: **items first**, then **skill XP**, then **reputation**. Any deny fails closed without store record (compensating rollback of all prior grant types on partial apply). Reputation **`deltaId`**: **`{idempotencyKey}:rep:{factionId}`**; race/compensating revert uses **`:rollback`** suffix on inverse delta. Idempotent replay checks **`TryGet`** before apply. If grants succeed but **`TryRecord`** returns `false` (concurrent race), this call **rolls back** item grants, reverts skill XP via **`SkillProgressionGrantOperations.TryRevertAppliedSkillGrants`**, and reverts reputation via inverse **`TryApplyDelta`** — then returns the existing store event (mirror **`EncounterCompletionOperations`** compensating rollback when **`TryMarkCompleted`** loses). **Quest-state wiring (NEO-128):** **`QuestStateOperations.TryMarkComplete`** and **`QuestObjectiveWiring`** terminal-step completion call this operation before **`IPlayerQuestStateStore.TryMarkComplete`**. Bruno quest-flow smoke: `bruno/neon-sprawl-server/quest-progress/Accept grid contract after operator chain.bru` (NEO-138). Plans: [NEO-127](../../docs/plans/NEO-127-implementation-plan.md), [NEO-128](../../docs/plans/NEO-128-implementation-plan.md), [NEO-138](../../docs/plans/NEO-138-implementation-plan.md).
|
||||||
|
|
||||||
### Reward telemetry hooks (NEO-130)
|
### Reward telemetry hooks (NEO-130)
|
||||||
|
|
||||||
|
|
@ -379,6 +381,7 @@ Comment-only hook sites in **`RewardRouterOperations.TryDeliverQuestCompletion`*
|
||||||
| Event | Anchor | When |
|
| Event | Anchor | When |
|
||||||
|-------|--------|------|
|
|-------|--------|------|
|
||||||
| **`reward_delivery`** | **`TryDeliverQuestCompletion`** | After **`TryRecord`** returns **`true`** on first-time delivery (not idempotent **`TryGet`** replay or race-loser paths). |
|
| **`reward_delivery`** | **`TryDeliverQuestCompletion`** | After **`TryRecord`** returns **`true`** on first-time delivery (not idempotent **`TryGet`** replay or race-loser paths). |
|
||||||
|
| **`reputation_delta`** | **`TryDeliverQuestCompletion`** (stub) | After each successful reputation row apply (NEO-138); consolidated in NEO-141. |
|
||||||
| **`unlock_granted`** | **`TryDeliverQuestCompletion`** (stub) | Future **`UnlockGrant`** apply from bundle rows — prototype bundles have item + skill XP only; no runtime unlock apply in Slice 2. |
|
| **`unlock_granted`** | **`TryDeliverQuestCompletion`** (stub) | Future **`UnlockGrant`** apply from bundle rows — prototype bundles have item + skill XP only; no runtime unlock apply in Slice 2. |
|
||||||
|
|
||||||
**`perk_unlock`** side effects from skill XP grants use **`PerkUnlockEngine`** ([NEO-49](#perk-unlock-engine-and-telemetry-hooks-neo-47-neo-49)) — distinct from content **`UnlockGrant`** rows. Manual QA: [`docs/manual-qa/NEO-130.md`](../../docs/manual-qa/NEO-130.md); plan: [NEO-130 implementation plan](../../docs/plans/NEO-130-implementation-plan.md).
|
**`perk_unlock`** side effects from skill XP grants use **`PerkUnlockEngine`** ([NEO-49](#perk-unlock-engine-and-telemetry-hooks-neo-47-neo-49)) — distinct from content **`UnlockGrant`** rows. Manual QA: [`docs/manual-qa/NEO-130.md`](../../docs/manual-qa/NEO-130.md); plan: [NEO-130 implementation plan](../../docs/plans/NEO-130-implementation-plan.md).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue