Skip to main content

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}