@shipload/sdk 2.0.0-rc1 → 2.0.0-rc3

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 (52) hide show
  1. package/README.md +349 -1
  2. package/lib/shipload.d.ts +809 -314
  3. package/lib/shipload.js +2745 -1740
  4. package/lib/shipload.js.map +1 -1
  5. package/lib/shipload.m.js +2158 -1154
  6. package/lib/shipload.m.js.map +1 -1
  7. package/package.json +1 -2
  8. package/src/capabilities/extraction.ts +30 -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 +506 -183
  15. package/src/data/items.json +16 -0
  16. package/src/derivation/index.ts +25 -0
  17. package/src/derivation/location-size.ts +15 -0
  18. package/src/derivation/resources.ts +141 -0
  19. package/src/derivation/stratum.ts +116 -0
  20. package/src/entities/cargo-utils.ts +98 -3
  21. package/src/entities/container.ts +70 -0
  22. package/src/entities/entity-inventory.ts +13 -9
  23. package/src/entities/inventory-accessor.ts +46 -0
  24. package/src/entities/location.ts +31 -17
  25. package/src/entities/makers.ts +69 -0
  26. package/src/entities/player.ts +2 -1
  27. package/src/entities/ship.ts +93 -440
  28. package/src/entities/warehouse.ts +29 -145
  29. package/src/errors.ts +4 -4
  30. package/src/index-module.ts +62 -4
  31. package/src/managers/actions.ts +75 -31
  32. package/src/managers/entities.ts +22 -5
  33. package/src/managers/locations.ts +34 -15
  34. package/src/managers/trades.ts +7 -7
  35. package/src/market/items.ts +31 -0
  36. package/src/market/market.ts +12 -13
  37. package/src/scheduling/accessor.ts +82 -0
  38. package/src/scheduling/projection.ts +126 -54
  39. package/src/scheduling/schedule.ts +24 -0
  40. package/src/trading/collect.ts +25 -26
  41. package/src/trading/deal.ts +8 -9
  42. package/src/trading/trade.ts +9 -9
  43. package/src/travel/travel.ts +69 -8
  44. package/src/types/capabilities.ts +79 -0
  45. package/src/types/entity-traits.ts +70 -0
  46. package/src/types/entity.ts +36 -0
  47. package/src/types/index.ts +3 -0
  48. package/src/types.ts +92 -15
  49. package/src/utils/hash.ts +1 -1
  50. package/src/utils/system.ts +97 -4
  51. package/src/data/goods.json +0 -23
  52. package/src/market/goods.ts +0 -31
@@ -1,46 +1,47 @@
1
- import {Bytes, Checksum256, UInt16Type, UInt64} from '@wharfkit/antelope'
1
+ import {Bytes, Checksum256, UInt16Type, UInt64, UInt64Type} from '@wharfkit/antelope'
2
2
  import {BaseManager} from './base'
3
- import {CoordinatesType, Distance, GoodPrice} from '../types'
3
+ import {CoordinatesType, coordsToLocationId, Distance, ItemPrice} from '../types'
4
4
  import {marketPrice, marketPrices} from '../market/market'
5
5
  import {hasSystem} from '../utils/system'
6
6
  import {findNearbyPlanets} from '../travel/travel'
7
7
  import {Location, toLocation} from '../entities/location'
8
+ import {ServerContract} from '../contracts'
8
9
 
