2 #include "door_rotating.qh"
5 Doors are similar to buttons, but can spawn a fat trigger field around them
6 to open without a touch, and they link together to form simultanious
9 Door.owner is the master door. If there is only one door, it points to itself.
10 If multiple doors, all will point to a single one.
12 Door.enemy chains from the master door through all doors linked in the chain.
18 =============================================================================
22 =============================================================================
25 void door_go_down(entity this);
26 void door_go_up(entity this, entity actor, entity trigger);
28 void door_blocked(entity this, entity blocker)
31 if((this.spawnflags & DOOR_CRUSH)
34 && (blocker.takedamage != DAMAGE_NO)
41 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
47 if (this.dmg && blocker.takedamage != DAMAGE_NO) // Shall we bite?
48 Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
51 // don't change direction for dead or dying stuff
54 && blocker.takedamage != DAMAGE_NO
57 && !(Q3COMPAT_COMMON && (this.spawnflags & Q3_DOOR_CRUSHER))
60 if (this.state == STATE_DOWN)
62 if (this.classname == "door")
63 door_go_up(this, NULL, NULL);
65 door_rotating_go_up(this, blocker);
69 if (this.classname == "door")
72 door_rotating_go_down(this);
79 //gib dying stuff just to make sure
80 if (this.dmg && blocker.takedamage != DAMAGE_NO && IS_DEAD(blocker)) // Shall we bite?
81 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
85 // if we didn't change direction and are using a non-linear movement controller, we must pause it
86 if (!reverse && this.classname == "door" && this.move_controller)
87 SUB_CalcMovePause(this);
90 void door_hit_top(entity this)
92 if (this.noise1 != "")
93 _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
94 this.state = STATE_TOP;
95 if (this.spawnflags & DOOR_TOGGLE)
96 return; // don't come down automatically
97 if (this.classname == "door")
99 setthink(this, door_go_down);
102 setthink(this, door_rotating_go_down);
104 this.nextthink = this.ltime + this.wait;
107 void door_hit_bottom(entity this)
109 if (this.noise1 != "")
110 _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
111 this.state = STATE_BOTTOM;
114 void door_go_down(entity this)
116 if (this.noise2 != "")
117 _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
120 this.takedamage = DAMAGE_YES;
121 SetResourceExplicit(this, RES_HEALTH, this.max_health);
124 this.state = STATE_DOWN;
125 SUB_CalcMove (this, this.pos1, TSPEED_LINEAR, this.speed, door_hit_bottom);
128 void door_go_up(entity this, entity actor, entity trigger)
130 if (this.state == STATE_UP)
131 return; // already going up
133 if (this.state == STATE_TOP)
134 { // reset top wait time
135 this.nextthink = this.ltime + this.wait;
139 if (this.noise2 != "")
140 _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
141 this.state = STATE_UP;
142 SUB_CalcMove (this, this.pos2, TSPEED_LINEAR, this.speed, door_hit_top);
145 oldmessage = this.message;
147 SUB_UseTargets(this, actor, trigger);
148 this.message = oldmessage;
153 =============================================================================
157 =============================================================================
160 bool door_check_keys(entity door, entity player)
169 // this door require a key
170 // only a player can have a key
171 if(!IS_PLAYER(player))
174 entity store = player;
178 int valid = (door.itemkeys & store.itemkeys);
179 door.itemkeys &= ~valid; // only some of the needed keys were given
184 play2(player, door.noise);
185 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_UNLOCKED);
193 if(player.key_door_messagetime <= time)
195 play2(player, door.noise3);
196 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_LOCKED_NEED, item_keys_keylist(door.itemkeys));
197 player.key_door_messagetime = time + 2;
203 // door needs keys the player doesn't have
205 if(player.key_door_messagetime <= time)
207 play2(player, door.noise3);
208 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_LOCKED_ALSONEED, item_keys_keylist(door.itemkeys));
209 player.key_door_messagetime = time + 2;
216 void door_use(entity this, entity actor, entity trigger)
218 //dprint("door_use (model: ");dprint(this.model);dprint(")\n");
224 if (this.spawnflags & DOOR_TOGGLE)
226 if (this.state == STATE_UP || this.state == STATE_TOP)
230 if (e.classname == "door") {
233 door_rotating_go_down(e);
236 } while ((e != this) && (e != NULL));
241 // trigger all paired doors
244 if (e.classname == "door") {
245 door_go_up(e, actor, trigger);
247 // if the BIDIR spawnflag (==2) is set and the trigger has set trigger_reverse, reverse the opening direction
248 if ((e.spawnflags & DOOR_ROTATING_BIDIR) && trigger.trigger_reverse!=0 && e.lip != 666 && e.state == STATE_BOTTOM) {
249 e.lip = 666; // e.lip is used to remember reverse opening direction for door_rotating
250 e.pos2 = '0 0 0' - e.pos2;
252 // if BIDIR_IN_DOWN (==8) is set, prevent the door from reoping during closing if it is triggered from the wrong side
253 if (!((e.spawnflags & DOOR_ROTATING_BIDIR) && (e.spawnflags & DOOR_ROTATING_BIDIR_IN_DOWN) && e.state == STATE_DOWN
254 && (((e.lip == 666) && (trigger.trigger_reverse == 0)) || ((e.lip != 666) && (trigger.trigger_reverse != 0)))))
256 door_rotating_go_up(e, trigger);
260 } while ((e != this) && (e != NULL));
263 void door_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
265 if(this.spawnflags & NOSPLASH)
266 if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH))
268 TakeResource(this, RES_HEALTH, damage);
272 // don't allow opening doors through damage if keys are required
276 if (GetResource(this, RES_HEALTH) <= 0)
278 SetResourceExplicit(this.owner, RES_HEALTH, this.owner.max_health);
279 this.owner.takedamage = DAMAGE_NO; // will be reset upon return
280 door_use(this.owner, attacker, NULL);
284 .float door_finished;
294 void door_touch(entity this, entity toucher)
296 if (!IS_PLAYER(toucher))
298 if (this.owner.door_finished > time)
301 this.owner.door_finished = time + 2;
304 if (!(this.owner.dmg) && (this.owner.message != ""))
306 if (IS_CLIENT(toucher))
307 centerprint(toucher, this.owner.message);
308 play2(toucher, this.owner.noise);
313 void door_generic_plat_blocked(entity this, entity blocker)
315 if((this.spawnflags & DOOR_CRUSH) && (blocker.takedamage != DAMAGE_NO)) { // Kill Kill Kill!!
317 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
324 if((this.dmg) && (blocker.takedamage == DAMAGE_YES)) // Shall we bite?
325 Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
328 //Dont chamge direction for dead or dying stuff
329 if(IS_DEAD(blocker) && (blocker.takedamage == DAMAGE_NO))
333 if (this.state == STATE_DOWN)
334 door_rotating_go_up (this, blocker);
336 door_rotating_go_down (this);
342 //gib dying stuff just to make sure
343 if((this.dmg) && (blocker.takedamage != DAMAGE_NO)) // Shall we bite?
344 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
351 =========================================
354 Spawned if a door lacks a real activator
355 =========================================
358 void door_trigger_touch(entity this, entity toucher)
360 if (GetResource(toucher, RES_HEALTH) < 1)
362 if (!((toucher.iscreature || (toucher.flags & FL_PROJECTILE)) && !IS_DEAD(toucher)))
364 if(!((IS_CLIENT(toucher) || toucher.classname == "ENT_CLIENT_PROJECTILE") && !IS_DEAD(toucher)))
368 if (this.owner.state == STATE_UP)
371 // check if door is locked
372 if (!door_check_keys(this, toucher))
375 if (this.owner.state == STATE_TOP)
377 if (this.owner.nextthink < this.owner.ltime + this.owner.wait)
379 entity e = this.owner;
381 e.nextthink = e.ltime + e.wait;
383 } while (e != this.owner);
388 door_use(this.owner, toucher, NULL);
391 void door_spawnfield(entity this, vector fmins, vector fmaxs)
394 vector t1 = fmins, t2 = fmaxs;
396 trigger = new(doortriggerfield);
397 set_movetype(trigger, MOVETYPE_NONE);
398 trigger.solid = SOLID_TRIGGER;
399 trigger.owner = this;
401 settouch(trigger, door_trigger_touch);
404 setsize (trigger, t1 - '60 60 8', t2 + '60 60 8');
416 entity LinkDoors_nextent(entity cur, entity near, entity pass)
418 while ((cur = find(cur, classname, pass.classname))
419 && ((!Q3COMPAT_COMMON && (cur.spawnflags & DOOR_DONT_LINK)) || cur.enemy))
425 bool LinkDoors_isconnected(entity e1, entity e2, entity pass)
428 return e1.team == e2.team;
431 if((e1.absmin_x > e2.absmax_x + DELTA)
432 || (e1.absmin_y > e2.absmax_y + DELTA)
433 || (e1.absmin_z > e2.absmax_z + DELTA)
434 || (e2.absmin_x > e1.absmax_x + DELTA)
435 || (e2.absmin_y > e1.absmax_y + DELTA)
436 || (e2.absmin_z > e1.absmax_z + DELTA)
444 void LinkDoors(entity this)
454 return; // already linked by another door
456 // Q3 door linking is done for teamed doors only and is not affected by spawnflags or bmodel proximity
457 if ((!Q3COMPAT_COMMON && (this.spawnflags & DOOR_DONT_LINK)) || (Q3COMPAT_COMMON && !this.team))
459 this.owner = this.enemy = this;
461 if (GetResource(this, RES_HEALTH))
463 if(this.targetname && this.targetname != "")
468 door_spawnfield(this, this.absmin, this.absmax);
470 return; // don't want to link this door
473 FindConnectedComponent(this, enemy, LinkDoors_nextent, LinkDoors_isconnected, this);
475 // set owner, and make a loop of the chain
476 LOG_TRACE("LinkDoors: linking doors:");
477 for(t = this; ; t = t.enemy)
479 LOG_TRACE(" ", etos(t));
489 // collect health, targetname, message, size
492 for(t = this; ; t = t.enemy)
494 if(GetResource(t, RES_HEALTH) && !GetResource(this, RES_HEALTH))
495 SetResourceExplicit(this, RES_HEALTH, GetResource(t, RES_HEALTH));
496 if((t.targetname != "") && (this.targetname == ""))
497 this.targetname = t.targetname;
498 if((t.message != "") && (this.message == ""))
499 this.message = t.message;
500 if (t.absmin_x < cmins_x)
501 cmins_x = t.absmin_x;
502 if (t.absmin_y < cmins_y)
503 cmins_y = t.absmin_y;
504 if (t.absmin_z < cmins_z)
505 cmins_z = t.absmin_z;
506 if (t.absmax_x > cmaxs_x)
507 cmaxs_x = t.absmax_x;
508 if (t.absmax_y > cmaxs_y)
509 cmaxs_y = t.absmax_y;
510 if (t.absmax_z > cmaxs_z)
511 cmaxs_z = t.absmax_z;
516 // distribute health, targetname, message
517 for(t = this; t; t = t.enemy)
519 SetResourceExplicit(t, RES_HEALTH, GetResource(this, RES_HEALTH));
520 t.targetname = this.targetname;
521 t.message = this.message;
526 // shootable, or triggered doors just needed the owner/enemy links,
527 // they don't spawn a field
529 if (GetResource(this, RES_HEALTH))
531 if(this.targetname && this.targetname != "")
536 door_spawnfield(this, cmins, cmaxs);
539 REGISTER_NET_LINKED(ENT_CLIENT_DOOR)
542 /*QUAKED spawnfunc_func_door (0 .5 .8) ? START_OPEN x DOOR_DONT_LINK GOLD_KEY SILVER_KEY TOGGLE
543 if two doors touch, they are assumed to be connected and operate as a unit.
545 TOGGLE causes the door to wait in both the start and end states for a trigger event.
547 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).
549 GOLD_KEY causes the door to open only if the activator holds a gold key.
551 SILVER_KEY causes the door to open only if the activator holds a silver key.
553 "message" is printed when the door is touched if it is a trigger door and it hasn't been fired yet
554 "angle" determines the opening direction
555 "targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door.
556 "health" if set, door must be shot open
557 "speed" movement speed (100 default)
558 "wait" wait before returning (3 default, -1 = never return)
559 "lip" lip remaining at end of move (8 default)
560 "dmg" damage to inflict when blocked (0 default)
567 FIXME: only one sound set available at the time being
571 float door_send(entity this, entity to, float sf)
573 WriteHeader(MSG_ENTITY, ENT_CLIENT_DOOR);
574 WriteByte(MSG_ENTITY, sf);
576 if(sf & SF_TRIGGER_INIT)
578 WriteString(MSG_ENTITY, this.classname);
579 WriteByte(MSG_ENTITY, this.spawnflags);
581 WriteString(MSG_ENTITY, this.model);
583 trigger_common_write(this, true);
585 WriteVector(MSG_ENTITY, this.pos1);
586 WriteVector(MSG_ENTITY, this.pos2);
588 WriteVector(MSG_ENTITY, this.size);
590 WriteShort(MSG_ENTITY, this.wait);
591 WriteShort(MSG_ENTITY, this.speed);
592 WriteByte(MSG_ENTITY, this.lip);
593 WriteByte(MSG_ENTITY, this.state);
594 WriteCoord(MSG_ENTITY, this.ltime);
597 if(sf & SF_TRIGGER_RESET)
599 // client makes use of this, we do not
602 if(sf & SF_TRIGGER_UPDATE)
604 WriteVector(MSG_ENTITY, this.origin);
606 WriteVector(MSG_ENTITY, this.pos1);
607 WriteVector(MSG_ENTITY, this.pos2);
615 //Net_LinkEntity(this, false, 0, door_send);
619 void door_init_startopen(entity this)
621 setorigin(this, this.pos2);
622 this.pos2 = this.pos1;
623 this.pos1 = this.origin;
626 this.SendFlags |= SF_TRIGGER_UPDATE;
630 void door_reset(entity this)
632 setorigin(this, this.pos1);
633 this.velocity = '0 0 0';
634 this.state = STATE_BOTTOM;
635 setthink(this, func_null);
639 this.SendFlags |= SF_TRIGGER_RESET;
645 // common code for func_door and func_door_rotating spawnfuncs
646 void door_init_shared(entity this)
648 this.max_health = GetResource(this, RES_HEALTH);
653 this.noise = "misc/talk.wav";
655 // door still locked sound
656 if(this.noise3 == "")
658 this.noise3 = "misc/talk.wav";
660 precache_sound(this.noise);
661 precache_sound(this.noise3);
663 if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message == ""))
665 this.message = "was squished";
667 if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message2 == ""))
669 this.message2 = "was squished by";
672 // TODO: other soundpacks
673 if (this.sounds > 0 || q3compat)
675 // Doors in Q3 always have sounds (they're hard coded)
676 this.noise2 = "plats/medplat1.wav";
677 this.noise1 = "plats/medplat2.wav";
682 // CPMA adds these fields for overriding the Q3 default sounds
683 string s = GetField_fullspawndata(this, "sound_start", true);
684 string e = GetField_fullspawndata(this, "sound_end", true);
686 // Quake Live adds these ones, because of course it had to be different from CPMA
687 if (!s) s = GetField_fullspawndata(this, "startsound", true);
688 if (!e) e = GetField_fullspawndata(this, "endsound", true);
691 this.noise2 = strzone(s);
694 // PK3s supporting Q3A sometimes include custom sounds at Q3 default paths
695 s = "sound/movers/doors/dr1_strt.wav";
696 if (FindFileInMapPack(s))
701 this.noise1 = strzone(e);
704 e = "sound/movers/doors/dr1_end.wav";
705 if (FindFileInMapPack(e))
710 // sound when door stops moving
711 if(this.noise1 && this.noise1 != "")
713 precache_sound(this.noise1);
715 // sound when door is moving
716 if(this.noise2 && this.noise2 != "")
718 precache_sound(this.noise2);
721 if(autocvar_sv_doors_always_open)
727 this.wait = q3compat ? 2 : 3;
735 this.state = STATE_BOTTOM;
737 if (GetResource(this, RES_HEALTH) || (q3compat && this.targetname == ""))
739 //this.canteamdamage = true; // TODO
740 this.takedamage = DAMAGE_YES;
741 this.event_damage = door_damage;
750 // spawnflags require key (for now only func_door)
753 // Quake 1 and QL keys compatibility
754 if (this.spawnflags & SPAWNFLAGS_GOLD_KEY)
755 this.itemkeys |= BIT(0);
756 if (this.spawnflags & SPAWNFLAGS_SILVER_KEY)
757 this.itemkeys |= BIT(1);
761 if (!InitMovingBrushTrigger(this))
763 this.effects |= EF_LOWPRECISION;
764 this.classname = "door";
766 setblocked(this, door_blocked);
769 if(this.spawnflags & DOOR_NONSOLID)
770 this.solid = SOLID_NOT;
772 // DOOR_START_OPEN is to allow an entity to be lighted in the closed position
773 // but spawn in the open position
774 // the tuba door on xoylent requires the delayed init
775 if (this.spawnflags & DOOR_START_OPEN)
776 InitializeEntity(this, door_init_startopen, INITPRIO_SETLOCATION);
778 door_init_shared(this);
780 this.pos1 = this.origin;
782 absmovedir.x = fabs(this.movedir.x);
783 absmovedir.y = fabs(this.movedir.y);
784 absmovedir.z = fabs(this.movedir.z);
785 this.pos2 = this.pos1 + this.movedir * (absmovedir * this.size - this.lip);
787 if(autocvar_sv_doors_always_open)
789 this.speed = max(750, this.speed);
791 else if (!this.speed)
806 string t = GetField_fullspawndata(this, "team");
807 // bones_was_here: same hack as used to support teamed items on Q3 maps
808 if (t) this.team = crc16(false, t);
812 settouch(this, door_touch);
814 // LinkDoors can't be done until all of the doors have been spawned, so
815 // the sizes can be detected properly.
816 InitializeEntity(this, LinkDoors, INITPRIO_LINKDOORS);
818 this.reset = door_reset;
823 NET_HANDLE(ENT_CLIENT_DOOR, bool isnew)
827 if(sf & SF_TRIGGER_INIT)
829 this.classname = strzone(ReadString());
830 this.spawnflags = ReadByte();
832 this.mdl = strzone(ReadString());
833 _setmodel(this, this.mdl);
835 trigger_common_read(this, true);
837 this.pos1 = ReadVector();
838 this.pos2 = ReadVector();
840 this.size = ReadVector();
842 this.wait = ReadShort();
843 this.speed = ReadShort();
844 this.lip = ReadByte();
845 this.state = ReadByte();
846 this.ltime = ReadCoord();
848 this.solid = SOLID_BSP;
849 set_movetype(this, MOVETYPE_PUSH);
854 if(this.spawnflags & DOOR_START_OPEN)
855 door_init_startopen(this);
857 this.move_time = time;
858 set_movetype(this, MOVETYPE_PUSH);
861 if(sf & SF_TRIGGER_RESET)
866 if(sf & SF_TRIGGER_UPDATE)
868 this.origin = ReadVector();
869 setorigin(this, this.origin);
871 this.pos1 = ReadVector();
872 this.pos2 = ReadVector();