• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!
  • ✅ The POLL for Hive's Texturing Contest #34 is OPEN! Vote for the TOP 3 SKINS! 🔗Click here to cast your vote!
  • ✅ The POLL for Hive's Techtree Contest #20 is OPEN! Vote for the TOP 3 FACTIONS! 🔗Click here to cast your vote!

[Campaign] Battle for the Undercity (WOTLK) Co-Op

Level 16
Joined
Feb 22, 2025
Messages
315
Wallpaper BFTUC.webp




Hi everyone,

Figured I'd finally start a thread to act as a dev diary for my current project. I've been chipping away at this since about February. Progress has been a bit on the slow side, mostly because you know how I like to obsess over the polish and get things behaving exactly right.

I won't write out a massive wall of text to hype it up; here’s an intro and some gameplay—a 5-minute video showing the latest iteration and some testing runs.


----> INTRO //(stay on web, no need to install Medal app)



NEW UPDATE:
----> GAMEPLAY



BFTUC1.webp

BFTUC3.webp

BFTUC2.webp

BFTUC4.webp

BFTUC5.webp

BFTUC6.webp


Code:
Dev Diary:
5.3.2026 - added A state-based AI system that dynamically reacts to catapult danger zones,
navigates to optimal positions, and performs timed area-cleansing attacks.
Built with hashtable persistence, order deduplication,
and timer-driven actions to ensure smooth, non-spammy behavior in large-scale battles.
5.6.2026 - added scs of ambient environment



