Implement AdventureBot class rotations, group roles and instance encounters
This commit is contained in:
1 parent
d3273c677e
commit
a758239da9
33 files changed
+5102
-66
No files matched your search
@@ -1,3 +1,72 @@
|
||||
# Aktuell: Klassen-/Rollen-Kampf und Spieler-Instanzmodus implementiert
|
||||
|
||||
Nutzer autorisierte die gemeinsam besprochene Umsetzung. Ausgangsstand sauber
|
||||
bei d3273c677e auf feature/clientless-player-core. Dieser Implementierungsstand wird auf ausdrücklichen Nutzerauftrag lokal
|
||||
committet; kein Push. Doku: doc/clientless/class-combat.md.
|
||||
|
||||
Neue AdventureRotationPolicy/TalentPolicy und AdventureEncounterMgr.28 feste
|
||||
Cataclysm-PvE-Talentvorlagen (27 Spezialisierungen plus separates Bär/Katze) gegen
|
||||
lokale4.3.4-DBC abgeglichen; alle Punktbudgets1..41 testbar. Solo-Spec im neuen
|
||||
Lua-Profil, alte Profile deterministisch aus gespeichertem Seed. Alle Klassen
|
||||
mit gemeinsamen Prioritäten und nativen Casts; native Ressourcen/Procs/Combo/
|
||||
HolyPower, Cata-Spezialisierungen, Heiler/Tanks/Damage und DungeonBot-Unterstützung.
|
||||
Gruppenwechsel schaltet Skillung/Ausrüstung sicher um, nie im Kampf/Flug/Fallen.
|
||||
Alternativausrüstung geschützt/wiederverwendet. Level85 episch ilvl>=359,
|
||||
Furor vor31 Talentpunkten Einhand; Frost-DK zwei Einhandwaffen/Unholy Presence.
|
||||
Native Gifte/Schamanenimbues, Begleiter und Rollenunterstützung.
|
||||
|
||||
Spielergruppen erhalten echten Instanzmodus mit ausgesetzter Planausführung,
|
||||
nativen Portalwegen, allgemeinem DynamicObject-Flächenausweichen und C++-Hooks
|
||||
für spätere Instanz-/Boss-Skripte. Alte performative Visits werden ausdrücklich
|
||||
beim echten Eintritt ohne künstlichen Ausgang/Verwundung beendet. Reine Botgruppen
|
||||
bleiben performativ. Tote warten auf Wiederbelebung, nach Wipe Eingangsrückweg
|
||||
mit Teleport bei fehlendem/gescheitertem Weg, malusfreie Wiederbelebung, Sammeln,
|
||||
Rückkehr/Todespunkt bzw. lebenden Spieler priorisieren. Kick verlässt Instanz
|
||||
sicher und nimmt Solo-Spec/Tagesplan wieder auf. Keine Boss-Skripte eingeführt.
|
||||
|
||||
Native Integration deckte CORE-Bug auf: Hunter Lock and Load56342 leert Effect2,
|
||||
SpellValue berechnete trotzdem diesen parentlosen Default-Effekt. Scaling war
|
||||
zudem uninitialisiert. Crash untergdb reproduziert; ROOTFIX jetzt SpellInfo.h
|
||||
Scaling{} und Spell.cpp IsEffect-Gate. Regression test_empty_spell_effect.py.
|
||||
Erste zwei Starts scheiterten daran, dritter korrigierter Start läuft stabil.
|
||||
Alter vorheriger Server PID139425 zeigte außerdem vorhandenen Shutdown-Segfault.
|
||||
|
||||
80/80 automatisierte Tests erfolgreich: /tmp/vcore-role-tests-recovery.log.
|
||||
Danach letzte gezielte Klassen-/Talent-/Ausrüstungs-/Empty-Effect-Tests erfolgreich.
|
||||
Vollständiger1046-Schritt-Build /tmp/vcore-role-build-recovery.log und finaler
|
||||
9-Schritt-Build /tmp/vcore-role-build-final-fixed.log erfolgreich, installiert
|
||||
/tmp/vcore-role-install-fixed.log. Testserver LIVE PID167319, tmux
|
||||
vcore-worldserver, Console /tmp/vcore-class-combat-20261028-start3.log.
|
||||
Frisches ready, fortlaufender Prozess und Port8085 geprüft. Loader unverändert
|
||||
AKTIV10 persistent+2 temporär je Fraktion, alle24 online/pending0/error leer.
|
||||
NICHT auf historische deaktivierte Defaults zurücksetzen.
|
||||
|
||||
Native Hunter44 gab einmal initial unvollständige Talentvorbereitung aus,
|
||||
automatisch korrigiert: gespeicherter Survival-Baum809/28 Punkte bei Level63
|
||||
passt zur nativen DBC. Vorhandene Missing-map719 und Into-the-Wilderness84729
|
||||
misc9-Warnungen bleiben, kein behobenes vollständiges Terrain/System versprechen.
|
||||
Inventarhashes von menschlichem Allitester21 und Arthoran26 unverändert.
|
||||
Belege: doc/clientless/checkpoints/2026-10-28-class-combat/.
|
||||
|
||||
Noch abnehmen: tatsächliche Tank-/Heiler-/DD-Kämpfe mit Spielern, DungeonBot-
|
||||
Zusammenspiel und Wipe/Portalwege in der Spielwelt. Automatisierte Adaptertests
|
||||
sind KEINE Ingame-Abnahme. Bossmechaniken jenseits generischer Bodenflächen
|
||||
werden später pro Instanz ergänzt. Kein laufender Build.
|
||||
|
||||
Nächster besprochener, NOCH NICHT implementierter Schritt: gemeinsame Gruppenreise
|
||||
für Spieler-/Botführer, getrennte Formationsplätze, sichere Boden-/Flugverfolgung,
|
||||
Bodenreittiere/Taxi/Schiffe/Tiefenbahn/Portale. Im Stand kleine Formation, unterwegs
|
||||
Kolonne, mehrreihig nur bei geprüftem Platz; Engstellen einreihig. Kein gültiger
|
||||
Pfad -> stehenbleiben/wiederprüfen, keine Klippensprünge. >500Yards über10Minuten
|
||||
führt unter bisherigen Austrittsschutzregeln zum Verlassen; bekannte Übergänge,
|
||||
Nachreisen und Transport-Recovery pausieren den Abstandszähler. Nicht flugfähige
|
||||
Bots folgen nur zu sicheren erreichbaren Bodenpunkten bei begrenzter Höhe.
|
||||
Transportverlust -> letzter sicherer Einstiegsknoten, zuerst sicherer Weg, bei
|
||||
Scheitern gezielter Rücksetzteleport, nächste Fahrt abwarten. Nutzer bat jetzt
|
||||
zuerst um Commit der vorherigen Klassen-/Rollenänderungen.
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Loader auf Nutzerauftrag für24 Bots aktiviert
|
||||
|
||||
Nutzer hat ausdrücklich aktive Testpopulation festgelegt: Allianz10 persistent
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
TC> Adventure loader enabled=1 schema=1 busy=0 pools=10/2/10/2 known=25 rotations=0 manager=24 error=''
|
||||
pool=0 faction=alliance persistent=1 target=10 online=10 pending=0 saved=10
|
||||
pool=1 faction=alliance persistent=0 target=2 online=2 pending=0 saved=3
|
||||
pool=2 faction=horde persistent=1 target=10 online=10 pending=0 saved=10
|
||||
pool=3 faction=horde persistent=0 target=2 online=2 pending=0 saved=2
|
||||
@@ -0,0 +1,13 @@
|
||||
Thread 1 (Thread 0x7ffff7e22380 (LWP 160076) "worldserver"):
|
||||
#0 SpellEffectInfo::CalcBaseValue (this=0x7fffdff27230, caster=0x7fffaa8bd000, target=0x0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Spells/SpellInfo.cpp:550
|
||||
#1 0x0000555557ebd906 in SpellValue::SpellValue (this=0x7fff12f6a520, proto=0x7fffdff26f00, caster=0x7fffaa8bd000) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Spells/Spell.cpp:539
|
||||
#2 0x0000555557ebdaa1 in Spell::Spell (this=0x7fff4c696000, caster=0x7fffaa8bd000, info=0x7fffdff26f00, triggerFlags=TRIGGERED_FULL_MASK, originalCasterGUID=...) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Spells/Spell.cpp:566
|
||||
#3 0x00005555575d8d49 in WorldObject::CastSpell (this=0x7fffaa8bd000, targets=..., spellId=56342, args=...) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Entities/Object/Object.cpp:2959
|
||||
#4 0x000055555761a312 in Player::AddSpell (this=0x7fffaa8bd000, spellId=56342, active=true, learning=true, dependent=false, disabled=false, loading=false, fromSkill=0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Entities/Player/Player.cpp:2918
|
||||
#5 0x000055555761b9d5 in Player::LearnSpell (this=0x7fffaa8bd000, spell_id=56342, dependent=false, fromSkill=0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Entities/Player/Player.cpp:3109
|
||||
#6 0x00005555576a6ccd in Player::LearnTalent (this=0x7fffaa8bd000, talentId=9452, talentRank=0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/Entities/Player/Player.cpp:25810
|
||||
#7 0x00005555580b2532 in operator() (__closure=0x7fffffffba80, id=9452, rank=0) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureGeneratorMgr.cpp:117
|
||||
#8 0x00005555580bca94 in AdventureTalentPolicy::Spend<(anonymous namespace)::Talents(Player*, const AdventureGeneratorPolicy::Archetype&)::<lambda(uint32, unsigned int)>, (anonymous namespace)::Talents(Player*, const AdventureGeneratorPolicy::Archetype&)::<lambda()> >(const std::vector<AdventureTalentPolicy::Talent, std::allocator<AdventureTalentPolicy::Talent> > &, struct {...}, struct {...}) (build=std::vector of length 19, capacity 19 = {...}, learn=..., remaining=...) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureTalentPolicy.h:18
|
||||
#9 0x00005555580b28fc in (anonymous namespace)::Talents (p=0x7fffaa8bd000, a=...) at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureGeneratorMgr.cpp:117
|
||||
#10 0x00005555580b7385 in AdventureGeneratorMgr::Prepare (this=0x55555a18ff00 <AdventureGeneratorMgr::instance()::mgr>, p=0x7fffaa8bd000, a=..., error="") at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureGeneratorMgr.cpp:419
|
||||
#11 0x00005555580bad71 in AdventureGeneratorMgr::Reconcile (this=0x55555a18ff00 <AdventureGeneratorMgr::instance()::mgr>, player=0x7fffaa8bd000, error="") at /home/wow_admin/wow_cata_server/source/TrinityCore/src/server/game/World/AdventureGeneratorMgr.cpp:601
|
||||
@@ -0,0 +1,11 @@
|
||||
Build: Debug, existing CMake configuration, -j10. Full1046-step rebuild
|
||||
for SpellInfo header fix plus final9-step class refinements succeeded.
|
||||
Installed with cmake --install after verifying no worldserver/authserver active.
|
||||
Third fresh startup: /tmp/vcore-class-combat-20261028-start3.log.
|
||||
Fresh ready marker, persistent worldserver PID167319 and port8085 LISTEN verified.
|
||||
Loader enabled, pools online10/2/10/2, pending0, manager24, error empty.
|
||||
Initial native Hunter44 talent preparation warning recovered: saved level63
|
||||
Survival tree809 with28 talent points, matching NumTalentsAtLevel.dbc.
|
||||
Original inventories21/26 unchanged against pre-task hashes.
|
||||
No in-game tank/healer/player-party acceptance or raid completion claimed.
|
||||
Existing missing-map719 and Into the Wilderness84729 misc9 warnings remain.
|
||||
@@ -0,0 +1,257 @@
|
||||
TEST tests/clientless/test_adventure_area_node.py
|
||||
Production area destination excludes outside and unreachable graph nodes.
|
||||
|
||||
TEST tests/clientless/test_adventure_async_database.py
|
||||
Async creation DB contracts: prepared connection masks, native security column names, no future waits, successful-save cache publication and world-update-only loader hook passed
|
||||
|
||||
TEST tests/clientless/test_adventure_behavior_policy.py
|
||||
24-hour generation, shared intents with changing destinations, broad location classification and repetition penalties passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_class_rotation.py
|
||||
Production class rotations: persistent PvE solo pools, native rank aura IDs, combo/holy power/proc/execute/resource gates, Eclipse and healer coverage passed
|
||||
|
||||
TEST tests/clientless/test_adventure_combat_exercise.py
|
||||
Production exercise: stance/charge/melee/casts, native validation delegation, bounded lifetime, invalid targets and unchanged native phases passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_combat_preparation.py
|
||||
Production talent learner: native tiers/dependencies/class gates, all 26 legal level60 points, no capstone bypass and repeat safety passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_combat_reactions.py
|
||||
Production combat focus, damage ordering, risk filters, corpse radius/delay, flight/group override, blocked escape, danger avoidance and malus-free persisted fallback passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_day_persistence.py
|
||||
Production day planner: all 24 TINYINT slots restored across logins, incomplete/old-day replacement, full transaction and curiosity/level weighting passed
|
||||
|
||||
TEST tests/clientless/test_adventure_economy.py
|
||||
Economy: native service roles, friendly/phase/LOS/bounds gates, no bot service targets and coherent performative cues passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_economy_catalog.py
|
||||
Economy catalog: eleven actions, three faction-bound service anchors, native flag gates and idempotent preservation passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_encounter.py
|
||||
Production encounters: bot-only exclusion, mixed mode, extension hook, wait/revive, wipe fallback/reentry/gather/return, living-player priority, kick exit and offline cleanup passed
|
||||
|
||||
TEST tests/clientless/test_adventure_equipment_maintenance.py
|
||||
Production gear maintenance: same birth/retention gate rejects wrong armor/stats, malformed effects, level, temporary, reputation, map, holiday, native-use and slot mismatches passed
|
||||
|
||||
TEST tests/clientless/test_adventure_exploration.py
|
||||
Exploration: visible native props only, no chests/traps/furniture, phase/map/LOS/range gates and contextual preparation passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_exploration_catalog.py
|
||||
Exploration catalog: ten actions, shared scenes, default wander replacement, explicit category eligibility and preserved edits passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_generator_equipment.py
|
||||
Production equipment affinity: mana-only/direct/triggered procs, caster/physical stat effects and bounded cycles passed
|
||||
|
||||
TEST tests/clientless/test_adventure_generator_ownership.py
|
||||
Production generator commands: batch bounds, dedicated ownership, persistent/online/removing retirement protection and retired login refusal passed
|
||||
|
||||
TEST tests/clientless/test_adventure_generator_policy.py
|
||||
Production generator: 6144 cultural names and 61440 real Lua profiles, profession coherence, personality ranges, escaping and equipment/armor gates passed
|
||||
|
||||
TEST tests/clientless/test_adventure_graph_handover.py
|
||||
Production graph handover waits for ship/taxi/flight/teleport and stops at safe road/node boundaries.
|
||||
|
||||
TEST tests/clientless/test_adventure_ground_avoidance.py
|
||||
Production ground avoidance: harmful/friendly, phase169, height, reachable safe endpoint, rejected paths and reuse without cast/movement thrashing passed
|
||||
|
||||
TEST tests/clientless/test_adventure_group_plan.py
|
||||
Production group plans: leader meeting/goal/deadline inheritance, untouched personal day, manual preparation and instance-state guards passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_groups.py
|
||||
Production recruitment: capitals, different goals, native inviter leadership, joining existing groups, capacity/reservations, raid conversion, planned transition and local contact passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_instance.py
|
||||
Production instance lifecycle: time budget, true entry, denied/shared entry, native rear exit, restart, expired copy, late arrival, HP and return replay passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_instance_autoresume.py
|
||||
Deferred instance script: missing schema/visits stays idle; no login work on startup; saved visitors resume with Adventure controllers and native accounts.
|
||||
|
||||
TEST tests/clientless/test_adventure_instance_begin.py
|
||||
Production entrance travel uses the existing location planner, requires mapping and retains normal social/area travel.
|
||||
|
||||
TEST tests/clientless/test_adventure_instance_catalog.py
|
||||
Instance migrations: outside entrances only, native graph preserved, disabled/missing transitions excluded, edits and saved visits preserved.
|
||||
|
||||
TEST tests/clientless/test_adventure_instance_loader.py
|
||||
Production instance recovery: no startup DB/login work, asynchronous delayed/paced manual recovery, generated-bot quota ownership, absent-schema compatibility and existing preparation ticks passed
|
||||
|
||||
TEST tests/clientless/test_adventure_instance_persistence.py
|
||||
Production instance persistence: phases, original deadline, instance ID, transactional HP marker, failed commit and intent release passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_intent_state.py
|
||||
Production intent arrival, failed/partial travel, deadlines and safe schedule handover passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_interactions.py
|
||||
/tmp/tmpefb2v_zu/test.cpp: In member function ‘void AdventureInteractionMgr::Leave(Player*, Record&, const char*)’:
|
||||
/tmp/tmpefb2v_zu/test.cpp:142:69: warning: unused parameter ‘reason’ [-Wunused-parameter]
|
||||
142 | void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
|
||||
| ~~~~~~~~~~~~^~~~~~
|
||||
production interaction manager: invitations, factions, reservations, follow, AFK and deferred departure passed
|
||||
|
||||
TEST tests/clientless/test_adventure_leisure.py
|
||||
Leisure: substantial curved MMAP routes, mirrored return, interrupted return, rejected corridors, actual arrival, companion follow, riding, flight and fishing passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_leisure_catalog.py
|
||||
Leisure migration: native city anchor, shared scene references, pet-walk mapping and preservation of edited weights passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_location_import.py
|
||||
Locations: actual Debug SMALLINT reads retain continent IDs; explicit location eligibility and edited weight overrides passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_manager_inventory.py
|
||||
Production inventory maintenance: only full backpacks, native stack trimming, level-appropriate supplies, tools, leash, fishing pole, containers and untouched equipped slots passed
|
||||
|
||||
TEST tests/clientless/test_adventure_micro_reset.py
|
||||
Micro cleanup ends physical local movement, preserves transit and does not stand up dead players.
|
||||
|
||||
TEST tests/clientless/test_adventure_population.py
|
||||
Production population: four independent faction/persistence targets, fair selection, pending reservations, retries, existing inventory, once-per-boot rotation and connected/persistent protection passed
|
||||
|
||||
TEST tests/clientless/test_adventure_profession_catalog.py
|
||||
Profession catalog: native workstation roles, no workshop/resource inference, idempotence and preserved edits passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_profession_profile.py
|
||||
Production profession parser, real Lua profiles, primary restrictions and secondary permissions passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_profession_sightline.py
|
||||
Native workstation working-surface sightline and blocked geometry rejection passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_profession_targets.py
|
||||
Production profession targets: native locks, phase/spawn/loot, stations, corpses, history and trainers passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_profile_selection.py
|
||||
Production profile selection: real Lua ID dictionaries, exact 9:1 companion weighting, zero/negative/infinite/NaN/oversized weights rejected and legacy companion fallback passed
|
||||
|
||||
TEST tests/clientless/test_adventure_pvp.py
|
||||
Production PvP selector: level boundaries, radius, factions, native legality, navigation, intent, personality and resources passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_repair_catalog.py
|
||||
Repair action catalog: proper native service requirement, economy-only intent and preserved administrator weights passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_rest.py
|
||||
Recovery: authored places, bounds, arrival, occupancy, full rest duration, failure, reload and interruption passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_rest_fire.py
|
||||
Rest fire: shared scene mapping, additive migration, preserved weights and authored creation-place policy passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_rest_space.py
|
||||
Sleep footprint: furniture, body extent, overhead collision, ledges, stairs, missing heights, rotation and scale passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_rest_targets.py
|
||||
Native sleep targets: chair exclusion, phase, despawn, floor search, indoors, water, storey, occupancy, bed/surface preference passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_rotation.py
|
||||
/tmp/tmpk5e83d51/test.cpp: In instantiation of ‘std::string Trinity::StringFormat(const char*, A ...) [with A = {}; std::string = std::__cxx11::basic_string<char>]’:
|
||||
/tmp/tmpk5e83d51/test.cpp:22:126: required from ‘void Tx::PAppend(const char*, A ...) [with A = {}]’
|
||||
22 | struct Tx{std::vector<std::string>sql;template<class...A>void PAppend(char const*f,A...a){sql.push_back(Trinity::StringFormat(f,a...));}};
|
||||
| ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
|
||||
/tmp/tmpk5e83d51/test.cpp:51:75: required from here
|
||||
51 | auto barrier=CharacterDatabase.BeginTransaction();barrier->PAppend("DO 0");
|
||||
| ~~~~~~~~~~~~~~~~^~~~~~~~
|
||||
/tmp/tmpk5e83d51/test.cpp:11:113: warning: format not a string literal and no format arguments [-Wformat-security]
|
||||
11 | namespace Trinity{template<class...A>std::string StringFormat(char const* f,A...args){char b[4096];std::snprintf(b,sizeof(b),f,args...);return b;}}
|
||||
| ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~
|
||||
Production rotation: connected/native-online protection, ownership refusal, native delete failure, world cleanup failure and journal-retained retry before successful profile/plan/state removal passed
|
||||
|
||||
TEST tests/clientless/test_adventure_social.py
|
||||
Production social flow: approach before talk, coherent greeting/talk/listen/farewell, lost targets, native chair confirmation and real fire confirmation passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_social_fire.py
|
||||
Shared fire: native detection/provisioning, buildings/city/parent/zone restrictions, existing fires and outdoor/water checks passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_supplies.py
|
||||
Production supplies: exact10% boundary, bandage-before-food, native lockout fallback, recovery completion, combat potions, mana-only drinking and daily/post-combat top-ups passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_supply_catalog.py
|
||||
Supply migrations: all five roles, level60 netherweave tier, editable/disabled catalog preservation and persistent daily stamp passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_supply_items.py
|
||||
Production native-item delegation: usable bandage tiers, native cooldown/lockout gates, real inventory consumption, self targeting and level60 secondary caps passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_talent_templates.py
|
||||
Canonical Cata PvE builds: 4.3.4 class/tree/rank/prerequisite gates, all 1..41 point budgets, solo selection coverage passed
|
||||
|
||||
TEST tests/clientless/test_adventure_training_catalog.py
|
||||
Training catalog: real dummy requirement, leisure-only intent, bounded duration and preserved administrator weights passed.
|
||||
|
||||
TEST tests/clientless/test_adventure_walk.py
|
||||
Production travel gait reaches native spline, survives repaths, prevents mounting and preserves legacy running defaults.
|
||||
|
||||
TEST tests/clientless/test_clientless_character_factory.py
|
||||
Production native factory: character/profile atomicity, failed preparation/commit, cache publication order, account/security/name/cap gates passed
|
||||
|
||||
TEST tests/clientless/test_clientless_companion.py
|
||||
Native companion: provisioning, delayed summon, ownership, real item target, leash confirmation/timeout, reuse and cleanup passed.
|
||||
|
||||
TEST tests/clientless/test_clientless_damage_notifications.py
|
||||
Production damage handoff: 16 concurrent map-worker writers, ordered GUID-only events, clientless-only/positive damage gates and post-health notification passed.
|
||||
|
||||
TEST tests/clientless/test_clientless_near_teleport_observer.py
|
||||
Production near-teleport observer packet includes actual mover GUID and destination; clientless timestamp refreshed, regular client timestamp preserved.
|
||||
|
||||
TEST tests/clientless/test_clientless_skill_preparation.py
|
||||
Production offline skill preparation: native tier caps, no downgrading, rank/level/race/tier gates and no modification of online players passed
|
||||
|
||||
TEST tests/clientless/test_clientless_social_notifications.py
|
||||
Production social/helper queues: concurrent GUID-only delivery, bounded queues, human/self/zero-damage gates, pet attribution and lethal-cleanup ordering passed
|
||||
|
||||
TEST tests/clientless/test_default_phase.py
|
||||
Native PhaseShift: implicit/default 169 visibility, genuine custom-phase separation and reference updates passed.
|
||||
|
||||
TEST tests/clientless/test_empty_spell_effect.py
|
||||
Native empty spell effects: poisoned-memory scaling initialization, Lock and Load cleared slot, active effect values and all-empty spell values passed
|
||||
|
||||
TEST tests/clientless/test_flight_area_query.py
|
||||
Native terrain query map/phase, area/zone ordering and cache passed.
|
||||
|
||||
TEST tests/clientless/test_flight_handoff.py
|
||||
Flight handoff before spline end, terminal landing, return corners, collision stop and actual distance passed.
|
||||
|
||||
TEST tests/clientless/test_flight_policy.py
|
||||
Flight native eligibility, license/profile/zone policy, RP costs and fail-safe return decisions passed.
|
||||
|
||||
TEST tests/clientless/test_follow_spline_velocity.py
|
||||
Native follow: actual player/creature spline velocity, client gait and independent follower speeds passed.
|
||||
|
||||
TEST tests/clientless/test_free_flight.py
|
||||
Flight geometry passed: takeoff/landing, smoothed peak, ceiling, lateral/climbing avoidance, missing terrain and blocked corridors.
|
||||
|
||||
TEST tests/clientless/test_free_flight_planner.py
|
||||
ground planner regressions and dynamic flight/node/location/virtual-exit integration passed
|
||||
|
||||
TEST tests/clientless/test_ground_exit_continuation.py
|
||||
optional exits: repeated probes, retained route, delayed success, forward resume, flight fallback passed
|
||||
|
||||
TEST tests/clientless/test_item_equip_cooldown.py
|
||||
Native item equip cooldown: zero/sentinel/missing spells safely skipped, valid use cooldown preserved and longer/disabled cooldowns untouched passed
|
||||
|
||||
TEST tests/clientless/test_local_spline_segments.py
|
||||
Production local spline clipping passes native path-length validation, tiny remainders and 1000 sampled paths.
|
||||
|
||||
TEST tests/clientless/test_lua_reload.py
|
||||
Lua reload regression tests passed (production reload/bindings, real Lua 5.4).
|
||||
|
||||
TEST tests/clientless/test_player_pure_max_skill.py
|
||||
Native pure skill cap: learned skills return stored cap; missing/deleted/zero-rank skills return0.
|
||||
|
||||
TEST tests/clientless/test_portal_transfer.py
|
||||
Portal near/far ACK and denied same-map transfer regressions passed.
|
||||
|
||||
TEST tests/clientless/test_portal_use.py
|
||||
Interactive portal native click, near/far/global ACK, denial, timeout and target checks passed.
|
||||
Native portal destination discovery and unsupported/ambiguous spell rejection passed.
|
||||
Production clientless ACK completion sequence increments only after completed near/far transfers.
|
||||
|
||||
TEST tests/clientless/test_shared_group_route.py
|
||||
Production generic shared routes: same edges/taxi/free flight, independent execution state, final local goal, late catch-up, permissions and stale-goal rejection passed.
|
||||
|
||||
TEST tests/clientless/test_taxi_import_disabled.py
|
||||
Taxi importer preserves disabled rows and their reasons.
|
||||
|
||||
TEST tests/clientless/test_transport.py
|
||||
Transport dock, frame, recorder, cross-map copy and ride regressions passed
|
||||
|
||||
TEST tests/clientless/test_transport_grid_loading.py
|
||||
Core transport grid initialization regressions passed (no CPC dependency).
|
||||
|
||||
TOTAL 80 FAILED []
|
||||
@@ -0,0 +1,146 @@
|
||||
# AdventureBot class combat and player instance groups
|
||||
|
||||
Implementation target: Cataclysm 4.3.4. DungeonBot CreatureAI remains independent.
|
||||
|
||||
## Shared class behavior
|
||||
|
||||
AdventureRotationPolicy supplies deterministic PvE priorities for the supported
|
||||
specializations. AdventureTalentPolicy supplies one canonical 41-point template
|
||||
per specialization (separate bear/cat templates). At lower levels native talent
|
||||
point, tier and prerequisite checks select the attainable prefix. The same
|
||||
class/spec/level uses the same rules; personality does not randomly select spells.
|
||||
Native player casts enforce ranks, cooldowns, resources, equipment, line of sight,
|
||||
range, procs, combo points and holy power. Successful casts use COMBAT_CAST logs.
|
||||
|
||||
Each generated profile stores identity.solo_specialization alongside the existing
|
||||
group specialization and role. Older v2 profiles derive the same stable choice
|
||||
from their persisted seed. Solo choices are Arms/Fury, Retribution,
|
||||
Marksmanship/Survival, Assassination/Combat, Shadow, Frost/Unholy,
|
||||
Elemental/Enhancement, Arcane/Fire, all three Warlock specs, Balance/Cat.
|
||||
Group membership selects the profile group specialization. Leaving/kicking selects
|
||||
solo again at a safe out-of-combat opportunity. Reconciliation retries with a
|
||||
backoff. Named profiles share talent templates without replacing their inventory,
|
||||
professions or fixed level. Temporary poison/weapon imbues use native enchant/proc
|
||||
handling; no synthetic damage or generated combat resources.
|
||||
|
||||
Generated loadouts reuse stored compatible equipment through native swaps. Full
|
||||
inventory cleanup protects strongest candidates for both loadouts. Level 85 uses
|
||||
native epic items of item level >=359 (no legendaries); class, stats, armor, native
|
||||
use and existing restriction checks still apply. Tank avoidance/mastery/stamina
|
||||
and healer spirit receive extra weight; resilience receives a PvE penalty.
|
||||
|
||||
## Group roles
|
||||
|
||||
Healers prioritize living allies and summoned DungeonBots by missing health and
|
||||
attack pressure, with a large emergency-health priority. They use direct healing,
|
||||
HoTs/shields, suitable group healing, dispels and native mana abilities. They
|
||||
approach an injured ally when range/LOS prevents healing. Capable classes resurrect
|
||||
players outside group combat. Food/bandages yield to group combat; personal
|
||||
recovery rules otherwise remain unchanged.
|
||||
|
||||
Tanks pick up attackers threatening non-tanks, use native taunts and AoE threat
|
||||
abilities, and do not taunt from recognized player or DungeonBot tanks. Damage
|
||||
bots prefer tank targets. Group protection supersedes solo first-damager focus;
|
||||
solo focus and the no-group-flight rule are unchanged. A leading tank can pull
|
||||
nearby hostiles while the player advances, or a valid player-selected target,
|
||||
after checking nearby companions, own health and healer mana. Missing roles do
|
||||
not block play and are not filled by changing somebody's profile. Secondary
|
||||
tanks stay near the leading tank in actual instance groups.
|
||||
|
||||
Ranged classes attack from range; approaching melee assistants prefer the target's
|
||||
rear when another member tanks. Native spell validation and conservative AoE
|
||||
checks prevent casting into nearby unengaged hostile creatures. Hunter pets are
|
||||
loaded or created through native tame/pet functions; Warlock summons and Unholy
|
||||
ghouls use native spells. Pet AI assists against the owner's current combat target.
|
||||
|
||||
## Actual and performative visits
|
||||
|
||||
Only player-containing groups enter actual encounter mode. Bot-only visits retain
|
||||
their existing abstract entrance stay, block deadline, exit and injury behavior.
|
||||
Following a player into an instance routes to a native entrance via TravelGraph
|
||||
and local MMAP, then submits the normal area-trigger handler: native access,
|
||||
difficulty and instance binding still apply.
|
||||
|
||||
AdventureEncounterMgr pauses personal intent execution inside actual instances.
|
||||
An old abstract visit record is finished without abstract exit/injury before real
|
||||
play proceeds. The current civil-hour plan resumes outside, without replaying
|
||||
missed slots. A dismissed bot leaves to the native exit destination when safe.
|
||||
|
||||
Dead bots accept native resurrection requests. Otherwise they wait while players
|
||||
or DungeonBot companions survive. After a full wipe they release, path to the
|
||||
outside entrance and re-enter with a 50% resurrection without sickness or additional spirit-healer durability
|
||||
penalties. Failed/rejected/stuck entrance paths allow an exterior entrance teleport.
|
||||
Bots gather inside before returning to the recorded death point; a living player
|
||||
in the instance takes precedence. A ghost login may reconstruct context from its
|
||||
native corpse. Surviving players outside cause companions to leave safely too.
|
||||
|
||||
## Encounter extensions and limits
|
||||
|
||||
Register a map-specific C++ callback with AdventureEncounterMgr::Register(map,
|
||||
callback) during script registration. Return true when it owns that update;
|
||||
false runs the generic ground-avoidance behavior. An empty callback unregisters
|
||||
an override. Callbacks can inspect encounter states, spells, auras and threat,
|
||||
and delegate normal class casting to the common combat loop by returning false.
|
||||
Do not mutate the callback registry from concurrent map updates.
|
||||
|
||||
The generic fallback detects hostile damaging DynamicObjects in the current
|
||||
phase and height band, then selects a complete, unsuspicious MMAP path to a safe
|
||||
endpoint. It retains an existing safe escape path and interrupts casting only
|
||||
when starting a necessary escape. It does not claim to understand every scripted
|
||||
NPC hazard, frontal attack, soak, stack mechanic, vehicle or special-action button.
|
||||
Those belong in later map/boss-specific callbacks. No specific boss scripts are
|
||||
introduced in this change. This is functional shared combat, not a promise of
|
||||
simulator-optimal DPS or verified completion of every raid.
|
||||
|
||||
## Cataclysm sources
|
||||
|
||||
Retrieved 2026-10-28. Only the Cataclysm paths/isCata data were used; no TBC,
|
||||
Wrath, MoP or Retail builds were substituted. Talent positions and names were
|
||||
matched against local 4.3.4 Talent/TalentTab/Spell DBC data, including rejecting
|
||||
a duplicate obsolete Arcane talent whose spell no longer exists. Numeric DBC
|
||||
metadata needed for reproducible rank/tier/prerequisite tests lives in
|
||||
`tests/clientless/fixtures/cata_pve_talents.json`.
|
||||
|
||||
- https://wowtbc.gg/cata/class-guides/arms-warrior/
|
||||
- https://wowtbc.gg/cata/class-guides/fury-warrior/
|
||||
- https://wowtbc.gg/cata/class-guides/protection-warrior/
|
||||
- https://wowtbc.gg/cata/class-guides/holy-paladin/
|
||||
- https://wowtbc.gg/cata/class-guides/protection-paladin/
|
||||
- https://wowtbc.gg/cata/class-guides/retribution-paladin/
|
||||
- https://wowtbc.gg/cata/class-guides/marksmanship-hunter/
|
||||
- https://wowtbc.gg/cata/class-guides/survival-hunter/
|
||||
- https://wowtbc.gg/cata/class-guides/assassination-rogue/
|
||||
- https://wowtbc.gg/cata/class-guides/combat-rogue/
|
||||
- https://wowtbc.gg/cata/class-guides/discipline-priest/
|
||||
- https://wowtbc.gg/cata/class-guides/holy-priest/
|
||||
- https://wowtbc.gg/cata/class-guides/shadow-priest/
|
||||
- https://wowtbc.gg/cata/class-guides/blood-tank-death-knight/
|
||||
- https://wowtbc.gg/cata/class-guides/frost-death-knight/
|
||||
- https://wowtbc.gg/cata/class-guides/unholy-death-knight/
|
||||
- https://wowtbc.gg/cata/class-guides/elemental-shaman/
|
||||
- https://wowtbc.gg/cata/class-guides/enhancement-shaman/
|
||||
- https://wowtbc.gg/cata/class-guides/restoration-shaman/
|
||||
- https://wowtbc.gg/cata/class-guides/arcane-mage/
|
||||
- https://wowtbc.gg/cata/class-guides/fire-mage/
|
||||
- https://wowtbc.gg/cata/class-guides/affliction-warlock/
|
||||
- https://wowtbc.gg/cata/class-guides/demonology-warlock/
|
||||
- https://wowtbc.gg/cata/class-guides/destruction-warlock/
|
||||
- https://wowtbc.gg/cata/class-guides/balance-druid/
|
||||
- https://wowtbc.gg/cata/class-guides/feral-dps-druid/
|
||||
- https://wowtbc.gg/cata/class-guides/restoration-druid/
|
||||
- https://wowtbc.gg/cata/class-guides/feral-tank-druid/
|
||||
|
||||
Additional healing/tanking context:
|
||||
|
||||
- https://www.icy-veins.com/cataclysm-classic/holy-priest-pve-rotation-cooldowns-abilities
|
||||
- https://www.wowhead.com/cata/guide/classes/warrior/protection/tank-rotation-cooldowns-abilities-pve
|
||||
|
||||
## Native integration correction
|
||||
|
||||
Canonical Survival talents exposed a native Lock and Load (56342) crash:
|
||||
SpellMgr clears one effect slot, while SpellValue previously evaluated every
|
||||
slot unconditionally. Empty SpellEffectInfo also left its Scaling fields
|
||||
uninitialized. Empty scaling now initializes to zero and SpellValue evaluates
|
||||
only active effects. The native reproduction and regression test are separate
|
||||
from gameplay acceptance. Fury equipment additionally respects the canonical
|
||||
31-point Titan's Grip unlock before choosing two-handed weapons.
|
||||
@@ -536,7 +536,8 @@ SpellValue::SpellValue(SpellInfo const* proto, WorldObject const* caster)
|
||||
{
|
||||
memset(EffectBasePoints, 0, sizeof(EffectBasePoints));
|
||||
for (uint32 i = 0; i < MAX_SPELL_EFFECTS; ++i)
|
||||
EffectBasePoints[i] = proto->Effects[i].CalcBaseValue(caster, nullptr);
|
||||
if (proto->Effects[i].IsEffect())
|
||||
EffectBasePoints[i] = proto->Effects[i].CalcBaseValue(caster, nullptr);
|
||||
|
||||
CustomBasePointsMask = 0;
|
||||
MaxAffectedTargets = proto->MaxAffectedTargets;
|
||||
|
||||
@@ -252,7 +252,7 @@ public:
|
||||
SpellEffectInfo() : _spellInfo(nullptr), _effIndex(0), Effect(0), ApplyAuraName(0), AuraPeriod(0), DieSides(0),
|
||||
RealPointsPerLevel(0.f), BasePoints(0), PointsPerComboPoint(0), Amplitude(0.f), DamageMultiplier(0.f),
|
||||
BonusMultiplier(0.f), MiscValue(0), MiscValueB(0), Mechanic(MECHANIC_NONE), TargetARadiusEntry(nullptr), TargetBRadiusEntry(nullptr),
|
||||
ChainTarget(0), ItemType(0), TriggerSpell(0), ImplicitTargetConditions(nullptr) { }
|
||||
ChainTarget(0), ItemType(0), TriggerSpell(0), ImplicitTargetConditions(nullptr), Scaling{} { }
|
||||
|
||||
SpellEffectInfo(SpellEntry const* spellEntry, SpellInfo const* spellInfo, uint8 effIndex, SpellEffectEntry const* effect);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureEncounterMgr.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include "Group.h"
|
||||
#include "AdventureLocationMgr.h"
|
||||
@@ -198,6 +199,12 @@ void AdventureBehaviorMgr::Online(Player * player)
|
||||
if (! record.Schedule && Known(profile->InitialIntent)) SetWanted(player, record, profile->InitialIntent, profile->InitialLocation, GameTime::GetGameTime() + profile->InitialDurationSeconds);
|
||||
}
|
||||
|
||||
void AdventureBehaviorMgr::EnterPlayerInstance(Player* player)
|
||||
{
|
||||
auto it=_records.find(player->GetGUID().GetCounter());
|
||||
if(it!=_records.end() && it->second.Instance.Active)FinishInstance(player,it->second);
|
||||
}
|
||||
|
||||
void AdventureBehaviorMgr::RefreshDay(Player* player)
|
||||
{
|
||||
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end() || !it->second.Schedule)return;
|
||||
@@ -1249,6 +1256,11 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
|
||||
auto entry = _records.find(guid);
|
||||
if (entry == _records.end() || ! entry->second.Enabled || ! player->IsInWorld()) return;
|
||||
auto & r = entry->second;
|
||||
if(sAdventureEncounterMgr->Active(player))
|
||||
{
|
||||
if(r.Instance.Active)FinishInstance(player,r);
|
||||
return;
|
||||
}
|
||||
r.MicroTimer = r.MicroTimer > diff ? r.MicroTimer - diff : 0;
|
||||
r.GestureTimer = r.GestureTimer > diff ? r.GestureTimer - diff : 0;
|
||||
r.RetryTimer = r.RetryTimer > diff ? r.RetryTimer - diff : 0;
|
||||
|
||||
@@ -30,6 +30,7 @@ public:
|
||||
void IntentChanged(Player* player, ClientlessIntent const* intent);
|
||||
void Update(Player* player, uint32 diff);
|
||||
void RefreshDay(Player* player);
|
||||
void EnterPlayerInstance(Player* player);
|
||||
bool Command(ChatHandler* handler, char const* args);
|
||||
void InterruptForCombat(Player* player);
|
||||
void InterruptForInteraction(Player* player);
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "Group.h"
|
||||
#include "LFG.h"
|
||||
#include "AdventureCombatPolicy.h"
|
||||
#include "AdventureRotationPolicy.h"
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
#include "AdventureEncounterMgr.h"
|
||||
#include "Pet.h"
|
||||
#include "CreatureAI.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
@@ -18,6 +25,7 @@
|
||||
#include "MoveSpline.h"
|
||||
#include "Map.h"
|
||||
#include "Spell.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "SpellAuras.h"
|
||||
#include "WorldSession.h"
|
||||
#include "WorldPacket.h"
|
||||
@@ -27,6 +35,33 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool AdventureTank(Unit* unit)
|
||||
{
|
||||
if(auto creature=unit->ToCreature())
|
||||
return creature->IsVianoAdventurer() && creature->GetScriptName().find("_tank")!=std::string::npos;
|
||||
auto player=unit->ToPlayer();if(!player)return false;
|
||||
if(auto group=player->GetGroup())if(group->GetLfgRoles(player->GetGUID())&lfg::PLAYER_ROLE_TANK)return true;
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
if(profile)return profile->Role=="tank";
|
||||
auto spec=PlayerAI::GetPlayerSpec(player);
|
||||
return (player->getClass()==CLASS_WARRIOR && spec==2) || (player->getClass()==CLASS_PALADIN && spec==1) ||
|
||||
(player->getClass()==CLASS_DEATH_KNIGHT && spec==0) || (player->getClass()==CLASS_DRUID && player->HasAura(5487));
|
||||
}
|
||||
std::list<Unit*> AdventureAllies(Player* player)
|
||||
{
|
||||
std::list<Unit*> result;player->GetPartyMembers(result);
|
||||
if(auto group=player->GetGroup())for(auto ref=group->GetFirstMember();ref;ref=ref->next())
|
||||
if(auto member=ref->GetSource())if(player->IsInMap(member))
|
||||
{
|
||||
result.push_back(member);
|
||||
std::list<Unit*> subgroup;member->GetPartyMembers(subgroup);
|
||||
result.splice(result.end(),subgroup);
|
||||
}
|
||||
result.sort([](Unit* a,Unit* b){return a->GetGUID()<b->GetGUID();});result.unique();return result;
|
||||
}
|
||||
}
|
||||
// Native player spell checks, cooldowns, rage and autoattacks. No charmer/fake AI
|
||||
// installation and no generated resources or synthetic damage.
|
||||
class AdventureCombatMgr::Fighter : public PlayerAI
|
||||
@@ -34,33 +69,215 @@ class AdventureCombatMgr::Fighter : public PlayerAI
|
||||
public:
|
||||
explicit Fighter(Player* player):PlayerAI(player) { }
|
||||
void UpdateAI(uint32) override { }
|
||||
void Tick(Unit* target,uint32 diff)
|
||||
bool Ranged() const {return AdventureRotationPolicy::Ranged(me->getClass(),GetPlayerSpec(me));}
|
||||
bool Resurrect(Unit* target)
|
||||
{
|
||||
uint32 spell=me->getClass()==CLASS_PRIEST?2006:me->getClass()==CLASS_PALADIN?7328:me->getClass()==CLASS_SHAMAN?2008:me->getClass()==CLASS_DRUID?50769:0;
|
||||
return spell && Cast(spell,target);
|
||||
}
|
||||
bool Heal(Unit* target,unsigned injured=0)
|
||||
{
|
||||
if(me->HasUnitState(UNIT_STATE_CASTING))return true;
|
||||
auto klass=me->getClass();
|
||||
float mana=me->GetMaxPower(POWER_MANA)?100.0f*me->GetPower(POWER_MANA)/me->GetMaxPower(POWER_MANA):100;
|
||||
if(mana<30 && target->GetHealthPct()>50)
|
||||
{
|
||||
uint32 recovery=klass==CLASS_DRUID?29166:klass==CLASS_SHAMAN?16190:klass==CLASS_PALADIN?54428:0;
|
||||
if(recovery && Cast(recovery,me))return true;
|
||||
}
|
||||
if(target->GetHealthPct()>45 && injured>=3)
|
||||
{
|
||||
uint32 groupSpell=klass==CLASS_DRUID?48438:klass==CLASS_SHAMAN?1064:klass==CLASS_PRIEST?34861:klass==CLASS_PALADIN?85222:0;
|
||||
if(groupSpell && Allowed({groupSpell},target) && Cast(groupSpell,target))return true;
|
||||
if(klass==CLASS_PRIEST && Cast(596,target))return true;
|
||||
}
|
||||
if(target->GetHealthPct()>50)
|
||||
{
|
||||
bool harmful=false;
|
||||
for(auto const& pair:target->GetAppliedAuras())
|
||||
if(!pair.second->IsPositive() && pair.second->GetBase()->GetSpellInfo()->Dispel!=DISPEL_NONE){harmful=true;break;}
|
||||
uint32 dispel=klass==CLASS_PRIEST?527:klass==CLASS_PALADIN?4987:klass==CLASS_DRUID?88423:klass==CLASS_SHAMAN?51886:0;
|
||||
if(harmful && dispel && Cast(dispel,target))return true;
|
||||
}
|
||||
for(auto const& action:AdventureRotationPolicy::Healing(me->getClass(),GetPlayerSpec(me),target->GetHealthPct()))
|
||||
if(Allowed(action,target) && Cast(action.Spell,target))return true;
|
||||
return false;
|
||||
}
|
||||
void Tick(Unit* target,uint32)
|
||||
{
|
||||
_rend=_rend>diff?_rend-diff:0;_filler=_filler>diff?_filler-diff:0;
|
||||
me->SetFacingToObject(target);
|
||||
if(me->HasUnitState(UNIT_STATE_CASTING))return;
|
||||
if(me->getClass()==CLASS_WARRIOR && !me->HasAura(2457) && Cast(2457,me))return;
|
||||
if(!me->IsWithinMeleeRange(target))return; // MMAP approach is owned by manager.
|
||||
if(me->GetVictim()!=target)me->Attack(target,true);
|
||||
DoAutoAttackIfReady();
|
||||
if(me->getClass()!=CLASS_WARRIOR)return;
|
||||
if(!me->HasAura(6673) && Cast(6673,me))return;
|
||||
if(Cast(12294,target))return;
|
||||
if(!_rend && !target->GetAuraApplicationOfRankedSpell(772,me->GetGUID()) && Cast(772,target))
|
||||
{_rend=12000;return;}
|
||||
if(!_filler)for(uint32 id:{7384u,1464u,78u})if(Cast(id,target)){_filler=3000;break;}
|
||||
if(auto pet=me->GetPet())
|
||||
{
|
||||
if(pet->IsAlive() && pet->GetVictim()!=target)pet->AI()->AttackStart(target);
|
||||
if(!pet->IsAlive())
|
||||
{
|
||||
if(me->getClass()==CLASS_HUNTER && Cast(982,pet))return;
|
||||
if(me->getClass()==CLASS_WARLOCK && Cast(688,me))return;
|
||||
}
|
||||
}
|
||||
else if(me->getClass()==CLASS_WARLOCK)
|
||||
{
|
||||
if(GetPlayerSpec(me)==1 && Cast(30146,me))return;
|
||||
if(Cast(688,me))return;
|
||||
}
|
||||
else if(me->getClass()==CLASS_DEATH_KNIGHT && GetPlayerSpec(me)==2)
|
||||
{
|
||||
if(Cast(46584,me))return;
|
||||
}
|
||||
else if(me->getClass()==CLASS_HUNTER && me->HasSpell(883))
|
||||
{
|
||||
if(auto creature=sObjectMgr->GetCreatureTemplate(299))
|
||||
if(creature->type==CREATURE_TYPE_BEAST)
|
||||
{
|
||||
auto pet=me->SummonPet(0,me->GetPositionX(),me->GetPositionY(),me->GetPositionZ(),me->GetOrientation(),HUNTER_PET,0);
|
||||
if(!pet)if(auto created=me->CreateTamedPetFrom(299))
|
||||
{
|
||||
if(me->GetMap()->AddToMap(created->ToCreature()))
|
||||
{
|
||||
me->SetMinion(created,true);created->InitTalentForLevel();
|
||||
created->SavePetToDB(PET_SAVE_NEW_PET);me->PetSpellInitialize();
|
||||
}
|
||||
else delete created;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(me->getClass()==CLASS_WARRIOR && !me->HasAura(6673) && Cast(6673,me))return;
|
||||
if(me->getClass()==CLASS_HUNTER && !me->HasAura(13165) && Cast(13165,me))return;
|
||||
if(me->getClass()==CLASS_PALADIN && !me->HasAura(31801) && !me->HasAura(20154))
|
||||
if(Cast(31801,me) || Cast(20154,me))return;
|
||||
bool ranged=Ranged();
|
||||
if(me->GetVictim()!=target)me->Attack(target,!ranged);
|
||||
if(me->getClass()==CLASS_HUNTER && !me->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL) && Cast(75,target))return;
|
||||
if(!ranged && me->IsWithinMeleeRange(target))DoAutoAttackIfReady();
|
||||
if(target->HasUnitState(UNIT_STATE_CASTING))
|
||||
{
|
||||
uint32 interrupt=0;
|
||||
switch(me->getClass())
|
||||
{
|
||||
case CLASS_WARRIOR:interrupt=6552;break;case CLASS_PALADIN:interrupt=96231;break;
|
||||
case CLASS_ROGUE:interrupt=1766;break;case CLASS_DEATH_KNIGHT:interrupt=47528;break;
|
||||
case CLASS_SHAMAN:interrupt=57994;break;case CLASS_MAGE:interrupt=2139;break;
|
||||
case CLASS_DRUID:interrupt=80965;break;default:break;
|
||||
}
|
||||
if(interrupt && Cast(interrupt,target))return;
|
||||
}
|
||||
auto manaMax=me->GetMaxPower(POWER_MANA);
|
||||
float mana=manaMax?100.0f*me->GetPower(POWER_MANA)/manaMax:100;
|
||||
if(me->getClass()==CLASS_MAGE && mana<20 && me->getAttackers().empty() && Cast(12051,me))return;
|
||||
if(me->getClass()==CLASS_WARLOCK && mana<25 && me->GetHealthPct()>70 && Cast(1454,me))return;
|
||||
if(me->getClass()==CLASS_PRIEST && mana<40 && Cast(34433,target))return;
|
||||
bool boss=false;if(auto creature=target->ToCreature())boss=creature->IsDungeonBoss() || creature->isWorldBoss();
|
||||
if(boss || target->GetHealth()>me->GetMaxHealth())
|
||||
{
|
||||
uint32 cooldown=0;auto spec=GetPlayerSpec(me);
|
||||
switch(me->getClass())
|
||||
{
|
||||
case CLASS_WARRIOR:cooldown=spec==2?0:spec==1?12292:1719;break;
|
||||
case CLASS_PALADIN:cooldown=31884;break;
|
||||
case CLASS_HUNTER:cooldown=3045;break;
|
||||
case CLASS_ROGUE:cooldown=spec==1?13750:0;break;
|
||||
case CLASS_DEATH_KNIGHT:cooldown=spec==1?51271:0;break;
|
||||
case CLASS_SHAMAN:cooldown=spec==0?16166:0;break;
|
||||
case CLASS_WARLOCK:cooldown=spec==1?47241:77801;break;
|
||||
case CLASS_DRUID:cooldown=spec==1?50334:0;break;
|
||||
default:break;
|
||||
}
|
||||
if(cooldown && Cast(cooldown,me))return;
|
||||
}
|
||||
if(boss)
|
||||
{
|
||||
if(me->getClass()==CLASS_SHAMAN && Cast(me->GetTeam()==ALLIANCE?32182:2825,me))return;
|
||||
if(me->getClass()==CLASS_MAGE && Cast(80353,me))return;
|
||||
}
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(me->GetGUID().GetCounter());
|
||||
bool tank=me->GetGroup() && profile && profile->Role=="tank";
|
||||
if(tank && target->GetVictim() && target->GetVictim()!=me)
|
||||
{
|
||||
// Never steal from another tank, including summoned DungeonBots.
|
||||
bool otherTank=AdventureTank(target->GetVictim());
|
||||
uint32 taunt=me->getClass()==CLASS_WARRIOR?355:me->getClass()==CLASS_PALADIN?62124:me->getClass()==CLASS_DEATH_KNIGHT?56222:me->getClass()==CLASS_DRUID?6795:0;
|
||||
if(!otherTank && taunt && Cast(taunt,target))return;
|
||||
}
|
||||
std::vector<Creature*> nearby;me->GetCreatureListWithEntryInGrid(nearby,0,12);
|
||||
unsigned engaged=0;
|
||||
for(auto enemy:nearby)if(enemy->IsAlive() && enemy->IsInCombat() && me->IsHostileTo(enemy))++engaged;
|
||||
if(tank && engaged>=2)
|
||||
{
|
||||
auto klass=me->getClass();
|
||||
if(klass==CLASS_WARRIOR){if(Cast(6343,me) || Cast(46968,target))return;}
|
||||
if(klass==CLASS_PALADIN){if(Cast(53595,target) || Cast(26573,me))return;}
|
||||
if(klass==CLASS_DEATH_KNIGHT){if(Cast(48721,me) || Cast(50842,target))return;}
|
||||
if(klass==CLASS_DRUID && me->HasAura(5487)){if(Cast(77758,me) || Cast(779,me))return;}
|
||||
}
|
||||
if(tank && me->getClass()==CLASS_DRUID)
|
||||
{
|
||||
if(!me->HasAura(5487) && Cast(5487,me))return;
|
||||
if(me->GetHealthPct()<40 && Cast(61336,me))return;
|
||||
for(uint32 id:{33878u,33745u,6807u})if(Cast(id,target))return;
|
||||
return;
|
||||
}
|
||||
for(auto const& action:AdventureRotationPolicy::Damage(me->getClass(),GetPlayerSpec(me)))
|
||||
{
|
||||
Unit* recipient=action.Self?me:target;
|
||||
if(Allowed(action,recipient) && Cast(action.Spell,recipient))return;
|
||||
}
|
||||
}
|
||||
private:
|
||||
bool Allowed(AdventureRotationPolicy::Action const& action,Unit* target) const
|
||||
{
|
||||
if(action.Combo && me->GetComboPoints()<action.Combo)return false;
|
||||
if((action.Spell==85256 || action.Spell==53600 || action.Spell==85222) &&
|
||||
me->GetPower(POWER_HOLY_POWER)<3 && !me->HasAura(90174))return false;
|
||||
if(action.Spell==8042 && me->getClass()==CLASS_SHAMAN && GetPlayerSpec(me)==0)
|
||||
{
|
||||
auto aura=me->GetAura(324);if(!aura || aura->GetCharges()<7)return false;
|
||||
}
|
||||
if(action.Spell==403 && me->getClass()==CLASS_SHAMAN && GetPlayerSpec(me)==1)
|
||||
{auto aura=me->GetAura(53817);if(!aura || aura->GetStackAmount()<5)return false;}
|
||||
if(action.Spell==2912 && me->HasAura(67483))return false;
|
||||
if(action.Spell==5176 && me->HasAura(67484))return false;
|
||||
if(action.Proc && !me->HasAura(action.Proc))return false;
|
||||
using C=AdventureRotationPolicy::Condition;
|
||||
switch(action.When)
|
||||
{
|
||||
case C::MissingAura:return !target->GetAuraApplicationOfRankedSpell(action.Aura?action.Aura:action.Spell,me->GetGUID());
|
||||
case C::Execute:return target->GetHealthPct()<=20;
|
||||
case C::Injured:return me->GetHealthPct()<65;
|
||||
case C::Emergency:return me->GetHealthPct()<35;
|
||||
case C::Resource:
|
||||
case C::LowResource:
|
||||
{
|
||||
auto type=me->GetPowerType();auto maximum=me->GetMaxPower(type);
|
||||
if(!maximum)return false;
|
||||
auto percent=me->GetPower(type)*100/maximum;
|
||||
return action.When==C::Resource?percent>=70:percent<40;
|
||||
}
|
||||
case C::Interrupt:return target->HasUnitState(UNIT_STATE_CASTING);
|
||||
default:return true;
|
||||
}
|
||||
}
|
||||
bool Cast(uint32 id,Unit* target)
|
||||
{
|
||||
auto cast=VerifySpellCast(id,target);if(!cast)return false;
|
||||
uint32 actual=cast.first->GetSpellInfo()->Id;
|
||||
auto info=cast.first->GetSpellInfo();
|
||||
uint32 actual=info->Id;
|
||||
if(!info->IsPositive() && info->IsAffectingArea())
|
||||
{
|
||||
float radius=8;
|
||||
for(auto const& effect:info->Effects)radius=std::max(radius,effect.CalcRadius(me));
|
||||
std::vector<Creature*> nearby;me->GetCreatureListWithEntryInGrid(nearby,0,std::min(80.0f,radius+me->GetExactDist(target)));
|
||||
for(auto enemy:nearby)
|
||||
if(enemy!=target && enemy->IsAlive() && me->IsInPhase(enemy) && me->IsHostileTo(enemy) &&
|
||||
!enemy->IsInCombat() && (enemy->GetExactDist(target)<radius || enemy->GetExactDist(me)<radius))
|
||||
{delete cast.first;return false;}
|
||||
}
|
||||
SpellCastTargets targets;targets.SetUnitTarget(cast.second);
|
||||
if(info->GetExplicitTargetMask()&TARGET_FLAG_DEST_LOCATION)targets.SetDst(cast.second->GetPosition());
|
||||
auto result=cast.first->prepare(targets);
|
||||
if(result==SPELL_CAST_OK)TC_LOG_INFO("server.adventurebot","COMBAT_CAST guid=%u target='%s' spell=%u",me->GetGUID().GetCounter(),target->GetGUID().ToString().c_str(),actual);
|
||||
return result==SPELL_CAST_OK;
|
||||
}
|
||||
uint32 _rend=0,_filler=0;
|
||||
};
|
||||
AdventureCombatMgr* AdventureCombatMgr::instance(){static AdventureCombatMgr mgr;return &mgr;}
|
||||
bool AdventureCombatMgr::Valid(Player* p,Unit* c,bool training) const
|
||||
@@ -151,7 +368,7 @@ void AdventureCombatMgr::Stop(Player* p)
|
||||
{
|
||||
auto it=_records.find(p->GetGUID().GetCounter());if(it!=_records.end())Finish(p,it->second);
|
||||
}
|
||||
void AdventureCombatMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());}
|
||||
void AdventureCombatMgr::Offline(Player* p){sAdventureEncounterMgr->Offline(p);_records.erase(p->GetGUID().GetCounter());}
|
||||
Unit* AdventureCombatMgr::PvPTarget(Player* p) const
|
||||
{
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
||||
@@ -341,6 +558,13 @@ bool AdventureCombatMgr::Death(Player* p,Record& r)
|
||||
TC_LOG_INFO("server.adventurebot","DEATH_RELEASE guid=%u",p->GetGUID().GetCounter());
|
||||
}
|
||||
if(p->InBattleground() || p->InArena() || p->IsBeingTeleported())return true;
|
||||
if(p->IsResurrectRequested()){p->ResurrectUsingRequestData();return true;}
|
||||
if(sAdventureEncounterMgr->Death(p))return true;
|
||||
if(p->GetMap()->IsDungeon() && p->GetGroup())
|
||||
{
|
||||
std::list<Unit*> allies;p->GetPartyMembers(allies);
|
||||
if(std::any_of(allies.begin(),allies.end(),[&](Unit* ally){return ally!=p && ally->IsAlive() && p->IsInMap(ally);}))return true;
|
||||
}
|
||||
if(!p->HasFlag(PLAYER_FLAGS,PLAYER_FLAGS_GHOST))
|
||||
{
|
||||
WorldPacket release(CMSG_REPOP_REQUEST,1);release<<uint8(0);p->GetSession()->HandleRepopRequestOpcode(release);
|
||||
@@ -403,6 +627,129 @@ bool AdventureCombatMgr::Death(Player* p,Record& r)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool AdventureCombatMgr::GroupTarget(Player* p,Record& r)
|
||||
{
|
||||
if(!p->GetGroup() || r.Training)return false;
|
||||
auto allies=AdventureAllies(p);Unit* tank=nullptr;Player* human=nullptr;
|
||||
for(auto ally:allies)
|
||||
{
|
||||
if(!ally->IsAlive() || !p->IsInMap(ally))continue;
|
||||
if(AdventureTank(ally) && (!tank || ally->GetGUID()<tank->GetGUID()))tank=ally;
|
||||
if(auto player=ally->ToPlayer())if(!player->GetSession()->IsClientless())
|
||||
if(!human || player->GetGUID()==p->GetGroup()->GetLeaderGUID())human=player;
|
||||
}
|
||||
Unit* chosen=nullptr;bool isTank=AdventureTank(p);
|
||||
if(isTank)
|
||||
{
|
||||
// Protect companions first, but do not steal another tank's targets.
|
||||
for(auto ally:allies)if(ally->IsAlive() && !AdventureTank(ally))
|
||||
for(auto attacker:ally->getAttackers())
|
||||
if(Valid(p,attacker,false) && attacker->GetVictim()==ally && p->GetExactDist(attacker)<35 &&
|
||||
(!chosen || p->GetExactDist(attacker)<p->GetExactDist(chosen)))chosen=attacker;
|
||||
}
|
||||
if(!chosen && tank && tank->IsInCombat() && Valid(p,tank->GetVictim(),false))chosen=tank->GetVictim();
|
||||
if(!chosen && human && human->IsInCombat() && Valid(p,human->GetVictim(),false))chosen=human->GetVictim();
|
||||
if(!chosen)for(auto ally:allies)
|
||||
{
|
||||
if(ally->IsInCombat() && Valid(p,ally->GetVictim(),false)){chosen=ally->GetVictim();break;}
|
||||
for(auto attacker:ally->getAttackers())if(Valid(p,attacker,false)){chosen=attacker;break;}
|
||||
if(chosen)break;
|
||||
}
|
||||
if(!chosen && isTank && tank==p && human && sAdventureEncounterMgr->Active(p) && !r.AI && !r.Cooldown)
|
||||
{
|
||||
bool ready=p->GetHealthPct()>=70 && human->IsAlive() && p->GetExactDist(human)<25;
|
||||
for(auto ally:allies)if(ally->IsAlive())
|
||||
{
|
||||
if(p->GetExactDist(ally)>35)ready=false;
|
||||
if(auto member=ally->ToPlayer())
|
||||
{
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(member->GetGUID().GetCounter());
|
||||
if((profile && profile->Role=="healer") || PlayerAI::IsPlayerHealer(member))
|
||||
if(member->GetMaxPower(POWER_MANA) && member->GetPower(POWER_MANA)*100/member->GetMaxPower(POWER_MANA)<35)ready=false;
|
||||
}
|
||||
if(auto creature=ally->ToCreature())
|
||||
if(creature->IsVianoAdventurer() && creature->GetScriptName().find("_healer")!=std::string::npos &&
|
||||
creature->GetMaxPower(POWER_MANA) && creature->GetPower(POWER_MANA)*100/creature->GetMaxPower(POWER_MANA)<35)ready=false;
|
||||
}
|
||||
if(ready)
|
||||
{
|
||||
auto selected=ObjectAccessor::GetUnit(*p,human->GetTarget());
|
||||
if(selected && Valid(p,selected,false) && p->IsHostileTo(selected) && p->GetExactDist(selected)<25)chosen=selected;
|
||||
if(!chosen && human->isMoving())
|
||||
{
|
||||
std::vector<Creature*> enemies;p->GetCreatureListWithEntryInGrid(enemies,0,20);
|
||||
for(auto enemy:enemies)
|
||||
if(Valid(p,enemy,false) && p->IsHostileTo(enemy) && !enemy->GetControllingPlayer() &&
|
||||
p->HasInArc(3.14159265f,enemy) && (!chosen || p->GetExactDist(enemy)<p->GetExactDist(chosen)) && Reachable(p,enemy))chosen=enemy;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!chosen && isTank && tank && tank!=p && sAdventureEncounterMgr->Active(p) && !r.AI && !p->IsInCombat())
|
||||
{
|
||||
if(p->GetExactDist(tank)>4)
|
||||
{
|
||||
auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(!travel || travel->State!=ClientlessTravelState::Moving)
|
||||
{
|
||||
Position point;tank->GetNearPoint(p,point.m_positionX,point.m_positionY,point.m_positionZ,2,tank->GetAngle(p));
|
||||
sClientlessTravelMgr->StartTravel(p,point);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if(chosen && p->GetExactDist(chosen)<=60 && (!r.AI || r.Target!=chosen->GetGUID()))
|
||||
{
|
||||
// Group role changes intentionally supersede solo first-damager focus.
|
||||
if(r.AI)Finish(p,r);
|
||||
Start(p,r,chosen,false,false,0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool AdventureCombatMgr::GroupHeal(Player* p,Record& r)
|
||||
{
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
||||
if(!p->GetGroup() || !profile)return false;
|
||||
auto allies=AdventureAllies(p);
|
||||
bool fighting=std::any_of(allies.begin(),allies.end(),[](Unit* ally){return ally->IsAlive() && ally->IsInCombat();});
|
||||
if(!fighting && !p->HasUnitState(UNIT_STATE_CASTING))
|
||||
for(auto ally:allies)if(auto player=ally->ToPlayer())
|
||||
if(!player->IsAlive() && !player->IsResurrectRequested() && p->IsInMap(player) && p->IsWithinLOSInMap(player) && p->GetExactDist(player)<30)
|
||||
{
|
||||
if(!r.Support)r.Support=std::make_shared<Fighter>(p);
|
||||
if(r.Support->Resurrect(player))return true;
|
||||
}
|
||||
if(profile->Role!="healer")return false;
|
||||
Unit* best=nullptr;float priority=-1;
|
||||
for(auto ally:allies)
|
||||
{
|
||||
if(!ally->IsAlive() || !p->IsInMap(ally) || !p->IsInPhase(ally) || !p->IsFriendlyTo(ally) ||
|
||||
p->GetExactDist(ally)>60 || ally->GetHealthPct()>90)continue;
|
||||
float score=100-ally->GetHealthPct()+std::min<size_t>(ally->getAttackers().size(),5)*8;
|
||||
if(ally->GetHealthPct()<35)score+=150;
|
||||
if(!best || score>priority){best=ally;priority=score;}
|
||||
}
|
||||
if(!best)return false;
|
||||
if(p->GetExactDist(best)>35 || !p->IsWithinLOSInMap(best))
|
||||
{
|
||||
auto current=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(current && current->State==ClientlessTravelState::Moving)return true;
|
||||
Position point;best->GetNearPoint(p,point.m_positionX,point.m_positionY,point.m_positionZ,15,best->GetAngle(p));
|
||||
ClientlessPathDiagnostics path;
|
||||
if(sClientlessTravelMgr->AnalyzePath(p,point,path) && path.Complete && !path.Suspicious)
|
||||
return sClientlessTravelMgr->StartTravel(p,point);
|
||||
return best->GetHealthPct()<70;
|
||||
}
|
||||
if(!r.Support)r.Support=std::make_shared<Fighter>(p);
|
||||
if(p->HasUnitState(UNIT_STATE_CASTING))return true;
|
||||
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());p->StopMoving();
|
||||
unsigned injured=0;
|
||||
for(auto ally:allies)if(ally->IsAlive() && ally->GetHealthPct()<80 && ally->GetExactDist(best)<15)++injured;
|
||||
p->SetFacingToObject(best);
|
||||
if(r.Support->Heal(best,injured))return true;
|
||||
// A healer awaiting resources/cooldowns keeps its assignment rather than
|
||||
// starting a damage rotation while somebody still needs substantial help.
|
||||
return best->GetHealthPct()<70;
|
||||
}
|
||||
bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
{
|
||||
if(!p->IsInWorld())return false;
|
||||
@@ -411,11 +758,32 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
if(p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return r.Fleeing || r.AI || !p->IsAlive();
|
||||
if(r.Timer>diff){r.Timer-=diff;return r.Fleeing || r.AI || !p->IsAlive() || r.Dead;}
|
||||
r.Timer=500;
|
||||
if(p->IsAlive() && sAdventureEncounterMgr->Update(p,500))return true;
|
||||
if(!p->IsAlive())return Death(p,r);
|
||||
if(p->IsResurrectRequested())p->ClearResurrectRequestData();
|
||||
if(!p->IsInCombat() && !p->HasUnitState(UNIT_STATE_CASTING) && !p->IsFlying() && !p->IsFalling() && GameTime::GetGameTime()>=r.NextRole)
|
||||
{
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
||||
if(profile)
|
||||
{
|
||||
unsigned desired=profile->Class?(p->GetGroup()?profile->Specialization:profile->SoloSpecialization):
|
||||
AdventureRotationPolicy::SoloSpec(p->getClass(),p->GetGUID().GetCounter());
|
||||
if(p->getLevel()>=10 && (PlayerAI::GetPlayerSpec(p)!=desired || r.PreparedMode!=int(p->GetGroup()!=nullptr)))
|
||||
{
|
||||
r.NextRole=GameTime::GetGameTime()+10;
|
||||
std::string error;
|
||||
if(!sAdventureGeneratorMgr->PrepareCombat(p,error))
|
||||
TC_LOG_WARN("server.adventurebot","COMBAT_ROLE_PREPARE guid=%u error='%s'",p->GetGUID().GetCounter(),error.c_str());
|
||||
else r.PreparedMode=int(p->GetGroup()!=nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(GroupHeal(p,r))return true;
|
||||
if(GroupTarget(p,r))return true;
|
||||
if(r.Dead){r.Dead=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;return false;}
|
||||
// First actual damager wins, including interrupting a proactive/training
|
||||
// target. Keep all subsequent damagers ordered for after that focus dies.
|
||||
if(!r.Defending)while(!r.Damagers.empty())
|
||||
if(!r.Defending && !p->GetGroup())while(!r.Damagers.empty())
|
||||
{
|
||||
auto id=r.Damagers.front();r.Damagers.pop_front();auto c=ObjectAccessor::GetUnit(*p,id);
|
||||
if(!Valid(p,c,false))continue;
|
||||
@@ -441,19 +809,22 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
auto c=ObjectAccessor::GetUnit(*p,r.Target);
|
||||
if(!Valid(p,c,r.Training) || (r.Until && GameTime::GetGameTime()>=r.Until) || p->GetExactDist(c)>80)
|
||||
{Finish(p,r);return true;}
|
||||
if(!Reachable(p,c))
|
||||
if(!Reachable(p,c) && !(r.AI->Ranged() && p->GetExactDist(c)<=25 && p->IsWithinLOSInMap(c)))
|
||||
{
|
||||
r.UnreachableMs+=500;
|
||||
if(r.UnreachableMs>=20000){TC_LOG_WARN("server.adventurebot","COMBAT_TARGET_UNREACHABLE guid=%u",p->GetGUID().GetCounter());Finish(p,r);}
|
||||
return true;
|
||||
}
|
||||
r.UnreachableMs=0;
|
||||
if(!p->IsWithinMeleeRange(c))
|
||||
bool inRange=r.AI->Ranged()?(p->GetExactDist(c)<=25 && p->IsWithinLOSInMap(c)):p->IsWithinMeleeRange(c);
|
||||
if(!inRange)
|
||||
{
|
||||
auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(c)>5)
|
||||
{
|
||||
Position end;c->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,0.5f,c->GetAngle(p));
|
||||
float angle=c->GetAngle(p);
|
||||
if(p->GetGroup() && !AdventureTank(p) && c->GetVictim()!=p && !r.AI->Ranged())angle=c->GetOrientation()+3.14159265f;
|
||||
Position end;c->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,0.5f,angle);
|
||||
sClientlessTravelMgr->StartTravel(p,end);
|
||||
}
|
||||
return true;
|
||||
@@ -465,6 +836,7 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
|
||||
bool AdventureCombatMgr::Busy(Player* player) const
|
||||
{
|
||||
if(player->GetGroup())for(auto ally:AdventureAllies(player))if(ally->IsAlive() && ally->IsInCombat())return true;
|
||||
auto it=_records.find(player->GetGUID().GetCounter());
|
||||
return it!=_records.end() && (it->second.Fleeing || it->second.AI || it->second.Dead || !it->second.Damagers.empty());
|
||||
}
|
||||
|
||||
@@ -29,13 +29,14 @@ private:
|
||||
class Fighter;
|
||||
struct Record
|
||||
{
|
||||
std::shared_ptr<Fighter> AI;
|
||||
std::shared_ptr<Fighter> AI,Support;
|
||||
ObjectGuid Target,Helper;
|
||||
std::deque<ObjectGuid> Damagers;
|
||||
bool Fleeing=false,Training=false,Defending=false,Suspended=false,Dead=false,CemeteryPending=false;
|
||||
time_t Until=0,NextCorpseSearch=0,NextPvP=0,NextEscape=0;
|
||||
time_t Until=0,NextCorpseSearch=0,NextPvP=0,NextEscape=0,NextRole=0;
|
||||
std::vector<Position> FailedCorpsePoints,ThreatPositions;
|
||||
uint32 Timer=0,UnreachableMs=0,Cooldown=0;
|
||||
int PreparedMode=-1;
|
||||
};
|
||||
bool Start(Player* player,Record& record,Unit* target,bool training,bool defending,time_t until);
|
||||
bool Valid(Player* player,Unit* target,bool training) const;
|
||||
@@ -46,6 +47,8 @@ private:
|
||||
Creature* Proactive(Player* player) const;
|
||||
bool Flee(Player* player,Record& record);
|
||||
bool Death(Player* player,Record& record);
|
||||
bool GroupHeal(Player* player,Record& record);
|
||||
bool GroupTarget(Player* player,Record& record);
|
||||
void Finish(Player* player,Record& record);
|
||||
std::unordered_map<uint32,Record> _records;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "AdventureEncounterMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "DBCStores.h"
|
||||
#include "Corpse.h"
|
||||
#include "Player.h"
|
||||
#include "Group.h"
|
||||
#include "WorldSession.h"
|
||||
#include "WorldPacket.h"
|
||||
#include "Opcodes.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "Map.h"
|
||||
#include "DynamicObject.h"
|
||||
#include "SpellInfo.h"
|
||||
#include "CellImpl.h"
|
||||
#include "GridNotifiersImpl.h"
|
||||
#include "MotionMaster.h"
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
AdventureEncounterMgr* AdventureEncounterMgr::instance(){static AdventureEncounterMgr mgr;return &mgr;}
|
||||
bool AdventureEncounterMgr::MixedGroup(Player* p) const
|
||||
{
|
||||
if(auto group=p->GetGroup())for(auto ref=group->GetFirstMember();ref;ref=ref->next())
|
||||
if(auto member=ref->GetSource())if(member->GetSession() && !member->GetSession()->IsClientless())return true;
|
||||
return false;
|
||||
}
|
||||
bool AdventureEncounterMgr::Active(Player* p) const
|
||||
{
|
||||
auto it=_records.find(p->GetGUID().GetCounter());
|
||||
return p->GetMap()->IsDungeon() && p->GetGroup() &&
|
||||
(MixedGroup(p) || (it!=_records.end() && it->second.Map==p->GetMapId()));
|
||||
}
|
||||
void AdventureEncounterMgr::Register(uint32 map,Behavior behavior){if(behavior)_behaviors[map]=std::move(behavior);else _behaviors.erase(map);}
|
||||
void AdventureEncounterMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());}
|
||||
bool AdventureEncounterMgr::Update(Player* p,uint32 diff)
|
||||
{
|
||||
auto guid=p->GetGUID().GetCounter();auto it=_records.find(guid);
|
||||
if(!Active(p))
|
||||
{
|
||||
// A dismissed companion leaves via the native exit destination before
|
||||
// resuming its personal plan. Never move a bot out during combat.
|
||||
if(it!=_records.end() && p->GetMapId()==it->second.Map && !p->GetGroup() &&
|
||||
p->IsAlive() && !p->IsInCombat() && !p->IsBeingTeleported())
|
||||
{
|
||||
auto const& r=it->second;
|
||||
return p->TeleportTo(r.OutsideMap,r.Outside.GetPositionX(),r.Outside.GetPositionY(),r.Outside.GetPositionZ(),r.Outside.GetOrientation());
|
||||
}
|
||||
if(p->IsAlive() && !p->GetMap()->IsDungeon())_records.erase(guid);
|
||||
return p->IsAlive() && !p->GetMap()->IsDungeon() && FollowIntoInstance(p);
|
||||
}
|
||||
sAdventureBehaviorMgr->EnterPlayerInstance(p);
|
||||
if(it==_records.end() || it->second.Map!=p->GetMapId() || it->second.Instance!=p->GetInstanceId())
|
||||
{
|
||||
auto entrance=sObjectMgr->GetMapEntranceTrigger(p->GetMapId());
|
||||
auto exit=sObjectMgr->GetGoBackTrigger(p->GetMapId());
|
||||
if(!entrance || !exit)return false;
|
||||
Record r;r.Map=p->GetMapId();r.Instance=p->GetInstanceId();r.OutsideMap=exit->target_mapId;
|
||||
r.Entrance.Relocate(entrance->target_X,entrance->target_Y,entrance->target_Z,entrance->target_Orientation);
|
||||
r.Outside.Relocate(exit->target_X,exit->target_Y,exit->target_Z,exit->target_Orientation);
|
||||
it=_records.insert_or_assign(guid,r).first;
|
||||
}
|
||||
if(!p->IsAlive())return false; // Death() owns wipe/release/re-entry.
|
||||
bool humanPresent=false,offlineMember=false,humanOutside=false;
|
||||
for(auto ref=p->GetGroup()->GetFirstMember();ref;ref=ref->next())
|
||||
{
|
||||
auto member=ref->GetSource();if(!member){offlineMember=true;continue;}
|
||||
if(!member->GetSession()->IsClientless())
|
||||
{humanPresent=true;if(member->IsAlive() && !member->GetMap()->IsDungeon())humanOutside=true;}
|
||||
}
|
||||
if(!p->IsInCombat() && (humanOutside || (!humanPresent && !offlineMember)))
|
||||
{
|
||||
auto const& r=it->second;
|
||||
return p->TeleportTo(r.OutsideMap,r.Outside.GetPositionX(),r.Outside.GetPositionY(),r.Outside.GetPositionZ(),r.Outside.GetOrientation());
|
||||
}
|
||||
if(it->second.Returning)
|
||||
{
|
||||
bool gathering=false;Player* livingHuman=nullptr;
|
||||
for(auto ref=p->GetGroup()->GetFirstMember();ref;ref=ref->next())if(auto member=ref->GetSource())
|
||||
{
|
||||
if(!member->GetSession()->IsClientless() && member->IsAlive() && p->IsInMap(member))livingHuman=member;
|
||||
if(member->GetSession()->IsClientless() && (!member->IsAlive() || !p->IsInMap(member)))gathering=true;
|
||||
}
|
||||
if(livingHuman){it->second.Returning=false;return false;}
|
||||
if(gathering)return true;
|
||||
auto const point=it->second.DeathPoint;
|
||||
if(p->GetExactDist(&point)>5)
|
||||
{
|
||||
auto status=sClientlessTravelMgr->GetStatus(guid);
|
||||
if(!status || status->State!=ClientlessTravelState::Moving)sClientlessTravelMgr->StartTravel(p,point);
|
||||
return true;
|
||||
}
|
||||
it->second.Returning=false;
|
||||
}
|
||||
if(auto behavior=_behaviors.find(p->GetMapId());behavior!=_behaviors.end())
|
||||
if(behavior->second(p,diff))return true;
|
||||
return AvoidGroundDamage(p);
|
||||
}
|
||||
bool AdventureEncounterMgr::Death(Player* p)
|
||||
{
|
||||
auto it=_records.find(p->GetGUID().GetCounter());
|
||||
if(it==_records.end() && Active(p))
|
||||
{
|
||||
Update(p,0);it=_records.find(p->GetGUID().GetCounter());
|
||||
}
|
||||
if(it==_records.end() && MixedGroup(p))if(auto corpse=p->GetCorpse())
|
||||
{
|
||||
auto map=sMapStore.LookupEntry(corpse->GetMapId());
|
||||
auto entrance=sObjectMgr->GetMapEntranceTrigger(corpse->GetMapId());
|
||||
auto exit=sObjectMgr->GetGoBackTrigger(corpse->GetMapId());
|
||||
if(map && map->IsDungeon() && entrance && exit)
|
||||
{
|
||||
Record restored;restored.Map=corpse->GetMapId();restored.Instance=corpse->GetInstanceId();restored.OutsideMap=exit->target_mapId;
|
||||
restored.Entrance.Relocate(entrance->target_X,entrance->target_Y,entrance->target_Z,entrance->target_Orientation);
|
||||
restored.Outside.Relocate(exit->target_X,exit->target_Y,exit->target_Z,exit->target_Orientation);
|
||||
restored.DeathPoint.Relocate(corpse);restored.Returning=p->HasFlag(PLAYER_FLAGS,PLAYER_FLAGS_GHOST);
|
||||
it=_records.emplace(p->GetGUID().GetCounter(),restored).first;
|
||||
}
|
||||
}
|
||||
if(it==_records.end())return false;
|
||||
auto& r=it->second;
|
||||
if(p->IsResurrectRequested()){p->ResurrectUsingRequestData();return true;}
|
||||
if(!r.Returning)
|
||||
{
|
||||
if(auto group=p->GetGroup())
|
||||
{
|
||||
// Native enumeration includes living summoned DungeonBots.
|
||||
std::list<Unit*> units;p->GetPartyMembers(units);
|
||||
for(auto unit:units)if(unit!=p && unit->IsAlive() && unit->GetMapId()==r.Map)return true;
|
||||
for(auto ref=group->GetFirstMember();ref;ref=ref->next())
|
||||
if(auto member=ref->GetSource())if(member!=p && member->IsAlive() && member->GetMapId()==r.Map)return true;
|
||||
}
|
||||
r.DeathPoint.Relocate(p);r.Returning=true;
|
||||
}
|
||||
if(!p->HasFlag(PLAYER_FLAGS,PLAYER_FLAGS_GHOST))
|
||||
{
|
||||
WorldPacket release(CMSG_REPOP_REQUEST,1);release<<uint8(0);p->GetSession()->HandleRepopRequestOpcode(release);return true;
|
||||
}
|
||||
if(p->IsBeingTeleported())return true;
|
||||
if(p->GetMapId()!=r.OutsideMap || p->GetExactDist(&r.Outside)>5)
|
||||
{
|
||||
auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(travel && travel->State==ClientlessTravelState::Moving)return true;
|
||||
ClientlessPathDiagnostics path;
|
||||
bool failed=travel && (travel->State==ClientlessTravelState::Stuck || travel->State==ClientlessTravelState::PathRejected);
|
||||
if(!failed && p->GetMapId()==r.OutsideMap && sClientlessTravelMgr->AnalyzePath(p,r.Outside,path) && path.Complete && !path.Suspicious &&
|
||||
sClientlessTravelMgr->StartTravel(p,r.Outside))return true;
|
||||
p->TeleportTo(r.OutsideMap,r.Outside.GetPositionX(),r.Outside.GetPositionY(),r.Outside.GetPositionZ(),r.Outside.GetOrientation());
|
||||
return true;
|
||||
}
|
||||
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
||||
if(p->TeleportTo(r.Map,r.Entrance.GetPositionX(),r.Entrance.GetPositionY(),r.Entrance.GetPositionZ(),r.Entrance.GetOrientation()))
|
||||
{p->ResurrectPlayer(0.5f,false);p->SpawnCorpseBones();r.AtEntrance=true;}
|
||||
return true;
|
||||
}
|
||||
bool AdventureEncounterMgr::FollowIntoInstance(Player* p)
|
||||
{
|
||||
if(!MixedGroup(p) || p->IsInCombat() || p->IsBeingTeleported())return false;
|
||||
Player* human=nullptr;
|
||||
for(auto ref=p->GetGroup()->GetFirstMember();ref;ref=ref->next())if(auto member=ref->GetSource())
|
||||
if(member->IsAlive() && !member->GetSession()->IsClientless() && member->GetMap()->IsDungeon())
|
||||
{human=member;break;}
|
||||
if(!human)return false;
|
||||
AreaTriggerEntry const* entrance=nullptr;float distance=1e30f;
|
||||
for(auto area:sAreaTriggerStore)
|
||||
{
|
||||
auto target=sObjectMgr->GetAreaTrigger(area->ID);
|
||||
if(!target || target->target_mapId!=human->GetMapId())continue;
|
||||
// Prefer an entrance on our continent, then its nearest native portal.
|
||||
float score=area->ContinentID==p->GetMapId()?p->GetExactDist(area->Pos.X,area->Pos.Y,area->Pos.Z):1e20f+area->ID;
|
||||
if(!entrance || score<distance){entrance=area;distance=score;}
|
||||
}
|
||||
if(!entrance)return false;
|
||||
if(p->IsInAreaTriggerRadius(entrance))
|
||||
{
|
||||
WorldPacket packet(CMSG_AREATRIGGER,4);packet<<entrance->ID;p->GetSession()->HandleAreaTriggerOpcode(packet);
|
||||
return true; // Native access, difficulty and group-bind checks apply.
|
||||
}
|
||||
auto execution=sClientlessActionExecutorMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(execution && (execution->State==ClientlessActionExecutionState::Moving || execution->State==ClientlessActionExecutionState::SelectingTarget))return true;
|
||||
auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(travel && travel->State==ClientlessTravelState::Moving)return true;
|
||||
if(entrance->ContinentID==p->GetMapId() && distance<80)
|
||||
{
|
||||
Position point(entrance->Pos.X,entrance->Pos.Y,entrance->Pos.Z,p->GetOrientation());
|
||||
return sClientlessTravelMgr->StartTravel(p,point,false,true);
|
||||
}
|
||||
std::string node;float nearest=0;sClientlessTravelGraphMgr->EnsureLoaded();
|
||||
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(entrance->ContinentID,entrance->Pos.X,entrance->Pos.Y,entrance->Pos.Z,node,nearest) && nearest<1000 &&
|
||||
!sClientlessPlayerMgr->GetQueuedActionCount(p->GetGUID().GetCounter()))
|
||||
{
|
||||
ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node;
|
||||
sClientlessPlayerMgr->QueueAction(p->GetGUID().GetCounter(),std::move(action));return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool AdventureEncounterMgr::AvoidGroundDamage(Player* p)
|
||||
{
|
||||
std::vector<WorldObject*> objects;
|
||||
auto check=[](WorldObject* object){return object->ToDynObject()!=nullptr;};
|
||||
Trinity::WorldObjectListSearcher<decltype(check)> searcher(p,objects,check,GRID_MAP_TYPE_MASK_DYNAMICOBJECT);
|
||||
Cell::VisitAllObjects(p,searcher,60);
|
||||
struct Danger{Position Center;float Radius;};std::vector<Danger> dangers;
|
||||
for(auto object:objects)
|
||||
{
|
||||
auto area=object->ToDynObject();auto caster=area->GetCaster();auto spell=area->GetSpellInfo();
|
||||
if(!caster || !spell || !p->IsInPhase(area) || !p->IsHostileTo(caster) || std::fabs(area->GetPositionZ()-p->GetPositionZ())>4)continue;
|
||||
bool damage=false;
|
||||
for(auto const& effect:spell->Effects)
|
||||
if(effect.Effect==SPELL_EFFECT_SCHOOL_DAMAGE || effect.ApplyAuraName==SPELL_AURA_PERIODIC_DAMAGE || effect.ApplyAuraName==SPELL_AURA_PERIODIC_DAMAGE_PERCENT)damage=true;
|
||||
if(damage)dangers.push_back({*area,area->GetRadius()+2});
|
||||
}
|
||||
auto safe=[&](Position const& point){for(auto const& d:dangers)if(point.GetExactDist2d(&d.Center)<d.Radius)return false;return true;};
|
||||
if(dangers.empty() || safe(*p))return false;
|
||||
auto current=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(current && current->State==ClientlessTravelState::Moving && safe(current->Target))return true;
|
||||
float shortest=1e30f;Position best;
|
||||
for(unsigned ring=1;ring<=4;++ring)for(unsigned i=0;i<16;++i)
|
||||
{
|
||||
float angle=i*6.2831853f/16;float radius=ring*5.0f;
|
||||
Position point(p->GetPositionX()+std::cos(angle)*radius,p->GetPositionY()+std::sin(angle)*radius,p->GetPositionZ(),p->GetOrientation());
|
||||
float height=p->GetMap()->GetHeight(p->GetPhaseShift(),point.GetPositionX(),point.GetPositionY(),p->GetPositionZ()+3,true,8);
|
||||
if(height<=INVALID_HEIGHT || std::fabs(height-p->GetPositionZ())>4)continue;
|
||||
point.m_positionZ=height;if(!safe(point))continue;
|
||||
ClientlessPathDiagnostics path;
|
||||
if(sClientlessTravelMgr->AnalyzePath(p,point,path) && path.Complete && !path.Suspicious && path.PathLength<shortest)
|
||||
{shortest=path.PathLength;best=point;}
|
||||
}
|
||||
if(shortest==1e30f)return false;
|
||||
p->CastStop();return sClientlessTravelMgr->StartTravel(p,best);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef VCORE_ADVENTURE_ENCOUNTER_MGR_H
|
||||
#define VCORE_ADVENTURE_ENCOUNTER_MGR_H
|
||||
#include "Common.h"
|
||||
#include "Position.h"
|
||||
#include "ObjectGuid.h"
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
class Player;
|
||||
// Adventure policy only. DungeonBot CreatureAI and generic CPC stay independent.
|
||||
class TC_GAME_API AdventureEncounterMgr
|
||||
{
|
||||
public:
|
||||
using Behavior=std::function<bool(Player*,uint32)>;
|
||||
static AdventureEncounterMgr* instance();
|
||||
bool MixedGroup(Player* player) const;
|
||||
bool Active(Player* player) const;
|
||||
bool Update(Player* player,uint32 diff);
|
||||
bool Death(Player* player);
|
||||
void Offline(Player* player);
|
||||
// A map-specific behavior may consume an update; false uses generic behavior.
|
||||
// Register once when scripts are loaded, never from concurrent world updates.
|
||||
void Register(uint32 map,Behavior behavior);
|
||||
private:
|
||||
struct Record
|
||||
{
|
||||
uint32 Map=0,Instance=0;
|
||||
Position Entrance,Outside,DeathPoint;
|
||||
uint32 OutsideMap=0;
|
||||
bool Returning=false,AtEntrance=false;
|
||||
};
|
||||
bool AvoidGroundDamage(Player* player);
|
||||
bool FollowIntoInstance(Player* player);
|
||||
std::unordered_map<uint32,Record> _records;
|
||||
std::unordered_map<uint32,Behavior> _behaviors;
|
||||
};
|
||||
#define sAdventureEncounterMgr AdventureEncounterMgr::instance()
|
||||
#endif
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
#include "AdventureCombatPreparationPolicy.h"
|
||||
#include "AdventureTalentPolicy.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureGroupPolicy.h"
|
||||
#include "AdventureLocationMgr.h"
|
||||
@@ -7,6 +8,7 @@
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "ClientlessCompanionMgr.h"
|
||||
#include "Bag.h"
|
||||
#include "Group.h"
|
||||
#include "CharacterCache.h"
|
||||
#include "Chat.h"
|
||||
#include "ClientlessCharacterFactory.h"
|
||||
@@ -103,27 +105,46 @@ namespace
|
||||
bool Talents(Player* p,Archetype const& a)
|
||||
{
|
||||
if(p->getLevel()<10)return true;
|
||||
p->InitTalentForLevel();
|
||||
if(!p->GetPrimaryTalentTree(p->GetActiveSpec()) && !p->LearnPrimaryTalentSpecialization(a.Spec))return false;
|
||||
static std::unordered_map<uint32,uint32> prepared;
|
||||
uint32 key=(p->getLevel()*10+a.Spec)*2+(std::string(AdventureGeneratorPolicy::Role(a))=="tank");
|
||||
auto tabs=sDBCManager.GetTalentTabPages(p->getClass());if(!tabs)return false;
|
||||
if(a.Class==CLASS_WARRIOR && a.Spec==0)
|
||||
AdventureCombatPreparationPolicy::SpendArms60([&](uint32 id,unsigned rank){return p->LearnTalent(id,rank);},[&](){return p->GetFreeTalentPoints();});
|
||||
// Native LearnTalent enforces tier/dependency/rank and 31-point gates.
|
||||
// The named solo spec is preferred; secondary trees only use leftovers.
|
||||
for(unsigned tree=0;tree<MAX_TALENT_TABS && p->GetFreeTalentPoints();++tree)
|
||||
if(auto it=prepared.find(p->GetGUID().GetCounter());it!=prepared.end() && it->second==key &&
|
||||
!p->GetFreeTalentPoints() && p->GetPrimaryTalentTree(p->GetActiveSpec())==tabs[a.Spec])return true;
|
||||
auto build=AdventureTalentPolicy::Build(a.Class,a.Spec,std::string(AdventureGeneratorPolicy::Role(a))=="tank");
|
||||
if(build.empty())return false;
|
||||
p->ResetTalents(true);p->InitTalentForLevel();
|
||||
if(!p->GetPrimaryTalentTree(p->GetActiveSpec()) && !p->LearnPrimaryTalentSpecialization(a.Spec))return false;
|
||||
bool complete=AdventureTalentPolicy::Spend(build,[&](uint32 id,unsigned rank){return p->LearnTalent(id,rank);},[&](){return p->GetFreeTalentPoints();});
|
||||
if(complete)prepared[p->GetGUID().GetCounter()]=key;
|
||||
return complete;
|
||||
}
|
||||
void WeaponEnhancements(Player* p)
|
||||
{
|
||||
for(auto slot:{EQUIPMENT_SLOT_MAINHAND,EQUIPMENT_SLOT_OFFHAND})
|
||||
{
|
||||
uint32 tab=tabs[(a.Spec+tree)%MAX_TALENT_TABS];std::vector<TalentEntry const*> talents;
|
||||
for(auto entry:sTalentStore)if(entry->TabID==tab)talents.push_back(entry);
|
||||
std::sort(talents.begin(),talents.end(),[](TalentEntry const* x,TalentEntry const* y){return std::tie(x->TierID,x->ColumnIndex,x->ID)<std::tie(y->TierID,y->ColumnIndex,y->ID);});
|
||||
for(unsigned pass=0;pass<50 && p->GetFreeTalentPoints();++pass)
|
||||
auto item=p->GetItemByPos(INVENTORY_SLOT_BAG_0,slot);if(!item)continue;
|
||||
if(item->GetTemplate()->GetClass()!=ITEM_CLASS_WEAPON)continue;
|
||||
uint32 spell=0;
|
||||
if(p->getClass()==CLASS_ROGUE)spell=slot==EQUIPMENT_SLOT_MAINHAND?8681:2823;
|
||||
if(p->getClass()==CLASS_SHAMAN)spell=slot==EQUIPMENT_SLOT_MAINHAND && p->GetPrimaryTalentTree(p->GetActiveSpec())==sDBCManager.GetTalentTabPages(CLASS_SHAMAN)[1]?8232:8024;
|
||||
if(!spell)continue;
|
||||
uint32 selected=0;
|
||||
for(uint32 next=spell;next;next=sSpellMgr->GetNextSpellInChain(next))
|
||||
{
|
||||
bool progress=false;
|
||||
for(auto talent:talents)for(unsigned rank=0;rank<MAX_TALENT_RANK && p->GetFreeTalentPoints();++rank)
|
||||
if(talent->SpellRank[rank] && p->LearnTalent(talent->ID,rank))progress=true;
|
||||
if(!progress)break;
|
||||
auto info=sSpellMgr->GetSpellInfo(next);
|
||||
if(info && info->SpellLevel<=p->getLevel() && (p->getClass()==CLASS_ROGUE || p->HasSpell(next)))selected=next;
|
||||
}
|
||||
auto info=sSpellMgr->GetSpellInfo(selected);if(!info)continue;
|
||||
for(auto const& effect:info->Effects)if(effect.Effect==SPELL_EFFECT_ENCHANT_ITEM_TEMPORARY && effect.MiscValue>0 &&
|
||||
sSpellItemEnchantmentStore.LookupEntry(effect.MiscValue) && item->GetEnchantmentId(TEMP_ENCHANTMENT_SLOT)!=uint32(effect.MiscValue))
|
||||
{
|
||||
// Same native enchant/proc machinery as applying the supplied
|
||||
// poison or weapon imbue. No direct damage or fake resources.
|
||||
p->ApplyEnchantment(item,TEMP_ENCHANTMENT_SLOT,false);
|
||||
item->SetEnchantment(TEMP_ENCHANTMENT_SLOT,effect.MiscValue,30*60*1000,0,p->GetGUID());
|
||||
p->ApplyEnchantment(item,TEMP_ENCHANTMENT_SLOT,true);
|
||||
}
|
||||
}
|
||||
return !p->GetFreeTalentPoints();
|
||||
}
|
||||
bool Profession(Player* p,unsigned wanted)
|
||||
{
|
||||
@@ -190,13 +211,18 @@ namespace
|
||||
case EQUIPMENT_SLOT_TRINKET1:case EQUIPMENT_SLOT_TRINKET2:return type==INVTYPE_TRINKET;
|
||||
case EQUIPMENT_SLOT_BACK:return type==INVTYPE_CLOAK;
|
||||
case EQUIPMENT_SLOT_MAINHAND:
|
||||
if(a.Class==CLASS_ROGUE)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
|
||||
if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && a.Class!=CLASS_DEATH_KNIGHT)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
|
||||
if(a.Class==CLASS_ROGUE)return (type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND) && (a.Spec!=0 || item->GetSubClass()==ITEM_SUBCLASS_WEAPON_DAGGER);
|
||||
if(a.Class==CLASS_PALADIN && a.Spec==0)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
|
||||
if((a.Class==CLASS_SHAMAN || a.Class==CLASS_DEATH_KNIGHT) && a.Spec==1)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
|
||||
if(a.Class==CLASS_WARRIOR && a.Spec==1)return type==INVTYPE_2HWEAPON || type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
|
||||
if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && (a.Class==CLASS_WARRIOR || a.Class==CLASS_PALADIN))return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
|
||||
if(a.Attribute==ITEM_MOD_INTELLECT)return type==INVTYPE_2HWEAPON && item->GetSubClass()==ITEM_SUBCLASS_WEAPON_STAFF;
|
||||
return type==INVTYPE_2HWEAPON;
|
||||
case EQUIPMENT_SLOT_OFFHAND:
|
||||
if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && a.Class!=CLASS_DEATH_KNIGHT)return type==INVTYPE_SHIELD;
|
||||
return a.Class==CLASS_ROGUE && (type==INVTYPE_WEAPON || type==INVTYPE_WEAPONOFFHAND);
|
||||
if(std::string(AdventureGeneratorPolicy::Role(a))=="tank" && (a.Class==CLASS_WARRIOR || a.Class==CLASS_PALADIN))return type==INVTYPE_SHIELD;
|
||||
if(a.Class==CLASS_PALADIN && a.Spec==0)return type==INVTYPE_SHIELD;
|
||||
if(a.Class==CLASS_WARRIOR && a.Spec==1)return type==INVTYPE_2HWEAPON || type==INVTYPE_WEAPON || type==INVTYPE_WEAPONOFFHAND;
|
||||
return ((a.Class==CLASS_ROGUE && (a.Spec!=0 || item->GetSubClass()==ITEM_SUBCLASS_WEAPON_DAGGER)) || ((a.Class==CLASS_SHAMAN || a.Class==CLASS_DEATH_KNIGHT) && a.Spec==1)) && (type==INVTYPE_WEAPON || type==INVTYPE_WEAPONOFFHAND);
|
||||
case EQUIPMENT_SLOT_RANGED:
|
||||
return a.Class==CLASS_HUNTER?item->IsRangedWeapon():a.Armor==ITEM_SUBCLASS_ARMOR_CLOTH && item->GetClass()==ITEM_CLASS_WEAPON && item->GetSubClass()==ITEM_SUBCLASS_WEAPON_WAND;
|
||||
default:return false;
|
||||
@@ -243,6 +269,9 @@ namespace
|
||||
float EquipmentScore(Player* p,ItemTemplate const* item,unsigned slot,Archetype const& a)
|
||||
{
|
||||
float invalid=-std::numeric_limits<float>::infinity();
|
||||
// The shared Fury template unlocks Titan's Grip at 31 talent points.
|
||||
// Before that, both weapons must be one-handed, including stored gear.
|
||||
if(a.Class==CLASS_WARRIOR && a.Spec==1 && item->GetInventoryType()==INVTYPE_2HWEAPON && p->CalculateTalentsPoints()<31)return invalid;
|
||||
if(!Slot(item,slot,a) ||
|
||||
!AdventureGeneratorPolicy::EligibleItem(p->getLevel(),std::max(0,item->GetRequiredLevel()),item->GetQuality(),item->GetBaseItemLevel(),
|
||||
item->GetDuration() || (item->GetFlags()&ITEM_FLAG_CONJURED),item->GetRequiredReputationFaction() || item->GetArea() || (item->GetMap()!=0 && item->GetMap()!=uint32(-1)) || item->GetHolidayID()!=HOLIDAY_NONE || item->GetRandomProperty() || item->GetRandomSuffix()) ||
|
||||
@@ -263,18 +292,51 @@ namespace
|
||||
(a.Attribute!=ITEM_MOD_INTELLECT && (type==ITEM_MOD_SPIRIT || type==ITEM_MOD_MANA || type==ITEM_MOD_MANA_REGENERATION || type==ITEM_MOD_SPELL_POWER || type==ITEM_MOD_SPELL_PENETRATION)))wrong+=value;
|
||||
}
|
||||
if(wrong>0 && stat==0)return invalid;
|
||||
return item->GetBaseItemLevel()+stat*2-wrong;
|
||||
float roleStats=0;bool tank=std::string(AdventureGeneratorPolicy::Role(a))=="tank";
|
||||
bool healer=std::string(AdventureGeneratorPolicy::Role(a))=="healer";
|
||||
for(unsigned index=0;index<MAX_ITEM_PROTO_STATS;++index)
|
||||
{
|
||||
auto type=item->GetItemStatType(index);float value=item->GetStatValue(index,p);
|
||||
if(tank && (type==ITEM_MOD_DODGE_RATING || type==ITEM_MOD_PARRY_RATING || type==ITEM_MOD_MASTERY_RATING))roleStats+=value;
|
||||
if(tank && type==ITEM_MOD_STAMINA)roleStats+=value*0.5f;
|
||||
if(healer && type==ITEM_MOD_SPIRIT)roleStats+=value;
|
||||
if(type==ITEM_MOD_RESILIENCE_RATING)roleStats-=value;
|
||||
}
|
||||
return item->GetBaseItemLevel()+stat*2+roleStats-wrong;
|
||||
}
|
||||
bool Equip(Player* p,Archetype const& a,std::mt19937& random,std::string& error,bool missingOnly=false)
|
||||
bool Equip(Player* p,Archetype const& requested,std::mt19937& random,std::string& error,bool missingOnly=false)
|
||||
{
|
||||
auto a=requested;
|
||||
if(a.Class==CLASS_PALADIN)a.Attribute=a.Spec==0?ITEM_MOD_INTELLECT:ITEM_MOD_STRENGTH;
|
||||
if(a.Class==CLASS_SHAMAN || a.Class==CLASS_DRUID)a.Attribute=a.Spec==1?ITEM_MOD_AGILITY:ITEM_MOD_INTELLECT;
|
||||
for(unsigned slot=EQUIPMENT_SLOT_START;slot<EQUIPMENT_SLOT_END;++slot)
|
||||
{
|
||||
if(missingOnly)
|
||||
if(auto current=p->GetItemByPos(INVENTORY_SLOT_BAG_0,slot))
|
||||
if(std::isfinite(EquipmentScore(p,current->GetTemplate(),slot,a)))continue;
|
||||
if(slot==EQUIPMENT_SLOT_BODY || slot==EQUIPMENT_SLOT_TABARD ||
|
||||
(slot==EQUIPMENT_SLOT_OFFHAND && a.Class!=CLASS_ROGUE && (std::string(AdventureGeneratorPolicy::Role(a))!="tank" || a.Class==CLASS_DEATH_KNIGHT)) ||
|
||||
(slot==EQUIPMENT_SLOT_OFFHAND && a.Class!=CLASS_ROGUE && !(a.Class==CLASS_WARRIOR && a.Spec==1) && !((a.Class==CLASS_SHAMAN || a.Class==CLASS_DEATH_KNIGHT) && a.Spec==1) && !(a.Class==CLASS_PALADIN && a.Spec==0) && (std::string(AdventureGeneratorPolicy::Role(a))!="tank" || a.Class==CLASS_DEATH_KNIGHT || a.Class==CLASS_DRUID)) ||
|
||||
(slot==EQUIPMENT_SLOT_RANGED && a.Class!=CLASS_HUNTER && a.Armor!=ITEM_SUBCLASS_ARMOR_CLOTH))continue;
|
||||
Item* stored=nullptr;float storedScore=-std::numeric_limits<float>::infinity();
|
||||
auto consider=[&](Item* item)
|
||||
{
|
||||
if(!item)return;
|
||||
auto score=EquipmentScore(p,item->GetTemplate(),slot,a);
|
||||
if(score>storedScore){stored=item;storedScore=score;}
|
||||
};
|
||||
for(uint8 bagSlot=INVENTORY_SLOT_ITEM_START;bagSlot<INVENTORY_SLOT_ITEM_END;++bagSlot)
|
||||
consider(p->GetItemByPos(INVENTORY_SLOT_BAG_0,bagSlot));
|
||||
for(uint8 bag=INVENTORY_SLOT_BAG_START;bag<INVENTORY_SLOT_BAG_END;++bag)
|
||||
if(auto container=p->GetBagByPos(bag))for(uint8 bagSlot=0;bagSlot<container->GetBagSize();++bagSlot)consider(container->GetItemByPos(bagSlot));
|
||||
if(stored)
|
||||
{
|
||||
uint16 destination=0;
|
||||
if(p->CanEquipItem(slot,destination,stored,true)==EQUIP_ERR_OK)
|
||||
{
|
||||
p->SwapItem(stored->GetPos(),destination);
|
||||
if(p->GetItemByPos(INVENTORY_SLOT_BAG_0,slot)==stored)continue;
|
||||
}
|
||||
}
|
||||
struct Candidate {uint32 Id;float Score;};std::vector<Candidate> candidates;
|
||||
uint32 salt=random();
|
||||
for(auto const& pair:*sObjectMgr->GetItemTemplateStore())
|
||||
@@ -358,6 +420,7 @@ bool AdventureGeneratorMgr::Prepare(Player* p,Archetype const& a,std::string& er
|
||||
{
|
||||
ClassTraining(p);
|
||||
if(!Talents(p,a)){error="Native talent plan could not be completed.";return false;}
|
||||
ClassTraining(p); // specialization-dependent trainer abilities after native talent changes
|
||||
for(auto skill:a.Skills)if(!Profession(p,skill) && p->getLevel()>=5){error="No native profession rank could be learned for skill "+std::to_string(skill);return false;}
|
||||
if(p->getLevel()>=5)p->SetFreePrimaryProfessions(0); // the generated profile owns exactly two primaries
|
||||
if(!sAdventureSupplyMgr->PrepareCharacter(p)){error="Could not prepare secondary professions and bags.";return false;}
|
||||
@@ -458,6 +521,58 @@ void AdventureGeneratorMgr::GenerateAsync(uint32 account,uint32 seed,uint8 level
|
||||
{
|
||||
if(!Create(nullptr,account,seed,level,faction,"any",persistent,done))done(0,"No valid name, archetype or home in loaded catalogs.");
|
||||
}
|
||||
std::unordered_set<uint32> AdventureGeneratorMgr::ProtectedEquipment(Player* player) const
|
||||
{
|
||||
std::unordered_set<uint32> result;
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
if(!profile || profile->SourcePath.find("generated:")!=0)return result;
|
||||
std::vector<Item*> items;
|
||||
for(uint8 slot=INVENTORY_SLOT_ITEM_START;slot<INVENTORY_SLOT_ITEM_END;++slot)
|
||||
if(auto item=player->GetItemByPos(INVENTORY_SLOT_BAG_0,slot))items.push_back(item);
|
||||
for(uint8 bag=INVENTORY_SLOT_BAG_START;bag<INVENTORY_SLOT_BAG_END;++bag)
|
||||
if(auto container=player->GetBagByPos(bag))for(uint8 slot=0;slot<container->GetBagSize();++slot)
|
||||
if(auto item=container->GetItemByPos(slot))items.push_back(item);
|
||||
auto found=std::find_if(AdventureGeneratorPolicy::Archetypes().begin(),AdventureGeneratorPolicy::Archetypes().end(),
|
||||
[&](Archetype const& a){return a.Class==player->getClass();});
|
||||
if(found==AdventureGeneratorPolicy::Archetypes().end())return result;
|
||||
for(auto spec:{profile->Specialization,profile->SoloSpecialization})
|
||||
{
|
||||
auto a=*found;a.Spec=spec;
|
||||
if(a.Class==CLASS_DRUID && spec==profile->Specialization && profile->Role=="tank")a.RoleOverride="tank";
|
||||
if(a.Class==CLASS_PALADIN)a.Attribute=spec==0?ITEM_MOD_INTELLECT:ITEM_MOD_STRENGTH;
|
||||
if(a.Class==CLASS_SHAMAN || a.Class==CLASS_DRUID)a.Attribute=spec==1?ITEM_MOD_AGILITY:ITEM_MOD_INTELLECT;
|
||||
for(unsigned slot=EQUIPMENT_SLOT_START;slot<EQUIPMENT_SLOT_END;++slot)
|
||||
{
|
||||
float best=-std::numeric_limits<float>::infinity();Item* selected=nullptr;
|
||||
for(auto item:items)
|
||||
{
|
||||
if(result.count(item->GetGUID().GetCounter()))continue;
|
||||
auto score=EquipmentScore(player,item->GetTemplate(),slot,a);
|
||||
if(score>best){best=score;selected=item;}
|
||||
}
|
||||
if(selected)result.insert(selected->GetGUID().GetCounter());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool AdventureGeneratorMgr::PrepareCombat(Player* player,std::string& error)
|
||||
{
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
if(!profile)return false;
|
||||
if(profile->Class && profile->SourcePath.find("generated:")==0)return Reconcile(player,error);
|
||||
// Named Adventure profiles share the canonical combat templates, while
|
||||
// retaining their manually assigned inventory, professions and level.
|
||||
auto found=std::find_if(AdventureGeneratorPolicy::Archetypes().begin(),AdventureGeneratorPolicy::Archetypes().end(),
|
||||
[&](Archetype const& a){return a.Class==player->getClass();});
|
||||
if(found==AdventureGeneratorPolicy::Archetypes().end())return false;
|
||||
auto a=*found;
|
||||
a.Spec=player->GetGroup() && profile->Class?profile->Specialization:
|
||||
profile->Class?profile->SoloSpecialization:AdventureRotationPolicy::SoloSpec(player->getClass(),player->GetGUID().GetCounter());
|
||||
if(a.Class==CLASS_DRUID && a.Spec==1 && player->GetGroup() && profile->Role=="tank")a.RoleOverride="tank";
|
||||
if(!Talents(player,a)){error="Cannot apply canonical class talent template.";return false;}
|
||||
WeaponEnhancements(player);
|
||||
return true;
|
||||
}
|
||||
bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error)
|
||||
{
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
||||
@@ -467,7 +582,12 @@ bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error)
|
||||
[&](Archetype const& a){return a.Class==profile->Class;});
|
||||
if(found==AdventureGeneratorPolicy::Archetypes().end())return false;
|
||||
auto a=*found;a.Spec=profile->Specialization;
|
||||
if(a.Class==CLASS_DRUID && a.Spec==1 && profile->Role=="tank")a.RoleOverride="tank";
|
||||
if(profile->Role!=AdventureGeneratorPolicy::Role(a)){error="Profile role and specialization disagree.";return false;}
|
||||
if(!player->GetGroup()){a.Spec=profile->SoloSpecialization;a.RoleOverride=nullptr;player->RemoveAurasDueToSpell(25780);}
|
||||
if(a.Class==CLASS_PALADIN && a.Spec==0)a.Attribute=ITEM_MOD_INTELLECT;
|
||||
if(a.Class==CLASS_SHAMAN)a.Attribute=a.Spec==1?ITEM_MOD_AGILITY:ITEM_MOD_INTELLECT;
|
||||
if(a.Class==CLASS_DRUID)a.Attribute=a.Spec==1?ITEM_MOD_AGILITY:ITEM_MOD_INTELLECT;
|
||||
bool changedLevel=player->getLevel()!=profile->Level;
|
||||
if(changedLevel)
|
||||
{
|
||||
@@ -479,10 +599,12 @@ bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error)
|
||||
for(auto id:remove)player->RemoveSpell(id,false,false);
|
||||
}
|
||||
auto tabs=sDBCManager.GetTalentTabPages(player->getClass());
|
||||
if(changedLevel || (tabs && player->getLevel()>=10 && player->GetPrimaryTalentTree(player->GetActiveSpec())!=tabs[a.Spec]))player->ResetTalents(true);
|
||||
if(changedLevel || (tabs && player->getLevel()>=10 && player->GetPrimaryTalentTree(player->GetActiveSpec())!=tabs[a.Spec]))
|
||||
{player->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT);player->ResetTalents(true);}
|
||||
if(!Prepare(player,a,error))return false;
|
||||
std::mt19937 random(player->GetGUID().GetCounter());
|
||||
if(!Equip(player,a,random,error,true))return false;
|
||||
WeaponEnhancements(player);
|
||||
for(auto const& pet:profile->Companions)ClientlessCompanionMgr::Prepare(player,pet.DisplayId,profile->CompanionLeashItem);
|
||||
if(player->getLevel()>=20 && profile->CanGroundMount)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "AdventureGeneratorPolicy.h"
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_set>
|
||||
class Player;
|
||||
class ChatHandler;
|
||||
class TC_GAME_API AdventureGeneratorMgr
|
||||
@@ -17,6 +18,8 @@ public:
|
||||
using Completion=std::function<void(uint32,std::string)>;
|
||||
void GenerateAsync(uint32 account,uint32 seed,uint8 level,unsigned faction,bool persistent,Completion done);
|
||||
bool Reconcile(Player* player,std::string& error);
|
||||
bool PrepareCombat(Player* player,std::string& error);
|
||||
std::unordered_set<uint32> ProtectedEquipment(Player* player) const;
|
||||
private:
|
||||
bool Create(ChatHandler* handler,uint32 account,uint32 seed,uint8 level,unsigned faction,
|
||||
std::string const& archetype,bool persistent,Completion done={});
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include "AdventureProfessionPolicy.h"
|
||||
#include "AdventureRotationPolicy.h"
|
||||
namespace AdventureGeneratorPolicy
|
||||
{
|
||||
struct Archetype
|
||||
@@ -14,6 +15,7 @@ namespace AdventureGeneratorPolicy
|
||||
char const* Key;unsigned Class,Spec,Attribute,Armor;
|
||||
std::array<unsigned,2> Skills;
|
||||
char const* Vocation;
|
||||
char const* RoleOverride = nullptr;
|
||||
};
|
||||
inline std::array<Archetype,10> const& Archetypes()
|
||||
{
|
||||
@@ -81,15 +83,16 @@ namespace AdventureGeneratorPolicy
|
||||
{return level<40 && a.Armor>=3?a.Armor-1:a.Armor;}
|
||||
inline bool EligibleItem(unsigned level,unsigned required,unsigned quality,unsigned itemLevel,bool temporary,bool restricted)
|
||||
{
|
||||
return required<=level && quality>=1 && quality<=3 && !temporary && !restricted &&
|
||||
return required<=level && (level==85?(quality==4 && itemLevel>=359):quality>=1 && quality<=3) && !temporary && !restricted &&
|
||||
(required+15>=level || level<20) && itemLevel<=level*5+20;
|
||||
}
|
||||
inline unsigned Capital(unsigned race)
|
||||
{return race==3 || race==7?1537:race==4?1657:race==11?3557:race==5?1497:race==6?1638:race==10?3487:Faction(race)==1?1519:1637;}
|
||||
inline char const* Role(Archetype const& a)
|
||||
{
|
||||
if(a.RoleOverride)return a.RoleOverride;
|
||||
if ((a.Class==1 && a.Spec==2) || (a.Class==2 && a.Spec==1) || (a.Class==6 && a.Spec==0)) return "tank";
|
||||
if ((a.Class==5 && a.Spec!=2) || (a.Class==7 && a.Spec==2) || (a.Class==11 && a.Spec==2)) return "healer";
|
||||
if ((a.Class==2 && a.Spec==0) || (a.Class==5 && a.Spec!=2) || (a.Class==7 && a.Spec==2) || (a.Class==11 && a.Spec==2)) return "healer";
|
||||
return "damage";
|
||||
}
|
||||
inline Archetype Personalize(Archetype a, std::mt19937& random)
|
||||
@@ -102,6 +105,8 @@ namespace AdventureGeneratorPolicy
|
||||
if(a.Class==5)a.Spec=random()%2;
|
||||
if(a.Class==7 || a.Class==11)a.Spec=2;
|
||||
}
|
||||
if(a.Class==11 && random()%4==0){a.Spec=1;a.Attribute=3;a.RoleOverride="tank";}
|
||||
if(a.Class==2 && random()%4==0){a.Spec=0;a.Attribute=5;}
|
||||
return a;
|
||||
}
|
||||
inline std::string Profile(std::string const& name,Archetype const& a,unsigned race,
|
||||
@@ -114,6 +119,7 @@ namespace AdventureGeneratorPolicy
|
||||
auto personality=[&](){return 0.25f+float(random()%651)/1000;};
|
||||
lua<<"return { identity = { name = "<<quote(name)<<", home_region = "<<quote(region)
|
||||
<<", profile_version = 2, level = "<<level<<", class = "<<a.Class<<", specialization = "<<a.Spec
|
||||
<<", solo_specialization = "<<AdventureRotationPolicy::SoloSpec(a.Class,seed)
|
||||
<<", role = "<<quote(Role(a))<<", seed = "<<seed<<", home_zone = "<<homeZone
|
||||
<<", home_capital = "<<Capital(race)<<", favorite_zone = "<<favoriteZone<<", favorite_region = "<<quote(favoriteRegion)
|
||||
<<", vocation = "<<quote(a.Vocation)<<", background = "
|
||||
|
||||
@@ -229,11 +229,12 @@ void AdventurePopulationMgr::CleanInventory(Player* p)
|
||||
if(p->GetFreeInventorySpace()>0)return;
|
||||
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
||||
auto allowance=sAdventureSupplyMgr->InventoryAllowance(p);
|
||||
auto equipment=sAdventureGeneratorMgr->ProtectedEquipment(p);
|
||||
auto clean=[&](uint8 bag,uint8 slot)
|
||||
{
|
||||
auto item=p->GetItemByPos(bag,slot);if(!item)return;
|
||||
auto proto=item->GetTemplate();
|
||||
bool keep=proto->GetClass()==ITEM_CLASS_CONTAINER ||
|
||||
bool keep=equipment.count(item->GetGUID().GetCounter()) || proto->GetClass()==ITEM_CLASS_CONTAINER ||
|
||||
(proto->GetClass()==ITEM_CLASS_WEAPON && proto->GetSubClass()==ITEM_SUBCLASS_WEAPON_FISHING_POLE) ||
|
||||
(profile && (profile->KeepItems.count(item->GetEntry()) || item->GetEntry()==profile->CompanionLeashItem));
|
||||
if(auto it=allowance.find(item->GetEntry());it!=allowance.end())
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#ifndef VCORE_ADVENTURE_ROTATION_POLICY_H
|
||||
#define VCORE_ADVENTURE_ROTATION_POLICY_H
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
namespace AdventureRotationPolicy
|
||||
{
|
||||
enum class Condition { Always, MissingAura, Execute, Injured, Emergency, Resource, LowResource, Interrupt };
|
||||
struct Action { uint32_t Spell; Condition When = Condition::Always; bool Self = false; uint32_t Aura = 0; unsigned Combo = 0; uint32_t Proc = 0; };
|
||||
// Stable, shared PvE priorities. Native spell validation handles level,
|
||||
// resources, procs, cooldowns and learned ranks; no random action selection.
|
||||
inline unsigned SoloSpec(unsigned klass, uint32_t seed)
|
||||
{
|
||||
switch (klass)
|
||||
{
|
||||
case 1: case 4: case 7: case 8: case 11: return seed % 2;
|
||||
case 3: case 6: return 1 + seed % 2;
|
||||
case 2: case 5: return 2;
|
||||
case 9: return seed % 3;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
inline bool Ranged(unsigned klass, unsigned spec)
|
||||
{ return klass==3 || klass==5 || klass==8 || klass==9 || (klass==7 && spec!=1) || (klass==11 && spec!=1); }
|
||||
inline std::vector<Action> Damage(unsigned klass, unsigned spec)
|
||||
{
|
||||
using C=Condition;
|
||||
switch (klass)
|
||||
{
|
||||
case 1:
|
||||
if(spec==2)return {{71,C::MissingAura,true},{871,C::Emergency,true},{12975,C::Emergency,true},{2565,C::Injured,true},{23922},{6572},{772,C::MissingAura},{20243},{78,C::Resource}};
|
||||
if(spec==1)return {{2458,C::MissingAura,true},{23881},{85288},{5308,C::Execute},{86346},{1464,C::Always,false,0,0,46916},{78,C::Resource}};
|
||||
return {{2457,C::MissingAura,true},{772,C::MissingAura},{12294},{86346},{7384},{5308,C::Execute},{1464},{78,C::Resource}};
|
||||
case 2:
|
||||
if(spec==0)return {{20271},{879},{35395}};
|
||||
if(spec==1)return {{25780,C::MissingAura,true},{498,C::Injured,true},{31850,C::Emergency,true},{31935},{53600},{35395},{20271},{24275,C::Execute}};
|
||||
return {{85256},{35395},{24275,C::Execute},{20271},{879}};
|
||||
case 3:
|
||||
if(spec==2)return {{1978,C::MissingAura},{53301},{3674},{53351,C::Execute},{77767},{3044}};
|
||||
return {{1978,C::MissingAura},{53209},{53351,C::Execute},{19434},{56641},{3044}};
|
||||
case 4:
|
||||
if(spec==0)return {{5171,C::MissingAura,true,0,3},{1943,C::MissingAura,false,0,1},{32645,C::Always,false,0,5},{2098,C::Always,false,0,5},{1329},{1752}};
|
||||
return {{5171,C::MissingAura,true,0,3},{1943,C::MissingAura,false,0,5},{2098,C::Always,false,0,5},{84617},{1752}};
|
||||
case 5:
|
||||
if(spec!=2)return {{585}};
|
||||
return {{15473,C::MissingAura,true},{34914,C::MissingAura},{589,C::MissingAura},{2944,C::MissingAura},{8092},{32379,C::Execute},{15407},{585}};
|
||||
case 6:
|
||||
if(spec==0)return {{48263,C::MissingAura,true},{48792,C::Emergency,true},{45462,C::MissingAura,false,55078},{45477,C::MissingAura,false,55095},{49998},{55050},{47541}};
|
||||
if(spec==1)return {{48265,C::MissingAura,true},{45477,C::MissingAura,false,55095},{45462,C::MissingAura,false,55078},{49020},{49143},{49184},{47541}};
|
||||
return {{48265,C::MissingAura,true},{45477,C::MissingAura,false,55095},{45462,C::MissingAura,false,55078},{55090},{85948},{47541}};
|
||||
case 7:
|
||||
if(spec==2)return {{403}};
|
||||
if(spec==1)return {{17364},{60103},{8050,C::MissingAura},{8042},{73899},{403}};
|
||||
return {{324,C::MissingAura,true},{8050,C::MissingAura},{51505},{8042},{403}};
|
||||
case 8:
|
||||
if(spec==1)return {{30482,C::MissingAura,true},{44457,C::MissingAura},{11366,C::Always,false,0,0,48108},{2136},{133}};
|
||||
return {{6117,C::MissingAura,true},{12042,C::Resource,true},{30451},{5143,C::Always,false,0,0,79808},{44425}};
|
||||
case 9:
|
||||
if(spec==1)return {{28176,C::MissingAura,true},{348,C::MissingAura},{172,C::MissingAura},{603,C::MissingAura},{71521},{29722},{686}};
|
||||
if(spec==2)return {{28176,C::MissingAura,true},{348,C::MissingAura},{17962},{50796},{29722},{17877,C::Execute}};
|
||||
return {{28176,C::MissingAura,true},{172,C::MissingAura},{30108,C::MissingAura},{980,C::MissingAura},{48181},{1120,C::Execute},{686}};
|
||||
case 11:
|
||||
if(spec==2)return {{5176}};
|
||||
if(spec==1)return {{768,C::MissingAura,true},{5217,C::LowResource,true},{52610,C::MissingAura,true,0,3},{33876,C::MissingAura},{1079,C::MissingAura,false,0,5},{1822,C::MissingAura},{22568,C::Always,false,0,5},{5221},{33876},{1082}};
|
||||
return {{24858,C::MissingAura,true},{8921,C::MissingAura},{5570,C::MissingAura},{78674},{2912},{5176}};
|
||||
default:return {};
|
||||
}
|
||||
}
|
||||
inline std::vector<Action> Healing(unsigned klass, unsigned spec, float hp)
|
||||
{
|
||||
using C=Condition;
|
||||
switch(klass)
|
||||
{
|
||||
case 2: if(hp<30)return {{633},{20473},{19750},{82326},{635}}; return {{20473},{82326},{635},{19750}};
|
||||
case 5:
|
||||
if(spec==0 && hp<40)return {{33206},{17,C::MissingAura},{47540},{2061},{2060}};
|
||||
if(hp<35)return {{47788},{2061},{2050}};
|
||||
if(hp<70)return {{33076,C::MissingAura},{47540},{2060},{2050}};
|
||||
return {{139,C::MissingAura},{2050},{2061}};
|
||||
case 7: if(hp<35)return {{61295},{8004},{77472},{331}}; return {{61295,C::MissingAura},{77472},{331}};
|
||||
case 11: if(hp<35)return {{18562},{8936},{5185}}; return {{33763,C::MissingAura},{774,C::MissingAura},{50464},{5185}};
|
||||
default:return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -244,7 +244,7 @@ bool AdventureSupplyMgr::Update(Player* p,uint32 diff,bool combatReaction)
|
||||
}
|
||||
return false; // Native combat and attacks keep running.
|
||||
}
|
||||
if(combatReaction)return false;
|
||||
if(combatReaction){if(r.Recovering)Cancel(p,r);return false;}
|
||||
if((r.LastDay!=Day(now) || r.AfterCombat) && now>=r.RetryStock)
|
||||
{
|
||||
if(!Stock(p,r,Day(now)))
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef VCORE_ADVENTURE_TALENT_POLICY_H
|
||||
#define VCORE_ADVENTURE_TALENT_POLICY_H
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
namespace AdventureTalentPolicy
|
||||
{
|
||||
struct Talent {uint32_t Id;unsigned Ranks;};
|
||||
// Cataclysm PvE templates, checked against the local 4.3.4 Talent.dbc.
|
||||
// Source URLs and retrieval date: doc/clientless/class-combat.md.
|
||||
template<class Learn,class Remaining>
|
||||
bool Spend(std::vector<Talent> const& build,Learn learn,Remaining remaining)
|
||||
{
|
||||
for(unsigned pass=0;pass<50 && remaining();++pass)
|
||||
{
|
||||
bool progress=false;
|
||||
for(auto const& talent:build)
|
||||
for(unsigned rank=0;rank<talent.Ranks && remaining();++rank)
|
||||
if(learn(talent.Id,rank))progress=true;
|
||||
if(!progress)break;
|
||||
}
|
||||
return !remaining();
|
||||
}
|
||||
inline std::vector<Talent> Build(unsigned klass,unsigned spec,bool tank=false)
|
||||
{
|
||||
switch(klass*10+spec)
|
||||
{
|
||||
case 10: return {{9664,2},{10134,3},{8176,3},{8184,1},{11416,2},{8192,1},{10138,3},{10741,2},{9662,2},{11223,1},{11418,2},{8208,1},{8214,2},{10520,3},{8194,2},{8222,1},{9606,3},{9608,2},{9644,2},{10464,1},{10480,2}};
|
||||
case 11: return {{9606,3},{9608,2},{9624,2},{9644,2},{11415,2},{9612,3},{9630,1},{9636,3},{9648,1},{9650,1},{11208,1},{9642,2},{10743,2},{9654,3},{10744,2},{9658,1},{9664,2},{10134,3},{8176,3},{10464,2}};
|
||||
case 12: return {{10464,3},{10474,3},{10480,2},{10466,1},{10472,3},{11170,2},{10478,1},{10482,1},{10494,1},{10934,2},{10470,2},{10486,1},{10484,2},{10488,2},{10492,1},{10496,3},{10498,1},{10134,3},{11163,2},{8176,3},{9608,2}};
|
||||
case 20: return {{10127,3},{12189,3},{10097,1},{11213,3},{10129,2},{11202,1},{11771,2},{10113,1},{10121,1},{10133,1},{11215,3},{10115,1},{11779,3},{12151,2},{11168,3},{11203,1},{12198,3},{10651,3},{11612,2},{11611,2}};
|
||||
case 21: return {{10324,2},{12198,3},{10332,3},{10372,2},{10344,2},{10346,3},{10374,1},{11159,2},{11161,2},{11193,2},{11607,1},{10334,1},{10356,1},{10680,1},{10340,3},{10370,2},{10350,1},{10651,3},{11612,2},{11269,2},{11611,2}};
|
||||
case 22: return {{10647,2},{10651,3},{11612,2},{11269,3},{11611,2},{10661,3},{10665,1},{11204,1},{11610,2},{10643,1},{10669,3},{11207,1},{11372,1},{10633,2},{10677,3},{11211,1},{11222,1},{10099,2},{10127,3},{11780,2},{10324,2}};
|
||||
case 31: return {{9380,3},{9390,2},{9396,2},{9398,2},{9402,3},{11225,3},{9408,2},{9412,1},{9416,2},{9404,1},{9418,3},{9422,2},{9426,2},{9428,2},{9430,1},{9490,3},{9492,2},{9512,2},{9432,3}};
|
||||
case 32: return {{9432,3},{9450,2},{9444,2},{10753,3},{9452,2},{9484,3},{9460,3},{9462,2},{9464,2},{9468,1},{9474,2},{9476,1},{9478,3},{11698,1},{9480,1},{9490,3},{9380,3},{9390,2},{9396,2}};
|
||||
case 40: return {{269,3},{276,3},{273,3},{277,3},{1721,2},{280,1},{682,3},{283,2},{11209,1},{281,1},{1715,1},{6516,2},{2070,3},{6517,2},{2071,1},{181,2},{244,2},{2244,3},{261,3}};
|
||||
case 41: return {{181,3},{201,3},{6395,2},{1122,3},{1827,2},{186,3},{203,2},{11171,1},{1825,3},{205,1},{2074,2},{6513,2},{11174,3},{2076,1},{269,2},{276,3},{273,2},{2244,3}};
|
||||
case 50: return {{8577,3},{8595,3},{10736,2},{8593,2},{8607,2},{11608,1},{8591,1},{8611,1},{11224,2},{11812,2},{8617,3},{11523,2},{8609,3},{8623,1},{8625,2},{8603,1},{9549,3},{9553,2},{9561,2},{9032,3}};
|
||||
case 51: return {{9549,3},{9553,3},{10746,2},{9561,2},{11669,1},{11765,2},{9577,2},{9593,2},{11666,1},{12184,2},{9573,2},{14738,1},{11667,1},{11755,1},{9595,1},{9597,3},{11668,2},{9601,1},{8577,3},{8595,3},{9032,3}};
|
||||
case 52: return {{9032,3},{9036,2},{9046,2},{9042,3},{9062,2},{11673,2},{9064,1},{11606,2},{9054,1},{9060,2},{11778,2},{9074,1},{9076,2},{9070,3},{11605,2},{9080,1},{8577,3},{8595,3},{8581,1},{8593,2},{11608,1}};
|
||||
case 60: return {{1938,3},{2017,3},{1948,2},{7462,2},{12223,2},{2105,2},{7458,3},{7459,1},{1936,2},{1960,2},{7461,2},{1941,1},{1959,3},{2019,1},{2259,3},{7463,2},{1961,1},{2031,3},{1963,3}};
|
||||
case 61: return {{2022,3},{2031,2},{1971,2},{2048,3},{1981,2},{1993,2},{2044,3},{1979,1},{1980,2},{1992,3},{2223,1},{1999,1},{2029,2},{2284,3},{1989,1},{1938,3},{1939,2},{12223,2},{1932,3}};
|
||||
case 62: return {{1932,3},{1963,3},{2025,1},{11178,3},{2047,2},{7574,1},{11179,3},{12119,2},{2009,2},{2082,3},{1996,1},{2013,2},{2085,1},{2043,2},{7575,3},{2000,1},{1938,3},{1939,2},{12223,2},{2031,1}};
|
||||
case 70: return {{563,3},{11218,3},{561,2},{575,2},{1685,3},{574,1},{1641,2},{11767,2},{2049,2},{2051,3},{573,1},{5565,1},{11769,1},{5566,2},{11368,3},{607,1},{611,2},{5568,2},{11177,3},{595,2}};
|
||||
case 71: return {{607,2},{611,2},{5560,3},{602,3},{605,2},{11216,3},{901,1},{2055,3},{2083,3},{1689,2},{1693,1},{2057,3},{5563,2},{2058,1},{563,2},{11218,3},{561,2},{1685,3}};
|
||||
case 72: return {{593,3},{5568,1},{11177,3},{583,2},{595,2},{5567,3},{581,2},{591,1},{1696,3},{588,2},{2084,1},{590,1},{7705,2},{15487,1},{2063,3},{2064,1},{563,2},{11218,3},{1685,3},{611,2}};
|
||||
case 80: return {{9154,3},{9200,3},{9170,3},{9172,2},{10737,2},{9174,1},{9178,3},{9192,2},{9198,2},{9188,2},{10733,1},{11825,2},{9180,2},{9196,1},{9194,2},{10578,1},{9186,1},{10531,3},{10545,2},{11157,3}};
|
||||
case 81: return {{10531,3},{10545,2},{10529,3},{10537,2},{11434,3},{10547,2},{10551,1},{10573,1},{11433,2},{10561,1},{11431,1},{12121,2},{10563,3},{10571,1},{10734,2},{10541,3},{10559,2},{10577,1},{9200,3},{11157,3}};
|
||||
case 90: return {{11100,2},{11104,3},{11110,2},{11112,2},{11214,2},{11420,2},{11134,3},{11124,3},{11366,1},{11122,2},{11142,3},{11419,1},{11150,3},{11200,2},{11152,1},{10992,3},{10936,3},{10938,3}};
|
||||
case 91: return {{10992,3},{10994,3},{11206,1},{11014,2},{11020,2},{11190,2},{11713,2},{11198,3},{11024,3},{11201,1},{11034,2},{11188,2},{11189,1},{11042,1},{11199,2},{11044,1},{11104,3},{10936,3},{10960,2},{11181,2}};
|
||||
case 92: return {{10936,3},{10938,3},{10960,2},{11181,2},{10940,2},{10948,1},{10978,3},{10970,2},{11182,2},{12120,1},{10964,2},{10980,1},{10984,3},{10962,1},{10982,2},{10986,1},{11104,3},{10992,3},{11206,2},{11020,2}};
|
||||
case 110: return {{8349,3},{8359,3},{11281,2},{8353,2},{8383,2},{11284,3},{8381,2},{8389,2},{11278,1},{11282,1},{8361,1},{8391,3},{8399,1},{11277,1},{12149,2},{12150,1},{8405,1},{11716,1},{8237,2},{11715,3},{8277,1},{11279,3}};
|
||||
case 111: if(tank)return {{8295,2},{11716,3},{8305,3},{8761,2},{11285,2},{8293,3},{8299,1},{8301,2},{8323,3},{8325,1},{8313,1},{8758,2},{8759,2},{8319,1},{8343,3},{8347,1},{8237,2},{11715,3},{8277,1},{11279,3}}; return {{8295,2},{9026,2},{11716,3},{8305,3},{8761,2},{11285,2},{8299,1},{8301,2},{8323,3},{8303,1},{8307,2},{8325,1},{8313,1},{8335,2},{8759,2},{8341,2},{8343,3},{8347,1},{8237,2},{11715,3},{8277,1}};
|
||||
case 112: return {{8227,2},{8237,2},{11699,2},{11715,3},{8245,3},{8277,1},{8249,1},{8269,2},{8255,1},{8762,2},{12146,2},{8263,3},{8279,1},{8763,1},{8265,1},{11280,3},{8271,1},{8359,3},{11281,2},{8353,2},{11284,3}};
|
||||
default:return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AdventureProfessionPolicy.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "AdventureRotationPolicy.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
@@ -405,6 +406,8 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
|
||||
lua_getfield(_state,-1,"seed");
|
||||
if(lua_isinteger(_state,-1)){auto value=lua_tointeger(_state,-1);if(value>=0 && value<=0xFFFFFFFFll)profile.Seed=uint32(value);}
|
||||
lua_pop(_state,1);
|
||||
profile.SoloSpecialization=uint32(std::clamp(ReadFloatField(_state,-1,"solo_specialization",
|
||||
float(AdventureRotationPolicy::SoloSpec(profile.Class,profile.Seed))),0.0f,2.0f));
|
||||
profile.Role = ReadStringField(_state,-1,"role","damage");
|
||||
if(profile.Role!="tank" && profile.Role!="healer" && profile.Role!="damage")profile.Role="damage";
|
||||
profile.HomeZone = uint32(std::max(0.0f,ReadFloatField(_state,-1,"home_zone",0)));
|
||||
|
||||
@@ -29,6 +29,7 @@ struct TC_GAME_API ClientlessAdventureProfile
|
||||
std::string HomeRegion;
|
||||
std::string Background, Vocation;
|
||||
uint32 ProfileVersion = 0, Level = 0, Class = 0, Specialization = 0, Seed = 0;
|
||||
uint32 SoloSpecialization = 0;
|
||||
std::string Role = "damage", FavoriteRegion;
|
||||
uint32 HomeZone = 0, HomeCapital = 0, FavoriteZone = 0;
|
||||
std::unordered_map<uint32, float> ZoneAffinity;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,58 @@
|
||||
"""Exercise production spell conditions, resources, procs and stable solo choices."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/AdventureCombatMgr.cpp').read_text();a=s.index(' bool Allowed(');b=s.index('{',a);e=b+1;depth=1
|
||||
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
|
||||
method=s[a:e]
|
||||
code=r'''
|
||||
#include "AdventureRotationPolicy.h"
|
||||
#include <cassert>
|
||||
#include <set>
|
||||
using uint32=unsigned;
|
||||
constexpr unsigned CLASS_SHAMAN=7,POWER_HOLY_POWER=1;
|
||||
struct Aura{unsigned stacks=7,charges=7;unsigned GetStackAmount(){return stacks;}unsigned GetCharges(){return charges;}}aura;
|
||||
struct Unit{
|
||||
std::set<unsigned>auras;float hp=100;bool casting=false;
|
||||
bool HasAura(unsigned id){return auras.count(id);}bool GetAuraApplicationOfRankedSpell(unsigned id,unsigned){return auras.count(id);}
|
||||
float GetHealthPct(){return hp;}bool HasUnitState(unsigned){return casting;}
|
||||
};
|
||||
constexpr unsigned UNIT_STATE_CASTING=1;
|
||||
struct Player:Unit{
|
||||
unsigned combo=0,holy=0,power=0,max=100,klass=1,spec=0;
|
||||
unsigned GetComboPoints(){return combo;}unsigned GetPower(unsigned type){return type==POWER_HOLY_POWER?holy:power;}
|
||||
unsigned GetPowerType(){return 2;}unsigned GetMaxPower(unsigned){return max;}unsigned getClass(){return klass;}unsigned GetGUID(){return 1;}
|
||||
Aura*GetAura(unsigned id){return HasAura(id)?&aura:nullptr;}
|
||||
};
|
||||
unsigned GetPlayerSpec(Player*p){return p->spec;}
|
||||
struct Fighter{Player*me;
|
||||
'''+method+r'''
|
||||
};
|
||||
int main(){
|
||||
using namespace AdventureRotationPolicy;Player p;Unit target;Fighter fighter{&p};
|
||||
for(unsigned klass:{1,2,3,4,5,6,7,8,9,11}){
|
||||
std::set<unsigned>choices;for(unsigned seed=0;seed<300;++seed){auto spec=SoloSpec(klass,seed);assert(spec==SoloSpec(klass,seed));choices.insert(spec);assert(!Damage(klass,spec).empty());}
|
||||
if(klass==2 || klass==5)assert(choices==std::set<unsigned>{2});
|
||||
if(klass==1 || klass==4 || klass==7 || klass==8 || klass==11)assert((choices==std::set<unsigned>{0,1}));
|
||||
if(klass==3 || klass==6)assert((choices==std::set<unsigned>{1,2}));
|
||||
if(klass==9)assert((choices==std::set<unsigned>{0,1,2}));
|
||||
}
|
||||
assert(fighter.Allowed({45477,Condition::MissingAura,false,55095},&target));target.auras.insert(55095);assert(!fighter.Allowed({45477,Condition::MissingAura,false,55095},&target));
|
||||
Action finisher{2098,Condition::Always,false,0,5};p.combo=4;assert(!fighter.Allowed(finisher,&target));p.combo=5;assert(fighter.Allowed(finisher,&target));
|
||||
Action proc{11366,Condition::Always,false,0,0,48108};assert(!fighter.Allowed(proc,&target));p.auras.insert(48108);assert(fighter.Allowed(proc,&target));
|
||||
assert(!fighter.Allowed({5308,Condition::Execute},&target));target.hp=20;assert(fighter.Allowed({5308,Condition::Execute},&target));
|
||||
p.holy=2;assert(!fighter.Allowed({85256},&target));p.holy=3;assert(fighter.Allowed({85256},&target));p.holy=0;p.auras.insert(90174);assert(fighter.Allowed({85256},&target));
|
||||
p.hp=34;assert(fighter.Allowed({871,Condition::Emergency,true},&p));p.hp=35;assert(!fighter.Allowed({871,Condition::Emergency,true},&p));
|
||||
p.power=69;assert(!fighter.Allowed({78,Condition::Resource},&target));p.power=70;assert(fighter.Allowed({78,Condition::Resource},&target));
|
||||
p.klass=7;p.spec=0;assert(!fighter.Allowed({8042},&target));p.auras.insert(324);assert(fighter.Allowed({8042},&target));aura.stacks=9;aura.charges=6;assert(!fighter.Allowed({8042},&target));
|
||||
p.spec=1;p.auras.insert(53817);aura.stacks=4;assert(!fighter.Allowed({403},&target));aura.stacks=5;assert(fighter.Allowed({403},&target));
|
||||
p.auras.insert(67483);assert(!fighter.Allowed({2912},&target));p.auras.erase(67483);assert(fighter.Allowed({2912},&target));
|
||||
assert(Ranged(3,1) && Ranged(5,2) && !Ranged(1,2) && !Ranged(11,1));
|
||||
for(unsigned klass:{2,5,7,11})for(float hp:{20.f,60.f,85.f})assert(!Healing(klass,klass==5?1:2,hp).empty());
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['g++','-std=c++17','-I',str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Production class rotations: persistent PvE solo pools, native rank aura IDs, combo/holy power/proc/execute/resource gates, Eclipse and healer coverage passed')
|
||||
@@ -20,6 +20,8 @@ code=r'''
|
||||
#include <algorithm>
|
||||
#include <ctime>
|
||||
#include <set>
|
||||
#include <list>
|
||||
constexpr int UNIT_STATE_CASTING=1;
|
||||
using uint32=unsigned;using uint8=unsigned char;
|
||||
#define TC_GAME_API
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
@@ -33,14 +35,14 @@ struct Position{
|
||||
float GetExactDist(Position const& b)const{return std::sqrt((x-b.x)*(x-b.x)+(y-b.y)*(y-b.y)+(z-b.z)*(z-b.z));}
|
||||
float GetExactDist(Position const* b)const{return GetExactDist(*b);}
|
||||
};
|
||||
struct Map{float GetHeight(int,float,float,float,bool,float){return 0;}}map;
|
||||
struct Map{bool IsDungeon(){return false;}float GetHeight(int,float,float,float,bool,float){return 0;}}map;
|
||||
struct Creature;struct Player;
|
||||
struct Unit:Position{
|
||||
ObjectGuid guid;bool valid=true,alive=true,phase=true,visible=true,inWorld=true;
|
||||
virtual unsigned getLevel(){return 60;}
|
||||
virtual Creature* ToCreature(){return nullptr;}virtual Player* ToPlayer(){return nullptr;}
|
||||
virtual bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}virtual ObjectGuid GetGUID(){return guid;}
|
||||
Position const& GetPosition()const{return *this;}
|
||||
float GetOrientation(){return 0;}Unit*GetVictim(){return nullptr;}Position const& GetPosition()const{return *this;}
|
||||
void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}float GetAngle(Player*){return 0;}
|
||||
};
|
||||
struct Creature:Unit{bool controlled=false,evading=false,boss=false,elite=false;bool isElite(){return elite;}unsigned level=60;std::string script="mob";Creature(unsigned n){guid={n};}ObjectGuid GetGUID(){return guid;}Creature* ToCreature()override{return this;}bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};
|
||||
@@ -54,12 +56,14 @@ struct Session{Player* owner=nullptr;int releases=0,reclaims=0;void HandleRepopR
|
||||
std::vector<Creature*> escapeCreatures;
|
||||
struct Group{ObjectGuid leader{26};ObjectGuid GetLeaderGUID(){return leader;}}group;
|
||||
struct Player:Unit{
|
||||
bool IsResurrectRequested(){return false;}void ResurrectUsingRequestData(){}void ClearResurrectRequestData(){}
|
||||
bool IsWithinLOSInMap(Unit*){return los;}bool IsFlying(){return false;}bool IsFalling(){return false;}bool HasUnitState(int){return false;}void GetPartyMembers(std::list<Unit*>&){}bool IsInMap(Unit*){return true;}
|
||||
bool grouped=false;float health=100;std::set<Unit*> attackers;
|
||||
bool alive=true,ghost=false,teleport=false,combat=false,melee=true,los=true;unsigned delay=30;int saves=0,revives=0,bones=0,graveyards=0;bool sickness=false;int durability=75;
|
||||
Session session;Player(){session.owner=this;guid={26};}ObjectGuid GetGUID(){return guid;}Player* ToPlayer()override{return this;}Group* GetGroup(){return grouped?&group:nullptr;}bool IsAlive(){return alive;}bool IsInWorld(){return true;}bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleport;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}
|
||||
unsigned GetMapId(){return 0;}unsigned GetInstanceId(){return 0;}bool InBattleground(){return false;}bool InArena(){return false;}bool HasFlag(int,int){return ghost;}Session*GetSession(){return &session;}Corpse*GetCorpse(){return &corpse;}
|
||||
uint32 GetCorpseReclaimDelay(bool){return delay;}Map*GetMap(){return ↦}int GetPhaseShift(){return 0;}
|
||||
bool IsInPhase(Unit*c){return c->phase;}bool CanSeeOrDetect(Unit*c){return c->visible;}bool IsValidAttackTarget(Unit*c){return c->valid;}unsigned getLevel(){return 60;}bool IsWithinMeleeRange(Unit*){return melee;}float GetCombatReach(){return 1;}float GetAngle(Player*){return 0;}
|
||||
bool IsInPhase(Unit*c){return c->phase;}bool CanSeeOrDetect(Unit*c){return c->visible;}bool IsValidAttackTarget(Unit*c){return c->valid;}unsigned getLevel(){return 60;}unsigned getClass(){return 1;}bool IsWithinMeleeRange(Unit*){return melee;}float GetCombatReach(){return 1;}float GetAngle(Player*){return 0;}
|
||||
void SaveToDB(){++saves;}void RepopAtGraveyard(){++graveyards;teleport=true;Relocate(-100,0,0);}void ResurrectPlayer(float hp,bool sick){assert(hp==0.5f);sickness=sick;alive=true;ghost=false;++revives;}void SpawnCorpseBones(){++bones;}
|
||||
bool IsHostileTo(Unit*){return true;}float GetHealthPct(){return health;}
|
||||
auto const& getAttackers(){return attackers;}
|
||||
@@ -90,16 +94,26 @@ struct Travel{
|
||||
struct Behavior{int interrupts=0;void InterruptForCombat(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
|
||||
'''
|
||||
code+=r'''
|
||||
struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
|
||||
struct Profile{unsigned Class=0,Specialization=0,SoloSpecialization=0;std::string SourcePath;};
|
||||
struct Lua{Profile*GetAdventureProfile(unsigned){return nullptr;}void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
|
||||
'''
|
||||
# production Update uses direct Position members and creature near point.
|
||||
code=code.replace('float x=0,y=0,z=0;','union{float x=0;float m_positionX;};union{float y=0;float m_positionY;};union{float z=0;float m_positionZ;};',1)
|
||||
code=code.replace('bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}};', 'bool GetControllingPlayer(){return controlled;}bool IsAlive(){return alive;}bool IsInWorld(){return inWorld;}bool IsEvadingAttacks(){return evading;}bool isWorldBoss(){return boss;}unsigned getLevel(){return level;}std::string GetScriptName(){return script;}float GetAngle(Player*){return 0;}void GetNearPoint(Player*,float&a,float&b,float&c,float,float){a=x;b=y;c=z;}};')
|
||||
# Forward declaration needed in Creature's adapter.
|
||||
code=code.replace('struct Creature;','struct Creature;struct Player;',1)
|
||||
code+=r'''
|
||||
struct Encounter{bool Death(Player*){return false;}bool Update(Player*,unsigned){return false;}}encounter;auto sAdventureEncounterMgr=&encounter;
|
||||
struct Generator{bool PrepareCombat(Player*,std::string&){return true;}}generator;auto sAdventureGeneratorMgr=&generator;
|
||||
namespace AdventureRotationPolicy{unsigned SoloSpec(unsigned,unsigned){return 0;}}
|
||||
bool AdventureTank(Unit*){return false;}
|
||||
struct PlayerAI{static unsigned GetPlayerSpec(Player*){return 0;}};
|
||||
'''
|
||||
code+=header+r'''
|
||||
class AdventureCombatMgr::Fighter{public:int ticks=0;void Tick(Unit*,uint32){++ticks;}};
|
||||
class AdventureCombatMgr::Fighter{public:bool Ranged(){return false;}int ticks=0;void Tick(Unit*,uint32){++ticks;}};
|
||||
|
||||
bool AdventureCombatMgr::GroupHeal(Player*,Record&){return false;}
|
||||
bool AdventureCombatMgr::GroupTarget(Player*,Record&){return false;}
|
||||
bool AdventureCombatMgr::Reachable(Player*p,Unit*)const{return p->los;}
|
||||
Unit* AdventureCombatMgr::PvPTarget(Player*)const{return nullptr;}
|
||||
Creature* AdventureCombatMgr::Proactive(Player*)const{return nullptr;}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Run production mixed-group instance/wipe lifecycle with native-world adapters."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/AdventureEncounterMgr.cpp').read_text()
|
||||
def function(sig):
|
||||
a=s.index(sig);b=s.index('{',a);e=b+1;depth=1
|
||||
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
|
||||
return s[a:e]
|
||||
header=(root/'src/server/game/World/AdventureEncounterMgr.h').read_text()
|
||||
header='\n'.join(l for l in header.splitlines() if not l.startswith('#include')).replace('private:','public:')
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <cmath>
|
||||
using uint32=unsigned;using uint8=unsigned char;
|
||||
#define TC_GAME_API
|
||||
struct ObjectGuid{unsigned value=1;unsigned GetCounter(){return value;}};
|
||||
struct Position{float x=0,y=0,z=0,o=0;void Relocate(float a,float b,float c,float d=0){x=a;y=b;z=c;o=d;}void Relocate(Position const*p){*this=*p;}float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}float GetExactDist(Position const*p)const{return std::hypot(x-p->x,y-p->y);}};
|
||||
struct Session{bool clientless=true;bool IsClientless(){return clientless;}};
|
||||
struct Unit:Position{bool alive=true;unsigned map=100;bool IsAlive(){return alive;}unsigned GetMapId(){return map;}};
|
||||
struct Player;
|
||||
struct Ref{Player*p;Ref*nextRef=nullptr;Player*GetSource(){return p;}Ref*next(){return nextRef;}};
|
||||
struct Group{Ref*first=nullptr;Ref*GetFirstMember(){return first;}}group;
|
||||
struct Map{bool dungeon=true;bool IsDungeon(){return dungeon;}}inside,outside{false};
|
||||
constexpr unsigned PLAYER_FLAGS=0,PLAYER_FLAGS_GHOST=1,CMSG_REPOP_REQUEST=2;
|
||||
struct WorldPacket{WorldPacket(int,int){}template<class T>WorldPacket&operator<<(T){return *this;}};
|
||||
struct Corpse:Position{unsigned GetMapId(){return 100;}unsigned GetInstanceId(){return 10;}};
|
||||
struct Player:Unit{
|
||||
ObjectGuid guid;Session session;Group*groupPtr=&group;bool combat=false,ghost=false,requested=false,teleporting=false;unsigned instance=10,teleports=0,revives=0;std::list<Unit*>companions;
|
||||
struct PlayerSession:Session{Player*owner=nullptr;void HandleRepopRequestOpcode(WorldPacket&){owner->ghost=true;owner->map=0;owner->Relocate(-50,0,0);}}playerSession;
|
||||
Player(){playerSession.owner=this;}ObjectGuid GetGUID(){return guid;}Group*GetGroup(){return groupPtr;}PlayerSession*GetSession(){return &playerSession;}
|
||||
Corpse*GetCorpse(){return nullptr;}Map*GetMap(){return map==100?&inside:&outside;}unsigned GetInstanceId(){return instance;}
|
||||
bool IsInCombat(){return combat;}bool IsBeingTeleported(){return teleporting;}bool IsInMap(Unit*u){return map==u->map;}
|
||||
bool IsResurrectRequested(){return requested;}void ResurrectUsingRequestData(){alive=true;ghost=false;requested=false;++revives;}
|
||||
bool HasFlag(unsigned,unsigned){return ghost;}void GetPartyMembers(std::list<Unit*>&v){v=companions;}
|
||||
bool TeleportTo(unsigned m,float x,float y,float z,float o){map=m;Relocate(x,y,z,o);++teleports;return true;}
|
||||
void ResurrectPlayer(float hp,bool sickness){assert(hp==.5f && !sickness);alive=true;ghost=false;++revives;}void SpawnCorpseBones(){}
|
||||
};
|
||||
struct Trigger{unsigned target_mapId;float target_X,target_Y,target_Z,target_Orientation;};
|
||||
struct MapStore{Map*LookupEntry(unsigned){return &inside;}}sMapStore;
|
||||
struct Objects{Trigger entrance{100,1,0,0,0},exit{0,10,0,0,0};Trigger const*GetMapEntranceTrigger(unsigned){return &entrance;}Trigger const*GetGoBackTrigger(unsigned){return &exit;}}objects;auto sObjectMgr=&objects;
|
||||
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};struct Status{ClientlessTravelState State=ClientlessTravelState::Arrived;Position Target;};
|
||||
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;};
|
||||
struct Travel{bool present=false,path=true,accept=true;unsigned starts=0;Status status;Status*GetStatus(unsigned){return present?&status:nullptr;}void CancelTravel(unsigned){present=false;}bool AnalyzePath(Player*,Position const&,ClientlessPathDiagnostics&){return path;}bool StartTravel(Player*,Position const&p){++starts;if(accept){present=true;status.State=ClientlessTravelState::Moving;status.Target=p;}return accept;}}travel;auto sClientlessTravelMgr=&travel;
|
||||
struct Behavior{unsigned actual=0;void EnterPlayerInstance(Player*){++actual;}}behavior;auto sAdventureBehaviorMgr=&behavior;
|
||||
'''+header+r'''
|
||||
AdventureEncounterMgr* AdventureEncounterMgr::instance(){static AdventureEncounterMgr m;return &m;}
|
||||
bool AdventureEncounterMgr::AvoidGroundDamage(Player*){return false;}
|
||||
bool AdventureEncounterMgr::FollowIntoInstance(Player*){return false;}
|
||||
'''
|
||||
for name in ('bool AdventureEncounterMgr::MixedGroup(', 'bool AdventureEncounterMgr::Active(', 'void AdventureEncounterMgr::Register(', 'void AdventureEncounterMgr::Offline(', 'bool AdventureEncounterMgr::Update(', 'bool AdventureEncounterMgr::Death('):code+=function(name)+'\n'
|
||||
code+=r'''
|
||||
int main(){
|
||||
AdventureEncounterMgr mgr;Player bot,human,other;human.playerSession.clientless=false;human.guid.value=2;other.guid.value=3;
|
||||
Ref a{&bot},b{&human},c{&other};a.nextRef=&b;b.nextRef=&c;group.first=&a;
|
||||
assert(mgr.MixedGroup(&bot) && mgr.Active(&bot));human.playerSession.clientless=true;assert(!mgr.Active(&bot));assert(!mgr.Update(&bot,500));assert(mgr._records.empty());human.playerSession.clientless=false;
|
||||
assert(!mgr.Update(&bot,500));assert(mgr._records.count(1));assert(behavior.actual==1);
|
||||
unsigned hook=0;mgr.Register(100,[&](Player*,unsigned){++hook;return true;});assert(mgr.Update(&bot,500) && hook==1);mgr.Register(100,{});mgr._behaviors.clear();
|
||||
bot.alive=false;bot.Relocate(35,0,0);bot.companions={&human};assert(mgr.Death(&bot));assert(!bot.ghost && bot.teleports==0); // waits while ally lives
|
||||
human.alive=false;other.alive=false;bot.companions.clear();assert(mgr.Death(&bot));assert(bot.ghost && bot.map==0);
|
||||
travel.path=false;assert(mgr.Death(&bot));assert(bot.map==0 && bot.x==10 && bot.teleports==1); // failed route permits entrance fallback
|
||||
assert(mgr.Death(&bot));assert(bot.alive && !bot.ghost && bot.map==100 && bot.revives==1);
|
||||
assert(mgr.Update(&bot,500));assert(travel.starts==0); // gather other dead bots first
|
||||
other.alive=true;other.map=100;assert(mgr.Update(&bot,500));assert(travel.starts==1 && travel.status.Target.x==35); // returns to death point
|
||||
human.alive=true;assert(!mgr.Update(&bot,500));assert(!mgr._records.at(1).Returning); // follows living player instead
|
||||
bot.alive=false;bot.requested=true;assert(mgr.Death(&bot));assert(bot.alive && !bot.requested && bot.revives==2);
|
||||
bot.groupPtr=nullptr;assert(mgr.Update(&bot,500));assert(bot.map==0 && bot.x==10); // kick exits safely
|
||||
assert(!mgr.Update(&bot,500));assert(mgr._records.empty());
|
||||
bot.groupPtr=&group;bot.map=100;bot.alive=false;bot.requested=false;assert(mgr.Death(&bot));assert(mgr._records.count(1)); // login already dead still establishes context
|
||||
mgr.Offline(&bot);assert(mgr._records.empty());
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Production encounters: bot-only exclusion, mixed mode, extension hook, wait/revive, wipe fallback/reentry/gather/return, living-player priority, kick exit and offline cleanup passed')
|
||||
@@ -11,13 +11,13 @@ code=r'''
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
using uint32=unsigned;using Archetype=AdventureGeneratorPolicy::Archetype;
|
||||
enum{EQUIP_ERR_OK=0,ITEM_FLAG_CONJURED=1,HOLIDAY_NONE=0,ITEM_CLASS_ARMOR=4,
|
||||
enum{CLASS_WARRIOR=1,INVTYPE_2HWEAPON=17,EQUIP_ERR_OK=0,ITEM_FLAG_CONJURED=1,HOLIDAY_NONE=0,ITEM_CLASS_ARMOR=4,
|
||||
EQUIPMENT_SLOT_NECK=1,EQUIPMENT_SLOT_BACK=14,EQUIPMENT_SLOT_FINGER1=10,EQUIPMENT_SLOT_FINGER2=11,EQUIPMENT_SLOT_TRINKET1=12,EQUIPMENT_SLOT_TRINKET2=13,EQUIPMENT_SLOT_OFFHAND=16,
|
||||
ITEM_MOD_STRENGTH=4,ITEM_MOD_AGILITY=3,ITEM_MOD_INTELLECT=5,ITEM_MOD_ATTACK_POWER=38,ITEM_MOD_RANGED_ATTACK_POWER=39,ITEM_MOD_SPIRIT=6,ITEM_MOD_MANA=0,ITEM_MOD_MANA_REGENERATION=43,ITEM_MOD_SPELL_POWER=45,ITEM_MOD_SPELL_PENETRATION=47,MAX_ITEM_PROTO_STATS=1};
|
||||
struct Player{unsigned level=60;bool usable=true;unsigned getLevel(){return level;}template<class T>unsigned CanUseItem(T*){return usable?0:1;}};
|
||||
ITEM_MOD_DODGE_RATING=13,ITEM_MOD_PARRY_RATING=14,ITEM_MOD_MASTERY_RATING=49,ITEM_MOD_STAMINA=7,ITEM_MOD_RESILIENCE_RATING=35,ITEM_MOD_STRENGTH=4,ITEM_MOD_AGILITY=3,ITEM_MOD_INTELLECT=5,ITEM_MOD_ATTACK_POWER=38,ITEM_MOD_RANGED_ATTACK_POWER=39,ITEM_MOD_SPIRIT=6,ITEM_MOD_MANA=0,ITEM_MOD_MANA_REGENERATION=43,ITEM_MOD_SPELL_POWER=45,ITEM_MOD_SPELL_PENETRATION=47,MAX_ITEM_PROTO_STATS=1};
|
||||
struct Player{unsigned level=60;bool usable=true;unsigned getLevel(){return level;}unsigned CalculateTalentsPoints(){return level>=70?41:26;}template<class T>unsigned CanUseItem(T*){return usable?0:1;}};
|
||||
struct Effect{unsigned SpellID;};
|
||||
struct ItemTemplate{
|
||||
bool fits=true;unsigned quality=3,ilevel=80,cls=4,sub=4,flags=0,holiday=0,map=0,duration=0,rep=0,area=0,prop=0,suffix=0;int req=60,stat=4;float value=10;std::vector<Effect>Effects;
|
||||
unsigned type=1;unsigned GetInventoryType()const{return type;}bool fits=true;unsigned quality=3,ilevel=80,cls=4,sub=4,flags=0,holiday=0,map=0,duration=0,rep=0,area=0,prop=0,suffix=0;int req=60,stat=4;float value=10;std::vector<Effect>Effects;
|
||||
int GetRequiredLevel()const{return req;}unsigned GetQuality()const{return quality;}unsigned GetBaseItemLevel()const{return ilevel;}
|
||||
unsigned GetDuration()const{return duration;}unsigned GetFlags()const{return flags;}unsigned GetRequiredReputationFaction()const{return rep;}unsigned GetArea()const{return area;}unsigned GetMap()const{return map;}unsigned GetHolidayID()const{return holiday;}unsigned GetRandomProperty()const{return prop;}unsigned GetRandomSuffix()const{return suffix;}
|
||||
unsigned GetClass()const{return cls;}unsigned GetSubClass()const{return sub;}int GetItemStatType(unsigned)const{return stat;}float GetStatValue(unsigned,Player*)const{return value;}
|
||||
@@ -27,6 +27,7 @@ bool UsefulSpell(unsigned id,Archetype const&){return id==42;}
|
||||
'''+s[a:e]+r'''
|
||||
int main(){Player p;ItemTemplate item;auto a=AdventureGeneratorPolicy::Archetypes()[0];
|
||||
auto check=[&]{return EquipmentScore(&p,&item,4,a);};assert(check()==100);
|
||||
a.Spec=1;item.type=17;assert(!std::isfinite(check()));item.type=1;assert(std::isfinite(check()));a.Spec=0;
|
||||
item.sub=1;assert(!std::isfinite(check()));item.sub=4; // wearable cloth is wrong for this warrior
|
||||
item.stat=ITEM_MOD_INTELLECT;assert(!std::isfinite(check()));item.stat=4;
|
||||
item.Effects={{0xFFFFFFFFu}};assert(!std::isfinite(check()));item.Effects={{42}};assert(std::isfinite(check()));
|
||||
@@ -36,7 +37,12 @@ int main(){Player p;ItemTemplate item;auto a=AdventureGeneratorPolicy::Archetype
|
||||
item.map=719;assert(!std::isfinite(check()));item.map=0;
|
||||
item.holiday=1;assert(!std::isfinite(check()));item.holiday=0;
|
||||
p.usable=false;assert(!std::isfinite(check()));p.usable=true;
|
||||
item.fits=false;assert(!std::isfinite(check()));
|
||||
item.fits=false;assert(!std::isfinite(check()));item.fits=true;
|
||||
p.level=85;item.req=85;item.ilevel=416;item.quality=3;assert(!std::isfinite(check()));
|
||||
item.quality=4;assert(std::isfinite(check()));item.ilevel=346;assert(!std::isfinite(check()));item.ilevel=416;
|
||||
item.quality=5;assert(!std::isfinite(check()));item.quality=4;
|
||||
a.Spec=2;item.stat=ITEM_MOD_DODGE_RATING;assert(check()==426);
|
||||
|
||||
}
|
||||
'''
|
||||
# Both birth candidate ranking and retention of already equipped items consume this exact rule.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Execute generic harmful-ground detection and safe-path choice from production."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/AdventureEncounterMgr.cpp').read_text();a=s.index('bool AdventureEncounterMgr::AvoidGroundDamage(');b=s.index('{',a);e=b+1;depth=1
|
||||
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
|
||||
code=r'''
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
using uint32=unsigned;
|
||||
constexpr unsigned GRID_MAP_TYPE_MASK_DYNAMICOBJECT=1,SPELL_EFFECT_SCHOOL_DAMAGE=1,SPELL_AURA_PERIODIC_DAMAGE=2,SPELL_AURA_PERIODIC_DAMAGE_PERCENT=3;
|
||||
constexpr float INVALID_HEIGHT=-100000;
|
||||
struct Position{float m_positionX=0,m_positionY=0,m_positionZ=0,o=0;Position()=default;Position(float x,float y,float z,float d):m_positionX(x),m_positionY(y),m_positionZ(z),o(d){}float GetPositionX()const{return m_positionX;}float GetPositionY()const{return m_positionY;}float GetPositionZ()const{return m_positionZ;}float GetOrientation()const{return o;}float GetExactDist2d(Position const*p)const{return std::hypot(m_positionX-p->m_positionX,m_positionY-p->m_positionY);}};
|
||||
struct Dynamic;struct Unit{};
|
||||
struct WorldObject:Position{virtual Dynamic*ToDynObject(){return nullptr;}};
|
||||
struct Effect{unsigned Effect=0,ApplyAuraName=0;};struct Spell{std::vector<Effect>Effects;};
|
||||
struct Dynamic:WorldObject{bool hostile=true,phase=true;float radius=6;Unit caster;Spell spell{{{0,SPELL_AURA_PERIODIC_DAMAGE}}};Dynamic*ToDynObject()override{return this;}Unit*GetCaster(){return &caster;}Spell*GetSpellInfo(){return &spell;}float GetRadius(){return radius;}};
|
||||
struct Guid{unsigned GetCounter(){return 1;}};
|
||||
struct Map{bool invalid=false;float GetHeight(unsigned,float,float,float,bool,float){return invalid?INVALID_HEIGHT:0;}}map;
|
||||
std::vector<WorldObject*> areaObjects;
|
||||
struct Player:Position{unsigned stops=0;Guid GetGUID(){return {};}Map*GetMap(){return ↦}unsigned GetPhaseShift(){return 169;}bool IsInPhase(Dynamic*d){return d->phase;}bool IsHostileTo(Unit*u){for(auto obj:areaObjects)if(auto d=obj->ToDynObject())if(&d->caster==u)return d->hostile;return false;}void CastStop(){++stops;}};
|
||||
namespace Trinity{template<class Check>struct WorldObjectListSearcher{std::vector<WorldObject*>&objects;Check✓WorldObjectListSearcher(Player*,std::vector<WorldObject*>&v,Check&c,unsigned):objects(v),check(c){}};}
|
||||
namespace Cell{template<class T>void VisitAllObjects(Player*,T&search,float){for(auto obj:areaObjects)if(search.check(obj))search.objects.push_back(obj);}}
|
||||
enum class ClientlessTravelState{Moving,Arrived};struct Status{ClientlessTravelState State=ClientlessTravelState::Moving;Position Target;};
|
||||
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=0;};
|
||||
struct Travel{bool present=false,path=true;unsigned starts=0;Status status;Status*GetStatus(unsigned){return present?&status:nullptr;}bool AnalyzePath(Player*p,Position const&point,ClientlessPathDiagnostics&d){d.PathLength=p->GetExactDist2d(&point);return path;}bool StartTravel(Player*,Position const&point){++starts;present=true;status.Target=point;return true;}}travel;auto sClientlessTravelMgr=&travel;
|
||||
struct AdventureEncounterMgr{bool AvoidGroundDamage(Player*);};
|
||||
'''+s[a:e]+r'''
|
||||
int main(){
|
||||
AdventureEncounterMgr mgr;Player p;Dynamic hostile,friendly;areaObjects={&hostile,&friendly};friendly.hostile=false;friendly.m_positionX=10;
|
||||
assert(mgr.AvoidGroundDamage(&p));assert(travel.starts==1 && p.stops==1 && travel.status.Target.GetExactDist2d(&hostile)>=8);
|
||||
assert(mgr.AvoidGroundDamage(&p));assert(travel.starts==1 && p.stops==1); // existing safe destination retained
|
||||
p.m_positionX=20;assert(!mgr.AvoidGroundDamage(&p));
|
||||
p.m_positionX=0;travel.present=false;travel.path=false;assert(!mgr.AvoidGroundDamage(&p));assert(travel.starts==1);
|
||||
travel.path=true;map.invalid=true;assert(!mgr.AvoidGroundDamage(&p));map.invalid=false;
|
||||
hostile.phase=false;assert(!mgr.AvoidGroundDamage(&p));hostile.phase=true;
|
||||
hostile.m_positionZ=8;assert(!mgr.AvoidGroundDamage(&p));hostile.m_positionZ=0;
|
||||
hostile.spell.Effects.clear();assert(!mgr.AvoidGroundDamage(&p));
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Production ground avoidance: harmful/friendly, phase169, height, reachable safe endpoint, rejected paths and reuse without cast/movement thrashing passed')
|
||||
@@ -31,6 +31,7 @@ struct Player {
|
||||
} player;
|
||||
namespace ObjectAccessor{Player*FindPlayer(Guid){return nullptr;}}
|
||||
struct BotGroups{bool BotLed(Player*){return false;}bool FollowMember(Player*){return false;}}botGroups;auto sAdventureGroupMgr=&botGroups;
|
||||
struct Encounters{bool active=false;bool Active(Player*){return active;}}encounters;auto sAdventureEncounterMgr=&encounters;
|
||||
bool unsafe=false;bool Unsafe(Player*){return unsafe;}
|
||||
bool busy=false;bool Busy(uint32){return busy;}
|
||||
time_t now=100;int hour=5;
|
||||
@@ -66,12 +67,14 @@ public:
|
||||
int begun=0,fallbacks=0,micros=0,resets=0,switches=0,queues=0,leisureUpdates=0;
|
||||
bool AdoptGroup(Player*,Player*){return false;}
|
||||
void Update(Player*,uint32);
|
||||
void EnterPlayerInstance(Player*);
|
||||
void Generate(Player*,Record&,uint32){}
|
||||
void SetWanted(Player*,Record& r,std::string const&i,std::string const&l,time_t d){++switches;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=d;r.Dirty=true;r.State=Phase::Idle;}
|
||||
void Begin(Player*,Record&){++begun;}
|
||||
void Fallback(Player*,Record&){++fallbacks;}
|
||||
void ResetMicro(Player*,Record&r){++resets;r.Micro.clear();}
|
||||
void Micro(Player*,Record&){++micros;}
|
||||
void FinishInstance(Player*,Record&r){r.Instance.Active=false;}
|
||||
bool UpdateInstance(Player*,Record&,time_t){return false;}
|
||||
bool UpdateLeisure(Player*,Record&,uint32){++leisureUpdates;return true;}
|
||||
bool UpdateSocial(Player*,Record&,uint32){return true;}
|
||||
@@ -79,9 +82,15 @@ public:
|
||||
void Queue(Player*,Record&,std::string const&,std::string const&,std::string const&,bool=false){++queues;}
|
||||
};
|
||||
'''
|
||||
start=s.index('void AdventureBehaviorMgr::EnterPlayerInstance(');end=s.index('void AdventureBehaviorMgr::RefreshDay(',start)
|
||||
code+=s[start:end]
|
||||
code+=s[a:b]+r'''
|
||||
int main(){
|
||||
AdventureBehaviorMgr mgr;auto& r=mgr._records.at(26);
|
||||
r.Instance.Active=true;mgr.EnterPlayerInstance(&player);assert(!r.Instance.Active);
|
||||
encounters.active=true;r.Schedule=true;r.Deadline=1;r.Plan[6]={"profession","mine"};hour=6;
|
||||
mgr.Update(&player,1000);assert(mgr.switches==0 && mgr.micros==0 && r.Hour==5);
|
||||
encounters.active=false;r.Schedule=false;r.Deadline=1000;hour=5;
|
||||
// A completed travel at the exact location opens micro actions.
|
||||
mgr.Update(&player,1000);assert(r.State==AdventureBehaviorMgr::Phase::OnSite&&mgr.micros==1);
|
||||
// Best-effort graph progress must not pretend an area has been reached.
|
||||
|
||||
@@ -15,9 +15,10 @@ using uint8=unsigned char;using uint32=unsigned;
|
||||
constexpr unsigned ITEM_CLASS_CONTAINER=1,ITEM_CLASS_WEAPON=2,ITEM_SUBCLASS_WEAPON_FISHING_POLE=3;
|
||||
constexpr unsigned INVENTORY_SLOT_ITEM_START=0,INVENTORY_SLOT_ITEM_END=4,INVENTORY_SLOT_BAG_START=4,INVENTORY_SLOT_BAG_END=6,INVENTORY_SLOT_BAG_0=255;
|
||||
struct Proto{unsigned cls=0,sub=0;unsigned GetClass(){return cls;}unsigned GetSubClass(){return sub;}};
|
||||
struct Item{unsigned entry,count=1;Proto proto;Proto*GetTemplate(){return &proto;}unsigned GetEntry(){return entry;}unsigned GetCount(){return count;}};
|
||||
struct Guid{unsigned value=50;unsigned GetCounter(){return value;}};
|
||||
struct Item{Guid GetGUID(){return {entry};}unsigned entry,count=1;Proto proto;Proto*GetTemplate(){return &proto;}unsigned GetEntry(){return entry;}unsigned GetCount(){return count;}};
|
||||
struct Bag{unsigned GetBagSize(){return 8;}};
|
||||
struct Guid{unsigned GetCounter(){return 50;}};
|
||||
|
||||
struct Player{
|
||||
unsigned free=0,destroys=0;std::map<std::pair<unsigned,unsigned>,Item>items;Bag bag;
|
||||
unsigned GetFreeInventorySpace(){return free;}Guid GetGUID(){return {};}
|
||||
@@ -29,16 +30,17 @@ struct Player{
|
||||
struct Profile{std::unordered_set<unsigned>KeepItems{2901};unsigned CompanionLeashItem=37460;}profile;
|
||||
struct Lua{Profile*GetAdventureProfile(unsigned){return &profile;}}lua;auto sClientlessLuaEngine=&lua;
|
||||
struct Supply{std::unordered_map<unsigned,unsigned>InventoryAllowance(Player*){return {{100,20},{101,20}};}}supply;auto sAdventureSupplyMgr=&supply;
|
||||
struct Generator{std::unordered_set<unsigned>ProtectedEquipment(Player*){return {555};}}generator;auto sAdventureGeneratorMgr=&generator;
|
||||
struct AdventurePopulationMgr{void CleanInventory(Player*);};
|
||||
'''+s[a:e]+r'''
|
||||
int main(){
|
||||
Player p;AdventurePopulationMgr mgr;
|
||||
p.items={{ {255,0},Item{100,15}},{{255,1},Item{999,1}},{{255,2},Item{2901,1}},{{255,3},Item{37460,1}},
|
||||
{{4,0},Item{100,10}},{{4,1},Item{100,20}},{{4,2},Item{101,20}},{{4,3},Item{6256,1,{ITEM_CLASS_WEAPON,ITEM_SUBCLASS_WEAPON_FISHING_POLE}}},
|
||||
{{4,4},Item{54443,1,{ITEM_CLASS_CONTAINER,0}}},{{4,5},Item{777,1,{ITEM_CLASS_WEAPON,0}}},{{0,10},Item{888,1}}};
|
||||
p.free=1;mgr.CleanInventory(&p);assert(!p.destroys && p.items.size()==11);
|
||||
{{4,4},Item{54443,1,{ITEM_CLASS_CONTAINER,0}}},{{4,5},Item{777,1,{ITEM_CLASS_WEAPON,0}}},{{4,6},Item{555,1,{ITEM_CLASS_WEAPON,0}}},{{0,10},Item{888,1}}};
|
||||
p.free=1;mgr.CleanInventory(&p);assert(!p.destroys && p.items.size()==12);
|
||||
p.free=0;mgr.CleanInventory(&p);
|
||||
assert(!p.GetItemByPos(255,1));assert(!p.GetItemByPos(4,1));assert(!p.GetItemByPos(4,5));
|
||||
assert(p.GetItemByPos(4,6));assert(!p.GetItemByPos(255,1));assert(!p.GetItemByPos(4,1));assert(!p.GetItemByPos(4,5));
|
||||
assert(p.GetItemByPos(255,0)->count==15 && p.GetItemByPos(4,0)->count==5);
|
||||
for(auto pos:{std::pair{255,2},std::pair{255,3},std::pair{4,2},std::pair{4,3},std::pair{4,4},std::pair{0,10}})assert(p.GetItemByPos(pos.first,pos.second));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Exercise real PvE templates against 4.3.4 DBC rank, tree and dependency gates."""
|
||||
from pathlib import Path
|
||||
import json,subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
data=json.loads((Path(__file__).parent/'fixtures/cata_pve_talents.json').read_text())
|
||||
rows=','.join('{'+k+',{'+','.join(map(str,v))+'}}' for k,v in data['talents'].items())
|
||||
tabs=','.join('{'+k+',{'+','.join(map(str,v))+'}}' for k,v in data['tabs'].items())
|
||||
code=r'''
|
||||
#include "AdventureTalentPolicy.h"
|
||||
#include "AdventureRotationPolicy.h"
|
||||
#include <map>
|
||||
#include <cassert>
|
||||
struct Talent{unsigned tab,tier,ranks,dependency,dependencyRank;};
|
||||
struct Tab{unsigned mask,order;};
|
||||
std::map<unsigned,Talent> talents={ROWS};std::map<unsigned,Tab> tabs={TABS};
|
||||
int main(){
|
||||
for(unsigned klass:{1,2,3,4,5,6,7,8,9,11})for(unsigned spec=0;spec<3;++spec)for(bool tank:{false,true}){
|
||||
auto build=AdventureTalentPolicy::Build(klass,spec,tank);if(build.empty())continue;
|
||||
unsigned primary=0;for(auto const& t:tabs)if(t.second.mask==(1u<<(klass-1)) && t.second.order==spec)primary=t.first;
|
||||
assert(primary);unsigned total=0;
|
||||
for(auto t:build){assert(talents.count(t.Id));auto d=talents.at(t.Id);assert(t.Ranks<=d.ranks);assert(tabs.at(d.tab).mask==(1u<<(klass-1)));total+=t.Ranks;}
|
||||
assert(total==41);
|
||||
for(unsigned budget=1;budget<=41;++budget){
|
||||
std::map<unsigned,unsigned> known;unsigned left=budget;
|
||||
auto learn=[&](unsigned id,unsigned rank){
|
||||
auto d=talents.at(id);if(known[id]>rank || rank>=d.ranks)return false;
|
||||
auto cost=rank+1-known[id];if(cost>left)return false;
|
||||
if(d.dependency && known[d.dependency]<=d.dependencyRank)return false;
|
||||
unsigned spent=0,main=0;for(auto p:known){if(talents.at(p.first).tab==d.tab)spent+=p.second;if(talents.at(p.first).tab==primary)main+=p.second;}
|
||||
if(spent<d.tier*5 || (d.tab!=primary && main<31))return false;
|
||||
known[id]=rank+1;left-=cost;return true;
|
||||
};
|
||||
assert(AdventureTalentPolicy::Spend(build,learn,[&]{return left;}));assert(!left);
|
||||
if(budget==41)for(auto t:build)assert(known[t.Id]==t.Ranks);
|
||||
}
|
||||
}
|
||||
for(unsigned klass:{1,2,3,4,5,6,7,8,9,11})for(unsigned seed=0;seed<100;++seed){
|
||||
auto spec=AdventureRotationPolicy::SoloSpec(klass,seed);assert(!AdventureTalentPolicy::Build(klass,spec).empty());
|
||||
}
|
||||
}
|
||||
'''.replace('ROWS',rows).replace('TABS',tabs)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['g++','-std=c++17','-I',str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Canonical Cata PvE builds: 4.3.4 class/tree/rank/prerequisite gates, all 1..41 point budgets, solo selection coverage passed')
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Lock and Load correction clears effect 2: never scale an empty effect."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
h=(root/'src/server/game/Spells/SpellInfo.h').read_text()
|
||||
a=h.index(' SpellEffectInfo() :');b=h.index('{ }',a)+3;constructor=h[a:b]
|
||||
s=(root/'src/server/game/Spells/Spell.cpp').read_text();a=s.index('SpellValue::SpellValue(');b=s.index('{',a);e=b+1;depth=1
|
||||
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
using uint32=unsigned;constexpr int MAX_SPELL_EFFECTS=3,MECHANIC_NONE=0;
|
||||
struct WorldObject{};
|
||||
struct SpellEffectInfo{
|
||||
void const*_spellInfo;unsigned _effIndex,Effect,ApplyAuraName,AuraPeriod,DieSides;
|
||||
float RealPointsPerLevel,BasePoints,PointsPerComboPoint,Amplitude,DamageMultiplier,BonusMultiplier;
|
||||
int MiscValue,MiscValueB,Mechanic;void const*TargetARadiusEntry;void const*TargetBRadiusEntry;
|
||||
unsigned ChainTarget,ItemType,TriggerSpell;void*ImplicitTargetConditions;
|
||||
struct{float Coefficient,Variance,ComboPointsCoefficient;}Scaling;
|
||||
'''+constructor+r'''
|
||||
bool IsEffect()const{return Effect!=0;}
|
||||
int CalcBaseValue(WorldObject const*,void const*)const{assert(_spellInfo && IsEffect());return int(BasePoints);}
|
||||
};
|
||||
struct SpellInfo{SpellEffectInfo Effects[3];unsigned MaxAffectedTargets=0;};
|
||||
struct SpellValue{int EffectBasePoints[3];unsigned CustomBasePointsMask,MaxAffectedTargets;float RadiusMod,AuraStackAmount,CriticalChance;SpellValue(SpellInfo const*,WorldObject const*);};
|
||||
'''+s[a:e]+r'''
|
||||
int main(){
|
||||
alignas(SpellEffectInfo) unsigned char memory[sizeof(SpellEffectInfo)];memset(memory,0xA5,sizeof(memory));
|
||||
auto empty=new(memory)SpellEffectInfo;
|
||||
assert(!empty->_spellInfo && !empty->Effect && empty->Scaling.Coefficient==0 && empty->Scaling.Variance==0 && empty->Scaling.ComboPointsCoefficient==0);
|
||||
SpellInfo spell;spell.Effects[0]._spellInfo=&spell;spell.Effects[0].Effect=1;spell.Effects[0].BasePoints=7;
|
||||
spell.Effects[1]._spellInfo=&spell;spell.Effects[1].Effect=2;spell.Effects[1].BasePoints=12;
|
||||
// Exact state produced by the 56342 native correction.
|
||||
spell.Effects[2]=SpellEffectInfo();SpellValue value(&spell,nullptr);
|
||||
assert(value.EffectBasePoints[0]==7 && value.EffectBasePoints[1]==12 && value.EffectBasePoints[2]==0);
|
||||
spell.Effects[0]=SpellEffectInfo();spell.Effects[1]=SpellEffectInfo();SpellValue allEmpty(&spell,nullptr);
|
||||
for(auto points:allEmpty.EffectBasePoints)assert(points==0);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Native empty spell effects: poisoned-memory scaling initialization, Lock and Load cleared slot, active effect values and all-empty spell values passed')
|
||||
Reference in new issue
Block a user