From: Mario Date: Tue, 3 Dec 2013 13:12:20 +0000 (+1100) Subject: Merge branch 'master' into Mario/monsters X-Git-Tag: xonotic-v0.8.0~241^2^2~29 X-Git-Url: https://git.rm.cloudns.org/?a=commitdiff_plain;h=b56d8435ab6710d101761c6c2b9746e38e3cc813;p=xonotic%2Fxonotic-data.pk3dir.git Merge branch 'master' into Mario/monsters --- b56d8435ab6710d101761c6c2b9746e38e3cc813 diff --cc gamemodes.cfg index e333cf0b6,feed7488b..d523c88ee --- a/gamemodes.cfg +++ b/gamemodes.cfg @@@ -137,21 -127,8 +130,11 @@@ set g_cts_weapon_stay set g_ft_respawn_waves 0 set g_ft_respawn_delay 0 set g_ft_weapon_stay 0 +set g_invasion_respawn_waves 0 +set g_invasion_respawn_delay 0 +set g_invasion_weapon_stay 0 - // ======= - // arena - // ======= - set g_arena 0 "Arena: many one-on-one rounds are played to find the winner" - set g_arena_maxspawned 2 "maximum number of players to spawn at once (the rest is spectating, waiting for their turn)" - set g_arena_roundbased 1 "if disabled, the next player will spawn as soon as someone dies" - set g_arena_round_timelimit 180 - set g_arena_warmup 5 "time, newly spawned players have to prepare themselves in round based matches" - - // ========= // assault // ========= diff --cc qcsrc/client/scoreboard.qc index 5a6283f70,b037d03d2..6c204f8be --- a/qcsrc/client/scoreboard.qc +++ b/qcsrc/client/scoreboard.qc @@@ -1123,21 -1115,11 +1123,21 @@@ vector HUD_DrawMapStats(vector pos, vec else drawpic_tiled(pos, "gfx/scoreboard/scoreboard_bg", bg_size, tmp, rgb, scoreboard_alpha_bg, DRAWFLAG_NORMAL); drawborderlines(autocvar_scoreboard_border_thickness, pos, tmp, '0 0 0', scoreboard_alpha_bg * 0.75, DRAWFLAG_NORMAL); - + + // draw monsters + if(stat_monsters_total) + { + val = sprintf("%d/%d", stat_monsters_killed, stat_monsters_total); + pos = HUD_DrawKeyValue(pos, _("Monsters killed:"), val); + } + // draw secrets - val = sprintf("%d/%d", stat_secrets_found, stat_secrets_total); - pos = HUD_DrawKeyValue(pos, _("Secrets found:"), val); + if(stat_secrets_total) + { + val = sprintf("%d/%d", stat_secrets_found, stat_secrets_total); + pos = HUD_DrawKeyValue(pos, _("Secrets found:"), val); + } - + // update position pos_y += 1.25 * hud_fontsize_y; return pos; diff --cc qcsrc/common/csqcmodel_settings.qh index 74482b925,9f202d2b3..8e794cc76 --- a/qcsrc/common/csqcmodel_settings.qh +++ b/qcsrc/common/csqcmodel_settings.qh @@@ -46,10 -49,7 +49,10 @@@ CSQCMODEL_PROPERTY(512, float, ReadApproxPastTime, WriteApproxPastTime, anim_upper_time) \ CSQCMODEL_PROPERTY(1024, float, ReadAngle, WriteAngle, v_angle_x) \ CSQCMODEL_ENDIF \ + CSQCMODEL_IF(!isplayer) \ + CSQCMODEL_PROPERTY(2048, float, ReadByte, WriteByte, monsterid) \ + CSQCMODEL_ENDIF \ - CSQCMODEL_PROPERTY_SCALED(4096, float, ReadShort, WriteShort, scale, 256, 0, 16384) + CSQCMODEL_PROPERTY_SCALED(4096, float, ReadByte, WriteByte, scale, 16, 0, 255) // TODO get rid of colormod/glowmod here, find good solution for nex charge glowmod hack; also get rid of some useless properties on non-players that only exist for CopyBody // add hook function calls here diff --cc qcsrc/common/monsters/monster/mage.qc index c6e1b1898,000000000..83924ab7d mode 100644,000000..100644 --- a/qcsrc/common/monsters/monster/mage.qc +++ b/qcsrc/common/monsters/monster/mage.qc @@@ -1,427 -1,0 +1,427 @@@ +#ifdef REGISTER_MONSTER +REGISTER_MONSTER( - /* MON_##id */ MAGE, - /* function */ m_mage, ++/* MON_##id */ MAGE, ++/* function */ m_mage, +/* spawnflags */ MON_FLAG_MELEE | MON_FLAG_RANGED, +/* mins,maxs */ '-36 -36 -24', '36 36 50', - /* model */ "mage.dpm", - /* netname */ "mage", - /* fullname */ _("Mage") ++/* model */ "mage.dpm", ++/* netname */ "mage", ++/* fullname */ _("Mage") +); + +#else +#ifdef SVQC +float autocvar_g_monster_mage_health; +float autocvar_g_monster_mage_attack_spike_damage; +float autocvar_g_monster_mage_attack_spike_radius; +float autocvar_g_monster_mage_attack_spike_delay; +float autocvar_g_monster_mage_attack_spike_accel; +float autocvar_g_monster_mage_attack_spike_decel; +float autocvar_g_monster_mage_attack_spike_turnrate; +float autocvar_g_monster_mage_attack_spike_speed_max; +float autocvar_g_monster_mage_attack_spike_smart; +float autocvar_g_monster_mage_attack_spike_smart_trace_min; +float autocvar_g_monster_mage_attack_spike_smart_trace_max; +float autocvar_g_monster_mage_attack_spike_smart_mindist; +float autocvar_g_monster_mage_attack_push_damage; +float autocvar_g_monster_mage_attack_push_radius; +float autocvar_g_monster_mage_attack_push_delay; +float autocvar_g_monster_mage_attack_push_force; +float autocvar_g_monster_mage_heal_self; +float autocvar_g_monster_mage_heal_allies; +float autocvar_g_monster_mage_heal_minhealth; +float autocvar_g_monster_mage_heal_range; +float autocvar_g_monster_mage_heal_delay; +float autocvar_g_monster_mage_shield_time; +float autocvar_g_monster_mage_shield_delay; +float autocvar_g_monster_mage_shield_blockpercent; +float autocvar_g_monster_mage_speed_stop; +float autocvar_g_monster_mage_speed_run; +float autocvar_g_monster_mage_speed_walk; + +const float mage_anim_idle = 0; - const float mage_anim_walk = 1; - const float mage_anim_attack = 2; - const float mage_anim_pain = 3; - const float mage_anim_death = 4; ++const float mage_anim_walk = 1; ++const float mage_anim_attack = 2; ++const float mage_anim_pain = 3; ++const float mage_anim_death = 4; +const float mage_anim_run = 5; + +void() mage_heal; +void() mage_shield; + +.entity mage_spike; +.float shield_ltime; + +float friend_needshelp(entity e) +{ + if(e == world) + return FALSE; + if(e.health <= 0) + return FALSE; + if(DIFF_TEAM(e, self) && e != self.monster_owner) + return FALSE; + if(e.frozen) + return FALSE; + if(!IS_PLAYER(e)) + return (e.flags & FL_MONSTER && e.health < e.max_health); + if(e.items & IT_INVINCIBLE) + return FALSE; + + switch(self.skin) + { + case 0: return (e.health < autocvar_g_balance_health_regenstable); + case 1: return ((e.ammo_cells && e.ammo_cells < g_pickup_cells_max) || (e.ammo_rockets && e.ammo_rockets < g_pickup_rockets_max) || (e.ammo_nails && e.ammo_nails < g_pickup_nails_max) || (e.ammo_shells && e.ammo_shells < g_pickup_shells_max)); + case 2: return (e.armorvalue < autocvar_g_balance_armor_regenstable); + case 3: return (e.health > 0); + } - ++ + return FALSE; +} + +void mage_spike_explode() +{ + self.event_damage = func_null; - ++ + sound(self, CH_SHOTS, "weapons/grenade_impact.wav", VOL_BASE, ATTEN_NORM); - ++ + self.realowner.mage_spike = world; - ++ + pointparticles(particleeffectnum("explosion_small"), self.origin, '0 0 0', 1); + RadiusDamage (self, self.realowner, (autocvar_g_monster_mage_attack_spike_damage), (autocvar_g_monster_mage_attack_spike_damage) * 0.5, (autocvar_g_monster_mage_attack_spike_radius), world, 0, DEATH_MONSTER_MAGE, other); + + remove (self); +} + +void mage_spike_touch() +{ + PROJECTILE_TOUCH; + + mage_spike_explode(); +} + +// copied from W_Seeker_Think +void mage_spike_think() +{ + entity e; + vector desireddir, olddir, newdir, eorg; + float turnrate; + float dist; + float spd; + + if (time > self.ltime || self.enemy.health <= 0 || self.owner.health <= 0) + { + self.projectiledeathtype |= HITTYPE_SPLASH; + mage_spike_explode(); + } + + spd = vlen(self.velocity); + spd = bound( + spd - (autocvar_g_monster_mage_attack_spike_decel) * frametime, + (autocvar_g_monster_mage_attack_spike_speed_max), + spd + (autocvar_g_monster_mage_attack_spike_accel) * frametime + ); + + if (self.enemy != world) + if (self.enemy.takedamage != DAMAGE_AIM || self.enemy.deadflag != DEAD_NO) + self.enemy = world; + + if (self.enemy != world) + { - e = self.enemy; - eorg = 0.5 * (e.absmin + e.absmax); - turnrate = (autocvar_g_monster_mage_attack_spike_turnrate); // how fast to turn - desireddir = normalize(eorg - self.origin); - olddir = normalize(self.velocity); // get my current direction - dist = vlen(eorg - self.origin); ++ e = self.enemy; ++ eorg = 0.5 * (e.absmin + e.absmax); ++ turnrate = (autocvar_g_monster_mage_attack_spike_turnrate); // how fast to turn ++ desireddir = normalize(eorg - self.origin); ++ olddir = normalize(self.velocity); // get my current direction ++ dist = vlen(eorg - self.origin); + + // Do evasive maneuvers for world objects? ( this should be a cpu hog. :P ) + if ((autocvar_g_monster_mage_attack_spike_smart) && (dist > (autocvar_g_monster_mage_attack_spike_smart_mindist))) + { + // Is it a better idea (shorter distance) to trace to the target itself? + if ( vlen(self.origin + olddir * self.wait) < dist) + traceline(self.origin, self.origin + olddir * self.wait, FALSE, self); + else + traceline(self.origin, eorg, FALSE, self); + + // Setup adaptive tracelength + self.wait = bound((autocvar_g_monster_mage_attack_spike_smart_trace_min), vlen(self.origin - trace_endpos), self.wait = (autocvar_g_monster_mage_attack_spike_smart_trace_max)); + + // Calc how important it is that we turn and add this to the desierd (enemy) dir. - desireddir = normalize(((trace_plane_normal * (1 - trace_fraction)) + (desireddir * trace_fraction)) * 0.5); ++ desireddir = normalize(((trace_plane_normal * (1 - trace_fraction)) + (desireddir * trace_fraction)) * 0.5); + } - ++ + newdir = normalize(olddir + desireddir * turnrate); // take the average of the 2 directions; not the best method but simple & easy + self.velocity = newdir * spd; // make me fly in the new direction at my flight speed + } + else + dist = 0; - ++ + /////////////// + + //self.angles = vectoangles(self.velocity); // turn model in the new flight direction + self.nextthink = time;// + 0.05; // csqc projectiles + UpdateCSQCProjectile(self); +} + +void mage_attack_spike() +{ + entity missile; + vector dir = normalize((self.enemy.origin + '0 0 10') - self.origin); + + makevectors(self.angles); + + missile = spawn (); + missile.owner = missile.realowner = self; + missile.think = mage_spike_think; + missile.ltime = time + 7; + missile.nextthink = time; + missile.solid = SOLID_BBOX; + missile.movetype = MOVETYPE_FLYMISSILE; + missile.flags = FL_PROJECTILE; + setorigin(missile, self.origin + v_forward * 14 + '0 0 30' + v_right * -14); - setsize (missile, '0 0 0', '0 0 0'); ++ setsize (missile, '0 0 0', '0 0 0'); + missile.velocity = dir * 400; + missile.avelocity = '300 300 300'; + missile.enemy = self.enemy; + missile.touch = mage_spike_touch; - ++ + self.mage_spike = missile; - ++ + CSQCProjectile(missile, TRUE, PROJECTILE_MAGE_SPIKE, TRUE); +} + +void mage_heal() +{ + entity head; + float washealed = FALSE; - ++ + for(head = findradius(self.origin, (autocvar_g_monster_mage_heal_range)); head; head = head.chain) if(friend_needshelp(head)) + { + washealed = TRUE; + string fx = ""; + if(IS_PLAYER(head)) + { + switch(self.skin) + { + case 0: + if(head.health < autocvar_g_balance_health_regenstable) head.health = bound(0, head.health + (autocvar_g_monster_mage_heal_allies), autocvar_g_balance_health_regenstable); + fx = "healing_fx"; + break; + case 1: + if(head.ammo_cells) head.ammo_cells = bound(head.ammo_cells, head.ammo_cells + 1, g_pickup_cells_max); + if(head.ammo_rockets) head.ammo_rockets = bound(head.ammo_rockets, head.ammo_rockets + 1, g_pickup_rockets_max); + if(head.ammo_shells) head.ammo_shells = bound(head.ammo_shells, head.ammo_shells + 2, g_pickup_shells_max); + if(head.ammo_nails) head.ammo_nails = bound(head.ammo_nails, head.ammo_nails + 5, g_pickup_nails_max); + fx = "ammoregen_fx"; + break; + case 2: + if(head.armorvalue < autocvar_g_balance_armor_regenstable) + { + head.armorvalue = bound(0, head.armorvalue + (autocvar_g_monster_mage_heal_allies), autocvar_g_balance_armor_regenstable); + fx = "armorrepair_fx"; + } + break; + case 3: + head.health = bound(0, head.health - ((head == self) ? (autocvar_g_monster_mage_heal_self) : (autocvar_g_monster_mage_heal_allies)), autocvar_g_balance_health_regenstable); + fx = "rage"; + break; + } - ++ + pointparticles(particleeffectnum(fx), head.origin, '0 0 0', 1); + } + else + { + pointparticles(particleeffectnum("healing_fx"), head.origin, '0 0 0', 1); + head.health = bound(0, head.health + (autocvar_g_monster_mage_heal_allies), head.max_health); + WaypointSprite_UpdateHealth(head.sprite, head.health); + } + } - ++ + if(washealed) + { + self.frame = mage_anim_attack; + self.attack_finished_single = time + (autocvar_g_monster_mage_heal_delay); + } +} + +void mage_push() +{ + sound(self, CH_SHOTS, "weapons/tagexp1.wav", 1, ATTEN_NORM); + RadiusDamage (self, self, (autocvar_g_monster_mage_attack_push_damage), (autocvar_g_monster_mage_attack_push_damage), (autocvar_g_monster_mage_attack_push_radius), world, (autocvar_g_monster_mage_attack_push_force), DEATH_MONSTER_MAGE, self.enemy); + pointparticles(particleeffectnum("TE_EXPLOSION"), self.origin, '0 0 0', 1); - ++ + self.frame = mage_anim_attack; + self.attack_finished_single = time + (autocvar_g_monster_mage_attack_push_delay); +} + +void mage_teleport() +{ + if(vlen(self.enemy.origin - self.origin) >= 500) + return; + + makevectors(self.enemy.angles); + tracebox(self.enemy.origin + ((v_forward * -1) * 200), self.mins, self.maxs, self.origin, MOVE_NOMONSTERS, self); - ++ + if(trace_fraction < 1) + return; - ++ + pointparticles(particleeffectnum("spawn_event_neutral"), self.origin, '0 0 0', 1); + setorigin(self, self.enemy.origin + ((v_forward * -1) * 200)); - ++ + self.attack_finished_single = time + 0.2; +} + +void mage_shield_remove() +{ + self.effects &= ~(EF_ADDITIVE | EF_BLUE); + self.armorvalue = 0; + self.m_armor_blockpercent = autocvar_g_monsters_armor_blockpercent; +} + +void mage_shield() +{ + self.effects |= (EF_ADDITIVE | EF_BLUE); + self.lastshielded = time + (autocvar_g_monster_mage_shield_delay); + self.m_armor_blockpercent = (autocvar_g_monster_mage_shield_blockpercent); + self.armorvalue = self.health; + self.shield_ltime = time + (autocvar_g_monster_mage_shield_time); + self.frame = mage_anim_attack; + self.attack_finished_single = time + 1; +} + +float mage_attack(float attack_type) +{ + switch(attack_type) + { + case MONSTER_ATTACK_MELEE: + { + if(random() <= 0.7) + { + mage_push(); + return TRUE; + } - ++ + return FALSE; + } + case MONSTER_ATTACK_RANGED: + { + if(!self.mage_spike) + { + if(random() <= 0.4) + { + mage_teleport(); + return TRUE; + } + else + { + self.frame = mage_anim_attack; + self.attack_finished_single = time + (autocvar_g_monster_mage_attack_spike_delay); + defer(0.2, mage_attack_spike); + return TRUE; + } + } - ++ + if(self.mage_spike) + return TRUE; + else + return FALSE; + } + } - ++ + return FALSE; +} + +void spawnfunc_monster_mage() +{ + self.classname = "monster_mage"; - ++ + self.monster_spawnfunc = spawnfunc_monster_mage; - ++ + if(Monster_CheckAppearFlags(self)) + return; - ++ + if(!monster_initialize(MON_MAGE, FALSE)) { remove(self); return; } +} + +// compatibility with old spawns +void spawnfunc_monster_shalrath() { spawnfunc_monster_mage(); } + +float m_mage(float req) +{ + switch(req) + { + case MR_THINK: + { + entity head; + float need_help = FALSE; - ++ + for(head = findradius(self.origin, (autocvar_g_monster_mage_heal_range)); head; head = head.chain) + if(head != self) + if(friend_needshelp(head)) + { + need_help = TRUE; + break; + } - ++ + if(self.health < (autocvar_g_monster_mage_heal_minhealth) || need_help) + if(time >= self.attack_finished_single) + if(random() < 0.5) + mage_heal(); - ++ + if(time >= self.shield_ltime && self.armorvalue) + mage_shield_remove(); - ++ + if(self.enemy) + if(self.health < self.max_health) + if(time >= self.lastshielded) + if(random() < 0.5) + mage_shield(); - ++ + monster_move((autocvar_g_monster_mage_speed_run), (autocvar_g_monster_mage_speed_walk), (autocvar_g_monster_mage_speed_stop), mage_anim_walk, mage_anim_run, mage_anim_idle); + return TRUE; + } + case MR_DEATH: + { + self.frame = mage_anim_death; + return TRUE; + } + case MR_SETUP: + { + if(!self.health) self.health = (autocvar_g_monster_mage_health); - ++ + self.monster_loot = spawnfunc_item_health_large; + self.monster_attackfunc = mage_attack; + self.frame = mage_anim_walk; - ++ + return TRUE; + } + case MR_PRECACHE: + { + precache_model ("models/monsters/mage.dpm"); + precache_sound ("weapons/grenade_impact.wav"); + precache_sound ("weapons/tagexp1.wav"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // SVQC +#ifdef CSQC +float m_mage(float req) +{ + switch(req) + { + case MR_PRECACHE: + { + precache_model ("models/monsters/mage.dpm"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // CSQC +#endif // REGISTER_MONSTER diff --cc qcsrc/common/monsters/monster/shambler.qc index 499a9ff73,000000000..5da537c27 mode 100644,000000..100644 --- a/qcsrc/common/monsters/monster/shambler.qc +++ b/qcsrc/common/monsters/monster/shambler.qc @@@ -1,260 -1,0 +1,260 @@@ +#ifdef REGISTER_MONSTER +REGISTER_MONSTER( - /* MON_##id */ SHAMBLER, - /* function */ m_shambler, ++/* MON_##id */ SHAMBLER, ++/* function */ m_shambler, +/* spawnflags */ MONSTER_SIZE_BROKEN | MON_FLAG_SUPERMONSTER | MON_FLAG_MELEE | MON_FLAG_RANGED, +/* mins,maxs */ '-41 -41 -31', '41 41 65', - /* model */ "shambler.mdl", - /* netname */ "shambler", - /* fullname */ _("Shambler") ++/* model */ "shambler.mdl", ++/* netname */ "shambler", ++/* fullname */ _("Shambler") +); + +#else +#ifdef SVQC +float autocvar_g_monster_shambler_health; +float autocvar_g_monster_shambler_attack_smash_damage; +float autocvar_g_monster_shambler_attack_claw_damage; +float autocvar_g_monster_shambler_attack_lightning_damage; +float autocvar_g_monster_shambler_attack_lightning_force; +float autocvar_g_monster_shambler_attack_lightning_radius; +float autocvar_g_monster_shambler_attack_lightning_radius_zap; +float autocvar_g_monster_shambler_attack_lightning_speed; +float autocvar_g_monster_shambler_attack_lightning_speed_up; +float autocvar_g_monster_shambler_speed_stop; +float autocvar_g_monster_shambler_speed_run; +float autocvar_g_monster_shambler_speed_walk; + - const float shambler_anim_stand = 0; - const float shambler_anim_walk = 1; - const float shambler_anim_run = 2; - const float shambler_anim_smash = 3; - const float shambler_anim_swingr = 4; - const float shambler_anim_swingl = 5; - const float shambler_anim_magic = 6; - const float shambler_anim_pain = 7; - const float shambler_anim_death = 8; ++const float shambler_anim_stand = 0; ++const float shambler_anim_walk = 1; ++const float shambler_anim_run = 2; ++const float shambler_anim_smash = 3; ++const float shambler_anim_swingr = 4; ++const float shambler_anim_swingl = 5; ++const float shambler_anim_magic = 6; ++const float shambler_anim_pain = 7; ++const float shambler_anim_death = 8; + +.float shambler_lastattack; // delay attacks separately + +void shambler_smash() +{ + makevectors(self.angles); + pointparticles(particleeffectnum("explosion_medium"), (self.origin + (v_forward * 150)) - ('0 0 1' * self.maxs_z), '0 0 0', 1); + sound(self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTEN_NORM); - ++ + tracebox(self.origin + v_forward * 50, self.mins * 0.5, self.maxs * 0.5, self.origin + v_forward * 500, MOVE_NORMAL, self); - ++ + if(trace_ent.takedamage) + Damage(trace_ent, self, self, (autocvar_g_monster_shambler_attack_smash_damage) * Monster_SkillModifier(), DEATH_MONSTER_SHAMBLER_SMASH, trace_ent.origin, normalize(trace_ent.origin - self.origin)); +} + +void shambler_swing() +{ + float r = (random() < 0.5); + monster_melee(self.enemy, (autocvar_g_monster_shambler_attack_claw_damage), ((r) ? shambler_anim_swingr : shambler_anim_swingl), self.attack_range, 0.8, DEATH_MONSTER_SHAMBLER_CLAW, TRUE); + if(r) + { + defer(0.5, shambler_swing); + self.attack_finished_single += 0.5; + } +} + +void shambler_lightning_explode() +{ + entity head; - ++ + sound(self, CH_SHOTS, "weapons/electro_impact.wav", VOL_BASE, ATTEN_NORM); + pointparticles(particleeffectnum("electro_impact"), '0 0 0', '0 0 0', 1); + + self.event_damage = func_null; + self.takedamage = DAMAGE_NO; + self.movetype = MOVETYPE_NONE; + self.velocity = '0 0 0'; + + if(self.movetype == MOVETYPE_NONE) + self.velocity = self.oldvelocity; + + RadiusDamage (self, self.realowner, (autocvar_g_monster_shambler_attack_lightning_damage), (autocvar_g_monster_shambler_attack_lightning_damage), (autocvar_g_monster_shambler_attack_lightning_radius), world, (autocvar_g_monster_shambler_attack_lightning_force), self.projectiledeathtype, other); - ++ + for(head = findradius(self.origin, (autocvar_g_monster_shambler_attack_lightning_radius_zap)); head; head = head.chain) if(head != self.realowner) if(head.takedamage) + { + te_csqc_lightningarc(self.origin, head.origin); + Damage(head, self, self.realowner, (autocvar_g_monster_shambler_attack_lightning_damage) * Monster_SkillModifier(), DEATH_MONSTER_SHAMBLER_ZAP, head.origin, '0 0 0'); + } + + self.think = SUB_Remove; + self.nextthink = time + 0.2; +} + +void shambler_lightning_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) +{ + if (self.health <= 0) + return; - ++ + if (!W_CheckProjectileDamage(inflictor.realowner, self.realowner, deathtype, -1)) // no exceptions + return; // g_projectiles_damage says to halt - ++ + self.health = self.health - damage; - ++ + if (self.health <= 0) + W_PrepareExplosionByDamage(attacker, self.use); +} + +void shambler_lightning_touch() +{ + PROJECTILE_TOUCH; - ++ + self.use (); +} + +void shambler_lightning_think() +{ + self.nextthink = time; + if (time > self.cnt) + { + other = world; + shambler_lightning_explode(); + return; + } +} + +void shambler_lightning() +{ + entity gren; - ++ + monster_makevectors(self.enemy); + + gren = spawn (); + gren.owner = gren.realowner = self; + gren.classname = "grenade"; + gren.bot_dodge = TRUE; + gren.bot_dodgerating = (autocvar_g_monster_shambler_attack_lightning_damage); + gren.movetype = MOVETYPE_BOUNCE; + PROJECTILE_MAKETRIGGER(gren); + gren.projectiledeathtype = DEATH_MONSTER_SHAMBLER_ZAP; + setorigin(gren, CENTER_OR_VIEWOFS(self)); + setsize(gren, '-8 -8 -8', '8 8 8'); + gren.scale = 2.5; + + gren.cnt = time + 5; + gren.nextthink = time; + gren.think = shambler_lightning_think; + gren.use = shambler_lightning_explode; + gren.touch = shambler_lightning_touch; + + gren.takedamage = DAMAGE_YES; + gren.health = 50; + gren.damageforcescale = 0; + gren.event_damage = shambler_lightning_damage; + gren.damagedbycontents = TRUE; + gren.missile_flags = MIF_SPLASH | MIF_ARC; + W_SetupProjectileVelocityEx(gren, v_forward, v_up, (autocvar_g_monster_shambler_attack_lightning_speed), (autocvar_g_monster_shambler_attack_lightning_speed_up), 0, 0, FALSE); + + gren.angles = vectoangles (gren.velocity); + gren.flags = FL_PROJECTILE; + + CSQCProjectile(gren, TRUE, PROJECTILE_SHAMBLER_LIGHTNING, TRUE); +} + +float shambler_attack(float attack_type) +{ + switch(attack_type) + { + case MONSTER_ATTACK_MELEE: + { + shambler_swing(); + return TRUE; + } + case MONSTER_ATTACK_RANGED: + { + if(time >= self.shambler_lastattack) // shambler doesn't attack much + if(random() <= 0.5 && vlen(self.enemy.origin - self.origin) <= 500) + { + self.frame = shambler_anim_smash; + defer(0.7, shambler_smash); + self.attack_finished_single = time + 1.1; + self.shambler_lastattack = time + 3; + return TRUE; + } + else if(random() <= 0.1) // small chance, don't want this spammed + { + self.frame = shambler_anim_magic; + self.attack_finished_single = time + 1.1; + self.shambler_lastattack = time + 3; + defer(0.6, shambler_lightning); + return TRUE; + } - ++ + return FALSE; + } + } - ++ + return FALSE; +} + +void spawnfunc_monster_shambler() +{ + self.classname = "monster_shambler"; - ++ + self.monster_spawnfunc = spawnfunc_monster_shambler; - ++ + if(Monster_CheckAppearFlags(self)) + return; - ++ + if(!monster_initialize(MON_SHAMBLER, FALSE)) { remove(self); return; } +} + +float m_shambler(float req) +{ + switch(req) + { + case MR_THINK: + { + monster_move((autocvar_g_monster_shambler_speed_run), (autocvar_g_monster_shambler_speed_walk), (autocvar_g_monster_shambler_speed_stop), shambler_anim_run, shambler_anim_walk, shambler_anim_stand); + return TRUE; + } + case MR_DEATH: + { + self.frame = shambler_anim_death; + return TRUE; + } + case MR_SETUP: + { + if(!self.health) self.health = (autocvar_g_monster_shambler_health); + if(!self.attack_range) self.attack_range = 150; - ++ + self.monster_loot = spawnfunc_item_health_mega; + self.monster_attackfunc = shambler_attack; + self.frame = shambler_anim_stand; + self.weapon = WEP_NEX; - ++ + return TRUE; + } + case MR_PRECACHE: + { + precache_model ("models/monsters/shambler.mdl"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // SVQC +#ifdef CSQC +float m_shambler(float req) +{ + switch(req) + { + case MR_PRECACHE: + { + precache_model ("models/monsters/shambler.mdl"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // CSQC +#endif // REGISTER_MONSTER diff --cc qcsrc/common/monsters/monster/spider.qc index 7e3c9f0c3,000000000..2de82422a mode 100644,000000..100644 --- a/qcsrc/common/monsters/monster/spider.qc +++ b/qcsrc/common/monsters/monster/spider.qc @@@ -1,183 -1,0 +1,183 @@@ +#ifdef REGISTER_MONSTER +REGISTER_MONSTER( - /* MON_##id */ SPIDER, - /* function */ m_spider, ++/* MON_##id */ SPIDER, ++/* function */ m_spider, +/* spawnflags */ MON_FLAG_MELEE | MON_FLAG_RANGED, +/* mins,maxs */ '-18 -18 -25', '18 18 30', - /* model */ "spider.dpm", - /* netname */ "spider", - /* fullname */ _("Spider") ++/* model */ "spider.dpm", ++/* netname */ "spider", ++/* fullname */ _("Spider") +); + +#else +#ifdef SVQC +float autocvar_g_monster_spider_health; +float autocvar_g_monster_spider_attack_bite_damage; +float autocvar_g_monster_spider_attack_bite_delay; +float autocvar_g_monster_spider_attack_web_damagetime; +float autocvar_g_monster_spider_attack_web_speed; +float autocvar_g_monster_spider_attack_web_speed_up; +float autocvar_g_monster_spider_attack_web_delay; +float autocvar_g_monster_spider_speed_stop; +float autocvar_g_monster_spider_speed_run; +float autocvar_g_monster_spider_speed_walk; + +const float spider_anim_idle = 0; +const float spider_anim_walk = 1; +const float spider_anim_attack = 2; +const float spider_anim_attack2 = 3; + +.float spider_web_delay; + +void spider_web_explode() +{ + entity e; + if(self) + { + pointparticles(particleeffectnum("electro_impact"), self.origin, '0 0 0', 1); + RadiusDamage(self, self.realowner, 0, 0, 25, world, 25, self.projectiledeathtype, world); - ++ + for(e = findradius(self.origin, 25); e; e = e.chain) if(e != self) if(e.takedamage && e.deadflag == DEAD_NO) if(e.health > 0) + e.spider_slowness = time + (autocvar_g_monster_spider_attack_web_damagetime); - ++ + remove(self); + } +} + +void spider_web_touch() +{ + PROJECTILE_TOUCH; - ++ + spider_web_explode(); +} + +void spider_shootweb() +{ + monster_makevectors(self.enemy); - ++ + sound(self, CH_SHOTS, "weapons/electro_fire2.wav", VOL_BASE, ATTEN_NORM); + + entity proj = spawn (); + proj.classname = "plasma"; + proj.owner = proj.realowner = self; + proj.use = spider_web_touch; + proj.think = adaptor_think2use_hittype_splash; + proj.bot_dodge = TRUE; + proj.bot_dodgerating = 0; + proj.nextthink = time + 5; + PROJECTILE_MAKETRIGGER(proj); + proj.projectiledeathtype = DEATH_MONSTER_SPIDER; + setorigin(proj, CENTER_OR_VIEWOFS(self)); + + //proj.glow_size = 50; + //proj.glow_color = 45; + proj.movetype = MOVETYPE_BOUNCE; + W_SetupProjectileVelocityEx(proj, v_forward, v_up, (autocvar_g_monster_spider_attack_web_speed), (autocvar_g_monster_spider_attack_web_speed_up), 0, 0, FALSE); + proj.touch = spider_web_touch; + setsize(proj, '-4 -4 -4', '4 4 4'); + proj.takedamage = DAMAGE_NO; + proj.damageforcescale = 0; + proj.health = 500; + proj.event_damage = func_null; + proj.flags = FL_PROJECTILE; + proj.damagedbycontents = TRUE; + + proj.bouncefactor = 0.3; + proj.bouncestop = 0.05; + proj.missile_flags = MIF_SPLASH | MIF_ARC; + + CSQCProjectile(proj, TRUE, PROJECTILE_ELECTRO, TRUE); +} + +float spider_attack(float attack_type) +{ + switch(attack_type) + { + case MONSTER_ATTACK_MELEE: - { ++ { + return monster_melee(self.enemy, (autocvar_g_monster_spider_attack_bite_damage), ((random() > 0.5) ? spider_anim_attack : spider_anim_attack2), self.attack_range, (autocvar_g_monster_spider_attack_bite_delay), DEATH_MONSTER_SPIDER, TRUE); + } + case MONSTER_ATTACK_RANGED: + { + if(time >= self.spider_web_delay) + { + self.frame = spider_anim_attack2; + self.attack_finished_single = time + (autocvar_g_monster_spider_attack_web_delay); + spider_shootweb(); + self.spider_web_delay = time + 3; + return TRUE; + } - ++ + return FALSE; + } + } - ++ + return FALSE; +} + - void spawnfunc_monster_spider() ++void spawnfunc_monster_spider() +{ + self.classname = "monster_spider"; - ++ + self.monster_spawnfunc = spawnfunc_monster_spider; - ++ + if(Monster_CheckAppearFlags(self)) + return; - ++ + if(!monster_initialize(MON_SPIDER, FALSE)) { remove(self); return; } +} + +float m_spider(float req) +{ + switch(req) + { + case MR_THINK: + { + monster_move((autocvar_g_monster_spider_speed_run), (autocvar_g_monster_spider_speed_walk), (autocvar_g_monster_spider_speed_stop), spider_anim_walk, spider_anim_walk, spider_anim_idle); + return TRUE; + } + case MR_DEATH: + { + self.frame = spider_anim_attack; + self.angles_x = 180; + return TRUE; + } + case MR_SETUP: + { + if(!self.health) self.health = (autocvar_g_monster_spider_health); - ++ + self.monster_loot = spawnfunc_item_health_medium; + self.monster_attackfunc = spider_attack; + self.frame = spider_anim_idle; - ++ + return TRUE; + } + case MR_PRECACHE: + { + precache_model ("models/monsters/spider.dpm"); + precache_sound ("weapons/electro_fire2.wav"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // SVQC +#ifdef CSQC +float m_spider(float req) +{ + switch(req) + { + case MR_PRECACHE: + { + precache_model ("models/monsters/spider.dpm"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // CSQC +#endif // REGISTER_MONSTER diff --cc qcsrc/common/monsters/monster/wyvern.qc index eadfab950,000000000..3774d9bdb mode 100644,000000..100644 --- a/qcsrc/common/monsters/monster/wyvern.qc +++ b/qcsrc/common/monsters/monster/wyvern.qc @@@ -1,164 -1,0 +1,164 @@@ +#ifdef REGISTER_MONSTER +REGISTER_MONSTER( - /* MON_##id */ WYVERN, - /* function */ m_wyvern, ++/* MON_##id */ WYVERN, ++/* function */ m_wyvern, +/* spawnflags */ MONSTER_TYPE_FLY | MONSTER_SIZE_BROKEN | MON_FLAG_RANGED, +/* mins,maxs */ '-20 -20 -58', '20 20 20', - /* model */ "wizard.mdl", - /* netname */ "wyvern", - /* fullname */ _("Wyvern") ++/* model */ "wizard.mdl", ++/* netname */ "wyvern", ++/* fullname */ _("Wyvern") +); + +#else +#ifdef SVQC +float autocvar_g_monster_wyvern_health; +float autocvar_g_monster_wyvern_attack_fireball_damage; +float autocvar_g_monster_wyvern_attack_fireball_edgedamage; +float autocvar_g_monster_wyvern_attack_fireball_damagetime; +float autocvar_g_monster_wyvern_attack_fireball_force; +float autocvar_g_monster_wyvern_attack_fireball_radius; +float autocvar_g_monster_wyvern_attack_fireball_speed; +float autocvar_g_monster_wyvern_speed_stop; +float autocvar_g_monster_wyvern_speed_run; +float autocvar_g_monster_wyvern_speed_walk; + - const float wyvern_anim_hover = 0; - const float wyvern_anim_fly = 1; - const float wyvern_anim_magic = 2; - const float wyvern_anim_pain = 3; - const float wyvern_anim_death = 4; ++const float wyvern_anim_hover = 0; ++const float wyvern_anim_fly = 1; ++const float wyvern_anim_magic = 2; ++const float wyvern_anim_pain = 3; ++const float wyvern_anim_death = 4; + +void wyvern_fireball_explode() +{ + entity e; + if(self) + { + pointparticles(particleeffectnum("fireball_explode"), self.origin, '0 0 0', 1); - ++ + RadiusDamage(self, self.realowner, (autocvar_g_monster_wyvern_attack_fireball_damage), (autocvar_g_monster_wyvern_attack_fireball_edgedamage), (autocvar_g_monster_wyvern_attack_fireball_force), world, (autocvar_g_monster_wyvern_attack_fireball_radius), self.projectiledeathtype, world); - ++ + for(e = world; (e = findfloat(e, takedamage, DAMAGE_AIM)); ) if(vlen(e.origin - self.origin) <= (autocvar_g_monster_wyvern_attack_fireball_radius)) + Fire_AddDamage(e, self, 5 * Monster_SkillModifier(), (autocvar_g_monster_wyvern_attack_fireball_damagetime), self.projectiledeathtype); - ++ + remove(self); + } +} + +void wyvern_fireball_touch() +{ + PROJECTILE_TOUCH; - ++ + wyvern_fireball_explode(); +} + +void wyvern_fireball() +{ + entity missile = spawn(); + vector dir = normalize((self.enemy.origin + '0 0 10') - self.origin); - ++ + monster_makevectors(self.enemy); + + missile.owner = missile.realowner = self; + missile.solid = SOLID_TRIGGER; + missile.movetype = MOVETYPE_FLYMISSILE; + missile.projectiledeathtype = DEATH_MONSTER_WYVERN; - setsize(missile, '-6 -6 -6', '6 6 6'); ++ setsize(missile, '-6 -6 -6', '6 6 6'); + setorigin(missile, self.origin + self.view_ofs + v_forward * 14); + missile.flags = FL_PROJECTILE; + missile.velocity = dir * (autocvar_g_monster_wyvern_attack_fireball_speed); + missile.avelocity = '300 300 300'; + missile.nextthink = time + 5; + missile.think = wyvern_fireball_explode; + missile.enemy = self.enemy; + missile.touch = wyvern_fireball_touch; + CSQCProjectile(missile, TRUE, PROJECTILE_FIREMINE, TRUE); +} + +float wyvern_attack(float attack_type) +{ + switch(attack_type) + { + case MONSTER_ATTACK_MELEE: + case MONSTER_ATTACK_RANGED: + { + self.attack_finished_single = time + 1.2; - ++ + wyvern_fireball(); - ++ + return TRUE; + } + } - ++ + return FALSE; +} + +void spawnfunc_monster_wyvern() +{ + self.classname = "monster_wyvern"; - ++ + self.monster_spawnfunc = spawnfunc_monster_wyvern; - ++ + if(Monster_CheckAppearFlags(self)) + return; - ++ + if(!monster_initialize(MON_WYVERN, TRUE)) { remove(self); return; } +} + +// compatibility with old spawns +void spawnfunc_monster_wizard() { spawnfunc_monster_wyvern(); } + +float m_wyvern(float req) +{ + switch(req) + { + case MR_THINK: + { + monster_move((autocvar_g_monster_wyvern_speed_run), (autocvar_g_monster_wyvern_speed_walk), (autocvar_g_monster_wyvern_speed_stop), wyvern_anim_fly, wyvern_anim_hover, wyvern_anim_hover); + return TRUE; + } + case MR_DEATH: + { + self.frame = wyvern_anim_death; + self.velocity_x = -200 + 400 * random(); + self.velocity_y = -200 + 400 * random(); + self.velocity_z = 100 + 100 * random(); + return TRUE; + } + case MR_SETUP: + { + if(!self.health) self.health = (autocvar_g_monster_wyvern_health); - ++ + self.monster_loot = spawnfunc_item_cells; + self.monster_attackfunc = wyvern_attack; + self.frame = wyvern_anim_hover; - ++ + return TRUE; + } + case MR_PRECACHE: + { + precache_model ("models/monsters/wizard.mdl"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // SVQC +#ifdef CSQC +float m_wyvern(float req) +{ + switch(req) + { + case MR_PRECACHE: + { + precache_model ("models/monsters/wizard.mdl"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // CSQC +#endif // REGISTER_MONSTER diff --cc qcsrc/common/monsters/monster/zombie.qc index 26e88b2f2,000000000..5d1616914 mode 100644,000000..100644 --- a/qcsrc/common/monsters/monster/zombie.qc +++ b/qcsrc/common/monsters/monster/zombie.qc @@@ -1,202 -1,0 +1,202 @@@ +#ifdef REGISTER_MONSTER +REGISTER_MONSTER( - /* MON_##id */ ZOMBIE, - /* function */ m_zombie, ++/* MON_##id */ ZOMBIE, ++/* function */ m_zombie, +/* spawnflags */ MON_FLAG_MELEE, +/* mins,maxs */ '-18 -18 -25', '18 18 47', - /* model */ "zombie.dpm", - /* netname */ "zombie", - /* fullname */ _("Zombie") ++/* model */ "zombie.dpm", ++/* netname */ "zombie", ++/* fullname */ _("Zombie") +); + +#else +#ifdef SVQC +float autocvar_g_monster_zombie_health; +float autocvar_g_monster_zombie_attack_melee_damage; +float autocvar_g_monster_zombie_attack_melee_delay; +float autocvar_g_monster_zombie_attack_leap_damage; +float autocvar_g_monster_zombie_attack_leap_force; +float autocvar_g_monster_zombie_attack_leap_speed; +float autocvar_g_monster_zombie_attack_leap_delay; +float autocvar_g_monster_zombie_speed_stop; +float autocvar_g_monster_zombie_speed_run; +float autocvar_g_monster_zombie_speed_walk; + +const float zombie_anim_attackleap = 0; +const float zombie_anim_attackrun1 = 1; +const float zombie_anim_attackrun2 = 2; +const float zombie_anim_attackrun3 = 3; +const float zombie_anim_attackstanding1 = 4; +const float zombie_anim_attackstanding2 = 5; +const float zombie_anim_attackstanding3 = 6; - const float zombie_anim_blockend = 7; ++const float zombie_anim_blockend = 7; +const float zombie_anim_blockstart = 8; +const float zombie_anim_deathback1 = 9; +const float zombie_anim_deathback2 = 10; +const float zombie_anim_deathback3 = 11; +const float zombie_anim_deathfront1 = 12; +const float zombie_anim_deathfront2 = 13; +const float zombie_anim_deathfront3 = 14; +const float zombie_anim_deathleft1 = 15; +const float zombie_anim_deathleft2 = 16; +const float zombie_anim_deathright1 = 17; +const float zombie_anim_deathright2 = 18; - const float zombie_anim_idle = 19; - const float zombie_anim_painback1 = 20; - const float zombie_anim_painback2 = 21; ++const float zombie_anim_idle = 19; ++const float zombie_anim_painback1 = 20; ++const float zombie_anim_painback2 = 21; +const float zombie_anim_painfront1 = 22; +const float zombie_anim_painfront2 = 23; - const float zombie_anim_runbackwards = 24; - const float zombie_anim_runbackwardsleft = 25; - const float zombie_anim_runbackwardsright = 26; ++const float zombie_anim_runbackwards = 24; ++const float zombie_anim_runbackwardsleft = 25; ++const float zombie_anim_runbackwardsright = 26; +const float zombie_anim_runforward = 27; - const float zombie_anim_runforwardleft = 28; ++const float zombie_anim_runforwardleft = 28; +const float zombie_anim_runforwardright = 29; - const float zombie_anim_spawn = 30; ++const float zombie_anim_spawn = 30; + +void zombie_attack_leap_touch() +{ + if (self.health <= 0) + return; - ++ + vector angles_face; + + if(other.takedamage) + { + angles_face = vectoangles(self.moveto - self.origin); + angles_face = normalize(angles_face) * (autocvar_g_monster_zombie_attack_leap_force); + Damage(other, self, self, (autocvar_g_monster_zombie_attack_leap_damage) * Monster_SkillModifier(), DEATH_MONSTER_ZOMBIE_JUMP, other.origin, angles_face); + self.touch = MonsterTouch; // instantly turn it off to stop damage spam + } + + if (trace_dphitcontents) + self.touch = MonsterTouch; +} + +void zombie_blockend() +{ + if(self.health <= 0) + return; + + self.frame = zombie_anim_blockend; + self.armorvalue = 0; + self.m_armor_blockpercent = autocvar_g_monsters_armor_blockpercent; +} + +float zombie_block() +{ + self.frame = zombie_anim_blockstart; + self.armorvalue = 100; + self.m_armor_blockpercent = 0.9; + self.state = MONSTER_STATE_ATTACK_MELEE; // freeze monster + self.attack_finished_single = time + 2.1; - ++ + defer(2, zombie_blockend); - ++ + return TRUE; +} + +float zombie_attack(float attack_type) +{ + switch(attack_type) + { + case MONSTER_ATTACK_MELEE: + { + float rand = random(), chosen_anim; - ++ + if(rand < 0.33) + chosen_anim = zombie_anim_attackstanding1; + else if(rand < 0.66) + chosen_anim = zombie_anim_attackstanding2; + else + chosen_anim = zombie_anim_attackstanding3; - ++ + if(random() < 0.3 && self.health < 75 && self.enemy.health > 10) + return zombie_block(); - ++ + return monster_melee(self.enemy, (autocvar_g_monster_zombie_attack_melee_damage), chosen_anim, self.attack_range, (autocvar_g_monster_zombie_attack_melee_delay), DEATH_MONSTER_ZOMBIE_MELEE, TRUE); + } + case MONSTER_ATTACK_RANGED: + { + makevectors(self.angles); + return monster_leap(zombie_anim_attackleap, zombie_attack_leap_touch, v_forward * (autocvar_g_monster_zombie_attack_leap_speed) + '0 0 200', (autocvar_g_monster_zombie_attack_leap_delay)); + } + } - ++ + return FALSE; +} + - void spawnfunc_monster_zombie() ++void spawnfunc_monster_zombie() +{ + self.classname = "monster_zombie"; - ++ + self.monster_spawnfunc = spawnfunc_monster_zombie; - ++ + self.spawnflags |= MONSTER_RESPAWN_DEATHPOINT; - ++ + if(Monster_CheckAppearFlags(self)) + return; - ++ + if(!monster_initialize(MON_ZOMBIE, FALSE)) { remove(self); return; } +} + +float m_zombie(float req) +{ + switch(req) + { + case MR_THINK: + { + monster_move((autocvar_g_monster_zombie_speed_run), (autocvar_g_monster_zombie_speed_walk), (autocvar_g_monster_zombie_speed_stop), zombie_anim_runforward, zombie_anim_runforward, zombie_anim_idle); + return TRUE; + } + case MR_DEATH: + { + self.armorvalue = 0; + self.m_armor_blockpercent = autocvar_g_monsters_armor_blockpercent; + self.frame = ((random() > 0.5) ? zombie_anim_deathback1 : zombie_anim_deathfront1); + return TRUE; + } + case MR_SETUP: + { + if(!self.health) self.health = (autocvar_g_monster_zombie_health); - ++ + if(self.spawnflags & MONSTERFLAG_NORESPAWN) + self.spawnflags &= ~MONSTERFLAG_NORESPAWN; // zombies always respawn - ++ + self.monster_loot = spawnfunc_item_health_medium; + self.monster_attackfunc = zombie_attack; + self.frame = zombie_anim_spawn; + self.spawn_time = time + 2.1; + self.spawnshieldtime = self.spawn_time; + self.respawntime = 0.2; - ++ + return TRUE; + } + case MR_PRECACHE: + { + precache_model ("models/monsters/zombie.dpm"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // SVQC +#ifdef CSQC +float m_zombie(float req) +{ + switch(req) + { + case MR_PRECACHE: + { + precache_model ("models/monsters/zombie.dpm"); + return TRUE; + } + } - ++ + return TRUE; +} + +#endif // CSQC +#endif // REGISTER_MONSTER diff --cc qcsrc/common/monsters/monsters.qc index f40d30d29,000000000..70802dbbd mode 100644,000000..100644 --- a/qcsrc/common/monsters/monsters.qc +++ b/qcsrc/common/monsters/monsters.qc @@@ -1,49 -1,0 +1,49 @@@ +#include "all.qh" + +// MONSTER PLUGIN SYSTEM +entity monster_info[MON_MAXCOUNT]; +entity dummy_monster_info; + +void register_monster(float id, float(float) func, float monsterflags, vector min_s, vector max_s, string modelname, string shortname, string mname) +{ + entity e; + monster_info[id - 1] = e = spawn(); + e.classname = "monster_info"; + e.monsterid = id; + e.netname = shortname; + e.monster_name = mname; + e.monster_func = func; + e.mdl = modelname; + e.spawnflags = monsterflags; + e.mins = min_s; + e.maxs = max_s; + e.model = strzone(strcat("models/monsters/", modelname)); - ++ + #ifndef MENUQC + func(MR_PRECACHE); + #endif +} +float m_null(float dummy) { return 0; } +void register_monsters_done() +{ + dummy_monster_info = spawn(); + dummy_monster_info.classname = "monster_info"; + dummy_monster_info.monsterid = 0; // you can recognize dummies by this + dummy_monster_info.netname = ""; + dummy_monster_info.monster_name = "Monster"; + dummy_monster_info.monster_func = m_null; + dummy_monster_info.mdl = ""; + dummy_monster_info.mins = '-0 -0 -0'; + dummy_monster_info.maxs = '0 0 0'; + dummy_monster_info.model = ""; +} +entity get_monsterinfo(float id) +{ + entity m; + if(id < MON_FIRST || id > MON_LAST) + return dummy_monster_info; + m = monster_info[id - 1]; + if(m) + return m; + return dummy_monster_info; +} diff --cc qcsrc/common/monsters/monsters.qh index f6db09c89,000000000..c355e12a7 mode 100644,000000..100644 --- a/qcsrc/common/monsters/monsters.qh +++ b/qcsrc/common/monsters/monsters.qh @@@ -1,67 -1,0 +1,67 @@@ +// monster requests - #define MR_SETUP 1 // (SERVER) setup monster data ++#define MR_SETUP 1 // (SERVER) setup monster data +#define MR_THINK 2 // (SERVER) logic to run every frame - #define MR_DEATH 3 // (SERVER) called when monster dies - #define MR_PRECACHE 4 // (BOTH) precaches models/sounds used by this monster ++#define MR_DEATH 3 // (SERVER) called when monster dies ++#define MR_PRECACHE 4 // (BOTH) precaches models/sounds used by this monster + +// functions: +entity get_monsterinfo(float id); + +// special spawn flags +const float MONSTER_RESPAWN_DEATHPOINT = 16; // re-spawn where we died +const float MONSTER_TYPE_FLY = 32; +const float MONSTER_TYPE_SWIM = 64; +const float MONSTER_SIZE_BROKEN = 128; // TODO: remove when bad models are replaced +const float MON_FLAG_SUPERMONSTER = 256; // incredibly powerful monster +const float MON_FLAG_RANGED = 512; // monster shoots projectiles +const float MON_FLAG_MELEE = 1024; + +// entity properties of monsterinfo: +.float monsterid; // MON_... +.string netname; // short name +.string monster_name; // human readable name +.float(float) monster_func; // m_... +.string mdl; // currently a copy of the model +.string model; // full name of model +.float spawnflags; +.vector mins, maxs; // monster hitbox size + +// other useful macros +#define MON_ACTION(monstertype,mrequest) (get_monsterinfo(monstertype)).monster_func(mrequest) +#define M_NAME(monstertype) (get_monsterinfo(monstertype)).monster_name + +// ===================== - // Monster Registration ++// Monster Registration +// ===================== + +float m_null(float dummy); +void register_monster(float id, float(float) func, float monsterflags, vector min_s, vector max_s, string modelname, string shortname, string mname); +void register_monsters_done(); + +const float MON_MAXCOUNT = 24; +#define MON_FIRST 1 +float MON_COUNT; +float MON_LAST; + +#define REGISTER_MONSTER_2(id,func,monsterflags,min_s,max_s,modelname,shortname,mname) \ + float id; \ + float func(float); \ + void RegisterMonsters_##id() \ + { \ + MON_LAST = (id = MON_FIRST + MON_COUNT); \ + ++MON_COUNT; \ + register_monster(id,func,monsterflags,min_s,max_s,modelname,shortname,mname); \ + } \ + ACCUMULATE_FUNCTION(RegisterMonsters, RegisterMonsters_##id) +#ifdef MENUQC +#define REGISTER_MONSTER(id,func,monsterflags,min_s,max_s,modelname,shortname,mname) \ + REGISTER_MONSTER_2(MON_##id,m_null,monsterflags,min_s,max_s,modelname,shortname,mname) +#else +#define REGISTER_MONSTER(id,func,monsterflags,min_s,max_s,modelname,shortname,mname) \ + REGISTER_MONSTER_2(MON_##id,func,monsterflags,min_s,max_s,modelname,shortname,mname) +#endif + +#include "all.qh" + +#undef REGISTER_MONSTER +ACCUMULATE_FUNCTION(RegisterMonsters, register_monsters_done); diff --cc qcsrc/common/monsters/spawn.qc index 61bf56e32,000000000..3fe8567ae mode 100644,000000..100644 --- a/qcsrc/common/monsters/spawn.qc +++ b/qcsrc/common/monsters/spawn.qc @@@ -1,57 -1,0 +1,57 @@@ +entity spawnmonster (string monster, float monster_id, entity spawnedby, entity own, vector orig, float respwn, float moveflag) +{ + // ensure spawnfunc database is initialized + initialize_field_db(); - ++ + entity e = spawn(); - ++ + e.spawnflags = MONSTERFLAG_SPAWNED; - ++ + if(!respwn) + e.spawnflags |= MONSTERFLAG_NORESPAWN; - ++ + setorigin(e, orig); - ++ + if(monster != "") + { + float i, found = 0; + entity mon; + for(i = MON_FIRST; i <= MON_LAST; ++i) + { + mon = get_monsterinfo(i); + if(mon.netname == monster) + { + found = TRUE; + break; + } + } + if(!found) + monster = (get_monsterinfo(MON_FIRST)).netname; + } - ++ + if(monster == "") + if(monster_id) + monster = (get_monsterinfo(monster_id)).netname; - ++ + e.realowner = spawnedby; - ++ + if(moveflag) + e.monster_moveflags = moveflag; - ++ + if(IS_PLAYER(spawnedby)) + { + if(teamplay && autocvar_g_monsters_teams) + e.team = spawnedby.team; // colors handled in spawn code - ++ + if(autocvar_g_monsters_owners) + e.monster_owner = own; // using .owner makes the monster non-solid for its master - ++ + e.angles = spawnedby.angles; + } - ++ + monster = strcat("$ spawnfunc_monster_", monster); - ++ + target_spawn_edit_entity(e, monster, world, world, world, world, world); - ++ + return e; +} diff --cc qcsrc/common/monsters/sv_monsters.qc index 1acc47626,000000000..8176dee3d mode 100644,000000..100644 --- a/qcsrc/common/monsters/sv_monsters.qc +++ b/qcsrc/common/monsters/sv_monsters.qc @@@ -1,1124 -1,0 +1,1124 @@@ +// ========================= - // SVQC Monster Properties ++// SVQC Monster Properties +// ========================= + + +void monster_item_spawn() +{ + if(self.monster_loot) + self.monster_loot(); + + self.gravity = 1; + self.reset = SUB_Remove; + self.noalign = TRUE; + self.velocity = randomvec() * 175 + '0 0 325'; + self.classname = "droppedweapon"; // hax + self.item_spawnshieldtime = time + 0.7; + + SUB_SetFade(self, time + autocvar_g_monsters_drop_time, 1); +} + +void monster_dropitem() +{ + if(!self.candrop || !self.monster_loot) + return; + + vector org = self.origin + ((self.mins + self.maxs) * 0.5); + entity e = spawn(); + + setorigin(e, org); + + e.monster_loot = self.monster_loot; + + other = e; + MUTATOR_CALLHOOK(MonsterDropItem); + e = other; + + if(e) + { + e.think = monster_item_spawn; + e.nextthink = time + 0.3; + } +} + +float Monster_SkillModifier() +{ + float t = 0.5+self.monster_skill*((1.2-0.3)/10); - ++ + return t; +} + +float monster_isvalidtarget (entity targ, entity ent) +{ + if(!targ || !ent) + return FALSE; // someone doesn't exist + + if(targ == ent) + return FALSE; // don't attack ourselves + + traceline(ent.origin, targ.origin, MOVE_NORMAL, ent); + + if(trace_ent != targ) + return FALSE; + + if(targ.vehicle_flags & VHF_ISVEHICLE) + if(!((get_monsterinfo(ent.monsterid)).spawnflags & MON_FLAG_RANGED)) + return FALSE; // melee attacks are useless against vehicles + + if(time < game_starttime) + return FALSE; // monsters do nothing before the match has started + + if(vlen(targ.origin - ent.origin) >= ent.target_range) + return FALSE; // enemy is too far away + + if(targ.takedamage == DAMAGE_NO) + return FALSE; // enemy can't be damaged + + if(targ.items & IT_INVISIBILITY) + return FALSE; // enemy is invisible + + if(substring(targ.classname, 0, 10) == "onslaught_") + return FALSE; // don't attack onslaught targets + + if(IS_SPEC(targ) || IS_OBSERVER(targ)) + return FALSE; // enemy is a spectator + + if(!(targ.vehicle_flags & VHF_ISVEHICLE)) + if(targ.deadflag != DEAD_NO || ent.deadflag != DEAD_NO || targ.health <= 0 || ent.health <= 0) + return FALSE; // enemy/self is dead + + if(ent.monster_owner == targ) + return FALSE; // don't attack our master + + if(targ.monster_owner == ent) + return FALSE; // don't attack our pet + + if(!(targ.vehicle_flags & VHF_ISVEHICLE)) + if(targ.flags & FL_NOTARGET) + return FALSE; // enemy can't be targeted + + if(!autocvar_g_monsters_typefrag) + if(targ.BUTTON_CHAT) + return FALSE; // no typefragging! + + if(SAME_TEAM(targ, ent)) + return FALSE; // enemy is on our team - ++ + if (targ.frozen) + return FALSE; // ignore frozen + + if(autocvar_g_monsters_target_infront || ent.spawnflags & MONSTERFLAG_INFRONT) + if(ent.enemy != targ) + { + float dot; + + makevectors (ent.angles); + dot = normalize (targ.origin - ent.origin) * v_forward; + + if(dot <= 0.3) + return FALSE; + } + + return TRUE; +} + +entity FindTarget (entity ent) +{ + if(MUTATOR_CALLHOOK(MonsterFindTarget)) { return ent.enemy; } // Handled by a mutator + + entity head, closest_target = world; + head = findradius(ent.origin, ent.target_range); + + while(head) // find the closest acceptable target to pass to + { + if(head.monster_attack) + if(monster_isvalidtarget(head, ent)) + { + // if it's a player, use the view origin as reference (stolen from RadiusDamage functions in g_damage.qc) + vector head_center = CENTER_OR_VIEWOFS(head); + vector ent_center = CENTER_OR_VIEWOFS(ent); + + //if(ctf_CheckPassDirection(head_center, ent_center, ent.v_angle, head.WarpZone_findradius_nearest)) + if(closest_target) + { + vector closest_target_center = CENTER_OR_VIEWOFS(closest_target); + if(vlen(ent_center - head_center) < vlen(ent_center - closest_target_center)) + { closest_target = head; } + } + else { closest_target = head; } + } + + head = head.chain; + } + + return closest_target; +} + +void MonsterTouch () +{ + if(other == world) + return; + + if(self.enemy != other) + if(!(other.flags & FL_MONSTER)) + if(monster_isvalidtarget(other, self)) + self.enemy = other; +} + +string get_monster_model_datafilename(string m, float sk, string fil) +{ + if(m) + m = strcat(m, "_"); + else + m = "models/monsters/*_"; + if(sk >= 0) + m = strcat(m, ftos(sk)); + else + m = strcat(m, "*"); + return strcat(m, ".", fil); +} + +void PrecacheMonsterSounds(string f) +{ + float fh; + string s; + fh = fopen(f, FILE_READ); + if(fh < 0) + return; + while((s = fgets(fh))) + { + if(tokenize_console(s) != 3) + { + dprint("Invalid sound info line: ", s, "\n"); + continue; + } + PrecacheGlobalSound(strcat(argv(1), " ", argv(2))); + } + fclose(fh); +} + +void precache_monstersounds() +{ + string m = (get_monsterinfo(self.monsterid)).model; + float globhandle, n, i; + string f; + + globhandle = search_begin(strcat(m, "_*.sounds"), TRUE, FALSE); + if (globhandle < 0) + return; + n = search_getsize(globhandle); + for (i = 0; i < n; ++i) + { + //print(search_getfilename(globhandle, i), "\n"); + f = search_getfilename(globhandle, i); + PrecacheMonsterSounds(f); + } + search_end(globhandle); +} + +void ClearMonsterSounds() +{ +#define _MSOUND(m) if(self.monstersound_##m) { strunzone(self.monstersound_##m); self.monstersound_##m = string_null; } + ALLMONSTERSOUNDS +#undef _MSOUND +} + +.string GetMonsterSoundSampleField(string type) +{ + GetMonsterSoundSampleField_notFound = 0; + switch(type) + { +#define _MSOUND(m) case #m: return monstersound_##m; + ALLMONSTERSOUNDS +#undef _MSOUND + } + GetMonsterSoundSampleField_notFound = 1; + return string_null; +} + +float LoadMonsterSounds(string f, float first) +{ + float fh; + string s; + var .string field; + fh = fopen(f, FILE_READ); + if(fh < 0) + { + dprint("Monster sound file not found: ", f, "\n"); + return 0; + } + while((s = fgets(fh))) + { + if(tokenize_console(s) != 3) + continue; + field = GetMonsterSoundSampleField(argv(0)); + if(GetMonsterSoundSampleField_notFound) + continue; + if(self.field) + strunzone(self.field); + self.field = strzone(strcat(argv(1), " ", argv(2))); + } + fclose(fh); + return 1; +} + +.float skin_for_monstersound; +void UpdateMonsterSounds() +{ + entity mon = get_monsterinfo(self.monsterid); + + if(self.skin == self.skin_for_monstersound) + return; + self.skin_for_monstersound = self.skin; + ClearMonsterSounds(); + //LoadMonsterSounds("sound/monsters/default.sounds", 1); + if(!autocvar_g_debug_defaultsounds) + if(!LoadMonsterSounds(get_monster_model_datafilename(mon.model, self.skin, "sounds"), 0)) + LoadMonsterSounds(get_monster_model_datafilename(mon.model, 0, "sounds"), 0); +} + +void MonsterSound(.string samplefield, float sound_delay, float delaytoo, float chan) +{ + if(delaytoo && time < self.msound_delay) + return; // too early + GlobalSound(self.samplefield, chan, VOICETYPE_PLAYERSOUND); + + self.msound_delay = time + sound_delay; +} + +void monster_makevectors(entity e) +{ + vector v; + + v = e.origin + (e.mins + e.maxs) * 0.5; + self.v_angle = vectoangles(v - (self.origin + self.view_ofs)); + self.v_angle_x = -self.v_angle_x; + + makevectors(self.v_angle); +} + +float monster_melee(entity targ, float damg, float anim, float er, float anim_finished, float deathtype, float dostop) +{ + if (self.health <= 0) + return FALSE; // attacking while dead?! + + if(dostop) + { + self.velocity_x = 0; + self.velocity_y = 0; + self.state = MONSTER_STATE_ATTACK_MELEE; + } + + self.frame = anim; + + if(anim_finished != 0) + self.attack_finished_single = time + anim_finished; + + monster_makevectors(targ); + + traceline(self.origin + self.view_ofs, self.origin + v_forward * er, 0, self); + + if(trace_ent.takedamage) + Damage(trace_ent, self, self, damg * Monster_SkillModifier(), deathtype, trace_ent.origin, normalize(trace_ent.origin - self.origin)); + + return TRUE; +} + +void Monster_CheckMinibossFlag () +{ + if(MUTATOR_CALLHOOK(MonsterCheckBossFlag)) + return; + + float chance = random() * 100; + + // g_monsters_miniboss_chance cvar or spawnflags 64 causes a monster to be a miniboss + if ((self.spawnflags & MONSTERFLAG_MINIBOSS) || (chance < autocvar_g_monsters_miniboss_chance)) + { + self.health += autocvar_g_monsters_miniboss_healthboost; + self.effects |= EF_RED; + if(!self.weapon) + self.weapon = WEP_NEX; + } +} + +float Monster_CanRespawn(entity ent) +{ + other = ent; + if(ent.deadflag == DEAD_DEAD) // don't call when monster isn't dead + if(MUTATOR_CALLHOOK(MonsterRespawn)) + return TRUE; // enabled by a mutator + + if(ent.spawnflags & MONSTERFLAG_NORESPAWN) + return FALSE; + + if(!autocvar_g_monsters_respawn) + return FALSE; + + return TRUE; +} + +void Monster_Fade () +{ + if(Monster_CanRespawn(self)) + { + self.monster_respawned = TRUE; + self.think = self.monster_spawnfunc; + self.nextthink = time + self.respawntime; + self.ltime = 0; + self.deadflag = DEAD_RESPAWNING; + if(self.spawnflags & MONSTER_RESPAWN_DEATHPOINT) + { + self.pos1 = self.origin; + self.pos2 = self.angles; + } + self.event_damage = func_null; + self.takedamage = DAMAGE_NO; + setorigin(self, self.pos1); + self.angles = self.pos2; + self.health = self.max_health; + setmodel(self, "null"); + } + else + SUB_SetFade(self, time + 3, 1); +} + +float Monster_CanJump (vector vel) +{ + if(self.state) + return FALSE; // already attacking + if(!(self.flags & FL_ONGROUND)) + return FALSE; // not on the ground + if(self.health <= 0) + return FALSE; // called when dead? + if(time < self.attack_finished_single) + return FALSE; // still attacking + + vector old = self.velocity; + + self.velocity = vel; + tracetoss(self, self); + self.velocity = old; + if (trace_ent != self.enemy) + return FALSE; + + return TRUE; +} + +float monster_leap (float anm, void() touchfunc, vector vel, float anim_finished) +{ + if(!Monster_CanJump(vel)) + return FALSE; + + self.frame = anm; + self.state = MONSTER_STATE_ATTACK_LEAP; + self.touch = touchfunc; + self.origin_z += 1; + self.velocity = vel; + self.flags &= ~FL_ONGROUND; + + self.attack_finished_single = time + anim_finished; + + return TRUE; +} + +void monster_checkattack(entity e, entity targ) +{ + if(e == world) + return; + if(targ == world) + return; + + if(!e.monster_attackfunc) + return; + + if(time < e.attack_finished_single) + return; + + if(vlen(targ.origin - e.origin) <= e.attack_range) + if(e.monster_attackfunc(MONSTER_ATTACK_MELEE)) + { + MonsterSound(monstersound_melee, 0, FALSE, CH_VOICE); + return; + } + + if(vlen(targ.origin - e.origin) > e.attack_range) + if(e.monster_attackfunc(MONSTER_ATTACK_RANGED)) + { + MonsterSound(monstersound_ranged, 0, FALSE, CH_VOICE); + return; + } +} + +void monster_use () +{ + if(!self.enemy) + if(self.health > 0) + if(monster_isvalidtarget(activator, self)) + self.enemy = activator; +} + +.float last_trace; +.float last_enemycheck; // for checking enemy +vector monster_pickmovetarget(entity targ) +{ + // enemy is always preferred target + if(self.enemy) + { + makevectors(self.angles); + self.monster_movestate = MONSTER_MOVE_ENEMY; + self.last_trace = time + 1.2; + return self.enemy.origin; + } + + switch(self.monster_moveflags) + { + case MONSTER_MOVE_OWNER: + { + self.monster_movestate = MONSTER_MOVE_OWNER; + self.last_trace = time + 0.3; + return (self.monster_owner) ? self.monster_owner.origin : self.origin; + } + case MONSTER_MOVE_SPAWNLOC: + { + self.monster_movestate = MONSTER_MOVE_SPAWNLOC; + self.last_trace = time + 2; + return self.pos1; + } + case MONSTER_MOVE_NOMOVE: + { + self.monster_movestate = MONSTER_MOVE_NOMOVE; + self.last_trace = time + 2; + return self.origin; + } + default: + case MONSTER_MOVE_WANDER: + { + vector pos; + self.monster_movestate = MONSTER_MOVE_WANDER; + self.last_trace = time + 2; + + self.angles_y = rint(random() * 500); + makevectors(self.angles); + pos = self.origin + v_forward * 600; + + if(self.flags & FL_FLY || self.flags & FL_SWIM) + if(self.spawnflags & MONSTERFLAG_FLY_VERTICAL) + { + pos_z = random() * 200; + if(random() >= 0.5) + pos_z *= -1; + } + + if(targ) + { + self.last_trace = time + 0.5; + pos = targ.origin; + } + + return pos; + } + } +} + +void monster_move(float runspeed, float walkspeed, float stopspeed, float manim_run, float manim_walk, float manim_idle) +{ + fixedmakevectors(self.angles); + + if(self.target2) + self.goalentity = find(world, targetname, self.target2); + + entity targ; + + if(self.frozen) + { + self.revive_progress = bound(0, self.revive_progress + self.ticrate * self.revive_speed, 1); + self.health = max(1, self.max_health * self.revive_progress); - ++ + WaypointSprite_UpdateHealth(self.sprite, self.health); - ++ + movelib_beak_simple(stopspeed); - ++ + self.frame = manim_idle; - ++ + self.enemy = world; + self.nextthink = time + self.ticrate; + + if(self.revive_progress >= 1) + Unfreeze(self); // wait for next think before attacking + + return; // no moving while frozen + } + + if(self.flags & FL_SWIM) + { + if(self.waterlevel < WATERLEVEL_WETFEET) + { + if(time >= self.last_trace) + { + self.fish_wasdrowning = TRUE; + self.last_trace = time + 0.4; + + Damage (self, world, world, 2, DEATH_DROWN, self.origin, '0 0 0'); + self.angles = '90 90 0'; + if(random() < 0.5) + { + self.velocity_y += random() * 50; + self.velocity_x -= random() * 50; + } + else + { + self.velocity_y -= random() * 50; + self.velocity_x += random() * 50; + } + self.velocity_z += random() * 150; + } + + + self.movetype = MOVETYPE_BOUNCE; + //self.velocity_z = -200; + + return; + } + else if(self.fish_wasdrowning) + { + self.fish_wasdrowning = FALSE; + self.angles_x = 0; + self.movetype = MOVETYPE_WALK; + } + } + + targ = self.goalentity; + + monster_target = targ; + monster_speed_run = runspeed; + monster_speed_walk = walkspeed; + + if(MUTATOR_CALLHOOK(MonsterMove) || gameover || (round_handler_IsActive() && !round_handler_IsRoundStarted()) || time < game_starttime || (autocvar_g_campaign && !campaign_bots_may_start) || time < self.spawn_time) + { + runspeed = walkspeed = 0; + if(time >= self.spawn_time) + self.frame = manim_idle; + movelib_beak_simple(stopspeed); + return; + } + + targ = monster_target; + runspeed = bound(0, monster_speed_run * Monster_SkillModifier(), runspeed * 2); // limit maxspeed to prevent craziness + walkspeed = bound(0, monster_speed_walk * Monster_SkillModifier(), walkspeed * 2); // limit maxspeed to prevent craziness - ++ + if(time < self.spider_slowness) + { + runspeed *= 0.5; + walkspeed *= 0.5; + } + + if(teamplay) + if(autocvar_g_monsters_teams) + if(DIFF_TEAM(self.monster_owner, self)) + self.monster_owner = world; + + if(self.enemy && self.enemy.health < 1) + self.enemy = world; // enough! + + if(time >= self.last_enemycheck) + { + if(!monster_isvalidtarget(self.enemy, self)) + self.enemy = world; + + if(!self.enemy) + { + self.enemy = FindTarget(self); + if(self.enemy) + MonsterSound(monstersound_sight, 0, FALSE, CH_VOICE); + } + + self.last_enemycheck = time + 0.5; + } + + if(self.state == MONSTER_STATE_ATTACK_MELEE && time >= self.attack_finished_single) + self.state = 0; + + if(self.state != MONSTER_STATE_ATTACK_MELEE) // don't move if set + if(time >= self.last_trace || self.enemy) // update enemy instantly + self.moveto = monster_pickmovetarget(targ); + + if(!self.enemy) + MonsterSound(monstersound_idle, 7, TRUE, CH_VOICE); + + if(self.state != MONSTER_STATE_ATTACK_LEAP && self.state != MONSTER_STATE_ATTACK_MELEE) + self.steerto = steerlib_attract2(self.moveto, 0.5, 500, 0.95); + + if(self.state == MONSTER_STATE_ATTACK_LEAP && (self.flags & FL_ONGROUND)) + { + self.state = 0; + self.touch = MonsterTouch; + } + + //self.steerto = steerlib_attract2(self.moveto, 0.5, 500, 0.95); + + float turny = 0; + vector real_angle = vectoangles(self.steerto) - self.angles; + + if(self.state != MONSTER_STATE_ATTACK_LEAP && self.state != MONSTER_STATE_ATTACK_MELEE) + turny = 20; + + if(self.flags & FL_SWIM) + turny = vlen(self.angles - self.moveto); + + if(turny) + { + turny = bound(turny * -1, shortangle_f(real_angle_y, self.angles_y), turny); + self.angles_y += turny; + } + + if(self.state == MONSTER_STATE_ATTACK_MELEE) + self.moveto = self.origin; + + if(self.enemy && self.enemy.vehicle) + runspeed = 0; + + if(((self.flags & FL_FLY) || (self.flags & FL_SWIM)) && self.spawnflags & MONSTERFLAG_FLY_VERTICAL) + v_forward = normalize(self.moveto - self.origin); + else + self.moveto_z = self.origin_z; + + if(vlen(self.origin - self.moveto) > 64) + { + if(self.flags & FL_FLY || self.flags & FL_SWIM) + movelib_move_simple(v_forward, ((self.enemy) ? runspeed : walkspeed), 0.6); + else + movelib_move_simple_gravity(v_forward, ((self.enemy) ? runspeed : walkspeed), 0.6); + + if(time > self.pain_finished) + if(time > self.attack_finished_single) + if(vlen(self.velocity) > 10) + self.frame = ((self.enemy) ? manim_run : manim_walk); + else + self.frame = manim_idle; + } + else + { + entity e = find(world, targetname, self.target2); + if(e.target2) + self.target2 = e.target2; + else if(e.target) + self.target2 = e.target; + + movelib_beak_simple(stopspeed); + if(time > self.attack_finished_single) + if(time > self.pain_finished) + if (vlen(self.velocity) <= 30) + self.frame = manim_idle; + } + + monster_checkattack(self, self.enemy); +} + +void monster_remove(entity mon) +{ + if(!mon) + return; // nothing to remove - ++ + pointparticles(particleeffectnum("item_pickup"), mon.origin, '0 0 0', 1); - ++ + if(mon.weaponentity) + remove(mon.weaponentity); - ++ + if(mon.iceblock) + remove(mon.iceblock); - ++ + WaypointSprite_Kill(mon.sprite); - ++ + remove(mon); +} + +void monster_dead_think() +{ + self.nextthink = time + self.ticrate; - ++ + CSQCMODEL_AUTOUPDATE(); + + if(self.ltime != 0) + if(time >= self.ltime) + { + Monster_Fade(); + return; + } +} + +void monsters_setstatus() +{ + self.stat_monsters_total = monsters_total; + self.stat_monsters_killed = monsters_killed; +} + +void Monster_Appear() +{ + self.enemy = activator; + self.spawnflags &= ~MONSTERFLAG_APPEAR; + self.monster_spawnfunc(); +} + +float Monster_CheckAppearFlags(entity ent) +{ + if(!(ent.spawnflags & MONSTERFLAG_APPEAR)) + return FALSE; + + ent.think = func_null; + ent.nextthink = 0; + ent.use = Monster_Appear; + ent.flags = FL_MONSTER; // set so this monster can get butchered + + return TRUE; +} + +void monsters_reset() +{ + setorigin(self, self.pos1); + self.angles = self.pos2; - ++ + Unfreeze(self); // remove any icy remains + + self.health = self.max_health; + self.velocity = '0 0 0'; + self.enemy = world; + self.goalentity = world; + self.attack_finished_single = 0; + self.moveto = self.origin; +} + +void monsters_corpse_damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) +{ + self.health -= damage; + + Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, self, attacker); + + if(self.health <= -100) // 100 health until gone? + { + Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, self, attacker); - ++ + if(IS_CLIENT(self.realowner)) + if(!self.monster_respawned) + self.realowner.monstercount -= 1; + + self.think = SUB_Remove; + self.nextthink = time + 0.1; + } +} + +void monster_die(entity attacker, float gibbed) +{ + self.think = monster_dead_think; + self.nextthink = time; + self.ltime = time + 5; - ++ + if ( self.frozen ) + { + Unfreeze(self); // remove any icy remains + self.health = 0; // reset by Unfreeze + } - ++ + monster_dropitem(); + + MonsterSound(monstersound_death, 0, FALSE, CH_VOICE); + + if(!(self.spawnflags & MONSTERFLAG_SPAWNED) && !self.monster_respawned) + monsters_killed += 1; - ++ + if(IS_PLAYER(attacker)) - if( autocvar_g_monsters_score_spawned || ++ if( autocvar_g_monsters_score_spawned || + ( !(self.spawnflags & MONSTERFLAG_SPAWNED) && !self.monster_respawned) ) + PlayerScore_Add(attacker, SP_SCORE, +autocvar_g_monsters_score_kill); + + if(!Monster_CanRespawn(self) || gibbed) + { + // number of monsters spawned with mobspawn command + totalspawned -= 1; - ++ + if(IS_CLIENT(self.realowner)) + if(!self.monster_respawned) + self.realowner.monstercount -= 1; + } + + if(self.candrop && self.weapon) + W_ThrowNewWeapon(self, self.weapon, 0, self.origin, randomvec() * 150 + '0 0 325'); + + self.event_damage = monsters_corpse_damage; + self.solid = SOLID_CORPSE; + self.takedamage = DAMAGE_AIM; + self.deadflag = DEAD_DEAD; + self.enemy = world; + self.movetype = MOVETYPE_TOSS; + self.moveto = self.origin; + self.touch = MonsterTouch; // reset incase monster was pouncing + self.reset = func_null; + self.state = 0; + self.attack_finished_single = 0; + + if(!(self.flags & FL_FLY)) + self.velocity = '0 0 0'; + + MON_ACTION(self.monsterid, MR_DEATH); +} + +void monsters_damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) +{ - ++ + if(self.frozen && deathtype != DEATH_KILL) + return; + + if(time < self.pain_finished && deathtype != DEATH_KILL) + return; + + if(time < self.spawnshieldtime && deathtype != DEATH_KILL) + return; + + vector v; + float take, save; + + v = healtharmor_applydamage(self.armorvalue, self.m_armor_blockpercent, deathtype, damage); + take = v_x; + save = v_y; + + self.health -= take; - ++ + WaypointSprite_UpdateHealth(self.sprite, self.health); + + self.dmg_time = time; + + if(sound_allowed(MSG_BROADCAST, attacker) && deathtype != DEATH_DROWN) + spamsound (self, CH_PAIN, "misc/bodyimpact1.wav", VOL_BASE, ATTEN_NORM); // FIXME: PLACEHOLDER + + self.velocity += force * self.damageforcescale; + + if(deathtype != DEATH_DROWN) + { + Violence_GibSplash_At(hitloc, force, 2, bound(0, take, 200) / 16, self, attacker); + if (take > 50) + Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, self, attacker); + if (take > 100) + Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, self, attacker); + } + + if(self.health <= 0) + { + if(deathtype == DEATH_KILL) + self.candrop = FALSE; // killed by mobkill command + + // TODO: fix this? + activator = attacker; + other = self.enemy; + SUB_UseTargets(); + self.target2 = self.oldtarget2; // reset to original target on death, incase we respawn + + monster_die(attacker, (self.health <= -100 || deathtype == DEATH_KILL)); - ++ + WaypointSprite_Kill(self.sprite); - ++ + frag_attacker = attacker; + frag_target = self; + MUTATOR_CALLHOOK(MonsterDies); + + if(self.health <= -100 || deathtype == DEATH_KILL) // check if we're already gibbed + { + Violence_GibSplash(self, 1, 0.5, attacker); + + self.think = SUB_Remove; + self.nextthink = time + 0.1; + } + } +} + +void monster_setupcolors() +{ + if(IS_PLAYER(self.monster_owner)) + self.colormap = self.monster_owner.colormap; + else if(teamplay && self.team) + self.colormap = 1024 + (self.team - 1) * 17; + else + { + if(self.monster_skill <= MONSTER_SKILL_EASY) + self.colormap = 1029; + else if(self.monster_skill <= MONSTER_SKILL_MEDIUM) + self.colormap = 1027; + else if(self.monster_skill <= MONSTER_SKILL_HARD) + self.colormap = 1038; + else if(self.monster_skill <= MONSTER_SKILL_INSANE) + self.colormap = 1028; + else if(self.monster_skill <= MONSTER_SKILL_NIGHTMARE) + self.colormap = 1032; + else + self.colormap = 1024; + } +} + +void monster_think() +{ + self.think = monster_think; + self.nextthink = self.ticrate; - ++ + if(self.ltime) + if(time >= self.ltime) + { + Damage(self, self, self, self.health + self.max_health, DEATH_KILL, self.origin, self.origin); + return; + } + + MON_ACTION(self.monsterid, MR_THINK); - ++ + CSQCMODEL_AUTOUPDATE(); +} + +float monster_spawn() +{ + MON_ACTION(self.monsterid, MR_SETUP); + + if(!self.monster_respawned) + { + Monster_CheckMinibossFlag(); + self.health *= Monster_SkillModifier(); + } + + self.max_health = self.health; + self.pain_finished = self.nextthink; - ++ + if(IS_PLAYER(self.monster_owner)) + self.effects |= EF_DIMLIGHT; + + if(!self.monster_respawned) + if(!self.skin) + self.skin = rint(random() * 4); + + if(!self.attack_range) + self.attack_range = autocvar_g_monsters_attack_range; - ++ + precache_monstersounds(); + UpdateMonsterSounds(); + + if(teamplay) + self.monster_attack = TRUE; // we can have monster enemies in team games - ++ + MonsterSound(monstersound_spawn, 0, FALSE, CH_VOICE); - ++ + WaypointSprite_Spawn(M_NAME(self.monsterid), 0, 1024, self, '0 0 1' * (self.maxs_z + 15), world, self.team, self, sprite, TRUE, RADARICON_DANGER, ((self.team) ? Team_ColorRGB(self.team) : '1 0 0')); + WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health); + WaypointSprite_UpdateHealth(self.sprite, self.health); + + self.think = monster_think; + self.nextthink = time + self.ticrate; - ++ + if(MUTATOR_CALLHOOK(MonsterSpawn)) + return FALSE; - ++ + return TRUE; +} + +float monster_initialize(float mon_id, float nodrop) +{ + if(!autocvar_g_monsters) + return FALSE; + + entity mon = get_monsterinfo(mon_id); - ++ + if(!self.monster_skill) + self.monster_skill = cvar("g_monsters_skill"); + + // support for quake style removing monsters based on skill + if(self.monster_skill == MONSTER_SKILL_EASY) if(self.spawnflags & MONSTERSKILL_NOTEASY) { return FALSE; } + if(self.monster_skill == MONSTER_SKILL_MEDIUM) if(self.spawnflags & MONSTERSKILL_NOTMEDIUM) { return FALSE; } + if(self.monster_skill == MONSTER_SKILL_HARD) if(self.spawnflags & MONSTERSKILL_NOTHARD) { return FALSE; } + + if(self.team && !teamplay) + self.team = 0; + + if(!(self.spawnflags & MONSTERFLAG_SPAWNED)) // naturally spawned monster + if(!self.monster_respawned) + monsters_total += 1; + + setmodel(self, mon.model); + setsize(self, mon.mins, mon.maxs); + self.flags = FL_MONSTER; + self.takedamage = DAMAGE_AIM; + self.bot_attack = TRUE; + self.iscreature = TRUE; + self.teleportable = TRUE; + self.damagedbycontents = TRUE; + self.monsterid = mon_id; + self.damageforcescale = 0; + self.event_damage = monsters_damage; + self.touch = MonsterTouch; + self.use = monster_use; + self.solid = SOLID_BBOX; + self.movetype = MOVETYPE_WALK; + self.spawnshieldtime = time + autocvar_g_monsters_spawnshieldtime; + self.enemy = world; + self.velocity = '0 0 0'; + self.moveto = self.origin; + self.pos1 = self.origin; + self.pos2 = self.angles; + self.reset = monsters_reset; + self.netname = mon.netname; + self.monster_name = M_NAME(mon_id); + self.candrop = TRUE; + self.view_ofs = '0 0 1' * (self.maxs_z * 0.5); + self.oldtarget2 = self.target2; + self.deadflag = DEAD_NO; + self.scale = 1; + self.noalign = nodrop; + self.spawn_time = time; + self.spider_slowness = 0; + self.gravity = 1; + self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP; - ++ + if(autocvar_g_fullbrightplayers) + self.effects |= EF_FULLBRIGHT; - ++ + if(autocvar_g_nodepthtestplayers) + self.effects |= EF_NODEPTHTEST; + + if(mon.spawnflags & MONSTER_TYPE_SWIM) + self.flags |= FL_SWIM; + + if(mon.spawnflags & MONSTER_TYPE_FLY) + { + self.flags |= FL_FLY; + self.movetype = MOVETYPE_FLY; + } + + if(mon.spawnflags & MONSTER_SIZE_BROKEN) + self.scale = 1.3; + + if(!self.ticrate) + self.ticrate = autocvar_g_monsters_think_delay; + + self.ticrate = bound(sys_frametime, self.ticrate, 60); + + if(!self.m_armor_blockpercent) + self.m_armor_blockpercent = 0.5; + + if(!self.target_range) + self.target_range = autocvar_g_monsters_target_range; + + if(!self.respawntime) + self.respawntime = autocvar_g_monsters_respawn_delay; + + if(!self.monster_moveflags) + self.monster_moveflags = MONSTER_MOVE_WANDER; - ++ + if(!self.noalign) + { + setorigin(self, self.origin + '0 0 20'); + tracebox(self.origin + '0 0 64', self.mins, self.maxs, self.origin - '0 0 10000', MOVE_WORLDONLY, self); + setorigin(self, trace_endpos); + } - ++ + if(!monster_spawn()) + return FALSE; - ++ + if(!self.monster_respawned) + monster_setupcolors(); - ++ + CSQCMODEL_AUTOINIT(); + + return TRUE; +} diff --cc qcsrc/common/monsters/sv_monsters.qh index 5bdb23b5a,000000000..a35d88805 mode 100644,000000..100644 --- a/qcsrc/common/monsters/sv_monsters.qh +++ b/qcsrc/common/monsters/sv_monsters.qh @@@ -1,87 -1,0 +1,86 @@@ +.string spawnmob; +.float monster_attack; + +.entity monster_owner; // new monster owner entity, fixes non-solid monsters +.float monstercount; // per player monster count + +.float stat_monsters_killed; // stats +.float stat_monsters_total; +float monsters_total; +float monsters_killed; +void monsters_setstatus(); // monsters.qc +.float monster_moveflags; // checks where to move when not attacking + +.float spider_slowness; // special spider timer + +void monster_remove(entity mon); // removes a monster + +.float(float attack_type) monster_attackfunc; +const float MONSTER_ATTACK_MELEE = 1; +const float MONSTER_ATTACK_RANGED = 2; + +.float monster_skill; +const float MONSTER_SKILL_EASY = 1; +const float MONSTER_SKILL_MEDIUM = 3; +const float MONSTER_SKILL_HARD = 5; +const float MONSTER_SKILL_INSANE = 7; +const float MONSTER_SKILL_NIGHTMARE = 10; + +.float fish_wasdrowning; // used to reset a drowning fish's angles if it reaches water again + +.float candrop; + +.float attack_range; + +.float spawn_time; // stop monster from moving around right after spawning + +.string oldtarget2; +.float lastshielded; + +.vector oldangles; + +.float m_armor_blockpercent; + +// monster sounds +// copied from player sounds +.float msound_delay; // temporary antilag system +#define ALLMONSTERSOUNDS \ + _MSOUND(death) \ + _MSOUND(sight) \ + _MSOUND(ranged) \ + _MSOUND(melee) \ + _MSOUND(pain) \ + _MSOUND(spawn) \ - _MSOUND(idle) ++ _MSOUND(idle) + +#define _MSOUND(m) .string monstersound_##m; +ALLMONSTERSOUNDS +#undef _MSOUND + +float GetMonsterSoundSampleField_notFound; + +.float monster_respawned; // used to make sure we're not recounting respawned monster stats + +const float MONSTERSKILL_NOTEASY = 256; // monster will not spawn on skill <= 1 +const float MONSTERSKILL_NOTMEDIUM = 512; // monster will not spawn on skill 2 +const float MONSTERSKILL_NOTHARD = 1024; // monster will not spawn on skill >= 3 + +// new flags +const float MONSTERFLAG_APPEAR = 2; // delay spawn until triggered +const float MONSTERFLAG_NORESPAWN = 4; +const float MONSTERFLAG_FLY_VERTICAL = 8; // fly/swim vertically +const float MONSTERFLAG_INFRONT = 32; // only check for enemies infront of us - const float MONSTERFLAG_MINIBOSS = 64; // monster spawns as mini-boss (also has a chance of naturally becoming one) ++const float MONSTERFLAG_MINIBOSS = 64; // monster spawns as mini-boss (also has a chance of naturally becoming one) +const float MONSTERFLAG_SPAWNED = 16384; // flag for spawned monsters + +.void() monster_spawnfunc; + +.float monster_movestate; // used to tell what the monster is currently doing +const float MONSTER_MOVE_OWNER = 1; // monster will move to owner if in range, or stand still +const float MONSTER_MOVE_WANDER = 2; // monster will ignore owner & wander around +const float MONSTER_MOVE_SPAWNLOC = 3; // monster will move to its spawn location when not attacking +const float MONSTER_MOVE_NOMOVE = 4; // monster simply stands still +const float MONSTER_MOVE_ENEMY = 5; // used only as a movestate + +const float MONSTER_STATE_ATTACK_LEAP = 1; +const float MONSTER_STATE_ATTACK_MELEE = 2; - diff --cc qcsrc/menu/xonotic/mainwindow.c index 120d80213,0af90bbe2..72cecea19 --- a/qcsrc/menu/xonotic/mainwindow.c +++ b/qcsrc/menu/xonotic/mainwindow.c @@@ -198,12 -198,8 +198,12 @@@ void MainWindow_configureMainWindow(ent i = spawnXonoticTeamSelectDialog(); i.configureDialog(i); me.addItemCentered(me, i, i.intendedWidth * eX + i.intendedHeight * eY, SKINALPHAS_MAINMENU_z); - + + i = spawnXonoticMonsterToolsDialog(); + i.configureDialog(i); + me.addItemCentered(me, i, i.intendedWidth * eX + i.intendedHeight * eY, SKINALPHAS_MAINMENU_z * SKINALPHA_DIALOG_SANDBOXTOOLS); - - ++ + // main dialogs/windows me.mainNexposee = n = spawnXonoticNexposee(); /* diff --cc qcsrc/server/cl_client.qc index 5dba8fc43,d803602ad..ebb9ec528 --- a/qcsrc/server/cl_client.qc +++ b/qcsrc/server/cl_client.qc @@@ -167,9 -167,7 +167,9 @@@ void PutObserverInServer (void MUTATOR_CALLHOOK(MakePlayerObserver); Portal_ClearAll(self); - + + Unfreeze(self); - ++ if(self.alivetime) { if(!warmup_stage) @@@ -940,8 -933,8 +940,8 @@@ void ClientKill (void { if(gameover) return; if(self.player_blocked) return; - if(self.freezetag_frozen) return; + if(self.frozen) return; - + ClientKill_TeamChange(0); } @@@ -2431,13 -2415,10 +2436,13 @@@ void PlayerPreThink (void if(frametime) player_anim(); - + // secret status secrets_setstatus(); - + + // monsters status + monsters_setstatus(); - ++ self.dmg_team = max(0, self.dmg_team - autocvar_g_teamdamage_resetspeed * frametime); //self.angles_y=self.v_angle_y + 90; // temp diff --cc qcsrc/server/g_damage.qc index 633f9b166,195829b40..4b7c29731 --- a/qcsrc/server/g_damage.qc +++ b/qcsrc/server/g_damage.qc @@@ -748,13 -677,7 +748,13 @@@ void Damage (entity targ, entity inflic force = force * g_weaponforcefactor; mirrorforce *= g_weaponforcefactor; } - + + if(targ.frozen && deathtype != DEATH_HURTTRIGGER) + { + damage = 0; + force *= 0.2; + } - ++ // should this be changed at all? If so, in what way? frag_attacker = attacker; frag_target = targ; diff --cc qcsrc/server/mutators/gamemode_ctf.qc index 8db0df90d,19cc563f8..862504d72 --- a/qcsrc/server/mutators/gamemode_ctf.qc +++ b/qcsrc/server/mutators/gamemode_ctf.qc @@@ -439,21 -437,18 +439,21 @@@ void ctf_Handle_Return(entity flag, ent ctf_EventLog("return", flag.team, player); // scoring - PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return - PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns + if(IS_PLAYER(player)) + { + PlayerTeamScore_AddScore(player, autocvar_g_ctf_score_return); // reward for return + PlayerScore_Add(player, SP_CTF_RETURNS, 1); // add to count of returns + } TeamScore_AddToTeam(flag.team, ST_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the team who was last carrying it - - if(flag.ctf_dropper) + + if(flag.ctf_dropper) { PlayerScore_Add(flag.ctf_dropper, SP_SCORE, -autocvar_g_ctf_score_penalty_returned); // punish the player who dropped the flag - ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag + ctf_CaptureShield_Update(flag.ctf_dropper, 0); // shield player from picking up flag flag.ctf_dropper.next_take_time = time + autocvar_g_ctf_flag_collect_delay; // set next take time } - + // reset the flag ctf_RespawnFlag(flag); } @@@ -783,10 -778,9 +783,10 @@@ void ctf_FlagTouch( ctf_CheckFlagReturn(self, RETURN_NEEDKILL); return; } - + // special touch behaviors - if(toucher.vehicle_flags & VHF_ISVEHICLE) + if(toucher.frozen) { return; } + else if(toucher.vehicle_flags & VHF_ISVEHICLE) { if(autocvar_g_ctf_allow_vehicle_touch) toucher = toucher.owner; // the player is actually the vehicle owner, not other @@@ -810,13 -799,13 +810,13 @@@ } else if(toucher.deadflag != DEAD_NO) { return; } - switch(self.ctf_status) - { + switch(self.ctf_status) + { case FLAG_BASE: { - if(SAME_TEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self)) + if(SAME_TEAM(toucher, self) && (toucher.flagcarried) && DIFF_TEAM(toucher.flagcarried, self) && !(toucher.flags & FL_MONSTER)) ctf_Handle_Capture(self, toucher, CAPTURE_NORMAL); // toucher just captured the enemies flag to his base - else if(DIFF_TEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time)) + else if(DIFF_TEAM(toucher, self) && (!toucher.flagcarried) && (!toucher.ctf_captureshielded) && (time > toucher.next_take_time) && !(toucher.flags & FL_MONSTER)) ctf_Handle_Pickup(self, toucher, PICKUP_BASE); // toucher just stole the enemies flag break; } diff --cc qcsrc/server/mutators/gamemode_invasion.qc index de8b13e44,000000000..f7442c1ca mode 100644,000000..100644 --- a/qcsrc/server/mutators/gamemode_invasion.qc +++ b/qcsrc/server/mutators/gamemode_invasion.qc @@@ -1,335 -1,0 +1,335 @@@ +void invasion_spawnpoint() +{ + if(!g_invasion) { remove(self); return; } - ++ + self.classname = "invasion_spawnpoint"; +} + +float invasion_PickMonster(float supermonster_count) +{ + if(autocvar_g_invasion_zombies_only) + return MON_ZOMBIE; + + float i; + entity mon; - ++ + RandomSelection_Init(); - ++ + for(i = MON_FIRST; i <= MON_LAST; ++i) + { + mon = get_monsterinfo(i); + if((mon.spawnflags & MONSTER_TYPE_FLY) || (mon.spawnflags & MONSTER_TYPE_SWIM) || (mon.spawnflags & MON_FLAG_SUPERMONSTER && supermonster_count >= 1)) + continue; // flying/swimming monsters not yet supported - ++ + RandomSelection_Add(world, i, "", 1, 1); + } - ++ + return RandomSelection_chosen_float; +} + +entity invasion_PickSpawn() +{ + entity e; - ++ + RandomSelection_Init(); - ++ + for(e = world;(e = find(e, classname, "invasion_spawnpoint")); ) + RandomSelection_Add(e, 0, string_null, 1, 1); - ++ + return RandomSelection_chosen_ent; +} + +void invasion_SpawnChosenMonster(float mon) +{ + entity spawn_point, monster; - ++ + spawn_point = invasion_PickSpawn(); - ++ + if(spawn_point == world) + { + dprint("Warning: couldn't find any invasion_spawnpoint spawnpoints, no monsters will spawn!\n"); + return; + } - ++ + monster = spawnmonster("", mon, spawn_point, spawn_point, spawn_point.origin, FALSE, 2); - ++ + if(roundcnt >= maxrounds) + monster.spawnflags |= MONSTERFLAG_MINIBOSS; +} + +void invasion_SpawnMonsters(float supermonster_count) +{ + float chosen_monster = invasion_PickMonster(supermonster_count); - ++ + invasion_SpawnChosenMonster(chosen_monster); +} + +float Invasion_CheckWinner() +{ + entity head; + if(round_handler_GetEndTime() > 0 && round_handler_GetEndTime() - time <= 0) + { + FOR_EACH_MONSTER(head) + monster_remove(head); - ++ + if(roundcnt >= maxrounds) + { + NextLevel(); + return 1; + } - ++ + Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_OVER); + Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_OVER); + round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit); + return 1; + } - ++ + // boss round + if(roundcnt >= maxrounds) + { + if(numspawned < 1) + { + maxspawned = 1; + invasion_SpawnMonsters(0); + } + } + else + { + float total_alive_monsters = 0, supermonster_count = 0; - ++ + FOR_EACH_MONSTER(head) if(head.health > 0) + { + if((get_monsterinfo(head.monsterid)).spawnflags & MON_FLAG_SUPERMONSTER) + ++supermonster_count; + ++total_alive_monsters; + } + + if((total_alive_monsters + numkilled) < maxspawned && maxcurrent < 10) // 10 at a time should be plenty + { + if(time >= last_check) + { + invasion_SpawnMonsters(supermonster_count); + last_check = time + 2; + } - ++ + return 0; + } + } - ++ + if(numspawned < 1 || numkilled < maxspawned) + return 0; // nothing has spawned yet, or there are still alive monsters - ++ + if(roundcnt >= maxrounds) + { + NextLevel(); + return 1; + } - ++ + entity winner = world; + float winning_score = 0; - ++ + FOR_EACH_PLAYER(head) + { + float cs = PlayerScore_Add(head, SP_KILLS, 0); + if(cs > winning_score) + { + winning_score = cs; + winner = head; + } + } - ++ + FOR_EACH_MONSTER(head) + monster_remove(head); + + if(winner) + { + Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_ROUND_PLAYER_WIN, winner.netname); + Send_Notification(NOTIF_ALL, world, MSG_INFO, INFO_ROUND_PLAYER_WIN, winner.netname); + } - ++ + round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit); + + return 1; +} + +float Invasion_CheckPlayers() +{ + return TRUE; +} + +void Invasion_RoundStart() +{ + entity e; + float numplayers = 0; + FOR_EACH_PLAYER(e) + { + e.player_blocked = 0; + ++numplayers; + } - ++ + roundcnt += 1; - ++ + invasion_monsterskill = roundcnt + (numplayers * 0.3); - ++ + maxcurrent = 0; + numspawned = 0; + numkilled = 0; - ++ + if(roundcnt > 1) + maxspawned = rint(autocvar_g_invasion_monster_count * (roundcnt * 0.5)); + else + maxspawned = autocvar_g_invasion_monster_count; +} + +MUTATOR_HOOKFUNCTION(invasion_MonsterDies) +{ + if(!self.monster_respawned) + { + numkilled += 1; + maxcurrent -= 1; - ++ + if(IS_PLAYER(frag_attacker)) + PlayerScore_Add(frag_attacker, SP_KILLS, +1); + } - ++ + return FALSE; +} + +MUTATOR_HOOKFUNCTION(invasion_MonsterSpawn) +{ + if(!(self.spawnflags & MONSTERFLAG_SPAWNED)) + { + monster_remove(self); + return FALSE; + } - ++ + if(roundcnt < maxrounds && self.spawnflags & MONSTERFLAG_MINIBOSS) + self.spawnflags &= ~MONSTERFLAG_MINIBOSS; - ++ + if(!self.monster_respawned) + { + numspawned += 1; + maxcurrent += 1; + } - ++ + self.monster_skill = invasion_monsterskill; - ++ + if((get_monsterinfo(self.monsterid)).spawnflags & MON_FLAG_SUPERMONSTER) + Send_Notification(NOTIF_ALL, world, MSG_CENTER, CENTER_INVASION_SUPERMONSTER, M_NAME(self.monsterid)); - ++ + self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_BODY | DPCONTENTS_BOTCLIP | DPCONTENTS_MONSTERCLIP; - ++ + return FALSE; +} + +MUTATOR_HOOKFUNCTION(invasion_PlayerThink) +{ + monsters_total = maxspawned; // TODO: make sure numspawned never exceeds maxspawned + monsters_killed = numkilled; - ++ + return FALSE; +} + +MUTATOR_HOOKFUNCTION(invasion_PlayerSpawn) +{ + self.bot_attack = FALSE; + return FALSE; +} + +MUTATOR_HOOKFUNCTION(invasion_PlayerDamage) +{ + if(IS_PLAYER(frag_attacker) && IS_PLAYER(frag_target) && frag_attacker != frag_target) + { + frag_damage = 0; + frag_force = '0 0 0'; + } - ++ + return FALSE; +} + +MUTATOR_HOOKFUNCTION(invasion_PlayerCommand) +{ + if(MUTATOR_RETURNVALUE) // command was already handled? + return FALSE; - ++ + if(cmd_name == "debuginvasion") + { + sprint(self, strcat("maxspawned = ", ftos(maxspawned), "\n")); + sprint(self, strcat("numspawned = ", ftos(numspawned), "\n")); + sprint(self, strcat("numkilled = ", ftos(numkilled), "\n")); + sprint(self, strcat("roundcnt = ", ftos(roundcnt), "\n")); + sprint(self, strcat("monsters_total = ", ftos(monsters_total), "\n")); + sprint(self, strcat("monsters_killed = ", ftos(monsters_killed), "\n")); + sprint(self, strcat("invasion_monsterskill = ", ftos(invasion_monsterskill), "\n")); - ++ + return TRUE; + } - ++ + return FALSE; +} + +MUTATOR_HOOKFUNCTION(invasion_SetStartItems) +{ + start_health = 200; + start_armorvalue = 200; - ++ + return FALSE; +} + +void invasion_ScoreRules() +{ + ScoreRules_basics(0, 0, 0, FALSE); + ScoreInfo_SetLabel_PlayerScore(SP_KILLS, "frags", SFL_SORT_PRIO_PRIMARY); + ScoreRules_basics_end(); +} + +void invasion_Initialize() +{ + independent_players = 1; // to disable extra useless scores + + invasion_ScoreRules(); - ++ + independent_players = 0; + + round_handler_Spawn(Invasion_CheckPlayers, Invasion_CheckWinner, Invasion_RoundStart); + round_handler_Init(5, autocvar_g_invasion_warmup, autocvar_g_invasion_round_timelimit); - ++ + allowed_to_spawn = TRUE; - ++ + roundcnt = 0; +} + +MUTATOR_DEFINITION(gamemode_invasion) +{ + MUTATOR_HOOK(MonsterDies, invasion_MonsterDies, CBC_ORDER_ANY); + MUTATOR_HOOK(MonsterSpawn, invasion_MonsterSpawn, CBC_ORDER_ANY); + MUTATOR_HOOK(PlayerPreThink, invasion_PlayerThink, CBC_ORDER_ANY); + MUTATOR_HOOK(PlayerSpawn, invasion_PlayerSpawn, CBC_ORDER_ANY); + MUTATOR_HOOK(PlayerDamage_Calculate, invasion_PlayerDamage, CBC_ORDER_ANY); + MUTATOR_HOOK(SV_ParseClientCommand, invasion_PlayerCommand, CBC_ORDER_ANY); + MUTATOR_HOOK(SetStartItems, invasion_SetStartItems, CBC_ORDER_ANY); - ++ + MUTATOR_ONADD + { + if(time > 1) // game loads at time 1 + error("This is a game type and it cannot be added at runtime."); + invasion_Initialize(); - ++ + cvar_settemp("g_monsters", "1"); + } + + MUTATOR_ONROLLBACK_OR_REMOVE + { + // we actually cannot roll back invasion_Initialize here + // BUT: we don't need to! If this gets called, adding always + // succeeds. + } + + MUTATOR_ONREMOVE + { + print("This is a game type and it cannot be removed at runtime."); + return -1; + } + + return 0; +} diff --cc qcsrc/server/mutators/mutator_dodging.qc index b979ee064,3f808499a..6e187b90c --- a/qcsrc/server/mutators/mutator_dodging.qc +++ b/qcsrc/server/mutators/mutator_dodging.qc @@@ -34,8 -34,8 +34,8 @@@ MUTATOR_HOOKFUNCTION(dodging_PlayerPhys float velocity_difference; float clean_up_and_do_nothing; float horiz_speed = autocvar_sv_dodging_horiz_speed; - + - if(self.freezetag_frozen) + if(self.frozen) horiz_speed = autocvar_sv_dodging_horiz_speed_frozen; if (self.deadflag != DEAD_NO) @@@ -168,9 -168,9 +168,9 @@@ MUTATOR_HOOKFUNCTION(dodging_GetPressed tap_direction_x = 0; tap_direction_y = 0; - + float frozen_dodging; - frozen_dodging = (self.freezetag_frozen && autocvar_sv_dodging_frozen); + frozen_dodging = (self.frozen && autocvar_sv_dodging_frozen); float dodge_detected; if (g_dodging == 0) diff --cc qcsrc/server/progs.src index c082d09fa,f29324a47..78f69eb83 --- a/qcsrc/server/progs.src +++ b/qcsrc/server/progs.src @@@ -232,14 -225,8 +231,13 @@@ round_handler.q ../common/explosion_equation.qc +../common/monsters/sv_monsters.qc +../common/monsters/monsters.qc + +../common/monsters/spawn.qc + mutators/base.qc mutators/gamemode_assault.qc - mutators/gamemode_arena.qc mutators/gamemode_ca.qc mutators/gamemode_ctf.qc mutators/gamemode_domination.qc diff --cc qcsrc/server/sv_main.qc index 623b54651,043cb7713..86f2e80fb --- a/qcsrc/server/sv_main.qc +++ b/qcsrc/server/sv_main.qc @@@ -7,11 -7,10 +7,11 @@@ void CreatureFrame (void for(self = world; (self = findfloat(self, damagedbycontents, TRUE)); ) { if (self.movetype == MOVETYPE_NOCLIP) { continue; } - + float vehic = (self.vehicle_flags & VHF_ISVEHICLE); float projectile = (self.flags & FL_PROJECTILE); + float monster = (self.flags & FL_MONSTER); - + if (self.watertype <= CONTENT_WATER && self.waterlevel > 0) // workaround a retarded bug made by id software :P (yes, it's that old of a bug) { if (!(self.flags & FL_INWATER)) diff --cc qcsrc/server/vehicles/vehicles.qc index 1734b0275,1e5537a25..3e9a96f01 --- a/qcsrc/server/vehicles/vehicles.qc +++ b/qcsrc/server/vehicles/vehicles.qc @@@ -642,8 -636,7 +642,8 @@@ void vehicles_enter( self.team = self.owner.team; self.flags -= FL_NOTARGET; + self.monster_attack = TRUE; - + if (IS_REAL_CLIENT(other)) { msg_entity = other; @@@ -816,19 -809,18 +816,19 @@@ void vehicles_exit(float eject _vehicle = vh_vehicle; _vehicle.team = _vehicle.tur_head.team; - + sound (_vehicle, CH_TRIGGER_SINGLE, "misc/null.wav", 1, ATTEN_NORM); - _vehicle.vehicle_hudmodel.viewmodelforclient = _vehicle; + _vehicle.vehicle_hudmodel.viewmodelforclient = _vehicle; _vehicle.phase = time + 1; + _vehicle.monster_attack = FALSE; - + _vehicle.vehicle_exit(eject); - + vehicles_setreturn(); - vehicles_reset_colors(); + vehicles_reset_colors(); _vehicle.owner = world; self = _oldself; - + vehicles_exit_running = FALSE; } diff --cc qcsrc/server/w_shotgun.qc index c0353da69,2fb621401..84573547c --- a/qcsrc/server/w_shotgun.qc +++ b/qcsrc/server/w_shotgun.qc @@@ -96,22 -96,21 +96,22 @@@ void shotgun_meleethink (void + (v_right * swing_factor * autocvar_g_balance_shotgun_secondary_melee_swing_side)); WarpZone_traceline_antilag(self, self.realowner.origin + self.realowner.view_ofs, targpos, FALSE, self, ANTILAG_LATENCY(self.realowner)); - + // draw lightning beams for debugging - //te_lightning2(world, targpos, self.realowner.origin + self.realowner.view_ofs + v_forward * 5 - v_up * 5); + //te_lightning2(world, targpos, self.realowner.origin + self.realowner.view_ofs + v_forward * 5 - v_up * 5); //te_customflash(targpos, 40, 2, '1 1 1'); - + is_player = (IS_PLAYER(trace_ent) || trace_ent.classname == "body"); + is_monster = (trace_ent.flags & FL_MONSTER); if((trace_fraction < 1) // if trace is good, apply the damage and remove self - && (trace_ent.takedamage == DAMAGE_AIM) + && (trace_ent.takedamage == DAMAGE_AIM) && (trace_ent != self.swing_alreadyhit) - && (is_player || autocvar_g_balance_shotgun_secondary_melee_nonplayerdamage)) + && ((is_player || is_monster) || autocvar_g_balance_shotgun_secondary_melee_nonplayerdamage)) { target_victim = trace_ent; // so it persists through other calls - + - if(is_player) // this allows us to be able to nerf the non-player damage done in e.g. assault or onslaught. + if(is_player || is_monster) // this allows us to be able to nerf the non-player damage done in e.g. assault or onslaught. swing_damage = (autocvar_g_balance_shotgun_secondary_damage * min(1, swing_factor + 1)); else swing_damage = (autocvar_g_balance_shotgun_secondary_melee_nonplayerdamage * min(1, swing_factor + 1));