diff options
| author | fusion32 <marcopuzziello@gmail.com> | 2025-06-05 02:00:10 -0300 |
|---|---|---|
| committer | fusion32 <marcopuzziello@gmail.com> | 2025-06-05 02:00:10 -0300 |
| commit | 3c8bcba6b540f5cb84572994d9ec2426dd348a93 (patch) | |
| tree | 54af53f02d549951446d5a87845b42d4ee777559 /src/magic.cc | |
| parent | 2c91caa1dd4ea53dfeb63751d04170d9a9a61474 (diff) | |
| download | game-3c8bcba6b540f5cb84572994d9ec2426dd348a93.tar.gz game-3c8bcba6b540f5cb84572994d9ec2426dd348a93.zip | |
finish `magic.cc`
Diffstat (limited to 'src/magic.cc')
| -rw-r--r-- | src/magic.cc | 1235 |
1 files changed, 1175 insertions, 60 deletions
diff --git a/src/magic.cc b/src/magic.cc index 3db1456..b3d70b1 100644 --- a/src/magic.cc +++ b/src/magic.cc @@ -7,6 +7,26 @@ #include "stubs.hh" #include <fstream> +#include <sstream> + +struct TCircle { + int x[32]; + int y[32]; + int Count; +}; + +struct TSpellList { + uint8 Syllable[10]; + uint8 RuneGr; + uint8 RuneNr; + const char *Comment; + uint16 Level; + uint16 RuneLevel; + uint16 Flags; + int Mana; + int SoulPoints; + int Amount; +}; static TSpellList SpellList[256]; static TCircle Circle[10]; @@ -593,44 +613,6 @@ void AngleShapeSpell(TCreature *Actor, int Angle, int Range, TImpact *Impact, in } } -// Spell Casting -// ============================================================================= -int GetDirection(int dx, int dy){ - // TODO(fusion): This function originally returned directions different from - // the ones used by creatures. I've converted it to use the same values which - // are also defined in `enums.hh` for simplicity. - int Result; - if(dx == 0){ - if(dy < 0){ - Result = DIRECTION_NORTH; - }else if(dy > 0){ - Result = DIRECTION_SOUTH; - }else{ - Result = DIRECTION_INVALID; - } - }else{ - // NOTE(fusion): This function uses the approximate tangent value, avoiding - // floating point calculations, for whatever reason. The tangent is unique - // and odd in the interval (-PI/2, +PI/2). We also need to recall that the - // Y-axis is inverted in Tibia, so we need to negate `dy`. - constexpr int Tangent_67_5 = 618; // => 618 / 256 ~ 2.41 ~ tan(67.5 deg) - constexpr int Tangent_22_5 = 106; // => 106 / 256 ~ 0.41 ~ tan(22.5 deg) - int Tangent = (-dy * 256) / dx; // => (dy * 256) / dx ~ (dy / dx) * 256 - if(Tangent >= Tangent_67_5){ - Result = DIRECTION_NORTH; - }else if(Tangent >= Tangent_22_5){ - Result = (dx < 0) ? DIRECTION_NORTHWEST : DIRECTION_NORTHEAST; - }else if(Tangent >= -Tangent_22_5){ - Result = (dx < 0) ? DIRECTION_WEST : DIRECTION_EAST; - }else if(Tangent >= -Tangent_67_5){ - Result = (dx < 0) ? DIRECTION_SOUTHWEST : DIRECTION_SOUTHEAST; - }else{ - Result = DIRECTION_SOUTH; - } - } - return Result; -} - void CheckSpellbook(TCreature *Actor, int SpellNr){ if(Actor == NULL){ error("CheckSpellbook: Übergebene Kreatur existiert nicht.\n"); @@ -908,6 +890,42 @@ void Combat(TCreature *Actor, Object Target, int ManaPoints, int SoulPoints, INT_MAX, Animation, 0, &Impact, Effect); } +int GetDirection(int dx, int dy){ + // TODO(fusion): This function originally returned directions different from + // the ones used by creatures. I've converted it to use the same values which + // are also defined in `enums.hh` for simplicity. + int Result; + if(dx == 0){ + if(dy < 0){ + Result = DIRECTION_NORTH; + }else if(dy > 0){ + Result = DIRECTION_SOUTH; + }else{ + Result = DIRECTION_INVALID; + } + }else{ + // NOTE(fusion): This function uses the approximate tangent value, avoiding + // floating point calculations, for whatever reason. The tangent is unique + // and odd in the interval (-PI/2, +PI/2). We also need to recall that the + // Y-axis is inverted in Tibia, so we need to negate `dy`. + constexpr int Tangent_67_5 = 618; // => 618 / 256 ~ 2.41 ~ tan(67.5 deg) + constexpr int Tangent_22_5 = 106; // => 106 / 256 ~ 0.41 ~ tan(22.5 deg) + int Tangent = (-dy * 256) / dx; // => (dy * 256) / dx ~ (dy / dx) * 256 + if(Tangent >= Tangent_67_5){ + Result = DIRECTION_NORTH; + }else if(Tangent >= Tangent_22_5){ + Result = (dx < 0) ? DIRECTION_NORTHWEST : DIRECTION_NORTHEAST; + }else if(Tangent >= -Tangent_22_5){ + Result = (dx < 0) ? DIRECTION_WEST : DIRECTION_EAST; + }else if(Tangent >= -Tangent_67_5){ + Result = (dx < 0) ? DIRECTION_SOUTHWEST : DIRECTION_SOUTHEAST; + }else{ + Result = DIRECTION_SOUTH; + } + } + return Result; +} + // Spell Functions // ============================================================================= void KillAllMonsters(TCreature *Actor, int Effect, int Radius){ @@ -966,6 +984,72 @@ void KillAllMonsters(TCreature *Actor, int Effect, int Radius){ } } +void CreateField(int x, int y, int z, int FieldType, uint32 Owner, bool Peaceful){ + if(!FieldPossible(x, y, z, FieldType)){ + return; + } + + SPECIALMEANING Meaning; + switch(FieldType){ + case FIELD_TYPE_FIRE:{ + if(Peaceful){ + Meaning = MAGICFIELD_FIRE_HARMLESS; + }else{ + Meaning = MAGICFIELD_FIRE_DANGEROUS; + } + break; + } + + case FIELD_TYPE_POISON:{ + if(Peaceful){ + Meaning = MAGICFIELD_POISON_HARMLESS; + }else{ + Meaning = MAGICFIELD_POISON_DANGEROUS; + } + break; + } + + case FIELD_TYPE_ENERGY:{ + if(Peaceful){ + Meaning = MAGICFIELD_ENERGY_HARMLESS; + }else{ + Meaning = MAGICFIELD_ENERGY_DANGEROUS; + } + break; + } + + case FIELD_TYPE_MAGICWALL:{ + Meaning = MAGICFIELD_MAGICWALL; + break; + } + + case FIELD_TYPE_WILDGROWTH:{ + Meaning = MAGICFIELD_RUSHWOOD; + break; + } + + default:{ + error("CreateField: Ungültiger Feldtyp %d.\n", FieldType); + throw ERROR; + } + } + + // NOTE(fusion): Delete other magic fields? + Object Obj = GetFirstObject(x, y, z); + while(Obj != NONE){ + Object Next = Obj.getNextObject(); + if(Obj.getObjectType().getFlag(MAGICFIELD)){ + Delete(Obj, -1); + } + Obj = Next; + } + + // NOTE(fusion): Create field, at last. + Create(GetMapContainer(x, y, z), + GetSpecialObject(Meaning), + Owner); +} + void CreateField(TCreature *Actor, Object Target, int ManaPoints, int SoulPoints, int FieldType){ if(Actor == NULL){ error("CreateField: Ungültige Kreatur übergeben.\n"); @@ -993,7 +1077,7 @@ void CreateField(TCreature *Actor, Object Target, int ManaPoints, int SoulPoints CheckMana(Actor, ManaPoints, SoulPoints, Delay); int Animation = ANIMATION_ENERGY; - if(FieldType == 1){ + if(FieldType == FIELD_TYPE_FIRE){ Animation = ANIMATION_FIRE; } Missile(Actor->CrObject, Target, Animation); @@ -1001,8 +1085,9 @@ void CreateField(TCreature *Actor, Object Target, int ManaPoints, int SoulPoints bool Peaceful = (WorldType == NON_PVP && Actor->IsPeaceful()); CreateField(TargetX, TargetY, TargetZ, FieldType, Actor->ID, Peaceful); - // TODO(fusion): Probably damaging fields? - if(FieldType == 1 || FieldType == 2 || FieldType == 3){ + if(FieldType == FIELD_TYPE_FIRE + || FieldType == FIELD_TYPE_POISON + || FieldType == FIELD_TYPE_ENERGY){ Actor->BlockLogout(60, true); } } @@ -1058,7 +1143,7 @@ void MassCreateField(TCreature *Actor, Object Target, if(Actor->posx != TargetX || Actor->posy != TargetY || Actor->posz != TargetZ){ int Animation = ANIMATION_ENERGY; - if(FieldType == 1){ + if(FieldType == FIELD_TYPE_FIRE){ Animation = ANIMATION_FIRE; } Missile(Actor->CrObject, Target, Animation); @@ -1089,7 +1174,9 @@ void MassCreateField(TCreature *Actor, Object Target, } } - if(FieldType == 1 || FieldType == 2 || FieldType == 3){ + if(FieldType == FIELD_TYPE_FIRE + || FieldType == FIELD_TYPE_POISON + || FieldType == FIELD_TYPE_ENERGY){ Actor->BlockLogout(60, true); } } @@ -1121,7 +1208,7 @@ void CreateFieldWall(TCreature *Actor, Object Target, if(ActorX != TargetX || ActorY != TargetY || ActorZ != TargetZ){ int Animation = ANIMATION_ENERGY; - if(FieldType == 1){ + if(FieldType == FIELD_TYPE_FIRE){ Animation = ANIMATION_FIRE; } Missile(Actor->CrObject, Target, Animation); @@ -1216,7 +1303,9 @@ void CreateFieldWall(TCreature *Actor, Object Target, } } - if(FieldType == 1 || FieldType == 2 || FieldType == 3){ + if(FieldType == FIELD_TYPE_FIRE + || FieldType == FIELD_TYPE_POISON + || FieldType == FIELD_TYPE_ENERGY){ Actor->BlockLogout(60, true); } } @@ -1321,7 +1410,7 @@ void CleanupField(TCreature *Actor){ CleanupField(Actor, Target, 0, 0); } -void Teleport(TCreature *Actor, char *Param){ +void Teleport(TCreature *Actor, const char *Param){ if(Actor == NULL){ error("Teleport: Ungültige Kreatur übergeben.\n"); throw ERROR; @@ -1414,7 +1503,7 @@ void Teleport(TCreature *Actor, char *Param){ } Object Dest = GetMapContainer(DestX, DestY, DestZ); Move(0, Actor->CrObject, Dest, -1, false, NONE); - GraphicalEffect(DestX, DestY, DestZ, EFFECT_TELEPORT); + GraphicalEffect(DestX, DestY, DestZ, EFFECT_ENERGY); }else if(MDGoStrength != Actor->Skills[SKILL_GO_STRENGTH]->MDAct){ Actor->Skills[SKILL_GO_STRENGTH]->SetMDAct(MDGoStrength); AnnounceChangedCreature(Actor->ID, 4); // CREATURE_GO_STRENGTH_CHANGED ? @@ -1455,7 +1544,7 @@ void TeleportToCreature(TCreature *Actor, const char *Name){ } if(Actor == Player){ - GraphicalEffect(Actor->CrObject, EFFECT_TELEPORT); + GraphicalEffect(Actor->CrObject, EFFECT_ENERGY); return; } @@ -1475,7 +1564,7 @@ void TeleportToCreature(TCreature *Actor, const char *Name){ Object Dest = GetMapContainer(DestX, DestY, DestZ); Move(0, Actor->CrObject, Dest, -1, false, NONE); - GraphicalEffect(DestX, DestY, DestZ, EFFECT_TELEPORT); + GraphicalEffect(DestX, DestY, DestZ, EFFECT_ENERGY); Log("banish", "%s teleportiert sich zu %s.\n", Actor->Name, Player->Name); } @@ -1523,7 +1612,7 @@ void TeleportPlayerToMe(TCreature *Actor, const char *Name){ Object Dest = GetMapContainer(DestX, DestY, DestZ); Move(0, Player->CrObject, Dest, -1, false, NONE); - GraphicalEffect(DestX, DestY, DestZ, EFFECT_TELEPORT); + GraphicalEffect(DestX, DestY, DestZ, EFFECT_ENERGY); } void MagicRope(TCreature *Actor, int ManaPoints, int SoulPoints){ @@ -1543,7 +1632,7 @@ void MagicRope(TCreature *Actor, int ManaPoints, int SoulPoints){ Object Dest = GetMapContainer(OrigX, OrigY + 1, OrigZ - 1); Move(0, Actor->CrObject, Dest, -1, false, NONE); - GraphicalEffect(OrigX, OrigY, OrigZ, EFFECT_TELEPORT); + GraphicalEffect(OrigX, OrigY, OrigZ, EFFECT_ENERGY); } void MagicClimbing(TCreature *Actor, int ManaPoints, int SoulPoints, const char *Param){ @@ -1594,7 +1683,7 @@ void MagicClimbing(TCreature *Actor, int ManaPoints, int SoulPoints, const char Object Dest = GetMapContainer(DestX, DestY, DestZ); Move(0, Actor->CrObject, Dest, -1, false, NONE); - GraphicalEffect(OrigX, OrigY, OrigZ, EFFECT_TELEPORT); + GraphicalEffect(OrigX, OrigY, OrigZ, EFFECT_ENERGY); } void MagicClimbing(TCreature *Actor, const char *Param){ @@ -1825,7 +1914,7 @@ void SummonCreature(TCreature *Actor, int ManaPoints, int Race, bool God){ GraphicalEffect(Actor->posx, Actor->posy, Actor->posz, EFFECT_MAGIC_BLUE); } -void SummonCreature(TCreature *Actor, int Mana, const char *RaceName, bool God){ +void SummonCreature(TCreature *Actor, int ManaPoints, const char *RaceName, bool God){ if(Actor == NULL){ error("SummonCreature: Ungültige Kreatur übergeben.\n"); throw ERROR; @@ -1841,7 +1930,7 @@ void SummonCreature(TCreature *Actor, int Mana, const char *RaceName, bool God){ throw CREATURENOTEXISTING; } - SummonCreature(Actor, Mana, Race, God); + SummonCreature(Actor, ManaPoints, Race, God); } void StartMonsterraid(TCreature *Actor, const char *RaidName){ @@ -2025,7 +2114,6 @@ void Heal(TCreature *Actor, int ManaPoints, int SoulPoints, int Amount){ } } - void MassHeal(TCreature *Actor, Object Target, int ManaPoints, int SoulPoints, int Amount, int Radius){ if(Actor == NULL){ error("MassHeal: Ungültige Kreatur übergeben.\n"); @@ -2098,7 +2186,7 @@ void MassHeal(TCreature *Actor, Object Target, int ManaPoints, int SoulPoints, i } } -void HealFriend(TCreature *Actor, char *TargetName, int ManaPoints, int SoulPoints, int Amount){ +void HealFriend(TCreature *Actor, const char *TargetName, int ManaPoints, int SoulPoints, int Amount){ if(Actor == NULL){ error("HealFriend: Ungültige Kreatur übergeben.\n"); throw ERROR; @@ -2352,7 +2440,7 @@ void CancelInvisibility(TCreature *Actor, Object Target, int ManaPoints, int Sou } } -void CreatureIllusion(TCreature *Actor, const char *RaceName, int ManaPoints, int SoulPoints, int Duration){ +void CreatureIllusion(TCreature *Actor, int ManaPoints, int SoulPoints, const char *RaceName, int Duration){ if(Actor == NULL){ error("CreatureIllusion: Ungültige Kreatur übergeben.\n"); throw ERROR; @@ -2381,7 +2469,7 @@ void CreatureIllusion(TCreature *Actor, const char *RaceName, int ManaPoints, in GraphicalEffect(Actor->posx, Actor->posy, Actor->posz, EFFECT_MAGIC_BLUE); } -void ObjectIllusion(TCreature *Actor, Object Target, int ManaPoints, int SoulPoints, int Duration){ +void ObjectIllusion(TCreature *Actor, int ManaPoints, int SoulPoints, Object Target, int Duration){ if(Actor == NULL){ error("ObjectIllusion: Ungültige Kreatur übergeben.\n"); throw ERROR; @@ -2570,7 +2658,7 @@ void Challenge(TCreature *Actor, int ManaPoints, int SoulPoints, int Radius){ } } -void FindPerson(TCreature *Actor, const char *TargetName, int ManaPoints, int SoulPoints){ +void FindPerson(TCreature *Actor, int ManaPoints, int SoulPoints, const char *TargetName){ // TODO(fusion): And we're back. if(Actor == NULL){ error("FindPerson: Ungültige Kreatur übergeben.\n"); @@ -3220,13 +3308,1040 @@ void HomeTeleport(TCreature *Actor, const char *Name){ Object Dest = GetMapContainer(Player->startx, Player->starty, Player->startz); GraphicalEffect(Player->posx, Player->posy, Player->posz, EFFECT_POFF); Move(0, Player->CrObject, Dest, -1, false, NONE); - GraphicalEffect(Player->posx, Player->posy, Player->posz, EFFECT_TELEPORT); + GraphicalEffect(Player->posx, Player->posy, Player->posz, EFFECT_ENERGY); SendMessage(Actor->Connection, TALK_INFO_MESSAGE, "Player %s has been moved to the temple.", Player->Name); Log("banish", "%s teleportiert %s zum Tempel.\n", Actor->Name, Player->Name); } +// Spell Casting +// ============================================================================= +static void SpellFailed(uint32 CreatureID){ + TCreature *Actor = GetCreature(CreatureID); + if(Actor == NULL){ + error("SpellFailed: Kreatur existiert nicht.\n"); + return; + } + + GraphicalEffect(Actor->posx, Actor->posy, Actor->posz, EFFECT_POFF); +} + +static void CharacterRightSpell(uint32 CreatureID, int SpellNr, const char (*SpellStr)[512]){ + TCreature *Actor = GetCreature(CreatureID); + if(Actor == NULL){ + error("CharacterRightSpell: Kreatur existiert nicht.\n"); + throw ERROR; + } + + if(Actor->Type != PLAYER){ + return; + } + + switch(SpellNr) { + case 34: CreateThing(Actor, SpellStr[2], SpellStr[3]); break; + case 35: CreateThing(Actor, SpellStr[2], NULL); break; + case 37: Teleport(Actor, SpellStr[2]); break; + case 40: CreateKnowledge(Actor, SpellStr[3], NULL); break; + case 41: ChangeData(Actor, SpellStr[2]); break; + case 46: CreateKnowledge(Actor, SpellStr[3], SpellStr[4]); break; + case 47: TeleportToCreature(Actor, SpellStr[3]); break; + case 52: TeleportPlayerToMe(Actor, SpellStr[3]); break; + case 53: SummonCreature(Actor, 0, SpellStr[3], true); break; + case 58: GetPosition(Actor); break; + case 63: CreateMoney(Actor, SpellStr[3]); break; + case 64: ChangeProfession(Actor, SpellStr[3]); break; + case 71: EditGuests(Actor); break; + case 72: EditSubowners(Actor); break; + case 73: KickGuest(Actor, SpellStr[3]); break; + case 74: EditNameDoor(Actor); break; + case 96: GetQuestValue(Actor, SpellStr[3]); break; + case 97: SetQuestValue(Actor, SpellStr[3], SpellStr[4]); break; + case 98: CleanupField(Actor); break; + case 99: MagicClimbing(Actor, SpellStr[3]); break; + case 100: ClearQuestValues(Actor); break; + case 101: KillAllMonsters(Actor, EFFECT_DEATH, 2); break; + case 102: StartMonsterraid(Actor, SpellStr[4]); break; + } +} + +static void AccountRightSpell(uint32 CreatureID, int SpellNr, const char (*SpellStr)[512]){ + TCreature *Actor = GetCreature(CreatureID); + if(Actor == NULL){ + error("AccountRightSpell: Kreatur existiert nicht.\n"); + throw ERROR; + } + + if(Actor->Type != PLAYER){ + return; + } + + switch(SpellNr) { + case 57: BanishAccount(Actor, SpellStr[3], atoi(SpellStr[4]), SpellStr[5]); break; + case 60: HomeTeleport(Actor, SpellStr[2]); break; + case 61: DeleteAccount(Actor, SpellStr[4], SpellStr[5]); break; + case 62: SetNameRule(Actor, SpellStr[2]); break; + case 65: Notation(Actor, SpellStr[2], SpellStr[3]); break; + case 66: NameLock(Actor, SpellStr[3]); break; + case 67: KickPlayer(Actor, SpellStr[2]); break; + case 68: DeleteCharacter(Actor, SpellStr[4], SpellStr[5]); break; + case 69: IPBanishment(Actor, SpellStr[3], SpellStr[4]); break; + case 70: BanishCharacter(Actor, SpellStr[3], atoi(SpellStr[4]), SpellStr[5]); break; + } +} + +static void CastSpell(uint32 CreatureID, int SpellNr, const char (*SpellStr)[512]){ + TCreature *Actor = GetCreature(CreatureID); + if(Actor == NULL){ + error("CastSpell: Kreatur existiert nicht.\n"); + throw ERROR; + } + + CheckSpellbook(Actor, SpellNr); + CheckAccount(Actor, SpellNr); + CheckLevel(Actor, SpellNr); + CheckRing(Actor, SpellNr); + + if(Actor->EarliestSpellTime > ServerMilliseconds){ + throw EXHAUSTED; + } + + if(IsAggressiveSpell(SpellNr) && Actor->Type == PLAYER + && !CheckRight(Actor->ID, ATTACK_EVERYWHERE) + && IsProtectionZone(Actor->posx, Actor->posy, Actor->posz)){ + throw PROTECTIONZONE; + } + + int ManaPoints = SpellList[SpellNr].Mana; + int SoulPoints = SpellList[SpellNr].SoulPoints; + switch(SpellNr) { + case 1:{ + int Amount = ComputeDamage(Actor, SpellNr, 20, 10); + Heal(Actor, ManaPoints, SoulPoints, Amount); + break; + } + + case 2:{ + int Amount = ComputeDamage(Actor, SpellNr, 40, 20); + Heal(Actor, ManaPoints, SoulPoints, Amount); + break; + } + + case 3:{ + int Amount = ComputeDamage(Actor, SpellNr, 250, 50); + Heal(Actor, ManaPoints, SoulPoints, Amount); + break; + } + + case 6:{ + MagicGoStrength(Actor, Actor, ManaPoints, SoulPoints, 30, 3); + break; + } + + case 9:{ + SummonCreature(Actor, ManaPoints, SpellStr[3], false); + break; + } + + case 10:{ + Enlight(Actor, ManaPoints, SoulPoints, 6, 500); + break; + } + + case 11:{ + Enlight(Actor, ManaPoints, SoulPoints, 8, 1000); + break; + } + + case 13:{ + int Damage = ComputeDamage(Actor, SpellNr, 150, 50); + AngleCombat(Actor, ManaPoints, SoulPoints, Damage, + EFFECT_ENERGY, 5, 30, DAMAGE_ENERGY); + break; + } + + case 19:{ + int Damage = ComputeDamage(Actor, SpellNr, 30, 10); + AngleCombat(Actor, ManaPoints, SoulPoints, Damage, + EFFECT_FIRE, 4, 45, DAMAGE_FIRE); + break; + } + + case 20:{ + FindPerson(Actor, ManaPoints, SoulPoints, SpellStr[2]); + break; + } + + case 22:{ + int Damage = ComputeDamage(Actor, SpellNr, 60, 20); + AngleCombat(Actor, ManaPoints, SoulPoints, Damage, + EFFECT_FIRE_BURST, 5, 0, DAMAGE_ENERGY); + break; + } + + case 23:{ + int Damage = ComputeDamage(Actor, SpellNr, 120, 80); + AngleCombat(Actor, ManaPoints, SoulPoints, Damage, + EFFECT_FIRE_BURST, 8, 0, DAMAGE_ENERGY); + break; + } + + case 24:{ + int Damage = ComputeDamage(Actor, SpellNr, 250, 50); + MassCombat(Actor, Actor->CrObject, ManaPoints, SoulPoints, Damage, + EFFECT_FIRE_EXPLOSION, 6, DAMAGE_PHYSICAL, ANIMATION_FIRE); + break; + } + + case 29:{ + NegatePoison(Actor, Actor, ManaPoints, SoulPoints); + break; + } + + case 38:{ + CreatureIllusion(Actor, ManaPoints, SoulPoints, SpellStr[4], 200); + break; + } + + case 39:{ + MagicGoStrength(Actor, Actor, ManaPoints, SoulPoints, 70, 2); + break; + } + + case 42:{ + CreateFood(Actor, ManaPoints, SoulPoints); + break; + } + + case 44:{ + Shielding(Actor, ManaPoints, SoulPoints, 200); + break; + } + + case 45:{ + Invisibility(Actor, ManaPoints, SoulPoints, 200); + break; + } + + case 48:{ + CreateArrows(Actor, ManaPoints, SoulPoints, 1, 5); + break; + } + + case 49:{ + CreateArrows(Actor, ManaPoints, SoulPoints, 2, 3); + break; + } + + case 51:{ + CreateArrows(Actor, ManaPoints, SoulPoints, 0, 10); + break; + } + + case 56:{ + int Damage = ComputeDamage(Actor, SpellNr, 200, 50); + MassCombat(Actor, Actor->CrObject, ManaPoints, SoulPoints, Damage, + EFFECT_POISON, 8, DAMAGE_POISON_PERIODIC, ANIMATION_NONE); + break; + } + + case 75:{ + Enlight(Actor, ManaPoints, SoulPoints, 9, 2000); + break; + } + + case 76:{ + MagicRope(Actor, ManaPoints, SoulPoints); + break; + } + + case 79:{ + CreateArrows(Actor, ManaPoints, SoulPoints, 3, 5); + break; + } + + case 80:{ + int Level = Actor->Skills[SKILL_LEVEL]->Get(); + int Damage = (Level * ComputeDamage(Actor, SpellNr, 80, 20)) / 25; + MassCombat(Actor, Actor->CrObject, Level * 4, 0, Damage, + EFFECT_BONE_HIT, 2, DAMAGE_PHYSICAL, ANIMATION_NONE); + break; + } + + case 81:{ + MagicClimbing(Actor, ManaPoints, SoulPoints, SpellStr[3]); + break; + } + + case 82:{ + int Amount = ComputeDamage(Actor, SpellNr, 200, 40); + MassHeal(Actor, Actor->CrObject, ManaPoints, SoulPoints, Amount, 4); + break; + } + + case 84:{ + int Amount = ComputeDamage(Actor,SpellNr,120,40); + HealFriend(Actor, SpellStr[3], ManaPoints, SoulPoints, Amount); + break; + } + + case 85:{ + MassRaiseDead(Actor, Actor->CrObject, ManaPoints, SoulPoints, 4); + break; + } + + case 87:{ + int Damage = ComputeDamage(Actor, SpellNr, 45, 10); + AngleCombat(Actor, ManaPoints, SoulPoints, Damage, + EFFECT_DEATH, 1, 0, DAMAGE_PHYSICAL); + break; + } + + case 88:{ + int Damage = ComputeDamage(Actor, SpellNr, 45, 10); + AngleCombat(Actor, ManaPoints, SoulPoints, Damage, + EFFECT_ENERGY, 1, 0, DAMAGE_ENERGY); + break; + } + + case 89:{ + int Damage = ComputeDamage(Actor, SpellNr, 45, 10); + AngleCombat(Actor, ManaPoints, SoulPoints, Damage, + EFFECT_FIRE, 1, 0, DAMAGE_FIRE); + break; + } + + case 90:{ + CancelInvisibility(Actor, Actor->CrObject, ManaPoints, SoulPoints, 4); + break; + } + + case 92:{ + ObjectType OldType = GetNewObjectType(90, 25); + ObjectType NewType = GetNewObjectType(90, 57); + EnchantObject(Actor, ManaPoints, SoulPoints, OldType, NewType); + break; + } + + case 93:{ + Challenge(Actor, ManaPoints, SoulPoints, 2); + break; + } + + case 94:{ + CreateField(Actor, ManaPoints, SoulPoints, FIELD_TYPE_WILDGROWTH); + break; + } + + case 95:{ + CreateArrows(Actor, ManaPoints, SoulPoints, 4, 1); + break; + } + } + + if(IsAggressiveSpell(SpellNr)){ + Actor->BlockLogout(60, false); + } +} + +static void RuneSpell(uint32 CreatureID, int SpellNr){ + TCreature *Actor = GetCreature(CreatureID); + if(Actor == NULL){ + error("RuneSpell: Kreatur existiert nicht.\n"); + throw ERROR; + } + + uint8 RuneGr = SpellList[SpellNr].RuneGr; + uint8 RuneNr = SpellList[SpellNr].RuneNr; + int ManaPoints = SpellList[SpellNr].Mana; + int SoulPoints = SpellList[SpellNr].SoulPoints; + int Amount = SpellList[SpellNr].Amount; + + if(RuneGr == 0){ + error("RuneSpell: Spell %d ist Runenspruch, hat aber keine Rune.\n", SpellNr); + throw ERROR; + } + + CheckSpellbook(Actor, SpellNr); + CheckAccount(Actor, SpellNr); + CheckLevel(Actor, SpellNr); + CheckRing(Actor, SpellNr); + if(Actor->EarliestSpellTime > ServerMilliseconds){ + throw EXHAUSTED; + } + + bool RuneCreated = false; + ObjectType BlankType = GetSpecialObject(RUNE_BLANK); + + Object RightHand = GetBodyObject(Actor->ID, 5); // RIGHTHAND + if(RightHand.exists() && RightHand.getObjectType() == BlankType){ + CheckMana(Actor, ManaPoints, SoulPoints, 1000); + ObjectType RuneType = GetNewObjectType(RuneGr, RuneNr); + Change(RightHand, RuneType, Amount); + RuneCreated = true; + } + + Object LeftHand = GetBodyObject(Actor->ID, 6); // LEFTHAND + if(LeftHand.exists() && LeftHand.getObjectType() == BlankType){ + // TODO(fusion): Ughh... I'm not sure why we're trying to cast the spell + // twice but we need to make so errors from the second cast are only + // relevant if there wasn't a first cast. + try{ + CheckMana(Actor, ManaPoints, SoulPoints, 1000); + ObjectType RuneType = GetNewObjectType(RuneGr, RuneNr); + Change(LeftHand, RuneType, Amount); + RuneCreated = true; + }catch(...){ + if(!RuneCreated){ + throw; + } + } + } + + if(!RuneCreated){ + throw MAGICITEM; + } + + GraphicalEffect(Actor->posx, Actor->posy, Actor->posz, EFFECT_MAGIC_RED); +} + +static int TypeOfSpell(const char *Text){ + // NOTE(fusion): We're checking `Text` against the first 5 spell syllables + // which are "al", "ad", "ex", "ut", "om". They determine the type of the + // spell. See `CheckForSpell`. + int SpellType = 0; + if(Text != NULL){ + for(int SyllableNr = 1; + SyllableNr < 6; + SyllableNr += 1){ + if(stricmp(Text, SpellSyllable[SyllableNr], 2) == 0){ + SpellType = SyllableNr; + break; + } + } + } + return SpellType; +} + +static int FindSpell(const uint8 *Syllable){ + int BestMatch = 0; + int MinParams = 100; + for(int SpellNr = 0; + SpellNr < NARRAY(SpellList); + SpellNr += 1){ + int Params = 0; + TSpellList *Spell = &SpellList[SpellNr]; + for(int i = 0; + i < NARRAY(Spell->Syllable); + i += 1){ + // NOTE(fusion): SpellSyllable[6] is "para" which refers to a spell + // parameter. + if(Spell->Syllable[i] == 6){ + if(Syllable[i] == 0) + break; + Params += 1; + }else if(Spell->Syllable[i] != Syllable[i]){ + break; + } + + if(Spell->Syllable[i] == 0){ + if(Params < MinParams){ + MinParams = Params; + BestMatch = SpellNr; + } + break; + } + } + } + + return BestMatch; +} + +static int FindSpell(Object Obj){ + if(!Obj.exists()){ + error("FindSpell: Übergebenes Objekt existiert nicht.\n"); + return 0; + } + + ObjectType ObjType = Obj.getObjectType(); + if(!ObjType.getFlag(RUNE)){ + error("FindSpell: Übergebenes Objekt ist nicht magisch.\n"); + return 0; + } + + int Result = 0; + for(int SpellNr = 0; + SpellNr < NARRAY(SpellList); + SpellNr += 1){ + TSpellList *Spell = &SpellList[SpellNr]; + ObjectType RuneType = GetNewObjectType(Spell->RuneGr, Spell->RuneNr); + if(ObjType == RuneType){ + Result = SpellNr; + break; + } + } + return Result; +} + +static void GetSpellString(int SpellNr, char *Text){ + Text[0] = 0; + + if(SpellNr < 1 || SpellNr >= NARRAY(SpellList)){ + error("GetSpellString: Ungültige Zauberspruchnummer %d.\n", SpellNr); + return; + } + + TSpellList *Spell = &SpellList[SpellNr]; + for(int i = 0; i < NARRAY(Spell->Syllable); i += 1){ + const char *Syllable = SpellSyllable[Spell->Syllable[i]]; + if(Syllable[0] == 0){ + break; + } + + // TODO(fusion): Review. I think we don't add a space between the first + // and second syllable and it's probably correct. + if(i >= 2){ + strcat(Text, " "); + } + + strcat(Text, Syllable); + } +} + +void GetMagicItemDescription(Object Obj, char *SpellString, int *MagicLevel){ + SpellString[0] = 0; + *MagicLevel = 0; + + if(!Obj.exists()){ + error("GetMagicItemDescription: Übergebenes Objekt existiert nicht.\n"); + return; + } + + ObjectType ObjType = Obj.getObjectType(); + if(!ObjType.getFlag(RUNE)){ + error("GetMagicItemDescription: Übergebenes Objekt ist nicht magisch.\n"); + return; + } + + int SpellNr = FindSpell(Obj); + if(SpellNr == 0){ + error("GetMagicItemDescription: Objekt %d hat keinen Zauberspruch.\n", ObjType.TypeID); + return; + } + + GetSpellString(SpellNr, SpellString); + *MagicLevel = (int)SpellList[SpellNr].RuneLevel; +} + +void GetSpellbook(uint32 CharacterID, char *Buffer){ + TPlayer *Player = GetPlayer(CharacterID); + if(Player == NULL){ + error("GetSpellbook: Spieler existiert nicht.\n"); + return; + } + + if(Buffer == NULL){ + error("GetSpellbook: Übergebener Puffer existiert nicht.\n"); + return; + } + + // TODO(fusion): We're iterating over all spells for each level. This is bad + // because we could have a list of spell numbers sorted by level and achieve + // the same thing. We also had a hard coded max level but since we're already + // iterating so many times over the spell list, what's another one? + // Also, the overuse of `strcat` doesn't help. + + int MaxLevel = 1; + for(int SpellNr = 0; + SpellNr < NARRAY(SpellList); + SpellNr += 1){ + int Level = (int)SpellList[SpellNr].Level; + if(Level > MaxLevel){ + Level = MaxLevel; + } + } + + char Help[256]; + for(int Level = 1; Level <= MaxLevel; Level += 1){ + bool First = true; + for(int SpellNr = 0; + SpellNr < NARRAY(SpellList); + SpellNr += 1){ + TSpellList *Spell = &SpellList[SpellNr]; + if((int)Spell->Level != Level || !Player->SpellKnown(SpellNr)){ + continue; + } + + if(First){ + sprintf(Help, "Spells for Level %d\n", Level); + strcat(Buffer, Help); + First = false; + } + + GetSpellString(SpellNr, Help); + if(Help[0] == 0){ + error("GetSpellbook: Zauberspruch %d hat keine Zauberformel.\n", SpellNr); + continue; + } + + strcat(Buffer, " "); + strcat(Buffer, Help); + if(Spell->Comment != NULL && Spell->Comment[0] != 0){ + strcat(Buffer, " - "); + strcat(Buffer, Spell->Comment); + if(SpellNr == 9 || SpellNr == 12){ + strcpy(Help, ": var"); + }else if(SpellNr == 80){ + strcpy(Help, ": 4*Level"); + }else{ + sprintf(Help, ": %d", Spell->Mana); + } + strcat(Buffer, Help); + } + strcat(Buffer, "\n"); + } + + if(!First){ + strcat(Buffer, "\n"); + } + } +} + +int GetSpellLevel(int SpellNr){ + if(SpellNr < 1 || SpellNr >= NARRAY(SpellList)){ + error("GetSpellLevel: Ungültige Spruchnummer %d.\n", SpellNr); + return 1; + } + + return (int)SpellList[SpellNr].Level; +} + +int CheckForSpell(uint32 CreatureID, const char *Text){ + // IMPORTANT(fusion): The first syllable also determines the type of the spell + // which is why we can quickly rule out a spell cast if it is unknown. + // It is also handled separately in the parsing loop below to allow for the + // second syllable to be glued together (or not). + int SpellType = TypeOfSpell(Text); + if(SpellType == 0){ + return 0; + } + + // TODO(fusion): Keeping syllables in text form doesn't make sense. `SpellStr` + // should ideally only contain parameters. + + // TODO(fusion): Did we really need to use `stringstream` here? It allocates + // an internal `std::string` from the input string which is just wasteful, + // specially for the minimal processing we're doing here. + + int SyllableCount = 1; + uint8 Syllable[10] = { (uint8)SpellType }; + char SpellStr[10][512] = {}; + strcpy(SpellStr[0], SpellSyllable[SpellType]); + + std::istringstream IS(Text); + IS.get(); + IS.get(); + while(!IS.eof()){ + while(isSpace(IS.peek())){ + IS.get(); + } + + int Index = SyllableCount; + if(IS.peek() == '"'){ + IS.get(); + IS.get(SpellStr[Index], sizeof(SpellStr[0]), '"'); + IS.get(); + }else{ + IS.get(SpellStr[Index], sizeof(SpellStr[0]), ' '); + } + + // TODO(fusion): This could be a problem if there is a "" parameter? + if(SpellStr[Index][0] == 0){ + break; + } + + for(int SyllableNr = 0; + SyllableNr < NARRAY(SpellSyllable); + SyllableNr += 1){ + if(stricmp(SpellStr[Index], SpellSyllable[SyllableNr]) == 0){ + Syllable[Index] = (uint8)SyllableNr; + break; + } + } + + // NOTE(fusion): SpellSyllable[6] is "para" which refers to a spell + // parameter. This is setting up `Syllable` to be used by `FindSpell`. + if(Syllable[Index] == 0){ + Syllable[Index] = 6; + } + + SyllableCount += 1; + } + + int SpellNr = FindSpell(Syllable); + if(SpellNr != 0){ + try{ + switch(SpellType){ + case 1: CharacterRightSpell(CreatureID, SpellNr, SpellStr); break; + case 2: RuneSpell(CreatureID, SpellNr); break; + case 3: CastSpell(CreatureID, SpellNr, SpellStr); break; + case 4: CastSpell(CreatureID, SpellNr, SpellStr); break; + case 5: AccountRightSpell(CreatureID, SpellNr, SpellStr); break; + default:{ + error("CheckForSpell: Spruchklasse %d existiert nicht.\n", SpellType); + break; + } + } + }catch(RESULT r){ + // TODO(fusion): `SpellFailed` is inlined in here but I think it's + // cleaner to keep it this way. + TCreature *Actor = GetCreature(CreatureID); + if(Actor == NULL){ + error("SpellFailed: Kreatur existiert nicht.\n"); + }else{ + if(r != ERROR){ + GraphicalEffect(Actor->posx, Actor->posy, Actor->posz, EFFECT_POFF); + } + + if(Actor->Type == PLAYER){ + SendResult(Actor->Connection, r); + } + } + + return -SpellType; + } + } + + return SpellType; +} + +static void DeleteRune(Object Obj){ + if(!Obj.exists()){ + error("DeleteRune: Übergebenes Objekt existiert nicht.\n"); + throw ERROR; + } + + // TODO(fusion): Should probably check if object is a rune? + uint32 Charges = Obj.getAttribute(CHARGES); + if(Charges > 1){ + Change(Obj, CHARGES, Charges - 1); + }else{ + Delete(Obj, -1); + } +} + +void UseMagicItem(uint32 CreatureID, Object Obj, Object Dest){ + TPlayer *Actor = GetPlayer(CreatureID); + if(Actor == NULL){ + error("UseMagicItem: Kreatur existiert nicht.\n"); + throw ERROR; + } + + if(!Obj.exists()){ + error("UseMagicItem: Übergebenes Objekt existiert nicht.\n"); + throw ERROR; + } + + if(!Dest.exists()){ + error("UseMagicItem: Übergebenes Ziel existiert nicht (Objekt %d).\n", + Obj.getObjectType().TypeID); + throw ERROR; + } + + if(CheckRight(Actor->ID, NO_RUNES)){ + throw NOTUSABLE; + } + + int SpellNr = FindSpell(Obj); + if(SpellNr == 0){ + error("UseMagicItem: Für Objekt %d existiert kein Spruch.\n", + Obj.getObjectType().TypeID); + throw ERROR; + } + + + // NOTE(fusion): This target picking logic is probably to avoid picking + // obviously wrong candidates when there are multiple creatures on a single + // field. + TCreature *Target = NULL; + bool Aggressive = IsAggressiveSpell(SpellNr); + { + if(Dest.getObjectType().isCreatureContainer()){ + Target = GetCreature(Dest); + } + + Object Other = GetFirstContainerObject(Dest.getContainer()); + while(Other != NONE){ + if(Other.getObjectType().isCreatureContainer()){ + uint32 OtherID = Other.getCreatureID(); + if(Target == NULL + || (Aggressive && OtherID != Actor->ID) + || (!Aggressive && OtherID == Actor->ID)){ + Target = GetCreature(OtherID); + Dest = Other; + } + } + Other = Other.getNextObject(); + } + } + + // NOTE(fusion): Rune check. + CheckRuneLevel(Actor, SpellNr); + + if(Actor->EarliestSpellTime > ServerMilliseconds){ + throw EXHAUSTED; + } + + if(Aggressive){ + int DestX, DestY, DestZ; + GetObjectCoordinates(Dest, &DestX, &DestY, &DestZ); + if(!CheckRight(Actor->ID, ATTACK_EVERYWHERE) + && (IsProtectionZone(Actor->posx, Actor->posy, Actor->posz) + || IsProtectionZone(DestX, DestY, DestZ))){ + throw PROTECTIONZONE; + } + + if(Dest.getContainer().getObjectType().isMapContainer()){ + throw NOROOM; + } + } + + try{ + switch(SpellNr){ + case 4:{ + if(Target == NULL){ + throw NOCREATURE; + } + + int Amount = ComputeDamage(Actor, SpellNr, 70, 30); + Heal(Target, -1, 0, Amount); // -1 ? + break; + } + + case 5:{ + if(Target == NULL){ + throw NOCREATURE; + } + + int Amount = ComputeDamage(Actor, SpellNr, 250, 30); + Heal(Target, -1, 0, Amount); // -1 ? + break; + } + + case 7:{ + int Damage = ComputeDamage(Actor, SpellNr, 15, 5); + Combat(Actor, Dest, 0, 0, Damage, EFFECT_FIRE_BURST, + ANIMATION_FIRE, DAMAGE_ENERGY); + break; + } + + case 8:{ + int Damage = ComputeDamage(Actor, SpellNr, 30, 10); + Combat(Actor, Dest, 0, 0, Damage, EFFECT_FIRE_BURST, + ANIMATION_FIRE, DAMAGE_ENERGY); + break; + } + + case 12:{ + if(Target == NULL){ + throw NOCREATURE; + } + + Convince(Actor, Target); + break; + } + + case 14:{ + ObjectIllusion(Actor, 0, 0, Dest, 200); + break; + } + + case 15:{ + int Damage = ComputeDamage(Actor, SpellNr, 20, 5); + MassCombat(Actor, Dest, 0, 0, Damage, EFFECT_FIRE, + 3, DAMAGE_FIRE, ANIMATION_FIRE); + break; + } + + case 16:{ + int Damage = ComputeDamage(Actor, SpellNr, 50, 15); + MassCombat(Actor, Dest, 0, 0, Damage, EFFECT_FIRE, + 4, DAMAGE_FIRE, ANIMATION_FIRE); + break; + } + + case 17:{ + MassCreateField(Actor, Dest, 0, 0, FIELD_TYPE_FIRE, 3); + break; + } + + case 18:{ + int Damage = ComputeDamage(Actor, SpellNr, 60, 40); + MassCombat(Actor, Dest, 0, 0, Damage, EFFECT_FIRE_EXPLOSION, + 1, DAMAGE_PHYSICAL, ANIMATION_FIRE); + break; + } + + case 21:{ + int Damage = ComputeDamage(Actor, SpellNr, 150, 20); + Combat(Actor, Dest, 0, 0, Damage, EFFECT_DEATH, + ANIMATION_DEATH, DAMAGE_PHYSICAL); + break; + } + + case 25:{ + CreateField(Actor, Dest, 0, 0, FIELD_TYPE_FIRE); + break; + } + + case 26:{ + CreateField(Actor, Dest, 0, 0, FIELD_TYPE_POISON); + break; + } + + case 27:{ + CreateField(Actor, Dest, 0, 0, FIELD_TYPE_ENERGY); + break; + } + + case 28:{ + CreateFieldWall(Actor, Dest, 0, 0, FIELD_TYPE_FIRE, 2); + break; + } + + case 30:{ + DeleteField(Actor, Dest, 0, 0); + break; + } + + case 31:{ + if(Target == NULL){ + throw NOCREATURE; + } + + NegatePoison(Actor, Target, 0, 0); + break; + } + + case 32:{ + CreateFieldWall(Actor, Dest, 0, 0, FIELD_TYPE_POISON, 2); + break; + } + + case 33:{ + CreateFieldWall(Actor, Dest, 0, 0, FIELD_TYPE_ENERGY, 3); + break; + } + + case 50:{ + int Damage = ComputeDamage(Actor, SpellNr, 120, 20); + Combat(Actor, Dest, 0, 0, Damage, EFFECT_FIRE_HIT, + ANIMATION_FIRE, DAMAGE_FIRE_PERIODIC); + break; + } + + case 54:{ + if(Target == NULL){ + throw NOCREATURE; + } + + if(Actor->GetEffectiveProfession() != PROFESSION_DRUID){ + throw NOTUSABLE; + } + + int ManaPoints = SpellList[SpellNr].Mana; + int SoulPoints = SpellList[SpellNr].SoulPoints; + MagicGoStrength(Actor, Target, ManaPoints, SoulPoints, -101, 1); + break; + } + + case 55:{ + MassCreateField(Actor, Dest, 0, 0, FIELD_TYPE_ENERGY, 3); + break; + } + + case 77:{ + int Damage = ComputeDamage(Actor, SpellNr, 70, 20); + Combat(Actor, Dest, 0, 0, Damage, EFFECT_POISON_HIT, + ANIMATION_ENERGY, DAMAGE_POISON_PERIODIC); + break; + } + + case 78:{ + CleanupField(Actor, Dest, 0, 0); + break; + } + + case 83:{ + RaiseDead(Actor, Dest, 0, 0); + break; + } + + case 86:{ + CreateField(Actor, Dest, 0, 0, FIELD_TYPE_MAGICWALL); + break; + } + + case 91:{ + MassCreateField(Actor, Dest, 0, 0, FIELD_TYPE_POISON, 3); + break; + } + + default:{ + error("UseMagicItem: Spell %d noch nicht implementiert.\n", SpellNr); + throw ERROR; + } + } + }catch(RESULT r){ + // TODO(fusion): Same as with `CheckForSpell` except that `Actor` is + // already in scope. + if(r != ERROR){ + GraphicalEffect(Actor->posx, Actor->posy, Actor->posz, EFFECT_POFF); + } + SendResult(Actor->Connection, r); + return; + } + + DeleteRune(Obj); + + if(Aggressive){ + Actor->BlockLogout(60, false); + } +} + +void DrinkPotion(uint32 CreatureID, Object Obj){ + TPlayer *Player = GetPlayer(CreatureID); + if(Player == NULL){ + error("DrinkPotion: Kreatur existiert nicht.\n"); + throw ERROR; + } + + if(!Obj.exists()){ + error("DrinkPotion: Übergebenes Objekt existiert nicht.\n"); + throw ERROR; + } + + ObjectType ObjType = Obj.getObjectType(); + if(!ObjType.getFlag(LIQUIDCONTAINER)){ + error("DrinkPotion: Übergebenes Objekt ist kein Flüssigkeitscontainer.\n"); + throw ERROR; + } + + int LiquidType = (int)Obj.getAttribute(CONTAINERLIQUIDTYPE); + if(LiquidType == LIQUID_MANA){ + int Amount = ComputeDamage(NULL, 0, 100, 50); + RefreshMana(Player, 0, 0, Amount); + }else if(LiquidType == LIQUID_LIFE){ + int Amount = ComputeDamage(NULL, 0, 50, 25); + Heal(Player, 0, 0, Amount); + }else{ + error("DrinkPotion: Objekt enthält keinen Zaubertrank.\n"); + throw ERROR; + } + + Change(Obj, CONTAINERLIQUIDTYPE, LIQUID_NONE); +} + // Magic Init Functions // ============================================================================= static void InitCircles(void){ |
