• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Return-To-The-Roots / s25client / 22044569181

15 Feb 2026 10:50PM UTC coverage: 50.34% (-0.5%) from 50.826%
22044569181

Pull #1720

github

web-flow
Merge 4dbe54b70 into 6db06730b
Pull Request #1720: Add leather addon

274 of 1055 new or added lines in 65 files covered. (25.97%)

286 existing lines in 28 files now uncovered.

23017 of 45723 relevant lines covered (50.34%)

43559.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.21
/libs/s25main/figures/noFigure.cpp
1
// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org)
2
//
3
// SPDX-License-Identifier: GPL-2.0-or-later
4

5
#include "figures/noFigure.h"
6
#include "EventManager.h"
7
#include "FindWhConditions.h"
8
#include "GamePlayer.h"
9
#include "GlobalGameSettings.h"
10
#include "LeatherLoader.h"
11
#include "Loader.h"
12
#include "SerializedGameData.h"
13
#include "WineLoader.h"
14
#include "addons/const_addons.h"
15
#include "buildings/nobBaseWarehouse.h"
16
#include "buildings/nobHarborBuilding.h"
17
#include "helpers/containerUtils.h"
18
#include "network/GameClient.h"
19
#include "nofCarrier.h"
20
#include "ogl/glArchivItem_Bitmap.h"
21
#include "ogl/glArchivItem_Bitmap_Player.h"
22
#include "ogl/glArchivItem_Bob.h"
23
#include "ogl/glFont.h"
24
#include "ogl/glSmartBitmap.h"
25
#include "pathfinding/PathConditionHuman.h"
26
#include "random/Random.h"
27
#include "world/GameWorld.h"
28
#include "nodeObjs/noFlag.h"
29
#include "nodeObjs/noRoadNode.h"
30
#include "nodeObjs/noSkeleton.h"
31
#include "gameTypes/DirectionToImgDir.h"
32
#include "gameData/GameConsts.h"
33
#include "gameData/JobConsts.h"
34
#include "s25util/Log.h"
35
#include "s25util/colors.h"
36

37
const RoadSegment noFigure::emulated_wanderroad(RoadType::Normal, nullptr, nullptr,
38
                                                std::vector<Direction>(0, Direction::East));
39
/// Welche Strecke soll minimal und maximal zurückgelegt werden beim Rumirren, bevor eine Flagge gesucht wird
40
const unsigned short WANDER_WAY_MIN = 20;
41
const unsigned short WANDER_WAY_MAX = 40;
42
/// Versuche, eine Flagge zu finden, bis er stirbt beim Rumirren
43
const unsigned short WANDER_TRYINGS = 3;
44
// Größe des Rechtecks um den Punkt, wo er die Flaggen sucht beim Rumirren
45
const unsigned short WANDER_RADIUS = 10;
46
/// Dasselbe nochmal für Soldaten
47
const unsigned short WANDER_TRYINGS_SOLDIERS = 6;
48
const unsigned short WANDER_RADIUS_SOLDIERS = 15;
49

50
noFigure::noFigure(const Job job, const MapPoint pos, const unsigned char player, noRoadNode* const goal)
702✔
51
    : noMovable(NodalObjectType::Figure, pos), fs(FigureState::GotToGoal), job_(job), player(player), cur_rs(nullptr),
52
      rs_pos(0), rs_dir(false), on_ship(false), goal_(goal), waiting_for_free_node(false), wander_way(0),
53
      wander_tryings(0), flagPos_(MapPoint::Invalid()), flag_obj_id(0), burned_wh_id(0xFFFFFFFF), last_id(0xFFFFFFFF),
702✔
54
      armor(false)
702✔
55
{
56
    // If the goal is a storehouse we won't work there but go to the new home
57
    if(goal && nobBaseWarehouse::isStorehouseGOT(goal->GetGOT()))
702✔
58
        fs = FigureState::GoHome;
72✔
59
}
702✔
60

61
noFigure::noFigure(const Job job, const MapPoint pos, const unsigned char player)
56✔
62
    : noMovable(NodalObjectType::Figure, pos), fs(FigureState::Job), job_(job), player(player), cur_rs(nullptr),
63
      rs_pos(0), rs_dir(false), on_ship(false), goal_(nullptr), waiting_for_free_node(false), wander_way(0),
64
      wander_tryings(0), flagPos_(MapPoint::Invalid()), flag_obj_id(0), burned_wh_id(0xFFFFFFFF), last_id(0xFFFFFFFF),
56✔
65
      armor(false)
56✔
66
{}
56✔
67

68
void noFigure::Destroy()
236✔
69
{
70
    RTTR_Assert(HasNoGoal());
236✔
71
    RTTR_Assert(!cur_rs);
236✔
72
    noMovable::Destroy();
236✔
73

74
    RTTR_Assert(!world->GetPlayer(player).IsDependentFigure(*this));
236✔
75
}
236✔
76

77
void noFigure::Serialize(SerializedGameData& sgd) const
8✔
78
{
79
    noMovable::Serialize(sgd);
8✔
80

81
    sgd.PushEnum<uint8_t>(fs);
8✔
82
    sgd.PushEnum<uint8_t>(job_);
8✔
83
    sgd.PushUnsignedChar(player);
8✔
84
    sgd.PushObject(cur_rs, true);
8✔
85
    sgd.PushUnsignedShort(rs_pos);
8✔
86
    sgd.PushBool(rs_dir);
8✔
87
    sgd.PushBool(on_ship);
8✔
88
    sgd.PushBool(armor);
8✔
89

90
    if(fs == FigureState::GotToGoal || fs == FigureState::GoHome)
8✔
91
        sgd.PushObject(goal_);
6✔
92

93
    sgd.PushBool(waiting_for_free_node);
8✔
94

95
    if(fs == FigureState::Wander)
8✔
96
    {
97
        sgd.PushUnsignedShort(wander_way);
×
98
        sgd.PushUnsignedShort(wander_tryings);
×
99
        helpers::pushPoint(sgd, flagPos_);
×
100
        sgd.PushUnsignedInt(flag_obj_id);
×
101
        sgd.PushUnsignedInt(burned_wh_id);
×
102
    }
103
}
8✔
104

