User:Nexii Malthus/LLCS2 Armor
< User:Nexii Malthus
Jump to navigation
Jump to search
Revision as of 01:40, 11 August 2024 by Nexii Malthus (talk | contribs) (Updated script to include OBJECT_DEATH messages)
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(count);
float amount = llList2Float(damage, 0);
health -= amount;
// Check which damage dealt a deadly blow unless healed
if(health <= 0 && deadlyDamage == -1) deadlyDamage = index;
else if(health > 0 && deadlyDamage >= 0) deadlyDamage = -1;
/*
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.
*/
}
if(health < 0) health = 0;
else if(health > MAX_HEALTH * OVERHEALTH) health = MAX_HEALTH * OVERHEALTH;
health_updated();
if(health == 0)
{
// Generate synthetic object death event as per https://wiki.secondlife.com/wiki/Talk:Combat_Log#Damageable_Object_Deaths
key source = llDetectedKey(deadlyDamage);
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", source,
"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;
}
}