After some amount of work I'm happy to be able to share my Christmas gift for TDM! Or at least half of it, considering the other half is still only in design phase.
I created an addon that implements detailed player functionality, inspired by the first DeusEx game (The Conspiracy). It's NOT a mission script but an addon, meaning you place the pk4 in your TDM directory to enable the system and it will automatically work in each and every FM. Note that due to using tdm_user_addons.script / user_addon_init() it may disable or get disabled by other addons you have installed... this is a design limitation which can hopefully be lifted at some point in the future. This plugin will be included in my cyberpunk total conversion The Dark Module and automatically active with it, but first I shall design it and make it available as a small independent addon for vanilla TDM.
In the current initial release it implements just per-limb damage; The upcoming plan is to add a skill / augmentation system, allowing the player to use loot as skill points to enhance various body parts and gain new or improved abilities. Due to the scripting system being very limited in what it lets me modify, I'm still gathering documentation on how to implement those skills and which I can have. So until then detailed body damage with penalties remains the only part that's theoretically finished so far (further improvements are required here too)... including a HUD component above the lightgem showing the status of each body part: Green = full health, yellow = half health, red = (close to) no health, black = no health left.
The individual limbs available: Head, Torso, Left Arm, Right Arm, Left Leg, Right Leg... arms and legs work in unity however. They each take damage with the player as well as healing together with them. The more damaged a group is, the more a respective penalty is applied to the player. Groups and penalties include:
Head: When the head is damaged, the player begins to get dizzy and has their vision impaired. Currently the only effect replicates the flashbomb, causing white dots to appear around the player and disrupt their view until the head is healed. As the player can't live without a head, reaching 0 will cause instant death. More effects are possible and pending.
Torso: Damage to the torso translates to damage to the cloak, increasing the player's lightgem and rendering them more visible even in dark spots. As the player can't live without a torso, reaching 0 will cause instant death. Given script limitations I'm unable to simulate lung damage and decrease / limit the amount of air the player has.
Arms: Arm damage makes it difficult for the player to hold items: In-world objects being held will probabilistically get dropped, more often the worse your arms are hurt. When both arms reach 0 health, the player can no longer pick up anything in the world without instantly dropping it... you also become unable to use any weapons. Due to limitations in the scripting system, I'm unable to decrease the speed or accuracy of the blackjack / sword / bow as was desired.
Legs: As expected leg damage will make the player walk more slowly. It was desired that when one leg is lost the player can no longer jump, whereas when both legs are gone you remain stuck in crouch mode until healed... due to limitations in the scripting system this part is also not possible at the moment.
A crude limitation is the fact that limb damage is primarily based on the direction the player is walking toward... for example, increased likelihood of suffering damage to your right arm and leg if strafing right the moment you take the damage. The script system doesn't let you extract the vector direction of the last damage event, thus I can't use the real direction the hit came from when calculating which body part should absorb the most health loss. This means that even if an arrow comes from above and hits the player's head area, the player will only take damage to the legs if they're falling downward the moment they got hit... for the time being this provides a bare minimum amount of realism but is a very bitter implementation.
For this reason it would be greatly appreciated if any of the code developers could join this discussion and verify if they can help with adding the necessary hooks to external scripts: With 2.09 getting periodic beta releases at this point in time, it would be a great opportunity to make changes to the builtin player script that allow an external function to modify more player variables. This includes the efficiency of weapons, if the player is allowed to jump or forced to always crouch, and I'd also really appreciate a hook to manipulate the breath so air can be lowered as if the player is underwater. I understand other priorities exist or if the work may be considered too much, however this would help in being able to finish this mod with the proper functionality and planned skill set.
In the meantime let me know what you think of this idea and how I went about it! So far no new assets are included except the GUI graphics: Everything is done with less than 250 lines of script which I'd say is a good achievement I've attached the pk4 and since it's so lightweight I'll also add the main script straight in this post.
player_damage_1.0.pk4
#define DAMAGE_WAIT 0.0166667
#define EXPOSURE_ARM_LEFT 2
#define EXPOSURE_ARM_RIGHT 2
#define EXPOSURE_LEG_LEFT 2
#define EXPOSURE_LEG_RIGHT 2
#define EXPOSURE_HEAD 3
#define EXPOSURE_TORSO 1
#define PENALTY_TORSO_LIGHTGEM 4
player self;
float damage_gui;
boolean dizzy;
entity dizzy_particles;
float bound(float val, float min, float max)
{
if(val < min)
return min;
if(val > max)
return max;
return val;
}
// Range based probability: Calculates a probability per frame independent of wait time (0 - 1 range at 1 chance per second)
boolean prob(float x)
{
return sys.random(1) > x && sys.random(1) < DAMAGE_WAIT;
}
// Directional exposure calculator
float dex(vector dir, float ex_front, float ex_back, float ex_right, float ex_left, float ex_up, float ex_down)
{
float maxvel = 100;
float dir_front = bound(dir_x / maxvel, 0, 1) * ex_front;
float dir_back = bound(-dir_x / maxvel, 0, 1) * ex_back;
float dir_right = bound(dir_y / maxvel, 0, 1) * ex_right;
float dir_left = bound(-dir_y / maxvel, 0, 1) * ex_left;
float dir_up = bound(dir_z / maxvel, 0, 1) * ex_up;
float dir_down = bound(-dir_z / maxvel, 0, 1) * ex_down;
return dir_front + dir_back + dir_right + dir_left + dir_up + dir_down;
}
void player_damage_update_arm(float dmg_l, float dmg_r)
{
float hl_l = self.getFloatKey("health_arm_left");
float hl_r = self.getFloatKey("health_arm_right");
float hl = (hl_l + hl_r) / 2;
if(dmg_l != 0 || dmg_r != 0)
{
hl_l = bound(hl_l - dmg_l, 0, 1);
hl_r = bound(hl_r - dmg_r, 0, 1);
hl = (hl_l + hl_r) / 2;
self.setKey("health_arm_left", hl_l);
self.setKey("health_arm_right", hl_r);
self.setGuiFloat(damage_gui, "PlayerDamage_ItemArmLeft", hl_l);
self.setGuiFloat(damage_gui, "PlayerDamage_ItemArmRight", hl_r);
// Penalty #1: Disable the weapon once the arm are damaged to minimum health
if(hl == 0)
{
self.selectWeapon(WEAPON_UNARMED);
self.disableWeapon();
}
else
{
self.enableWeapon();
}
}
// Penalty #2: Probabilistically drop held items based on arm damage
if(hl == 0 || prob(hl))
if(self.heldEntity() != $null_entity)
self.holdEntity($null_entity);
}
void player_damage_update_leg(float dmg_l, float dmg_r)
{
float hl_l = self.getFloatKey("health_leg_left");
float hl_r = self.getFloatKey("health_leg_right");
float hl = (hl_l + hl_r) / 2;
if(dmg_l != 0 || dmg_r != 0)
{
hl_l = bound(hl_l - dmg_l, 0, 1);
hl_r = bound(hl_r - dmg_r, 0, 1);
hl = (hl_l + hl_r) / 2;
self.setKey("health_leg_left", hl_l);
self.setKey("health_leg_right", hl_r);
self.setGuiFloat(damage_gui, "PlayerDamage_ItemLegLeft", hl_l);
self.setGuiFloat(damage_gui, "PlayerDamage_ItemLegRight", hl_r);
// #Penalty #1: Make movement slower
self.setHinderance("health", 0.25 + hl * 0.75, 1);
}
}
void player_damage_update_head(float dmg)
{
float hl = self.getFloatKey("health_head");
float time_current = sys.getTime();
if(dmg != 0)
{
hl = bound(hl - dmg, 0, 1);
self.setKey("health_head", hl);
self.setGuiFloat(damage_gui, "PlayerDamage_ItemHead", hl);
// Penalty #1: Without a head the player dies
if(hl == 0)
self.damage(self, self, self.getOrigin(), "damage_suicide", 1);
// Penalty #2: Simulate dizzyness starting at half health
if(hl <= 0.5)
{
if(!dizzy)
{
dizzy_particles = sys.spawn("func_emitter");
dizzy_particles.setModel("flashbomb.prt");
dizzy_particles.setOrigin(self.getEyePos());
dizzy_particles.bind(self);
dizzy = true;
}
}
else
{
if(dizzy)
{
dizzy_particles.remove();
dizzy = false;
}
}
}
}
void player_damage_update_torso(float dmg)
{
float hl = self.getFloatKey("health_torso");
if(dmg != 0)
{
hl = bound(hl - dmg, 0, 1);
self.setKey("health_torso", hl);
self.setGuiFloat(damage_gui, "PlayerDamage_ItemTorso", hl);
// Penalty #1: Without a torso the player dies
if(hl == 0)
self.damage(self, self, self.getOrigin(), "damage_suicide", 1);
// Penalty #2: Torso damage negatively affects the lightgem
self.setLightgemModifier("damage", (1 - hl) * PENALTY_TORSO_LIGHTGEM);
}
}
void player_damage()
{
sys.waitFrame();
self = $player1;
damage_gui = self.createOverlay("guis/player_damage.gui", 1);
float health_old = 100;
// Init by sending a heal event filling the limbs to full health
player_damage_update_arm(-1, -1);
player_damage_update_leg(-1, -1);
player_damage_update_head(-1);
player_damage_update_torso(-1);
while(1)
{
// sys.waitFrame();
sys.wait(DAMAGE_WAIT);
float health_current = self.getHealth();
float dmg = (health_old - health_current) / 100;
float dmg_arm_left = dmg * EXPOSURE_ARM_LEFT;
float dmg_arm_right = dmg * EXPOSURE_ARM_RIGHT;
float dmg_leg_left = dmg * EXPOSURE_LEG_LEFT;
float dmg_leg_right = dmg * EXPOSURE_LEG_RIGHT;
float dmg_head = dmg * EXPOSURE_HEAD;
float dmg_torso = dmg * EXPOSURE_TORSO;
// If this is damage and not healing, apply directional damage to each limb
if(dmg > 0)
{
// Currently we estimate damage direction based on the player's velocity, we should fetch the real direction of a damage event when this becomes possible
vector dir = self.getMove();
vector ang = self.getViewAngles();
// Protections based on the player's position and relation to the environment
// protection_look: 1 when looking up, 0 when looking down
// protection_low: Higher as the lower part of the body is exposed
float protection_look = 1 - (90 + ang_x) / 180;
float protection_low = 1;
if(self.AI_CROUCH)
protection_low = 0;
else if(self.AI_ONGROUND)
protection_low = 0.75;
// Use the dex function to calculate directional exposure patterns, direction order: Front, back, right, left, up, down
// Arms: Somewhat likely to be hit, no added protection
// Legs: Somewhat likely to be hit, added protection when the player is crouching
// Head: Unlikely to be hit, added protection when the player is looking down
// Torso: Likely to be hit, no added protection
float exposure_arm_left = bound(sys.random(0.375) + dex(dir, 0.5, 0.25, 0.0, 1.0, 0.0, 0.25), 0, 1);
float exposure_arm_right = bound(sys.random(0.375) + dex(dir, 0.5, 0.25, 1.0, 0.0, 0.0, 0.25), 0, 1);
float exposure_leg_left = bound(sys.random(0.375) + dex(dir, 0.75, 0.5, 0.0, 0.5, 0.0, 1.0) * protection_low, 0, 1);
float exposure_leg_right = bound(sys.random(0.375) + dex(dir, 0.75, 0.5, 0.5, 0.0, 0.0, 1.0) * protection_low, 0, 1);
float exposure_head = bound(sys.random(0.25) + dex(dir, 0.25, 0.75, 0.5, 0.5, 1.0, 0.0) * protection_look, 0, 1);
float exposure_torso = bound(sys.random(0.5) + dex(dir, 0.75, 1.0, 0.0, 0.0, 0.0, 0.0), 0, 1);
// Apply the exposure to damage, multiplied to simulate the sensitivity / resistance of each limb
dmg_arm_left = exposure_arm_left * dmg * EXPOSURE_ARM_LEFT;
dmg_arm_right = exposure_arm_right * dmg * EXPOSURE_ARM_RIGHT;
dmg_leg_left = exposure_leg_left * dmg * EXPOSURE_LEG_LEFT;
dmg_leg_right = exposure_leg_right * dmg * EXPOSURE_LEG_RIGHT;
dmg_head = exposure_head * dmg * EXPOSURE_HEAD;
dmg_torso = exposure_torso * dmg * EXPOSURE_TORSO;
}
player_damage_update_arm(dmg_arm_left, dmg_arm_right);
player_damage_update_leg(dmg_leg_left, dmg_leg_right);
player_damage_update_head(dmg_head);
player_damage_update_torso(dmg_torso);
health_old = health_current;
}
}