mirror of
https://github.com/PhantomGamesDevelopment/TWM2.git
synced 2026-07-16 00:44:41 +00:00
Zombie Lord Update
Added in the new code for zombie lords.
This commit is contained in:
parent
1e9d2bf1b5
commit
76f973712a
3 changed files with 503 additions and 230 deletions
|
|
@ -78,6 +78,11 @@ PLEASE NOTE: I've moved all old changelogs into the version_history folder. This
|
||||||
* Ravager
|
* Ravager
|
||||||
* Ravagers will now perform ambush style attacks on targets, making them much more challenging
|
* Ravagers will now perform ambush style attacks on targets, making them much more challenging
|
||||||
* Increased the XP reward from killing ravager zombies
|
* Increased the XP reward from killing ravager zombies
|
||||||
|
* Lord
|
||||||
|
* Modified the behavior of zombie lords
|
||||||
|
* Replaced the acid cannon with an anti-tank photon cannon
|
||||||
|
* Zombie lords will now preferential target enemy ground armor before infantry, and engage their photon cannon on targets
|
||||||
|
* Zombie lords can now activate a defensive barrier to protect themselves and allies from damage temporarily... enjoy this new rage inducing mechanic :)
|
||||||
* Added Boss Proficiency
|
* Added Boss Proficiency
|
||||||
* Hidden challenges embedded in boss fights that award additional experience for completing tough feats
|
* Hidden challenges embedded in boss fights that award additional experience for completing tough feats
|
||||||
* For example: Defeat the shade lord without dying by the elemental shades
|
* For example: Defeat the shade lord without dying by the elemental shades
|
||||||
|
|
|
||||||
|
|
@ -39,15 +39,20 @@ $Zombie::SpeedUpdateTime[3] = 500;
|
||||||
|
|
||||||
//$Zombie::LungeDistance: How far (m) a zombie must be to lunge at a target
|
//$Zombie::LungeDistance: How far (m) a zombie must be to lunge at a target
|
||||||
$Zombie::LungeDistance = 10;
|
$Zombie::LungeDistance = 10;
|
||||||
|
|
||||||
|
// SPECIFIC ZOMBIE TYPE GLOBALS
|
||||||
|
|
||||||
//$Zombie::LordGrabDistance: How far (m) a zombie lord must be to grab a target
|
//$Zombie::LordGrabDistance: How far (m) a zombie lord must be to grab a target
|
||||||
$Zombie::LordGrabDistance = 5;
|
$Zombie::LordGrabDistance = 5;
|
||||||
//$Zombie::RapierUpwardScaling: How fast a rapier zombie will ascend when holding a player
|
|
||||||
$Zombie::RapierUpwardScaling = 750;
|
|
||||||
|
|
||||||
//$Zombie::ZombieLordShieldHealth: How much health the zombie lord energy barrier hasCP
|
//$Zombie::ZombieLordShieldHealth: How much health the zombie lord energy barrier hasCP
|
||||||
$Zombie::ZombieLordShieldHealth = 10.0;
|
$Zombie::ZombieLordShieldHealth = 10.0;
|
||||||
//$Zombie::ZombieLordShieldEnergy: How much energy the shield starts with. Note: Multiply this value by $Zombie::SpeedUpdateTime[3] to determine how long in MS the shield will be up.
|
//$Zombie::ZombieLordShieldEnergy: How much energy the shield starts with. Note: Multiply this value by $Zombie::SpeedUpdateTime[3] to determine how long in MS the shield will be up.
|
||||||
$Zombie::ZombieLordShieldEnergy = 50;
|
$Zombie::ZombieLordShieldEnergy = 50;
|
||||||
|
//$Zombie::ZombieLordWeaponCooldown: How long in MS that the zombie lord's photon cannon must cool down for
|
||||||
|
$Zombie::ZombieLordPhotonCooldown = 15000;
|
||||||
|
|
||||||
|
//$Zombie::RapierUpwardScaling: How fast a rapier zombie will ascend when holding a player
|
||||||
|
$Zombie::RapierUpwardScaling = 750;
|
||||||
|
|
||||||
//MISC Globals, Do not edit.
|
//MISC Globals, Do not edit.
|
||||||
$Zombie::killpoints = 5;
|
$Zombie::killpoints = 5;
|
||||||
|
|
@ -197,6 +202,10 @@ function TWM2Lib_Zombie_Core(%functionName, %arg1, %arg2, %arg3, %arg4) {
|
||||||
%arg1.recentShift = %arg3;
|
%arg1.recentShift = %arg3;
|
||||||
case "canshield":
|
case "canshield":
|
||||||
%arg1.canShield = %arg3;
|
%arg1.canShield = %arg3;
|
||||||
|
case "firingweapon":
|
||||||
|
%arg1.firingWeapon = %arg3;
|
||||||
|
case "canfireweapon":
|
||||||
|
%arg1.canFireWeapon = %arg3;
|
||||||
|
|
||||||
//lookForTarget(%zombie, [%pilot], [%groundVeh]): Identify the closest target, and the distance to that target
|
//lookForTarget(%zombie, [%pilot], [%groundVeh]): Identify the closest target, and the distance to that target
|
||||||
case "lookfortarget":
|
case "lookfortarget":
|
||||||
|
|
@ -398,9 +407,9 @@ function TWM2Lib_Zombie_Core(%functionName, %arg1, %arg2, %arg3, %arg4) {
|
||||||
%zombie.mountImage(ZBack, 4);
|
%zombie.mountImage(ZBack, 4);
|
||||||
%zombie.mountImage(ZDummyslotImg, 5);
|
%zombie.mountImage(ZDummyslotImg, 5);
|
||||||
%zombie.mountImage(ZDummyslotImg2, 6);
|
%zombie.mountImage(ZDummyslotImg2, 6);
|
||||||
%zombie.setInventory(AcidCannon, 1, true);
|
%zombie.mountImage(zLordPhotonCannonImg, 7);
|
||||||
%zombie.use(AcidCannon);
|
%zombie.canFireWeapon = 1;
|
||||||
%zombie.firstFired = 0;
|
%zombie.canShield = 1;
|
||||||
%zombie.canmove = 1;
|
%zombie.canmove = 1;
|
||||||
|
|
||||||
//Demon Zombie
|
//Demon Zombie
|
||||||
|
|
|
||||||
|
|
@ -1,258 +1,517 @@
|
||||||
datablock AudioProfile(ZLordFootSound)
|
datablock AudioProfile(ZLordFootSound) {
|
||||||
{
|
filename = "fx/weapons/grenade_explode_UW.wav";
|
||||||
filename = "fx/weapons/grenade_explode_UW.wav";
|
description = AudioBomb3d;
|
||||||
description = AudioBomb3d;
|
preload = true;
|
||||||
preload = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
datablock AudioProfile(HZLordFootSound)
|
datablock AudioProfile(HZLordFootSound) {
|
||||||
{
|
filename = "fx/weapons/SpinFusor_explode_UW.wav";
|
||||||
filename = "fx/weapons/SpinFusor_explode_UW.wav";
|
description = AudioBomb3d;
|
||||||
description = AudioBomb3d;
|
preload = true;
|
||||||
preload = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
datablock PlayerData(LordZombieArmor) : HeavyMaleBiodermArmor
|
datablock PlayerData(LordZombieArmor) : HeavyMaleBiodermArmor {
|
||||||
{
|
shapefile = "TR2medium_male.dts";
|
||||||
shapefile = "TR2medium_male.dts";
|
mass = 500;
|
||||||
mass = 500;
|
maxDamage = 18.0;
|
||||||
maxDamage = 18.0;
|
minImpactSpeed = 50;
|
||||||
minImpactSpeed = 50;
|
speedDamageScale = 0.015;
|
||||||
speedDamageScale = 0.015;
|
boundingBox = "2.9 2.9 4.8";
|
||||||
boundingBox = "2.9 2.9 4.8";
|
|
||||||
|
|
||||||
underwaterJetForce = 10;
|
underwaterJetForce = 10;
|
||||||
|
|
||||||
LFootSoftSound = ZLordFootSound;
|
LFootSoftSound = ZLordFootSound;
|
||||||
RFootSoftSound = ZLordFootSound;
|
RFootSoftSound = ZLordFootSound;
|
||||||
LFootHardSound = HZLordFootSound;
|
LFootHardSound = HZLordFootSound;
|
||||||
RFootHardSound = HZLordFootSound;
|
RFootHardSound = HZLordFootSound;
|
||||||
LFootMetalSound = ZLordFootSound;
|
LFootMetalSound = ZLordFootSound;
|
||||||
RFootMetalSound = ZLordFootSound;
|
RFootMetalSound = ZLordFootSound;
|
||||||
LFootSnowSound = ZLordFootSound;
|
LFootSnowSound = ZLordFootSound;
|
||||||
RFootSnowSound = ZLordFootSound;
|
RFootSnowSound = ZLordFootSound;
|
||||||
|
|
||||||
damageScale[$DamageType::M1700] = 1.5;
|
damageScale[$DamageType::M1700] = 1.5;
|
||||||
|
|
||||||
max[RepairKit] = 0;
|
max[RepairKit] = 0;
|
||||||
max[Mine] = 0;
|
max[Mine] = 0;
|
||||||
max[Grenade] = 0;
|
max[Grenade] = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
datablock ShapeBaseImageData(ZHead)
|
datablock ShapeBaseImageData(ZHead) {
|
||||||
{
|
shapeFile = "bioderm_heavy.dts";
|
||||||
shapeFile = "bioderm_heavy.dts";
|
emap = false;
|
||||||
emap = false;
|
mountPoint = 1;
|
||||||
mountPoint = 1;
|
offset = "0 0.25 -1.5";
|
||||||
offset = "0 0.25 -1.5";
|
rotation = "1 0 0 15";
|
||||||
rotation = "1 0 0 15";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
datablock ShapeBaseImageData(ZBack)
|
datablock ShapeBaseImageData(ZBack) {
|
||||||
{
|
shapeFile = "bioderm_medium.dts";
|
||||||
shapeFile = "bioderm_medium.dts";
|
emap = false;
|
||||||
emap = false;
|
mountPoint = 1;
|
||||||
mountPoint = 1;
|
offset = "0 0.25 -1.25";
|
||||||
offset = "0 0.25 -1.25";
|
rotation = "-1 0 0 10";
|
||||||
rotation = "-1 0 0 10";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
datablock ShapeBaseImageData(ZDummyslotImg)
|
datablock ShapeBaseImageData(ZDummyslotImg) {
|
||||||
{
|
shapeFile = "turret_muzzlepoint.dts";
|
||||||
shapeFile = "turret_muzzlepoint.dts";
|
emap = false;
|
||||||
emap = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
datablock ShapeBaseImageData(ZDummyslotImg2)
|
datablock ShapeBaseImageData(ZDummyslotImg2) {
|
||||||
{
|
shapeFile = "turret_muzzlepoint.dts";
|
||||||
shapeFile = "turret_muzzlepoint.dts";
|
emap = false;
|
||||||
emap = false;
|
offset = "-1.5 0 0";
|
||||||
offset = "-1.5 0 0";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function LZombiemovetotarget(%zombie){
|
datablock ShapeBaseImageData(zLordPhotonCannonImg) {
|
||||||
if(!isobject(%Zombie))
|
shapeFile = "turret_fusion_large.dts";
|
||||||
return;
|
emap = false;
|
||||||
if(%Zombie.getState() $= "dead") {
|
offset = "-1.5 0 1.0";
|
||||||
%zombie.throwweapon(1);
|
};
|
||||||
return;
|
|
||||||
}
|
datablock ForceFieldBareData(ZombieLordDefensiveBarrier) {
|
||||||
%pos = %zombie.getworldboxcenter();
|
className = "forcefield";
|
||||||
%closestClient = ZombieLookForTarget(%zombie);
|
fadeMS = 1000;
|
||||||
%closestDistance = getWord(%closestClient,1);
|
baseTranslucency = $noPassTrans;
|
||||||
%closestClient = getWord(%closestClient,0).Player;
|
powerOffTranslucency = $noPassTrans / $dimDiv;
|
||||||
if(%closestDistance <= $zombie::detectDist && %zombie.canmove != 0){
|
teamPermiable = false;
|
||||||
if(%zombie.hastarget != 1){
|
otherPermiable = false;
|
||||||
serverPlay3d("ZombieHOWL",%zombie.getWorldBoxCenter());
|
color = "0.0 1.0 0.0";
|
||||||
%zombie.hastarget = 1;
|
powerOffColor = "0.0 0.0 0.0";
|
||||||
|
targetNameTag = 'Zombie Lord Defensive Barrier';
|
||||||
|
targetTypeTag = 'ZLordDefensiveBarrier';
|
||||||
|
texture[0] = "skins/forcef1";
|
||||||
|
texture[1] = "skins/forcef2";
|
||||||
|
texture[2] = "skins/forcef3";
|
||||||
|
texture[3] = "skins/forcef4";
|
||||||
|
texture[4] = "skins/forcef5";
|
||||||
|
framesPerSec = 1; // 10
|
||||||
|
numFrames = 5;
|
||||||
|
scrollSpeed = 15;
|
||||||
|
umapping = 0.01; // 1.0
|
||||||
|
vmapping = 0.01; // 0.15
|
||||||
|
needsPower = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ZombieLordDefensiveBarrier::damageObject(%data, %targetObject, %sourceObject, %position, %amount, %damageType) {
|
||||||
|
//Apply the damage to the zombie lord's energy...
|
||||||
|
%zombie = %targetObject.sourceZombie;
|
||||||
|
%zombie.shieldHealth -= %amount;
|
||||||
|
//Flash the zombie lord...
|
||||||
|
%zombie.lastShieldDamageSource = %sourceObject;
|
||||||
|
%zombie.playShieldEffect("1 1 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
datablock LinearFlareProjectileData(ZLPhotonCannonProjectile) {
|
||||||
|
scale = "4 4 4";//6
|
||||||
|
sound = PlasmaProjectileSound;
|
||||||
|
|
||||||
|
faceViewer = true;
|
||||||
|
directDamage = 0.0;
|
||||||
|
hasDamageRadius = true;
|
||||||
|
indirectDamage = 2.5;
|
||||||
|
damageRadius = 10.0;
|
||||||
|
kickBackStrength = 10000;
|
||||||
|
radiusDamageType = $DamageType::Photon; //obviously change this
|
||||||
|
|
||||||
|
explosion = "PhotonMissileExplosion";
|
||||||
|
underwaterExplosion = "PhotonMissileExplosion";
|
||||||
|
splash = BlasterSplash;
|
||||||
|
|
||||||
|
dryVelocity = 500.0;
|
||||||
|
wetVelocity = 500.0;
|
||||||
|
velInheritFactor = 0.6;
|
||||||
|
fizzleTimeMS = 10000;
|
||||||
|
lifetimeMS = 10500;
|
||||||
|
explodeOnDeath = true;
|
||||||
|
|
||||||
|
reflectOnWaterImpactAngle = 0.0;
|
||||||
|
explodeOnWaterImpact = false;
|
||||||
|
deflectionOnWaterImpact = 0.0;
|
||||||
|
fizzleUnderwaterMS = 15000;
|
||||||
|
|
||||||
|
activateDelayMS = 0;
|
||||||
|
numFlares = 35;
|
||||||
|
flareColor = "0.0 1.1 0";
|
||||||
|
flareModTexture = "flaremod";
|
||||||
|
flareBaseTexture = "flarebase";
|
||||||
|
|
||||||
|
size[0] = 1;
|
||||||
|
size[1] = 10;
|
||||||
|
size[2] = 2;
|
||||||
|
|
||||||
|
hasLight = true;
|
||||||
|
lightRadius = 1.0;
|
||||||
|
lightColor = "0.6 1.1 0";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Zombie Lord
|
||||||
|
// TWM2 3.9.2
|
||||||
|
// - Old Behavior:
|
||||||
|
// - Heavy anti-ground assault unit that would grab and punch enemy infantry
|
||||||
|
// - Armed with an anti-infantry acid cannon weapon
|
||||||
|
// - New Behavior:
|
||||||
|
// - Heavy anti-tank unit. Still grabs enemy infantry.
|
||||||
|
// - Has preferential targeting, will try to target any available enemy ground armor before infantry.
|
||||||
|
// - Armed with a photon cannon to hit tanks hard.
|
||||||
|
// - When not engaged with enemy armor, will try to attack infantry. Uses weapon a lot less, but will sometimes fire.
|
||||||
|
// - The zombie lord may sometimes engage a energy barrier to prevent enemy projectile weapons from striking itself and nearby allies.
|
||||||
|
|
||||||
|
function LordZombieArmor::AI(%datablock, %zombie) {
|
||||||
|
if(!isObject(%zombie) || %zombie.getState() $= "dead") {
|
||||||
|
if(isObject(%zombie.shieldObject)) {
|
||||||
|
%zombie.shieldObject.delete();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
%chance = (getrandom() * 20);
|
%zPos = %zombie.getPosition();
|
||||||
if(%chance >= 19)
|
//Are we currently in the firing weapon state?
|
||||||
serverPlay3d("ZombieMoan",%zombie.getWorldBoxCenter());
|
if(%zombie.firingWeapon) {
|
||||||
|
%datablock.schedule(%zombie.updateTimeFrequency, "AI", %zombie);
|
||||||
%vector = ZgetFacingDirection(%zombie,%closestClient,%pos);
|
return;
|
||||||
if(%zombie.ATCount >= 20){
|
}
|
||||||
%zombie.ATCount = 0;
|
//Check if we are currently shielding
|
||||||
%zombie.nomove = 1;
|
if(%zombie.usingShield) {
|
||||||
%zombie.Fire = schedule(1250, %zombie, "ZFire", %zombie, %closestClient);
|
//Is the shield broken?
|
||||||
if(Game.CheckModifier("OhLordy") == 1) {
|
if(%zombie.shieldHealth <= 0) {
|
||||||
%zombie.Fire = schedule(1750, %zombie, "ZFire", %zombie, %closestClient);
|
//Yes...
|
||||||
}
|
%zombie.shieldObject.delete();
|
||||||
%zombie.Charge = schedule(500, %zombie, "ChargeEmitter", %zombie);
|
%zombie.usingShield = 0;
|
||||||
%zombie.chargecount = 0;
|
if(isObject(%zombie.lastShieldDamageSource)) {
|
||||||
|
%srcPlayer = %zombie.lastShieldDamageSource.sourceObject;
|
||||||
|
if(isObject(%srcPlayer.client)) {
|
||||||
|
CompleteNWChallenge(%srcPlayer.client, "ShieldBreaker");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Initialize the cooldown, note: a broken shield cooldown is 10 seconds longer than the non-broken cooldown
|
||||||
|
TWM2Lib_Zombie_Core("setZFlag", %zombie, "canShield", 0);
|
||||||
|
schedule(35000, 0, TWM2Lib_Zombie_Core, "setZFlag", %zombie, "canShield", 1);
|
||||||
|
}
|
||||||
|
%zombie.setActionThread($Zombie::RogThread, true);
|
||||||
|
//Still going, reduce energy counter and check if we're out of energy
|
||||||
|
%zombie.shieldEnergy--;
|
||||||
|
if(%zombie.shieldEnergy <= 0) {
|
||||||
|
%zombie.shieldObject.delete();
|
||||||
|
%zombie.usingShield = 0;
|
||||||
|
TWM2Lib_Zombie_Core("setZFlag", %zombie, "canShield", 0);
|
||||||
|
schedule(25000, 0, TWM2Lib_Zombie_Core, "setZFlag", %zombie, "canShield", 1);
|
||||||
|
}
|
||||||
|
%datablock.schedule(%zombie.updateTimeFrequency, "AI", %zombie);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if(%zombie.nomove != 1) {
|
//Am I engaged with something?
|
||||||
%fmultiplier = $Zombie::LForwardSpeed;
|
if(%zombie.hasTarget) {
|
||||||
|
//Is it a tank?
|
||||||
|
%tPos = %zombie.targetedPlayer.getPosition();
|
||||||
|
if(%zombie.targetIsTank) {
|
||||||
|
//If I'm not close enough, move up a bit
|
||||||
|
if(!%zombie.movingToPosition) {
|
||||||
|
if(vectorDist(%zPos, %tPos) > 200) {
|
||||||
|
//If I'm not close enough to the target, then I need to move into range...
|
||||||
|
%direction = vectorNormalize(vectorSub(%zPos, %tPos));
|
||||||
|
%zombie.movePoint = vectorScale(%direction, 150);
|
||||||
|
%zombie.movingToPosition = true;
|
||||||
|
}
|
||||||
|
//If I'm in range, then I need to determine my course of action
|
||||||
|
else {
|
||||||
|
//Fire...
|
||||||
|
if(%zombie.canFireWeapon) {
|
||||||
|
//LOS?
|
||||||
|
%direction = vectorNormalize(vectorSub(%tPos, %zPos));
|
||||||
|
%dVec = vectorAdd(%zPos, vectorscale(%direction, 200));
|
||||||
|
%rInfo = containerRaycast(%zPos, %tPos, $AllObjMask, %zombie);
|
||||||
|
if(getWord(%rInfo, 0) == %zombie.targetedPlayer || getWord(%rInfo, 0) == %zombie.targetedPlayer.client.vehicleMounted) {
|
||||||
|
//Target acquired... Fire
|
||||||
|
%datablock.zFire(%zombie, %zombie.targetedPlayer.client.vehicleMounted);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//Something's in the way... let's try moving...
|
||||||
|
%random = vectorAdd(%zPos, TWM2Lib_MainControl("getRandomPosition", "20\t1"));
|
||||||
|
%zombie.movePoint = %random;
|
||||||
|
%zombie.movingToPosition = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//Shield?
|
||||||
|
if(%zombie.canShield) {
|
||||||
|
%datablock.makeShield(%zombie);
|
||||||
|
%zombie.setActionThread($Zombie::RogThread, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//For now, I can't fire or shield, move up a bit...
|
||||||
|
%datablock.move(%zombie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//I'm currently moving to the target position, so continue moving...
|
||||||
|
%datablock.move(%zombie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//If it's not a tank, it's a player... which is a bit easier...
|
||||||
|
else {
|
||||||
|
//Check the distance to the target, if we're close enough, grab it...
|
||||||
|
%tPos = %zombie.targetedPlayer.getPosition();
|
||||||
|
%distance = vectorDist(%zPos, %tPos);
|
||||||
|
%vector = TWM2Lib_Zombie_Core("zombieGetFacingDirection", %zombie, %zombie.targetedPlayer.getPosition());
|
||||||
|
if(%distance <= $Zombie::LordGrabDistance && %zombie.canjump == 1) {
|
||||||
|
%vec = vectoradd(%pos,vectorScale(%vector,($Zombie::LordGrabDistance - 1.6)));
|
||||||
|
%mask = $TypeMasks::InteriorObjectType | $TypeMasks::StaticShapeObjectType | $TypeMasks::ForceFieldObjectType | $TypeMasks::VehicleObjectType | $TypeMasks::TerrainObjectType | $TypeMasks::PlayerObjectType;
|
||||||
|
%searchResult = containerRayCast(vectoradd(%pos,vectorScale(%vector,1.6)), %vec, %mask, %zombie);
|
||||||
|
if(%searchResult) {
|
||||||
|
%searchObj = getWord(%searchResult, 0);
|
||||||
|
if(%searchObj $= %zombie.targetedPlayer) {
|
||||||
|
%chance = getrandom(1,5);
|
||||||
|
if(%chance == 1) {
|
||||||
|
%dir = %zombie.getEyeVector();
|
||||||
|
%dir = vectornormalize(getword(%dir, 0)@" "@getword(%dir, 1)@" 0");
|
||||||
|
%dir = vectoradd(vectorscale(%dir, 7500),"0 0 1000");
|
||||||
|
%zombie.targetedPlayer.applyimpulse(%clpos, %dir);
|
||||||
|
%zombie.targetedPlayer.damage(0, %clpos, 0.8, $DamageType::ZombieL);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
%zombie.setvelocity("0 0 0");
|
||||||
|
TWM2Lib_Zombie_Core("setZFlag", %zombie, "canJump", 0);
|
||||||
|
schedule($Zombie::BaseJumpCooldown, 0, TWM2Lib_Zombie_Core, "setZFlag", %zombie, "canJump", 1);
|
||||||
|
%datablock.zLordLift(%zombie, %zombie.targetedPlayer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Should I fire?
|
||||||
|
if(%zombie.canFireWeapon && %distance > 35) {
|
||||||
|
if(getRandom(1, 20) == 1) {
|
||||||
|
//Fire!
|
||||||
|
%datablock.zFire(%zombie, %zombie.targetedPlayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Should I shield?
|
||||||
|
if(%zombie.canShield && %distance > 40) {
|
||||||
|
if(getRandom(1, 35) == 1) {
|
||||||
|
%datablock.makeShield(%zombie);
|
||||||
|
%zombie.setActionThread($Zombie::RogThread, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
%datablock.move(%zombie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//Scan for tanks
|
||||||
|
%targetParams = TWM2Lib_Zombie_Core("lookForTarget", %zombie, true, true);
|
||||||
|
%target = getWord(targetParams, 0);
|
||||||
|
%distance = getWord(%targetParams, 1);
|
||||||
|
if(isObject(%target.player)) {
|
||||||
|
//I see a tank, I will destroy that tank...
|
||||||
|
if(%distance <= $zombie::detectDist) {
|
||||||
|
%zombie.hasTarget = 1;
|
||||||
|
%zombie.targetIsTank = true;
|
||||||
|
%zombie.targetedPlayer = %target.player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Scan for infantry
|
||||||
|
%targetParams = TWM2Lib_Zombie_Core("lookForTarget", %zombie);
|
||||||
|
%target = getWord(targetParams, 0);
|
||||||
|
%distance = getWord(%targetParams, 1);
|
||||||
|
if(isObject(%target.player)) {
|
||||||
|
//Got a target player... let's go!
|
||||||
|
if(%distance <= $zombie::detectDist) {
|
||||||
|
%zombie.hasTarget = 1;
|
||||||
|
%zombie.targetedPlayer = %target.player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Nothing to hunt... random movement...
|
||||||
|
if(!%zombie.hasTarget) {
|
||||||
|
%zombie.zombieRmove = schedule(%zombie.updateTimeFrequency, %zombie, "TWM2Lib_Zombie_Core", "zRandomMoveLoop", %zombie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
%datablock.schedule(%zombie.updateTimeFrequency, "AI", %zombie);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LordZombieArmor::Move(%datablock, %zombie) {
|
||||||
|
if(!isObject(%zombie) || %zombie.getState() $= "dead") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
%upvec = "150";
|
%upvec = "150";
|
||||||
if(%closestDistance <= $zombie::LKillDist && %zombie.canjump == 1){
|
%pos = %zombie.getWorldBoxCenter();
|
||||||
%vec = vectoradd(%pos,vectorScale(%vector,($zombie::LkillDist - 1.6)));
|
%chance = (getrandom() * 20);
|
||||||
%mask = $TypeMasks::InteriorObjectType | $TypeMasks::StaticShapeObjectType | $TypeMasks::ForceFieldObjectType | $TypeMasks::VehicleObjectType | $TypeMasks::TerrainObjectType | $TypeMasks::PlayerObjectType;
|
%mult = 1;
|
||||||
%searchResult = containerRayCast(vectoradd(%pos,vectorScale(%vector,1.6)), %vec, %mask, %zombie);
|
if(%chance >= 19) {
|
||||||
if(%searchResult){
|
serverPlay3d("ZombieMoan", %zombie.getWorldBoxCenter());
|
||||||
%searchObj = getWord(%searchResult,0);
|
|
||||||
if(%searchObj $= %closestClient){
|
|
||||||
%chance = getrandom(1,5);
|
|
||||||
if(%chance == 1){
|
|
||||||
%dir = %zombie.getEyeVector();
|
|
||||||
%dir = vectornormalize(getword(%dir,0)@" "@getword(%dir,1)@" 0");
|
|
||||||
%dir = vectoradd(vectorscale(%dir,7500),"0 0 1000");
|
|
||||||
%closestclient.applyimpulse(%clpos,%dir);
|
|
||||||
%closestclient.damage(0, %clpos, 0.8, $DamageType::ZombieL);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
%zombie.setvelocity("0 0 0");
|
|
||||||
%zombie.canjump = 0;
|
|
||||||
schedule(1000, %zombie, "Zsetjump", %zombie);
|
|
||||||
Llifttarget(%zombie,%closestclient,0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if(%closestDistance <= ($zombie::LKillDist + $zombie::lungDist)){
|
if(%zombie.movingToPosition) {
|
||||||
%zombie.setvelocity("0 0 0");
|
%vector = TWM2Lib_Zombie_Core("zombieGetFacingDirection", %zombie, %zombie.movePoint);
|
||||||
%fmultiplier = (%fmultiplier * 2.5);
|
|
||||||
}
|
}
|
||||||
%vector = vectorscale(%vector, %Fmultiplier);
|
else {
|
||||||
%x = Getword(%vector,0);
|
%vector = TWM2Lib_Zombie_Core("zombieGetFacingDirection", %zombie, %zombie.targetedPlayer.getPosition());
|
||||||
%y = Getword(%vector,1);
|
if(vectorDist(%pos, %zombie.targetedPlayer.getPosition()) <= ($Zombie::LordGrabDistance + $Zombie::LungeDistance)) {
|
||||||
%z = Getword(%vector,2);
|
%zombie.setvelocity("0 0 0");
|
||||||
if(%z >= 4000)
|
%mult = 2.5;
|
||||||
%upvec = (%upvec * 5);
|
}
|
||||||
|
}
|
||||||
|
//Scale to speed
|
||||||
|
%vector = vectorScale(vectorScale(%vector, %zombie.speed), $Zombie::SpeedMultiplier[%zombie.type]);
|
||||||
|
%vector = vectorScale(%vector, %mult);
|
||||||
|
%x = getWord(%vector, 0);
|
||||||
|
%y = getWord(%vector, 1);
|
||||||
%vector = %x@" "@%y@" "@%upvec;
|
%vector = %x@" "@%y@" "@%upvec;
|
||||||
%zombie.applyImpulse(%pos, %vector);
|
%zombie.applyImpulse(%pos, %vector);
|
||||||
%zombie.ATCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(%zombie.hastarget == 1 && %zombie.canmove != 0){
|
|
||||||
%zombie.hastarget = 0;
|
|
||||||
%zombie.zombieRmove = schedule(100, %zombie, "ZSetRandomMove", %zombie);
|
|
||||||
}
|
|
||||||
%zombie.moveloop = schedule(500, %zombie, "LZombiemovetotarget", %zombie);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ZFire(%zombie, %target){
|
function LordZombieArmor::zFire(%datablock, %zombie, %targetObject) {
|
||||||
if(isobject(%zombie) && isobject(%target)){
|
if(!isObject(%zombie) || %zombie.getState() $= "dead") {
|
||||||
if(%Zombie.firstFired == 1){
|
return;
|
||||||
%vec = vectorsub(%target.getworldboxcenter(),%zombie.getMuzzlePoint(6));
|
}
|
||||||
%vec = vectoradd(%vec, vectorscale(%target.getvelocity(),vectorlen(%vec)/100));
|
if(!isObject(%targetObject)) {
|
||||||
%zombie.firstFired = 0;
|
return;
|
||||||
%zombie.nomove = 0;
|
}
|
||||||
%p = new TracerProjectile()
|
TWM2Lib_Zombie_Core("setZFlag", %zombie, "firingWeapon", 1);
|
||||||
{
|
TWM2Lib_Zombie_Core("setZFlag", %zombie, "canFireWeapon", 0);
|
||||||
dataBlock = LZombieAcidBall;
|
%vec = vectorsub(%targetObject.getWorldBoxCenter(), %zombie.getMuzzlePoint(7));
|
||||||
initialDirection = %vec;
|
%vec = vectoradd(%vec, vectorScale(%targetObject.getVelocity(), vectorlen(%vec)/100));
|
||||||
initialPosition = %zombie.getMuzzlePoint(6);
|
%p = new LinearFlareProjectile() {
|
||||||
sourceObject = %zombie;
|
dataBlock = ZLPhotonCannonProjectile;
|
||||||
sourceSlot = 6;
|
initialDirection = %vec;
|
||||||
};
|
initialPosition = %zombie.getMuzzlePoint(7);
|
||||||
}
|
sourceObject = %zombie;
|
||||||
else{
|
sourceSlot = 7;
|
||||||
%vec = vectorsub(%target.getworldboxcenter(),%zombie.getMuzzlePoint(5));
|
};
|
||||||
%vec = vectoradd(%vec, vectorscale(%target.getvelocity(),vectorlen(%vec)/100));
|
MissionCleanup.add(%p);
|
||||||
%p = new TracerProjectile()
|
schedule(2000, 0, "TWM2Lib_Zombie_Core", "setZFlag", %zombie, "firingWeapon", 0);
|
||||||
{
|
schedule($Zombie::ZombieLordPhotonCooldown, 0, "TWM2Lib_Zombie_Core", "setZFlag", %zombie, "canFireWeapon", 1);
|
||||||
dataBlock = LZombieAcidBall;
|
|
||||||
initialDirection = %vec;
|
|
||||||
initialPosition = %zombie.getMuzzlePoint(5);
|
|
||||||
sourceObject = %zombie;
|
|
||||||
sourceSlot = 5;
|
|
||||||
};
|
|
||||||
%zombie.firstFired = 1;
|
|
||||||
%zombie.Fire = schedule(250, %zombie, "ZFire", %zombie, %target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
%zombie.firstFired = 0;
|
|
||||||
%zombie.nomove = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Llifttarget(%zombie,%closestclient,%count){
|
function LordZombieArmor::makeShield(%datablock, %zombie) {
|
||||||
if(%count == 0)
|
if(isObject(%zombie) || %zombie.getState() $= "dead") {
|
||||||
%zombie.canmove = 0;
|
return false;
|
||||||
if(%closestclient.getState() $= "dead" || %Zombie.getState() $= "dead"){
|
|
||||||
%zombie.canmove = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
%target = %closestclient;
|
|
||||||
if(!isobject(%target)){
|
|
||||||
%zombie.canmove = 1;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
%pos = %zombie.getworldboxcenter();
|
|
||||||
%Tpos = %target.getworldboxcenter();
|
|
||||||
%ZtoT = vectorsub(%tpos,%pos);
|
|
||||||
if (%count <= 12){
|
|
||||||
%newpos = vectoradd(%ZtoT,vectoradd(%pos,"0 0 -0.6"));
|
|
||||||
%target.setTransform(%newpos);
|
|
||||||
%target.setvelocity("0 0 0");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
%killtype = getrandom(1,2);
|
|
||||||
if(%killtype == 1){
|
|
||||||
%closestwall = 20;
|
|
||||||
%nv2 = (getword(%ZtoT, 0) * -1);
|
|
||||||
%nv1 = getword(%ZtoT, 1);
|
|
||||||
%vector1 = vectorscale(vectornormalize(%nv1@" "@%nv2@" 0"),20);
|
|
||||||
%nvector1 = vectoradd(%tpos,%vector1);
|
|
||||||
%nv2 = getword(%ZtoT, 0);
|
|
||||||
%nv1 = (getword(%ZtoT, 1) * -1);
|
|
||||||
%vector2 = vectorscale(vectornormalize(%nv1@" "@%nv2@" 0"),20);
|
|
||||||
%nvector2 = vectoradd(%tpos,%vector2);
|
|
||||||
%searchresultR = containerRayCast(%tpos, %nvector1, $TypeMasks::StaticShapeObjectType | $TypeMasks::InteriorObjectType | $TypeMasks::ForceFieldObjectType | $TypeMasks::VehicleObjectType);
|
|
||||||
%searchresultL = containerRayCast(%tpos, %nvector2, $TypeMasks::StaticShapeObjectType | $TypeMasks::InteriorObjectType | $TypeMasks::ForceFieldObjectType | $TypeMasks::VehicleObjectType);
|
|
||||||
if(%searchresultR){
|
|
||||||
%Hpos = getword(%searchresultR,1)@" "@getword(%searchresultR,2)@" "@getword(%searchresultR,3);
|
|
||||||
%distance = vectordist(%Tpos, %Hpos);
|
|
||||||
if(%distance <= %closestwall){
|
|
||||||
%closestwall = %distance;
|
|
||||||
%vector = vectoradd(vectorscale(%vector1,1500),"0 0 100");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(%searchresultL){
|
|
||||||
%Hpos = getword(%searchresultL,1)@" "@getword(%searchresultL,2)@" "@getword(%searchresultL,3);
|
|
||||||
%distance = vectordist(%Tpos, %Hpos);
|
|
||||||
if(%distance <= %closestwall){
|
|
||||||
%closestwall = %distance;
|
|
||||||
%vector = vectoradd(vectorscale(%vector2,1500),"0 0 100");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(%closestwall == 20){
|
|
||||||
%x = getword(%ZtoT, 0);
|
|
||||||
%y = getword(%ZtoT, 1);
|
|
||||||
%outvec = vectorscale(vectornormalize(%x@" "@%y@" 0"),50);
|
|
||||||
%vector = vectoradd("0 0 -15000",%outvec);
|
|
||||||
}
|
|
||||||
%target.applyimpulse(%target.getposition(),%vector);
|
|
||||||
}
|
}
|
||||||
else if(%killtype == 2){
|
if(!%zombie.canShield) {
|
||||||
%target.infected = 1;
|
return false;
|
||||||
%target.damage(0, %target.getposition(), 10.0, $DamageType::ZombieL);
|
|
||||||
}
|
}
|
||||||
%count = 0;
|
%zPos = %zombie.getPosition();
|
||||||
%zombie.canmove = 1;
|
%eyePoint = posFromTransform(%zombie.getEyeTransform());
|
||||||
return;
|
%eVec = vectorNormalize(%zombie.getEyeVector());
|
||||||
}
|
//Raycast out 25m from the eye line and make sure nothing's blocking us...
|
||||||
%count++;
|
%shieldPos = vectorAdd(%zPos, vectorScale(%eVec, 25));
|
||||||
%zombie.killingplayer = schedule(150, %zombie, "Llifttarget", %zombie, %closestclient, %count);
|
%rInfo = containerRaycast(%zPos, %shieldPos, $AllObjMask, %zombie);
|
||||||
|
if(getWord(%rInfo, 0) != 0 && isObject(getWord(%rInfo, 0))) {
|
||||||
|
//Something is blocking our view, cancel shield.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//Next, find the first object we hit below the point that can serve as the shield base.
|
||||||
|
%mask = $TypeMasks::TerrainObjectType | $TypeMasks::InteriorObjectType | $TypeMasks::StaticShapeObjectType;
|
||||||
|
%tH = getTerrainHeight(%shieldPos);
|
||||||
|
%shieldSpawnPos = getWord(%shieldPos, 0) SPC getWord(%shieldPos, 1) spc %tH;
|
||||||
|
%lookVector = vectorSub(%shieldSpawnPos, %eyePoint);
|
||||||
|
%searchResult = containerRayCast(%eyePoint, %lookVector, %mask, 0);
|
||||||
|
if(getWord(%searchResult, 0) == 0) {
|
||||||
|
//Nothing to mount to.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
//Fetch some important parameters
|
||||||
|
%terrPt = posFromRaycast(%searchResult);
|
||||||
|
%terrNrm = normalFromRaycast(%searchResult);
|
||||||
|
%intAngle = getTerrainAngle(%terrNrm); // getTerrainAngle() function found in staticShape.cs
|
||||||
|
%rotAxis = vectorNormalize(vectorCross(%terrNrm, "0 0 1"));
|
||||||
|
if (getWord(%terrNrm, 2) == 1 || getWord(%terrNrm, 2) == -1) {
|
||||||
|
%rotAxis = vectorNormalize(vectorCross(%terrNrm, "0 1 0"));
|
||||||
|
}
|
||||||
|
%rotation = %rotAxis @ " " @ %intAngle;
|
||||||
|
//We've got it all, deploy...
|
||||||
|
%zombie.usingShield = 1;
|
||||||
|
%zombie.shieldHealth = $Zombie::ZombieLordShieldHealth;
|
||||||
|
%zombie.shieldEnergy = $Zombie::ZombieLordShieldEnergy;
|
||||||
|
TWM2Lib_Zombie_Core("setZFlag", %zombie, "canShield", 0);
|
||||||
|
//Create the shield
|
||||||
|
%shieldObject = new (ForceFieldBare)() {
|
||||||
|
dataBlock = ZombieLordDefensiveBarrier;
|
||||||
|
scale = "1 1 1";
|
||||||
|
rotation = %rotation;
|
||||||
|
};
|
||||||
|
MissionCleanup.add(%shieldObject);
|
||||||
|
%zombie.shieldObject = %shieldObject;
|
||||||
|
//Grow it over 3 seconds...
|
||||||
|
%shieldObject.schedule(500, setRealSize, "3 1 3");
|
||||||
|
%shieldObject.schedule(500, setTransform, %shieldObject.getTransform());
|
||||||
|
%shieldObject.schedule(1000, setRealSize, "6 1 6");
|
||||||
|
%shieldObject.schedule(1000, setTransform, %shieldObject.getTransform());
|
||||||
|
%shieldObject.schedule(1500, setRealSize, "8 1 8");
|
||||||
|
%shieldObject.schedule(1500, setTransform, %shieldObject.getTransform());
|
||||||
|
%shieldObject.schedule(2000, setRealSize, "10 1 10");
|
||||||
|
%shieldObject.schedule(2000, setTransform, %shieldObject.getTransform());
|
||||||
|
%shieldObject.schedule(2500, setRealSize, "12.5 1 12.5");
|
||||||
|
%shieldObject.schedule(2500, setTransform, %shieldObject.getTransform());
|
||||||
|
%shieldObject.schedule(3000, setRealSize, "15 1 15");
|
||||||
|
%shieldObject.schedule(3000, setTransform, %shieldObject.getTransform());
|
||||||
|
}
|
||||||
|
|
||||||
|
function LordZombieArmor::zLordLift(%datablock, %zombie, %target, %count) {
|
||||||
|
if(%count == 0) {
|
||||||
|
%zombie.setMoveState(true);
|
||||||
|
}
|
||||||
|
if(%target.getState() $= "dead" || %zombie.getState() $= "dead"){
|
||||||
|
%zombie.setMoveState(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!isobject(%target)) {
|
||||||
|
%zombie.setMoveState(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
%pos = %zombie.getWorldBoxCenter();
|
||||||
|
%Tpos = %target.getWorldBoxCenter();
|
||||||
|
%ZtoT = vectorsub(%tpos, %pos);
|
||||||
|
if (%count <= 12) {
|
||||||
|
%newpos = vectoradd(%ZtoT, vectoradd(%pos, "0 0 -0.6"));
|
||||||
|
%target.setTransform(%newpos);
|
||||||
|
%target.setvelocity("0 0 0");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
%killtype = getrandom(1, 2);
|
||||||
|
if(%killtype == 1) {
|
||||||
|
%closestwall = 20;
|
||||||
|
%nv2 = (getword(%ZtoT, 0) * -1);
|
||||||
|
%nv1 = getword(%ZtoT, 1);
|
||||||
|
%vector1 = vectorscale(vectornormalize(%nv1@" "@%nv2@" 0"), 20);
|
||||||
|
%nvector1 = vectoradd(%tpos, %vector1);
|
||||||
|
%nv2 = getword(%ZtoT, 0);
|
||||||
|
%nv1 = (getword(%ZtoT, 1) * -1);
|
||||||
|
%vector2 = vectorscale(vectornormalize(%nv1@" "@%nv2@" 0"), 20);
|
||||||
|
%nvector2 = vectoradd(%tpos, %vector2);
|
||||||
|
%searchresultR = containerRayCast(%tpos, %nvector1, $TypeMasks::StaticShapeObjectType | $TypeMasks::InteriorObjectType | $TypeMasks::ForceFieldObjectType | $TypeMasks::VehicleObjectType);
|
||||||
|
%searchresultL = containerRayCast(%tpos, %nvector2, $TypeMasks::StaticShapeObjectType | $TypeMasks::InteriorObjectType | $TypeMasks::ForceFieldObjectType | $TypeMasks::VehicleObjectType);
|
||||||
|
if(%searchresultR) {
|
||||||
|
%Hpos = getword(%searchresultR, 1)@" "@getword(%searchresultR, 2)@" "@getword(%searchresultR, 3);
|
||||||
|
%distance = vectordist(%Tpos, %Hpos);
|
||||||
|
if(%distance <= %closestwall){
|
||||||
|
%closestwall = %distance;
|
||||||
|
%vector = vectoradd(vectorscale(%vector1, 1500), "0 0 100");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(%searchresultL) {
|
||||||
|
%Hpos = getword(%searchresultL, 1)@" "@getword(%searchresultL, 2)@" "@getword(%searchresultL, 3);
|
||||||
|
%distance = vectordist(%Tpos, %Hpos);
|
||||||
|
if(%distance <= %closestwall) {
|
||||||
|
%closestwall = %distance;
|
||||||
|
%vector = vectoradd(vectorscale(%vector2, 1500), "0 0 100");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(%closestwall == 20) {
|
||||||
|
%x = getword(%ZtoT, 0);
|
||||||
|
%y = getword(%ZtoT, 1);
|
||||||
|
%outvec = vectorscale(vectornormalize(%x@" "@%y@" 0"), 50);
|
||||||
|
%vector = vectoradd("0 0 -15000", %outvec);
|
||||||
|
}
|
||||||
|
%target.applyimpulse(%target.getposition(), %vector);
|
||||||
|
}
|
||||||
|
else if(%killtype == 2) {
|
||||||
|
%target.infected = 1;
|
||||||
|
%target.damage(0, %target.getposition(), 10.0, $DamageType::ZombieL);
|
||||||
|
}
|
||||||
|
%count = 0;
|
||||||
|
%zombie.setMoveState(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
%count++;
|
||||||
|
%zombie.killingplayer = %datablock.schedule(150, "zLordLift", %zombie, %closestclient, %count);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue