]> git.localhorst.tv Git - blobs.git/blob - src/creature/goal.cpp
41f91f37e0ec63fa0b6f9b0cb432a3de48346cad
[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(GetCreature().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 (GetCreature().GetStats().Damage().Critical()) {
54                 // flee
55                 GetCreature().GetSteering().Pass(diff * 5.0);
56                 GetCreature().GetSteering().DontSeparate();
57                 GetCreature().GetSteering().Haste(1.0);
58         } else if (glm::length2(diff) > hit_dist * hit_dist) {
59                 // full throttle chase
60                 GetCreature().GetSteering().Pass(target.GetSituation().Position());
61                 GetCreature().GetSteering().DontSeparate();
62                 GetCreature().GetSteering().Haste(1.0);
63         } else {
64                 // attack
65                 GetCreature().GetSteering().Halt();
66                 GetCreature().GetSteering().DontSeparate();
67                 GetCreature().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 = GetCreature().GetStats().Breath().gain * -(1.0 + GetCreature().ExhaustionFactor());
118                 GetCreature().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 (GetCreature().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 = GetCreature().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         // when in bad shape, don't make much effort
179         if (stats.Damage().Bad() || stats.Exhaustion().Bad() || stats.Fatigue().Critical()) {
180                 GetCreature().GetSteering().DontSeparate();
181         } else {
182                 GetCreature().GetSteering().ResumeSeparate();
183         }
184 }
185
186 void BlobBackgroundTask::CheckSplit() {
187         if (GetCreature().Mass() > GetCreature().OffspringMass() * 2.0
188                 && GetCreature().OffspringChance() > Random().UNorm()) {
189                 GetCreature().GetSimulation().Log() << GetCreature().Name() << " split" << std::endl;
190                 Split(GetCreature());
191                 return;
192         }
193 }
194
195 void BlobBackgroundTask::CheckMutate() {
196         // check for random property mutation
197         if (GetCreature().MutateChance() > Random().UNorm()) {
198                 double amount = 1.0 + (Random().SNorm() * 0.05);
199                 math::Distribution &d = GetCreature().GetGenome().properties.props[Random().UInt(9)];
200                 if (Random().UNorm() < 0.5) {
201                         d.Mean(d.Mean() * amount);
202                 } else {
203                         d.StandardDeviation(d.StandardDeviation() * amount);
204                 }
205         }
206 }
207
208
209 Goal::Goal(Creature &c)
210 : c(c)
211 , on_complete()
212 , on_foreground()
213 , on_background()
214 , urgency(0.0)
215 , interruptible(true)
216 , complete(false) {
217 }
218
219 Goal::~Goal() noexcept {
220 }
221
222 Situation &Goal::GetSituation() noexcept {
223         return c.GetSituation();
224 }
225
226 const Situation &Goal::GetSituation() const noexcept {
227         return c.GetSituation();
228 }
229
230 Steering &Goal::GetSteering() noexcept {
231         return c.GetSteering();
232 }
233
234 const Steering &Goal::GetSteering() const noexcept {
235         return c.GetSteering();
236 }
237
238 app::Assets &Goal::Assets() noexcept {
239         return c.GetSimulation().Assets();
240 }
241
242 const app::Assets &Goal::Assets() const noexcept {
243         return c.GetSimulation().Assets();
244 }
245
246 math::GaloisLFSR &Goal::Random() noexcept {
247         return Assets().random;
248 }
249
250 void Goal::SetComplete() {
251         if (!complete) {
252                 complete = true;
253                 OnComplete();
254                 if (on_complete) {
255                         on_complete(*this);
256                 }
257         }
258 }
259
260 void Goal::SetForeground() {
261         OnForeground();
262         if (on_foreground) {
263                 on_foreground(*this);
264         }
265 }
266
267 void Goal::SetBackground() {
268         OnBackground();
269         if (on_background) {
270                 on_background(*this);
271         }
272 }
273
274 void Goal::WhenComplete(std::function<void(Goal &)> cb) noexcept {
275         on_complete = cb;
276         if (complete) {
277                 on_complete(*this);
278         }
279 }
280
281 void Goal::WhenForeground(std::function<void(Goal &)> cb) noexcept {
282         on_foreground = cb;
283 }
284
285 void Goal::WhenBackground(std::function<void(Goal &)> cb) noexcept {
286         on_background = cb;
287 }
288
289
290 IdleGoal::IdleGoal(Creature &c)
291 : Goal(c) {
292         Urgency(-1.0);
293         Interruptible(true);
294 }
295
296 IdleGoal::~IdleGoal() {
297 }
298
299 std::string IdleGoal::Describe() const {
300         return "idle";
301 }
302
303 void IdleGoal::Action() {
304         // use boredom as chance per minute
305         if (Random().UNorm() < GetCreature().GetStats().Boredom().value * (1.0 / 3600.0)) {
306                 PickActivity();
307         }
308 }
309
310 void IdleGoal::PickActivity() {
311         GetCreature().AddGoal(std::unique_ptr<Goal>(new StrollGoal(GetCreature())));
312 }
313
314
315 namespace {
316
317 std::string summarize(const Composition &comp, const app::Assets &assets) {
318         std::stringstream s;
319         bool first = true;
320         for (const auto &c : comp) {
321                 if (first) {
322                         first = false;
323                 } else {
324                         s << " or ";
325                 }
326                 s << assets.data.resources[c.resource].label;
327         }
328         return s.str();
329 }
330
331 }
332
333 IngestGoal::IngestGoal(Creature &c, Creature::Stat &stat)
334 : Goal(c)
335 , stat(stat)
336 , accept(Assets().data.resources)
337 , locate_subtask(nullptr)
338 , ingesting(false)
339 , resource(-1)
340 , yield(0.0) {
341         Urgency(stat.value);
342 }
343
344 IngestGoal::~IngestGoal() {
345 }
346
347 void IngestGoal::Accept(int resource, double value) {
348         accept.Add(resource, value);
349 }
350
351 std::string IngestGoal::Describe() const {
352         if (resource == -1) {
353                 return "ingest " + summarize(accept, Assets());
354         } else {
355                 const world::Resource &r = Assets().data.resources[resource];
356                 if (r.state == world::Resource::SOLID) {
357                         return "eat " + r.label;
358                 } else {
359                         return "drink " + r.label;
360                 }
361         }
362 }
363
364 void IngestGoal::Enable() {
365 }
366
367 void IngestGoal::Tick(double dt) {
368         Urgency(stat.value);
369         if (locate_subtask) {
370                 locate_subtask->Urgency(Urgency() + 0.1);
371         }
372         if (ingesting) {
373                 if (OnSuitableTile() && !GetSituation().Moving()) {
374                         GetCreature().Ingest(resource, yield * GetCreature().GetComposition().Compatibility(resource) * dt);
375                         stat.Add(-1.0 * yield * dt);
376                         if (stat.Empty()) {
377                                 SetComplete();
378                         }
379                 } else {
380                         // left tile somehow, some idiot probably pushed us off
381                         ingesting = false;
382                         Interruptible(true);
383                 }
384         }
385 }
386
387 void IngestGoal::Action() {
388         if (ingesting) {
389                 // all good
390                 return;
391         }
392         if (OnSuitableTile()) {
393                 if (GetSituation().Moving()) {
394                         // break with maximum force
395                         GetSteering().Haste(1.0);
396                         GetSteering().Halt();
397                 } else {
398                         // finally
399                         // TODO: somehow this still gets interrupted
400                         Interruptible(false);
401                         ingesting = true;
402                 }
403         } else {
404                 locate_subtask = new LocateResourceGoal(GetCreature());
405                 for (const auto &c : accept) {
406                         locate_subtask->Accept(c.resource, c.value);
407                 }
408                 locate_subtask->SetMinimum(stat.gain * -1.1);
409                 locate_subtask->Urgency(Urgency() + 0.1);
410                 locate_subtask->WhenComplete([&](Goal &){ locate_subtask = nullptr; });
411                 GetCreature().AddGoal(std::unique_ptr<Goal>(locate_subtask));
412         }
413 }
414
415 bool IngestGoal::OnSuitableTile() {
416         if (!GetSituation().OnGround()) {
417                 return false;
418         }
419         const world::TileType &t = GetSituation().GetTileType();
420         auto found = t.FindBestResource(accept);
421         if (found != t.resources.end()) {
422                 resource = found->resource;
423                 yield = found->ubiquity;
424                 return true;
425         } else {
426                 resource = -1;
427                 return false;
428         }
429 }
430
431
432 LocateResourceGoal::LocateResourceGoal(Creature &c)
433 : Goal(c)
434 , accept(Assets().data.resources)
435 , found(false)
436 , target_pos(0.0)
437 , searching(false)
438 , reevaluate(0.0)
439 , minimum(0.0) {
440 }
441
442 LocateResourceGoal::~LocateResourceGoal() noexcept {
443 }
444
445 void LocateResourceGoal::Accept(int resource, double value) {
446         accept.Add(resource, value);
447 }
448
449 std::string LocateResourceGoal::Describe() const {
450         return "locate " + summarize(accept, Assets());
451 }
452
453 void LocateResourceGoal::Enable() {
454
455 }
456
457 void LocateResourceGoal::Tick(double dt) {
458         reevaluate -= dt;
459 }
460
461 void LocateResourceGoal::Action() {
462         if (reevaluate < 0.0) {
463                 LocateResource();
464                 reevaluate = 3.0;
465         } else if (!found) {
466                 if (!searching) {
467                         LocateResource();
468                 } else {
469                         double dist = glm::length2(GetSituation().Position() - target_pos);
470                         if (dist < 0.0001) {
471                                 searching = false;
472                                 LocateResource();
473                         } else {
474                                 GetSteering().GoTo(target_pos);
475                         }
476                 }
477         } else if (NearTarget()) {
478                 GetSteering().Halt();
479                 if (!GetSituation().Moving()) {
480                         SetComplete();
481                 }
482         } else {
483                 GetSteering().GoTo(target_pos);
484         }
485         GetSteering().Haste(Urgency());
486 }
487
488 void LocateResourceGoal::LocateResource() {
489         if (GetSituation().OnSurface()) {
490                 const world::TileType &t = GetSituation().GetTileType();
491                 auto yield = t.FindBestResource(accept);
492                 if (yield != t.resources.cend()) {
493                         // hoooray
494                         GetSteering().Halt();
495                         found = true;
496                         searching = false;
497                         target_pos = GetSituation().Position();
498                 } else {
499                         // go find somewhere else
500                         SearchVicinity();
501                         if (!found) {
502                                 Remember();
503                                 if (!found) {
504                                         RandomWalk();
505                                 }
506                         }
507                 }
508         } else {
509                 // well, what now?
510                 found = false;
511                 searching = false;
512         }
513 }
514
515 void LocateResourceGoal::SearchVicinity() {
516         const world::Planet &planet = GetSituation().GetPlanet();
517         const glm::dvec3 &pos = GetSituation().Position();
518         const glm::dvec3 normal(planet.NormalAt(pos));
519         const glm::dvec3 step_x(glm::normalize(glm::cross(normal, glm::dvec3(normal.z, normal.x, normal.y))));
520         const glm::dvec3 step_y(glm::normalize(glm::cross(step_x, normal)));
521
522         constexpr int search_radius = 2;
523         double rating[2 * search_radius + 1][2 * search_radius + 1] = {0};
524
525         // find close and rich field
526         for (int y = -search_radius; y < search_radius + 1; ++y) {
527                 for (int x = -search_radius; x < search_radius + 1; ++x) {
528                         const glm::dvec3 tpos(pos + (double(x) * step_x) + (double(y) * step_y));
529                         if (!GetCreature().PerceptionTest(tpos)) continue;
530                         const world::TileType &type = planet.TileTypeAt(tpos);
531                         auto yield = type.FindBestResource(accept);
532                         if (yield != type.resources.cend()) {
533                                 // TODO: subtract minimum yield
534                                 rating[y + search_radius][x + search_radius] = yield->ubiquity * accept.Get(yield->resource);
535                                 // penalize distance
536                                 double dist = std::max(0.125, 0.25 * glm::length2(tpos - pos));
537                                 rating[y + search_radius][x + search_radius] /= dist;
538                         }
539                 }
540         }
541
542         // penalize crowding
543         for (auto &c : planet.Creatures()) {
544                 if (&*c == &GetCreature()) continue;
545                 for (int y = -search_radius; y < search_radius + 1; ++y) {
546                         for (int x = -search_radius; x < search_radius + 1; ++x) {
547                                 const glm::dvec3 tpos(pos + (double(x) * step_x) + (double(y) * step_y));
548                                 if (glm::length2(tpos - c->GetSituation().Position()) < 1.0) {
549                                         rating[y + search_radius][x + search_radius] *= 0.8;
550                                 }
551                         }
552                 }
553         }
554
555         glm::ivec2 best_pos(0);
556         double best_rating = -1.0;
557
558         for (int y = -search_radius; y < search_radius + 1; ++y) {
559                 for (int x = -search_radius; x < search_radius + 1; ++x) {
560                         if (rating[y + search_radius][x + search_radius] > best_rating) {
561                                 best_pos = glm::ivec2(x, y);
562                                 best_rating = rating[y + search_radius][x + search_radius];
563                         }
564                 }
565         }
566
567         if (best_rating > minimum) {
568                 found = true;
569                 searching = false;
570                 target_pos = glm::normalize(pos + (double(best_pos.x) * step_x) + (double(best_pos.y) * step_y)) * planet.Radius();
571                 GetSteering().GoTo(target_pos);
572         }
573 }
574
575 void LocateResourceGoal::Remember() {
576         glm::dvec3 pos(0.0);
577         if (GetCreature().GetMemory().RememberLocation(accept, pos)) {
578                 found = true;
579                 searching = false;
580                 target_pos = pos;
581                 GetSteering().GoTo(target_pos);
582         }
583 }
584
585 void LocateResourceGoal::RandomWalk() {
586         if (searching) {
587                 return;
588         }
589
590         const world::Planet &planet = GetSituation().GetPlanet();
591         const glm::dvec3 &pos = GetSituation().Position();
592         const glm::dvec3 normal(planet.NormalAt(pos));
593         const glm::dvec3 step_x(glm::normalize(glm::cross(normal, glm::dvec3(normal.z, normal.x, normal.y))));
594         const glm::dvec3 step_y(glm::normalize(glm::cross(step_x, normal)));
595
596         found = false;
597         searching = true;
598         target_pos = GetSituation().Position();
599         target_pos += Random().SNorm() * step_x;
600         target_pos += Random().SNorm() * step_y;
601         // bias towards current heading
602         target_pos += GetSituation().Heading() * 1.5;
603         target_pos = glm::normalize(target_pos) * planet.Radius();
604         GetSteering().GoTo(target_pos);
605 }
606
607 bool LocateResourceGoal::NearTarget() const noexcept {
608         const Situation &s = GetSituation();
609         return s.OnSurface() && glm::length2(s.Position() - target_pos) < 0.5;
610 }
611
612
613 StrollGoal::StrollGoal(Creature &c)
614 : Goal(c)
615 , last(GetSituation().Position())
616 , next(last) {
617 }
618
619 StrollGoal::~StrollGoal() {
620 }
621
622 std::string StrollGoal::Describe() const {
623         return "take a walk";
624 }
625
626 void StrollGoal::Enable() {
627         last = GetSituation().Position();
628         GetSteering().Haste(0.0);
629         PickTarget();
630 }
631
632 void StrollGoal::Action() {
633         if (glm::length2(next - GetSituation().Position()) < 0.0001) {
634                 PickTarget();
635         }
636 }
637
638 void StrollGoal::OnBackground() {
639         SetComplete();
640 }
641
642 void StrollGoal::PickTarget() noexcept {
643         last = next;
644         next += GetSituation().Heading() * 1.5;
645         const glm::dvec3 normal(GetSituation().GetPlanet().NormalAt(GetSituation().Position()));
646         glm::dvec3 rand_x(GetSituation().Heading());
647         if (std::abs(glm::dot(normal, rand_x)) > 0.999) {
648                 rand_x = glm::dvec3(normal.z, normal.x, normal.y);
649         }
650         glm::dvec3 rand_y = glm::cross(normal, rand_x);
651         next += ((rand_x * Random().SNorm()) + (rand_y * Random().SNorm())) * 1.5;
652         next = glm::normalize(next) * GetSituation().GetPlanet().Radius();
653         GetSteering().GoTo(next);
654 }
655
656 }
657 }