From 124d113b578a4d344f6fe8e3681dacb593813016 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 2 Dec 2023 13:26:48 +0000 Subject: [PATCH] Extract spell data to a TSV --- CMake/Assets.cmake | 1 + Source/data/iterators.hpp | 42 ++++- Source/data/record_reader.hpp | 37 +++-- Source/diablo.cpp | 1 + Source/spelldat.cpp | 241 ++++++++++++++++++++++------- Source/spelldat.h | 8 +- Source/translation_dummy.cpp | 51 ++++++ assets/txtdata/spells/spelldat.tsv | 52 +++++++ test/inv_test.cpp | 1 + test/pack_test.cpp | 2 + test/timedemo_test.cpp | 1 + test/writehero_test.cpp | 1 + tools/extract_translation_data.py | 7 + 13 files changed, 368 insertions(+), 77 deletions(-) create mode 100644 assets/txtdata/spells/spelldat.tsv diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index f792dcfc1d6..da98f020e0a 100644 --- a/CMake/Assets.cmake +++ b/CMake/Assets.cmake @@ -163,6 +163,7 @@ set(devilutionx_assets txtdata/monsters/monstdat.tsv txtdata/monsters/unique_monstdat.tsv txtdata/sound/effects.tsv + txtdata/spells/spelldat.tsv ui_art/diablo.pal ui_art/hellfire.pal ui_art/creditsw.clx diff --git a/Source/data/iterators.hpp b/Source/data/iterators.hpp index 23d102ab8d8..ccaab1c5da4 100644 --- a/Source/data/iterators.hpp +++ b/Source/data/iterators.hpp @@ -1,13 +1,14 @@ #pragma once #include +#include #include #include -#include #include "parser.hpp" #include "utils/parse_int.hpp" +#include "utils/str_cat.hpp" #include "utils/str_split.hpp" namespace devilution { @@ -157,6 +158,45 @@ class DataFileField { return parseIntArray(destination.data(), N); } + template + [[nodiscard]] tl::expected parseEnumArray(T *destination, size_t n, std::optional fillMissing, ParseFn &&parseFn) + { + size_t i = 0; + const std::string_view str = value(); + if (!str.empty()) { + for (const std::string_view part : SplitByChar(str, ',')) { + if (i == n) + return tl::make_unexpected(StrCat("Too many values, max: ", n)); + auto result = parseFn(part); + if (!result.has_value()) { + return tl::make_unexpected(std::move(result).error()); + } + destination[i++] = *result; + } + } + if (i != n) { + if (!fillMissing.has_value()) { + return tl::make_unexpected(StrCat("Too few values, expected ", n, " got ", i)); + } + while (i < n) { + destination[i++] = *fillMissing; + } + } + return {}; + } + + template + [[nodiscard]] tl::expected parseEnumArray(T (&destination)[N], std::optional fillMissing, ParseFn &&parseFn) + { + return parseEnumArray(destination, N, std::move(fillMissing), std::forward(parseFn)); + } + + template + [[nodiscard]] tl::expected parseIntArray(std::array &destination, std::optional fillMissing, ParseFn &&parseFn) + { + return parseEnumArray(destination.data(), N, std::move(fillMissing), std::forward(parseFn)); + } + template [[nodiscard]] tl::expected parseEnumList(T &destination, ParseFn &&parseFn) { diff --git a/Source/data/record_reader.hpp b/Source/data/record_reader.hpp index 65a09eb45e2..df3795401cf 100644 --- a/Source/data/record_reader.hpp +++ b/Source/data/record_reader.hpp @@ -28,8 +28,7 @@ class RecordReader { typename std::enable_if_t, void> readInt(std::string_view name, T &out) { - advance(); - DataFileField field = *it_; + DataFileField field = nextField(); failOnError(field.parseInt(out), name, field); } @@ -37,8 +36,7 @@ class RecordReader { typename std::enable_if_t, void> readOptionalInt(std::string_view name, T &out) { - advance(); - DataFileField field = *it_; + DataFileField field = nextField(); if (field.value().empty()) return; failOnError(field.parseInt(out), name, field); } @@ -46,16 +44,21 @@ class RecordReader { template void readIntArray(std::string_view name, T (&out)[N]) { - advance(); - DataFileField field = *it_; + DataFileField field = nextField(); failOnError(field.parseIntArray(out), name, field); } + template + void readEnumArray(std::string_view name, std::optional fillMissing, T (&out)[N], F &&parseFn) + { + DataFileField field = nextField(); + failOnError(field.parseEnumArray(out, fillMissing, parseFn), name, field, DataFileField::Error::InvalidValue); + } + template void readIntArray(std::string_view name, std::array &out) { - advance(); - DataFileField field = *it_; + DataFileField field = nextField(); failOnError(field.parseIntArray(out), name, field); } @@ -63,15 +66,13 @@ class RecordReader { typename std::enable_if_t, void> readFixed6(std::string_view name, T &out) { - advance(); - DataFileField field = *it_; + DataFileField field = nextField(); failOnError(field.parseFixed6(out), name, field); } void readBool(std::string_view name, bool &out) { - advance(); - DataFileField field = *it_; + DataFileField field = nextField(); failOnError(field.parseBool(out), name, field); } @@ -84,8 +85,7 @@ class RecordReader { template void read(std::string_view name, T &out, F &&parseFn) { - advance(); - DataFileField field = *it_; + DataFileField field = nextField(); tl::expected result = parseFn(field.value()); failOnError(result, name, field, DataFileField::Error::InvalidValue); out = *std::move(result); @@ -94,8 +94,7 @@ class RecordReader { template void readEnumList(std::string_view name, T &out, F &&parseFn) { - advance(); - DataFileField field = *it_; + DataFileField field = nextField(); failOnError(field.parseEnumList(out, std::forward(parseFn)), name, field, DataFileField::Error::InvalidValue); } @@ -109,6 +108,12 @@ class RecordReader { void advance(); + DataFileField nextField() + { + advance(); + return *it_; + } + private: template void failOnError(const tl::expected &result, std::string_view name, const DataFileField &field) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index ac97f276755..f7b256b61c0 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -2476,6 +2476,7 @@ int DiabloMain(int argc, char **argv) LoadPlayerDataFiles(); // TODO: We can probably load this much later (when the game is starting). + LoadSpellData(); LoadMissileData(); LoadMonsterData(); LoadItemData(); diff --git a/Source/spelldat.cpp b/Source/spelldat.cpp index f3bb911abdb..a48c6916060 100644 --- a/Source/spelldat.cpp +++ b/Source/spelldat.cpp @@ -5,11 +5,14 @@ */ #include "spelldat.h" +#include #include #include -#include "utils/language.h" +#include "data/file.hpp" +#include "data/iterators.hpp" +#include "data/record_reader.hpp" namespace devilution { @@ -19,66 +22,158 @@ const auto Lightning = SpellDataFlags::Lightning; const auto Magic = SpellDataFlags::Magic; const auto Targeted = SpellDataFlags::Targeted; const auto AllowedInTown = SpellDataFlags::AllowedInTown; + +void AddNullSpell() +{ + SpellData &null = SpellsData.emplace_back(); + null.sSFX = SfxID::None; + null.bookCost10 = null.staffCost10 = null.sManaCost = 0; + null.flags = SpellDataFlags::Fire; + null.sBookLvl = null.sStaffLvl = 0; + null.minInt = 0; + null.sMissiles[0] = null.sMissiles[1] = MissileID::Null; + null.sManaAdj = null.sMinMana = 0; + null.sStaffMin = 40; + null.sStaffMax = 80; +} + +// A temporary solution for parsing soundID until we have a more general one. +tl::expected ParseSpellSoundId(std::string_view value) +{ + if (value == "CastFire") return SfxID::CastFire; + if (value == "CastHealing") return SfxID::CastHealing; + if (value == "CastLightning") return SfxID::CastLightning; + if (value == "CastSkill") return SfxID::CastSkill; + return tl::make_unexpected("Unknown enum value (only a few are supported for now)"); +} + +tl::expected ParseSpellDataFlag(std::string_view value) +{ + if (value == "Fire") return SpellDataFlags::Fire; + if (value == "Lightning") return SpellDataFlags::Lightning; + if (value == "Magic") return SpellDataFlags::Magic; + if (value == "Targeted") return SpellDataFlags::Targeted; + if (value == "AllowedInTown") return SpellDataFlags::AllowedInTown; + return tl::make_unexpected("Unknown enum value"); +} + +tl::expected ParseMissileId(std::string_view value) +{ + if (value == "Arrow") return MissileID::Arrow; + if (value == "Firebolt") return MissileID::Firebolt; + if (value == "Guardian") return MissileID::Guardian; + if (value == "Phasing") return MissileID::Phasing; + if (value == "NovaBall") return MissileID::NovaBall; + if (value == "FireWall") return MissileID::FireWall; + if (value == "Fireball") return MissileID::Fireball; + if (value == "LightningControl") return MissileID::LightningControl; + if (value == "Lightning") return MissileID::Lightning; + if (value == "MagmaBallExplosion") return MissileID::MagmaBallExplosion; + if (value == "TownPortal") return MissileID::TownPortal; + if (value == "FlashBottom") return MissileID::FlashBottom; + if (value == "FlashTop") return MissileID::FlashTop; + if (value == "ManaShield") return MissileID::ManaShield; + if (value == "FlameWave") return MissileID::FlameWave; + if (value == "ChainLightning") return MissileID::ChainLightning; + if (value == "ChainBall") return MissileID::ChainBall; + if (value == "BloodHit") return MissileID::BloodHit; + if (value == "BoneHit") return MissileID::BoneHit; + if (value == "MetalHit") return MissileID::MetalHit; + if (value == "Rhino") return MissileID::Rhino; + if (value == "MagmaBall") return MissileID::MagmaBall; + if (value == "ThinLightningControl") return MissileID::ThinLightningControl; + if (value == "ThinLightning") return MissileID::ThinLightning; + if (value == "BloodStar") return MissileID::BloodStar; + if (value == "BloodStarExplosion") return MissileID::BloodStarExplosion; + if (value == "Teleport") return MissileID::Teleport; + if (value == "FireArrow") return MissileID::FireArrow; + if (value == "DoomSerpents") return MissileID::DoomSerpents; + if (value == "FireOnly") return MissileID::FireOnly; + if (value == "StoneCurse") return MissileID::StoneCurse; + if (value == "BloodRitual") return MissileID::BloodRitual; + if (value == "Invisibility") return MissileID::Invisibility; + if (value == "Golem") return MissileID::Golem; + if (value == "Etherealize") return MissileID::Etherealize; + if (value == "Spurt") return MissileID::Spurt; + if (value == "ApocalypseBoom") return MissileID::ApocalypseBoom; + if (value == "Healing") return MissileID::Healing; + if (value == "FireWallControl") return MissileID::FireWallControl; + if (value == "Infravision") return MissileID::Infravision; + if (value == "Identify") return MissileID::Identify; + if (value == "FlameWaveControl") return MissileID::FlameWaveControl; + if (value == "Nova") return MissileID::Nova; + if (value == "Rage") return MissileID::Rage; + if (value == "Apocalypse") return MissileID::Apocalypse; + if (value == "ItemRepair") return MissileID::ItemRepair; + if (value == "StaffRecharge") return MissileID::StaffRecharge; + if (value == "TrapDisarm") return MissileID::TrapDisarm; + if (value == "Inferno") return MissileID::Inferno; + if (value == "InfernoControl") return MissileID::InfernoControl; + if (value == "FireMan") return MissileID::FireMan; + if (value == "Krull") return MissileID::Krull; + if (value == "ChargedBolt") return MissileID::ChargedBolt; + if (value == "HolyBolt") return MissileID::HolyBolt; + if (value == "Resurrect") return MissileID::Resurrect; + if (value == "Telekinesis") return MissileID::Telekinesis; + if (value == "LightningArrow") return MissileID::LightningArrow; + if (value == "Acid") return MissileID::Acid; + if (value == "AcidSplat") return MissileID::AcidSplat; + if (value == "AcidPuddle") return MissileID::AcidPuddle; + if (value == "HealOther") return MissileID::HealOther; + if (value == "Elemental") return MissileID::Elemental; + if (value == "ResurrectBeam") return MissileID::ResurrectBeam; + if (value == "BoneSpirit") return MissileID::BoneSpirit; + if (value == "WeaponExplosion") return MissileID::WeaponExplosion; + if (value == "RedPortal") return MissileID::RedPortal; + if (value == "DiabloApocalypseBoom") return MissileID::DiabloApocalypseBoom; + if (value == "DiabloApocalypse") return MissileID::DiabloApocalypse; + if (value == "Mana") return MissileID::Mana; + if (value == "Magi") return MissileID::Magi; + if (value == "LightningWall") return MissileID::LightningWall; + if (value == "LightningWallControl") return MissileID::LightningWallControl; + if (value == "Immolation") return MissileID::Immolation; + if (value == "SpectralArrow") return MissileID::SpectralArrow; + if (value == "FireballBow") return MissileID::FireballBow; + if (value == "LightningBow") return MissileID::LightningBow; + if (value == "ChargedBoltBow") return MissileID::ChargedBoltBow; + if (value == "HolyBoltBow") return MissileID::HolyBoltBow; + if (value == "Warp") return MissileID::Warp; + if (value == "Reflect") return MissileID::Reflect; + if (value == "Berserk") return MissileID::Berserk; + if (value == "RingOfFire") return MissileID::RingOfFire; + if (value == "StealPotions") return MissileID::StealPotions; + if (value == "StealMana") return MissileID::StealMana; + if (value == "RingOfLightning") return MissileID::RingOfLightning; + if (value == "Search") return MissileID::Search; + if (value == "Aura") return MissileID::Aura; + if (value == "Aura2") return MissileID::Aura2; + if (value == "SpiralFireball") return MissileID::SpiralFireball; + if (value == "RuneOfFire") return MissileID::RuneOfFire; + if (value == "RuneOfLight") return MissileID::RuneOfLight; + if (value == "RuneOfNova") return MissileID::RuneOfNova; + if (value == "RuneOfImmolation") return MissileID::RuneOfImmolation; + if (value == "RuneOfStone") return MissileID::RuneOfStone; + if (value == "BigExplosion") return MissileID::BigExplosion; + if (value == "HorkSpawn") return MissileID::HorkSpawn; + if (value == "Jester") return MissileID::Jester; + if (value == "OpenNest") return MissileID::OpenNest; + if (value == "OrangeFlare") return MissileID::OrangeFlare; + if (value == "BlueFlare") return MissileID::BlueFlare; + if (value == "RedFlare") return MissileID::RedFlare; + if (value == "YellowFlare") return MissileID::YellowFlare; + if (value == "BlueFlare2") return MissileID::BlueFlare2; + if (value == "YellowExplosion") return MissileID::YellowExplosion; + if (value == "RedExplosion") return MissileID::RedExplosion; + if (value == "BlueExplosion") return MissileID::BlueExplosion; + if (value == "BlueExplosion2") return MissileID::BlueExplosion2; + if (value == "OrangeExplosion") return MissileID::OrangeExplosion; + return tl::make_unexpected("Unknown enum value"); +} + } // namespace /** Data related to each spell ID. */ -const SpellData SpellsData[] = { - // clang-format off -// id sNameText, sSFX, bookCost10, staffCost10, sManaCost, flags, sBookLvl, sStaffLvl, minInt, { sMissiles[2] } sManaAdj, sMinMana, sStaffMin, sStaffMax -/*SpellID::Null*/ { nullptr, SfxID::None, 0, 0, 0, Fire, 0, 0, 0, { MissileID::Null, MissileID::Null, }, 0, 0, 40, 80 }, -/*SpellID::Firebolt*/ { P_("spell", "Firebolt"), SfxID::CastFire, 100, 5, 6, Fire | Targeted, 1, 1, 15, { MissileID::Firebolt, MissileID::Null, }, 1, 3, 40, 80 }, -/*SpellID::Healing*/ { P_("spell", "Healing"), SfxID::CastHealing, 100, 5, 5, Magic | AllowedInTown, 1, 1, 17, { MissileID::Healing, MissileID::Null, }, 3, 1, 20, 40 }, -/*SpellID::Lightning*/ { P_("spell", "Lightning"), SfxID::CastLightning, 300, 15, 10, Lightning | Targeted, 4, 3, 20, { MissileID::LightningControl, MissileID::Null, }, 1, 6, 20, 60 }, -/*SpellID::Flash*/ { P_("spell", "Flash"), SfxID::CastLightning, 750, 50, 30, Lightning, 5, 4, 33, { MissileID::FlashBottom, MissileID::FlashTop }, 2, 16, 20, 40 }, -/*SpellID::Identify*/ { P_("spell", "Identify"), SfxID::CastSkill, 0, 10, 13, Magic | AllowedInTown, -1, -1, 23, { MissileID::Identify, MissileID::Null, }, 2, 1, 8, 12 }, -/*SpellID::FireWall*/ { P_("spell", "Fire Wall"), SfxID::CastFire, 600, 40, 28, Fire | Targeted, 3, 2, 27, { MissileID::FireWallControl, MissileID::Null, }, 2, 16, 8, 16 }, -/*SpellID::TownPortal*/ { P_("spell", "Town Portal"), SfxID::CastSkill, 300, 20, 35, Magic | Targeted, 3, 3, 20, { MissileID::TownPortal, MissileID::Null, }, 3, 18, 8, 12 }, -/*SpellID::StoneCurse*/ { P_("spell", "Stone Curse"), SfxID::CastFire, 1200, 80, 60, Magic | Targeted, 6, 5, 51, { MissileID::StoneCurse, MissileID::Null, }, 3, 40, 8, 16 }, -/*SpellID::Infravision*/ { P_("spell", "Infravision"), SfxID::CastHealing, 0, 60, 40, Magic, -1, -1, 36, { MissileID::Infravision, MissileID::Null, }, 5, 20, 0, 0 }, -/*SpellID::Phasing*/ { P_("spell", "Phasing"), SfxID::CastFire, 350, 20, 12, Magic, 7, 6, 39, { MissileID::Phasing, MissileID::Null, }, 2, 4, 40, 80 }, -/*SpellID::ManaShield*/ { P_("spell", "Mana Shield"), SfxID::CastFire, 1600, 120, 33, Magic, 6, 5, 25, { MissileID::ManaShield, MissileID::Null, }, 0, 33, 4, 10 }, -/*SpellID::Fireball*/ { P_("spell", "Fireball"), SfxID::CastFire, 800, 30, 16, Fire | Targeted, 8, 7, 48, { MissileID::Fireball, MissileID::Null, }, 1, 10, 40, 80 }, -/*SpellID::Guardian*/ { P_("spell", "Guardian"), SfxID::CastFire, 1400, 95, 50, Fire | Targeted, 9, 8, 61, { MissileID::Guardian, MissileID::Null, }, 2, 30, 16, 32 }, -/*SpellID::ChainLightning*/ { P_("spell", "Chain Lightning"), SfxID::CastFire, 1100, 75, 30, Lightning, 8, 7, 54, { MissileID::ChainLightning, MissileID::Null, }, 1, 18, 20, 60 }, -/*SpellID::FlameWave*/ { P_("spell", "Flame Wave"), SfxID::CastFire, 1000, 65, 35, Fire | Targeted, 9, 8, 54, { MissileID::FlameWaveControl, MissileID::Null, }, 3, 20, 20, 40 }, -/*SpellID::DoomSerpents*/ { P_("spell", "Doom Serpents"), SfxID::CastFire, 0, 0, 0, Lightning, -1, -1, 0, { MissileID::Null, MissileID::Null, }, 0, 0, 40, 80 }, -/*SpellID::BloodRitual*/ { P_("spell", "Blood Ritual"), SfxID::CastFire, 0, 0, 0, Magic, -1, -1, 0, { MissileID::Null, MissileID::Null, }, 0, 0, 40, 80 }, -/*SpellID::Nova*/ { P_("spell", "Nova"), SfxID::CastLightning, 2100, 130, 60, Magic, 14, 10, 87, { MissileID::Nova, MissileID::Null, }, 3, 35, 16, 32 }, -/*SpellID::Invisibility*/ { P_("spell", "Invisibility"), SfxID::CastFire, 0, 0, 0, Magic, -1, -1, 0, { MissileID::Null, MissileID::Null, }, 0, 0, 40, 80 }, -/*SpellID::Inferno*/ { P_("spell", "Inferno"), SfxID::CastFire, 200, 10, 11, Fire | Targeted, 3, 2, 20, { MissileID::InfernoControl, MissileID::Null, }, 1, 6, 20, 40 }, -/*SpellID::Golem*/ { P_("spell", "Golem"), SfxID::CastFire, 1800, 110, 100, Fire | Targeted, 11, 9, 81, { MissileID::Golem, MissileID::Null, }, 6, 60, 16, 32 }, -/*SpellID::Rage*/ { P_("spell", "Rage"), SfxID::CastHealing, 0, 0, 15, Magic, -1, -1, 0, { MissileID::Rage, MissileID::Null, }, 1, 1, 0, 0 }, -/*SpellID::Teleport*/ { P_("spell", "Teleport"), SfxID::CastSkill, 2000, 125, 35, Magic | Targeted, 14, 12, 105, { MissileID::Teleport, MissileID::Null, }, 3, 15, 16, 32 }, -/*SpellID::Apocalypse*/ { P_("spell", "Apocalypse"), SfxID::CastFire, 3000, 200, 150, Fire, 19, 15, 149, { MissileID::Apocalypse, MissileID::Null, }, 6, 90, 8, 12 }, -/*SpellID::Etherealize*/ { P_("spell", "Etherealize"), SfxID::CastFire, 2600, 160, 100, Magic, -1, -1, 93, { MissileID::Etherealize, MissileID::Null, }, 0, 100, 2, 6 }, -/*SpellID::ItemRepair*/ { P_("spell", "Item Repair"), SfxID::CastSkill, 0, 0, 0, Magic | AllowedInTown, -1, -1, 255, { MissileID::ItemRepair, MissileID::Null, }, 0, 0, 40, 80 }, -/*SpellID::StaffRecharge*/ { P_("spell", "Staff Recharge"), SfxID::CastSkill, 0, 0, 0, Magic | AllowedInTown, -1, -1, 255, { MissileID::StaffRecharge, MissileID::Null, }, 0, 0, 40, 80 }, -/*SpellID::TrapDisarm*/ { P_("spell", "Trap Disarm"), SfxID::CastSkill, 0, 0, 0, Magic, -1, -1, 255, { MissileID::TrapDisarm, MissileID::Null, }, 0, 0, 40, 80 }, -/*SpellID::Elemental*/ { P_("spell", "Elemental"), SfxID::CastFire, 1050, 70, 35, Fire, 8, 6, 68, { MissileID::Elemental, MissileID::Null, }, 2, 20, 20, 60 }, -/*SpellID::ChargedBolt*/ { P_("spell", "Charged Bolt"), SfxID::CastFire, 100, 5, 6, Lightning | Targeted, 1, 1, 25, { MissileID::ChargedBolt, MissileID::Null, }, 1, 6, 40, 80 }, -/*SpellID::HolyBolt*/ { P_("spell", "Holy Bolt"), SfxID::CastFire, 100, 5, 7, Magic | Targeted, 1, 1, 20, { MissileID::HolyBolt, MissileID::Null, }, 1, 3, 40, 80 }, -/*SpellID::Resurrect*/ { P_("spell", "Resurrect"), SfxID::CastHealing, 400, 25, 20, Magic | AllowedInTown, -1, 5, 30, { MissileID::Resurrect, MissileID::Null, }, 0, 20, 4, 10 }, -/*SpellID::Telekinesis*/ { P_("spell", "Telekinesis"), SfxID::CastFire, 250, 20, 15, Magic, 2, 2, 33, { MissileID::Telekinesis, MissileID::Null, }, 2, 8, 20, 40 }, -/*SpellID::HealOther*/ { P_("spell", "Heal Other"), SfxID::CastHealing, 100, 5, 5, Magic | AllowedInTown, 1, 1, 17, { MissileID::HealOther, MissileID::Null, }, 3, 1, 20, 40 }, -/*SpellID::BloodStar*/ { P_("spell", "Blood Star"), SfxID::CastFire, 2750, 180, 25, Magic, 14, 13, 70, { MissileID::BloodStar, MissileID::Null, }, 2, 14, 20, 60 }, -/*SpellID::BoneSpirit*/ { P_("spell", "Bone Spirit"), SfxID::CastFire, 1150, 80, 24, Magic, 9, 7, 34, { MissileID::BoneSpirit, MissileID::Null, }, 1, 12, 20, 60 }, -/*SpellID::Mana*/ { P_("spell", "Mana"), SfxID::CastHealing, 100, 5, 255, Magic | AllowedInTown, -1, 5, 17, { MissileID::Mana, MissileID::Null, }, 3, 1, 12, 24 }, -/*SpellID::Magi*/ { P_("spell", "the Magi"), SfxID::CastHealing, 10000, 20, 255, Magic | AllowedInTown, -1, 20, 45, { MissileID::Magi, MissileID::Null, }, 3, 1, 15, 30 }, -/*SpellID::Jester*/ { P_("spell", "the Jester"), SfxID::CastHealing, 10000, 20, 255, Magic | Targeted, -1, 4, 30, { MissileID::Jester, MissileID::Null, }, 3, 1, 15, 30 }, -/*SpellID::LightningWall*/ { P_("spell", "Lightning Wall"), SfxID::CastLightning, 600, 40, 28, Lightning | Targeted, 3, 2, 27, { MissileID::LightningWallControl, MissileID::Null, }, 2, 16, 8, 16 }, -/*SpellID::Immolation*/ { P_("spell", "Immolation"), SfxID::CastFire, 2100, 130, 60, Fire, 14, 10, 87, { MissileID::Immolation, MissileID::Null, }, 3, 35, 16, 32 }, -/*SpellID::Warp*/ { P_("spell", "Warp"), SfxID::CastSkill, 300, 20, 35, Magic, 3, 3, 25, { MissileID::Warp, MissileID::Null, }, 3, 18, 8, 12 }, -/*SpellID::Reflect*/ { P_("spell", "Reflect"), SfxID::CastSkill, 300, 20, 35, Magic, 3, 3, 25, { MissileID::Reflect, MissileID::Null, }, 3, 15, 8, 12 }, -/*SpellID::Berserk*/ { P_("spell", "Berserk"), SfxID::CastSkill, 300, 20, 35, Magic | Targeted, 3, 3, 35, { MissileID::Berserk, MissileID::Null, }, 3, 15, 8, 12 }, -/*SpellID::RingOfFire*/ { P_("spell", "Ring of Fire"), SfxID::CastFire, 600, 40, 28, Fire, 5, 5, 27, { MissileID::RingOfFire, MissileID::Null, }, 2, 16, 8, 16 }, -/*SpellID::Search*/ { P_("spell", "Search"), SfxID::CastSkill, 300, 20, 15, Magic, 1, 3, 25, { MissileID::Search, MissileID::Null, }, 1, 1, 8, 12 }, -/*SpellID::RuneOfFire*/ { P_("spell", "Rune of Fire"), SfxID::CastHealing, 800, 30, 255, Magic | Targeted, -1, -1, 48, { MissileID::RuneOfFire, MissileID::Null, }, 1, 10, 40, 80 }, -/*SpellID::RuneOfLight*/ { P_("spell", "Rune of Light"), SfxID::CastHealing, 800, 30, 255, Magic | Targeted, -1, -1, 48, { MissileID::RuneOfLight, MissileID::Null, }, 1, 10, 40, 80 }, -/*SpellID::RuneOfNova*/ { P_("spell", "Rune of Nova"), SfxID::CastHealing, 800, 30, 255, Magic | Targeted, -1, -1, 48, { MissileID::RuneOfNova, MissileID::Null, }, 1, 10, 40, 80 }, -/*SpellID::RuneOfImmolation*/ { P_("spell", "Rune of Immolation"), SfxID::CastHealing, 800, 30, 255, Magic | Targeted, -1, -1, 48, { MissileID::RuneOfImmolation, MissileID::Null, }, 1, 10, 40, 80 }, -/*SpellID::RuneOfStone*/ { P_("spell", "Rune of Stone"), SfxID::CastHealing, 800, 30, 255, Magic | Targeted, -1, -1, 48, { MissileID::RuneOfStone, MissileID::Null, }, 1, 10, 40, 80 }, - // clang-format on -}; +std::vector SpellsData; tl::expected ParseSpellId(std::string_view value) { @@ -137,4 +232,34 @@ tl::expected ParseSpellId(std::string_view value) return tl::make_unexpected("Unknown enum value"); } +void LoadSpellData() +{ + SpellsData.clear(); + const std::string_view filename = "txtdata\\spells\\spelldat.tsv"; + DataFile dataFile = DataFile::loadOrDie(filename); + SpellsData.reserve(dataFile.numRecords() + 1); + AddNullSpell(); + dataFile.skipHeaderOrDie(filename); + for (DataFileRecord record : dataFile) { + RecordReader reader { record, filename }; + SpellData &item = SpellsData.emplace_back(); + reader.advance(); // skip id + reader.readString("name", item.sNameText); + reader.read("soundId", item.sSFX, ParseSpellSoundId); + reader.readInt("bookCost10", item.bookCost10); + reader.readInt("staffCost10", item.staffCost10); + reader.readInt("manaCost", item.sManaCost); + reader.readEnumList("flags", item.flags, ParseSpellDataFlag); + reader.readInt("bookLevel", item.sBookLvl); + reader.readInt("staffLevel", item.sStaffLvl); + reader.readInt("minIntelligence", item.minInt); + reader.readEnumArray("missiles", /*fillMissing=*/std::make_optional(MissileID::Null), item.sMissiles, ParseMissileId); + reader.readInt("manaMultiplier", item.sManaAdj); + reader.readInt("minMana", item.sMinMana); + reader.readInt("staffMin", item.sStaffMin); + reader.readInt("staffMax", item.sStaffMax); + } + SpellsData.shrink_to_fit(); +} + } // namespace devilution diff --git a/Source/spelldat.h b/Source/spelldat.h index d2076756ed5..3e9db791279 100644 --- a/Source/spelldat.h +++ b/Source/spelldat.h @@ -6,8 +6,10 @@ #pragma once #include +#include #include #include +#include #include @@ -221,7 +223,7 @@ enum class SpellDataFlags : uint8_t { use_enum_as_flags(SpellDataFlags); struct SpellData { - const char *sNameText; + std::string sNameText; SfxID sSFX; uint16_t bookCost10; uint8_t staffCost10; @@ -262,11 +264,13 @@ struct SpellData { } }; -extern const SpellData SpellsData[]; +extern std::vector SpellsData; inline const SpellData &GetSpellData(SpellID spellId) { return SpellsData[static_cast::type>(spellId)]; } +void LoadSpellData(); + } // namespace devilution diff --git a/Source/translation_dummy.cpp b/Source/translation_dummy.cpp index c60f27e0928..cbfdbcd64a2 100644 --- a/Source/translation_dummy.cpp +++ b/Source/translation_dummy.cpp @@ -781,3 +781,54 @@ const char *ITEM_SUFFIX_94_NAME = N_("blocking"); const char *ITEM_SUFFIX_95_NAME = N_("devastation"); const char *ITEM_SUFFIX_96_NAME = N_("decay"); const char *ITEM_SUFFIX_97_NAME = N_("peril"); +const char *SPELL_FIREBOLT_NAME = P_("spell", "Firebolt"); +const char *SPELL_HEALING_NAME = P_("spell", "Healing"); +const char *SPELL_LIGHTNING_NAME = P_("spell", "Lightning"); +const char *SPELL_FLASH_NAME = P_("spell", "Flash"); +const char *SPELL_IDENTIFY_NAME = P_("spell", "Identify"); +const char *SPELL_FIRE_WALL_NAME = P_("spell", "Fire Wall"); +const char *SPELL_TOWN_PORTAL_NAME = P_("spell", "Town Portal"); +const char *SPELL_STONE_CURSE_NAME = P_("spell", "Stone Curse"); +const char *SPELL_INFRAVISION_NAME = P_("spell", "Infravision"); +const char *SPELL_PHASING_NAME = P_("spell", "Phasing"); +const char *SPELL_MANA_SHIELD_NAME = P_("spell", "Mana Shield"); +const char *SPELL_FIREBALL_NAME = P_("spell", "Fireball"); +const char *SPELL_GUARDIAN_NAME = P_("spell", "Guardian"); +const char *SPELL_CHAIN_LIGHTNING_NAME = P_("spell", "Chain Lightning"); +const char *SPELL_FLAME_WAVE_NAME = P_("spell", "Flame Wave"); +const char *SPELL_DOOM_SERPENTS_NAME = P_("spell", "Doom Serpents"); +const char *SPELL_BLOOD_RITUAL_NAME = P_("spell", "Blood Ritual"); +const char *SPELL_NOVA_NAME = P_("spell", "Nova"); +const char *SPELL_INVISIBILITY_NAME = P_("spell", "Invisibility"); +const char *SPELL_INFERNO_NAME = P_("spell", "Inferno"); +const char *SPELL_GOLEM_NAME = P_("spell", "Golem"); +const char *SPELL_RAGE_NAME = P_("spell", "Rage"); +const char *SPELL_TELEPORT_NAME = P_("spell", "Teleport"); +const char *SPELL_APOCALYPSE_NAME = P_("spell", "Apocalypse"); +const char *SPELL_ETHEREALIZE_NAME = P_("spell", "Etherealize"); +const char *SPELL_ITEM_REPAIR_NAME = P_("spell", "Item Repair"); +const char *SPELL_STAFF_RECHARGE_NAME = P_("spell", "Staff Recharge"); +const char *SPELL_TRAP_DISARM_NAME = P_("spell", "Trap Disarm"); +const char *SPELL_ELEMENTAL_NAME = P_("spell", "Elemental"); +const char *SPELL_CHARGED_BOLT_NAME = P_("spell", "Charged Bolt"); +const char *SPELL_HOLY_BOLT_NAME = P_("spell", "Holy Bolt"); +const char *SPELL_RESURRECT_NAME = P_("spell", "Resurrect"); +const char *SPELL_TELEKINESIS_NAME = P_("spell", "Telekinesis"); +const char *SPELL_HEAL_OTHER_NAME = P_("spell", "Heal Other"); +const char *SPELL_BLOOD_STAR_NAME = P_("spell", "Blood Star"); +const char *SPELL_BONE_SPIRIT_NAME = P_("spell", "Bone Spirit"); +const char *SPELL_MANA_NAME = P_("spell", "Mana"); +const char *SPELL_THE_MAGI_NAME = P_("spell", "the Magi"); +const char *SPELL_THE_JESTER_NAME = P_("spell", "the Jester"); +const char *SPELL_LIGHTNING_WALL_NAME = P_("spell", "Lightning Wall"); +const char *SPELL_IMMOLATION_NAME = P_("spell", "Immolation"); +const char *SPELL_WARP_NAME = P_("spell", "Warp"); +const char *SPELL_REFLECT_NAME = P_("spell", "Reflect"); +const char *SPELL_BERSERK_NAME = P_("spell", "Berserk"); +const char *SPELL_RING_OF_FIRE_NAME = P_("spell", "Ring of Fire"); +const char *SPELL_SEARCH_NAME = P_("spell", "Search"); +const char *SPELL_RUNE_OF_FIRE_NAME = P_("spell", "Rune of Fire"); +const char *SPELL_RUNE_OF_LIGHT_NAME = P_("spell", "Rune of Light"); +const char *SPELL_RUNE_OF_NOVA_NAME = P_("spell", "Rune of Nova"); +const char *SPELL_RUNE_OF_IMMOLATION_NAME = P_("spell", "Rune of Immolation"); +const char *SPELL_RUNE_OF_STONE_NAME = P_("spell", "Rune of Stone"); diff --git a/assets/txtdata/spells/spelldat.tsv b/assets/txtdata/spells/spelldat.tsv new file mode 100644 index 00000000000..5f135d211ac --- /dev/null +++ b/assets/txtdata/spells/spelldat.tsv @@ -0,0 +1,52 @@ +id name soundId bookCost10 staffCost10 manaCost flags bookLevel staffLevel minIntelligence missiles manaMultiplier minMana staffMin staffMax +Firebolt Firebolt CastFire 100 5 6 Fire,Targeted 1 1 15 Firebolt 1 3 40 80 +Healing Healing CastHealing 100 5 5 Magic,AllowedInTown 1 1 17 Healing 3 1 20 40 +Lightning Lightning CastLightning 300 15 10 Lightning,Targeted 4 3 20 LightningControl 1 6 20 60 +Flash Flash CastLightning 750 50 30 Lightning 5 4 33 FlashBottom,FlashTop 2 16 20 40 +Identify Identify CastSkill 0 10 13 Magic,AllowedInTown -1 -1 23 Identify 2 1 8 12 +FireWall Fire Wall CastFire 600 40 28 Fire,Targeted 3 2 27 FireWallControl 2 16 8 16 +TownPortal Town Portal CastSkill 300 20 35 Magic,Targeted 3 3 20 TownPortal 3 18 8 12 +StoneCurse Stone Curse CastFire 1200 80 60 Magic,Targeted 6 5 51 StoneCurse 3 40 8 16 +Infravision Infravision CastHealing 0 60 40 Magic -1 -1 36 Infravision 5 20 0 0 +Phasing Phasing CastFire 350 20 12 Magic 7 6 39 Phasing 2 4 40 80 +ManaShield Mana Shield CastFire 1600 120 33 Magic 6 5 25 ManaShield 0 33 4 10 +Fireball Fireball CastFire 800 30 16 Fire,Targeted 8 7 48 Fireball 1 10 40 80 +Guardian Guardian CastFire 1400 95 50 Fire,Targeted 9 8 61 Guardian 2 30 16 32 +ChainLightning Chain Lightning CastFire 1100 75 30 Lightning 8 7 54 ChainLightning 1 18 20 60 +FlameWave Flame Wave CastFire 1000 65 35 Fire,Targeted 9 8 54 FlameWaveControl 3 20 20 40 +DoomSerpents Doom Serpents CastFire 0 0 0 Lightning -1 -1 0 0 0 40 80 +BloodRitual Blood Ritual CastFire 0 0 0 Magic -1 -1 0 0 0 40 80 +Nova Nova CastLightning 2100 130 60 Magic 14 10 87 Nova 3 35 16 32 +Invisibility Invisibility CastFire 0 0 0 Magic -1 -1 0 0 0 40 80 +Inferno Inferno CastFire 200 10 11 Fire,Targeted 3 2 20 InfernoControl 1 6 20 40 +Golem Golem CastFire 1800 110 100 Fire,Targeted 11 9 81 Golem 6 60 16 32 +Rage Rage CastHealing 0 0 15 Magic -1 -1 0 Rage 1 1 0 0 +Teleport Teleport CastSkill 2000 125 35 Magic,Targeted 14 12 105 Teleport 3 15 16 32 +Apocalypse Apocalypse CastFire 3000 200 150 Fire 19 15 149 Apocalypse 6 90 8 12 +Etherealize Etherealize CastFire 2600 160 100 Magic -1 -1 93 Etherealize 0 100 2 6 +ItemRepair Item Repair CastSkill 0 0 0 Magic,AllowedInTown -1 -1 255 ItemRepair 0 0 40 80 +StaffRecharge Staff Recharge CastSkill 0 0 0 Magic,AllowedInTown -1 -1 255 StaffRecharge 0 0 40 80 +TrapDisarm Trap Disarm CastSkill 0 0 0 Magic -1 -1 255 TrapDisarm 0 0 40 80 +Elemental Elemental CastFire 1050 70 35 Fire 8 6 68 Elemental 2 20 20 60 +ChargedBolt Charged Bolt CastFire 100 5 6 Lightning,Targeted 1 1 25 ChargedBolt 1 6 40 80 +HolyBolt Holy Bolt CastFire 100 5 7 Magic,Targeted 1 1 20 HolyBolt 1 3 40 80 +Resurrect Resurrect CastHealing 400 25 20 Magic,AllowedInTown -1 5 30 Resurrect 0 20 4 10 +Telekinesis Telekinesis CastFire 250 20 15 Magic 2 2 33 Telekinesis 2 8 20 40 +HealOther Heal Other CastHealing 100 5 5 Magic,AllowedInTown 1 1 17 HealOther 3 1 20 40 +BloodStar Blood Star CastFire 2750 180 25 Magic 14 13 70 BloodStar 2 14 20 60 +BoneSpirit Bone Spirit CastFire 1150 80 24 Magic 9 7 34 BoneSpirit 1 12 20 60 +Mana Mana CastHealing 100 5 255 Magic,AllowedInTown -1 5 17 Mana 3 1 12 24 +Magi the Magi CastHealing 10000 20 255 Magic,AllowedInTown -1 20 45 Magi 3 1 15 30 +Jester the Jester CastHealing 10000 20 255 Magic,Targeted -1 4 30 Jester 3 1 15 30 +LightningWall Lightning Wall CastLightning 600 40 28 Lightning,Targeted 3 2 27 LightningWallControl 2 16 8 16 +Immolation Immolation CastFire 2100 130 60 Fire 14 10 87 Immolation 3 35 16 32 +Warp Warp CastSkill 300 20 35 Magic 3 3 25 Warp 3 18 8 12 +Reflect Reflect CastSkill 300 20 35 Magic 3 3 25 Reflect 3 15 8 12 +Berserk Berserk CastSkill 300 20 35 Magic,Targeted 3 3 35 Berserk 3 15 8 12 +RingOfFire Ring of Fire CastFire 600 40 28 Fire 5 5 27 RingOfFire 2 16 8 16 +Search Search CastSkill 300 20 15 Magic 1 3 25 Search 1 1 8 12 +RuneOfFire Rune of Fire CastHealing 800 30 255 Magic,Targeted -1 -1 48 RuneOfFire 1 10 40 80 +RuneOfLight Rune of Light CastHealing 800 30 255 Magic,Targeted -1 -1 48 RuneOfLight 1 10 40 80 +RuneOfNova Rune of Nova CastHealing 800 30 255 Magic,Targeted -1 -1 48 RuneOfNova 1 10 40 80 +RuneOfImmolation Rune of Immolation CastHealing 800 30 255 Magic,Targeted -1 -1 48 RuneOfImmolation 1 10 40 80 +RuneOfStone Rune of Stone CastHealing 800 30 255 Magic,Targeted -1 -1 48 RuneOfStone 1 10 40 80 diff --git a/test/inv_test.cpp b/test/inv_test.cpp index e3a19391ba7..29a9ba28b85 100644 --- a/test/inv_test.cpp +++ b/test/inv_test.cpp @@ -18,6 +18,7 @@ class InvTest : public ::testing::Test { static void SetUpTestSuite() { + LoadSpellData(); LoadItemData(); } }; diff --git a/test/pack_test.cpp b/test/pack_test.cpp index 6d706b48786..1601fcb92a2 100644 --- a/test/pack_test.cpp +++ b/test/pack_test.cpp @@ -414,6 +414,7 @@ class PackTest : public ::testing::Test { static void SetUpTestSuite() { + LoadSpellData(); LoadItemData(); } }; @@ -953,6 +954,7 @@ class NetPackTest : public ::testing::Test { static void SetUpTestSuite() { + LoadSpellData(); LoadPlayerDataFiles(); LoadMonsterData(); LoadItemData(); diff --git a/test/timedemo_test.cpp b/test/timedemo_test.cpp index 105f2872d34..28fd3915508 100644 --- a/test/timedemo_test.cpp +++ b/test/timedemo_test.cpp @@ -52,6 +52,7 @@ void RunTimedemo(std::string timedemoFolderName) HeadlessMode = true; demo::InitPlayBack(demoNumber, true); + LoadSpellData(); LoadPlayerDataFiles(); LoadMissileData(); LoadMonsterData(); diff --git a/test/writehero_test.cpp b/test/writehero_test.cpp index 0cf3f5aecc2..5b5e59cbff2 100644 --- a/test/writehero_test.cpp +++ b/test/writehero_test.cpp @@ -374,6 +374,7 @@ TEST(Writehero, pfile_write_hero) MyPlayerId = 0; MyPlayer = &Players[MyPlayerId]; + LoadSpellData(); LoadPlayerDataFiles(); LoadItemData(); _uiheroinfo info {}; diff --git a/tools/extract_translation_data.py b/tools/extract_translation_data.py index f22c3575fe3..35c101f45f6 100755 --- a/tools/extract_translation_data.py +++ b/tools/extract_translation_data.py @@ -10,6 +10,7 @@ unique_itemdat_path = root.joinpath("assets/txtdata/items/unique_itemdat.tsv") item_prefixes_path = root.joinpath("assets/txtdata/items/item_prefixes.tsv") item_suffixes_path = root.joinpath("assets/txtdata/items/item_suffixes.tsv") +spelldat_path = root.joinpath("assets/txtdata/spells/spelldat.tsv") with open(translation_dummy_path, 'w') as temp_source: temp_source.write(f'/**\n') @@ -62,3 +63,9 @@ name = row['name'] var_name = f'ITEM_SUFFIX_{i}' temp_source.write(f'const char *{var_name}_NAME = N_("{name}");\n') + with open(spelldat_path, 'r') as tsv: + reader = csv.DictReader(tsv, delimiter='\t') + for i, row in enumerate(reader): + name = row['name'] + var_name = 'SPELL_' + name.upper().replace(' ', '_').replace('-', '_') + temp_source.write(f'const char *{var_name}_NAME = P_("spell", "{name}");\n')