105
noFigure::noFigure(SerializedGameData& sgd, const unsigned obj_id)
4✔
106
    : noMovable(sgd, obj_id), fs(sgd.Pop<FigureState>()), job_(sgd.Pop<Job>()), player(sgd.PopUnsignedChar()),
16✔
107
      cur_rs(sgd.PopObject<RoadSegment>(GO_Type::Roadsegment)), rs_pos(sgd.PopUnsignedShort()), rs_dir(sgd.PopBool()),
4✔
108
      on_ship(sgd.PopBool()), last_id(0xFFFFFFFF), armor(sgd.GetGameDataVersion() >= 12 ? sgd.PopBool() : false)
8✔
109
{
110
    if(fs == FigureState::GotToGoal || fs == FigureState::GoHome)
4✔
111
        goal_ = sgd.PopObject<noRoadNode>();
3✔
112
    else
113
        goal_ = nullptr;
1✔
114

115
    waiting_for_free_node = sgd.PopBool();
4✔
116

117
    if(fs == FigureState::Wander)
4✔
118
    {
119
        wander_way = sgd.PopUnsignedShort();
×
120
        wander_tryings = sgd.PopUnsignedShort();
×
121
        flagPos_ = sgd.PopMapPoint();
×
122
        flag_obj_id = sgd.PopUnsignedInt();
×
123
        burned_wh_id = sgd.PopUnsignedInt();
×
124
    }
125
}
4✔
126

127
bool noFigure::IsSoldier() const
375✔
128
{
129
    return isSoldierJob(job_);
375✔
130
}
131

132
void noFigure::ActAtFirst()
352✔
133
{
134
    // Je nach unserem Status bestimmte Dinge tun
135
    switch(fs)
352✔
136
    {
137
        default: break;
×
138
        case FigureState::GotToGoal: WalkToGoal(); break;
154✔
139
        case FigureState::Job:
138✔
140
            StartWalking(Direction::SouthEast);
138✔
141
            break; // erstmal rauslaufen, darum kümmern sich dann die abgeleiteten Klassen
138✔
142
        case FigureState::GoHome:
56✔
143
        {
144
            // Wenn ich gleich wieder nach Hause geschickt wurde und aus einem Lagerhaus rauskomme, gar nicht erst
145
            // rausgehen!
146
            if(goal_->GetPos() == pos)
56✔
147
            {
148
                // Reset goal before re-adding to wh
149
                auto* wh = checkedCast<nobBaseWarehouse*>(goal_);
×
150
                goal_ = nullptr;
×
151
                cur_rs = nullptr;
×
152
                wh->AddFigure(world->RemoveFigure(pos, *this));
×
153
            } else
154
                // ansonsten ganz normal rausgehen
155
                WalkToGoal();
56✔
156
        }
157
        break;
56✔
158
        case FigureState::Wander:
4✔
159
            StartWalking(Direction::SouthEast);
4✔
160
            break; // erstmal rauslaufen, darum kümmern sich dann die Wander-Funktionen
4✔
161
    }
162
}
352✔
163

164
/// Gibt den Sichtradius dieser Figur zurück (0, falls nicht-spähend)
165
unsigned noFigure::GetVisualRange() const
3,564✔
166
{
167
    return 0;
3,564✔
168
}
169

170
/// Legt die Anfangsdaten für das Laufen auf Wegen fest
171
void noFigure::InitializeRoadWalking(const RoadSegment* const road, const unsigned short rs_pos, const bool rs_dir)
291✔
172
{
173
    this->cur_rs = road;
291✔
174
    this->rs_pos = rs_pos;
291✔
175
    this->rs_dir = rs_dir;
291✔
176
}
291✔
177

178
DrawPoint noFigure::CalcFigurRelative() const
×
179
{
180
    MapPoint targetPt = world->GetNeighbour(pos, GetCurMoveDir());
×
181
    Position curPt = world->GetNodePos(pos);
×
182
    Position nextPt = world->GetNodePos(targetPt);
×
183

184
    Position offset(0, 0);
×
185

186
    if(GetCurMoveDir() == Direction::NorthWest
×
187
       && (world->GetNO(targetPt)->GetType() == NodalObjectType::Buildingsite
×
188
           || world->GetNO(targetPt)->GetType() == NodalObjectType::Building))
×
189
    {
190
        auto* const bld = world->GetSpecObj<noBaseBuilding>(targetPt);
×
191
        nextPt += bld->GetDoorPoint();
×
192
    } else if(GetCurMoveDir() == Direction::SouthEast
×
193
              && (world->GetNO(pos)->GetType() == NodalObjectType::Buildingsite
×
194
                  || world->GetNO(pos)->GetType() == NodalObjectType::Building))
×
195
    {
196
        auto* const bld = world->GetSpecObj<noBaseBuilding>(pos);
×
197
        curPt += bld->GetDoorPoint();
×
198
        offset = bld->GetDoorPoint();
×
199
    }
200

201
    return offset + CalcRelative(curPt, nextPt);
×
202
}
203

204
void noFigure::StartWalking(const Direction dir)
4,458✔
205
{
206
    RTTR_Assert(!(GetGOT() == GO_Type::NofPassivesoldier && fs == FigureState::Job));
4,458✔
207

208
    // Gehen wir in ein Gebäude?
209
    if(dir == Direction::NorthWest
8,916✔
210
       && world->GetNO(world->GetNeighbour(pos, Direction::NorthWest))->GetType() == NodalObjectType::Building)
4,458✔
211
        world->GetSpecObj<noBuilding>(world->GetNeighbour(pos, Direction::NorthWest))
212
          ->OpenDoor(); // Dann die Tür aufmachen
431✔
213
    // oder aus einem raus?
214
    else if(dir == Direction::SouthEast && world->GetNO(pos)->GetType() == NodalObjectType::Building)
4,027✔
215
        world->GetSpecObj<noBuilding>(pos)->OpenDoor(); // Dann die Tür aufmachen
568✔
216

217
    // Ist der Platz schon besetzt, wo wir hinlaufen wollen und laufen wir auf Straßen?
218
    if(!world->IsRoadNodeForFigures(world->GetNeighbour(pos, dir)) && cur_rs)
4,458✔
219
    {
220
        // Dann stehen bleiben!
221
        FaceDir(dir);
×
222
        waiting_for_free_node = true;
×
223
        // Andere Figuren stoppen
224
        world->StopOnRoads(pos, dir);
×
225
    } else
226
    {
227
        // Normal hinlaufen
228
        StartMoving(dir, 20);
4,458✔
229
    }
230
}
4,458✔
231

