@driftengine/chemistry 3.61.0
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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +194 -0
- package/dist/conservation.d.ts +9 -0
- package/dist/conservation.js +38 -0
- package/dist/element/elements.d.ts +31 -0
- package/dist/element/elements.js +73 -0
- package/dist/element/matrix.d.ts +27 -0
- package/dist/element/matrix.js +80 -0
- package/dist/field/ambient.d.ts +49 -0
- package/dist/field/ambient.js +82 -0
- package/dist/field/atmosphere.d.ts +308 -0
- package/dist/field/atmosphere.js +1033 -0
- package/dist/ignition/criteria.d.ts +97 -0
- package/dist/ignition/criteria.js +135 -0
- package/dist/ignition/events.d.ts +56 -0
- package/dist/ignition/events.js +90 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +66 -0
- package/dist/library/biological.d.ts +6 -0
- package/dist/library/biological.js +229 -0
- package/dist/library/common.d.ts +26 -0
- package/dist/library/common.js +208 -0
- package/dist/library/food.d.ts +9 -0
- package/dist/library/food.js +272 -0
- package/dist/library/fuel.d.ts +19 -0
- package/dist/library/fuel.js +290 -0
- package/dist/library/library.d.ts +45 -0
- package/dist/library/library.js +29 -0
- package/dist/library/metal.d.ts +6 -0
- package/dist/library/metal.js +181 -0
- package/dist/library/mineral.d.ts +4 -0
- package/dist/library/mineral.js +177 -0
- package/dist/library/organic.d.ts +12 -0
- package/dist/library/organic.js +489 -0
- package/dist/library/polymer.d.ts +4 -0
- package/dist/library/polymer.js +237 -0
- package/dist/parcel/conduction.d.ts +20 -0
- package/dist/parcel/conduction.js +160 -0
- package/dist/parcel/shells.d.ts +49 -0
- package/dist/parcel/shells.js +73 -0
- package/dist/parcel/store.d.ts +395 -0
- package/dist/parcel/store.js +1152 -0
- package/dist/parcel/tier.d.ts +48 -0
- package/dist/parcel/tier.js +51 -0
- package/dist/present/audio.d.ts +41 -0
- package/dist/present/audio.js +50 -0
- package/dist/present/blackbody.d.ts +55 -0
- package/dist/present/blackbody.js +112 -0
- package/dist/present/flame.d.ts +34 -0
- package/dist/present/flame.js +103 -0
- package/dist/present/index.d.ts +25 -0
- package/dist/present/index.js +22 -0
- package/dist/present/install.d.ts +87 -0
- package/dist/present/install.js +157 -0
- package/dist/present/smoke.d.ts +63 -0
- package/dist/present/smoke.js +85 -0
- package/dist/present/surface.d.ts +40 -0
- package/dist/present/surface.js +76 -0
- package/dist/reaction/localise.d.ts +16 -0
- package/dist/reaction/localise.js +95 -0
- package/dist/reaction/rates.d.ts +59 -0
- package/dist/reaction/rates.js +101 -0
- package/dist/reaction/reaction.d.ts +178 -0
- package/dist/reaction/reaction.js +104 -0
- package/dist/reaction/registry.d.ts +47 -0
- package/dist/reaction/registry.js +215 -0
- package/dist/reaction/solver.d.ts +51 -0
- package/dist/reaction/solver.js +277 -0
- package/dist/species/registry.d.ts +26 -0
- package/dist/species/registry.js +97 -0
- package/dist/species/species.d.ts +48 -0
- package/dist/species/species.js +20 -0
- package/dist/species/standard.d.ts +30 -0
- package/dist/species/standard.js +436 -0
- package/dist/species/thermal.d.ts +18 -0
- package/dist/species/thermal.js +72 -0
- package/dist/substance/boiling.d.ts +29 -0
- package/dist/substance/boiling.js +54 -0
- package/dist/substance/composition.d.ts +34 -0
- package/dist/substance/composition.js +53 -0
- package/dist/substance/registry.d.ts +86 -0
- package/dist/substance/registry.js +187 -0
- package/dist/substance/substance.d.ts +94 -0
- package/dist/substance/substance.js +48 -0
- package/dist/testing/drift.d.ts +15 -0
- package/dist/testing/drift.js +29 -0
- package/dist/transport/contacts.d.ts +22 -0
- package/dist/transport/contacts.js +50 -0
- package/dist/transport/radiation.d.ts +101 -0
- package/dist/transport/radiation.js +199 -0
- package/dist/transport/world.d.ts +92 -0
- package/dist/transport/world.js +381 -0
- package/dist/world/fingerprint.d.ts +31 -0
- package/dist/world/fingerprint.js +70 -0
- package/package.json +100 -0
- package/src/conservation.ts +44 -0
- package/src/element/elements.ts +80 -0
- package/src/element/matrix.ts +89 -0
- package/src/field/ambient.ts +111 -0
- package/src/field/atmosphere.ts +1159 -0
- package/src/ignition/criteria.ts +177 -0
- package/src/ignition/events.ts +100 -0
- package/src/index.ts +213 -0
- package/src/library/biological.ts +252 -0
- package/src/library/common.ts +224 -0
- package/src/library/food.ts +302 -0
- package/src/library/fuel.ts +299 -0
- package/src/library/library.ts +62 -0
- package/src/library/metal.ts +186 -0
- package/src/library/mineral.ts +194 -0
- package/src/library/organic.ts +545 -0
- package/src/library/polymer.ts +243 -0
- package/src/parcel/conduction.ts +188 -0
- package/src/parcel/shells.ts +100 -0
- package/src/parcel/store.ts +1270 -0
- package/src/parcel/tier.ts +60 -0
- package/src/present/audio.ts +54 -0
- package/src/present/blackbody.ts +118 -0
- package/src/present/flame.ts +111 -0
- package/src/present/index.ts +25 -0
- package/src/present/install.ts +209 -0
- package/src/present/smoke.ts +139 -0
- package/src/present/surface.ts +107 -0
- package/src/reaction/localise.ts +126 -0
- package/src/reaction/rates.ts +153 -0
- package/src/reaction/reaction.ts +235 -0
- package/src/reaction/registry.ts +257 -0
- package/src/reaction/solver.ts +326 -0
- package/src/species/registry.ts +117 -0
- package/src/species/species.ts +53 -0
- package/src/species/standard.ts +449 -0
- package/src/species/thermal.ts +74 -0
- package/src/substance/boiling.ts +64 -0
- package/src/substance/composition.ts +81 -0
- package/src/substance/registry.ts +223 -0
- package/src/substance/substance.ts +148 -0
- package/src/testing/drift.ts +28 -0
- package/src/transport/contacts.ts +56 -0
- package/src/transport/radiation.ts +238 -0
- package/src/transport/world.ts +437 -0
- package/src/world/fingerprint.ts +106 -0
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parcels: lumps of matter with an identity, in structure-of-arrays typed columns.
|
|
3
|
+
*
|
|
4
|
+
* A parcel is a log, a steak, a puddle, a wall panel, a barrel of oil. It is **not** a mesh, a body
|
|
5
|
+
* or an entity — it is bound to one by a handle the consumer keeps, and it knows nothing about what
|
|
6
|
+
* it is bound to. That is what lets the package import no other engine package.
|
|
7
|
+
*
|
|
8
|
+
* **Enthalpy in joules, mass in kilograms, and temperature is neither.** The store holds a total
|
|
9
|
+
* and the curve holds a specific value, so two kilograms at one temperature hold twice the enthalpy
|
|
10
|
+
* of one and `temperatureOf` divides before it looks up. Every column is `Float64Array`, per `§15`:
|
|
11
|
+
* enthalpy differences across a burn span twelve orders of magnitude and `Float32Array` loses the
|
|
12
|
+
* small ones, which is not an optimisation to revisit later.
|
|
13
|
+
*
|
|
14
|
+
* **One lump, one temperature, until CH-2.** The design's parcel has a depth stack of shells, and
|
|
15
|
+
* without it a thick log heats through as fast as a twig — which is the single most important thing
|
|
16
|
+
* a fire model has to get right. That is CH-2's, and this store is shaped to grow into it: a shell
|
|
17
|
+
* is another stride on the enthalpy column and nothing above it changes.
|
|
18
|
+
*
|
|
19
|
+
* **A destroyed handle is never reused.** Physics numbers bodies the same way, and reuse without a
|
|
20
|
+
* generation lets a stale handle address a live parcel. Compaction is a budget question and belongs
|
|
21
|
+
* with the rest of them in CH-6; until then a destroyed parcel costs its columns and nothing else.
|
|
22
|
+
*/
|
|
23
|
+
import { sensibleEnthalpy, temperatureFromCapacity } from '../species/thermal.ts';
|
|
24
|
+
import type { SubstanceRegistry } from '../substance/registry.ts';
|
|
25
|
+
import { conductShells } from './conduction.ts';
|
|
26
|
+
import { MAX_SUBSTANCE_SPECIES, reactShell } from '../reaction/solver.ts';
|
|
27
|
+
import { ELEMENT_COUNT } from '../element/elements.ts';
|
|
28
|
+
import { MAX_SHELLS, shellGeometry } from './shells.ts';
|
|
29
|
+
import {
|
|
30
|
+
TIER_CADENCE,
|
|
31
|
+
TIER_HERO,
|
|
32
|
+
TIER_SHELLS,
|
|
33
|
+
TIER_SUBSTEPS,
|
|
34
|
+
type Tier,
|
|
35
|
+
foldShare,
|
|
36
|
+
foldSpan,
|
|
37
|
+
} from './tier.ts';
|
|
38
|
+
import { PHASE_GAS, PHASE_LIQUID, PHASE_SOLID } from '../species/species.ts';
|
|
39
|
+
import type { AppearanceModel } from '../substance/substance.ts';
|
|
40
|
+
import type { IgnitionModel } from '../ignition/criteria.ts';
|
|
41
|
+
|
|
42
|
+
/** Parcels held before the first growth. Doubles from here. */
|
|
43
|
+
const INITIAL_CAPACITY = 64;
|
|
44
|
+
|
|
45
|
+
/* Scratch for a fold, module-level so a tier change on a hot path allocates nothing. */
|
|
46
|
+
const foldScratch = new Float64Array(MAX_SHELLS * MAX_SUBSTANCE_SPECIES);
|
|
47
|
+
const foldEnthalpy = new Float64Array(MAX_SHELLS);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A parcel made of more than one phase at once, which is most of them.
|
|
51
|
+
*
|
|
52
|
+
* Not a `Phase`, because it is not one: wet wood is a solid and a liquid together, and the design's
|
|
53
|
+
* `§20.3` gives it its own value rather than making a consumer guess which half was reported.
|
|
54
|
+
*/
|
|
55
|
+
export const PHASE_MIXED = 3;
|
|
56
|
+
|
|
57
|
+
/** Share of a parcel's mass one phase must hold before that phase is the answer. */
|
|
58
|
+
const PHASE_DOMINANT = 0.95;
|
|
59
|
+
|
|
60
|
+
/** Share of a shell's mass that must be char before the shell counts as charred through. */
|
|
61
|
+
const CHAR_SHELL_FRACTION = 0.5;
|
|
62
|
+
|
|
63
|
+
const FLAG_BURNING = 1;
|
|
64
|
+
const FLAG_SMOULDERING = 2;
|
|
65
|
+
const FLAG_PILOT = 4;
|
|
66
|
+
const FLAG_SLEEPING = 8;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ticks a parcel must be still for before it sleeps, and how still "still" is.
|
|
70
|
+
*
|
|
71
|
+
* `§14`: "a parcel whose net flux is below a threshold, whose composition changed by less than a
|
|
72
|
+
* threshold last tick, and which has been that way for `N` ticks, sleeps." The counter is what stops
|
|
73
|
+
* a parcel that is merely between two events from being put to sleep and woken every other tick,
|
|
74
|
+
* which would cost more than never sleeping it.
|
|
75
|
+
*
|
|
76
|
+
* The flux threshold is three orders of magnitude below the critical mass flux at piloted ignition,
|
|
77
|
+
* so nothing anywhere near catching is ever asleep.
|
|
78
|
+
*/
|
|
79
|
+
const QUIET_TICKS = 30;
|
|
80
|
+
const QUIET_FLUX = 1e-6;
|
|
81
|
+
/** Joules per second of reaction output below which a parcel counts as chemically still. */
|
|
82
|
+
const QUIET_RELEASE = 1e-3;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* What a parcel is made of and how big it is. An object rather than seven positional arguments,
|
|
86
|
+
* which is how `PhysicsWorld.addBody` takes a body and for the same reason.
|
|
87
|
+
*/
|
|
88
|
+
export interface ParcelSpec {
|
|
89
|
+
readonly substance: number;
|
|
90
|
+
readonly mass: number;
|
|
91
|
+
/** K. Every shell starts here. */
|
|
92
|
+
readonly temperature: number;
|
|
93
|
+
/**
|
|
94
|
+
* Exposed surface area, m². Optional, and **conduction refuses a parcel without one**, naming it:
|
|
95
|
+
* a characteristic depth is volume over area, so a parcel with no area has no depth and there is
|
|
96
|
+
* no honest default to invent. `parcelFromBounds` supplies it from a bounding box in CH-9.
|
|
97
|
+
*/
|
|
98
|
+
readonly area?: number;
|
|
99
|
+
readonly x?: number;
|
|
100
|
+
readonly y?: number;
|
|
101
|
+
readonly z?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class ParcelStore {
|
|
105
|
+
private substance = new Int32Array(INITIAL_CAPACITY);
|
|
106
|
+
private mass = new Float64Array(INITIAL_CAPACITY);
|
|
107
|
+
/** Strided by `MAX_SHELLS`: shell `s` of parcel `p` is at `p * MAX_SHELLS + s`, in joules. */
|
|
108
|
+
private enthalpy = new Float64Array(INITIAL_CAPACITY * MAX_SHELLS);
|
|
109
|
+
private area = new Float64Array(INITIAL_CAPACITY);
|
|
110
|
+
private x = new Float64Array(INITIAL_CAPACITY);
|
|
111
|
+
private y = new Float64Array(INITIAL_CAPACITY);
|
|
112
|
+
private z = new Float64Array(INITIAL_CAPACITY);
|
|
113
|
+
private live = new Uint8Array(INITIAL_CAPACITY);
|
|
114
|
+
/** `FLAG_BURNING | FLAG_SMOULDERING | FLAG_PILOT`. */
|
|
115
|
+
private flags = new Uint8Array(INITIAL_CAPACITY);
|
|
116
|
+
/** kg/(m²·s) of gas leaving the surface, and of the combustible part of it. */
|
|
117
|
+
private flux = new Float64Array(INITIAL_CAPACITY);
|
|
118
|
+
private fuelFlux = new Float64Array(INITIAL_CAPACITY);
|
|
119
|
+
/**
|
|
120
|
+
* Watts the reactions put into this parcel over the last `react`, and it is a *readout*.
|
|
121
|
+
*
|
|
122
|
+
* **A fire's size is its heat release rate**, not its temperature — every fire-engineering figure
|
|
123
|
+
* in the design is quoted in kilowatts — and it cannot be recovered from outside, because
|
|
124
|
+
* conduction and a radiative source change the same enthalpy in the same tick. So it is measured
|
|
125
|
+
* where only reactions are running, by summing the thermal enthalpy either side of them. That sum
|
|
126
|
+
* is one pass over at most sixteen shells, which is nothing against the reaction step it brackets.
|
|
127
|
+
*/
|
|
128
|
+
private release = new Float64Array(INITIAL_CAPACITY);
|
|
129
|
+
/** Which of `§14`'s four tiers each parcel is being simulated at. */
|
|
130
|
+
private tier = new Uint8Array(INITIAL_CAPACITY);
|
|
131
|
+
/** Shells actually resolved right now, which a tier may reduce below what the substance declared. */
|
|
132
|
+
private shells = new Int32Array(INITIAL_CAPACITY);
|
|
133
|
+
/** Seconds owed to a parcel its cadence has been skipping, handed over when its turn comes. */
|
|
134
|
+
private owed = new Float64Array(INITIAL_CAPACITY);
|
|
135
|
+
/** Consecutive ticks a parcel has been still. At `QUIET_TICKS` it sleeps. */
|
|
136
|
+
private quiet = new Int32Array(INITIAL_CAPACITY);
|
|
137
|
+
/**
|
|
138
|
+
* Where each parcel's composition block starts, and the block itself.
|
|
139
|
+
*
|
|
140
|
+
* A block is `shells × the substance's own species count` kilograms — **dense over the few
|
|
141
|
+
* species that substance can hold**, not a row over all sixty-eight. Blocks are appended and
|
|
142
|
+
* never moved, which is what makes an offset stable; compaction is a budget question and belongs
|
|
143
|
+
* with the rest of them in CH-6.
|
|
144
|
+
*/
|
|
145
|
+
private compositionOffset = new Int32Array(INITIAL_CAPACITY);
|
|
146
|
+
private composition = new Float64Array(INITIAL_CAPACITY * 8);
|
|
147
|
+
private compositionUsed = 0;
|
|
148
|
+
private used = 0;
|
|
149
|
+
|
|
150
|
+
constructor(
|
|
151
|
+
private readonly substances: SubstanceRegistry,
|
|
152
|
+
private readonly species = substances.species,
|
|
153
|
+
) {}
|
|
154
|
+
|
|
155
|
+
/** Handles ever issued, live or not. A destroyed parcel still occupies its index. */
|
|
156
|
+
get count(): number {
|
|
157
|
+
return this.used;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
spawn(spec: ParcelSpec): number {
|
|
161
|
+
const { substance, mass, temperature } = spec;
|
|
162
|
+
if (!(mass > 0) || !Number.isFinite(mass)) {
|
|
163
|
+
throw new Error(`a parcel was spawned with a mass of ${mass}`);
|
|
164
|
+
}
|
|
165
|
+
const area = spec.area ?? 0;
|
|
166
|
+
if (area < 0 || !Number.isFinite(area)) {
|
|
167
|
+
throw new Error(`a parcel was spawned with a surface area of ${spec.area}`);
|
|
168
|
+
}
|
|
169
|
+
/* Resolves the substance, which refuses an index this registry never issued. */
|
|
170
|
+
const thermal = this.substances.reactionsOf(substance);
|
|
171
|
+
if (this.used === this.substance.length) this.grow();
|
|
172
|
+
|
|
173
|
+
const parcel = this.used++;
|
|
174
|
+
this.substance[parcel] = substance;
|
|
175
|
+
this.mass[parcel] = mass;
|
|
176
|
+
const count = this.substances.conductionOf(substance).geometry.count;
|
|
177
|
+
const shellMass = mass / count;
|
|
178
|
+
|
|
179
|
+
/* The composition block: every shell starts as the substance says it is made. */
|
|
180
|
+
const set = this.substances.speciesSetOf(substance);
|
|
181
|
+
const block = count * set.length;
|
|
182
|
+
while (this.compositionUsed + block > this.composition.length) {
|
|
183
|
+
const grown = new Float64Array(this.composition.length * 2);
|
|
184
|
+
grown.set(this.composition);
|
|
185
|
+
this.composition = grown;
|
|
186
|
+
}
|
|
187
|
+
this.compositionOffset[parcel] = this.compositionUsed;
|
|
188
|
+
this.composition.fill(0, this.compositionUsed, this.compositionUsed + block);
|
|
189
|
+
const source = this.substances.definitionOf(substance).composition;
|
|
190
|
+
for (let i = 0; i < source.species.length; i++) {
|
|
191
|
+
const local = this.substances.localSlotOf(substance, source.species[i] as number);
|
|
192
|
+
const kilograms = (source.fraction[i] as number) * shellMass;
|
|
193
|
+
for (let shell = 0; shell < count; shell++) {
|
|
194
|
+
this.composition[this.compositionUsed + shell * set.length + local] = kilograms;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Enthalpy from what the shell actually holds, at the temperature asked for. Written after the
|
|
199
|
+
composition rather than before, because with CH-6's model it is a function of it. */
|
|
200
|
+
for (let shell = 0; shell < count; shell++) {
|
|
201
|
+
const base = this.compositionUsed + shell * set.length;
|
|
202
|
+
let joules = 0;
|
|
203
|
+
for (let i = 0; i < set.length; i++) {
|
|
204
|
+
const kilograms = this.composition[base + i] as number;
|
|
205
|
+
if (kilograms === 0) continue;
|
|
206
|
+
joules +=
|
|
207
|
+
kilograms *
|
|
208
|
+
sensibleEnthalpy(thermal.cpA[i] as number, thermal.cpB[i] as number, temperature);
|
|
209
|
+
}
|
|
210
|
+
this.enthalpy[parcel * MAX_SHELLS + shell] = joules;
|
|
211
|
+
}
|
|
212
|
+
this.compositionUsed += block;
|
|
213
|
+
this.area[parcel] = area;
|
|
214
|
+
this.x[parcel] = spec.x ?? 0;
|
|
215
|
+
this.y[parcel] = spec.y ?? 0;
|
|
216
|
+
this.z[parcel] = spec.z ?? 0;
|
|
217
|
+
this.live[parcel] = 1;
|
|
218
|
+
this.flags[parcel] = 0;
|
|
219
|
+
this.flux[parcel] = 0;
|
|
220
|
+
this.fuelFlux[parcel] = 0;
|
|
221
|
+
this.release[parcel] = 0;
|
|
222
|
+
this.tier[parcel] = TIER_HERO;
|
|
223
|
+
this.shells[parcel] = count;
|
|
224
|
+
this.owed[parcel] = 0;
|
|
225
|
+
this.quiet[parcel] = 0;
|
|
226
|
+
return parcel;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** m², what the environment touches. Zero means the caller has not said, and conduction refuses. */
|
|
230
|
+
areaOf(parcel: number): number {
|
|
231
|
+
return this.area[this.check(parcel)] as number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
destroy(parcel: number): void {
|
|
235
|
+
this.live[this.check(parcel)] = 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
alive(parcel: number): boolean {
|
|
239
|
+
if (!Number.isInteger(parcel) || parcel < 0 || parcel >= this.used) return false;
|
|
240
|
+
return this.live[parcel] === 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
substanceOf(parcel: number): number {
|
|
244
|
+
return this.substance[this.check(parcel)] as number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
massOf(parcel: number): number {
|
|
248
|
+
return this.mass[this.check(parcel)] as number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** m³, from mass and the substance's density. Shrinks as mass leaves, which is why a log burns down. */
|
|
252
|
+
volumeOf(parcel: number): number {
|
|
253
|
+
const at = this.check(parcel);
|
|
254
|
+
return (this.mass[at] as number) / this.substances.densityOf(this.substance[at] as number);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** How many shells this parcel's substance divides it into. */
|
|
258
|
+
/**
|
|
259
|
+
* Shells resolved right now, which a level-of-detail tier may have reduced.
|
|
260
|
+
*
|
|
261
|
+
* Everything that walks a parcel's depth reads this rather than the substance's declared count, so
|
|
262
|
+
* a `Distant` log is one shell everywhere at once and nothing has to be told which tier it is at.
|
|
263
|
+
*/
|
|
264
|
+
shellCount(parcel: number): number {
|
|
265
|
+
return this.shells[this.check(parcel)] as number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** What the substance asked for, which is the resolution `Hero` restores to. */
|
|
269
|
+
declaredShellsOf(parcel: number): number {
|
|
270
|
+
return this.substances.conductionOf(this.substance[this.check(parcel)] as number).geometry
|
|
271
|
+
.count;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
tierOf(parcel: number): Tier {
|
|
275
|
+
return this.tier[this.check(parcel)] as Tier;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Ticks between updates at this parcel's tier. `§14`'s cadence column. */
|
|
279
|
+
cadenceOf(parcel: number): number {
|
|
280
|
+
return TIER_CADENCE[this.tier[this.check(parcel)] as number] as number;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** The most sub-steps a reaction solve will take at this parcel's tier. */
|
|
284
|
+
substepsOf(parcel: number): number {
|
|
285
|
+
return TIER_SUBSTEPS[this.tier[this.check(parcel)] as number] as number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Move a parcel to a tier, **conserving mass, every element and enthalpy exactly**.
|
|
290
|
+
*
|
|
291
|
+
* `§14`: "a tier change does not lose or gain mass or energy... which is the property that makes
|
|
292
|
+
* the LOD safe to apply to a burning object mid-burn." Shells are equal-mass by construction, so a
|
|
293
|
+
* fold is a proportional redistribution — target `j` takes source shells `[j·n/m, (j+1)·n/m)`,
|
|
294
|
+
* splitting one where the ratio is not an integer — and the targets come out equal-mass too.
|
|
295
|
+
*
|
|
296
|
+
* **What it loses is the profile inside a fold**, which is the point of a tier: two shells at
|
|
297
|
+
* 400 K and 300 K become one at their mass-weighted mean, and going back up hands each of them the
|
|
298
|
+
* same mean. Resolution in depth is what a distant object gives away.
|
|
299
|
+
*/
|
|
300
|
+
setTier(parcel: number, tier: Tier): void {
|
|
301
|
+
const at = this.check(parcel);
|
|
302
|
+
const declared = this.declaredShellsOf(at);
|
|
303
|
+
const wanted = Math.min(declared, TIER_SHELLS[tier] as number);
|
|
304
|
+
this.tier[at] = tier;
|
|
305
|
+
const current = this.shells[at] as number;
|
|
306
|
+
if (wanted === current) return;
|
|
307
|
+
this.refold(at, current, wanted);
|
|
308
|
+
this.shells[at] = wanted;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Redistribute a parcel's `from` shells into `to`, exactly.
|
|
313
|
+
*
|
|
314
|
+
* One pass into scratch and one pass back, because a fold reads every source before it has written
|
|
315
|
+
* any target and doing it in place would read a slot it had already overwritten.
|
|
316
|
+
*/
|
|
317
|
+
private refold(at: number, from: number, to: number): void {
|
|
318
|
+
const substance = this.substance[at] as number;
|
|
319
|
+
const width = this.substances.speciesSetOf(substance).length;
|
|
320
|
+
const start = this.compositionOffset[at] as number;
|
|
321
|
+
|
|
322
|
+
for (let j = 0; j < to; j++) {
|
|
323
|
+
const span = foldSpan(from, to, j);
|
|
324
|
+
let joules = 0;
|
|
325
|
+
for (let s = 0; s < width; s++) foldScratch[j * MAX_SUBSTANCE_SPECIES + s] = 0;
|
|
326
|
+
for (let i = 0; i < from; i++) {
|
|
327
|
+
const share = foldShare(i, span.start, span.end);
|
|
328
|
+
if (share === 0) continue;
|
|
329
|
+
for (let s = 0; s < width; s++) {
|
|
330
|
+
foldScratch[j * MAX_SUBSTANCE_SPECIES + s] =
|
|
331
|
+
(foldScratch[j * MAX_SUBSTANCE_SPECIES + s] as number) +
|
|
332
|
+
(this.composition[start + i * width + s] as number) * share;
|
|
333
|
+
}
|
|
334
|
+
joules += (this.enthalpy[at * MAX_SHELLS + i] as number) * share;
|
|
335
|
+
}
|
|
336
|
+
foldEnthalpy[j] = joules;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (let j = 0; j < to; j++) {
|
|
340
|
+
for (let s = 0; s < width; s++) {
|
|
341
|
+
this.composition[start + j * width + s] = foldScratch[
|
|
342
|
+
j * MAX_SUBSTANCE_SPECIES + s
|
|
343
|
+
] as number;
|
|
344
|
+
}
|
|
345
|
+
this.enthalpy[at * MAX_SHELLS + j] = foldEnthalpy[j] as number;
|
|
346
|
+
}
|
|
347
|
+
/* Beyond the new count nothing is read, and leaving stale values there would show up the day
|
|
348
|
+
somebody added a loop over `MAX_SHELLS` instead of over `shellCount`. */
|
|
349
|
+
for (let j = to; j < from; j++) {
|
|
350
|
+
for (let s = 0; s < width; s++) this.composition[start + j * width + s] = 0;
|
|
351
|
+
this.enthalpy[at * MAX_SHELLS + j] = 0;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Total joules held across every shell, not joules per kilogram. */
|
|
356
|
+
enthalpyOf(parcel: number): number {
|
|
357
|
+
const at = this.check(parcel);
|
|
358
|
+
const count = this.shellCount(at);
|
|
359
|
+
let total = 0;
|
|
360
|
+
for (let i = 0; i < count; i++) total += this.enthalpy[at * MAX_SHELLS + i] as number;
|
|
361
|
+
return total;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
shellEnthalpyOf(parcel: number, shell: number): number {
|
|
365
|
+
return this.enthalpy[
|
|
366
|
+
this.check(parcel) * MAX_SHELLS + this.checkShell(parcel, shell)
|
|
367
|
+
] as number;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The temperature of one shell, K. Shell 0 is the surface and the last is the core.
|
|
372
|
+
*
|
|
373
|
+
* **`surfaceTemperatureOf` is what decides ignition and `coreTemperatureOf` is what decides
|
|
374
|
+
* doneness**, and the gap between them is the whole reason shells exist.
|
|
375
|
+
*/
|
|
376
|
+
shellTemperatureOf(parcel: number, shell: number): number {
|
|
377
|
+
const at = this.check(parcel);
|
|
378
|
+
const index = this.checkShell(at, shell);
|
|
379
|
+
const substance = this.substance[at] as number;
|
|
380
|
+
const thermal = this.substances.reactionsOf(substance);
|
|
381
|
+
const width = this.substances.speciesSetOf(substance).length;
|
|
382
|
+
const base = (this.compositionOffset[at] as number) + index * width;
|
|
383
|
+
let A = 0;
|
|
384
|
+
let B = 0;
|
|
385
|
+
for (let i = 0; i < width; i++) {
|
|
386
|
+
const kilograms = this.composition[base + i] as number;
|
|
387
|
+
if (kilograms === 0) continue;
|
|
388
|
+
A += kilograms * (thermal.cpA[i] as number);
|
|
389
|
+
B += kilograms * (thermal.cpB[i] as number);
|
|
390
|
+
}
|
|
391
|
+
return temperatureFromCapacity(A, B, this.enthalpy[at * MAX_SHELLS + index] as number);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
surfaceTemperatureOf(parcel: number): number {
|
|
395
|
+
return this.shellTemperatureOf(parcel, 0);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
coreTemperatureOf(parcel: number): number {
|
|
399
|
+
return this.shellTemperatureOf(parcel, this.shellCount(parcel) - 1);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
positionX(parcel: number): number {
|
|
403
|
+
return this.x[this.check(parcel)] as number;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
positionY(parcel: number): number {
|
|
407
|
+
return this.y[this.check(parcel)] as number;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
positionZ(parcel: number): number {
|
|
411
|
+
return this.z[this.check(parcel)] as number;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
move(parcel: number, x: number, y: number, z: number): void {
|
|
415
|
+
const at = this.check(parcel);
|
|
416
|
+
this.x[at] = x;
|
|
417
|
+
this.y[at] = y;
|
|
418
|
+
this.z[at] = z;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The temperature this parcel would have if it were stirred, K.
|
|
423
|
+
*
|
|
424
|
+
* A read rather than a stored field, and that is the design. Nothing writes a temperature except
|
|
425
|
+
* `setTemperature`, which writes the enthalpy that produces one — so a parcel on a plateau simply
|
|
426
|
+
* does not move, and no code anywhere had to know a plateau exists.
|
|
427
|
+
*
|
|
428
|
+
* **The temperature of the total enthalpy**, not the mean of the shell temperatures. The two
|
|
429
|
+
* differ wherever the curve bends, and only this one has the property that matters: heat in,
|
|
430
|
+
* temperature out, with nothing lost between them.
|
|
431
|
+
*/
|
|
432
|
+
temperatureOf(parcel: number): number {
|
|
433
|
+
const at = this.check(parcel);
|
|
434
|
+
const substance = this.substance[at] as number;
|
|
435
|
+
const thermal = this.substances.reactionsOf(substance);
|
|
436
|
+
const width = this.substances.speciesSetOf(substance).length;
|
|
437
|
+
const count = this.shellCount(at);
|
|
438
|
+
const start = this.compositionOffset[at] as number;
|
|
439
|
+
let A = 0;
|
|
440
|
+
let B = 0;
|
|
441
|
+
for (let shell = 0; shell < count; shell++) {
|
|
442
|
+
const base = start + shell * width;
|
|
443
|
+
for (let i = 0; i < width; i++) {
|
|
444
|
+
const kilograms = this.composition[base + i] as number;
|
|
445
|
+
if (kilograms === 0) continue;
|
|
446
|
+
A += kilograms * (thermal.cpA[i] as number);
|
|
447
|
+
B += kilograms * (thermal.cpB[i] as number);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return temperatureFromCapacity(A, B, this.enthalpyOf(at));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Joules in, spread evenly through the depth. Negative cools. A microwave, or a stirred pot. */
|
|
454
|
+
addHeat(parcel: number, joules: number): void {
|
|
455
|
+
const at = this.check(parcel);
|
|
456
|
+
this.wake(at);
|
|
457
|
+
const count = this.shellCount(at);
|
|
458
|
+
const share = joules / count;
|
|
459
|
+
for (let i = 0; i < count; i++) {
|
|
460
|
+
this.enthalpy[at * MAX_SHELLS + i] = (this.enthalpy[at * MAX_SHELLS + i] as number) + share;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Joules into shell 0 alone, which is what a flux from outside actually does.
|
|
466
|
+
*
|
|
467
|
+
* Every radiative, convective and contact term in CH-5 lands here. The difference between this
|
|
468
|
+
* and `addHeat` is the difference between a log in front of a fire and a log in an oven.
|
|
469
|
+
*/
|
|
470
|
+
addSurfaceHeat(parcel: number, joules: number): void {
|
|
471
|
+
const at = this.check(parcel);
|
|
472
|
+
this.wake(at);
|
|
473
|
+
this.enthalpy[at * MAX_SHELLS] = (this.enthalpy[at * MAX_SHELLS] as number) + joules;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Conduct heat inward one step of `dt` seconds.
|
|
478
|
+
*
|
|
479
|
+
* Conserves this parcel's total enthalpy exactly — the arithmetic is in `conduction.ts` and so is
|
|
480
|
+
* the reason.
|
|
481
|
+
*/
|
|
482
|
+
conduct(parcel: number, dt: number): void {
|
|
483
|
+
const at = this.check(parcel);
|
|
484
|
+
const substance = this.substance[at] as number;
|
|
485
|
+
/* The geometry for the shells this parcel is *currently* resolved to, which its tier decides. */
|
|
486
|
+
const model = this.substances.conductionAt(substance, this.shells[at] as number);
|
|
487
|
+
if (model.geometry.count < 2) return;
|
|
488
|
+
if (model.conductivity === null) {
|
|
489
|
+
throw new Error(
|
|
490
|
+
`substance "${this.substances.idOf(substance)}" has no conductivity, so it cannot conduct`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
const area = this.area[at] as number;
|
|
494
|
+
if (!(area > 0)) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`parcel ${parcel} has no surface area, and a characteristic depth is volume over area`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const volume = (this.mass[at] as number) / this.substances.densityOf(substance);
|
|
500
|
+
const depth = (model.geometry.depthFactor * volume) / area;
|
|
501
|
+
conductShells(
|
|
502
|
+
this.enthalpy,
|
|
503
|
+
at * MAX_SHELLS,
|
|
504
|
+
model,
|
|
505
|
+
this.substances.reactionsOf(substance),
|
|
506
|
+
this.composition,
|
|
507
|
+
this.compositionOffset[at] as number,
|
|
508
|
+
(this.mass[at] as number) / model.geometry.count,
|
|
509
|
+
area,
|
|
510
|
+
depth,
|
|
511
|
+
dt,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Force a temperature by writing the enthalpy that produces it.
|
|
517
|
+
*
|
|
518
|
+
* For authoring and for tests. **Not for a tick**: it discards whatever enthalpy the parcel held,
|
|
519
|
+
* so a boiling parcel set to its own boiling point loses however much of the plateau it had
|
|
520
|
+
* crossed, and the energy budget does not close across the call.
|
|
521
|
+
*/
|
|
522
|
+
setTemperature(parcel: number, temperature: number): void {
|
|
523
|
+
const at = this.check(parcel);
|
|
524
|
+
const count = this.shellCount(at);
|
|
525
|
+
for (let i = 0; i < count; i++) this.setShellTemperature(at, i, temperature);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Force one shell's temperature. Authoring and tests only, for the reason above. */
|
|
529
|
+
setShellTemperature(parcel: number, shell: number, temperature: number): void {
|
|
530
|
+
const at = this.check(parcel);
|
|
531
|
+
const index = this.checkShell(at, shell);
|
|
532
|
+
const substance = this.substance[at] as number;
|
|
533
|
+
const thermal = this.substances.reactionsOf(substance);
|
|
534
|
+
const width = this.substances.speciesSetOf(substance).length;
|
|
535
|
+
const base = (this.compositionOffset[at] as number) + index * width;
|
|
536
|
+
let joules = 0;
|
|
537
|
+
for (let i = 0; i < width; i++) {
|
|
538
|
+
const kilograms = this.composition[base + i] as number;
|
|
539
|
+
if (kilograms === 0) continue;
|
|
540
|
+
joules +=
|
|
541
|
+
kilograms *
|
|
542
|
+
sensibleEnthalpy(thermal.cpA[i] as number, thermal.cpB[i] as number, temperature);
|
|
543
|
+
}
|
|
544
|
+
this.enthalpy[at * MAX_SHELLS + index] = joules;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Kilograms of one species in one shell. Zero where the substance cannot hold it at all. */
|
|
548
|
+
speciesMassOf(parcel: number, shell: number, species: number): number {
|
|
549
|
+
const at = this.check(parcel);
|
|
550
|
+
const index = this.checkShell(at, shell);
|
|
551
|
+
const substance = this.substance[at] as number;
|
|
552
|
+
const local = this.substances.localSlotOf(substance, species);
|
|
553
|
+
if (local < 0) return 0;
|
|
554
|
+
const width = this.substances.speciesSetOf(substance).length;
|
|
555
|
+
return this.composition[
|
|
556
|
+
(this.compositionOffset[at] as number) + index * width + local
|
|
557
|
+
] as number;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** The same, summed over every shell. */
|
|
561
|
+
parcelSpeciesMass(parcel: number, species: number): number {
|
|
562
|
+
const at = this.check(parcel);
|
|
563
|
+
const count = this.shellCount(at);
|
|
564
|
+
let total = 0;
|
|
565
|
+
for (let shell = 0; shell < count; shell++) total += this.speciesMassOf(at, shell, species);
|
|
566
|
+
return total;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Kilograms in one shell, summed over its species. Invariant under reaction. */
|
|
570
|
+
shellMassOf(parcel: number, shell: number): number {
|
|
571
|
+
const at = this.check(parcel);
|
|
572
|
+
const index = this.checkShell(at, shell);
|
|
573
|
+
const substance = this.substance[at] as number;
|
|
574
|
+
const width = this.substances.speciesSetOf(substance).length;
|
|
575
|
+
const start = (this.compositionOffset[at] as number) + index * width;
|
|
576
|
+
let total = 0;
|
|
577
|
+
for (let i = 0; i < width; i++) total += this.composition[start + i] as number;
|
|
578
|
+
return total;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Moles of each element this parcel holds. Writes into `out`, allocates nothing. */
|
|
582
|
+
elementTotalsOf(parcel: number, out: Float64Array): void {
|
|
583
|
+
if (out.length !== ELEMENT_COUNT) {
|
|
584
|
+
throw new Error(`a totals vector is ${ELEMENT_COUNT} wide, not ${out.length}`);
|
|
585
|
+
}
|
|
586
|
+
const at = this.check(parcel);
|
|
587
|
+
const substance = this.substance[at] as number;
|
|
588
|
+
const set = this.substances.speciesSetOf(substance);
|
|
589
|
+
const model = this.substances.reactionsOf(substance);
|
|
590
|
+
const count = this.shellCount(at);
|
|
591
|
+
const start = this.compositionOffset[at] as number;
|
|
592
|
+
out.fill(0);
|
|
593
|
+
for (let shell = 0; shell < count; shell++) {
|
|
594
|
+
for (let i = 0; i < set.length; i++) {
|
|
595
|
+
const kilograms = this.composition[start + shell * set.length + i] as number;
|
|
596
|
+
if (kilograms === 0) continue;
|
|
597
|
+
this.species.elements.addScaledRow(
|
|
598
|
+
set[i] as number,
|
|
599
|
+
kilograms / (model.molarMass[i] as number),
|
|
600
|
+
out,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Energy locked in this parcel's chemical bonds, J.
|
|
608
|
+
*
|
|
609
|
+
* Neither this nor the thermal enthalpy is conserved on its own — a reaction moves energy between
|
|
610
|
+
* them. **Their sum is**, exactly, because the same number is added to one and taken from the
|
|
611
|
+
* other in `reactShell`.
|
|
612
|
+
*/
|
|
613
|
+
chemicalEnergyOf(parcel: number): number {
|
|
614
|
+
const at = this.check(parcel);
|
|
615
|
+
const substance = this.substance[at] as number;
|
|
616
|
+
const set = this.substances.speciesSetOf(substance);
|
|
617
|
+
const model = this.substances.reactionsOf(substance);
|
|
618
|
+
const count = this.shellCount(at);
|
|
619
|
+
const start = this.compositionOffset[at] as number;
|
|
620
|
+
let total = 0;
|
|
621
|
+
for (let shell = 0; shell < count; shell++) {
|
|
622
|
+
for (let i = 0; i < set.length; i++) {
|
|
623
|
+
const kilograms = this.composition[start + shell * set.length + i] as number;
|
|
624
|
+
if (kilograms === 0) continue;
|
|
625
|
+
total +=
|
|
626
|
+
(kilograms / (model.molarMass[i] as number)) *
|
|
627
|
+
this.species.formationEnthalpyOf(set[i] as number);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return total;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Run this parcel's reactions in every shell for `dt` seconds. */
|
|
634
|
+
react(parcel: number, dt: number, transportFactor = 1): void {
|
|
635
|
+
const at = this.check(parcel);
|
|
636
|
+
const substance = this.substance[at] as number;
|
|
637
|
+
const model = this.substances.reactionsOf(substance);
|
|
638
|
+
if (model.count === 0) {
|
|
639
|
+
this.release[at] = 0;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const count = this.shellCount(at);
|
|
643
|
+
/* Bracketing the reaction step is the only place heat release can honestly be read: outside it,
|
|
644
|
+
conduction and a radiative source have moved the same joules in the same tick. */
|
|
645
|
+
const before = this.thermalEnthalpy(at, count);
|
|
646
|
+
const width = this.substances.speciesSetOf(substance).length;
|
|
647
|
+
const start = this.compositionOffset[at] as number;
|
|
648
|
+
const shellMass = (this.mass[at] as number) / count;
|
|
649
|
+
for (let shell = 0; shell < count; shell++) {
|
|
650
|
+
reactShell(
|
|
651
|
+
this.composition,
|
|
652
|
+
start + shell * width,
|
|
653
|
+
this.enthalpy,
|
|
654
|
+
at * MAX_SHELLS + shell,
|
|
655
|
+
model,
|
|
656
|
+
shellMass,
|
|
657
|
+
dt,
|
|
658
|
+
transportFactor,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
this.release[at] = dt > 0 ? (this.thermalEnthalpy(at, count) - before) / dt : 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Joules of sensible heat across a parcel's shells. One pass over at most sixteen doubles. */
|
|
665
|
+
private thermalEnthalpy(at: number, count: number): number {
|
|
666
|
+
let total = 0;
|
|
667
|
+
for (let shell = 0; shell < count; shell++) {
|
|
668
|
+
total += this.enthalpy[at * MAX_SHELLS + shell] as number;
|
|
669
|
+
}
|
|
670
|
+
return total;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Whether this parcel is being skipped entirely.
|
|
675
|
+
*
|
|
676
|
+
* `§14`'s thermal quiescence, and the vocabulary is deliberately Track B's. A cold stone floor is a
|
|
677
|
+
* thousand of these and costs a bounded scan.
|
|
678
|
+
*/
|
|
679
|
+
sleeping(parcel: number): boolean {
|
|
680
|
+
return ((this.flags[this.check(parcel)] as number) & FLAG_SLEEPING) !== 0;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Wake it, and reset the count that would put it back.
|
|
685
|
+
*
|
|
686
|
+
* **Called by everything that could change what a parcel is**: heat arriving, water poured on,
|
|
687
|
+
* mass added, a contact, or a source coming within range. Cheap enough — a flag and an integer —
|
|
688
|
+
* that a caller never has to decide whether it is worth it.
|
|
689
|
+
*/
|
|
690
|
+
wake(parcel: number): void {
|
|
691
|
+
const at = this.check(parcel);
|
|
692
|
+
this.flags[at] = (this.flags[at] as number) & ~FLAG_SLEEPING;
|
|
693
|
+
this.quiet[at] = 0;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Decide whether this parcel is still enough to sleep, and put it to sleep if it has been for long
|
|
698
|
+
* enough. Returns whether it is now asleep.
|
|
699
|
+
*
|
|
700
|
+
* **A burning or smouldering parcel never sleeps, whatever its numbers say.** A steady flame is a
|
|
701
|
+
* *steady* parcel by every measure here, and sleeping one would put out a fire by optimising it.
|
|
702
|
+
*/
|
|
703
|
+
settle(parcel: number): boolean {
|
|
704
|
+
const at = this.check(parcel);
|
|
705
|
+
if (((this.flags[at] as number) & (FLAG_BURNING | FLAG_SMOULDERING)) !== 0) {
|
|
706
|
+
this.quiet[at] = 0;
|
|
707
|
+
return false;
|
|
708
|
+
}
|
|
709
|
+
const still =
|
|
710
|
+
(this.flux[at] as number) < QUIET_FLUX &&
|
|
711
|
+
Math.abs(this.release[at] as number) < QUIET_RELEASE;
|
|
712
|
+
if (!still) {
|
|
713
|
+
this.quiet[at] = 0;
|
|
714
|
+
this.flags[at] = (this.flags[at] as number) & ~FLAG_SLEEPING;
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
const count = (this.quiet[at] as number) + 1;
|
|
718
|
+
this.quiet[at] = count;
|
|
719
|
+
if (count < QUIET_TICKS) return false;
|
|
720
|
+
this.flags[at] = (this.flags[at] as number) | FLAG_SLEEPING;
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Seconds this parcel is owed by its cadence, and whether its turn has come.
|
|
726
|
+
*
|
|
727
|
+
* A `Far` parcel is stepped every fourth tick with four ticks of `dt`, so the same seconds are
|
|
728
|
+
* integrated either way — which is what makes a cadence conserving for free rather than by care.
|
|
729
|
+
*/
|
|
730
|
+
accrue(parcel: number, dt: number): number {
|
|
731
|
+
const at = this.check(parcel);
|
|
732
|
+
const owed = (this.owed[at] as number) + dt;
|
|
733
|
+
const cadence = TIER_CADENCE[this.tier[at] as number] as number;
|
|
734
|
+
if (cadence <= 1 || owed >= dt * cadence * 0.999999) {
|
|
735
|
+
this.owed[at] = 0;
|
|
736
|
+
return owed;
|
|
737
|
+
}
|
|
738
|
+
this.owed[at] = owed;
|
|
739
|
+
return 0;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Whether a flame stands over this parcel. */
|
|
743
|
+
burning(parcel: number): boolean {
|
|
744
|
+
return ((this.flags[this.check(parcel)] as number) & FLAG_BURNING) !== 0;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** Whether its char is glowing, which needs no flame and far less oxygen. */
|
|
748
|
+
smouldering(parcel: number): boolean {
|
|
749
|
+
return ((this.flags[this.check(parcel)] as number) & FLAG_SMOULDERING) !== 0;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** Whether a pilot is present this tick. */
|
|
753
|
+
piloted(parcel: number): boolean {
|
|
754
|
+
return ((this.flags[this.check(parcel)] as number) & FLAG_PILOT) !== 0;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Supply a pilot for one tick. **This does not start a fire**, and `§11` is why.
|
|
759
|
+
*
|
|
760
|
+
* Ignition is five conditions, and a pilot is one of them. Holding a match to wet wood does
|
|
761
|
+
* exactly nothing here, which is correct and is the API telling the truth about what a match is.
|
|
762
|
+
*/
|
|
763
|
+
ignite(parcel: number): void {
|
|
764
|
+
this.wake(parcel);
|
|
765
|
+
this.flags[this.check(parcel)] = (this.flags[parcel] as number) | FLAG_PILOT;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** Put the flame out. Embers survive, which is the point — see `§12`. */
|
|
769
|
+
douse(parcel: number): void {
|
|
770
|
+
this.flags[this.check(parcel)] = (this.flags[parcel] as number) & ~FLAG_BURNING;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
setBurning(parcel: number, burning: boolean): void {
|
|
774
|
+
const at = this.check(parcel);
|
|
775
|
+
this.flags[at] = burning
|
|
776
|
+
? (this.flags[at] as number) | FLAG_BURNING
|
|
777
|
+
: (this.flags[at] as number) & ~FLAG_BURNING;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
setSmouldering(parcel: number, smouldering: boolean): void {
|
|
781
|
+
const at = this.check(parcel);
|
|
782
|
+
this.flags[at] = smouldering
|
|
783
|
+
? (this.flags[at] as number) | FLAG_SMOULDERING
|
|
784
|
+
: (this.flags[at] as number) & ~FLAG_SMOULDERING;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
clearPilot(parcel: number): void {
|
|
788
|
+
this.flags[this.check(parcel)] = (this.flags[parcel] as number) & ~FLAG_PILOT;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** kg/(m²·s) of gas leaving the surface, all of it. */
|
|
792
|
+
massFluxOf(parcel: number): number {
|
|
793
|
+
return this.flux[this.check(parcel)] as number;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* And of the part of it that can burn — which is the criterion ignition turns on.
|
|
798
|
+
*
|
|
799
|
+
* The two differ by exactly what a wet log gives off. Steam leaving a surface counts toward
|
|
800
|
+
* diluting the mixture above it and not toward feeding it, which is why 60% moisture stops a log
|
|
801
|
+
* catching in front of a fire that lights a seasoned one in eighty seconds.
|
|
802
|
+
*/
|
|
803
|
+
fuelFluxOf(parcel: number): number {
|
|
804
|
+
return this.fuelFlux[this.check(parcel)] as number;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
setMassFlux(parcel: number, total: number, fuel: number): void {
|
|
808
|
+
const at = this.check(parcel);
|
|
809
|
+
this.flux[at] = total > 0 ? total : 0;
|
|
810
|
+
this.fuelFlux[at] = fuel > 0 ? fuel : 0;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Watts the reactions released last step: positive exothermic, negative endothermic.
|
|
815
|
+
*
|
|
816
|
+
* **A fire's size is its heat release rate**, which is what every figure in the design is quoted
|
|
817
|
+
* against and is not the temperature. A pyrolysing surface reads *negative* here while it is
|
|
818
|
+
* gasifying, which is right and is why a flame has to keep feeding heat back to sustain one.
|
|
819
|
+
*/
|
|
820
|
+
heatReleaseOf(parcel: number): number {
|
|
821
|
+
return this.release[this.check(parcel)] as number;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Kilograms of water per kilogram of dry matter — **the dry basis, which is how it is quoted**.
|
|
826
|
+
*
|
|
827
|
+
* "12% moisture content" means twelve kilograms of water per hundred of dry wood, and
|
|
828
|
+
* `wetComposition` folded that into a wet-basis 0.107 to store it. Undoing the same conversion
|
|
829
|
+
* here is what makes a script comparing against the 0.25 that decides whether a log is worth
|
|
830
|
+
* burning read the quantity the author wrote.
|
|
831
|
+
*
|
|
832
|
+
* Ice counts as water. A frozen log is not a dry one.
|
|
833
|
+
*/
|
|
834
|
+
moistureOf(parcel: number): number {
|
|
835
|
+
const at = this.check(parcel);
|
|
836
|
+
const liquid = this.species.indexOf('H2O(l)');
|
|
837
|
+
const solid = this.species.indexOf('H2O(s)');
|
|
838
|
+
let water = 0;
|
|
839
|
+
if (liquid >= 0) water += this.parcelSpeciesMass(at, liquid);
|
|
840
|
+
if (solid >= 0) water += this.parcelSpeciesMass(at, solid);
|
|
841
|
+
const dry = (this.mass[at] as number) - water;
|
|
842
|
+
return dry > 0 ? water / dry : 0;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* `PHASE_SOLID`, `PHASE_LIQUID`, `PHASE_GAS`, or `PHASE_MIXED` where no one of them dominates.
|
|
847
|
+
*
|
|
848
|
+
* Mixed is a real answer rather than a failure to decide: wet wood is wood and water at once, and
|
|
849
|
+
* calling it either would be a lie a consumer would then draw.
|
|
850
|
+
*/
|
|
851
|
+
parcelPhaseOf(parcel: number): number {
|
|
852
|
+
const at = this.check(parcel);
|
|
853
|
+
const substance = this.substance[at] as number;
|
|
854
|
+
const set = this.substances.speciesSetOf(substance);
|
|
855
|
+
const count = this.shellCount(at);
|
|
856
|
+
const start = this.compositionOffset[at] as number;
|
|
857
|
+
let total = 0;
|
|
858
|
+
let solid = 0;
|
|
859
|
+
let liquid = 0;
|
|
860
|
+
let gas = 0;
|
|
861
|
+
for (let shell = 0; shell < count; shell++) {
|
|
862
|
+
for (let i = 0; i < set.length; i++) {
|
|
863
|
+
const kilograms = this.composition[start + shell * set.length + i] as number;
|
|
864
|
+
if (kilograms === 0) continue;
|
|
865
|
+
total += kilograms;
|
|
866
|
+
const phase = this.species.phaseOf(set[i] as number);
|
|
867
|
+
if (phase === PHASE_SOLID) solid += kilograms;
|
|
868
|
+
else if (phase === PHASE_LIQUID) liquid += kilograms;
|
|
869
|
+
else gas += kilograms;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (!(total > 0)) return PHASE_MIXED;
|
|
873
|
+
const dominant = PHASE_DOMINANT * total;
|
|
874
|
+
if (solid >= dominant) return PHASE_SOLID;
|
|
875
|
+
if (liquid >= dominant) return PHASE_LIQUID;
|
|
876
|
+
if (gas >= dominant) return PHASE_GAS;
|
|
877
|
+
return PHASE_MIXED;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Kilograms of **free** water on the surface, per square metre of it.
|
|
882
|
+
*
|
|
883
|
+
* **Free, meaning beyond what the material holds as its own moisture**, and that distinction is
|
|
884
|
+
* the whole reading. A seasoned oak log carries 12% water on a dry basis and is not glossy; a log
|
|
885
|
+
* somebody threw a bucket over is. Reporting the shell's total water would make the two identical
|
|
886
|
+
* and every piece of wood in a scene look rained on.
|
|
887
|
+
*
|
|
888
|
+
* **Derived rather than a column**, because `wet` puts real water in the surface shell rather than
|
|
889
|
+
* setting a flag — so `§17`'s wet-film roughness reads a quantity the boiling reaction is
|
|
890
|
+
* simultaneously consuming, which is what makes drying visible.
|
|
891
|
+
*/
|
|
892
|
+
wetnessOf(parcel: number): number {
|
|
893
|
+
const at = this.check(parcel);
|
|
894
|
+
const area = this.area[at] as number;
|
|
895
|
+
if (!(area > 0)) return 0;
|
|
896
|
+
const water = this.species.indexOf('H2O(l)');
|
|
897
|
+
if (water < 0) return 0;
|
|
898
|
+
|
|
899
|
+
/*
|
|
900
|
+
* Measured **on a dry basis against the shell's own dry matter**, which is the one formulation
|
|
901
|
+
* that is stable under pouring water on it. Against a fraction of the shell's *total* mass, the
|
|
902
|
+
* baseline would grow with every drop added and a bucket would read as a third of itself.
|
|
903
|
+
*/
|
|
904
|
+
const substance = this.substance[at] as number;
|
|
905
|
+
const composition = this.substances.definitionOf(substance).composition;
|
|
906
|
+
let wetBasis = 0;
|
|
907
|
+
for (let i = 0; i < composition.species.length; i++) {
|
|
908
|
+
if (composition.species[i] === water) wetBasis = composition.fraction[i] as number;
|
|
909
|
+
}
|
|
910
|
+
if (!(wetBasis < 1)) return 0;
|
|
911
|
+
const held = this.speciesMassOf(at, 0, water);
|
|
912
|
+
const dry = this.shellMassOf(at, 0) - held;
|
|
913
|
+
const authored = dry * (wetBasis / (1 - wetBasis));
|
|
914
|
+
const free = held - authored;
|
|
915
|
+
return free > 0 ? free / area : 0;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Pour water on it: liquid water into the surface shell, where the boiling reaction will find it.
|
|
920
|
+
*
|
|
921
|
+
* **Not a flag, and that is the whole point.** The heat sink `§12` describes is the same
|
|
922
|
+
* 2.44 MJ/kg every other drop of water in this model carries, so a doused fire goes out for the
|
|
923
|
+
* reason a real one does and a light sprinkling on a hot ember does not.
|
|
924
|
+
*
|
|
925
|
+
* Refused, naming the substance, where that substance cannot hold water at all — which means it
|
|
926
|
+
* declares no reaction that touches it. Silently doing nothing would make "I poured a bucket on
|
|
927
|
+
* it" and "this material has no water in its model" indistinguishable.
|
|
928
|
+
*/
|
|
929
|
+
wet(parcel: number, kilograms: number): void {
|
|
930
|
+
const at = this.check(parcel);
|
|
931
|
+
this.wake(at);
|
|
932
|
+
if (!(kilograms > 0)) return;
|
|
933
|
+
const water = this.species.indexOf('H2O(l)');
|
|
934
|
+
const substance = this.substance[at] as number;
|
|
935
|
+
if (water < 0 || this.substances.localSlotOf(substance, water) < 0) {
|
|
936
|
+
throw new Error(
|
|
937
|
+
`parcel ${parcel} is ${this.substances.idOf(substance)}, which holds no liquid water — ` +
|
|
938
|
+
'declare a reaction that touches `H2O(l)`, such as `boil-water`, to make it wettable',
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
this.addSpeciesMass(at, 0, water, kilograms);
|
|
942
|
+
this.mass[at] = (this.mass[at] as number) + kilograms;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Take surface water away: a cloth, a hot dry pan, or wind. Never below what is there. */
|
|
946
|
+
dry(parcel: number, kilograms: number): void {
|
|
947
|
+
const at = this.check(parcel);
|
|
948
|
+
if (!(kilograms > 0)) return;
|
|
949
|
+
const water = this.species.indexOf('H2O(l)');
|
|
950
|
+
if (water < 0 || this.substances.localSlotOf(this.substance[at] as number, water) < 0) return;
|
|
951
|
+
const held = this.speciesMassOf(at, 0, water);
|
|
952
|
+
const taken = kilograms < held ? kilograms : held;
|
|
953
|
+
this.addSpeciesMass(at, 0, water, -taken);
|
|
954
|
+
this.mass[at] = (this.mass[at] as number) - taken;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Metres of char measured inward from the surface.
|
|
959
|
+
*
|
|
960
|
+
* The shells that are mostly carbon, summed by their own thickness. A shell counts once it is past
|
|
961
|
+
* half char, which is a threshold on a continuum and is said out loud: a finer answer needs a char
|
|
962
|
+
* *front* within a shell, which is a sub-shell quantity this model does not carry.
|
|
963
|
+
*/
|
|
964
|
+
charDepthOf(parcel: number): number {
|
|
965
|
+
const at = this.check(parcel);
|
|
966
|
+
const char = this.species.indexOf('C(char)');
|
|
967
|
+
if (char < 0) return 0;
|
|
968
|
+
const count = this.shellCount(at);
|
|
969
|
+
let depth = 0;
|
|
970
|
+
for (let shell = 0; shell < count; shell++) {
|
|
971
|
+
const mass = this.shellMassOf(at, shell);
|
|
972
|
+
if (!(mass > 0)) continue;
|
|
973
|
+
if (this.speciesMassOf(at, shell, char) / mass < CHAR_SHELL_FRACTION) break;
|
|
974
|
+
depth += this.shellDepthOf(at, shell);
|
|
975
|
+
}
|
|
976
|
+
return depth;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* How much of this parcel is still load-bearing, 1 down to 0.
|
|
981
|
+
*
|
|
982
|
+
* **`§18` refuses structural failure in writing and this is the refusal reporting.** A beam whose
|
|
983
|
+
* char depth has eaten a fraction of its thickness has lost that fraction of its strength, and
|
|
984
|
+
* what happens next — a joint releasing, a body splitting, a building coming down — belongs to
|
|
985
|
+
* whoever owns the solver. This package writes the scalar, emits an event at a threshold, and
|
|
986
|
+
* breaks nothing.
|
|
987
|
+
*/
|
|
988
|
+
structuralIntegrityOf(parcel: number): number {
|
|
989
|
+
const at = this.check(parcel);
|
|
990
|
+
const count = this.shellCount(at);
|
|
991
|
+
let thickness = 0;
|
|
992
|
+
for (let shell = 0; shell < count; shell++) thickness += this.shellDepthOf(at, shell);
|
|
993
|
+
if (!(thickness > 0)) return 1;
|
|
994
|
+
const left = 1 - this.charDepthOf(at) / thickness;
|
|
995
|
+
return left > 0 ? (left < 1 ? left : 1) : 0;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Pour `from` into `into`; `from` is consumed. Mass, elements and enthalpy all conserved exactly.
|
|
1000
|
+
*
|
|
1001
|
+
* **Both must be the same substance, and two that are not are refused naming both.** Two
|
|
1002
|
+
* substances are two species sets and two shell counts, so mixing them would silently discard
|
|
1003
|
+
* whichever set was narrower — and a stew is not a substance change. What a consumer combining two
|
|
1004
|
+
* genuinely different materials wants is a third substance that is the mixture, authored as one.
|
|
1005
|
+
*/
|
|
1006
|
+
mix(from: number, into: number): void {
|
|
1007
|
+
const a = this.check(from);
|
|
1008
|
+
const b = this.check(into);
|
|
1009
|
+
if (a === b) return;
|
|
1010
|
+
const substance = this.substance[a] as number;
|
|
1011
|
+
if (substance !== (this.substance[b] as number)) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`cannot mix ${this.substances.idOf(substance)} into ` +
|
|
1014
|
+
`${this.substances.idOf(this.substance[b] as number)}: two substances are two species ` +
|
|
1015
|
+
'sets, and a mixture of them is a third substance rather than either',
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
const set = this.substances.speciesSetOf(substance);
|
|
1019
|
+
const countA = this.shellCount(a);
|
|
1020
|
+
const countB = this.shellCount(b);
|
|
1021
|
+
const startA = this.compositionOffset[a] as number;
|
|
1022
|
+
const startB = this.compositionOffset[b] as number;
|
|
1023
|
+
|
|
1024
|
+
/* Spread over the receiver's shells in proportion to what each already holds, so a hot core
|
|
1025
|
+
stays hotter than a cold surface rather than being averaged flat. */
|
|
1026
|
+
const massB = this.mass[b] as number;
|
|
1027
|
+
for (let shell = 0; shell < countB; shell++) {
|
|
1028
|
+
const share = massB > 0 ? this.shellMassOf(b, shell) / massB : 1 / countB;
|
|
1029
|
+
for (let i = 0; i < set.length; i++) {
|
|
1030
|
+
let incoming = 0;
|
|
1031
|
+
for (let s = 0; s < countA; s++)
|
|
1032
|
+
incoming += this.composition[startA + s * set.length + i] as number;
|
|
1033
|
+
this.composition[startB + shell * set.length + i] =
|
|
1034
|
+
(this.composition[startB + shell * set.length + i] as number) + incoming * share;
|
|
1035
|
+
}
|
|
1036
|
+
let joules = 0;
|
|
1037
|
+
for (let s = 0; s < countA; s++) joules += this.enthalpy[a * MAX_SHELLS + s] as number;
|
|
1038
|
+
this.enthalpy[b * MAX_SHELLS + shell] =
|
|
1039
|
+
(this.enthalpy[b * MAX_SHELLS + shell] as number) + joules * share;
|
|
1040
|
+
}
|
|
1041
|
+
this.mass[b] = massB + (this.mass[a] as number);
|
|
1042
|
+
this.destroy(a);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Joules into one named shell.
|
|
1047
|
+
*
|
|
1048
|
+
* The difference between this and `addSurfaceHeat` is a microwave and an oven: one deposits where
|
|
1049
|
+
* the field penetrates to, the other at the surface. Nothing here decides which is physical.
|
|
1050
|
+
*/
|
|
1051
|
+
addShellHeat(parcel: number, shell: number, joules: number): void {
|
|
1052
|
+
const at = this.check(parcel);
|
|
1053
|
+
this.wake(at);
|
|
1054
|
+
const index = this.checkShell(at, shell);
|
|
1055
|
+
this.enthalpy[at * MAX_SHELLS + index] =
|
|
1056
|
+
(this.enthalpy[at * MAX_SHELLS + index] as number) + joules;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* What fraction of its original volume is left, as mass leaves it.
|
|
1061
|
+
*
|
|
1062
|
+
* **The number a shrinking log is drawn from.** A parcel's volume is its mass over its substance's
|
|
1063
|
+
* density, and the density does not change — so this is simply what share of the mass is still
|
|
1064
|
+
* here, which falls as volatiles leave and rises for nothing.
|
|
1065
|
+
*/
|
|
1066
|
+
volumeShareOf(parcel: number): number {
|
|
1067
|
+
const at = this.check(parcel);
|
|
1068
|
+
let held = 0;
|
|
1069
|
+
const count = this.shellCount(at);
|
|
1070
|
+
for (let shell = 0; shell < count; shell++) held += this.shellMassOf(at, shell);
|
|
1071
|
+
const original = this.mass[at] as number;
|
|
1072
|
+
return original > 0 ? held / original : 0;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Joules per kelvin of the surface shell, from what it is made of right now.
|
|
1077
|
+
*
|
|
1078
|
+
* **What every explicit surface term has to be bounded against.** A flux times an area times a
|
|
1079
|
+
* step is joules, and joules into a small enough shell is an arbitrarily large temperature change
|
|
1080
|
+
* — so a term that does not know this number cannot tell the difference between heating something
|
|
1081
|
+
* and destroying it. `ChemistryWorld` uses it to stop a radiative or convective step carrying a
|
|
1082
|
+
* surface past the thing driving it.
|
|
1083
|
+
*/
|
|
1084
|
+
surfaceHeatCapacityOf(parcel: number): number {
|
|
1085
|
+
const at = this.check(parcel);
|
|
1086
|
+
const substance = this.substance[at] as number;
|
|
1087
|
+
const set = this.substances.speciesSetOf(substance);
|
|
1088
|
+
const model = this.substances.reactionsOf(substance);
|
|
1089
|
+
const start = this.compositionOffset[at] as number;
|
|
1090
|
+
const temperature = this.shellTemperatureOf(at, 0);
|
|
1091
|
+
let capacity = 0;
|
|
1092
|
+
for (let i = 0; i < set.length; i++) {
|
|
1093
|
+
const kilograms = this.composition[start + i] as number;
|
|
1094
|
+
if (kilograms === 0) continue;
|
|
1095
|
+
capacity +=
|
|
1096
|
+
kilograms * ((model.cpA[i] as number) + (model.cpB[i] as number) * (temperature - 273.15));
|
|
1097
|
+
}
|
|
1098
|
+
return capacity;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** What this parcel's substance looks like, or `null` where nobody said. */
|
|
1102
|
+
appearanceOf(parcel: number): AppearanceModel | null {
|
|
1103
|
+
return this.substances.appearanceOf(this.substance[this.check(parcel)] as number);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/** Share of this parcel's mass that is char, 0..1. */
|
|
1107
|
+
charFractionOf(parcel: number): number {
|
|
1108
|
+
const at = this.check(parcel);
|
|
1109
|
+
const char = this.species.indexOf('C(char)');
|
|
1110
|
+
if (char < 0) return 0;
|
|
1111
|
+
const mass = this.mass[at] as number;
|
|
1112
|
+
return mass > 0 ? this.parcelSpeciesMass(at, char) / mass : 0;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/** Add (or with a negative value, remove) mass of one species in one shell. */
|
|
1116
|
+
addSpeciesMass(parcel: number, shell: number, species: number, kilograms: number): void {
|
|
1117
|
+
const at = this.check(parcel);
|
|
1118
|
+
this.wake(at);
|
|
1119
|
+
const index = this.checkShell(at, shell);
|
|
1120
|
+
const substance = this.substance[at] as number;
|
|
1121
|
+
const local = this.substances.localSlotOf(substance, species);
|
|
1122
|
+
if (local < 0) {
|
|
1123
|
+
throw new Error(
|
|
1124
|
+
`parcel ${parcel} is ${this.substances.idOf(substance)}, which cannot hold ` +
|
|
1125
|
+
`"${this.species.idOf(species)}"`,
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
const width = this.substances.speciesSetOf(substance).length;
|
|
1129
|
+
const slot = (this.compositionOffset[at] as number) + index * width + local;
|
|
1130
|
+
const next = (this.composition[slot] as number) + kilograms;
|
|
1131
|
+
this.composition[slot] = next > 0 ? next : 0;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/** How many substances this store's registry holds. `fingerprintChemistry` covers all of them. */
|
|
1135
|
+
get substanceCount(): number {
|
|
1136
|
+
return this.substances.count;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* A number standing for what a registered substance *is*, for the fingerprint.
|
|
1141
|
+
*
|
|
1142
|
+
* Its density, porosity, emissivity, shell count and the species it can hold — enough that two
|
|
1143
|
+
* libraries differing anywhere disagree here, and cheap enough to compute once a tick. `§15` asks
|
|
1144
|
+
* for the registry to be covered because two worlds with different substances are different
|
|
1145
|
+
* worlds, and a replay crossing them should say so rather than diverge later.
|
|
1146
|
+
*/
|
|
1147
|
+
substanceDigestOf(substance: number): number {
|
|
1148
|
+
const set = this.substances.speciesSetOf(substance);
|
|
1149
|
+
let digest =
|
|
1150
|
+
this.substances.densityOf(substance) +
|
|
1151
|
+
this.substances.porosityOf(substance) * 7 +
|
|
1152
|
+
this.substances.emissivityOf(substance) * 13 +
|
|
1153
|
+
set.length * 31;
|
|
1154
|
+
for (let i = 0; i < set.length; i++) digest += (set[i] as number) * (i + 1);
|
|
1155
|
+
return digest;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/** Global species indices this parcel's substance can hold. */
|
|
1159
|
+
speciesSetOf(substance: number): Int32Array {
|
|
1160
|
+
return this.substances.speciesSetOf(substance);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
idOfSpecies(species: number): string {
|
|
1164
|
+
return this.species.idOf(species);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
phaseOf(species: number): number {
|
|
1168
|
+
return this.species.phaseOf(species);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
emissivityOf(parcel: number): number {
|
|
1172
|
+
return this.substances.emissivityOf(this.substance[this.check(parcel)] as number);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/** When this parcel's substance catches, or `null` where nobody said it does. */
|
|
1176
|
+
ignitionOf(substance: number): IgnitionModel | null {
|
|
1177
|
+
return this.substances.ignitionOf(substance);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
porosityOf(parcel: number): number {
|
|
1181
|
+
return this.substances.porosityOf(this.substance[this.check(parcel)] as number);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/** W/(m·K) at a temperature, or zero where the substance never said. */
|
|
1185
|
+
conductivityOf(parcel: number, temperature: number): number {
|
|
1186
|
+
const model = this.substances.conductionOf(this.substance[this.check(parcel)] as number);
|
|
1187
|
+
const k = model.conductivity;
|
|
1188
|
+
if (k === null) return 0;
|
|
1189
|
+
const value = k.k0 + k.k1 * (temperature - 298.15);
|
|
1190
|
+
return value > 0 ? value : 0;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/** Metres from the outside of shell `shell` to the inside of it. */
|
|
1194
|
+
shellDepthOf(parcel: number, shell: number): number {
|
|
1195
|
+
const at = this.check(parcel);
|
|
1196
|
+
const index = this.checkShell(at, shell);
|
|
1197
|
+
const substance = this.substance[at] as number;
|
|
1198
|
+
/* At the shells this parcel is currently resolved to: a `Distant` log is one thick shell, and a
|
|
1199
|
+
depth read against the declared four would be a quarter of the real thing. */
|
|
1200
|
+
const model = this.substances.conductionAt(substance, this.shells[at] as number);
|
|
1201
|
+
const area = this.area[at] as number;
|
|
1202
|
+
if (!(area > 0)) return 0;
|
|
1203
|
+
const volume = (this.mass[at] as number) / this.substances.densityOf(substance);
|
|
1204
|
+
const depth = (model.geometry.depthFactor * volume) / area;
|
|
1205
|
+
return depth * (model.geometry.thicknessFraction[index] as number);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
private checkShell(parcel: number, shell: number): number {
|
|
1209
|
+
const count = this.shellCount(parcel);
|
|
1210
|
+
if (!Number.isInteger(shell) || shell < 0 || shell >= count) {
|
|
1211
|
+
throw new Error(`shell ${shell} is outside a parcel of ${count}`);
|
|
1212
|
+
}
|
|
1213
|
+
return shell;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
private grow(): void {
|
|
1217
|
+
const size = this.substance.length * 2;
|
|
1218
|
+
const substance = new Int32Array(size);
|
|
1219
|
+
substance.set(this.substance);
|
|
1220
|
+
this.substance = substance;
|
|
1221
|
+
/* Explicitly `Float64Array<ArrayBuffer>` rather than inferred: TypeScript 7 parameterises a
|
|
1222
|
+
typed array by its buffer, and an unannotated parameter widens to `ArrayBufferLike` — which
|
|
1223
|
+
includes `SharedArrayBuffer` and so will not assign back to the fields. */
|
|
1224
|
+
const grow64 = (from: Float64Array<ArrayBuffer>): Float64Array<ArrayBuffer> => {
|
|
1225
|
+
const to = new Float64Array(size);
|
|
1226
|
+
to.set(from);
|
|
1227
|
+
return to;
|
|
1228
|
+
};
|
|
1229
|
+
this.mass = grow64(this.mass);
|
|
1230
|
+
this.area = grow64(this.area);
|
|
1231
|
+
const enthalpy = new Float64Array(size * MAX_SHELLS);
|
|
1232
|
+
enthalpy.set(this.enthalpy);
|
|
1233
|
+
this.enthalpy = enthalpy;
|
|
1234
|
+
this.x = grow64(this.x);
|
|
1235
|
+
this.y = grow64(this.y);
|
|
1236
|
+
this.z = grow64(this.z);
|
|
1237
|
+
const live = new Uint8Array(size);
|
|
1238
|
+
live.set(this.live);
|
|
1239
|
+
this.live = live;
|
|
1240
|
+
const flags = new Uint8Array(size);
|
|
1241
|
+
flags.set(this.flags);
|
|
1242
|
+
this.flags = flags;
|
|
1243
|
+
this.flux = grow64(this.flux);
|
|
1244
|
+
this.fuelFlux = grow64(this.fuelFlux);
|
|
1245
|
+
this.release = grow64(this.release);
|
|
1246
|
+
this.owed = grow64(this.owed);
|
|
1247
|
+
const tier = new Uint8Array(size);
|
|
1248
|
+
tier.set(this.tier);
|
|
1249
|
+
this.tier = tier;
|
|
1250
|
+
const shells = new Int32Array(size);
|
|
1251
|
+
shells.set(this.shells);
|
|
1252
|
+
this.shells = shells;
|
|
1253
|
+
const quiet = new Int32Array(size);
|
|
1254
|
+
quiet.set(this.quiet);
|
|
1255
|
+
this.quiet = quiet;
|
|
1256
|
+
const offsets = new Int32Array(size);
|
|
1257
|
+
offsets.set(this.compositionOffset);
|
|
1258
|
+
this.compositionOffset = offsets;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
private check(parcel: number): number {
|
|
1262
|
+
if (!Number.isInteger(parcel) || parcel < 0 || parcel >= this.used) {
|
|
1263
|
+
throw new Error(`parcel ${parcel} is outside a store of ${this.used}`);
|
|
1264
|
+
}
|
|
1265
|
+
if (this.live[parcel] !== 1) {
|
|
1266
|
+
throw new Error(`parcel ${parcel} was destroyed`);
|
|
1267
|
+
}
|
|
1268
|
+
return parcel;
|
|
1269
|
+
}
|
|
1270
|
+
}
|