mirror of
https://github.com/ZDoom/gzdoom.git
synced 2024-11-27 22:33:17 +00:00
823 lines
17 KiB
Text
823 lines
17 KiB
Text
class PowerupGiver : Inventory native
|
|
{
|
|
|
|
native Class<Actor> PowerupType;
|
|
native int EffectTics; // Non-0 to override the powerup's default tics
|
|
native color BlendColor; // Non-0 to override the powerup's default blend
|
|
native Name Mode; // Meaning depends on powerup - used for Invulnerability and Invisibility
|
|
native double Strength; // Meaning depends on powerup - currently used only by Invisibility
|
|
|
|
Default
|
|
{
|
|
Inventory.DefMaxAmount;
|
|
+INVENTORY.INVBAR
|
|
+INVENTORY.FANCYPICKUPSOUND
|
|
Inventory.PickupSound "misc/p_pkup";
|
|
}
|
|
}
|
|
|
|
class Powerup : Inventory native
|
|
{
|
|
native int EffectTics;
|
|
native color BlendColor;
|
|
native Name Mode; // Meaning depends on powerup - used for Invulnerability and Invisibility
|
|
native double Strength; // Meaning depends on powerup - currently used only by Invisibility
|
|
|
|
// Note, that while this is an inventory flag, it only has meaning on an active powerup.
|
|
override bool GetNoTeleportFreeze() { return bNoTeleportFreeze; }
|
|
|
|
native virtual void InitEffect();
|
|
native virtual void EndEffect();
|
|
native bool isBlinking();
|
|
|
|
}
|
|
|
|
class PowerInvulnerable : Powerup native
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -30;
|
|
inventory.icon "SPSHLD0";
|
|
}
|
|
}
|
|
|
|
class PowerStrength : Powerup native
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration 1;
|
|
Powerup.Color "ff 00 00", 0.5;
|
|
+INVENTORY.HUBPOWER
|
|
}
|
|
}
|
|
|
|
class PowerInvisibility : Powerup native
|
|
{
|
|
Default
|
|
{
|
|
+SHADOW;
|
|
Powerup.Duration -60;
|
|
Powerup.Strength 80;
|
|
Powerup.Mode "Fuzzy";
|
|
}
|
|
}
|
|
|
|
class PowerGhost : PowerInvisibility
|
|
{
|
|
Default
|
|
{
|
|
+GHOST;
|
|
Powerup.Duration -60;
|
|
Powerup.Strength 60;
|
|
Powerup.Mode "None";
|
|
}
|
|
}
|
|
|
|
class PowerShadow : PowerInvisibility
|
|
{
|
|
Default
|
|
{
|
|
+INVENTORY.HUBPOWER
|
|
Powerup.Duration -55;
|
|
Powerup.Strength 75;
|
|
Powerup.Mode "Cumulative";
|
|
}
|
|
}
|
|
|
|
class PowerIronFeet : Powerup native
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -60;
|
|
Powerup.Color "00 ff 00", 0.125;
|
|
}
|
|
}
|
|
|
|
class PowerMask : PowerIronFeet native
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -80;
|
|
Powerup.Color "00 00 00", 0;
|
|
+INVENTORY.HUBPOWER
|
|
Inventory.Icon "I_MASK";
|
|
}
|
|
}
|
|
|
|
class PowerLightAmp : Powerup native
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -120;
|
|
}
|
|
}
|
|
|
|
class PowerTorch : PowerLightAmp native {}
|
|
|
|
class PowerFlight : Powerup native
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -60;
|
|
+INVENTORY.HUBPOWER
|
|
}
|
|
}
|
|
|
|
class PowerWeaponLevel2 : Powerup native
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -40;
|
|
Inventory.Icon "SPINBK0";
|
|
+INVENTORY.NOTELEPORTFREEZE
|
|
}
|
|
}
|
|
|
|
class PowerSpeed : Powerup native
|
|
{
|
|
native int SpeedFlags;
|
|
|
|
Default
|
|
{
|
|
Powerup.Duration -45;
|
|
Speed 1.5;
|
|
Inventory.Icon "SPBOOT0";
|
|
+INVENTORY.NOTELEPORTFREEZE
|
|
}
|
|
|
|
override double GetSpeedFactor() { return Speed; }
|
|
}
|
|
|
|
// Player Speed Trail (used by the Speed Powerup) ----------------------------
|
|
|
|
class PlayerSpeedTrail : Actor
|
|
{
|
|
Default
|
|
{
|
|
+NOBLOCKMAP
|
|
+NOGRAVITY
|
|
Alpha 0.6;
|
|
RenderStyle "Translucent";
|
|
}
|
|
|
|
override void Tick()
|
|
{
|
|
Alpha -= .6 / 8;
|
|
if (Alpha <= 0)
|
|
{
|
|
Destroy ();
|
|
}
|
|
}
|
|
}
|
|
|
|
class PowerMinotaur : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -25;
|
|
Inventory.Icon "SPMINO0";
|
|
}
|
|
}
|
|
|
|
class PowerTargeter : Powerup native
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -160;
|
|
+INVENTORY.HUBPOWER
|
|
}
|
|
States
|
|
{
|
|
Targeter:
|
|
TRGT A -1;
|
|
Stop;
|
|
TRGT B -1;
|
|
Stop;
|
|
TRGT C -1;
|
|
Stop;
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// Frightener
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerFrightener : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -60;
|
|
}
|
|
|
|
override void InitEffect ()
|
|
{
|
|
Super.InitEffect();
|
|
|
|
if (Owner== null || Owner.player == null)
|
|
return;
|
|
|
|
Owner.player.cheats |= CF_FRIGHTENING;
|
|
}
|
|
|
|
override void EndEffect ()
|
|
{
|
|
Super.EndEffect();
|
|
|
|
if (Owner== null || Owner.player == null)
|
|
return;
|
|
|
|
Owner.player.cheats &= ~CF_FRIGHTENING;
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// Buddha
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerBuddha : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -60;
|
|
}
|
|
|
|
override void InitEffect ()
|
|
{
|
|
Super.InitEffect();
|
|
|
|
if (Owner== null || Owner.player == null)
|
|
return;
|
|
|
|
Owner.player.cheats |= CF_BUDDHA;
|
|
}
|
|
|
|
override void EndEffect ()
|
|
{
|
|
Super.EndEffect();
|
|
|
|
if (Owner== null || Owner.player == null)
|
|
return;
|
|
|
|
Owner.player.cheats &= ~CF_BUDDHA;
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// Scanner (this is active just by being present)
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerScanner : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -80;
|
|
+INVENTORY.HUBPOWER
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// TimeFreezer
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerTimeFreezer : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -12;
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// InitEffect
|
|
//
|
|
//===========================================================================
|
|
|
|
override void InitEffect()
|
|
{
|
|
int freezemask;
|
|
|
|
Super.InitEffect();
|
|
|
|
if (Owner == null || Owner.player == null)
|
|
return;
|
|
|
|
// When this powerup is in effect, pause the music.
|
|
S_PauseSound(false, false);
|
|
|
|
// Give the player and his teammates the power to move when time is frozen.
|
|
freezemask = 1 << Owner.PlayerNumber();
|
|
Owner.player.timefreezer |= freezemask;
|
|
for (int i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (playeringame[i] &&
|
|
players[i].mo != null &&
|
|
players[i].mo.IsTeammate(Owner)
|
|
)
|
|
{
|
|
players[i].timefreezer |= freezemask;
|
|
}
|
|
}
|
|
|
|
// [RH] The effect ends one tic after the counter hits zero, so make
|
|
// sure we start at an odd count.
|
|
EffectTics += !(EffectTics & 1);
|
|
if ((EffectTics & 1) == 0)
|
|
{
|
|
EffectTics++;
|
|
}
|
|
// Make sure the effect starts and ends on an even tic.
|
|
if ((level.time & 1) == 0)
|
|
{
|
|
level.frozen = true;;
|
|
}
|
|
else
|
|
{
|
|
// Compensate for skipped tic, but beware of overflow.
|
|
if(EffectTics < 0x7fffffff)
|
|
EffectTics++;
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// APowerTimeFreezer :: DoEffect
|
|
//
|
|
//===========================================================================
|
|
|
|
override void DoEffect()
|
|
{
|
|
Super.DoEffect();
|
|
// [RH] Do not change LEVEL_FROZEN on odd tics, or the Revenant's tracer
|
|
// will get thrown off.
|
|
// [ED850] Don't change it if the player is predicted either.
|
|
if (level.time & 1 || (Owner != null && Owner.player != null && Owner.player.cheats & CF_PREDICTING))
|
|
{
|
|
return;
|
|
}
|
|
// [RH] The "blinking" can't check against EffectTics exactly or it will
|
|
// never happen, because InitEffect ensures that EffectTics will always
|
|
// be odd when level.time is even.
|
|
level.frozen = ( EffectTics > 4*32
|
|
|| (( EffectTics > 3*32 && EffectTics <= 4*32 ) && ((EffectTics + 1) & 15) != 0 )
|
|
|| (( EffectTics > 2*32 && EffectTics <= 3*32 ) && ((EffectTics + 1) & 7) != 0 )
|
|
|| (( EffectTics > 32 && EffectTics <= 2*32 ) && ((EffectTics + 1) & 3) != 0 )
|
|
|| (( EffectTics > 0 && EffectTics <= 1*32 ) && ((EffectTics + 1) & 1) != 0 ));
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// APowerTimeFreezer :: EndEffect
|
|
//
|
|
//===========================================================================
|
|
|
|
override void EndEffect()
|
|
{
|
|
Super.EndEffect();
|
|
|
|
// If there is an owner, remove the timefreeze flag corresponding to
|
|
// her from all players.
|
|
if (Owner != null && Owner.player != null)
|
|
{
|
|
int freezemask = ~(1 << Owner.PlayerNumber());
|
|
for (int i = 0; i < MAXPLAYERS; ++i)
|
|
{
|
|
players[i].timefreezer &= freezemask;
|
|
}
|
|
}
|
|
|
|
// Are there any players who still have timefreezer bits set?
|
|
for (int i = 0; i < MAXPLAYERS; ++i)
|
|
{
|
|
if (playeringame[i] && players[i].timefreezer != 0)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// No, so allow other actors to move about freely once again.
|
|
level.frozen = false;
|
|
|
|
// Also, turn the music back on.
|
|
S_ResumeSound(false);
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// Damage
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerDamage : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -25;
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// InitEffect
|
|
//
|
|
//===========================================================================
|
|
|
|
override void InitEffect()
|
|
{
|
|
Super.InitEffect();
|
|
|
|
if (Owner != null)
|
|
{
|
|
Owner.A_PlaySound(SeeSound, CHAN_5, 1.0, false, ATTN_NONE);
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// EndEffect
|
|
//
|
|
//===========================================================================
|
|
|
|
override void EndEffect()
|
|
{
|
|
Super.EndEffect();
|
|
if (Owner != null)
|
|
{
|
|
Owner.A_PlaySound(DeathSound, CHAN_5, 1.0, false, ATTN_NONE);
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// ModifyDamage
|
|
//
|
|
//===========================================================================
|
|
|
|
override void ModifyDamage(int damage, Name damageType, out int newdamage, bool passive)
|
|
{
|
|
if (!passive && damage > 0)
|
|
{
|
|
newdamage = max(1, ApplyDamageFactors(GetClass(), damageType, damage, damage * 4));
|
|
if (Owner != null && newdamage > damage) Owner.A_PlaySound(ActiveSound, CHAN_AUTO, 1.0, false, ATTN_NONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// Protection
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerProtection : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -25;
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// InitEffect
|
|
//
|
|
//===========================================================================
|
|
|
|
override void InitEffect()
|
|
{
|
|
Super.InitEffect();
|
|
|
|
let o = Owner; // copy to a local variable for quicker access.
|
|
if (o != null)
|
|
{
|
|
o.A_PlaySound(SeeSound, CHAN_AUTO, 1.0, false, ATTN_NONE);
|
|
|
|
// Transfer various protection flags if owner does not already have them.
|
|
// If the owner already has the flag, clear it from the powerup.
|
|
// If the powerup still has a flag set, add it to the owner.
|
|
bNoRadiusDmg &= !o.bNoRadiusDmg;
|
|
o.bNoRadiusDmg |= bNoRadiusDmg;
|
|
|
|
bDontMorph &= !o.bDontMorph;
|
|
o.bDontMorph |= bDontMorph;
|
|
|
|
bDontSquash &= !o.bDontSquash;
|
|
o.bDontSquash |= bDontSquash;
|
|
|
|
bDontBlast &= !o.bDontBlast;
|
|
o.bDontBlast |= bDontBlast;
|
|
|
|
bNoTeleOther &= !o.bNoTeleOther;
|
|
o.bNoTeleOther |= bNoTeleOther;
|
|
|
|
bNoPain &= !o.bNoPain;
|
|
o.bNoPain |= bNoPain;
|
|
|
|
bDontRip &= !o.bDontRip;
|
|
o.bDontRip |= bDontRip;
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// EndEffect
|
|
//
|
|
//===========================================================================
|
|
|
|
override void EndEffect()
|
|
{
|
|
Super.EndEffect();
|
|
let o = Owner; // copy to a local variable for quicker access.
|
|
if (o != null)
|
|
{
|
|
o.A_PlaySound(DeathSound, CHAN_AUTO, 1.0, false, ATTN_NONE);
|
|
|
|
o.bNoRadiusDmg &= !bNoRadiusDmg;
|
|
o.bDontMorph &= !bDontMorph;
|
|
o.bDontSquash &= !bDontSquash;
|
|
o.bDontBlast &= !bDontBlast;
|
|
o.bNoTeleOther &= !bNoTeleOther;
|
|
o.bNoPain &= !bNoPain;
|
|
o.bDontRip &= !bDontRip;
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// AbsorbDamage
|
|
//
|
|
//===========================================================================
|
|
|
|
override void ModifyDamage(int damage, Name damageType, out int newdamage, bool passive)
|
|
{
|
|
if (passive && damage > 0)
|
|
{
|
|
newdamage = max(1, ApplyDamageFactors(GetClass(), damageType, damage, damage / 4));
|
|
if (Owner != null && newdamage < damage) Owner.A_PlaySound(ActiveSound, CHAN_AUTO, 1.0, false, ATTN_NONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// Drain
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerDrain : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -60;
|
|
}
|
|
|
|
override void InitEffect()
|
|
{
|
|
Super.InitEffect();
|
|
|
|
if (Owner!= null && Owner.player != null)
|
|
{
|
|
// Give the player the power to drain life from opponents when he damages them.
|
|
Owner.player.cheats |= CF_DRAIN;
|
|
}
|
|
}
|
|
|
|
override void EndEffect()
|
|
{
|
|
Super.EndEffect();
|
|
|
|
// Nothing to do if there's no owner.
|
|
if (Owner!= null && Owner.player != null)
|
|
{
|
|
// Take away the drain power.
|
|
Owner.player.cheats &= ~CF_DRAIN;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// Regeneration
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerRegeneration : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -120;
|
|
Powerup.Strength 5;
|
|
}
|
|
|
|
override void DoEffect()
|
|
{
|
|
Super.DoEffect();
|
|
if (Owner != null && Owner.health > 0 && (level.time & 31) == 0)
|
|
{
|
|
if (Owner.GiveBody(int(Strength)))
|
|
{
|
|
Owner.A_PlaySound("*regenerate", CHAN_ITEM);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// HighJump
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerHighJump : Powerup
|
|
{
|
|
override void InitEffect()
|
|
{
|
|
Super.InitEffect();
|
|
|
|
if (Owner!= null && Owner.player != null)
|
|
{
|
|
// Give the player the power to jump much higher.
|
|
Owner.player.cheats |= CF_HIGHJUMP;
|
|
}
|
|
}
|
|
|
|
override void EndEffect()
|
|
{
|
|
Super.EndEffect();
|
|
|
|
// Nothing to do if there's no owner.
|
|
if (Owner!= null && Owner.player != null)
|
|
{
|
|
// Take away the high jump power.
|
|
Owner.player.cheats &= ~CF_HIGHJUMP;
|
|
}
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// DoubleFiringSpeed
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerDoubleFiringSpeed : Powerup
|
|
{
|
|
override void InitEffect()
|
|
{
|
|
Super.InitEffect();
|
|
|
|
if (Owner!= null && Owner.player != null)
|
|
{
|
|
// Give the player the power to shoot twice as fast.
|
|
Owner.player.cheats |= CF_DOUBLEFIRINGSPEED;
|
|
}
|
|
}
|
|
|
|
override void EndEffect()
|
|
{
|
|
Super.EndEffect();
|
|
|
|
// Nothing to do if there's no owner.
|
|
if (Owner!= null && Owner.player != null)
|
|
{
|
|
// Take away the shooting twice as fast power.
|
|
Owner.player.cheats &= ~CF_DOUBLEFIRINGSPEED;
|
|
}
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// InfiniteAmmo
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerInfiniteAmmo : Powerup
|
|
{
|
|
Default
|
|
{
|
|
Powerup.Duration -30;
|
|
}
|
|
|
|
override void InitEffect()
|
|
{
|
|
Super.InitEffect();
|
|
|
|
if (Owner!= null && Owner.player != null)
|
|
{
|
|
// Give the player infinite ammo
|
|
Owner.player.cheats |= CF_INFINITEAMMO;
|
|
}
|
|
}
|
|
|
|
override void EndEffect()
|
|
{
|
|
Super.EndEffect();
|
|
|
|
// Nothing to do if there's no owner.
|
|
if (Owner!= null && Owner.player != null)
|
|
{
|
|
// Take away the limitless ammo
|
|
Owner.player.cheats &= ~CF_INFINITEAMMO;
|
|
}
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// PowerMorph
|
|
//
|
|
//===========================================================================
|
|
|
|
class PowerMorph : Powerup native
|
|
{
|
|
native Class<PlayerPawn> PlayerClass;
|
|
native Class<Actor> MorphFlash, UnMorphFlash;
|
|
native int MorphStyle;
|
|
native PlayerInfo MorphedPlayer;
|
|
|
|
Default
|
|
{
|
|
Powerup.Duration -40;
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// InitEffect
|
|
//
|
|
//===========================================================================
|
|
|
|
override void InitEffect()
|
|
{
|
|
Super.InitEffect();
|
|
|
|
if (Owner != null && Owner.player != null && PlayerClass != null)
|
|
{
|
|
let realplayer = Owner.player; // Remember the identity of the player
|
|
if (realplayer.MorphPlayer(realplayer, PlayerClass, 0x7fffffff/*INDEFINITELY*/, MorphStyle, MorphFlash, UnMorphFlash))
|
|
{
|
|
Owner = realplayer.mo; // Replace the new owner in our owner; safe because we are not attached to anything yet
|
|
bCreateCopyMoved = true; // Let the caller know the "real" owner has changed (to the morphed actor)
|
|
MorphedPlayer = realplayer; // Store the player identity (morphing clears the unmorphed actor's "player" field)
|
|
}
|
|
else // morph failed - give the caller an opportunity to fail the pickup completely
|
|
{
|
|
bInitEffectFailed = true; // Let the caller know that the activation failed (can fail the pickup if appropriate)
|
|
}
|
|
}
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// EndEffect
|
|
//
|
|
//===========================================================================
|
|
|
|
override void EndEffect()
|
|
{
|
|
Super.EndEffect();
|
|
|
|
// Abort if owner already destroyed or unmorphed
|
|
if (Owner == null || MorphedPlayer == null || Owner.alternative == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Abort if owner is dead; their Die() method will
|
|
// take care of any required unmorphing on death.
|
|
if (MorphedPlayer.health <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int savedMorphTics = MorphedPlayer.morphTics;
|
|
MorphedPlayer.UndoPlayerMorph (MorphedPlayer, 0, !!(MorphedPlayer.MorphStyle & MRF_UNDOALWAYS));
|
|
|
|
// Abort if unmorph failed; in that case,
|
|
// set the usual retry timer and return.
|
|
if (MorphedPlayer != null && MorphedPlayer.morphTics)
|
|
{
|
|
// Transfer retry timeout
|
|
// to the powerup's timer.
|
|
EffectTics = MorphedPlayer.morphTics;
|
|
// Reload negative morph tics;
|
|
// use actual value; it may
|
|
// be in use for animation.
|
|
MorphedPlayer.morphTics = savedMorphTics;
|
|
// Try again some time later
|
|
return;
|
|
}
|
|
// Unmorph suceeded
|
|
MorphedPlayer = null;
|
|
}
|
|
|
|
|
|
}
|
|
|