Building burn

I couldn't find a way to configure it in the editor--I believe it is hardcoded in the engine.

But you could always make a system for it:
  1. Register a unit-type to have custom fire behavior
  2. Remove the default fire abilities from the buildings on the map (these are hidden abilities each building/mechanical has, e.g. 'Afih' (On Fire))
  3. Monitor the health of units on the map and add/remove fire effects when they go above/below a life percentage threshold (ignoring units who are still being constructed)
Here is a system I created to control this:
JASS:
library BuildingFire requires Table
    /**
     *
     * BuildingFire v1.0
     *
     *  Description
     *  ===========
     *
     *    This system simulates the fire effect that occurs when buildings are reduced to certain
     *    health percentages. This allows you to control:
     *       - The percentages in which fire is emitted
     *       - The fire effect that is emitted
     *       - Where the fire effect is attached
     *
     *    Sadly, the default fire effect attachment points aren't exposed to us in code, so once
     *    you add a unit-type to this system, you'll need to manually configure which effects show
     *    during the small, medium, and large fire phases (and where they show up).
     *
     *    Most buildings use "sprite first", "sprite second", "sprite third", "sprite fourth", "sprite fifth",
     *    or "sprite sixth" as attachment points for their fires.
     *
     *    While this system is designed around buildings, you can also use it for units.
     *
     *   
     *  API
     *  ===
     *
     *    struct BuildingFireDescription
     *
     *        static method create takes integer unitId returns BuildingFireDescription
     *          - Registers a unit-type into this system. The system will periodically scan the health
     *            of these units on the map to add/remove fire effects based on this configuration.
     *
     *        real smallFireThreshold
     *          - The health percentage when small fire effects will be shown (e.g. 0.75 => 75% health)
     *          - Default is 0.75
     *
     *        real mediumFireThreshold
     *          - The health percentage when medium fire effects will be shown (e.g. 0.5 => 50% health)
     *          - Default is 0.5
     *          - Assumed to be lower than `smallFireThreshold`      
     *
     *        real largeFireThreshold
     *          - The health percentage when large fire effects will be shown (e.g. 0.25 => 25% health)
     *          - Default is 0.25
     *          - Assumed to be lower than `mediumFireThreshold`
     *
     *        method addSmallFireEffect takes string path, string attachmentPoint returns nothing
     *          - Registers an effect to be attached to the building when the building's health becomes
     *            lower than `smallFireThreshold`.
     *          - You can add up to 6 individual effects (controlled by MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING).
     *
     *        method addMediumFireEffect takes string path, string attachmentPoint returns nothing
     *          - Registers an effect to be attached to the building when the building's health becomes
     *            lower than `mediumFireThreshold`.
     *          - You can add up to 6 individual effects (controlled by MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING).
     *
     *        method addLargeFireEffect takes string path, string attachmentPoint returns nothing
     *          - Registers an effect to be attached to the building when the building's health becomes
     *            lower than `largeFireThreshold`.
     *          - You can add up to 6 individual effects (controlled by MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING).
     *
     *
     *  Sample
     *  ======
     *
     *     // Adds the type 'hhou' (Farm) to this system so we can customize its fire effects
     *     local BuildingFireDescription farmDescription = BuildingFireDescription.create('hhou')
     *
     *     // Small fire effects will show below 90% health
     *     set farmDescription.smallFireThreshold = 0.9
     *
     *     // Medium fire effects will show below 60% health
     *     set farmDescription.mediumFireThreshold = 0.6
     *
     *     // Large fire effects will show below 30% health
     *     set farmDescription.largeFireThreshold = 0.3
     *
     *     // Registers which effects show during small, medium, and large fire phases
     *     // You can specify up to 6 effects to appear
     *     call farmDescription.addSmallFireEffect(EFFECT_UNDEAD_BUILDING_FIRE_SMALL, "sprite first")
     *     call farmDescription.addMediumFireEffect(EFFECT_UNDEAD_BUILDING_FIRE_MEDIUM, "sprite first")
     *     call farmDescription.addLargeFireEffect(EFFECT_UNDEAD_BUILDING_FIRE_LARGE, "sprite first")
     */

    globals
        /**
         *
         * Utility Constants
         *
         */

        // Orcs and Humans use the same fire effects
        constant string EFFECT_HUMAN_BUILDING_FIRE_SMALL = "Environment\\SmallBuildingFire\\SmallBuildingFire2.mdl"
        constant string EFFECT_HUMAN_BUILDING_FIRE_MEDIUM = "Environment\\LargeBuildingFire\\LargeBuildingFire2.mdl"
        constant string EFFECT_HUMAN_BUILDING_FIRE_LARGE = "Environment\\LargeBuildingFire\\LargeBuildingFire1.mdl"

        constant string EFFECT_NIGHT_ELF_BUILDING_FIRE_SMALL = "Environment\\NightElfBuildingFire\\ElfSmallBuildingFire2.mdl"
        constant string EFFECT_NIGHT_ELF_BUILDING_FIRE_MEDIUM = "Environment\\NightElfBuildingFire\\ElfLargeBuildingFire2.mdl"
        constant string EFFECT_NIGHT_ELF_BUILDING_FIRE_LARGE = "Environment\\NightElfBuildingFire\\ElfLargeBuildingFire1.mdl"

        constant string EFFECT_UNDEAD_BUILDING_FIRE_SMALL = "Environment\\UndeadBuildingFire\\UndeadSmallBuildingFire2.mdl"
        constant string EFFECT_UNDEAD_BUILDING_FIRE_MEDIUM = "Environment\\UndeadBuildingFire\\UndeadLargeBuildingFire2.mdl"
        constant string EFFECT_UNDEAD_BUILDING_FIRE_LARGE = "Environment\\UndeadBuildingFire\\UndeadLargeBuildingFire1.mdl"

        /**
         *
         * Configuration
         *
         */

        // How often to scan unit health
        private constant real SCAN_INTERVAL = 0.1

        // Maximum number of units to scan per tick (for performance)
        private constant integer MAX_UNITS_TO_SCAN_PER_TICK = 30

        // Maximum number of fire effects that can be added per building for each phase (small, medium, large)
        private constant integer MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING = 6

        /**
         *
         * Other Globals
         *
         */
        private constant string SMALL_FIRE_THRESHOLD = "small"
        private constant string MEDIUM_FIRE_THRESHOLD = "medium"
        private constant string LARGE_FIRE_THRESHOLD = "large"

        private Table fireDescriptions = 0
        private Table fireInstances = 0
        private Table fireEffectGroups = 0
        private Table constructionStates = 0

        private integer filterUnitId = 0
        private rect playableArea = null
        private group enumGroup = CreateGroup()
        private timer scanTimer = CreateTimer()
    endglobals

    private function IsUnitUnderConstruction takes unit u returns boolean
        return constructionStates.boolean[GetHandleId(u)]
    endfunction

    private struct FireEffectGroup
        integer count

        method destroy takes nothing returns nothing
            local integer index = 0
            local effect fireEffect = null
            loop
                exitwhen index >= count

                set fireEffect = fireEffectGroups[this].effect[index]
                if fireEffect != null then
                    call DestroyEffect(fireEffect)
                endif

                set index = index + 1
            endloop

            call this.deallocate()
            set fireEffect = null
        endmethod

        method addFireEffect takes unit building, string path, string attachmentPoint returns nothing
            local effect fireEffect = null
 
            if building == null or path == null or path == "" or attachmentPoint == null or attachmentPoint == "" then
                return
            endif
           
            set fireEffect = AddSpecialEffectTarget(path, building, attachmentPoint)
            call fireEffectGroups.link(this).effect.save(count, fireEffect)
            set count = count + 1

            set fireEffect = null
        endmethod
    endstruct

    private struct BuildingFireInstance
        private static thistype lastScannedNode = 0

        unit building
        string activeThreshold
        FireEffectGroup effectGroup

        thistype next
        thistype prev

        private method removeDefaultFireBehavior takes nothing returns nothing
            call UnitRemoveAbility(building, 'Afir') // On Fire (Other)
            call UnitRemoveAbility(building, 'Afih') // On Fire (Human)
            call UnitRemoveAbility(building, 'Afio') // On Fire (Orc)
            call UnitRemoveAbility(building, 'Afiu') // On Fire (Undead)
            call UnitRemoveAbility(building, 'Afin') // On Fire (Night Elf)
        endmethod

        private method destroyExistingFireEffects takes nothing returns nothing
            if effectGroup != 0 then
                call effectGroup.destroy()
                set effectGroup = 0
            endif
        endmethod

        method destroy takes nothing returns nothing
            set this.next.prev = this.prev
            set this.prev.next = this.next

            // If the list is now empty, disable the scan
            if thistype(0).next == 0 then
                call PauseTimer(scanTimer)
            endif

            call this.destroyExistingFireEffects()
            call this.deallocate()
        endmethod

        private method transitionToNoFire takes nothing returns nothing
            if activeThreshold == "" then
                return
            endif

            set activeThreshold = ""
            call this.destroyExistingFireEffects()
        endmethod

        private method transitionToSmallFire takes BuildingFireDescription fireDescription returns nothing
            local integer index = 0

            if activeThreshold == SMALL_FIRE_THRESHOLD then
                return
            endif

            set activeThreshold = SMALL_FIRE_THRESHOLD
            call this.destroyExistingFireEffects()
            set effectGroup = FireEffectGroup.create()

            loop
                exitwhen index >= fireDescription.smallFireEffectCount
                call effectGroup.addFireEffect(building, fireDescription.smallFireEffectPath[index], fireDescription.smallFireEffectAttachmentPoint[index])
                set index = index + 1
            endloop
        endmethod

        private method transitionToMediumFire takes BuildingFireDescription fireDescription returns nothing
            local integer index = 0

            if activeThreshold == MEDIUM_FIRE_THRESHOLD then
                return
            endif

            set activeThreshold = MEDIUM_FIRE_THRESHOLD
            call this.destroyExistingFireEffects()
            set effectGroup = FireEffectGroup.create()

            loop
                exitwhen index >= fireDescription.mediumFireEffectCount
                call effectGroup.addFireEffect(building, fireDescription.mediumFireEffectPath[index], fireDescription.mediumFireEffectAttachmentPoint[index])
                set index = index + 1
            endloop
        endmethod

        private method transitionToLargeFire takes BuildingFireDescription fireDescription returns nothing
            local integer index = 0

            if activeThreshold == LARGE_FIRE_THRESHOLD then
                return
            endif

            set activeThreshold = LARGE_FIRE_THRESHOLD
            call this.destroyExistingFireEffects()
            set effectGroup = FireEffectGroup.create()

            loop
                exitwhen index >= fireDescription.largeFireEffectCount
                call effectGroup.addFireEffect(building, fireDescription.largeFireEffectPath[index], fireDescription.largeFireEffectAttachmentPoint[index])
                set index = index + 1
            endloop
        endmethod

        method checkHealth takes nothing returns nothing
            local BuildingFireDescription fireDescription = fireDescriptions[GetUnitTypeId(building)]
            local real lifePercentage

            if IsUnitUnderConstruction(building) then
                return
            endif

            if GetUnitTypeId(building) == 0 or IsUnitType(building, UNIT_TYPE_DEAD) or fireDescription == 0 then
                call this.destroy()
                return
            endif

            set lifePercentage = GetWidgetLife(building) / GetUnitState(building, UNIT_STATE_MAX_LIFE)
           
            if lifePercentage > fireDescription.smallFireThreshold then
               call this.transitionToNoFire()
            elseif lifePercentage > fireDescription.mediumFireThreshold then
               call this.transitionToSmallFire(fireDescription)
            elseif lifePercentage > fireDescription.largeFireThreshold then
               call this.transitionToMediumFire(fireDescription)
            else
               call this.transitionToLargeFire(fireDescription)
            endif           
        endmethod

        private static method periodicScan takes nothing returns nothing
            local thistype current = lastScannedNode.next
            local integer scanCount = 0
            loop
                exitwhen current == 0 or scanCount >= MAX_UNITS_TO_SCAN_PER_TICK
                call current.checkHealth()
                set lastScannedNode = current
                set current = current.next
                set scanCount = scanCount + 1
            endloop

            // If reached the end of the list without scanning the max limit
            // then restart at the beginning of the list next tick
            if scanCount < MAX_UNITS_TO_SCAN_PER_TICK then
                set lastScannedNode = thistype(0)
            endif
        endmethod

        static method create takes unit building returns thistype
            local thistype this
            local integer handleId = GetHandleId(building)

            if fireInstances[handleId] != 0 then
                return fireInstances[handleId]
            endif

            set this = thistype.allocate()
            set this.building = building
            set this.activeThreshold = ""
            set this.effectGroup = 0
            set fireInstances[handleId] = this

            // If the list is no longer empty, start scanning
            if thistype(0).next == 0 then
                call TimerStart(scanTimer, SCAN_INTERVAL, true, function thistype.periodicScan)
            endif

            set thistype(0).next.prev = this
            set this.next = thistype(0).next
            set thistype(0).next = this
            set this.prev = thistype(0)

            call this.removeDefaultFireBehavior()

            return this
        endmethod

        private static method enumUnitTypeFilter takes nothing returns boolean
            if GetUnitTypeId(GetFilterUnit()) == filterUnitId then
                call BuildingFireInstance.create(GetFilterUnit())
            endif

            return false
        endmethod

        static method createInstancesForUnitType takes integer unitId returns nothing
            set filterUnitId = unitId
            call GroupEnumUnitsInRect(enumGroup, playableArea, Condition(function thistype.enumUnitTypeFilter))
        endmethod

        private static method onEnterMap takes nothing returns nothing
            if fireDescriptions[GetUnitTypeId(GetTriggerUnit())] != 0 then
                call BuildingFireInstance.create(GetTriggerUnit())
            endif
        endmethod

        private static method onConstructStart takes nothing returns nothing
            call constructionStates.boolean.store(GetTriggerUnit(), true)
        endmethod

        private static method onConstructFinish takes nothing returns nothing
            call constructionStates.boolean.forget(GetTriggerUnit())
        endmethod

        private static method onInit takes nothing returns nothing
            local trigger enterMapTrigger = CreateTrigger()
            local region playableRegion = CreateRegion()
            local trigger startConstructionTrigger = CreateTrigger()
            local trigger finishConstructionTrigger = CreateTrigger()

            set playableArea = Rect(GetCameraBoundMinX() - GetCameraMargin(CAMERA_MARGIN_LEFT), GetCameraBoundMinY() - GetCameraMargin(CAMERA_MARGIN_BOTTOM), GetCameraBoundMaxX() + GetCameraMargin(CAMERA_MARGIN_RIGHT), GetCameraBoundMaxY() + GetCameraMargin(CAMERA_MARGIN_TOP))
            call RegionAddRect(playableRegion, playableArea)
            call TriggerRegisterEnterRegion(enterMapTrigger, playableRegion, null)
            call TriggerAddAction(enterMapTrigger, function thistype.onEnterMap)

            call TriggerRegisterAnyUnitEventBJ(startConstructionTrigger, EVENT_PLAYER_UNIT_CONSTRUCT_START)
            call TriggerAddAction(startConstructionTrigger, function thistype.onConstructStart)

            call TriggerRegisterAnyUnitEventBJ(finishConstructionTrigger, EVENT_PLAYER_UNIT_CONSTRUCT_FINISH)
            call TriggerAddAction(finishConstructionTrigger, function thistype.onConstructFinish)

            set enterMapTrigger = null
            set playableRegion = null
        endmethod
    endstruct

    struct BuildingFireDescription
        real smallFireThreshold
        real mediumFireThreshold
        real largeFireThreshold
   
        readonly string array smallFireEffectPath[MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING]
        readonly string array smallFireEffectAttachmentPoint[MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING]
        readonly integer smallFireEffectCount

        readonly string array mediumFireEffectPath[MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING]
        readonly string array mediumFireEffectAttachmentPoint[MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING]
        readonly integer mediumFireEffectCount

        readonly string array largeFireEffectPath[MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING]
        readonly string array largeFireEffectAttachmentPoint[MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING]
        readonly integer largeFireEffectCount

        method addSmallFireEffect takes string path, string attachmentPoint returns nothing
            if smallFireEffectCount >= MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING then
                return
            endif

            set smallFireEffectPath[smallFireEffectCount] = path
            set smallFireEffectAttachmentPoint[smallFireEffectCount] = attachmentPoint
            set smallFireEffectCount = smallFireEffectCount + 1
        endmethod      

        method addMediumFireEffect takes string path, string attachmentPoint returns nothing
            if mediumFireEffectCount >= MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING then
                return
            endif

            set mediumFireEffectPath[mediumFireEffectCount] = path
            set mediumFireEffectAttachmentPoint[mediumFireEffectCount] = attachmentPoint
            set mediumFireEffectCount = mediumFireEffectCount + 1
        endmethod

        method addLargeFireEffect takes string path, string attachmentPoint returns nothing
            if largeFireEffectCount >= MAX_ACTIVE_FIRE_EFFECTS_PER_BUILDING then
                return
            endif

            set largeFireEffectPath[largeFireEffectCount] = path
            set largeFireEffectAttachmentPoint[largeFireEffectCount] = attachmentPoint
            set largeFireEffectCount = largeFireEffectCount + 1
        endmethod   

        static method create takes integer unitId returns thistype
            local thistype this

            if fireDescriptions[unitId] != 0 then
                return fireDescriptions[unitId]
            endif
           
            set this = thistype.allocate()
            set this.smallFireThreshold = 0.75
            set this.mediumFireThreshold = 0.50
            set this.largeFireThreshold = 0.25
            set this.smallFireEffectCount = 0
            set this.mediumFireEffectCount = 0
            set this.largeFireEffectCount = 0

            set fireDescriptions[unitId] = this
            call BuildingFireInstance.createInstancesForUnitType(unitId)

            return this
        endmethod

        private static method onInit takes nothing returns nothing
            set fireInstances = Table.create()
            set fireDescriptions = Table.create()
            set fireEffectGroups = Table.create()
            set constructionStates = Table.create()
        endmethod
    endstruct
