2006-02-24 04:48:15 +00:00
|
|
|
#include "actor.h"
|
|
|
|
#include "info.h"
|
|
|
|
#include "m_random.h"
|
|
|
|
#include "p_local.h"
|
|
|
|
#include "p_enemy.h"
|
|
|
|
#include "s_sound.h"
|
|
|
|
#include "a_doomglobal.h"
|
|
|
|
#include "statnums.h"
|
2008-06-22 09:13:19 +00:00
|
|
|
#include "thingdef/thingdef.h"
|
2006-02-24 04:48:15 +00:00
|
|
|
|
|
|
|
void A_Fire (AActor *); // from m_archvile.cpp
|
|
|
|
|
|
|
|
void A_BrainAwake (AActor *);
|
|
|
|
void A_BrainPain (AActor *);
|
|
|
|
void A_BrainScream (AActor *);
|
|
|
|
void A_BrainExplode (AActor *);
|
|
|
|
void A_BrainDie (AActor *);
|
|
|
|
void A_BrainSpit (AActor *);
|
|
|
|
void A_SpawnFly (AActor *);
|
|
|
|
void A_SpawnSound (AActor *);
|
|
|
|
|
|
|
|
static FRandom pr_brainscream ("BrainScream");
|
|
|
|
static FRandom pr_brainexplode ("BrainExplode");
|
|
|
|
static FRandom pr_spawnfly ("SpawnFly");
|
|
|
|
|
|
|
|
class ABossTarget : public AActor
|
|
|
|
{
|
|
|
|
DECLARE_STATELESS_ACTOR (ABossTarget, AActor)
|
|
|
|
public:
|
|
|
|
void BeginPlay ();
|
|
|
|
};
|
|
|
|
|
|
|
|
class DBrainState : public DThinker
|
|
|
|
{
|
|
|
|
DECLARE_CLASS (DBrainState, DThinker)
|
|
|
|
public:
|
|
|
|
DBrainState ()
|
|
|
|
: DThinker (STAT_BOSSTARGET),
|
|
|
|
Targets (STAT_BOSSTARGET),
|
|
|
|
SerialTarget (NULL),
|
|
|
|
Easy (false)
|
|
|
|
{}
|
|
|
|
void Serialize (FArchive &arc);
|
|
|
|
ABossTarget *GetTarget ();
|
|
|
|
protected:
|
|
|
|
TThinkerIterator<ABossTarget> Targets;
|
|
|
|
ABossTarget *SerialTarget;
|
|
|
|
bool Easy;
|
|
|
|
};
|
|
|
|
|
|
|
|
FState ABossBrain::States[] =
|
|
|
|
{
|
|
|
|
#define S_BRAINEXPLODE 0
|
|
|
|
S_BRIGHT (MISL, 'B', 10, NULL , &States[S_BRAINEXPLODE+1]),
|
|
|
|
S_BRIGHT (MISL, 'C', 10, NULL , &States[S_BRAINEXPLODE+2]),
|
|
|
|
S_BRIGHT (MISL, 'D', 10, A_BrainExplode , NULL),
|
|
|
|
|
|
|
|
#define S_BRAIN (S_BRAINEXPLODE+3)
|
|
|
|
S_NORMAL (BBRN, 'A', -1, NULL , NULL),
|
|
|
|
|
|
|
|
#define S_BRAIN_PAIN (S_BRAIN+1)
|
|
|
|
S_NORMAL (BBRN, 'B', 36, A_BrainPain , &States[S_BRAIN]),
|
|
|
|
|
|
|
|
#define S_BRAIN_DIE (S_BRAIN_PAIN+1)
|
|
|
|
S_NORMAL (BBRN, 'A', 100, A_BrainScream , &States[S_BRAIN_DIE+1]),
|
|
|
|
S_NORMAL (BBRN, 'A', 10, NULL , &States[S_BRAIN_DIE+2]),
|
|
|
|
S_NORMAL (BBRN, 'A', 10, NULL , &States[S_BRAIN_DIE+3]),
|
|
|
|
S_NORMAL (BBRN, 'A', -1, A_BrainDie , NULL)
|
|
|
|
};
|
|
|
|
|
|
|
|
IMPLEMENT_ACTOR (ABossBrain, Doom, 88, 0)
|
|
|
|
PROP_SpawnHealth (250)
|
|
|
|
//PROP_HeightFixed (86) // don't do this; it messes up some non-id levels
|
|
|
|
PROP_MassLong (10000000)
|
|
|
|
PROP_PainChance (255)
|
|
|
|
PROP_Flags (MF_SOLID|MF_SHOOTABLE)
|
|
|
|
PROP_Flags4 (MF4_NOICEDEATH)
|
2006-07-02 21:58:10 +00:00
|
|
|
PROP_Flags5 (MF5_OLDRADIUSDMG)
|
2006-02-24 04:48:15 +00:00
|
|
|
|
|
|
|
PROP_SpawnState (S_BRAIN)
|
|
|
|
PROP_PainState (S_BRAIN_PAIN)
|
|
|
|
PROP_DeathState (S_BRAIN_DIE)
|
|
|
|
|
|
|
|
PROP_PainSound ("brain/pain")
|
|
|
|
PROP_DeathSound ("brain/death")
|
|
|
|
END_DEFAULTS
|
|
|
|
|
|
|
|
class ABossEye : public AActor
|
|
|
|
{
|
|
|
|
DECLARE_ACTOR (ABossEye, AActor)
|
|
|
|
};
|
|
|
|
|
|
|
|
FState ABossEye::States[] =
|
|
|
|
{
|
|
|
|
#define S_BRAINEYE 0
|
|
|
|
S_NORMAL (SSWV, 'A', 10, A_Look , &States[S_BRAINEYE]),
|
|
|
|
|
|
|
|
#define S_BRAINEYESEE (S_BRAINEYE+1)
|
|
|
|
S_NORMAL (SSWV, 'A', 181, A_BrainAwake , &States[S_BRAINEYESEE+1]),
|
|
|
|
S_NORMAL (SSWV, 'A', 150, A_BrainSpit , &States[S_BRAINEYESEE+1])
|
|
|
|
};
|
|
|
|
|
|
|
|
IMPLEMENT_ACTOR (ABossEye, Doom, 89, 0)
|
|
|
|
PROP_HeightFixed (32)
|
|
|
|
PROP_Flags (MF_NOBLOCKMAP|MF_NOSECTOR)
|
|
|
|
|
|
|
|
PROP_SpawnState (S_BRAINEYE)
|
|
|
|
PROP_SeeState (S_BRAINEYESEE)
|
|
|
|
END_DEFAULTS
|
|
|
|
|
|
|
|
IMPLEMENT_STATELESS_ACTOR (ABossTarget, Doom, 87, 0)
|
|
|
|
PROP_HeightFixed (32)
|
|
|
|
PROP_Flags (MF_NOBLOCKMAP|MF_NOSECTOR)
|
|
|
|
END_DEFAULTS
|
|
|
|
|
|
|
|
void ABossTarget::BeginPlay ()
|
|
|
|
{
|
|
|
|
Super::BeginPlay ();
|
|
|
|
ChangeStatNum (STAT_BOSSTARGET);
|
|
|
|
}
|
|
|
|
|
|
|
|
class ASpawnShot : public AActor
|
|
|
|
{
|
|
|
|
DECLARE_ACTOR (ASpawnShot, AActor)
|
|
|
|
};
|
|
|
|
|
|
|
|
FState ASpawnShot::States[] =
|
|
|
|
{
|
|
|
|
S_BRIGHT (BOSF, 'A', 3, A_SpawnSound , &States[1]),
|
|
|
|
S_BRIGHT (BOSF, 'B', 3, A_SpawnFly , &States[2]),
|
|
|
|
S_BRIGHT (BOSF, 'C', 3, A_SpawnFly , &States[3]),
|
|
|
|
S_BRIGHT (BOSF, 'D', 3, A_SpawnFly , &States[0])
|
|
|
|
};
|
|
|
|
|
|
|
|
IMPLEMENT_ACTOR (ASpawnShot, Doom, -1, 0)
|
|
|
|
PROP_RadiusFixed (6)
|
|
|
|
PROP_HeightFixed (32)
|
|
|
|
PROP_SpeedFixed (10)
|
|
|
|
PROP_Damage (3)
|
|
|
|
PROP_Flags (MF_NOBLOCKMAP|MF_MISSILE|MF_DROPOFF|MF_NOGRAVITY|MF_NOCLIP)
|
|
|
|
PROP_Flags2 (MF2_NOTELEPORT)
|
|
|
|
PROP_Flags4 (MF4_RANDOMIZE)
|
|
|
|
|
|
|
|
PROP_SpawnState (0)
|
|
|
|
|
|
|
|
PROP_SeeSound ("brain/spit")
|
|
|
|
PROP_DeathSound ("brain/cubeboom")
|
|
|
|
END_DEFAULTS
|
|
|
|
|
|
|
|
class ASpawnFire : public AActor
|
|
|
|
{
|
|
|
|
DECLARE_ACTOR (ASpawnFire, AActor)
|
|
|
|
};
|
|
|
|
|
|
|
|
FState ASpawnFire::States[] =
|
|
|
|
{
|
|
|
|
S_BRIGHT (FIRE, 'A', 4, A_Fire , &States[1]),
|
|
|
|
S_BRIGHT (FIRE, 'B', 4, A_Fire , &States[2]),
|
|
|
|
S_BRIGHT (FIRE, 'C', 4, A_Fire , &States[3]),
|
|
|
|
S_BRIGHT (FIRE, 'D', 4, A_Fire , &States[4]),
|
|
|
|
S_BRIGHT (FIRE, 'E', 4, A_Fire , &States[5]),
|
|
|
|
S_BRIGHT (FIRE, 'F', 4, A_Fire , &States[6]),
|
|
|
|
S_BRIGHT (FIRE, 'G', 4, A_Fire , &States[7]),
|
|
|
|
S_BRIGHT (FIRE, 'H', 4, A_Fire , NULL)
|
|
|
|
};
|
|
|
|
|
|
|
|
IMPLEMENT_ACTOR (ASpawnFire, Doom, -1, 0)
|
|
|
|
PROP_HeightFixed (78)
|
|
|
|
PROP_Flags (MF_NOBLOCKMAP|MF_NOGRAVITY)
|
|
|
|
PROP_RenderStyle (STYLE_Add)
|
|
|
|
|
|
|
|
PROP_SpawnState (0)
|
|
|
|
END_DEFAULTS
|
|
|
|
|
|
|
|
void A_BrainAwake (AActor *self)
|
|
|
|
{
|
|
|
|
// killough 3/26/98: only generates sound now
|
2008-03-21 05:13:59 +00:00
|
|
|
S_Sound (self, CHAN_VOICE, "brain/sight", 1, ATTN_NONE);
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void A_BrainPain (AActor *self)
|
|
|
|
{
|
2008-03-21 05:13:59 +00:00
|
|
|
S_Sound (self, CHAN_VOICE, "brain/pain", 1, ATTN_NONE);
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void BrainishExplosion (fixed_t x, fixed_t y, fixed_t z)
|
|
|
|
{
|
2007-04-28 09:06:32 +00:00
|
|
|
AActor *boom = Spawn("Rocket", x, y, z, NO_REPLACE);
|
2006-02-24 04:48:15 +00:00
|
|
|
if (boom != NULL)
|
|
|
|
{
|
2008-06-15 02:25:09 +00:00
|
|
|
boom->DeathSound = "misc/brainexplode";
|
2006-02-24 04:48:15 +00:00
|
|
|
boom->momz = pr_brainscream() << 9;
|
|
|
|
boom->SetState (&ABossBrain::States[S_BRAINEXPLODE]);
|
|
|
|
boom->effects = 0;
|
2008-01-10 11:02:07 +00:00
|
|
|
boom->Damage = 0; // disables collision detection which is not wanted here
|
2006-02-24 04:48:15 +00:00
|
|
|
boom->tics -= pr_brainscream() & 7;
|
|
|
|
if (boom->tics < 1)
|
|
|
|
boom->tics = 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void A_BrainScream (AActor *self)
|
|
|
|
{
|
|
|
|
fixed_t x;
|
|
|
|
|
|
|
|
for (x = self->x - 196*FRACUNIT; x < self->x + 320*FRACUNIT; x += 8*FRACUNIT)
|
|
|
|
{
|
|
|
|
BrainishExplosion (x, self->y - 320*FRACUNIT,
|
|
|
|
128 + (pr_brainscream() << (FRACBITS + 1)));
|
|
|
|
}
|
2008-03-21 05:13:59 +00:00
|
|
|
S_Sound (self, CHAN_VOICE, "brain/death", 1, ATTN_NONE);
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void A_BrainExplode (AActor *self)
|
|
|
|
{
|
|
|
|
fixed_t x = self->x + pr_brainexplode.Random2()*2048;
|
|
|
|
fixed_t z = 128 + pr_brainexplode()*2*FRACUNIT;
|
|
|
|
BrainishExplosion (x, self->y, z);
|
|
|
|
}
|
|
|
|
|
|
|
|
void A_BrainDie (AActor *self)
|
|
|
|
{
|
|
|
|
// [RH] If noexit, then don't end the level.
|
|
|
|
if ((deathmatch || alwaysapplydmflags) && (dmflags & DF_NO_EXIT))
|
|
|
|
return;
|
|
|
|
|
|
|
|
G_ExitLevel (0, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void A_BrainSpit (AActor *self)
|
|
|
|
{
|
|
|
|
TThinkerIterator<DBrainState> iterator (STAT_BOSSTARGET);
|
|
|
|
DBrainState *state;
|
|
|
|
AActor *targ;
|
|
|
|
AActor *spit;
|
|
|
|
|
|
|
|
// shoot a cube at current target
|
|
|
|
if (NULL == (state = iterator.Next ()))
|
|
|
|
{
|
|
|
|
state = new DBrainState;
|
|
|
|
}
|
|
|
|
targ = state->GetTarget ();
|
|
|
|
|
|
|
|
if (targ != NULL)
|
|
|
|
{
|
2008-06-22 09:13:19 +00:00
|
|
|
const PClass *spawntype = NULL;
|
|
|
|
int index = CheckIndex (1, NULL);
|
|
|
|
if (index >= 0) spawntype = PClass::FindClass ((ENamedName)StateParameters[index]);
|
|
|
|
if (spawntype == NULL) spawntype = RUNTIME_CLASS(ASpawnShot);
|
|
|
|
|
2006-02-24 04:48:15 +00:00
|
|
|
// spawn brain missile
|
2008-06-22 09:13:19 +00:00
|
|
|
spit = P_SpawnMissile (self, targ, spawntype);
|
2006-02-24 04:48:15 +00:00
|
|
|
|
|
|
|
if (spit != NULL)
|
|
|
|
{
|
|
|
|
spit->target = targ;
|
2008-06-22 09:13:19 +00:00
|
|
|
spit->master = self;
|
2006-02-24 04:48:15 +00:00
|
|
|
// [RH] Do this correctly for any trajectory. Doom would divide by 0
|
|
|
|
// if the target had the same y coordinate as the spitter.
|
|
|
|
if ((spit->momx | spit->momy) == 0)
|
|
|
|
{
|
|
|
|
spit->reactiontime = 0;
|
|
|
|
}
|
|
|
|
else if (abs(spit->momy) > abs(spit->momx))
|
|
|
|
{
|
|
|
|
spit->reactiontime = (targ->y - self->y) / spit->momy;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
spit->reactiontime = (targ->x - self->x) / spit->momx;
|
|
|
|
}
|
2008-06-26 17:32:48 +00:00
|
|
|
// [GZ] Calculates when the projectile will have reached destination
|
|
|
|
spit->reactiontime += level.maptime;
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
|
2008-06-22 09:13:19 +00:00
|
|
|
if (index >= 0)
|
|
|
|
{
|
|
|
|
S_Sound(self, CHAN_WEAPON, self->AttackSound, 1, ATTN_NONE);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// compatibility fallback
|
|
|
|
S_Sound (self, CHAN_WEAPON, "brain/spit", 1, ATTN_NONE);
|
|
|
|
}
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void A_SpawnFly (AActor *self)
|
|
|
|
{
|
|
|
|
AActor *newmobj;
|
|
|
|
AActor *fog;
|
|
|
|
AActor *targ;
|
|
|
|
int r;
|
|
|
|
|
2008-06-26 17:32:48 +00:00
|
|
|
// [GZ] Should be more fiable than a countdown...
|
|
|
|
if (self->reactiontime > level.maptime)
|
2006-02-24 04:48:15 +00:00
|
|
|
return; // still flying
|
|
|
|
|
|
|
|
targ = self->target;
|
|
|
|
|
|
|
|
|
2008-06-22 09:13:19 +00:00
|
|
|
const PClass *spawntype = NULL;
|
|
|
|
int index = CheckIndex (1, NULL);
|
|
|
|
// First spawn teleport fire.
|
|
|
|
if (index >= 0)
|
|
|
|
{
|
|
|
|
spawntype = PClass::FindClass ((ENamedName)StateParameters[index]);
|
|
|
|
if (spawntype != NULL)
|
2006-02-24 04:48:15 +00:00
|
|
|
{
|
2008-06-22 09:13:19 +00:00
|
|
|
fog = Spawn (spawntype, targ->x, targ->y, targ->z, ALLOW_REPLACE);
|
|
|
|
if (fog != NULL) S_Sound (fog, CHAN_BODY, fog->SeeSound, 1, ATTN_NORM);
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
2008-06-22 09:13:19 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
fog = Spawn<ASpawnFire> (targ->x, targ->y, targ->z, ALLOW_REPLACE);
|
|
|
|
if (fog != NULL) S_Sound (fog, CHAN_BODY, "brain/spawn", 1, ATTN_NORM);
|
|
|
|
}
|
|
|
|
|
|
|
|
FName SpawnName;
|
|
|
|
|
|
|
|
if (self->master != NULL)
|
|
|
|
{
|
|
|
|
FDropItem *di; // di will be our drop item list iterator
|
|
|
|
FDropItem *drop; // while drop stays as the reference point.
|
|
|
|
int n=0;
|
|
|
|
|
|
|
|
drop = di = GetDropItems(self->master->GetClass());
|
|
|
|
if (di != NULL)
|
|
|
|
{
|
|
|
|
while (di != NULL)
|
|
|
|
{
|
|
|
|
if (di->Name != NAME_None)
|
|
|
|
{
|
|
|
|
if (di->amount < 0) di->amount = 1; // default value is -1, we need a positive value.
|
|
|
|
n += di->amount; // this is how we can weight the list.
|
|
|
|
di = di->Next;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
di = drop;
|
|
|
|
n = pr_spawnfly(n);
|
|
|
|
while (n > 0)
|
|
|
|
{
|
|
|
|
if (di->Name != NAME_None)
|
|
|
|
{
|
|
|
|
n -= di->amount; // logically, none of the -1 values have survived by now.
|
2008-06-26 17:32:48 +00:00
|
|
|
if (n > -1) di = di->Next; // If we get into the negatives, we've reached the end of the list.
|
2008-06-22 09:13:19 +00:00
|
|
|
}
|
|
|
|
}
|
2006-02-24 04:48:15 +00:00
|
|
|
|
2008-06-22 09:13:19 +00:00
|
|
|
SpawnName = di->Name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (SpawnName == NAME_None)
|
|
|
|
{
|
|
|
|
const char *type;
|
|
|
|
// Randomly select monster to spawn.
|
|
|
|
r = pr_spawnfly ();
|
|
|
|
|
|
|
|
// Probability distribution (kind of :),
|
|
|
|
// decreasing likelihood.
|
|
|
|
if (r < 50) type = "DoomImp";
|
|
|
|
else if (r < 90) type = "Demon";
|
|
|
|
else if (r < 120) type = "Spectre";
|
|
|
|
else if (r < 130) type = "PainElemental";
|
|
|
|
else if (r < 160) type = "Cacodemon";
|
|
|
|
else if (r < 162) type = "Archvile";
|
|
|
|
else if (r < 172) type = "Revenant";
|
|
|
|
else if (r < 192) type = "Arachnotron";
|
|
|
|
else if (r < 222) type = "Fatso";
|
|
|
|
else if (r < 246) type = "HellKnight";
|
|
|
|
else type = "BaronOfHell";
|
|
|
|
|
|
|
|
SpawnName = type;
|
|
|
|
}
|
|
|
|
spawntype = PClass::FindClass(SpawnName);
|
|
|
|
if (spawntype != NULL)
|
|
|
|
{
|
|
|
|
newmobj = Spawn (spawntype, targ->x, targ->y, targ->z, ALLOW_REPLACE);
|
|
|
|
if (newmobj != NULL)
|
2006-02-24 04:48:15 +00:00
|
|
|
{
|
2008-06-22 09:13:19 +00:00
|
|
|
// Make the new monster hate what the boss eye hates
|
|
|
|
AActor *eye = self->target;
|
|
|
|
|
|
|
|
if (eye != NULL)
|
|
|
|
{
|
|
|
|
newmobj->CopyFriendliness (eye, false);
|
|
|
|
}
|
|
|
|
if (newmobj->SeeState != NULL && P_LookForPlayers (newmobj, true))
|
|
|
|
newmobj->SetState (newmobj->SeeState);
|
|
|
|
|
|
|
|
if (!(newmobj->ObjectFlags & OF_EuthanizeMe))
|
|
|
|
{
|
|
|
|
// telefrag anything in this spot
|
|
|
|
P_TeleportMove (newmobj, newmobj->x, newmobj->y, newmobj->z, true);
|
|
|
|
}
|
2006-02-24 04:48:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// remove self (i.e., cube).
|
|
|
|
self->Destroy ();
|
|
|
|
}
|
|
|
|
|
|
|
|
// travelling cube sound
|
|
|
|
void A_SpawnSound (AActor *self)
|
|
|
|
{
|
|
|
|
S_Sound (self, CHAN_BODY, "brain/cube", 1, ATTN_IDLE);
|
|
|
|
A_SpawnFly (self);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Each brain on the level shares a single global state
|
|
|
|
IMPLEMENT_CLASS (DBrainState)
|
|
|
|
|
|
|
|
ABossTarget *DBrainState::GetTarget ()
|
|
|
|
{
|
|
|
|
Easy = !Easy;
|
|
|
|
|
2007-10-29 22:15:46 +00:00
|
|
|
if (G_SkillProperty(SKILLP_EasyBossBrain) && !Easy)
|
2006-02-24 04:48:15 +00:00
|
|
|
return NULL;
|
|
|
|
|
|
|
|
ABossTarget *target;
|
|
|
|
|
|
|
|
if (SerialTarget)
|
|
|
|
{
|
|
|
|
do
|
|
|
|
{
|
|
|
|
target = Targets.Next ();
|
|
|
|
} while (target != NULL && target != SerialTarget);
|
|
|
|
SerialTarget = NULL;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
target = Targets.Next ();
|
|
|
|
}
|
|
|
|
return (target == NULL) ? Targets.Next () : target;
|
|
|
|
}
|
|
|
|
|
|
|
|
void DBrainState::Serialize (FArchive &arc)
|
|
|
|
{
|
|
|
|
Super::Serialize (arc);
|
|
|
|
arc << Easy;
|
|
|
|
if (arc.IsStoring ())
|
|
|
|
{
|
|
|
|
ABossTarget *target = Targets.Next ();
|
|
|
|
arc << target;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
arc << SerialTarget;
|
|
|
|
}
|
|
|
|
}
|
2008-06-22 09:13:19 +00:00
|
|
|
|