@shipload/sdk 1.0.0-next.66 → 1.0.0-next.68
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/lib/shipload.d.ts +90 -6
- package/lib/shipload.js +210 -5
- package/lib/shipload.js.map +1 -1
- package/lib/shipload.m.js +205 -6
- package/lib/shipload.m.js.map +1 -1
- package/lib/testing.d.ts +18 -0
- package/lib/testing.js +56 -2
- package/lib/testing.js.map +1 -1
- package/lib/testing.m.js +56 -2
- package/lib/testing.m.js.map +1 -1
- package/package.json +1 -1
- package/src/contracts/server.ts +56 -1
- package/src/data/charters.json +1 -1
- package/src/data/entities.json +5 -0
- package/src/data/metadata.ts +9 -2
- package/src/influence/civic-shuttle.test.ts +202 -0
- package/src/influence/civic-shuttle.ts +142 -0
- package/src/influence/civic.test.ts +23 -14
- package/src/influence/index.ts +10 -0
- package/src/managers/actions.ts +34 -0
- package/src/managers/epochs.ts +32 -1
- package/src/scheduling/epoch.ts +1 -0
- package/src/utils/system.ts +6 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {UInt8, UInt16, UInt32, UInt64} from '@wharfkit/antelope'
|
|
2
|
+
import {expect, test} from 'bun:test'
|
|
3
|
+
import type {ServerContract} from '../contracts'
|
|
4
|
+
import {encodeStats} from '../derivation/crafting'
|
|
5
|
+
import {ITEM_DEPOT_T1_PACKED, ITEM_LOADER_T1, ITEM_STORAGE_T1} from '../data/item-ids'
|
|
6
|
+
import {JOB_QUEUE_CAP} from '../scheduling/jobs'
|
|
7
|
+
import {HoldKind, TaskType} from '../types'
|
|
8
|
+
import {
|
|
9
|
+
civicShuttleBays,
|
|
10
|
+
civicTransfersAtCap,
|
|
11
|
+
civicTransfersFrom,
|
|
12
|
+
pendingCivicTransfers,
|
|
13
|
+
selectCivicShuttleBay,
|
|
14
|
+
CIVIC_TRANSFER_PER_PLAYER_CAP,
|
|
15
|
+
} from './civic-shuttle'
|
|
16
|
+
|
|
17
|
+
type Lane = ServerContract.Types.lane
|
|
18
|
+
type ModuleEntry = ServerContract.Types.module_entry
|
|
19
|
+
type Task = ServerContract.Types.task
|
|
20
|
+
|
|
21
|
+
const T0 = new Date('2026-01-01T00:00:00.000Z')
|
|
22
|
+
const loaderStats = encodeStats([300, 500])
|
|
23
|
+
const storageStats = encodeStats([300, 300])
|
|
24
|
+
|
|
25
|
+
function makeModuleEntry(itemId: number, stats: bigint): ModuleEntry {
|
|
26
|
+
return {
|
|
27
|
+
type: UInt8.from(0),
|
|
28
|
+
installed: {item_id: UInt16.from(itemId), stats: UInt64.from(stats)},
|
|
29
|
+
} as unknown as ModuleEntry
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The live Depot layout: two loader bays then four storage.
|
|
33
|
+
function depotModules(): ModuleEntry[] {
|
|
34
|
+
return [
|
|
35
|
+
makeModuleEntry(ITEM_LOADER_T1, loaderStats),
|
|
36
|
+
makeModuleEntry(ITEM_LOADER_T1, loaderStats),
|
|
37
|
+
makeModuleEntry(ITEM_STORAGE_T1, storageStats),
|
|
38
|
+
makeModuleEntry(ITEM_STORAGE_T1, storageStats),
|
|
39
|
+
makeModuleEntry(ITEM_STORAGE_T1, storageStats),
|
|
40
|
+
makeModuleEntry(ITEM_STORAGE_T1, storageStats),
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shuttleTask(senderId: number, receiverId: number, group?: number): Task {
|
|
45
|
+
return {
|
|
46
|
+
type: UInt8.from(TaskType.SHUTTLE),
|
|
47
|
+
duration: UInt32.from(60),
|
|
48
|
+
cancelable: UInt8.from(2),
|
|
49
|
+
cargo: [],
|
|
50
|
+
couplings: [
|
|
51
|
+
{
|
|
52
|
+
counterpart: {entity_id: UInt64.from(senderId), entity_type: UInt8.from(1)},
|
|
53
|
+
hold: UInt64.from(1),
|
|
54
|
+
kind: UInt8.from(HoldKind.PULL),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
counterpart: {entity_id: UInt64.from(receiverId), entity_type: UInt8.from(1)},
|
|
58
|
+
hold: UInt64.from(1),
|
|
59
|
+
kind: UInt8.from(HoldKind.PUSH),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
entitygroup: group === undefined ? undefined : UInt64.from(group),
|
|
63
|
+
} as unknown as Task
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeLane(laneKey: number, tasks: Task[]): Lane {
|
|
67
|
+
return {
|
|
68
|
+
lane_key: UInt8.from(laneKey),
|
|
69
|
+
schedule: {started: {toDate: () => T0}, tasks},
|
|
70
|
+
} as unknown as Lane
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test('civicShuttleBays lists both Depot bays with their queue depth', () => {
|
|
74
|
+
const lanes = [makeLane(1, [shuttleTask(20, 21)])]
|
|
75
|
+
const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, lanes, T0)
|
|
76
|
+
expect(bays.map((b) => b.laneKey)).toEqual([1, 2])
|
|
77
|
+
expect(bays.map((b) => b.slotIndex)).toEqual([0, 1])
|
|
78
|
+
expect(bays[0].queued).toBe(1)
|
|
79
|
+
expect(bays[1].queued).toBe(0)
|
|
80
|
+
expect(bays.every((b) => !b.full)).toBe(true)
|
|
81
|
+
expect(bays.every((b) => b.thrust > 0 && b.mass > 0)).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('a bay is full at the job queue cap', () => {
|
|
85
|
+
const filled = Array.from({length: JOB_QUEUE_CAP}, () => shuttleTask(20, 21))
|
|
86
|
+
const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, [makeLane(1, filled)], T0)
|
|
87
|
+
expect(bays[0].full).toBe(true)
|
|
88
|
+
expect(bays[1].full).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const AFTER = new Date(T0.getTime() + 3_600_000)
|
|
92
|
+
|
|
93
|
+
test('a landed booking leaves the bay free, since booking clears it', () => {
|
|
94
|
+
const filled = Array.from({length: JOB_QUEUE_CAP}, () => shuttleTask(20, 21))
|
|
95
|
+
const lanes = [makeLane(1, filled)]
|
|
96
|
+
const bays = civicShuttleBays(depotModules(), ITEM_DEPOT_T1_PACKED, lanes, AFTER)
|
|
97
|
+
expect(bays[0].queued).toBe(0)
|
|
98
|
+
expect(bays[0].full).toBe(false)
|
|
99
|
+
expect(selectCivicShuttleBay(depotModules(), ITEM_DEPOT_T1_PACKED, lanes, AFTER)?.laneKey).toBe(
|
|
100
|
+
1
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('a bay part-way through its queue counts only what is still unfinished', () => {
|
|
105
|
+
const lanes = [makeLane(1, [shuttleTask(20, 21), shuttleTask(22, 23), shuttleTask(24, 25)])]
|
|
106
|
+
const bays = civicShuttleBays(
|
|
107
|
+
depotModules(),
|
|
108
|
+
ITEM_DEPOT_T1_PACKED,
|
|
109
|
+
lanes,
|
|
110
|
+
new Date(T0.getTime() + 90_000)
|
|
111
|
+
)
|
|
112
|
+
expect(bays[0].queued).toBe(2)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('a landed transfer is listed for settling but is not callable', () => {
|
|
116
|
+
const lanes = [makeLane(1, [shuttleTask(20, 21)])]
|
|
117
|
+
const [transfer] = pendingCivicTransfers({lanes} as never, AFTER)
|
|
118
|
+
expect(transfer.isTail).toBe(true)
|
|
119
|
+
expect(transfer.complete).toBe(true)
|
|
120
|
+
expect(transfer.callable).toBe(false)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('landed transfers do not count against the per-player cap', () => {
|
|
124
|
+
const mine = Array.from({length: CIVIC_TRANSFER_PER_PLAYER_CAP}, (_, i) =>
|
|
125
|
+
shuttleTask(20 + i, 50)
|
|
126
|
+
)
|
|
127
|
+
const senderIds = mine.map((_, i) => 20 + i)
|
|
128
|
+
const at = {lanes: [makeLane(1, mine)]} as never
|
|
129
|
+
expect(civicTransfersAtCap(at, senderIds, T0)).toBe(true)
|
|
130
|
+
expect(civicTransfersAtCap(at, senderIds, AFTER)).toBe(false)
|
|
131
|
+
expect(civicTransfersFrom(at, senderIds, AFTER).length).toBe(CIVIC_TRANSFER_PER_PLAYER_CAP)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('selectCivicShuttleBay takes the first free bay, then the lowest occupied one', () => {
|
|
135
|
+
const modules = depotModules()
|
|
136
|
+
expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [], T0)?.laneKey).toBe(1)
|
|
137
|
+
|
|
138
|
+
const firstBusy = [makeLane(1, [shuttleTask(20, 21)])]
|
|
139
|
+
expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, firstBusy, T0)?.laneKey).toBe(2)
|
|
140
|
+
|
|
141
|
+
const bothBusy = [makeLane(1, [shuttleTask(20, 21)]), makeLane(2, [shuttleTask(22, 23)])]
|
|
142
|
+
expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, bothBusy, T0)?.laneKey).toBe(1)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('selectCivicShuttleBay returns nothing when the building has no loaders', () => {
|
|
146
|
+
const modules = [makeModuleEntry(ITEM_STORAGE_T1, storageStats)]
|
|
147
|
+
expect(selectCivicShuttleBay(modules, ITEM_DEPOT_T1_PACKED, [], T0)).toBeUndefined()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('pendingCivicTransfers reports the bay, both counterparties, and the tail', () => {
|
|
151
|
+
const lanes = [
|
|
152
|
+
makeLane(1, [shuttleTask(20, 21), shuttleTask(22, 23)]),
|
|
153
|
+
makeLane(2, [shuttleTask(24, 25)]),
|
|
154
|
+
]
|
|
155
|
+
const transfers = pendingCivicTransfers({lanes} as never, T0)
|
|
156
|
+
expect(transfers.length).toBe(3)
|
|
157
|
+
|
|
158
|
+
const first = transfers.find((t) => String(t.senderId) === '20')!
|
|
159
|
+
expect(first.laneKey).toBe(1)
|
|
160
|
+
expect(String(first.receiverId)).toBe('21')
|
|
161
|
+
expect(first.isTail).toBe(false)
|
|
162
|
+
expect(first.callable).toBe(false)
|
|
163
|
+
expect(first.startsAt).toEqual(T0)
|
|
164
|
+
expect(first.completesAt).toEqual(new Date(T0.getTime() + 60_000))
|
|
165
|
+
|
|
166
|
+
const second = transfers.find((t) => String(t.senderId) === '22')!
|
|
167
|
+
expect(second.isTail).toBe(true)
|
|
168
|
+
expect(second.callable).toBe(true)
|
|
169
|
+
|
|
170
|
+
const onBayTwo = transfers.find((t) => String(t.senderId) === '24')!
|
|
171
|
+
expect(onBayTwo.laneKey).toBe(2)
|
|
172
|
+
expect(onBayTwo.callable).toBe(true)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('a grouped tail transfer is not callable', () => {
|
|
176
|
+
const lanes = [makeLane(1, [shuttleTask(20, 21, 7)])]
|
|
177
|
+
const [transfer] = pendingCivicTransfers({lanes} as never, T0)
|
|
178
|
+
expect(transfer.isTail).toBe(true)
|
|
179
|
+
expect(transfer.callable).toBe(false)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('civicTransfersFrom counts only the ships a player sends from', () => {
|
|
183
|
+
const lanes = [
|
|
184
|
+
makeLane(1, [shuttleTask(20, 21), shuttleTask(30, 31)]),
|
|
185
|
+
makeLane(2, [shuttleTask(22, 23)]),
|
|
186
|
+
]
|
|
187
|
+
const entity = {lanes} as never
|
|
188
|
+
expect(civicTransfersFrom(entity, [20, 22], T0).length).toBe(2)
|
|
189
|
+
expect(civicTransfersFrom(entity, [30], T0).length).toBe(1)
|
|
190
|
+
expect(civicTransfersFrom(entity, [99], T0).length).toBe(0)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('civicTransfersAtCap trips at the per-player cap', () => {
|
|
194
|
+
const mine = Array.from({length: CIVIC_TRANSFER_PER_PLAYER_CAP}, (_, i) =>
|
|
195
|
+
shuttleTask(20 + i, 50)
|
|
196
|
+
)
|
|
197
|
+
const senderIds = mine.map((_, i) => 20 + i)
|
|
198
|
+
const under = {lanes: [makeLane(1, mine.slice(0, -1))]} as never
|
|
199
|
+
const at = {lanes: [makeLane(1, mine)]} as never
|
|
200
|
+
expect(civicTransfersAtCap(under, senderIds, T0)).toBe(false)
|
|
201
|
+
expect(civicTransfersAtCap(at, senderIds, T0)).toBe(true)
|
|
202
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type {UInt64} from '@wharfkit/antelope'
|
|
2
|
+
import type {ServerContract} from '../contracts'
|
|
3
|
+
import {getItem} from '../data/catalog'
|
|
4
|
+
import {JOB_QUEUE_CAP} from '../scheduling/jobs'
|
|
5
|
+
import {laneKeyForModule, resolveLaneLoader} from '../scheduling/lanes'
|
|
6
|
+
import {orderedTasks, type ScheduleData} from '../scheduling/schedule'
|
|
7
|
+
import {HoldKind, TaskType} from '../types'
|
|
8
|
+
|
|
9
|
+
type CargoItem = ServerContract.Types.cargo_item
|
|
10
|
+
type Lane = ServerContract.Types.lane
|
|
11
|
+
type ModuleEntry = ServerContract.Types.module_entry
|
|
12
|
+
|
|
13
|
+
export const CIVIC_TRANSFER_PER_PLAYER_CAP = 4
|
|
14
|
+
|
|
15
|
+
export interface CivicShuttleBay {
|
|
16
|
+
laneKey: number
|
|
17
|
+
slotIndex: number
|
|
18
|
+
thrust: number
|
|
19
|
+
mass: number
|
|
20
|
+
outputPct: number
|
|
21
|
+
queued: number
|
|
22
|
+
full: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// a landed booking clears during the next civicshuttle, so only unfinished tasks occupy a bay
|
|
26
|
+
function unfinishedTasks(lane: Lane | undefined, now: Date): number {
|
|
27
|
+
if (!lane) return 0
|
|
28
|
+
const startedMs = lane.schedule.started.toDate().getTime()
|
|
29
|
+
const nowMs = now.getTime()
|
|
30
|
+
let endSec = 0
|
|
31
|
+
let count = 0
|
|
32
|
+
for (const task of lane.schedule.tasks) {
|
|
33
|
+
endSec += task.duration.toNumber()
|
|
34
|
+
if (startedMs + endSec * 1000 > nowMs) count++
|
|
35
|
+
}
|
|
36
|
+
return count
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function civicShuttleBays(
|
|
40
|
+
modules: ModuleEntry[],
|
|
41
|
+
entityItemId: number,
|
|
42
|
+
lanes: Lane[],
|
|
43
|
+
now: Date = new Date()
|
|
44
|
+
): CivicShuttleBay[] {
|
|
45
|
+
const bays: CivicShuttleBay[] = []
|
|
46
|
+
for (let slotIndex = 0; slotIndex < modules.length; slotIndex++) {
|
|
47
|
+
const installed = modules[slotIndex].installed
|
|
48
|
+
if (!installed) continue
|
|
49
|
+
if (getItem(installed.item_id).moduleType !== 'loader') continue
|
|
50
|
+
const laneKey = laneKeyForModule(slotIndex)
|
|
51
|
+
const loader = resolveLaneLoader(modules, entityItemId, laneKey)
|
|
52
|
+
const lane = lanes.find((l) => l.lane_key.toNumber() === laneKey)
|
|
53
|
+
const queued = unfinishedTasks(lane, now)
|
|
54
|
+
bays.push({
|
|
55
|
+
laneKey,
|
|
56
|
+
slotIndex,
|
|
57
|
+
thrust: loader.thrust,
|
|
58
|
+
mass: loader.mass,
|
|
59
|
+
outputPct: loader.outputPct,
|
|
60
|
+
queued,
|
|
61
|
+
full: queued >= JOB_QUEUE_CAP,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
return bays
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Mirrors worker_lane_key_or_mobility for loaders: first free bay, else the lowest bay.
|
|
68
|
+
export function selectCivicShuttleBay(
|
|
69
|
+
modules: ModuleEntry[],
|
|
70
|
+
entityItemId: number,
|
|
71
|
+
lanes: Lane[],
|
|
72
|
+
now: Date = new Date()
|
|
73
|
+
): CivicShuttleBay | undefined {
|
|
74
|
+
const bays = civicShuttleBays(modules, entityItemId, lanes, now)
|
|
75
|
+
return bays.find((bay) => bay.queued === 0) ?? bays[0]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PendingCivicTransfer {
|
|
79
|
+
laneKey: number
|
|
80
|
+
taskIndex: number
|
|
81
|
+
senderId: UInt64
|
|
82
|
+
receiverId: UInt64
|
|
83
|
+
cargo: CargoItem[]
|
|
84
|
+
startsAt: Date
|
|
85
|
+
completesAt: Date
|
|
86
|
+
isTail: boolean
|
|
87
|
+
complete: boolean
|
|
88
|
+
callable: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function pendingCivicTransfers(
|
|
92
|
+
entity: ScheduleData,
|
|
93
|
+
now: Date = new Date()
|
|
94
|
+
): PendingCivicTransfer[] {
|
|
95
|
+
const tailIndex = new Map<number, number>()
|
|
96
|
+
for (const l of entity.lanes ?? []) {
|
|
97
|
+
tailIndex.set(l.lane_key.toNumber(), l.schedule.tasks.length - 1)
|
|
98
|
+
}
|
|
99
|
+
const out: PendingCivicTransfer[] = []
|
|
100
|
+
for (const entry of orderedTasks(entity)) {
|
|
101
|
+
if (entry.task.type.toNumber() !== TaskType.SHUTTLE) continue
|
|
102
|
+
const pull = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PULL)
|
|
103
|
+
const push = entry.task.couplings.find((c) => c.kind.toNumber() === HoldKind.PUSH)
|
|
104
|
+
if (!pull || !push) continue
|
|
105
|
+
const isTail = tailIndex.get(entry.laneKey) === entry.taskIndex
|
|
106
|
+
const complete = entry.completesAt.getTime() <= now.getTime()
|
|
107
|
+
out.push({
|
|
108
|
+
laneKey: entry.laneKey,
|
|
109
|
+
taskIndex: entry.taskIndex,
|
|
110
|
+
senderId: pull.counterpart.entity_id,
|
|
111
|
+
receiverId: push.counterpart.entity_id,
|
|
112
|
+
cargo: entry.task.cargo,
|
|
113
|
+
startsAt: entry.startsAt,
|
|
114
|
+
completesAt: entry.completesAt,
|
|
115
|
+
isTail,
|
|
116
|
+
complete,
|
|
117
|
+
callable: isTail && !complete && !entry.task.entitygroup,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
return out
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Mirrors civic_transfers_for: the sender side of every shuttle the building holds, by owner.
|
|
124
|
+
export function civicTransfersFrom(
|
|
125
|
+
entity: ScheduleData,
|
|
126
|
+
senderIds: Array<UInt64 | string | number>,
|
|
127
|
+
now: Date = new Date()
|
|
128
|
+
): PendingCivicTransfer[] {
|
|
129
|
+
const owned = new Set(senderIds.map((id) => String(id)))
|
|
130
|
+
return pendingCivicTransfers(entity, now).filter((t) => owned.has(String(t.senderId)))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function civicTransfersAtCap(
|
|
134
|
+
entity: ScheduleData,
|
|
135
|
+
senderIds: Array<UInt64 | string | number>,
|
|
136
|
+
now: Date = new Date()
|
|
137
|
+
): boolean {
|
|
138
|
+
return (
|
|
139
|
+
civicTransfersFrom(entity, senderIds, now).filter((t) => !t.complete).length >=
|
|
140
|
+
CIVIC_TRANSFER_PER_PLAYER_CAP
|
|
141
|
+
)
|
|
142
|
+
}
|
|
@@ -54,6 +54,8 @@ const NODE_DOCK_TUNEUPS = [30100301, 30100302, 30100303, 30100304]
|
|
|
54
54
|
const NODE_DEPOT = 40100001
|
|
55
55
|
const NODE_DEPOT_BAYS = [40100101, 40100102, 40100103, 40100104]
|
|
56
56
|
const NODE_DEPOT_LOADERS = [40100401, 40100402, 40100403, 40100404]
|
|
57
|
+
const DEPOT_LOADER_SLOTS = [0, 1]
|
|
58
|
+
const DEPOT_BAY_SLOTS = [2, 3, 4, 5]
|
|
57
59
|
|
|
58
60
|
function node(nodeId: number): CharterNode {
|
|
59
61
|
const found = charterNode(nodeId)
|
|
@@ -168,14 +170,16 @@ describe('the fourteen nodes reproduce the chain-fitted buildings', () => {
|
|
|
168
170
|
expect(raised).toBe(400)
|
|
169
171
|
})
|
|
170
172
|
|
|
171
|
-
test('the Depot node fits
|
|
173
|
+
test('the Depot node fits two baseline loaders and leaves the bays empty', () => {
|
|
172
174
|
const standing = complete(complete({}, NODE_WORKSHOP), NODE_DEPOT)
|
|
173
175
|
const depot = {modules: standing[CIVIC_DEPOT]!}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
for (const slot of DEPOT_LOADER_SLOTS) {
|
|
177
|
+
expect(stats(depot, slot)).toEqual({
|
|
178
|
+
itemId: ITEM_LOADER_T1,
|
|
179
|
+
stats: [CHARTER_BASELINE_STAT, CHARTER_BASELINE_STAT],
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
for (const slot of DEPOT_BAY_SLOTS) expect(depot.modules[slot].installed).toBeUndefined()
|
|
179
183
|
})
|
|
180
184
|
|
|
181
185
|
test('the four bays fill the storage slots in order at the baseline stat', () => {
|
|
@@ -183,8 +187,9 @@ describe('the fourteen nodes reproduce the chain-fitted buildings', () => {
|
|
|
183
187
|
for (let i = 0; i < NODE_DEPOT_BAYS.length; i++) {
|
|
184
188
|
standing = complete(standing, NODE_DEPOT_BAYS[i])
|
|
185
189
|
const depot = {modules: standing[CIVIC_DEPOT]!}
|
|
186
|
-
for (let
|
|
187
|
-
|
|
190
|
+
for (let bay = 0; bay < DEPOT_BAY_SLOTS.length; bay++) {
|
|
191
|
+
const slot = DEPOT_BAY_SLOTS[bay]
|
|
192
|
+
if (bay <= i) {
|
|
188
193
|
expect(stats(depot, slot)).toEqual({
|
|
189
194
|
itemId: ITEM_STORAGE_T1,
|
|
190
195
|
stats: [
|
|
@@ -201,14 +206,16 @@ describe('the fourteen nodes reproduce the chain-fitted buildings', () => {
|
|
|
201
206
|
}
|
|
202
207
|
})
|
|
203
208
|
|
|
204
|
-
test('the four loader rungs step
|
|
209
|
+
test('the four loader rungs step both loaders by one rung each, reaching 400', () => {
|
|
205
210
|
let standing = complete(complete({}, NODE_WORKSHOP), NODE_DEPOT)
|
|
206
211
|
for (let i = 0; i < NODE_DEPOT_LOADERS.length; i++) {
|
|
207
212
|
standing = complete(standing, NODE_DEPOT_LOADERS[i])
|
|
208
213
|
const depot = {modules: standing[CIVIC_DEPOT]!}
|
|
209
214
|
const stat = CHARTER_BASELINE_STAT + CHARTER_RUNG_STEP * (i + 1)
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
for (const slot of DEPOT_LOADER_SLOTS) {
|
|
216
|
+
expect(stats(depot, slot)).toEqual({itemId: ITEM_LOADER_T1, stats: [stat, stat]})
|
|
217
|
+
}
|
|
218
|
+
for (const slot of DEPOT_BAY_SLOTS)
|
|
212
219
|
expect(depot.modules[slot].installed).toBeUndefined()
|
|
213
220
|
}
|
|
214
221
|
})
|
|
@@ -223,8 +230,10 @@ describe('grant mechanics', () => {
|
|
|
223
230
|
stat: CIVIC_STAT_TRANSFER_SPEED,
|
|
224
231
|
value: 50,
|
|
225
232
|
})
|
|
226
|
-
const
|
|
227
|
-
expect(stats({modules:
|
|
233
|
+
const withSecondLoader = fitCivicModules(raised, 1)
|
|
234
|
+
expect(stats({modules: withSecondLoader}, 1).stats).toEqual([50, 50])
|
|
235
|
+
const withBay = fitCivicModules(withSecondLoader, 1)
|
|
236
|
+
expect(stats({modules: withBay}, 2).stats).toEqual([0, 0, 0, 0])
|
|
228
237
|
const withSecondBay = fitCivicModules(
|
|
229
238
|
raiseCivicStat(withBay, {
|
|
230
239
|
kind: CIVIC_GRANT_RUNG,
|
|
@@ -234,7 +243,7 @@ describe('grant mechanics', () => {
|
|
|
234
243
|
}),
|
|
235
244
|
1
|
|
236
245
|
)
|
|
237
|
-
expect(stats({modules: withSecondBay},
|
|
246
|
+
expect(stats({modules: withSecondBay}, 3).stats).toEqual([7, 7, 7, 7])
|
|
238
247
|
})
|
|
239
248
|
|
|
240
249
|
test('a rung caps at the civic speed cap', () => {
|
package/src/influence/index.ts
CHANGED
|
@@ -43,6 +43,16 @@ export {
|
|
|
43
43
|
type CivicLoaderStats,
|
|
44
44
|
type DepotTransferParams,
|
|
45
45
|
} from './duration'
|
|
46
|
+
export {
|
|
47
|
+
civicShuttleBays,
|
|
48
|
+
civicTransfersAtCap,
|
|
49
|
+
civicTransfersFrom,
|
|
50
|
+
pendingCivicTransfers,
|
|
51
|
+
selectCivicShuttleBay,
|
|
52
|
+
CIVIC_TRANSFER_PER_PLAYER_CAP,
|
|
53
|
+
type CivicShuttleBay,
|
|
54
|
+
type PendingCivicTransfer,
|
|
55
|
+
} from './civic-shuttle'
|
|
46
56
|
export {citizenryName, citizenryPatternCount} from './citizenry'
|
|
47
57
|
export {
|
|
48
58
|
charterBuildingEntity,
|
package/src/managers/actions.ts
CHANGED
|
@@ -366,6 +366,28 @@ export class ActionsManager extends BaseManager {
|
|
|
366
366
|
})
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
civicshuttle(
|
|
370
|
+
buildingId: UInt64Type,
|
|
371
|
+
fromId: UInt64Type,
|
|
372
|
+
toId: UInt64Type,
|
|
373
|
+
items: ServerContract.ActionParams.Type.cargo_item[]
|
|
374
|
+
): Action {
|
|
375
|
+
return this.server.action('civicshuttle', {
|
|
376
|
+
building: UInt64.from(buildingId),
|
|
377
|
+
from_id: UInt64.from(fromId),
|
|
378
|
+
to_id: UInt64.from(toId),
|
|
379
|
+
items,
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
cancelcivic(buildingId: UInt64Type, laneKey: UInt8Type, fromId: UInt64Type): Action {
|
|
384
|
+
return this.server.action('cancelcivic', {
|
|
385
|
+
building: UInt64.from(buildingId),
|
|
386
|
+
lane_key: UInt8.from(laneKey),
|
|
387
|
+
from_id: UInt64.from(fromId),
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
369
391
|
launch(
|
|
370
392
|
launcherId: UInt64Type,
|
|
371
393
|
catcherId: UInt64Type,
|
|
@@ -982,6 +1004,18 @@ export class ActionsManager extends BaseManager {
|
|
|
982
1004
|
})
|
|
983
1005
|
}
|
|
984
1006
|
|
|
1007
|
+
setdeadline(deadline: UInt32Type): Action {
|
|
1008
|
+
return this.server.action('setdeadline', {
|
|
1009
|
+
deadline: UInt32.from(deadline),
|
|
1010
|
+
})
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
closeepoch(epoch: UInt64Type): Action {
|
|
1014
|
+
return this.server.action('closeepoch', {
|
|
1015
|
+
epoch: UInt64.from(epoch),
|
|
1016
|
+
})
|
|
1017
|
+
}
|
|
1018
|
+
|
|
985
1019
|
cleanrsvp(epoch: UInt64Type, maxRows: UInt64Type): Action {
|
|
986
1020
|
return this.server.action('cleanrsvp', {
|
|
987
1021
|
epoch: UInt64.from(epoch),
|
package/src/managers/epochs.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
import {UInt64, type UInt64Type} from '@wharfkit/antelope'
|
|
1
|
+
import {Checksum256, UInt64, type UInt64Type} from '@wharfkit/antelope'
|
|
2
2
|
import {BaseManager} from './base'
|
|
3
3
|
import {type EpochInfo, getCurrentEpoch, getEpochInfo} from '../scheduling/epoch'
|
|
4
4
|
import type {ServerContract} from '../contracts'
|
|
5
5
|
|
|
6
|
+
export const DEFAULT_EPOCH_DEADLINE = 10800
|
|
7
|
+
|
|
8
|
+
const EMPTY_SEED = Checksum256.from('0'.repeat(64))
|
|
9
|
+
|
|
10
|
+
export interface EpochQuorumState {
|
|
11
|
+
threshold: number
|
|
12
|
+
finalized: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export class EpochsManager extends BaseManager {
|
|
7
16
|
async getCurrentHeight(): Promise<UInt64> {
|
|
8
17
|
const game = await this.getGame()
|
|
@@ -56,6 +65,12 @@ export class EpochsManager extends BaseManager {
|
|
|
56
65
|
return this.server.table('epoch').get(target)
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
async getEpochState(epoch: UInt64Type): Promise<EpochQuorumState> {
|
|
69
|
+
const row = await this.getEpochRow(epoch)
|
|
70
|
+
if (!row) return {threshold: 0, finalized: false}
|
|
71
|
+
return {threshold: Number(row.threshold), finalized: !row.seed.equals(EMPTY_SEED)}
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
async getActiveEpochInfo(): Promise<ServerContract.Types.epoch_row | undefined> {
|
|
60
75
|
const rows = await this.server.table('epoch').all()
|
|
61
76
|
if (rows.length === 0) {
|
|
@@ -73,6 +88,22 @@ export class EpochsManager extends BaseManager {
|
|
|
73
88
|
return cfg ? Number(cfg.threshold) : 0
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
async getDeadline(): Promise<number> {
|
|
92
|
+
const cfg = await this.server.table('oraclecfg').get()
|
|
93
|
+
return cfg ? Number(cfg.deadline) : DEFAULT_EPOCH_DEADLINE
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async getCloseDueAt(epoch: UInt64Type): Promise<Date> {
|
|
97
|
+
const info = await this.getByHeight(epoch)
|
|
98
|
+
const deadline = await this.getDeadline()
|
|
99
|
+
return new Date(info.start.getTime() + deadline * 1000)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getSecondsUntilClose(epoch: UInt64Type): Promise<number> {
|
|
103
|
+
const dueAt = await this.getCloseDueAt(epoch)
|
|
104
|
+
return Math.ceil((dueAt.getTime() - Date.now()) / 1000)
|
|
105
|
+
}
|
|
106
|
+
|
|
76
107
|
async getCommitsFor(epoch: UInt64Type): Promise<ServerContract.Types.commit_row[]> {
|
|
77
108
|
const target = UInt64.from(epoch)
|
|
78
109
|
const rows = await this.server.table('commit').all()
|
package/src/scheduling/epoch.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface EpochInfo {
|
|
|
10
10
|
export function getCurrentEpoch(game: PlatformContract.Types.game_row): UInt64 {
|
|
11
11
|
const current = new Date().getTime()
|
|
12
12
|
const difference = (current - game.config.start.toMilliseconds()) / 1000
|
|
13
|
+
if (difference < 0) return UInt64.from(0)
|
|
13
14
|
const epoch = Math.floor(difference / Number(game.config.epochtime)) + 1
|
|
14
15
|
return UInt64.from(epoch)
|
|
15
16
|
}
|
package/src/utils/system.ts
CHANGED
|
@@ -161,7 +161,12 @@ export function isLocationBuildable(
|
|
|
161
161
|
gameSeed: Checksum256Type,
|
|
162
162
|
coordinates: CoordinatesType
|
|
163
163
|
): boolean {
|
|
164
|
-
|
|
164
|
+
const type = getLocationType(gameSeed, coordinates)
|
|
165
|
+
return (
|
|
166
|
+
type === LocationType.ASTEROID ||
|
|
167
|
+
type === LocationType.NEBULA ||
|
|
168
|
+
type === LocationType.ICE_FIELD
|
|
169
|
+
)
|
|
165
170
|
}
|
|
166
171
|
|
|
167
172
|
export function deriveLocation(
|