--Code Snippet:
  • Kodo AI
    • Events
      • Time - Every 0.10 seconds of game time
    • Conditions
      • (Kodo_Unit is alive) Equal to True
    • Actions
      • Custom script: set udg_Kodo_State = LoadInteger(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 330)
      • -------- // ---- IDLE & RETURN ---- --------
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • Kodo_State Equal to 0
        • Then - Actions
          • -------- ///////// Cooldown check////// --------
          • Custom script: set udg_TempTime = TimerGetElapsed(udg_AI_GameTimer) - LoadReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 334)
          • Custom script: if udg_TempTime < udg_Kodo_BreathCD then
          • Custom script: return
          • Custom script: endif
          • -------- // Find the nearest LANDED danger zone --------
          • Set TempInt = -1
          • Set TempDist = 999999.00
          • Custom script: set udg_TempTime = TimerGetElapsed(udg_AI_GameTimer)
          • For each (Integer DangerCheck) from 0 to 1, do (Actions)
            • Loop - Actions
              • -------- // Zone must be active AND already impacted --------
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • Danger_EndTime[DangerCheck] Greater than TempTime
                  • TempTime Greater than or equal to Danger_ImpactTime[DangerCheck]
                • Then - Actions
                  • Custom script: set udg_TempReal = (GetUnitX(udg_Kodo_Unit)-udg_Danger_X[udg_DangerCheck])[I](GetUnitX(udg_Kodo_Unit)-udg_Danger_X[udg_DangerCheck]) + (GetUnitY(udg_Kodo_Unit)-udg_Danger_Y[udg_DangerCheck])[/I](GetUnitY(udg_Kodo_Unit)-udg_Danger_Y[udg_DangerCheck])
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • TempReal Less than TempDist
                    • Then - Actions
                      • Set TempInt = DangerCheck
                      • Set TempDist = TempReal
                    • Else - Actions
                • Else - Actions
          • -------- // If a zone was found, immediately move there // break combat --------
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • TempInt Not equal to -1
            • Then - Actions
              • -------- / Safe approach point: 600 units from zone centre toward the Kodo --------
              • Custom script: set udg_Kodo_SafeX = udg_Danger_X[udg_TempInt] + 600.0 * Cos(Atan2(GetUnitY(udg_Kodo_Unit)-udg_Danger_Y[udg_TempInt], GetUnitX(udg_Kodo_Unit)-udg_Danger_X[udg_TempInt]))
              • Custom script: set udg_Kodo_SafeY = udg_Danger_Y[udg_TempInt] + 600.0 * Sin(Atan2(GetUnitY(udg_Kodo_Unit)-udg_Danger_Y[udg_TempInt], GetUnitX(udg_Kodo_Unit)-udg_Danger_X[udg_TempInt]))
              • -------- // Break combat and move --------
              • Custom script: set udg_TempReal = LoadReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 338)
              • Custom script: set udg_TempReal2 = LoadReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 339)
              • Custom script: if (udg_TempReal != udg_Kodo_SafeX) or (udg_TempReal2 != udg_Kodo_SafeY) then
              • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 338, udg_Kodo_SafeX)
              • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 339, udg_Kodo_SafeY)
              • Custom script: call IssueImmediateOrder(udg_Kodo_Unit, "stop")
              • Custom script: call IssuePointOrder(udg_Kodo_Unit, "move", udg_Kodo_SafeX, udg_Kodo_SafeY)
              • Custom script: endif
              • -------- /////// Store state/target ////// --------
              • Custom script: call SaveInteger(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 330, 1)
              • Custom script: call SaveInteger(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 331, udg_TempInt)
              • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 332, udg_Kodo_SafeX)
              • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 333, udg_Kodo_SafeY)
              • -------- ///////////// --------
            • Else - Actions
              • -------- // No danger zone – return to home rect --------
              • Custom script: set udg_Kodo_HomeX = GetRectCenterX(udg_Kodo_HomeRect)
              • Custom script: set udg_Kodo_HomeY = GetRectCenterY(udg_Kodo_HomeRect)
              • Custom script: set udg_TempDist = (GetUnitX(udg_Kodo_Unit) - udg_Kodo_HomeX)[I](GetUnitX(udg_Kodo_Unit) - udg_Kodo_HomeX) + (GetUnitY(udg_Kodo_Unit) - udg_Kodo_HomeY)[/I](GetUnitY(udg_Kodo_Unit) - udg_Kodo_HomeY)
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • TempDist Greater than 40000.00
                • Then - Actions
                  • -------- -------- // > 200 units away: Only issue Move if target changed -------- --------
                  • Custom script: set udg_TempReal = LoadReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 338)
                  • Custom script: set udg_TempReal2 = LoadReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 339)
                  • Custom script: if (udg_TempReal != udg_Kodo_HomeX) or (udg_TempReal2 != udg_Kodo_HomeY) then
                  • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 338, udg_Kodo_HomeX)
                  • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 339, udg_Kodo_HomeY)
                  • Custom script: call IssuePointOrder(udg_Kodo_Unit, "move", GetRectCenterX(udg_Kodo_HomeRect), GetRectCenterY(udg_Kodo_HomeRect))
                  • Custom script: endif
                • Else - Actions
                  • -------- -------- // Arrived home: Clear saved coords so she doesn't get stuck later -------- --------
                  • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 338, 0.0)
                  • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 339, 0.0)
        • Else - Actions
          • -------- // ── MOVING: check if close enough to safe point ── --------
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • Kodo_State Equal to 1
            • Then - Actions
              • -------- // Validate that the target zone is still active --------
              • Custom script: set udg_TempInt = LoadInteger(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 331)
              • Custom script: if udg_Danger_EndTime[udg_TempInt] <= TimerGetElapsed(udg_AI_GameTimer) then
              • Custom script: call SaveInteger(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 330, 0)
              • Custom script: return
              • Custom script: endif
              • -------- ///////////////// --------
              • Custom script: set udg_Kodo_SafeX = LoadReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 332)
              • Custom script: set udg_Kodo_SafeY = LoadReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 333)
              • Custom script: set udg_TempDist = (GetUnitX(udg_Kodo_Unit)-udg_Kodo_SafeX)[I](GetUnitX(udg_Kodo_Unit)-udg_Kodo_SafeX) + (GetUnitY(udg_Kodo_Unit)-udg_Kodo_SafeY)[/I](GetUnitY(udg_Kodo_Unit)-udg_Kodo_SafeY)
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • TempDist Greater than 40000.00
                • Then - Actions
                  • -------- // Still far – keep moving, only if target changed --------
                  • Custom script: set udg_TempReal = LoadReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 338)
                  • Custom script: set udg_TempReal2 = LoadReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 339)
                  • Custom script: if (udg_TempReal != udg_Kodo_SafeX) or (udg_TempReal2 != udg_Kodo_SafeY) then
                  • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 338, udg_Kodo_SafeX)
                  • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 339, udg_Kodo_SafeY)
                  • Custom script: call IssueImmediateOrder(udg_Kodo_Unit, "stop")
                  • Custom script: call IssuePointOrder(udg_Kodo_Unit, "move", udg_Kodo_SafeX, udg_Kodo_SafeY)
                  • Custom script: endif
                • Else - Actions
                  • -------- // Close enough – begin channeling --------
                  • Custom script: set udg_TempInt = LoadInteger(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 331)
                  • -------- // expire the zone --------
                  • Custom script: set udg_Danger_EndTime[udg_TempInt] = TimerGetElapsed(udg_AI_GameTimer)
                  • -------- // FACE the danger zone centre --------
                  • Custom script: set udg_TempReal = bj_RADTODEG * Atan2(udg_Danger_Y[udg_TempInt] - GetUnitY(udg_Kodo_Unit), udg_Danger_X[udg_TempInt] - GetUnitX(udg_Kodo_Unit))
                  • Custom script: call SetUnitFacing(udg_Kodo_Unit, udg_TempReal)
                  • -------- // Switch to CHANNELING --------
                  • Custom script: call SaveInteger(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 330, 2)
                  • -------- // cooldown start --------
                  • Custom script: call SaveReal(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 334, TimerGetElapsed(udg_AI_GameTimer))
                  • -------- // Pause and start fireeeee --------
                  • Custom script: call PauseUnit(udg_Kodo_Unit, true)
                  • Animation - Play Kodo_Unit's spell puke animation
                  • Animation - Change Kodo_Unit's animation speed to 50.00% of its original speed
                  • -------- // ----- Fire breath via SFX Manager ----- --------
                  • -------- // Compute direction toward the danger zone (radians) --------
                  • Custom script: set udg_TempReal = Atan2(udg_Danger_Y[udg_TempInt] - GetUnitY(udg_Kodo_Unit), udg_Danger_X[udg_TempInt] - GetUnitX(udg_Kodo_Unit))
                  • -------- // Mouth position: 150 units in front of the Kodo --------
                  • Custom script: set udg_GLOBAL_Temp_Point = Location(GetUnitX(udg_Kodo_Unit) + 250.0 * Cos(udg_TempReal), GetUnitY(udg_Kodo_Unit) + 250.0 * Sin(udg_TempReal))
                  • Set SFX_ModelPath = Doodads\Cinematic\FireTrapSide\FireTrapSide.mdl
                  • Set SFX_CreatePoint = GLOBAL_Temp_Point
                  • Set SFX_CreateDelay = 0.50
                  • Set SFX_DestroyDelay = 3.00
                  • Set SFX_Scale = 3.00
                  • Set SFX_TimeScale = 0.40
                  • -------- // correct model orientation --------
                  • Custom script: set udg_SFX_Yaw = udg_TempReal - bj_PI/2
                  • Set SFX_RemovePoint = True
                  • Custom script: call GuiSpecialEffectSystem_Run()
                  • -------- // Start 3‑second timer --------
                  • Custom script: set udg_Kodo_ChannelTimer = CreateTimer()
                  • Custom script: call SaveUnitHandle(udg_AI_Hashtable, GetHandleId(udg_Kodo_ChannelTimer), 0, udg_Kodo_Unit)
                  • Custom script: call TimerStart(udg_Kodo_ChannelTimer, 3.00, false, function KodoChannelEnd)
            • Else - Actions
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • Kodo_State Equal to 2
                • Then - Actions
                  • -------- // Safety: if somehow unpaused, reset to idle --------
                  • Custom script: if not IsUnitPaused(udg_Kodo_Unit) then
                  • Custom script: call SaveInteger(udg_AI_Hashtable, GetHandleId(udg_Kodo_Unit), 330, 0)
                  • Custom script: endif
                • Else - Actions



