1
/*
2
 *  The Mana World
3
 *  Copyright (C) 2004  The Mana World Development Team
4
 *
5
 *  This file is part of The Mana World.
6
 *
7
 *  This program is free software; you can redistribute it and/or modify
8
 *  it under the terms of the GNU General Public License as published by
9
 *  the Free Software Foundation; either version 2 of the License, or
10
 *  any later version.
11
 *
12
 *  This program is distributed in the hope that it will be useful,
13
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 *  GNU General Public License for more details.
16
 *
17
 *  You should have received a copy of the GNU General Public License
18
 *  along with this program; if not, write to the Free Software
19
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
 */
21
22
#include "being.h"
23
24
#include "animatedsprite.h"
25
#include "configuration.h"
26
#include "effectmanager.h"
27
#include "game.h"
28
#include "graphics.h"
29
#include "localplayer.h"
30
#include "log.h"
31
#include "map.h"
32
#include "particle.h"
33
#include "simpleanimation.h"
34
#include "sound.h"
35
#include "text.h"
36
#include "statuseffect.h"
37
38
#include "gui/speechbubble.h"
39
40
#include "resources/colordb.h"
41
#include "resources/emotedb.h"
42
#include "resources/image.h"
43
#include "resources/itemdb.h"
44
#include "resources/iteminfo.h"
45
#include "resources/resourcemanager.h"
46
47
#include "gui/gui.h"
48
#include "gui/palette.h"
49
#include "gui/speechbubble.h"
50
51
#include "utils/dtor.h"
52
#include "utils/stringutils.h"
53
#include "utils/xml.h"
54
55
#include <cassert>
56
#include <cmath>
57
58
#define BEING_EFFECTS_FILE "effects.xml"
59
#define HAIR_FILE "hair.xml"
60
61
static const int DEFAULT_BEING_WIDTH = 32;
62
static const int DEFAULT_BEING_HEIGHT = 32;
63
extern const int MILLISECONDS_IN_A_TICK;
64
65
66
int Being::mNumberOfHairstyles = 1;
67
68
Being::Being(int id, int job, Map *map):
69
#ifdef EATHENA_SUPPORT
70
    mWalkTime(0),
71
#endif
72
    mEmotion(0), mEmotionTime(0),
73
    mSpeechTime(0),
74
    mAttackSpeed(350),
75
    mAction(STAND),
76
    mJob(job),
77
    mId(id),
78
    mDirection(DOWN),
79
    mSpriteDirection(DIRECTION_DOWN),
80
    mMap(NULL),
81
    mParticleEffects(config.getValue("particleeffects", 1)),
82
    mDispName(0),
83
    mShowName(false),
84
    mEquippedWeapon(NULL),
85
    mText(0),
86
    mStunMode(0),
87
    mAlpha(1.0f),
88
    mStatusParticleEffects(&mStunParticleEffects, false),
89
    mChildParticleEffects(&mStatusParticleEffects, false),
90
    mMustResetParticles(false),
91
#ifdef TMWSERV_SUPPORT
92
    mWalkSpeed(6.0f), // default speed in tile per second
93
#else
94
    mWalkSpeed(150),
95
#endif
96
    mPx(0), mPy(0),
97
    mX(0), mY(0),
98
    mUsedTargetCursor(NULL)
