#include "door.qh" #include "door_rotating.qh" /* Doors are similar to buttons, but can spawn a fat trigger field around them to open without a touch, and they link together to form simultanious double/quad doors. Door.owner is the master door. If there is only one door, it points to itself. If multiple doors, all will point to a single one. Door.enemy chains from the master door through all doors linked in the chain. */ /* ============================================================================= THINK FUNCTIONS ============================================================================= */ void door_go_down(entity this); void door_go_up(entity this, entity actor, entity trigger); void door_blocked(entity this, entity blocker) { bool reverse = false; if((this.spawnflags & DOOR_CRUSH) && !Q3COMPAT_COMMON #ifdef SVQC && (blocker.takedamage != DAMAGE_NO) #elif defined(CSQC) && !IS_DEAD(blocker) #endif ) { // KIll Kill Kill!! #ifdef SVQC Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0'); #endif } else { #ifdef SVQC if (this.dmg && blocker.takedamage != DAMAGE_NO) // Shall we bite? Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0'); #endif // don't change direction for dead or dying stuff if (!IS_DEAD(blocker) #ifdef SVQC && blocker.takedamage != DAMAGE_NO #endif && this.wait >= 0 && !(Q3COMPAT_COMMON && (this.spawnflags & Q3_DOOR_CRUSHER)) ) { if (this.state == STATE_DOWN) { if (this.classname == "door") door_go_up(this, NULL, NULL); else door_rotating_go_up(this, blocker); } else { if (this.classname == "door") door_go_down(this); else door_rotating_go_down(this); } reverse = true; } #ifdef SVQC else { //gib dying stuff just to make sure if (this.dmg && blocker.takedamage != DAMAGE_NO && IS_DEAD(blocker)) // Shall we bite? Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0'); } #endif } // if we didn't change direction and are using a non-linear movement controller, we must pause it if (!reverse && this.classname == "door" && this.move_controller) SUB_CalcMovePause(this); } void door_hit_top(entity this) { if (this.noise1 != "") _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM); this.state = STATE_TOP; if (this.spawnflags & DOOR_TOGGLE) return; // don't come down automatically if (this.classname == "door") { setthink(this, door_go_down); } else { setthink(this, door_rotating_go_down); } this.nextthink = this.ltime + this.wait; } void door_hit_bottom(entity this) { if (this.noise1 != "") _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM); this.state = STATE_BOTTOM; } void door_go_down(entity this) { if (this.noise2 != "") _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM); if (this.max_health) { this.takedamage = DAMAGE_YES; SetResourceExplicit(this, RES_HEALTH, this.max_health); } this.state = STATE_DOWN; SUB_CalcMove (this, this.pos1, TSPEED_LINEAR, this.speed, door_hit_bottom); } void door_go_up(entity this, entity actor, entity trigger) { if (this.state == STATE_UP) return; // already going up if (this.state == STATE_TOP) { // reset top wait time this.nextthink = this.ltime + this.wait; return; } if (this.noise2 != "") _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM); this.state = STATE_UP; SUB_CalcMove (this, this.pos2, TSPEED_LINEAR, this.speed, door_hit_top); string oldmessage; oldmessage = this.message; this.message = ""; SUB_UseTargets(this, actor, trigger); this.message = oldmessage; } /* ============================================================================= ACTIVATION FUNCTIONS ============================================================================= */ bool door_check_keys(entity door, entity player) { if(door.owner) door = door.owner; // no key needed if(!door.itemkeys) return true; // this door require a key // only a player can have a key if(!IS_PLAYER(player)) return false; entity store = player; #ifdef SVQC store = PS(player); #endif int valid = (door.itemkeys & store.itemkeys); door.itemkeys &= ~valid; // only some of the needed keys were given if(!door.itemkeys) { #ifdef SVQC play2(player, door.noise); Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_UNLOCKED); #endif return true; } if(!valid) { #ifdef SVQC if(player.key_door_messagetime <= time) { play2(player, door.noise3); Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_LOCKED_NEED, item_keys_keylist(door.itemkeys)); player.key_door_messagetime = time + 2; } #endif return false; } // door needs keys the player doesn't have #ifdef SVQC if(player.key_door_messagetime <= time) { play2(player, door.noise3); Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_LOCKED_ALSONEED, item_keys_keylist(door.itemkeys)); player.key_door_messagetime = time + 2; } #endif return false; } void door_use(entity this, entity actor, entity trigger) { //dprint("door_use (model: ");dprint(this.model);dprint(")\n"); if (!this.owner) return; this = this.owner; if (this.spawnflags & DOOR_TOGGLE) { if (this.state == STATE_UP || this.state == STATE_TOP) { entity e = this; do { if (e.classname == "door") { door_go_down(e); } else { door_rotating_go_down(e); } e = e.enemy; } while ((e != this) && (e != NULL)); return; } } // trigger all paired doors entity e = this; do { if (e.classname == "door") { door_go_up(e, actor, trigger); } else { // if the BIDIR spawnflag (==2) is set and the trigger has set trigger_reverse, reverse the opening direction if ((e.spawnflags & DOOR_ROTATING_BIDIR) && trigger.trigger_reverse!=0 && e.lip != 666 && e.state == STATE_BOTTOM) { e.lip = 666; // e.lip is used to remember reverse opening direction for door_rotating e.pos2 = '0 0 0' - e.pos2; } // if BIDIR_IN_DOWN (==8) is set, prevent the door from reoping during closing if it is triggered from the wrong side if (!((e.spawnflags & DOOR_ROTATING_BIDIR) && (e.spawnflags & DOOR_ROTATING_BIDIR_IN_DOWN) && e.state == STATE_DOWN && (((e.lip == 666) && (trigger.trigger_reverse == 0)) || ((e.lip != 666) && (trigger.trigger_reverse != 0))))) { door_rotating_go_up(e, trigger); } } e = e.enemy; } while ((e != this) && (e != NULL)); } void door_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force) { if(this.spawnflags & NOSPLASH) if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH)) return; TakeResource(this, RES_HEALTH, damage); if (this.itemkeys) { // don't allow opening doors through damage if keys are required return; } if (GetResource(this, RES_HEALTH) <= 0) { SetResourceExplicit(this.owner, RES_HEALTH, this.owner.max_health); this.owner.takedamage = DAMAGE_NO; // will be reset upon return door_use(this.owner, attacker, NULL); } } .float door_finished; /* ================ door_touch Prints messages ================ */ void door_touch(entity this, entity toucher) { if (!IS_PLAYER(toucher)) return; if (this.owner.door_finished > time) return; this.owner.door_finished = time + 2; #ifdef SVQC if (!(this.owner.dmg) && (this.owner.message != "")) { if (IS_CLIENT(toucher)) centerprint(toucher, this.owner.message); play2(toucher, this.owner.noise); } #endif } void door_generic_plat_blocked(entity this, entity blocker) { if((this.spawnflags & DOOR_CRUSH) && (blocker.takedamage != DAMAGE_NO)) { // Kill Kill Kill!! #ifdef SVQC Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0'); #endif } else { #ifdef SVQC if((this.dmg) && (blocker.takedamage == DAMAGE_YES)) // Shall we bite? Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0'); #endif //Dont chamge direction for dead or dying stuff if(IS_DEAD(blocker) && (blocker.takedamage == DAMAGE_NO)) { if (this.wait >= 0) { if (this.state == STATE_DOWN) door_rotating_go_up (this, blocker); else door_rotating_go_down (this); } } #ifdef SVQC else { //gib dying stuff just to make sure if((this.dmg) && (blocker.takedamage != DAMAGE_NO)) // Shall we bite? Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0'); } #endif } } /* ========================================= door trigger Spawned if a door lacks a real activator ========================================= */ void door_trigger_touch(entity this, entity toucher) { if (GetResource(toucher, RES_HEALTH) < 1) #ifdef SVQC if (!((toucher.iscreature || (toucher.flags & FL_PROJECTILE)) && !IS_DEAD(toucher))) #elif defined(CSQC) if(!((IS_CLIENT(toucher) || toucher.classname == "ENT_CLIENT_PROJECTILE") && !IS_DEAD(toucher))) #endif return; if (this.owner.state == STATE_UP) return; // check if door is locked if (!door_check_keys(this, toucher)) return; if (this.owner.state == STATE_TOP) { if (this.owner.nextthink < this.owner.ltime + this.owner.wait) { entity e = this.owner; do { e.nextthink = e.ltime + e.wait; e = e.enemy; } while (e != this.owner); } return; } door_use(this.owner, toucher, NULL); } void door_spawnfield(entity this, vector fmins, vector fmaxs) { entity trigger; vector t1 = fmins, t2 = fmaxs; trigger = new(doortriggerfield); set_movetype(trigger, MOVETYPE_NONE); trigger.solid = SOLID_TRIGGER; trigger.owner = this; #ifdef SVQC settouch(trigger, door_trigger_touch); #endif setsize (trigger, t1 - '60 60 8', t2 + '60 60 8'); } /* ============= LinkDoors ============= */ entity LinkDoors_nextent(entity cur, entity near, entity pass) { while ((cur = find(cur, classname, pass.classname)) && ((!Q3COMPAT_COMMON && (cur.spawnflags & DOOR_DONT_LINK)) || cur.enemy)) { } return cur; } bool LinkDoors_isconnected(entity e1, entity e2, entity pass) { if(Q3COMPAT_COMMON) return e1.team == e2.team; float DELTA = 4; if((e1.absmin_x > e2.absmax_x + DELTA) || (e1.absmin_y > e2.absmax_y + DELTA) || (e1.absmin_z > e2.absmax_z + DELTA) || (e2.absmin_x > e1.absmax_x + DELTA) || (e2.absmin_y > e1.absmax_y + DELTA) || (e2.absmin_z > e1.absmax_z + DELTA) ) { return false; } return true; } #ifdef SVQC void door_link(); #endif void LinkDoors(entity this) { entity t; vector cmins, cmaxs; #ifdef SVQC door_link(); #endif if (this.enemy) return; // already linked by another door // Q3 door linking is done for teamed doors only and is not affected by spawnflags or bmodel proximity if ((!Q3COMPAT_COMMON && (this.spawnflags & DOOR_DONT_LINK)) || (Q3COMPAT_COMMON && !this.team)) { this.owner = this.enemy = this; if (GetResource(this, RES_HEALTH)) return; if(this.targetname && this.targetname != "") return; if (this.items) return; door_spawnfield(this, this.absmin, this.absmax); return; // don't want to link this door } FindConnectedComponent(this, enemy, LinkDoors_nextent, LinkDoors_isconnected, this); // set owner, and make a loop of the chain LOG_TRACE("LinkDoors: linking doors:"); for(t = this; ; t = t.enemy) { LOG_TRACE(" ", etos(t)); t.owner = this; if(t.enemy == NULL) { t.enemy = this; break; } } LOG_TRACE(""); // collect health, targetname, message, size cmins = this.absmin; cmaxs = this.absmax; for(t = this; ; t = t.enemy) { if(GetResource(t, RES_HEALTH) && !GetResource(this, RES_HEALTH)) SetResourceExplicit(this, RES_HEALTH, GetResource(t, RES_HEALTH)); if((t.targetname != "") && (this.targetname == "")) this.targetname = t.targetname; if((t.message != "") && (this.message == "")) this.message = t.message; if (t.absmin_x < cmins_x) cmins_x = t.absmin_x; if (t.absmin_y < cmins_y) cmins_y = t.absmin_y; if (t.absmin_z < cmins_z) cmins_z = t.absmin_z; if (t.absmax_x > cmaxs_x) cmaxs_x = t.absmax_x; if (t.absmax_y > cmaxs_y) cmaxs_y = t.absmax_y; if (t.absmax_z > cmaxs_z) cmaxs_z = t.absmax_z; if(t.enemy == this) break; } // distribute health, targetname, message for(t = this; t; t = t.enemy) { SetResourceExplicit(t, RES_HEALTH, GetResource(this, RES_HEALTH)); t.targetname = this.targetname; t.message = this.message; if(t.enemy == this) break; } // shootable, or triggered doors just needed the owner/enemy links, // they don't spawn a field if (GetResource(this, RES_HEALTH)) return; if(this.targetname && this.targetname != "") return; if (this.items) return; door_spawnfield(this, cmins, cmaxs); } REGISTER_NET_LINKED(ENT_CLIENT_DOOR) #ifdef SVQC /*QUAKED spawnfunc_func_door (0 .5 .8) ? START_OPEN x DOOR_DONT_LINK GOLD_KEY SILVER_KEY TOGGLE if two doors touch, they are assumed to be connected and operate as a unit. TOGGLE causes the door to wait in both the start and end states for a trigger event. START_OPEN causes the door to move to its destination when spawned, and operate in reverse. It is used to temporarily or permanently close off an area when triggered (not useful for touch or takedamage doors). GOLD_KEY causes the door to open only if the activator holds a gold key. SILVER_KEY causes the door to open only if the activator holds a silver key. "message" is printed when the door is touched if it is a trigger door and it hasn't been fired yet "angle" determines the opening direction "targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door. "health" if set, door must be shot open "speed" movement speed (100 default) "wait" wait before returning (3 default, -1 = never return) "lip" lip remaining at end of move (8 default) "dmg" damage to inflict when blocked (0 default) "sounds" 0) no sound 1) stone 2) base 3) stone chain 4) screechy metal FIXME: only one sound set available at the time being */ float door_send(entity this, entity to, float sf) { WriteHeader(MSG_ENTITY, ENT_CLIENT_DOOR); WriteByte(MSG_ENTITY, sf); if(sf & SF_TRIGGER_INIT) { WriteString(MSG_ENTITY, this.classname); WriteByte(MSG_ENTITY, this.spawnflags); WriteString(MSG_ENTITY, this.model); trigger_common_write(this, true); WriteVector(MSG_ENTITY, this.pos1); WriteVector(MSG_ENTITY, this.pos2); WriteVector(MSG_ENTITY, this.size); WriteShort(MSG_ENTITY, this.wait); WriteShort(MSG_ENTITY, this.speed); WriteByte(MSG_ENTITY, this.lip); WriteByte(MSG_ENTITY, this.state); WriteCoord(MSG_ENTITY, this.ltime); } if(sf & SF_TRIGGER_RESET) { // client makes use of this, we do not } if(sf & SF_TRIGGER_UPDATE) { WriteVector(MSG_ENTITY, this.origin); WriteVector(MSG_ENTITY, this.pos1); WriteVector(MSG_ENTITY, this.pos2); } return true; } void door_link() { //Net_LinkEntity(this, false, 0, door_send); } void door_init_keys(entity this) { // Quake 1 and QL keys compatibility if (this.spawnflags & SPAWNFLAGS_GOLD_KEY) this.itemkeys |= BIT(0); if (this.spawnflags & SPAWNFLAGS_SILVER_KEY) this.itemkeys |= BIT(1); } #endif void door_init_startopen(entity this) { setorigin(this, this.pos2); this.pos2 = this.pos1; this.pos1 = this.origin; #ifdef SVQC this.SendFlags |= SF_TRIGGER_UPDATE; #endif } void door_reset(entity this) { setorigin(this, this.pos1); this.velocity = '0 0 0'; this.state = STATE_BOTTOM; setthink(this, func_null); this.nextthink = 0; #ifdef SVQC this.SendFlags |= SF_TRIGGER_RESET; door_init_keys(this); #endif } #ifdef SVQC // common code for func_door and func_door_rotating spawnfuncs void door_init_shared(entity this) { this.max_health = GetResource(this, RES_HEALTH); // unlock sound if(this.noise == "") { this.noise = "misc/talk.wav"; } // door still locked sound if(this.noise3 == "") { this.noise3 = "misc/talk.wav"; } precache_sound(this.noise); precache_sound(this.noise3); if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message == "")) { this.message = "was squished"; } if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message2 == "")) { this.message2 = "was squished by"; } // TODO: other soundpacks if (this.sounds > 0 || q3compat) { // Doors in Q3 always have sounds (they're hard coded) this.noise2 = "plats/medplat1.wav"; this.noise1 = "plats/medplat2.wav"; } if (q3compat) { // CPMA adds these fields for overriding the Q3 default sounds string s = GetField_fullspawndata(this, "sound_start", true); string e = GetField_fullspawndata(this, "sound_end", true); // Quake Live adds these ones, because of course it had to be different from CPMA if (!s) s = GetField_fullspawndata(this, "startsound", true); if (!e) e = GetField_fullspawndata(this, "endsound", true); if (s) this.noise2 = strzone(s); else { // PK3s supporting Q3A sometimes include custom sounds at Q3 default paths s = "sound/movers/doors/dr1_strt.wav"; if (FindFileInMapPack(s)) this.noise2 = s; } if (e) this.noise1 = strzone(e); else { e = "sound/movers/doors/dr1_end.wav"; if (FindFileInMapPack(e)) this.noise1 = e; } } // sound when door stops moving if(this.noise1 && this.noise1 != "") { precache_sound(this.noise1); } // sound when door is moving if(this.noise2 && this.noise2 != "") { precache_sound(this.noise2); } if(autocvar_sv_doors_always_open) { this.wait = -1; } else if (!this.wait) { this.wait = q3compat ? 2 : 3; } if (!this.lip) { this.lip = 8; } this.state = STATE_BOTTOM; if (GetResource(this, RES_HEALTH) || (q3compat && this.targetname == "")) { //this.canteamdamage = true; // TODO this.takedamage = DAMAGE_YES; this.event_damage = door_damage; } if (this.items) { this.wait = -1; } } // spawnflags require key (for now only func_door) spawnfunc(func_door) { door_init_keys(this); SetMovedir(this); if (!InitMovingBrushTrigger(this)) return; this.effects |= EF_LOWPRECISION; this.classname = "door"; setblocked(this, door_blocked); this.use = door_use; if(this.spawnflags & DOOR_NONSOLID) this.solid = SOLID_NOT; // DOOR_START_OPEN is to allow an entity to be lighted in the closed position // but spawn in the open position // the tuba door on xoylent requires the delayed init if (this.spawnflags & DOOR_START_OPEN) InitializeEntity(this, door_init_startopen, INITPRIO_SETLOCATION); door_init_shared(this); this.pos1 = this.origin; vector absmovedir; absmovedir.x = fabs(this.movedir.x); absmovedir.y = fabs(this.movedir.y); absmovedir.z = fabs(this.movedir.z); this.pos2 = this.pos1 + this.movedir * (absmovedir * this.size - this.lip); if(autocvar_sv_doors_always_open) { this.speed = max(750, this.speed); } else if (!this.speed) { if (q3compat) this.speed = 400; else this.speed = 100; } if (q3compat) { if (!this.dmg) this.dmg = 2; if (!this.team) { string t = GetField_fullspawndata(this, "team", false); // bones_was_here: same hack as used to support teamed items on Q3 maps if (t) this.team = crc16(false, t); } } settouch(this, door_touch); // LinkDoors can't be done until all of the doors have been spawned, so // the sizes can be detected properly. InitializeEntity(this, LinkDoors, INITPRIO_LINKDOORS); this.reset = door_reset; } #elif defined(CSQC) NET_HANDLE(ENT_CLIENT_DOOR, bool isnew) { int sf = ReadByte(); if(sf & SF_TRIGGER_INIT) { this.classname = strzone(ReadString()); this.spawnflags = ReadByte(); this.mdl = strzone(ReadString()); _setmodel(this, this.mdl); trigger_common_read(this, true); this.pos1 = ReadVector(); this.pos2 = ReadVector(); this.size = ReadVector(); this.wait = ReadShort(); this.speed = ReadShort(); this.lip = ReadByte(); this.state = ReadByte(); this.ltime = ReadCoord(); this.solid = SOLID_BSP; set_movetype(this, MOVETYPE_PUSH); this.use = door_use; LinkDoors(this); if(this.spawnflags & DOOR_START_OPEN) door_init_startopen(this); this.move_time = time; set_movetype(this, MOVETYPE_PUSH); } if(sf & SF_TRIGGER_RESET) { door_reset(this); } if(sf & SF_TRIGGER_UPDATE) { this.origin = ReadVector(); setorigin(this, this.origin); this.pos1 = ReadVector(); this.pos2 = ReadVector(); } return true; } #endif