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

Return-To-The-Roots / s25client / 24084873464

07 Apr 2026 01:48PM UTC coverage: 50.359% (+0.02%) from 50.337%
24084873464

Pull #1910

github

web-flow
Merge f20cbf041 into e4146df45
Pull Request #1910: Fix wrongly shown soldiers & Refactor HQ start wares and inventory handling

197 of 409 new or added lines in 22 files covered. (48.17%)

99 existing lines in 25 files now uncovered.

23062 of 45795 relevant lines covered (50.36%)

41987.54 hits per line

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

58.49
/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)
627✔
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),
627✔
54
      hasArmor_(false)
627✔
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()))
627✔
58
        fs = FigureState::GoHome;
54✔
59
}
627✔
60

61
noFigure::noFigure(const Job job, const MapPoint pos, const unsigned char player)
63✔
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),
63✔
65
      hasArmor_(false)
63✔
66
{}
63✔
67

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

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

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

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

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

93
    sgd.PushBool(waiting_for_free_node);
4✔
94

95
    if(fs == FigureState::Wander)
4✔
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
}
4✔
104

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

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

117
    if(fs == FigureState::Wander)
2✔
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
}
2✔
126

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

132
void noFigure::ActAtFirst()
333✔
133
{
134
    // Je nach unserem Status bestimmte Dinge tun
135
    switch(fs)
333✔
136
    {
137
        default: break;
×
138
        case FigureState::GotToGoal: WalkToGoal(); break;
124✔
139
        case FigureState::Job:
149✔
140
            StartWalking(Direction::SouthEast);
149✔
141
            break; // erstmal rauslaufen, darum kümmern sich dann die abgeleiteten Klassen
149✔
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
}
333✔
163

164
/// Gibt den Sichtradius dieser Figur zurück (0, falls nicht-spähend)
165
unsigned noFigure::GetVisualRange() const
3,007✔
166
{
167
    return 0;
3,007✔
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)
241✔
172
{
173
    this->cur_rs = road;
241✔
174
    this->rs_pos = rs_pos;
241✔
175
    this->rs_dir = rs_dir;
241✔
176
}
241✔
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)
3,908✔
205
{
206
    RTTR_Assert(!(GetGOT() == GO_Type::NofPassivesoldier && fs == FigureState::Job));
3,908✔
207

208
    // Gehen wir in ein Gebäude?
209
    if(dir == Direction::NorthWest
7,816✔
210
       && world->GetNO(world->GetNeighbour(pos, Direction::NorthWest))->GetType() == NodalObjectType::Building)
3,908✔
211
        world->GetSpecObj<noBuilding>(world->GetNeighbour(pos, Direction::NorthWest))
212
          ->OpenDoor(); // Dann die Tür aufmachen
391✔
213
    // oder aus einem raus?
214
    else if(dir == Direction::SouthEast && world->GetNO(pos)->GetType() == NodalObjectType::Building)
3,517✔
215
        world->GetSpecObj<noBuilding>(pos)->OpenDoor(); // Dann die Tür aufmachen
524✔
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)
3,908✔
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);
3,908✔
229
    }
230
}
3,908✔
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()
3,810✔
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)
3,810✔
243
        world->GetSpecObj<noBuilding>(pos)->CloseDoor();
501✔
244

245
    Walk();
3,810✔
246

247
    if(cur_rs)
3,810✔
248
        ++rs_pos;
1,738✔
249

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

255
void noFigure::WalkToGoal()
1,337✔
256
{
257
    // Kein Ziel mehr --> Rumirren
258
    if(!goal_)
1,337✔
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,337✔
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)
835✔
272
        {
273
            RTTR_Assert(dynamic_cast<nofCarrier*>(this));
81✔
274
            auto* carrier = static_cast<nofCarrier*>(this);
81✔
275
            noRoadNode* flag = carrier->GetFirstFlag();
81✔
276
            if(flag && flag->GetPos() == pos)
81✔
277
                reachedGoal = true;
22✔
278
            else
279
            {
280
                flag = carrier->GetSecondFlag();
59✔
281
                reachedGoal = flag && flag->GetPos() == pos;
59✔
282
            }
283
        } else
