1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
//! Fire, smoke and explosions, as particles.
//!
//! A *plume* is a stream of particles that spawn around a base point, travel away from it and
//! fade out on the way: the shape shared by a campfire, the smoke above it, a rocket's exhaust
//! and the trail from a damaged engine. [`Fire`] and [`Smoke`] are the two this module ships.
//! An [`Explosion`] is the other thing particles do: not a stream but a burst, everything it has
//! thrown out at once and gone in a quarter of a second. None of them cost anything but code —
//! no sprites, no map, no assets of any kind.
//!
//! They are sized at compile time and never allocate: an effect owns a fixed-capacity buffer of
//! particles, six bytes each. A full-size [`Fire`] holds 260 of them (about 1.5 KiB), a
//! [`SmokingFire`] twice that, and an [`Explosion`] 50.
//!
//! They are not free, though, and the bill lands in `draw`: every particle is one filled circle,
//! costing about 0.05% of the draw budget. A full-size [`Fire`] runs at roughly 6% of `update`
//! and 12% of `draw`, and a [`SmokingFire`] or a [`Smoke`] — twice the particles — about 10% and
//! 24%. An [`Explosion`] at its default size is 50 particles, so about 2% of `draw` for the
//! quarter-second it lasts and nothing at all of `update` — a burst carries no simulation, only
//! its age. Scale a plume down and both fall away with the particle count.
//! Budget for the ones on screen at once, and reach for a smaller `SCALE` before giving up on
//! the effect.
//!
//! Those are budget figures, which count the cart's side of each circle and not the console's
//! rasterizing of it — see [`Context::cpu_draw`]. Particle radii are small, so the two track
//! each other closely here, but on a slow device trust [`Context::fps`] over the percentages.
//!
//! Everything here stays in this module — a cart's `use pixel8::*;` does not reach it, so name
//! what the effect needs:
//!
//! ```no_run
//! use pixel8::{plume::SmokingFire, *};
//!
//! struct Camp {
//! fire: SmokingFire,
//! }
//!
//! impl Game for Camp {
//! fn update(&mut self, ctx: &mut Context) {
//! self.fire.update(ctx);
//! }
//!
//! fn draw(&self, gfx: &mut Graphics) {
//! gfx.clear(Color::BLACK);
//! self.fire.draw(gfx);
//! }
//! }
//! ```
//!
//! # Size
//!
//! The `SCALE` parameter sizes a plume. [`FULL_SCALE`] is a fire about 30 pixels tall, a `2` or
//! a `3` the flame of a candle or a torch, and anything up to [`MAX_SCALE`] grows it further:
//!
//! ```no_run
//! # use pixel8::plume::Fire;
//! let candle: Fire<2> = Fire::new(64, 100);
//! let bonfire: Fire<20> = Fire::new(64, 100);
//! ```
//!
//! `SCALE` is also how many particles go in each puff, and that is the one part of a plume that
//! cannot keep shrinking: a puff holds at least one particle however small the plume is, while
//! the ground it covers shrinks with the square. So the smaller a plume gets the more crowded it
//! is — a `Fire<1>` is around twelve times as dense as a `Fire<10>` — which is what
//! [`with_puffs`](Fire::with_puffs) is for.
//!
//! # Thinning a small plume
//!
//! A plume puffs once an update by default, which at [`FULL_SCALE`] is what makes it look like
//! something billowing. Far below that the puffs land on top of each other and it reads as a
//! solid lump instead. Building the plume out of fewer, further-apart puffs fixes it:
//!
//! ```no_run
//! # use pixel8::{plume::Smoke, Direction};
//! // A cigarette: the smallest plume there is, and thinned, or it is a blob on someone's face.
//! let wisp: Smoke<1> = Smoke::new(64, 100)
//! .with_direction(Direction::UpLeft)
//! .with_puffs(8);
//! ```
//!
//! Particles still move every update, so the plume keeps the reach, pace and direction it had —
//! there is simply less in it, spaced further apart. The colors spread out with the puffs too,
//! so a thinned plume greys along its length instead of in its first pixel, and it costs
//! proportionally less to update and draw. What it does not give back is memory: the buffer is
//! sized for a puff an update whether or not the plume uses them.
//!
//! # Direction
//!
//! Plumes travel in one of eight [`Direction`]s. They rise unless told otherwise, which is what
//! a fire wants; smoke pouring from a damaged aircraft flying up-screen wants
//! [`Down`], and a plume in a crosswind one of the diagonals. Particles sway
//! from side to side as they travel, so the sway follows the direction too.
//!
//! ```no_run
//! # use pixel8::{plume::Smoke, Direction};
//! let exhaust: Smoke<4> = Smoke::new(64, 40).with_direction(Direction::Down);
//! ```
//!
//! A plume can be turned as it runs, with [`Fire::set_direction`] / [`Smoke::set_direction`].
//! Particles already in the air carry on the way they were going, so a plume that turns bends
//! rather than swinging around all at once.
// Everything this section documents — `blown_by`, the `Wind` it takes — exists only with the
// `physics` feature, and it links to all of it. So the section is gated too, or a cart that
// linked the plumes alone would document itself with links that resolve to nothing. Doc
// attributes render in source order, so it stays where it reads.
//! # Starting and stopping
//!
//! [`Fire::set_puffing`] / [`Smoke::set_puffing`] turn the source off and on. A plume that has
//! stopped is not a plume that has vanished: it keeps everything it has already let go of, and
//! that carries on rising, greying and thinning out until it ages away. So the plume empties over
//! a `LIFETIME` from the base up, which is how a real one goes out.
//!
//! Simply not drawing it would blink the whole thing away instead — the difference between a
//! cigarette between draws and one that stops existing:
//!
//! ```no_run
//! # use pixel8::{plume::Smoke, *};
//! # struct Smoker { smoke: Smoke<1>, inhaling: bool }
//! impl Smoker {
//! fn update(&mut self, ctx: &mut Context) {
//! // Nothing new off the cigarette while it is at their lips; the last of it drifts off.
//! self.smoke.set_puffing(!self.inhaling);
//! self.smoke.update(ctx);
//! }
//! }
//! ```
//!
//! # Trails
//!
//! A plume is not pinned to where it started: [`Fire::move_to`] and [`Smoke::move_to`] move the
//! point it spawns from. Particles already in the air keep the base they came off, so moving a
//! plume trails it rather than dragging everything it has emitted along — which is what makes a
//! plume a trail. That damaged aircraft is a `Smoke` moved to the plane every frame:
//!
//! ```no_run
//! # use pixel8::{plume::Smoke, *};
//! struct Plane {
//! x: i16,
//! y: i16,
//! smoke: Smoke<4>,
//! }
//!
//! impl Plane {
//! fn update(&mut self, ctx: &mut Context) {
//! self.y -= 1;
//! // Move first, and this frame's puff comes off where the plane is now.
//! self.smoke.move_to(self.x, self.y);
//! self.smoke.update(ctx);
//! }
//! }
//! ```
//!
//! # Smoke from fire
//!
//! [`Smoke`] on its own is a plume that starts wherever it is placed. A fire that *turns into*
//! smoke is a different thing: its particles have to keep the position and the sway they had as
//! flames, or the two effects read as unrelated. That is what [`SmokingFire`] is — one plume
//! whose particles live twice as long, spending the second half of their life grey and drifting
//! at half the pace.
//!
//! # Bursts
//!
//! An [`Explosion`] is the same particles gone the other way about. There is no source to point,
//! to move or to stop: the sparks are thrown by the first [`update`](Explosion::update), each on
//! its own heading and at its own pace, and from then on the burst only thins. When
//! [`finished`](Explosion::finished) says so there is nothing left of it, which is a cart's cue
//! to drop it:
//!
//! ```no_run
//! # use pixel8::{plume::Explosion, *};
//! # struct Mine { blast: Option<Explosion> }
//! impl Mine {
//! fn update(&mut self, ctx: &mut Context) {
//! let Some(blast) = &mut self.blast else { return };
//!
//! blast.update(ctx);
//! if blast.finished() {
//! self.blast = None;
//! }
//! }
//! }
//! ```
//!
//! A spark costs what any other particle costs to draw and nothing at all to update: it carries a
//! heading rather than a position, so where it is now is that heading times its age, and ageing
//! the whole burst is one addition.
//!
//! [`Context::cpu_draw`]: crate::Context::cpu_draw
//! [`Context::fps`]: crate::Context::fps
//! [`Direction`]: crate::Direction
//! [`Down`]: crate::Direction::Down
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use MAX_WIND_SPEED;