228 lines
7.7 KiB
JavaScript
228 lines
7.7 KiB
JavaScript
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,
|
|
};
|