banished_game/behaviour/character/actions.rs
1//! The character actions (`Action`): what each one does when it starts, each update, and when it
2//! stops or is aborted, and dropping off what a citizen carries.
3
4use super::*;
5
6/// One task.
7#[derive(Debug, Clone)]
8pub enum Action {
9 /// `MoveTo`: walk to a target.
10 MoveTo(Box<MoveTo>),
11 // vtable 0x14015ea70
12 /// `PlayAnimation`: play `anim` for `duration`.
13 PlayAnimation {
14 /// The clip.
15 anim: usize,
16 /// How long it plays, in microseconds.
17 duration: i64,
18 /// The time played so far.
19 elapsed: i64,
20 },
21 // vtable 0x1401662c0
22 /// `PerformWork`: face `facing` (if within ±180), play `anim` for `duration`.
23 PerformWork {
24 /// The heading to face, in degrees.
25 facing: f32,
26 /// The work clip.
27 anim: usize,
28 /// How long the work takes, in microseconds.
29 duration: i64,
30 /// The time worked so far.
31 elapsed: i64,
32 // +0x40; +0xac
33 /// Decrement the citizen's counter on start.
34 consume: bool,
35 },
36 // vtable 0x1401664e0
37 /// `SendComponentMessage`: deliver a message on start.
38 SendMessage(Message),
39 // vtable 0x140165f80
40 /// `CompleteRoad`: the cell becomes its road (or ground) on start.
41 ///
42 /// Original: `FUN_1400caea0`.
43 CompleteRoad {
44 /// The cell.
45 cell: i32,
46 /// The cell type it gets.
47 ty: u8,
48 /// The road kind built there.
49 built: u8,
50 },
51 /// Reserve at construction; consume on start.
52 ///
53 /// Original: `FUN_1400cb880`, `FUN_1400cb980`.
54 ConsumeStoredRawMaterial {
55 /// The storage.
56 storage: usize,
57 /// What it uses: `(raw material, count)`.
58 items: Vec<(usize, i32)>,
59 /// Whether the goods were reserved.
60 reserved: bool,
61 },
62 /// Reserve at construction; transfer on start.
63 ///
64 /// Original: `FUN_1400cf9f0`, `FUN_1400cf700`.
65 PickupStoredRawMaterial {
66 /// The storage.
67 storage: usize,
68 /// What it takes: `(raw material, count)`.
69 items: Vec<(usize, i32)>,
70 /// Whether it may take less than asked.
71 partial: bool,
72 },
73 // vtable 0x14015ead0
74 /// `ShowTool`: attach tool mesh `n` (0..10), the carried goods (−2), or nothing (−1). The town
75 /// draws it on the citizen's `tool` node (`StartingTown::model_instances`).
76 ///
77 /// Original: `FUN_140043070`, `FUN_14011c880`.
78 ShowTool(i32),
79 // vtable 0x140165fe0
80 /// `CompleteTask`: tell the job manager the task is done or abandoned.
81 ///
82 /// Original: `FUN_1400cb270`, `FUN_1400cb010`; abandoned `FUN_1400caf90`.
83 CompleteTask {
84 /// The work's id.
85 work: i32,
86 /// The task's parameter.
87 param: i32,
88 },
89 // vtable 0x140165f20
90 /// `AnimateNaturalResource`: play the resource's animation `anim`; with `wait`, run until it
91 /// ends.
92 ///
93 /// Original: `FUN_1400caca0`, `FUN_1400caad0`, `FUN_1400caa80`.
94 AnimateResource {
95 /// The resource.
96 entity: usize,
97 /// The clip.
98 anim: usize,
99 /// Whether the action waits until the clip ends.
100 wait: bool,
101 },
102 // vtable 0x140166470
103 /// `RemoveNaturalResource`: remove the resource and leave its pile; abort releases the claim
104 /// (flag `0x100000`).
105 ///
106 /// Original: `FUN_1400d02c0`, `FUN_1400cffe0`, `FUN_1400cff70`.
107 RemoveResource {
108 /// The resource.
109 entity: usize,
110 /// The removal flag passed to the resource.
111 flag: bool,
112 },
113 // vtable 0x140166330
114 /// `PickupRawMaterial`: take the whole pile into the carried storage and destroy it.
115 ///
116 /// Original: `FUN_1400cf040`, `FUN_1400ceda0`.
117 PickupPile {
118 /// The pile.
119 entity: usize,
120 },
121 // vtable 0x140166130
122 /// `CreateRawMaterial`: leave the output as piles near `cell`, `batch` weight each (the
123 /// worker's capacity).
124 ///
125 /// Original: `FUN_1400cc670`, `FUN_1400cc010`.
126 CreateRawMaterial {
127 /// The template of the output piles.
128 template: usize,
129 /// The cell they are left near.
130 cell: i32,
131 /// The weight of each pile.
132 batch: i32,
133 /// The workplace that made them.
134 workplace: Option<usize>,
135 // +0x4c
136 /// The yield is scaled by this (1 for production; a dock's share of the fish).
137 factor: f32,
138 },
139 // vtable 0x140166200
140 /// `HuntAnimal`, attached to a hunter's walk or wait: running until its notify (run by the
141 /// citizens' update) has started an attack.
142 ///
143 /// Original: `FUN_1400cd8d0`, `FUN_1400cd2b0`.
144 HuntAnimal {
145 /// The hunter's lodge.
146 lodge: usize,
147 /// Whether the attack has started.
148 done: bool,
149 },
150 // vtable 0x140166400
151 /// `RemoveAnimal`.
152 ///
153 /// Original: `FUN_1400cfcc0`.
154 RemoveAnimal {
155 /// The animal.
156 entity: usize,
157 },
158 // vtable 0x1401660c0
159 /// `CreateNaturalResource`: plant `template` on `cell`, which must hold no object and
160 /// `occupant` as its occupant.
161 ///
162 /// Original: `FUN_1400cbef0`.
163 CreateNaturalResource {
164 /// The resource's template.
165 template: usize,
166 /// The cell's required occupant.
167 occupant: Option<usize>,
168 /// The cell.
169 cell: i32,
170 },
171 // vtable 0x1401661a0; +0x20
172 /// `DropOffRawMaterial`: move up to `count` carried units into `storage`; with `redirect` a
173 /// storage that cannot take them sends the citizen on to another, or the goods are left on the
174 /// ground.
175 ///
176 /// Original: `FUN_1400cd1a0`, `FUN_1400cc7d0`.
177 DropOff {
178 /// The storage.
179 storage: usize,
180 /// The raw material.
181 material: usize,
182 /// The most units to move.
183 count: i32,
184 /// Whether a storage that cannot take them sends the citizen on.
185 redirect: bool,
186 },
187}
188
189/// `FUN_1400ccd90`: `n` units of `m` leave the citizen for a loose pile on the ground.
190pub(super) fn drop_pile(world: &mut World, m: usize, n: i32) {
191 if n == 0 {
192 return;
193 }
194 world.messages.push(Message {
195 component: Component::DropPile,
196 message: 0,
197 entity: None,
198 a: m as i32,
199 b: n,
200 });
201 world.carried.remove(m, n);
202}
203
204/// `FUN_1400ccaf0`: as many as carried, up to `count`, into `target` when they fit; else with
205/// `redirect`, the nearest storage that takes them (the action now points there and a walk
206/// is pushed) or, with none, a loose pile. True when the goods were left on the ground.
207pub(super) fn drop_off_one(
208 ch: &Character,
209 world: &mut World,
210 target: usize,
211 storage: &mut usize,
212 m: usize,
213 count: i32,
214 redirect: bool,
215) -> bool {
216 let carried = world.carried.available(m);
217 let n = if 0 <= count {
218 carried.min(count)
219 } else {
220 carried
221 };
222 if 0 < n && world.storages[target].fits(world.materials, m, n) {
223 world.storages[target].add(m, n);
224 world.carried.remove(m, n);
225 return false;
226 }
227 if !redirect {
228 return false;
229 }
230 let cell = world.cell(ch.position);
231 let found = match world.storage_search.as_mut() {
232 Some(search) => search(world.storages, world.rng, m, n, cell),
233 None => None,
234 };
235 match found {
236 Some((s, point)) => {
237 *storage = s;
238 let walk = MoveTo::new(point, 1, 0, crate::pathfind::WALKABLE, 0, -1.0, world.rng);
239 world.pushes.push(Action::move_to(walk));
240 false
241 }
242 None => {
243 drop_pile(world, m, n);
244 true
245 }
246 }
247}
248
249impl Action {
250 /// A `MoveTo` action.
251 pub fn move_to(m: MoveTo) -> Action {
252 Action::MoveTo(Box::new(m))
253 }
254
255 pub(super) fn start(
256 &mut self,
257 ch: &mut Character,
258 world: &mut World,
259 group: &AnimationGroup,
260 ) -> Status {
261 match self {
262 Action::MoveTo(m) => m.start(ch, world, group),
263 Action::PlayAnimation { anim, .. } => {
264 ch.play(*anim, group, 1.0);
265 Status::Running
266 }
267 Action::PerformWork {
268 facing,
269 anim,
270 consume,
271 ..
272 } => {
273 // Start (slot 4, `FUN_1400ce8f0`): the clip at speed 0 (`FUN_14010f6c0(anim,
274 // 1, 0)`), then `FUN_14010f530(FromYawPitch(facing))`.
275 ch.play(*anim, group, 0.0);
276 if (-180.0..=180.0).contains(facing) {
277 // FUN_14010f530: the direction (x, y, 0) normalized, the heading back
278 // from the vector.
279 let v = crate::math::from_yaw_pitch(*facing, 0.0);
280 ch.flag_28 = false;
281 ch.direction = crate::math::normalize3([v[0], v[1], 0.0]);
282 ch.target_heading = crate::math::heading(v[0], v[1]);
283 }
284 // `+0x40`: spend one tool use (never below zero).
285 if *consume && *world.tool_uses > 0 {
286 *world.tool_uses = (*world.tool_uses - 1).max(0);
287 }
288 Status::Running
289 }
290 Action::SendMessage(m) => {
291 world.messages.push(*m);
292 Status::Done
293 }
294 Action::AnimateResource { entity, anim, wait } => {
295 world.messages.push(Message {
296 component: Component::NaturalResource,
297 message: 2,
298 entity: Some(*entity),
299 a: *anim as i32,
300 b: 0,
301 });
302 if let Some(a) = world.animating.get_mut(*entity) {
303 *a = true;
304 }
305 if *wait {
306 Status::Running
307 } else {
308 Status::Done
309 }
310 }
311 Action::RemoveResource { entity, flag } => {
312 world.messages.push(Message {
313 component: Component::NaturalResource,
314 message: 0,
315 entity: Some(*entity),
316 a: *flag as i32,
317 b: 0,
318 });
319 Status::Done
320 }
321 Action::CompleteRoad { cell, ty, built } => {
322 world.messages.push(Message {
323 component: Component::Road,
324 message: *built as i32,
325 entity: None,
326 a: *cell,
327 b: *ty as i32,
328 });
329 Status::Done
330 }
331 Action::CreateRawMaterial {
332 template,
333 cell,
334 workplace,
335 factor,
336 ..
337 } => {
338 world.messages.push(Message {
339 component: Component::CreateRawMaterial,
340 message: factor.to_bits() as i32,
341 entity: *workplace,
342 a: *template as i32,
343 b: *cell,
344 });
345 Status::Done
346 }
347 // `FUN_1400cbb20`: the cell must be free (`FUN_1400a1fd0`: not cell types 0 or 1, no
348 // road kind `+0x2a`; a road plan always has one) and without flags. Otherwise the
349 // action fails.
350 Action::CreateNaturalResource {
351 template,
352 occupant,
353 cell,
354 } => {
355 let i = *cell as usize;
356 let free =
357 world.objects.get(i) == Some(&None) && world.occupants.get(i) == Some(occupant);
358 let ty = 1u32 << (world.types[i].0 & 0x1f);
359 if !free || ty & 3 != 0 || world.roads[i] != 0xff || world.flags[i] != 0 {
360 return Status::Failed;
361 }
362 world.messages.push(Message {
363 component: Component::CreateNaturalResource,
364 message: 0,
365 entity: None,
366 a: *template as i32,
367 b: *cell,
368 });
369 Status::Done
370 }
371 // `FUN_1400cd720`.
372 Action::HuntAnimal { done, .. } => {
373 if *done {
374 Status::Done
375 } else {
376 Status::Running
377 }
378 }
379 Action::RemoveAnimal { entity } => {
380 world.messages.push(Message {
381 component: Component::Animal,
382 message: 0,
383 entity: Some(*entity),
384 a: 0,
385 b: 0,
386 });
387 Status::Done
388 }
389 Action::PickupPile { entity } => {
390 world.messages.push(Message {
391 component: Component::Pile,
392 message: 0,
393 entity: Some(*entity),
394 a: 0,
395 b: 0,
396 });
397 Status::Done
398 }
399 Action::ShowTool(n) => {
400 // Above 10 (except −2) detaches (`FUN_14011cbb0`); otherwise attach (`FUN_14011c880`).
401 *world.shown_tool = if (*n as u32) > 10 && *n != -2 { -1 } else { *n };
402 Status::Running
403 }
404 Action::CompleteTask { work, param } => {
405 if *work >= 0 {
406 world.messages.push(Message {
407 component: Component::Jobs,
408 message: 0,
409 entity: None,
410 a: *work,
411 b: *param,
412 });
413 }
414 Status::Done
415 }
416 Action::DropOff {
417 storage,
418 material,
419 count,
420 redirect,
421 } => {
422 // `FUN_1400cc7d0`. No storage: everything carried is left on the ground
423 // (`FUN_1400cca40`).
424 if *storage == usize::MAX {
425 let items: Vec<(usize, i32)> = world
426 .carried
427 .items
428 .iter()
429 .filter(|i| 0 < i.count)
430 .map(|i| (i.material, i.count))
431 .collect();
432 for (m, n) in items {
433 drop_pile(world, m, n);
434 }
435 return Status::Running;
436 }
437 // Every material tries the storage the action was made for; a redirect points
438 // the action at the storage found.
439 let target = *storage;
440 let one = |world: &mut World, m: usize, storage: &mut usize| -> bool {
441 drop_off_one(ch, world, target, storage, m, *count, *redirect)
442 };
443 if *material != usize::MAX {
444 let m = *material;
445 return if one(world, m, storage) {
446 Status::Failed
447 } else {
448 Status::Running
449 };
450 }
451 let items: Vec<usize> = world
452 .carried
453 .items
454 .iter()
455 .filter(|i| 0 < i.count)
456 .map(|i| i.material)
457 .collect();
458 let mut dropped = false;
459 for m in items {
460 dropped |= one(world, m, storage);
461 }
462 if dropped {
463 Status::Failed
464 } else {
465 Status::Running
466 }
467 }
468 Action::ConsumeStoredRawMaterial {
469 storage,
470 items,
471 reserved,
472 } => {
473 // Storage `usize::MAX`: what the citizen carries (`FUN_1400cb880` on its own
474 // StorageComponent).
475 let target = if *storage == usize::MAX {
476 &mut *world.carried
477 } else {
478 &mut world.storages[*storage]
479 };
480 if consume_stored(target, items, *reserved) {
481 // `FUN_1400cb420`: each item's amount counts as used (`+0xc`).
482 world.used.extend(items.iter().copied());
483 Status::Done
484 } else {
485 Status::Failed
486 }
487 }
488 Action::PickupStoredRawMaterial {
489 storage,
490 items,
491 partial,
492 } => {
493 if pickup_stored(
494 &mut world.storages[*storage],
495 world.carried,
496 world.materials,
497 items,
498 *partial,
499 ) {
500 Status::Done
501 } else {
502 Status::Failed
503 }
504 }
505 }
506 }
507
508 pub(super) fn update(
509 &mut self,
510 ch: &mut Character,
511 world: &mut World,
512 group: &AnimationGroup,
513 dt: i64,
514 ) -> Status {
515 match self {
516 Action::MoveTo(m) => m.update(ch, world, group),
517 Action::PlayAnimation {
518 duration, elapsed, ..
519 } => {
520 *elapsed += dt;
521 if *elapsed < *duration {
522 Status::Running
523 } else {
524 Status::Done
525 }
526 }
527 Action::PerformWork {
528 duration, elapsed, ..
529 } => {
530 if *duration <= *elapsed {
531 // `FUN_14011bb20`: an injury fails the work.
532 return if world.injury_check() {
533 Status::Failed
534 } else {
535 Status::Done
536 };
537 }
538 *elapsed += dt;
539 Status::Running
540 }
541 Action::AnimateResource { entity, .. } => {
542 if world.animating.get(*entity).copied().unwrap_or(false) {
543 Status::Running
544 } else {
545 Status::Done
546 }
547 }
548 // The base update returns 0.
549 Action::RemoveResource { .. }
550 | Action::PickupPile { .. }
551 | Action::CreateRawMaterial { .. }
552 | Action::CompleteRoad { .. }
553 | Action::CreateNaturalResource { .. }
554 | Action::RemoveAnimal { .. } => Status::Done,
555 // `FUN_1400cd710`.
556 Action::HuntAnimal { done, .. } => {
557 if *done {
558 Status::Done
559 } else {
560 Status::Running
561 }
562 }
563 Action::SendMessage(_)
564 | Action::ConsumeStoredRawMaterial { .. }
565 | Action::PickupStoredRawMaterial { .. }
566 | Action::ShowTool(_)
567 | Action::CompleteTask { .. }
568 | Action::DropOff { .. } => Status::Done,
569 }
570 }
571
572 /// Slot `+0x38` (stop) and `+0x40` (abort): MoveTo and PerformWork return to clip 0.
573 pub(super) fn stop(&mut self, ch: &mut Character, group: &AnimationGroup) {
574 match self {
575 Action::MoveTo(m) => ch.play(m.stand, group, 1.0),
576 Action::PerformWork { .. } => ch.play(0, group, 1.0),
577 _ => {}
578 }
579 }
580
581 /// FUN_1400cb300 / FUN_1400cf140: abort releases pending reservations.
582 pub(super) fn abort(
583 &mut self,
584 ch: &mut Character,
585 group: &AnimationGroup,
586 storages: &mut [Storage],
587 ) {
588 match self {
589 Action::ConsumeStoredRawMaterial {
590 storage,
591 items,
592 reserved: true,
593 }
594 | Action::PickupStoredRawMaterial { storage, items, .. } => {
595 for &(material, count) in items.iter() {
596 storages[*storage].unreserve(material, count);
597 }
598 }
599 _ => {}
600 }
601 self.stop(ch, group);
602 }
603}