@shipload/sdk 0.7.1 → 2.0.0-rc2

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.
Files changed (62) hide show
  1. package/README.md +349 -1
  2. package/lib/shipload.d.ts +2016 -230
  3. package/lib/shipload.js +5733 -2094
  4. package/lib/shipload.js.map +1 -1
  5. package/lib/shipload.m.js +5425 -2012
  6. package/lib/shipload.m.js.map +1 -1
  7. package/package.json +5 -5
  8. package/src/capabilities/extraction.ts +37 -0
  9. package/src/capabilities/guards.ts +43 -0
  10. package/src/capabilities/index.ts +5 -0
  11. package/src/capabilities/loading.ts +8 -0
  12. package/src/capabilities/movement.ts +29 -0
  13. package/src/capabilities/storage.ts +67 -0
  14. package/src/contracts/server.ts +788 -202
  15. package/src/data/goods.json +23 -0
  16. package/src/data/syllables.json +1184 -0
  17. package/src/entities/cargo-utils.ts +142 -0
  18. package/src/entities/container.ts +70 -0
  19. package/src/entities/entity-inventory.ts +39 -0
  20. package/src/entities/gamestate.ts +152 -0
  21. package/src/entities/inventory-accessor.ts +46 -0
  22. package/src/entities/location.ts +255 -0
  23. package/src/entities/makers.ts +69 -0
  24. package/src/entities/player.ts +288 -0
  25. package/src/entities/ship.ts +208 -0
  26. package/src/entities/warehouse.ts +89 -0
  27. package/src/errors.ts +46 -9
  28. package/src/index-module.ts +152 -7
  29. package/src/managers/actions.ts +200 -0
  30. package/src/managers/base.ts +25 -0
  31. package/src/managers/context.ts +104 -0
  32. package/src/managers/entities.ts +103 -0
  33. package/src/managers/epochs.ts +47 -0
  34. package/src/managers/index.ts +9 -0
  35. package/src/managers/locations.ts +122 -0
  36. package/src/managers/players.ts +13 -0
  37. package/src/managers/trades.ts +119 -0
  38. package/src/market/goods.ts +31 -0
  39. package/src/{market.ts → market/market.ts} +31 -37
  40. package/src/{rolls.ts → market/rolls.ts} +3 -3
  41. package/src/scheduling/accessor.ts +82 -0
  42. package/src/{epoch.ts → scheduling/epoch.ts} +1 -1
  43. package/src/scheduling/projection.ts +290 -0
  44. package/src/scheduling/schedule.ts +179 -0
  45. package/src/shipload.ts +39 -157
  46. package/src/trading/collect.ts +938 -0
  47. package/src/trading/deal.ts +207 -0
  48. package/src/trading/trade.ts +203 -0
  49. package/src/travel/travel.ts +486 -0
  50. package/src/types/capabilities.ts +79 -0
  51. package/src/types/entity-traits.ts +70 -0
  52. package/src/types/entity.ts +36 -0
  53. package/src/types/index.ts +3 -0
  54. package/src/types.ts +127 -25
  55. package/src/{hash.ts → utils/hash.ts} +1 -1
  56. package/src/utils/system.ts +155 -0
  57. package/src/goods.ts +0 -124
  58. package/src/ship.ts +0 -36
  59. package/src/state.ts +0 -0
  60. package/src/syllables.ts +0 -1184
  61. package/src/system.ts +0 -37
  62. package/src/travel.ts +0 -259
