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)
33 && (blocker.takedamage != DAMAGE_NO)
40 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
46 if (this.dmg && blocker.takedamage != DAMAGE_NO) // Shall we bite?
47 Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
50 // don't change direction for dead or dying stuff
53 && blocker.takedamage != DAMAGE_NO
58 if (this.state == STATE_DOWN)
60 if (this.classname == "door")
61 door_go_up(this, NULL, NULL);
63 door_rotating_go_up(this, blocker);
67 if (this.classname == "door")
70 door_rotating_go_down(this);
77 //gib dying stuff just to make sure
78 if((this.dmg) && (blocker.takedamage != DAMAGE_NO)) // Shall we bite?
79 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
83 // if we didn't change direction and are using a non-linear movement controller, we must pause it
84 if (!reverse && this.classname == "door" && this.move_controller)
85 SUB_CalcMovePause(this);
88 void door_hit_top(entity this)
90 if (this.noise1 != "")
91 _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
92 this.state = STATE_TOP;
93 if (this.spawnflags & DOOR_TOGGLE)
94 return; // don't come down automatically
95 if (this.classname == "door")
97 setthink(this, door_go_down);
100 setthink(this, door_rotating_go_down);
102 this.nextthink = this.ltime + this.wait;
105 void door_hit_bottom(entity this)
107 if (this.noise1 != "")
108 _sound (this, CH_TRIGGER_SINGLE, this.noise1, VOL_BASE, ATTEN_NORM);
109 this.state = STATE_BOTTOM;
112 void door_go_down(entity this)
114 if (this.noise2 != "")
115 _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
118 this.takedamage = DAMAGE_YES;
119 SetResourceExplicit(this, RES_HEALTH, this.max_health);
122 this.state = STATE_DOWN;
123 SUB_CalcMove (this, this.pos1, TSPEED_LINEAR, this.speed, door_hit_bottom);
126 void door_go_up(entity this, entity actor, entity trigger)
128 if (this.state == STATE_UP)
129 return; // already going up
131 if (this.state == STATE_TOP)
132 { // reset top wait time
133 this.nextthink = this.ltime + this.wait;
137 if (this.noise2 != "")
138 _sound (this, CH_TRIGGER_SINGLE, this.noise2, VOL_BASE, ATTEN_NORM);
139 this.state = STATE_UP;
140 SUB_CalcMove (this, this.pos2, TSPEED_LINEAR, this.speed, door_hit_top);
143 oldmessage = this.message;
145 SUB_UseTargets(this, actor, trigger);
146 this.message = oldmessage;
151 =============================================================================
155 =============================================================================
158 bool door_check_keys(entity door, entity player)
167 // this door require a key
168 // only a player can have a key
169 if(!IS_PLAYER(player))
172 entity store = player;
176 int valid = (door.itemkeys & store.itemkeys);
177 door.itemkeys &= ~valid; // only some of the needed keys were given
182 play2(player, door.noise);
183 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_UNLOCKED);
191 if(player.key_door_messagetime <= time)
193 play2(player, door.noise3);
194 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_LOCKED_NEED, item_keys_keylist(door.itemkeys));
195 player.key_door_messagetime = time + 2;
201 // door needs keys the player doesn't have
203 if(player.key_door_messagetime <= time)
205 play2(player, door.noise3);
206 Send_Notification(NOTIF_ONE, player, MSG_CENTER, CENTER_DOOR_LOCKED_ALSONEED, item_keys_keylist(door.itemkeys));
207 player.key_door_messagetime = time + 2;
214 void door_fire(entity this, entity actor, entity trigger)
216 if (this.owner != this)
217 objerror (this, "door_fire: this.owner != this");
219 if (this.spawnflags & DOOR_TOGGLE)
221 if (this.state == STATE_UP || this.state == STATE_TOP)
225 if (e.classname == "door") {
228 door_rotating_go_down(e);
231 } while ((e != this) && (e != NULL));
236 // trigger all paired doors
239 if (e.classname == "door") {
240 door_go_up(e, actor, trigger);
242 // if the BIDIR spawnflag (==2) is set and the trigger has set trigger_reverse, reverse the opening direction
243 if ((e.spawnflags & DOOR_ROTATING_BIDIR) && trigger.trigger_reverse!=0 && e.lip != 666 && e.state == STATE_BOTTOM) {
244 e.lip = 666; // e.lip is used to remember reverse opening direction for door_rotating
245 e.pos2 = '0 0 0' - e.pos2;
247 // if BIDIR_IN_DOWN (==8) is set, prevent the door from reoping during closing if it is triggered from the wrong side
248 if (!((e.spawnflags & DOOR_ROTATING_BIDIR) && (e.spawnflags & DOOR_ROTATING_BIDIR_IN_DOWN) && e.state == STATE_DOWN
249 && (((e.lip == 666) && (trigger.trigger_reverse == 0)) || ((e.lip != 666) && (trigger.trigger_reverse != 0)))))
251 door_rotating_go_up(e, trigger);
255 } while ((e != this) && (e != NULL));
258 void door_use(entity this, entity actor, entity trigger)
260 //dprint("door_use (model: ");dprint(this.model);dprint(")\n");
263 door_fire(this.owner, actor, trigger);
266 void door_damage(entity this, entity inflictor, entity attacker, float damage, int deathtype, .entity weaponentity, vector hitloc, vector force)
268 if(this.spawnflags & NOSPLASH)
269 if(!(DEATH_ISSPECIAL(deathtype)) && (deathtype & HITTYPE_SPLASH))
271 TakeResource(this, RES_HEALTH, damage);
275 // don't allow opening doors through damage if keys are required
279 if (GetResource(this, RES_HEALTH) <= 0)
281 SetResourceExplicit(this.owner, RES_HEALTH, this.owner.max_health);
282 this.owner.takedamage = DAMAGE_NO; // will be reset upon return
283 door_use(this.owner, attacker, NULL);
287 .float door_finished;
297 void door_touch(entity this, entity toucher)
299 if (!IS_PLAYER(toucher))
301 if (this.owner.door_finished > time)
304 this.owner.door_finished = time + 2;
307 if (!(this.owner.dmg) && (this.owner.message != ""))
309 if (IS_CLIENT(toucher))
310 centerprint(toucher, this.owner.message);
311 play2(toucher, this.owner.noise);
316 void door_generic_plat_blocked(entity this, entity blocker)
318 if((this.spawnflags & DOOR_CRUSH) && (blocker.takedamage != DAMAGE_NO)) { // Kill Kill Kill!!
320 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
327 if((this.dmg) && (blocker.takedamage == DAMAGE_YES)) // Shall we bite?
328 Damage (blocker, this, this, this.dmg, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
331 //Dont chamge direction for dead or dying stuff
332 if(IS_DEAD(blocker) && (blocker.takedamage == DAMAGE_NO))
336 if (this.state == STATE_DOWN)
337 door_rotating_go_up (this, blocker);
339 door_rotating_go_down (this);
345 //gib dying stuff just to make sure
346 if((this.dmg) && (blocker.takedamage != DAMAGE_NO)) // Shall we bite?
347 Damage (blocker, this, this, 10000, DEATH_HURTTRIGGER.m_id, DMG_NOWEP, blocker.origin, '0 0 0');
354 =========================================
357 Spawned if a door lacks a real activator
358 =========================================
361 void door_trigger_touch(entity this, entity toucher)
363 if (GetResource(toucher, RES_HEALTH) < 1)
365 if (!((toucher.iscreature || (toucher.flags & FL_PROJECTILE)) && !IS_DEAD(toucher)))
367 if(!((IS_CLIENT(toucher) || toucher.classname == "ENT_CLIENT_PROJECTILE") && !IS_DEAD(toucher)))
371 if (this.owner.state == STATE_UP)
374 // check if door is locked
375 if (!door_check_keys(this, toucher))
378 if (this.owner.state == STATE_TOP)
380 if (this.owner.nextthink < this.owner.ltime + this.owner.wait)
382 entity e = this.owner;
384 e.nextthink = e.ltime + e.wait;
386 } while (e != this.owner);
391 door_use(this.owner, toucher, NULL);
394 void door_spawnfield(entity this, vector fmins, vector fmaxs)
397 vector t1 = fmins, t2 = fmaxs;
399 trigger = new(doortriggerfield);
400 set_movetype(trigger, MOVETYPE_NONE);
401 trigger.solid = SOLID_TRIGGER;
402 trigger.owner = this;
404 settouch(trigger, door_trigger_touch);
407 setsize (trigger, t1 - '60 60 8', t2 + '60 60 8');
419 entity LinkDoors_nextent(entity cur, entity near, entity pass)
421 while ((cur = find(cur, classname, pass.classname))
422 && ((!Q3COMPAT_COMMON && (cur.spawnflags & DOOR_DONT_LINK)) || cur.enemy))
428 bool LinkDoors_isconnected(entity e1, entity e2, entity pass)
431 return e1.team == e2.team;
434 if((e1.absmin_x > e2.absmax_x + DELTA)
435 || (e1.absmin_y > e2.absmax_y + DELTA)
436 || (e1.absmin_z > e2.absmax_z + DELTA)
437 || (e2.absmin_x > e1.absmax_x + DELTA)
438 || (e2.absmin_y > e1.absmax_y + DELTA)
439 || (e2.absmin_z > e1.absmax_z + DELTA)
447 void LinkDoors(entity this)
457 return; // already linked by another door
459 // Q3 door linking is done for teamed doors only and is not affected by spawnflags or bmodel proximity
460 if ((!Q3COMPAT_COMMON && (this.spawnflags & DOOR_DONT_LINK)) || (Q3COMPAT_COMMON && !this.team))
462 this.owner = this.enemy = this;
464 if (GetResource(this, RES_HEALTH))
466 if(this.targetname && this.targetname != "")
471 door_spawnfield(this, this.absmin, this.absmax);
473 return; // don't want to link this door
476 FindConnectedComponent(this, enemy, LinkDoors_nextent, LinkDoors_isconnected, this);
478 // set owner, and make a loop of the chain
479 LOG_TRACE("LinkDoors: linking doors:");
480 for(t = this; ; t = t.enemy)
482 LOG_TRACE(" ", etos(t));
492 // collect health, targetname, message, size
495 for(t = this; ; t = t.enemy)
497 if(GetResource(t, RES_HEALTH) && !GetResource(this, RES_HEALTH))
498 SetResourceExplicit(this, RES_HEALTH, GetResource(t, RES_HEALTH));
499 if((t.targetname != "") && (this.targetname == ""))
500 this.targetname = t.targetname;
501 if((t.message != "") && (this.message == ""))
502 this.message = t.message;
503 if (t.absmin_x < cmins_x)
504 cmins_x = t.absmin_x;
505 if (t.absmin_y < cmins_y)
506 cmins_y = t.absmin_y;
507 if (t.absmin_z < cmins_z)
508 cmins_z = t.absmin_z;
509 if (t.absmax_x > cmaxs_x)
510 cmaxs_x = t.absmax_x;
511 if (t.absmax_y > cmaxs_y)
512 cmaxs_y = t.absmax_y;
513 if (t.absmax_z > cmaxs_z)
514 cmaxs_z = t.absmax_z;
519 // distribute health, targetname, message
520 for(t = this; t; t = t.enemy)
522 SetResourceExplicit(t, RES_HEALTH, GetResource(this, RES_HEALTH));
523 t.targetname = this.targetname;
524 t.message = this.message;
529 // shootable, or triggered doors just needed the owner/enemy links,
530 // they don't spawn a field
532 if (GetResource(this, RES_HEALTH))
534 if(this.targetname && this.targetname != "")
539 door_spawnfield(this, cmins, cmaxs);
542 REGISTER_NET_LINKED(ENT_CLIENT_DOOR)
545 /*QUAKED spawnfunc_func_door (0 .5 .8) ? START_OPEN x DOOR_DONT_LINK GOLD_KEY SILVER_KEY TOGGLE
546 if two doors touch, they are assumed to be connected and operate as a unit.
548 TOGGLE causes the door to wait in both the start and end states for a trigger event.
550 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).
552 GOLD_KEY causes the door to open only if the activator holds a gold key.
554 SILVER_KEY causes the door to open only if the activator holds a silver key.
556 "message" is printed when the door is touched if it is a trigger door and it hasn't been fired yet
557 "angle" determines the opening direction
558 "targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door.
559 "health" if set, door must be shot open
560 "speed" movement speed (100 default)
561 "wait" wait before returning (3 default, -1 = never return)
562 "lip" lip remaining at end of move (8 default)
563 "dmg" damage to inflict when blocked (0 default)
570 FIXME: only one sound set available at the time being
574 float door_send(entity this, entity to, float sf)
576 WriteHeader(MSG_ENTITY, ENT_CLIENT_DOOR);
577 WriteByte(MSG_ENTITY, sf);
579 if(sf & SF_TRIGGER_INIT)
581 WriteString(MSG_ENTITY, this.classname);
582 WriteByte(MSG_ENTITY, this.spawnflags);
584 WriteString(MSG_ENTITY, this.model);
586 trigger_common_write(this, true);
588 WriteVector(MSG_ENTITY, this.pos1);
589 WriteVector(MSG_ENTITY, this.pos2);
591 WriteVector(MSG_ENTITY, this.size);
593 WriteShort(MSG_ENTITY, this.wait);
594 WriteShort(MSG_ENTITY, this.speed);
595 WriteByte(MSG_ENTITY, this.lip);
596 WriteByte(MSG_ENTITY, this.state);
597 WriteCoord(MSG_ENTITY, this.ltime);
600 if(sf & SF_TRIGGER_RESET)
602 // client makes use of this, we do not
605 if(sf & SF_TRIGGER_UPDATE)
607 WriteVector(MSG_ENTITY, this.origin);
609 WriteVector(MSG_ENTITY, this.pos1);
610 WriteVector(MSG_ENTITY, this.pos2);
618 //Net_LinkEntity(this, false, 0, door_send);
622 void door_init_startopen(entity this)
624 setorigin(this, this.pos2);
625 this.pos2 = this.pos1;
626 this.pos1 = this.origin;
629 this.SendFlags |= SF_TRIGGER_UPDATE;
633 void door_reset(entity this)
635 setorigin(this, this.pos1);
636 this.velocity = '0 0 0';
637 this.state = STATE_BOTTOM;
638 setthink(this, func_null);
642 this.SendFlags |= SF_TRIGGER_RESET;
648 // common code for func_door and func_door_rotating spawnfuncs
649 void door_init_shared(entity this)
651 this.max_health = GetResource(this, RES_HEALTH);
656 this.noise = "misc/talk.wav";
658 // door still locked sound
659 if(this.noise3 == "")
661 this.noise3 = "misc/talk.wav";
663 precache_sound(this.noise);
664 precache_sound(this.noise3);
666 if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message == ""))
668 this.message = "was squished";
670 if((this.dmg || (this.spawnflags & DOOR_CRUSH)) && (this.message2 == ""))
672 this.message2 = "was squished by";
675 // TODO: other soundpacks
676 if (this.sounds > 0 || q3compat)
678 // Doors in Q3 always have sounds (they're hard coded)
679 this.noise2 = "plats/medplat1.wav";
680 this.noise1 = "plats/medplat2.wav";
685 // CPMA adds these fields for overriding the engine sounds
686 string s = GetField_fullspawndata(this, "sound_start", true);
687 string e = GetField_fullspawndata(this, "sound_end", true);
690 this.noise2 = strzone(s);
692 this.noise1 = strzone(e);
695 // sound when door stops moving
696 if(this.noise1 && this.noise1 != "")
698 precache_sound(this.noise1);
700 // sound when door is moving
701 if(this.noise2 && this.noise2 != "")
703 precache_sound(this.noise2);
706 if(autocvar_sv_doors_always_open)
712 this.wait = q3compat ? 2 : 3;
720 this.state = STATE_BOTTOM;
722 if (GetResource(this, RES_HEALTH) || (q3compat && this.targetname == ""))
724 //this.canteamdamage = true; // TODO
725 this.takedamage = DAMAGE_YES;
726 this.event_damage = door_damage;
735 // spawnflags require key (for now only func_door)
738 // Quake 1 keys compatibility
739 if (this.spawnflags & SPAWNFLAGS_GOLD_KEY)
740 this.itemkeys |= ITEM_KEY_BIT(0);
741 if (this.spawnflags & SPAWNFLAGS_SILVER_KEY)
742 this.itemkeys |= ITEM_KEY_BIT(1);
746 if (!InitMovingBrushTrigger(this))
748 this.effects |= EF_LOWPRECISION;
749 this.classname = "door";
751 setblocked(this, door_blocked);
754 if(this.spawnflags & DOOR_NONSOLID)
755 this.solid = SOLID_NOT;
757 // DOOR_START_OPEN is to allow an entity to be lighted in the closed position
758 // but spawn in the open position
759 // the tuba door on xoylent requires the delayed init
760 if (this.spawnflags & DOOR_START_OPEN)
761 InitializeEntity(this, door_init_startopen, INITPRIO_SETLOCATION);
763 door_init_shared(this);
765 this.pos1 = this.origin;
767 absmovedir.x = fabs(this.movedir.x);
768 absmovedir.y = fabs(this.movedir.y);
769 absmovedir.z = fabs(this.movedir.z);
770 this.pos2 = this.pos1 + this.movedir * (absmovedir * this.size - this.lip);
772 if(autocvar_sv_doors_always_open)
774 this.speed = max(750, this.speed);
776 else if (!this.speed)
791 string t = GetField_fullspawndata(this, "team");
792 // bones_was_here: same hack as used to support teamed items on Q3 maps
793 if (t) this.team = crc16(false, t);
797 settouch(this, door_touch);
799 // LinkDoors can't be done until all of the doors have been spawned, so
800 // the sizes can be detected properly.
801 InitializeEntity(this, LinkDoors, INITPRIO_LINKDOORS);
803 this.reset = door_reset;
808 NET_HANDLE(ENT_CLIENT_DOOR, bool isnew)
812 if(sf & SF_TRIGGER_INIT)
814 this.classname = strzone(ReadString());
815 this.spawnflags = ReadByte();
817 this.mdl = strzone(ReadString());
818 _setmodel(this, this.mdl);
820 trigger_common_read(this, true);
822 this.pos1 = ReadVector();
823 this.pos2 = ReadVector();
825 this.size = ReadVector();
827 this.wait = ReadShort();
828 this.speed = ReadShort();
829 this.lip = ReadByte();
830 this.state = ReadByte();
831 this.ltime = ReadCoord();
833 this.solid = SOLID_BSP;
834 set_movetype(this, MOVETYPE_PUSH);
839 if(this.spawnflags & DOOR_START_OPEN)
840 door_init_startopen(this);
842 this.move_time = time;
843 set_movetype(this, MOVETYPE_PUSH);
846 if(sf & SF_TRIGGER_RESET)
851 if(sf & SF_TRIGGER_UPDATE)
853 this.origin = ReadVector();
854 setorigin(this, this.origin);
856 this.pos1 = ReadVector();
857 this.pos2 = ReadVector();