Technical Notes & Performance

I’m currently running this on Quenching graphics, which has been great for the aesthetic I’m aiming for. However, I’ve been keeping a close eye on technical stability. Lately, I’ve been looking into how the engine handles DirectX 12.

While it’s running okay for now, I’m a bit worried about potential performance bottlenecks or memory issues as the project grows in complexity—especially since DX12 can sometimes be a bit "temperamental" with the x86_64 client[cite: 1]. If anyone here is knowledgeable about the engine's response to DX12—specifically regarding draw call overhead or how it manages the rendering pipeline; I’d love to hear your thoughts or experiences.


What's Next?

The current focus is on refining the core mechanics and ensuring the "feel" of the gameplay is responsive. I’m trying to avoid the "feature creep" trap by polishing what’s already there before moving on to the next major milestone.


Why the Slow Burn?

I should mention that a big reason for the paced development is that I’m building most of my own systems and mechanics from the ground up. Rather than relying on standard templates, I’m trying to create a very specific RPG/RTS merge.

The project is quite heavy on:

  • Custom Gameplay Systems: Tailored mechanics to bridge the gap between hero-focused RPG play and large-scale RTS strategy.
  • Narrative & Scenarios: Deeply integrated story-driven missions.
  • Cinematics: High-effort in-engine sequences to keep the immersion consistent.