232
void noFigure::DrawShadow(DrawPoint drawPt, const unsigned char anistep, Direction dir)
×
233
{
234
    auto* bitmap = LOADER.GetMapTexture(calcWalkFrameIndex(900, dir, anistep));
×
235
    if(bitmap)
×
236
        bitmap->DrawFull(drawPt, COLOR_SHADOW);
×
237
}
×
238

239
void noFigure::WalkFigure()
4,353✔
240
{
241
    // Tür hinter sich zumachen, wenn wir aus einem Gebäude kommen
242
    if(GetCurMoveDir() == Direction::SouthEast && world->GetNO(pos)->GetType() == NodalObjectType::Building)
4,353✔
243
        world->GetSpecObj<noBuilding>(pos)->CloseDoor();
554✔
244

245
    Walk();
4,353✔
246

247
    if(cur_rs)
4,353✔
248
        ++rs_pos;
2,227✔
249

250
    // oder in eins reingegangen sind
251
    if(GetCurMoveDir() == Direction::NorthWest && world->GetNO(pos)->GetType() == NodalObjectType::Building)
4,353✔
252
        world->GetSpecObj<noBuilding>(pos)->CloseDoor();
424✔
253
}
4,353✔
254

255
void noFigure::WalkToGoal()
1,589✔
256
{
257
    // Kein Ziel mehr --> Rumirren
258
    if(!goal_)
1,589✔
259
    {
260
        StartWandering();
×
261
        Wander();
×
262
        return;
×
263
    }
264

265
    // Straße abgelaufen oder noch gar keine Straße vorhanden?
266
    if(!cur_rs || rs_pos == cur_rs->GetLength())
1,589✔
267
    {
268
        // Ziel erreicht?
269
        // Bei dem Träger können das beide Flaggen sein!
270
        bool reachedGoal;
271
        if(GetGOT() == GO_Type::NofCarrier && fs == FigureState::GotToGoal)
923✔
272
        {
273
            RTTR_Assert(dynamic_cast<nofCarrier*>(this));
87✔
274
            auto* carrier = static_cast<nofCarrier*>(this);
87✔
275
            noRoadNode* flag = carrier->GetFirstFlag();
87✔
276
            if(flag && flag->GetPos() == pos)
87✔
277
                reachedGoal = true;
24✔
278
            else
279
            {
280
                flag = carrier->GetSecondFlag();
63✔
281
                reachedGoal = flag && flag->GetPos() == pos;
63✔
282
            }
283
        } else
284
        {
285
            reachedGoal = goal_->GetPos() == pos;
836✔
286
        }
287

288
        if(reachedGoal)
923✔
289
        {
290
            noRoadNode* goal = goal_;
575✔
291
            // Zeug nullen
292
            cur_rs = nullptr;
575✔
293
            goal_ = nullptr;
575✔
294
            rs_dir = false;
575✔
295
            rs_pos = 0;
575✔
296
            if(fs == FigureState::GoHome)
575✔
297
            {
298
                // Mann im Lagerhaus angekommen
299
                static_cast<nobBaseWarehouse*>(goal)->AddFigure(world->RemoveFigure(pos, *this));
62✔
300
            } else
301
            {
302
                // abgeleiteter Klasse sagen, dass das Ziel erreicht wurde
303
                fs = FigureState::Job;
513✔
304
                GoalReached();
513✔
305
            }
306

307
        } else
308
        {
309
            MapPoint next_harbor;
348✔
310
            // Neuen Weg berechnen
311
            auto* const curRoadNode = world->GetSpecObj<noRoadNode>(pos);
348✔
312
            RoadPathDirection route = curRoadNode ?
348✔
313
                                        world->FindHumanPathOnRoads(*curRoadNode, *goal_, nullptr, &next_harbor) :
348✔
314
                                        RoadPathDirection::None;
348✔
315
            // Kein Weg zum Ziel... nächstes Lagerhaus suchen
316
            if(route == RoadPathDirection::None)
348✔
317
            {
318
                // Arbeisplatz oder Laghaus Bescheid sagen
319
                Abrogate();
1✔
320
                // Wir gehen jetzt nach Hause
321
                GoHome();
1✔
322
                // Evtl wurde kein Lagerhaus gefunden und wir sollen rumirren, dann tun wir das gleich
323
                if(fs == FigureState::Wander)
1✔
324
                    Wander();
×
325
                else
326
                    WalkToGoal(); // Nach Hause laufen...
1✔
327
            }
328
            // Oder müssen wir das Schiff nehmen?
329
            else if(route == RoadPathDirection::Ship)
347✔
330
            {
331
                // Uns in den Hafen einquartieren
332
                noBase* hb = world->GetNO(pos);
×
333
                if(hb->GetGOT() != GO_Type::NobHarborbuilding)
×
334
                {
335
                    // Es gibt keinen Hafen mehr -> nach Hause gehen
336

337
                    // Arbeitsplatz oder Lagerhaus Bescheid sagen
338
                    Abrogate();
×
339
                    // Wir gehen jetzt nach Hause
340
                    GoHome();
×
341
                    // Evtl wurde kein Lagerhaus gefunden und wir sollen rumirren, dann tun wir das gleich
342
                    if(fs == FigureState::Wander)
×
343
                        Wander();
×
344
                    else
345
                        WalkToGoal(); // Nach Hause laufen...
×
346
                } else
347
                {
348
                    // Uns in den Hafen einquartieren
349
                    cur_rs = nullptr; // wir laufen nicht mehr auf einer Straße
×
350
                    static_cast<nobHarborBuilding*>(hb)->AddFigureForShip(world->RemoveFigure(pos, *this), next_harbor);
×
351
                }
352
            } else
353
            {
354
                // Get next street we are walking on
355
                const Direction walkDir = toDirection(route);
347✔
356
                cur_rs = curRoadNode->GetRoute(walkDir);
347✔
357
                StartWalking(walkDir);
347✔
358
                rs_pos = 0;
347✔
359
                rs_dir = curRoadNode != cur_rs->GetF1();
347✔
360
            }
361
        }
362

363
    } else
364
    {
365
        StartWalking(cur_rs->GetDir(rs_dir, rs_pos));
666✔
366
    }
367
}
368

