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