/* * Copyright (C) 1997-2001 Id Software, Inc. * * 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. * */ /* Modifications Copyright 2003-2004 Bytonic Software Copyright 2010 Google Inc. */ package com.googlecode.gwtquake.shared.game; import com.googlecode.gwtquake.shared.client.ClientMonsterMethods; import com.googlecode.gwtquake.shared.common.CM; import com.googlecode.gwtquake.shared.common.Com; import com.googlecode.gwtquake.shared.common.Constants; import com.googlecode.gwtquake.shared.common.Globals; import com.googlecode.gwtquake.shared.game.adapters.EntityThinkAdapter; import com.googlecode.gwtquake.shared.game.adapters.EntityUseAdapter; import com.googlecode.gwtquake.shared.server.ServerGame; import com.googlecode.gwtquake.shared.server.ServerInit; import com.googlecode.gwtquake.shared.server.World; import com.googlecode.gwtquake.shared.util.Lib; import com.googlecode.gwtquake.shared.util.Math3D; public class GameUtil { public static void checkClassname(Entity ent) { if (ent.classname == null) { Com.Printf("edict with classname = null: " + ent.index); } } /** * Use the targets. * * The global "activator" should be set to the entity that initiated the * firing. * * If self.delay is set, a DelayedUse entity will be created that will * actually do the SUB_UseTargets after that many seconds have passed. * * Centerprints any self.message to the activator. * * Search for (string)targetname in all entities that match * (string)self.target and call their .use function */ public static void G_UseTargets(Entity ent, Entity activator) { Entity t; checkClassname(ent); // check for a delay if (ent.delay != 0) { // create a temp object to fire at a later time t = G_Spawn(); t.classname = "DelayedUse"; t.nextthink = GameBase.level.time + ent.delay; t.think = Think_Delay; t.activator = activator; if (activator == null) ServerGame.PF_dprintf("Think_Delay with no activator\n"); t.message = ent.message; t.target = ent.target; t.killtarget = ent.killtarget; return; } // print the message if ((ent.message != null) && (activator.svflags & Constants.SVF_MONSTER) == 0) { ServerGame.PF_centerprintf(activator, "" + ent.message); if (ent.noise_index != 0) ServerGame.PF_StartSound(activator, Constants.CHAN_AUTO, ent.noise_index, (float) 1, (float) Constants.ATTN_NORM, (float) 0); else ServerGame.PF_StartSound(activator, Constants.CHAN_AUTO, ServerInit.SV_SoundIndex("misc/talk1.wav"), (float) 1, (float) Constants.ATTN_NORM, (float) 0); } // kill killtargets EntityIterator edit = null; if (ent.killtarget != null) { while ((edit = GameBase.G_Find(edit, GameBase.findByTarget, ent.killtarget)) != null) { t = edit.o; G_FreeEdict(t); if (!ent.inuse) { ServerGame.PF_dprintf("entity was removed while using killtargets\n"); return; } } } // fire targets if (ent.target != null) { edit = null; while ((edit = GameBase.G_Find(edit, GameBase.findByTarget, ent.target)) != null) { t = edit.o; // doors fire area portals in a specific way if (Lib.Q_stricmp("func_areaportal", t.classname) == 0 && (Lib.Q_stricmp("func_door", ent.classname) == 0 || Lib .Q_stricmp("func_door_rotating", ent.classname) == 0)) continue; if (t == ent) { ServerGame.PF_dprintf("WARNING: Entity used itself.\n"); } else { if (t.use != null) t.use.use(t, ent, activator); } if (!ent.inuse) { ServerGame.PF_dprintf("entity was removed while using targets\n"); return; } } } } public static void G_InitEdict(Entity e, int i) { e.inuse = true; e.classname = "noclass"; e.gravity = 1.0f; //e.s.number= e - g_edicts; e.s = new EntityState(e); e.s.number = i; e.index = i; } /** * Either finds a free edict, or allocates a new one. Try to avoid reusing * an entity that was recently freed, because it can cause the client to * think the entity morphed into something else instead of being removed and * recreated, which can cause interpolated angles and bad trails. */ public static Entity G_Spawn() { int i; Entity e = null; for (i = (int) GameBase.maxclients.value + 1; i < GameBase.num_edicts; i++) { e = GameBase.g_edicts[i]; // the first couple seconds of server time can involve a lot of // freeing and allocating, so relax the replacement policy if (!e.inuse && (e.freetime < 2 || GameBase.level.time - e.freetime > 0.5)) { e = GameBase.g_edicts[i] = new Entity(i); G_InitEdict(e, i); return e; } } if (i == GameBase.game.maxentities) Com.Error(Constants.ERR_FATAL, "ED_Alloc: no free edicts"); e = GameBase.g_edicts[i] = new Entity(i); GameBase.num_edicts++; G_InitEdict(e, i); return e; } /** * Marks the edict as free */ public static void G_FreeEdict(Entity ed) { World.SV_UnlinkEdict(ed); // unlink from world //if ((ed - g_edicts) <= (maxclients.value + BODY_QUEUE_SIZE)) if (ed.index <= (GameBase.maxclients.value + Constants.BODY_QUEUE_SIZE)) { // gi.dprintf("tried to free special edict\n"); return; } GameBase.g_edicts[ed.index] = new Entity(ed.index); ed.classname = "freed"; ed.freetime = GameBase.level.time; ed.inuse = false; } /** * Call after linking a new trigger in during gameplay to force all entities * it covers to immediately touch it. */ public static void G_ClearEdict(Entity ent) { int i = ent.index; GameBase.g_edicts[i] = new Entity(i); } /** * Kills all entities that would touch the proposed new positioning of ent. * Ent should be unlinked before calling this! */ public static boolean KillBox(Entity ent) { Trace tr; while (true) { tr = World.SV_Trace(ent.s.origin, ent.mins, ent.maxs, ent.s.origin, null, Constants.MASK_PLAYERSOLID); if (tr.ent == null || tr.ent == GameBase.g_edicts[0]) break; // nail it GameCombat.T_Damage(tr.ent, ent, ent, Globals.vec3_origin, ent.s.origin, Globals.vec3_origin, 100000, 0, Constants.DAMAGE_NO_PROTECTION, Constants.MOD_TELEFRAG); // if we didn't kill it, fail if (tr.ent.solid != 0) return false; } return true; // all clear } /** * Returns true, if two edicts are on the same team. */ public static boolean OnSameTeam(Entity ent1, Entity ent2) { if (0 == ((int) (GameBase.dmflags.value) & (Constants.DF_MODELTEAMS | Constants.DF_SKINTEAMS))) return false; if (ClientTeam(ent1).equals(ClientTeam(ent2))) return true; return false; } /** * Returns the team string of an entity * with respect to rteam_by_model and team_by_skin. */ static String ClientTeam(Entity ent) { String value; if (ent.client == null) return ""; value = Info.Info_ValueForKey(ent.client.pers.userinfo, "skin"); int p = value.indexOf("/"); if (p == -1) return value; if (((int) (GameBase.dmflags.value) & Constants.DF_MODELTEAMS) != 0) { return value.substring(0, p); } return value.substring(p + 1, value.length()); } static void ValidateSelectedItem(Entity ent) { GameClient cl; cl = ent.client; if (cl.pers.inventory[cl.pers.selected_item] != 0) return; // valid GameItems.SelectNextItem(ent, -1); } /** * Returns the range catagorization of an entity reletive to self 0 melee * range, will become hostile even if back is turned 1 visibility and * infront, or visibility and show hostile 2 infront and show hostile 3 only * triggered by damage. */ public static int range(Entity self, Entity other) { float[] v = { 0, 0, 0 }; float len; Math3D.VectorSubtract(self.s.origin, other.s.origin, v); len = Math3D.VectorLength(v); if (len < Constants.MELEE_DISTANCE) return Constants.RANGE_MELEE; if (len < 500) return Constants.RANGE_NEAR; if (len < 1000) return Constants.RANGE_MID; return Constants.RANGE_FAR; } static void AttackFinished(Entity self, float time) { self.monsterinfo.attack_finished = GameBase.level.time + time; } /** * Returns true if the entity is in front (in sight) of self */ public static boolean infront(Entity self, Entity other) { float[] vec = { 0, 0, 0 }; float dot; float[] forward = { 0, 0, 0 }; Math3D.AngleVectors(self.s.angles, forward, null, null); Math3D.VectorSubtract(other.s.origin, self.s.origin, vec); Math3D.VectorNormalize(vec); dot = Math3D.DotProduct(vec, forward); if (dot > 0.3) return true; return false; } /** * Returns 1 if the entity is visible to self, even if not infront(). */ public static boolean visible(Entity self, Entity other) { float[] spot1 = { 0, 0, 0 }; float[] spot2 = { 0, 0, 0 }; Trace trace; Math3D.VectorCopy(self.s.origin, spot1); spot1[2] += self.viewheight; Math3D.VectorCopy(other.s.origin, spot2); spot2[2] += other.viewheight; trace = World.SV_Trace(spot1, Globals.vec3_origin, Globals.vec3_origin, spot2, self, Constants.MASK_OPAQUE); if (trace.fraction == 1.0) return true; return false; } /** * Finds a target. * * Self is currently not attacking anything, so try to find a target * * Returns TRUE if an enemy was sighted * * When a player fires a missile, the point of impact becomes a fakeplayer * so that monsters that see the impact will respond as if they had seen the * player. * * To avoid spending too much time, only a single client (or fakeclient) is * checked each frame. This means multi player games will have slightly * slower noticing monsters. */ static boolean FindTarget(Entity self) { Entity client; boolean heardit; int r; if ((self.monsterinfo.aiflags & Constants.AI_GOOD_GUY) != 0) { if (self.goalentity != null && self.goalentity.inuse && self.goalentity.classname != null) { if (self.goalentity.classname.equals("target_actor")) return false; } //FIXME look for monsters? return false; } // if we're going to a combat point, just proceed if ((self.monsterinfo.aiflags & Constants.AI_COMBAT_POINT) != 0) return false; // if the first spawnflag bit is set, the monster will only wake up on // really seeing the player, not another monster getting angry or // hearing something // revised behavior so they will wake up if they "see" a player make a // noise but not weapon impact/explosion noises heardit = false; if ((GameBase.level.sight_entity_framenum >= (GameBase.level.framenum - 1)) && 0 == (self.spawnflags & 1)) { client = GameBase.level.sight_entity; if (client.enemy == self.enemy) return false; } else if (GameBase.level.sound_entity_framenum >= (GameBase.level.framenum - 1)) { client = GameBase.level.sound_entity; heardit = true; } else if (null != (self.enemy) && (GameBase.level.sound2_entity_framenum >= (GameBase.level.framenum - 1)) && 0 != (self.spawnflags & 1)) { client = GameBase.level.sound2_entity; heardit = true; } else { client = GameBase.level.sight_client; if (client == null) return false; // no clients to get mad at } // if the entity went away, forget it if (!client.inuse) return false; if (client.client != null) { if ((client.flags & Constants.FL_NOTARGET) != 0) return false; } else if ((client.svflags & Constants.SVF_MONSTER) != 0) { if (client.enemy == null) return false; if ((client.enemy.flags & Constants.FL_NOTARGET) != 0) return false; } else if (heardit) { if ((client.owner.flags & Constants.FL_NOTARGET) != 0) return false; } else return false; if (!heardit) { r = range(self, client); if (r == Constants.RANGE_FAR) return false; // this is where we would check invisibility // is client in an spot too dark to be seen? if (client.light_level <= 5) return false; if (!visible(self, client)) return false; if (r == Constants.RANGE_NEAR) { if (client.show_hostile < GameBase.level.time && !infront(self, client)) return false; } else if (r == Constants.RANGE_MID) { if (!infront(self, client)) return false; } if (client == self.enemy) return true; // JDC false; self.enemy = client; if (!self.enemy.classname.equals("player_noise")) { self.monsterinfo.aiflags &= ~Constants.AI_SOUND_TARGET; if (self.enemy.client == null) { self.enemy = self.enemy.enemy; if (self.enemy.client == null) { self.enemy = null; return false; } } } } else { // heard it float[] temp = { 0, 0, 0 }; if ((self.spawnflags & 1) != 0) { if (!visible(self, client)) return false; } else { if (!ServerGame.PF_inPHS(self.s.origin, client.s.origin)) return false; } Math3D.VectorSubtract(client.s.origin, self.s.origin, temp); if (Math3D.VectorLength(temp) > 1000) // too far to hear return false; // check area portals - if they are different and not connected then // we can't hear it if (client.areanum != self.areanum) if (!CM.CM_AreasConnected(self.areanum, client.areanum)) return false; self.ideal_yaw = Math3D.vectoyaw(temp); ClientMonsterMethods.M_ChangeYaw(self); // hunt the sound for a bit; hopefully find the real player self.monsterinfo.aiflags |= Constants.AI_SOUND_TARGET; if (client == self.enemy) return true; // JDC false; self.enemy = client; } // got one FoundTarget(self); if (0 == (self.monsterinfo.aiflags & Constants.AI_SOUND_TARGET) && (self.monsterinfo.sight != null)) self.monsterinfo.sight.interact(self, self.enemy); return true; } public static void FoundTarget(Entity self) { // let other monsters see this monster for a while if (self.enemy.client != null) { GameBase.level.sight_entity = self; GameBase.level.sight_entity_framenum = GameBase.level.framenum; GameBase.level.sight_entity.light_level = 128; } self.show_hostile = (int) GameBase.level.time + 1; // wake up other // monsters Math3D.VectorCopy(self.enemy.s.origin, self.monsterinfo.last_sighting); self.monsterinfo.trail_time = GameBase.level.time; if (self.combattarget == null) { GameAI.HuntTarget(self); return; } self.goalentity = self.movetarget = GameBase .G_PickTarget(self.combattarget); if (self.movetarget == null) { self.goalentity = self.movetarget = self.enemy; GameAI.HuntTarget(self); ServerGame.PF_dprintf("" + self.classname + "at " + Lib.vtos(self.s.origin) + ", combattarget " + self.combattarget + " not found\n"); return; } // clear out our combattarget, these are a one shot deal self.combattarget = null; self.monsterinfo.aiflags |= Constants.AI_COMBAT_POINT; // clear the targetname, that point is ours! self.movetarget.targetname = null; self.monsterinfo.pausetime = 0; // run for it self.monsterinfo.run.think(self); } public static EntityThinkAdapter Think_Delay = new EntityThinkAdapter() { public String getID() { return "Think_Delay"; } public boolean think(Entity ent) { G_UseTargets(ent, ent.activator); G_FreeEdict(ent); return true; } }; public static EntityThinkAdapter G_FreeEdictA = new EntityThinkAdapter() { public String getID() { return "G_FreeEdictA"; } public boolean think(Entity ent) { G_FreeEdict(ent); return false; } }; static EntityThinkAdapter MegaHealth_think = new EntityThinkAdapter() { public String getID() { return "MegaHealth_think"; } public boolean think(Entity self) { if (self.owner.health > self.owner.max_health) { self.nextthink = GameBase.level.time + 1; self.owner.health -= 1; return false; } if (!((self.spawnflags & Constants.DROPPED_ITEM) != 0) && (GameBase.deathmatch.value != 0)) GameItems.SetRespawn(self, 20); else G_FreeEdict(self); return false; } }; public static EntityThinkAdapter M_CheckAttack = new EntityThinkAdapter() { public String getID() { return "M_CheckAttack"; } public boolean think(Entity self) { float[] spot1 = { 0, 0, 0 }; float[] spot2 = { 0, 0, 0 }; float chance; Trace tr; if (self.enemy.health > 0) { // see if any entities are in the way of the shot Math3D.VectorCopy(self.s.origin, spot1); spot1[2] += self.viewheight; Math3D.VectorCopy(self.enemy.s.origin, spot2); spot2[2] += self.enemy.viewheight; tr = World.SV_Trace(spot1, null, null, spot2, self, Constants.CONTENTS_SOLID | Constants.CONTENTS_MONSTER | Constants.CONTENTS_SLIME | Constants.CONTENTS_LAVA | Constants.CONTENTS_WINDOW); // do we have a clear shot? if (tr.ent != self.enemy) return false; } // melee attack if (GameAI.enemy_range == Constants.RANGE_MELEE) { // don't always melee in easy mode if (GameBase.skill.value == 0 && (Lib.rand() & 3) != 0) return false; if (self.monsterinfo.melee != null) self.monsterinfo.attack_state = Constants.AS_MELEE; else self.monsterinfo.attack_state = Constants.AS_MISSILE; return true; } // missile attack if (self.monsterinfo.attack == null) return false; if (GameBase.level.time < self.monsterinfo.attack_finished) return false; if (GameAI.enemy_range == Constants.RANGE_FAR) return false; if ((self.monsterinfo.aiflags & Constants.AI_STAND_GROUND) != 0) { chance = 0.4f; } else if (GameAI.enemy_range == Constants.RANGE_MELEE) { chance = 0.2f; } else if (GameAI.enemy_range == Constants.RANGE_NEAR) { chance = 0.1f; } else if (GameAI.enemy_range == Constants.RANGE_MID) { chance = 0.02f; } else { return false; } if (GameBase.skill.value == 0) chance *= 0.5; else if (GameBase.skill.value >= 2) chance *= 2; if (Lib.random() < chance) { self.monsterinfo.attack_state = Constants.AS_MISSILE; self.monsterinfo.attack_finished = GameBase.level.time + 2 * Lib.random(); return true; } if ((self.flags & Constants.FL_FLY) != 0) { if (Lib.random() < 0.3f) self.monsterinfo.attack_state = Constants.AS_SLIDING; else self.monsterinfo.attack_state = Constants.AS_STRAIGHT; } return false; } }; static EntityUseAdapter monster_use = new EntityUseAdapter() { public String getID() { return "monster_use"; } public void use(Entity self, Entity other, Entity activator) { if (self.enemy != null) return; if (self.health <= 0) return; if ((activator.flags & Constants.FL_NOTARGET) != 0) return; if ((null == activator.client) && 0 == (activator.monsterinfo.aiflags & Constants.AI_GOOD_GUY)) return; // delay reaction so if the monster is teleported, its sound is // still heard self.enemy = activator; FoundTarget(self); } }; }