369
void noFigure::HandleEvent(const unsigned id)
4,968✔
370
{
371
    // Bei ID = 0 ists ein Laufevent, bei allen anderen an abgeleitete Klassen weiterleiten
372
    if(id)
4,968✔
373
    {
374
        HandleDerivedEvent(id);
615✔
375
    } else
376
    {
377
        current_ev = nullptr;
4,353✔
378
        WalkFigure();
4,353✔
379

380
        // Alte Richtung und Position für die Berechnung der Sichtbarkeiten merken
381
        const Direction old_dir = GetCurMoveDir();
4,353✔
382

383
        const MapPoint old_pos(pos);
4,353✔
384

385
        switch(fs)
4,353✔
386
        {
387
            case FigureState::GoHome:
1,002✔
388
            case FigureState::GotToGoal: WalkToGoal(); break;
1,002✔
389

390
            case FigureState::Job: Walked(); break;
2,938✔
391
            case FigureState::Wander: Wander(); break;
413✔
392
        }
393

394
        // Ggf. Sichtbereich testen
395
        const unsigned visualRange = GetVisualRange();
4,353✔
396
        if(visualRange)
4,353✔
397
        {
398
            // Use old position (don't use this->pos because it might be different now
399
            // Figure could be in a ship etc.)
400
            world->RecalcMovingVisibilities(old_pos, player, visualRange, old_dir, nullptr);
789✔
401

402
            // Wenn Figur verschwunden ist, muss ihr ehemaliger gesamter Sichtbereich noch einmal
403
            // neue berechnet werden
404
            if(!world->HasFigureAt(old_pos, *this))
789✔
405
                CalcVisibilities(old_pos);
60✔
406
        }
407
    }
408
}
4,968✔
409

410
void noFigure::GoHome(noRoadNode* goal)
62✔
411
{
412
    if(on_ship)
62✔
413
    {
414
        // Wir befinden uns gerade an Deck, also einfach goal auf Null setzen und dann sehen wir, was so passiert
415
        this->goal_ = nullptr;
×
416
        return;
×
417
    }
418
    // Nächstes Lagerhaus suchen
419
    else if(!goal)
62✔
420
    {
421
        // Wenn wir cur_rs == 0, dann hängen wir wahrscheinlich noch im Lagerhaus in der Warteschlange
422
        if(cur_rs == nullptr)
58✔
423
        {
424
            RTTR_Assert(nobBaseWarehouse::isStorehouseGOT(world->GetNO(pos)->GetGOT()));
39✔
425
            goal_ = nullptr;
39✔
426
            world->GetSpecObj<nobBaseWarehouse>(pos)->CancelFigure(this);
39✔
427
            return;
39✔
428
        } else
429
            this->goal_ = world->GetPlayer(player).FindWarehouse((rs_dir) ? *cur_rs->GetF1() : *cur_rs->GetF2(),
38✔
430
                                                                 FW::AcceptsFigure(job_), true, false);
38✔
431
    } else
432
        this->goal_ = goal;
4✔
433

434
    if(this->goal_)
23✔
435
    {
436
        fs = FigureState::GoHome;
22✔
437
        // Lagerhaus Bescheid sagen
438
        static_cast<nobBaseWarehouse*>(this->goal_)->AddDependentFigure(*this);
22✔
439

440
        // Wenn wir stehen, zusätzlich noch loslaufen!
441
        if(waiting_for_free_node)
22✔
442
        {
443
            waiting_for_free_node = false;
×
444
            WalkToGoal();
×
445
            // anderen Leuten noch ggf Bescheid sagen
446
            world->RoadNodeAvailable(pos);
×
447
        }
448
    } else
449
    {
450
        // Kein Lagerhaus gefunden --> Rumirren
451
        StartWandering();
1✔
452
        cur_rs = nullptr;
1✔
453
    }
454
}
455

456
void noFigure::StartWandering(const unsigned burned_wh_id)
47✔
457
{
458
    RTTR_Assert(HasNoGoal());
47✔
459
    fs = FigureState::Wander;
47✔
460
    cur_rs = nullptr;
47✔
461
    rs_pos = 0;
47✔
462
    this->burned_wh_id = burned_wh_id;
47✔
463
    // eine bestimmte Strecke rumirren und dann eine Flagge suchen
464
    // 3x rumirren und eine Flagge suchen, wenn dann keine gefunden wurde, stirbt die Figur
465
    wander_way = WANDER_WAY_MIN + RANDOM_RAND(WANDER_WAY_MAX - WANDER_WAY_MIN);
47✔
466
    // Soldaten sind härter im Nehmen
467
    wander_tryings = IsSoldier() ? WANDER_TRYINGS_SOLDIERS : WANDER_TRYINGS;
47✔
468

469
    // Wenn wir stehen, zusätzlich noch loslaufen!
470
    if(waiting_for_free_node)
47✔
471
    {
472
        waiting_for_free_node = false;
×
473
        // We should be paused, so just continue moving now
474
        if(IsStoppedBetweenNodes())
×
475
            StartMoving(GetCurMoveDir(), GetPausedEvent().length);
×
476
        else
477
            Wander();
×
478
    }
479
}
47✔
480

