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_fire(entity this, entity actor, entity trigger)
218 if (this.owner != this)
219 objerror (this, "door_fire: this.owner != this");
221 if (this.spawnflags & DOOR_TOGGLE)
223 if (this.state == STATE_UP || this.state == STATE_TOP)
227 if (e.classname == "door") {
230 door_rotating_go_down(e);
233 } while ((e != this) && (e != NULL));
238 // trigger all paired doors
241 if (e.classname == "door") {
242 door_go_up(e, actor, trigger);
244 // if the BIDIR spawnflag (==2) is set and the trigger has set trigger_reverse, reverse the opening direction
245 if ((e.spawnflags & DOOR_ROTATING_BIDIR) && trigger.trigger_reverse!=0 && e.lip != 666 && e.state == STATE_BOTTOM) {
246 e.lip = 666; // e.lip is used to remember reverse opening direction for door_rotating
247 e.pos2 = '0 0 0' - e.pos2;
249 // if BIDIR_IN_DOWN (==8) is set, prevent the door from reoping during closing if it is triggered from the wrong side
250 if (!((e.spawnflags & DOOR_ROTATING_BIDIR) && (e.spawnflags & DOOR_ROTATING_BIDIR_IN_DOWN) && e.state == STATE_DOWN
251 && (((e.lip == 666) && (trigger.trigger_reverse == 0)) || ((e.lip != 666) && (trigger.trigger_reverse != 0)))))
253 door_rotating_go_up(e, trigger);
257 } while ((e != this) && (e != NULL));
260 void door_use(entity this, entity actor, entity trigger)
262 //dprint("door_use (model: ");dprint(this.model);dprint(")\n");
265 door_fire(this.owner, actor, trigger);
268 void door_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
270 if(this.spawnflags & NOSPLASH)
271 if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH))
273 TakeResource(this, RES_HEALTH, damage);
277 // don't allow opening doors through damage if keys are required
281 if (GetResource(this, RES_HEALTH) <= 0)
283 SetResourceExplicit(this.owner, RES_HEALTH, this.owner.max_health);
284 this.owner.takedamage = DAMAGE_NO; // will be reset upon return
285 door_use(this.owner, attacker, NULL);
289 .float door_finished;
299 void door_touch(entity this, entity toucher)
301 if (!IS_PLAYER(toucher))
303 if (this.owner.door_finished > time)
306 this.owner.door_finished = time + 2;
309 if (!(this.owner.dmg) && (this.owner.message != ""))
311 if (IS_CLIENT(toucher))
312 centerprint(toucher, this.owner.message);
313 play2(toucher, this.owner.noise);
318 void door_generic_plat_blocked(entity this, entity blocker)
320 if((this.spawnflags & DOOR_CRUSH) && (blocker.takedamage != DAMAGE_NO)) { // Kill Kill Kill!!
322 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
329 if((this.dmg) && (blocker.takedamage == DAMAGE_YES)) // Shall we bite?
330 Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
333 //Dont chamge direction for dead or dying stuff
334 if(IS_DEAD(blocker) && (blocker.takedamage == DAMAGE_NO))
338 if (this.state == STATE_DOWN)
339 door_rotating_go_up (this, blocker);
341 door_rotating_go_down (this);
347 //gib dying stuff just to make sure
348 if((this.dmg) && (blocker.takedamage != DAMAGE_NO)) // Shall we bite?
349 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
356 =========================================
359 Spawned if a door lacks a real activator
360 =========================================
363 void door_trigger_touch(entity this, entity toucher)
365 if (GetResource(toucher, RES_HEALTH) < 1)
367 if (!((toucher.iscreature || (toucher.flags & FL_PROJECTILE)) && !IS_DEAD(toucher)))
369 if(!((IS_CLIENT(toucher) || toucher.classname == "ENT_CLIENT_PROJECTILE") && !IS_DEAD(toucher)))
373 if (this.owner.state == STATE_UP)
376 // check if door is locked
377 if (!door_check_keys(this, toucher))
380 if (this.owner.state == STATE_TOP)
382 if (this.owner.nextthink < this.owner.ltime + this.owner.wait)
384 entity e = this.owner;
386 e.nextthink = e.ltime + e.wait;
388 } while (e != this.owner);
393 door_use(this.owner, toucher, NULL);
396 void door_spawnfield(entity this, vector fmins, vector fmaxs)
399 vector t1 = fmins, t2 = fmaxs;
401 trigger = new(doortriggerfield);
402 set_movetype(trigger, MOVETYPE_NONE);
403 trigger.solid = SOLID_TRIGGER;
404 trigger.owner = this;
406 settouch(trigger, door_trigger_touch);
409 setsize (trigger, t1 - '60 60 8', t2 + '60 60 8');
421 entity LinkDoors_nextent(entity cur, entity near, entity pass)
423 while ((cur = find(cur, classname, pass.classname))
424 && ((!Q3COMPAT_COMMON && (cur.spawnflags & DOOR_DONT_LINK)) || cur.enemy))
430 bool LinkDoors_isconnected(entity e1, entity e2, entity pass)
433 return e1.team == e2.team;
436 if((e1.absmin_x > e2.absmax_x + DELTA)
437 || (e1.absmin_y > e2.absmax_y + DELTA)
438 || (e1.absmin_z > e2.absmax_z + DELTA)
439 || (e2.absmin_x > e1.absmax_x + DELTA)
440 || (e2.absmin_y > e1.absmax_y + DELTA)
441 || (e2.absmin_z > e1.absmax_z + DELTA)
449 void LinkDoors(entity this)
459 return; // already linked by another door
461 // Q3 door linking is done for teamed doors only and is not affected by spawnflags or bmodel proximity
462 if ((!Q3COMPAT_COMMON && (this.spawnflags & DOOR_DONT_LINK)) || (Q3COMPAT_COMMON && !this.team))
464 this.owner = this.enemy = this;
466 if (GetResource(this, RES_HEALTH))
468 if(this.targetname && this.targetname != "")
473 door_spawnfield(this, this.absmin, this.absmax);
475 return; // don't want to link this door
478 FindConnectedComponent(this, enemy, LinkDoors_nextent, LinkDoors_isconnected, this);
480 // set owner, and make a loop of the chain
481 LOG_TRACE("LinkDoors: linking doors:");
482 for(t = this; ; t = t.enemy)
484 LOG_TRACE(" ", etos(t));
494 // collect health, targetname, message, size
497 for(t = this; ; t = t.enemy)
499 if(GetResource(t, RES_HEALTH) && !GetResource(this, RES_HEALTH))
500 SetResourceExplicit(this, RES_HEALTH, GetResource(t, RES_HEALTH));
501 if((t.targetname != "") && (this.targetname == ""))
502 this.targetname = t.targetname;
503 if((t.message != "") && (this.message == ""))
504 this.message = t.message;
505 if (t.absmin_x < cmins_x)
506 cmins_x = t.absmin_x;
507 if (t.absmin_y < cmins_y)
508 cmins_y = t.absmin_y;
509 if (t.absmin_z < cmins_z)
510 cmins_z = t.absmin_z;
511 if (t.absmax_x > cmaxs_x)
512 cmaxs_x = t.absmax_x;
513 if (t.absmax_y > cmaxs_y)
514 cmaxs_y = t.absmax_y;
515 if (t.absmax_z > cmaxs_z)
516 cmaxs_z = t.absmax_z;
521 // distribute health, targetname, message
522 for(t = this; t; t = t.enemy)
524 SetResourceExplicit(t, RES_HEALTH, GetResource(this, RES_HEALTH));
525 t.targetname = this.targetname;
526 t.message = this.message;
531 // shootable, or triggered doors just needed the owner/enemy links,
532 // they don't spawn a field
534 if (GetResource(this, RES_HEALTH))
536 if(this.targetname && this.targetname != "")
541 door_spawnfield(this, cmins, cmaxs);
544 REGISTER_NET_LINKED(ENT_CLIENT_DOOR)
547 /*QUAKED spawnfunc_func_door (0 .5 .8) ? START_OPEN x DOOR_DONT_LINK GOLD_KEY SILVER_KEY TOGGLE
548 if two doors touch, they are assumed to be connected and operate as a unit.
550 TOGGLE causes the door to wait in both the start and end states for a trigger event.
552 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).
554 GOLD_KEY causes the door to open only if the activator holds a gold key.
556 SILVER_KEY causes the door to open only if the activator holds a silver key.
558 "message" is printed when the door is touched if it is a trigger door and it hasn't been fired yet
559 "angle" determines the opening direction
560 "targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door.
561 "health" if set, door must be shot open
562 "speed" movement speed (100 default)
563 "wait" wait before returning (3 default, -1 = never return)
564 "lip" lip remaining at end of move (8 default)
565 "dmg" damage to inflict when blocked (0 default)
572 FIXME: only one sound set available at the time being
576 float door_send(entity this, entity to, float sf)
578 WriteHeader(MSG_ENTITY, ENT_CLIENT_DOOR);
579 WriteByte(MSG_ENTITY, sf);
581 if(sf & SF_TRIGGER_INIT)
583 WriteString(MSG_ENTITY, this.classname);
584 WriteByte(MSG_ENTITY, this.spawnflags);
586 WriteString(MSG_ENTITY, this.model);
588 trigger_common_write(this, true);
590 WriteVector(MSG_ENTITY, this.pos1);
591 WriteVector(MSG_ENTITY, this.pos2);
593 WriteVector(MSG_ENTITY, this.size);
595 WriteShort(MSG_ENTITY, this.wait);
596 WriteShort(MSG_ENTITY, this.speed);
597 WriteByte(MSG_ENTITY, this.lip);
598 WriteByte(MSG_ENTITY, this.state);
599 WriteCoord(MSG_ENTITY, this.ltime);
602 if(sf & SF_TRIGGER_RESET)
604 // client makes use of this, we do not
607 if(sf & SF_TRIGGER_UPDATE)
609 WriteVector(MSG_ENTITY, this.origin);
611 WriteVector(MSG_ENTITY, this.pos1);
612 WriteVector(MSG_ENTITY, this.pos2);
620 //Net_LinkEntity(this, false, 0, door_send);
624 void door_init_startopen(entity this)
626 setorigin(this, this.pos2);
627 this.pos2 = this.pos1;
628 this.pos1 = this.origin;
631 this.SendFlags |= SF_TRIGGER_UPDATE;
635 void door_reset(entity this)
637 setorigin(this, this.pos1);
638 this.velocity = '0 0 0';
639 this.state = STATE_BOTTOM;
640 setthink(this, func_null);
644 this.SendFlags |= SF_TRIGGER_RESET;
650 // common code for func_door and func_door_rotating spawnfuncs
651 void door_init_shared(entity this)
653 this.max_health = GetResource(this, RES_HEALTH);
658 this.noise = "misc/talk.wav";
660 // door still locked sound
661 if(this.noise3 == "")
663 this.noise3 = "misc/talk.wav";
665 precache_sound(this.noise);
666 precache_sound(this.noise3);
668 if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message == ""))
670 this.message = "was squished";
672 if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message2 == ""))
674 this.message2 = "was squished by";
677 // TODO: other soundpacks
678 if (this.sounds > 0 || q3compat)
680 // Doors in Q3 always have sounds (they're hard coded)
681 this.noise2 = "plats/medplat1.wav";
682 this.noise1 = "plats/medplat2.wav";
687 // CPMA adds these fields for overriding the engine sounds
688 string s = GetField_fullspawndata(this, "sound_start", true);
689 string e = GetField_fullspawndata(this, "sound_end", true);
692 this.noise2 = strzone(s);
694 this.noise1 = strzone(e);
697 // sound when door stops moving
698 if(this.noise1 && this.noise1 != "")
700 precache_sound(this.noise1);
702 // sound when door is moving
703 if(this.noise2 && this.noise2 != "")
705 precache_sound(this.noise2);
708 if(autocvar_sv_doors_always_open)
714 this.wait = q3compat ? 2 : 3;
722 this.state = STATE_BOTTOM;
724 if (GetResource(this, RES_HEALTH) || (q3compat && this.targetname == ""))
726 //this.canteamdamage = true; // TODO
727 this.takedamage = DAMAGE_YES;
728 this.event_damage = door_damage;
737 // spawnflags require key (for now only func_door)
740 // Quake 1 keys compatibility
741 if (this.spawnflags & SPAWNFLAGS_GOLD_KEY)
742 this.itemkeys |= ITEM_KEY_BIT(0);
743 if (this.spawnflags & SPAWNFLAGS_SILVER_KEY)
744 this.itemkeys |= ITEM_KEY_BIT(1);
748 if (!InitMovingBrushTrigger(this))
750 this.effects |= EF_LOWPRECISION;
751 this.classname = "door";
753 setblocked(this, door_blocked);
756 if(this.spawnflags & DOOR_NONSOLID)
757 this.solid = SOLID_NOT;
759 // DOOR_START_OPEN is to allow an entity to be lighted in the closed position
760 // but spawn in the open position
761 // the tuba door on xoylent requires the delayed init
762 if (this.spawnflags & DOOR_START_OPEN)
763 InitializeEntity(this, door_init_startopen, INITPRIO_SETLOCATION);
765 door_init_shared(this);
767 this.pos1 = this.origin;
769 absmovedir.x = fabs(this.movedir.x);
770 absmovedir.y = fabs(this.movedir.y);
771 absmovedir.z = fabs(this.movedir.z);
772 this.pos2 = this.pos1 + this.movedir * (absmovedir * this.size - this.lip);
774 if(autocvar_sv_doors_always_open)
776 this.speed = max(750, this.speed);
778 else if (!this.speed)
793 string t = GetField_fullspawndata(this, "team");
794 // bones_was_here: same hack as used to support teamed items on Q3 maps
795 if (t) this.team = crc16(false, t);
799 settouch(this, door_touch);
801 // LinkDoors can't be done until all of the doors have been spawned, so
802 // the sizes can be detected properly.
803 InitializeEntity(this, LinkDoors, INITPRIO_LINKDOORS);
805 this.reset = door_reset;
810 NET_HANDLE(ENT_CLIENT_DOOR, bool isnew)
814 if(sf & SF_TRIGGER_INIT)
816 this.classname = strzone(ReadString());
817 this.spawnflags = ReadByte();
819 this.mdl = strzone(ReadString());
820 _setmodel(this, this.mdl);
822 trigger_common_read(this, true);
824 this.pos1 = ReadVector();
825 this.pos2 = ReadVector();
827 this.size = ReadVector();
829 this.wait = ReadShort();
830 this.speed = ReadShort();
831 this.lip = ReadByte();
832 this.state = ReadByte();
833 this.ltime = ReadCoord();
835 this.solid = SOLID_BSP;
836 set_movetype(this, MOVETYPE_PUSH);
841 if(this.spawnflags & DOOR_START_OPEN)
842 door_init_startopen(this);
844 this.move_time = time;
845 set_movetype(this, MOVETYPE_PUSH);
848 if(sf & SF_TRIGGER_RESET)
853 if(sf & SF_TRIGGER_UPDATE)
855 this.origin = ReadVector();
856 setorigin(this, this.origin);
858 this.pos1 = ReadVector();
859 this.pos2 = ReadVector();