/* 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); void door_rotating_go_down(entity this); void door_rotating_go_up(entity this); void door_blocked(entity this) { if((this.spawnflags & 8) #ifdef SVQC && (other.takedamage != DAMAGE_NO) #elif defined(CSQC) && !IS_DEAD(other) #endif ) { // KIll Kill Kill!! #ifdef SVQC Damage (other, this, this, 10000, DEATH_HURTTRIGGER.m_id, other.origin, '0 0 0'); #endif } else { #ifdef SVQC if((this.dmg) && (other.takedamage == DAMAGE_YES)) // Shall we bite? Damage (other, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, other.origin, '0 0 0'); #endif // don't change direction for dead or dying stuff if(IS_DEAD(other) #ifdef SVQC && (other.takedamage == DAMAGE_NO) #endif ) { if (this.wait >= 0) { if (this.state == STATE_DOWN) if (this.classname == "door") { door_go_up (this); } else { door_rotating_go_up (this); } else if (this.classname == "door") { door_go_down (this); } else { door_rotating_go_down (this); } } } #ifdef SVQC else { //gib dying stuff just to make sure if((this.dmg) && (other.takedamage != DAMAGE_NO)) // Shall we bite? Damage (other, this, this, 10000, DEATH_HURTTRIGGER.m_id, other.origin, '0 0 0'); } #endif } } 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") { SUB_THINK(this, door_go_down); } else { SUB_THINK(this, door_rotating_go_down); } this.SUB_NEXTTHINK = this.SUB_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; this.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) { if (this.state == STATE_UP) return; // already going up if (this.state == STATE_TOP) { // reset top wait time this.SUB_NEXTTHINK = this.SUB_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, NULL, NULL); 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; int valid = (door.itemkeys & player.itemkeys); door.itemkeys &= ~valid; // only some of the needed keys were given if(!door.itemkeys) { #ifdef SVQC play2(player, SND(TALK)); 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_fire(entity this, entity actor, entity trigger) { if (this.owner != this) objerror (this, "door_fire: this.owner != this"); 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); } else { // if the BIDIR spawnflag (==2) is set and the trigger has set trigger_reverse, reverse the opening direction if ((e.spawnflags & 2) && other.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 & 2) && (e.spawnflags & 8) && e.state == STATE_DOWN && (((e.lip == 666) && (other.trigger_reverse == 0)) || ((e.lip != 666) && (other.trigger_reverse != 0))))) { door_rotating_go_up(e); } } e = e.enemy; } while ((e != this) && (e != NULL)); } void door_use(entity this, entity actor, entity trigger) { //dprint("door_use (model: ");dprint(this.model);dprint(")\n"); if (this.owner) door_fire(this.owner, actor, trigger); } void door_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, vector hitloc, vector force) { if(this.spawnflags & DOOR_NOSPLASH) if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH)) return; this.health = this.health - damage; if (this.itemkeys) { // don't allow opening doors through damage if keys are required return; } if (this.health <= 0) { this.owner.health = this.owner.max_health; this.owner.takedamage = DAMAGE_NO; // wil be reset upon return door_use(this.owner, NULL, NULL); } } .float door_finished; /* ================ door_touch Prints messages ================ */ void door_touch(entity this) { if (!IS_PLAYER(other)) 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(other)) centerprint(other, this.owner.message); play2(other, this.owner.noise); } #endif } void door_generic_plat_blocked(entity this) { if((this.spawnflags & 8) && (other.takedamage != DAMAGE_NO)) { // KIll Kill Kill!! #ifdef SVQC Damage (other, this, this, 10000, DEATH_HURTTRIGGER.m_id, other.origin, '0 0 0'); #endif } else { #ifdef SVQC if((this.dmg) && (other.takedamage == DAMAGE_YES)) // Shall we bite? Damage (other, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, other.origin, '0 0 0'); #endif //Dont chamge direction for dead or dying stuff if(IS_DEAD(other) && (other.takedamage == DAMAGE_NO)) { if (this.wait >= 0) { if (this.state == STATE_DOWN) door_rotating_go_up (this); else door_rotating_go_down (this); } } #ifdef SVQC else { //gib dying stuff just to make sure if((this.dmg) && (other.takedamage != DAMAGE_NO)) // Shall we bite? Damage (other, this, this, 10000, DEATH_HURTTRIGGER.m_id, other.origin, '0 0 0'); } #endif } } void door_rotating_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 SUB_THINK(this, door_rotating_go_down); this.SUB_NEXTTHINK = this.SUB_LTIME + this.wait; } void door_rotating_hit_bottom(entity this) { if (this.noise1 != "") _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM); if (this.lip==666) // this.lip is used to remember reverse opening direction for door_rotating { this.pos2 = '0 0 0' - this.pos2; this.lip = 0; } this.state = STATE_BOTTOM; } void door_rotating_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; this.health = this.max_health; } this.state = STATE_DOWN; SUB_CalcAngleMove (this, this.pos1, TSPEED_LINEAR, this.speed, door_rotating_hit_bottom); } void door_rotating_go_up(entity this) { if (this.state == STATE_UP) return; // already going up if (this.state == STATE_TOP) { // reset top wait time this.SUB_NEXTTHINK = this.SUB_LTIME + this.wait; return; } if (this.noise2 != "") _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM); this.state = STATE_UP; SUB_CalcAngleMove (this, this.pos2, TSPEED_LINEAR, this.speed, door_rotating_hit_top); string oldmessage; oldmessage = this.message; this.message = ""; SUB_UseTargets(this, NULL, other); // TODO: is other needed here? this.message = oldmessage; } /* ========================================= door trigger Spawned if a door lacks a real activator ========================================= */ void door_trigger_touch(entity this) { if (other.health < 1) #ifdef SVQC if (!((other.iscreature || (other.flags & FL_PROJECTILE)) && !IS_DEAD(other))) #elif defined(CSQC) if(!((IS_CLIENT(other) || other.classname == "csqcprojectile") && !IS_DEAD(other))) #endif return; if (time < this.door_finished) return; // check if door is locked if (!door_check_keys(this, other)) return; this.door_finished = time + 1; door_use(this.owner, other, NULL); } void door_spawnfield(entity this, vector fmins, vector fmaxs) { entity trigger; vector t1 = fmins, t2 = fmaxs; trigger = new(doortriggerfield); trigger.movetype = 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)) && ((cur.spawnflags & 4) || cur.enemy)) { } return cur; } bool LinkDoors_isconnected(entity e1, entity e2, entity pass) { 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 if (this.spawnflags & 4) { this.owner = this.enemy = this; if (this.health) return; IFTARGETED 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("\n"); // collect health, targetname, message, size cmins = this.absmin; cmaxs = this.absmax; for(t = this; ; t = t.enemy) { if(t.health && !this.health) this.health = t.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) { t.health = this.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 (this.health) return; IFTARGETED 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 (2 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); WriteCoord(MSG_ENTITY, this.pos1_x); WriteCoord(MSG_ENTITY, this.pos1_y); WriteCoord(MSG_ENTITY, this.pos1_z); WriteCoord(MSG_ENTITY, this.pos2_x); WriteCoord(MSG_ENTITY, this.pos2_y); WriteCoord(MSG_ENTITY, this.pos2_z); WriteCoord(MSG_ENTITY, this.size_x); WriteCoord(MSG_ENTITY, this.size_y); WriteCoord(MSG_ENTITY, this.size_z); WriteShort(MSG_ENTITY, this.wait); WriteShort(MSG_ENTITY, this.speed); WriteByte(MSG_ENTITY, this.lip); WriteByte(MSG_ENTITY, this.state); WriteCoord(MSG_ENTITY, this.SUB_LTIME); } if(sf & SF_TRIGGER_RESET) { // client makes use of this, we do not } if(sf & SF_TRIGGER_UPDATE) { WriteCoord(MSG_ENTITY, this.origin_x); WriteCoord(MSG_ENTITY, this.origin_y); WriteCoord(MSG_ENTITY, this.origin_z); WriteCoord(MSG_ENTITY, this.pos1_x); WriteCoord(MSG_ENTITY, this.pos1_y); WriteCoord(MSG_ENTITY, this.pos1_z); WriteCoord(MSG_ENTITY, this.pos2_x); WriteCoord(MSG_ENTITY, this.pos2_y); WriteCoord(MSG_ENTITY, this.pos2_z); } return true; } void door_link() { // set size now, as everything is loaded //FixSize(this); //Net_LinkEntity(this, false, 0, door_send); } #endif void door_init_startopen(entity this) { SUB_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) { SUB_SETORIGIN(this, this.pos1); this.SUB_VELOCITY = '0 0 0'; this.state = STATE_BOTTOM; SUB_THINK(this, func_null); this.SUB_NEXTTHINK = 0; #ifdef SVQC this.SendFlags |= SF_TRIGGER_RESET; #endif } #ifdef SVQC // spawnflags require key (for now only func_door) spawnfunc(func_door) { // Quake 1 keys compatibility if (this.spawnflags & SPAWNFLAGS_GOLD_KEY) this.itemkeys |= ITEM_KEY_BIT(0); if (this.spawnflags & SPAWNFLAGS_SILVER_KEY) this.itemkeys |= ITEM_KEY_BIT(1); SetMovedir(this); this.max_health = this.health; if (!InitMovingBrushTrigger(this)) return; this.effects |= EF_LOWPRECISION; this.classname = "door"; if(this.noise == "") this.noise = "misc/talk.wav"; if(this.noise3 == "") this.noise3 = "misc/talk.wav"; precache_sound(this.noise); precache_sound(this.noise3); setblocked(this, door_blocked); this.use = door_use; if(this.dmg && (this.message == "")) this.message = "was squished"; if(this.dmg && (this.message2 == "")) this.message2 = "was squished by"; if (this.sounds > 0) { precache_sound ("plats/medplat1.wav"); precache_sound ("plats/medplat2.wav"); this.noise2 = "plats/medplat1.wav"; this.noise1 = "plats/medplat2.wav"; } if (!this.speed) this.speed = 100; if (!this.wait) this.wait = 3; if (!this.lip) this.lip = 8; this.pos1 = this.SUB_ORIGIN; this.pos2 = this.pos1 + this.movedir*(fabs(this.movedir*this.size) - this.lip); 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 if (this.spawnflags & DOOR_START_OPEN) InitializeEntity(this, door_init_startopen, INITPRIO_SETLOCATION); this.state = STATE_BOTTOM; if (this.health) { this.takedamage = DAMAGE_YES; this.event_damage = door_damage; } if (this.items) this.wait = -1; 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); vector v; v.x = ReadCoord(); v.y = ReadCoord(); v.z = ReadCoord(); this.pos1 = v; v.x = ReadCoord(); v.y = ReadCoord(); v.z = ReadCoord(); this.pos2 = v; v.x = ReadCoord(); v.y = ReadCoord(); v.z = ReadCoord(); this.size = v; this.wait = ReadShort(); this.speed = ReadShort(); this.lip = ReadByte(); this.state = ReadByte(); this.SUB_LTIME = ReadCoord(); this.solid = SOLID_BSP; this.movetype = MOVETYPE_PUSH; this.use = door_use; LinkDoors(this); if(this.spawnflags & DOOR_START_OPEN) door_init_startopen(this); this.move_time = time; this.move_origin = this.origin; this.move_movetype = MOVETYPE_PUSH; this.move_angles = this.angles; this.move_blocked = door_blocked; } if(sf & SF_TRIGGER_RESET) { door_reset(this); } if(sf & SF_TRIGGER_UPDATE) { this.origin_x = ReadCoord(); this.origin_y = ReadCoord(); this.origin_z = ReadCoord(); setorigin(this, this.origin); this.move_origin = this.origin; this.pos1_x = ReadCoord(); this.pos1_y = ReadCoord(); this.pos1_z = ReadCoord(); this.pos2_x = ReadCoord(); this.pos2_y = ReadCoord(); this.pos2_z = ReadCoord(); } return true; } #endif