481
namespace {
482
struct Point2Flag
483
{
484
    World& gwb;
485
    Point2Flag(World& gwb) : gwb(gwb) {}
8✔
486
    noFlag* operator()(const MapPoint pt, unsigned /*r*/) const { return gwb.GetSpecObj<noFlag>(pt); }
3,030✔
487
};
488

489
struct IsValidFlag
490
{
491
    const unsigned playerId_;
492
    IsValidFlag(const unsigned playerId) : playerId_(playerId) {}
8✔
493
    bool operator()(const noFlag* const flag) const { return flag && flag->GetPlayer() == playerId_; }
3,030✔
494
};
495
} // namespace
496

497
void noFigure::Wander()
415✔
498
{
499
    // Sind wir noch auf der Suche nach einer Flagge?
500
    if(wander_way == 0xFFFF)
415✔
501
    {
502
        // Wir laufen schon zur Flagge
503
        WanderToFlag();
21✔
504
        return;
21✔
505
    }
506

507
    // Ist es mal wieder an der Zeit, eine Flagge zu suchen?
508
    if(!wander_way)
394✔
509
    {
510
        // Soldaten sind härter im Nehmen
511
        const unsigned short wander_radius = IsSoldier() ? WANDER_RADIUS_SOLDIERS : WANDER_RADIUS;
8✔
512

513
        // Flaggen sammeln und dann zufällig eine auswählen
514
        const std::vector<noFlag*> flags =
515
          world->GetPointsInRadius(pos, wander_radius, Point2Flag(*world), IsValidFlag(player));
8✔
516

517
        unsigned best_way = 0xFFFFFFFF;
8✔
518
        const noFlag* best_flag = nullptr;
8✔
519

520
        for(auto* flag : flags)
30✔
521
        {
522
            // Ist das ein Flüchtling aus einem abgebrannten Lagerhaus?
523
            if(burned_wh_id != 0xFFFFFFFF)
22✔
524
            {
525
                // Dann evtl gucken, ob anderen Mitglieder schon gesagt haben, dass die Flagge nicht zugänglich ist
526
                if(flag->IsImpossibleForBWU(burned_wh_id))
21✔
527
                {
528
                    // Dann können wir die Flagge überspringen
529
                    continue;
×
530
                }
531
            }
532

533
            // würde die die bisher beste an Weg unterbieten?
534
            unsigned way = world->CalcDistance(pos, flag->GetPos());
22✔
535
            if(way < best_way)
22✔
536
            {
537
                // Are we at that flag or is there a path to it?
538
                if(way == 0 || world->FindHumanPath(pos, flag->GetPos(), wander_radius, false, &way))
8✔
539
                {
540
                    // gucken, ob ein Weg zu einem Warenhaus führt
541
                    if(world->GetPlayer(player).FindWarehouse(*flag, FW::AcceptsFigure(job_), true, false))
8✔
542
                    {
543
                        // dann nehmen wir die doch glatt
544
                        best_way = way;
7✔
545
                        best_flag = flag;
7✔
546
                        if(way == 0)
7✔
547
                            break; // Can't get better
×
548
                    }
549
                } else if(burned_wh_id != 0xFFFFFFFF)
×
550
                {
551
                    // Flagge nicht möglich zugänglich bei einem Flüchting aus einem abgebrannten Lagerhaus?
552
                    // --> der ganzen Gruppe Bescheid sagen, damit die nicht auch alle sinnlos einen Weg zu
553
                    // dieser Flagge suchen
554

555
                    // TODO: Actually it is possible! E.g. between us and the flag is a river, so we won't find a path
556
                    // within the radius but others (on the other side) could --> Remove ImpossibleForBWU?
557
                    flag->ImpossibleForBWU(burned_wh_id);
×
558
                }
559
            }
560
        }
561

562
        if(best_flag)
8✔
563
        {
564
            // bestmögliche schließlich nehmen
565
            wander_way = 0xFFFF;
7✔
566
            flagPos_ = best_flag->GetPos();
7✔
567
            flag_obj_id = best_flag->GetObjId();
7✔
568
            WanderToFlag();
7✔
569
            return;
7✔
570
        }
571

572
        // Wurde keine Flagge gefunden?
573

574
        // Haben wir noch Versuche?
575
        RTTR_Assert(wander_tryings > 0);
1✔
576
        if(--wander_tryings > 0)
1✔
577
        {
578
            // von vorne beginnen wieder mit Rumirren
579
            wander_way = WANDER_WAY_MIN + RANDOM_RAND(WANDER_WAY_MAX - WANDER_WAY_MIN);
1✔
580
        } else
581
        {
582
            // Genug rumgeirrt, wir finden halt einfach nichts --> Sterben
583
            Die();
×
584
            return;
×
585
        }
586
    }
587

588
    if(WalkInRandomDir())
387✔
589
    {
590
        RTTR_Assert(wander_way > 0);
387✔
591
        --wander_way;
387✔
592
    } else
593
    {
594
        // Wir sind eingesperrt! Kein Weg mehr gefunden --> Sterben
595
        Die();
×
596
    }
597
}
598

599
bool noFigure::WalkInRandomDir()
387✔
600
{
601
    PathConditionHuman pathChecker(*world);
387✔
602
    // Check all dirs starting with a random one and taking the first possible
603
    for(const Direction dir : helpers::enumRange(RANDOM_ENUM(Direction)))
2,071✔
604
    {
605
        if(pathChecker.IsNodeOk(world->GetNeighbour(pos, dir)) && pathChecker.IsEdgeOk(pos, dir))
1,303✔
606
        {
607
            StartWalking(dir);
387✔
608
            return true;
387✔
609
        }
610
    }
611
    return false;
×
612
}
613

614
void noFigure::WanderFailedTrade()
10✔
615
{
616
    DieFailedTrade();
10✔
617
}
10✔
618