284
        {
285
            reachedGoal = goal_->GetPos() == pos;
754✔
286
        }
287

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

307
        } else
308
        {
309
            MapPoint next_harbor;
267✔
310
            // Neuen Weg berechnen
311
            auto* const curRoadNode = world->GetSpecObj<noRoadNode>(pos);
267✔
312
            RoadPathDirection route = curRoadNode ?
267✔
313
                                        world->FindHumanPathOnRoads(*curRoadNode, *goal_, nullptr, &next_harbor) :
267✔
314
                                        RoadPathDirection::None;
267✔
315
            // Kein Weg zum Ziel... nächstes Lagerhaus suchen
316
            if(route == RoadPathDirection::None)
267✔
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)
266✔
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);
266✔
356
                cur_rs = curRoadNode->GetRoute(walkDir);
266✔
357
                StartWalking(walkDir);
266✔
358
                rs_pos = 0;
266✔
359
                rs_dir = curRoadNode != cur_rs->GetF1();
266✔
360
            }
361
        }
362

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

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

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

383
        const MapPoint old_pos(pos);
3,810✔
384

385
        switch(fs)
3,810✔
386
        {
387
            case FigureState::GoHome:
756✔
388
            case FigureState::GotToGoal: WalkToGoal(); break;
756✔
389

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

394
        // Ggf. Sichtbereich testen
395
        const unsigned visualRange = GetVisualRange();
3,810✔
396
        if(visualRange)
3,810✔
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);
803✔
401

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

410
void noFigure::GoHome(noRoadNode* goal)
29✔
411
{
412
    if(on_ship)
29✔
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)
29✔
420
    {
421
        // Wenn wir cur_rs == 0, dann hängen wir wahrscheinlich noch im Lagerhaus in der Warteschlange
422
        if(cur_rs == nullptr)
25✔
423
        {
424
            RTTR_Assert(nobBaseWarehouse::isStorehouseGOT(world->GetNO(pos)->GetGOT()));
9✔
425
            goal_ = nullptr;
9✔
426
            world->GetSpecObj<nobBaseWarehouse>(pos)->CancelFigure(this);
9✔
427
            return;
9✔
428
        } else
429
            this->goal_ = world->GetPlayer(player).FindWarehouse((rs_dir) ? *cur_rs->GetF1() : *cur_rs->GetF2(),
32✔
430
                                                                 FW::AcceptsFigure(job_), true, false);
32✔
431
    } else
432
        this->goal_ = goal;
4✔
433

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

440
        // Wenn wir stehen, zusätzlich noch loslaufen!
441
        if(waiting_for_free_node)
19✔
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)
44✔
457
{
458
    RTTR_Assert(HasNoGoal());
44✔
459
    fs = FigureState::Wander;
44✔
460
    cur_rs = nullptr;
44✔
461
    rs_pos = 0;
44✔
462
    this->burned_wh_id = burned_wh_id;
44✔
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);
44✔
466
    // Soldaten sind härter im Nehmen
467
    wander_tryings = IsSoldier() ? WANDER_TRYINGS_SOLDIERS : WANDER_TRYINGS;
44✔
468

469
    // Wenn wir stehen, zusätzlich noch loslaufen!
470
    if(waiting_for_free_node)
44✔
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
}
44✔
480

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

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

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

507
    // Ist es mal wieder an der Zeit, eine Flagge zu suchen?
508
    if(!wander_way)
367✔
509
    {
510
        // Soldaten sind härter im Nehmen
511
        const unsigned short wander_radius = IsSoldier() ? WANDER_RADIUS_SOLDIERS : WANDER_RADIUS;
6✔
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));
6✔
516

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

520
        for(auto* flag : flags)
