User:Nexii Malthus/LLCS2 Armor

From Second Life Wiki
Jump to navigation Jump to search

Uses Firestorm Preprocessor for the defined constants but you can just rewrite them as integer DAMAGE_TYPE_* = #; each.

Code might get moved to GitHub later for easier version management, README and multiple script variants.


LLCS2 Armor.lsl

#define DAMAGE_TYPE_LBA -2
#define DAMAGE_TYPE_MEDICAL 100
#define DAMAGE_TYPE_REPAIR 101
#define DAMAGE_TYPE_EXPLOSIVE 102
#define DAMAGE_TYPE_CRUSHING 103
#define DAMAGE_TYPE_ANTI_TANK 104

#define LINK_ARMOR_DEATH 1
#define LINK_ARMOR_RESPAWN 2


health_updated()
{
    // We compute some variables here that we'll use later
    // Current and maximum health are used in both hovertext & object desc, so we'll stringify them beforehand
    string current = (string)llRound(health);
    string maximum = (string)llRound(MAX_HEALTH);
    // To render our healthbar later we need to figure out how many full and empty characters to display
    integer full = llRound((health/MAX_HEALTH) * 8);
    if(full > 8) full = 8;
    integer empty = 8 - full;
    
    // Hover text color redness and greenness is based on how empty or full the health is to max
    vector color = llVecNorm(<empty/8.0, full/8.0, 0>);
    
    // Draws healthbar, you can use different shapes
    string text;
    while(full --> 0) text += "▰"; // █ ▰
    while(empty -- > 0) text += "▱"; // ▁ ▱ ░
    
    // Tack on numerical values for easy readout on hovertext
    text += " " + current + " ⁄ " + maximum;
    
    // This is a formatted string that LBA requires
    // in the format of "LBA.v.<name>,<health>,<maxhealth>
    string desc = "LBA.v.LLCS2," + current + "," + maximum;
    
    // Update everything in one go (optimal), from object health, to hovertext and object desc
    llSetLinkPrimitiveParamsFast(LINK_THIS, [
        PRIM_HEALTH, health,
        PRIM_TEXT, text, color, 1.0,
        PRIM_DESC, desc
    ]);
}


float health; // Current health
float MAX_HEALTH = 5000.0; // Maximum health
float OVERHEALTH = 1.0; // Overhealth is when something is repaired or healed over the max health. 1.25 = allows repairing up to 125% of the max health

key lba_owner; // For passing owner of damage onto LLCS2
integer damage_reports; // For clearing linkset data