619
void noFigure::WanderToFlag()
28✔
620
{
621
    // Existiert die Flagge überhaupt noch?
622
    noBase* no = world->GetNO(flagPos_);
28✔
623
    if(no->GetObjId() != flag_obj_id)
28✔
624
    {
625
        // Wenn nicht, wieder normal weiter rumirren
626
        StartWandering();
×
627
        Wander();
×
628
        return;
5✔
629
    }
630

631
    // Sind wir schon da?
632
    if(pos == flagPos_)
28✔
633
    {
634
        // Gibts noch nen Weg zu einem Lagerhaus?
635
        RTTR_Assert(world->GetSpecObj<noRoadNode>(pos));
5✔
636
        if(nobBaseWarehouse* wh = world->GetPlayer(player).FindWarehouse(*world->GetSpecObj<noRoadNode>(pos),
10✔
637
                                                                         FW::AcceptsFigure(job_), true, false))
10✔
638
        {
639
            // ja, dann können wir ja hingehen
640
            goal_ = wh;
5✔
641
            cur_rs = nullptr;
5✔
642
            rs_pos = 0;
5✔
643
            fs = FigureState::GoHome;
5✔
644
            wh->AddDependentFigure(*this);
5✔
645
            WalkToGoal();
5✔
646
            return;
5✔
647
        } else
648
        {
649
            // Wenn nicht, wieder normal weiter rumirren
650
            StartWandering();
×
651
            Wander();
×
652
            return;
×
653
        }
654
    }
655

656
    // Weiter zur Flagge gehen
657
    // Gibts noch nen Weg dahin bzw. existiert die Flagge noch?
658
    const auto dir = world->FindHumanPath(pos, flagPos_, 60, false);
23✔
659
    if(dir)
23✔
660
    {
661
        // weiter hinlaufen
662
        StartWalking(*dir);
23✔
663
    } else
664
    {
665
        // Wenn nicht, wieder normal weiter rumirren
666
        StartWandering();
×
667
        Wander();
×
668
    }
669
}
670

671
void noFigure::CorrectSplitData(const RoadSegment* const rs2)
×
672
{
673
    // cur_rs entspricht Teilstück 1 !
674

675
    // Wenn man sich auf den ersten Teilstück befindet...
676
    if((rs_pos < cur_rs->GetLength() && !rs_dir) || (rs_pos > rs2->GetLength() && rs_dir))
×
677
    {
678
        // Nur Position berichtigen
679
        if(rs_dir)
×
680
            rs_pos -= rs2->GetLength();
×
681
    }
682

683
    // Wenn man auf dem 2. steht, ...
684
    else if((rs_pos > cur_rs->GetLength() && !rs_dir) || (rs_pos < rs2->GetLength() && rs_dir))
×
685
    {
686
        // Position berichtigen (wenn man in umgekehrter Richtung läuft, beibehalten!)
687
        if(!rs_dir)
×
688
            rs_pos -= cur_rs->GetLength();
×
689

690
        // wir laufen auf dem 2. Teilstück
691
        cur_rs = rs2;
×
692
    } else if((rs_pos == cur_rs->GetLength() && !rs_dir) || (rs_pos == rs2->GetLength() && rs_dir))
×
693
    {
694
        // wir stehen genau in der Mitte
695
        // abhängig von der Richtung machen, in die man gerade läuft
696
        if(GetCurMoveDir() == rs2->GetRoute(0))
×
697
        {
698
            // wir laufen auf dem 2. Teilstück
699
            cur_rs = rs2;
×
700
            // und wir sind da noch am Anfang
701
            rs_pos = 0;
×
702
        } else if(GetCurMoveDir() == cur_rs->GetRoute(cur_rs->GetLength() - 1) + 3u)
×
703
        {
704
            // wir laufen auf dem 1. Teilstück
705

706
            // und wir sind da noch am Anfang
707
            rs_pos = 0;
×
708
        } else
709
        {
710
            // Wahrscheinlich stehen wir
711
            // dann einfach auf das 2. gehen
712
            cur_rs = rs2;
×
713
            rs_pos = 0;
×
714
            rs_dir = false;
×
715
        }
716
    }
717

718
    CorrectSplitData_Derived();
×
719
}
×
720

721
/// Wird aufgerufen, wenn die Straße unter der Figur geteilt wurde (für abgeleitete Klassen)
722
void noFigure::CorrectSplitData_Derived() {}
×
723

724
unsigned noFigure::CalcWalkAnimationFrame() const
×
725
{
726
    // If we are waiting for a free node use the 2nd frame, else interpolate
727
    return waiting_for_free_node ? 2 : GAMECLIENT.Interpolate(ASCENT_ANIMATION_STEPS[GetAscent()], current_ev) % 8;
×
728
}
729

730
/// Calculate the index of the current frame for a figure walking in the given direction
731
/// The sprites start at `imgSetIndex` and contain 8 images per direction, starting at EAST going clockwise.
732
/// Note that some of the 48 indices might be empty entries in the archive if only specific directions can ever be drawn
733
unsigned noFigure::calcWalkFrameIndex(const unsigned imgSetIndex, const Direction dir, const unsigned animationStep)
×
734
{
735
    RTTR_Assert(animationStep < 8);
×
736
    return imgSetIndex + rttr::enum_cast(toImgDir(dir)) * 8 + animationStep;
×
737
}
738

739
DrawPoint noFigure::InterpolateWalkDrawPos(DrawPoint drawPt) const
×
740
{
741
    // Add an offset relative to the starting point calculated by how far we already walked
742
    // Don't if we are waiting on our starting node
743
    if(!waiting_for_free_node || IsStoppedBetweenNodes())
×
744
        drawPt += CalcFigurRelative();
×
745
    return drawPt;
×
746
}
747

748
void noFigure::DrawWalkingCarrier(DrawPoint drawPt, helpers::OptionalEnum<GoodType> ware, bool fat)
×
749
{
750
    const unsigned ani_step = CalcWalkAnimationFrame();
×
751
    const GamePlayer& owner = world->GetPlayer(player);
×
752
    auto& sprite = ware ? LOADER.getCarrierSprite(*ware, fat, GetCurMoveDir(), ani_step) :
×
753
                          LOADER.getCarrierBobSprite(owner.nation, fat, GetCurMoveDir(), ani_step);
×
754
    sprite.drawForPlayer(InterpolateWalkDrawPos(drawPt), owner.color);
×
755
}
×
756

