]> git.localhorst.tv Git - blobs.git/blob - src/creature/goal.cpp
72fc9a814d4eaec47708301b2c36b923c759275a
[blobs.git] / src / creature / goal.cpp
1 #include "AttackGoal.hpp"
2 #include "BlobBackgroundTask.hpp"
3 #include "Goal.hpp"
4 #include "IdleGoal.hpp"
5 #include "IngestGoal.hpp"
6 #include "LocateResourceGoal.hpp"
7 #include "StrollGoal.hpp"
8
9 #include "Creature.hpp"
10 #include "../app/Assets.hpp"
11 #include "../ui/string.hpp"
12 #include "../world/Planet.hpp"
13 #include "../world/Resource.hpp"
14 #include "../world/Simulation.hpp"
15 #include "../world/TileType.hpp"
16
17 #include <cstring>
18 #include <iostream>
19 #include <sstream>
20 #include <glm/gtx/io.hpp>
21
22
23 namespace blobs {
24 namespace creature {
25
26 AttackGoal::AttackGoal(Creature &self, Creature &target)
27 : Goal(self)
28 , target(target)
29 , damage_target(0.25)
30 , damage_dealt(0.0)
31 , cooldown(0.0) {
32 }
33
34 AttackGoal::~AttackGoal() {
35 }
36
37 std::string AttackGoal::Describe() const {
38         return "attack " + target.Name();
39 }
40
41 void AttackGoal::Tick(double dt) {
42         cooldown -= dt;
43 }
44
45 void AttackGoal::Action() {
46         if (target.Dead() || !GetCreature().PerceptionTest(target.GetSituation().Position())) {
47                 SetComplete();
48                 return;
49         }
50         const glm::dvec3 diff(GetSituation().Position() - target.GetSituation().Position());
51         const double hit_range = GetCreature().Size() * 0.5 * GetCreature().DexertyFactor();
52         const double hit_dist = hit_range + (0.5 * GetCreature().Size()) + 0.5 * (target.Size());
53         if (GetStats().Damage().Critical()) {
54                 // flee
55                 GetSteering().Pass(diff * 5.0);
56                 GetSteering().DontSeparate();
57                 GetSteering().Haste(1.0);
58         } else if (glm::length2(diff) > hit_dist * hit_dist) {
59                 // full throttle chase
60                 GetSteering().Pass(target.GetSituation().Position());
61                 GetSteering().DontSeparate();
62                 GetSteering().Haste(1.0);
63         } else {
64                 // attack
65                 GetSteering().Halt();
66                 GetSteering().DontSeparate();
67                 GetSteering().Haste(1.0);
68                 if (cooldown <= 0.0) {
69                         constexpr double impulse = 0.05;
70                         const double force = GetCreature().Strength();
71                         const double damage =
72                                 force * impulse
73                                 * (GetCreature().GetComposition().TotalDensity() / target.GetComposition().TotalDensity())
74                                 * (GetCreature().Mass() / target.Mass())
75                                 / target.Mass();
76                         GetCreature().DoWork(force * impulse * glm::length(diff));
77                         target.Hurt(damage);
78                         target.GetSituation().Accelerate(glm::normalize(diff) * force * -impulse);
79                         damage_dealt += damage;
80                         if (damage_dealt >= damage_target || target.Dead()) {
81                                 SetComplete();
82                                 if (target.Dead()) {
83                                         GetCreature().GetSimulation().Log() << GetCreature().Name()
84                                                 << " killed " << target.Name() << std::endl;
85                                 }
86                         }
87                         cooldown = 1.0 + (4.0 * (1.0 - GetCreature().DexertyFactor()));
88                 }
89         }
90 }
91
92 void AttackGoal::OnBackground() {
93         // abort if something more important comes up
94         SetComplete();
95 }
96
97
98 BlobBackgroundTask::BlobBackgroundTask(Creature &c)
99 : Goal(c)
100 , breathing(false)
101 , drink_subtask(nullptr)
102 , eat_subtask(nullptr) {
103 }
104
105 BlobBackgroundTask::~BlobBackgroundTask() {
106 }
107
108 std::string BlobBackgroundTask::Describe() const {
109         return "being a blob";
110 }
111
112 void BlobBackgroundTask::Tick(double dt) {
113         if (breathing) {
114                 // TODO: derive breathing ability
115                 int gas = Assets().data.resources["air"].id;
116                 // TODO: check if in compatible atmosphere
117                 double amount = GetStats().Breath().gain * -(1.0 + GetCreature().ExhaustionFactor());
118                 GetStats().Breath().Add(amount * dt);
119                 // maintain ~1% gas composition
120                 double gas_amount = GetCreature().GetComposition().Get(gas);
121                 if (gas_amount < GetCreature().GetComposition().TotalMass() * 0.01) {
122                         double add = std::min(GetCreature().GetComposition().TotalMass() * 0.025 - gas_amount, -amount * dt);
123                         GetCreature().Ingest(gas, add);
124                 }
125                 if (GetStats().Breath().Empty()) {
126                         breathing = false;
127                 }
128         }
129 }
130
131 void BlobBackgroundTask::Action() {
132         CheckStats();
133         CheckSplit();
134         CheckMutate();
135 }
136
137 void BlobBackgroundTask::CheckStats() {
138         Creature::Stats &stats = GetStats();
139
140         if (!breathing && stats.Breath().Bad()) {
141                 breathing = true;
142         }
143
144         if (!drink_subtask && stats.Thirst().Bad()) {
145                 drink_subtask = new IngestGoal(GetCreature(), stats.Thirst());
146                 for (const auto &cmp : GetCreature().GetComposition()) {
147                         if (Assets().data.resources[cmp.resource].state == world::Resource::LIQUID) {
148                                 double value = cmp.value / GetCreature().GetComposition().TotalMass();
149                                 drink_subtask->Accept(cmp.resource, value);
150                                 for (const auto &compat : Assets().data.resources[cmp.resource].compatibility) {
151                                         if (Assets().data.resources[compat.first].state == world::Resource::LIQUID) {
152                                                 drink_subtask->Accept(compat.first, value * compat.second);
153                                         }
154                                 }
155                         }
156                 }
157                 drink_subtask->WhenComplete([&](Goal &) { drink_subtask = nullptr; });
158                 GetCreature().AddGoal(std::unique_ptr<Goal>(drink_subtask));
159         }
160
161         if (!eat_subtask && stats.Hunger().Bad()) {
162                 eat_subtask = new IngestGoal(GetCreature(), stats.Hunger());
163                 for (const auto &cmp : GetCreature().GetComposition()) {
164                         if (Assets().data.resources[cmp.resource].state == world::Resource::SOLID) {
165                                 double value = cmp.value / GetCreature().GetComposition().TotalMass();
166                                 eat_subtask->Accept(cmp.resource, value);
167                                 for (const auto &compat : Assets().data.resources[cmp.resource].compatibility) {
168                                         if (Assets().data.resources[compat.first].state == world::Resource::SOLID) {
169                                                 eat_subtask->Accept(compat.first, value * compat.second);
170                                         }
171                                 }
172                         }
173                 }
174                 eat_subtask->WhenComplete([&](Goal &) { eat_subtask = nullptr; });
175                 GetCreature().AddGoal(std::unique_ptr<Goal>(eat_subtask));
176         }
177 }
178
179 void BlobBackgroundTask::CheckSplit() {
180         if (GetCreature().Mass() > GetCreature().OffspringMass() * 2.0
181                 && GetCreature().OffspringChance() > Random().UNorm()) {
182                 GetCreature().GetSimulation().Log() << GetCreature().Name() << " split" << std::endl;
183                 Split(GetCreature());
184                 return;
185         }
186 }
187
188 void BlobBackgroundTask::CheckMutate() {
189         // check for random property mutation
190         if (GetCreature().MutateChance() > Random().UNorm()) {
191                 double amount = 1.0 + (Random().SNorm() * 0.05);
192                 math::Distribution &d = GetCreature().GetGenome().properties.props[Random().UInt(9)];
193                 if (Random().UNorm() < 0.5) {
194                         d.Mean(d.Mean() * amount);
195                 } else {
196                         d.StandardDeviation(d.StandardDeviation() * amount);
197                 }
198         }
199 }
200
201
202 Goal::Goal(Creature &c)
203 : c(c)
204 , on_complete()
205 , on_foreground()
206 , on_background()
207 , urgency(0.0)
208 , interruptible(true)
209 , complete(false) {
210 }
211
212 Goal::~Goal() noexcept {
213 }
214
215 app::Assets &Goal::Assets() noexcept {
216         return c.GetSimulation().Assets();
217 }
218
219 const app::Assets &Goal::Assets() const noexcept {
220         return c.GetSimulation().Assets();
221 }
222
223 math::GaloisLFSR &Goal::Random() noexcept {
224         return Assets().random;
225 }
226
227 void Goal::SetComplete() {
228         if (!complete) {
229                 complete = true;
230                 OnComplete();
231                 if (on_complete) {
232                         on_complete(*this);
233                 }
234         }
235 }
236
237 void Goal::SetForeground() {
238         OnForeground();
239         if (on_foreground) {
240                 on_foreground(*this);
241         }
242 }
243
244 void Goal::SetBackground() {
245         OnBackground();
246         if (on_background) {
247                 on_background(*this);
248         }
249 }
250
251 void Goal::WhenComplete(std::function<void(Goal &)> cb) noexcept {
252         on_complete = cb;
253         if (complete) {
254                 on_complete(*this);
255         }
256 }
257
258 void Goal::WhenForeground(std::function<void(Goal &)> cb) noexcept {
259         on_foreground = cb;
260 }
261
262 void Goal::WhenBackground(std::function<void(Goal &)> cb) noexcept {
263         on_background = cb;
264 }
265
266
267 IdleGoal::IdleGoal(Creature &c)
268 : Goal(c) {
269         Urgency(-1.0);
270         Interruptible(true);
271 }
272
273 IdleGoal::~IdleGoal() {
274 }
275
276 std::string IdleGoal::Describe() const {
277         return "idle";
278 }
279
280 void IdleGoal::Action() {
281         // when in bad shape, don't make much effort
282         if (GetStats().Damage().Bad() || GetStats().Exhaustion().Bad() || GetStats().Fatigue().Critical()) {
283                 GetSteering().DontSeparate();
284         } else {
285                 GetSteering().ResumeSeparate();
286         }
287
288         // use boredom as chance per minute
289         if (Random().UNorm() < GetStats().Boredom().value * (1.0 / 3600.0)) {
290                 PickActivity();
291         }
292 }
293
294 void IdleGoal::PickActivity() {
295         GetCreature().AddGoal(std::unique_ptr<Goal>(new StrollGoal(GetCreature())));
296 }
297
298
299 namespace {
300
301 std::string summarize(const Composition &comp, const app::Assets &assets) {
302         std::stringstream s;
303         bool first = true;
304         for (const auto &c : comp) {
305                 if (first) {
306                         first = false;
307                 } else {
308                         s << " or ";
309                 }
310                 s << assets.data.resources[c.resource].label;
311         }
312         return s.str();
313 }
314
315 }
316
317 IngestGoal::IngestGoal(Creature &c, Creature::Stat &stat)
318 : Goal(c)
319 , stat(stat)
320 , accept(Assets().data.resources)
321 , locate_subtask(nullptr)
322 , ingesting(false)
323 , resource(-1)
324 , yield(0.0) {
325         Urgency(stat.value);
326 }
327
328 IngestGoal::~IngestGoal() {
329 }
330
331 void IngestGoal::Accept(int resource, double value) {
332         accept.Add(resource, value);
333 }
334
335 std::string IngestGoal::Describe() const {
336         if (resource == -1) {
337                 return "ingest " + summarize(accept, Assets());
338         } else {
339                 const world::Resource &r = Assets().data.resources[resource];
340                 if (r.state == world::Resource::SOLID) {
341                         return "eat " + r.label;
342                 } else {
343                         return "drink " + r.label;
344                 }
345         }
346 }
347
348 void IngestGoal::Enable() {
349 }
350
351 void IngestGoal::Tick(double dt) {
352         Urgency(stat.value);
353         if (locate_subtask) {
354                 locate_subtask->Urgency(Urgency() + 0.1);
355         }
356         if (ingesting) {
357                 if (OnSuitableTile() && !GetSituation().Moving()) {
358                         GetCreature().Ingest(resource, yield * GetCreature().GetComposition().Compatibility(resource) * dt);
359                         stat.Add(-1.0 * yield * dt);
360                         if (stat.Empty()) {
361                                 SetComplete();
362                         }
363                 } else {
364                         // left tile somehow, some idiot probably pushed us off
365                         ingesting = false;
366                         Interruptible(true);
367                 }
368         }
369 }
370
371 void IngestGoal::Action() {
372         if (ingesting) {
373                 // all good
374                 return;
375         }
376         if (OnSuitableTile()) {
377                 if (GetSituation().Moving()) {
378                         // break with maximum force
379                         GetSteering().Haste(1.0);
380                         GetSteering().Halt();
381                 } else {
382                         // finally
383                         // TODO: somehow this still gets interrupted
384                         Interruptible(false);
385                         ingesting = true;
386                 }
387         } else {
388                 locate_subtask = new LocateResourceGoal(GetCreature());
389                 for (const auto &c : accept) {
390                         locate_subtask->Accept(c.resource, c.value);
391                 }
392                 locate_subtask->SetMinimum(stat.gain * -1.1);
393                 locate_subtask->Urgency(Urgency() + 0.1);
394                 locate_subtask->WhenComplete([&](Goal &){ locate_subtask = nullptr; });
395                 GetCreature().AddGoal(std::unique_ptr<Goal>(locate_subtask));
396         }
397 }
398
399 bool IngestGoal::OnSuitableTile() {
400         if (!GetSituation().OnGround()) {
401                 return false;
402         }
403         const world::TileType &t = GetSituation().GetTileType();
404         auto found = t.FindBestResource(accept);
405         if (found != t.resources.end()) {
406                 resource = found->resource;
407                 yield = found->ubiquity;
408                 return true;
409         } else {
410                 resource = -1;
411                 return false;
412         }
413 }
414
415
416 LocateResourceGoal::LocateResourceGoal(Creature &c)
417 : Goal(c)
418 , accept(Assets().data.resources)
419 , found(false)
420 , target_pos(0.0)
421 , searching(false)
422 , reevaluate(0.0)
423 , minimum(0.0) {
424 }
425
426 LocateResourceGoal::~LocateResourceGoal() noexcept {
427 }
428
429 void LocateResourceGoal::Accept(int resource, double value) {
430         accept.Add(resource, value);
431 }
432
433 std::string LocateResourceGoal::Describe() const {
434         return "locate " + summarize(accept, Assets());
435 }
436
437 void LocateResourceGoal::Enable() {
438
439 }
440
441 void LocateResourceGoal::Tick(double dt) {
442         reevaluate -= dt;
443 }
444
445 void LocateResourceGoal::Action() {
446         if (reevaluate < 0.0) {
447                 LocateResource();
448                 reevaluate = 3.0;
449         } else if (!found) {
450                 if (!searching) {
451                         LocateResource();
452                 } else {
453                         if (OnTarget()) {
454                                 searching = false;
455                                 LocateResource();
456                         } else {
457                                 GetSteering().GoTo(target_pos);
458                         }
459                 }
460         } else if (OnTarget()) {
461                 GetSteering().Halt();
462                 if (!GetSituation().Moving()) {
463                         SetComplete();
464                 }
465         } else {
466                 GetSteering().GoTo(target_pos);
467         }
468         GetSteering().Haste(Urgency());
469 }
470
471 void LocateResourceGoal::LocateResource() {
472         if (GetSituation().OnSurface()) {
473                 const world::TileType &t = GetSituation().GetTileType();
474                 auto yield = t.FindBestResource(accept);
475                 if (yield != t.resources.cend()) {
476                         // hoooray
477                         GetSteering().Halt();
478                         found = true;
479                         searching = false;
480                         target_pos = GetSituation().Position();
481                 } else {
482                         // go find somewhere else
483                         SearchVicinity();
484                         if (!found) {
485                                 Remember();
486                                 if (!found) {
487                                         RandomWalk();
488                                 }
489                         }
490                 }
491         } else {
492                 // well, what now?
493                 found = false;
494                 searching = false;
495         }
496 }
497
498 void LocateResourceGoal::SearchVicinity() {
499         const world::Planet &planet = GetSituation().GetPlanet();
500         const glm::dvec3 &pos = GetSituation().Position();
501         const glm::dvec3 normal(planet.NormalAt(pos));
502         const glm::dvec3 step_x(glm::normalize(glm::cross(normal, glm::dvec3(normal.z, normal.x, normal.y))) * (GetCreature().PerceptionOmniRange() * 0.7));
503         const glm::dvec3 step_y(glm::normalize(glm::cross(step_x, normal)) * (GetCreature().PerceptionOmniRange() * 0.7));
504
505         const int search_radius = int(GetCreature().PerceptionRange() / (GetCreature().PerceptionOmniRange() * 0.7));
506         double rating[2 * search_radius + 1][2 * search_radius + 1];
507         std::memset(rating, '\0', (2 * search_radius + 1) * (2 * search_radius + 1) * sizeof(double));
508
509         // find close and rich field
510         for (int y = -search_radius; y < search_radius + 1; ++y) {
511                 for (int x = -search_radius; x < search_radius + 1; ++x) {
512                         const glm::dvec3 tpos(pos + (double(x) * step_x) + (double(y) * step_y));
513                         if (!GetCreature().PerceptionTest(tpos)) continue;
514                         const world::TileType &type = planet.TileTypeAt(tpos);
515                         auto yield = type.FindBestResource(accept);
516                         if (yield != type.resources.cend()) {
517                                 rating[y + search_radius][x + search_radius] = yield->ubiquity * accept.Get(yield->resource);
518                                 // penalize distance
519                                 double dist = std::max(0.125, 0.25 * glm::length2(tpos - pos));
520                                 rating[y + search_radius][x + search_radius] /= dist;
521                         }
522                 }
523         }
524
525         // penalize crowding
526         for (auto &c : planet.Creatures()) {
527                 if (&*c == &GetCreature()) continue;
528                 for (int y = -search_radius; y < search_radius + 1; ++y) {
529                         for (int x = -search_radius; x < search_radius + 1; ++x) {
530                                 const glm::dvec3 tpos(pos + (double(x) * step_x) + (double(y) * step_y));
531                                 if (glm::length2(tpos - c->GetSituation().Position()) < 1.0) {
532                                         rating[y + search_radius][x + search_radius] *= 0.8;
533                                 }
534                         }
535                 }
536         }
537
538         glm::ivec2 best_pos(0);
539         double best_rating = -1.0;
540
541         for (int y = -search_radius; y < search_radius + 1; ++y) {
542                 for (int x = -search_radius; x < search_radius + 1; ++x) {
543                         if (rating[y + search_radius][x + search_radius] > best_rating) {
544                                 best_pos = glm::ivec2(x, y);
545                                 best_rating = rating[y + search_radius][x + search_radius];
546                         }
547                 }
548         }
549
550         if (best_rating > minimum) {
551                 found = true;
552                 searching = false;
553                 target_pos = glm::normalize(pos + (double(best_pos.x) * step_x) + (double(best_pos.y) * step_y)) * planet.Radius();
554                 GetSteering().GoTo(target_pos);
555         }
556 }
557
558 void LocateResourceGoal::Remember() {
559         glm::dvec3 pos(0.0);
560         if (GetCreature().GetMemory().RememberLocation(accept, pos)) {
561                 found = true;
562                 searching = false;
563                 target_pos = pos;
564                 GetSteering().GoTo(target_pos);
565         }
566 }
567
568 void LocateResourceGoal::RandomWalk() {
569         if (searching) {
570                 return;
571         }
572
573         const world::Planet &planet = GetSituation().GetPlanet();
574         const glm::dvec3 &pos = GetSituation().Position();
575         const glm::dvec3 normal(planet.NormalAt(pos));
576         const glm::dvec3 step_x(glm::normalize(glm::cross(normal, glm::dvec3(normal.z, normal.x, normal.y))));
577         const glm::dvec3 step_y(glm::normalize(glm::cross(step_x, normal)));
578
579         found = false;
580         searching = true;
581         target_pos = GetSituation().Position();
582         target_pos += Random().SNorm() * 3.0 * step_x;
583         target_pos += Random().SNorm() * 3.0 * step_y;
584         // bias towards current heading
585         target_pos += GetSituation().Heading() * 1.5;
586         target_pos = glm::normalize(target_pos) * planet.Radius();
587         GetSteering().GoTo(target_pos);
588 }
589
590 bool LocateResourceGoal::OnTarget() const noexcept {
591         const Situation &s = GetSituation();
592         return s.OnGround() && glm::length2(s.Position() - target_pos) < 0.0001;
593 }
594
595
596 StrollGoal::StrollGoal(Creature &c)
597 : Goal(c)
598 , last(GetSituation().Position())
599 , next(last) {
600 }
601
602 StrollGoal::~StrollGoal() {
603 }
604
605 std::string StrollGoal::Describe() const {
606         return "take a walk";
607 }
608
609 void StrollGoal::Enable() {
610         last = GetSituation().Position();
611         GetSteering().Haste(0.0);
612         PickTarget();
613 }
614
615 void StrollGoal::Action() {
616         if (glm::length2(next - GetSituation().Position()) < 0.0001) {
617                 PickTarget();
618         }
619 }
620
621 void StrollGoal::OnBackground() {
622         SetComplete();
623 }
624
625 void StrollGoal::PickTarget() noexcept {
626         last = next;
627         next += GetSituation().Heading() * 1.5;
628         const glm::dvec3 normal(GetSituation().GetPlanet().NormalAt(GetSituation().Position()));
629         glm::dvec3 rand_x(GetSituation().Heading());
630         if (std::abs(glm::dot(normal, rand_x)) > 0.999) {
631                 rand_x = glm::dvec3(normal.z, normal.x, normal.y);
632         }
633         glm::dvec3 rand_y = glm::cross(normal, rand_x);
634         next += ((rand_x * Random().SNorm()) + (rand_y * Random().SNorm())) * 1.5;
635         next = glm::normalize(next) * GetSituation().GetPlanet().Radius();
636         GetSteering().GoTo(next);
637 }
638
639 }
640 }