default
{
    state_entry()
    {
        // Read linkset data
        string ARMOR_HEALTH = llLinksetDataRead("ARMOR_HEALTH");
        string ARMOR_HEALTH_MAX = llLinksetDataRead("ARMOR_HEALTH_MAX");
        string ARMOR_OVERHEALTH = llLinksetDataRead("ARMOR_OVERHEALTH");
        
        // Custom max health / overhealth
        if(ARMOR_HEALTH_MAX) MAX_HEALTH = (float)ARMOR_HEALTH_MAX;
        if(ARMOR_OVERHEALTH) OVERHEALTH = (float)ARMOR_OVERHEALTH;
        
        // Customised starting health
        if(ARMOR_HEALTH) health = (float)ARMOR_HEALTH;
        else health = MAX_HEALTH;
        
        // Initialise
        health_updated();
        
        // Listen on LBA channel for backwards compatibility
        integer CHANNEL_LBA = (integer)("0x"+llGetSubString(llMD5String((string)llGetKey(),0),0,3));
        llListen(CHANNEL_LBA, "", "", "");
    }
    
    on_rez(integer param)
    {
        // The start string can be a JSON which offers customisation for the armor script
        // See REZ_PARAM_STRING of llRezObjectWithParams
        // JSON would be an object with optional properties "health", "health_max", "overhealth"
        // e.g. [REZ_PARAM_STRING, llList2Json(JSON_OBJECT, ["health", 450, "health_max", 750])]
        string params = llGetStartString();
        if(llJsonValueType(params, ["health"]) != JSON_INVALID)
            llLinksetDataWrite("ARMOR_HEALTH", llJsonGetValue(params, ["health"]));
        if(llJsonValueType(params, ["health_max"]) != JSON_INVALID)
            llLinksetDataWrite("ARMOR_HEALTH_MAX", llJsonGetValue(params, ["health_max"]));
        if(llJsonValueType(params, ["overhealth"]) != JSON_INVALID)
            llLinksetDataWrite("ARMOR_OVERHEALTH", llJsonGetValue(params, ["overhealth"]));
    }
    
    // The listener is for parsing messages by LBA and converting them into Combat2 damage
    listen(integer channel, string name, key identifier, string text)
    {
        list params = llCSV2List(text);
        if(llList2Key(params, 0) != llGetKey()) return;
        float amount = llList2Float(params, 1);
        lba_owner = llGetOwnerKey(identifier);
        llDamage(llGetKey(), amount * 100.0, DAMAGE_TYPE_LBA);
        // We are damaging ourselves but by saving lba_owner we recognise the real damage owner
        // You could save other details, such as the rezzer to emulate llDetectedRezzer
    }
    
    /*
        This is where we first receive damage reports and have the opportunity
        to adjust any damage for weaknesses and resistances.
        
        Best practice is to use simple, understandable multipliers.
        
        But you can also make multipliers location dependant to create weakspots
        instead of being applied generally. This would make things more fun.
        
        I've implemented Combat Log Writing in the form of simple description reports
        via the linkset data system. Using LSD I can avoid spamming the log and also
        target the logs via llRegionSayTo - This would be used in combination with a PvP HUD
    */
    on_damage(integer count)
    {
        while(count --> 0)
        {
            key owner = llDetectedOwner(count);
            list damage = llDetectedDamage(count);
            float amount = llList2Float(damage, 0);
            integer type = llList2Integer(damage, 1);
            
            if(type == DAMAGE_TYPE_ELECTRIC)
            {
                llAdjustDamage(count, amount * 2.00);
                llLinksetDataWrite(
                    "DAMAGE_" + (string)owner + "_" + (string)type,
                    "+100% against electrical components"
                );
            }
            
            else if(type == DAMAGE_TYPE_FORCE
                 || type == DAMAGE_TYPE_CRUSHING
                 || type == DAMAGE_TYPE_EXPLOSIVE)
            {
                llAdjustDamage(count, amount * 1.25);
                llLinksetDataWrite(
                    "DAMAGE_" + (string)owner + "_" + (string)type,
                    "+25% due to surface area"
                );
            }
            
            else if(type == DAMAGE_TYPE_PIERCING)
            {
                llAdjustDamage(count, amount * 1.50);
                llLinksetDataWrite(
                    "DAMAGE_" + (string)owner + "_" + (string)type,
                    "+50% piercing through armor"
                );
            }
            
            else if(type == DAMAGE_TYPE_ANTI_TANK
                 || type == DAMAGE_TYPE_LBA)
            {
                if(type == DAMAGE_TYPE_LBA) owner = lba_owner;
                llAdjustDamage(count, amount * 2.50);
                llLinksetDataWrite(
                    "DAMAGE_" + (string)owner + "_" + (string)type,
                    "+150% due to anti-tank"
                );
            }
            
            else if(type == DAMAGE_TYPE_MEDICAL)
            {
                llAdjustDamage(count, amount * 0.0);
                llLinksetDataWrite(
                    "DAMAGE_" + (string)owner + "_" + (string)type,
                    "Negate medical entirely as inorganic"
                );
            }
            
            /*
            
            Adding general resistance/weakness is simple and only requires and if and two functions:
            
            else if(type == DAMAGE_TYPE_THAT_YOU_FANCY)
            {
                llAdjustDamage(count, amount * multiplier);
                llLinksetDataWrite(
                    "DAMAGE_" + (string)owner + "_" + (string)type,
                    "Very short description about this"
                );
            }
            
            TODO: Location-based weakness/resistance; weak spots vs armored spots
            
            */
        }
    }
    
    
    // This is where all damage is summarised and applied to the health
    // Additionally other side effects may be triggered to react to applied damage
    final_damage(integer count)
    {
        integer deadlyDamage = -1;
        integer index;
        for(index = 0; index < count; ++index)
        {
            list damage = llDetectedDamage(index);
            float amount = llList2Float(damage, 0);
            health -= amount;
            
            // If health hits 0 or below, we're immediately dead, break the loop
            if(health <= 0)
            {
                health = 0;
                deadlyDamage = index;
                index = count;
            }
            
            // Max health, allow overhealing if configured
            else if(health > MAX_HEALTH * OVERHEALTH) health = MAX_HEALTH * OVERHEALTH;
            
            
            /*
                You could trigger any visual and auditory effects here based on type & amount
                For example what I did on my walking spider mech:
            
            // Constants defined at top of script:
            #define FX_MINIMUM_AMOUNT 10.0
            #define FX_THRESHOLD_DURATION 1.5
            
            // Code in this while loop:
            // Limit how many times we trigger a special effect based on minimum damage and duration
            if(amount > FX_MINIMUM_AMOUNT && llGetTime() - timeLastHit > FX_THRESHOLD_DURATION)
            {
                timeLastHit = llGetTime();
                
                if(type == DAMAGE_TYPE_GENERIC) llTriggerSound(SOUND_SOFT_HIT, 0.4);
                else if(type == DAMAGE_TYPE_ELECTRIC) llTriggerSound(SOUND_ELECTRIC_HIT, 0.7);
                else if(type == DAMAGE_TYPE_FORCE
                     || type == DAMAGE_TYPE_CRUSHING
                     || type == DAMAGE_TYPE_EXPLOSIVE) llTriggerSound(SOUND_EXPLOSIVE_HIT, 0.3);
                else if(type == DAMAGE_TYPE_PIERCING) llTriggerSound(SOUND_BULLET_HIT, 0.4);
                else if(type == DAMAGE_TYPE_ANTI_TANK) llTriggerSound(SOUND_HARD_HIT, 1.0);
            }
            
                You could do fun things like tweaking sound volume and particle size/amount
                based on the amount of damage and type inflicted.
                Such as an electric buzz on electric or metallic sparks on piercing.
                Checking with llGetTime() you can avoid spamming the effects too frequently.
                
                If you want to keep things easy to manage you can use llMessageLinked
                to have another script manage the visual and auditory effects for you.
                
                Just make sure to check with llGetTime() like I did above before firing
                off those linked message :P as there may be a LOT of damage events
                being processed here.
            */
        }
        
        health_updated();
        
        
        if(health == 0)
        {
            // Generate synthetic object death event as per https://wiki.secondlife.com/wiki/Talk:Combat_Log#Damageable_Object_Deaths
            vector sourcePos = llDetectedPos(deadlyDamage);
            vector targetPos = llGetPos();
            llRegionSay(COMBAT_CHANNEL, llList2Json(JSON_ARRAY, [
                llList2Json(JSON_OBJECT, [
                    "event", "OBJECT_DEATH",
                    "owner", llDetectedOwner(deadlyDamage),
                    "rezzer", llDetectedRezzer(deadlyDamage),
                    "source", llDetectedKey(deadlyDamage),
                    "source_pos", llList2Json(JSON_ARRAY, [sourcePos.x, sourcePos.y, sourcePos.z]),
                    "target", llGetKey(),
                    "target_pos", llList2Json(JSON_ARRAY, [targetPos.x, targetPos.y, targetPos.z]),
                    "type", llList2Integer(llDetectedDamage(deadlyDamage), 1)
                ])
            ]));
            
            state dead;
        }
    }
    
    // We use linkset_data events to cleverly report damage adjustments
    // Because these events are ONLY triggers when a value has been created or updated
    // This means duplicates based on [damage type + owner + type] are skipped!
    linkset_data(integer action, string name, string value)
    {
        if(action != LINKSETDATA_UPDATE) return;
        if(llGetSubString(name, 0, 6) != "DAMAGE_") return;
        key owner = (key)llGetSubString(name, 7, 42);
        integer type = (integer)llGetSubString(name, 44, -1);
        
        // Rather than spamming combat channel we are doing a targetted say
        // that can be picked up by a PvP HUD
        llRegionSayTo(owner, COMBAT_CHANNEL, llList2Json(JSON_OBJECT, [
            "event", "ADJUSTMENT",
            "type", type,
            "target", llGetKey(),
            "description", value
        ]));
        
        // We'll limit the amount of damage reports from accumulating too much
        // This does mean people may eventually see messages show up again in large sessions
        // where the object has been around for a long time
        if((++damage_reports % 100) == 0) llLinksetDataDeleteFound("^DAMAGE_", "");
    }
}


state dead
{
    state_entry()
    {
        // We are dead, jim
        // Code your dead routine as you wish!
        
        // In my case, I tell the main script that we're dead 
        // and I set it up to react to a respawn command to go back alive
        llMessageLinked(LINK_SET, LINK_ARMOR_DEATH, "", "");
    }
    
    link_message(integer sender, integer value, string message, key identifier)
    {
        if(value == LINK_ARMOR_RESPAWN) state default;
    }
}