99
{
100
    setMap(map);
101
102
    mSpeechBubble = new SpeechBubble;
103
104
    mNameColor = &guiPalette->getColor(Palette::NPC);
105
    mTextColor = &guiPalette->getColor(Palette::CHAT);
106
}
107
108
Being::~Being()
109
{
110
    mUsedTargetCursor = NULL;
111
    delete_all(mSprites);
112
113
    if (player_node && player_node->getTarget() == this)
114
        player_node->setTarget(NULL);
115
116
    setMap(NULL);
117
118
    delete mSpeechBubble;
119
    delete mDispName;
120
    delete mText;
121
}
122
123
void Being::setPosition(const Vector &pos)
124
{
125
    mPos = pos;
126
127
    // Update pixel coordinates (convert once, for performance reasons)
128
    mPx = (int) pos.x;
129
    mPy = (int) pos.y;
130
131
    updateCoords();
132
133
    if (mText)
134
        mText->adviseXY(mPx,
135
                        mPy - getHeight() - mText->getHeight() - 6);
136
}
137
138
#ifdef EATHENA_SUPPORT
139
void Being::setDestination(Uint16 destX, Uint16 destY)
140
{
141
    if (mMap)
142
        setPath(mMap->findPath(mX, mY, destX, destY, getWalkMask()));
143
}
144
#endif
145
146
#ifdef TMWSERV_SUPPORT
147
void Being::setDestination(int dstX, int dstY)
148
{
149
    mDest.x = dstX;
150
    mDest.y = dstY;
151
    int srcX = mPos.x;
152
    int srcY = mPos.y;
153
154
    Path thisPath;
155
156
    if (mMap)
157
    {
158
        thisPath = mMap->findPath(mPos.x / 32, mPos.y / 32,
159
                                  mDest.x / 32, mDest.y / 32, getWalkMask());
160
    }
161
162
    if (thisPath.empty())
163
    {
164
        setPath(Path());
165
        return;
166
    }
167
168
    // FIXME: Look into making this code neater.
169
    // Interpolate the offsets.  Also convert from tile based to pixel based
170
171
    // Find the starting offset
172
    float startX = (srcX % 32);
173
    float startY = (srcY % 32);
174
175
    // Find the ending offset
176
    float endX = (dstX % 32);
177
    float endY = (dstY % 32);
178
179
    // Find the distance, and divide it by the number of steps
180
    int changeX = (int)((endX - startX) / thisPath.size());
181
    int changeY = (int)((endY - startY) / thisPath.size());
182
183
    // Convert the map path to pixels over tiles
184
    // And add interpolation between the starting and ending offsets
185
    Path::iterator it = thisPath.begin();
186
    int i = 0;
187
    while(it != thisPath.end())
188
    {
189
       it->x = (it->x * 32) + startX + (changeX * i);
190
       it->y = (it->y * 32) + startY + (changeY * i);
191
       i++;
192
       it++;
193
    }
194
195
    // Remove the last path node, as it's more clever to go to mDest instead.
196
    // It also permit to avoid zigzag at the end of the path,
197
    // especially with mouse.
198
    thisPath.pop_back();
199
    thisPath.push_back(Position(mDest.x, mDest.y));
200
201
    setPath(thisPath);
202
}
203
#endif  // TMWSERV_SUPPORT
204
205
void Being::clearPath()
206
{
207
    mPath.clear();
208
}
209
210
void Being::setPath(const Path &path)
211
{
212
    mPath = path;
213
#ifdef EATHENA_SUPPORT
214
    if (mAction != WALK && mAction != DEAD)
215
    {
216
        nextStep();
217
        mWalkTime = tick_time;
218
    }
219
#endif
220
}
221
222
void Being::setSpeech(const std::string &text, int time)
223
{
224
    // Remove colors
225
    mSpeech = removeColors(text);
226
227
    // Trim whitespace
228
    trim(mSpeech);
229
230
    // Check for links
231
    std::string::size_type start = mSpeech.find('[');
232
    std::string::size_type end = mSpeech.find(']', start);
233
234
    while (start != std::string::npos && end != std::string::npos)
235
    {
236
        // Catch multiple embeds and ignore them so it doesn't crash the client.
237
        while ((mSpeech.find('[', start + 1) != std::string::npos) &&
238
               (mSpeech.find('[', start + 1) < end))
239
        {
240
            start = mSpeech.find('[', start + 1);
241
        }
242
243
        std::string::size_type position = mSpeech.find('|');
244
        if (mSpeech[start + 1] == '@' && mSpeech[start + 2] == '@')
245
        {
246
            mSpeech.erase(end, 1);
247
            mSpeech.erase(start, (position - start) + 1);
248
        }
249
        position = mSpeech.find('@');
250
251
        while (position != std::string::npos)
252
        {
253
            mSpeech.erase(position, 2);
254
            position = mSpeech.find('@');
255
        }
256
257
        start = mSpeech.find('[', start + 1);
258
        end = mSpeech.find(']', start);
259
    }
260
261
    if (!mSpeech.empty())
262
        mSpeechTime = time <= SPEECH_MAX_TIME ? time : SPEECH_MAX_TIME;
263
264
    const int speech = (int) config.getValue("speech", TEXT_OVERHEAD);
265
    if (speech == TEXT_OVERHEAD)
266
    {
267
        if (mText)
268
            delete mText;
269
270
        mText = new Text(mSpeech,
271
                         mPx, mPy - getHeight(),
272
                         gcn::Graphics::CENTER,
273
                         &guiPalette->getColor(Palette::PARTICLE),
274
                         true);
275
    }
276
}
277
278
void Being::takeDamage(Being *attacker, int amount, AttackType type)
279
{
280
    gcn::Font *font;
281
    std::string damage = amount ? toString(amount) : type == FLEE ?
282
            "dodge" : "miss";
283
    const gcn::Color *color;
284
285
    font = gui->getInfoParticleFont();
286
287
    // Selecting the right color
288
    if (type == CRITICAL || type == FLEE)
289
    {
290
        color = &guiPalette->getColor(Palette::HIT_CRITICAL);
291
    }
292
    else if (!amount)
293
    {
294
        if (attacker == player_node)
295
        {
296
            // This is intended to be the wrong direction to visually
297
            // differentiate between hits and misses
298
            color = &guiPalette->getColor(Palette::HIT_MONSTER_PLAYER);
299
        }
300
        else
301
        {
302
            color = &guiPalette->getColor(Palette::MISS);
303
        }
304
    }
305
    else if (getType() == MONSTER)
306
    {
307
        color = &guiPalette->getColor(Palette::HIT_PLAYER_MONSTER);
308
    }
309
    else
310
    {
311
        color = &guiPalette->getColor(Palette::HIT_MONSTER_PLAYER);
312
    }
313
314
    // Show damage number
315
    particleEngine->addTextSplashEffect(damage,
316
                                        mPx, mPy - 16,
317
                                        color, font, true);
318
319
    if (amount > 0)
320
    {
321
        if (type != CRITICAL)
322
        {
323
            effectManager->trigger(26, this);
324
        }
325
        else
326
        {
327
            effectManager->trigger(28, this);
328
        }
329
    }
330
}
331
332
void Being::handleAttack(Being *victim, int damage, AttackType type)
333
{
334
    if (this != player_node)
335
        setAction(Being::ATTACK, 1);
336
    if (getType() == PLAYER && victim)
337
    {
338
        if (mEquippedWeapon && mEquippedWeapon->getAttackType() == ACTION_ATTACK_BOW)
339
        {
340
            Particle *p = new Particle(NULL);
341
            p->setLifetime(1000);
342
            p->moveBy(Vector(0.0f, 0.0f, 32.0f));
343
            victim->controlParticle(p);
344
345
            Particle *p2 = particleEngine->addEffect("graphics/particles/arrow.particle.xml", mPx, mPy);
346
            if (p2)
347
            {
348
                p2->setLifetime(900);
349
                p2->setDestination(p, 7, 0);
350
                p2->setDieDistance(8);
351
            }
352
        }
353
    }
354
#ifdef EATHENA_SUPPORT
355
    mFrame = 0;
356
    mWalkTime = tick_time;
357
#endif
358
}
359
360
void Being::setName(const std::string &name)
361
{
362
    mName = name;
363
364
    if (getShowName())
365
        showName();
366
}
367
368
void Being::setShowName(bool doShowName)
369
{
370
    bool oldShow = mShowName;
371
    mShowName = doShowName;
372
373
    if (doShowName != oldShow)
374
    {
375
        if (doShowName)
376
            showName();
377
        else
378
        {
379
            delete mDispName;
380
            mDispName = 0;
381
        }
382
    }
383
}
384
385
void Being::setMap(Map *map)
386
{
387
    // Remove sprite from potential previous map
388
    if (mMap)
389
        mMap->removeSprite(mMapSprite);
390
391
    mMap = map;
392
393
    // Add sprite to potential new map
394
    if (mMap)
395
        mMapSprite = mMap->addSprite(this);
396
397
    // Clear particle effect list because child particles became invalid
398
    mChildParticleEffects.clear();
399
    mMustResetParticles = true; // Reset status particles on next redraw
400
}
401
402
void Being::controlParticle(Particle *particle)
403
{
404
    mChildParticleEffects.addLocally(particle);
405
}
406
407
void Being::setAction(Action action, int attackType)
408
{
409
    SpriteAction currentAction = ACTION_INVALID;
410
411
    switch (action)
412
    {
413
        case WALK:
414
            currentAction = ACTION_WALK;
415
            break;
416
        case SIT:
417
            currentAction = ACTION_SIT;
418
            break;
419
        case ATTACK:
420
            if (mEquippedWeapon)
421
                currentAction = mEquippedWeapon->getAttackType();
422
            else
423
                currentAction = ACTION_ATTACK;
424
425
            for (SpriteIterator it = mSprites.begin(); it != mSprites.end(); it++)
426
                if (*it)
427
                    (*it)->reset();
428
            break;
429
        case HURT:
430
            //currentAction = ACTION_HURT;  // Buggy: makes the player stop
431
                                            // attacking and unable to attack
432
                                            // again until he moves
433
            break;
434
        case DEAD:
435
            currentAction = ACTION_DEAD;
436
            break;
437
        case STAND:
438
            currentAction = ACTION_STAND;
439
            break;
440
    }
441
442
    if (currentAction != ACTION_INVALID)
443
    {
444
        for (SpriteIterator it = mSprites.begin(); it != mSprites.end(); it++)
445
            if (*it)
446
                (*it)->play(currentAction);
447
        mAction = action;
448
    }
449
}
450
451
void Being::setDirection(Uint8 direction)
452
{
453
    if (mDirection == direction)
454
        return;
455
456
    mDirection = direction;
457
458
    // if the direction does not change much, keep the common component
459
    int mFaceDirection = mDirection & direction;
460
    if (!mFaceDirection)
461
        mFaceDirection = direction;
462
463
    SpriteDirection dir;
464
    if (mFaceDirection & UP)
465
        dir = DIRECTION_UP;
466
    else if (mFaceDirection & DOWN)
467
        dir = DIRECTION_DOWN;
468
    else if (mFaceDirection & RIGHT)
469
        dir = DIRECTION_RIGHT;
470
    else
471
        dir = DIRECTION_LEFT;
472
    mSpriteDirection = dir;
473
474
    for (SpriteIterator it = mSprites.begin(); it != mSprites.end(); it++)
475
        if (*it)
476
           (*it)->setDirection(dir);
477
}
478
479
#ifdef EATHENA_SUPPORT
480
void Being::nextStep()
481
{
482
    if (mPath.empty())
483
    {
484
        setAction(STAND);
485
        return;
486
    }
487
488
    Position pos = mPath.front();
489
    mPath.pop_front();
490
491
    int dir = 0;
492
    if (pos.x > mX)
493
        dir |= RIGHT;
494
    else if (pos.x < mX)
495
        dir |= LEFT;
496
    if (pos.y > mY)
497
        dir |= DOWN;
498
    else if (pos.y < mY)
499
        dir |= UP;
500
501
    setDirection(dir);
502
503
    if (!mMap->getWalk(pos.x, pos.y, getWalkMask()))
504
    {
505
        setAction(STAND);
506
        return;
507
    }
508
509
    mX = pos.x;
510
    mY = pos.y;
511
    setAction(WALK);
512
    mWalkTime += mWalkSpeed / 10;
513
}
514
#endif
515
516
void Being::logic()
517
{
518
    // Reduce the time that speech is still displayed
519
    if (mSpeechTime > 0)
520
        mSpeechTime--;
521
522
    // Remove text and speechbubbles if speech boxes aren't being used
523
    if (mSpeechTime == 0 && mText)
524
    {
525
        delete mText;
526
        mText = 0;
527
    }
528
529
#ifdef TMWSERV_SUPPORT
530
    if (mAction != DEAD)
531
    {
532
        const Vector dest = (mPath.empty()) ?
533
            mDest : Vector(mPath.front().x,
534
                           mPath.front().y);
535
536
        // The Vector representing the difference between current position
537
        // and the next destination path node.
538
        Vector dir = dest - mPos;
539
540
        const float nominalLength = dir.length();
541
542
        // When we've not reached our destination, move to it.
543
        if (nominalLength > 1.0f && mWalkSpeed > 0.0f)
544
        {
545
546
            // The private mWalkSpeed member is the speed in tiles per second.
547
            // We translate it into pixels per tick,
548
            // because the logic is called every ticks.
549
            const float speedInTicks = ((float)DEFAULT_TILE_SIDE_LENGTH * mWalkSpeed)
550
                                      / 1000 * (float)MILLISECONDS_IN_A_TICK;
551
552
            // The deplacement of a point along a vector is calculated
553
            // using the Unit Vector (â) multiplied by the point speed.
554
            // â = a / ||a|| (||a|| is the a length.)
555
            // Then, diff = (dir/||dir||)*speed, or (dir / ||dir|| / 1/speed).
556
            Vector diff = (dir / (nominalLength / speedInTicks));
557
558
            // Test if we don't miss the destination by a move too far:
559
            if (diff.length() > nominalLength)
560
            {
561
                setPosition(mPos + dir);
562
563
                // Also, if the destination is reached, try to get the next
564
                // path point, if existing.
565
                if (!mPath.empty())
566
                    mPath.pop_front();
567
            }
568
            // Otherwise, go to it using the nominal speed.
569
            else
570
                setPosition(mPos + diff);
571
572
            if (mAction != WALK)
573
                setAction(WALK);
574
575
            // Update the player sprite direction
576
            int direction = 0;
577
            const float dx = std::abs(dir.x);
578
            float dy = std::abs(dir.y);
579
580
            // When not using mouse for the player, we slightly prefer
581
            // UP and DOWN position, especially when walking diagonally.
582
            if (this == player_node && !player_node->isPathSetByMouse())
583
                dy = dy + 2;
584
585
            if (dx > dy)
586
                 direction |= (dir.x > 0) ? RIGHT : LEFT;
587
            else
588
                 direction |= (dir.y > 0) ? DOWN : UP;
589
            setDirection(direction);
590
        }
591
        else if (!mPath.empty())
592
        {
593
            // If the current path node has been reached,
594
            // remove it and go to the next one.
595
            mPath.pop_front();
596
        }
597
        else if (mAction == WALK)
598
        {
599
            setAction(STAND);
600
        }
601
    }
602
#else
603
    // Update pixel coordinates
604
    setPosition(mX * 32 + 16 + getXOffset(),
605
                mY * 32 + 32 + getYOffset());
606
#endif
607
608
    if (mEmotion != 0)
609
    {
610
        mEmotionTime--;
611
        if (mEmotionTime == 0)
612
            mEmotion = 0;
613
    }
614
615
    // Update sprite animations
616
    if (mUsedTargetCursor)
617
        mUsedTargetCursor->update(tick_time * 10);
618
619
    for (SpriteIterator it = mSprites.begin(); it != mSprites.end(); it++)
620
        if (*it)
621
            (*it)->update(tick_time * 10);
622
623
    // Restart status/particle effects, if needed
624
    if (mMustResetParticles) {
625
        mMustResetParticles = false;
626
        for (std::set<int>::iterator it = mStatusEffects.begin();
627
             it != mStatusEffects.end(); it++) {
628
            const StatusEffect *effect = StatusEffect::getStatusEffect(*it, true);
629
            if (effect && effect->particleEffectIsPersistent())
630
                updateStatusEffect(*it, true);
631
        }
632
    }
633
634
    // Update particle effects
635
    mChildParticleEffects.moveTo(mPos.x, mPos.y);
636
}
637
638
void Being::draw(Graphics *graphics, int offsetX, int offsetY) const
639
{
640
    // TODO: Eventually, we probably should fix all sprite offsets so that
641
    //       these translations aren't necessary anymore. The sprites know
642
    //       best where their centerpoint should be.
643
    const int px = mPx + offsetX - 16;
644
    const int py = mPy + offsetY - 32;
645
646
    if (mUsedTargetCursor)
647
        mUsedTargetCursor->draw(graphics, px, py);
648
649
    for (SpriteConstIterator it = mSprites.begin(); it != mSprites.end(); it++)
650
        if (*it)
651
        {
652
            if ((*it)->getAlpha() != mAlpha)
653
                (*it)->setAlpha(mAlpha);
654
            (*it)->draw(graphics, px, py);
655
        }
656
}
657
658
void Being::drawEmotion(Graphics *graphics, int offsetX, int offsetY)
659
{
660
    if (!mEmotion)
661
        return;
662
663
    const int px = mPx - offsetX - 16;
664
    const int py = mPy - offsetY - 64 - 32;
665
    const int emotionIndex = mEmotion - 1;
666
667
    if (emotionIndex >= 0 && emotionIndex <= EmoteDB::getLast())
668
        EmoteDB::getAnimation(emotionIndex)->draw(graphics, px, py);
669
}
670
671
void Being::drawSpeech(int offsetX, int offsetY)
672
{
673
    const int px = mPx - offsetX;
674
    const int py = mPy - offsetY;
675
    const int speech = (int) config.getValue("speech", TEXT_OVERHEAD);
676
677
    // Draw speech above this being
678
    if (mSpeechTime == 0)
679
    {
680
        if (mSpeechBubble->isVisible())
681
            mSpeechBubble->setVisible(false);
682
    }
683
    else if (mSpeechTime > 0 && (speech == NAME_IN_BUBBLE ||
684
             speech == NO_NAME_IN_BUBBLE))
685
    {
686
        const bool showName = (speech == NAME_IN_BUBBLE);
687
688
        if (mText)
689
        {
690
            delete mText;
691
            mText = NULL;
692
        }
693
694
        mSpeechBubble->setCaption(showName ? mName : "", mTextColor);
695
696
        mSpeechBubble->setText(mSpeech, showName);
697
        mSpeechBubble->setPosition(px - (mSpeechBubble->getWidth() / 2),
698
                                   py - getHeight() - (mSpeechBubble->getHeight()));
699
        mSpeechBubble->setVisible(true);
700
    }
701
    else if (mSpeechTime > 0 && speech == TEXT_OVERHEAD)
702
    {
703
        mSpeechBubble->setVisible(false);
704
705
        if (! mText) {
706
            mText = new Text(mSpeech,
707
                             mPx, mPy - getHeight(),
708
                             gcn::Graphics::CENTER,
709
                             &guiPalette->getColor(Palette::PARTICLE),
710
                             true);
711
        }
712
    }
713
    else if (speech == NO_SPEECH)
714
    {
715
        mSpeechBubble->setVisible(false);
716
717
        if (mText)
718
            delete mText;
719
720
        mText = NULL;
721
    }
722
}
723
724
void Being::setStatusEffectBlock(int offset, Uint16 newEffects)
725
{
726
    for (int i = 0; i < STATUS_EFFECTS; i++) {
727
        int index = StatusEffect::blockEffectIndexToEffectIndex(offset + i);
728
729
        if (index != -1)
730
            setStatusEffect(index, (newEffects & (1 << i)) > 0);
731
    }
732
}
733
734
void Being::handleStatusEffect(StatusEffect *effect, int effectId)
735
{
736
    if (!effect)
737
        return;
738
739
    // TODO: Find out how this is meant to be used
740
    // (SpriteAction != Being::Action)
741
    //SpriteAction action = effect->getAction();
742
    //if (action != ACTION_INVALID)
743
    //    setAction(action);
744
745
    Particle *particle = effect->getParticle();
746
747
    if (effectId >= 0)
748
        mStatusParticleEffects.setLocally(effectId, particle);
749
    else {
750
        mStunParticleEffects.clearLocally();
751
        if (particle)
752
            mStunParticleEffects.addLocally(particle);
753
    }
754
}
755
756
void Being::updateStunMode(int oldMode, int newMode)
757
{
758
    handleStatusEffect(StatusEffect::getStatusEffect(oldMode, false), -1);
759
    handleStatusEffect(StatusEffect::getStatusEffect(newMode, true), -1);
760
}
761
762
void Being::updateStatusEffect(int index, bool newStatus)
763
{
764
    handleStatusEffect(StatusEffect::getStatusEffect(index, newStatus), index);
765
}
766
767
void Being::setStatusEffect(int index, bool active)
768
{
769
    const bool wasActive = mStatusEffects.find(index) != mStatusEffects.end();
770
771
    if (active != wasActive) {
772
        updateStatusEffect(index, active);
773
        if (active)
774
            mStatusEffects.insert(index);
775
        else
776
            mStatusEffects.erase(index);
777
    }
778
}
779
780
#ifdef EATHENA_SUPPORT
781
int Being::getOffset(char pos, char neg) const
782
{
783
    // Check whether we're walking in the requested direction
784
    if (mAction != WALK ||  !(mDirection & (pos | neg)))
785
        return 0;
786
787
    int offset = (get_elapsed_time(mWalkTime) * 32) / mWalkSpeed;
788
789
    // We calculate the offset _from_ the _target_ location
790
    offset -= 32;
791
    if (offset > 0)
792
        offset = 0;
793
794
    // Going into negative direction? Invert the offset.
795
    if (mDirection & pos)
796
        offset = -offset;
797
798
    return offset;
799
}
800
#endif
801
802
int Being::getWidth() const
803
{
804
    AnimatedSprite *base = NULL;
805
806
    for (SpriteConstIterator it = mSprites.begin(); it != mSprites.end(); it++)
807
        if ((base = (*it)))
808
            break;
809
810
    if (base)
811
        return std::max(base->getWidth(), DEFAULT_BEING_WIDTH);
812
    else
813
        return DEFAULT_BEING_WIDTH;
814
}
815
816
int Being::getHeight() const
817
{
818
    AnimatedSprite *base = NULL;
819
820
    for (SpriteConstIterator it = mSprites.begin(); it != mSprites.end(); it++)
821
        if ((base = (*it)))
822
            break;
823
824
    if (base)
825
        return std::max(base->getHeight(), DEFAULT_BEING_HEIGHT);
826
    else
827
        return DEFAULT_BEING_HEIGHT;
828
}
829
830
void Being::setTargetAnimation(SimpleAnimation* animation)
831
{
832
    mUsedTargetCursor = animation;
833
    mUsedTargetCursor->reset();
834
}
835
836
struct EffectDescription {
837
    std::string mGFXEffect;
838
    std::string mSFXEffect;
839
};
840
841
static EffectDescription *default_effect = NULL;
842
static std::map<int, EffectDescription *> effects;
843
static bool effects_initialized = false;
844
845
static EffectDescription *getEffectDescription(xmlNodePtr node, int *id)
846
{
847
    EffectDescription *ed = new EffectDescription;
848
849
    *id = atoi(XML::getProperty(node, "id", "-1").c_str());
850
    ed->mSFXEffect = XML::getProperty(node, "audio", "");
851
    ed->mGFXEffect = XML::getProperty(node, "particle", "");
852
853
    return ed;
854
}
855
856
static EffectDescription *getEffectDescription(int effectId)
857
{
858
    if (!effects_initialized)
859
    {
860
        XML::Document doc(BEING_EFFECTS_FILE);
861
        xmlNodePtr root = doc.rootNode();
862
863
        if (!root || !xmlStrEqual(root->name, BAD_CAST "being-effects"))
864
        {
865
            logger->log("Error loading being effects file: "
866
                    BEING_EFFECTS_FILE);
867
            return NULL;
868
        }
869
870
        for_each_xml_child_node(node, root)
871
        {
872
            int id;
873
874
            if (xmlStrEqual(node->name, BAD_CAST "effect"))
875
            {
876
                EffectDescription *EffectDescription =
877
                    getEffectDescription(node, &id);
878
                effects[id] = EffectDescription;
879
            } else if (xmlStrEqual(node->name, BAD_CAST "default"))
880
            {
881
                EffectDescription *EffectDescription =
882
                    getEffectDescription(node, &id);
883
884
                if (default_effect)
885
                    delete default_effect;
886
887
                default_effect = EffectDescription;
888
            }
889
        }
890
891
        effects_initialized = true;
892
    } // done initializing
893
894
    EffectDescription *ed = effects[effectId];
895
896
    if (!ed)
897
        return default_effect;
898
    else
899
        return ed;
900
}
901
902
void Being::internalTriggerEffect(int effectId, bool sfx, bool gfx)
903
{
904
    logger->log("Special effect #%d on %s", effectId,
905
                getId() == player_node->getId() ? "self" : "other");
906
907
    EffectDescription *ed = getEffectDescription(effectId);
908
909
    if (!ed) {
910
        logger->log("Unknown special effect and no default recorded");
911
        return;
912
    }
913
914
    if (gfx && !ed->mGFXEffect.empty()) {
915
        Particle *selfFX;
916
917
        selfFX = particleEngine->addEffect(ed->mGFXEffect, 0, 0);
918
        controlParticle(selfFX);
919
    }
920
921
    if (sfx && !ed->mSFXEffect.empty()) {
922
        sound.playSfx(ed->mSFXEffect);
923
    }
924
}
925
926
void Being::updateCoords()
927
{
928
    if (mDispName)
929
    {
930
        mDispName->adviseXY(getPixelX(), getPixelY());
931
    }
932
}
933
934
void Being::flashName(int time)
935
{
936
    if (mDispName)
937
        mDispName->flash(time);
938
}
939
940
void Being::showName()
941
{
942
    delete mDispName;
943
    mDispName = 0;
944
945
    mDispName = new FlashText(mName, getPixelX(), getPixelY(),
946
                             gcn::Graphics::CENTER, mNameColor);
947
}
948
949
int Being::getNumberOfLayers() const
950
{
951
    return mSprites.size();
952
}
953
954
void Being::load()
955
{
956
    // Hairstyles are encoded as negative numbers. Count how far negative
957
    // we can go.
958
    int hairstyles = 1;
959
960
    while (ItemDB::get(-hairstyles).getSprite(GENDER_MALE) != "error.xml")
961
        hairstyles++;
962
963
    mNumberOfHairstyles = hairstyles;
964
}