]> de.git.xonotic.org Git - xonotic/xonotic-data.pk3dir.git/commitdiff
Merge branch 'master' into mario/monsters
authorMario <mario.mario@y7mail.com>
Sun, 3 Mar 2013 21:30:21 +0000 (08:30 +1100)
committerMario <mario.mario@y7mail.com>
Sun, 3 Mar 2013 21:30:21 +0000 (08:30 +1100)
119 files changed:
commands.cfg
defaultXonotic.cfg
gamemodes.cfg
models/monsters/demon.mdl [new file with mode: 0644]
models/monsters/demon.mdl.framegroups [new file with mode: 0644]
models/monsters/demon.txt [new file with mode: 0644]
models/monsters/dog.dpm [new file with mode: 0644]
models/monsters/dog.dpm.framegroups [new file with mode: 0644]
models/monsters/dog.txt [new file with mode: 0644]
models/monsters/fish.mdl [new file with mode: 0644]
models/monsters/fish.mdl.framegroups [new file with mode: 0644]
models/monsters/gpl.txt [new file with mode: 0644]
models/monsters/hknight.mdl [new file with mode: 0644]
models/monsters/hknight.mdl.framegroups [new file with mode: 0644]
models/monsters/hknight.txt [new file with mode: 0644]
models/monsters/knight.mdl [new file with mode: 0644]
models/monsters/knight.mdl.framegroups [new file with mode: 0644]
models/monsters/knight.txt [new file with mode: 0644]
models/monsters/mage.dpm [new file with mode: 0644]
models/monsters/mage.dpm.framegroups [new file with mode: 0644]
models/monsters/monsters.txt [new file with mode: 0644]
models/monsters/ogre.mdl [new file with mode: 0644]
models/monsters/ogre.mdl.framegroups [new file with mode: 0644]
models/monsters/shambler.mdl [new file with mode: 0644]
models/monsters/shambler.mdl.framegroups [new file with mode: 0644]
models/monsters/soldier.zym [new file with mode: 0644]
models/monsters/spider.dpm [new file with mode: 0644]
models/monsters/spider.dpm.framegroups [new file with mode: 0644]
models/monsters/spider.txt [new file with mode: 0644]
models/monsters/tarbaby.mdl [new file with mode: 0644]
models/monsters/tarbaby.mdl.framegroups [new file with mode: 0644]
models/monsters/wizard.mdl [new file with mode: 0644]
models/monsters/wizard.mdl.framegroups [new file with mode: 0644]
models/monsters/wizard.txt [new file with mode: 0644]
models/monsters/zombie.dpm_0.skin [new file with mode: 0644]
models/monsters/zombie.dpm_1.skin [new file with mode: 0644]
models/monsters/zombie.dpm_2.skin [new file with mode: 0644]
models/td/barricade.md3 [new file with mode: 0644]
monster_zombie.cfg [deleted file]
monsters.cfg [new file with mode: 0644]
qcsrc/client/hud.qc
qcsrc/client/projectile.qc
qcsrc/client/scoreboard.qc
qcsrc/common/constants.qh
qcsrc/common/mapinfo.qc
qcsrc/common/mapinfo.qh
qcsrc/menu/classes.c
qcsrc/menu/xonotic/dialog_monstertools.c [new file with mode: 0644]
qcsrc/menu/xonotic/mainwindow.c
qcsrc/server/accuracy.qc
qcsrc/server/autocvars.qh
qcsrc/server/cl_client.qc
qcsrc/server/cl_physics.qc
qcsrc/server/cl_player.qc
qcsrc/server/cl_weapons.qc
qcsrc/server/cl_weaponsystem.qc
qcsrc/server/command/cmd.qc
qcsrc/server/command/cmd.qh
qcsrc/server/command/sv_cmd.qc
qcsrc/server/defs.qh
qcsrc/server/g_damage.qc
qcsrc/server/g_hook.qc
qcsrc/server/g_world.qc
qcsrc/server/miscfunctions.qc
qcsrc/server/monsters/lib/defs.qh [new file with mode: 0644]
qcsrc/server/monsters/lib/monsters.qc [new file with mode: 0644]
qcsrc/server/monsters/lib/spawn.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/demon.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/dog.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/enforcer.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/fish.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/hknight.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/knight.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/ogre.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/shalrath.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/shambler.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/soldier.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/spawner.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/spider.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/tarbaby.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/wizard.qc [new file with mode: 0644]
qcsrc/server/monsters/monster/zombie.qc [new file with mode: 0644]
qcsrc/server/monsters/monsters.qh [new file with mode: 0644]
qcsrc/server/movelib.qc
qcsrc/server/mutators/base.qh
qcsrc/server/mutators/gamemode_rts.qc [new file with mode: 0644]
qcsrc/server/mutators/gamemode_rts.qh [new file with mode: 0644]
qcsrc/server/mutators/gamemode_td.qc [new file with mode: 0644]
qcsrc/server/mutators/gamemode_td.qh [new file with mode: 0644]
qcsrc/server/mutators/mutator_zombie_apocalypse.qc [new file with mode: 0644]
qcsrc/server/mutators/mutators.qh
qcsrc/server/progs.src
qcsrc/server/sv_main.qc
qcsrc/server/teamplay.qc
qcsrc/server/tturrets/system/system_damage.qc
qcsrc/server/tturrets/system/system_main.qc
qcsrc/server/tturrets/system/system_scoreprocs.qc
qcsrc/server/tturrets/units/unit_fusionreactor.qc
qcsrc/server/tturrets/units/unit_walker.qc
qcsrc/server/vehicles/vehicles.qc
qcsrc/server/w_electro.qc
scripts/barricade.shader [new file with mode: 0644]
scripts/mage.shader [new file with mode: 0644]
scripts/shaderlist.txt
textures/barricade.tga [new file with mode: 0644]
textures/barricade_norm.tga [new file with mode: 0644]
textures/bloodyskull_pants.jpg [new file with mode: 0644]
textures/cerberus/cerberus_text.png [new file with mode: 0644]
textures/cerberus/cerberus_text_pants.png [new file with mode: 0644]
textures/mage.tga [new file with mode: 0644]
textures/marine.tga [new file with mode: 0644]
textures/marine_gloss.tga [new file with mode: 0644]
textures/marine_glow.tga [new file with mode: 0644]
textures/marine_norm.tga [new file with mode: 0644]
textures/marine_pants.tga [new file with mode: 0644]
textures/marine_shirt.tga [new file with mode: 0644]
textures/meat_pants.tga [new file with mode: 0644]
textures/spider/spidertex.tga [new file with mode: 0644]
za.cfg [new file with mode: 0644]

index 413953df5242e03cf5fdc934e237d89860f2cb4c..fda08fc2e4f50d8f62a798e8e23ae0f1621f4723 100644 (file)
@@ -156,6 +156,9 @@ alias reportcvar           "qc_cmd_cmd    reportcvar           ${* ?}" // Old sy
 alias selectteam           "qc_cmd_cmd    selectteam           ${* ?}" // Attempt to choose a team to join into
 alias selfstuff            "qc_cmd_cmd    selfstuff            ${* ?}" // Stuffcmd a command to your own client
 alias sentcvar             "qc_cmd_cmd    sentcvar             ${* ?}" // New system for sending a client cvar to the server
+alias editmob                     "qc_cmd_cmd    mobedit                          ${* ?}" // Edit a monster's properties
+alias killmob                     "qc_cmd_cmd    mobkill                          ${* ?}" // Kill a monster
+alias spawnmob                    "qc_cmd_cmd    mobspawn                         ${* ?}" // Spawn a monster infront of the player
 alias spectate             "qc_cmd_cmd    spectate             ${* ?}" // Become an observer
 alias suggestmap           "qc_cmd_cmd    suggestmap           ${* ?}" // Suggest a map to the mapvote at match end
 //alias tell               "qc_cmd_cmd    tell                 ${* ?}" // Send a message directly to a player
@@ -174,6 +177,11 @@ alias spec "spectate"
 
 // mutator aliases
 alias sandbox "cmd g_sandbox ${* ?}"
+alias spawnturret "cmd turretspawn ${* ?}"
+alias debugmonsters "cmd debugmonsters ${* ?}"
+alias upgradeturret "cmd buffturret ${* ?}"
+alias rmturret "cmd turretremove ${* ?}"
+alias repairturret "cmd repairturret ${* ?}"
 
 
 // ============================================================
@@ -185,6 +193,7 @@ alias allspec              "qc_cmd_sv     allspec              ${* ?}" // Force
 alias anticheat            "qc_cmd_sv     anticheat            ${* ?}" // Create an anticheat report for a client
 alias bbox                 "qc_cmd_sv     bbox                 ${* ?}" // Print detailed information about world size
 alias bot_cmd              "qc_cmd_sv     bot_cmd              ${* ?}" // Control and send commands to bots
+alias butcher                     "qc_cmd_sv     butcher                          ${* ?}" // Remove all monsters on the map
 alias cointoss             "qc_cmd_sv     cointoss             ${* ?}" // Flip a virtual coin and give random result
 alias database             "qc_cmd_sv     database             ${* ?}" // Extra controls of the serverprogs database
 alias defer_clear          "qc_cmd_sv     defer_clear          ${* ?}" // Clear all queued defer commands for a specific client
index 4ea9639e5c3ccdbf81ca2f997586455bb455af4b..a7c08d1942e1c885987df266c6dd46e271dab444 100644 (file)
@@ -436,6 +436,12 @@ seta menu_sandbox_edit_material ""
 
 bind f7 menu_showsandboxtools
 
+seta menu_monsters_spawn_monster ""
+seta menu_monsters_edit_name ""
+seta menu_monsters_edit_skin 0
+seta menu_monsters_edit_color ""
+seta menu_monsters_edit_movetarget 1
+
 set g_playerclip_collisions 1 "0 = disable collision testing against playerclips, might be useful on some defrag maps"
 set g_botclip_collisions 1 "0 = disable collision testing against botclips, might be useful on some defrag maps"
 
@@ -1552,6 +1558,8 @@ exec turrets.cfg
 exec vehicles.cfg
 exec crosshairs.cfg
 exec gamemodes.cfg
+exec monsters.cfg
+exec za.cfg
 
 // load console command aliases and settings
 exec commands.cfg
index 6cff7e5a42666ab32954c8ec8d5abb757b1a09f6..9a62eb26c58edb01407728d193ba471d1b007986 100644 (file)
@@ -38,6 +38,8 @@ alias cl_hook_gamestart_nb
 alias cl_hook_gamestart_cts
 alias cl_hook_gamestart_ka
 alias cl_hook_gamestart_ft
+alias cl_hook_gamestart_td
+alias cl_hook_gamestart_rts
 alias cl_hook_gameend
 alias cl_hook_activeweapon
 
@@ -60,6 +62,8 @@ alias sv_hook_gamestart_nb
 alias sv_hook_gamestart_cts
 alias sv_hook_gamestart_ka
 alias sv_hook_gamestart_ft
+alias sv_hook_gamestart_td
+alias sv_hook_gamestart_rts
 alias sv_hook_gamerestart
 alias sv_hook_gameend
 
@@ -141,6 +145,12 @@ set g_cts_weapon_stay 2
 set g_ft_respawn_waves 0
 set g_ft_respawn_delay 0
 set g_ft_weapon_stay 0
+set g_td_respawn_waves 0
+set g_td_respawn_delay 0
+set g_td_weapon_stay 0
+set g_rts_respawn_waves 0
+set g_rts_respawn_delay 0
+set g_rts_weapon_stay 0
 
 
 // =======
@@ -472,3 +482,42 @@ set g_balance_rune_speed_highspeed                 1.5
 set g_balance_curse_slow_highspeed                     0.6
 set g_balance_rune_speed_combo_highspeed                       0.9
 
+// ===============
+//  tower defense
+// ===============
+set g_td 0 "Tower Defense: protect the generator/s from waves of monsters"
+set g_td_force_settings 0 "if enabled, don't use map settings (monster count, start wave etc.)"
+set g_td_start_wave 1
+set g_td_generator_health 700
+set g_td_generator_damaged_points 20 "player loses this many points if the generator was damaged during the wave"
+set g_td_current_monsters 10 "maximum monsters that can be spawned simultaneously"
+set g_td_monster_count 10
+set g_td_monster_count_increment 5
+set g_td_buildphase_time 20
+set g_td_generator_dontend 0 "don't change maps when a generator is destroyed (only if there is more than 1 generator)"
+set g_td_pvp 0
+set g_td_monsters_skill_start 1 "set to 0 to use g_monsters_skill instead"
+set g_td_monsters_skill_increment 0.1
+set g_td_monsters_spawnshield_time 2
+set g_td_monsters_ignore_turrets 0
+set g_td_max_waves 8
+set g_td_kill_points 5
+set g_td_turretkill_points 3
+set g_td_turret_max 4
+set g_td_turret_plasma_cost 50
+set g_td_turret_mlrs_cost 80
+set g_td_turret_walker_cost 100
+set g_td_tower_buff_cost 70
+set g_td_barricade_damage 10
+set g_td_barricade_cost 20
+set g_td_turret_flac_cost 40
+set g_td_turret_upgrade_cost 100
+set g_td_turret_repair_cost 20
+set g_td_monsters_speed_walk 75
+set g_td_monsters_speed_run 110
+set g_td_monsters_spawn_delay 3
+
+// ====================
+//  real-time strategy
+// ====================
+set g_rts 0 "Real-Time Strategy: defend your generator & turrets by controlling monsters"
\ No newline at end of file
diff --git a/models/monsters/demon.mdl b/models/monsters/demon.mdl
new file mode 100644 (file)
index 0000000..d2a566b
Binary files /dev/null and b/models/monsters/demon.mdl differ
diff --git a/models/monsters/demon.mdl.framegroups b/models/monsters/demon.mdl.framegroups
new file mode 100644 (file)
index 0000000..b87a087
--- /dev/null
@@ -0,0 +1 @@
+1 12 10 1 // demon stand\r14 7 10 1 // demon walk\r22 5 10 1 // demon run\r28 11 10 0 // demon leap\r40 5 10 0 // demon pain\r46 8 10 0 // demon death\r55 14 10 1 // demon melee
\ No newline at end of file
diff --git a/models/monsters/demon.txt b/models/monsters/demon.txt
new file mode 100644 (file)
index 0000000..fa60336
--- /dev/null
@@ -0,0 +1,25 @@
+Fiendish Deamon Doll model
+
+Copyright (C) 2003 Ulrich Gablraith
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+History:
+
+7-5-04
+
+The model was given to Mapes so he could port it into OpenQuartz by permission.
+
+Greg Mapes
\ No newline at end of file
diff --git a/models/monsters/dog.dpm b/models/monsters/dog.dpm
new file mode 100644 (file)
index 0000000..e220158
Binary files /dev/null and b/models/monsters/dog.dpm differ
diff --git a/models/monsters/dog.dpm.framegroups b/models/monsters/dog.dpm.framegroups
new file mode 100644 (file)
index 0000000..9ace1c4
--- /dev/null
@@ -0,0 +1,11 @@
+/*
+Generated framegroups file for dog
+Used by DarkPlaces to simulate frame groups in DPM models.
+*/
+
+1 25 60 1 // dog idle
+26 37 60 1 // dog walk
+63 37 120 1 // dog run
+100 50 60 1 // dog attack
+150 42 60 0 // dog die
+192 25 60 1 // dog pain
diff --git a/models/monsters/dog.txt b/models/monsters/dog.txt
new file mode 100644 (file)
index 0000000..4cd5dde
--- /dev/null
@@ -0,0 +1 @@
+This cerberus model is GPL 2.0 compatible (http://opengameart.org/content/animated-cerberus).
\ No newline at end of file
diff --git a/models/monsters/fish.mdl b/models/monsters/fish.mdl
new file mode 100644 (file)
index 0000000..e1db999
Binary files /dev/null and b/models/monsters/fish.mdl differ
diff --git a/models/monsters/fish.mdl.framegroups b/models/monsters/fish.mdl.framegroups
new file mode 100644 (file)
index 0000000..bf18b81
--- /dev/null
@@ -0,0 +1 @@
+1 17 10 1 // fish attack\r19 19 10 0 // fish death\r41 16 10 1 // fish swim\r59 8 10 0 // fish pain
\ No newline at end of file
diff --git a/models/monsters/gpl.txt b/models/monsters/gpl.txt
new file mode 100644 (file)
index 0000000..6bf61e7
--- /dev/null
@@ -0,0 +1 @@
+All the models in this folder (except zombie.dpm) were copied from the GPL project OpenQuartz 2 (http://www.quakewiki.net/openquartz-2).
\ No newline at end of file
diff --git a/models/monsters/hknight.mdl b/models/monsters/hknight.mdl
new file mode 100644 (file)
index 0000000..a0cd4d7
Binary files /dev/null and b/models/monsters/hknight.mdl differ
diff --git a/models/monsters/hknight.mdl.framegroups b/models/monsters/hknight.mdl.framegroups
new file mode 100644 (file)
index 0000000..cc16b30
--- /dev/null
@@ -0,0 +1 @@
+1 8 10 1 // hellknight stand\r10 19 10 1 // hellknight walk\r30 7 10 1 // hellknight run\r38 4 10 0 // hellknight pain\r43 11 10 0 // hellknight death1\r55 8 10 0 // hellknight death2\r64 15 10 0 // hellknight charge1\r80 13 10 0 // hellknight magic1\r94 12 10 0 // hellknight magic2\r107 5 10 0 // hellknight charge2\r113 9 10 1 // hellknight slice\r123 9 10 1 // hellknight smash\r133 21 10 1 // hellknight weapon attack\r155 10 10 0 //hellknight magic3
\ No newline at end of file
diff --git a/models/monsters/hknight.txt b/models/monsters/hknight.txt
new file mode 100644 (file)
index 0000000..1dffdb7
--- /dev/null
@@ -0,0 +1,25 @@
+Hooded model
+
+Copyright (C) 2003 Ulrich Gablraith
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+History:
+
+7-5-04
+
+The model was given to Mapes so he could port it into OpenQuartz by permission.
+
+Greg Mapes
\ No newline at end of file
diff --git a/models/monsters/knight.mdl b/models/monsters/knight.mdl
new file mode 100644 (file)
index 0000000..36ebd61
Binary files /dev/null and b/models/monsters/knight.mdl differ
diff --git a/models/monsters/knight.mdl.framegroups b/models/monsters/knight.mdl.framegroups
new file mode 100644 (file)
index 0000000..c4acc5e
--- /dev/null
@@ -0,0 +1 @@
+1 8 10 1 // knight stand\r10 7 10 1 // knight run\r18 10 10 1 // knight run attack\r29 2 10 0 // knight pain1\r32 10 10 0 // knight pain2\r43 9 10 1 // knight attack\r53 13 10 1 // knight walk\r67 4 10 0 // knight kneel\r72 4 10 1 // knight standing\r77 9 10 0 // knight death1\r88 10 10 0 // knight death2
\ No newline at end of file
diff --git a/models/monsters/knight.txt b/models/monsters/knight.txt
new file mode 100644 (file)
index 0000000..59cec6c
--- /dev/null
@@ -0,0 +1,25 @@
+Knight model
+
+Copyright (C) 2004 Ulrich Gablraith
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+History:
+
+7-5-04
+
+The model was given to Mapes so he could port it into OpenQuartz by permission.
+
+Greg Mapes
\ No newline at end of file
diff --git a/models/monsters/mage.dpm b/models/monsters/mage.dpm
new file mode 100644 (file)
index 0000000..3686b74
Binary files /dev/null and b/models/monsters/mage.dpm differ
diff --git a/models/monsters/mage.dpm.framegroups b/models/monsters/mage.dpm.framegroups
new file mode 100644 (file)
index 0000000..4a72c65
--- /dev/null
@@ -0,0 +1,11 @@
+/*
+Generated framegroups file for mage
+Used by DarkPlaces to simulate frame groups in DPM models.
+*/
+
+1 31 30 1 // mage idle
+32 31 30 1 // mage walk
+63 16 30 1 // mage attack
+79 16 30 1 // mage hit
+95 31 30 0 // mage die
+126 31 60 1 // mage walk
diff --git a/models/monsters/monsters.txt b/models/monsters/monsters.txt
new file mode 100644 (file)
index 0000000..9e55cb8
--- /dev/null
@@ -0,0 +1,31 @@
+Monsters -Phantom Knight, Magical Knight, Master Knight, Knight, Jack-o-Lantern,
+        -Doll Fiend.
+
+Copyright (C) 2004  Ulrich Galbraith
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Ulrich Galbraith
+ugalbrai@krl.org
+
+
+7-22-04
+
+First release
+
+I had to give some of them proper animation and proper connecting models so they would work.
+
+-Mapes
\ No newline at end of file
diff --git a/models/monsters/ogre.mdl b/models/monsters/ogre.mdl
new file mode 100644 (file)
index 0000000..a0cd4d7
Binary files /dev/null and b/models/monsters/ogre.mdl differ
diff --git a/models/monsters/ogre.mdl.framegroups b/models/monsters/ogre.mdl.framegroups
new file mode 100644 (file)
index 0000000..0d69386
--- /dev/null
@@ -0,0 +1 @@
+1 8 10 1 // ogre stand\r10 15 10 1 // ogre walk\r26 7 10 1 // ogre run\r34 13 10 1 // ogre swing\r48 13 10 1 // ogre smash\r62 5 10 1 // ogre shoot\r68 4 10 0 // ogre pain1\r73 2 10 0 // ogre pain2\r76 5 10 0 // ogre pain3\r82 15 10 0 // ogre pain4\r98 14 10 0 // ogre pain5\r113 13 10 0 // ogre death1\r127 9 10 0 // ogre death2\r137 10 10 0 // ogre pull
\ No newline at end of file
diff --git a/models/monsters/shambler.mdl b/models/monsters/shambler.mdl
new file mode 100644 (file)
index 0000000..2e23dfa
Binary files /dev/null and b/models/monsters/shambler.mdl differ
diff --git a/models/monsters/shambler.mdl.framegroups b/models/monsters/shambler.mdl.framegroups
new file mode 100644 (file)
index 0000000..b003d56
--- /dev/null
@@ -0,0 +1 @@
+1 16 10 1 // shambler stand\r18 11 10 1 // shambler walk\r31 5 10 1 // shambler run\r37 11 10 1 // shambler smash\r49 8 10 1 // shambler swing right\r58 8 10 1 // shambler swing left\r67 11 10 1 // shambler magic\r79 5 10 0 // shambler pain\r85 10 10 0 // shambler death
\ No newline at end of file
diff --git a/models/monsters/soldier.zym b/models/monsters/soldier.zym
new file mode 100644 (file)
index 0000000..1615af4
Binary files /dev/null and b/models/monsters/soldier.zym differ
diff --git a/models/monsters/spider.dpm b/models/monsters/spider.dpm
new file mode 100644 (file)
index 0000000..d0bc9ce
Binary files /dev/null and b/models/monsters/spider.dpm differ
diff --git a/models/monsters/spider.dpm.framegroups b/models/monsters/spider.dpm.framegroups
new file mode 100644 (file)
index 0000000..039a7c3
--- /dev/null
@@ -0,0 +1,9 @@
+/*
+Generated framegroups file for spider
+Used by DarkPlaces to simulate frame groups in DPM models.
+*/
+
+1 60 60 1 // spider idle
+61 41 120 1 // spider walk
+102 24 60 1 // spider attack
+125 10 60 1 // spider attack2
\ No newline at end of file
diff --git a/models/monsters/spider.txt b/models/monsters/spider.txt
new file mode 100644 (file)
index 0000000..7d7cdaf
--- /dev/null
@@ -0,0 +1 @@
+This spider model is GPL 2.0 compatible (http://opengameart.org/content/low-poly-spider-glest).
\ No newline at end of file
diff --git a/models/monsters/tarbaby.mdl b/models/monsters/tarbaby.mdl
new file mode 100644 (file)
index 0000000..db8c318
Binary files /dev/null and b/models/monsters/tarbaby.mdl differ
diff --git a/models/monsters/tarbaby.mdl.framegroups b/models/monsters/tarbaby.mdl.framegroups
new file mode 100644 (file)
index 0000000..7271df4
--- /dev/null
@@ -0,0 +1 @@
+1 24 10 1 // tarbaby walk\r26 24 10 1 // tarbaby run\r51 5 10 0 // tarbaby jump\r57 3 10 1 // tarbaby fly\r61 1 10 0 // tarbaby explode
\ No newline at end of file
diff --git a/models/monsters/wizard.mdl b/models/monsters/wizard.mdl
new file mode 100644 (file)
index 0000000..b130ba0
Binary files /dev/null and b/models/monsters/wizard.mdl differ
diff --git a/models/monsters/wizard.mdl.framegroups b/models/monsters/wizard.mdl.framegroups
new file mode 100644 (file)
index 0000000..c51109a
--- /dev/null
@@ -0,0 +1 @@
+1 14 10 1 // wizard hover\r16 13 10 1 // wizard fly\r30 12 10 1 // wizard magic attack\r43 3 10 0 // wizard pain\r47 7 10 0 // wizard death
\ No newline at end of file
diff --git a/models/monsters/wizard.txt b/models/monsters/wizard.txt
new file mode 100644 (file)
index 0000000..3b00dde
--- /dev/null
@@ -0,0 +1,31 @@
+pterascragg model
+
+Copyright (C) 2000  Dylan "lithiumbat" Sartain
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+History:
+
+7-11-00
+
+First release
+build time: about 2 hrs spread over three days
+model was made as a replacement for the scragg or wizard.
+
+7-13-00
+
+Seth Galbraith (sgalbrai@krl.org):
+Lowered death frame to lie on the ground
+Created head gib model
diff --git a/models/monsters/zombie.dpm_0.skin b/models/monsters/zombie.dpm_0.skin
new file mode 100644 (file)
index 0000000..19ad7a7
--- /dev/null
@@ -0,0 +1,2 @@
+bloodyskull,bloodyskull
+meat,meat
\ No newline at end of file
diff --git a/models/monsters/zombie.dpm_1.skin b/models/monsters/zombie.dpm_1.skin
new file mode 100644 (file)
index 0000000..1561520
--- /dev/null
@@ -0,0 +1,2 @@
+bloodyskull,bloodyskull_alien
+meat,meat_alien
\ No newline at end of file
diff --git a/models/monsters/zombie.dpm_2.skin b/models/monsters/zombie.dpm_2.skin
new file mode 100644 (file)
index 0000000..e00379e
--- /dev/null
@@ -0,0 +1,2 @@
+bloodyskull,bloodyskull_robot
+meat,meat_robot
\ No newline at end of file
diff --git a/models/td/barricade.md3 b/models/td/barricade.md3
new file mode 100644 (file)
index 0000000..8d58d9b
Binary files /dev/null and b/models/td/barricade.md3 differ
diff --git a/monster_zombie.cfg b/monster_zombie.cfg
deleted file mode 100644 (file)
index 65d9e71..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-set g_monster_zombie_respawntime    2
-set g_monster_zombie_movespeed      350
-set g_monster_zombie_health         275
-set g_monster_zombie_stopspeed      90
-set g_monster_zombie_turnspeed      360
-set g_monster_zombie_idle_timer_min 5
-set g_monster_zombie_idle_timer_max 10
-set g_monster_zombie_targetrange    2048
-
-set g_monster_zombie_attack_run_range    128
-set g_monster_zombie_attack_run_hitrange 96
-set g_monster_zombie_attack_run_delay    0.35
-set g_monster_zombie_attack_run_damage   30
-set g_monster_zombie_attack_run_force    300
-
-set g_monster_zombie_attack_stand_range    64
-set g_monster_zombie_attack_stand_hitrange 64
-set g_monster_zombie_attack_stand_delay    0.25
-set g_monster_zombie_attack_stand_damage   60
-set g_monster_zombie_attack_stand_force    250
-
diff --git a/monsters.cfg b/monsters.cfg
new file mode 100644 (file)
index 0000000..bf05631
--- /dev/null
@@ -0,0 +1,207 @@
+// Misc
+set g_monsters 1 "Enable monsters (master switch)"
+set g_monsters_skill 1 "Monster skill (affecting some of their attributes). 1 - easy, 2 - medium, 3 - hard, 4 - insane, 5 - nightmare"
+set g_monsters_miniboss_chance 5
+set g_monsters_miniboss_healthboost 100
+set g_monsters_forcedrop 0 "Force all monsters to drop this item on death. Use g_monsters_drop_* cvars to change forced drop item"
+set g_monsters_drop_type armor "Type of item to drop when forced. Possible values are: health, armor, ammo"
+set g_monsters_drop_size medium "Size of the item monsters drop. Possible health/amor values are: small, medium, large. Possible ammo values are: shells, bullets, cells, rockets"
+set g_monsters_owners 1 "Monsters will not attack their owners if set to 1"
+set g_monsters_teams 1
+set g_monster_spawnshieldtime 2 "Monsters will not take damage for this amount of seconds"
+set g_monsters_typefrag 1
+set g_monsters_healthbars 1 "Show health bars above monsters"
+set g_monsters_giants_only 0
+set g_monsters_nogiants 1
+set g_monsters_target_range 2000
+set g_monsters_respawn 1 "Enable monster respawning"
+set g_monsters_respawn_delay 20 "Monsters respawn in this amount of seconds"
+set g_monsters_score_kill 1 "Get this many points for killing a naturally spawned monster"
+set g_monsters_max 20 "Global maximum player-spawned monsters"
+set g_monsters_max_perplayer 3 "Maximum monsters per-player"
+set g_monsters_skill_easy 2 "Monster easy skill level (used for skill based functions)"
+set g_monsters_skill_normal 4 "Monster normal skill level (used for skill based functions)"
+set g_monsters_skill_hard 5 "Monster hard skill level (used for skill based functions)"
+set g_monsters_skill_insane 7 "Monster insane skill level (used for skill based functions)"
+set g_monsters_skill_nightmare 10 "Monster nightmare skill level (used for skill based functions)"
+set g_monsters_spawn_list "ogre demon shambler knight soldier scrag dog spawn hellknight fish vore enforcer zombie spawner spider" "monsters not listed here will spawn as knights"
+
+// Enforcer
+set g_monster_enforcer 1 "Enable Enforcers"
+set g_monster_enforcer_health 80 "Enforcer health"
+set g_monster_enforcer_drop armor "Enforcer drops this item on death"
+set g_monster_enforcer_drop_size large "Size of the item Enforcers drop. Possible values are: small, medium, large"
+set g_monster_enforcer_speed_walk 75 "Enforcer walk speed"
+set g_monster_enforcer_speed_run 100 "Enforcer run speed"
+set g_monster_enforcer_attack_uzi_bullets 3 "Number of machine gun bullets Enforcer fires"
+
+// Ogre
+set g_monster_ogre 1 "Enable Ogres"
+set g_monster_ogre_health 200 "Ogre health"
+set g_monster_ogre_chainsaw_damage 15 "Ogre chainsaw damage (hits multiple times)"
+set g_monster_ogre_drop ammo "Ogre drops this item on death"
+set g_monster_ogre_drop_size bullets "Size of the item Ogres drop. Possible values are: small, medium, large"
+set g_monster_ogre_speed_walk 100 "Ogre walk speed"
+set g_monster_ogre_speed_run 150 "Ogre run speed"
+set g_monster_ogre_attack_uzi_bullets 3 "Number of machine gun bullets Ogre fires"
+
+// Fiend
+set g_monster_demon 1 "Enable Fiends"
+set g_monster_demon_health 300 "Fiend health"
+set g_monster_demon_attack_jump_damage 40 "Fiend jump attack damage"
+set g_monster_demon_damage 20 "Fiend melee attack damage"
+set g_monster_demon_drop health "Fiend drops this item on death"
+set g_monster_demon_drop_size medium "Size of the item Fiends drop. Possible values are: small, medium, large"
+set g_monster_demon_speed_walk 150 "Fiend walk speed"
+set g_monster_demon_speed_run 300 "Fiend run speed"
+
+// Shambler
+set g_monster_shambler 1 "Enable Shamblers"
+set g_monster_shambler_health 600 "Shambler health"
+set g_monster_shambler_damage 50 "Shambler melee attack damage"
+set g_monster_shambler_attack_lightning_damage 20 "Shambler lightning attack damage per frame"
+set g_monster_shambler_attack_claw_damage 30 "Shambler claw attack damage"
+set g_monster_shambler_drop health "Shambler drops this item on death"
+set g_monster_shambler_drop_size large "Size of the item Shamblers drop. Possible values are: small, medium, large"
+set g_monster_shambler_speed_walk 100 "Shambler walk speed"
+set g_monster_shambler_speed_run 150 "Shambler run speed"
+
+// Knight
+set g_monster_knight 1 "Enable Knights"
+set g_monster_knight_health 75 "Knight Health"
+set g_monster_knight_drop armor "Knight drops this item on death"
+set g_monster_knight_drop_size medium "Size of the item Knights drop. Possible values are: small, medium, large"
+set g_monster_knight_melee_damage 20 "Knight melee attack damage"
+set g_monster_knight_melee_side_damage 10 "Knight melee attack side damage"
+set g_monster_knight_speed_walk 40 "Knight walk speed"
+set g_monster_knight_speed_run 70 "Knight run speed"
+
+// Grunt
+set g_monster_soldier 1 "Enable Grunts"
+set g_monster_soldier_health 100 "Grunt Health"
+set g_monster_soldier_drop ammo "Grunt drops this item on death"
+set g_monster_soldier_drop_size shells "Size of the item Grunts drop. Possible values are: small, medium, large"
+set g_monster_soldier_melee_damage 20 "Grunt melee attack damage"
+set g_monster_soldier_speed_walk 30 "Grunt walk speed"
+set g_monster_soldier_speed_run 50 "Grunt run speed"
+set g_monster_soldier_ammo 5 "Grunt weapon ammo"
+set g_monster_soldier_weapon_laser_chance 6 "Chance of Grunt weapon being laser"
+set g_monster_soldier_weapon_shotgun_chance 8 "Chance of Grunt weapon being shotgun"
+set g_monster_soldier_weapon_machinegun_chance 4 "Chance of Grunt weapon being machine gun"
+set g_monster_soldier_weapon_rocketlauncher_chance 2 "Chance of Grunt weapon being rocket launcher"
+set g_monster_soldier_attack_uzi_bullets 3 "Number of machine gun bullets Grunt fires"
+
+// Scrag
+set g_monster_wizard 1 "Enable Scrags"
+set g_monster_wizard_health 80 "Scrag health"
+set g_monster_wizard_drop ammo "Scrag drops this item on death"
+set g_monster_wizard_drop_size cells "Size of the item Scrags drop. Possible values are: small, medium, large"
+set g_monster_wizard_speed_walk 40 "Scrag walk speed"
+set g_monster_wizard_speed_run 70 "Scrag run speed"
+set g_monster_wizard_spike_damage 15 "Scrag spike damage"
+set g_monster_wizard_spike_damage 7 "Scrag spike edge damage"
+set g_monster_wizard_spike_radius 20 "Scrag spike damage radius"
+set g_monster_wizard_spike_speed 400 "Scrag spike speed"
+
+// Rottweiler
+set g_monster_dog 1 "Enable Rottweilers"
+set g_monster_dog_health 25 "Rottweiler health"
+set g_monster_dog_bite_damage 15 "Rottweiler bite attack damage"
+set g_monster_dog_attack_jump_damage 30 "Rottweiler jump attack damage"
+set g_monster_dog_drop health "Rottweiler drops this item on death"
+set g_monster_dog_drop_size small "Size of the item Rottweilers drop. Possible values are: small, medium, large"
+set g_monster_dog_speed_walk 60 "Rottweiler walk speed"
+set g_monster_dog_speed_run 120 "Rottweiler run speed"
+
+// Spawn
+set g_monster_tarbaby 1 "Enable Spawns"
+set g_monster_tarbaby_health 80 "Spawn health"
+set g_monster_tarbaby_drop ammo "Spawn drops this item when it explodes"
+set g_monster_tarbaby_drop_size rockets "Size of the item Spawns drop. Possible values are: small, medium, large"
+set g_monster_tarbaby_speed_walk 20 "Spawn walk speed"
+set g_monster_tarbaby_speed_run 30 "Spawn run speed"
+
+// Hell-Knight
+set g_monster_hellknight 1 "Enable Hell-Knights"
+set g_monster_hellknight_health 250 "Hell-Knight health"
+set g_monster_hellknight_drop armor "Hell-Knight drops this item on death"
+set g_monster_hellknight_drop_size medium "Size of the item Hell-Knights drop. Possible values are: small, medium, large"
+set g_monster_hellknight_inferno_damage 40 "Hell-Knight inferno damage"
+set g_monster_hellknight_inferno_chance 0.4 "Hell-Knight inferno attack chance"
+set g_monster_hellknight_inferno_damagetime 3 "How long the inferno should burn the player"
+set g_monster_hellknight_fireball_damage 30 "Hell-Knight fireball projectile damage"
+set g_monster_hellknight_fireball_edgedamage 10 "Hell-Knight fireball indirect hit damage"
+set g_monster_hellknight_fireball_force 50 "Hell-Knight fireball projectile push force"
+set g_monster_hellknight_fireball_radius 70 "Hell-Knight fireball projectile damage radius"
+set g_monster_hellknight_fireball_speed 600 "Hell-Knight fireball projectile speed"
+set g_monster_hellknight_fireball_spread 0 "Hell-Knight fireball projectile spread"
+set g_monster_hellknight_fireball_chance 0.3 "Chance for Hell-Knight to throw a fireball"
+set g_monster_hellknight_jump_chance 0.2 "Chance for Hell-Knight to jump at the player (always 1 if enemy is further than _dist)"
+set g_monster_hellknight_jump_damage 25 "Hell-Knight jump attack damage"
+set g_monster_hellknight_jump_dist 500 "Hell-Knight will prioritise jumping if the enemy is this far away"
+set g_monster_hellknight_melee_damage 20 "Hell-Knight melee attack damage"
+set g_monster_hellknight_spike_damage 5 "Hell-Knight spike projectile damage"
+set g_monster_hellknight_spike_edgedamage 5 "Hell-Knight spike projectile indirect hit damage"
+set g_monster_hellknight_spike_radius 20 "Hell-Knight spike projectile damage radius"
+set g_monster_hellknight_spike_force 5 "Hell-Knight spike projectile force"
+set g_monster_hellknight_spike_chance 0.5 "Hell-Knight spike attack chance"
+set g_monster_hellknight_speed_walk 75 "Hell-Knight walk speed"
+set g_monster_hellknight_speed_run 150 "Hell-Knight run speed"
+
+// Rotfish
+set g_monster_fish 1 "Enable Rotfish"
+set g_monster_fish_health 25 "Rotfish health"
+set g_monster_fish_damage 10 "Rotfish bite attack damage"
+set g_monster_fish_drop health "Rotfish drops this item on death"
+set g_monster_fish_drop_size small "Size of the item Rotfish drop. Possible values are: small, medium, large"
+set g_monster_fish_speed_walk 40 "Rotfish walk speed"
+set g_monster_fish_speed_run 70 "Rotfish run speed"
+
+// Vore
+set g_monster_shalrath 1 "Enable Vores"
+set g_monster_shalrath_health 400 "Vore health"
+set g_monster_shalrath_drop health "Vore drops this item on death"
+set g_monster_shalrath_drop_size medium "Size of the item Vores drop. Possible values are: small, medium, large"
+set g_monster_shalrath_speed 50 "Vore move speed"
+set g_monster_shalrath_attack_spike_damage 30 "Vore homing spike explosion damage"
+set g_monster_shalrath_attack_spike_radius 60 "Vore homing spike explosion radius"
+set g_monster_shalrath_attack_spike_delay 2 "Delay between Vore homing spike attacks"
+set g_monster_shalrath_attack_melee_damage 30 "Vore magic attack damage"
+set g_monster_shalrath_attack_melee_delay 0.7 "Delay between Vore melee attacks"
+
+// Spawner
+set g_monster_spawner 1 "Enable Monster Spawner"
+set g_monster_spawner_health 100 "Spawner health"
+set g_monster_spawner_maxmobs 4 "Maximum number of spawned monsters"
+set g_monster_spawner_forcespawn "" "Force spawner to spawn this type of monster"
+
+// Zombie
+set g_monster_zombie 1 "Enable Zombies"
+set g_monster_zombie_attack_leap_damage 45 "Damage when zombie performs an attack leap"
+set g_monster_zombie_attack_leap_delay 1.5 "Delay after zombie attack leap"
+set g_monster_zombie_attack_leap_force 55 "Force of zombie attack leap"
+set g_monster_zombie_attack_leap_range 96 "Range of zombie attack leap"
+set g_monster_zombie_attack_leap_speed 500 "The speed of a zombie attack leap"
+set g_monster_zombie_attack_stand_damage 35 "Damage when zombie hits from a standing position"
+set g_monster_zombie_attack_stand_delay 1.2 "Delay after a zombie hits from a standing position"
+set g_monster_zombie_attack_stand_range 48 "Range of a zombie standing position attack"
+set g_monster_zombie_health 200 "Zombie health"
+set g_monster_zombie_speed_walk 150 "Zombie walk speed"
+set g_monster_zombie_speed_run 400 "Zombie run speed"
+set g_monster_zombie_stopspeed 100 "Speed at which zombie stops"
+set g_monster_zombie_drop health "Zombie drops this item on death"
+set g_monster_zombie_drop_size large "Size of the item zombies drop. Possible values are: small, medium, large"
+
+// Spider
+set g_monster_spider 1 "Enable Spiders"
+set g_monster_spider_attack_type 0 "Spider attack type (0 = ice, 1 = fire, ...)"
+set g_monster_spider_attack_leap_delay 1.5 "Delay after spider attack leap"
+set g_monster_spider_attack_stand_damage 35 "Damage when spider hits from a standing position"
+set g_monster_spider_attack_stand_delay 1.2 "Delay after a spider hits from a standing position"
+set g_monster_spider_health 200 "Spider health"
+set g_monster_spider_idle_timer_min 1 "Minimum time a spider can stay idle"
+set g_monster_spider_speed_walk 150 "Spider walk speed"
+set g_monster_spider_speed_run 400 "Spider run speed"
+set g_monster_spider_stopspeed 100 Speed at which spider stops"
+set g_monster_spider_drop health "Spider drops this item on death"
+set g_monster_spider_drop_size large "Size of the item spiders drop. Possible values are: small, medium, large"
\ No newline at end of file
index afa6f23da9c88865623443d1cc94364fa9d17662..548e0554819be0c570335cf87cb94bab128bad6e 100644 (file)
@@ -1994,7 +1994,74 @@ void HUD_KillNotify(string s1, string s2, string s3, float type, float msg) // s
                                        }
                                }
                        }
