]> git.localhorst.tv Git - blobs.git/blob - src/creature/goal.cpp
c3200a6ca028a64ceef5ebb21e313a15e10d641a
[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                 }
416         } else {
417                 // well, what now?
418                 found = false;
419                 searching = false;
420         }
421 }
422
423 void LocateResourceGoal::SearchVicinity() {
424         const world::Planet &planet = GetSituation().GetPlanet();
425         const glm::dvec3 &pos = GetSituation().Position();
426         const glm::dvec3 normal(planet.NormalAt(pos));
427         const glm::dvec3 step_x(glm::normalize(glm::cross(normal, glm::dvec3(normal.z, normal.x, normal.y))));
428         const glm::dvec3 step_y(glm::normalize(glm::cross(step_x, normal)));
429
430         constexpr int search_radius = 2;
431         double rating[2 * search_radius + 1][2 * search_radius + 1] = {0};
432
433         // find close and rich field
434         for (int y = -search_radius; y < search_radius + 1; ++y) {
435                 for (int x = -search_radius; x < search_radius + 1; ++x) {
436                         const glm::dvec3 tpos(pos + (double(x) * step_x) + (double(y) * step_y));
437                         if (!GetCreature().PerceptionTest(tpos)) continue;
438                         const world::TileType &type = planet.TileTypeAt(tpos);
439                         auto yield = type.FindBestResource(accept);
440                         if (yield != type.resources.cend()) {
441                                 // TODO: subtract minimum yield
442                                 rating[y + search_radius][x + search_radius] = yield->ubiquity * accept.Get(yield->resource);
443                                 // penalize distance
444                                 double dist = std::max(0.125, 0.25 * glm::length(tpos - pos));
445                                 rating[y + search_radius][x + search_radius] /= dist;
446                         }
447                 }
448         }
449
450         // penalize crowding
451         for (auto &c : planet.Creatures()) {
452                 if (&*c == &GetCreature()) continue;
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 (glm::length2(tpos - c->GetSituation().Position()) < 1.0) {
457                                         rating[y + search_radius][x + search_radius] *= 0.8;
458                                 }
459                         }
460                 }
461         }
462
463         glm::ivec2 best_pos(0);
464         double best_rating = -1.0;
465
466         for (int y = -search_radius; y < search_radius + 1; ++y) {
467                 for (int x = -search_radius; x < search_radius + 1; ++x) {
468                         if (rating[y + search_radius][x + search_radius] > best_rating) {
469                                 best_pos = glm::ivec2(x, y);
470                                 best_rating = rating[y + search_radius][x + search_radius];
471                         }
472                 }
473         }
474
475         if (best_rating > 0.0) {
476                 found = true;
477                 searching = false;
478                 target_pos = glm::normalize(pos + (double(best_pos.x) * step_x) + (double(best_pos.y) * step_y)) * planet.Radius();
479                 GetSteering().GoTo(target_pos);
480         } else if (!searching) {
481                 found = false;
482                 searching = true;
483                 target_pos = GetSituation().Position();
484                 target_pos += Random().SNorm() * step_x;
485                 target_pos += Random().SNorm() * step_y;
486                 // bias towards current heading
487                 target_pos += GetSituation().Heading() * 1.5;
488                 target_pos = glm::normalize(target_pos) * planet.Radius();
489                 GetSteering().GoTo(target_pos);
490         }
491 }
492
493 bool LocateResourceGoal::NearTarget() const noexcept {
494         const Situation &s = GetSituation();
495         return s.OnSurface() && glm::length2(s.Position() - target_pos) < 0.5;
496 }
497
498
499 StrollGoal::StrollGoal(Creature &c)
500 : Goal(c)
501 , last(GetSituation().Position())
502 , next(last) {
503 }
504
505 StrollGoal::~StrollGoal() {
506 }
507
508 std::string StrollGoal::Describe() const {
509         return "take a walk";
510 }
511
512 void StrollGoal::Enable() {
513         last = GetSituation().Position();
514         GetSteering().Haste(0.0);
515         PickTarget();
516 }
517
518 void StrollGoal::Action() {
519         if (glm::length2(next - GetSituation().Position()) < 0.0001) {
520                 PickTarget();
521         }
522 }
523
524 void StrollGoal::OnBackground() {
525         SetComplete();
526 }
527
528 void StrollGoal::PickTarget() noexcept {
529         last = next;
530         next += GetSituation().Heading() * 1.5;
531         const glm::dvec3 normal(GetSituation().GetPlanet().NormalAt(GetSituation().Position()));
532         glm::dvec3 rand_x(GetSituation().Heading());
533         if (std::abs(glm::dot(normal, rand_x)) > 0.999) {
534                 rand_x = glm::dvec3(normal.z, normal.x, normal.y);
535         }
536         glm::dvec3 rand_y = glm::cross(normal, rand_x);
537         next += ((rand_x * Random().SNorm()) + (rand_y * Random().SNorm())) * 1.5;
538         next = glm::normalize(next) * GetSituation().GetPlanet().Radius();
539         GetSteering().GoTo(next);
540 }
541
542 }
543 }