endlibrary

The main downside is that you need to specify which attachment points to place the fires on yourself (since that information isn't exposed to us in the code [or the editor either?]). But if you wanted this just for a few buildings, it isn't so bad. If you want to do it on any building though, it gets a bit tedious. They typically go on the "sprite" attachment points, e.g. "sprite first", "sprite second", etc.

Here is a sample of configuring the system for a castle, a farm, and even a knight:
JASS:
scope SetupFires initializer Init
    private function Init takes nothing returns nothing
        local BuildingFireDescription castleDescription = BuildingFireDescription.create('hcas')
        local BuildingFireDescription farmDescription = BuildingFireDescription.create('hhou')
        local BuildingFireDescription knightDescription = BuildingFireDescription.create('hkni')
        local string knightFireEffect = "Abilities\\Spells\\Human\\FlameStrike\\FlameStrikeDamageTarget.mdl"

        // Set-up Castle Thresholds

        set castleDescription.smallFireThreshold = 0.9
        set castleDescription.mediumFireThreshold = 0.6
        set castleDescription.largeFireThreshold = 0.3

        call castleDescription.addSmallFireEffect(EFFECT_HUMAN_BUILDING_FIRE_SMALL, "sprite first")
        call castleDescription.addSmallFireEffect(EFFECT_HUMAN_BUILDING_FIRE_SMALL, "sprite second")

        call castleDescription.addMediumFireEffect(EFFECT_HUMAN_BUILDING_FIRE_MEDIUM, "sprite first")
        call castleDescription.addMediumFireEffect(EFFECT_HUMAN_BUILDING_FIRE_MEDIUM, "sprite second")
        call castleDescription.addMediumFireEffect(EFFECT_HUMAN_BUILDING_FIRE_MEDIUM, "sprite third")

        call castleDescription.addLargeFireEffect(EFFECT_HUMAN_BUILDING_FIRE_LARGE, "sprite first")
        call castleDescription.addLargeFireEffect(EFFECT_HUMAN_BUILDING_FIRE_LARGE, "sprite second")
        call castleDescription.addLargeFireEffect(EFFECT_HUMAN_BUILDING_FIRE_LARGE, "sprite third")
        call castleDescription.addLargeFireEffect(EFFECT_HUMAN_BUILDING_FIRE_LARGE, "sprite fourth")

        // Set-up Farm Thresholds

        set farmDescription.smallFireThreshold = 0.75
        set farmDescription.mediumFireThreshold = 0.5
        set farmDescription.largeFireThreshold = 0.25

        call farmDescription.addSmallFireEffect(EFFECT_UNDEAD_BUILDING_FIRE_SMALL, "sprite first")
        call farmDescription.addMediumFireEffect(EFFECT_UNDEAD_BUILDING_FIRE_MEDIUM, "sprite first")
        call farmDescription.addLargeFireEffect(EFFECT_UNDEAD_BUILDING_FIRE_LARGE, "sprite first")


        // Set-up Knight Thresholds

        set knightDescription.smallFireThreshold = 0.8
        set knightDescription.mediumFireThreshold = 0.5
        set knightDescription.largeFireThreshold = 0.2

        call knightDescription.addSmallFireEffect(knightFireEffect, "chest")
        call knightDescription.addMediumFireEffect(knightFireEffect, "chest")
        call knightDescription.addMediumFireEffect(knightFireEffect, "left hand")
        call knightDescription.addMediumFireEffect(knightFireEffect, "right hand")
        call knightDescription.addLargeFireEffect(knightFireEffect, "chest")
        call knightDescription.addLargeFireEffect(knightFireEffect, "left hand")
        call knightDescription.addLargeFireEffect(knightFireEffect, "right hand")
        call knightDescription.addLargeFireEffect(knightFireEffect, "origin")
        call knightDescription.addLargeFireEffect(knightFireEffect, "overhead")
    endfunction
endscope

I've attached a sample map as well, hopefully that helps if you want to pursue this further. :thumbs_up:
FireSample.png
 

Attachments

  • FireSample.w3m
    59 KB · Views: 1
Top