9
10
  export class LocationsManager extends BaseManager {
10
- async getMarketPrice(location: CoordinatesType, goodId: number): Promise<GoodPrice> {
11
+ async getMarketPrice(location: CoordinatesType, goodId: number): Promise<ItemPrice> {
11
12
  const game = await this.getGame()
12
13
  const state = await this.getState()
13
14
  return marketPrice(location, goodId, game.config.seed, state)
14
15
  }
15
16
 
16
- async getMarketPrices(location: CoordinatesType): Promise<GoodPrice[]> {
17
+ async getMarketPrices(location: CoordinatesType): Promise<ItemPrice[]> {
17
18
  const game = await this.getGame()
18
19
  const state = await this.getState()
19
20
  return marketPrices(location, game.config.seed, state)
20
21
  }
21
22
 
22
- async getMarketPricesWithSupply(location: CoordinatesType): Promise<GoodPrice[]> {
23
- const [game, state, locationRows] = await Promise.all([
23
+ async getMarketPricesWithSupply(location: CoordinatesType): Promise<ItemPrice[]> {
24
+ const [game, state, supplyRows] = await Promise.all([
24
25
  this.getGame(),
25
26
  this.getState(),
26
- this.getLocation(location),
27
+ this.getSupplyRows(location),
27
28
  ])
28
29
 
29
30
  const prices = marketPrices(location, game.config.seed, state)
30
31
 
31
32
  const supplyMap = new Map<number, number>()
32
- for (const row of locationRows) {
33
+ for (const row of supplyRows) {
33
34
  if (UInt64.from(row.epoch).equals(state.epoch)) {
34
- supplyMap.set(Number(row.good_id), Number(row.supply))
35
+ supplyMap.set(Number(row.item_id), Number(row.supply))
35
36
  }
36
37
  }
37
38
 
38
39
  return prices.map((price) => {
39
40
  const actualSupply = supplyMap.get(Number(price.id))
40
41
  if (actualSupply !== undefined) {
41
- return GoodPrice.from({
42
+ return ItemPrice.from({
42
43
  id: price.id,
43
- good: price.good,
44
+ item: price.item,
44
45
  price: price.price,
45
46
  supply: UInt64.from(actualSupply),
46
47
  })
@@ -62,9 +63,9 @@ export class LocationsManager extends BaseManager {
62
63
  return findNearbyPlanets(game.config.seed, origin, maxDistance)
63
64
  }
64
65
 
65
- async getLocation(location: CoordinatesType) {
66
+ async getSupplyRows(location: CoordinatesType) {
66
67
  const hash = Checksum256.hash(Bytes.from(`${location.x}-${location.y}`, 'utf8'))
67
- return this.server.table('location').all({
68
+ return this.server.table('supply').all({
68
69
  index_position: 'secondary',
69
70
  from: hash,
70
71
  to: hash,
@@ -81,7 +82,7 @@ export class LocationsManager extends BaseManager {
81
82
  async getLocationWithSupply(coords: CoordinatesType): Promise<Location> {
82
83
  const location = toLocation(coords)
83
84
  const [rows, state] = await Promise.all([
84
- this.getLocation(location.coordinates),
85
+ this.getSupplyRows(location.coordinates),
85
86
  this.getState(),
86
87
  ])
87
88
  location.setLocationRows(rows, state.epoch)
@@ -92,7 +93,7 @@ export class LocationsManager extends BaseManager {
92
93
  const location = toLocation(coords)
93
94
  const [prices, rows, state] = await Promise.all([
94
95
  this.getMarketPrices(location.coordinates),
95
- this.getLocation(location.coordinates),
96
+ this.getSupplyRows(location.coordinates),
96
97
  this.getState(),
97
98
  ])
98
99
 
@@ -100,4 +101,22 @@ export class LocationsManager extends BaseManager {
100
101
  location.setLocationRows(rows, state.epoch)
101
102
  return location
102
103
  }
104
+
105
+ async getLocationEntity(
106
+ id: UInt64Type
107
+ ): Promise<ServerContract.Types.location_row | undefined> {
108
+ const row = await this.server.table('location').get(UInt64.from(id))
109
+ return row ?? undefined
110
+ }
111
+
112
+ async getLocationEntityAt(
113
+ coords: CoordinatesType
114
+ ): Promise<ServerContract.Types.location_row | undefined> {
115
+ const id = coordsToLocationId(coords)
116
+ return this.getLocationEntity(id)
117
+ }
118
+
119
+ async getAllLocationEntities(): Promise<ServerContract.Types.location_row[]> {
120
+ return this.server.table('location').all()
121
+ }
103
122
  }
@@ -8,13 +8,13 @@ import {
8
8
  CollectAnalysisCallbacks,
9
9
  CollectAnalysisOptions,
10
10
  } from '../trading/collect'
11
- import {Coordinates, GoodPrice} from '../types'
11
+ import {Coordinates, ItemPrice} from '../types'
12
12
  import {Location, toLocation} from '../entities/location'
13
13
  import {findNearbyPlanets} from '../travel/travel'
14
14
  import {getCurrentEpoch} from '../scheduling/epoch'
15
15
 
16
16
  export class TradesManager extends BaseManager {
17
- private priceCache = new Map<string, GoodPrice[]>()
17
+ private priceCache = new Map<string, ItemPrice[]>()
18
18
  private priceCacheEpoch: UInt64 | undefined
19
19
 
20
20
  private makePriceCacheKey(location: Coordinates): string {
@@ -39,7 +39,7 @@ export class TradesManager extends BaseManager {
39
39
  return nearby.map((d) => toLocation(d.destination))
40
40
  }
41
41
 
42
- const getMarketPrices = async (location: Coordinates): Promise<GoodPrice[]> => {
42
+ const getMarketPrices = async (location: Coordinates): Promise<ItemPrice[]> => {
43
43
  const cacheKey = this.makePriceCacheKey(location)
44
44
  const cached = this.priceCache.get(cacheKey)
45
45
  if (cached) {
@@ -53,9 +53,9 @@ export class TradesManager extends BaseManager {
53
53
  const actualSupply = locationWithSupply.getSupply(price.id)
54
54
 
55
55
  if (actualSupply !== undefined) {
56
- return GoodPrice.from({
56
+ return ItemPrice.from({
57
57
  id: price.id,
58
- good: price.good,
58
+ item: price.item,
59
59
  price: price.price,
60
60
  supply: actualSupply,
61
61
  })
@@ -83,7 +83,7 @@ export class TradesManager extends BaseManager {
83
83
  originLocation?: Coordinates,
84
84
  options: FindDealsOptions = {}
85
85
  ): Promise<Deal[]> {
86
- const origin = originLocation || ship.currentLocation
86
+ const origin = originLocation || Coordinates.from(ship.coordinates)
87
87
  const callbacks = await this.createCallbacks()
88
88
 
89
89
  const deals = await findDealsForShip(
@@ -111,7 +111,7 @@ export class TradesManager extends BaseManager {
111
111
  arrivedAt?: Coordinates,
112
112
  options: CollectAnalysisOptions = {}
113
113
  ): Promise<CollectAnalysis> {
114
- const location = arrivedAt || ship.currentLocation
114
+ const location = arrivedAt || Coordinates.from(ship.coordinates)
115
115
  const callbacks = await this.createCallbacks()
116
116
 
117
117
  return analyzeCollectOptions(ship, location, callbacks, options)
@@ -0,0 +1,31 @@
1
+ import {UInt16, UInt16Type} from '@wharfkit/antelope'
2
+ import {Item} from '../types'
3
+ import itemsData from '../data/items.json'
4
+
5
+ const items: Item[] = itemsData.map((g) =>
6
+ Item.from({
7
+ id: g.id,
8
+ name: g.name,
9
+ description: g.description,
10
+ base_price: g.base_price,
11
+ mass: g.mass,
12
+ category: g.category,
13
+ rarity: g.rarity,
14
+ color: g.color,
15
+ })
16
+ )
17
+
18
+ export const itemIds = items.map((i) => i.id)
19
+
20
+ export function getItem(itemId: UInt16Type): Item {
21
+ const id = UInt16.from(itemId)
22
+ const item = items.find((i) => i.id.equals(id))
23
+ if (!item) {
24
+ throw new Error(`Item with id ${id} not found`)
25
+ }
26
+ return item
27
+ }
28
+
29
+ export function getItems(): Item[] {
30
+ return items
31
+ }
@@ -1,5 +1,5 @@
1
- import {CoordinatesType, GoodPrice} from '../types'
2
- import {getGood, getGoods} from './goods'
1
+ import {CoordinatesType, ItemPrice} from '../types'
2
+ import {getItem, getItems} from './items'
3
3
  import {Checksum256Type, UInt16, UInt16Type, UInt32} from '@wharfkit/antelope'
4
4
  import {roll} from './rolls'
5
5
  import {ServerContract} from '../contracts'
@@ -163,11 +163,10 @@ export function getSupply(
163
163
  const r = roll(gameSeed, seed)
164
164
  const percent = r / 65535
165
165
  const epoch = 1 + Number(state.epoch) / 90
166
- // NOTE: Contract has bug where 1/3 is integer division = 0, so pow(ships, 0) = 1
167
- // TODO: Update this when contract is fixed to use (double)1/(double)3
168
- const ship = Math.pow(Number(state.ships), 0)
166
+ const ship = 1
169
167
  const goodIdNum = Number(goodId)
170
- return Math.floor((128 / goodIdNum) * percent * ship * epoch)
168
+ const base = Math.floor(128 / goodIdNum)
169
+ return Math.floor(base * percent * ship * epoch)
171
170
  }
172
171
 
173
172
  export function marketPrice(
@@ -175,9 +174,9 @@ export function marketPrice(
175
174
  goodId: UInt16Type,
176
175
  gameSeed: Checksum256Type,
177
176
  state: ServerContract.Types.state_row
178
- ): GoodPrice {
179
- const good = getGood(goodId)
180
- let price = Number(good.base_price)
177
+ ): ItemPrice {
178
+ const item = getItem(goodId)
179
+ let price = Number(item.base_price)
181
180
 
182
181
  // Rarity multiplier of the deal (changes with epoch)
183
182
  // Large impact range on price, from 0.285x to 3.0x
@@ -192,9 +191,9 @@ export function marketPrice(
192
191
  // Determine the current supply of the good at the location
193
192
  const supply = getSupply(gameSeed, state, location, goodId)
194
193
 
195
- return GoodPrice.from({
194
+ return ItemPrice.from({
196
195
  id: goodId,
197
- good,
196
+ item,
198
197
  price: UInt32.from(price),
199
198
  supply: UInt16.from(supply),
200
199
  })
@@ -204,6 +203,6 @@ export function marketPrices(
204
203
  location: ServerContract.ActionParams.Type.coordinates,
205
204
  gameSeed: Checksum256Type,
206
205
  state: ServerContract.Types.state_row
207
- ): GoodPrice[] {
208
- return getGoods().map((good) => marketPrice(location, good.id, gameSeed, state))
206
+ ): ItemPrice[] {
207
+ return getItems().map((item) => marketPrice(location, item.id, gameSeed, state))
209
208
  }
@@ -0,0 +1,82 @@
1
+ import {ServerContract} from '../contracts'
2
+ import {TaskType} from '../types'
3
+ import {ScheduleData} from './schedule'
4
+ import * as schedule from './schedule'
5
+
6
+ type Task = ServerContract.Types.task
7
+
8
+ export class ScheduleAccessor {
9
+ constructor(private entity: ScheduleData) {}
10
+
11
+ get hasSchedule(): boolean {
12
+ return schedule.hasSchedule(this.entity)
13
+ }
14
+
15
+ get isIdle(): boolean {
16
+ return schedule.isIdle(this.entity)
17
+ }
18
+
19
+ get tasks(): Task[] {
20
+ return schedule.getTasks(this.entity)
21
+ }
22
+
23
+ duration(): number {
24
+ return schedule.scheduleDuration(this.entity)
25
+ }
26
+
27
+ elapsed(now: Date): number {
28
+ return schedule.scheduleElapsed(this.entity, now)
29
+ }
30
+
31
+ remaining(now: Date): number {
32
+ return schedule.scheduleRemaining(this.entity, now)
33
+ }
34
+
35
+ complete(now: Date): boolean {
36
+ return schedule.scheduleComplete(this.entity, now)
37
+ }
38
+
39
+ currentTaskIndex(now: Date): number {
40
+ return schedule.currentTaskIndex(this.entity, now)
41
+ }
42
+
43
+ currentTask(now: Date): Task | undefined {
44
+ return schedule.currentTask(this.entity, now)
45
+ }
46
+
47
+ currentTaskType(now: Date): TaskType | undefined {
48
+ return schedule.currentTaskType(this.entity, now)
49
+ }
50
+
51
+ taskStartTime(index: number): number {
52
+ return schedule.getTaskStartTime(this.entity, index)
53
+ }
54
+
55
+ taskElapsed(index: number, now: Date): number {
56
+ return schedule.getTaskElapsed(this.entity, index, now)
57
+ }
58
+
59
+ taskRemaining(index: number, now: Date): number {
60
+ return schedule.getTaskRemaining(this.entity, index, now)
61
+ }
62
+
63
+ taskComplete(index: number, now: Date): boolean {
64
+ return schedule.isTaskComplete(this.entity, index, now)
65
+ }
66
+
67
+ taskInProgress(index: number, now: Date): boolean {
68
+ return schedule.isTaskInProgress(this.entity, index, now)
69
+ }
70
+
71
+ currentTaskProgress(now: Date): number {
72
+ return schedule.currentTaskProgress(this.entity, now)
73
+ }
74
+
75
+ progress(now: Date): number {
76
+ return schedule.scheduleProgress(this.entity, now)
77
+ }
78
+ }
79
+
80
+ export function createScheduleAccessor(entity: ScheduleData): ScheduleAccessor {
81
+ return new ScheduleAccessor(entity)
82
+ }
@@ -1,15 +1,19 @@
1
- import {UInt16, UInt32, UInt64} from '@wharfkit/antelope'
1
+ import {Name, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
2
2
  import {ServerContract} from '../contracts'
3
3
  import {Coordinates, PRECISION, TaskType} from '../types'
4
+ import {
5
+ capsHasLoaders,
6
+ capsHasMovement,
7
+ capsHasStorage,
8
+ EntityCapabilities,
9
+ EntityState,
10
+ } from '../types/capabilities'
4
11
  import {distanceBetweenCoordinates, lerp} from '../travel/travel'
5
- import {getGood} from '../market/goods'
12
+ import {calcCargoMass} from '../capabilities/storage'
13
+ import {getItem} from '../market/items'
6
14
  import * as schedule from './schedule'
7
15
  import {ScheduleData} from './schedule'
8
16
 
9
- /**
10
- * Projected state of an entity after scheduled tasks complete.
11
- * Mirrors contract's projected_entity struct.
12
- */
13
17
  export interface ProjectedEntity {
14
18
  location: Coordinates
15
19
  energy: UInt16
@@ -19,41 +23,56 @@ export interface ProjectedEntity {
19
23
  engines?: ServerContract.Types.movement_stats
20
24
  loaders?: ServerContract.Types.loader_stats
21
25
  generator?: ServerContract.Types.energy_stats
26
+ trade?: ServerContract.Types.trade_stats
22
27
  readonly totalMass: UInt64
28
+
29
+ hasMovement(): boolean
30
+ hasStorage(): boolean
31
+ hasLoaders(): boolean
32
+ hasTrade(): boolean
33
+
34
+ capabilities(): EntityCapabilities
35
+ state(): EntityState
23
36
  }
24
37
 
25
- /**
26
- * Interface for entities that can be projected.
27
- * Ships and Warehouses both implement this.
28
- */
29
38
  export interface Projectable extends ScheduleData {
30
- location: Coordinates
31
- energy: UInt16
32
- mass: UInt32
39
+ coordinates: Coordinates | ServerContract.Types.coordinates
40
+ energy?: UInt16
41
+ hullmass?: UInt32
33
42
  generator?: ServerContract.Types.energy_stats
34
43
  engines?: ServerContract.Types.movement_stats
35
44
  loaders?: ServerContract.Types.loader_stats
36
- capacity?: UInt64
37
- calcCargoMass(): UInt64
45
+ trade?: ServerContract.Types.trade_stats
46
+ capacity?: UInt32
47
+ cargo: ServerContract.Types.cargo_item[]
48
+ cargomass: UInt32
49
+ owner?: Name
50
+ }
51
+
52
+ function getHullMass(entity: Projectable): UInt32 {
53
+ return UInt32.from(entity.hullmass ?? 0)
38
54
  }
39
55
 
40
- /**
41
- * Create initial projected entity state from a projectable entity.
42
- */
43
56
  export function createProjectedEntity(entity: Projectable): ProjectedEntity {
44
- const cargoMass = entity.calcCargoMass()
45
- const shipMass = UInt32.from(entity.mass)
57
+ const cargoMass = calcCargoMass(entity)
58
+ const shipMass = getHullMass(entity)
46
59
  const loaders = entity.loaders
60
+ const engines = entity.engines
61
+ const generator = entity.generator
62
+ const trade = entity.trade
63
+ const capacity = entity.capacity
47
64
 
48
- return {
49
- location: Coordinates.from(entity.location),
50
- energy: UInt16.from(entity.energy),
65
+ const projected: ProjectedEntity = {
66
+ location: Coordinates.from(entity.coordinates),
67
+ energy: UInt16.from(entity.energy ?? 0),
51
68
  cargoMass,
52
69
  shipMass,
53
- capacity: entity.capacity,
54
- engines: entity.engines,
55
- generator: entity.generator,
70
+ capacity: capacity ? UInt64.from(capacity) : undefined,
71
+ engines,
72
+ generator,
56
73
  loaders,
74
+ trade,
75
+
57
76
  get totalMass() {
58
77
  let mass = UInt64.from(this.shipMass).adding(this.cargoMass)
59
78
  if (this.loaders) {
@@ -61,12 +80,48 @@ export function createProjectedEntity(entity: Projectable): ProjectedEntity {
61
80
  }
62
81
  return mass
63
82
  },
83
+
84
+ hasMovement() {
85
+ return capsHasMovement(this.capabilities())
86
+ },
87
+
88
+ hasStorage() {
89
+ return capsHasStorage(this.capabilities())
90
+ },
91
+
92
+ hasLoaders() {
93
+ return capsHasLoaders(this.capabilities())
94
+ },
95
+
96
+ hasTrade() {
97
+ return this.trade !== undefined
98
+ },
99
+
100
+ capabilities(): EntityCapabilities {
101
+ return {
102
+ hullmass: this.shipMass,
103
+ capacity: this.capacity ? UInt32.from(this.capacity) : undefined,
104
+ engines: this.engines,
105
+ generator: this.generator,
106
+ loaders: this.loaders,
107
+ trade: this.trade,
108
+ }
109
+ },
110
+
111
+ state(): EntityState {
112
+ return {
113
+ owner: entity.owner ?? Name.from(''),
114
+ location: ServerContract.Types.coordinates.from(this.location),
115
+ energy: this.energy,
116
+ cargomass: UInt32.from(this.cargoMass),
117
+ cargo: entity.cargo,
118
+ }
119
+ },
64
120
  }
121
+
122
+ return projected
65
123
  }
66
124
 
67
- /**
68
- * Apply a recharge task to projected state.
69
- */
70
125
  function applyRechargeTask(
71
126
  projected: ProjectedEntity,
72
127
  _task: ServerContract.Types.task,
@@ -84,19 +139,16 @@ function applyRechargeTask(
84
139
  }
85
140
  }
86
141
 
87
- /**
88
- * Apply a flight task to projected state.
89
- */
90
142
  function applyFlightTask(
91
143
  projected: ProjectedEntity,
92
144
  task: ServerContract.Types.task,
93
145
  options: {complete: boolean; progress?: number}
94
146
  ): void {
95
- if (!task.location || !projected.engines) return
147
+ if (!task.coordinates || !projected.engines) return
96
148
 
97
149
  const origin = projected.location
98
- const destination = Coordinates.from(task.location)
99
- const distance = distanceBetweenCoordinates(origin, task.location)
150
+ const destination = Coordinates.from(task.coordinates)
151
+ const distance = distanceBetweenCoordinates(origin, task.coordinates)
100
152
  const energyUsage = distance.dividing(PRECISION).multiplying(projected.engines.drain)
101
153
 
102
154
  if (options.complete) {
@@ -117,33 +169,48 @@ function applyFlightTask(
117
169
  }
118
170
  }
119
171
 
120
- /**
121
- * Apply a load task to projected state.
122
- */
172
+ function getItemMass(item_id: UInt16 | number): UInt32 {
173
+ const item = getItem(item_id)
174
+ return item.mass
175
+ }
176
+
123
177
  function applyLoadTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
124
178
  for (const item of task.cargo) {
125
- const good = getGood(item.good_id)
126
- projected.cargoMass = projected.cargoMass.adding(good.mass.multiplying(item.quantity))
179
+ const good_mass = getItemMass(item.item_id)
180
+ projected.cargoMass = projected.cargoMass.adding(good_mass.multiplying(item.quantity))
127
181
  }
128
182
  }
129
183
 
130
- /**
131
- * Apply an unload task to projected state.
132
- */
133
184
  function applyUnloadTask(projected: ProjectedEntity, task: ServerContract.Types.task): void {
134
185
  for (const item of task.cargo) {
135
- const good = getGood(item.good_id)
136
- const cargoMass = good.mass.multiplying(item.quantity)
186
+ const good_mass = getItemMass(item.item_id)
187
+ const cargoMass = good_mass.multiplying(item.quantity)
137
188
  projected.cargoMass = projected.cargoMass.gt(cargoMass)
138
189
  ? projected.cargoMass.subtracting(cargoMass)
139
190
  : UInt64.from(0)
140
191
  }
141
192
  }
142
193
 
143
- /**
144
- * Project entity state after all scheduled tasks complete.
145
- * Mirrors contract's project_ship/project_warehouse methods.
146
- */
194
+ function applyExtractTask(
195
+ projected: ProjectedEntity,
196
+ task: ServerContract.Types.task,
197
+ options: {complete: boolean}
198
+ ): void {
199
+ if (!options.complete) return
200
+
201
+ if (task.energy_cost) {
202
+ const energyCost = UInt16.from(task.energy_cost)
203
+ projected.energy = projected.energy.gt(energyCost)
204
+ ? UInt16.from(projected.energy.subtracting(energyCost))
205
+ : UInt16.from(0)
206
+ }
207
+
208
+ for (const item of task.cargo) {
209
+ const good_mass = getItemMass(item.item_id)
210
+ projected.cargoMass = projected.cargoMass.adding(good_mass.multiplying(item.quantity))
211
+ }
212
+ }
213
+
147
214
  export function projectEntity(entity: Projectable): ProjectedEntity {
148
215
  const projected = createProjectedEntity(entity)
149
216
 
@@ -156,7 +223,7 @@ export function projectEntity(entity: Projectable): ProjectedEntity {
156
223
  case TaskType.RECHARGE:
157
224
  applyRechargeTask(projected, task, {complete: true})
158
225
  break
159
- case TaskType.FLIGHT:
226
+ case TaskType.TRAVEL:
160
227
  applyFlightTask(projected, task, {complete: true})
161
228
  break
162
229
  case TaskType.LOAD:
@@ -165,15 +232,15 @@ export function projectEntity(entity: Projectable): ProjectedEntity {
165
232
  case TaskType.UNLOAD:
166
233
  applyUnloadTask(projected, task)
167
234
  break
235
+ case TaskType.EXTRACT:
236
+ applyExtractTask(projected, task, {complete: true})
237
+ break
168
238
  }
169
239
  }
170
240
 
171
241
  return projected
172
242
  }
173
243
 
174
- /**
175
- * Project entity state at a specific time (partial task execution).
176
- */
177
244
  export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity {
178
245
  const projected = createProjectedEntity(entity)
179
246
 
@@ -198,7 +265,7 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
198
265
  case TaskType.RECHARGE:
199
266
  applyRechargeTask(projected, task, {complete: taskComplete, progress})
200
267
  break
201
- case TaskType.FLIGHT:
268
+ case TaskType.TRAVEL:
202
269
  applyFlightTask(projected, task, {complete: taskComplete, progress})
203
270
  break
204
271
  case TaskType.LOAD:
@@ -211,6 +278,11 @@ export function projectEntityAt(entity: Projectable, now: Date): ProjectedEntity
211
278
  applyUnloadTask(projected, task)
212
279
  }
213
280
  break
281
+ case TaskType.EXTRACT:
282
+ if (taskComplete) {
283
+ applyExtractTask(projected, task, {complete: true})
284
+ }
285
+ break
214
286
  }
215
287
  }
216
288
 
@@ -153,3 +153,27 @@ export function scheduleProgress(entity: ScheduleData, now: Date): number {
153
153
  const elapsed = scheduleElapsed(entity, now)
154
154
  return Math.min(1, elapsed / duration)
155
155
  }
156
+
157
+ export function isTaskType(entity: ScheduleData, taskType: TaskType, now: Date): boolean {
158
+ return currentTaskType(entity, now) === taskType
159
+ }
160
+
161
+ export function isInFlight(entity: ScheduleData, now: Date): boolean {
162
+ return isTaskType(entity, TaskType.TRAVEL, now)
163
+ }
164
+
165
+ export function isRecharging(entity: ScheduleData, now: Date): boolean {
166
+ return isTaskType(entity, TaskType.RECHARGE, now)
167
+ }
168
+
169
+ export function isLoading(entity: ScheduleData, now: Date): boolean {
170
+ return isTaskType(entity, TaskType.LOAD, now)
171
+ }
172
+
173
+ export function isUnloading(entity: ScheduleData, now: Date): boolean {
174
+ return isTaskType(entity, TaskType.UNLOAD, now)
175
+ }
176
+
177
+ export function isExtracting(entity: ScheduleData, now: Date): boolean {
178
+ return isTaskType(entity, TaskType.EXTRACT, now)
179
+ }