1 /*
2 * XPilot NG, a multiplayer space war game.
3 *
4 * Copyright (C) 1991-2001 by
5 *
6 * Bj�rn Stabell <bjoern@xpilot.org>
7 * Ken Ronny Schouten <ken@xpilot.org>
8 * Bert Gijsbers <bert@xpilot.org>
9 * Dick Balaska <dick@xpilot.org>
10 *
11 * Copyright (C) 2003-2004 Kristian S�derblom <kps@users.sourceforge.net>
12 *
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License
24 * along with this program; if not, write to the Free Software
25 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
26 */
27
28 #include "xpserver.h"
29
30 #define MISSILE_POWER_SPEED_FACT 0.25
31 #define MISSILE_POWER_TURNSPEED_FACT 0.75
32 #define MINI_TORPEDO_SPREAD_TIME 6
33 #define MINI_TORPEDO_SPREAD_SPEED 20
34 #define MINI_TORPEDO_SPREAD_ANGLE 90
35 #define MINI_MINE_SPREAD_TIME 18
36 #define MINI_MINE_SPREAD_SPEED 8
37 #define MINI_MISSILE_SPREAD_ANGLE 45
38
39 #define CONFUSED_UPDATE_GRANULARITY 10
40
41
42 /***********************
43 * Functions for shots.
44 */
45
Player_can_place_mine(player_t * pl)46 static inline bool Player_can_place_mine(player_t *pl)
47 {
48 if (pl->item[ITEM_MINE] <= 0)
49 return false;
50 if (Player_is_phasing(pl))
51 return false;
52 if (BIT(pl->used, HAS_SHIELD)
53 && !options.shieldedMining)
54 return false;
55 return true;
56 }
57
Place_mine(player_t * pl)58 void Place_mine(player_t *pl)
59 {
60 vector_t zero_vel = { 0.0, 0.0 };
61
62 if (!Player_can_place_mine(pl))
63 return;
64
65 if (options.minMineSpeed > 0) {
66 Place_moving_mine(pl);
67 return;
68 }
69
70 /*
71 * Dropped mines are immune to gravity. The latest theory is that mines
72 * contain some sort of anti-gravity device. An even later theory
73 * claims that they are anchored to space the same way as the walls are.
74 */
75 Place_general_mine(pl->id, pl->team, 0, pl->pos, zero_vel, pl->mods);
76 }
77
78
Place_moving_mine(player_t * pl)79 void Place_moving_mine(player_t *pl)
80 {
81 vector_t vel = pl->vel;
82
83 if (!Player_can_place_mine(pl))
84 return;
85
86 if (options.minMineSpeed > 0) {
87 if (pl->velocity < options.minMineSpeed) {
88 if (pl->velocity >= 1) {
89 vel.x *= (options.minMineSpeed / pl->velocity);
90 vel.y *= (options.minMineSpeed / pl->velocity);
91 } else {
92 vel.x = options.minMineSpeed * tcos(pl->dir);
93 vel.y = options.minMineSpeed * tsin(pl->dir);
94 }
95 }
96 }
97
98 Place_general_mine(pl->id, pl->team, GRAVITY, pl->pos, vel, pl->mods);
99 }
100
Place_general_mine(int id,int team,int status,clpos_t pos,vector_t vel,modifiers_t mods)101 void Place_general_mine(int id, int team, int status,
102 clpos_t pos, vector_t vel, modifiers_t mods)
103 {
104 int used, i, minis;
105 double life, drain, mass;
106 vector_t mv;
107 player_t *pl = Player_by_id(id);
108 cannon_t *cannon = Cannon_by_id(id);
109
110 if (NumObjs + Mods_get(mods, ModsMini) >= MAX_TOTAL_SHOTS)
111 return;
112
113 pos = World_wrap_clpos(pos);
114
115 if (pl && Player_is_killed(pl))
116 life = rfrac() * 12;
117 else if (BIT(status, FROMCANNON))
118 life = Cannon_get_shot_life(cannon);
119 else
120 life = options.mineLife;
121
122 if (!Mods_get(mods, ModsCluster))
123 Mods_set(&mods, ModsVelocity, 0);
124 if (!Mods_get(mods, ModsMini))
125 Mods_set(&mods, ModsSpread, 0);
126
127 if (options.nukeMinSmarts <= 0)
128 Mods_set(&mods, ModsNuclear, 0);
129 if (Mods_get(mods, ModsNuclear)) {
130 if (pl) {
131 used = ((Mods_get(mods, ModsNuclear) & MODS_FULLNUCLEAR)
132 ? pl->item[ITEM_MINE]
133 : options.nukeMinMines);
134 if (pl->item[ITEM_MINE] < options.nukeMinMines) {
135 Set_player_message_f(pl,
136 "You need at least %d mines to %s %s!",
137 options.nukeMinMines,
138 (BIT(status, GRAVITY) ? "throw" : "drop"),
139 Describe_shot (OBJ_MINE, status, mods, 0));
140 return;
141 }
142 } else
143 used = options.nukeMinMines;
144 mass = MINE_MASS * used * NUKE_MASS_MULT;
145 } else {
146 mass = (BIT(status, FROMCANNON) ? MINE_MASS * 0.6 : MINE_MASS);
147 used = 1;
148 }
149
150 if (pl) {
151 drain = ED_MINE;
152 if (Mods_get(mods, ModsCluster))
153 drain += CLUSTER_MASS_DRAIN(mass);
154 if (pl->fuel.sum < -drain) {
155 Set_player_message_f(pl,
156 "You need at least %.1f fuel units to %s %s!",
157 -drain, (BIT(status, GRAVITY) ? "throw" : "drop"),
158 Describe_shot(OBJ_MINE, status, mods, 0));
159 return;
160 }
161 if (options.baseMineRange) {
162 for (i = 0; i < NumPlayers; i++) {
163 player_t *pl_i = Player_by_index(i);
164
165 if (pl_i->home_base == NULL)
166 continue;
167 if (pl_i->id != pl->id
168 && !Team_immune(pl_i->id, pl->id)
169 && !Player_is_tank(pl_i)) {
170 if (Wrap_length(pos.cx - pl_i->home_base->pos.cx,
171 pos.cy - pl_i->home_base->pos.cy)
172 <= options.baseMineRange * BLOCK_CLICKS) {
173 Set_player_message(pl, "No base mining!");
174 return;
175 }
176 }
177 }
178 }
179 Player_add_fuel(pl, drain);
180 pl->item[ITEM_MINE] -= used;
181
182 if (used > 1) {
183 Set_message_f("%s has %s %s!", pl->name,
184 (BIT(status, GRAVITY) ? "thrown" : "dropped"),
185 Describe_shot(OBJ_MINE, status, mods, 0));
186 sound_play_all(NUKE_LAUNCH_SOUND);
187 } else
188 sound_play_sensors(pl->pos,
189 BIT(status, GRAVITY)
190 ? DROP_MOVING_MINE_SOUND : DROP_MINE_SOUND);
191 }
192
193 minis = Mods_get(mods, ModsMini) + 1;
194 SET_BIT(status, OWNERIMMUNE);
195
196 for (i = 0; i < minis; i++) {
197 mineobject_t *mine;
198
199 if ((mine = MINE_PTR(Object_allocate())) == NULL)
200 break;
201
202 mine->type = OBJ_MINE;
203 mine->color = BLUE;
204 mine->fuse = options.mineFuseTicks > 0 ? options.mineFuseTicks : -1;
205 mine->obj_status = status;
206 mine->id = (pl ? pl->id : NO_ID);
207 mine->team = team;
208 mine->mine_owner = mine->id;
209 mine->mine_count = 0.0;
210 Object_position_init_clpos(OBJ_PTR(mine), pos);
211 if (minis > 1) {
212 int space = RES/minis, dir;
213 double spread = (double)(Mods_get(mods, ModsSpread) + 1);
214
215 /*
216 * Dir gives (S is ship upwards);
217 *
218 * o o o
219 * X2: o S o X3: S X4: S
220 * o o o o
221 */
222 dir = (i * space) + space/2 + (minis-2)*(RES/2) + (pl?pl->dir:0);
223 dir += (int)((rfrac() - 0.5) * space * 0.5);
224 dir = MOD2(dir, RES);
225 mv.x = MINI_MINE_SPREAD_SPEED * tcos(dir) / spread;
226 mv.y = MINI_MINE_SPREAD_SPEED * tsin(dir) / spread;
227 /*
228 * This causes the added initial velocity to reduce to
229 * zero over the MINI_MINE_SPREAD_TIME.
230 */
231 mine->mine_spread_left = MINI_MINE_SPREAD_TIME;
232 mine->acc.x = -mv.x / MINI_MINE_SPREAD_TIME;
233 mine->acc.y = -mv.y / MINI_MINE_SPREAD_TIME;
234 } else {
235 mv.x = mv.y = mine->acc.x = mine->acc.y = 0.0;
236 mine->mine_spread_left = 0;
237 }
238 mine->vel = mv;
239 mine->vel.x += vel.x * MINE_SPEED_FACT;
240 mine->vel.y += vel.y * MINE_SPEED_FACT;
241 mine->mass = mass / minis;
242 mine->life = life / minis;
243 mine->mods = mods;
244 mine->pl_range = (int)(MINE_RANGE / minis);
245 mine->pl_radius = MINE_RADIUS;
246 Cell_add_object(OBJ_PTR(mine));
247 }
248 }
249
250 /*
251 * Up to and including 3.2.6 it was:
252 * Cause all of the given player's dropped/thrown mines to explode.
253 * Since this caused a slowdown when many mines detonated it
254 * is changed into:
255 * Cause the mine which is closest to a player and owned
256 * by that player to detonate.
257 */
Detonate_mines(player_t * pl)258 void Detonate_mines(player_t *pl)
259 {
260 int i, closest = -1;
261 double dist, min_dist = world->hypotenuse * CLICK + 1;
262
263 if (Player_is_phasing(pl))
264 return;
265
266 for (i = 0; i < NumObjs; i++) {
267 object_t *mine = Obj[i];
268
269 if (! (mine->type == OBJ_MINE))
270 continue;
271 /*
272 * Mines which have been ECM reprogrammed should only be detonatable
273 * by the reprogrammer, not by the original mine placer:
274 */
275 if (mine->id == pl->id) {
276 dist = Wrap_length(pl->pos.cx - mine->pos.cx,
277 pl->pos.cy - mine->pos.cy);
278 if (dist < min_dist) {
279 min_dist = dist;
280 closest = i;
281 }
282 }
283 }
284 if (closest != -1)
285 Obj[closest]->life = 0;
286
287 return;
288 }
289
290 /*
291 * Describes shot of 'type' which has 'status' and 'mods'. If 'hit' is
292 * non-zero this description is part of a collision, otherwise its part
293 * of a launch message.
294 */
Describe_shot(int type,int status,modifiers_t mods,int hit)295 char *Describe_shot(int type, int status, modifiers_t mods, int hit)
296 {
297 const char *name, *howmany = "a ", *plural = "";
298 static char msg[MSG_LEN];
299
300 switch (type) {
301 case OBJ_MINE:
302 if (BIT(status, GRAVITY))
303 name = "bomb";
304 else
305 name = "mine";
306 break;
307 case OBJ_SMART_SHOT:
308 name = "smart missile";
309 break;
310 case OBJ_TORPEDO:
311 name = "torpedo";
312 break;
313 case OBJ_HEAT_SHOT:
314 name = "heatseeker";
315 break;
316 case OBJ_CANNON_SHOT:
317 if (Mods_get(mods, ModsCluster)) {
318 howmany = "";
319 name = "flak";
320 } else
321 name = "shot";
322 break;
323 default:
324 /*
325 * Cluster shots are actual debris from a cluster explosion
326 * so we describe it as "cluster debris".
327 */
328 if (Mods_get(mods, ModsCluster)) {
329 howmany = "";
330 name = "debris";
331 } else
332 name = "shot";
333 break;
334 }
335
336 if (Mods_get(mods, ModsMini) && !hit) {
337 howmany = "some ";
338 plural = (type == OBJ_TORPEDO) ? "es" : "s";
339 }
340
341 sprintf (msg, "%s%s%s%s%s%s%s%s%s",
342 howmany,
343 ((Mods_get(mods, ModsVelocity)
344 || Mods_get(mods, ModsSpread)
345 || Mods_get(mods, ModsPower)) ? "modified " : ""),
346 (Mods_get(mods, ModsMini) ? "mini " : ""),
347 ((Mods_get(mods, ModsNuclear) & MODS_FULLNUCLEAR) ? "full " : ""),
348 ((Mods_get(mods, ModsNuclear) & MODS_NUCLEAR) ? "nuclear " : ""),
349 (Mods_get(mods, ModsImplosion) ? "imploding " : ""),
350 (Mods_get(mods, ModsCluster) ? "cluster " : ""),
351 name,
352 plural);
353
354 return msg;
355 }
356
Player_can_fire_shot(player_t * pl)357 static inline bool Player_can_fire_shot(player_t *pl)
358 {
359 if (pl->shots >= options.maxPlayerShots
360 || BIT(pl->used, HAS_SHIELD)
361 || Player_is_phasing(pl))
362 return false;
363 return true;
364 }
365
Fire_main_shot(player_t * pl,int type,int dir)366 void Fire_main_shot(player_t *pl, int type, int dir)
367 {
368 clpos_t m_gun, pos;
369
370 if (!Player_can_fire_shot(pl))
371 return;
372
373 m_gun = Ship_get_m_gun_clpos(pl->ship, pl->dir);
374 pos.cx = pl->pos.cx + m_gun.cx;
375 pos.cy = pl->pos.cy + m_gun.cy;
376
377 Fire_general_shot(pl->id, pl->team, pos, type,
378 dir, pl->mods, NO_ID);
379 }
380
Fire_shot(player_t * pl,int type,int dir)381 void Fire_shot(player_t *pl, int type, int dir)
382 {
383 if (!Player_can_fire_shot(pl))
384 return;
385
386 Fire_general_shot(pl->id, pl->team, pl->pos, type,
387 dir, pl->mods, NO_ID);
388 }
389
Fire_left_shot(player_t * pl,int type,int dir,int gun)390 void Fire_left_shot(player_t *pl, int type, int dir, int gun)
391 {
392 clpos_t l_gun, pos;
393
394 if (!Player_can_fire_shot(pl))
395 return;
396
397 l_gun = Ship_get_l_gun_clpos(pl->ship, gun, pl->dir);
398 pos.cx = pl->pos.cx + l_gun.cx;
399 pos.cy = pl->pos.cy + l_gun.cy;
400
401 Fire_general_shot(pl->id, pl->team, pos, type,
402 dir, pl->mods, NO_ID);
403 }
404
Fire_right_shot(player_t * pl,int type,int dir,int gun)405 void Fire_right_shot(player_t *pl, int type, int dir, int gun)
406 {
407 clpos_t r_gun, pos;
408
409 if (!Player_can_fire_shot(pl))
410 return;
411
412 r_gun = Ship_get_r_gun_clpos(pl->ship, gun, pl->dir);
413 pos.cx = pl->pos.cx + r_gun.cx;
414 pos.cy = pl->pos.cy + r_gun.cy;
415
416 Fire_general_shot(pl->id, pl->team, pos, type,
417 dir, pl->mods, NO_ID);
418 }
419
Fire_left_rshot(player_t * pl,int type,int dir,int gun)420 void Fire_left_rshot(player_t *pl, int type, int dir, int gun)
421 {
422 clpos_t l_rgun, pos;
423
424 if (!Player_can_fire_shot(pl))
425 return;
426
427 l_rgun = Ship_get_l_rgun_clpos(pl->ship, gun, pl->dir);
428 pos.cx = pl->pos.cx + l_rgun.cx;
429 pos.cy = pl->pos.cy + l_rgun.cy;
430
431 Fire_general_shot(pl->id, pl->team, pos, type,
432 dir, pl->mods, NO_ID);
433 }
434
Fire_right_rshot(player_t * pl,int type,int dir,int gun)435 void Fire_right_rshot(player_t *pl, int type, int dir, int gun)
436 {
437 clpos_t r_rgun, pos;
438
439 if (!Player_can_fire_shot(pl))
440 return;
441
442 r_rgun = Ship_get_r_rgun_clpos(pl->ship, gun, pl->dir);
443 pos.cx = pl->pos.cx + r_rgun.cx;
444 pos.cy = pl->pos.cy + r_rgun.cy;
445
446 Fire_general_shot(pl->id, pl->team, pos, type,
447 dir, pl->mods, NO_ID);
448 }
449
Fire_general_shot(int id,int team,clpos_t pos,int type,int dir,modifiers_t mods,int target_id)450 void Fire_general_shot(int id, int team,
451 clpos_t pos, int type, int dir,
452 modifiers_t mods, int target_id)
453 {
454 int used, fuse = 0, lock = 0, status = GRAVITY, i, ldir, minis;
455 int pl_range, pl_radius, rack_no = 0, racks_left = 0, r, on_this_rack = 0;
456 int side = 0, fired = 0;
457 double drain, mass = options.shotMass, life = options.shotLife;
458 double speed = options.shotSpeed, turnspeed = 0, max_speed = SPEED_LIMIT;
459 double angle, spread;
460 vector_t mv;
461 clpos_t shotpos;
462 object_t *mini_objs[MODS_MINI_MAX + 1];
463 torpobject_t *torp;
464 player_t *pl = Player_by_id(id);
465 cannon_t *cannon = Cannon_by_id(id);
466
467 if (NumObjs >= MAX_TOTAL_SHOTS)
468 return;
469
470 if (!Mods_get(mods, ModsCluster))
471 Mods_set(&mods, ModsVelocity, 0);
472 if (!Mods_get(mods, ModsMini))
473 Mods_set(&mods, ModsSpread, 0);
474
475 if (cannon) {
476 mass = CANNON_SHOT_MASS;
477 speed = Cannon_get_shot_speed(cannon);
478 life = Cannon_get_shot_life(cannon);
479 SET_BIT(status, FROMCANNON);
480 }
481
482 switch (type) {
483 default:
484 return;
485
486 case OBJ_SHOT:
487 Mods_clear(&mods); /* Shots can't be modified! */
488 /* FALLTHROUGH */
489 case OBJ_CANNON_SHOT:
490 pl_range = pl_radius = 0;
491 if (pl) {
492 if (pl->fuel.sum < -ED_SHOT)
493 return;
494 Player_add_fuel(pl, ED_SHOT);
495 sound_play_sensors(pl->pos, FIRE_SHOT_SOUND);
496 Rank_fire_shot(pl);
497 }
498 if (!options.shotsGravity)
499 CLR_BIT(status, GRAVITY);
500 break;
501
502 case OBJ_SMART_SHOT:
503 case OBJ_HEAT_SHOT:
504 if (type == OBJ_HEAT_SHOT
505 ? !options.allowHeatSeekers : !options.allowSmartMissiles) {
506 if (options.allowTorpedoes)
507 type = OBJ_TORPEDO;
508 else
509 return;
510 }
511 /* FALLTHROUGH */
512 case OBJ_TORPEDO:
513 /*
514 * Make sure there are enough object entries for the mini shots.
515 */
516 if (NumObjs + Mods_get(mods, ModsMini) >= MAX_TOTAL_SHOTS)
517 return;
518
519 if (pl && pl->item[ITEM_MISSILE] <= 0)
520 return;
521
522 if (options.nukeMinSmarts <= 0)
523 Mods_set(&mods, ModsNuclear, 0);
524 if (Mods_get(mods, ModsNuclear)) {
525 if (pl) {
526 used = ((Mods_get(mods, ModsNuclear) & MODS_FULLNUCLEAR)
527 ? pl->item[ITEM_MISSILE]
528 : options.nukeMinSmarts);
529 if (pl->item[ITEM_MISSILE] < options.nukeMinSmarts) {
530 Set_player_message_f(pl,
531 "You need at least %d missiles to fire %s!",
532 options.nukeMinSmarts,
533 Describe_shot (type, status, mods, 0));
534 return;
535 }
536 } else
537 used = options.nukeMinSmarts;
538 mass = MISSILE_MASS * used * NUKE_MASS_MULT;
539 pl_range = (type == OBJ_TORPEDO)
540 ? (int)NUKE_RANGE : MISSILE_RANGE;
541 } else {
542 mass = MISSILE_MASS;
543 used = 1;
544 pl_range = (type == OBJ_TORPEDO)
545 ? (int)TORPEDO_RANGE : MISSILE_RANGE;
546 }
547 pl_range /= Mods_get(mods, ModsMini) + 1;
548 pl_radius = MISSILE_LEN;
549
550 drain = used * ED_SMART_SHOT;
551 if (Mods_get(mods, ModsCluster)) {
552 if (pl)
553 drain += CLUSTER_MASS_DRAIN(mass);
554 }
555
556 if (pl && Player_is_killed(pl))
557 life = rfrac() * 12;
558 else if (!cannon)
559 life = options.missileLife;
560
561 switch (type) {
562 case OBJ_HEAT_SHOT:
563 #ifndef HEAT_LOCK
564 lock = NO_ID;
565 #else /* HEAT_LOCK */
566 if (pl == NULL)
567 lock = target_id;
568 else {
569 if (!BIT(pl->lock.tagged, LOCK_PLAYER)
570 || ((pl->lock.distance > pl->sensor_range)
571 && BIT(world->rules->mode, LIMITED_VISIBILITY))) {
572 lock = NO_ID;
573 } else
574 lock = pl->lock.pl_id;
575 }
576 #endif /* HEAT_LOCK */
577 if (pl)
578 sound_play_sensors(pl->pos, FIRE_HEAT_SHOT_SOUND);
579 max_speed = SMART_SHOT_MAX_SPEED * HEAT_SPEED_FACT;
580 turnspeed = SMART_TURNSPEED * HEAT_SPEED_FACT;
581 speed *= HEAT_SPEED_FACT;
582 break;
583
584 case OBJ_SMART_SHOT:
585 if (pl == NULL)
586 lock = target_id;
587 else {
588 if (!BIT(pl->lock.tagged, LOCK_PLAYER)
589 || ((pl->lock.distance > pl->sensor_range)
590 && BIT(world->rules->mode, LIMITED_VISIBILITY))
591 || !pl->visibility[GetInd(pl->lock.pl_id)].canSee)
592 return;
593 lock = pl->lock.pl_id;
594 }
595 max_speed = SMART_SHOT_MAX_SPEED;
596 turnspeed = SMART_TURNSPEED;
597 break;
598
599 case OBJ_TORPEDO:
600 lock = NO_ID;
601 fuse = 8;
602 break;
603
604 default:
605 break;
606 }
607
608 if (pl) {
609 if (pl->fuel.sum < -drain) {
610 Set_player_message_f(pl,
611 "You need at least %.1f fuel units to fire %s!",
612 -drain, Describe_shot(type, status, mods, 0));
613 return;
614 }
615 Player_add_fuel(pl, drain);
616 pl->item[ITEM_MISSILE] -= used;
617
618 if (used > 1) {
619 Set_message_f("%s has launched %s!", pl->name,
620 Describe_shot(type, status, mods, 0));
621 sound_play_all(NUKE_LAUNCH_SOUND);
622 } else if (type == OBJ_SMART_SHOT)
623 sound_play_sensors(pl->pos, FIRE_SMART_SHOT_SOUND);
624 else if (type == OBJ_TORPEDO)
625 sound_play_sensors(pl->pos, FIRE_TORPEDO_SOUND);
626 }
627 break;
628 }
629
630 minis = (Mods_get(mods, ModsMini) + 1);
631 speed *= (1 + (Mods_get(mods, ModsPower)
632 * MISSILE_POWER_SPEED_FACT));
633 max_speed *= (1 + (Mods_get(mods, ModsPower)
634 * MISSILE_POWER_SPEED_FACT));
635 turnspeed *= (1 + (Mods_get(mods, ModsPower)
636 * MISSILE_POWER_TURNSPEED_FACT));
637 spread = (double)(Mods_get(mods, ModsSpread) + 1);
638 /*
639 * Calculate the maximum time it would take to cross one ships width,
640 * don't fuse the shot/missile/torpedo for the owner only until that
641 * time passes. This is a hack to stop various odd missile and shot
642 * mounting points killing the player when they're firing.
643 */
644 fuse += (int)((2.0 * (double)SHIP_SZ) / speed + 1.0);
645
646 /*
647 * Missile Racks and Spread
648 * ------------------------
649 *
650 * A short story by H. J. Thompson
651 *
652 * Once upon a time, back in the "good old days" of XPilot, it was
653 * relatively easy thing to remember the few keys needed to fly and shoot.
654 * It was the day of Sopwith Camels biplanes, albeit triangular ones,
655 * doing close to-the-death machine gun combat with other triangular
656 * Red Barons, the hard vacuum of space whistling silently by as only
657 * something that doesn't exist could do (this was later augmented by
658 * artificial aural feedback devices on certain advanced hardware).
659 *
660 * Eventually the weapon designers came up with "smart" missiles, and
661 * another key was added to the control board, causing one missile to
662 * launch straight forwards from the front of the triangular ship.
663 * Soon other types of missiles were added, including "heat" seekers,
664 * and fast straight travelling "torpedoes" (hark, is that the sonorous
665 * ping-ping-ping of sonar equipment I hear?).
666 *
667 * Then one day along came a certain fellow who thought, among other
668 * things, that it would be neat to fire up to four missiles with one
669 * key press, just so the enemy pilot would be scared witless by the
670 * sudden appearance of four missiles hot on their tail. To make things
671 * fair these "mini" missiles would have the same total damage of a
672 * normal missile, but would travel at the speed of a normal missile.
673 *
674 * However this fellow mused that simply launching all the missiles in
675 * the same direction and from the same point would cause the missiles
676 * to appear on top of each other. Thus he added code to "spread" the
677 * missiles out at various angular offsets from the ship. Indeed the
678 * angular offsets could be controlled using a spread modifier, and yet
679 * more keys appeared on a now crowded control desk.
680 *
681 * Interestingly the future would see the same fellow adding a two seater
682 * variant of the standard single seater ship, allowing one person
683 * to concentrate on flying the ship, while another could flick through
684 * out-of-date manuals searching for the right key combinations on
685 * the now huge console which would launch four full nuclear slow-cluster
686 * imploding mini super speed close spread torpedoes at the currently
687 * targetted enemy, and then engage emergency thrust and shields before
688 * the ominous looking tri-winged dagger ship recoiled at high velocity
689 * into a rocky wall half way across the other side of the universe.
690 *
691 * Back to our story, and this same fellow was musing at the design of
692 * multiple "mini" missiles, and noted that the angle of launch would
693 * also require a different launch point on the ship (actually it was
694 * the same position as if the front of the ship was rotated to point in
695 * the direction of missile launch, mainly because it was easier to
696 * write the launch/guidance computer software that way).
697 *
698 * Later, some artistically (or sadistically) minded person decided that
699 * triangular ships just didn't look good (even though they were very
700 * spatially dynamic, cheap and easy to build), and wouldn't it be just
701 * fantastic if one could have a ship shaped like a banana! Sensibly,
702 * however, he restricted missiles and guns to the normal single frontal
703 * launching point.
704 *
705 * A few weeks later, somebody else decided how visually pleasing it
706 * would be if one could design where missiles could be fired from by
707 * adding "missile rack" points on the ship. Up to four racks were
708 * available, and missiles would fire from exactly these points on the
709 * ship. Since one to four missiles could be fired in one go, the
710 * combinations with various ship designs were numerous (16).
711 *
712 * What would happen if somebody fired four missiles in one go, from a
713 * ship that only had three missile racks? How about two missiles from
714 * one with four racks? Sadly the missile launch software hadn't been
715 * designed to take this sort of thing into account, and incredibly the
716 * original programmer wasn't notified until after First Customer Ship
717 * [sic], the launch software only slightly modified by the ship
718 * designer, who didn't know the first thing about launch acceleration
719 * curves or electronic owner immunity fuse timers.
720 *
721 * Pilots found their missiles were being fired from random points and
722 * in sometimes very odd directions, occasionally even destroying the
723 * ship without trace, severely annoying the ship's owners and several
724 * insurance underwriters. Not soon after several ship designers were
725 * mysteriously killed in a freak "accident" involving a stray nuclear
726 * cluster bomb, and the remaining ship designers became very careful
727 * to place missile racks and extra gun turrets well away from the
728 * ship's superstructure.
729 *
730 * The original programmer who invented multiple "mini" spreading
731 * missiles quickly decided to revisit his code before any "accidents"
732 * came his way, and spent a good few hours making sure one couldn't
733 * shoot oneself in the "foot", and that missiles where launched in some
734 * reasonable and sensible directions based on the position of the
735 * missile racks.
736 *
737 * How It Actually Works
738 * ---------------------
739 *
740 * The first obstacle is getting the right number of missiles fired
741 * from each combination of missile rack configurations;
742 *
743 *
744 * Minis 1 2 3 4
745 * Racks
746 * 1 1 2 3 4
747 *
748 * 2 1/- 1/1 2/1 2/2
749 * -/1 1/2
750 *
751 * 3 1/-/- 1/1/- 1/1/1 2/1/1
752 * -/1/- -/1/1 1/2/1
753 * -/-/1 1/-/1 1/1/2
754 *
755 * 4 1/-/-/- 1/1/-/- 1/1/1/- 1/1/1/1
756 * -/1/-/- -/1/1/- -/1/1/1
757 * -/-/1/- -/-/1/1 1/-/1/1
758 * -/-/-/1 1/-/-/1 1/1/-/1
759 *
760 * To read; For example with 2 Minis and 3 Racks, the first round will
761 * fire 1/1/-, which is one missile from left and middle racks. The
762 * next time fired will be -/1/1; middle and right, next fire is
763 * 1/-/1; left and right. Next time goes to the beggining state.
764 *
765 * Comment Point 1
766 * ---------------
767 *
768 * The *starting* rack number for each salvo cycles through the number
769 * of missiles racks. This is stored in the player variable
770 * 'pl->missile_rack', and is only incremented after each salvo (not
771 * after each mini missile is fired). This value is used to initialise
772 * 'rack_no', which stores the current rack that missiles are fired from.
773 *
774 * 'on_this_rack' is computed to be the number of missiles that will be
775 * fired from 'rack_no', and 'r' is used as a counter to this value.
776 *
777 * 'racks_left' count how many unused missiles racks are left on the ship
778 * in this mini missile salvo.
779 *
780 * Comment Point 2
781 * ---------------
782 *
783 * When 'r' reaches 'on_this_rack' all the missiles have been fired for
784 * this rack, and the next rack should be used. 'rack_no' is incremented
785 * modulo the number of available racks, and 'racks_left' is decremented.
786 * At this point 'on_this_rack' is recomputed for the next rack, and 'r'
787 * reset to zero. Thus initially these two variables are both zero, and
788 * 'rack_no' is one less, such that these variables can be computed inside
789 * the loop to make the code simpler.
790 *
791 * The computation of 'on_this_rack' is as follows; Given that there
792 * are M missiles and R racks remaining;
793 *
794 * on_this_rack = int(M / R); (ie. round down to lowest int)
795 *
796 * Then;
797 *
798 * (M - on_this_rack) / (R - 1) < (M / R).
799 *
800 * That is, the number of missiles fired on the next rack will be
801 * more precise, and trivially can be seen that when R is 1, will
802 * give an exact number of missiles to fire on the last rack.
803 *
804 * In the code 'M' is (minis - i), and 'R' is racks_left.
805 *
806 * Comment Point 3
807 * ---------------
808 *
809 * In order that multiple missiles fired from one rack do not conincide,
810 * each missile has to be "spread" based on the number of missiles
811 * fired from this rack point.
812 *
813 * This is computed similar to the wide shot code;
814 *
815 * angle = (N - 1 - 2 * i) / (N - 1)
816 *
817 * Where N is the number of shots/missiles to be fired, and i is a counter
818 * from 0 .. N-1.
819 *
820 * i 0 1 2 3
821 * N
822 * 1 0
823 * 2 1 -1
824 * 3 1 0 -1
825 * 4 1 0.333 -0.333 -1
826 *
827 * In this code 'N' is 'on_this_rack'.
828 *
829 * Also the position of the missile rack from the center line of the
830 * ship (stored in 'side') has a linear effect on the angle, such that
831 * a point farthest from the center line contributes the largest angle;
832 *
833 * angle += (side / SHIP_SZ)
834 *
835 * Since the eventual 'angle' value used in the code should be a
836 * percentage of the unmodified launch angle, it should be ranged between
837 * -1.00 and +1.00, and thus the first angle is reduced by 33% and the
838 * second by 66%.
839 *
840 * Contact: harveyt@sco.com
841 */
842
843 if (pl && type != OBJ_SHOT) {
844 /*
845 * Initialise missile rack spread variables. (See Comment Point 1)
846 */
847 on_this_rack = 0;
848 racks_left = pl->ship->num_m_rack;
849 rack_no = pl->missile_rack - 1;
850 if (++pl->missile_rack >= pl->ship->num_m_rack)
851 pl->missile_rack = 0;
852 }
853
854 for (r = 0, i = 0; i < minis; i++, r++) {
855 object_t *shot;
856
857 if ((shot = Object_allocate()) == NULL)
858 break;
859
860 shot->life = life / minis;
861 shot->fuse = fuse;
862 shot->mass = mass / minis;
863 shot->type = type;
864 shot->id = (pl ? pl->id : NO_ID);
865 shot->team = team;
866 shot->color = WHITE;
867
868 /* shot->count = 0;
869 shot->info = lock; */
870 switch (shot->type) {
871 case OBJ_TORPEDO:
872 TORP_PTR(shot)->torp_count = 0;
873 break;
874 case OBJ_HEAT_SHOT:
875 HEAT_PTR(shot)->heat_count = 0;
876 HEAT_PTR(shot)->heat_lock_id = lock;
877 break;
878 case OBJ_SMART_SHOT:
879 SMART_PTR(shot)->smart_count = 0;
880 SMART_PTR(shot)->smart_lock_id = lock;
881 break;
882 default:
883 break;
884 }
885
886 shotpos = pos;
887 if (pl && type != OBJ_SHOT) {
888 clpos_t m_rack;
889 if (r == on_this_rack) {
890 /*
891 * We've fired all the mini missiles for the current rack,
892 * we now move onto the next one. (See Comment Point 2)
893 */
894 on_this_rack = (minis - i) / racks_left--;
895 if (on_this_rack < 1) on_this_rack = 1;
896 if (++rack_no >= pl->ship->num_m_rack)
897 rack_no = 0;
898 r = 0;
899 }
900 m_rack = Ship_get_m_rack_clpos(pl->ship, rack_no, pl->dir);
901 shotpos.cx += m_rack.cx;
902 shotpos.cy += m_rack.cy;
903 /*side = CLICK_TO_PIXEL(pl->ship->m_rack[rack_no][0].cy);*/
904 side = CLICK_TO_PIXEL(
905 Ship_get_m_rack_clpos(pl->ship, rack_no, 0).cy);
906 }
907 shotpos = World_wrap_clpos(shotpos);
908 Object_position_init_clpos(shot, shotpos);
909
910 if (type == OBJ_SHOT || !pl)
911 angle = 0.0;
912 else {
913 /*
914 * Calculate the percentage unmodified launch angle for missiles.
915 * (See Comment Point 3).
916 */
917 if (on_this_rack <= 1)
918 angle = 0.0;
919 else {
920 angle = (double)(on_this_rack - 1 - 2 * r);
921 angle /= (3.0 * (double)(on_this_rack - 1));
922 }
923 angle += (double)(2 * side) / (double)(3 * SHIP_SZ);
924 }
925
926 /*
927 * Torpedoes spread like mines, except the launch direction
928 * is preset over the range +/- MINI_TORPEDO_SPREAD_ANGLE.
929 * (This is not modified by the spread, the initial velocity is)
930 *
931 * Other missiles are just launched in a different direction
932 * which varies over the range +/- MINI_MISSILE_SPREAD_ANGLE,
933 * which the spread modifier varies.
934 */
935 switch (type) {
936 case OBJ_TORPEDO:
937 torp = TORP_PTR(shot);
938
939 angle *= (MINI_TORPEDO_SPREAD_ANGLE / 360.0) * RES;
940 ldir = MOD2(dir + (int)angle, RES);
941 mv.x = MINI_TORPEDO_SPREAD_SPEED * tcos(ldir) / spread;
942 mv.y = MINI_TORPEDO_SPREAD_SPEED * tsin(ldir) / spread;
943 /*
944 * This causes the added initial velocity to reduce to
945 * zero over the MINI_TORPEDO_SPREAD_TIME.
946 * FIX: torpedoes should have the same speed
947 * regardless of minification.
948 */
949 torp->torp_spread_left = MINI_TORPEDO_SPREAD_TIME;
950 torp->acc.x = -mv.x / MINI_TORPEDO_SPREAD_TIME;
951 torp->acc.y = -mv.y / MINI_TORPEDO_SPREAD_TIME;
952 ldir = dir;
953 break;
954
955 default:
956 angle *= (MINI_MISSILE_SPREAD_ANGLE / 360.0) * RES / spread;
957 ldir = MOD2(dir + (int)angle, RES);
958 mv.x = mv.y = shot->acc.x = shot->acc.y = 0;
959 break;
960 }
961
962 /*
963 * Option constantSpeed affects shots' initial velocity.
964 * This lets you accelerate shots nicely.
965 */
966 if (pl && options.constantSpeed) {
967 pl->vel.x += options.constantSpeed * pl->acc.x;
968 pl->vel.y += options.constantSpeed * pl->acc.y;
969 }
970 if (options.ngControls && pl && ldir == pl->dir) {
971 /*
972 * If using "NG controls", use float dir when shooting
973 * straight ahead.
974 */
975 shot->vel.x = mv.x + pl->vel.x + pl->float_dir_cos * speed;
976 shot->vel.y = mv.y + pl->vel.y + pl->float_dir_sin * speed;
977 } else {
978 shot->vel.x = mv.x + (pl ? pl->vel.x : 0.0) + tcos(ldir) * speed;
979 shot->vel.y = mv.y + (pl ? pl->vel.y : 0.0) + tsin(ldir) * speed;
980 }
981 /* remove constantSpeed */
982 if (pl && options.constantSpeed) {
983 pl->vel.x -= options.constantSpeed * pl->acc.x;
984 pl->vel.y -= options.constantSpeed * pl->acc.y;
985 }
986
987 shot->obj_status = status;
988
989 if (shot->type == OBJ_TORPEDO
990 || shot->type == OBJ_HEAT_SHOT
991 || shot->type == OBJ_SMART_SHOT) {
992 missileobject_t *missile = MISSILE_PTR(shot);
993
994 missile->missile_turnspeed = turnspeed;
995 missile->missile_max_speed = max_speed;
996 missile->missile_dir = ldir;
997 }
998
999 shot->mods = mods;
1000 shot->pl_range = pl_range;
1001 shot->pl_radius = pl_radius;
1002 Cell_add_object(shot);
1003
1004 mini_objs[fired] = shot;
1005 fired++;
1006 }
1007
1008 /*
1009 * Recoil must be done instantaneously otherwise ship moves back after
1010 * firing each mini missile.
1011 */
1012 if (pl) {
1013 double dx, dy;
1014
1015 dx = dy = 0;
1016 for (i = 0; i < fired; i++) {
1017 dx += (mini_objs[i]->vel.x - pl->vel.x) * mini_objs[i]->mass;
1018 dy += (mini_objs[i]->vel.y - pl->vel.y) * mini_objs[i]->mass;
1019 }
1020 pl->vel.x -= dx / pl->mass;
1021 pl->vel.y -= dy / pl->mass;
1022 }
1023 }
1024
Can_shoot_normal_shot(player_t * pl)1025 bool Can_shoot_normal_shot(player_t *pl)
1026 {
1027 int i, shot_angle;
1028
1029 /* same test as in Fire_normal_shots */
1030 if (frame_time <= pl->shot_time + options.fireRepeatRate - timeStep + 1e-3)
1031 return false;
1032 else
1033 return true;
1034 }
1035
Fire_normal_shots(player_t * pl)1036 void Fire_normal_shots(player_t *pl)
1037 {
1038 int i, shot_angle;
1039
1040 /* Average non-integer repeat rates, so that smaller gap occurs first.
1041 * 1e-3 "fudge factor" because "should be equal" cases return. */
1042 if (frame_time <= pl->shot_time + options.fireRepeatRate - timeStep + 1e-3)
1043 return;
1044 pl->shot_time = MAX(frame_time, pl->shot_time + options.fireRepeatRate);
1045
1046 shot_angle = MODS_SPREAD_MAX - Mods_get(pl->mods, ModsSpread);
1047
1048 Fire_main_shot(pl, OBJ_SHOT, pl->dir);
1049 for (i = 0; i < pl->item[ITEM_WIDEANGLE]; i++) {
1050 if (pl->ship->num_l_gun > 0) {
1051 Fire_left_shot(pl, OBJ_SHOT, MOD2(pl->dir + (1 + i) * shot_angle,
1052 RES), i % pl->ship->num_l_gun);
1053 }
1054 else {
1055 Fire_main_shot(pl, OBJ_SHOT, MOD2(pl->dir + (1 + i) * shot_angle,
1056 RES));
1057 }
1058 if (pl->ship->num_r_gun > 0) {
1059 Fire_right_shot(pl, OBJ_SHOT, MOD2(pl->dir - (1 + i) * shot_angle,
1060 RES), i % pl->ship->num_r_gun);
1061 }
1062 else {
1063 Fire_main_shot(pl, OBJ_SHOT, MOD2(pl->dir - (1 + i) * shot_angle,
1064 RES));
1065 }
1066 }
1067 for (i = 0; i < pl->item[ITEM_REARSHOT]; i++) {
1068 if ((pl->item[ITEM_REARSHOT] - 1 - 2 * i) < 0) {
1069 if (pl->ship->num_l_rgun > 0) {
1070 Fire_left_rshot(pl, OBJ_SHOT, MOD2(pl->dir + RES/2
1071 + ((pl->item[ITEM_REARSHOT] - 1 - 2 * i) * shot_angle) / 2,
1072 RES), (i - (pl->item[ITEM_REARSHOT] + 1) / 2)
1073 % pl->ship->num_l_rgun);
1074 }
1075 else {
1076 Fire_shot(pl, OBJ_SHOT, MOD2(pl->dir + RES/2
1077 + ((pl->item[ITEM_REARSHOT] - 1 - 2 * i) * shot_angle) / 2,
1078 RES));
1079 }
1080 }
1081 if ((pl->item[ITEM_REARSHOT] - 1 - 2 * i) > 0) {
1082 if (pl->ship->num_r_rgun > 0) {
1083 Fire_right_rshot(pl, OBJ_SHOT, MOD2(pl->dir + RES/2
1084 + ((pl->item[ITEM_REARSHOT] - 1 - 2 * i) * shot_angle) / 2,
1085 RES), (pl->item[ITEM_REARSHOT] / 2 - i - 1)
1086 % pl->ship->num_r_rgun);
1087 }
1088 else {
1089 Fire_shot(pl, OBJ_SHOT, MOD2(pl->dir + RES/2
1090 + ((pl->item[ITEM_REARSHOT] - 1 - 2 * i) * shot_angle) / 2,
1091 RES));
1092 }
1093 }
1094 if ((pl->item[ITEM_REARSHOT] - 1 - 2 * i) == 0)
1095 Fire_shot(pl, OBJ_SHOT, MOD2(pl->dir + RES/2
1096 + ((pl->item[ITEM_REARSHOT] - 1 - 2 * i) * shot_angle) / 2,
1097 RES));
1098 }
1099 }
1100
1101
1102 /* Removes shot from array */
Delete_shot(int ind)1103 void Delete_shot(int ind)
1104 {
1105 object_t *shot = Obj[ind]; /* Used when swapping places */
1106 ballobject_t *ball;
1107 itemobject_t *item;
1108 player_t *pl;
1109 bool addMine = false, addHeat = false, addBall = false;
1110 modifiers_t mods;
1111 int i, intensity, type, color, num_debris, status;
1112 double modv, speed_modv, life_modv, num_modv, mass, min_life, max_life;
1113
1114 switch (shot->type) {
1115
1116 case OBJ_SPARK:
1117 case OBJ_DEBRIS:
1118 case OBJ_WRECKAGE:
1119 break;
1120
1121 case OBJ_ASTEROID:
1122 Break_asteroid(WIRE_PTR(shot));
1123 break;
1124
1125 case OBJ_BALL:
1126 ball = BALL_PTR(shot);
1127 if (ball->id != NO_ID)
1128 Detach_ball(Player_by_id(ball->id), ball);
1129 else {
1130 /*
1131 * Maybe some player is still busy trying to connect to this ball.
1132 */
1133 for (i = 0; i < NumPlayers; i++) {
1134 player_t *pl_i = Player_by_index(i);
1135
1136 if (pl_i->ball == ball)
1137 pl_i->ball = NULL;
1138 }
1139 }
1140 if (ball->ball_owner == NO_ID) {
1141 /*
1142 * If the ball has never been owned, the only way it could
1143 * have been destroyed is by being knocked out of the goal.
1144 * Therefore we force the ball to be recreated.
1145 */
1146 ball->ball_treasure->have = false;
1147 SET_BIT(ball->obj_status, RECREATE);
1148 }
1149 if (BIT(ball->obj_status, RECREATE)) {
1150 addBall = true;
1151 if (BIT(ball->obj_status, NOEXPLOSION))
1152 break;
1153 sound_play_sensors(ball->pos, EXPLODE_BALL_SOUND);
1154
1155 /* The ball could be inside a BallArea, check whether
1156 * the sparks can exist here. Should we set a team? */
1157 if (is_inside(ball->prevpos.cx, ball->prevpos.cy,
1158 NONBALL_BIT | NOTEAM_BIT, OBJ_PTR(ball)) != NO_GROUP)
1159 break;
1160
1161 Make_debris(ball->prevpos,
1162 ball->vel,
1163 ball->id,
1164 ball->team,
1165 OBJ_DEBRIS,
1166 DEBRIS_MASS,
1167 GRAVITY,
1168 RED,
1169 8,
1170 (int)(10 + 10 * rfrac()),
1171 0, RES - 1,
1172 10.0, 50.0,
1173 10.0, 54.0);
1174 }
1175 break;
1176 /* Shots related to a player. */
1177
1178 case OBJ_MINE:
1179 case OBJ_HEAT_SHOT:
1180 case OBJ_TORPEDO:
1181 case OBJ_SMART_SHOT:
1182 case OBJ_CANNON_SHOT:
1183 if (shot->mass == 0)
1184 break;
1185
1186 status = GRAVITY;
1187 if (shot->type == OBJ_MINE)
1188 status |= COLLISIONSHOVE;
1189 if (BIT(shot->obj_status, FROMCANNON))
1190 status |= FROMCANNON;
1191
1192 if (Mods_get(shot->mods, ModsNuclear))
1193 sound_play_all(NUKE_EXPLOSION_SOUND);
1194 else if (shot->type == OBJ_MINE)
1195 sound_play_sensors(shot->pos, MINE_EXPLOSION_SOUND);
1196 else
1197 sound_play_sensors(shot->pos, OBJECT_EXPLOSION_SOUND);
1198
1199 if (Mods_get(shot->mods, ModsCluster)) {
1200 type = OBJ_SHOT;
1201 color = WHITE;
1202 mass = options.shotMass * 3;
1203 modv = 1 << Mods_get(shot->mods, ModsVelocity);
1204 num_modv = 4;
1205 if (Mods_get(shot->mods, ModsNuclear)) {
1206 modv *= 4.0;
1207 num_modv = 1;
1208 }
1209 life_modv = modv * 0.20;
1210 speed_modv = 1.0 / modv;
1211 intensity = (int)CLUSTER_MASS_SHOTS(shot->mass);
1212 } else {
1213 type = OBJ_DEBRIS;
1214 color = RED;
1215 mass = DEBRIS_MASS;
1216 modv = 1;
1217 num_modv = 1;
1218 life_modv = modv;
1219 speed_modv = modv;
1220 if (shot->type == OBJ_MINE)
1221 intensity = 512;
1222 else
1223 intensity = 32;
1224 num_modv /= ((double)(Mods_get(shot->mods, ModsMini) + 1));
1225 }
1226
1227 if (Mods_get(shot->mods, ModsNuclear)) {
1228 double nuke_factor;
1229
1230 if (shot->type == OBJ_MINE)
1231 nuke_factor = NUKE_MINE_EXPL_MULT * shot->mass / MINE_MASS;
1232 else
1233 nuke_factor = NUKE_SMART_EXPL_MULT * shot->mass / MISSILE_MASS;
1234
1235 nuke_factor *= ((Mods_get(shot->mods, ModsMini) + 1)
1236 / SHOT_MULT(shot));
1237 intensity = (int)(intensity * nuke_factor);
1238 }
1239
1240 if (Mods_get(shot->mods, ModsImplosion))
1241 mass = -mass;
1242
1243 if (shot->type == OBJ_TORPEDO
1244 || shot->type == OBJ_HEAT_SHOT
1245 || shot->type == OBJ_SMART_SHOT)
1246 intensity /= (1 + Mods_get(shot->mods, ModsPower));
1247
1248 num_debris = (int)(intensity * num_modv * (0.20 + (0.10 * rfrac())));
1249 min_life = 8 * life_modv;
1250 max_life = (intensity >> 1) * life_modv;
1251
1252 /* Hack - make sure nuke debris don't get insanely long life time. */
1253 if (Mods_get(shot->mods, ModsNuclear))
1254 max_life = options.nukeDebrisLife;
1255
1256 #if 0
1257 warn("type = %16s (%c%c) inten = %-6d num = %-6d life: %.1f - %.1f",
1258 Object_typename(shot),
1259 (Mods_get(shot->mods, ModsNuclear) ? 'N' : '-'),
1260 (Mods_get(shot->mods, ModsCluster) ? 'C' : '-'),
1261 intensity, num_debris, min_life, max_life);
1262 #endif
1263
1264 Make_debris(shot->prevpos,
1265 shot->vel,
1266 shot->id,
1267 shot->team,
1268 type,
1269 mass,
1270 status,
1271 color,
1272 6,
1273 num_debris,
1274 0, RES - 1,
1275 20 * speed_modv, (intensity >> 2) * speed_modv,
1276 min_life, max_life);
1277 break;
1278
1279 case OBJ_SHOT:
1280 if (shot->id == NO_ID
1281 || BIT(shot->obj_status, FROMCANNON)
1282 || Mods_get(shot->mods, ModsCluster))
1283 break;
1284 pl = Player_by_id(shot->id);
1285 if (--pl->shots <= 0)
1286 pl->shots = 0;
1287 break;
1288
1289 case OBJ_PULSE:
1290 if (shot->id == NO_ID
1291 || BIT(shot->obj_status, FROMCANNON))
1292 break;
1293 pl = Player_by_id(shot->id);
1294 if (--pl->num_pulses <= 0)
1295 pl->num_pulses = 0;
1296 break;
1297
1298 /* Special items. */
1299 case OBJ_ITEM:
1300 item = ITEM_PTR(shot);
1301
1302 switch (item->item_type) {
1303
1304 case ITEM_MISSILE:
1305 /* If -timeStep < item->life <= 0, then it died of old age. */
1306 /* If it was picked up, then life was set to 0 and it is now
1307 * -timeStep after the substract in update.c. */
1308 if (-timeStep < item->life && item->life <= 0) {
1309 if (item->color != WHITE) {
1310 item->color = WHITE;
1311 item->life = WARN_TIME;
1312 return;
1313 }
1314 if (rfrac() < options.rogueHeatProb)
1315 addHeat = true;
1316 }
1317 break;
1318
1319 case ITEM_MINE:
1320 /* See comment for ITEM_MISSILE above */
1321 if (-timeStep < item->life && item->life <= 0) {
1322 if (item->color != WHITE) {
1323 item->color = WHITE;
1324 item->life = WARN_TIME;
1325 return;
1326 }
1327 if (rfrac() < options.rogueMineProb)
1328 addMine = true;
1329 }
1330 break;
1331
1332 default:
1333 break;
1334 }
1335
1336 world->items[item->item_type].num--;
1337
1338 break;
1339
1340 default:
1341 xpprintf("%s Delete_shot(): Unknown shot type %d.\n",
1342 showtime(), shot->type);
1343 break;
1344 }
1345
1346 Cell_remove_object(shot);
1347 shot->life = 0;
1348 shot->type = 0;
1349 shot->mass = 0;
1350
1351 Object_free_ind(ind);
1352
1353 if (addMine || addHeat) {
1354 Mods_clear(&mods);
1355 if (rfrac() <= 0.333)
1356 Mods_set(&mods, ModsCluster, 1);
1357 if (rfrac() <= 0.333)
1358 Mods_set(&mods, ModsImplosion, 1);
1359 Mods_set(&mods, ModsVelocity,
1360 (int)(rfrac() * (MODS_VELOCITY_MAX + 1)));
1361 Mods_set(&mods, ModsPower,
1362 (int)(rfrac() * (MODS_POWER_MAX + 1)));
1363 if (addMine) {
1364 long gravity_status = ((rfrac() < 0.5) ? GRAVITY : 0);
1365 vector_t zero_vel = { 0.0, 0.0 };
1366
1367 Place_general_mine(NO_ID, TEAM_NOT_SET, gravity_status,
1368 shot->pos, zero_vel, mods);
1369 }
1370 else if (addHeat)
1371 Fire_general_shot(NO_ID, TEAM_NOT_SET, shot->pos,
1372 OBJ_HEAT_SHOT, (int)(rfrac() * RES),
1373 mods, NO_ID);
1374 }
1375 else if (addBall) {
1376 ball = BALL_PTR(shot);
1377 Make_treasure_ball(ball->ball_treasure);
1378 }
1379 }
1380
1381
1382 /*
1383 * The new ball movement code since XPilot version 3.4.0 as made
1384 * by Bretton Wade. The code was submitted in context diff format
1385 * by Mark Boyns. Here is a an excerpt from a post in
1386 * rec.games.computer.xpilot by Bretton Wade dated 27 Jun 1995:
1387 *
1388 * If I'm not mistaken (not having looked very closely at the code
1389 * because I wasn't sure what it was trying to do), the original move_ball
1390 * routine was trying to model a Hook's law spring, but squared the
1391 * deformation term, which would lead to exagerated behavior as the spring
1392 * stretched too far. Not really a divide by zero, but effectively
1393 * producing large numbers.
1394 *
1395 * When I coded up the spring myself, I found that I could recreate the
1396 * effect by using a VERY strong spring. This can be defeated, however, by
1397 * damping. Specifically, If you compute the critical damping factor, then
1398 * you could have the cable always be the correct length. This makes me
1399 * wonder how to decide when the cable snaps.
1400 *
1401 * I chose a relatively strong spring, and a small damping factor, to make
1402 * for a nice realistic bounce when you grab at the treasure. It also
1403 * gives a fairley close approximation to the "normal" feel of the
1404 * treasure.
1405 *
1406 * I modeled the cable as having zero mass, or at least insignificant mass
1407 * as compared to the ship and ball. This greatly simplifies the math, and
1408 * leads to the conclusion that there will be no change in velocity when
1409 * the cable breaks. You can check this by integrating the momentum along
1410 * the cable, and the ship or ball.
1411 *
1412 * If you assume that the cable snaps in the middle, then half of the
1413 * potential energy goes to each object attached. However, as you said, the
1414 * total momentum of the system cannot change. Because the weight of the
1415 * cable is small, the vast majority of the potential energy will become
1416 * heat. I've had two physicists verify this for me, and they both worked
1417 * really hard on the problem because they found it interesting.
1418 *
1419 * End of post.
1420 *
1421 * Changes since then:
1422 *
1423 * Comment from people was that the string snaps too soon.
1424 * Changed the value (max_spring_ratio) at which the string snaps
1425 * from 0.25 to 0.30. Not sure if that helps enough, or too much.
1426 */
Update_connector_force(ballobject_t * ball)1427 void Update_connector_force(ballobject_t *ball)
1428 {
1429 player_t *pl = Player_by_id(ball->id);
1430 vector_t D;
1431 double length, force, ratio, accell, damping;
1432 /* const double k = 1500.0, b = 2.0; */
1433 /* const double max_spring_ratio = 0.30; */
1434
1435 UNUSED_PARAM(world);
1436
1437 /* no player connected ? */
1438 if (!pl)
1439 return;
1440
1441 /* being connected makes the player visible */
1442 if(pl->forceVisible == 0){
1443 pl->forceVisible =2;
1444 }
1445
1446 /* compute the normalized vector between the ball and the player */
1447 D.x = WRAP_DCX(pl->pos.cx - ball->pos.cx);
1448 D.y = WRAP_DCY(pl->pos.cy - ball->pos.cy);
1449 length = VECTOR_LENGTH(D);
1450 if (length > 0.0) {
1451 D.x /= length;
1452 D.y /= length;
1453 } else
1454 D.x = D.y = 0.0;
1455
1456 /* compute the ratio for the spring action */
1457 ratio = 1 - length / (options.ballConnectorLength * CLICK);
1458
1459 /* compute force by spring for this length */
1460 force = options.ballConnectorSpringConstant * ratio;
1461
1462 /* If we have string-style connectors then it is allowed to be
1463 * shorted than its natural length. */
1464 if (options.connectorIsString && ratio > 0.0)
1465 return;
1466
1467 /* if the tether is too long or too short, release it */
1468 if (ABS(ratio) > options.maxBallConnectorRatio) {
1469 Detach_ball(pl, ball);
1470 return;
1471 }
1472
1473 damping = -options.ballConnectorDamping
1474 * ((pl->vel.x - ball->vel.x) * D.x + (pl->vel.y - ball->vel.y) * D.y);
1475
1476 /* compute accelleration for player, assume t = 1 */
1477 accell = (force + damping) / pl->mass;
1478 pl->vel.x += D.x * accell * timeStep;
1479 pl->vel.y += D.y * accell * timeStep;
1480
1481 /* compute accelleration for ball, assume t = 1 */
1482 accell = (force + damping) / ball->mass;
1483 ball->vel.x += -D.x * accell * timeStep;
1484 ball->vel.y += -D.y * accell * timeStep;
1485 }
1486
Update_torpedo(torpobject_t * torp)1487 void Update_torpedo(torpobject_t *torp)
1488 {
1489 double acc;
1490
1491 UNUSED_PARAM(world);
1492 if (Mods_get(torp->mods, ModsNuclear))
1493 acc = (torp->torp_count < NUKE_SPEED_TIME) ? NUKE_ACC : 0.0;
1494 else
1495 acc = (torp->torp_count < TORPEDO_SPEED_TIME) ? TORPEDO_ACC : 0.0;
1496 torp->torp_count += timeStep;
1497 acc *= (1 + (Mods_get(torp->mods, ModsPower) * MISSILE_POWER_SPEED_FACT));
1498 if ((torp->torp_spread_left -= timeStep) <= 0) {
1499 torp->acc.x = 0;
1500 torp->acc.y = 0;
1501 torp->torp_spread_left = 0;
1502 }
1503 torp->vel.x += acc * tcos(torp->missile_dir);
1504 torp->vel.y += acc * tsin(torp->missile_dir);
1505 }
1506
Update_missile(missileobject_t * missile)1507 void Update_missile(missileobject_t *missile)
1508 {
1509 player_t *pl;
1510 int angle, theta;
1511 double range = 0.0, acc = SMART_SHOT_ACC;
1512 double x_dif = 0.0, y_dif = 0.0, shot_speed, a;
1513
1514 if (missile->type == OBJ_HEAT_SHOT) {
1515 heatobject_t *heat = HEAT_PTR(missile);
1516
1517 acc = SMART_SHOT_ACC * HEAT_SPEED_FACT;
1518 if (heat->heat_lock_id >= 0) {
1519 clpos_t engine;
1520
1521 /* Get player and set min to distance */
1522 pl = Player_by_id(heat->heat_lock_id);
1523 /* kps - bandaid since Player_by_id can return NULL. */
1524 if (!pl)
1525 return;
1526 engine = Ship_get_engine_clpos(pl->ship, pl->dir);
1527 range = Wrap_length(pl->pos.cx + engine.cx - heat->pos.cx,
1528 pl->pos.cy + engine.cy - heat->pos.cy)
1529 / CLICK;
1530 } else {
1531 /* No player. Number of moves so that new target is searched */
1532 pl = NULL;
1533 heat->heat_count = HEAT_WIDE_TIMEOUT + HEAT_WIDE_ERROR;
1534 }
1535 if (pl && Player_is_thrusting(pl)) {
1536 /*
1537 * Target is thrusting,
1538 * set number to moves to correct error value
1539 */
1540 if (range < HEAT_CLOSE_RANGE)
1541 heat->heat_count = HEAT_CLOSE_ERROR;
1542 else if (range < HEAT_MID_RANGE)
1543 heat->heat_count = HEAT_MID_ERROR;
1544 else
1545 heat->heat_count = HEAT_WIDE_ERROR;
1546 } else {
1547 heat->heat_count += timeStep;
1548 /* Look for new target */
1549 if ((range < HEAT_CLOSE_RANGE
1550 && heat->heat_count > HEAT_CLOSE_TIMEOUT + HEAT_CLOSE_ERROR)
1551 || (range < HEAT_MID_RANGE
1552 && heat->heat_count > HEAT_MID_TIMEOUT + HEAT_MID_ERROR)
1553 || heat->heat_count > HEAT_WIDE_TIMEOUT + HEAT_WIDE_ERROR) {
1554 double l;
1555 int i;
1556
1557 range = HEAT_RANGE * (heat->heat_count / HEAT_CLOSE_TIMEOUT);
1558 for (i = 0; i < NumPlayers; i++) {
1559 player_t *pl_i = Player_by_index(i);
1560 clpos_t engine;
1561
1562 if (!Player_is_thrusting(pl_i))
1563 continue;
1564
1565 engine = Ship_get_engine_clpos(pl_i->ship, pl_i->dir);
1566 l = Wrap_length(pl_i->pos.cx + engine.cx - heat->pos.cx,
1567 pl_i->pos.cy + engine.cy - heat->pos.cy)
1568 / CLICK;
1569 /*
1570 * After burners can be detected easier;
1571 * so scale the length:
1572 */
1573 l *= MAX_AFTERBURNER + 1 - pl_i->item[ITEM_AFTERBURNER];
1574 l /= MAX_AFTERBURNER + 1;
1575 if (Player_has_afterburner(pl_i))
1576 l *= 16 - pl_i->item[ITEM_AFTERBURNER];
1577 if (l < range) {
1578 heat->heat_lock_id = pl_i->id;
1579 range = l;
1580 heat->heat_count =
1581 l < HEAT_CLOSE_RANGE ?
1582 HEAT_CLOSE_ERROR : l < HEAT_MID_RANGE ?
1583 HEAT_MID_ERROR : HEAT_WIDE_ERROR;
1584 pl = pl_i;
1585 }
1586 }
1587 }
1588 }
1589 if (heat->heat_lock_id < 0)
1590 return;
1591 /*
1592 * Heat seekers cannot fly exactly, if target is far away or thrust
1593 * isn't active. So simulate the error:
1594 */
1595 x_dif = rfrac() * 4 * heat->heat_count;
1596 y_dif = rfrac() * 4 * heat->heat_count;
1597
1598 }
1599 else if (missile->type == OBJ_SMART_SHOT) {
1600 smartobject_t *smart = SMART_PTR(missile);
1601
1602 /*
1603 * kps - this can cause Arithmetic Exception (division by zero)
1604 * since CONFUSED_UPDATE_GRANULARITY / options.gameSpeed is most often
1605 * < 1 and when it is cast to int it will be 0, and then
1606 * we get frameloops % 0, which is not good.
1607 */
1608 /*if (BIT(smart->obj_status, CONFUSED)
1609 && (!(frame_loops
1610 % (int)(CONFUSED_UPDATE_GRANULARITY / options.gameSpeed)
1611 || smart->smart_count == CONFUSED_TIME))) {*/
1612 /* not going to fix now, I'll just remove the '/ gamespeed' part */
1613
1614 if (BIT(smart->obj_status, CONFUSED)
1615 && (!(frame_loops % CONFUSED_UPDATE_GRANULARITY)
1616 || smart->smart_count == CONFUSED_TIME)) {
1617
1618 if (smart->smart_count > 0) {
1619 smart->smart_lock_id
1620 = Player_by_index((int)(rfrac() * NumPlayers))->id;
1621 smart->smart_count -= timeStep;
1622 } else {
1623 smart->smart_count = 0;
1624 CLR_BIT(smart->obj_status, CONFUSED);
1625
1626 /* range is percentage from center to periphery of ecm burst */
1627 range = (ECM_DISTANCE - smart->smart_ecm_range) / ECM_DISTANCE;
1628 range *= 100.0;
1629
1630 /*
1631 * range% lock%
1632 * 100 100
1633 * 50 75
1634 * 0 50
1635 */
1636 if ((int)(rfrac() * 100) <= ((int)(range/2)+50))
1637 smart->smart_lock_id = smart->smart_relock_id;
1638 }
1639 }
1640 pl = Player_by_id(smart->smart_lock_id);
1641 }
1642 else
1643 /*NOTREACHED*/
1644 return;
1645
1646 /* kps - Player_by_id might return NULL. */
1647 if (!pl)
1648 return;
1649
1650 /*
1651 * Use a little look ahead to fly more exact
1652 */
1653 acc *= (1 + (Mods_get(missile->mods, ModsPower)
1654 * MISSILE_POWER_SPEED_FACT));
1655 if ((shot_speed = VECTOR_LENGTH(missile->vel)) < 1)
1656 shot_speed = 1;
1657 range = Wrap_length(pl->pos.cx - missile->pos.cx,
1658 pl->pos.cy - missile->pos.cy) / CLICK;
1659 x_dif += pl->vel.x * (range / shot_speed);
1660 y_dif += pl->vel.y * (range / shot_speed);
1661 a = Wrap_cfindDir(pl->pos.cx + PIXEL_TO_CLICK(x_dif) - missile->pos.cx,
1662 pl->pos.cy + PIXEL_TO_CLICK(y_dif) - missile->pos.cy);
1663 theta = MOD2((int) (a + 0.5), RES);
1664
1665 {
1666 double x, y, vx, vy;
1667 int i, xi, yi, j, freemax, k, foundw;
1668 static struct {
1669 int dx, dy;
1670 } sur[8] = {
1671 {1,0}, {1,1}, {0,1}, {-1,1}, {-1,0}, {-1,-1}, {0,-1}, {1,-1}
1672 };
1673 blkpos_t sbpos;
1674
1675 #define BLOCK_PARTS 2
1676 vx = missile->vel.x;
1677 vy = missile->vel.y;
1678 x = shot_speed / (BLOCK_SZ*BLOCK_PARTS);
1679 vx /= x; vy /= x;
1680 x = CLICK_TO_PIXEL(missile->pos.cx);
1681 y = CLICK_TO_PIXEL(missile->pos.cy);
1682 foundw = 0;
1683
1684 for (i = SMART_SHOT_LOOK_AH; i > 0 && foundw == 0; i--) {
1685 xi = (int)((x += vx) / BLOCK_SZ);
1686 yi = (int)((y += vy) / BLOCK_SZ);
1687 if (BIT(world->rules->mode, WRAP_PLAY)) {
1688 if (xi < 0) xi += world->x;
1689 else if (xi >= world->x) xi -= world->x;
1690 if (yi < 0) yi += world->y;
1691 else if (yi >= world->y) yi -= world->y;
1692 }
1693 if (xi < 0 || xi >= world->x || yi < 0 || yi >= world->y)
1694 break;
1695
1696 /*
1697 * kps -
1698 * Someone please write polygon based missile navigation code.
1699 */
1700
1701 switch(world->block[xi][yi]) {
1702 case TARGET:
1703 case TREASURE:
1704 case FUEL:
1705 case FILLED:
1706 case REC_LU:
1707 case REC_RU:
1708 case REC_LD:
1709 case REC_RD:
1710 case CANNON:
1711 if (range > (SMART_SHOT_LOOK_AH-i)*(BLOCK_SZ/BLOCK_PARTS)) {
1712 if (shot_speed > SMART_SHOT_MIN_SPEED)
1713 shot_speed -= acc * (SMART_SHOT_DECFACT+1);
1714 }
1715 foundw = 1;
1716 break;
1717 default:
1718 break;
1719 }
1720 }
1721
1722 i = ((int)(missile->missile_dir * 8 / RES)&7) + 8;
1723 sbpos = Clpos_to_blkpos(missile->pos);
1724 xi = sbpos.bx;
1725 yi = sbpos.by;
1726
1727 for (j = 2, angle = -1, freemax = 0; j >= -2; --j) {
1728 int si, xt, yt;
1729
1730 for (si=1, k=0; si >= -1; --si) {
1731 xt = xi + sur[(i+j+si)&7].dx;
1732 yt = yi + sur[(i+j+si)&7].dy;
1733
1734 if (xt >= 0 && xt < world->x && yt >= 0 && yt < world->y) {
1735
1736 switch (world->block[xt][yt]) {
1737 case TARGET:
1738 case TREASURE:
1739 case FUEL:
1740 case FILLED:
1741 case REC_LU:
1742 case REC_RU:
1743 case REC_LD:
1744 case REC_RD:
1745 case CANNON:
1746 if (!si)
1747 k = -32;
1748 break;
1749 default:
1750 ++k;
1751 break;
1752 }
1753 }
1754
1755 }
1756 if (k > freemax
1757 || (k == freemax
1758 && ((j == -1 && (rfrac() < 0.5)) || j == 0 || j == 1))) {
1759 freemax = k > 2 ? 2 : k;
1760 angle = i + j;
1761 }
1762
1763 if (k == 3 && !j) {
1764 angle = -1;
1765 break;
1766 }
1767 }
1768
1769 if (angle >= 0) {
1770 i = angle&7;
1771 a = Wrap_findDir(
1772 (yi + sur[i].dy) * BLOCK_SZ - (CLICK_TO_PIXEL(missile->pos.cy)
1773 + 2 * missile->vel.y),
1774 (xi + sur[i].dx) * BLOCK_SZ - (CLICK_TO_PIXEL(missile->pos.cx)
1775 - 2 * missile->vel.x));
1776 theta = MOD2((int) (a + 0.5), RES);
1777 #ifdef SHOT_EXTRA_SLOWDOWN
1778 if (!foundw && range > (SHOT_LOOK_AH-i) * BLOCK_SZ) {
1779 if (shot_speed
1780 > (SMART_SHOT_MIN_SPEED + SMART_SHOT_MAX_SPEED)/2)
1781 shot_speed -= SMART_SHOT_DECC+SMART_SHOT_ACC;
1782 }
1783 #endif
1784 }
1785 }
1786 angle = theta;
1787
1788 if (angle < 0)
1789 angle += RES;
1790 angle %= RES;
1791
1792 if (angle < missile->missile_dir)
1793 angle += RES;
1794 angle = angle - missile->missile_dir - RES/2;
1795
1796 if (angle < 0)
1797 missile->missile_dir += (u_byte)(((-angle < missile->missile_turnspeed)
1798 ? -angle
1799 : missile->missile_turnspeed));
1800 else
1801 missile->missile_dir -= (u_byte)(((angle < missile->missile_turnspeed)
1802 ? angle
1803 : missile->missile_turnspeed));
1804
1805 missile->missile_dir = MOD2(missile->missile_dir, RES); /* NOTE!!!! */
1806
1807 if (shot_speed < missile->missile_max_speed)
1808 shot_speed += acc;
1809
1810 /* missile->velocity = MIN(missile->velocity, missile->max_speed); */
1811
1812 missile->vel.x = tcos(missile->missile_dir) * shot_speed;
1813 missile->vel.y = tsin(missile->missile_dir) * shot_speed;
1814 }
1815
Update_mine(mineobject_t * mine)1816 void Update_mine(mineobject_t *mine)
1817 {
1818 UNUSED_PARAM(world);
1819
1820 if (BIT(mine->obj_status, CONFUSED)) {
1821 if ((mine->mine_count -= timeStep) <= 0) {
1822 CLR_BIT(mine->obj_status, CONFUSED);
1823 mine->mine_count = 0;
1824 }
1825 }
1826
1827 /*
1828 * If the value of option mineFuseTicks is 0, owner immunity never expires.
1829 * In this case, mine->fuse has the special value -1.
1830 */
1831 if (BIT(mine->obj_status, OWNERIMMUNE) && mine->fuse == 0)
1832 CLR_BIT(mine->obj_status, OWNERIMMUNE);
1833
1834 if (Mods_get(mine->mods, ModsMini)) {
1835 if ((mine->mine_spread_left -= timeStep) <= 0) {
1836 mine->acc.x = 0;
1837 mine->acc.y = 0;
1838 mine->mine_spread_left = 0;
1839 }
1840 }
1841 }
1842