757
void noFigure::DrawWalkingBobJobs(DrawPoint drawPt, Job job)
×
758
{
759
    const unsigned ani_step = CalcWalkAnimationFrame();
×
760
    const GamePlayer& owner = world->GetPlayer(player);
×
761
    LOADER.getBobSprite(owner.nation, job, GetCurMoveDir(), ani_step)
×
762
      .drawForPlayer(InterpolateWalkDrawPos(drawPt), owner.color);
×
763

NEW
764
    if(armor)
×
NEW
765
        DrawArmor(InterpolateWalkDrawPos(drawPt));
×
UNCOV
766
}
×
767

768
void noFigure::DrawWalking(DrawPoint drawPt, glArchivItem_Bob* file, unsigned id, bool fat)
×
769
{
770
    const unsigned ani_step = CalcWalkAnimationFrame();
×
771
    drawPt = InterpolateWalkDrawPos(drawPt);
×
772

773
    if(file)
×
774
        file->Draw(id, toImgDir(GetCurMoveDir()), fat, ani_step, drawPt, world->GetPlayer(player).color);
×
775
    DrawShadow(drawPt, ani_step, GetCurMoveDir());
×
776
}
×
777

778
void noFigure::DrawWalking(DrawPoint drawPt, const ResourceId& file, unsigned id)
×
779
{
780
    const unsigned ani_step = CalcWalkAnimationFrame();
×
781
    drawPt = InterpolateWalkDrawPos(drawPt);
×
782

783
    LOADER.GetPlayerImage(file, calcWalkFrameIndex(id, GetCurMoveDir(), ani_step))
×
784
      ->DrawFull(drawPt, COLOR_WHITE, world->GetPlayer(player).color);
×
785
    DrawShadow(drawPt, ani_step, GetCurMoveDir());
×
786
}
×
787

788
void noFigure::DrawWalking(DrawPoint drawPt)
×
789
{
790
    switch(job_)
×
791
    {
792
        case Job::PackDonkey:
×
793
        {
794
            const unsigned ani_step = CalcWalkAnimationFrame();
×
795
            drawPt = InterpolateWalkDrawPos(drawPt);
×
796

797
            LOADER.GetMapTexture(calcWalkFrameIndex(2000, GetCurMoveDir(), ani_step))->DrawFull(drawPt);
×
798
            // donkey shadow
799
            LOADER.GetMapTexture(2048 + rttr::enum_cast(GetCurMoveDir()) % 3)->DrawFull(drawPt, COLOR_SHADOW);
×
800
        }
801
        break;
×
802
        case Job::CharBurner: DrawWalking(drawPt, "charburner_bobs", 53); break;
×
803
        case Job::Vintner:
×
804
            DrawWalking(drawPt, "wine_bobs", wineaddon::bobIndex[wineaddon::BobTypes::VINTNER_WALKING]);
×
805
            break;
×
806
        case Job::Winegrower:
×
807
            DrawWalking(drawPt, "wine_bobs", wineaddon::bobIndex[wineaddon::BobTypes::WINEGROWER_WALKING_WITH_SHOVEL]);
×
808
            break;
×
809
        case Job::TempleServant:
×
810
            DrawWalking(drawPt, "wine_bobs", wineaddon::bobIndex[wineaddon::BobTypes::TEMPLESERVANT_WALKING]);
×
811
            break;
×
NEW
812
        case Job::Skinner:
×
NEW
813
            DrawWalking(drawPt, "leather_bobs", leatheraddon::bobIndex[leatheraddon::BobType::SkinnerWalking]);
×
NEW
814
            break;
×
NEW
815
        case Job::Tanner:
×
NEW
816
            DrawWalking(drawPt, "leather_bobs", leatheraddon::bobIndex[leatheraddon::BobType::TannerWalking]);
×
NEW
817
            break;
×
NEW
818
        case Job::LeatherWorker:
×
NEW
819
            DrawWalking(drawPt, "leather_bobs", leatheraddon::bobIndex[leatheraddon::BobType::LeatherworkerWalking]);
×
NEW
820
            break;
×
UNCOV
821
        default: DrawWalkingBobJobs(drawPt, job_); break;
×
822
    }
823
}
×
824

NEW
825
void noFigure::DrawArmor(DrawPoint drawPt)
×
826
{
NEW
827
    if(world->GetGGS().isEnabled(AddonId::MILITARY_HITPOINTS))
×
828
    {
NEW
829
        SmallFont->Draw(drawPt + DrawPoint(7, -20), "+", FontStyle::CENTER, COLOR_RED);
×
NEW
830
        SmallFont->Draw(drawPt + DrawPoint(10, -20), "1", FontStyle::CENTER, COLOR_RED);
×
831
    }
832

NEW
833
    LOADER.GetImageN("leather_bobs", leatheraddon::bobIndex[leatheraddon::BobType::ArmorIconAboveArmoredSoldier])
×
NEW
834
      ->DrawFull(drawPt + DrawPoint(0, -22));
×
NEW
835
}
×
836

UNCOV
837
void noFigure::Die()
×
838
{
839
    // Weg mit mir
840
    GetEvMgr().AddToKillList(world->RemoveFigure(pos, *this));
×
841
    // ggf. Leiche hinlegen, falls da nix ist
842
    if(!world->GetSpecObj<noBase>(pos))
×
843
        world->SetNO(pos, new noSkeleton(pos));
×
844

845
    RemoveFromInventory();
×
846

847
    // Sichtbarkeiten neu berechnen für Erkunder und Soldaten
848
    CalcVisibilities(pos);
×
849
}
×
850

851
void noFigure::RemoveFromInventory()
×
852
{
853
    // Wars ein Bootmann? Dann Boot und Träger abziehen
854
    if(job_ == Job::BoatCarrier)
×
855
    {
856
        world->GetPlayer(player).DecreaseInventoryJob(Job::Helper, 1);
×
857
        world->GetPlayer(player).DecreaseInventoryWare(GoodType::Boat, 1);
×
858
    } else
859
    {
860
        world->GetPlayer(player).DecreaseInventoryJob(job_, 1);
×
NEW
861
        if(isSoldier(GetJobType()))
×
862
        {
NEW
863
            if(armor)
×
NEW
864
                world->GetPlayer(player).DecreaseInventoryJob(jobEnumToAmoredSoldierEnum(GetJobType()), 1);
×
865
        }
866
    }
UNCOV
867
}
×
868