@@ -0,0 +1,142 @@
1
+ import {UInt32, UInt64, UInt64Type} from '@wharfkit/antelope'
2
+ import {EntityInventory} from './entity-inventory'
3
+ import {ServerContract} from '../contracts'
4
+
5
+ export interface CargoData {
6
+ cargo: EntityInventory[]
7
+ }
8
+
9
+ export function totalCargoMass(cargo: EntityInventory[]): UInt64 {
10
+ return cargo.reduce((sum, c) => {
11
+ return sum.adding(c.totalMass)
12
+ }, UInt64.from(0))
13
+ }
14
+
15
+ export function cargoValue(cargo: EntityInventory[]): UInt64 {
16
+ return cargo.reduce((sum, c) => {
17
+ return sum.adding(c.totalCost)
18
+ }, UInt64.from(0))
19
+ }
20
+
21
+ export function getCargoForGood(
22
+ cargo: EntityInventory[],
23
+ goodId: UInt64Type
24
+ ): EntityInventory | undefined {
25
+ return cargo.find((c) => c.good_id.equals(goodId))
26
+ }
27
+
28
+ export function hasSpace(
29
+ currentMass: UInt64,
30
+ maxCapacity: UInt64,
31
+ goodMass: UInt64,
32
+ quantity: number
33
+ ): boolean {
34
+ const additionalMass = goodMass.multiplying(quantity)
35
+ const totalMass = currentMass.adding(additionalMass)
36
+ return totalMass.lte(maxCapacity)
37
+ }
38
+
39
+ export function availableCapacity(currentMass: UInt64, maxCapacity: UInt64): UInt64 {
40
+ if (currentMass.gte(maxCapacity)) {
41
+ return UInt64.from(0)
42
+ }
43
+ return maxCapacity.subtracting(currentMass)
44
+ }
45
+
46
+ export function isFull(currentMass: UInt64, maxCapacity: UInt64): boolean {
47
+ return currentMass.gte(maxCapacity)
48
+ }
49
+
50
+ export interface SaleValue {
51
+ revenue: UInt64
52
+ profit: UInt64
53
+ cost: UInt64
54
+ }
55
+
56
+ export function calculateSaleValue(
57
+ cargo: ServerContract.Types.cargo_item[],
58
+ prices: Map<number, UInt64>
59
+ ): SaleValue {
60
+ if (cargo.length === 0) {
61
+ return {revenue: UInt64.from(0), profit: UInt64.from(0), cost: UInt64.from(0)}
62
+ }
63
+
64
+ let revenue = UInt64.from(0)
65
+ let cost = UInt64.from(0)
66
+
67
+ for (const item of cargo) {
68
+ if (UInt32.from(item.quantity).equals(UInt32.from(0))) continue
69
+
70
+ const goodId = Number(item.good_id)
71
+ const salePrice = prices.get(goodId)
72
+
73
+ if (salePrice) {
74
+ revenue = revenue.adding(salePrice.multiplying(item.quantity))
75
+ }
76
+
77
+ cost = cost.adding(item.unit_cost.multiplying(item.quantity))
78
+ }
79
+
80
+ const profit = revenue.gte(cost) ? revenue.subtracting(cost) : UInt64.from(0)
81
+
82
+ return {
83
+ revenue,
84
+ profit,
85
+ cost,
86
+ }
87
+ }
88
+
89
+ export function calculateSaleValueFromArray(
90
+ cargo: ServerContract.Types.cargo_item[],
91
+ prices: UInt64[]
92
+ ): SaleValue {
93
+ const priceMap = new Map<number, UInt64>()
94
+ prices.forEach((price, index) => {
95
+ priceMap.set(index, price)
96
+ })
97
+ return calculateSaleValue(cargo, priceMap)
98
+ }
99
+
100
+ export function afterSellGoods(
101
+ cargo: ServerContract.Types.cargo_item[],
102
+ goodsToSell: Array<{goodId: number; quantity: number}>
103
+ ): EntityInventory[] {
104
+ if (cargo.length === 0) {
105
+ return []
106
+ }
107
+
108
+ return cargo.map((item) => {
109
+ const saleItem = goodsToSell.find((s) => Number(item.good_id) === s.goodId)
110
+ if (!saleItem) {
111
+ return new EntityInventory(item)
112
+ }
113
+
114
+ const currentQty = Number(item.quantity)
115
+ const newQty = Math.max(0, currentQty - saleItem.quantity)
116
+
117
+ return new EntityInventory(
118
+ ServerContract.Types.cargo_item.from({
119
+ good_id: item.good_id,
120
+ quantity: UInt32.from(newQty),
121
+ unit_cost: item.unit_cost,
122
+ })
123
+ )
124
+ })
125
+ }
126
+
127
+ export function afterSellAllGoods(cargo: ServerContract.Types.cargo_item[]): EntityInventory[] {
128
+ if (cargo.length === 0) {
129
+ return []
130
+ }
131
+
132
+ return cargo.map(
133
+ (item) =>
134
+ new EntityInventory(
135
+ ServerContract.Types.cargo_item.from({
136
+ good_id: item.good_id,
137
+ quantity: UInt32.from(0),
138
+ unit_cost: item.unit_cost,
139
+ })
140
+ )
141
+ )
142
+ }
@@ -0,0 +1,70 @@
1
+ import {UInt64, UInt64Type} from '@wharfkit/antelope'
2
+ import {ServerContract} from '../contracts'
3
+ import {CoordinatesType} from '../types'
4
+ import {Location} from './location'
5
+ import {ScheduleAccessor} from '../scheduling/accessor'
6
+ import * as schedule from '../scheduling/schedule'
7
+
8
+ export interface ContainerStateInput {
9
+ id: UInt64Type
10
+ owner: string
11
+ name: string
12
+ coordinates: CoordinatesType | {x: number; y: number; z?: number}
13
+ hullmass: number
14
+ capacity: number
15
+ cargomass?: number
16
+ schedule?: ServerContract.Types.schedule
17
+ }
18
+
19
+ export class Container extends ServerContract.Types.entity_info {
20
+ private _sched?: ScheduleAccessor
21
+
22
+ get name(): string {
23
+ return this.entity_name
24
+ }
25
+
26
+ get sched(): ScheduleAccessor {
27
+ return (this._sched ??= new ScheduleAccessor(this))
28
+ }
29
+
30
+ get isIdle(): boolean {
31
+ return this.is_idle
32
+ }
33
+
34
+ isLoading(now: Date): boolean {
35
+ return schedule.isLoading(this, now)
36
+ }
37
+
38
+ isUnloading(now: Date): boolean {
39
+ return schedule.isUnloading(this, now)
40
+ }
41
+
42
+ get location(): Location {
43
+ return Location.from(this.coordinates)
44
+ }
45
+
46
+ get totalMass(): UInt64 {
47
+ return UInt64.from(this.hullmass ?? 0).adding(this.cargomass)
48
+ }
49
+
50
+ get maxCapacity(): UInt64 {
51
+ return UInt64.from(this.capacity)
52
+ }
53
+
54
+ get availableCapacity(): UInt64 {
55
+ const cargo = UInt64.from(this.cargomass)
56
+ return cargo.gte(this.maxCapacity) ? UInt64.from(0) : this.maxCapacity.subtracting(cargo)
57
+ }
58
+
59
+ hasSpace(additionalMass: UInt64): boolean {
60
+ return UInt64.from(this.cargomass).adding(additionalMass).lte(this.maxCapacity)
61
+ }
62
+
63
+ get isFull(): boolean {
64
+ return UInt64.from(this.cargomass).gte(this.maxCapacity)
65
+ }
66
+
67
+ get orbitalAltitude(): number {
68
+ return this.coordinates.z?.toNumber() || 0
69
+ }
70
+ }
@@ -0,0 +1,39 @@
1
+ import {UInt32, UInt64} from '@wharfkit/antelope'
2
+ import {ServerContract} from '../contracts'
3
+ import {getGood} from '../market/goods'
4
+ import {Good} from '../types'
5
+
6
+ export class EntityInventory extends ServerContract.Types.cargo_item {
7
+ private _good?: Good
8
+
9
+ get good(): Good {
10
+ if (!this._good) {
11
+ this._good = getGood(this.good_id)
12
+ }
13
+ return this._good
14
+ }
15
+
16
+ get name(): string {
17
+ return this.good.name
18
+ }
19
+
20
+ get unitMass(): UInt32 {
21
+ return this.good.mass
22
+ }
23
+
24
+ get totalMass(): UInt64 {
25
+ return UInt64.from(this.unitMass).multiplying(this.quantity)
26
+ }
27
+
28
+ get totalCost(): UInt64 {
29
+ return this.unit_cost.multiplying(this.quantity)
30
+ }
31
+
32
+ get hasCargo(): boolean {
33
+ return UInt32.from(this.quantity).gt(UInt32.from(0))
34
+ }
35
+
36
+ get isEmpty(): boolean {
37
+ return UInt32.from(this.quantity).equals(UInt32.from(0))
38
+ }
39
+ }
@@ -0,0 +1,152 @@
1
+ import {Checksum256, Int64, UInt64} from '@wharfkit/antelope'
2
+ import {PlatformContract, ServerContract} from '../contracts'
3
+ import {EpochInfo, getCurrentEpoch, getEpochInfo} from '../scheduling/epoch'
4
+ import {hasSystem} from '../utils/system'
5
+
6
+ /**
7
+ * GameState class extends the state_row from the server contract
8
+ * with helper methods for epoch management and system generation
9
+ */
10
+ export class GameState extends ServerContract.Types.state_row {
11
+ private _game?: PlatformContract.Types.game_row
12
+
13
+ /**
14
+ * Create a GameState instance from a state_row
15
+ */
16
+ static from(
17
+ state: ServerContract.Types.state_row,
18
+ game?: PlatformContract.Types.game_row
19
+ ): GameState {
20
+ const gameState = Object.create(GameState.prototype) as GameState
21
+ Object.assign(gameState, state)
22
+ gameState._game = game
23
+ return gameState
24
+ }
25
+
26
+ /**
27
+ * Set the game configuration (needed for epoch calculations)
28
+ */
29
+ setGame(game: PlatformContract.Types.game_row): void {
30
+ this._game = game
31
+ }
32
+
33
+ /**
34
+ * Get the current epoch number from the state
35
+ */
36
+ get currentEpoch(): UInt64 {
37
+ return this.epoch
38
+ }
39
+
40
+ /**
41
+ * Get the epoch seed (used for market pricing and system generation)
42
+ */
43
+ get epochSeed(): Checksum256 {
44
+ return this.seed
45
+ }
46
+
47
+ /**
48
+ * Get the game seed (from game config, if available)
49
+ */
50
+ get gameSeed(): Checksum256 | undefined {
51
+ return this._game?.config.seed
52
+ }
53
+
54
+ /**
55
+ * Check if the game is currently enabled
56
+ */
57
+ get isEnabled(): boolean {
58
+ return this.enabled
59
+ }
60
+
61
+ /**
62
+ * Get the total number of ships in the game
63
+ */
64
+ get shipCount(): number {
65
+ return Number(this.ships)
66
+ }
67
+
68
+ /**
69
+ * Get the current salt value (used for random number generation)
70
+ */
71
+ get currentSalt(): UInt64 {
72
+ return this.salt
73
+ }
74
+
75
+ /**
76
+ * Get the commit hash for the next epoch
77
+ */
78
+ get nextEpochCommit(): Checksum256 {
79
+ return this.commit
80
+ }
81
+
82
+ /**
83
+ * Calculate the current epoch from game config (if game is set)
84
+ * This might differ from state.epoch if the blockchain hasn't advanced yet
85
+ */
86
+ get calculatedCurrentEpoch(): UInt64 | undefined {
87
+ if (!this._game) {
88
+ return undefined
89
+ }
90
+ return getCurrentEpoch(this._game)
91
+ }
92
+
93
+ /**
94
+ * Get epoch info (start/end times) for the current epoch
95
+ */
96
+ get currentEpochInfo(): EpochInfo | undefined {
97
+ if (!this._game) {
98
+ return undefined
99
+ }
100
+ return getEpochInfo(this._game, this.epoch)
101
+ }
102
+
103
+ /**
104
+ * Get epoch info for a specific epoch number
105
+ */
106
+ getEpochInfo(epoch: UInt64): EpochInfo | undefined {
107
+ if (!this._game) {
108
+ return undefined
109
+ }
110
+ return getEpochInfo(this._game, epoch)
111
+ }
112
+
113
+ /**
114
+ * Check if a system exists at given coordinates
115
+ * Requires game seed from game config
116
+ */
117
+ hasSystemAt(x: number, y: number): boolean {
118
+ if (!this._game) {
119
+ return false
120
+ }
121
+ return hasSystem(this._game.config.seed, {x: Int64.from(x), y: Int64.from(y)})
122
+ }
123
+
124
+ /**
125
+ * Check if a system exists at coordinates object
126
+ */
127
+ hasSystemAtCoords(coords: ServerContract.Types.coordinates): boolean {
128
+ if (!this._game) {
129
+ return false
130
+ }
131
+ return hasSystem(this._game.config.seed, coords)
132
+ }
133
+
134
+ /**
135
+ * Get a summary of the game state
136
+ */
137
+ get summary(): {
138
+ enabled: boolean
139
+ epoch: string
140
+ ships: number
141
+ hasSeed: boolean
142
+ hasCommit: boolean
143
+ } {
144
+ return {
145
+ enabled: this.enabled,
146
+ epoch: this.epoch.toString(),
147
+ ships: this.shipCount,
148
+ hasSeed: !this.seed.equals(Checksum256.from('0'.repeat(64))),
149
+ hasCommit: !this.commit.equals(Checksum256.from('0'.repeat(64))),
150
+ }
151
+ }
152
+ }
@@ -0,0 +1,46 @@
1
+ import {UInt64, UInt64Type} from '@wharfkit/antelope'
2
+ import {EntityInventory} from './entity-inventory'
3
+ import {HasCargo} from '../capabilities/storage'
4
+
5
+ export type {HasCargo}
6
+
7
+ export class InventoryAccessor {
8
+ private _items?: EntityInventory[]
9
+
10
+ constructor(private readonly entity: HasCargo) {}
11
+
12
+ get items(): EntityInventory[] {
13
+ if (!this._items) {
14
+ this._items = this.entity.cargo.map((item) => new EntityInventory(item))
15
+ }
16
+ return this._items
17
+ }
18
+
19
+ get totalMass(): UInt64 {
20
+ return this.items.reduce((sum, c) => sum.adding(c.totalMass), UInt64.from(0))
21
+ }
22
+
23
+ get totalValue(): UInt64 {
24
+ return this.items.reduce((sum, c) => sum.adding(c.totalCost), UInt64.from(0))
25
+ }
26
+
27
+ forGood(goodId: UInt64Type): EntityInventory | undefined {
28
+ return this.items.find((c) => c.good_id.equals(goodId))
29
+ }
30
+
31
+ get sellable(): EntityInventory[] {
32
+ return this.items.filter((c) => c.hasCargo)
33
+ }
34
+
35
+ get hasSellable(): boolean {
36
+ return this.items.some((c) => c.hasCargo)
37
+ }
38
+
39
+ get sellableCount(): number {
40
+ return this.items.filter((c) => c.hasCargo).length
41
+ }
42
+ }
43
+
44
+ export function createInventoryAccessor(entity: HasCargo): InventoryAccessor {
45
+ return new InventoryAccessor(entity)
46
+ }
@@ -0,0 +1,255 @@
1
+ import {Checksum256, Checksum256Type, UInt16, UInt16Type, UInt64} from '@wharfkit/antelope'
2
+ import {ServerContract} from '../contracts'
3
+ import {Coordinates, CoordinatesType, Distance, GoodPrice, LocationType} from '../types'
4
+ import {getLocationType, hasSystem, isExtractableLocation} from '../utils/system'
5
+ import {findNearbyPlanets} from '../travel/travel'
6
+
7
+ /**
8
+ * Location helper class for working with game coordinates.
9
+ * Provides system detection, market price caching, nearby planet finding, and supply tracking.
10
+ */
11
+ export class Location {
12
+ readonly coordinates: Coordinates
13
+ private _marketPrices?: GoodPrice[]
14
+ private _gameSeed?: Checksum256
15
+ private _hasSystem?: boolean
16
+ private _locationRows?: ServerContract.Types.supply_row[]
17
+ private _epoch?: UInt64
18
+
19
+ constructor(coordinates: CoordinatesType) {
20
+ this.coordinates = Coordinates.from(coordinates)
21
+ }
22
+
23
+ /**
24
+ * Create a Location from coordinates
25
+ */
26
+ static from(coordinates: CoordinatesType): Location {
27
+ return new Location(Coordinates.from(coordinates))
28
+ }
29
+
30
+ /**
31
+ * Check if this location has a system (planet, asteroid, or nebula)
32
+ */
33
+ hasSystemAt(gameSeed: Checksum256Type): boolean {
34
+ const seed = Checksum256.from(gameSeed)
35
+ if (this._hasSystem === undefined || !this._gameSeed?.equals(seed)) {
36
+ this._gameSeed = seed
37
+ this._hasSystem = hasSystem(seed, this.coordinates)
38
+ }
39
+ return this._hasSystem
40
+ }
41
+
42
+ /**
43
+ * Get the location type (EMPTY, PLANET, ASTEROID, or NEBULA)
44
+ */
45
+ getLocationTypeAt(gameSeed: Checksum256Type): LocationType {
46
+ return getLocationType(gameSeed, this.coordinates)
47
+ }
48
+
49
+ /**
50
+ * Check if this location is extractable (asteroid or nebula)
51
+ */
52
+ isExtractableAt(gameSeed: Checksum256Type): boolean {
53
+ return isExtractableLocation(this.getLocationTypeAt(gameSeed))
54
+ }
55
+
56
+ /**
57
+ * Set cached market prices for this location
58
+ */
59
+ setMarketPrices(prices: GoodPrice[]): void {
60
+ this._marketPrices = prices
61
+ }
62
+
63
+ /**
64
+ * Get cached market prices (returns undefined if not cached)
65
+ */
66
+ get marketPrices(): GoodPrice[] | undefined {
67
+ return this._marketPrices
68
+ }
69
+
70
+ /**
71
+ * Get price for a specific good (from cache)
72
+ */
73
+ getPrice(goodId: UInt16Type): GoodPrice | undefined {
74
+ if (!this._marketPrices) return undefined
75
+ return this._marketPrices.find((p) => p.id.equals(goodId))
76
+ }
77
+
78
+ /**
79
+ * Find nearby planets from this location
80
+ */
81
+ findNearby(gameSeed: Checksum256Type, maxDistance: UInt16Type = 20): Distance[] {
82
+ return findNearbyPlanets(Checksum256.from(gameSeed), this.coordinates, maxDistance)
83
+ }
84
+
85
+ /**
86
+ * Check if this location equals another location
87
+ */
88
+ equals(other: CoordinatesType | Location): boolean {
89
+ const otherCoords = other instanceof Location ? other.coordinates : Coordinates.from(other)
90
+ return this.coordinates.equals(otherCoords)
91
+ }
92
+
93
+ /**
94
+ * Set location rows (supply data) for this location
95
+ */
96
+ setLocationRows(rows: ServerContract.Types.supply_row[], epoch: UInt64): void {
97
+ this._locationRows = rows
98
+ this._epoch = epoch
99
+ }
100
+
101
+ /**
102
+ * Get cached location rows (supply data)
103
+ */
104
+ get locationRows(): ServerContract.Types.supply_row[] | undefined {
105
+ return this._locationRows
106
+ }
107
+
108
+ /**
109
+ * Get supply for a specific good at this location
110
+ * Returns undefined if location rows not cached or good not found
111
+ */
112
+ getSupply(goodId: UInt16Type): UInt16 | undefined {
113
+ if (!this._locationRows) return undefined
114
+ const row = this._locationRows.find(
115
+ (r) => r.good_id.equals(goodId) && this._epoch && r.epoch.equals(this._epoch)
116
+ )
117
+ return row ? row.supply : undefined
118
+ }
119
+
120
+ /**
121
+ * Get all available goods at this location (goods with supply > 0)
122
+ * Returns undefined if location rows not cached
123
+ */
124
+ get availableGoods(): ServerContract.Types.supply_row[] | undefined {
125
+ if (!this._locationRows) return undefined
126
+ return this._locationRows.filter(
127
+ (r) => this._epoch && r.epoch.equals(this._epoch) && r.supply.gt(UInt16.from(0))
128
+ )
129
+ }
130
+
131
+ /**
132
+ * Check if a specific good is available (has supply)
133
+ * Returns false if location rows not cached
134
+ */
135
+ hasGood(goodId: UInt16Type): boolean {
136
+ const supply = this.getSupply(goodId)
137
+ return supply !== undefined && supply.gt(UInt16.from(0))
138
+ }
139
+
140
+ /**
141
+ * Get the epoch for cached location data
142
+ */
143
+ get epoch(): UInt64 | undefined {
144
+ return this._epoch
145
+ }
146
+
147
+ /**
148
+ * Check if cached data exists
149
+ */
150
+ get hasCachedData(): boolean {
151
+ return this._marketPrices !== undefined || this._locationRows !== undefined
152
+ }
153
+
154
+ /**
155
+ * Check if supply data is cached
156
+ */
157
+ get hasSupplyData(): boolean {
158
+ return this._locationRows !== undefined
159
+ }
160
+
161
+ /**
162
+ * Clear all cached data
163
+ */
164
+ clearCache(): void {
165
+ this._marketPrices = undefined
166
+ this._locationRows = undefined
167
+ this._epoch = undefined
168
+ }
169
+
170
+ /**
171
+ * Create optimistic Location with updated supply after purchase/sale.
172
+ * Matches contract: update_location_supply (delta can be positive or negative)
173
+ * Contract reference: market.cpp:53, 123-151
174
+ *
175
+ * @param goodId - Good ID to update supply for
176
+ * @param quantityDelta - Change in supply (negative for purchase, positive for sale)
177
+ * @returns New Location with updated supply in cached data
178
+ *
179
+ * @example
180
+ * // After buying 10 units (supply decreases)
181
+ * const newLocation = location.withUpdatedSupply(1, -10)
182
+ *
183
+ * // After selling 5 units (supply increases)
184
+ * const newLocation = location.withUpdatedSupply(1, 5)
185
+ */
186
+ withUpdatedSupply(goodId: UInt16Type, quantityDelta: number): Location {
187
+ const newLocation = Location.from(this.coordinates)
188
+
189
+ // Copy market prices if cached
190
+ if (this._marketPrices) {
191
+ newLocation._marketPrices = this._marketPrices.map((price) => {
192
+ if (price.id.equals(goodId)) {
193
+ const currentSupply = UInt16.from(price.supply)
194
+ const delta = UInt16.from(Math.abs(quantityDelta))
195
+ const newSupply =
196
+ quantityDelta < 0
197
+ ? currentSupply.gte(delta)
198
+ ? currentSupply.subtracting(delta)
199
+ : UInt16.from(0)
200
+ : currentSupply.adding(quantityDelta)
201
+
202
+ return GoodPrice.from({
203
+ id: price.id,
204
+ good: price.good,
205
+ price: price.price,
206
+ supply: newSupply,
207
+ })
208
+ }
209
+ return price
210
+ })
211
+ }
212
+
213
+ // Copy location rows if cached
214
+ if (this._locationRows && this._epoch) {
215
+ newLocation._locationRows = this._locationRows.map((row) => {
216
+ if (row.good_id.equals(goodId) && row.epoch.equals(this._epoch!)) {
217
+ const currentSupply = UInt16.from(row.supply)
218
+ const delta = UInt16.from(Math.abs(quantityDelta))
219
+ const newSupply =
220
+ quantityDelta < 0
221
+ ? currentSupply.gte(delta)
222
+ ? currentSupply.subtracting(delta)
223
+ : UInt16.from(0)
224
+ : currentSupply.adding(quantityDelta)
225
+
226
+ return ServerContract.Types.supply_row.from({
227
+ id: row.id,
228
+ coordinates: row.coordinates,
229
+ epoch: row.epoch,
230
+ good_id: row.good_id,
231
+ supply: newSupply,
232
+ })
233
+ }
234
+ return row
235
+ })
236
+ newLocation._epoch = this._epoch
237
+ }
238
+
239
+ // Copy other cached data
240
+ newLocation._gameSeed = this._gameSeed
241
+ newLocation._hasSystem = this._hasSystem
242
+
243
+ return newLocation
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Helper function to convert various coordinate types to Location
249
+ */
250
+ export function toLocation(coords: CoordinatesType | Location): Location {
251
+ if (coords instanceof Location) {
252
+ return coords
253
+ }
254
+ return Location.from(coords)
255
+ }