It’s a lot of moving parts to balance, but getting that "feel" right is more important to me than rushing to the finish line.

I'll be updating this thread as I make more progress. ( I’ll keep you updated on medal too)
Thanks for checking it out!





- Axolotl
 
Last edited:
Spoiler: Closing Note
Are the voices AI? I'm trying myself to make some new lines from existing WC3 voices using AI, but all the local tools I've tried are giving results that don't sound like the originals. I've been trying Replay and Applio, both of which are RVC-type voice conversion tools (I think).

Any tips on better tools or how to improve my results?
 
Are the voices AI? I'm trying myself to make some new lines from existing WC3 voices using AI, but all the local tools I've tried are giving results that don't sound like the originals. I've been trying Replay and Applio, both of which are RVC-type voice conversion tools (I think).

Any tips on better tools or how to improve my results?
Yeah, most of the voices you hear in the videos are indeed AI-generated, but I use a process of accentuation optimization to get them to sound natural.

The reason generic tools often sound 'off' is that they struggle with the unique cadence and grit. To fix this, I don’t just rely on raw conversion; I use a specific voice sample for the base identity and then provide a short (3–7 second) accentuation reference.

Depending on the emotion I need—I record or find a reference that has the exact 'energy' I want( using the same voice sample). The AI then maps the original voice onto that performance. It takes more work than a standard TTS, but it’s the only way to get the emotional weight right. I would recommend MinMax Voice Cloning.

Here is a preview of an AI generated voiceline with accentuation
 

Attachments

Last edited:
The AI then maps the original voice onto that performance.
This is interesting.

I gave minimax a shot just now, and it did a surprisingly good job of replicating the voice I used, compared to the local tools I've been trying.

Only problem is, it's only TTS, right? Have you been able to get around the problem of TTS basically deciding for itself how to deliver a line, rather than doing what you imagine? I tried out the "surprised" and "fearful" emotions on the lines I generated, and they made something that I guess is servicable, but they sounded the same as each other lol, and not quite like the delivery that I was doing with my own voice with the two local voice conversion (speech-to-speech) tools I mentioned.

