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