-               } else if (type == DEATH_CUSTOM) {
+               } else if(DEATH_ISMONSTER(type)) {
+                       HUD_KillNotify_Push(s1, "", 0, DEATH_GENERIC);
+                       if(alsoprint)
+                       {
+                               if(gentle)
+                                       print (sprintf(_("^1%s^1 hit a monster, and the monster hit back\n"), s1));
+                               else
+                               {
+                                       switch(type)
+                                       {
+                                               case DEATH_MONSTER_DEMON_MELEE:
+                                                       print (sprintf(_("^1%s^1 was eviscerated by a Fiend \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_DEMON_JUMP:
+                                                       print (sprintf(_("^1%s^1 didn't see the Fiend pouncing at them \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_SHAMBLER_MELEE:
+                                                       print (sprintf(_("^1%s^1 was smashed by a Shambler \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_SHAMBLER_CLAW:
+                                                       print (sprintf(_("^1%s^1's insides were ripped out by a Shambler \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_SHAMBLER_LIGHTNING:
+                                                       print (sprintf(_("^1%s^1 was zapped to death by a Shambler \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_SOLDIER_NAIL:
+                                                       print (sprintf(_("^1%s^1 was riddled full of holes by a Soldier \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_ENFORCER_NAIL:
+                                                       print (sprintf(_("^1%s^1 was riddled full of holes by an Enforcer \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_DOG_BITE:
+                                                       print (sprintf(_("^1%s^1 was mauled by a Rottweiler \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_DOG_JUMP:
+                                                       print (sprintf(_("^1%s^1 didn't see the pouncing Rottweiler \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_TARBABY_BLOWUP:
+                                                       print (sprintf(_("^1%s^1 was slimed by a Spawn \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_FISH_BITE:
+                                                       print (sprintf(_("^1%s^1 was fed to the Rotfish \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_HELLFISH_BITE:                       
+                                                       print (sprintf(_("^1%s^1 was eaten alive by a Hellfish \n"), s1));
+                                                       break;
+                        case DEATH_MONSTER_SHALRATH_MELEE:
+                                                       print (sprintf(_("^1%s^1 was exploded by a Vore \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_OGRE_CHAINSAW:
+                                                       print (sprintf(_("^1%s^1 was destroyed by an Ogre \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_OGRE_NAIL:
+                                                       print (sprintf(_("^1%s^1 was riddled full of holes by an Ogre \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_ZOMBIE:
+                                                       print (sprintf(_("^1%s^1's brains were eaten by a Zombie \n"), s1));
+                                                       break;
+                                               case DEATH_MONSTER_HELLKNIGHT_FIREBALL:
+                                                       print (sprintf(_("^1%s^1 was exploded by a Hell-Knight's fireball \n"), s1));
+                                               case DEATH_MONSTER_MELEE:                       
+                                                       print (sprintf(_("^1%s^1 was killed by a monster \n"), s1));
+                                                       break;
+                                       }
+                               }
+                       }
+               }        
+        else if (type == DEATH_CUSTOM) {
                        HUD_KillNotify_Push(s1, "", 0, DEATH_CUSTOM);
                        if(alsoprint)
                                print("^1", sprintf(s2, strcat(s1, "^1")), "\n");
index fb4fdd5ef24e1fd245486a58d3a69ecf5fae33a8..a0fd4105f751538e06a09d6ef2c702b4975098b9 100644 (file)
@@ -289,6 +289,8 @@ void Ent_Projectile()
                        case PROJECTILE_TAG: setmodel(self, "models/laser.mdl"); self.traileffect = particleeffectnum("TR_ROCKET"); break;
                        case PROJECTILE_FLAC: setmodel(self, "models/hagarmissile.mdl"); self.scale = 0.4; self.traileffect = particleeffectnum("TR_SEEKER"); break;
                        case PROJECTILE_SEEKER: setmodel(self, "models/tagrocket.md3"); self.traileffect = particleeffectnum("TR_SEEKER"); break;
+                       
+                       case PROJECTILE_VORE_SPIKE: setmodel(self, "models/ebomb.mdl"); self.traileffect = particleeffectnum(""); break;
 
                        case PROJECTILE_RAPTORBOMB:    setmodel(self, "models/vehicles/clusterbomb.md3"); self.gravity = 1; self.avelocity = '0 0 180'; self.traileffect = particleeffectnum(""); break;
                        case PROJECTILE_RAPTORBOMBLET: setmodel(self, "models/vehicles/bomblet.md3");     self.gravity = 1; self.avelocity = '0 0 180'; self.traileffect = particleeffectnum(""); break;
index 1aa1b6a435ac5d015e5c27f0d4cd24cbbfdc61bf..408cfd036abfbf8c38e77ed84352579074120459 100644 (file)
@@ -1069,15 +1069,30 @@ vector HUD_DrawKeyValue(vector pos, string key, string value) {
 
 vector HUD_DrawMapStats(vector pos, vector rgb, vector bg_size) {
        float stat_secrets_found, stat_secrets_total;
-       float rows;
+       float stat_current_wave, stat_totalwaves;
+       float stat_monsters_killed, stat_monsters_total;
+       float rows = 0;
        string val;
+       
+       // get tower defense stats
+       stat_current_wave = getstatf(STAT_CURRENT_WAVE);
+       stat_totalwaves = getstatf(STAT_TOTALWAVES);
+       
+       // get monster stats
+       stat_monsters_killed = getstatf(STAT_MONSTERS_KILLED);
+       stat_monsters_total = getstatf(STAT_MONSTERS_TOTAL);
 
        // get secrets stats
        stat_secrets_found = getstatf(STAT_SECRETS_FOUND);
        stat_secrets_total = getstatf(STAT_SECRETS_TOTAL);
 
        // get number of rows
-       rows = (stat_secrets_total ? 1 : 0);
+       if(stat_secrets_total)
+               rows += 1;
+       if(stat_totalwaves)
+               rows += 1;
+       if(stat_monsters_total)
+               rows += 1;
 
        // if no rows, return
        if not(rows)
@@ -1097,10 +1112,27 @@ vector HUD_DrawMapStats(vector pos, vector rgb, vector bg_size) {
        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 waves
+       if(stat_totalwaves)
+       {
+               val = sprintf("%d/%d", stat_current_wave, stat_totalwaves);
+               pos = HUD_DrawKeyValue(pos, _("Current wave:"), val);
+       }
+       
+       // 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;
index cdc58cb55445fa99d28d899c5d5e3a464fab5f9e..b908bacec7db376eacb42b801b3fef5d244a8ec4 100644 (file)
@@ -181,6 +181,12 @@ const float STAT_SECRETS_FOUND = 71;
 
 const float STAT_RESPAWN_TIME = 72;
 
+const float STAT_CURRENT_WAVE = 73;
+const float STAT_TOTALWAVES = 74;
+
+const float STAT_MONSTERS_TOTAL = 75;
+const float STAT_MONSTERS_KILLED = 76;
+
 // mod stats (1xx)
 const float STAT_REDALIVE = 100;
 const float STAT_BLUEALIVE = 101;
@@ -356,6 +362,8 @@ float PROJECTILE_WAKICANNON     = 29;
 float PROJECTILE_BUMBLE_GUN     = 30;
 float PROJECTILE_BUMBLE_BEAM    = 31;
 
+float PROJECTILE_VORE_SPIKE            = 32;
+
 float SPECIES_HUMAN        =  0;
 float SPECIES_ROBOT_SOLID  =  1;
 float SPECIES_ALIEN        =  2;
@@ -427,6 +435,28 @@ float DEATH_TURRET_PHASER           = 10511;
 float DEATH_TURRET_TESLA            = 10512;
 float DEATH_TURRET_LAST            = 10512;
 
+// Monster death types
+float DEATH_MONSTER                        = 10513;
+float DEATH_MONSTER_DEMON_MELEE            = 10514;
+float DEATH_MONSTER_DEMON_JUMP             = 10515;
+float DEATH_MONSTER_SHAMBLER_MELEE         = 10516;
+float DEATH_MONSTER_SHAMBLER_CLAW          = 10517;
+float DEATH_MONSTER_SHAMBLER_LIGHTNING     = 10518;
+float DEATH_MONSTER_SOLDIER_NAIL           = 10519;
+float DEATH_MONSTER_ENFORCER_NAIL          = 10520;
+float DEATH_MONSTER_DOG_BITE               = 10521;
+float DEATH_MONSTER_DOG_JUMP               = 10522;
+float DEATH_MONSTER_TARBABY_BLOWUP         = 10523;
+float DEATH_MONSTER_FISH_BITE              = 10524;
+float DEATH_MONSTER_HELLFISH_BITE          = 10525;
+float DEATH_MONSTER_SHALRATH_MELEE         = 10526;
+float DEATH_MONSTER_OGRE_CHAINSAW          = 10527;
+float DEATH_MONSTER_OGRE_NAIL              = 10528;
+float DEATH_MONSTER_MELEE                  = 10529;
+float DEATH_MONSTER_ZOMBIE                                = 10530;
+float DEATH_MONSTER_HELLKNIGHT_FIREBALL           = 10531;
+float DEATH_MONSTER_LAST                   = 10532;
+
 float DEATH_WEAPONMASK = 0xFF;
 float DEATH_HITTYPEMASK = 0x1F00; // which is WAY below 10000 used for normal deaths
 float HITTYPE_SECONDARY = 0x100;
@@ -442,6 +472,7 @@ float HITTYPE_RESERVED = 0x1000; // unused yet
 #define DEATH_ISWEAPON(t,w)           (!DEATH_ISSPECIAL(t) && DEATH_WEAPONOFWEAPONDEATH(t) == (w))
 #define DEATH_WEAPONOF(t)             (DEATH_ISSPECIAL(t) ? 0 : DEATH_WEAPONOFWEAPONDEATH(t))
 #define WEP_VALID(w)                  ((w) >= WEP_FIRST && (w) <= WEP_LAST)
+#define DEATH_ISMONSTER(t)            ((t) >= DEATH_MONSTER && (t) <= DEATH_MONSTER_LAST)
 
 #define FRAGS_PLAYER 0
 #define FRAGS_SPECTATOR -666
index f7ced420579acb32837ea61f04511d39ddac86f5..606df668fafd96320a5aa6550137a6502b426ec0 100644 (file)
@@ -315,6 +315,8 @@ float _MapInfo_Generate(string pFilename) // 0: failure, 1: ok ent, 2: ok bsp
                                        MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_CTF;
                                else if(v == "runematch_spawn_point")
                                        MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_RUNEMATCH;
+                               else if(v == "td_generator")
+                                       MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_TD;
                                else if(v == "target_assault_roundend")
                                        MapInfo_Map_supportedGametypes |= MAPINFO_TYPE_ASSAULT;
                                else if(v == "onslaught_generator")
index bd67f67a825104abcd3108d8096d501fa8a27762..43ec2771eacd7d9e03264b7caee4af9332cac548 100644 (file)
@@ -39,6 +39,12 @@ REGISTER_GAMETYPE(_("Deathmatch"),dm,g_dm,DEATHMATCH,"timelimit=20 pointlimit=30
 REGISTER_GAMETYPE(_("Last Man Standing"),lms,g_lms,LMS,"timelimit=20 lives=9 leadlimit=0")
 #define g_lms IS_GAMETYPE(LMS)
 
+REGISTER_GAMETYPE(_("Tower Defense"),td,g_td,TD,"timelimit=0 pointlimit=10 leadlimit=0")
+#define g_td IS_GAMETYPE(TD)
+
+REGISTER_GAMETYPE(_("Real-Time Strategy"),rts,g_rts,RTS,"timelimit=0 pointlimit=10 leadlimit=0")
+#define g_rts IS_GAMETYPE(RTS)
+
 REGISTER_GAMETYPE(_("Arena"),arena,g_arena,ARENA,"timelimit=20 pointlimit=10 leadlimit=0")
 #define g_arena IS_GAMETYPE(ARENA)
 
index 0a3a55c5f1d4bc214045b2c32731adac5779c156..4e563e950eea153354e3c1c6c2bb600012ebabc6 100644 (file)
@@ -29,6 +29,7 @@
 #include "xonotic/dialog_firstrun.c"
 #include "xonotic/dialog_teamselect.c"
 #include "xonotic/dialog_sandboxtools.c"
+#include "xonotic/dialog_monstertools.c"
 #include "xonotic/dialog_settings.c"
 #include "xonotic/dialog_settings_video.c"
 #include "xonotic/dialog_settings_effects.c"
diff --git a/qcsrc/menu/xonotic/dialog_monstertools.c b/qcsrc/menu/xonotic/dialog_monstertools.c
new file mode 100644 (file)
index 0000000..64c0e1e
--- /dev/null
@@ -0,0 +1,57 @@
+#ifdef INTERFACE
+CLASS(XonoticMonsterToolsDialog) EXTENDS(XonoticRootDialog)
+       METHOD(XonoticMonsterToolsDialog, fill, void(entity)) // to be overridden by user to fill the dialog with controls
+       ATTRIB(XonoticMonsterToolsDialog, title, string, _("Monster Tools"))
+       ATTRIB(XonoticMonsterToolsDialog, color, vector, SKINCOLOR_DIALOG_SANDBOXTOOLS)
+       ATTRIB(XonoticMonsterToolsDialog, intendedWidth, float, 0.8)
+       ATTRIB(XonoticMonsterToolsDialog, rows, float, 16)
+       ATTRIB(XonoticMonsterToolsDialog, columns, float, 4)
+       ATTRIB(XonoticMonsterToolsDialog, name, string, "MonsterTools")
+ENDCLASS(XonoticMonsterToolsDialog)
+#endif
+
+#ifdef IMPLEMENTATION
+void XonoticMonsterToolsDialog_fill(entity me)
+{
+       entity e, box;
+
+       me.TR(me);
+               me.TD(me, 1, 0.25, e = makeXonoticTextLabel(0, _("Monster:")));
+               me.TD(me, 1, 1.5, box = makeXonoticInputBox(1, "menu_monsters_spawn_monster"));
+                       box.forbiddenCharacters = "\r\n\\\"$"; // don't care, isn't getting saved
+                       box.maxLength = -127; // negative means encoded length in bytes
+                       box.saveImmediately = 1;
+               me.TD(me, 1, 0.25, e = makeXonoticTextLabel(0, _("Name:")));
+               me.TD(me, 1, 1.5, box = makeXonoticInputBox(1, "menu_monsters_edit_name"));
+                       box.forbiddenCharacters = "\r\n\\\"$"; // don't care, isn't getting saved
+                       box.maxLength = -127; // negative means encoded length in bytes
+                       box.saveImmediately = 1;
+               me.TR(me);
+               me.TDempty(me, 0.5);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Spawn"), '0 0 0', "cmd mobspawn $menu_monsters_spawn_monster $menu_monsters_edit_movetarget \"$menu_monsters_edit_name\"", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Remove"), '0 0 0', "cmd mobkill", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Change name"), '0 0 0', "editmob name \"$menu_monsters_edit_name\"", 0));
+       me.TR(me);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Move target:"), '0 0 0', "editmob movetarget $menu_monsters_edit_movetarget", 0));
+               me.TD(me, 1, 0.5, e = makeXonoticRadioButton(2, "menu_monsters_edit_movetarget", "1", _("Follow")));
+               me.TD(me, 1, 0.5, e = makeXonoticRadioButton(2, "menu_monsters_edit_movetarget", "2", _("Wander")));
+               me.TD(me, 1, 0.5, e = makeXonoticRadioButton(2, "menu_monsters_edit_movetarget", "3", _("Spawnpoint")));
+               me.TD(me, 1, 0.5, e = makeXonoticRadioButton(2, "menu_monsters_edit_movetarget", "4", _("No moving")));
+       me.TR(me);
+       me.TD(me, 1, 1.5, e = makeXonoticTextLabel(0, _("Colors:")));
+       me.TR(me);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set skin:"), '0 0 0', "editmob skin $menu_monsters_edit_skin", 0));
+               me.TD(me, 1, 1.5, e = makeXonoticSlider(0, 99, 1, "menu_monsters_edit_skin"));
+       me.TR(me);
+               me.TD(me, 1, 0.5, e = makeXonoticCommandButton(_("Set color:"), '0 0 0', "editmob color \"$menu_monsters_edit_color\"", 0));
+               me.TD(me, 2, 1.5, e = makeXonoticColorpickerString("menu_monsters_edit_color", "menu_monsters_edit_color"));
+       me.TR(me);
+
+       me.gotoRC(me, me.rows - 1, 0);
+               me.TD(me, 1, me.columns, e = makeXonoticButton(_("OK"), '0 0 0'));
+                       e.onClick = Dialog_Close;
+                       e.onClickEntity = me;
+}
+#endif
+
+/* Click. The c-word is here so you can grep for it :-) */
index f9d86c2dd229276aadb7272c9484f165bb1ad606..120d80213c3d7facb79ff1e90d468162b523263d 100644 (file)
@@ -199,6 +199,10 @@ void MainWindow_configureMainWindow(entity me)
        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();
index 9271e03d0ad20ef4b7e8e9a6536d68b723b40348..f7a01d572293ca6c48cf7f500e253567fbfb5e87 100644 (file)
@@ -111,6 +111,7 @@ float accuracy_isgooddamage(entity attacker, entity targ)
 {
        if(!inWarmupStage)
        if(targ.flags & FL_CLIENT)
+       if(!(attacker.flags & FL_MONSTER)) // no accuracy for monsters
        if(targ.deadflag == DEAD_NO)
        if(IsDifferentTeam(attacker, targ))
                return TRUE;
index 08a6297ad44cf21e793edfc86e4949bb474dd0e4..1be6305d271ddad72e2761b6da671cb32c1ab795 100644 (file)
@@ -1272,3 +1272,57 @@ float autocvar_physics_ode;
 float autocvar_g_physical_items;
 float autocvar_g_physical_items_damageforcescale;
 float autocvar_g_physical_items_reset;
+float autocvar_g_td_start_wave;
+float autocvar_g_td_generator_health;
+float autocvar_g_td_current_monsters;
+float autocvar_g_td_generator_damaged_points;
+float autocvar_g_td_monster_count;
+float autocvar_g_td_monster_count_increment;
+float autocvar_g_td_buildphase_time;
+float autocvar_g_td_pvp;
+float autocvar_g_td_max_waves;
+float autocvar_g_td_kill_points;
+float autocvar_g_td_turretkill_points;
+float autocvar_g_td_generator_dontend;
+float autocvar_g_td_force_settings;
+float autocvar_g_td_turret_max;
+float autocvar_g_td_turret_plasma_cost;
+float autocvar_g_td_turret_mlrs_cost;
+float autocvar_g_td_turret_walker_cost;
+float autocvar_g_td_tower_buff_cost;
+float autocvar_g_td_turret_flac_cost;
+float autocvar_g_td_monsters_skill_start;
+float autocvar_g_td_monsters_skill_increment;
+float autocvar_g_td_monsters_speed_walk;
+float autocvar_g_td_monsters_speed_run;
+float autocvar_g_td_monsters_spawn_delay;
+float autocvar_g_td_monsters_spawnshield_time;
+float autocvar_g_td_monsters_ignore_turrets;
+float autocvar_g_td_turret_upgrade_cost;
+float autocvar_g_td_turret_repair_cost;
+float autocvar_g_td_barricade_damage;
+float autocvar_g_td_barricade_cost;
+float autocvar_g_za_monster_count;
+float autocvar_g_monsters;
+float autocvar_g_monsters_max;
+float autocvar_g_monsters_max_perplayer;
+float autocvar_g_monsters_giants_only;
+float autocvar_g_monsters_target_range;
+float autocvar_g_monsters_typefrag;
+float autocvar_g_monsters_owners;
+float autocvar_g_monsters_miniboss_chance;
+float autocvar_g_monsters_miniboss_healthboost;
+float autocvar_g_monsters_forcedrop;
+string autocvar_g_monsters_drop_type;
+string autocvar_g_monsters_drop_size;
+float autocvar_g_monsters_teams;
+float autocvar_g_monsters_healthbars;
+float autocvar_g_monsters_respawn_delay;
+float autocvar_g_monsters_respawn;
+float autocvar_g_monsters_nogiants;
+float autocvar_g_monsters_skill_easy;
+float autocvar_g_monsters_skill_normal;
+float autocvar_g_monsters_skill_hard;
+float autocvar_g_monsters_skill_insane;
+float autocvar_g_monsters_skill_nightmare;
+string autocvar_g_monsters_spawn_list;
index be5138c92d41af91bc93571431ac9f6df2757582..cdebc8c7b2a8a978a23f9b3f08a1cb7a1c87e2ed 100644 (file)
@@ -856,6 +856,7 @@ void PutClientInServer (void)
                self.event_damage = PlayerDamage;
 
                self.bot_attack = TRUE;
+               self.monster_attack = TRUE;
 
                self.statdraintime = time + 5;
                self.BUTTON_ATCK = self.BUTTON_JUMP = self.BUTTON_ATCK2 = 0;
@@ -910,6 +911,8 @@ void PutClientInServer (void)
                                self.target = s;
                        activator = world;
                self = oldself;
+               
+               Unfreeze(self);
 
                spawn_spot = spot;
                MUTATOR_CALLHOOK(PlayerSpawn);
@@ -2630,6 +2633,15 @@ void PlayerPreThink (void)
                return;
 #endif
 
+       if(self.frozen)
+       {
+               self.revive_progress = bound(0, self.revive_progress + frametime * self.revive_speed, 1);
+               self.health = max(1, self.revive_progress * autocvar_g_balance_health_start);
+
+               if(self.revive_progress >= 1)
+                       Unfreeze(self);
+       }
+
        MUTATOR_CALLHOOK(PlayerPreThink);
 
        if(!self.cvar_cl_newusekeysupported) // FIXME remove this - it was a stupid idea to begin with, we can JUST use the button
@@ -2796,7 +2808,7 @@ void PlayerPreThink (void)
                        do_crouch = 1;
                if(self.vehicle)
                        do_crouch = 0;
-               if(self.freezetag_frozen)
+               if(self.freezetag_frozen || self.frozen)
                        do_crouch = 0;
                if(self.weapon == WEP_SHOTGUN && self.weaponentity.wframe == WFRAME_FIRE2 && time < self.weapon_nextthink)
                        do_crouch = 0;
@@ -2868,6 +2880,9 @@ void PlayerPreThink (void)
                // 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
index 50401604ef905313d521380d6995b0cf202d6bec..062f5b5eacea58324a22509f96d71ee456d5b32c 100644 (file)
@@ -23,7 +23,7 @@ When you press the jump key
 */
 void PlayerJump (void)
 {
-       if(self.freezetag_frozen)
+       if(self.freezetag_frozen || self.frozen)
                return; // no jumping in freezetag when frozen
 
        float mjumpheight;
@@ -869,6 +869,12 @@ void SV_PlayerPhysics()
        self.disableclientprediction = 0;
        if(time < self.ladder_time)
                self.disableclientprediction = 1;
+               
+       if(self.frozen)
+       {
+               self.movement = '0 0 0';
+               self.disableclientprediction = 1;
+       }
 
        MUTATOR_CALLHOOK(PlayerPhysics);
 
@@ -1071,7 +1077,7 @@ void SV_PlayerPhysics()
                        PM_Accelerate(wishdir, wishspeed, wishspeed, autocvar_sv_accelerate*maxspd_mod, 1, 0, 0, 0);
                }
        }
-       else if ((self.items & IT_JETPACK) && self.BUTTON_HOOK && (!autocvar_g_jetpack_fuel || self.ammo_fuel >= 0.01 || self.items & IT_UNLIMITED_WEAPON_AMMO) && !self.freezetag_frozen)
+       else if ((self.items & IT_JETPACK) && self.BUTTON_HOOK && (!autocvar_g_jetpack_fuel || self.ammo_fuel >= 0.01 || self.items & IT_UNLIMITED_WEAPON_AMMO) && !self.freezetag_frozen && !self.frozen)
        {
                //makevectors(self.v_angle_y * '0 1 0');
                makevectors(self.v_angle);
index e81b08e58e73094a76f7c8c3e0fc1d84b757140a..cb051afda80a899226d967b153a8dfc677b52139 100644 (file)
@@ -245,7 +245,7 @@ void player_anim (void)
                else
                        deadbits = ANIMSTATE_DEAD2;
        float animbits = deadbits;
-       if(self.freezetag_frozen)
+       if(self.freezetag_frozen || self.frozen)
                animbits |= ANIMSTATE_FROZEN;
        if(self.crouch)
                animbits |= ANIMSTATE_DUCK;
index 4df59a09afde775e7e73cca9fb0be6e248ad1205..08467036542dc697c2786ea36ec427127e06146b 100644 (file)
@@ -344,6 +344,8 @@ void W_ThrowWeapon(vector velo, vector delta, float doreduce)
        w = self.weapon;
        if (w == 0)
                return; // just in case
+       if(self.frozen)
+               return;
        if(MUTATOR_CALLHOOK(ForbidThrowCurrentWeapon))
                return;
        if(!autocvar_g_weapon_throwable)
@@ -378,7 +380,7 @@ void W_WeaponFrame()
        if(((arena_roundbased || g_ca || g_freezetag) && time < warmup) || ((time < game_starttime) && !autocvar_sv_ready_restart_after_countdown))
                return;
 
-       if(self.freezetag_frozen == 1)
+       if(self.freezetag_frozen == 1 || self.frozen == 1)
                return;
 
        if (!self.weaponentity || self.health < 1)
index f697d3689c6247ab40a060198244fb147ade1673..311f42fbffea5a8b461b2a81b26b0533387fae3b 100644 (file)
@@ -103,7 +103,7 @@ void W_HitPlotAnalysis(entity player, vector screenforward, vector screenright,
        vector org;
        float lag;
 
-       if(player.hitplotfh >= 0)
+       if(player.hitplotfh >= 0 && !(player.flags & FL_MONSTER))
        {
                lag = ANTILAG_LATENCY(player);
                if(lag < 0.001)
@@ -1028,6 +1028,8 @@ vector W_CalculateProjectileVelocity(vector pvelocity, vector mvelocity, float f
 
 void W_AttachToShotorg(entity flash, vector offset)
 {
+       if(self.flags & FL_MONSTER)
+               return; // no flash for monsters
        entity xflash;
        flash.owner = self;
        flash.angles_z = random() * 360;
@@ -1291,6 +1293,9 @@ void W_SetupProjectileVelocity(entity missile, float pSpeed, float spread)
 
 void W_DecreaseAmmo(.float ammo_type, float ammo_use, float ammo_reload)
 {
+       if(self.flags & FL_MONSTER) // no ammo for monsters... yet
+               return;
+               
        if((self.items & IT_UNLIMITED_WEAPON_AMMO) && !ammo_reload)
                return;
 
index 530646afd2ec8fbb105dc8a080b41dadaaa25d0f..7e4737878c015c62379bff52d74837e7490bfe17 100644 (file)
@@ -182,6 +182,132 @@ void ClientCommand_join(float request)
        }
 }
 
+void ClientCommand_mobedit(float request, float argc)
+{
+       switch(request)
+       {
+               case CMD_REQUEST_COMMAND:
+               {
+                       makevectors(self.v_angle);
+                       WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * 100, MOVE_NORMAL, self);
+                       
+                       if not(trace_ent.flags & FL_MONSTER) { sprint(self, "You need to aim at your monster to edit its properties.\n"); return; }
+                       if(trace_ent.realowner != self) { sprint(self, "That monster does not belong to you.\n"); return; }
+                       
+                       switch(argv(1))
+                       {
+                               case "name": trace_ent.netname = strzone(strdecolorize(argv(2))); WaypointSprite_UpdateSprites(trace_ent.sprite, trace_ent.netname, "", ""); break;
+                               case "skin": trace_ent.skin = stof(argv(2)); break;
+                               case "color": trace_ent.colormod = stov(argv(2)); break;
+                               case "movetarget": trace_ent.monster_moveflags = stof(argv(2)); break;
+                               default: sprint(self, "Unknown parameter\n"); break;
+                       }
+                       
+                       return; // never fall through to usage
+               }
+               default:
+                       sprint(self, "Incorrect parameters for ^2mobedit^7\n");
+               case CMD_REQUEST_USAGE:
+               {
+                       sprint(self, "\nUsage:^3 cmd mobedit [argument]\n");
+                       sprint(self, "  Where 'argument' can be name, skin, color or movetarget.\n");
+                       return;
+               }
+       }
+}
+       
+void ClientCommand_mobkill(float request)
+{
+       switch(request)
+       {
+               case CMD_REQUEST_COMMAND:
+               {
+                       makevectors(self.v_angle);
+                       WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * 100, MOVE_NORMAL, self);
+                       
+                       if(trace_ent.flags & FL_MONSTER)
+                       {
+                               if(trace_ent.realowner != self)
+                               {
+                                       sprint(self, "That monster does not belong to you.\n");
+                                       return;
+                               }
+                               sprint(self, strcat("Your pet '", trace_ent.netname, "' has been brutally mutilated.\n"));
+                               Damage (trace_ent, world, world, trace_ent.health + trace_ent.max_health, DEATH_KILL, trace_ent.origin, '0 0 0');
+                               return;
+                       }
+                       else
+                               sprint(self, "You need to aim at your monster to kill it.\n");
+                       
+                       return;
+               }
+       
+               default:
+                       sprint(self, "Incorrect parameters for ^2mobkill^7\n");
+               case CMD_REQUEST_USAGE:
+               {
+                       sprint(self, "\nUsage:^3 cmd mobkill\n");
+                       sprint(self, "  Aim at your monster to kill it.\n");
+                       return;
+               }
+       }
+}
+
+void ClientCommand_mobspawn(float request, float argc)
+{
+       switch(request)
+       {
+               case CMD_REQUEST_COMMAND:
+               {
+                       entity e;
+                       string tospawn, mname;
+                       float moveflag;
+                       
+                       moveflag = (argv(2) ? stof(argv(2)) : 1); // follow owner if not defined
+                       tospawn = argv(1);
+                       mname = argv(3);
+                       
+                       if(tospawn == "list")
+                       {
+                               sprint(self, "Available monsters:\n");
+                               sprint(self, strcat(autocvar_g_monsters_spawn_list, "\n"));
+                               return;
+                       }
+                       
+                       if(self.classname != STR_PLAYER) { sprint(self, "You can't spawn monsters while spectating.\n"); }
+                       else if not(autocvar_g_monsters) { sprint(self, "Monsters aren't enabled.\n"); }
+                       else if(g_td) { sprint(self, "You can't spawn monsters in Tower Defense mode.\n"); }
+                       else if(self.deadflag) { sprint(self, "You can't spawn monsters while dead.\n"); }
+                       else if(self.monstercount >= autocvar_g_monsters_max_perplayer) { sprint(self, "You have spawned too many monsters, kill some before trying to spawn any more.\n"); }
+                       else if(totalspawned >= autocvar_g_monsters_max) { sprint(self, "The global maximum monster count has been reached, kill some before trying to spawn any more.\n"); }
+                       else // all worked out, so continue
+                       {
+                               self.monstercount += 1;
+                               totalspawned += 1;
+                       
+                               makevectors(self.v_angle);
+                               WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * 150, MOVE_NORMAL, self);
+                       
+                               e = spawnmonster(tospawn, self, self, trace_endpos, FALSE, moveflag);
+                               if(mname) e.netname = strzone(mname);
+                       
+                               sprint(self, strcat("Spawned 1 ", tospawn, "\n"));
+                       }
+                       
+                       return;
+               }
+       
+               default:
+                       sprint(self, "Incorrect parameters for ^2mobspawn^7\n");
+               case CMD_REQUEST_USAGE:
+               {
+                       sprint(self, "\nUsage:^3 cmd mobspawn monster\n");
+                       sprint(self, "  See 'cmd mobspawn list' for available arguments.\n");
+                       return;
+               }
+       }
+}
+
 void ClientCommand_ready(float request) // todo: anti-spam for toggling readyness
 {
        switch(request)
@@ -560,6 +686,9 @@ void ClientCommand_(float request)
        CLIENT_COMMAND("clientversion", ClientCommand_clientversion(request, arguments), "Release version of the game") \
        CLIENT_COMMAND("mv_getpicture", ClientCommand_mv_getpicture(request, arguments), "Retrieve mapshot picture from the server") \
        CLIENT_COMMAND("join", ClientCommand_join(request), "Become a player in the game") \
+       CLIENT_COMMAND("mobedit", ClientCommand_mobedit(request, arguments), "Edit your monster's properties") \
+       CLIENT_COMMAND("mobkill", ClientCommand_mobkill(request), "Kills your monster") \
+       CLIENT_COMMAND("mobspawn", ClientCommand_mobspawn(request, arguments), "Spawn monsters infront of yourself") \
        CLIENT_COMMAND("ready", ClientCommand_ready(request), "Qualify as ready to end warmup stage (or restart server if allowed)") \
        CLIENT_COMMAND("say", ClientCommand_say(request, arguments, command), "Print a message to chat to all players") \
        CLIENT_COMMAND("say_team", ClientCommand_say_team(request, arguments, command), "Print a message to chat to all team mates") \
index e64cbc267ac505e61d579318cd644a2e7b2d4595..b0118c5cf327503b77ee3245f3b9082cd4a37b72 100644 (file)
@@ -8,7 +8,12 @@
 .float lms_spectate_warning;
 .float checkfail;
 
+// number of monsters spawned with mobspawn command
+float totalspawned;
+
 string MapVote_Suggest(string m);
 
+entity spawnmonster(string monster, entity spawnedby, entity own, vector orig, float respwn, float moveflag);
+
 // used by common/command/generic.qc:GenericCommand_dumpcommands to list all commands into a .txt file
-void ClientCommand_macro_write_aliases(float fh);
\ No newline at end of file
+void ClientCommand_macro_write_aliases(float fh);
index 8f3ea869d49e24bcdf0febd74dc5564c1002f7b6..8a71dfb0543e85afb2bb860ae4e7a2f3920916bd 100644 (file)
@@ -139,6 +139,48 @@ void GameCommand_adminmsg(float request, float argc)
        }
 }
 
+void GameCommand_butcher(float request)
+{
+       switch(request)
+       {
+               case CMD_REQUEST_COMMAND:
+               {
+                       if(g_td) { print("This command doesn't work in Tower Defense.\n"); return; }
+               
+            float removed_count = 0;
+                       entity montokill, head;
+                       
+            FOR_EACH_MONSTER(montokill)
+            {
+                               WaypointSprite_Kill(montokill.sprite);
+                    
+                remove(montokill);
+                removed_count += 1;
+            }
+                       
+                       FOR_EACH_PLAYER(head)
+                               head.monstercount = 0;
+                               
+                       totalspawned = 0;
+                       
+                       if(removed_count <= 0)
+                               print("No monsters to kill\n");
+                       else
+                               print(strcat("Killed ", ftos(removed_count), " monster", ((removed_count == 1) ? "\n" : "s\n")));
+                               
+                       return; // never fall through to usage
+               }
+                       
+               default:
+               case CMD_REQUEST_USAGE:
+               {
+                       print("\nUsage:^3 sv_cmd butcher\n");
+                       print("  No arguments required.\n");
+                       return;
+               }
+       }
+}
+
 void GameCommand_allready(float request)
 {
        switch(request)
@@ -1728,6 +1770,7 @@ void GameCommand_(float request)
 // Common commands have double indentation to separate them a bit.
 #define SERVER_COMMANDS(request,arguments,command) \
        SERVER_COMMAND("adminmsg", GameCommand_adminmsg(request, arguments), "Send an admin message to a client directly") \
+       SERVER_COMMAND("butcher", GameCommand_butcher(request), "Instantly removes all monsters on the map") \
        SERVER_COMMAND("allready", GameCommand_allready(request), "Restart the server and reset the players") \
        SERVER_COMMAND("allspec", GameCommand_allspec(request, arguments), "Force all players to spectate") \
        SERVER_COMMAND("anticheat", GameCommand_anticheat(request, arguments), "Create an anticheat report for a client") \
index 2f3eeaa4f3994f3f976dc1fc567dd21efaebb6e7..dde8b86905a92fa7e67995236eca16b8d0bf43dc 100644 (file)
@@ -599,6 +599,10 @@ float serverflags;
 .float freezetag_frozen;
 .float freezetag_revive_progress;
 
+.float frozen; // for freeze attacks
+.float revive_progress;
+.float revive_speed; // NOTE: multiplier (anything above 1 is instaheal)
+
 .entity muzzle_flash;
 .float misc_bulletcounter;     // replaces uzi & hlac bullet counter.
 
@@ -629,3 +633,19 @@ string modname;
 #define MISSILE_IS_CONFUSABLE(m) ((m.missile_flags & MIF_GUIDED_CONFUSABLE) ? TRUE : FALSE)
 #define MISSILE_IS_GUIDED(m) ((m.missile_flags & MIF_GUIDED_ALL) ? TRUE : FALSE)
 #define MISSILE_IS_TRACKING(m) ((m.missile_flags & MIF_GUIDED_TRACKING) ? TRUE : FALSE)
+
+.string spawnmob;
+.float monster_attack;
+
+float monster_skill;
+float spawncode_first_load; // used to tell the player the monster database is loading (TODO: fix this?)
+
+.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
index e779ff45357bce54c3907b5aced103c70b308738..efa6c0ad5c36d56e324e44054bf3be7d33161f82 100644 (file)
@@ -518,6 +518,67 @@ void Obituary (entity attacker, entity inflictor, entity targ, float deathtype)
        }
 }
 
+void Ice_Think()
+{
+       if(self.owner.health < 1)
+       {
+               remove(self);
+               return;
+       }
+       setorigin(self, self.owner.origin - '0 0 16');
+       self.nextthink = time;
+}
+
+void Freeze (entity targ, float freeze_time)
+{
+       float monster = (targ.flags & FL_MONSTER);
+       float player = (targ.flags & FL_CLIENT);
+       
+       if(!player && !monster) // only specified entities can be freezed
+               return;
+               
+       if(targ.frozen || targ.freezetag_frozen)
+               return;
+               
+       targ.frozen = 1;
+       targ.revive_progress = 0;
+       targ.health = 1;
+       targ.revive_speed = freeze_time;
+
+       entity ice;
+       ice = spawn();
+       ice.owner = targ;
+       ice.classname = "ice";
+       ice.scale = targ.scale;
+       ice.think = Ice_Think;
+       ice.nextthink = time;
+       ice.frame = floor(random() * 21); // ice model has 20 different looking frames
+       setmodel(ice, "models/ice/ice.md3");
+
+       entity oldself;
+       oldself = self;
+       self = ice;
+       Ice_Think();
+       self = oldself;
+
+       RemoveGrapplingHook(targ);
+}
+
+void Unfreeze (entity targ)
+{
+       targ.frozen = 0;
+       targ.revive_progress = 0;
+       targ.health = ((targ.classname == STR_PLAYER) ? autocvar_g_balance_health_start : targ.max_health);
+
+       // remove the ice block
+       entity ice;
+       for(ice = world; (ice = find(ice, classname, "ice")); ) if(ice.owner == targ)
+       {
+               remove(ice);
+               break;
+       }
+}
+
 // these are updated by each Damage call for use in button triggering and such
 entity damage_targ;
 entity damage_inflictor;
@@ -706,6 +767,12 @@ void Damage (entity targ, entity inflictor, entity attacker, float damage, float
                        mirrorforce *= g_weaponforcefactor;
                }
                
+               if(targ.frozen && attacker.classname != "monster_spider")
+               {
+                       damage = 0;
+                       force *= 0.2;
+               }
+               
                // should this be changed at all? If so, in what way?
                frag_attacker = attacker;
                frag_target = targ;
@@ -789,7 +856,7 @@ void Damage (entity targ, entity inflictor, entity attacker, float damage, float
                        else
                                victim = targ;
 
-                       if(victim.classname == "player" || victim.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
+                       if(victim.classname == "player" || victim.turrcaps_flags & TFL_TURRCAPS_ISTURRET || victim.flags & FL_MONSTER)
                        {
                                if(IsDifferentTeam(victim, attacker))
                                {
@@ -1310,7 +1377,7 @@ void Fire_ApplyDamage(entity e)
                e.fire_endtime = 0;
 
        // ice stops fire
-       if(e.freezetag_frozen)
+       if(e.freezetag_frozen || e.frozen)
                e.fire_endtime = 0;
 
        t = min(frametime, e.fire_endtime - time);
index f7af47d924036f121a6b8340a8dc2df5a0edaee4..ad1da10f7f4be77db9181e8f87cb89cbc963f640 100644 (file)
@@ -302,7 +302,7 @@ void FireGrapplingHook (void)
        if((arena_roundbased && time < warmup) || (time < game_starttime))
                return;
 
-  if(self.freezetag_frozen)
+  if(self.freezetag_frozen || self.frozen)
                return;
        
        if(self.vehicle)
index d67ef26eba5ca4904655eab2fb73fd0e0c08f549..8649c07a6b25d4a8449aef37e8794cd67f89bb13 100644 (file)
@@ -264,6 +264,7 @@ void cvar_changes_init()
                BADCVAR("g_freezetag");
                BADCVAR("g_keepaway");
                BADCVAR("g_keyhunt");
+               BADCVAR("g_td");
                BADCVAR("g_keyhunt_teams");
                BADCVAR("g_keyhunt_teams");
                BADCVAR("g_lms");
@@ -814,6 +815,16 @@ void spawnfunc_worldspawn (void)
                addstat(STAT_FROZEN, AS_INT, freezetag_frozen);
                addstat(STAT_REVIVE_PROGRESS, AS_FLOAT, freezetag_revive_progress);
        }
+       
+       if(g_td)
+       {
+               addstat(STAT_CURRENT_WAVE, AS_FLOAT, stat_current_wave);
+               addstat(STAT_TOTALWAVES, AS_FLOAT, stat_totalwaves);
+       }
+       
+       // freeze attacks
+       addstat(STAT_FROZEN, AS_INT, frozen);
+       addstat(STAT_REVIVE_PROGRESS, AS_FLOAT, revive_progress);
 
        // g_movementspeed hack
        addstat(STAT_MOVEVARS_AIRSPEEDLIMIT_NONQW, AS_FLOAT, stat_sv_airspeedlimit_nonqw);
@@ -824,6 +835,10 @@ void spawnfunc_worldspawn (void)
        // secrets
        addstat(STAT_SECRETS_TOTAL, AS_FLOAT, stat_secrets_total);
        addstat(STAT_SECRETS_FOUND, AS_FLOAT, stat_secrets_found);
+       
+       // monsters
+       addstat(STAT_MONSTERS_TOTAL, AS_FLOAT, stat_monsters_total);
+       addstat(STAT_MONSTERS_KILLED, AS_FLOAT, stat_monsters_killed);
 
        // misc
        addstat(STAT_RESPAWN_TIME, AS_FLOAT, stat_respawn_time);
@@ -936,6 +951,8 @@ void spawnfunc_worldspawn (void)
        // weird mutators that deserve to count as mod
        if(autocvar_g_minstagib)
                modname = "MinstaGib";
+       if(autocvar_g_monsters)
+               modname = "Monsters";
        // extra mutators that deserve to count as mod
        MUTATOR_CALLHOOK(SetModname);
 
@@ -2111,6 +2128,36 @@ float WinningCondition_RanOutOfSpawns()
                return WINNING_NO;
 }
 
+// TD winning condition:
+// game terminates if there are no generators (or 1 dies if td_dontend is TRUE)
+float gensurvived;
+float WinningCondition_TowerDefense()
+{
+       WinningConditionHelper(); // set worldstatus
+
+       if(inWarmupStage)
+               return WINNING_NO;
+
+       // first check if the game has ended
+       if(gendestroyed == TRUE) // FALSE means either generator hasen't spawned yet, or mapper didn't add one
+       if(td_gencount < 1 || !td_dont_end)
+       {
+               ClearWinners();
+               dprint("Everyone lost, ending game.\n");
+               return WINNING_YES;
+       }
+       
+       if(gensurvived)
+       {
+               ClearWinners();
+               SetWinners(winning, 4);
+               return WINNING_YES;
+       }
+
+       // Two or more teams remain
+       return WINNING_NO;
+}
+
 /*
 ============
 CheckRules_World
@@ -2266,6 +2313,10 @@ void CheckRules_World()
        {
                checkrules_status = WinningCondition_Onslaught(); // TODO remove this?
        }
+       else if(g_td)
+       {
+               checkrules_status = WinningCondition_TowerDefense(); // TODO make these mutator hooks?
+       }
        else
        {
                checkrules_status = WinningCondition_Scores(fraglimit, leadlimit);
index e3689cc3d3337d7ced4df19b02ab9c6053b50697..20b383972ff549076c6e723f5e1d5247247eae9a 100644 (file)
@@ -97,6 +97,7 @@ string STR_OBSERVER = "observer";
 #define FOR_EACH_PLAYER(v) FOR_EACH_CLIENT(v) if(v.classname == STR_PLAYER)
 #define FOR_EACH_SPEC(v) FOR_EACH_CLIENT(v) if(v.classname != STR_PLAYER)
 #define FOR_EACH_REALPLAYER(v) FOR_EACH_REALCLIENT(v) if(v.classname == STR_PLAYER)
+#define FOR_EACH_MONSTER(v) for(v = world; (v = findflags(v, flags, FL_MONSTER)) != world; )
 #endif
 
 #define CENTER_OR_VIEWOFS(ent) (ent.origin + ((ent.classname == STR_PLAYER) ? ent.view_ofs : ((ent.mins + ent.maxs) * 0.5)))
@@ -1107,6 +1108,8 @@ string GetGametype(); // g_world.qc
 void readlevelcvars(void)
 {
        g_minstagib = cvar("g_minstagib");
+    
+        monster_skill = cvar("g_monsters_skill");
 
        // load ALL the mutators
        if(cvar("g_dodging"))
@@ -1134,6 +1137,9 @@ void readlevelcvars(void)
        // is this a mutator? is this a mode?
        if(cvar("g_sandbox"))
                MUTATOR_ADD(sandbox);
+        
+    if(cvar("g_za"))
+               MUTATOR_ADD(mutator_zombie_apocalypse);
 
        if(cvar("sv_allow_fullbright"))
                serverflags |= SERVERFLAG_ALLOW_FULLBRIGHT;
diff --git a/qcsrc/server/monsters/lib/defs.qh b/qcsrc/server/monsters/lib/defs.qh
new file mode 100644 (file)
index 0000000..a795bab
--- /dev/null
@@ -0,0 +1,39 @@
+.float sprite_height;
+
+.void()                attack_melee;
+.float()       attack_ranged;
+.float()       checkattack;
+
+.float spawner_monstercount;
+
+.float monster_respawned; // used to make sure we're not recounting respawned monster stats
+
+float monsters_spawned;
+
+const float MONSTERSKILL_NOTEASY = 256; // monster will not spawn on skill <= 2
+const float MONSTERSKILL_NOTMEDIUM = 512; // monster will not spawn on skill 3
+const float MONSTERSKILL_NOTHARD = 1024; // monster will not spawn on skill 4
+const float MONSTERSKILL_NOTINSANE = 2048; // monster will not spawn on skill 5
+const float MONSTERSKILL_NOTNIGHTMARE = 4096; // monster will not spawn on skill >= 6
+
+const float MONSTERFLAG_NORESPAWN = 2;
+const float MONSTERFLAG_MINIBOSS = 64;  // monster spawns as mini-boss (also has a chance of naturally becoming one)
+const float MONSTERFLAG_NOWANDER = 128; // disable wandering around (currently unused)
+const float MONSTERFLAG_APPEAR = 256; // delay spawn until triggered
+const float MONSTERFLAG_GIANT = 512; // experimental giant monsters feature
+const float MONSTERFLAG_SPAWNED = 1024; // flag for spawned monsters
+
+.void() monster_spawnfunc;
+.void() monster_die;
+.void() monster_delayedattack;
+
+.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
+
+float enemy_range () { return vlen(self.enemy.origin - self.origin); }
+
+float MONSTER_STATE_ATTACK_LEAP = 1; // the start of something big?
diff --git a/qcsrc/server/monsters/lib/monsters.qc b/qcsrc/server/monsters/lib/monsters.qc
new file mode 100644 (file)
index 0000000..dc7ea6f
--- /dev/null
@@ -0,0 +1,783 @@
+// TODO: clean up this file?
+
+void M_Item_Touch ()
+{
+       if(self && other.classname == STR_PLAYER && other.deadflag == DEAD_NO)
+       {
+               Item_Touch();
+               self.think = SUB_Remove;
+               self.nextthink = time + 0.1;
+       }
+}
+
+void Monster_DropItem (string itype, string itemsize)
+{
+       vector backuporigin = self.origin + ((self.mins + self.maxs) * 0.5);
+       entity oldself;
+       
+       oldself = self;
+       self = spawn();
+       
+       if (itype == "armor")
+       {
+               if(itemsize == "large") spawnfunc_item_armor_large();
+               else if (itemsize == "small") spawnfunc_item_armor_small();
+               else if (itemsize == "medium") spawnfunc_item_armor_medium();
+               else print("Invalid monster drop item selected.\n");
+       }
+       else if (itype == "health")
+       {
+               if(itemsize == "large") spawnfunc_item_health_large();
+               else if (itemsize == "small") spawnfunc_item_health_small();
+               else if (itemsize == "medium") spawnfunc_item_health_medium();
+               else if (itemsize == "mega") spawnfunc_item_health_mega();
+               else print("Invalid monster drop item selected.\n");
+       }
+       else if (itype == "ammo")
+       {
+               if(itemsize == "shells") spawnfunc_item_shells();
+               else if (itemsize == "cells") spawnfunc_item_cells();
+               else if (itemsize == "bullets") spawnfunc_item_bullets();
+               else if (itemsize == "rockets") spawnfunc_item_rockets();
+               else print("Invalid monster drop item selected.\n");
+       }
+       else
+       {
+               print("Invalid monster drop item selected.\n");
+               self = oldself;
+               return;
+       }
+       
+       self.velocity = randomvec() * 175 + '0 0 325';
+       
+       self.gravity = 1;
+       self.origin = backuporigin;
+       
+       self.touch = M_Item_Touch;
+       
+       SUB_SetFade(self, time + 5, 1);
+       
+       self = oldself;
+}
+
+float monster_isvalidtarget (entity targ, entity ent, float neutral)
+{
+       if(!targ || !ent)
+               return FALSE; // this check should fix a crash
+               
+       if(targ.vehicle_flags & VHF_ISVEHICLE)
+               targ = targ.vehicle;
+               
+       if(time < game_starttime)
+               return FALSE; // monsters do nothing before the match has started
+               
+       traceline(ent.origin, targ.origin, FALSE, ent);
+       
+       if(vlen(targ.origin - ent.origin) >= ent.target_range)
+               return FALSE; // enemy is too far away
+
+       if(trace_ent != targ)
+               return FALSE; // we can't see the enemy
+               
+       if(neutral == TRUE)
+               return TRUE; // we come in peace!
+               
+       if(targ.takedamage == DAMAGE_NO)
+               return FALSE; // enemy can't be damaged
+               
+       if(targ.items & IT_INVISIBILITY)
+               return FALSE; // enemy is invisible
+       
+       if(targ.classname == STR_SPECTATOR || targ.classname == STR_OBSERVER)
+               return FALSE; // enemy is a spectator
+       
+       if(targ.deadflag != DEAD_NO || ent.deadflag != DEAD_NO || targ.health <= 0 || ent.health <= 0)
+               return FALSE; // enemy/self is dead
+       
+       if(targ.monster_owner == ent || ent.monster_owner == targ)
+               return FALSE; // enemy owns us, or we own them
+       
+       if(targ.flags & FL_NOTARGET)
+               return FALSE; // enemy can't be targetted
+       
+       if not(autocvar_g_monsters_typefrag)
+       if(targ.BUTTON_CHAT)
+               return FALSE; // no typefragging!
+       
+       if not(IsDifferentTeam(targ, ent))
+               return FALSE; // enemy is on our team
+       
+       return TRUE;
+}
+
+float enemy_stillvalidtarget(entity targ, entity e)
+{
+       if(targ.health < 1 || targ.deadflag)
+               return FALSE; // let's not keep hurting a corpse
+               
+       if not(autocvar_g_monsters_typefrag)
+       if(targ.BUTTON_CHAT)
+               return FALSE; // no typefrags!
+               
+       if(vlen(targ.origin - e.origin) > e.target_range)
+               return FALSE; // out of our reach
+               
+       if not(targ.takedamage)
+               return FALSE; // can't hurt it
+               
+       if(targ.flags & FL_NOTARGET)
+               return FALSE; // can't target it
+               
+       if(targ.items & IT_INVISIBILITY)
+               return FALSE; // currently not used
+               
+       if(!IsDifferentTeam(targ, e))
+               return FALSE;
+               
+       return TRUE; // all is good, keep going!
+}
+
+entity FindTarget (entity ent) 
+{
+       if(MUTATOR_CALLHOOK(MonsterFindTarget)) { return ent.enemy; } // Handled by a mutator
+       local entity e;
+       for(e = world; (e = findflags(e, monster_attack, TRUE)); ) 
+       {
+               if(monster_isvalidtarget(e, ent, FALSE))
+               {
+                       return e;
+               }
+       }
+       return world;
+}
+
+void MonsterTouch ()
+{
+       if(other == world)
+               return;
+               
+       if(self.enemy != other)
+       if not(other.flags & FL_MONSTER)
+       if(monster_isvalidtarget(other, self, FALSE))
+               self.enemy = other;
+}
+
+void monster_melee (entity targ, float damg, float er, float deathtype)
+{
+       float bigdmg = 0, rdmg = damg * random();
+
+       if (self.health <= 0)
+               return;
+       if (targ == world)
+               return;
+
+       if (vlen(self.origin - targ.origin) > er * self.scale)
+               return;
+               
+       bigdmg = rdmg * self.scale;
+       
+       Damage(targ, self, self, bigdmg * monster_skill, deathtype, targ.origin, normalize(targ.origin - self.origin));
+}
+
+void Monster_CheckDropCvars (string mon)
+{
+       string dropitem;
+       string dropsize;
+       
+       dropitem = cvar_string(strcat("g_monster_", mon, "_drop"));
+       dropsize = cvar_string(strcat("g_monster_", mon, "_drop_size"));
+       
+       monster_dropitem = dropitem;
+       monster_dropsize = dropsize;
+       MUTATOR_CALLHOOK(MonsterDropItem);
+       dropitem = monster_dropitem;
+       dropsize = monster_dropsize;
+       
+       if(autocvar_g_monsters_forcedrop)
+               Monster_DropItem(autocvar_g_monsters_drop_type, autocvar_g_monsters_drop_size);
+       else if(dropitem != "")
+               Monster_DropItem(dropitem, dropsize);      
+       else
+               Monster_DropItem("armor", "medium");
+}
+
+void ScaleMonster (float scle)
+{
+       // this should prevent monster from falling through floor when scale changes
+       self.scale = scle;
+       setorigin(self, self.origin + ('0 0 30' * scle));
+}
+
+void Monster_CheckMinibossFlag ()
+{
+       if(MUTATOR_CALLHOOK(MonsterCheckBossFlag))
+               return;
+               
+       float r = random() * 4, 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;
+               ScaleMonster(1.5);
+               self.flags |= MONSTERFLAG_MINIBOSS;
+               
+               if (r < 2 || self.team == COLOR_TEAM2)
+               {
+                       self.strength_finished = -1;  
+                       self.effects |= (EF_FULLBRIGHT | EF_BLUE);
+               }
+               else if (r >= 1 || self.team == COLOR_TEAM1)
+               {
+                       self.invincible_finished = -1;
+                       self.effects |= (EF_FULLBRIGHT | EF_RED);
+               }
+               else
+                       self.effects |= (EF_FULLBRIGHT | EF_RED | EF_BLUE);
+               
+               if(teamplay)
+               if(self.team)
+                       return;
+                       
+               self.colormod = randomvec() * 4;
+       }
+}
+
+float Monster_CanRespawn(entity ent)
+{
+       other = ent;
+       if(MUTATOR_CALLHOOK(MonsterRespawn))
+               return TRUE; // enabled by a mutator
+               
+       if(ent.spawnflags & MONSTERFLAG_NORESPAWN)
+               return FALSE;
+               
+       if not(autocvar_g_monsters_respawn)
+               return FALSE;
+               
+       return TRUE;
+}
+
+void Monster_Fade ()
+{
+       if(Monster_CanRespawn(self))
+       {
+               self.monster_respawned = TRUE;
+               setmodel(self, "");
+               self.think = self.monster_spawnfunc;
+               self.nextthink = time + self.respawntime;
+               setorigin(self, self.pos1);
+               self.angles = self.pos2;
+               self.health = self.max_health; // TODO: check if resetting to max_health is wise here
+               return;
+       }
+       self.think = SUB_Remove;
+       self.nextthink = time + 4;
+       SUB_SetFade(self, time + 3, 1);
+}
+
+float Monster_CanJump (vector vel)
+{
+       local 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 not(self.flags & FL_ONGROUND)
+               return FALSE;
+       if(self.health < 1)
+               return FALSE; // called when dead?
+       if not(Monster_CanJump(vel))
+               return FALSE;
+               
+       self.frame = anm;
+       self.state = MONSTER_STATE_ATTACK_LEAP;
+       self.touch = touchfunc;
+       self.origin_z += 1;
+       self.velocity = vel;
+       if (self.flags & FL_ONGROUND)
+               self.flags -= FL_ONGROUND;
+               
+       self.attack_finished_single = time + anim_finished;
+       
+       return TRUE;
+}
+
+float GenericCheckAttack ()
+{
+       // checking attack while dead?
+       if (self.health <= 0 || self.enemy == world)
+               return FALSE;
+               
+       if(self.monster_delayedattack && self.delay != -1)
+       {
+               if(time < self.delay)
+                       return FALSE;
+                       
+               self.monster_delayedattack();
+       }
+       
+       if (time < self.attack_finished_single)
+               return FALSE;
+       
+       if (enemy_range() > 2000) // long traces are slow
+               return FALSE;   
+               
+       if(self.attack_melee)
+       if(enemy_range() <= 100 * self.scale)
+       {
+               self.attack_melee(); // don't wait for nextthink - too slow
+               return TRUE;
+       }
+       
+       // monster doesn't have a ranged attack function, so stop here
+       if(!self.attack_ranged)
+               return FALSE;
+
+       // see if any entities are in the way of the shot
+       if (!findtrajectorywithleading(self.origin, '0 0 0', '0 0 0', self.enemy, 800, 0, 2.5, 0, self))
+               return FALSE;
+
+       self.attack_ranged(); // don't wait for nextthink - too slow
+       return TRUE;
+}
+
+void monster_use ()
+{
+       if (self.enemy)
+               return;
+       if (self.health <= 0)
+               return;
+
+       if(!monster_isvalidtarget(activator, self, -1))
+               return;
+
+       self.enemy = activator;
+}
+
+float trace_path(vector from, vector to)
+{
+       vector dir = normalize(to - from) * 15, offset = '0 0 0';
+       float trace1 = trace_fraction;
+       
+       offset_x = dir_y;
+       offset_y = -dir_x;
+       traceline (from+offset, to+offset, TRUE, self);
+       
+       traceline(from-offset, to-offset, TRUE, self);
+               
+       return ((trace1 < trace_fraction) ? trace1 : trace_fraction);
+}
+
+vector monster_pickmovetarget(entity targ)
+{
+       // enemy is always preferred target
+       if(self.enemy)
+       {
+               self.monster_movestate = MONSTER_MOVE_ENEMY;
+               return self.enemy.origin;
+       }
+       if(targ)
+       {
+               self.monster_movestate = MONSTER_MOVE_WANDER;
+               return targ.origin;
+       }
+       
+       switch(self.monster_moveflags)
+       {
+               case MONSTER_MOVE_OWNER:
+               {
+                       self.monster_movestate = MONSTER_MOVE_OWNER;
+                       if(self.monster_owner && self.monster_owner.classname != "monster_swarm")
+                               return self.monster_owner.origin;
+               }
+               case MONSTER_MOVE_WANDER:
+               {
+                       self.monster_movestate = MONSTER_MOVE_WANDER;
+                               
+                       self.angles_y = random() * 500;
+                       makevectors(self.angles);
+                       return self.origin + v_forward * 600;
+               }
+               case MONSTER_MOVE_SPAWNLOC:
+               {
+                       self.monster_movestate = MONSTER_MOVE_SPAWNLOC;
+                       return self.pos1;
+               }
+               default:
+               case MONSTER_MOVE_NOMOVE:
+               {
+                       self.monster_movestate = MONSTER_MOVE_NOMOVE;
+                       return self.origin;
+               }
+       }
+}
+
+.float last_trace;
+void monster_move(float runspeed, float walkspeed, float stopspeed, float manim_run, float manim_walk, float manim_idle)
+{
+       if(self.target)
+               self.goalentity = find(world, targetname, self.target);
+               
+       entity targ;
+
+       if(self.frozen)
+       {
+               self.revive_progress = bound(0, self.revive_progress + frametime * self.revive_speed, 1);
+               self.health = max(1, self.max_health * self.revive_progress);
+               
+               if(self.sprite) WaypointSprite_UpdateHealth(self.sprite, self.health);
+                       
+               movelib_beak_simple(stopspeed);
+                       
+               self.velocity = '0 0 0';
+               self.enemy = world;
+               self.nextthink = time + 0.1;
+               
+               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.last_trace = time + 0.4;
+                               self.angles = '0 0 -90';
+                               Damage (self, world, world, 2, DEATH_DROWN, self.origin, '0 0 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
+               {
+                       self.angles = '0 0 0';
+                       self.movetype = MOVETYPE_WALK;
+               }
+       }
+       
+       if(gameover || time < game_starttime)
+       {
+               runspeed = walkspeed = 0;
+               self.frame = manim_idle;
+               movelib_beak_simple(stopspeed);
+               return;
+       }
+       
+       targ = self.goalentity;
+       
+       monster_target = targ;
+       monster_speed_run = runspeed;
+       monster_speed_walk = walkspeed;
+       MUTATOR_CALLHOOK(MonsterMove);
+       targ = monster_target;
+       runspeed = monster_speed_run;
+       walkspeed = monster_speed_walk;
+               
+       if(IsDifferentTeam(self.monster_owner, self))
+               self.monster_owner = world;
+               
+       if(!enemy_stillvalidtarget(self.enemy, self))
+               self.enemy = world;
+               
+       if not(self.enemy)
+               self.enemy = FindTarget(self);
+               
+       if(time >= self.last_trace)
+       {
+               if(self.monster_movestate == MONSTER_MOVE_WANDER && self.goalentity.classname != "td_waypoint")
+                       self.last_trace = time + 2;
+               else
+                       self.last_trace = time + 0.5;
+               self.moveto = monster_pickmovetarget(targ);
+       }
+       
+       vector angles_face = vectoangles(self.moveto - self.origin);
+       vector owner_face = vectoangles(self.monster_owner.origin - self.origin);
+       self.angles_y = angles_face_y;
+       
+       if(self.state == MONSTER_STATE_ATTACK_LEAP && (self.flags & FL_ONGROUND))
+       {
+               self.state = 0;
+               self.touch = MonsterTouch;
+       }
+        
+       v_forward = normalize(self.moveto - self.origin);
+       
+       float l = vlen(self.moveto - self.origin);
+       float t1 = trace_path(self.origin+'0 0 10', self.moveto+'0 0 10');
+       float t2 = trace_path(self.origin-'0 0 15', self.moveto-'0 0 15'); 
+       
+       if(t1*l-t2*l>50 && (t1*l > 100 || t1 > 0.8))
+       if(self.flags & FL_ONGROUND)
+               movelib_jump_simple(100);
+
+       if(vlen(self.origin - self.moveto) > 64)
+       {
+               if(self.flags & FL_FLY)
+                       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)
+                       self.frame = ((self.enemy) ? manim_run : manim_walk);
+       }
+       else
+       {
+               movelib_beak_simple(stopspeed);
+               if(time > self.attack_finished_single)
+               if(time > self.pain_finished)
+               if (vlen(self.velocity) <= 30)
+               {
+                       self.frame = manim_idle;
+                       self.angles_y = ((self.monster_owner) ? owner_face_y : self.pos2_y); // reset looking angle now?
+               }
+       }
+               
+       if(self.enemy && self.checkattack)
+               self.checkattack();
+}
+
+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();
+}
+
+void monsters_damage (entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+{
+       if(self.frozen)
+               return;
+               
+       if((ignore_turrets && !(attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET)) || !ignore_turrets)
+       if(monster_isvalidtarget(attacker, self, FALSE))
+               self.enemy = attacker;
+       
+       self.health -= damage;
+       
+       if(self.sprite)
+               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, ATTN_NORM);  // FIXME: PLACEHOLDER
+       
+       self.velocity += force * self.damageforcescale;
+               
+       if(deathtype != DEATH_DROWN)
+       {
+               Violence_GibSplash_At(hitloc, force, 2, bound(0, damage, 200) / 16, self, attacker);
+               if (damage > 50)
+                       Violence_GibSplash_At(hitloc, force * -0.1, 3, 1, self, attacker);
+               if (damage > 100)
+                       Violence_GibSplash_At(hitloc, force * -0.2, 3, 1, self, attacker);
+       }
+               
+       if(self.health <= 0)
+       {        
+               if(self.sprite)
+               {
+                       // Update one more time to avoid waypoint fading without emptying healthbar
+                       WaypointSprite_UpdateHealth(self.sprite, 0);
+               }
+               
+               if(self.flags & MONSTERFLAG_MINIBOSS) // TODO: cvarise the weapon drop?
+                       W_ThrowNewWeapon(self, WEP_NEX, 0, self.origin, self.velocity);
+                       
+               activator = attacker;
+               other = self.enemy;
+               self.target = self.target2;
+               self.target2 = "";
+               SUB_UseTargets();
+       
+               self.monster_die();
+               
+               frag_attacker = attacker;
+               frag_target = self;
+               MUTATOR_CALLHOOK(MonsterDies);
+       }
+}
+
+// used to hook into monster post death functions without a mutator
+void monster_hook_death()
+{
+       if(self.sprite)
+        WaypointSprite_Kill(self.sprite);
+               
+       if(!(self.spawnflags & MONSTERFLAG_SPAWNED) && !self.monster_respawned)
+               monsters_killed += 1;
+               
+       if(self.realowner.classname == "monster_spawner")
+               self.realowner.spawner_monstercount -= 1;
+               
+       if(self.realowner.flags & FL_CLIENT)
+               self.realowner.monstercount -= 1;
+               
+       totalspawned -= 1;
+}
+
+// used to hook into monster post spawn functions without a mutator
+void monster_hook_spawn()
+{
+       Monster_CheckMinibossFlag();
+
+       self.max_health = self.health;
+       
+       if(teamplay && self.team)
+       {
+               self.colormod = TeamColor(self.team);
+               self.monster_attack = TRUE;
+       }
+       
+       if (self.target)
+       {
+               self.target2 = self.target;
+               self.goalentity = find(world, targetname, self.target);
+       }
+               
+       if(autocvar_g_monsters_healthbars)
+       {
+               WaypointSprite_Spawn(self.netname, 0, 600, self, '0 0 1' * self.sprite_height, world, 0, self, sprite, FALSE, RADARICON_DANGER, ((teamplay) ? TeamColor(self.team) : '1 0 0')); 
+               WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
+               WaypointSprite_UpdateHealth(self.sprite, self.health);
+       }
+       
+       MUTATOR_CALLHOOK(MonsterSpawn);
+}
+
+float monster_initialize(string  net_name,
+                                                string  bodymodel,
+                                                vector  min_s,
+                                                vector  max_s,
+                                                float   nodrop,
+                                                void() dieproc,
+                                                void() spawnproc)
+{
+       if not(autocvar_g_monsters)
+               return FALSE;
+               
+       // support for quake style removing monsters based on skill
+       if(monster_skill <= autocvar_g_monsters_skill_easy && (self.spawnflags & MONSTERSKILL_NOTEASY)) { return FALSE; }
+       if(monster_skill == autocvar_g_monsters_skill_normal && (self.spawnflags & MONSTERSKILL_NOTMEDIUM)) { return FALSE; }
+       if(monster_skill == autocvar_g_monsters_skill_hard && (self.spawnflags & MONSTERSKILL_NOTHARD)) { return FALSE; }
+       if(monster_skill == autocvar_g_monsters_skill_insane && (self.spawnflags & MONSTERSKILL_NOTINSANE)) { return FALSE; }
+       if(monster_skill >= autocvar_g_monsters_skill_nightmare && (self.spawnflags & MONSTERSKILL_NOTNIGHTMARE)) { return FALSE; }
+
+       if(self.model == "")
+       if(bodymodel == "")
+               error("monsters: missing bodymodel!");
+
+       if(self.netname == "")
+       {
+               if(net_name != "" && self.realowner.classname == STR_PLAYER)
+                       net_name = strzone(strdecolorize(sprintf("%s's %s", self.realowner.netname, net_name)));
+               self.netname = ((net_name == "") ? self.classname : net_name);
+       }
+       
+       if not(self.scale)
+               self.scale = 1;
+       
+       if(self.spawnflags & MONSTERFLAG_GIANT && !autocvar_g_monsters_nogiants)
+               ScaleMonster(5);
+       else
+               ScaleMonster(self.scale);
+               
+       min_s *= self.scale;
+       max_s *= self.scale;
+
+       if(self.team && !teamplay)
+               self.team = 0;
+
+       self.flags = FL_MONSTER;
+       
+       if(self.model != "")
+               bodymodel = self.model;
+               
+       if not(self.spawnflags & MONSTERFLAG_SPAWNED) // naturally spawned monster
+       if not(self.monster_respawned)
+               monsters_total += 1;
+       
+       precache_model(bodymodel);
+
+       setmodel(self, bodymodel);
+       
+       setsize(self, min_s, max_s);
+
+       self.takedamage                 = DAMAGE_AIM;
+       self.bot_attack                 = TRUE;
+       self.iscreature                 = TRUE;
+       self.teleportable               = TRUE;
+       self.damagedbycontents  = TRUE;
+       self.damageforcescale   = 0.003;
+       self.monster_die                = dieproc;
+       self.event_damage               = monsters_damage;
+       self.touch                              = MonsterTouch;
+       self.use                                = monster_use;
+       self.solid                              = SOLID_BBOX;
+       self.movetype                   = MOVETYPE_WALK;
+       self.delay                              = -1; // used in attack delay code
+       monsters_spawned           += 1;
+       self.think                              = spawnproc;
+       self.nextthink                  = time;
+       self.enemy                              = world;
+       self.velocity                   = '0 0 0';
+       self.moveto                             = self.origin;
+       self.pos1                               = self.origin;
+       self.pos2                               = self.angles;
+       
+       if not(self.target_range)
+               self.target_range = autocvar_g_monsters_target_range;
+       
+       if not(self.respawntime)
+               self.respawntime = autocvar_g_monsters_respawn_delay;
+       
+       if not(self.monster_moveflags)
+               self.monster_moveflags = MONSTER_MOVE_WANDER;
+
+       if(autocvar_g_nodepthtestplayers)
+               self.effects |= EF_NODEPTHTEST;
+
+       if(autocvar_g_fullbrightplayers)
+               self.effects |= EF_FULLBRIGHT;
+
+       if not(nodrop)
+       {
+               setorigin(self, self.origin);
+               tracebox(self.origin + '0 0 100', min_s, max_s, self.origin - '0 0 10000', MOVE_WORLDONLY, self);
+               setorigin(self, trace_endpos);
+       }
+
+       return TRUE;
+}
diff --git a/qcsrc/server/monsters/lib/spawn.qc b/qcsrc/server/monsters/lib/spawn.qc
new file mode 100644 (file)
index 0000000..bb707b3
--- /dev/null
@@ -0,0 +1,70 @@
+float spawnmonster_checkinlist(string monster, string list)
+{
+       string l = strcat(" ", list, " ");
+       
+       if(strstrofs(l, strcat(" ", monster, " "), 0) >= 0)
+               return TRUE;
+       
+       return FALSE;
+}
+
+entity spawnmonster (string monster, entity spawnedby, entity own, vector orig, float respwn, float moveflag)
+{
+       if not(autocvar_g_monsters)
+       {
+               if(spawnedby.flags & FL_CLIENT)
+                       sprint(spawnedby, "Monsters are disabled. Enable g_monsters to spawn monsters\n");
+               return world;
+       }
+       
+       if(spawnedby.vehicle) // no vehicle player spawning...
+               return world;
+       
+       if(!spawncode_first_load)
+       {
+               initialize_field_db();
+               spawncode_first_load = TRUE;
+       }
+       
+       entity e = spawn();
+       
+       e.spawnflags = MONSTERFLAG_SPAWNED;
+       
+       if not(respwn)
+               e.spawnflags |= MONSTERFLAG_NORESPAWN;
+       
+       setorigin(e, orig);
+       
+       if not(spawnmonster_checkinlist(monster, autocvar_g_monsters_spawn_list))
+               monster = "knight";
+       
+       e.realowner = spawnedby;
+       
+       if(moveflag)
+               e.monster_moveflags = moveflag;
+       
+       if (spawnedby.classname == "monster_swarm")
+               e.monster_owner = own;  
+       else if(spawnedby.flags & FL_CLIENT)
+       {
+               if(teamplay && autocvar_g_monsters_teams)
+                       e.team = spawnedby.team; // colors handled in spawn code
+                       
+               if not(teamplay)
+                       e.colormap = spawnedby.colormap;
+                       
+               if(autocvar_g_monsters_owners)
+                       e.monster_owner = own; // using owner makes the monster non-solid for its master
+                       
+               e.angles = spawnedby.angles;
+       }
+       
+       if(autocvar_g_monsters_giants_only)
+               e.spawnflags |= MONSTERFLAG_GIANT;
+               
+       monster = strcat("$ spawnfunc_monster_", monster);
+               
+       target_spawn_edit_entity(e, monster, world, world, world, world, world);
+               
+       return e;
+}
diff --git a/qcsrc/server/monsters/monster/demon.qc b/qcsrc/server/monsters/monster/demon.qc
new file mode 100644 (file)
index 0000000..44c5990
--- /dev/null
@@ -0,0 +1,134 @@
+// cvars
+float autocvar_g_monster_demon;
+float autocvar_g_monster_demon_health;
+float autocvar_g_monster_demon_attack_jump_damage;
+float autocvar_g_monster_demon_damage;
+float autocvar_g_monster_demon_speed_walk;
+float autocvar_g_monster_demon_speed_run;
+
+// size
+const vector DEMON_MIN = '-32 -32 -24';
+const vector DEMON_MAX = '32 32 24';
+
+// animation
+#define demon_anim_stand  0
+#define demon_anim_walk   1
+#define demon_anim_run 2
+#define demon_anim_leap   3
+#define demon_anim_pain   4
+#define demon_anim_death  5
+#define demon_anim_attack 6
+
+void demon_think ()
+{
+       self.think = demon_think;
+       self.nextthink = time + 0.1;
+       
+       monster_move(autocvar_g_monster_demon_speed_run, autocvar_g_monster_demon_speed_walk, 100, demon_anim_run, demon_anim_walk, demon_anim_stand);
+}
+
+void demon_attack_melee ()
+{
+       float bigdmg = autocvar_g_monster_demon_damage * self.scale;
+       
+       self.frame = demon_anim_attack;
+       self.attack_finished_single = time + 1;
+       
+       monster_melee(self.enemy, bigdmg * monster_skill, 120, DEATH_MONSTER_DEMON_MELEE);
+}
+
+void Demon_JumpTouch ()
+{
+       if (self.health <= 0)
+               return;
+               
+       float bigdmg = autocvar_g_monster_demon_attack_jump_damage * self.scale;
+
+       if (monster_isvalidtarget(other, self, FALSE))
+       {
+               if (vlen(self.velocity) > 300)
+               {
+                       Damage(other, self, self, bigdmg * monster_skill, DEATH_MONSTER_DEMON_JUMP, other.origin, normalize(other.origin - self.origin));
+                       self.touch = MonsterTouch; // instantly turn it off to stop damage spam
+               }
+       }
+
+       if(self.flags & FL_ONGROUND)
+               self.touch = MonsterTouch;
+}
+
+float demon_jump ()
+{
+       makevectors(self.angles);
+       if(monster_leap(demon_anim_leap, Demon_JumpTouch, v_forward * 700 + '0 0 300', 0.8))
+               return TRUE;
+               
+       return FALSE;
+}
+
+void demon_die ()
+{
+       Monster_CheckDropCvars ("demon");
+       
+       self.frame                      = demon_anim_death;
+       self.think                      = Monster_Fade; 
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.movetype           = MOVETYPE_TOSS;
+       self.enemy                      = world;
+       self.nextthink          = time + 3;
+       self.pain_finished  = self.nextthink;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void demon_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_demon_health * self.scale;
+
+       self.damageforcescale   = 0;
+       self.classname                  = "monster_demon";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = demon_attack_melee;
+       self.attack_ranged              = demon_jump;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.frame                              = demon_anim_stand;
+       self.think                              = demon_think;
+       self.sprite_height              = 30 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+/* QUAKED monster_demon (1 0 0) (-32 -32 -24) (32 32 64) Ambush */
+void spawnfunc_monster_demon ()
+{      
+       if not(autocvar_g_monster_demon) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_demon;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Fiend",
+                        "models/monsters/demon.mdl",
+                        DEMON_MIN, DEMON_MAX,
+                        FALSE,
+                        demon_die, demon_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
+
+// Compatibility with old spawns
+void spawnfunc_monster_demon1 () { spawnfunc_monster_demon(); }
diff --git a/qcsrc/server/monsters/monster/dog.qc b/qcsrc/server/monsters/monster/dog.qc
new file mode 100644 (file)
index 0000000..c8cbc61
--- /dev/null
@@ -0,0 +1,123 @@
+// size
+const vector DOG_MAX = '16 16 12';
+const vector DOG_MIN = '-16 -16 -24';
+
+// cvars
+float autocvar_g_monster_dog;
+float autocvar_g_monster_dog_health;
+float autocvar_g_monster_dog_bite_damage;
+float autocvar_g_monster_dog_attack_jump_damage;
+float autocvar_g_monster_dog_speed_walk;
+float autocvar_g_monster_dog_speed_run;
+
+// animations
+#define dog_anim_idle          0
+#define dog_anim_walk          1
+#define dog_anim_run           2
+#define dog_anim_attack        3
+#define dog_anim_die           4
+#define dog_anim_pain          5
+
+void Dog_JumpTouch ()
+{
+       float bigdmg = autocvar_g_monster_dog_attack_jump_damage * self.scale;
+       if (self.health <= 0)
+               return;
+
+       if (other.takedamage)
+       {
+               if (vlen(self.velocity) > 300)
+                       Damage(self.enemy, self, self, bigdmg * monster_skill, DEATH_MONSTER_DOG_JUMP, self.enemy.origin, normalize(self.enemy.origin - self.origin));
+       }
+
+       if(self.flags & FL_ONGROUND)
+               self.touch = MonsterTouch;
+}
+
+void dog_think ()
+{
+       self.think = dog_think;
+       self.nextthink = time + 0.1;
+       
+       monster_move(autocvar_g_monster_dog_speed_run, autocvar_g_monster_dog_speed_walk, 50, dog_anim_run, dog_anim_walk, dog_anim_idle);
+}
+
+void dog_attack ()
+{
+       float bigdmg = autocvar_g_monster_dog_bite_damage * self.scale;
+       
+       self.frame = dog_anim_attack;
+       self.attack_finished_single = time + 0.7;
+
+       monster_melee(self.enemy, bigdmg * monster_skill, 100, DEATH_MONSTER_DOG_BITE);
+}
+
+float dog_jump ()
+{
+       makevectors(self.angles);
+       if(monster_leap(dog_anim_attack, Dog_JumpTouch, v_forward * 300 + '0 0 200', 0.8))
+               return TRUE;
+               
+       return FALSE;
+}
+
+void dog_die ()
+{
+       Monster_CheckDropCvars ("dog");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.think                      = Monster_Fade;
+       self.pain_finished  = self.nextthink;
+       self.movetype           = MOVETYPE_TOSS;
+       self.frame                      = dog_anim_die;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void dog_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_dog_health * self.scale;
+
+       self.damageforcescale   = 0;
+       self.classname                  = "monster_dog";
+       self.attack_melee               = dog_attack;
+       self.attack_ranged              = dog_jump;
+       self.checkattack                = GenericCheckAttack;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = dog_think;
+       self.frame                              = dog_anim_idle;
+       self.sprite_height              = 20 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_dog ()
+{      
+       if not(autocvar_g_monster_dog) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_dog;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       if not (monster_initialize(
+                        "Cerberus",
+                        "models/monsters/dog.dpm",
+                        DOG_MIN, DOG_MAX,
+                        FALSE,
+                        dog_die, dog_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/enforcer.qc b/qcsrc/server/monsters/monster/enforcer.qc
new file mode 100644 (file)
index 0000000..fb089af
--- /dev/null
@@ -0,0 +1,201 @@
+// size
+const vector ENFORCER_MIN = '-32 -32 0';
+const vector ENFORCER_MAX = '32 32 64';
+
+// cvars
+float autocvar_g_monster_enforcer;
+float autocvar_g_monster_enforcer_health;
+float autocvar_g_monster_enforcer_speed_walk;
+float autocvar_g_monster_enforcer_speed_run;
+float autocvar_g_monster_enforcer_attack_uzi_bullets;
+
+// animations
+#define enforcer_anim_stop             0
+#define enforcer_anim_walk             1
+#define enforcer_anim_run              2
+#define enforcer_anim_walkback         3
+#define enforcer_anim_runback  4
+
+void enforcer_think ()
+{
+       self.think = enforcer_think;
+       self.nextthink = time + 0.1;
+       
+       if(self.delay != -1)
+               self.nextthink = self.delay;
+       
+       monster_move(autocvar_g_monster_enforcer_speed_run, autocvar_g_monster_enforcer_speed_walk, 100, enforcer_anim_run, enforcer_anim_walk, enforcer_anim_stop);
+}
+
+void enforcer_laser ()
+{
+       self.attack_finished_single = time + 0.8;
+       W_Laser_Attack(0);
+}
+
+float enf_missile_laser ()
+{
+       enforcer_laser();
+       return TRUE;
+}
+
+void enforcer_shotgun ()
+{
+       self.attack_finished_single = time + 0.8;
+       W_Shotgun_Attack();
+}
+
+float enf_missile_shotgun ()
+{
+       enforcer_shotgun();
+       return TRUE;
+}
+
+.float enf_cycles;
+void enforcer_uzi_fire ()
+{
+       self.enf_cycles += 1;
+       
+       if(self.enf_cycles > autocvar_g_monster_enforcer_attack_uzi_bullets)
+       {
+               self.monster_delayedattack = func_null;
+               self.delay = -1;
+               return;
+       }
+       W_UZI_Attack(DEATH_MONSTER_ENFORCER_NAIL);
+       self.delay = time + 0.1;
+       self.monster_delayedattack = enforcer_uzi_fire;
+}
+
+void enforcer_uzi ()
+{
+       self.attack_finished_single = time + 0.8;
+       self.delay = time + 0.1;
+       self.monster_delayedattack = enforcer_uzi_fire;
+}
+
+float enf_missile_uzi ()
+{
+       self.enf_cycles = 0;
+       enforcer_uzi();
+       return TRUE;
+}
+
+void enforcer_rl ()
+{
+       self.attack_finished_single = time + 0.8;
+       W_Rocket_Attack();
+}
+
+float enf_missile_rocket ()
+{
+       enforcer_rl();
+       return TRUE;
+}
+
+void enforcer_electro ()
+{
+       self.attack_finished_single = time + 0.8;
+       W_Electro_Attack();
+}
+
+float enf_missile_plasma ()
+{
+       enforcer_electro();
+       return TRUE;
+}
+
+void enforcer_die ()
+{
+       Monster_CheckDropCvars ("enforcer");
+       
+       self.solid                      = SOLID_NOT;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       
+       remove(self.weaponentity);
+       self.weaponentity = world;
+       
+       if (self.attack_ranged == enf_missile_rocket)
+               W_ThrowNewWeapon(self, WEP_ROCKET_LAUNCHER, 0, self.origin, self.velocity);
+       else if (self.attack_ranged == enf_missile_plasma)
+               W_ThrowNewWeapon(self, WEP_ELECTRO, 0, self.origin, self.velocity);
+       else if (self.attack_ranged == enf_missile_shotgun)
+               W_ThrowNewWeapon(self, WEP_SHOTGUN, 0, self.origin, self.velocity);        
+       else if (self.attack_ranged == enf_missile_uzi)
+               W_ThrowNewWeapon(self, WEP_UZI, 0, self.origin, self.velocity);
+       else
+               W_ThrowNewWeapon(self, WEP_LASER, 0, self.origin, self.velocity);
+               
+       self.frame = enforcer_anim_stop;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void enforcer_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_enforcer_health * self.scale;
+
+       self.damageforcescale   = 0;
+       self.classname                  = "monster_enforcer";
+       self.checkattack                = GenericCheckAttack;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = enforcer_think;
+       self.items                              = (IT_SHELLS | IT_ROCKETS | IT_NAILS | IT_CELLS);
+       self.sprite_height              = 45 * self.scale;
+       
+       self.weaponentity = spawn();
+       self.weaponentity.owner = self;
+       self.weaponentity.team = self.team;
+       self.weaponentity.solid = SOLID_NOT;
+       self.weaponentity.owner = self.weaponentity.realowner = self;
+       self.weaponentity.movetype = MOVETYPE_NOCLIP;
+       setmodel(self.weaponentity, "models/turrets/ewheel-gun1.md3");
+       setattachment(self.weaponentity, self, "tag_head");
+       
+       local float r = random();
+       if (r < 0.20)
+               self.attack_ranged = enf_missile_rocket;
+       else if (r < 0.40)
+               self.attack_ranged = enf_missile_plasma;
+       else if (r < 0.60)
+               self.attack_ranged = enf_missile_shotgun;         
+       else if (r < 0.80)
+               self.attack_ranged = enf_missile_uzi;
+       else
+               self.attack_ranged = enf_missile_laser;
+               
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_enforcer ()
+{      
+       if not(autocvar_g_monster_enforcer) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_enforcer;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       if not (monster_initialize(
+                        "Enforcer",
+                        "models/turrets/ewheel-base2.md3",
+                        ENFORCER_MIN, ENFORCER_MAX,
+                        FALSE,
+                        enforcer_die, enforcer_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/fish.qc b/qcsrc/server/monsters/monster/fish.qc
new file mode 100644 (file)
index 0000000..0e6fe9e
--- /dev/null
@@ -0,0 +1,95 @@
+// size
+const vector FISH_MIN = '-16 -16 -24';
+const vector FISH_MAX = '16 16 16';
+
+// cvars
+float autocvar_g_monster_fish;
+float autocvar_g_monster_fish_health;
+float autocvar_g_monster_fish_damage;
+float autocvar_g_monster_fish_speed_walk;
+float autocvar_g_monster_fish_speed_run;
+
+// animations
+#define fish_anim_attack 0
+#define fish_anim_death  1
+#define fish_anim_swim   2
+#define fish_anim_pain   3
+
+void fish_think ()
+{
+       self.think = fish_think;
+       self.nextthink = time + 0.1;
+       
+       monster_move(autocvar_g_monster_fish_speed_run, autocvar_g_monster_fish_speed_walk, 10, fish_anim_swim, fish_anim_swim, fish_anim_swim);
+}
+
+void fish_attack ()
+{
+       float bigdmg = autocvar_g_monster_fish_damage * self.scale;
+       
+       self.frame = fish_anim_attack;
+       self.attack_finished_single = time + 0.5;
+
+       monster_melee(self.enemy, bigdmg * monster_skill, 60, DEATH_MONSTER_FISH_BITE);
+}
+
+void fish_die ()
+{
+       Monster_CheckDropCvars ("fish");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.pain_finished  = self.nextthink;
+       self.frame                      = fish_anim_death;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void fish_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_fish_health * self.scale;
+
+       self.damageforcescale   = 0.5;
+       self.classname                  = "monster_fish";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = fish_attack;
+       self.flags                         |= FL_SWIM;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = fish_think;
+       self.sprite_height              = 20 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_fish ()
+{      
+       if not(autocvar_g_monster_fish) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_fish;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Rotfish",
+                        "models/monsters/fish.mdl",
+                        FISH_MIN, FISH_MAX,
+                        TRUE,
+                        fish_die, fish_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/hknight.qc b/qcsrc/server/monsters/monster/hknight.qc
new file mode 100644 (file)
index 0000000..9749435
--- /dev/null
@@ -0,0 +1,471 @@
+// size
+const vector HELLKNIGHT_MIN = '-16 -16 -24';
+const vector HELLKNIGHT_MAX = '16 16 32';
+
+// cvars
+float autocvar_g_monster_hellknight;
+float autocvar_g_monster_hellknight_health;
+float autocvar_g_monster_hellknight_melee_damage;
+float autocvar_g_monster_hellknight_inferno_damage;
+float autocvar_g_monster_hellknight_inferno_damagetime;
+float autocvar_g_monster_hellknight_inferno_chance;
+float autocvar_g_monster_hellknight_speed_walk;
+float autocvar_g_monster_hellknight_speed_run;
+float autocvar_g_monster_hellknight_fireball_damage;
+float autocvar_g_monster_hellknight_fireball_force;
+float autocvar_g_monster_hellknight_fireball_radius;
+float autocvar_g_monster_hellknight_fireball_chance;
+float autocvar_g_monster_hellknight_fireball_edgedamage;
+float autocvar_g_monster_hellknight_spike_chance;
+float autocvar_g_monster_hellknight_spike_force;
+float autocvar_g_monster_hellknight_spike_radius;
+float autocvar_g_monster_hellknight_spike_edgedamage;
+float autocvar_g_monster_hellknight_spike_damage;
+float autocvar_g_monster_hellknight_jump_chance;
+float autocvar_g_monster_hellknight_jump_damage;
+float autocvar_g_monster_hellknight_jump_dist;
+
+// animations
+#define hellknight_anim_stand  0
+#define hellknight_anim_walk   1
+#define hellknight_anim_run    2
+#define hellknight_anim_pain   3
+#define hellknight_anim_death1         4
+#define hellknight_anim_death2         5
+#define hellknight_anim_charge1 6
+#define hellknight_anim_magic1         7
+#define hellknight_anim_magic2         8
+#define hellknight_anim_charge2 9
+#define hellknight_anim_slice  10
+#define hellknight_anim_smash  11
+#define hellknight_anim_wattack 12
+#define hellknight_anim_magic3         13
+
+void hknight_spike_think()
+{
+       if(self)
+       {
+               RadiusDamage (self, self.realowner, autocvar_g_monster_hellknight_spike_damage * self.realowner.scale, autocvar_g_monster_hellknight_spike_edgedamage, autocvar_g_monster_hellknight_spike_force, world, autocvar_g_monster_hellknight_spike_radius, WEP_CRYLINK, other);
+               remove(self);
+       }
+}
+
+void hknight_spike_touch()
+{
+       PROJECTILE_TOUCH;
+       
+       pointparticles(particleeffectnum("TE_WIZSPIKE"), self.origin, '0 0 0', 1);
+       
+       hknight_spike_think();
+}
+
+void() hellknight_think;
+void hknight_shoot ()
+{
+       local   entity  missile = world;
+       local   vector  dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
+       local   float   dist = vlen (self.enemy.origin - self.origin), flytime = 0;
+
+       flytime = dist * 0.002;
+       if (flytime < 0.1)
+               flytime = 0.1;
+
+       self.effects |= EF_MUZZLEFLASH;
+       sound (self, CHAN_WEAPON, "weapons/spike.wav", 1, ATTN_NORM);
+
+       missile = spawn ();
+       missile.owner = missile.realowner = self;
+       missile.solid = SOLID_TRIGGER;
+       missile.movetype = MOVETYPE_FLYMISSILE;
+       setsize (missile, '0 0 0', '0 0 0');            
+       setorigin(missile, self.origin + '0 0 10' + v_forward * 14);
+       missile.scale = self.scale;
+       missile.flags = FL_PROJECTILE;
+       missile.velocity = dir * 400;
+       missile.avelocity = '300 300 300';
+       missile.nextthink = time + 5;
+       missile.think = hknight_spike_think;
+       missile.enemy = self.enemy;
+       missile.touch = hknight_spike_touch;
+       CSQCProjectile(missile, TRUE, PROJECTILE_CRYLINK, TRUE);
+}
+
+void hknight_inferno ()
+{
+       traceline((self.absmin + self.absmax) * 0.5, (self.enemy.absmin + self.enemy.absmax) * 0.5, TRUE, world);
+       if (trace_fraction != 1)
+               return; // not visible
+       if(enemy_range() <= 2000)
+               Fire_AddDamage(self.enemy, self, autocvar_g_monster_hellknight_inferno_damage * monster_skill, autocvar_g_monster_hellknight_inferno_damagetime, self.projectiledeathtype);
+}
+
+void hknight_infernowarning ()
+{
+       if(!self.enemy)
+               return;
+               
+       traceline((self.absmin + self.absmax) * 0.5, (self.enemy.absmin + self.enemy.absmax) * 0.5, TRUE, world);
+       if (trace_fraction != 1)
+               return; // not visible
+       self.enemy.effects |= EF_MUZZLEFLASH;
+       sound(self.enemy, CHAN_AUTO, "player/lava.wav", 1, ATTN_NORM);
+       
+       hknight_inferno();
+}
+
+float() hknight_magic;
+float hknight_checkmagic ()
+{
+       local vector v1 = '0 0 0', v2 = '0 0 0';
+       local float dot = 0;
+
+       // use magic to kill zombies as they heal too fast for sword
+       if (self.enemy.classname == "monster_zombie")
+       {
+               traceline((self.absmin + self.absmax) * 0.5, (self.enemy.absmin + self.enemy.absmax) * 0.5, FALSE, self);
+               if (trace_ent == self.enemy)
+               {
+                       hknight_magic();
+                       return TRUE;
+               }
+       }
+
+       if (random() < 0.25)
+               return FALSE; // 25% of the time it won't do anything
+       v1 = normalize(self.enemy.velocity);
+       v2 = normalize(self.enemy.origin - self.origin);
+       dot = v1 * v2;
+       if (dot >= 0.7) // moving away
+       if (vlen(self.enemy.velocity) >= 150) // walking/running away
+               return hknight_magic();
+       return FALSE;
+}
+
+void() hellknight_charge;
+void CheckForCharge ()
+{
+       // check for mad charge
+       if (time < self.attack_finished_single)
+               return;
+       if (fabs(self.origin_z - self.enemy.origin_z) > 20)
+               return;         // too much height change
+       if (vlen (self.origin - self.enemy.origin) < 80)
+               return;         // use regular attack
+       if (hknight_checkmagic())
+               return; // chose magic
+
+       // charge
+       hellknight_charge();
+}
+
+void CheckContinueCharge ()
+{
+       if(hknight_checkmagic())
+               return; // chose magic
+       if(time >= self.attack_finished_single)
+       {
+               hellknight_think();
+               return;         // done charging
+       }
+}
+
+void hellknight_think ()
+{
+       self.think = hellknight_think;
+       self.nextthink = time + 0.1;
+       
+       monster_move(autocvar_g_monster_hellknight_speed_run, autocvar_g_monster_hellknight_speed_walk, 100, hellknight_anim_run, hellknight_anim_walk, hellknight_anim_stand);
+}
+
+.float hknight_cycles;
+void hellknight_magic ()
+{
+       self.hknight_cycles += 1;
+       self.think = hellknight_magic;
+       
+       if(self.hknight_cycles >= 5)
+       {
+               self.frame = hellknight_anim_magic1;
+               self.attack_finished_single = time + 0.7;
+               hknight_infernowarning();
+               self.think = hellknight_think;
+       }
+       
+       self.nextthink = time + 0.1;
+}
+
+void hknight_fireball_explode(entity targ)
+{
+       float scle = self.realowner.scale;
+       if(self)
+       {
+               RadiusDamage (self, self.realowner, autocvar_g_monster_hellknight_fireball_damage * scle, autocvar_g_monster_hellknight_fireball_edgedamage * scle, autocvar_g_monster_hellknight_fireball_force * scle, world, autocvar_g_monster_hellknight_fireball_radius * scle, WEP_FIREBALL, targ);
+               if(targ)
+                       Fire_AddDamage(targ, self, 5 * monster_skill, autocvar_g_monster_hellknight_inferno_damagetime, self.projectiledeathtype);
+               remove(self);
+       }
+}
+
+void hknight_fireball_think()
+{
+       hknight_fireball_explode(world);
+}
+
+void hknight_fireball_touch()
+{
+       PROJECTILE_TOUCH;
+       
+       hknight_fireball_explode(other);
+}
+
+void hellknight_fireball ()
+{
+       local   entity  missile = spawn();
+       local   vector  dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
+       vector fmins = ((self.scale >= 2) ? '-16 -16 -16' : '-4 -4 -4'), fmaxs = ((self.scale >= 2) ? '16 16 16' : '4 4 4');
+
+       self.effects |= EF_MUZZLEFLASH;
+       sound (self, CHAN_WEAPON, "weapons/fireball2.wav", 1, ATTN_NORM);
+
+       missile.owner = missile.realowner = self;
+       missile.solid = SOLID_TRIGGER;
+       missile.movetype = MOVETYPE_FLYMISSILE;
+       setsize (missile, fmins, fmaxs);                
+       setorigin(missile, self.origin + '0 0 10' + v_forward * 14);
+       missile.flags = FL_PROJECTILE;
+       missile.velocity = dir * 400;
+       missile.avelocity = '300 300 300';
+       missile.nextthink = time + 5;
+       missile.think = hknight_fireball_think;
+       missile.enemy = self.enemy;
+       missile.touch = hknight_fireball_touch;
+       CSQCProjectile(missile, TRUE, ((self.scale >= 2) ? PROJECTILE_FIREBALL : PROJECTILE_FIREMINE), TRUE);
+       
+       self.delay = -1;
+}
+
+void hellknight_magic2 ()
+{
+       self.frame = hellknight_anim_magic2;
+       self.attack_finished_single = time + 1.2;
+       self.delay = time + 0.4;
+       self.monster_delayedattack = hellknight_fireball;
+}
+
+void hellknight_spikes ()
+{
+       self.think = hellknight_spikes;
+       self.nextthink = time + 0.1;
+       self.hknight_cycles += 1;
+       hknight_shoot();
+       if(self.hknight_cycles >= 7)
+               self.think = hellknight_think;
+}
+
+void hellknight_magic3 ()
+{
+       self.frame = hellknight_anim_magic3;
+       self.attack_finished_single = time + 1;
+       self.think = hellknight_spikes;
+       self.nextthink = time + 0.4;
+}
+
+void hellknight_charge ()
+{
+       self.frame = hellknight_anim_charge1;
+       self.attack_finished_single = time + 0.5;
+       
+       hknight_checkmagic();
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+       hknight_checkmagic();
+}
+
+void hellknight_charge2 ()
+{
+       self.frame = hellknight_anim_charge2;
+       self.attack_finished_single = time + 0.5;
+       
+       CheckContinueCharge ();
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+}
+
+void hellknight_slice ()
+{
+       self.frame = hellknight_anim_slice;
+       self.attack_finished_single = time + 0.7;
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+}
+
+void hellknight_smash ()
+{
+       self.frame = hellknight_anim_smash;
+       self.attack_finished_single = time + 0.7;
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+}
+
+void hellknight_weapon_attack ()
+{
+       self.frame = hellknight_anim_wattack;
+       self.attack_finished_single = time + 0.7;
+       monster_melee(self.enemy, autocvar_g_monster_hellknight_melee_damage, 70, DEATH_MONSTER_MELEE);
+}
+
+float hknight_type;
+void hknight_melee ()
+{
+       hknight_type += 1;
+
+       if (hknight_type == 1)
+               hellknight_slice();
+       else if (hknight_type == 2)
+               hellknight_smash();
+       else
+       {
+               hellknight_weapon_attack();
+               hknight_type = 0;
+       }
+}
+
+float hknight_magic ()
+{
+       if not(self.flags & FL_ONGROUND)
+               return FALSE;
+               
+       if not(self.enemy)
+               return FALSE; // calling attack check with no enemy?!
+               
+       if(time < self.attack_finished_single)
+               return FALSE;
+               
+       self.hknight_cycles = 0;
+
+       if (self.enemy.classname == "monster_zombie")
+       {
+               // always use fireball to kill zombies
+               hellknight_magic2();
+               self.attack_finished_single = time + 2;
+               return TRUE;
+       }
+       RandomSelection_Init();
+       RandomSelection_Add(world, 0, "fireball", autocvar_g_monster_hellknight_fireball_chance, 1);
+       RandomSelection_Add(world, 0, "inferno", autocvar_g_monster_hellknight_inferno_chance, 1);
+       RandomSelection_Add(world, 0, "spikes", autocvar_g_monster_hellknight_spike_chance, 1);
+       if(self.health >= 100)
+               RandomSelection_Add(world, 0, "jump", ((enemy_range() > autocvar_g_monster_hellknight_jump_dist * self.scale) ? 1 : autocvar_g_monster_hellknight_jump_chance), 1);
+       
+       switch(RandomSelection_chosen_string)
+       {
+               case "fireball":
+               {
+                       hellknight_magic2();
+                       self.attack_finished_single = time + 2;
+                       return TRUE;
+               }
+               case "spikes":
+               {
+                       hellknight_magic3();
+                       self.attack_finished_single = time + 3;
+                       return TRUE;
+               }
+               case "inferno":
+               {
+                       hellknight_magic();
+                       self.attack_finished_single = time + 3;
+                       return TRUE;
+               }
+               case "jump":
+               {
+                       if (enemy_range() >= 400)
+                       if (findtrajectorywithleading(self.origin, self.mins, self.maxs, self.enemy, 1000, 0, 10, 0, self))
+                       {
+                               self.velocity = findtrajectory_velocity;
+                               Damage(self.enemy, self, self, autocvar_g_monster_hellknight_jump_damage * monster_skill, DEATH_VHCRUSH, self.enemy.origin, normalize(self.enemy.origin - self.origin));
+                               self.attack_finished_single = time + 2;
+                               return TRUE;
+                       }
+                       return FALSE;
+               }
+               default:
+                       return FALSE;
+       }
+       // never get here
+}
+
+void hellknight_die ()
+{
+       float chance = random();
+       Monster_CheckDropCvars ("hellknight");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       
+       if(chance < 0.10 || self.flags & MONSTERFLAG_MINIBOSS)
+       {
+               self.superweapons_finished = time + autocvar_g_balance_superweapons_time + 5; // give the player a few seconds to find the weapon
+               W_ThrowNewWeapon(self, WEP_FIREBALL, 0, self.origin, self.velocity);
+       }
+       
+       if (random() > 0.5)
+               self.frame = hellknight_anim_death1;
+       else
+               self.frame = hellknight_anim_death2;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void hellknight_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_hellknight_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_hellknight";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = hknight_melee;
+       self.attack_ranged              = hknight_magic;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = hellknight_think;
+       self.sprite_height              = 30 * self.scale;
+       self.frame                              = hellknight_anim_stand;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_hell_knight ()
+{      
+       if not(autocvar_g_monster_hellknight) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_hell_knight;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Hell-knight",
+                        "models/monsters/hknight.mdl",
+                        HELLKNIGHT_MIN, HELLKNIGHT_MAX,
+                        FALSE,
+                        hellknight_die, hellknight_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_sound ("weapons/spike.wav");
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_hellknight () { spawnfunc_monster_hell_knight(); }
diff --git a/qcsrc/server/monsters/monster/knight.qc b/qcsrc/server/monsters/monster/knight.qc
new file mode 100644 (file)
index 0000000..48026e6
--- /dev/null
@@ -0,0 +1,104 @@
+// size
+const vector KNIGHT_MIN = '-16 -16 -24';
+const vector KNIGHT_MAX = '16 16 32';
+       
+// cvars
+float autocvar_g_monster_knight;
+float autocvar_g_monster_knight_health;
+float autocvar_g_monster_knight_melee_damage;
+float autocvar_g_monster_knight_speed_walk;
+float autocvar_g_monster_knight_speed_run;
+
+// animations
+#define knight_anim_stand              0
+#define knight_anim_run                1
+#define knight_anim_runattack  2
+#define knight_anim_pain1              3
+#define knight_anim_pain2              4
+#define knight_anim_attack             5
+#define knight_anim_walk               6
+#define knight_anim_kneel              7
+#define knight_anim_standing   8
+#define knight_anim_death1             9
+#define knight_anim_death2             10
+
+void knight_think ()
+{
+       self.think = knight_think;
+       self.nextthink = time + 0.1;
+       
+       monster_move(autocvar_g_monster_knight_speed_run, autocvar_g_monster_knight_speed_walk, 50, knight_anim_run, knight_anim_walk, knight_anim_stand);
+}
+
+void knight_attack ()
+{
+       local float len = vlen(self.velocity);
+
+       self.frame = ((len < 50) ? knight_anim_attack : knight_anim_runattack);
+       
+       self.attack_finished_single = time + 0.9;
+       
+       monster_melee(self.enemy, autocvar_g_monster_knight_melee_damage, 80, DEATH_MONSTER_MELEE);
+}
+
+void knight_die ()
+{
+       Monster_CheckDropCvars ("knight");
+               
+       self.frame                      = ((random() > 0.5) ? knight_anim_death1 : knight_anim_death2);
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.think                      = Monster_Fade;
+       self.movetype           = MOVETYPE_TOSS;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void knight_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_knight_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_knight";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = knight_attack;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = knight_think;
+       self.sprite_height              = 30 * self.scale;
+       self.frame                              = knight_anim_stand;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_knight ()
+{      
+       if not(autocvar_g_monster_knight) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_knight;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Knight",
+                        "models/monsters/knight.mdl",
+                        KNIGHT_MIN, KNIGHT_MAX,
+                        FALSE,
+                        knight_die, knight_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/ogre.qc b/qcsrc/server/monsters/monster/ogre.qc
new file mode 100644 (file)
index 0000000..ccab7da
--- /dev/null
@@ -0,0 +1,192 @@
+// size
+const vector OGRE_MIN = '-32 -32 -24';
+const vector OGRE_MAX = '32 32 32';
+// cvars
+float autocvar_g_monster_ogre;
+float autocvar_g_monster_ogre_health;
+float autocvar_g_monster_ogre_chainsaw_damage;
+float autocvar_g_monster_ogre_speed_walk;
+float autocvar_g_monster_ogre_speed_run;
+float autocvar_g_monster_ogre_attack_uzi_bullets;
+
+// animations
+#define ogre_anim_stand        0
+#define ogre_anim_walk                 1
+#define ogre_anim_run          2
+#define ogre_anim_swing        3
+#define ogre_anim_smash        4
+#define ogre_anim_shoot        5
+#define ogre_anim_pain1        6
+#define ogre_anim_pain2        7
+#define ogre_anim_pain3        8
+#define ogre_anim_pain4        9
+#define ogre_anim_pain5        10
+#define ogre_anim_death1       11
+#define ogre_anim_death2       12
+#define ogre_anim_pull                 13
+
+void chainsaw (float side)
+{
+       if (!self.enemy)
+               return;
+
+       if (enemy_range() > 100 * self.scale)
+               return;
+
+       Damage(self.enemy, self, self, autocvar_g_monster_ogre_chainsaw_damage * monster_skill, DEATH_MONSTER_OGRE_CHAINSAW, self.enemy.origin, normalize(self.enemy.origin - self.origin));
+}
+
+void ogre_think ()
+{
+       self.think = ogre_think;
+       self.nextthink = time + 0.1;
+       
+       if(self.delay != -1)
+               self.nextthink = self.delay;
+       
+       monster_move(autocvar_g_monster_ogre_speed_run, autocvar_g_monster_ogre_speed_walk, 300, ogre_anim_run, ogre_anim_walk, ogre_anim_stand);
+}
+
+.float ogre_cycles;
+void ogre_swing ()
+{
+       self.ogre_cycles += 1;
+       self.frame = ogre_anim_swing;
+       if(self.ogre_cycles == 1)
+               self.attack_finished_single = time + 1.3;
+       self.angles_y = self.angles_y + random()* 25;
+       self.nextthink = time + 0.2;
+       self.think = ogre_swing;
+       
+       if(self.ogre_cycles <= 2)
+               chainsaw(200);
+       else if(self.ogre_cycles <= 4)
+               chainsaw(-200);
+       else
+               chainsaw(0);
+       
+       if(self.ogre_cycles >= 4)
+               self.think = ogre_think;
+}
+
+void ogre_uzi_fire ()
+{
+       self.ogre_cycles += 1;
+       
+       if(self.ogre_cycles > autocvar_g_monster_ogre_attack_uzi_bullets)
+       {
+               self.monster_delayedattack = func_null;
+               self.delay = -1;
+               return;
+       }
+       W_UZI_Attack(DEATH_MONSTER_OGRE_NAIL);
+       self.delay = time + 0.1;
+       self.monster_delayedattack = ogre_uzi_fire;
+}
+
+void ogre_uzi ()
+{
+       self.frame = ogre_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       self.delay = time + 0.1;
+       self.monster_delayedattack = ogre_uzi_fire;
+}
+
+void ogre_gl ()
+{
+       W_Grenade_Attack2();
+       self.frame = ogre_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+}
+
+float ogre_missile ()
+{
+       self.ogre_cycles = 0;
+       if (random() < 0.20)
+       {
+               ogre_uzi();
+               return TRUE;
+       }
+       else
+       {
+               ogre_gl();
+               return TRUE;
+       }
+}
+
+void ogre_melee ()
+{
+       self.ogre_cycles = 0;
+       ogre_swing();
+}
+
+void ogre_die()
+{
+       Monster_CheckDropCvars ("ogre");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       
+       W_ThrowNewWeapon(self, WEP_GRENADE_LAUNCHER, 0, self.origin, self.velocity);
+       if (random() < 0.5)
+               self.frame = ogre_anim_death1;
+       else
+               self.frame = ogre_anim_death2;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void ogre_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_ogre_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_ogre";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = ogre_melee;
+       self.frame                              = ogre_anim_pull;
+       self.attack_ranged              = ogre_missile;
+       self.nextthink                  = time + 1;
+       self.think                              = ogre_think;
+       self.sprite_height              = 40 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_ogre ()
+{      
+       if not(autocvar_g_monster_ogre) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_ogre;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Ogre",
+                        "models/monsters/ogre.mdl",
+                        OGRE_MIN, OGRE_MAX,
+                        FALSE,
+                        ogre_die, ogre_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       weapon_action(WEP_GRENADE_LAUNCHER, WR_PRECACHE);
+}
diff --git a/qcsrc/server/monsters/monster/shalrath.qc b/qcsrc/server/monsters/monster/shalrath.qc
new file mode 100644 (file)
index 0000000..76c56d2
--- /dev/null
@@ -0,0 +1,261 @@
+// size
+const vector SHALRATH_MIN = '-32 -32 -24';
+const vector SHALRATH_MAX = '32 32 32';
+
+// cvars
+float autocvar_g_monster_shalrath;
+float autocvar_g_monster_shalrath_health;
+float autocvar_g_monster_shalrath_speed;
+float autocvar_g_monster_shalrath_attack_spike_damage;
+float autocvar_g_monster_shalrath_attack_spike_radius;
+float autocvar_g_monster_shalrath_attack_spike_delay;
+float autocvar_g_monster_shalrath_attack_melee_damage;
+float autocvar_g_monster_shalrath_attack_melee_delay;
+
+// animations
+#define shalrath_anim_idle             0
+#define shalrath_anim_walk             1
+#define shalrath_anim_attack   2
+#define shalrath_anim_pain             3
+#define shalrath_anim_death    4
+#define shalrath_anim_run              5
+
+
+void() ShalMissile;
+
+void shalrath_think ()
+{
+       self.think = shalrath_think;
+       self.nextthink = time + 0.1;
+       
+       if(self.delay != -1)
+               self.nextthink = self.delay;
+       
+       monster_move(autocvar_g_monster_shalrath_speed, autocvar_g_monster_shalrath_speed, 50, shalrath_anim_walk, shalrath_anim_run, shalrath_anim_idle);
+}
+
+void shalrath_attack ()
+{
+       self.frame = shalrath_anim_attack;
+       self.delay = time + 0.2;
+       self.attack_finished_single = time + autocvar_g_monster_shalrath_attack_spike_delay;
+       self.monster_delayedattack = ShalMissile;
+}
+
+void shalrathattack_melee ()
+{
+       float bigdmg = 0, rdmg = autocvar_g_monster_shalrath_attack_melee_damage * random();
+
+       bigdmg = rdmg * self.scale;
+
+       monster_melee(self.enemy, bigdmg * monster_skill, 120, DEATH_MONSTER_SHALRATH_MELEE);
+}
+
+void shalrath_attack_melee ()
+{
+       self.monster_delayedattack = shalrathattack_melee;
+       self.delay = time + 0.2;
+       self.frame = shalrath_anim_attack;
+       self.attack_finished_single = time + autocvar_g_monster_shalrath_attack_melee_delay;
+}
+
+float shal_missile ()
+{
+       // don't throw if it is blocked
+       traceline(self.origin + '0 0 10', self.enemy.origin + '0 0 10', FALSE, self);
+       if (enemy_range() > 1000)
+               return FALSE;
+       if (trace_ent != self.enemy)
+               return FALSE;
+       shalrath_attack();
+       return TRUE;
+}
+
+.float shal_cycles;
+void ShalHome ()
+{
+       local vector dir = '0 0 0', vtemp = self.enemy.origin + '0 0 10';
+       
+       self.shal_cycles += 1;
+       if (self.enemy.health <= 0 || self.owner.health <= 0 || self.shal_cycles >= 20)
+       {
+               remove(self);
+               return;
+       }
+       dir = normalize(vtemp - self.origin);
+       UpdateCSQCProjectile(self);
+       if (monster_skill == 3)
+               self.velocity = dir * 350;
+       else
+               self.velocity = dir * 250;
+       self.nextthink = time + 0.2;
+       self.think = ShalHome;  
+}
+
+void shal_spike_explode ()
+{
+       self.event_damage = func_null;
+
+       pointparticles(particleeffectnum("explosion_small"), self.origin, '0 0 0', 1);
+       RadiusDamage (self, self.realowner, autocvar_g_monster_shalrath_attack_spike_damage, autocvar_g_monster_shalrath_attack_spike_damage * 0.5, autocvar_g_monster_shalrath_attack_spike_radius, world, 0, DEATH_MONSTER_SHALRATH_MELEE, other);
+
+       remove (self);
+}
+
+void shal_spike_touchexplode()
+{
+       PROJECTILE_TOUCH;
+
+       shal_spike_explode();
+}
+
+void ShalMissile ()
+{
+       local   entity  missile = world;
+       local   vector  dir = '0 0 0';
+       local   float   dist = 0;
+       
+       self.effects |= EF_MUZZLEFLASH;
+
+       missile = spawn ();
+       missile.owner = missile.realowner = self;
+       
+       self.v_angle = self.angles;
+       makevectors (self.angles);
+       
+       dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
+       dist = vlen (self.enemy.origin - self.origin);
+
+       missile.think = ShalHome;
+       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');    
+       missile.velocity = dir * 400;
+       missile.avelocity = '300 300 300';
+       missile.enemy = self.enemy;
+       missile.touch = shal_spike_touchexplode;
+       
+       CSQCProjectile(missile, TRUE, PROJECTILE_VORE_SPIKE, TRUE);
+}
+
+float ShalrathCheckAttack ()
+{
+       local vector spot1 = '0 0 0', spot2 = '0 0 0';
+       local entity targ = self.enemy;
+
+       if (self.health <= 0 || targ == world || targ.health < 1)
+               return FALSE;
+       
+       if(self.monster_delayedattack && self.delay != -1)
+       {
+               if(time < self.delay)
+                       return FALSE;
+                       
+               self.monster_delayedattack();
+               self.delay = -1;
+               self.monster_delayedattack = func_null;
+       }
+       
+       if(time < self.attack_finished_single)
+               return FALSE;
+       
+       if (vlen(self.enemy.origin - self.origin) <= 120)
+       {       // melee attack
+               if (self.attack_melee)
+               {
+                       self.attack_melee();
+                       return TRUE;
+               }
+       }
+
+       if (vlen(targ.origin - self.origin) >= 2000) // long traces are slow
+               return FALSE;
+
+// see if any entities are in the way of the shot
+       spot1 = self.origin + '0 0 10';
+       spot2 = targ.origin + '0 0 10';
+
+       traceline (spot1, spot2, FALSE, self);
+
+       if (trace_ent != targ && trace_fraction < 1)
+               return FALSE; // don't have a clear shot
+
+       //if (trace_inopen && trace_inwater)
+       //      return FALSE; // sight line crossed contents
+
+       if (random() < 0.2)
+       if (self.attack_ranged())
+               return TRUE;
+
+       return FALSE;
+}
+
+void shalrath_die ()
+{
+       Monster_CheckDropCvars ("shalrath");
+       
+       self.think                      = Monster_Fade;
+       self.frame                      = shalrath_anim_death;
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;   
+       self.movetype           = MOVETYPE_TOSS;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void shalrath_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_shalrath_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_shalrath";
+       self.checkattack                = ShalrathCheckAttack;
+       self.attack_ranged              = shal_missile;
+       self.attack_melee               = shalrath_attack_melee;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = shalrath_think;
+       self.frame                              = shalrath_anim_walk;
+       self.sprite_height              = 40 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_shalrath ()
+{      
+       if not(autocvar_g_monster_shalrath) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_shalrath;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Mage",
+                        "models/monsters/mage.dpm",
+                        SHALRATH_MIN, SHALRATH_MAX,
+                        FALSE,
+                        shalrath_die, shalrath_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_vore () { spawnfunc_monster_shalrath(); }
diff --git a/qcsrc/server/monsters/monster/shambler.qc b/qcsrc/server/monsters/monster/shambler.qc
new file mode 100644 (file)
index 0000000..102b40b
--- /dev/null
@@ -0,0 +1,205 @@
+// size
+const vector SHAMBLER_MIN = '-32 -32 -24';
+const vector SHAMBLER_MAX = '32 32 64';
+
+// cvars
+float autocvar_g_monster_shambler;
+float autocvar_g_monster_shambler_health;
+float autocvar_g_monster_shambler_damage;
+float autocvar_g_monster_shambler_attack_lightning_damage;
+float autocvar_g_monster_shambler_attack_claw_damage;
+float autocvar_g_monster_shambler_speed_walk;
+float autocvar_g_monster_shambler_speed_run;
+
+// animations
+#define shambler_anim_stand    0
+#define shambler_anim_walk             1
+#define shambler_anim_run              2
+#define shambler_anim_smash    3
+#define shambler_anim_swingr   4
+#define shambler_anim_swingl   5
+#define shambler_anim_magic    6
+#define shambler_anim_pain             7
+#define shambler_anim_death    8
+
+void shambler_think ()
+{
+       self.think = shambler_think;
+       self.nextthink = time + 0.1;
+       
+       monster_move(autocvar_g_monster_shambler_speed_run, autocvar_g_monster_shambler_speed_walk, 300, shambler_anim_run, shambler_anim_walk, shambler_anim_stand);
+}
+
+void shambler_smash ()
+{
+       float bigdmg = autocvar_g_monster_shambler_damage * self.scale;
+       
+       self.think = shambler_think;
+       self.attack_finished_single = time + 0.4;
+       self.nextthink = self.attack_finished_single;
+
+       if (!self.enemy)
+               return;
+
+       if (enemy_range() > 100 * self.scale)
+               return;
+       
+       Damage(self.enemy, self, self, bigdmg * monster_skill, DEATH_MONSTER_SHAMBLER_MELEE, self.enemy.origin, normalize(self.enemy.origin - self.origin));
+}
+
+void shambler_delayedsmash ()
+{
+       self.frame = shambler_anim_smash;
+       self.think = shambler_smash;
+       self.nextthink = time + 0.7;
+}
+
+void ShamClaw (float side)
+{
+       float bigdmg = autocvar_g_monster_shambler_attack_claw_damage * self.scale;
+       
+       monster_melee(self.enemy, bigdmg * monster_skill, 100, DEATH_MONSTER_SHAMBLER_CLAW);
+}
+
+void() shambler_swing_right;
+void shambler_swing_left ()
+{
+       self.frame = shambler_anim_swingl;
+       ShamClaw(250);
+       self.attack_finished_single = time + 0.8;
+       self.nextthink = self.attack_finished_single;
+       self.think = shambler_think;
+       if(random() < 0.5)
+               self.think = shambler_swing_right;
+}
+
+void shambler_swing_right ()
+{
+       self.frame = shambler_anim_swingr;
+       ShamClaw(-250);
+       self.attack_finished_single = time + 0.8;
+       self.nextthink = self.attack_finished_single;
+       self.think = shambler_think;
+       if(random() < 0.5)
+               self.think = shambler_swing_left;
+}
+
+void sham_melee ()
+{
+       local float chance = random();
+
+       if (chance > 0.6)
+               shambler_delayedsmash();
+       else if (chance > 0.3)
+               shambler_swing_right ();
+       else
+               shambler_swing_left ();
+}
+
+void CastLightning ()
+{
+       self.nextthink = time + 0.4;
+       self.think = shambler_think;
+       
+       local vector org = '0 0 0', dir = '0 0 0';
+       vector v = '0 0 0';
+
+       self.effects |= EF_MUZZLEFLASH;
+
+       org = self.origin + '0 0 40' * self.scale;
+
+       dir = self.enemy.origin + '0 0 16' - org;
+       dir = normalize (dir);
+
+       traceline (org, self.origin + dir * 1000, TRUE, self);
+               
+       FireRailgunBullet (org, org + dir * 1000, autocvar_g_monster_shambler_attack_lightning_damage * monster_skill, 0, 0, 0, 0, 0, DEATH_MONSTER_SHAMBLER_LIGHTNING);
+       
+       // teamcolor / hit beam effect
+       v = WarpZone_UnTransformOrigin(WarpZone_trace_transform, trace_endpos);
+       WarpZone_TrailParticles(world, particleeffectnum("TE_TEI_G3"), org, v);
+}
+
+void shambler_magic ()
+{
+       self.frame = shambler_anim_magic;
+       self.attack_finished_single = time + 1.1;
+       self.nextthink = time + 0.6;
+       self.think = CastLightning;
+}
+       
+float sham_lightning ()
+{
+       shambler_magic();
+       return TRUE;
+}
+
+void shambler_die ()
+{
+       Monster_CheckDropCvars ("shambler");
+       
+       W_ThrowNewWeapon(self, WEP_NEX, 0, self.origin, self.velocity);
+       
+       self.think                      = Monster_Fade;
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.nextthink          = time + 2.1;
+       self.frame                      = shambler_anim_death;
+       self.pain_finished  = self.nextthink;
+       self.movetype           = MOVETYPE_TOSS;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void shambler_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_shambler_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_shambler";
+       self.attack_melee               = sham_melee;
+       self.checkattack                = GenericCheckAttack;
+       self.attack_ranged              = sham_lightning;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.frame                              = shambler_anim_stand;
+       self.think                              = shambler_think;
+       self.sprite_height              = 70 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_shambler ()
+{      
+       if not(autocvar_g_monster_shambler) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_shambler;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Shambler",
+                        "models/monsters/shambler.mdl",
+                        SHAMBLER_MIN, SHAMBLER_MAX,
+                        FALSE,
+                        shambler_die, shambler_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_model ("progs/beam.mdl");
+       precache_model ("models/weapons/g_nex.md3");
+       
+       precache_sound ("weapons/lgbeam_fire.wav");
+}
diff --git a/qcsrc/server/monsters/monster/soldier.qc b/qcsrc/server/monsters/monster/soldier.qc
new file mode 100644 (file)
index 0000000..e5ad3cc
--- /dev/null
@@ -0,0 +1,370 @@
+// size
+const vector SOLDIER_MIN = '-16 -16 -30';
+const vector SOLDIER_MAX = '16 16 32';
+
+// cvars
+float autocvar_g_monster_soldier;
+float autocvar_g_monster_soldier_health;
+float autocvar_g_monster_soldier_melee_damage;
+float autocvar_g_monster_soldier_speed_walk;
+float autocvar_g_monster_soldier_speed_run;
+float autocvar_g_monster_soldier_ammo;
+float autocvar_g_monster_soldier_weapon_laser_chance;
+float autocvar_g_monster_soldier_weapon_shotgun_chance;
+float autocvar_g_monster_soldier_weapon_machinegun_chance;
+float autocvar_g_monster_soldier_weapon_rocketlauncher_chance;
+float autocvar_g_monster_soldier_attack_uzi_bullets;
+
+// animations
+#define soldier_anim_die1                      0
+#define soldier_anim_die2                      1
+#define soldier_anim_draw                      2
+#define soldier_anim_duck                      3
+#define soldier_anim_duckwalk          4
+#define soldier_anim_duckjump          5
+#define soldier_anim_duckidle          6
+#define soldier_anim_idle                      7
+#define soldier_anim_jump                      8
+#define soldier_anim_pain1                     9
+#define soldier_anim_pain2                     10
+#define soldier_anim_shoot                     11
+#define soldier_anim_taunt                     12
+#define soldier_anim_run                       13
+#define soldier_anim_runbackwards      14
+#define soldier_anim_strafeleft        15
+#define soldier_anim_straferight       16
+#define soldier_anim_dead1                     17
+#define soldier_anim_dead2                     18
+#define soldier_anim_forwardright      19
+#define soldier_anim_forwardleft       20
+#define soldier_anim_backright                 21
+#define soldier_anim_backleft          22
+
+//#define soldier_anim_stand   0
+//#define soldier_anim_death1 1
+//#define soldier_anim_death2 2
+//#define soldier_anim_reload 3
+//#define soldier_anim_pain1   4
+//#define soldier_anim_pain2   5
+//#define soldier_anim_pain3   6
+//#define soldier_anim_run     7
+//#define soldier_anim_shoot   8
+//#define soldier_anim_prowl   9
+
+void soldier_think ()
+{
+       self.think = soldier_think;
+       self.nextthink = time + 0.1;
+       
+       if(self.delay != -1)
+               self.nextthink = self.delay;
+       
+       if(time < self.attack_finished_single)
+               monster_move(0, 0, 0, soldier_anim_shoot, soldier_anim_shoot, soldier_anim_shoot);
+       else
+               monster_move(autocvar_g_monster_soldier_speed_run, autocvar_g_monster_soldier_speed_walk, 50, soldier_anim_run, soldier_anim_run, soldier_anim_idle);
+}
+
+void soldier_reload ()
+{
+       self.frame = soldier_anim_draw;
+       self.attack_finished_single = time + 2;
+       self.currentammo = autocvar_g_monster_soldier_ammo;
+       sound (self, CH_SHOTS, "weapons/reload.wav", VOL_BASE, ATTN_LARGE);
+}
+
+float SoldierCheckAttack ()
+{
+       local vector spot1 = '0 0 0', spot2 = '0 0 0';
+       local entity targ = self.enemy;
+       local float chance = 0;
+
+       if (self.health <= 0 || targ.health < 1 || targ == world)
+               return FALSE;
+
+       if (vlen(targ.origin - self.origin) > 2000) // long traces are slow
+               return FALSE;
+
+       // see if any entities are in the way of the shot
+       spot1 = self.origin + self.view_ofs;
+       spot2 = targ.origin + targ.view_ofs;
+
+       traceline (spot1, spot2, FALSE, self);
+
+       if (trace_ent != targ)
+               return FALSE; // don't have a clear shot
+
+       if (trace_inwater)
+       if (trace_inopen)
+               return FALSE; // sight line crossed contents
+               
+       if(self.monster_delayedattack && self.delay != -1)
+       {
+               if(time < self.delay)
+                       return FALSE;
+                       
+               self.monster_delayedattack();
+       }
+
+       // missile attack
+       if (time < self.attack_finished_single)
+               return FALSE;
+
+       if (enemy_range() >= 2000)
+               return FALSE;
+
+       if (enemy_range() <= 120)
+               chance = 0.9;
+       else if (enemy_range() <= 500)
+               chance = 0.6; // was 0.4
+       else if (enemy_range() <= 1000)
+               chance = 0.3; // was 0.05
+       else
+               chance = 0;
+
+       if (chance > 0)
+       if (chance > random())
+               return FALSE;
+               
+       if(self.currentammo <= 0 && enemy_range() <= 120)
+       {
+               self.attack_melee();
+               return TRUE;
+       }
+       
+       if(self.currentammo <= 0)
+       {
+               soldier_reload();
+               return FALSE;
+       }
+
+       if (self.attack_ranged())
+               return TRUE;
+
+       return FALSE;
+}
+
+void soldier_laser ()
+{
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       W_Laser_Attack(0);
+}
+
+float soldier_missile_laser ()
+{
+       // FIXME: check if it would hit
+       soldier_laser();
+       return TRUE;
+}
+
+.float grunt_cycles;
+void soldier_uzi_fire ()
+{
+       self.currentammo -= 1;
+       if(self.currentammo <= 0)
+               return;
+               
+       self.grunt_cycles += 1;
+       
+       if(self.grunt_cycles > autocvar_g_monster_soldier_attack_uzi_bullets)
+       {
+               self.monster_delayedattack = func_null;
+               self.delay = -1;
+               return;
+       }
+       W_UZI_Attack(DEATH_MONSTER_SOLDIER_NAIL);
+       self.delay = time + 0.1;
+       self.monster_delayedattack = soldier_uzi_fire;
+}
+
+void soldier_uzi ()
+{
+       if(self.currentammo <= 0)
+               return;
+               
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       self.delay = time + 0.1;
+       self.monster_delayedattack = soldier_uzi_fire;
+}
+
+float soldier_missile_uzi ()
+{
+       self.grunt_cycles = 0;
+       // FIXME: check if it would hit
+       soldier_uzi();
+       return TRUE;
+}
+
+void soldier_shotgun ()
+{
+       self.currentammo -= 1;
+       if(self.currentammo <= 0)
+               return;
+               
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       W_Shotgun_Attack();
+}
+
+float soldier_missile_shotgun ()
+{
+       // FIXME: check if it would hit
+       self.grunt_cycles = 0;
+       soldier_shotgun();
+       return TRUE;
+}
+
+void soldier_rl ()
+{
+       self.currentammo -= 1;
+       if(self.currentammo <= 0)
+               return;
+               
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       W_Rocket_Attack();
+}
+
+float soldier_missile_rl ()
+{
+       // FIXME: check if it would hit
+       soldier_rl();
+       return TRUE;
+}
+
+void soldier_bash ()
+{
+       self.frame = soldier_anim_shoot;
+       self.attack_finished_single = time + 0.8;
+       monster_melee(self.enemy, autocvar_g_monster_soldier_melee_damage, 70, DEATH_MONSTER_SOLDIER_NAIL);
+}
+
+void soldier_die()
+{
+       Monster_CheckDropCvars ("soldier");
+       
+       remove(self.weaponentity);
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       self.weaponentity       = world;
+       
+       if (self.attack_ranged == soldier_missile_uzi)
+               W_ThrowNewWeapon(self, WEP_UZI, 0, self.origin, self.velocity);    
+       else if (self.attack_ranged == soldier_missile_shotgun)
+               W_ThrowNewWeapon(self, WEP_SHOTGUN, 0, self.origin, self.velocity);
+       else if (self.attack_ranged == soldier_missile_rl)
+               W_ThrowNewWeapon(self, WEP_ROCKET_LAUNCHER, 0, self.origin, self.velocity);
+       else
+               W_ThrowNewWeapon(self, WEP_LASER, 0, self.origin, self.velocity);
+
+       if (random() < 0.5)
+               self.frame = soldier_anim_die1;
+       else
+               self.frame = soldier_anim_die2;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void soldier_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_soldier_health * self.scale;
+
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_soldier";
+       self.checkattack                = SoldierCheckAttack;
+       self.attack_melee               = soldier_bash;
+       self.frame                              = soldier_anim_draw;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = soldier_think;
+       self.sprite_height              = 45 * self.scale;
+       self.items                              = (IT_SHELLS | IT_ROCKETS | IT_NAILS);
+       
+       RandomSelection_Init();
+       RandomSelection_Add(world, WEP_LASER, string_null, autocvar_g_monster_soldier_weapon_laser_chance, 1);
+       RandomSelection_Add(world, WEP_SHOTGUN, string_null, autocvar_g_monster_soldier_weapon_shotgun_chance, 1);
+       RandomSelection_Add(world, WEP_UZI, string_null, autocvar_g_monster_soldier_weapon_machinegun_chance, 1);
+       RandomSelection_Add(world, WEP_ROCKET_LAUNCHER, string_null, autocvar_g_monster_soldier_weapon_rocketlauncher_chance, 1);
+       
+       self.weaponentity = spawn();
+       self.weaponentity.movetype = MOVETYPE_NOCLIP;
+       self.weaponentity.team = self.team;
+       self.weaponentity.solid = SOLID_NOT;
+       self.weaponentity.owner = self.weaponentity.realowner = self;
+       setmodel(self.weaponentity, "models/weapons/v_seeker.md3");
+       setattachment(self.weaponentity, self, "bip01 r hand");
+       
+       if (RandomSelection_chosen_float == WEP_ROCKET_LAUNCHER)
+       {
+               self.weapon = WEP_ROCKET_LAUNCHER;
+               self.currentammo = self.ammo_rockets;
+               self.armorvalue = 10;
+               self.attack_ranged = soldier_missile_rl;
+       }
+       else if (RandomSelection_chosen_float == WEP_UZI)
+       {
+               self.weapon = WEP_UZI;
+               self.currentammo = self.ammo_nails;
+               self.armorvalue = 100;
+               self.attack_ranged = soldier_missile_uzi;
+       }
+       else if (RandomSelection_chosen_float == WEP_SHOTGUN)
+       {
+               self.weapon = WEP_SHOTGUN;
+               self.currentammo = self.ammo_shells;
+               self.armorvalue = 25;
+               self.attack_ranged = soldier_missile_shotgun;
+       }
+       else
+       {
+               self.weapon = WEP_LASER;
+               self.armorvalue = 60;
+               self.currentammo = self.ammo_none;
+               self.attack_ranged = soldier_missile_laser;
+       }
+
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_soldier ()
+{      
+       if not(autocvar_g_monster_soldier) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_soldier;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       if not (monster_initialize(
+                        "Grunt",
+                        "models/monsters/soldier.zym",
+                        SOLDIER_MIN, SOLDIER_MAX,
+                        FALSE,
+                        soldier_die, soldier_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_sound ("weapons/shotgun_fire.wav");
+       precache_sound ("weapons/uzi_fire.wav");
+       precache_sound ("weapons/laser_fire.wav");
+       precache_sound ("weapons/reload.wav");
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_army () { spawnfunc_monster_soldier(); }
diff --git a/qcsrc/server/monsters/monster/spawner.qc b/qcsrc/server/monsters/monster/spawner.qc
new file mode 100644 (file)
index 0000000..76cc94f
--- /dev/null
@@ -0,0 +1,152 @@
+// size
+const vector SPAWNER_MIN = '-35 -35 -10';
+const vector SPAWNER_MAX = '35 35 70';
+
+// cvars
+float autocvar_g_monster_spawner;
+float autocvar_g_monster_spawner_health;
+float autocvar_g_monster_spawner_maxmobs;
+string autocvar_g_monster_spawner_forcespawn;
+
+void() spawner_think;
+
+void spawnmonsters ()
+{
+       if(self.spawner_monstercount >= autocvar_g_monster_spawner_maxmobs || self.frozen || self.freezetag_frozen)
+               return;
+               
+       vector p1, p2, p3, p4, chosenposi;
+       float r = random();
+       string type = "";
+       entity e;
+       
+       self.spawner_monstercount += 1;
+       
+       if(self.spawnmob != "")
+               type = self.spawnmob;
+               
+       if(autocvar_g_monster_spawner_forcespawn != "")
+               type = autocvar_g_monster_spawner_forcespawn;
+               
+       if(type == "" || type == "spawner") // spawner spawning spawners?!
+               type = "knight";
+       
+       p1 = self.origin - '0 70 -50' * self.scale;
+       p2 = self.origin + '0 70 50' * self.scale;
+       p3 = self.origin - '70 0 -50' * self.scale;
+       p4 = self.origin + '70 0 -50' * self.scale;
+          
+       if (r < 0.20)
+               chosenposi = p1;
+       else if (r < 0.50)
+               chosenposi = p2;
+       else if (r < 80)
+               chosenposi = p3;
+       else
+               chosenposi = p4;
+
+       e = spawnmonster(type, self, self, chosenposi, FALSE, MONSTER_MOVE_WANDER);
+       
+       if(teamplay && autocvar_g_monsters_teams)
+               e.team = self.team;
+       
+       if(self.spawnflags & MONSTERFLAG_GIANT)
+               e.spawnflags = MONSTERFLAG_GIANT;
+               
+       if(self.flags & MONSTERFLAG_MINIBOSS)
+               e.spawnflags = MONSTERFLAG_MINIBOSS;
+}
+
+void spawner_die () 
+{
+       setmodel(self, "");
+       pointparticles(particleeffectnum(((self.scale > 3) ? "explosion_big" : "explosion_medium")), self.origin, '0 0 0', 1);
+       sound (self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 1;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+void spawner_think() 
+{
+       float finished = FALSE;
+       self.think = spawner_think;
+       
+       if(self.spawner_monstercount >= autocvar_g_monster_spawner_maxmobs)
+       {
+               self.nextthink = time + 5;
+       }
+
+       if (self.spawner_monstercount <= autocvar_g_monster_spawner_maxmobs)
+       {
+               spawnmonsters();
+               finished = TRUE;
+       }               
+       
+       self.nextthink = time + 1;
+
+       if(self.spawner_monstercount <= autocvar_g_monster_spawner_maxmobs || !finished)
+               self.nextthink = time + 0.1;
+}
+
+void spawner_spawn() 
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_spawner_health * self.scale;
+       
+       self.classname                  = "monster_spawner";
+       self.nextthink                  = time + 0.2;
+       self.velocity                   = '0 0 0';
+       self.think                              = spawner_think;
+       self.touch                              = func_null;    
+       self.sprite_height      = 80 * self.scale;
+       
+       self.spawner_monstercount = 0;
+       
+       droptofloor();
+       self.movetype = MOVETYPE_NONE;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+/*QUAKED monster_spawner (1 0 0) (-18 -18 -25) (18 18 47)
+---------NOTES----------
+Spawns monsters when a player is nearby
+-------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY --------
+modeldisabled="models/containers/crate01.md3"
+*/
+void spawnfunc_monster_spawner() 
+{
+       if not(autocvar_g_monster_spawner) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_spawner;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 0.8;
+       
+       if not (monster_initialize(
+                        "Monster spawner",
+                        "models/containers/crate01.md3",
+                        SPAWNER_MIN, SPAWNER_MAX,
+                        FALSE,
+                        spawner_die, spawner_spawn))
+       {
+               remove(self);
+               return;
+       }
+
+       precache_sound("weapons/rocket_impact.wav");
+}
diff --git a/qcsrc/server/monsters/monster/spider.qc b/qcsrc/server/monsters/monster/spider.qc
new file mode 100644 (file)
index 0000000..af3864a
--- /dev/null
@@ -0,0 +1,237 @@
+// cvars
+float autocvar_g_monster_spider;
+float autocvar_g_monster_spider_stopspeed;
+float autocvar_g_monster_spider_attack_leap_delay;
+float autocvar_g_monster_spider_attack_stand_damage;
+float autocvar_g_monster_spider_attack_stand_delay;
+float autocvar_g_monster_spider_health;
+float autocvar_g_monster_spider_speed_walk;
+float autocvar_g_monster_spider_speed_run;
+float autocvar_g_monster_spider_attack_type;
+
+// spider animations
+#define spider_anim_idle                       0
+#define spider_anim_walk                       1
+#define spider_anim_attack                     2
+#define spider_anim_attack2                    3
+
+const vector SPIDER_MIN                                 = '-18 -18 -25';
+const vector SPIDER_MAX                                 = '18 18 30';
+
+.float spider_type; // used to switch between fire & ice attacks
+const float SPIDER_TYPE_ICE            = 0;
+const float SPIDER_TYPE_FIRE   = 1;
+
+void spider_spawn();
+void spawnfunc_monster_spider();
+void spider_think();
+
+void spider_die ()
+{
+       Monster_CheckDropCvars ("spider");
+       
+       self.angles += '180 0 0';
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       self.frame                      = spider_anim_attack;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+/**
+ * Performe a standing attack on self.enemy.
+ */
+void spider_attack_standing() {
+       float dot = 0, bigdmg = autocvar_g_monster_spider_attack_stand_damage * self.scale;
+
+       self.velocity_x = 0;
+       self.velocity_y = 0;
+       
+       if(self.monster_owner == self.enemy)
+       {
+               self.enemy = world;
+               return;
+       }
+
+       makevectors (self.angles);
+       dot = normalize (self.enemy.origin - self.origin) * v_forward;
+       if(dot > 0.3)
+       {
+               Damage(self.enemy, self, self, bigdmg * monster_skill, DEATH_MONSTER_MELEE, self.origin, '0 0 0');
+       }
+       
+       if (!monster_isvalidtarget(self.enemy, self, FALSE))
+               self.enemy = world;
+               
+       if(random() < 0.50)
+               self.frame = spider_anim_attack;
+       else
+               self.frame = spider_anim_attack2;
+
+       self.nextthink = time + autocvar_g_monster_spider_attack_stand_delay;
+}
+
+void spider_web_explode ()
+{
+       RadiusDamage (self, self.realowner, 0, 0, 1, world, 0, self.projectiledeathtype, other);
+       remove (self);
+}
+
+void spider_web_touch ()
+{
+       PROJECTILE_TOUCH;
+       if (other.takedamage == DAMAGE_AIM)
+               Freeze(other, 0.3);
+               
+       spider_web_explode();
+}
+
+void spider_shootweb()
+{
+       // clone of the electro secondary attack, with less bouncing
+       entity proj = world;
+       
+       makevectors(self.angles);
+
+       W_SetupShot_ProjectileSize (self, '0 0 -4', '0 0 -4', FALSE, 2, "weapons/electro_fire2.wav", CH_WEAPON_A, 0);
+
+       w_shotdir = v_forward; // no TrueAim for grenades please
+
+       pointparticles(particleeffectnum("electro_muzzleflash"), w_shotorg, w_shotdir * 1000, 1);
+
+       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 + autocvar_g_balance_electro_secondary_lifetime;
+       PROJECTILE_MAKETRIGGER(proj);
+       proj.projectiledeathtype = WEP_ELECTRO | HITTYPE_SECONDARY;
+       setorigin(proj, w_shotorg);
+
+       //proj.glow_size = 50;
+       //proj.glow_color = 45;
+       proj.movetype = MOVETYPE_BOUNCE;
+       W_SETUPPROJECTILEVELOCITY_UP(proj, g_balance_electro_secondary);
+       proj.touch = spider_web_touch;
+       setsize(proj, '0 0 -4', '0 0 -4');
+       proj.takedamage = DAMAGE_YES;
+       proj.damageforcescale = 0;
+       proj.health = 500;
+       proj.event_damage = W_Plasma_Damage;
+       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, FALSE); // no culling, it has sound
+
+       other = proj; MUTATOR_CALLHOOK(EditProjectile);
+}
+
+void spider_attack_leap()
+{
+       vector angles_face = vectoangles(self.enemy.origin - self.origin);
+
+       // face the enemy       
+       self.frame = spider_anim_attack2;
+       self.angles_y = angles_face_y ;
+       self.nextthink = time + autocvar_g_monster_spider_attack_leap_delay;
+       
+       makevectors(self.angles);
+       
+       switch(self.spider_type)
+       {
+               default:
+               case SPIDER_TYPE_ICE:
+                       spider_shootweb(); break; // must... remember... breaks!
+               case SPIDER_TYPE_FIRE:
+                       W_Fireball_Attack2(); break;
+       }
+}
+
+float spider_attack_ranged()
+{
+       if(self.enemy.frozen || self.enemy.freezetag_frozen)
+               return FALSE;
+               
+       spider_attack_leap();
+       return TRUE;
+}
+
+void spider_think()
+{
+       self.think = spider_think;
+       self.nextthink = time + 0.1;
+       
+       monster_move(autocvar_g_monster_spider_speed_run, autocvar_g_monster_spider_speed_walk, autocvar_g_monster_spider_stopspeed, spider_anim_walk, spider_anim_walk, spider_anim_idle);
+}
+
+/**
+ * Spawn the spider.
+ */
+void spider_spawn() 
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_spider_health * self.scale;
+       
+       self.classname                  = "monster_spider";
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.pain_finished      = self.nextthink;
+       self.frame                              = spider_anim_idle;
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = spider_attack_standing;
+       self.attack_ranged              = spider_attack_ranged;
+       self.think                              = spider_think;
+       self.sprite_height      = 40 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+/*QUAKED monster_spider (1 0 0) (-18 -18 -25) (18 18 47)
+Spider, 60 health points.
+-------- KEYS --------
+-------- SPAWNFLAGS --------
+MONSTERFLAG_APPEAR: monster will spawn when triggered.
+---------NOTES----------
+-------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY --------
+modeldisabled="models/monsters/spider.dpm"
+*/
+void spawnfunc_monster_spider() 
+{
+       if not(autocvar_g_monster_spider) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_spider;
+       self.classname = "monster_spider";
+       if(!self.spider_type)
+               self.spider_type = autocvar_g_monster_spider_attack_type;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       if not (monster_initialize(
+                        "Spider",
+                        "models/monsters/spider.dpm",
+                        SPIDER_MIN, SPIDER_MAX,
+                        FALSE,
+                        spider_die, spider_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monster/tarbaby.qc b/qcsrc/server/monsters/monster/tarbaby.qc
new file mode 100644 (file)
index 0000000..8da2de5
--- /dev/null
@@ -0,0 +1,157 @@
+// size
+const vector TARBABY_MIN = '-16 -16 -24';
+const vector TARBABY_MAX = '16 16 16';
+
+// cvars
+float autocvar_g_monster_tarbaby;
+float autocvar_g_monster_tarbaby_health;
+float autocvar_g_monster_tarbaby_speed_walk;
+float autocvar_g_monster_tarbaby_speed_run;
+
+// animations
+#define tarbaby_anim_walk              0
+#define tarbaby_anim_run               1
+#define tarbaby_anim_jump              2
+#define tarbaby_anim_fly               3
+#define tarbaby_anim_explode   4
+
+void tarbaby_think ()
+{
+       self.think = tarbaby_think;
+       self.nextthink = time + 0.1;
+       
+       monster_move(autocvar_g_monster_tarbaby_speed_run, autocvar_g_monster_tarbaby_speed_walk, 20, tarbaby_anim_run, tarbaby_anim_walk, tarbaby_anim_walk);
+}
+
+void Tar_JumpTouch ()
+{
+       // dunno why this would be called when dead, but to be safe
+       if (self.health <= 0)
+               return;
+               
+       if (other.takedamage)
+       if (vlen(self.velocity) > 200)
+       {
+               // make the monster die
+               self.event_damage(self, self, self.health + self.max_health, DEATH_TOUCHEXPLODE, self.origin, '0 0 0');
+                       
+               return;
+       }
+
+       if (trace_dphitcontents)
+       {
+               if not(self.flags & FL_ONGROUND)
+               {
+                       self.touch = MonsterTouch;
+                       self.flags |= FL_ONGROUND;
+                       self.movetype = MOVETYPE_WALK;
+               }
+       }
+}
+
+void tarbaby_jump ()
+{
+       if not(self.flags & FL_ONGROUND)
+               return;
+       self.frame = tarbaby_anim_jump;
+       // dunno why this would be called when dead, but to be safe
+       if (self.health <= 0)
+               return;
+       self.movetype = MOVETYPE_BOUNCE;
+       self.touch = Tar_JumpTouch;
+       makevectors (self.angles);
+       self.origin_z += 1;
+       self.velocity = v_forward * 600 + '0 0 200';
+       self.velocity_z += random()*150;
+       if (self.flags & FL_ONGROUND)
+               self.flags -= FL_ONGROUND;
+               
+       self.attack_finished_single = time + 0.5;
+}
+
+float tbaby_jump ()
+{
+       tarbaby_jump();
+       return TRUE;
+}
+
+void tarbaby_blowup ()
+{
+       float bigboom = 250 * (self.scale * 0.7);
+       RadiusDamage(self, self, 250 * monster_skill, 15, bigboom * (monster_skill * 0.7), world, 250, DEATH_MONSTER_TARBABY_BLOWUP, world);
+       pointparticles(particleeffectnum(((self.scale > 3) ? "explosion_big" : "explosion_medium")), self.origin, '0 0 0', 1);
+       sound(self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
+       
+       Monster_CheckDropCvars ("tarbaby"); // drop items after exploding to prevent player picking up item before dying
+       
+       setmodel(self, "");
+}
+
+void tarbaby_explode()
+{
+       tarbaby_blowup();
+       
+       monster_hook_death(); // calling this next frame should be ok...
+}
+
+void tarbaby_die ()
+{
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.movetype           = MOVETYPE_NONE;
+       self.enemy                      = world;
+       self.think                      = tarbaby_explode;
+       self.nextthink          = time + 0.1;
+}
+
+void tarbaby_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_tarbaby_health * self.scale;
+       
+       self.damageforcescale   = 0.003;
+       self.classname                  = "monster_tarbaby";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_ranged              = tbaby_jump;
+       self.attack_melee               = tarbaby_jump;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.think                              = tarbaby_think;
+       self.sprite_height              = 20 * self.scale;
+       self.frame                              = tarbaby_anim_walk;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_tarbaby ()
+{      
+       if not(autocvar_g_monster_tarbaby) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_tarbaby;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Spawn",
+                        "models/monsters/tarbaby.mdl",
+                        TARBABY_MIN, TARBABY_MAX,
+                        FALSE,
+                        tarbaby_die, tarbaby_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_sound ("weapons/rocket_impact.wav");
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_spawn () { spawnfunc_monster_tarbaby(); }
diff --git a/qcsrc/server/monsters/monster/wizard.qc b/qcsrc/server/monsters/monster/wizard.qc
new file mode 100644 (file)
index 0000000..bdbcf3a
--- /dev/null
@@ -0,0 +1,180 @@
+// size
+const vector WIZARD_MIN = '-16 -16 -24';
+const vector WIZARD_MAX = '16 16 24';
+
+// cvars
+float autocvar_g_monster_wizard;
+float autocvar_g_monster_wizard_health;
+float autocvar_g_monster_wizard_speed_walk;
+float autocvar_g_monster_wizard_speed_run;
+float autocvar_g_monster_wizard_spike_damage;
+float autocvar_g_monster_wizard_spike_edgedamage;
+float autocvar_g_monster_wizard_spike_radius;
+float autocvar_g_monster_wizard_spike_speed;
+
+// animations
+#define wizard_anim_hover      0
+#define wizard_anim_fly        1
+#define wizard_anim_magic      2
+#define wizard_anim_pain       3
+#define wizard_anim_death      4
+
+void Wiz_FastExplode()
+{
+       self.event_damage = func_null;
+       self.takedamage = DAMAGE_NO;
+       RadiusDamage (self, self.realowner, autocvar_g_monster_wizard_spike_damage, autocvar_g_monster_wizard_spike_edgedamage, autocvar_g_monster_wizard_spike_radius, world, 0, self.projectiledeathtype, other);
+
+       remove (self);
+}
+
+void Wiz_FastTouch ()
+{
+       PROJECTILE_TOUCH;
+       
+       if(other == self.owner)
+               return;
+               
+       if(teamplay)
+       if(other.team == self.owner.team)
+               return;
+               
+       pointparticles(particleeffectnum("TE_WIZSPIKE"), self.origin, '0 0 0', 1);
+       
+       Wiz_FastExplode();
+}
+
+void Wiz_StartFast ()
+{
+       local   entity  missile;
+       local   vector  dir = '0 0 0';
+       local   float   dist = 0, flytime = 0;
+
+       dir = normalize((self.enemy.origin + '0 0 10') - self.origin);
+       dist = vlen (self.enemy.origin - self.origin);
+       flytime = dist * 0.002;
+       if (flytime < 0.1)
+               flytime = 0.1;
+       
+       self.v_angle = self.angles;
+       makevectors (self.angles);
+
+       missile = spawn ();
+       missile.owner = missile.realowner = self;
+       setsize (missile, '0 0 0', '0 0 0');            
+       setorigin (missile, self.origin + v_forward * 14 + '0 0 30' + v_right * 14);
+       missile.enemy = self.enemy;
+       missile.nextthink = time + 3;
+       missile.think = Wiz_FastExplode;
+       missile.velocity = dir * autocvar_g_monster_wizard_spike_speed;
+       missile.avelocity = '300 300 300';
+       missile.solid = SOLID_BBOX;
+       missile.movetype = MOVETYPE_FLYMISSILE;
+       missile.touch = Wiz_FastTouch;
+       CSQCProjectile(missile, TRUE, PROJECTILE_CRYLINK, TRUE);
+       
+       missile = spawn ();
+       missile.owner = missile.realowner = self;
+       setsize (missile, '0 0 0', '0 0 0');            
+       setorigin (missile, self.origin + v_forward * 14 + '0 0 30' + v_right * -14);
+       missile.enemy = self.enemy;
+       missile.nextthink = time + 3;
+       missile.touch = Wiz_FastTouch;
+       missile.solid = SOLID_BBOX;
+       missile.movetype = MOVETYPE_FLYMISSILE;
+       missile.think = Wiz_FastExplode;
+       missile.velocity = dir * autocvar_g_monster_wizard_spike_speed;
+       missile.avelocity = '300 300 300';
+       CSQCProjectile(missile, TRUE, PROJECTILE_CRYLINK, TRUE);
+}
+
+void wizard_think ()
+{
+       self.think = wizard_think;
+       self.nextthink = time + 0.1;
+       
+       monster_move(autocvar_g_monster_wizard_speed_run, autocvar_g_monster_wizard_speed_walk, 300, wizard_anim_fly, wizard_anim_hover, wizard_anim_hover);
+}
+
+void wizard_fastattack ()
+{
+       Wiz_StartFast();
+}
+
+void wizard_die ()
+{
+       Monster_CheckDropCvars ("wizard");
+       
+       self.think                      = Monster_Fade;
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.flags                      = FL_ONGROUND;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink; 
+       self.velocity_x         = -200 + 400*random();
+       self.velocity_y         = -200 + 400*random();
+       self.velocity_z         = 100 + 100*random();
+       self.frame                      = wizard_anim_death;
+       
+       monster_hook_death(); // for post-death mods
+}
+
+float Wiz_Missile ()
+{
+       wizard_fastattack();
+       return TRUE;
+}
+
+void wizard_spawn ()
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_wizard_health * self.scale;
+       
+       self.classname                  = "monster_wizard";
+       self.checkattack                = GenericCheckAttack;
+       self.attack_ranged              = Wiz_Missile;
+       self.nextthink                  = time + random() * 0.5 + 0.1;
+       self.movetype                   = MOVETYPE_FLY; // TODO: make it fly up/down
+       self.flags                         |= FL_FLY;
+       self.think                              = wizard_think;
+       self.sprite_height              = 30 * self.scale;
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+void spawnfunc_monster_wizard ()
+{      
+       if not(autocvar_g_monster_wizard) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_wizard;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       self.scale = 1.3;
+       
+       if not (monster_initialize(
+                        "Scrag",
+                        "models/monsters/wizard.mdl",
+                        WIZARD_MIN, WIZARD_MAX,
+                        TRUE,
+                        wizard_die, wizard_spawn))
+       {
+               remove(self);
+               return;
+       }
+       
+       precache_model ("models/spike.mdl");
+       precache_sound ("weapons/spike.wav");
+}
+
+// compatibility with old spawns
+void spawnfunc_monster_scrag () { spawnfunc_monster_wizard(); }
diff --git a/qcsrc/server/monsters/monster/zombie.qc b/qcsrc/server/monsters/monster/zombie.qc
new file mode 100644 (file)
index 0000000..7261141
--- /dev/null
@@ -0,0 +1,212 @@
+/**
+ * Special purpose fields:
+ * .delay - time at which to check if zombie's enemy is still in range
+ * .enemy - enemy of this zombie
+ */
+// cvars
+float autocvar_g_monster_zombie;
+float autocvar_g_monster_zombie_stopspeed;
+float autocvar_g_monster_zombie_attack_leap_damage;
+float autocvar_g_monster_zombie_attack_leap_delay;
+float autocvar_g_monster_zombie_attack_leap_force;
+float autocvar_g_monster_zombie_attack_leap_speed;
+float autocvar_g_monster_zombie_attack_stand_damage;
+float autocvar_g_monster_zombie_attack_stand_delay;
+float autocvar_g_monster_zombie_health;
+float autocvar_g_monster_zombie_speed_walk;
+float autocvar_g_monster_zombie_speed_run;
+
+// zombie animations
+#define zombie_anim_attackleap                 0
+#define zombie_anim_attackrun1                 1
+#define zombie_anim_attackrun2                 2
+#define zombie_anim_attackrun3                 3
+#define zombie_anim_attackstanding1            4
+#define zombie_anim_attackstanding2            5
+#define zombie_anim_attackstanding3            6
+#define zombie_anim_blockend                   7
+#define zombie_anim_blockstart                 8
+#define zombie_anim_deathback1                 9
+#define zombie_anim_deathback2                 10
+#define zombie_anim_deathback3                 11
+#define zombie_anim_deathfront1                        12
+#define zombie_anim_deathfront2                        13
+#define zombie_anim_deathfront3                        14
+#define zombie_anim_deathleft1                 15
+#define zombie_anim_deathleft2                 16
+#define zombie_anim_deathright1                        17
+#define zombie_anim_deathright2                        18
+#define zombie_anim_idle                               19
+#define zombie_anim_painback1                  20
+#define zombie_anim_painback2                  21
+#define zombie_anim_painfront1                 22
+#define zombie_anim_painfront2                 23
+#define zombie_anim_runbackwards               24
+#define zombie_anim_runbackwardsleft           25
+#define zombie_anim_runbackwardsright          26
+#define zombie_anim_runforward                 27
+#define zombie_anim_runforwardleft             28
+#define zombie_anim_runforwardright            29
+#define zombie_anim_spawn                              30
+
+const vector ZOMBIE_MIN                                 = '-18 -18 -25';
+const vector ZOMBIE_MAX                                 = '18 18 47';
+
+void zombie_spawn();
+void spawnfunc_monster_zombie();
+void zombie_think();
+
+void zombie_die ()
+{
+       Monster_CheckDropCvars ("zombie");
+       
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       self.movetype           = MOVETYPE_TOSS;
+       self.think                      = Monster_Fade;
+       self.nextthink          = time + 2.1;
+       self.pain_finished  = self.nextthink;
+       
+       if (random() > 0.5)
+               self.frame = zombie_anim_deathback1;
+       else
+               self.frame = zombie_anim_deathfront1;
+               
+       monster_hook_death(); // for post-death mods
+}
+
+void zombie_attack_standing()
+{
+       float rand = random(), dot = 0, bigdmg = 0;
+
+       self.velocity_x = 0;
+       self.velocity_y = 0;
+       
+       if(self.monster_owner == self.enemy)
+       {
+               self.enemy = world;
+               return;
+       }
+       
+       bigdmg = autocvar_g_monster_zombie_attack_stand_damage * self.scale;
+
+       //print("zombie attacks!\n");
+       makevectors (self.angles);
+       dot = normalize (self.enemy.origin - self.origin) * v_forward;
+       if(dot > 0.3)
+       {
+               Damage(self.enemy, self, self, bigdmg * monster_skill, DEATH_MONSTER_ZOMBIE, self.origin, '0 0 0');
+       }
+       
+       if(self.enemy.health < 1)
+               self.enemy = world;
+               
+       if (rand < 0.33)
+               self.frame = zombie_anim_attackstanding1;
+       else if (rand < 0.66)
+               self.frame = zombie_anim_attackstanding2;
+       else
+               self.frame = zombie_anim_attackstanding3;
+
+       self.nextthink = time + autocvar_g_monster_zombie_attack_stand_delay;
+       self.attack_finished_single = self.nextthink;
+}
+
+void zombie_attack_leap_touch()
+{
+       vector angles_face;
+       float bigdmg = autocvar_g_monster_zombie_attack_leap_damage * self.scale;
+       
+       if (other.deadflag != DEAD_NO)
+               return;
+               
+       if (self.monster_owner == other)
+               return;
+       
+       if (other.takedamage == DAMAGE_NO)
+               return;
+               
+       //void Damage (entity targ, entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+       traceline(self.origin, other.origin, FALSE, self);
+
+       angles_face = vectoangles(self.moveto - self.origin);
+       angles_face = normalize(angles_face) * autocvar_g_monster_zombie_attack_leap_force;
+       Damage(other, self, self, bigdmg * monster_skill, DEATH_MONSTER_ZOMBIE, trace_endpos, angles_face);     
+               
+       self.touch = MonsterTouch;
+}
+
+float zombie_attack_ranged()
+{
+       makevectors(self.angles);
+       if(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 TRUE;
+               
+       return FALSE;
+}
+
+void zombie_think()
+{
+       self.think = zombie_think;
+       self.nextthink = time + 0.1;
+
+       monster_move(autocvar_g_monster_zombie_speed_run, autocvar_g_monster_zombie_speed_walk, autocvar_g_monster_zombie_stopspeed, zombie_anim_runforward, zombie_anim_runforward, zombie_anim_idle);
+}
+
+void zombie_spawn() 
+{
+       if not(self.health)
+               self.health = autocvar_g_monster_zombie_health * self.scale;
+       
+       self.classname                  = "monster_zombie";
+       self.nextthink                  = time + 2.1;
+       self.pain_finished      = self.nextthink;
+       self.frame                              = zombie_anim_spawn;
+       self.think                              = zombie_think;
+       self.sprite_height      = 50 * self.scale;
+       self.checkattack                = GenericCheckAttack;
+       self.attack_melee               = zombie_attack_standing;
+       self.attack_ranged              = zombie_attack_ranged;
+       self.skin                               = rint(random() * 4);
+       
+       monster_hook_spawn(); // for post-spawn mods
+}
+
+/*QUAKED monster_zombie (1 0 0) (-18 -18 -25) (18 18 47)
+Zombie, 60 health points.
+-------- KEYS --------
+-------- SPAWNFLAGS --------
+MONSTERFLAG_APPEAR: monster will spawn when triggered.
+---------NOTES----------
+Original Quake 1 zombie entity used a smaller box ('-16 -16 -24', '16 16 32').
+-------- MODEL FOR RADIANT ONLY - DO NOT SET THIS AS A KEY --------
+modeldisabled="models/monsters/zombie.dpm"
+*/
+void spawnfunc_monster_zombie() 
+{
+       if not(autocvar_g_monster_zombie) { remove(self); return; }
+       
+       self.monster_spawnfunc = spawnfunc_monster_zombie;
+       
+       if(self.spawnflags & MONSTERFLAG_APPEAR)
+       {
+               self.think = func_null;
+               self.nextthink = -1;
+               self.use = Monster_Appear;
+               return;
+       }
+       
+       if not (monster_initialize(
+                        "Zombie",
+                        "models/monsters/zombie.dpm",
+                        ZOMBIE_MIN, ZOMBIE_MAX,
+                        FALSE,
+                        zombie_die, zombie_spawn))
+       {
+               remove(self);
+               return;
+       }
+}
diff --git a/qcsrc/server/monsters/monsters.qh b/qcsrc/server/monsters/monsters.qh
new file mode 100644 (file)
index 0000000..a969549
--- /dev/null
@@ -0,0 +1,21 @@
+// Lib
+#include "lib/defs.qh"
+#include "lib/monsters.qc"
+
+// Monsters
+#include "lib/spawn.qc"
+#include "monster/ogre.qc"
+#include "monster/demon.qc"
+#include "monster/shambler.qc"
+#include "monster/knight.qc"
+#include "monster/soldier.qc"
+#include "monster/wizard.qc"
+#include "monster/dog.qc"
+#include "monster/tarbaby.qc"
+#include "monster/hknight.qc"
+#include "monster/fish.qc"
+#include "monster/shalrath.qc"
+#include "monster/enforcer.qc"
+#include "monster/zombie.qc"
+#include "monster/spider.qc"
+#include "monster/spawner.qc"
index 9b9f7fde01abe5dc22dcd51310665c265aa15e6d..633b0760917fa7438a592c1769140638ccf82e14 100644 (file)
@@ -102,6 +102,19 @@ void movelib_move(vector force,float max_velocity,float drag,float theMass,float
             self.velocity = normalize(self.velocity) * (mspeed - 50);//* max_velocity;
 }
 
+void movelib_move_simple_gravity(vector newdir,float velo,float blendrate)
+{
+    float z_speed = self.velocity_z;
+    self.movelib_lastupdate = time;
+    self.velocity = self.velocity * (1 - blendrate) + (newdir * blendrate) * velo;
+    self.velocity_z = z_speed;
+}
+
+void movelib_jump_simple(float power){
+    self.velocity_z=power;
+    self.movelib_lastupdate = time;
+}
+
 /*
 .float mass;
 .float side_friction;
index d90d564b501447acb3e3ef1def71530be1a521ee..b9203d844295ddf0a14bfeb4daf08e8b94d66d30 100644 (file)
@@ -105,6 +105,16 @@ MUTATOR_HOOKABLE(TurretSpawn);
        // return error to request removal
        // INPUT: self - turret
        
+MUTATOR_HOOKABLE(TurretDies);
+       // called when a turret dies
+       
+MUTATOR_HOOKABLE(TurretValidateTarget);
+       // return target score
+       // INPUT:
+               entity turret_target;
+               entity turret;
+               float turret_flags;
+       
 MUTATOR_HOOKABLE(OnEntityPreSpawn);
        // return error to prevent entity spawn, or modify the entity
 
@@ -129,6 +139,38 @@ MUTATOR_HOOKABLE(EditProjectile);
        // INPUT:
                entity self;
                entity other;
+        
+MUTATOR_HOOKABLE(MonsterSpawn);
+       // called when a monster spawns
+    
+MUTATOR_HOOKABLE(MonsterDies);
+       // called when a monster dies
+       // INPUT:
+               entity frag_attacker;
+               
+MUTATOR_HOOKABLE(MonsterRespawn);
+       // called when a monster wants to respawn
+       // INPUT:
+               entity other;
+               
+MUTATOR_HOOKABLE(MonsterDropItem);
+       // called when a monster is dropping loot
+       // INPUT, OUTPUT:
+               string monster_dropitem;
+               string monster_dropsize;
+       
+MUTATOR_HOOKABLE(MonsterMove);
+       // called when a monster moves
+       // INPUT:
+               float monster_speed_run;
+               float monster_speed_walk;
+               entity monster_target;
+    
+MUTATOR_HOOKABLE(MonsterFindTarget);
+       // called when a monster looks for another target
+    
+MUTATOR_HOOKABLE(MonsterCheckBossFlag);
+    // called to change a random monster to a miniboss
 
 MUTATOR_HOOKABLE(PlayerDamage_SplitHealthArmor);
        // called when a player gets damaged to e.g. remove stuff he was carrying.
@@ -235,6 +277,9 @@ MUTATOR_HOOKABLE(HelpMePing);
        // INPUT
        entity self; // the player who pressed impulse 33
        
+MUTATOR_HOOKABLE(VehicleSpawn);
+       // called when a vehicle spawns
+       
 MUTATOR_HOOKABLE(VehicleEnter);
        // called when a player enters a vehicle
        // allows mutators to set special settings in this event
diff --git a/qcsrc/server/mutators/gamemode_rts.qc b/qcsrc/server/mutators/gamemode_rts.qc
new file mode 100644 (file)
index 0000000..166ffad
--- /dev/null
@@ -0,0 +1,435 @@
+// Real-Time Strategy
+// Gamemode by Mario
+
+// basically a fusion reactor with a new classname
+void spawnfunc_healing_tower()
+{
+       self.spawnflags = TSL_NO_RESPAWN; // healing towers don't respawn?
+       self.netname = "Monster Healing Tower"; // not used by waypoints...
+       spawnfunc_turret_fusionreactor();
+       self.classname = "healing_tower";
+       self.target_range = 1000;
+       self.shot_dmg = 30;
+}
+
+void rts_waypoint_think()
+{
+       float goalcount = 0;
+       entity e;
+       
+       self.nextthink = time + 0.1;
+       
+       for(e = world; (e = findentity(e, goalentity, self)); )
+       {
+               ++goalcount;
+       }
+       
+       if(goalcount < 1)
+       {
+               WaypointSprite_Kill(self.sprite);
+               remove(self);
+               return;
+       }
+}
+
+void Monster_LevelUp(entity e)
+{
+       if(self.level >= 5)
+               return; // max level is 5 for now
+       e.speed += 0.25;
+       e.max_health += 20;
+       e.health = e.max_health;
+       e.level += 1;
+       WaypointSprite_UpdateHealth(e.sprite, e.health);
+}
+
+MUTATOR_HOOKFUNCTION(rts_PlayerSpawn)
+{
+       if(self.rts_viewangle)
+               self.angles_x = self.rts_viewangle;
+       else
+               self.angles_x = 30;
+               
+       self.effects |= EF_NODRAW;
+       self.oldorigin = self.origin;
+       self.monster_attack = FALSE;
+       self.last_click = time;
+       self.takedamage = DAMAGE_NO;
+       self.flags |= FL_NOTARGET;
+       self.movetype = MOVETYPE_NOCLIP;
+       stuffcmd(self, "cl_cmd settemp cl_prydoncursor 1\n");
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_FilterItem)
+{
+       // no items... yet
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_SetStartItems)
+{
+       WEPSET_COPY_AW(start_weapons, 0);
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_PlayerThink)
+{
+       if(self.classname != "player")
+               return FALSE; // dont do any checks for spectators
+               
+       switch(self.impulse)
+       {
+               case 10:
+        case 15:        
+        case 18:
+            self.oldorigin_z += 50;
+            break;
+        case 12:
+        case 16:
+        case 19:
+                       self.oldorigin_z -= 50;
+                       break;
+       }
+       self.hasweapon_complain_spam = time + 9999999999; // no spam
+               
+       entity head, wp = world;
+       if(!self.cursor_trace_ent && self.BUTTON_ATCK && time >= self.last_click)
+       {       
+               FOR_EACH_MONSTER(head)
+               {
+                       if(head.owner != self) continue;
+                       
+                       head.selected = FALSE;
+                       
+                       if(!self.enemy)
+                               head.owner = world;
+               }
+       }
+       if(self.cursor_trace_ent.flags & FL_MONSTER && self.BUTTON_ATCK && time >= self.last_click)
+       {
+               if(self.cursor_trace_ent.owner != self && self.cursor_trace_ent.owner != world)
+                       return FALSE; // someone else owns it
+               else if(self.cursor_trace_ent.team != self.team)
+                       return FALSE; // not our team
+               else if(self.cursor_trace_ent.selected)
+               {
+                       self.cursor_trace_ent.selected = FALSE;
+                       self.cursor_trace_ent.owner = world;
+                       self.last_click = time + 0.5; // prevent spamming
+               }
+               else
+               {
+                       self.cursor_trace_ent.owner = self;
+                       self.cursor_trace_ent.selected = TRUE;
+                       self.last_click = time + 0.5; // prevent spamming
+               }
+       }
+       if(self.BUTTON_ATCK2)
+       {
+               entity e = self.cursor_trace_ent;
+               
+               if not(e)
+               {
+                       entity t;
+                       for(t = world; (t = findflags(t, turrcaps_flags, TFL_TURRCAPS_ISTURRET)); )
+                       {
+                               if(vlen(self.cursor_trace_endpos - t.origin) < 80)
+                               {
+                                       if(IsDifferentTeam(e, t))
+                                       {
+                                               e = t;
+                                               break; // don't bother checking any other turrets
+                                       }
+                               }
+                       }
+               }
+               
+               if(e)
+               if not(e.takedamage)
+                       e = world;
+               
+               if not(e)
+               {
+                       wp = spawn();
+                       wp.classname = "monster_waypoint"; // set so we can kill this later
+                       wp.owner = self; // hmm...
+                       wp.think = rts_waypoint_think;
+                       wp.nextthink = time;
+                       WaypointSprite_Spawn("Here", 1, 0, wp, '0 0 10', world, self.team, wp, sprite, FALSE, RADARICON_DANGER, ((teamplay) ? TeamColor(self.team) : '1 0 0'));
+                       setorigin(wp, self.cursor_trace_endpos);
+               }
+               
+               FOR_EACH_MONSTER(head)
+               {
+                       if(head.owner != self) continue;
+                       if not(head.selected) continue;
+                       
+                       if(e)
+                       {
+                               float sheight = ((e.sprite_height) ? e.sprite_height + 20 : 80);
+                               if(IsDifferentTeam(e, self))
+                               {
+                                       WaypointSprite_Spawn("Attacking", 1, 0, e, '0 0 1' * sheight, world, self.team, self, sprite, FALSE, RADARICON_DANGER, ((teamplay) ? TeamColor(self.team) : '1 0 0'));
+                                       head.goalentity = world;
+                                       head.enemy = e;
+                               }
+                               else if(e.flags & FL_MONSTER)
+                               {
+                                       WaypointSprite_Spawn("Following", 1, 0, e, '0 0 1' * sheight, world, self.team, self, sprite, FALSE, RADARICON_DANGER, ((teamplay) ? TeamColor(self.team) : '1 0 0'));
+                                       head.goalentity = e;
+                               }
+                               else // its not a monster or an enemy, so revert to waypoint
+                               {
+                                       head.goalentity = wp;
+                                       head.enemy = world;
+                               }
+                                       
+                       }
+                       else
+                       {
+                               head.goalentity = wp;
+                               head.enemy = world;
+                       }
+               }
+       }
+
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_MonsterSpawn)
+{
+       // new monster
+       if not(self.monster_respawned)
+       {
+               self.level = 0;
+               self.speed = 1;
+       }
+       
+       self.spawnflags = MONSTERFLAG_NORESPAWN;
+       
+       self.goalentity = world;
+       self.enemy = world;
+       self.moveto = self.origin;
+               
+       self.respawntime = 10; // default to 10 seconds for now
+       self.effects |= EF_SELECTABLE;
+       self.monster_moveflags = MONSTER_MOVE_NOMOVE;
+       
+       WaypointSprite_Kill(self.sprite);
+       self.sprite = world;
+       self.heal_delay = -1; // this is reset when monster takes damage
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_MonsterThink)
+{
+       vector color = ((self.team) ? TeamColor(self.team) : '1 1 1');
+               
+       if(self.health >= self.max_health)
+               self.heal_delay = -1;   
+       else if(time >= self.heal_delay)
+       {
+               self.health = min(self.health + 5, self.max_health);
+               WaypointSprite_UpdateHealth(self.sprite, self.health);
+               self.heal_delay = time + 2;
+       }
+               
+       monster_speed_run = 150 * self.speed;
+       monster_speed_walk = 150 * self.speed;
+       
+       if(monster_target.classname == "player")
+               monster_target = world;
+               
+       if not(IsDifferentTeam(monster_target, self))
+       {
+               // following a fellow teammate, so attack their enemy
+               if(monster_target.deadflag != DEAD_NO || monster_target.health < 1)
+                       monster_target = world; // teammate died
+                       
+               if(monster_target.enemy)
+               {
+                       self.enemy = monster_target.enemy;
+                       monster_target = world; // don't follow anymore?
+               }
+       }
+       
+       if(self.selected)
+               self.colormod = color * 10;
+       else
+               self.colormod = color;
+               
+       if(monster_target)
+               self.enemy = world; // don't ignore our owner's commands
+       
+       if not(self.sprite)
+       {
+               WaypointSprite_Spawn(self.netname, 0, 0, self, '0 0 1' * self.sprite_height, world, self.team, self, sprite, FALSE, RADARICON_DANGER, ((teamplay) ? TeamColor(self.team) : '1 0 0'));
+               WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
+               WaypointSprite_UpdateHealth(self.sprite, self.health);
+       }
+       
+       if(self.owner)
+       if not(self.selected)
+               self.owner = world;
+       
+       if not(IsDifferentTeam(self, self.enemy))
+               self.enemy = world; // no same team fighting
+       
+       self.last_trace = time; // realtime moving?
+               
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_MonsterDies)
+{
+       entity e;
+       
+       if(IsDifferentTeam(frag_attacker, frag_target) && frag_attacker.team)
+               TeamScore_AddToTeam(frag_attacker.team, ST_SCORE, 1);
+       
+       // need to keep the monster selected to get the points... hmm (TODO: realowners?)
+       if(frag_attacker.owner.classname == "player")
+       {
+               PlayerScore_Add(frag_attacker.owner, SP_SCORE, 5);
+               PlayerScore_Add(frag_attacker.owner, SP_KILLS, 1);
+       }
+               
+       if(frag_attacker.flags & FL_MONSTER)
+       {
+               frag_attacker.monster_score += 5;
+               if(frag_attacker.monster_score == 25)
+                       Monster_LevelUp(frag_attacker);
+       }
+       
+       for(e = world; (e = findentity(e, goalentity, self)); )
+       {
+               e.goalentity = world; // fix teammates if they still see us as a valid target
+       }
+
+       self.effects &~= EF_SELECTABLE;
+       self.selected = FALSE;
+       
+       self.goalentity = world;
+       self.enemy = world;
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_MonsterRespawn)
+{
+       if(other.team)
+               return TRUE;
+               
+       return FALSE; // if no team is set, don't respawn
+}
+
+MUTATOR_HOOKFUNCTION(rts_MonsterTarget)
+{
+       // don't search for enemies, they are given to us
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_MonsterBossFlag)
+{
+       // no minibosses in RTS
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_PlayerDamage)
+{
+       if(frag_target.classname == "player")
+               frag_damage = 0; // don't damage the invincible players...
+               
+       if((frag_target.flags & FL_MONSTER) && frag_target.goalentity)
+               frag_target.enemy = world; // don't attack the attacker, we're probably pulling back
+               
+       if((frag_target.flags & FL_MONSTER) && !IsDifferentTeam(frag_target, frag_attacker))
+               frag_damage = 0; // no team damage
+               
+       if((frag_target.flags & FL_MONSTER) && frag_damage > 0)
+               frag_target.heal_delay = time + 2; // reset delay whenever hurt
+               
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_PlayerPhysics)
+{
+       if(self.classname != "player")
+               return FALSE;
+               
+       self.origin_z = self.oldorigin_z;
+       self.stat_sv_maxspeed *= 4; // lol
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(rts_PlayerDies)
+{
+       // prevent changing teams with selected monsters
+       entity head;
+       FOR_EACH_MONSTER(head)
+       {
+               if(head.owner != self) continue;
+               if not(head.selected) continue;
+               
+               if(IsDifferentTeam(self, head))
+               {
+                       head.selected = FALSE;
+                       head.owner = world;
+               }
+       }
+       
+       return FALSE;
+}
+
+void rts_ScoreRules()
+{
+       ScoreRules_basics(2, SFL_SORT_PRIO_PRIMARY, 0, TRUE);
+       ScoreRules_basics_end();
+}
+
+void rts_DelayedInit()
+{
+       rts_ScoreRules();
+}
+
+void rts_Initialize()
+{
+       InitializeEntity(world, rts_DelayedInit, INITPRIO_GAMETYPE);
+}
+
+MUTATOR_DEFINITION(gamemode_rts)
+{
+       MUTATOR_HOOK(PlayerPhysics, rts_PlayerPhysics, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerSpawn, rts_PlayerSpawn, CBC_ORDER_ANY);
+       MUTATOR_HOOK(SetStartItems, rts_SetStartItems, CBC_ORDER_ANY);
+       MUTATOR_HOOK(FilterItem, rts_FilterItem, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterSpawn, rts_MonsterSpawn, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerPreThink, rts_PlayerThink, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterMove, rts_MonsterThink, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterFindTarget, rts_MonsterTarget, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterDies, rts_MonsterDies, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterRespawn, rts_MonsterRespawn, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterCheckBossFlag, rts_MonsterBossFlag, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerDamage_Calculate, rts_PlayerDamage, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerDies, rts_PlayerDies, 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.");        
+               cvar_settemp("g_monsters", "1");
+               
+               rts_Initialize();
+       }
+
+       MUTATOR_ONREMOVE
+       {
+               error("This is a game type and it cannot be removed at runtime.");
+       }
+
+       return FALSE;
+}
diff --git a/qcsrc/server/mutators/gamemode_rts.qh b/qcsrc/server/mutators/gamemode_rts.qh
new file mode 100644 (file)
index 0000000..c08a5c1
--- /dev/null
@@ -0,0 +1,7 @@
+.vector oldorigin;
+.float selected;
+.float last_click;
+.float heal_delay;
+.float monster_score;
+.float level;
+.float rts_viewangle;
\ No newline at end of file
diff --git a/qcsrc/server/mutators/gamemode_td.qc b/qcsrc/server/mutators/gamemode_td.qc
new file mode 100644 (file)
index 0000000..67189f6
--- /dev/null
@@ -0,0 +1,1155 @@
+// Tower Defense
+// Gamemode by Mario
+void spawnfunc_td_controller()
+{
+       if not(g_td) { remove(self); return; }
+       
+       if(autocvar_g_td_force_settings)
+       {
+               // TODO: find a better way to do this?
+               self.dontend = FALSE;
+               self.maxwaves = 0;
+               self.monstercount = 0;
+               self.startwave = 0;
+               self.maxturrets = 0;
+               self.buildtime = 0;
+               self.mspeed_walk = 0;
+               self.mspeed_run = 0;
+               self.spawndelay = 0;
+               self.maxcurrent = 0;
+               self.ignoreturrets = 0;
+       }
+               
+       self.netname = "Tower Defense controller entity";
+       self.classname = "td_controller";
+               
+       gensurvived = FALSE;
+       td_dont_end = ((self.dontend) ? self.dontend : autocvar_g_td_generator_dontend);                
+       max_waves = ((self.maxwaves) ? self.maxwaves : autocvar_g_td_max_waves);        
+       totalmonsters = ((self.monstercount) ? self.monstercount : autocvar_g_td_monster_count);
+       wave_count = ((self.startwave) ? self.startwave : autocvar_g_td_start_wave);
+       max_turrets = ((self.maxturrets) ? self.maxturrets : autocvar_g_td_turret_max);
+       build_time = ((self.buildtime) ? self.buildtime : autocvar_g_td_buildphase_time);
+       m_speed_walk = ((self.mspeed_walk) ? self.mspeed_walk : autocvar_g_td_monsters_speed_walk);
+       m_speed_run = ((self.mspeed_run) ? self.mspeed_run : autocvar_g_td_monsters_speed_run);
+       spawn_delay = ((self.spawndelay) ? self.spawndelay : autocvar_g_td_monsters_spawn_delay);
+       max_current = ((self.maxcurrent) ? self.maxcurrent : autocvar_g_td_current_monsters);
+       ignore_turrets = ((self.ignoreturrets) ? self.ignoreturrets : autocvar_g_td_monsters_ignore_turrets);
+       
+       if(autocvar_g_td_monsters_skill_start)
+               monster_skill = autocvar_g_td_monsters_skill_start;
+               
+       wave_end(TRUE);
+}
+
+void td_generator_die() 
+{
+       entity tail;
+       
+       print((td_gencount > 1) ? "A generator was destroyed!\n" : "The generator was destroyed.\n");
+               
+       if(autocvar_sv_eventlog)
+               GameLogEcho(":gendestroyed");
+               
+       gendestroyed = TRUE;
+               
+       FOR_EACH_PLAYER(tail)
+       {
+               Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, ((td_gencount > 1) ? "A generator was destroyed!" : "The generator was destroyed."), 0, 0);
+       }
+       
+       setmodel(self, "models/onslaught/generator_dead.md3");
+       self.solid                      = SOLID_NOT;
+       self.takedamage         = DAMAGE_NO;
+       self.event_damage   = func_null;
+       self.enemy                      = world;
+       td_gencount                     -= 1;
+               
+       pointparticles(particleeffectnum("explosion_medium"), self.origin, '0 0 0', 1);
+       
+       WaypointSprite_Kill(self.sprite);
+}
+
+void td_generator_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force) 
+{
+       if(attacker.classname == STR_PLAYER || attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET || attacker.vehicle_flags & VHF_ISVEHICLE)
+               return;
+               
+       entity tail;
+       
+       FOR_EACH_PLAYER(tail)
+       {
+               Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, "The generator is under attack!", 0, 0);
+       }
+       
+       self.health -= damage;
+       
+       WaypointSprite_UpdateHealth(self.sprite, self.health);
+               
+       if(self.health <= 0) 
+               td_generator_die();
+}
+
+void spawnfunc_td_generator() 
+{
+       if not(g_td) { remove(self); return; }
+       
+       gendestroyed = FALSE;
+       
+       if not(self.health)
+               self.health = autocvar_g_td_generator_health;
+
+       // precache generator model
+       precache_model("models/onslaught/generator.md3");
+       precache_model("models/onslaught/generator_dead.md3");   
+       
+       self.model                  = "models/onslaught/generator.md3";
+       setmodel(self, self.model);
+       self.classname      = "td_generator";
+       self.solid                  = SOLID_BBOX;
+       self.takedamage     = DAMAGE_AIM;
+       self.event_damage   = td_generator_damage;
+       self.enemy                  = world;
+       self.nextthink      = -1;
+       self.think                  = func_null;
+       self.max_health     = self.health;
+       self.movetype       = MOVETYPE_NONE;
+       self.monster_attack = TRUE;
+       td_gencount                += 1;
+       self.netname            = "Generator";
+       
+       setsize(self, GENERATOR_MIN, GENERATOR_MAX);
+       
+       droptofloor();
+       
+       WaypointSprite_SpawnFixed(self.netname, self.origin + '0 0 60', self, sprite, RADARICON_OBJECTIVE, '1 0.5 0');  
+       WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
+       WaypointSprite_UpdateHealth(self.sprite, self.health);
+}
+
+entity PickGenerator()
+{
+       entity generator, head;
+       if(td_gencount == 1)
+               generator = find(world, classname, "td_generator");
+       else
+       {
+               RandomSelection_Init();
+               for(head = world;(head = find(head, classname, "td_generator")); )
+               {
+                       RandomSelection_Add(head, 0, string_null, 1, 1);
+               }
+               generator = RandomSelection_chosen_ent; 
+       }
+       return generator;
+}
+
+void spawn_td_fuel(float fuel_size)
+{
+       if not(g_td) {remove(self); return; }
+       
+       self.ammo_fuel = fuel_size * monster_skill;
+       StartItem("models/items/g_fuel.md3", "misc/itempickup.wav", g_pickup_respawntime_ammo, g_pickup_respawntimejitter_ammo, "Turret Fuel", IT_FUEL, 0, 0, commodity_pickupevalfunc, BOT_PICKUP_RATING_LOW);
+       
+       self.velocity = randomvec() * 175 + '0 0 325';
+}
+
+void spawnfunc_td_waypoint() 
+{
+       if not(g_td) { remove(self); return; }
+       
+       self.classname = "td_waypoint";
+}
+
+void spawnfunc_monster_swarm()
+{
+       if not(g_td) { remove(self); return; }
+       
+       self.flags = SWARM_NORMAL; // marked as a spawnpoint
+       self.classname = "monster_swarm";
+       
+       if(self.spawntype == SWARM_SWIM) waterspawns_count += 1;
+       if(self.spawntype == SWARM_FLY) flyspawns_count += 1;
+       
+       WaypointSprite_SpawnFixed("Monsters", self.origin + '0 0 60', self, sprite, RADARICON_HERE, '0 0 1');
+       
+       if(self.target == "")
+               print("Warning: monster_swarm entity without a set target\n");
+}
+
+void barricade_touch()
+{
+       if not(other.flags & FL_MONSTER)
+               return;
+               
+       if(time < self.dmg_time)
+               return;
+               
+       Damage(other, self, self, autocvar_g_td_barricade_damage, DEATH_HURTTRIGGER, self.origin, '0 0 0');
+       
+       self.dmg_time = time + 1;
+}
+
+void barricade_die()
+{
+       self.takedamage = DAMAGE_NO;
+       self.event_damage = func_null;
+       
+       WaypointSprite_Kill(self.sprite);
+       
+       pointparticles(particleeffectnum("explosion_medium"), self.origin, '0 0 0', 1);
+       sound(self, CH_SHOTS, "weapons/rocket_impact.wav", VOL_BASE, ATTN_NORM);
+       
+       if(self.realowner)
+               self.realowner.turret_cnt -= 1;
+               
+       self.think = SUB_Remove;
+       self.nextthink = time;
+}
+
+void barricade_damage(entity inflictor, entity attacker, float damage, float deathtype, vector hitloc, vector force)
+{
+       if not(attacker.flags & FL_MONSTER) return;
+       
+       self.health -= damage;
+       
+       WaypointSprite_UpdateHealth(self.sprite, self.health);
+       
+       if(self.health < 1)
+               barricade_die();
+}
+
+void spawn_barricade()
+{
+       self.health = 2000;
+       self.max_health = self.health;
+       self.dmg_time = time;
+       self.touch = barricade_touch;
+       self.think = func_null;
+       self.nextthink = -1;
+       self.takedamage = DAMAGE_AIM;
+       self.turrcaps_flags = TFL_TURRCAPS_ISTURRET; // for turretremove commands etc.
+       self.solid = SOLID_CORPSE; // hax
+       self.event_damage = barricade_damage;
+       self.netname = "Barricade";
+       
+       WaypointSprite_Spawn(self.netname, 0, 1200, self, '0 0 110', world, 0, self, sprite, FALSE, RADARICON_DOMPOINT, '1 1 0');       
+       WaypointSprite_UpdateMaxHealth(self.sprite, self.max_health);
+       WaypointSprite_UpdateHealth(self.sprite, self.health);
+       
+       precache_model("models/td/barricade.md3");
+       setmodel(self, "models/td/barricade.md3");
+       
+       droptofloor();
+       
+       self.movetype = MOVETYPE_NONE;
+}
+
+void spawnturret(entity spawnedby, entity own, string turet, vector orig)
+{
+       if(spawnedby.classname != STR_PLAYER)
+       {
+               print("Warning: A non-player entity tried to spawn a turret\n");
+               return;
+       }
+               
+       entity oldself;
+       
+       oldself = self;
+       self = spawn();
+       
+       setorigin(self, orig);
+       self.spawnflags = TSL_NO_RESPAWN;
+       self.monster_attack = TRUE;
+       self.realowner = own;
+       self.angles_y = spawnedby.v_angle_y;
+       spawnedby.turret_cnt += 1;
+       self.colormap = spawnedby.colormap;
+       
+       switch(turet)
+       {
+               default:
+               case "plasma": spawnfunc_turret_plasma(); break;
+               case "mlrs": spawnfunc_turret_mlrs(); break;
+               case "phaser": spawnfunc_turret_phaser(); break;
+               case "hellion": spawnfunc_turret_hellion(); break;
+               case "walker": spawnfunc_turret_walker(); break;
+               case "flac": spawnfunc_turret_flac(); break;
+               case "tesla": spawnfunc_turret_tesla(); break;
+               case "fusionreactor": spawnfunc_turret_fusionreactor(); break;
+               case "barricade": spawn_barricade(); break;
+       }
+               
+       self = oldself;
+}
+
+void buffturret (entity tur, float buff)
+{
+       tur.turret_buff           += 1;
+       tur.max_health            *= buff;
+       tur.tur_health             = tur.max_health;
+       tur.health                         = tur.max_health;
+       tur.ammo_max              *= buff;
+       tur.ammo_recharge     *= buff;
+    tur.shot_dmg          *= buff;
+    tur.shot_refire       -= buff * 0.2;
+    tur.shot_radius       *= buff;
+    tur.shot_speed        *= buff;
+    tur.shot_spread       *= buff;
+    tur.shot_force        *= buff;
+}
+
+void AnnounceSpawn(string anounce)
+{
+       entity tail;
+       FOR_EACH_PLAYER(tail)
+       {
+               Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, strcat("^1A ", anounce, " has arrived!"), 0, 0);
+       }
+}
+
+entity PickSpawn (float strngth, float type)
+{
+       entity e;
+       RandomSelection_Init();
+       for(e = world;(e = find(e, classname, "monster_swarm")); )
+       {
+               if(flyspawns_count > 0 && type == SWARM_FLY && e.spawntype != SWARM_FLY) continue;
+               if(waterspawns_count > 0 && type == SWARM_SWIM && e.spawntype != SWARM_SWIM) continue;
+               
+               RandomSelection_Add(e, 0, string_null, 1, 1);
+       }
+
+       return RandomSelection_chosen_ent;
+}
+
+void TD_SpawnMonster(string mnster, float strngth, float type)
+{
+       entity e, mon;
+       
+       e = PickSpawn(strngth, type);
+       
+       if(e == world) // couldn't find anything for our class, so check for normal spawns
+               e = PickSpawn(SWARM_NORMAL, SWARM_NORMAL);
+               
+       if(e == world)
+       {
+               print("Warning: couldn't find any monster_swarm spawnpoints, no monsters will spawn!\n");
+               return;
+       }
+  
+       mon = spawnmonster(mnster, e, e, e.origin, FALSE, 0);
+       if(e.target2)
+       {
+               if(random() > 0.5)
+                       mon.target = e.target2;
+               else
+                       mon.target = e.target;
+       }
+       else
+               mon.target = e.target;
+}
+
+float Monster_GetStrength(string mnster)
+{
+       switch(mnster)
+       {
+               case "knight":
+               case "wizard":
+               case "soldier":
+               case "enforcer":
+               case "zombie":
+               case "tarbaby":
+               case "dog":
+               case "spider":
+               case "fish":
+                       return SWARM_WEAK;
+               case "ogre":
+               case "shambler":
+               case "shalrath":
+               case "hellknight":
+               case "demon":
+                       return SWARM_STRONG;
+               default:
+                       return SWARM_NORMAL;
+       }
+}
+
+float Monster_GetType(string mnster)
+{
+       switch(mnster)
+       {
+               default:
+               case "knight":
+               case "soldier":
+               case "enforcer":
+               case "zombie":
+               case "spider":
+               case "tarbaby":
+               case "dog":
+               case "ogre":
+               case "shambler":
+               case "shalrath":
+               case "hellknight":
+               case "demon":
+                       return SWARM_NORMAL;
+               case "wizard":
+                       return SWARM_FLY;
+               case "fish":
+                       return SWARM_SWIM;
+       }
+}
+
+string RandomMonster()
+{
+       RandomSelection_Init();
+       
+       if(n_demons) RandomSelection_Add(world, 0, "demon", 1, 1);
+       if(n_shalraths) RandomSelection_Add(world, 0, "vore", 1, 1);
+       if(n_soldiers) RandomSelection_Add(world, 0, "soldier", 1, 1);
+       if(n_hknights) RandomSelection_Add(world, 0, "hellknight", 1, 1);
+       if(n_enforcers) RandomSelection_Add(world, 0, "enforcer", 1, 1);
+       if(n_zombies) RandomSelection_Add(world, 0, "zombie", 1, 1);
+       if(n_spiders) RandomSelection_Add(world, 0, "spider", 1, 1);
+       if(n_ogres) RandomSelection_Add(world, 0, "ogre", 1, 1);
+       if(n_dogs) RandomSelection_Add(world, 0, "dog", 1, 1);
+       if(n_knights) RandomSelection_Add(world, 0, "knight", 1, 1);
+       if(n_shamblers) RandomSelection_Add(world, 0, "shambler", 0.2, 0.2);
+       if(n_tarbabies) RandomSelection_Add(world, 0, "spawn", 0.2, 0.2);
+       if(n_wizards && flyspawns_count) RandomSelection_Add(world, 0, "scrag", 1, 1);
+       if(n_fish && waterspawns_count) RandomSelection_Add(world, 0, "fish", 0.2, 0.2);
+       
+       return RandomSelection_chosen_string;
+}
+
+void combat_phase()
+{
+       string whichmon;
+       float mstrength, montype;
+       
+       current_phase = PHASE_COMBAT;
+       
+       if(monster_count <= 0)
+       {
+               wave_end(FALSE);
+               return;
+       }
+       
+       self.think = combat_phase;
+       
+       whichmon = RandomMonster();
+       
+       mstrength = Monster_GetStrength(whichmon);
+       montype = Monster_GetType(whichmon);
+       
+       if(current_monsters <= max_current && whichmon != "")
+       {
+               TD_SpawnMonster(whichmon, mstrength, montype);
+               self.nextthink = time + spawn_delay;
+       }
+       else
+               self.nextthink = time + 6;
+}
+
+void queue_monsters(float maxmonsters)
+{
+       float mc = 11; // note: shambler + tarbaby = 1
+       
+       if(waterspawns_count > 0)
+               mc += 1;
+       if(flyspawns_count > 0)
+               mc += 1;
+               
+       DistributeEvenly_Init(maxmonsters, mc);
+       n_demons        = DistributeEvenly_Get(1);
+       n_ogres         = DistributeEvenly_Get(1);
+       n_dogs          = DistributeEvenly_Get(1);
+       n_knights   = DistributeEvenly_Get(1);
+       n_shalraths = DistributeEvenly_Get(1);
+       n_soldiers  = DistributeEvenly_Get(1);
+       n_hknights  = DistributeEvenly_Get(1);
+       n_enforcers = DistributeEvenly_Get(1);
+       n_zombies   = DistributeEvenly_Get(1);
+       n_spiders   = DistributeEvenly_Get(1);
+       n_tarbabies = DistributeEvenly_Get(0.7);
+       n_shamblers = DistributeEvenly_Get(0.3);
+       if(flyspawns_count > 0)
+               n_wizards = DistributeEvenly_Get(1);
+       if(waterspawns_count > 0)
+               n_fish = DistributeEvenly_Get(1);
+}
+
+void combat_phase_begin()
+{
+       monster_count = totalmonsters;
+       entity head, tail;
+       
+       print("^1Combat phase!\n");
+       FOR_EACH_PLAYER(tail)
+       {
+               Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, "^1Combat phase!", 0, 0);
+       }
+       if(autocvar_sv_eventlog)
+               GameLogEcho(":combatphase");
+       self.think = combat_phase;
+       self.nextthink = time + 1;
+       
+       for(head = world;(head = find(head, classname, "td_generator")); )
+               head.takedamage = DAMAGE_AIM;
+}
+
+float cphase_updates;
+void combat_phase_announce() // TODO: clean up these fail nextthinks...
+{
+       cphase_updates += 1;
+       
+       if(cphase_updates == 0)
+               Announce("prepareforbattle");
+       else if(cphase_updates == 3)
+               Announce("3");
+       else if(cphase_updates == 4)
+               Announce("2");
+       else if(cphase_updates == 5)
+               Announce("1");
+       else if(cphase_updates == 6)
+       {
+               Announce("begin");
+               combat_phase_begin();
+       }
+       
+       if(cphase_updates >= 6)
+               return;
+
+       self.think = combat_phase_announce;
+       self.nextthink = time + 1;
+}
+
+void build_phase()
+{
+       entity head;
+       float n_players = 0, gen_washealed = FALSE, player_washealed = FALSE;
+    string buildmsg, healmsg, countmsg, startmsg, genhealmsg;
+       
+       current_phase = PHASE_BUILD;
+       
+       for(head = world;(head = find(head, classname, "td_generator")); )
+       {
+               if(head.health <= 5 && head.max_health > 10)
+                       Announce("lastsecond");
+                       
+               if(head.health < head.max_health)
+               {
+                       gen_washealed = TRUE;
+                       head.health = head.max_health;
+                       WaypointSprite_UpdateHealth(head.sprite, head.health);
+               }
+               head.takedamage = DAMAGE_NO;
+       }
+       
+       FOR_EACH_PLAYER(head)
+       {
+               if(head.health < 100)
+               {
+                       player_washealed = TRUE;
+                       break; // we found 1, so no need to check the others
+               }
+       }
+               
+       totalmonsters += autocvar_g_td_monster_count_increment * wave_count;
+       monster_skill += autocvar_g_td_monsters_skill_increment;
+       
+       if(wave_count < 1) wave_count = 1;
+
+       genhealmsg = (gen_washealed) ? ((td_gencount == 1) ? " and generator " : " and generators ") : "";
+       buildmsg = sprintf("%s build phase... ", (wave_count == max_waves) ? "^1Final wave^3" : strcat("Wave ", ftos(wave_count)));
+       healmsg = (player_washealed) ? strcat("All players ", genhealmsg, "healed. ") : "";     
+    countmsg = strcat("Next monsters: ", ftos(totalmonsters), ". ");
+    startmsg = strcat("Wave starts in ", ftos(autocvar_g_td_buildphase_time), " seconds");
+       
+       FOR_EACH_PLAYER(head) 
+    {
+               if(head.health < 100)
+                       head.health = 100;
+                       
+               if(gen_washealed)
+                       PlayerScore_Add(head, SP_TD_SCORE, -autocvar_g_td_generator_damaged_points);
+                       
+        n_players += 1;
+        Send_CSQC_Centerprint_Generic(head, CPID_KH_MSG, strcat(buildmsg, healmsg, countmsg, startmsg), 5, 0);
+    }
+    
+    FOR_EACH_MONSTER(head)
+    {
+               if(head.health <= 0)
+                       continue;
+                       
+        print(strcat("Warning: Monster still alive during build phase! Monster name: ", head.netname, "\n"));
+               
+               WaypointSprite_Kill(head.sprite);
+        remove(head);
+    }
+       
+       if(n_players >= 2)
+       {
+               totalmonsters += n_players;
+               monster_skill += n_players * 0.05;
+       }
+       
+       if(monster_skill < 1) monster_skill = 1;
+               
+       if(totalmonsters < 1) totalmonsters = ((autocvar_g_td_monster_count > 0) ? autocvar_g_td_monster_count : 10);
+       
+       monsters_total = totalmonsters;
+       monsters_killed = 0;
+       
+       print(strcat(buildmsg, healmsg, countmsg, startmsg, "\n"));
+               
+       queue_monsters(totalmonsters);
+       
+       cphase_updates = -1;
+       
+       if(autocvar_sv_eventlog)
+        GameLogEcho(strcat(":buildphase:", ftos(wave_count), ":", ftos(totalmonsters)));
+       
+       self.think = combat_phase_announce;
+       self.nextthink = time + build_time - 6;
+}
+
+void wave_end(float starting)
+{
+       entity tail;
+       FOR_EACH_PLAYER(tail)
+       {
+               if(starting)
+                       Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, "Protect the generator from waves of monsters!", 0, 0);
+               else
+                       Send_CSQC_Centerprint_Generic(tail, CPID_KH_MSG, ((wave_count >= max_waves) ? "Level victory!" : "Wave victory!"), 0, 0);
+       }
+       
+       if not(starting)
+       {
+               print((wave_count >= max_waves) ? "^2Level victory!\n" : "^2Wave victory!\n");
+               if(autocvar_sv_eventlog)
+            GameLogEcho(strcat(":wave:", ftos(wave_count), ":victory"));
+       }
+       
+       if(wave_count >= max_waves)
+       {
+               gensurvived = TRUE;
+               return;
+       }
+       
+       if not(starting)
+               wave_count += 1;
+               
+       self.think = build_phase;
+       self.nextthink = time + 3;
+}
+
+void td_ScoreRules()
+{
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_SCORE,             "score",         SFL_SORT_PRIO_PRIMARY);
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_KILLS,             "kills",         SFL_LOWER_IS_BETTER);
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_TURKILLS,  "frags",         SFL_LOWER_IS_BETTER);
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_DEATHS,    "deaths",    SFL_LOWER_IS_BETTER);
+       ScoreInfo_SetLabel_PlayerScore(SP_TD_SUICIDES,  "suicides",  SFL_LOWER_IS_BETTER | SFL_ALLOW_HIDE);
+       ScoreRules_basics_end();
+}
+
+void td_SpawnController()
+{
+       entity oldself = self;
+       self = spawn();
+       self.classname = "td_controller";
+       spawnfunc_td_controller();
+       self = oldself;
+}
+
+void td_DelayedInit()
+{
+       if(find(world, classname, "td_controller") == world)
+       {
+               print("No ""td_controller"" entity found on this map, creating it anyway.\n");
+               td_SpawnController();
+       }
+       
+       td_ScoreRules();
+}
+
+void td_Init()
+{
+       InitializeEntity(world, td_DelayedInit, INITPRIO_GAMETYPE);
+}
+
+MUTATOR_HOOKFUNCTION(td_TurretValidateTarget)
+{
+       if(turret_flags & TFL_TARGETSELECT_MISSILESONLY)
+    if(turret_target.flags & FL_PROJECTILE)
+       if(turret_target.owner.flags & FL_MONSTER)
+        return TRUE; // flac support
+                       
+       if(turret.turrcaps_flags & TFL_TURRCAPS_SUPPORT && turret_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
+               return TRUE;
+       if not(turret_target.flags & FL_MONSTER)
+               turret_target = world;
+               
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerThink)
+{
+       self.stat_current_wave = wave_count;
+       self.stat_totalwaves = max_waves;
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerSpawn)
+{
+       self.bot_attack = FALSE;
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerDies)
+{
+       if(frag_attacker.flags & FL_MONSTER)
+               PlayerScore_Add(frag_target, SP_TD_DEATHS, 1);
+               
+       if(frag_target == frag_attacker)
+               PlayerScore_Add(frag_attacker, SP_TD_SUICIDES, 1);
+
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_GiveFragsForKill)
+{
+       frag_score = 0;
+               
+       return TRUE; // no frags counted in td
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerDamage)
+{
+       if(frag_attacker.realowner == frag_target)
+               frag_damage = 0;
+               
+       if(frag_target.flags & FL_MONSTER && time < frag_target.spawnshieldtime)
+               frag_damage = 0;
+               
+       if(frag_target.vehicle_flags & VHF_ISVEHICLE && !(frag_attacker.flags & FL_MONSTER))
+               frag_damage = 0;
+               
+       if(frag_attacker.vehicle_flags & VHF_ISVEHICLE && !(frag_target.flags & FL_MONSTER))
+               frag_damage = 0;
+               
+       if(!autocvar_g_td_pvp && frag_attacker != frag_target && frag_target.classname == STR_PLAYER && frag_attacker.classname == STR_PLAYER)
+               frag_damage = 0;
+               
+       if(frag_attacker.turrcaps_flags & TFL_TURRCAPS_ISTURRET && frag_target.classname == STR_PLAYER)
+               frag_damage = 0;
+               
+       if((frag_target.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && !(frag_attacker.flags & FL_MONSTER || frag_attacker.turrcaps_flags & TFL_TURRCAPS_SUPPORT))
+               frag_damage = 0;
+               
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(td_TurretDies)
+{
+       if(self.realowner)
+               self.realowner.turret_cnt -= 1;
+                       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterCheckBossFlag)
+{
+       // No minibosses in tower defense
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterMove)
+{
+       entity player;
+       float n_players = 0;
+       FOR_EACH_PLAYER(player) { ++n_players; }
+       
+       if(n_players < 1) // no players online, so do nothing
+       {
+               monster_target = world;
+               monster_speed_run = monster_speed_walk = 0;
+               return FALSE;
+       }
+       
+       if not(self.enemy) // don't change targets while attacking
+       if((vlen(self.goalentity.origin - self.origin) <= 100 && self.goalentity.classname == "td_waypoint") || (vlen(self.goalentity.origin - self.origin) <= 200 && self.flags & FL_FLY && self.goalentity.classname == "td_waypoint"))
+       {
+               if(self.goalentity.target2)
+               {
+                       if(random() > 0.5)
+                               self.target = self.goalentity.target2;
+                       else
+                               self.target = self.goalentity.target;
+               }
+               else
+                       self.target = self.goalentity.target;
+                               
+               self.goalentity = find(world, targetname, self.target);
+               
+               if(self.goalentity == world)
+                       self.goalentity = PickGenerator();
+       }
+       
+       monster_speed_run = m_speed_run * monster_skill;
+       monster_speed_walk = m_speed_walk * monster_skill;
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterSpawn)
+{
+       if(self.realowner == world) // nothing spawned it, so kill it
+       {
+               WaypointSprite_Kill(self.sprite);
+               remove(self);
+               return TRUE;
+       }
+       
+       current_monsters += 1;
+       
+       self.spawnshieldtime = time + autocvar_g_td_monsters_spawnshield_time;
+       
+       self.drop_size = self.health * 0.05;
+       
+       if(self.drop_size < 1) self.drop_size = 1;
+       
+       self.dphitcontentsmask = DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_BODY;
+       
+       self.origin += '0 0 25'; // hopefully this fixes monsters falling through the floor
+       
+       switch(self.classname)
+       {
+               case "monster_knight": n_knights -= 1; break;
+               case "monster_dog": n_dogs -= 1; break;
+               case "monster_ogre": n_ogres -= 1; break;
+               case "monster_shambler": n_shamblers -= 1; AnnounceSpawn("Shambler"); break;
+               case "monster_wizard": n_wizards -= 1; break;
+               case "monster_shalrath": n_shalraths -= 1; break;
+               case "monster_soldier": n_soldiers -= 1; break;
+               case "monster_hellknight": n_hknights -= 1; break;
+               case "monster_enforcer": n_enforcers -= 1; break;
+               case "monster_demon": n_demons -= 1; break;
+               case "monster_zombie": n_zombies -= 1; break;
+               case "monster_spider": n_spiders -= 1; break;
+               case "monster_tarbaby": n_tarbabies -= 1; break;
+       }
+       
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterDies)
+{
+       entity oldself;
+       vector backuporigin;
+
+       monster_count -= 1;
+       current_monsters -= 1;
+       monsters_killed += 1;
+       
+       if(frag_attacker.classname == STR_PLAYER)
+       {
+               PlayerScore_Add(frag_attacker, SP_TD_SCORE, autocvar_g_td_kill_points);
+               PlayerScore_Add(frag_attacker, SP_TD_KILLS, 1);
+       }
+       else if(frag_attacker.realowner.classname == STR_PLAYER)
+       {
+               PlayerScore_Add(frag_attacker.realowner, SP_TD_SCORE, autocvar_g_td_turretkill_points);
+               PlayerScore_Add(frag_attacker.realowner, SP_TD_TURKILLS, 1);
+       }
+
+       backuporigin = self.origin;
+       oldself = self;
+       self = spawn();
+       
+       self.gravity = 1;
+       setorigin(self, backuporigin + '0 0 5');
+       spawn_td_fuel(oldself.drop_size);
+       self.touch = M_Item_Touch;
+       if(self == world)
+       {
+               self = oldself;
+               return FALSE;
+       }
+       SUB_SetFade(self, time + 5, 1);
+       
+       self = oldself;
+
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_MonsterFindTarget)
+{
+       float n_players = 0;
+       entity player;
+       local entity e;
+       
+       FOR_EACH_PLAYER(player) { ++n_players; }
+       
+       if(n_players < 1) // no players online, so do nothing
+       {
+               self.enemy = world;
+               return TRUE;
+       }
+       
+       for(e = world;(e = findflags(e, monster_attack, TRUE)); ) 
+       {
+               if(ignore_turrets)
+               if(e.turrcaps_flags & TFL_TURRCAPS_ISTURRET)
+                       continue;
+               
+               if(monster_isvalidtarget(e, self, FALSE))
+               if((vlen(trace_endpos - self.origin) < 200 && e.turrcaps_flags & TFL_TURRCAPS_ISTURRET) || (vlen(trace_endpos - self.origin) < 200 && e.classname != "td_generator") || (vlen(trace_endpos - self.origin) < 500 && e.classname == "td_generator"))
+               {
+                       self.enemy = e;
+               }
+       }
+       
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(td_SetStartItems)
+{
+       start_ammo_fuel = 150; // to be nice...
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_TurretSpawn)
+{
+       if(self.realowner == world)
+               return TRUE; // wasn't spawned by a player
+               
+       self.bot_attack = FALSE;
+       self.turret_buff = 1;
+       
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(td_DisableVehicles)
+{
+       // you shall not spawn!
+       return TRUE;
+}
+
+MUTATOR_HOOKFUNCTION(td_PlayerCommand)
+{
+       if(MUTATOR_RETURNVALUE) { return FALSE; } // command was already handled?
+       
+       makevectors(self.v_angle);
+       WarpZone_TraceLine(self.origin + self.view_ofs, self.origin + self.view_ofs + v_forward * 100, MOVE_NORMAL, self);
+       
+       if(cmd_name == "turretspawn")
+       {
+               if(argv(1) == "list")
+               {
+                       sprint(self, "Available turrets:\n");
+                       sprint(self, "^3mlrs walker plasma towerbuff\n");
+                       return TRUE;
+               }
+               if(self.classname != STR_PLAYER || self.health <= 0)
+               { 
+                       sprint(self, "Can't spawn turrets while spectating/dead\n");
+                       return TRUE;
+               }
+               if(self.turret_cnt >= max_turrets)
+               {
+                       sprint(self, strcat("Can't spawn more than ", ftos(max_turrets), " turrets\n"));
+                       return TRUE;
+               }
+               switch(argv(1))
+               {
+                       case "plasma":
+                       {
+                if(self.ammo_fuel < autocvar_g_td_turret_plasma_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_turret_plasma_cost;
+                               spawnturret(self, self, "plasma", trace_endpos);
+                               sprint(self, "Spawned 1 plasma turret", "\n");
+                               return TRUE;
+                       }
+                       case "mlrs":
+                       {
+                if(self.ammo_fuel < autocvar_g_td_turret_mlrs_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_turret_mlrs_cost;
+                               spawnturret(self, self, "mlrs", trace_endpos);
+                               sprint(self, "Spawned 1 MLRS turret", "\n");
+                               return TRUE;
+                       }
+                       case "flac":
+                       {
+                if(self.ammo_fuel < autocvar_g_td_turret_flac_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_turret_flac_cost;
+                               spawnturret(self, self, "flac", trace_endpos);
+                               sprint(self, "Spawned 1 FLAC Cannon", "\n");
+                               return TRUE;
+                       }
+                       case "walker":
+                       {
+                if(self.ammo_fuel < autocvar_g_td_turret_walker_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_turret_walker_cost;
+                               spawnturret(self, self, "walker", trace_endpos);
+                               sprint(self, "Spawned 1 walker turret", "\n");
+                               return TRUE;
+                       }
+                       case "towerbuff":
+                       {
+                if(self.ammo_fuel < autocvar_g_td_tower_buff_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_tower_buff_cost;
+                               spawnturret(self, self, "fusionreactor", trace_endpos);
+                               sprint(self, "Spawned 1 tower buff turret\n");
+                               return TRUE;
+                       }
+                       case "barricade":
+                       {
+                               if(self.ammo_fuel < autocvar_g_td_barricade_cost) break;
+                               self.ammo_fuel -= autocvar_g_td_barricade_cost;
+                               spawnturret(self, self, "barricade", trace_endpos);
+                               sprint(self, "Spawned 1 barricade\n");
+                               return TRUE;
+                       }
+                       default:
+                       {
+                               sprint(self, "Invalid turret. type 'cmd turret list' to see a list of all available turrets.\n");
+                               return TRUE;
+                       }
+               }
+               sprint(self, strcat("You do not have enough fuel to spawn a ", argv(1), " turret\n"));
+               return TRUE;
+       }
+       if(cmd_name == "repairturret")
+       {
+               if(trace_ent.realowner != self || !(trace_ent.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
+               {
+                       sprint(self, "You need to aim at your turret to repair it\n");
+                       return TRUE;
+               }
+               if(self.ammo_fuel < autocvar_g_td_turret_repair_cost)   
+               {
+                       sprint(self, strcat("You need ", ftos(autocvar_g_td_turret_repair_cost), " fuel to repair this turret\n"));
+                       return TRUE;
+               }
+               if(trace_ent.health >= trace_ent.max_health)
+               {
+                       sprint(self, "This turret is already at max health\n");
+                       return TRUE;
+               }
+               
+               self.ammo_fuel -= autocvar_g_td_turret_repair_cost;
+               trace_ent.SendFlags |= TNSF_STATUS;
+               trace_ent.health = bound(1, trace_ent.health + 100, trace_ent.max_health);
+               sprint(self, "100 turret health repaired!\n");
+               
+               return TRUE;
+       }
+       if(cmd_name == "buffturret")
+       {
+               if(trace_ent.realowner != self || !(trace_ent.turrcaps_flags & TFL_TURRCAPS_ISTURRET))
+               {
+                       sprint(self, "You need to aim at your turret to upgrade it\n");
+                       return TRUE;
+               }
+               if(self.ammo_fuel < autocvar_g_td_turret_upgrade_cost)  
+               {
+                       sprint(self, strcat("You need ", ftos(autocvar_g_td_turret_upgrade_cost), " fuel to increase this turret's power\n"));
+                       return TRUE;
+               }
+               if(trace_ent.turret_buff >= 3)
+               {
+                       sprint(self, "This turret cannot be buffed up any higher\n");
+                       return TRUE;
+               }
+               
+               self.ammo_fuel -= autocvar_g_td_turret_upgrade_cost;
+               trace_ent.SendFlags |= TNSF_STATUS;
+               buffturret(trace_ent, 1.2);
+               sprint(self, "Turret power increased by 20%!\n");
+               
+               return TRUE;
+       }
+       if(cmd_name == "turretremove")
+       {
+               if((trace_ent.turrcaps_flags & TFL_TURRCAPS_ISTURRET) && trace_ent.realowner == self)
+               {
+                       self.turret_cnt -= 1;
+                       sprint(self, strcat("You removed your ", trace_ent.netname, "\n"));
+                       WaypointSprite_Kill(trace_ent.sprite);
+                       remove(trace_ent.tur_head);
+                       remove(trace_ent);
+                       return TRUE;
+               }
+               sprint(self, "You need to aim at your turret to remove it\n");
+               return TRUE;
+       }
+       if(cmd_name == "debugmonsters")
+       {
+        sprint(self, strcat("^3Current wave: ^1", ftos(wave_count), "\n"));
+               sprint(self, strcat("^3Maximum waves: ^1", ftos(max_waves), "\n"));
+               sprint(self, strcat("^3Monster skill: ^1", ftos(monster_skill), "\n"));
+               sprint(self, strcat("^3Current monsters: ^1", ftos(monster_count), "\n"));
+               sprint(self, strcat("^3Maximum monsters: ^1", ftos(totalmonsters), "\n"));
+        sprint(self, strcat("^3Current ogres: ^1", ftos(n_ogres), "\n"));
+        sprint(self, strcat("^3Current knights: ^1", ftos(n_knights), "\n"));
+        sprint(self, strcat("^3Current dogs: ^1", ftos(n_dogs), "\n"));
+        sprint(self, strcat("^3Current shamblers: ^1", ftos(n_shamblers), "\n"));
+        sprint(self, strcat("^3Current scrags: ^1", ftos(n_wizards), "\n"));
+        sprint(self, strcat("^3Current vores: ^1", ftos(n_shalraths), "\n"));
+        sprint(self, strcat("^3Current grunts: ^1", ftos(n_soldiers), "\n"));
+        sprint(self, strcat("^3Current hell knights: ^1", ftos(n_hknights), "\n"));
+        sprint(self, strcat("^3Current enforcers: ^1", ftos(n_enforcers), "\n"));
+        sprint(self, strcat("^3Current fiends: ^1", ftos(n_demons), "\n"));
+               sprint(self, strcat("^3Current zombies: ^1", ftos(n_zombies), "\n"));
+               sprint(self, strcat("^3Current spawns: ^1", ftos(n_tarbabies), "\n"));
+               sprint(self, strcat("^3Current rotfish: ^1", ftos(n_fish), "\n"));
+               sprint(self, strcat("^3Current spiders: ^1", ftos(n_spiders), "\n"));
+               return TRUE;
+       }
+       
+       return FALSE;
+}
+
+MUTATOR_DEFINITION(gamemode_td)
+{
+       MUTATOR_HOOK(MonsterSpawn, td_MonsterSpawn, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterDies, td_MonsterDies, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterMove, td_MonsterMove, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterFindTarget, td_MonsterFindTarget, CBC_ORDER_ANY);
+       MUTATOR_HOOK(MonsterCheckBossFlag, td_MonsterCheckBossFlag, CBC_ORDER_ANY);
+       MUTATOR_HOOK(SetStartItems, td_SetStartItems, CBC_ORDER_ANY);
+       MUTATOR_HOOK(TurretValidateTarget, td_TurretValidateTarget, CBC_ORDER_ANY);
+       MUTATOR_HOOK(TurretSpawn, td_TurretSpawn, CBC_ORDER_ANY);
+       MUTATOR_HOOK(TurretDies, td_TurretDies, CBC_ORDER_ANY);
+       MUTATOR_HOOK(GiveFragsForKill, td_GiveFragsForKill, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerPreThink, td_PlayerThink, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerDies, td_PlayerDies, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerDamage_Calculate, td_PlayerDamage, CBC_ORDER_ANY);
+       MUTATOR_HOOK(PlayerSpawn, td_PlayerSpawn, CBC_ORDER_ANY);
+       MUTATOR_HOOK(VehicleSpawn, td_DisableVehicles, CBC_ORDER_ANY);
+       MUTATOR_HOOK(SV_ParseClientCommand, td_PlayerCommand, 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.");        
+               cvar_settemp("g_monsters", "1");
+               cvar_settemp("g_turrets", "1");
+               td_Init();
+       }
+
+       MUTATOR_ONREMOVE
+       {
+               error("This is a game type and it cannot be removed at runtime.");
+       }
+
+       return FALSE;
+}
diff --git a/qcsrc/server/mutators/gamemode_td.qh b/qcsrc/server/mutators/gamemode_td.qh
new file mode 100644 (file)
index 0000000..b199fd5
--- /dev/null
@@ -0,0 +1,62 @@
+// Counters
+float monster_count, totalmonsters;
+float n_knights, n_dogs, n_ogres, n_shamblers, n_wizards, n_shalraths, n_soldiers, n_hknights, n_enforcers, n_demons, n_zombies, n_tarbabies, n_fish, n_spiders;
+float current_monsters;
+float waterspawns_count, flyspawns_count;
+float wave_count, max_waves;
+float max_turrets;
+
+// Monster defs
+.float drop_size;
+float m_speed_run;
+float m_speed_walk;
+
+// Turret defs
+.float turret_buff;
+
+// TD defs
+.float stat_current_wave;
+.float stat_totalwaves;
+.float spawntype;
+float spawn_delay;
+float max_current;
+float ignore_turrets;
+float SWARM_NORMAL     = 0;
+float SWARM_WEAK       = 1;
+float SWARM_STRONG     = 2;
+float SWARM_FLY                = 3;
+float SWARM_SWIM       = 4;
+float build_time;
+float td_dont_end;
+void(float starting) wave_end;
+.float turret_cnt;
+float td_gencount;
+void() spawnfunc_td_controller;
+float current_phase;
+#define PHASE_BUILD    1
+#define PHASE_COMBAT   2
+
+// Scores
+#define SP_TD_KILLS    0
+#define SP_TD_TURKILLS         2
+#define SP_TD_SCORE    4
+#define SP_TD_DEATHS   6
+#define SP_TD_SUICIDES         8
+
+// Controller
+.float maxwaves;
+.float monstercount;
+.float startwave;
+.float dontend;
+.float maxturrets;
+.float buildtime;
+.float mspeed_run;
+.float mspeed_walk;
+.float spawndelay;
+.float maxcurrent;
+.float ignoreturrets;
+
+// Generator
+float gendestroyed;
+#define GENERATOR_MIN '-52 -52 -14'
+#define GENERATOR_MAX '52 52 75'
\ No newline at end of file
diff --git a/qcsrc/server/mutators/mutator_zombie_apocalypse.qc b/qcsrc/server/mutators/mutator_zombie_apocalypse.qc
new file mode 100644 (file)
index 0000000..2c3030e
--- /dev/null
@@ -0,0 +1,152 @@
+// Zombie Apocalypse mutator - small side project
+// Spawns a defined number of zombies at the start of a match
+
+float za_numspawns, totalzombies, roundcnt, numzoms;
+entity PickZombieSpawn()
+{
+       entity sp;
+       
+       RandomSelection_Init();
+       
+       if(teamplay)
+       {
+               for(sp = world; (sp = find(sp, classname, "info_player_team1")); )
+               {
+                       RandomSelection_Add(sp, 0, string_null, 1, 1);
+               }
+       }
+       else
+       {
+               for(sp = world; (sp = find(sp, classname, "info_player_deathmatch")); )
+               {
+                       RandomSelection_Add(sp, 0, string_null, 1, 1);
+               }
+       }
+       
+       return RandomSelection_chosen_ent;
+}
+
+void zombie_spawn_somewhere ()
+{
+       if(gameover) { return; }
+    
+    entity mon, sp;
+       
+       if(MoveToRandomMapLocation(self, DPCONTENTS_SOLID | DPCONTENTS_CORPSE | DPCONTENTS_PLAYERCLIP, DPCONTENTS_SLIME | DPCONTENTS_LAVA | DPCONTENTS_SKY | DPCONTENTS_BODY | DPCONTENTS_DONOTENTER, Q3SURFACEFLAG_SKY, 10, 1024, 256))
+       {
+               mon = spawnmonster("zombie", self, self, self.origin, TRUE, 2);
+               tracebox(mon.origin, mon.mins, mon.maxs, mon.origin, MOVE_NOMONSTERS, mon);
+               mon.spawnflags |= MONSTERFLAG_NORESPAWN;
+
+               if(trace_startsolid)
+               {
+                       sp = PickZombieSpawn();
+                       if(sp)
+                               setorigin(mon, sp.origin);
+               }
+                       
+        za_numspawns += 1;
+       }
+       else
+               zombie_spawn_somewhere();
+}
+
+void() spawn_zombies;
+void za_roundwon()
+{
+       entity head;
+       FOR_EACH_PLAYER(head)
+       {
+               Send_CSQC_Centerprint_Generic(head, CPID_MINSTA_FINDAMMO, "All the zombies have been exterminated! Prepare for round 2!", 0, 0);
+       }
+       
+       roundcnt += 1;
+       
+       numzoms = autocvar_g_za_monster_count * roundcnt;
+       
+       monsters_total = numzoms;
+       totalzombies = numzoms;
+       
+       self.think = spawn_zombies;
+       self.nextthink = time + 10;
+}
+
+void spawn_zombies ()
+{
+       self.nextthink = time + 1;
+       
+       if(totalzombies < 1)
+       {
+               self.think = za_roundwon;
+               self.nextthink = time;
+               return;
+       }
+       
+       if(gameover || numzoms <= 0)
+               return;
+               
+    entity e;
+    
+    print("Them zombies be spawnin'!\n");
+
+       while(numzoms > 0)
+       {
+        e = spawn();
+               e.think = zombie_spawn_somewhere;
+        e.nextthink = time;
+
+               numzoms -= 1;
+       }
+}
+
+void za_init ()
+{
+    entity e;
+       
+       roundcnt = 1;
+       
+    numzoms = autocvar_g_za_monster_count * roundcnt;
+       
+       monsters_total = numzoms;
+       totalzombies = numzoms;
+       
+    e = spawn();
+       e.think = spawn_zombies;
+       e.nextthink = time + 3;
+}
+
+MUTATOR_HOOKFUNCTION(za_ZombieDies)
+{
+       if(frag_attacker.classname == "player")
+               PlayerScore_Add(frag_attacker, SP_SCORE, 1);
+       
+       totalzombies -= 1;
+       monsters_killed += 1;
+       return FALSE;
+}
+
+MUTATOR_HOOKFUNCTION(za_BuildMutatorsString)
+{
+       ret_string = strcat(ret_string, ":Zombies");
+       return 0;
+}
+
+MUTATOR_HOOKFUNCTION(za_BuildMutatorsPrettyString)
+{
+       ret_string = strcat(ret_string, ", Zombies");
+       return 0;
+}
+
+MUTATOR_DEFINITION(mutator_zombie_apocalypse)
+{
+       MUTATOR_HOOK(MonsterDies, za_ZombieDies, CBC_ORDER_ANY);
+       MUTATOR_HOOK(BuildMutatorsString, za_BuildMutatorsString, CBC_ORDER_ANY);
+       MUTATOR_HOOK(BuildMutatorsPrettyString, za_BuildMutatorsPrettyString, CBC_ORDER_ANY);
+    
+    MUTATOR_ONADD
+    {
+        za_init();
+    }
+
+       return 0;
+}
index 4bdcbb28234834629ecc13f8f52767fcd1e4ce90..7a8b5764a5394fc404a56d0cb66dca5e84f32785 100644 (file)
@@ -4,7 +4,9 @@ MUTATOR_DECLARATION(gamemode_keepaway);
 MUTATOR_DECLARATION(gamemode_ctf);
 MUTATOR_DECLARATION(gamemode_nexball);
 MUTATOR_DECLARATION(gamemode_onslaught);
+MUTATOR_DECLARATION(gamemode_td);
 MUTATOR_DECLARATION(gamemode_domination);
+MUTATOR_DECLARATION(gamemode_rts);
 
 MUTATOR_DECLARATION(mutator_dodging);
 MUTATOR_DECLARATION(mutator_invincibleprojectiles);
@@ -15,5 +17,6 @@ MUTATOR_DECLARATION(mutator_spawn_near_teammate);
 MUTATOR_DECLARATION(mutator_physical_items);
 MUTATOR_DECLARATION(mutator_vampire);
 MUTATOR_DECLARATION(mutator_superspec);
+MUTATOR_DECLARATION(mutator_zombie_apocalypse);
 
 MUTATOR_DECLARATION(sandbox);
index db93116109612dd97660f35306509908f43df41c..f688a86170bfd16873542d7b7b5bd0e18c3f02b7 100644 (file)
@@ -36,6 +36,8 @@ mutators/gamemode_keyhunt.qh // TODO fix this
 mutators/gamemode_keepaway.qh
 mutators/gamemode_nexball.qh 
 mutators/mutator_dodging.qh
+mutators/gamemode_td.qh
+mutators/gamemode_rts.qh
 
 //// tZork Turrets ////
 tturrets/include/turrets_early.qh
@@ -208,6 +210,8 @@ playerstats.qc
 
 ../common/explosion_equation.qc
 
+monsters/monsters.qh
+
 mutators/base.qc
 mutators/gamemode_ctf.qc
 mutators/gamemode_domination.qc
@@ -216,6 +220,8 @@ mutators/gamemode_keyhunt.qc
 mutators/gamemode_keepaway.qc
 mutators/gamemode_nexball.qc
 mutators/gamemode_onslaught.qc
+mutators/gamemode_td.qc
+mutators/gamemode_rts.qc
 mutators/mutator_invincibleproj.qc
 mutators/mutator_new_toys.qc
 mutators/mutator_nix.qc
@@ -226,6 +232,7 @@ mutators/mutator_spawn_near_teammate.qc
 mutators/mutator_physical_items.qc
 mutators/sandbox.qc
 mutators/mutator_superspec.qc
+mutators/mutator_zombie_apocalypse.qc
 
 ../warpzonelib/anglestransform.qc
 ../warpzonelib/mathlib.qc
index c91c1ac1895ecae9d49667175258f3b91b9f9486..579afd544718e0cbec1ba923afc90dda3cb29d36 100644 (file)
@@ -10,6 +10,7 @@ void CreatureFrame (void)
                
                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)
                {
@@ -19,7 +20,7 @@ void CreatureFrame (void)
                                self.dmgtime = 0;
                        }
 
-                       if(!vehic && !projectile) // vehicles and projectiles don't drown
+                       if(!vehic && !projectile && !monster) // vehicles, monsters and projectiles don't drown
                        {
                                if (self.waterlevel != WATERLEVEL_SUBMERGED)
                                {
index 1ed2f533b3a923c93981ef14ad5403eb847a0f25..564171c0c1813f38b71723e73fe46feb0bf301ea 100644 (file)
@@ -145,6 +145,22 @@ void InitGameplayMode()
                MUTATOR_ADD(gamemode_ctf);
                have_team_spawns = -1; // request team spawns
        }
+    
+       if(g_td)
+       {
+               fraglimit_override = 0; // no limits in TD - it's a survival mode
+               leadlimit_override = 0;
+               MUTATOR_ADD(gamemode_td);
+       }
+       
+       if(g_rts)
+       {
+               ActivateTeamplay();
+               fraglimit_override = 0;
+               leadlimit_override = 0;
+               MUTATOR_ADD(gamemode_rts);
+               have_team_spawns = -1; // request team spawns
+       }
 
        if(g_runematch)
        {
index 971d65cca3916eea9f99a17d28b5d41ee096211f..19974401f325f2407c148d6df02ccaaa45ec221c 100644 (file)
@@ -22,6 +22,8 @@ void turret_stdproc_die()
     self.takedamage             = DAMAGE_NO;
 
     self.health             = 0;
+       
+       MUTATOR_CALLHOOK(TurretDies);
 
 // Go boom
     //RadiusDamage (self,self, min(self.ammo,50),min(self.ammo,50) * 0.25,250,world,min(self.ammo,50)*5,DEATH_TURRET,world);
@@ -64,6 +66,7 @@ void turret_stdproc_respawn()
     self.tur_head.avelocity     = self.avelocity;
     self.tur_head.angles        = self.idle_aim;
     self.health                 = self.tur_health;
+       self.max_health                         = self.tur_health;
 
     self.enemy                  = world;
     self.volly_counter          = self.shot_volly;
index dd58b3cee2860e0c031dc131a47eab165c846967..ba42215aeb213567ba53047110125a7f24ef23cb 100644 (file)
@@ -571,6 +571,14 @@ float turret_stdproc_firecheck()
 float turret_validate_target(entity e_turret, entity e_target, float validate_flags)
 {
     vector v_tmp;
+       
+       turret_target = e_target;
+       turret_flags = validate_flags;
+       turret = e_turret;
+       if(MUTATOR_CALLHOOK(TurretValidateTarget))
+               return 1;
+       e_target = turret_target;
+       e_turret = turret;
         
     //if(!validate_flags & TFL_TARGETSELECT_NOBUILTIN)
     //    return -0.5;
@@ -1107,6 +1115,9 @@ float turret_stdproc_init (string cvar_base_name, string base, string head, floa
     if not (self.health)
         self.health = 1000;
     self.tur_health = max(1, self.health);
+       self.max_health = self.tur_health;
+       self.bot_attack = TRUE;
+    self.monster_attack = TRUE;
 
     if not (self.turrcaps_flags)
         self.turrcaps_flags = TFL_TURRCAPS_RADIUSDMG | TFL_TURRCAPS_MEDPROJ | TFL_TURRCAPS_PLAYERKILL;
@@ -1339,7 +1350,6 @@ float turret_stdproc_init (string cvar_base_name, string base, string head, floa
         self.turret_score_target    = turret_stdproc_targetscore_generic;
 
     self.use = turret_stdproc_use;
-    self.bot_attack = TRUE;
 
     ++turret_count;
     self.nextthink = time + 1;
index c542dab401ba2ee88da3f88df7d7c57f48329478..16ecbce5e72167a4dd42e0c311e67d792d48fb06 100644 (file)
@@ -44,7 +44,10 @@ float turret_stdproc_targetscore_generic(entity _turret, entity _target)
     if ((_turret.target_select_missilebias > 0) && (_target.flags & FL_PROJECTILE))
         m_score = 1;
 
-    if ((_turret.target_select_playerbias > 0) && (_target.flags & FL_CLIENT))
+    if ((_turret.target_select_playerbias > 0) && (_target.flags & FL_CLIENT) && !g_td)
+        p_score = 1;
+        
+    if(g_td && _target.flags & FL_MONSTER)
         p_score = 1;
 
     d_score = max(d_score, 0);
index 8118b8f234dfa1cb23d2c52d737489679dd47ab1..6301079d9b95f204c2f572fbbf1e35cb9538bde3 100644 (file)
@@ -7,6 +7,17 @@ void turret_fusionreactor_fire()
     vector fl_org;
 
     self.enemy.ammo = min(self.enemy.ammo + self.shot_dmg,self.enemy.ammo_max);
+    if(g_td) // auto repair?
+       {
+        self.enemy.health = min(self.enemy.health + self.shot_dmg,self.enemy.max_health);
+               self.enemy.tur_health = min(self.enemy.tur_health + self.shot_dmg,self.enemy.max_health);
+       }
+       if(g_rts) // monster healing
+       {
+               self.enemy.heal_delay = time + 0.5;
+               self.enemy.health = min(self.enemy.health + self.shot_dmg,self.enemy.max_health);
+               WaypointSprite_UpdateHealth(self.enemy.sprite, self.enemy.health);
+       }
     fl_org = 0.5 * (self.enemy.absmin + self.enemy.absmax);
     te_smallflash(fl_org);
 }
@@ -38,15 +49,43 @@ float turret_fusionreactor_firecheck()
                return 0;
 
        if (self.ammo < self.shot_dmg)
-               return 0;
+               return 0;       
+       
+       if (vlen(self.enemy.origin - self.origin) > self.target_range)
+               return 0;       
 
+    if(g_td)
+    {
+        if(self.realowner != self.enemy.realowner)
+            return 0;
+            
+               if(self.enemy.turrcaps_flags & TFL_TURRCAPS_AMMOSOURCE)
+                       return 0;
+                       
+        if(self.enemy.health >= self.enemy.max_health)
+            return 0;
+    }
+       if(g_rts)
+       {
+               if(IsDifferentTeam(self, self.enemy))
+                       return 0;
+                       
+               if not(self.enemy.flags & FL_MONSTER)
+                       return 0;
+               
+               if(self.enemy.health >= self.enemy.max_health)
+                       return 0;
+                       
+               if(time < self.enemy.heal_delay)
+                       return 0;
+                       
+               return 1; // this is for monsters, so don't do the other checks
+       }
+       
        if (self.enemy.ammo >= self.enemy.ammo_max)
                return 0;
        
-       if (vlen(self.enemy.origin - self.origin) > self.target_range)
-               return 0;                               
-       
-       if(self.team != self.enemy.team)
+       if(teamplay && self.team != self.enemy.team)
                return 0;
        
        if not (self.enemy.ammo_flags & TFL_AMMO_ENERGY)
index 4bd1d0760b96128192ebeb0f585f9fb44b3a8515..56cffd9a40065af6831e5a4bb1306ad644f0f4da 100644 (file)
@@ -107,7 +107,7 @@ void walker_rocket_think()
     m_speed = vlen(self.velocity);
 
     // Enemy dead? just keep on the current heading then.
-    if (self.enemy == world || self.enemy.deadflag != DEAD_NO)
+    if (self.enemy == world || self.enemy.deadflag != DEAD_NO || (g_td && !(self.enemy.flags & FL_MONSTER || self.enemy.classname == "td_generator")) || self.enemy.classname == "td_generator")
         self.enemy = world;
 
     if (self.enemy)
index 5e377079e01dd2fc0b1377fde2a884a17e50656a..de4e2f71df43ca9adb2d6b77e898fe6a8a54b14f 100644 (file)
@@ -506,7 +506,7 @@ float vehicles_crushable(entity e)
     if(e.classname == "player")
         return TRUE;
 
-    if(e.classname == "monster_zombie")
+    if(e.flags & FL_MONSTER)
         return TRUE;
 
     return FALSE;
@@ -1315,6 +1315,9 @@ float vehicle_initialize(string  net_name,
     self.pos1 = self.origin;
     self.pos2 = self.angles;
     self.tur_head.team = self.team;
+       
+       if(MUTATOR_CALLHOOK(VehicleSpawn))
+               return FALSE;
 
     return TRUE;
 }
index 082e454ace2de00516090a35270868346ba7a8c8..e02ee701b97d3a4f213d39455365444e8a8282ba 100644 (file)
@@ -253,7 +253,7 @@ void lgbeam_think()
                return;
        }
 
-       if (owner_player.weaponentity.state != WS_INUSE || !lgbeam_checkammo() || owner_player.deadflag != DEAD_NO || !owner_player.BUTTON_ATCK || owner_player.freezetag_frozen)
+       if (owner_player.weaponentity.state != WS_INUSE || !lgbeam_checkammo() || owner_player.deadflag != DEAD_NO || !owner_player.BUTTON_ATCK || owner_player.freezetag_frozen || owner_player.frozen)
        {
                if(self == owner_player.lgbeam)
                        owner_player.lgbeam = world;
diff --git a/scripts/barricade.shader b/scripts/barricade.shader
new file mode 100644 (file)
index 0000000..c650b5b
--- /dev/null
@@ -0,0 +1,8 @@
+barricade
+{
+ cull none
+ {
+       map textures/barricade.tga
+ }
+}
+
diff --git a/scripts/mage.shader b/scripts/mage.shader
new file mode 100644 (file)
index 0000000..7a235c1
--- /dev/null
@@ -0,0 +1,8 @@
+mage
+{
+       cull none
+
+       {
+               map textures/mage
+       }
+}
index a73e7e2064a38d6966bb223a7b425037d24b8901..8ed9769d00bf8942f3f4aaf6cfbd73a5dafd2e83 100644 (file)
@@ -14,3 +14,5 @@ tree
 tuba
 turrets
 weapons
+mage
+barricade
diff --git a/textures/barricade.tga b/textures/barricade.tga
new file mode 100644 (file)
index 0000000..d9cd774
Binary files /dev/null and b/textures/barricade.tga differ
diff --git a/textures/barricade_norm.tga b/textures/barricade_norm.tga
new file mode 100644 (file)
index 0000000..e664ed4
Binary files /dev/null and b/textures/barricade_norm.tga differ
diff --git a/textures/bloodyskull_pants.jpg b/textures/bloodyskull_pants.jpg
new file mode 100644 (file)
index 0000000..e0083ac
Binary files /dev/null and b/textures/bloodyskull_pants.jpg differ
diff --git a/textures/cerberus/cerberus_text.png b/textures/cerberus/cerberus_text.png
new file mode 100644 (file)
index 0000000..3ac7c05
Binary files /dev/null and b/textures/cerberus/cerberus_text.png differ
diff --git a/textures/cerberus/cerberus_text_pants.png b/textures/cerberus/cerberus_text_pants.png
new file mode 100644 (file)
index 0000000..d109711
Binary files /dev/null and b/textures/cerberus/cerberus_text_pants.png differ
diff --git a/textures/mage.tga b/textures/mage.tga
new file mode 100644 (file)
index 0000000..89c02fc
Binary files /dev/null and b/textures/mage.tga differ
diff --git a/textures/marine.tga b/textures/marine.tga
new file mode 100644 (file)
index 0000000..752c40f
Binary files /dev/null and b/textures/marine.tga differ
diff --git a/textures/marine_gloss.tga b/textures/marine_gloss.tga
new file mode 100644 (file)
index 0000000..deb4151
Binary files /dev/null and b/textures/marine_gloss.tga differ
diff --git a/textures/marine_glow.tga b/textures/marine_glow.tga
new file mode 100644 (file)
index 0000000..9af5a19
Binary files /dev/null and b/textures/marine_glow.tga differ
diff --git a/textures/marine_norm.tga b/textures/marine_norm.tga
new file mode 100644 (file)
index 0000000..850ce49
Binary files /dev/null and b/textures/marine_norm.tga differ
diff --git a/textures/marine_pants.tga b/textures/marine_pants.tga
new file mode 100644 (file)
index 0000000..1f295b8
Binary files /dev/null and b/textures/marine_pants.tga differ
diff --git a/textures/marine_shirt.tga b/textures/marine_shirt.tga
new file mode 100644 (file)
index 0000000..a31f3d0
Binary files /dev/null and b/textures/marine_shirt.tga differ
diff --git a/textures/meat_pants.tga b/textures/meat_pants.tga
new file mode 100644 (file)
index 0000000..1568b09
Binary files /dev/null and b/textures/meat_pants.tga differ
diff --git a/textures/spider/spidertex.tga b/textures/spider/spidertex.tga
new file mode 100644 (file)
index 0000000..e9f8df1
Binary files /dev/null and b/textures/spider/spidertex.tga differ
diff --git a/za.cfg b/za.cfg
new file mode 100644 (file)
index 0000000..1e6682e
--- /dev/null
+++ b/za.cfg
@@ -0,0 +1,2 @@
+set g_za 0 "Enable zombie apocalypse mutator"
+set g_za_monster_count 20
\ No newline at end of file