I feel like there are so many TTS tools out there, but I'm just looking for something that will take the voice I cloned and let me replace some audio with that voice. Davinci Resolve Studio seems to have that capability if you shell out $300 to find out if it's any good lol, but after struggling for a couple days with these other tools that aren't cutting the mustard, I'm not about to drop money on that lmao.

I might have to keep Minimax as a fallback, since it's honestly passable, but I feel like when you want voice acting, you want to be able to...act out the delivery (or accentuation/emotion/energy).

Would doing something with Minimax like...deleting the clone every time and remaking it every time you want a new line so that you can provide the exact accentuation reference you want...work? I'm hestitant to try that in case I use up all my free voices lol. 😅
 
Last edited:
This is interesting.

I gave minimax a shot just now, and it did a surprisingly good job of replicating the voice I used, compared to the local tools I've been trying.

Only problem is, it's only TTS, right? Have you been able to get around the problem of TTS basically deciding for itself how to deliver a line, rather than doing what you imagine? I tried out the "surprised" and "fearful" emotions on the lines I generated, and they made something that I guess is servicable, but they sounded the same as each other lol, and not quite like the delivery that I was doing with my own voice with the two local voice conversion (speech-to-speech) tools I mentioned.

I feel like there are so many TTS tools out there, but I'm just looking for something that will take the voice I cloned and let me replace some audio with that voice. Davinci Resolve Studio seems to have that capability if you shell out $300 to find out if it's any good lol, but after struggling for a couple days with these other tools that aren't cutting the mustard, I'm not about to drop money on that lmao.

I might have to keep Minimax as a fallback, since it's honestly passable, but I feel like when you want voice acting, you want to be able to...act out the delivery (or accentuation/emotion/energy).

Would doing something with Minimax like...deleting the clone every time and remaking it every time you want a new line so that you can provide the exact accentuation reference you want...work? I'm hestitant to try that in case I use up all my free voices lol. 😅
The trick to getting that 'acting' isn't actually in the TTS settings; it’s using Speech-to-Speech (STS). You don't need to delete your clones;

What you’re looking for is a tool that allows Inference. Basically in the accentuating section you can even upload your own sample of ‘voice acting’ and it will try to replicate using the voice sample and your accent as a reference, you can regenerate it if you don’t like it.

Also, don’t worry about the 0/3 slots; when you save and download the output voice, you can clear that slot.
 
xD that Rock music drop around 2:45 - refreshing!

I see several nifty and valuable design choices you've made - such as:
  • Heroes acting as commanders, no need to hustle with 10 command groups :thumbs_up:
  • Custom sounds and music, always nice to have
My only concern: How does it play with SD graphics?
You'd hamper your potential playerbase with going HD exclusively.
 
xD that Rock music drop around 2:45 - refreshing!

I see several nifty and valuable design choices you've made - such as:
  • Heroes acting as commanders, no need to hustle with 10 command groups :thumbs_up:
  • Custom sounds and music, always nice to have
My only concern: How does it play with SD graphics?
You'd hamper your potential playerbase with going HD exclusively.
Haha yuup, that drop was a bit of a why not moment xd

And yeah, that’s a very fair concern. I’m aware going full Quenching/HD does narrow the playerbase a bit.

Right now I’m prioritizing the visual identity and overall feel, since a lot of the mechanics are built around readability in that environment.

Visually it leans into that HD/Quenching look, even though most of imports are SD assets under the hood.

I’m mainly pushing the lighting and overall presentation to get a specific atmosphere, but I’m trying to keep it reasonably performant.
I’ll definitely test how it behaves across different settings as things stabilize.

// Glad you picked up on the commander thing, it’s actually a big part of what I’m trying to push. I wanted to move away from the usual control-group juggling and make it feel more like directing units rather than micromanaging them.

Thanks! :grin:
 
Back
Top