26✔
521
        {
522
            // Ist das ein Flüchtling aus einem abgebrannten Lagerhaus?
523
            if(burned_wh_id != 0xFFFFFFFF)
20✔
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))
19✔
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());
20✔
535
            if(way < best_way)
20✔
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))
6✔
539
                {
540
                    // gucken, ob ein Weg zu einem Warenhaus führt
541
                    if(world->GetPlayer(player).FindWarehouse(*flag, FW::AcceptsFigure(job_), true, false))
6✔
542
                    {
543
                        // dann nehmen wir die doch glatt
544
                        best_way = way;
6✔
545
                        best_flag = flag;
6✔
546
                        if(way == 0)
6✔
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)
6✔
563
        {
564
            // bestmögliche schließlich nehmen
565
            wander_way = 0xFFFF;
6✔
566
            flagPos_ = best_flag->GetPos();
6✔
567
            flag_obj_id = best_flag->GetObjId();
6✔
568
            WanderToFlag();
6✔
569
            return;
6✔
570
        }
571

572
        // Wurde keine Flagge gefunden?
573

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

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

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

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

619
void noFigure::WanderToFlag()
26✔
620
{
621
    // Existiert die Flagge überhaupt noch?
622
    noBase* no = world->GetNO(flagPos_);
26✔
623
    if(no->GetObjId() != flag_obj_id)
26✔
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_)
26✔
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);
21✔
659
    if(dir)
21✔
660
    {
661
        // weiter hinlaufen
662
        StartWalking(*dir);
21✔
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

764
    if(hasArmor_)
×
765
        DrawArmor(InterpolateWalkDrawPos(drawPt));
×
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;
×
812
        case Job::Skinner:
×
813
            DrawWalking(drawPt, "leather_bobs", leatheraddon::bobIndex[leatheraddon::BobType::SkinnerWalking]);
×
814
            break;
×
815
        case Job::Tanner:
×
816
            DrawWalking(drawPt, "leather_bobs", leatheraddon::bobIndex[leatheraddon::BobType::TannerWalking]);
×
817
            break;
×
818
        case Job::LeatherWorker:
×
819
            DrawWalking(drawPt, "leather_bobs", leatheraddon::bobIndex[leatheraddon::BobType::LeatherworkerWalking]);
×
820
            break;
×
821
        default: DrawWalkingBobJobs(drawPt, job_); break;
×
822
    }
823
}
×
824

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

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

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);
×
861
        if(isSoldier(GetJobType()) && hasArmor_)
×
862
            world->GetPlayer(player).DecreaseInventoryJob(jobEnumToAmoredSoldierEnum(GetJobType()), 1);
×
863
    }
864
}
×
865

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

875
void noFigure::NodeFreed(const MapPoint pt)
170✔
876
{
877
    // Stehen wir gerade aus diesem Grund?
878
    if(!waiting_for_free_node || pt != world->GetNeighbour(this->pos, GetCurMoveDir()))
170✔
879
        return;
169✔
880

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

889
    // Wir stehen nun nicht mehr
890
    waiting_for_free_node = false;
1✔
891

892
    // Dann loslaufen
893
    StartWalking(GetCurMoveDir());
1✔
894

895
    // anderen Leuten noch ggf Bescheid sagen
896
    world->RoadNodeAvailable(this->pos);
1✔
897
}
898

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

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

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

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

959
    pos = MapPoint::Invalid();
4✔
960
    on_ship = true;
4✔
961
}
4✔
962

963
void noFigure::ArrivedByShip(const MapPoint harborPos)
2✔
964
{
965
    RTTR_Assert(on_ship);
2✔
966
    pos = harborPos;
2✔
967
    on_ship = false;
2✔
968
}
2✔
969

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

981
    if(newDir == RoadPathDirection::None)
2✔
982
        Abrogate();
×
983

984
    // Going by ship?
985
    if(newDir == RoadPathDirection::Ship)
2✔
986
        // All ok, return next harbor (could be another one!)
987
        return next_harbor;
×
988
    else
989
        return MapPoint(0, 0);
2✔
990
}
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