869
void noFigure::DieFailedTrade()
10✔
870
{
871
    // Weg mit mir
872
    GetEvMgr().AddToKillList(world->RemoveFigure(pos, *this));
10✔
873
    // ggf. Leiche hinlegen, falls da nix ist
874
    if(!world->GetSpecObj<noBase>(pos))
10✔
875
        world->SetNO(pos, new noSkeleton(pos));
10✔
876
}
10✔
877

878
void noFigure::NodeFreed(const MapPoint pt)
178✔
879
{
880
    // Stehen wir gerade aus diesem Grund?
881
    if(!waiting_for_free_node || pt != world->GetNeighbour(this->pos, GetCurMoveDir()))
178✔
882
        return;
177✔
883

884
    // Gehen wir in ein Gebäude? Dann wieder ausgleichen, weil wir die Türen sonst doppelt aufmachen!
885
    if(GetCurMoveDir() == Direction::NorthWest
1✔
886
       && world->GetNO(world->GetNeighbour(this->pos, Direction::NorthWest))->GetType() == NodalObjectType::Building)
1✔
887
        world->GetSpecObj<noBuilding>(world->GetNeighbour(this->pos, Direction::NorthWest))->CloseDoor();
×
888
    // oder aus einem raus?
889
    if(GetCurMoveDir() == Direction::SouthEast && world->GetNO(this->pos)->GetType() == NodalObjectType::Building)
1✔
890
        world->GetSpecObj<noBuilding>(this->pos)->CloseDoor();
×
891

892
    // Wir stehen nun nicht mehr
893
    waiting_for_free_node = false;
1✔
894

895
    // Dann loslaufen
896
    StartWalking(GetCurMoveDir());
1✔
897

898
    // anderen Leuten noch ggf Bescheid sagen
899
    world->RoadNodeAvailable(this->pos);
1✔
900
}
901

902
void noFigure::Abrogate()
7✔
903
{
904
    // Arbeisplatz oder Laghaus Bescheid sagen
905
    if(fs == FigureState::GoHome)
7✔
906
    {
907
        // goal might by nullptr if goal was a harbor that got destroyed during sea travel
908
        if(goal_)
1✔
909
        {
910
            checkedCast<nobBaseWarehouse*>(goal_)->RemoveDependentFigure(*this);
1✔
911
            goal_ = nullptr;
1✔
912
        } else
913
        {
914
            if(!on_ship) // no goal but going home - should not happen
×
915
            {
916
                LOG.write("noFigure::Abrogate - GOHOME figure has no goal and is not on a ship - player %i state %i "
×
917
                          "pos %u,%u \n")
918
                  % player % rttr::enum_cast(fs) % pos.x % pos.y;
×
919
                // RTTR_Assert(false);
920
            }
921
        }
922
    } else
923
    {
924
        goal_ = nullptr;
6✔
925
        AbrogateWorkplace();
6✔
926
    }
927
}
7✔
928

929
void noFigure::StopIfNecessary(const MapPoint pt)
49✔
930
{
931
    // Lauf ich auf Wegen --> wenn man zum Ziel oder Weg läuft oder die Träger, die natürlich auch auf Wegen arbeiten
932
    if(fs == FigureState::GoHome || fs == FigureState::GotToGoal
49✔
933
       || (fs == FigureState::Job && GetGOT() == GO_Type::NofCarrier))
98✔
934
    {
935
        // Laufe ich zu diesem Punkt?
936
        if(current_ev && !waiting_for_free_node && world->GetNeighbour(this->pos, GetCurMoveDir()) == pt)
3✔
937
        {
938
            // Dann stehenbleiben
939
            PauseWalking();
1✔
940
            waiting_for_free_node = true;
1✔
941
            world->StopOnRoads(this->pos, GetCurMoveDir());
1✔
942
        }
943
    }
944
}
49✔
945

946
/// Sichtbarkeiten berechnen für Figuren mit Sichtradius (Soldaten, Erkunder) vor dem Laufen
947
void noFigure::CalcVisibilities(const MapPoint pt)
60✔
948
{
949
    const unsigned visualRange = GetVisualRange();
60✔
950
    if(visualRange)
60✔
951
        // An alter Position neu berechnen
952
        world->RecalcVisibilitiesAroundPoint(pt, visualRange, player, nullptr);
60✔
953
}
60✔
954

955
/// Informiert die Figur, dass für sie eine Schiffsreise beginnt
956
void noFigure::StartShipJourney()
4✔
957
{
958
    // We should not be in the world, as we start the journey from a harbor -> We are in that harbor
959
    RTTR_Assert(!world->HasFigureAt(pos, *this));
4✔
960
    RTTR_Assert(!on_ship);
4✔
961

962
    pos = MapPoint::Invalid();
4✔
963
    on_ship = true;
4✔
964
}
4✔
965

966
void noFigure::ArrivedByShip(const MapPoint harborPos)
2✔
967
{
968
    RTTR_Assert(on_ship);
2✔
969
    pos = harborPos;
2✔
970
    on_ship = false;
2✔
971
}
2✔
972

973
/// Examines the route (maybe harbor, road destroyed?) before start shipping
974
MapPoint noFigure::ExamineRouteBeforeShipping(RoadPathDirection& newDir)
2✔
975
{
976
    MapPoint next_harbor;
2✔
977
    // Calc new route
978
    const noRoadNode* roadNode = world->GetSpecObj<noRoadNode>(pos);
2✔
979
    if(!roadNode || !goal_)
2✔
980
        newDir = RoadPathDirection::None;
×
981
    else
982
        newDir = world->FindHumanPathOnRoads(*roadNode, *goal_, nullptr, &next_harbor);
2✔
983

984
    if(newDir == RoadPathDirection::None)
2✔
985
        Abrogate();
×
986

987
    // Going by ship?
988
    if(newDir == RoadPathDirection::Ship)
2✔
989
        // All ok, return next harbor (could be another one!)
990
        return next_harbor;
×
991
    else
992
        return MapPoint(0, 0);
2✔
993
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc