@aephia/atlas-kit 0.5.0 → 0.6.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -27
- package/dist/cargo/actions/deposit.d.ts +52 -0
- package/dist/cargo/actions/deposit.d.ts.map +1 -0
- package/dist/cargo/actions/deposit.js +192 -0
- package/dist/cargo/actions/deposit.js.map +1 -0
- package/dist/cargo/actions/index.d.ts +1 -0
- package/dist/cargo/actions/index.d.ts.map +1 -1
- package/dist/cargo/actions/index.js +1 -0
- package/dist/cargo/actions/index.js.map +1 -1
- package/dist/cargo/actions/wallet-token.d.ts +8 -0
- package/dist/cargo/actions/wallet-token.d.ts.map +1 -0
- package/dist/cargo/actions/wallet-token.js +58 -0
- package/dist/cargo/actions/wallet-token.js.map +1 -0
- package/dist/cargo/index.d.ts +1 -0
- package/dist/cargo/index.d.ts.map +1 -1
- package/dist/cargo/index.js +1 -0
- package/dist/cargo/index.js.map +1 -1
- package/dist/claim-stakes/actions/index.d.ts +393 -0
- package/dist/claim-stakes/actions/index.d.ts.map +1 -0
- package/dist/claim-stakes/actions/index.js +1747 -0
- package/dist/claim-stakes/actions/index.js.map +1 -0
- package/dist/claim-stakes/index.d.ts +64 -0
- package/dist/claim-stakes/index.d.ts.map +1 -1
- package/dist/claim-stakes/index.js +49 -0
- package/dist/claim-stakes/index.js.map +1 -1
- package/dist/client/context.d.ts.map +1 -1
- package/dist/client/context.js +2 -1
- package/dist/client/context.js.map +1 -1
- package/dist/client/errors.d.ts +12 -3
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +11 -0
- package/dist/client/errors.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/lifecycle.d.ts +2 -0
- package/dist/client/lifecycle.d.ts.map +1 -1
- package/dist/client/lifecycle.js +31 -0
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/combat/actions/index.d.ts +239 -0
- package/dist/combat/actions/index.d.ts.map +1 -0
- package/dist/combat/actions/index.js +1146 -0
- package/dist/combat/actions/index.js.map +1 -0
- package/dist/crafting/actions/index.d.ts +140 -0
- package/dist/crafting/actions/index.d.ts.map +1 -0
- package/dist/crafting/actions/index.js +728 -0
- package/dist/crafting/actions/index.js.map +1 -0
- package/dist/crafting/index.d.ts +74 -8
- package/dist/crafting/index.d.ts.map +1 -1
- package/dist/crafting/index.js +55 -9
- package/dist/crafting/index.js.map +1 -1
- package/dist/fleets/actions/index.d.ts +5 -4
- package/dist/fleets/actions/index.d.ts.map +1 -1
- package/dist/fleets/actions/index.js +85 -58
- package/dist/fleets/actions/index.js.map +1 -1
- package/dist/fleets/index.d.ts +1 -0
- package/dist/fleets/index.d.ts.map +1 -1
- package/dist/fleets/index.js +1 -0
- package/dist/fleets/index.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/c4/claim-stake-building-definitions.d.ts +50 -0
- package/dist/internal/c4/claim-stake-building-definitions.d.ts.map +1 -0
- package/dist/internal/c4/claim-stake-building-definitions.js +56 -0
- package/dist/internal/c4/claim-stake-building-definitions.js.map +1 -0
- package/dist/internal/c4/claim-stake-configuration.d.ts +17 -0
- package/dist/internal/c4/claim-stake-configuration.d.ts.map +1 -0
- package/dist/internal/c4/claim-stake-configuration.js +47 -0
- package/dist/internal/c4/claim-stake-configuration.js.map +1 -0
- package/dist/internal/c4/claim-stake-definitions.d.ts +37 -0
- package/dist/internal/c4/claim-stake-definitions.d.ts.map +1 -0
- package/dist/internal/c4/claim-stake-definitions.js +77 -0
- package/dist/internal/c4/claim-stake-definitions.js.map +1 -0
- package/dist/internal/c4/fleet.d.ts.map +1 -1
- package/dist/internal/c4/fleet.js +2 -102
- package/dist/internal/c4/fleet.js.map +1 -1
- package/dist/internal/c4/game-definition-layout.d.ts +11 -0
- package/dist/internal/c4/game-definition-layout.d.ts.map +1 -0
- package/dist/internal/c4/game-definition-layout.js +32 -0
- package/dist/internal/c4/game-definition-layout.js.map +1 -0
- package/dist/internal/c4/game.d.ts +19 -1
- package/dist/internal/c4/game.d.ts.map +1 -1
- package/dist/internal/c4/game.js +26 -37
- package/dist/internal/c4/game.js.map +1 -1
- package/dist/internal/c4/profile-faction.d.ts +6 -0
- package/dist/internal/c4/profile-faction.d.ts.map +1 -1
- package/dist/internal/c4/profile-faction.js +22 -1
- package/dist/internal/c4/profile-faction.js.map +1 -1
- package/dist/internal/c4/ship-stats.d.ts +5 -0
- package/dist/internal/c4/ship-stats.d.ts.map +1 -0
- package/dist/internal/c4/ship-stats.js +106 -0
- package/dist/internal/c4/ship-stats.js.map +1 -0
- package/dist/internal/c4/world.d.ts +23 -1
- package/dist/internal/c4/world.d.ts.map +1 -1
- package/dist/internal/c4/world.js +53 -1
- package/dist/internal/c4/world.js.map +1 -1
- package/dist/internal/crafting-process-state.d.ts +15 -0
- package/dist/internal/crafting-process-state.d.ts.map +1 -0
- package/dist/internal/crafting-process-state.js +12 -0
- package/dist/internal/crafting-process-state.js.map +1 -0
- package/dist/internal/planning/authorization-adapter.d.ts +79 -0
- package/dist/internal/planning/authorization-adapter.d.ts.map +1 -1
- package/dist/internal/planning/authorization-adapter.js +402 -0
- package/dist/internal/planning/authorization-adapter.js.map +1 -1
- package/dist/internal/read-meta.d.ts +39 -3
- package/dist/internal/read-meta.d.ts.map +1 -1
- package/dist/internal/read-meta.js +0 -10
- package/dist/internal/read-meta.js.map +1 -1
- package/dist/loyalty/actions/index.d.ts +40 -0
- package/dist/loyalty/actions/index.d.ts.map +1 -0
- package/dist/loyalty/actions/index.js +159 -0
- package/dist/loyalty/actions/index.js.map +1 -0
- package/dist/markets/actions/index.d.ts +100 -0
- package/dist/markets/actions/index.d.ts.map +1 -0
- package/dist/markets/actions/index.js +417 -0
- package/dist/markets/actions/index.js.map +1 -0
- package/dist/planning/assembly.d.ts +4 -0
- package/dist/planning/assembly.d.ts.map +1 -1
- package/dist/planning/assembly.js +13 -0
- package/dist/planning/assembly.js.map +1 -1
- package/dist/planning/composition.d.ts.map +1 -1
- package/dist/planning/composition.js +20 -1
- package/dist/planning/composition.js.map +1 -1
- package/dist/planning/execution.d.ts.map +1 -1
- package/dist/planning/execution.js +7 -4
- package/dist/planning/execution.js.map +1 -1
- package/dist/planning/index.d.ts +27 -8
- package/dist/planning/index.d.ts.map +1 -1
- package/dist/planning/index.js +57 -13
- package/dist/planning/index.js.map +1 -1
- package/dist/planning/simulation.d.ts.map +1 -1
- package/dist/planning/simulation.js +2 -1
- package/dist/planning/simulation.js.map +1 -1
- package/dist/registry/cargo.d.ts +17 -0
- package/dist/registry/cargo.d.ts.map +1 -0
- package/dist/registry/cargo.js +35 -0
- package/dist/registry/cargo.js.map +1 -0
- package/dist/registry/claim-stakes.d.ts +11 -0
- package/dist/registry/claim-stakes.d.ts.map +1 -0
- package/dist/registry/claim-stakes.js +15 -0
- package/dist/registry/claim-stakes.js.map +1 -0
- package/dist/registry/definition-section-snapshot.d.ts +42 -0
- package/dist/registry/definition-section-snapshot.d.ts.map +1 -0
- package/dist/registry/definition-section-snapshot.js +214 -0
- package/dist/registry/definition-section-snapshot.js.map +1 -0
- package/dist/registry/index.d.ts +114 -7
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +372 -101
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/publication.d.ts +8 -0
- package/dist/registry/publication.d.ts.map +1 -0
- package/dist/registry/publication.js +10 -0
- package/dist/registry/publication.js.map +1 -0
- package/dist/registry/ship-stats-snapshot.d.ts +8 -0
- package/dist/registry/ship-stats-snapshot.d.ts.map +1 -0
- package/dist/registry/ship-stats-snapshot.js +154 -0
- package/dist/registry/ship-stats-snapshot.js.map +1 -0
- package/dist/world/index.d.ts +22 -1
- package/dist/world/index.d.ts.map +1 -1
- package/dist/world/index.js +24 -1
- package/dist/world/index.js.map +1 -1
- package/package.json +21 -1
|
@@ -0,0 +1,1146 @@
|
|
|
1
|
+
import { getAddressEncoder, isAddress } from '@solana/kit';
|
|
2
|
+
import { InvalidActionInputError, SageSdkError } from '../../client/errors.js';
|
|
3
|
+
import { readContextRawAccounts } from '../../client/reconciliation.js';
|
|
4
|
+
import { sage } from '../../internal/c4/bindings.js';
|
|
5
|
+
import { lootAccountDefinition, } from '../../internal/c4/combat.js';
|
|
6
|
+
import { deriveFleetAddress, fleetAccountDefinition, } from '../../internal/c4/fleet.js';
|
|
7
|
+
import { deriveFleetCrewBindingAddress, fleetCrewBindingAccountDefinition, } from '../../internal/c4/fleet-crew-binding.js';
|
|
8
|
+
import { deriveFactionAccountAddress, deriveFactionEconomicsConfigAddress, deriveFactionOwnershipAddress, deriveFactionRelationTrackerAddress, deriveFactionTreasuryAddress, deriveKingSystemTrackerAddress, deriveRegionTrackerAddress, factionAccountDefinition, factionEconomicsConfigAccountDefinition, factionOwnershipAccountDefinition, factionRelationTrackerAccountDefinition, factionTreasuryAccountDefinition, kingSystemTrackerAccountDefinition, regionTrackerAccountDefinition, } from '../../internal/c4/factions.js';
|
|
9
|
+
import { characterAccountDefinition, findCharacterPda, } from '../../internal/c4/identity.js';
|
|
10
|
+
import { deriveLoyaltyContributionAddress, deriveLoyaltyEpochAddress, loyaltyContributionAccountDefinition, loyaltyEpochAccountDefinition, } from '../../internal/c4/loyalty.js';
|
|
11
|
+
import { atlasRewardConfigAccountDefinition, atlasRewardRegistryAccountDefinition, deriveAtlasRewardConfigAddress, deriveAtlasRewardRegistryAddress, } from '../../internal/c4/rewards.js';
|
|
12
|
+
import { deriveStarSystemAddress, starSystemAccountDefinition, } from '../../internal/c4/world.js';
|
|
13
|
+
import { requireGameAddress } from '../../internal/game-context.js';
|
|
14
|
+
import { adaptApplyCombatStimulantInstruction, adaptAttackFleetInstruction, adaptAttackStarbaseInstruction, adaptRetrieveLootInstruction, validatePlanAuthorization, } from '../../internal/planning/authorization-adapter.js';
|
|
15
|
+
import { createPlan, } from '../../planning/index.js';
|
|
16
|
+
import { readGameCombatConfiguration } from '../../registry/combat.js';
|
|
17
|
+
import { resolveCargo, } from '../../registry/index.js';
|
|
18
|
+
function invalid(field, value, acceptedRange, remedy) {
|
|
19
|
+
throw new InvalidActionInputError(field, value, acceptedRange, remedy);
|
|
20
|
+
}
|
|
21
|
+
function readRecord(value, field) {
|
|
22
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
23
|
+
return invalid(field, value, 'a plain object', `Provide ${field} as the documented object before retrying.`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function assertKeys(value, field, required, optional = []) {
|
|
28
|
+
const accepted = new Set([...required, ...optional]);
|
|
29
|
+
const missing = required.find((key) => !Object.hasOwn(value, key));
|
|
30
|
+
const unexpected = Object.keys(value).find((key) => !accepted.has(key));
|
|
31
|
+
if (missing !== undefined || unexpected !== undefined) {
|
|
32
|
+
invalid(field, missing === undefined ? unexpected : `missing ${missing}`, `exactly the fields ${[...required, ...optional].join(', ')}`, `Remove unknown fields and provide every required ${field} field before retrying.`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function validateOptions(value) {
|
|
36
|
+
const options = readRecord(value, 'options');
|
|
37
|
+
assertKeys(options, 'options', ['authorization']);
|
|
38
|
+
const authorizationRecord = readRecord(options.authorization, 'authorization');
|
|
39
|
+
assertKeys(authorizationRecord, 'authorization', ['profile', 'authority', 'keyIndex'], ['certificate']);
|
|
40
|
+
const authorization = authorizationRecord;
|
|
41
|
+
validatePlanAuthorization(authorization);
|
|
42
|
+
return { authorization };
|
|
43
|
+
}
|
|
44
|
+
function validateAttackOptions(value) {
|
|
45
|
+
const options = readRecord(value, 'options');
|
|
46
|
+
assertKeys(options, 'options', ['authorization', 'loot']);
|
|
47
|
+
const authorizationRecord = readRecord(options.authorization, 'authorization');
|
|
48
|
+
assertKeys(authorizationRecord, 'authorization', ['profile', 'authority', 'keyIndex'], ['certificate']);
|
|
49
|
+
const authorization = authorizationRecord;
|
|
50
|
+
validatePlanAuthorization(authorization);
|
|
51
|
+
if (typeof options.loot !== 'string' || !isAddress(options.loot)) {
|
|
52
|
+
invalid('options.loot', options.loot, 'one valid new Loot account address', 'Generate a new address-only Loot signer and provide its public address before retrying.');
|
|
53
|
+
}
|
|
54
|
+
return { authorization, loot: options.loot };
|
|
55
|
+
}
|
|
56
|
+
function validateAttackStance(value) {
|
|
57
|
+
const stance = readRecord(value, 'stance');
|
|
58
|
+
assertKeys(stance, 'stance', ['ammoUsage']);
|
|
59
|
+
if (stance.ammoUsage !== 'none' &&
|
|
60
|
+
stance.ammoUsage !== 'light' &&
|
|
61
|
+
stance.ammoUsage !== 'heavy') {
|
|
62
|
+
invalid('stance.ammoUsage', stance.ammoUsage, 'one of none, light, or heavy', 'Choose a documented Fleet attack ammunition stance before retrying.');
|
|
63
|
+
}
|
|
64
|
+
return { ammoUsage: stance.ammoUsage };
|
|
65
|
+
}
|
|
66
|
+
function validateRetrieveOptions(value) {
|
|
67
|
+
const options = readRecord(value, 'options');
|
|
68
|
+
assertKeys(options, 'options', ['authorization', 'atUnixSeconds']);
|
|
69
|
+
const authorizationRecord = readRecord(options.authorization, 'authorization');
|
|
70
|
+
assertKeys(authorizationRecord, 'authorization', ['profile', 'authority', 'keyIndex'], ['certificate']);
|
|
71
|
+
const authorization = authorizationRecord;
|
|
72
|
+
validatePlanAuthorization(authorization);
|
|
73
|
+
if (typeof options.atUnixSeconds !== 'bigint' || options.atUnixSeconds < 0n) {
|
|
74
|
+
invalid('options.atUnixSeconds', options.atUnixSeconds, 'a non-negative bigint Unix timestamp', 'Provide the explicit current Unix timestamp used to evaluate Loot exclusivity before retrying.');
|
|
75
|
+
}
|
|
76
|
+
return { atUnixSeconds: options.atUnixSeconds, authorization };
|
|
77
|
+
}
|
|
78
|
+
function denseArray(value, field) {
|
|
79
|
+
if (!Array.isArray(value)) {
|
|
80
|
+
return invalid(field, value, 'a dense ordered array', `Provide ${field} as a dense ordered array before retrying.`);
|
|
81
|
+
}
|
|
82
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
83
|
+
if (!Object.hasOwn(value, index)) {
|
|
84
|
+
invalid(field, value, 'a dense ordered array without holes', `Remove array holes from ${field} before retrying.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
const U64_MAX = (1n << 64n) - 1n;
|
|
90
|
+
// Measured by the boundary test using a distinct external fee payer, optional
|
|
91
|
+
// certificate and Loot creator, and the exact generated instruction in the v0
|
|
92
|
+
// message shape used by assembly.
|
|
93
|
+
const RETRIEVE_LOOT_FIXED_TRANSACTION_BYTES = 599;
|
|
94
|
+
const V0_TRANSACTION_SIZE_LIMIT_BYTES = 1_232;
|
|
95
|
+
const MAX_RETRIEVE_LOOT_INSTRUCTION_DATA_BYTES = V0_TRANSACTION_SIZE_LIMIT_BYTES - RETRIEVE_LOOT_FIXED_TRANSACTION_BYTES;
|
|
96
|
+
function validateLootSelection(value, loot, fleet, retrievingProfile, atUnixSeconds) {
|
|
97
|
+
const targets = denseArray(value, 'want');
|
|
98
|
+
if (targets.length === 0 || targets.length > 256) {
|
|
99
|
+
invalid('want', targets.length, 'between 1 and 256 requested Loot items', 'Request at least one available Loot item and no more than 256 before retrying.');
|
|
100
|
+
}
|
|
101
|
+
const seenTargets = new Set();
|
|
102
|
+
const generated = [];
|
|
103
|
+
const summary = [];
|
|
104
|
+
for (let targetPosition = 0; targetPosition < targets.length; targetPosition += 1) {
|
|
105
|
+
const field = `want[${String(targetPosition)}]`;
|
|
106
|
+
const target = readRecord(targets[targetPosition], field);
|
|
107
|
+
assertKeys(target, field, ['itemIndex', 'cargo']);
|
|
108
|
+
if (!Number.isSafeInteger(target.itemIndex) ||
|
|
109
|
+
target.itemIndex < 0 ||
|
|
110
|
+
target.itemIndex > 255) {
|
|
111
|
+
invalid(`${field}.itemIndex`, target.itemIndex, 'an integer from 0 through 255', 'Choose an existing Loot item index before retrying.');
|
|
112
|
+
}
|
|
113
|
+
const itemIndex = target.itemIndex;
|
|
114
|
+
if (seenTargets.has(itemIndex)) {
|
|
115
|
+
invalid(`${field}.itemIndex`, itemIndex, 'one unique Loot item index', 'Combine cargo for each Loot item into one target before retrying.');
|
|
116
|
+
}
|
|
117
|
+
seenTargets.add(itemIndex);
|
|
118
|
+
const item = loot.items[itemIndex];
|
|
119
|
+
if (item === undefined) {
|
|
120
|
+
invalid(`${field}.itemIndex`, itemIndex, `an available Loot item index below ${String(loot.items.length)}`, 'Refresh the Loot snapshot and select an existing item before retrying.');
|
|
121
|
+
}
|
|
122
|
+
if (item.destroyer.address !== fleet.address &&
|
|
123
|
+
atUnixSeconds < item.exclusivityUnlockAtUnixSeconds) {
|
|
124
|
+
fail(`Loot item ${String(itemIndex)} is exclusive to Fleet ${item.destroyer.address}, not Fleet ${fleet.address}. Use the destroying Fleet or wait for chain-authoritative exclusivity resolution before retrying.`, loot.address);
|
|
125
|
+
}
|
|
126
|
+
if (loot.profile.address !== retrievingProfile &&
|
|
127
|
+
atUnixSeconds < item.rewardReclaimAtUnixSeconds) {
|
|
128
|
+
fail(`Loot item ${String(itemIndex)} reward cargo is exclusive to Profile ${loot.profile.address}, not Profile ${retrievingProfile}, until ${String(item.rewardReclaimAtUnixSeconds)} Unix seconds. Use the reward-owning Profile or wait for chain-authoritative reward reclaim resolution before retrying.`, loot.address);
|
|
129
|
+
}
|
|
130
|
+
const cargoRequests = denseArray(target.cargo, `${field}.cargo`);
|
|
131
|
+
if (cargoRequests.length === 0 || cargoRequests.length > 65_536) {
|
|
132
|
+
invalid(`${field}.cargo`, cargoRequests.length, 'between 1 and 65536 requested cargo quantities', 'Request at least one known cargo quantity before retrying.');
|
|
133
|
+
}
|
|
134
|
+
const seenCargo = new Set();
|
|
135
|
+
const encoded = [];
|
|
136
|
+
for (let cargoPosition = 0; cargoPosition < cargoRequests.length; cargoPosition += 1) {
|
|
137
|
+
const cargoField = `${field}.cargo[${String(cargoPosition)}]`;
|
|
138
|
+
const request = readRecord(cargoRequests[cargoPosition], cargoField);
|
|
139
|
+
assertKeys(request, cargoField, ['cargoId', 'quantityRaw']);
|
|
140
|
+
if (!Number.isSafeInteger(request.cargoId) ||
|
|
141
|
+
request.cargoId < 0 ||
|
|
142
|
+
request.cargoId > 65_535) {
|
|
143
|
+
invalid(`${cargoField}.cargoId`, request.cargoId, 'an integer CargoId from 0 through 65535', 'Choose a cargo id present in the selected Loot item before retrying.');
|
|
144
|
+
}
|
|
145
|
+
const cargoId = request.cargoId;
|
|
146
|
+
if (seenCargo.has(cargoId)) {
|
|
147
|
+
invalid(`${cargoField}.cargoId`, cargoId, 'one unique CargoId per Loot item', 'Combine duplicate cargo quantities before retrying.');
|
|
148
|
+
}
|
|
149
|
+
seenCargo.add(cargoId);
|
|
150
|
+
if (typeof request.quantityRaw !== 'bigint' ||
|
|
151
|
+
request.quantityRaw <= 0n ||
|
|
152
|
+
request.quantityRaw > U64_MAX) {
|
|
153
|
+
invalid(`${cargoField}.quantityRaw`, request.quantityRaw, 'a positive raw-unit bigint fitting u64', 'Provide a positive raw cargo quantity no larger than u64 before retrying.');
|
|
154
|
+
}
|
|
155
|
+
const available = item.inventory.items.find(({ cargo }) => cargo.id === cargoId);
|
|
156
|
+
if (available === undefined ||
|
|
157
|
+
request.quantityRaw > available.quantityRaw) {
|
|
158
|
+
invalid(cargoField, { cargoId, quantityRaw: request.quantityRaw }, 'a known cargo id and quantity no greater than the selected Loot balance', 'Refresh the Loot snapshot and request no more than its known cargo quantity.');
|
|
159
|
+
}
|
|
160
|
+
encoded.push([cargoId, request.quantityRaw]);
|
|
161
|
+
summary.push(`${String(request.quantityRaw)} raw ${available.cargo.name}`);
|
|
162
|
+
}
|
|
163
|
+
generated.push([itemIndex, encoded]);
|
|
164
|
+
}
|
|
165
|
+
const instructionDataLength = sage
|
|
166
|
+
.getRetrieveLootInstructionDataEncoder()
|
|
167
|
+
.encode({
|
|
168
|
+
keyIndex: 0,
|
|
169
|
+
want: generated.map(([itemIndex, cargo]) => [
|
|
170
|
+
itemIndex,
|
|
171
|
+
cargo.map(([cargoId, quantity]) => [cargoId, quantity]),
|
|
172
|
+
]),
|
|
173
|
+
}).length;
|
|
174
|
+
if (instructionDataLength > MAX_RETRIEVE_LOOT_INSTRUCTION_DATA_BYTES) {
|
|
175
|
+
invalid('want', instructionDataLength, `a selection encoding to at most ${String(MAX_RETRIEVE_LOOT_INSTRUCTION_DATA_BYTES)} instruction-data bytes`, 'Reduce the requested Loot items or cargo quantities so the singleton instruction fits one z.ink/Solana transaction.');
|
|
176
|
+
}
|
|
177
|
+
return { generated, summary: summary.join(', ') };
|
|
178
|
+
}
|
|
179
|
+
function fail(message, address) {
|
|
180
|
+
throw new SageSdkError('ACTION_PRECONDITION_FAILED', message, { address });
|
|
181
|
+
}
|
|
182
|
+
function requireAccount(account, address, message) {
|
|
183
|
+
return account ?? fail(message, address);
|
|
184
|
+
}
|
|
185
|
+
function stimulantFingerprint(stimulant) {
|
|
186
|
+
const effects = stimulant.effects
|
|
187
|
+
.map(({ charges, deltaBps, stackRuleId, statId }) => `${String(charges)}:${String(deltaBps)}:${String(stackRuleId)}:${String(statId)}`)
|
|
188
|
+
.join(',');
|
|
189
|
+
return [
|
|
190
|
+
stimulant.cargo.id,
|
|
191
|
+
stimulant.cargo.categoryId,
|
|
192
|
+
stimulant.cargo.mint,
|
|
193
|
+
stimulant.cargo.name,
|
|
194
|
+
stimulant.cargo.storageCost,
|
|
195
|
+
effects,
|
|
196
|
+
].join('|');
|
|
197
|
+
}
|
|
198
|
+
function snapshotCargoFingerprint(fleet) {
|
|
199
|
+
const items = fleet.cargoHold.items
|
|
200
|
+
.map(({ amount, id }) => `${String(id)}:${String(amount)}`)
|
|
201
|
+
.sort()
|
|
202
|
+
.join(',');
|
|
203
|
+
return [fleet.cargoHold.seqId, fleet.cargoHold.storageCost, items].join('|');
|
|
204
|
+
}
|
|
205
|
+
function rawCargoFingerprint(fleet) {
|
|
206
|
+
const items = fleet.cargoHold.amounts
|
|
207
|
+
.map(({ amount, cargoId }) => `${String(cargoId)}:${String(amount)}`)
|
|
208
|
+
.sort()
|
|
209
|
+
.join(',');
|
|
210
|
+
return [fleet.cargoHold.seqId, fleet.cargoHold.storageCost, items].join('|');
|
|
211
|
+
}
|
|
212
|
+
async function hashAccountState(account) {
|
|
213
|
+
const owner = getAddressEncoder().encode(account.owner);
|
|
214
|
+
const canonical = new Uint8Array(owner.length + account.data.length);
|
|
215
|
+
canonical.set(owner);
|
|
216
|
+
canonical.set(account.data, owner.length);
|
|
217
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', canonical));
|
|
218
|
+
return [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
219
|
+
}
|
|
220
|
+
async function precondition(account, describes) {
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
address: account.address,
|
|
223
|
+
dataHash: await hashAccountState(account),
|
|
224
|
+
describes,
|
|
225
|
+
kind: 'account-state',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function absentPrecondition(address, describes) {
|
|
229
|
+
return Object.freeze({
|
|
230
|
+
address,
|
|
231
|
+
describes,
|
|
232
|
+
kind: 'account-absent',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/** Plans application of one configured combat stimulant without predicting its effects.
|
|
236
|
+
*
|
|
237
|
+
* The planner validates Fleet ownership and Game membership, current catalog
|
|
238
|
+
* membership, known cargo balance, crewed non-destroyed state, and fresh
|
|
239
|
+
* whole-account Fleet state. Permission, stacking, charge application, and
|
|
240
|
+
* combat arithmetic remain chain-authoritative.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* declare const ctx: SageContext;
|
|
245
|
+
* declare const fleet: FleetSnapshot;
|
|
246
|
+
* declare const stimulant: CombatStimulantDefinition;
|
|
247
|
+
* declare const authorization: PlanAuthorization;
|
|
248
|
+
* const plan = await planFleetApplyCombatStimulant(
|
|
249
|
+
* ctx,
|
|
250
|
+
* fleet,
|
|
251
|
+
* stimulant,
|
|
252
|
+
* { authorization },
|
|
253
|
+
* );
|
|
254
|
+
* console.log(plan.describe());
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export async function planFleetApplyCombatStimulant(ctx, fleet, stimulant, input) {
|
|
258
|
+
const options = validateOptions(input);
|
|
259
|
+
const game = requireGameAddress(ctx);
|
|
260
|
+
if (fleet.ownerProfile.address !== options.authorization.profile) {
|
|
261
|
+
fail(`Fleet ${fleet.address} does not belong to authorization Profile ${options.authorization.profile}. Choose a Fleet owned by that Profile and retry.`, fleet.address);
|
|
262
|
+
}
|
|
263
|
+
if (fleet.game !== game) {
|
|
264
|
+
fail(`Fleet ${fleet.address} belongs to Game ${fleet.game}, not configured Game ${game}. Refresh the Fleet through this context and retry.`, fleet.address);
|
|
265
|
+
}
|
|
266
|
+
if (fleet.state.kind === 'destroyed') {
|
|
267
|
+
fail(`Fleet ${fleet.name} (${fleet.address}) is destroyed and cannot apply a combat stimulant. Respawn the Fleet, refresh it, and retry.`, fleet.address);
|
|
268
|
+
}
|
|
269
|
+
const combat = await readGameCombatConfiguration(ctx);
|
|
270
|
+
const definitions = await Promise.all(combat.data.stimulantDefinitions.definitions.map(async (definition) => ({
|
|
271
|
+
cargo: await resolveCargo(ctx, definition.cargoId),
|
|
272
|
+
effects: definition.effects,
|
|
273
|
+
})));
|
|
274
|
+
const requestedStimulant = stimulantFingerprint(stimulant);
|
|
275
|
+
const catalogued = definitions.find((candidate) => stimulantFingerprint(candidate) === requestedStimulant);
|
|
276
|
+
if (catalogued === undefined) {
|
|
277
|
+
fail(`Cargo ${String(stimulant.cargo.id)} (${stimulant.cargo.name}) is not an exact member of the configured Game's combat stimulant catalog. Select a stimulant from getCombatConfiguration(ctx), refresh it, and retry.`, game);
|
|
278
|
+
}
|
|
279
|
+
const knownBalance = fleet.cargoHold.items
|
|
280
|
+
.filter(({ id }) => id === catalogued.cargo.id)
|
|
281
|
+
.reduce((total, { amount }) => total + amount, 0n);
|
|
282
|
+
if (knownBalance <= 0n) {
|
|
283
|
+
fail(`Fleet ${fleet.name} (${fleet.address}) has ${String(knownBalance)} raw ${catalogued.cargo.name} in its cargo hold. Add at least one, refresh the Fleet, and retry.`, fleet.address);
|
|
284
|
+
}
|
|
285
|
+
const [currentAccount] = await readContextRawAccounts(ctx, [fleet.address]);
|
|
286
|
+
const account = requireAccount(currentAccount, fleet.address, `Fleet ${fleet.name} (${fleet.address}) is missing. Refresh Fleet discovery and retry applying ${catalogued.cargo.name}.`);
|
|
287
|
+
const current = fleetAccountDefinition.decode(account);
|
|
288
|
+
const snapshotFingerprint = [
|
|
289
|
+
fleet.address,
|
|
290
|
+
fleet.game,
|
|
291
|
+
fleet.ownerProfile.address,
|
|
292
|
+
fleet.name,
|
|
293
|
+
fleet.state.kind,
|
|
294
|
+
fleet.crewCount,
|
|
295
|
+
snapshotCargoFingerprint(fleet),
|
|
296
|
+
].join('|');
|
|
297
|
+
const currentFingerprint = [
|
|
298
|
+
account.address,
|
|
299
|
+
current.game,
|
|
300
|
+
current.ownerProfile,
|
|
301
|
+
current.name,
|
|
302
|
+
current.state.kind,
|
|
303
|
+
current.crewCount,
|
|
304
|
+
rawCargoFingerprint(current),
|
|
305
|
+
].join('|');
|
|
306
|
+
if ((await deriveFleetAddress(current)) !== fleet.address ||
|
|
307
|
+
currentFingerprint !== snapshotFingerprint) {
|
|
308
|
+
fail(`Fleet ${fleet.name} (${fleet.address}) changed or does not match its canonical Game, owner, label, state, crew count, or cargo hold. Refresh the Fleet through this context and retry.`, fleet.address);
|
|
309
|
+
}
|
|
310
|
+
if (current.crewCount <= 0) {
|
|
311
|
+
fail(`Fleet ${fleet.name} (${fleet.address}) has no crew and cannot apply a combat stimulant. Crew the Fleet, refresh it through this context, and retry.`, fleet.address);
|
|
312
|
+
}
|
|
313
|
+
const description = `Apply one ${catalogued.cargo.name} combat stimulant from Fleet ${fleet.name} (${fleet.address}); permission, stacking, charge application, and combat outcomes remain chain-authoritative.`;
|
|
314
|
+
return createPlan({
|
|
315
|
+
kind: 'combat.apply-stimulant',
|
|
316
|
+
preconditions: [
|
|
317
|
+
await precondition(account, `Fleet ${fleet.name} retains its configured Game, owning Profile, supported crewed state, and known ${catalogued.cargo.name} cargo balance.`),
|
|
318
|
+
],
|
|
319
|
+
steps: [
|
|
320
|
+
{
|
|
321
|
+
describes: description,
|
|
322
|
+
instruction: adaptApplyCombatStimulantInstruction({
|
|
323
|
+
authorization: options.authorization,
|
|
324
|
+
fleet: fleet.address,
|
|
325
|
+
game,
|
|
326
|
+
stimCargoId: catalogued.cargo.id,
|
|
327
|
+
}),
|
|
328
|
+
signers: [options.authorization.authority],
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
summary: description,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function factionId(fleet) {
|
|
335
|
+
if (fleet.npcFactionId > 0)
|
|
336
|
+
return fleet.npcFactionId;
|
|
337
|
+
switch (fleet.faction) {
|
|
338
|
+
case 'mud':
|
|
339
|
+
return 1;
|
|
340
|
+
case 'oni':
|
|
341
|
+
return 2;
|
|
342
|
+
case 'ustur':
|
|
343
|
+
return 3;
|
|
344
|
+
case 'unaligned':
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function shipFingerprint(fleet) {
|
|
349
|
+
return fleet.ships
|
|
350
|
+
.map(({ id, quantity }) => `${String(id)}:${String(quantity)}`)
|
|
351
|
+
.sort()
|
|
352
|
+
.join(',');
|
|
353
|
+
}
|
|
354
|
+
function rawShipFingerprint(fleet) {
|
|
355
|
+
return fleet.fleetShips
|
|
356
|
+
.map(({ quantity, shipId }) => `${String(shipId)}:${String(quantity)}`)
|
|
357
|
+
.sort()
|
|
358
|
+
.join(',');
|
|
359
|
+
}
|
|
360
|
+
function attackStateFingerprint(state) {
|
|
361
|
+
if (state.kind !== 'mining')
|
|
362
|
+
return state.kind;
|
|
363
|
+
return `${state.kind}:${typeof state.asteroid === 'string' ? state.asteroid : state.asteroid.address}`;
|
|
364
|
+
}
|
|
365
|
+
function attackFleetFingerprint(address, fleet, ships) {
|
|
366
|
+
return [
|
|
367
|
+
address,
|
|
368
|
+
fleet.address,
|
|
369
|
+
fleet.game,
|
|
370
|
+
fleet.ownerProfile.address,
|
|
371
|
+
fleet.name,
|
|
372
|
+
attackStateFingerprint(fleet.state),
|
|
373
|
+
fleet.crewCount,
|
|
374
|
+
fleet.ap,
|
|
375
|
+
fleet.faction,
|
|
376
|
+
fleet.npcFactionId,
|
|
377
|
+
ships,
|
|
378
|
+
].join('|');
|
|
379
|
+
}
|
|
380
|
+
async function validateCurrentAttackFleet(fleet, account) {
|
|
381
|
+
const currentAccount = requireAccount(account, fleet.address, `Fleet ${fleet.name} (${fleet.address}) is missing. Refresh Fleet discovery and retry the attack.`);
|
|
382
|
+
const current = fleetAccountDefinition.decode(currentAccount);
|
|
383
|
+
const currentFingerprint = [
|
|
384
|
+
currentAccount.address,
|
|
385
|
+
currentAccount.address,
|
|
386
|
+
current.game,
|
|
387
|
+
current.ownerProfile,
|
|
388
|
+
current.name,
|
|
389
|
+
attackStateFingerprint(current.state),
|
|
390
|
+
current.crewCount,
|
|
391
|
+
current.ap,
|
|
392
|
+
current.faction,
|
|
393
|
+
current.npcFactionId,
|
|
394
|
+
rawShipFingerprint(current),
|
|
395
|
+
].join('|');
|
|
396
|
+
if ((await deriveFleetAddress(current)) !== fleet.address ||
|
|
397
|
+
currentFingerprint !==
|
|
398
|
+
attackFleetFingerprint(fleet.address, fleet, shipFingerprint(fleet))) {
|
|
399
|
+
fail(`Fleet ${fleet.name} (${fleet.address}) changed or does not match its canonical Game, owner, label, state, crew, AP, faction, or ship composition. Refresh both Fleets and retry.`, fleet.address);
|
|
400
|
+
}
|
|
401
|
+
return currentAccount;
|
|
402
|
+
}
|
|
403
|
+
async function optionalCrewBinding(ctx, fleet) {
|
|
404
|
+
const address = await deriveFleetCrewBindingAddress(fleet.address);
|
|
405
|
+
const [envelope] = await ctx.data.readMany(fleetCrewBindingAccountDefinition, [address]);
|
|
406
|
+
if (envelope !== undefined &&
|
|
407
|
+
(envelope.data.fleet !== fleet.address ||
|
|
408
|
+
envelope.data.ownerProfile !== fleet.ownerProfile.address)) {
|
|
409
|
+
fail(`FleetCrewBinding ${address} does not belong to Fleet ${fleet.address} and Profile ${fleet.ownerProfile.address}. Refresh the Fleet crew relationship and retry.`, address);
|
|
410
|
+
}
|
|
411
|
+
return envelope?.address;
|
|
412
|
+
}
|
|
413
|
+
async function resolveRewardAccounts(ctx, fleet, registry, config, epochIndex) {
|
|
414
|
+
const resolvedFaction = factionId(fleet);
|
|
415
|
+
if (resolvedFaction === 0)
|
|
416
|
+
return { config, registry };
|
|
417
|
+
const [epochAddress, contributionAddress] = await Promise.all([
|
|
418
|
+
deriveLoyaltyEpochAddress(fleet.game, resolvedFaction, epochIndex),
|
|
419
|
+
deriveLoyaltyContributionAddress(fleet.game, fleet.ownerProfile.address, resolvedFaction, epochIndex),
|
|
420
|
+
]);
|
|
421
|
+
const [[epoch], [contribution]] = await Promise.all([
|
|
422
|
+
ctx.data.readMany(loyaltyEpochAccountDefinition, [epochAddress]),
|
|
423
|
+
ctx.data.readMany(loyaltyContributionAccountDefinition, [
|
|
424
|
+
contributionAddress,
|
|
425
|
+
]),
|
|
426
|
+
]);
|
|
427
|
+
if (epoch !== undefined &&
|
|
428
|
+
(epoch.data.game !== fleet.game ||
|
|
429
|
+
epoch.data.factionId !== resolvedFaction ||
|
|
430
|
+
epoch.data.epochIndex !== epochIndex)) {
|
|
431
|
+
fail(`LoyaltyEpoch ${epochAddress} does not match Fleet ${fleet.address}'s Game, faction, and active reward epoch. Refresh reward relationships and retry.`, epochAddress);
|
|
432
|
+
}
|
|
433
|
+
if (contribution !== undefined &&
|
|
434
|
+
(contribution.data.game !== fleet.game ||
|
|
435
|
+
contribution.data.profile !== fleet.ownerProfile.address ||
|
|
436
|
+
contribution.data.factionId !== resolvedFaction ||
|
|
437
|
+
contribution.data.epochIndex !== epochIndex)) {
|
|
438
|
+
fail(`LoyaltyContribution ${contributionAddress} does not match Fleet ${fleet.address}'s Profile, Game, faction, and active reward epoch. Refresh reward relationships and retry.`, contributionAddress);
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
config,
|
|
442
|
+
...(contribution === undefined
|
|
443
|
+
? {}
|
|
444
|
+
: { contribution: contribution.address }),
|
|
445
|
+
...(epoch === undefined ? {} : { epoch: epoch.address }),
|
|
446
|
+
registry,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
async function resolveNpcAccounts(ctx, fleet, requireFactionAccount) {
|
|
450
|
+
if (fleet.npcFactionId === 0)
|
|
451
|
+
return {};
|
|
452
|
+
const ownershipAddress = await deriveFactionOwnershipAddress(fleet.game, 'fleet', fleet.address);
|
|
453
|
+
const [ownership] = await ctx.data.readMany(factionOwnershipAccountDefinition, [ownershipAddress]);
|
|
454
|
+
if (ownership === undefined ||
|
|
455
|
+
ownership.data.game !== fleet.game ||
|
|
456
|
+
ownership.data.asset.kind !== 'fleet' ||
|
|
457
|
+
ownership.data.asset.fleet !== fleet.address ||
|
|
458
|
+
ownership.data.controllingFactionId !== fleet.npcFactionId) {
|
|
459
|
+
fail(`NPC Fleet ${fleet.address} has no matching canonical FactionOwnership for faction ${String(fleet.npcFactionId)}. Refresh the Fleet and faction ownership relationship before retrying.`, ownershipAddress);
|
|
460
|
+
}
|
|
461
|
+
if (!requireFactionAccount)
|
|
462
|
+
return { ownership: ownership.address };
|
|
463
|
+
const factionAddress = await deriveFactionAccountAddress(fleet.game, fleet.npcFactionId);
|
|
464
|
+
const faction = await ctx.data.read(factionAccountDefinition, factionAddress);
|
|
465
|
+
if (faction.data.game !== fleet.game ||
|
|
466
|
+
faction.data.factionId !== fleet.npcFactionId) {
|
|
467
|
+
fail(`Faction ${factionAddress} does not match NPC Fleet ${fleet.address}'s Game and faction id. Refresh the faction registry relationship before retrying.`, factionAddress);
|
|
468
|
+
}
|
|
469
|
+
return { factionAccount: faction.address, ownership: ownership.address };
|
|
470
|
+
}
|
|
471
|
+
function generatedStance(stance) {
|
|
472
|
+
return {
|
|
473
|
+
ammoUsage: stance.ammoUsage === 'none' ? 0 : stance.ammoUsage === 'light' ? 1 : 2,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
/** Plans one singleton Fleet-on-Fleet attack without predicting its outcome.
|
|
477
|
+
*
|
|
478
|
+
* Range, randomness, recent slot hashes, final combat arithmetic, and chain
|
|
479
|
+
* races remain authoritative. This Plan cannot be combined with another
|
|
480
|
+
* gameplay instruction because the generated program contract requires a
|
|
481
|
+
* singleton transaction.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* ```ts
|
|
485
|
+
* declare const ctx: SageContext;
|
|
486
|
+
* declare const attacker: FleetSnapshot;
|
|
487
|
+
* declare const defender: FleetSnapshot;
|
|
488
|
+
* declare const authorization: PlanAuthorization;
|
|
489
|
+
* declare const loot: Address;
|
|
490
|
+
* const plan = await planFleetAttackFleet(
|
|
491
|
+
* ctx,
|
|
492
|
+
* attacker,
|
|
493
|
+
* defender,
|
|
494
|
+
* { ammoUsage: 'light' },
|
|
495
|
+
* { authorization, loot },
|
|
496
|
+
* );
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
export async function planFleetAttackFleet(ctx, attacker, defender, inputStance, inputOptions) {
|
|
500
|
+
const options = validateAttackOptions(inputOptions);
|
|
501
|
+
const stance = validateAttackStance(inputStance);
|
|
502
|
+
const game = requireGameAddress(ctx);
|
|
503
|
+
if (attacker.address === defender.address) {
|
|
504
|
+
fail(`Fleet ${attacker.address} cannot attack itself. Choose a distinct defending Fleet and retry.`, attacker.address);
|
|
505
|
+
}
|
|
506
|
+
if (attacker.ownerProfile.address !== options.authorization.profile) {
|
|
507
|
+
fail(`Attacking Fleet ${attacker.address} does not belong to authorization Profile ${options.authorization.profile}. Choose an owned Fleet and retry.`, attacker.address);
|
|
508
|
+
}
|
|
509
|
+
if (options.loot === options.authorization.authority ||
|
|
510
|
+
options.loot === options.authorization.profile) {
|
|
511
|
+
fail(`The new Loot address ${options.loot} must be distinct from the authorization authority and Profile. Generate a separate Loot signer address and retry.`, options.loot);
|
|
512
|
+
}
|
|
513
|
+
for (const [role, fleet] of [
|
|
514
|
+
['attacking', attacker],
|
|
515
|
+
['defending', defender],
|
|
516
|
+
]) {
|
|
517
|
+
if (fleet.game !== game) {
|
|
518
|
+
fail(`The ${role} Fleet ${fleet.address} belongs to Game ${fleet.game}, not configured Game ${game}. Refresh both Fleets through this context and retry.`, fleet.address);
|
|
519
|
+
}
|
|
520
|
+
if (fleet.state.kind === 'destroyed') {
|
|
521
|
+
fail(`The ${role} Fleet ${fleet.name} (${fleet.address}) is destroyed and cannot participate in this attack. Refresh the Fleet after respawn and retry.`, fleet.address);
|
|
522
|
+
}
|
|
523
|
+
if (!Number.isSafeInteger(fleet.ap) ||
|
|
524
|
+
fleet.ap < 0 ||
|
|
525
|
+
!Number.isSafeInteger(fleet.crewCount) ||
|
|
526
|
+
fleet.crewCount <= 0) {
|
|
527
|
+
fail(`The ${role} Fleet ${fleet.name} (${fleet.address}) has unknown or invalid AP or crew state. Refresh the Fleet through this context and retry.`, fleet.address);
|
|
528
|
+
}
|
|
529
|
+
if (fleet.ships.length === 0 ||
|
|
530
|
+
fleet.ships.some(({ quantity }) => quantity <= 0n)) {
|
|
531
|
+
fail(`The ${role} Fleet ${fleet.name} (${fleet.address}) has no valid known ship composition. Refresh Fleet composition and retry.`, fleet.address);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (attacker.ap === 0) {
|
|
535
|
+
fail(`Attacking Fleet ${attacker.name} (${attacker.address}) has no known AP available. Wait for AP reload, refresh the Fleet, and retry.`, attacker.address);
|
|
536
|
+
}
|
|
537
|
+
const currentAccounts = await readContextRawAccounts(ctx, [
|
|
538
|
+
attacker.address,
|
|
539
|
+
defender.address,
|
|
540
|
+
options.loot,
|
|
541
|
+
]);
|
|
542
|
+
if (currentAccounts[2] !== undefined) {
|
|
543
|
+
fail(`Loot account ${options.loot} already exists. Generate a new unused Loot signer address and retry.`, options.loot);
|
|
544
|
+
}
|
|
545
|
+
const [attackerAccount, defenderAccount] = await Promise.all([
|
|
546
|
+
validateCurrentAttackFleet(attacker, currentAccounts[0]),
|
|
547
|
+
validateCurrentAttackFleet(defender, currentAccounts[1]),
|
|
548
|
+
]);
|
|
549
|
+
const registryAddress = await deriveAtlasRewardRegistryAddress(game);
|
|
550
|
+
const registry = await ctx.data.read(atlasRewardRegistryAccountDefinition, registryAddress);
|
|
551
|
+
if (registry.data.game !== game) {
|
|
552
|
+
fail(`AtlasRewardRegistry ${registryAddress} belongs to another Game. Refresh the configured reward registry and retry.`, registryAddress);
|
|
553
|
+
}
|
|
554
|
+
const rewardConfig = await deriveAtlasRewardConfigAddress(game, registry.data.activeConfigVersion);
|
|
555
|
+
const [attackerRewards, defenderRewards, crewBinding, attackerNpc, defenderNpc,] = await Promise.all([
|
|
556
|
+
resolveRewardAccounts(ctx, attacker, registryAddress, rewardConfig, registry.data.currentEpoch),
|
|
557
|
+
resolveRewardAccounts(ctx, defender, registryAddress, rewardConfig, registry.data.currentEpoch),
|
|
558
|
+
optionalCrewBinding(ctx, attacker),
|
|
559
|
+
resolveNpcAccounts(ctx, attacker, true),
|
|
560
|
+
resolveNpcAccounts(ctx, defender, false),
|
|
561
|
+
]);
|
|
562
|
+
const [character, defenderCharacter, regionTracker, factionRelationTracker] = await Promise.all([
|
|
563
|
+
findCharacterPda(attacker.ownerProfile.address, game),
|
|
564
|
+
findCharacterPda(defender.ownerProfile.address, game),
|
|
565
|
+
deriveRegionTrackerAddress(game),
|
|
566
|
+
deriveFactionRelationTrackerAddress(game),
|
|
567
|
+
]);
|
|
568
|
+
const description = `Attack Fleet ${defender.name} (${defender.address}) with Fleet ${attacker.name} (${attacker.address}) using ${stance.ammoUsage} ammunition; range, randomness, rewards, Loot, damage, and final combat arithmetic remain chain-authoritative.`;
|
|
569
|
+
return createPlan({
|
|
570
|
+
kind: 'combat.attack-fleet',
|
|
571
|
+
preconditions: [
|
|
572
|
+
await precondition(attackerAccount, `Attacking Fleet ${attacker.name} retains its owner, Game, live state, crew, AP, faction, and ship composition.`),
|
|
573
|
+
await precondition(defenderAccount, `Defending Fleet ${defender.name} retains its owner, Game, live state, crew, faction, and ship composition.`),
|
|
574
|
+
],
|
|
575
|
+
steps: [
|
|
576
|
+
{
|
|
577
|
+
describes: description,
|
|
578
|
+
instruction: adaptAttackFleetInstruction({
|
|
579
|
+
...(attackerNpc.factionAccount === undefined
|
|
580
|
+
? {}
|
|
581
|
+
: { attackerFactionAccount: attackerNpc.factionAccount }),
|
|
582
|
+
...(attackerNpc.ownership === undefined
|
|
583
|
+
? {}
|
|
584
|
+
: { attackerFleetOwnership: attackerNpc.ownership }),
|
|
585
|
+
attackerRewardsConfig: attackerRewards.config,
|
|
586
|
+
...(attackerRewards.contribution === undefined
|
|
587
|
+
? {}
|
|
588
|
+
: { attackerRewardsContribution: attackerRewards.contribution }),
|
|
589
|
+
...(attackerRewards.epoch === undefined
|
|
590
|
+
? {}
|
|
591
|
+
: { attackerRewardsEpoch: attackerRewards.epoch }),
|
|
592
|
+
attackerRewardsRegistry: attackerRewards.registry,
|
|
593
|
+
...(defender.state.kind === 'mining'
|
|
594
|
+
? { asteroid: defender.state.asteroid.address }
|
|
595
|
+
: {}),
|
|
596
|
+
authorization: options.authorization,
|
|
597
|
+
character,
|
|
598
|
+
...(crewBinding === undefined ? {} : { crewBinding }),
|
|
599
|
+
defenderCharacter,
|
|
600
|
+
...(defenderNpc.ownership === undefined
|
|
601
|
+
? {}
|
|
602
|
+
: { defenderFleetOwnership: defenderNpc.ownership }),
|
|
603
|
+
defenderRewardsConfig: defenderRewards.config,
|
|
604
|
+
...(defenderRewards.contribution === undefined
|
|
605
|
+
? {}
|
|
606
|
+
: { defenderRewardsContribution: defenderRewards.contribution }),
|
|
607
|
+
...(defenderRewards.epoch === undefined
|
|
608
|
+
? {}
|
|
609
|
+
: { defenderRewardsEpoch: defenderRewards.epoch }),
|
|
610
|
+
defenderRewardsRegistry: defenderRewards.registry,
|
|
611
|
+
defendingFleet: defender.address,
|
|
612
|
+
factionRelationTracker,
|
|
613
|
+
fleet: attacker.address,
|
|
614
|
+
game,
|
|
615
|
+
loot: options.loot,
|
|
616
|
+
recentSlothashes: 'SysvarS1otHashes111111111111111111111111111',
|
|
617
|
+
regionTracker,
|
|
618
|
+
stance: generatedStance(stance),
|
|
619
|
+
}),
|
|
620
|
+
semantics: [{ kind: 'exclusive', scope: 'transaction' }],
|
|
621
|
+
signers: [options.authorization.authority, options.loot],
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
summary: description,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function systemFingerprint(system) {
|
|
628
|
+
return JSON.stringify({
|
|
629
|
+
address: system.address,
|
|
630
|
+
celestialBodyAddresses: system.celestialBodyAddresses,
|
|
631
|
+
connections: system.connections,
|
|
632
|
+
coordinates: system.coordinates,
|
|
633
|
+
game: system.game,
|
|
634
|
+
name: system.name,
|
|
635
|
+
regionId: system.regionId,
|
|
636
|
+
sequenceId: system.sequenceId,
|
|
637
|
+
starbase: system.starbase,
|
|
638
|
+
systemId: system.systemId,
|
|
639
|
+
version: system.version,
|
|
640
|
+
}, (_key, value) => typeof value === 'bigint' ? value.toString() : value);
|
|
641
|
+
}
|
|
642
|
+
function starbaseFactionId(owner) {
|
|
643
|
+
switch (owner) {
|
|
644
|
+
case 'mud':
|
|
645
|
+
return 1;
|
|
646
|
+
case 'oni':
|
|
647
|
+
return 2;
|
|
648
|
+
case 'ustur':
|
|
649
|
+
return 3;
|
|
650
|
+
case 'unaligned':
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/** Plans one singleton Fleet-on-Starbase attack without predicting its outcome.
|
|
655
|
+
*
|
|
656
|
+
* The planner validates the owned attacking Fleet, canonical target System and
|
|
657
|
+
* nested shared Starbase, configured faction ownership mode, reward
|
|
658
|
+
* configuration, and the optional dynamic-NPC capture economy. Range,
|
|
659
|
+
* capture-cost debit outcome, randomness, damage, rewards, and final
|
|
660
|
+
* admissibility remain chain-authoritative.
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* ```ts
|
|
664
|
+
* declare const ctx: SageContext;
|
|
665
|
+
* declare const attacker: FleetSnapshot;
|
|
666
|
+
* declare const target: StarSystemSnapshot;
|
|
667
|
+
* declare const authorization: PlanAuthorization;
|
|
668
|
+
* declare const loot: Address;
|
|
669
|
+
* const plan = await planFleetAttackStarbase(
|
|
670
|
+
* ctx,
|
|
671
|
+
* attacker,
|
|
672
|
+
* target,
|
|
673
|
+
* { ammoUsage: 'light' },
|
|
674
|
+
* { authorization, loot },
|
|
675
|
+
* );
|
|
676
|
+
* ```
|
|
677
|
+
*/
|
|
678
|
+
export async function planFleetAttackStarbase(ctx, attacker, target, inputStance, inputOptions) {
|
|
679
|
+
const options = validateAttackOptions(inputOptions);
|
|
680
|
+
const stance = validateAttackStance(inputStance);
|
|
681
|
+
const game = requireGameAddress(ctx);
|
|
682
|
+
if (attacker.ownerProfile.address !== options.authorization.profile) {
|
|
683
|
+
fail(`Attacking Fleet ${attacker.address} does not belong to authorization Profile ${options.authorization.profile}. Choose an owned Fleet and retry.`, attacker.address);
|
|
684
|
+
}
|
|
685
|
+
if (options.loot === options.authorization.authority ||
|
|
686
|
+
options.loot === options.authorization.profile) {
|
|
687
|
+
fail(`The new Loot address ${options.loot} must be distinct from the authorization authority and Profile. Generate a separate Loot signer address and retry.`, options.loot);
|
|
688
|
+
}
|
|
689
|
+
if (attacker.game !== game) {
|
|
690
|
+
fail(`Attacking Fleet ${attacker.address} belongs to Game ${attacker.game}, not configured Game ${game}. Refresh the Fleet through this context and retry.`, attacker.address);
|
|
691
|
+
}
|
|
692
|
+
if (attacker.state.kind === 'destroyed') {
|
|
693
|
+
fail(`Attacking Fleet ${attacker.name} (${attacker.address}) is destroyed and cannot attack a Starbase. Refresh the Fleet after respawn and retry.`, attacker.address);
|
|
694
|
+
}
|
|
695
|
+
if (!Number.isSafeInteger(attacker.ap) ||
|
|
696
|
+
attacker.ap <= 0 ||
|
|
697
|
+
!Number.isSafeInteger(attacker.crewCount) ||
|
|
698
|
+
attacker.crewCount <= 0) {
|
|
699
|
+
fail(`Attacking Fleet ${attacker.name} (${attacker.address}) has no known positive AP or valid crew state. Wait for AP reload, refresh the Fleet, and retry.`, attacker.address);
|
|
700
|
+
}
|
|
701
|
+
if (attacker.ships.length === 0 ||
|
|
702
|
+
attacker.ships.some(({ quantity }) => quantity <= 0n)) {
|
|
703
|
+
fail(`Attacking Fleet ${attacker.name} (${attacker.address}) has no valid known ship composition. Refresh Fleet composition and retry.`, attacker.address);
|
|
704
|
+
}
|
|
705
|
+
if (target.game !== game) {
|
|
706
|
+
fail(`Target Star System ${target.address} belongs to Game ${target.game}, not configured Game ${game}. Refresh the target through this context and retry.`, target.address);
|
|
707
|
+
}
|
|
708
|
+
if ((await deriveStarSystemAddress(game, target.systemId)) !== target.address) {
|
|
709
|
+
fail(`Target Star System ${target.address} is not the canonical member for Game ${game} and system id ${String(target.systemId)}. Refresh the target through this context and retry.`, target.address);
|
|
710
|
+
}
|
|
711
|
+
if (target.starbase === undefined) {
|
|
712
|
+
fail(`Target Star System ${target.name} (${target.address}) has no shared Starbase. Choose a System with a shared Starbase and retry.`, target.address);
|
|
713
|
+
}
|
|
714
|
+
const [relationTrackerAddress, ownershipAddress, registryAddress, kingTracker, character, regionTracker, crewBindingAddress,] = await Promise.all([
|
|
715
|
+
deriveFactionRelationTrackerAddress(game),
|
|
716
|
+
deriveFactionOwnershipAddress(game, 'starbase', target.address),
|
|
717
|
+
deriveAtlasRewardRegistryAddress(game),
|
|
718
|
+
deriveKingSystemTrackerAddress(game),
|
|
719
|
+
findCharacterPda(attacker.ownerProfile.address, game),
|
|
720
|
+
deriveRegionTrackerAddress(game),
|
|
721
|
+
deriveFleetCrewBindingAddress(attacker.address),
|
|
722
|
+
]);
|
|
723
|
+
const [probeRelationsAccount, probeRegistryAccount] = await readContextRawAccounts(ctx, [
|
|
724
|
+
relationTrackerAddress,
|
|
725
|
+
registryAddress,
|
|
726
|
+
]);
|
|
727
|
+
const probeRelations = factionRelationTrackerAccountDefinition.decode(requireAccount(probeRelationsAccount, relationTrackerAddress, `FactionRelationTracker ${relationTrackerAddress} is missing. Refresh faction relationships and retry.`));
|
|
728
|
+
const probeRegistry = atlasRewardRegistryAccountDefinition.decode(requireAccount(probeRegistryAccount, registryAddress, `AtlasRewardRegistry ${registryAddress} is missing. Refresh reward configuration and retry.`));
|
|
729
|
+
if (probeRelations.game !== game) {
|
|
730
|
+
fail(`FactionRelationTracker ${relationTrackerAddress} belongs to another Game. Refresh faction relationships and retry.`, relationTrackerAddress);
|
|
731
|
+
}
|
|
732
|
+
if (probeRegistry.game !== game) {
|
|
733
|
+
fail(`AtlasRewardRegistry ${registryAddress} belongs to another Game. Refresh the configured reward registry and retry.`, registryAddress);
|
|
734
|
+
}
|
|
735
|
+
const rewardConfigAddress = await deriveAtlasRewardConfigAddress(game, probeRegistry.activeConfigVersion);
|
|
736
|
+
const attackerOwnership = attacker.npcFactionId === 0
|
|
737
|
+
? undefined
|
|
738
|
+
: await deriveFactionOwnershipAddress(game, 'fleet', attacker.address);
|
|
739
|
+
const attackerFaction = attacker.npcFactionId === 0
|
|
740
|
+
? undefined
|
|
741
|
+
: await deriveFactionAccountAddress(game, attacker.npcFactionId);
|
|
742
|
+
const requiresNpcEconomy = attacker.npcFactionId > 0 &&
|
|
743
|
+
probeRelations.dynamicCaptureEnabled &&
|
|
744
|
+
probeRelations.nemesisActive;
|
|
745
|
+
let factionEconomicsConfig;
|
|
746
|
+
let captorTreasury;
|
|
747
|
+
if (requiresNpcEconomy) {
|
|
748
|
+
[factionEconomicsConfig, captorTreasury] = await Promise.all([
|
|
749
|
+
deriveFactionEconomicsConfigAddress(game),
|
|
750
|
+
deriveFactionTreasuryAddress(game, attacker.npcFactionId),
|
|
751
|
+
]);
|
|
752
|
+
}
|
|
753
|
+
const dependencyAddresses = [
|
|
754
|
+
attacker.address,
|
|
755
|
+
target.address,
|
|
756
|
+
options.loot,
|
|
757
|
+
relationTrackerAddress,
|
|
758
|
+
...(probeRelations.dynamicCaptureEnabled ? [ownershipAddress] : []),
|
|
759
|
+
registryAddress,
|
|
760
|
+
rewardConfigAddress,
|
|
761
|
+
kingTracker,
|
|
762
|
+
character,
|
|
763
|
+
regionTracker,
|
|
764
|
+
crewBindingAddress,
|
|
765
|
+
...(attackerOwnership === undefined ? [] : [attackerOwnership]),
|
|
766
|
+
...(attackerFaction === undefined ? [] : [attackerFaction]),
|
|
767
|
+
...(factionEconomicsConfig === undefined ? [] : [factionEconomicsConfig]),
|
|
768
|
+
...(captorTreasury === undefined ? [] : [captorTreasury]),
|
|
769
|
+
];
|
|
770
|
+
const dependencyAccounts = await readContextRawAccounts(ctx, dependencyAddresses);
|
|
771
|
+
const current = new Map(dependencyAddresses.map((address, index) => [
|
|
772
|
+
address,
|
|
773
|
+
dependencyAccounts[index],
|
|
774
|
+
]));
|
|
775
|
+
if (current.get(options.loot) !== undefined) {
|
|
776
|
+
fail(`Loot account ${options.loot} already exists. Generate a new unused Loot signer address and retry.`, options.loot);
|
|
777
|
+
}
|
|
778
|
+
const attackerAccount = await validateCurrentAttackFleet(attacker, current.get(attacker.address));
|
|
779
|
+
const systemAccount = requireAccount(current.get(target.address), target.address, `Target Star System ${target.name} (${target.address}) is missing. Refresh world state and retry the attack.`);
|
|
780
|
+
const currentSystem = starSystemAccountDefinition.decode(systemAccount);
|
|
781
|
+
if ((await deriveStarSystemAddress(currentSystem.game, currentSystem.systemId)) !== target.address ||
|
|
782
|
+
systemFingerprint({ ...currentSystem, address: systemAccount.address }) !==
|
|
783
|
+
systemFingerprint(target)) {
|
|
784
|
+
fail(`Target Star System ${target.name} (${target.address}) changed or does not match its canonical Game, identity, sequence, or shared Starbase state. Refresh the target and retry.`, target.address);
|
|
785
|
+
}
|
|
786
|
+
const relationsAccount = requireAccount(current.get(relationTrackerAddress), relationTrackerAddress, `FactionRelationTracker ${relationTrackerAddress} is missing. Refresh faction relationships and retry.`);
|
|
787
|
+
const relations = factionRelationTrackerAccountDefinition.decode(relationsAccount);
|
|
788
|
+
if (relations.game !== game ||
|
|
789
|
+
relations.dynamicCaptureEnabled !== probeRelations.dynamicCaptureEnabled ||
|
|
790
|
+
relations.nemesisActive !== probeRelations.nemesisActive) {
|
|
791
|
+
fail(`FactionRelationTracker ${relationTrackerAddress} changed or belongs to another Game. Refresh faction relationships and retry.`, relationTrackerAddress);
|
|
792
|
+
}
|
|
793
|
+
const ownershipAccount = relations.dynamicCaptureEnabled
|
|
794
|
+
? current.get(ownershipAddress)
|
|
795
|
+
: undefined;
|
|
796
|
+
const ownership = ownershipAccount === undefined
|
|
797
|
+
? undefined
|
|
798
|
+
: factionOwnershipAccountDefinition.decode(ownershipAccount);
|
|
799
|
+
if (ownership === undefined && relations.dynamicCaptureEnabled) {
|
|
800
|
+
fail(`Target Starbase ${target.address} requires canonical FactionOwnership while dynamic capture is enabled. Refresh Starbase ownership and retry.`, ownershipAddress);
|
|
801
|
+
}
|
|
802
|
+
const legacyOwner = starbaseFactionId(target.starbase.owner);
|
|
803
|
+
if (ownership !== undefined &&
|
|
804
|
+
(ownership.game !== game ||
|
|
805
|
+
ownership.asset.kind !== 'starbase' ||
|
|
806
|
+
ownership.asset.system !== target.address ||
|
|
807
|
+
ownership.capturedSystemSequenceId !== currentSystem.sequenceId)) {
|
|
808
|
+
fail(`FactionOwnership ${ownershipAddress} does not match target Starbase ${target.address}'s Game, System, or current System generation sequence. Refresh Starbase ownership and retry.`, ownershipAddress);
|
|
809
|
+
}
|
|
810
|
+
const effectiveOwner = ownership?.controllingFactionId ?? legacyOwner;
|
|
811
|
+
const registryAccount = requireAccount(current.get(registryAddress), registryAddress, `AtlasRewardRegistry ${registryAddress} is missing. Refresh reward configuration and retry.`);
|
|
812
|
+
const registry = atlasRewardRegistryAccountDefinition.decode(registryAccount);
|
|
813
|
+
if (registry.game !== game ||
|
|
814
|
+
registry.activeConfigVersion !== probeRegistry.activeConfigVersion) {
|
|
815
|
+
fail(`AtlasRewardRegistry ${registryAddress} changed or belongs to another Game. Refresh the configured reward registry and retry.`, registryAddress);
|
|
816
|
+
}
|
|
817
|
+
const rewardConfigAccount = requireAccount(current.get(rewardConfigAddress), rewardConfigAddress, `AtlasRewardConfig ${rewardConfigAddress} is missing. Refresh reward configuration and retry.`);
|
|
818
|
+
const rewardConfig = atlasRewardConfigAccountDefinition.decode(rewardConfigAccount);
|
|
819
|
+
if (rewardConfig.game !== game ||
|
|
820
|
+
rewardConfig.configVersion !== registry.activeConfigVersion ||
|
|
821
|
+
rewardConfig.effectiveEpoch > registry.currentEpoch) {
|
|
822
|
+
fail(`AtlasRewardConfig ${rewardConfigAddress} does not match the configured Game and active registry version or is not yet effective for the current epoch. Refresh reward configuration and retry.`, rewardConfigAddress);
|
|
823
|
+
}
|
|
824
|
+
const kingAccount = requireAccount(current.get(kingTracker), kingTracker, `KingSystemTracker ${kingTracker} is missing. Refresh King-system ownership and retry.`);
|
|
825
|
+
const king = kingSystemTrackerAccountDefinition.decode(kingAccount);
|
|
826
|
+
const kingTarget = king.systems.find(({ systemId }) => systemId === currentSystem.systemId);
|
|
827
|
+
if (king.game !== game ||
|
|
828
|
+
kingTarget === undefined ||
|
|
829
|
+
kingTarget.factionId !== effectiveOwner) {
|
|
830
|
+
fail(`KingSystemTracker ${kingTracker} does not contain target System ${String(currentSystem.systemId)} with its current effective owner ${String(effectiveOwner)}. Refresh King-system ownership and retry.`, kingTracker);
|
|
831
|
+
}
|
|
832
|
+
const characterAccount = requireAccount(current.get(character), character, `Character ${character} is missing for Profile ${attacker.ownerProfile.address}. Refresh the Profile Character registration and retry.`);
|
|
833
|
+
const characterData = characterAccountDefinition.decode(characterAccount);
|
|
834
|
+
if (characterData.game !== game ||
|
|
835
|
+
characterData.playerProfile !== attacker.ownerProfile.address) {
|
|
836
|
+
fail(`Character ${character} does not match Profile ${attacker.ownerProfile.address} and configured Game ${game}. Refresh the Profile Character registration and retry.`, character);
|
|
837
|
+
}
|
|
838
|
+
const regionTrackerAccount = requireAccount(current.get(regionTracker), regionTracker, `RegionTracker ${regionTracker} is missing for configured Game ${game}. Refresh world region data and retry.`);
|
|
839
|
+
const regionTrackerData = regionTrackerAccountDefinition.decode(regionTrackerAccount);
|
|
840
|
+
if (regionTrackerData.game !== game) {
|
|
841
|
+
fail(`RegionTracker ${regionTracker} does not match configured Game ${game}. Refresh world region data and retry.`, regionTracker);
|
|
842
|
+
}
|
|
843
|
+
const crewBindingAccount = current.get(crewBindingAddress);
|
|
844
|
+
const crewBinding = crewBindingAccount === undefined ? undefined : crewBindingAddress;
|
|
845
|
+
if (crewBindingAccount !== undefined) {
|
|
846
|
+
const data = fleetCrewBindingAccountDefinition.decode(crewBindingAccount);
|
|
847
|
+
if (data.fleet !== attacker.address ||
|
|
848
|
+
data.ownerProfile !== attacker.ownerProfile.address) {
|
|
849
|
+
fail(`FleetCrewBinding ${crewBindingAddress} does not belong to Fleet ${attacker.address} and Profile ${attacker.ownerProfile.address}. Refresh the Fleet crew relationship and retry.`, crewBindingAddress);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const attackerNpc = {};
|
|
853
|
+
let attackerOwnershipAccount;
|
|
854
|
+
let attackerFactionAccount;
|
|
855
|
+
if (attackerOwnership !== undefined && attackerFaction !== undefined) {
|
|
856
|
+
attackerOwnershipAccount = requireAccount(current.get(attackerOwnership), attackerOwnership, `NPC Fleet ${attacker.address} has no canonical FactionOwnership. Refresh the Fleet faction relationship and retry.`);
|
|
857
|
+
const npcOwnership = factionOwnershipAccountDefinition.decode(attackerOwnershipAccount);
|
|
858
|
+
if (npcOwnership.game !== game ||
|
|
859
|
+
npcOwnership.asset.kind !== 'fleet' ||
|
|
860
|
+
npcOwnership.asset.fleet !== attacker.address ||
|
|
861
|
+
npcOwnership.controllingFactionId !== attacker.npcFactionId) {
|
|
862
|
+
fail(`NPC Fleet ${attacker.address} has no matching canonical FactionOwnership for faction ${String(attacker.npcFactionId)}. Refresh the Fleet and faction ownership relationship before retrying.`, attackerOwnership);
|
|
863
|
+
}
|
|
864
|
+
attackerFactionAccount = requireAccount(current.get(attackerFaction), attackerFaction, `Faction ${attackerFaction} is missing for NPC Fleet ${attacker.address}. Refresh the faction registry relationship and retry.`);
|
|
865
|
+
const npcFaction = factionAccountDefinition.decode(attackerFactionAccount);
|
|
866
|
+
if (npcFaction.game !== game ||
|
|
867
|
+
npcFaction.factionId !== attacker.npcFactionId) {
|
|
868
|
+
fail(`Faction ${attackerFaction} does not match NPC Fleet ${attacker.address}'s Game and faction id. Refresh the faction registry relationship before retrying.`, attackerFaction);
|
|
869
|
+
}
|
|
870
|
+
attackerNpc.ownership = attackerOwnership;
|
|
871
|
+
attackerNpc.factionAccount = attackerFaction;
|
|
872
|
+
}
|
|
873
|
+
let economicsAccount;
|
|
874
|
+
let treasuryAccount;
|
|
875
|
+
if (factionEconomicsConfig !== undefined && captorTreasury !== undefined) {
|
|
876
|
+
economicsAccount = requireAccount(current.get(factionEconomicsConfig), factionEconomicsConfig, `FactionEconomicsConfig ${factionEconomicsConfig} is missing. Refresh faction economics and retry.`);
|
|
877
|
+
treasuryAccount = requireAccount(current.get(captorTreasury), captorTreasury, `FactionTreasury ${captorTreasury} is missing. Refresh the captor treasury and retry.`);
|
|
878
|
+
const economics = factionEconomicsConfigAccountDefinition.decode(economicsAccount);
|
|
879
|
+
const treasury = factionTreasuryAccountDefinition.decode(treasuryAccount);
|
|
880
|
+
if (economics.game !== game) {
|
|
881
|
+
fail(`FactionEconomicsConfig ${factionEconomicsConfig} belongs to another Game. Refresh faction economics and retry.`, factionEconomicsConfig);
|
|
882
|
+
}
|
|
883
|
+
if (treasury.game !== game ||
|
|
884
|
+
treasury.factionId !== attacker.npcFactionId) {
|
|
885
|
+
fail(`FactionTreasury ${captorTreasury} does not match NPC attacker faction ${String(attacker.npcFactionId)} and the configured Game. Refresh the captor treasury and retry.`, captorTreasury);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const statePreconditions = await Promise.all([
|
|
889
|
+
[
|
|
890
|
+
attackerAccount,
|
|
891
|
+
`Attacking Fleet ${attacker.name} retains its owner, Game, live state, crew, AP, faction, and ship composition.`,
|
|
892
|
+
],
|
|
893
|
+
[
|
|
894
|
+
systemAccount,
|
|
895
|
+
`Target Star System ${target.name} retains its canonical identity, sequence, and shared Starbase state.`,
|
|
896
|
+
],
|
|
897
|
+
[relationsAccount, 'Faction combat gates remain unchanged.'],
|
|
898
|
+
...(ownershipAccount === undefined
|
|
899
|
+
? []
|
|
900
|
+
: [
|
|
901
|
+
[
|
|
902
|
+
ownershipAccount,
|
|
903
|
+
'Target Starbase ownership and generation remain unchanged.',
|
|
904
|
+
],
|
|
905
|
+
]),
|
|
906
|
+
[
|
|
907
|
+
registryAccount,
|
|
908
|
+
'The active reward registry version and epoch remain unchanged.',
|
|
909
|
+
],
|
|
910
|
+
[
|
|
911
|
+
rewardConfigAccount,
|
|
912
|
+
'The active reward configuration remains unchanged.',
|
|
913
|
+
],
|
|
914
|
+
[
|
|
915
|
+
kingAccount,
|
|
916
|
+
'King-system target membership and ownership remain unchanged.',
|
|
917
|
+
],
|
|
918
|
+
[characterAccount, 'The attacking Profile Character remains canonical.'],
|
|
919
|
+
[
|
|
920
|
+
regionTrackerAccount,
|
|
921
|
+
'The configured Game RegionTracker remains canonical.',
|
|
922
|
+
],
|
|
923
|
+
...(crewBindingAccount === undefined
|
|
924
|
+
? []
|
|
925
|
+
: [
|
|
926
|
+
[
|
|
927
|
+
crewBindingAccount,
|
|
928
|
+
'The optional Fleet crew binding remains unchanged.',
|
|
929
|
+
],
|
|
930
|
+
]),
|
|
931
|
+
...(attackerOwnershipAccount === undefined
|
|
932
|
+
? []
|
|
933
|
+
: [
|
|
934
|
+
[
|
|
935
|
+
attackerOwnershipAccount,
|
|
936
|
+
'NPC Fleet ownership remains unchanged.',
|
|
937
|
+
],
|
|
938
|
+
]),
|
|
939
|
+
...(attackerFactionAccount === undefined
|
|
940
|
+
? []
|
|
941
|
+
: [
|
|
942
|
+
[
|
|
943
|
+
attackerFactionAccount,
|
|
944
|
+
'The NPC faction account remains unchanged.',
|
|
945
|
+
],
|
|
946
|
+
]),
|
|
947
|
+
...(economicsAccount === undefined
|
|
948
|
+
? []
|
|
949
|
+
: [
|
|
950
|
+
[
|
|
951
|
+
economicsAccount,
|
|
952
|
+
'Dynamic-capture faction economics remain unchanged.',
|
|
953
|
+
],
|
|
954
|
+
]),
|
|
955
|
+
...(treasuryAccount === undefined
|
|
956
|
+
? []
|
|
957
|
+
: [
|
|
958
|
+
[
|
|
959
|
+
treasuryAccount,
|
|
960
|
+
'The dynamic-capture treasury remains unchanged.',
|
|
961
|
+
],
|
|
962
|
+
]),
|
|
963
|
+
].map(([account, describes]) => precondition(account, describes)));
|
|
964
|
+
const absencePreconditions = [
|
|
965
|
+
absentPrecondition(options.loot, `Loot account ${options.loot} remains absent until execution.`),
|
|
966
|
+
...(crewBindingAccount === undefined
|
|
967
|
+
? [
|
|
968
|
+
absentPrecondition(crewBindingAddress, 'Optional Fleet crew binding remains absent.'),
|
|
969
|
+
]
|
|
970
|
+
: []),
|
|
971
|
+
];
|
|
972
|
+
const description = `Attack with Fleet ${attacker.name} (${attacker.address}) against the shared Starbase at ${target.name} (${target.address}) using ${stance.ammoUsage} ammunition; range, capture-cost debit outcome, randomness, rewards, Loot, damage, and final admissibility remain chain-authoritative.`;
|
|
973
|
+
return createPlan({
|
|
974
|
+
kind: 'combat.attack-starbase',
|
|
975
|
+
preconditions: [...statePreconditions, ...absencePreconditions],
|
|
976
|
+
steps: [
|
|
977
|
+
{
|
|
978
|
+
describes: description,
|
|
979
|
+
instruction: adaptAttackStarbaseInstruction({
|
|
980
|
+
...(attackerNpc.factionAccount === undefined
|
|
981
|
+
? {}
|
|
982
|
+
: { attackerFactionAccount: attackerNpc.factionAccount }),
|
|
983
|
+
...(attackerNpc.ownership === undefined
|
|
984
|
+
? {}
|
|
985
|
+
: { attackerFleetOwnership: attackerNpc.ownership }),
|
|
986
|
+
authorization: options.authorization,
|
|
987
|
+
...(captorTreasury === undefined ? {} : { captorTreasury }),
|
|
988
|
+
character,
|
|
989
|
+
...(crewBinding === undefined ? {} : { crewBinding }),
|
|
990
|
+
...(factionEconomicsConfig === undefined
|
|
991
|
+
? {}
|
|
992
|
+
: { factionEconomicsConfig }),
|
|
993
|
+
...(ownershipAccount === undefined
|
|
994
|
+
? {}
|
|
995
|
+
: { factionOwnership: ownershipAddress }),
|
|
996
|
+
factionRelationTracker: relationTrackerAddress,
|
|
997
|
+
fleet: attacker.address,
|
|
998
|
+
game,
|
|
999
|
+
kingTracker,
|
|
1000
|
+
loot: options.loot,
|
|
1001
|
+
recentSlothashes: 'SysvarS1otHashes111111111111111111111111111',
|
|
1002
|
+
regionTracker,
|
|
1003
|
+
rewardConfig: rewardConfigAddress,
|
|
1004
|
+
rewardRegistry: registryAddress,
|
|
1005
|
+
stance: generatedStance(stance),
|
|
1006
|
+
system: target.address,
|
|
1007
|
+
}),
|
|
1008
|
+
semantics: [{ kind: 'exclusive', scope: 'transaction' }],
|
|
1009
|
+
signers: [options.authorization.authority, options.loot],
|
|
1010
|
+
},
|
|
1011
|
+
],
|
|
1012
|
+
summary: description,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
function lootSnapshotFingerprint(loot) {
|
|
1016
|
+
return [
|
|
1017
|
+
loot.address,
|
|
1018
|
+
loot.version,
|
|
1019
|
+
loot.game.address,
|
|
1020
|
+
loot.profile.address,
|
|
1021
|
+
loot.coordinates.x,
|
|
1022
|
+
loot.coordinates.y,
|
|
1023
|
+
...loot.items.map((item) => [
|
|
1024
|
+
item.destroyer.address,
|
|
1025
|
+
item.exclusivityUnlockAtUnixSeconds,
|
|
1026
|
+
item.rewardReclaimAtUnixSeconds,
|
|
1027
|
+
item.inventory.sequenceId,
|
|
1028
|
+
item.inventory.storageCostRaw,
|
|
1029
|
+
...item.inventory.items
|
|
1030
|
+
.map(({ cargo, quantityRaw }) => `${String(cargo.id)}:${String(quantityRaw)}`)
|
|
1031
|
+
.sort(),
|
|
1032
|
+
].join(':')),
|
|
1033
|
+
].join('|');
|
|
1034
|
+
}
|
|
1035
|
+
function rawLootFingerprint(address, loot) {
|
|
1036
|
+
return [
|
|
1037
|
+
address,
|
|
1038
|
+
loot.version,
|
|
1039
|
+
loot.game,
|
|
1040
|
+
loot.profile,
|
|
1041
|
+
loot.coordinates.x,
|
|
1042
|
+
loot.coordinates.y,
|
|
1043
|
+
...loot.items.map((item) => [
|
|
1044
|
+
item.destroyer,
|
|
1045
|
+
item.exclusivityUnlockAtUnixSeconds,
|
|
1046
|
+
item.rewardReclaimAtUnixSeconds,
|
|
1047
|
+
item.inventory.sequenceId,
|
|
1048
|
+
item.inventory.storageCostRaw,
|
|
1049
|
+
...item.inventory.amounts
|
|
1050
|
+
.map(({ amount, cargoId }) => `${String(cargoId)}:${String(amount)}`)
|
|
1051
|
+
.sort(),
|
|
1052
|
+
].join(':')),
|
|
1053
|
+
].join('|');
|
|
1054
|
+
}
|
|
1055
|
+
/** Plans one transaction-exclusive retrieval from a loaded Loot snapshot.
|
|
1056
|
+
*
|
|
1057
|
+
* The planner validates the owned idle Fleet, Loot identity, destroyer
|
|
1058
|
+
* exclusivity, and every requested known quantity. Range, recent-slot inputs,
|
|
1059
|
+
* destination capacity, intervening races, and final acquired quantities
|
|
1060
|
+
* remain chain-authoritative.
|
|
1061
|
+
*
|
|
1062
|
+
* @example
|
|
1063
|
+
* ```ts
|
|
1064
|
+
* declare const ctx: SageContext;
|
|
1065
|
+
* declare const fleet: FleetSnapshot;
|
|
1066
|
+
* declare const loot: LootSnapshot;
|
|
1067
|
+
* declare const authorization: PlanAuthorization;
|
|
1068
|
+
* const plan = await planFleetRetrieveLoot(
|
|
1069
|
+
* ctx,
|
|
1070
|
+
* fleet,
|
|
1071
|
+
* loot,
|
|
1072
|
+
* [{ itemIndex: 0, cargo: [{ cargoId: 1, quantityRaw: 5n }] }],
|
|
1073
|
+
* { atUnixSeconds: 1_700_000_000n, authorization },
|
|
1074
|
+
* );
|
|
1075
|
+
* console.log(plan.describe());
|
|
1076
|
+
* ```
|
|
1077
|
+
*/
|
|
1078
|
+
export async function planFleetRetrieveLoot(ctx, fleet, loot, inputWant, inputOptions) {
|
|
1079
|
+
const options = validateRetrieveOptions(inputOptions);
|
|
1080
|
+
const game = requireGameAddress(ctx);
|
|
1081
|
+
if (fleet.ownerProfile.address !== options.authorization.profile) {
|
|
1082
|
+
fail(`Fleet ${fleet.address} does not belong to authorization Profile ${options.authorization.profile}. Choose an owned Fleet and retry.`, fleet.address);
|
|
1083
|
+
}
|
|
1084
|
+
if (fleet.game !== game) {
|
|
1085
|
+
fail(`Fleet ${fleet.address} belongs to Game ${fleet.game}, not configured Game ${game}. Refresh the Fleet through this context and retry.`, fleet.address);
|
|
1086
|
+
}
|
|
1087
|
+
if (fleet.state.kind !== 'idle') {
|
|
1088
|
+
fail(`Fleet ${fleet.name} (${fleet.address}) must be idle to retrieve Loot, but its loaded state is ${fleet.state.kind}. Settle or finish the current Fleet action, refresh it, and retry.`, fleet.address);
|
|
1089
|
+
}
|
|
1090
|
+
if (!Number.isSafeInteger(fleet.crewCount) || fleet.crewCount <= 0) {
|
|
1091
|
+
fail(`Fleet ${fleet.name} (${fleet.address}) has no valid normalized crew and cannot retrieve Loot. Crew the Fleet, refresh it, and retry.`, fleet.address);
|
|
1092
|
+
}
|
|
1093
|
+
if (loot.game.address !== game) {
|
|
1094
|
+
fail(`Loot ${loot.address} belongs to Game ${loot.game.address}, not configured Game ${game}. Refresh the Loot through this context and retry.`, loot.address);
|
|
1095
|
+
}
|
|
1096
|
+
const selection = validateLootSelection(inputWant, loot, fleet, options.authorization.profile, options.atUnixSeconds);
|
|
1097
|
+
const [fleetAccountValue, lootAccountValue] = await readContextRawAccounts(ctx, [fleet.address, loot.address]);
|
|
1098
|
+
const fleetAccount = requireAccount(fleetAccountValue, fleet.address, `Fleet ${fleet.name} (${fleet.address}) is missing. Refresh Fleet discovery and retry retrieving Loot.`);
|
|
1099
|
+
const currentFleet = fleetAccountDefinition.decode(fleetAccount);
|
|
1100
|
+
if ((await deriveFleetAddress(currentFleet)) !== fleet.address ||
|
|
1101
|
+
currentFleet.game !== fleet.game ||
|
|
1102
|
+
currentFleet.ownerProfile !== fleet.ownerProfile.address ||
|
|
1103
|
+
currentFleet.name !== fleet.name ||
|
|
1104
|
+
currentFleet.state.kind !== fleet.state.kind ||
|
|
1105
|
+
currentFleet.crewCount !== fleet.crewCount) {
|
|
1106
|
+
fail(`Fleet ${fleet.name} (${fleet.address}) changed or no longer matches its canonical Game, owner, label, idle state, or crew. Refresh the Fleet and retry.`, fleet.address);
|
|
1107
|
+
}
|
|
1108
|
+
const lootAccount = requireAccount(lootAccountValue, loot.address, `Loot ${loot.address} is no longer available. Refresh Loot discovery and choose an available account before retrying.`);
|
|
1109
|
+
const currentLoot = lootAccountDefinition.decode(lootAccount);
|
|
1110
|
+
if (rawLootFingerprint(lootAccount.address, currentLoot) !==
|
|
1111
|
+
lootSnapshotFingerprint(loot)) {
|
|
1112
|
+
fail(`Loot ${loot.address} changed or no longer matches its loaded identity, Profile, coordinates, exclusivity, or known inventory. Refresh the Loot snapshot and retry.`, loot.address);
|
|
1113
|
+
}
|
|
1114
|
+
const lootCreator = loot.profile.address === options.authorization.profile
|
|
1115
|
+
? undefined
|
|
1116
|
+
: loot.profile.address;
|
|
1117
|
+
const description = `Request ${selection.summary} from Loot ${loot.address} with Fleet ${fleet.name} (${fleet.address}); range, recent-slot randomness, cargo capacity, availability races, and final transfer quantities remain chain-authoritative.`;
|
|
1118
|
+
return createPlan({
|
|
1119
|
+
kind: 'combat.retrieve-loot',
|
|
1120
|
+
preconditions: [
|
|
1121
|
+
await precondition(fleetAccount, `Fleet ${fleet.name} retains its configured Game, owning Profile, idle state, and normalized crew.`),
|
|
1122
|
+
await precondition(lootAccount, `Loot ${loot.address} retains its identity, creator, destroyer exclusivity, and requested known quantities.`),
|
|
1123
|
+
],
|
|
1124
|
+
steps: [
|
|
1125
|
+
{
|
|
1126
|
+
describes: description,
|
|
1127
|
+
instruction: adaptRetrieveLootInstruction({
|
|
1128
|
+
authorization: options.authorization,
|
|
1129
|
+
fleet: fleet.address,
|
|
1130
|
+
game,
|
|
1131
|
+
loot: loot.address,
|
|
1132
|
+
...(lootCreator === undefined ? {} : { lootCreator }),
|
|
1133
|
+
recentSlothashes: 'SysvarS1otHashes111111111111111111111111111',
|
|
1134
|
+
want: selection.generated.map(([itemIndex, cargo]) => [
|
|
1135
|
+
itemIndex,
|
|
1136
|
+
cargo.map(([cargoId, quantity]) => [cargoId, quantity]),
|
|
1137
|
+
]),
|
|
1138
|
+
}),
|
|
1139
|
+
semantics: [{ kind: 'exclusive', scope: 'transaction' }],
|
|
1140
|
+
signers: [options.authorization.authority],
|
|
1141
|
+
},
|
|
1142
|
+
],
|
|
1143
|
+
summary: description,
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
//# sourceMappingURL=index.js.map
|