Oolite 1.91.0.7646-241128-10e222e
Loading...
Searching...
No Matches
ShipEntity.m
Go to the documentation of this file.
1/*
2
3ShipEntity.m
4
5
6Oolite
7Copyright (C) 2004-2013 Giles C Williams and contributors
8
9This program is free software; you can redistribute it and/or
10modify it under the terms of the GNU General Public License
11as published by the Free Software Foundation; either version 2
12of the License, or (at your option) any later version.
13
14This program is distributed in the hope that it will be useful,
15but WITHOUT ANY WARRANTY; without even the impllied warranty of
16MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17GNU General Public License for more details.
18
19You should have received a copy of the GNU General Public License
20along with this program; if not, write to the Free Software
21Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22MA 02110-1301, USA.
23
24*/
25
26#import "ShipEntity.h"
27#import "ShipEntityAI.h"
29
30#import "OOMaths.h"
31#import "Universe.h"
32#import "OOShaderMaterial.h"
34
35#import "ResourceManager.h"
36#import "OOStringExpander.h"
37#import "OOStringParsing.h"
39#import "OOConstToString.h"
40#import "OOConstToJSString.h"
43#import "OORoleSet.h"
44#import "OOShipGroup.h"
46#import "OOWeakSet.h"
47#import "GameController.h"
48#import "MyOpenGLView.h"
50#import "OOCharacter.h"
51#import "AI.h"
52
53#import "OOMesh.h"
54#import "OOPlanetDrawable.h"
55
56#import "Octree.h"
57#import "OOColor.h"
58#import "OOPolygonSprite.h"
59
60#import "OOParticleSystem.h"
61#import "StationEntity.h"
62#import "DockEntity.h"
63#import "OOSunEntity.h"
64#import "OOPlanetEntity.h"
65#import "PlanetEntity.h"
66#import "PlayerEntity.h"
67#import "WormholeEntity.h"
68#import "OOFlasherEntity.h"
70#import "OOSparkEntity.h"
71#import "OOECMBlastEntity.h"
75#import "ProxyPlayerEntity.h"
76#import "OOLaserShotEntity.h"
79
81#import "PlayerEntitySound.h"
82#import "GuiDisplayGen.h"
83#import "HeadUpDisplay.h"
85#import "OOShipRegistry.h"
86#import "OOEquipmentType.h"
87
88#import "OODebugGLDrawing.h"
89#import "OODebugFlags.h"
90#import "OODebugStandards.h"
91
92#import "OOJSScript.h"
93#import "OOJSVector.h"
95
96#define USEMASC 1
97
98
99static NSString * const kOOLogSyntaxAddShips = @"script.debug.syntax.addShips";
100#ifndef NDEBUG
101static NSString * const kOOLogEntityBehaviourChanged = @"entity.behaviour.changed";
102#endif
103
104#if MASS_DEPENDENT_FUEL_PRICES
105static GLfloat calcFuelChargeRate (GLfloat myMass)
106{
107#define kMassCharge 0.65 // the closer to 1 this number is, the more the fuel price changes from ship to ship.
108#define kBaseCharge (1.0 - kMassCharge) // proportion of price that doesn't change with ship's mass.
109
110 GLfloat baseMass = [PLAYER baseMass];
111 // if anything is wrong, use 1 (the default charge rate).
112 if (myMass <= 0.0 || baseMass <=0.0) return 1.0;
113
114 GLfloat result = (kMassCharge * myMass / baseMass) + kBaseCharge;
115
116 // round the result to the second decimal digit.
117 result = roundf(result * 100.0f) / 100.0f;
118
119 // Make sure that the rate is clamped to between three times and a third of the standard charge rate.
120 if (result > 3.0f) result = 3.0f;
121 else if (result < 0.33f) result = 0.33f;
122
123 return result;
124
125#undef kMassCharge
126#undef kBaseCharge
127}
128#endif
129
130
131@interface ShipEntity (Private)
132
133- (void)subEntityDied:(ShipEntity *)sub;
134- (void)subEntityReallyDied:(ShipEntity *)sub;
135
136#ifndef NDEBUG
137- (void) drawDebugStuff;
138#endif
139
140- (void) rescaleBy:(GLfloat)factor;
141- (void) rescaleBy:(GLfloat)factor writeToCache:(BOOL)writeToCache;
142
143- (BOOL) setUpOneSubentity:(NSDictionary *) subentDict;
144- (BOOL) setUpOneFlasher:(NSDictionary *) subentDict;
145
146- (Entity<OOStellarBody> *) lastAegisLock;
147
148- (void) addSubEntity:(Entity<OOSubEntity> *) subent;
149
151- (HPVector) coordinatesForEscortPosition:(unsigned)idx;
152- (void) setUpMixedEscorts;
153- (void) setUpOneEscort:(ShipEntity *)escorter inGroup:(OOShipGroup *)escortGroup withRole:(NSString *)escortRole atPosition:(HPVector)ex_pos andCount:(uint8_t)currentEscortCount;
154
155- (void) addSubentityToCollisionRadius:(Entity<OOSubEntity> *) subent;
156- (ShipEntity *) launchPodWithCrew:(NSArray *)podCrew;
157
158// equipment
159- (OOEquipmentType *) generateMissileEquipmentTypeFrom:(NSString *)role;
160
161- (void) setShipHitByLaser:(ShipEntity *)ship;
162
163- (void) noteFrustration:(NSString *)context;
164
165- (BOOL) cloakPassive;
166
167@end
168
169
171
172
173@implementation ShipEntity
174
175- (id) init
176{
177 /* -init used to set up a bunch of defaults that were different from
178 those in -reinit and -setUpShipFromDictionary:. However, it seems that
179 no ships are ever used which are not -setUpShipFromDictionary: (which
180 is as it should be), so these different defaults were meaningless.
181 */
182 return [self initWithKey:@"" definition:nil];
183}
184
185
186- (id) initBypassForPlayer
187{
188 return [super init];
189}
190
191
192// Designated initializer
193- (id)initWithKey:(NSString *)key definition:(NSDictionary *)dict
194{
196
197 NSParameterAssert(dict != nil);
198
199 self = [super init];
200 if (self == nil) return nil;
201
202 _shipKey = [key retain];
203
204 isShip = YES;
205 entity_personality = Ranrot() & ENTITY_PERSONALITY_MAX;
206 [self setStatus:STATUS_IN_FLIGHT];
207
208 zero_distance = SCANNER_MAX_RANGE2 * 2.0;
209 weapon_recharge_rate = 6.0;
210 shot_time = INITIAL_SHOT_TIME;
211 ship_temperature = SHIP_MIN_CABIN_TEMP;
212 weapon_temp = 0.0f;
213 currentWeaponFacing = WEAPON_FACING_FORWARD;
214 forward_weapon_temp = 0.0f;
215 aft_weapon_temp = 0.0f;
216 port_weapon_temp = 0.0f;
217 starboard_weapon_temp = 0.0f;
218
219 _nextAegisCheck = -0.1f;
220 aiScriptWakeTime = 0;
221
222 if (![self setUpShipFromDictionary:dict])
223 {
224 [self release];
225 self = nil;
226 }
227
228 // Problem observed in testing -- Ahruman
229 if (self != nil && !isfinite(maxFlightSpeed))
230 {
231 OOLog(@"ship.sanityCheck.failed", @"Ship %@ %@ infinite top speed, clamped to 300.", self, @"generated with");
232 maxFlightSpeed = 300;
233 }
234 return self;
235
237}
238
239
240- (BOOL) setUpFromDictionary:(NSDictionary *) shipDict
241{
243
244 // Settings shared by players & NPCs.
245 //
246 // In order for default values to work and float values to not be junk,
247 // replace nil with empty dictionary. -- Ahruman 2008-04-28
248 shipinfoDictionary = [shipDict copy];
249 if (shipinfoDictionary == nil) shipinfoDictionary = [[NSDictionary alloc] init];
250 shipDict = shipinfoDictionary; // Ensure no mutation.
251
252 // set these flags explicitly.
253 haveExecutedSpawnAction = NO;
254 haveStartedJSAI = NO;
255 scripted_misjump = NO;
256 _scriptedMisjumpRange = 0.5;
257 being_fined = NO;
258 isNearPlanetSurface = NO;
259 suppressAegisMessages = NO;
260 isMissile = NO;
261 suppressExplosion = NO;
262 _lightsActive = YES;
263
264
265 // set things from dictionary from here out - default values might require adjustment -- Kaks 20091130
266 _scaleFactor = [shipDict oo_floatForKey:@"model_scale_factor" defaultValue:1.0f];
267
268
269 float defaultSpeed = isStation ? 0.0f : 160.0f;
270 maxFlightSpeed = [shipDict oo_floatForKey:@"max_flight_speed" defaultValue:defaultSpeed];
271 max_flight_roll = [shipDict oo_floatForKey:@"max_flight_roll" defaultValue:2.0f];
272 max_flight_pitch = [shipDict oo_floatForKey:@"max_flight_pitch" defaultValue:1.0f];
273 max_flight_yaw = [shipDict oo_floatForKey:@"max_flight_yaw" defaultValue:max_flight_pitch]; // Note by default yaw == pitch
274 cruiseSpeed = maxFlightSpeed*0.8f;
275
276 max_thrust = [shipDict oo_floatForKey:@"thrust" defaultValue:15.0f];
277 thrust = max_thrust;
278
279 afterburner_rate = [shipDict oo_floatForKey:@"injector_burn_rate" defaultValue:AFTERBURNER_BURNRATE];
280 afterburner_speed_factor = [shipDict oo_floatForKey:@"injector_speed_factor" defaultValue:7.0f];
281 if (afterburner_speed_factor < 1.0)
282 {
283 OOLog(@"ship.setup.injectorSpeed",@"injector_speed_factor cannot be lower than 1.0 for %@",self);
284 afterburner_speed_factor = 1.0;
285 }
286#if OO_VARIABLE_TORUS_SPEED
287 else if (afterburner_speed_factor > MIN_HYPERSPEED_FACTOR)
288 {
289 OOLog(@"ship.setup.injectorSpeed",@"injector_speed_factor cannot be higher than minimum torus speed factor (%f) for %@.",MIN_HYPERSPEED_FACTOR,self);
290 afterburner_speed_factor = MIN_HYPERSPEED_FACTOR;
291 }
292#else
293 else if (afterburner_speed_factor > HYPERSPEED_FACTOR)
294 {
295 OOLog(@"ship.setup.injectorSpeed",@"injector_speed_factor cannot be higher than torus speed factor (%f) for %@.",HYPERSPEED_FACTOR,self);
296 afterburner_speed_factor = HYPERSPEED_FACTOR;
297 }
298#endif
299
300 maxEnergy = [shipDict oo_floatForKey:@"max_energy" defaultValue:200.0f];
301 energy_recharge_rate = [shipDict oo_floatForKey:@"energy_recharge_rate" defaultValue:1.0f];
302
303 _showDamage = [shipDict oo_boolForKey:@"show_damage" defaultValue:(energy_recharge_rate > 0)];
304 // Each new ship should start in seemingly good operating condition, unless specifically told not to - this does not affect the ship's energy levels
305 [self setThrowSparks:[shipDict oo_boolForKey:@"throw_sparks" defaultValue:NO]];
306
307 weapon_facings = [shipDict oo_intForKey:@"weapon_facings" defaultValue:VALID_WEAPON_FACINGS] & VALID_WEAPON_FACINGS;
308 if (weapon_facings & WEAPON_FACING_FORWARD)
309 forward_weapon_type = OOWeaponTypeFromString([shipDict oo_stringForKey:@"forward_weapon_type" defaultValue:@"EQ_WEAPON_NONE"]);
310 if (weapon_facings & WEAPON_FACING_AFT)
311 aft_weapon_type = OOWeaponTypeFromString([shipDict oo_stringForKey:@"aft_weapon_type" defaultValue:@"EQ_WEAPON_NONE"]);
312 if (weapon_facings & WEAPON_FACING_PORT)
313 port_weapon_type = OOWeaponTypeFromString([shipDict oo_stringForKey:@"port_weapon_type" defaultValue:@"EQ_WEAPON_NONE"]);
314 if (weapon_facings & WEAPON_FACING_STARBOARD)
315 starboard_weapon_type = OOWeaponTypeFromString([shipDict oo_stringForKey:@"starboard_weapon_type" defaultValue:@"EQ_WEAPON_NONE"]);
316
317 cloaking_device_active = NO;
318 military_jammer_active = NO;
319 cloakPassive = [shipDict oo_boolForKey:@"cloak_passive" defaultValue:YES]; // Nikos - switched passive cloak default to YES 20120523
320 cloakAutomatic = [shipDict oo_boolForKey:@"cloak_automatic" defaultValue:YES];
321
322 missiles = [shipDict oo_intForKey:@"missiles" defaultValue:0];
323 /* TODO: The following initializes the missile list to be blank, which prevents a crash caused by hasOneEquipmentItem trying to access a missile list
324 previously initialized but then released. See issue #204. We need to investigate further the cause of the missile list being released.
325 - kanthoney 10/03/2017
326 Update 20170818: The issue seems to have been resolved properly using the fix below and the problem was apparently an access of the
327 missile_list array elements before their initialization and while we were checking whether equipment can be added or not. There is
328 probably not much more that can be done here, unless someone would like to have a go at refactoring the entire ship initialization
329 code. In any case, the crash is no more and the applied solution is both simple and logical - Nikos
330 */
331 unsigned i;
332 for (i = 0; i < missiles; i++)
333 {
334 missile_list[i] = nil;
335 }
336 max_missiles = [shipDict oo_intForKey:@"max_missiles" defaultValue:missiles];
337 if (max_missiles > SHIPENTITY_MAX_MISSILES) max_missiles = SHIPENTITY_MAX_MISSILES;
338 if (missiles > max_missiles) missiles = max_missiles;
339 missile_load_time = fmax(0.0, [shipDict oo_doubleForKey:@"missile_load_time" defaultValue:0.0]); // no negative load times
340 missile_launch_time = [UNIVERSE getTime] + missile_load_time;
341
342 // upgrades:
343 equipment_weight = 0;
344 if ([shipDict oo_fuzzyBooleanForKey:@"has_ecm"]) [self addEquipmentItem:@"EQ_ECM" inContext:@"npc"];
345 if ([shipDict oo_fuzzyBooleanForKey:@"has_scoop"]) [self addEquipmentItem:@"EQ_FUEL_SCOOPS" inContext:@"npc"];
346 if ([shipDict oo_fuzzyBooleanForKey:@"has_escape_pod"]) [self addEquipmentItem:@"EQ_ESCAPE_POD" inContext:@"npc"];
347 if ([shipDict oo_fuzzyBooleanForKey:@"has_cloaking_device"]) [self addEquipmentItem:@"EQ_CLOAKING_DEVICE" inContext:@"npc"];
348 if ([shipDict oo_floatForKey:@"has_energy_bomb"] > 0)
349 {
350 /* NOTE: has_energy_bomb actually refers to QC mines.
351
352 max_missiles for NPCs is a newish addition, and ships have
353 traditionally not needed to reserve a slot for a Q-mine added this
354 way. If has_energy_bomb is possible, and max_missiles is not
355 explicit, we add an extra missile slot to compensate.
356 -- Ahruman 2011-03-25
357 */
358 if ([shipDict oo_fuzzyBooleanForKey:@"has_energy_bomb"])
359 {
360 if (max_missiles == missiles && max_missiles < SHIPENTITY_MAX_MISSILES && [shipDict objectForKey:@"max_missiles"] == nil)
361 {
362 max_missiles++;
363 }
364 [self addEquipmentItem:@"EQ_QC_MINE" inContext:@"npc"];
365 }
366 }
367
368 if ([shipDict oo_fuzzyBooleanForKey:@"has_fuel_injection"]) [self addEquipmentItem:@"EQ_FUEL_INJECTION" inContext:@"npc"];
369
370#if USEMASC
371 if ([shipDict oo_fuzzyBooleanForKey:@"has_military_jammer"]) [self addEquipmentItem:@"EQ_MILITARY_JAMMER" inContext:@"npc"];
372 if ([shipDict oo_fuzzyBooleanForKey:@"has_military_scanner_filter"]) [self addEquipmentItem:@"EQ_MILITARY_SCANNER_FILTER" inContext:@"npc"];
373#endif
374
375
376 // can it be 'mined' for alloys?
377 canFragment = [shipDict oo_fuzzyBooleanForKey:@"fragment_chance" defaultValue:0.9];
378 isWreckage = NO;
379
380 // can subentities be destroyed separately?
381 isFrangible = [shipDict oo_boolForKey:@"frangible" defaultValue:YES];
382
383 max_cargo = [shipDict oo_unsignedIntForKey:@"max_cargo"];
384 extra_cargo = [shipDict oo_unsignedIntForKey:@"extra_cargo" defaultValue:15];
385
386 hyperspaceMotorSpinTime = [shipDict oo_floatForKey:@"hyperspace_motor_spin_time" defaultValue:DEFAULT_HYPERSPACE_SPIN_TIME];
387 if(![shipDict oo_boolForKey:@"hyperspace_motor" defaultValue:YES]) hyperspaceMotorSpinTime = -1;
388
389 [name autorelease];
390 name = [[shipDict oo_stringForKey:@"name" defaultValue:@"?"] copy];
391
392 [shipUniqueName autorelease];
393 shipUniqueName = [[shipDict oo_stringForKey:@"ship_name" defaultValue:@""] copy];
394
395 [shipClassName autorelease];
396 shipClassName = [[shipDict oo_stringForKey:@"ship_class_name" defaultValue:name] copy];
397
398 [displayName autorelease];
399 displayName = [[shipDict oo_stringForKey:@"display_name" defaultValue:nil] copy];
400
401 // Load the model (must be before subentities)
402 NSString *modelName = [shipDict oo_stringForKey:@"model"];
403 if (modelName != nil)
404 {
405 OOMesh *mesh = nil;
406
407 mesh = [OOMesh meshWithName:modelName
408 cacheKey:[NSString stringWithFormat:@"%@-%.3f",_shipKey,_scaleFactor]
409 materialDictionary:[shipDict oo_dictionaryForKey:@"materials"]
410 shadersDictionary:[shipDict oo_dictionaryForKey:@"shaders"]
411 smooth:[shipDict oo_boolForKey:@"smooth" defaultValue:NO]
412 shaderMacros:OODefaultShipShaderMacros()
414 scaleFactor:_scaleFactor
415 cacheWriteable:YES];
416
417 if (mesh == nil) return NO;
418 [self setMesh:mesh];
419 }
420
421 float density = [shipDict oo_floatForKey:@"density" defaultValue:1.0f];
422 if (octree) mass = (GLfloat)(density * 20.0f * [octree volume]);
423
424 DESTROY(default_laser_color);
425 default_laser_color = [[OOColor brightColorWithDescription:[shipDict objectForKey:@"laser_color"]] retain];
426
427 if (default_laser_color == nil)
428 {
429 [self setLaserColor:[OOColor redColor]];
430 }
431 else
432 {
433 [self setLaserColor:default_laser_color];
434 }
435 // exhaust emissive color
436 OORGBAComponents defaultExhaustEmissiveColorComponents; // pale blue is exhaust default color
437 defaultExhaustEmissiveColorComponents.r = 0.7f;
438 defaultExhaustEmissiveColorComponents.g = 0.9f;
439 defaultExhaustEmissiveColorComponents.b = 1.0f;
440 defaultExhaustEmissiveColorComponents.a = 0.9f;
441 OOColor *color = [OOColor brightColorWithDescription:[shipDict objectForKey:@"exhaust_emissive_color"]];
442 if (color == nil) color = [OOColor colorWithRGBAComponents:defaultExhaustEmissiveColorComponents];
443 [self setExhaustEmissiveColor:color];
444
445 [self clearSubEntities];
446 [self setUpSubEntities];
447
448// correctly initialise weaponRange, etc. (must be after subentity setup)
449 if (isWeaponNone(forward_weapon_type))
450 {
451 OOWeaponType weapon_type = nil;
452 BOOL hasTurrets = NO;
453 NSEnumerator *subEnum = [self shipSubEntityEnumerator];
454 ShipEntity *se = nil;
455 while (isWeaponNone(weapon_type) && (se = [subEnum nextObject]))
456 {
457 weapon_type = se->forward_weapon_type;
458 if (se->behaviour == BEHAVIOUR_TRACK_AS_TURRET)
459 {
460 hasTurrets = YES;
461 }
462 }
463 if (isWeaponNone(weapon_type) && hasTurrets)
464 { /* safety for ships only equipped with turrets
465 note: this was hard-coded to 10000.0, although turrets have a notably
466 shorter range. We are using a multiplier of 1.667 in order to not change
467 something that already works, but probably it would be best to use
468 TURRET_SHOT_RANGE * COMBAT_WEAPON_RANGE_FACTOR here
469 */
470 weaponRange = TURRET_SHOT_RANGE * 1.667;
471 }
472 else
473 {
474 [self setWeaponDataFromType:weapon_type];
475 }
476 }
477 else
478 {
479 [self setWeaponDataFromType:forward_weapon_type];
480 }
481
482 // rotating subentities
483 subentityRotationalVelocity = kIdentityQuaternion;
484 if ([shipDict objectForKey:@"rotational_velocity"])
485 {
486 subentityRotationalVelocity = [shipDict oo_quaternionForKey:@"rotational_velocity"];
487 }
488
489 // set weapon offsets
490 NSString *weaponMountMode = [shipDict oo_stringForKey:@"weapon_mount_mode" defaultValue:@"single"];
491 _multiplyWeapons = [weaponMountMode isEqualToString:@"multiply"];
492 forwardWeaponOffset = [[self getWeaponOffsetFrom:shipDict withKey:@"weapon_position_forward" inMode:weaponMountMode] retain];
493 aftWeaponOffset = [[self getWeaponOffsetFrom:shipDict withKey:@"weapon_position_aft" inMode:weaponMountMode] retain];
494 portWeaponOffset = [[self getWeaponOffsetFrom:shipDict withKey:@"weapon_position_port" inMode:weaponMountMode] retain];
495 starboardWeaponOffset = [[self getWeaponOffsetFrom:shipDict withKey:@"weapon_position_starboard" inMode:weaponMountMode] retain];
496
497
498 tractor_position = vector_multiply_scalar([shipDict oo_vectorForKey:@"scoop_position"],_scaleFactor);
499
500
501
502 // sun glare filter - default is high filter, both for HDR and SDR
503 [self setSunGlareFilter:[shipDict oo_floatForKey:@"sun_glare_filter" defaultValue:0.97f]];
504
505 // Get scriptInfo dictionary, containing arbitrary stuff scripts might be interested in.
506 scriptInfo = [[shipDict oo_dictionaryForKey:@"script_info" defaultValue:nil] retain];
507
508 explosionType = [[shipDict oo_arrayForKey:@"explosion_type" defaultValue:nil] retain];
509
510 isDemoShip = NO;
511
512 return YES;
513
515}
516
517
518
519- (BOOL) setUpShipFromDictionary:(NSDictionary *) shipDict
520{
522
523 if (![self setUpFromDictionary:shipDict]) return NO;
524
525 // NPC-only settings.
526 //
527 orientation = kIdentityQuaternion;
528 rotMatrix = kIdentityMatrix;
529 v_forward = kBasisZVector;
530 v_up = kBasisYVector;
531 v_right = kBasisXVector;
532 reference = v_forward; // reference vector for (* turrets *)
533
534 isShip = YES;
535
536 // scan class settings. 'scanClass' is in common usage, but we could also have a more standard 'scan_class' key with higher precedence. Kaks 20090810
537 // let's see if scan_class is set...
538 scanClass = OOScanClassFromString([shipDict oo_stringForKey:@"scan_class" defaultValue:@"CLASS_NOT_SET"]);
539
540 // if not, try 'scanClass'. NOTE: non-standard capitalization is documented and entrenched.
541 if (scanClass == CLASS_NOT_SET)
542 {
543 scanClass = OOScanClassFromString([shipDict oo_stringForKey:@"scanClass" defaultValue:@"CLASS_NOT_SET"]);
544 }
545
546 [scan_description autorelease];
547 scan_description = [[shipDict oo_stringForKey:@"scan_description" defaultValue:nil] copy];
548
549 // FIXME: give NPCs shields instead.
550
551 if ([shipDict oo_fuzzyBooleanForKey:@"has_shield_booster"]) [self addEquipmentItem:@"EQ_SHIELD_BOOSTER" inContext:@"npc"];
552 if ([shipDict oo_fuzzyBooleanForKey:@"has_shield_enhancer"]) [self addEquipmentItem:@"EQ_SHIELD_ENHANCER" inContext:@"npc"];
553
554 // Start with full energy banks.
555 energy = maxEnergy;
556 weapon_temp = 0.0f;
557 forward_weapon_temp = 0.0f;
558 aft_weapon_temp = 0.0f;
559 port_weapon_temp = 0.0f;
560 starboard_weapon_temp = 0.0f;
561
562 // setWeaponDataFromType inside setUpFromDictionary should set weapon_damage from the front laser.
563 // no weapon_damage? It's a missile: set weapon_damage from shipdata!
564 if (weapon_damage == 0.0)
565 {
566 weapon_damage_override = weapon_damage = [shipDict oo_floatForKey:@"weapon_energy" defaultValue:0]; // any damage value for missiles/bombs
567 }
568 else
569 {
570 weapon_damage_override = 0;
571 }
572
573 scannerRange = [shipDict oo_floatForKey:@"scanner_range" defaultValue:(float)SCANNER_MAX_RANGE];
574
575 fuel = [shipDict oo_unsignedShortForKey:@"fuel"]; // Does it make sense that this defaults to 0? Should it not be 70? -- Ahruman
576
577 fuel_accumulator = 1.0;
578
579 [self setBounty:[shipDict oo_unsignedIntForKey:@"bounty" defaultValue:0] withReason:kOOLegalStatusReasonSetup];
580
581 [shipAI autorelease];
582 shipAI = [[AI alloc] init];
583 [shipAI setOwner:self];
584 [self setAITo:[shipDict oo_stringForKey:@"ai_type" defaultValue:@"nullAI.plist"]];
585
586 likely_cargo = [shipDict oo_unsignedIntForKey:@"likely_cargo"];
587 noRocks = [shipDict oo_fuzzyBooleanForKey:@"no_boulders"];
588
589 commodity_amount = 0;
590 commodity_type = nil;
591 NSString *cargoString = [shipDict oo_stringForKey:@"cargo_carried"];
592 if (cargoString != nil)
593 {
594 if ([cargoString isEqualToString:@"SCARCE_GOODS"])
595 {
596 cargo_flag = CARGO_FLAG_FULL_SCARCE;
597 }
598 else if ([cargoString isEqualToString:@"PLENTIFUL_GOODS"])
599 {
600 cargo_flag = CARGO_FLAG_FULL_PLENTIFUL;
601 }
602 else
603 {
604 cargo_flag = CARGO_FLAG_FULL_UNIFORM;
605
606 OOCommodityType c_commodity = nil;
607 int c_amount = 1;
608 NSScanner *scanner = [NSScanner scannerWithString:cargoString];
609 if ([scanner scanInt:&c_amount])
610 {
611 [scanner ooliteScanCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:NULL]; // skip whitespace
612 c_commodity = [[scanner string] substringFromIndex:[scanner scanLocation]];
613 if ([[UNIVERSE commodities] goodDefined:c_commodity])
614 {
615 [self setCommodityForPod:c_commodity andAmount:c_amount];
616 }
617 else
618 {
619 c_commodity = [[UNIVERSE commodities] goodNamed:c_commodity];
620 if ([[UNIVERSE commodities] goodDefined:c_commodity])
621 {
622 [self setCommodityForPod:c_commodity andAmount:c_amount];
623 }
624 }
625 }
626 else
627 {
628 c_amount = 1;
629 c_commodity = [shipDict oo_stringForKey:@"cargo_carried"];
630 if ([[UNIVERSE commodities] goodDefined:c_commodity])
631 {
632 [self setCommodityForPod:c_commodity andAmount:c_amount];
633 }
634 else
635 {
636 c_commodity = [[UNIVERSE commodities] goodNamed:c_commodity];
637 if ([[UNIVERSE commodities] goodDefined:c_commodity])
638 {
639 [self setCommodityForPod:c_commodity andAmount:c_amount];
640 }
641 }
642 }
643 }
644 }
645
646 cargoString = [shipDict oo_stringForKey:@"cargo_type"];
647 if (cargoString)
648 {
649 if (cargo != nil) [cargo autorelease];
650 cargo = [[NSMutableArray alloc] initWithCapacity:max_cargo]; // alloc retains;
651
652 [self setUpCargoType:cargoString];
653 }
654 else if (scanClass != CLASS_CARGO)
655 {
656 if (cargo != nil) [cargo autorelease];
657 cargo = [[NSMutableArray alloc] initWithCapacity:max_cargo]; // alloc retains;
658 // if not CLASS_CARGO, and no cargo type set, default to CARGO_NOT_CARGO
659 cargo_type = CARGO_NOT_CARGO;
660 }
661
662 hasScoopMessage = [shipDict oo_boolForKey:@"has_scoop_message" defaultValue:YES];
663
664
665 [roleSet release];
666 roleSet = [[[OORoleSet roleSetWithString:[shipDict oo_stringForKey:@"roles"]] roleSetWithRemovedRole:@"player"] retain];
667 [primaryRole release];
668 primaryRole = nil;
669
670 [self setOwner:self];
671 [self setHulk:[shipDict oo_boolForKey:@"is_hulk"]];
672
673 // these are the colors used for the "lollipop" of the ship. Any of the two (or both, for flash effect) can be defined. nil means use default from shipData.
674 [self setScannerDisplayColor1:nil];
675 [self setScannerDisplayColor2:nil];
676 // and the same for the "hostile" colours
677 [self setScannerDisplayColorHostile1:nil];
678 [self setScannerDisplayColorHostile2:nil];
679
680
681 // Populate the missiles here. Must come after scanClass.
682 _missileRole = [shipDict oo_stringForKey:@"missile_role"];
683 unsigned i, j;
684 for (i = 0, j = 0; i < missiles; i++)
685 {
686 missile_list[i] = [self selectMissile];
687 // could loop forever (if missile_role is badly defined, selectMissile might return nil in some cases) . Try 3 times, and if no luck, skip
688 if (missile_list[i] == nil && j < 3)
689 {
690 j++;
691 i--;
692 }
693 else
694 {
695 j = 0;
696 if (missile_list[i] == nil)
697 {
698 missiles--;
699 }
700 }
701 }
702
703 // accuracy. Must come after scanClass, because we are using scanClass to determine if this is a missile.
704
705// missiles: range 0 to +10
706// ships: range -5 to +10, but randomly only -5 <= accuracy < +5
707// enables "better" AIs at +5 and above
708// police and military always have positive accuracy
709
710 accuracy = [shipDict oo_floatForKey:@"accuracy" defaultValue:-100.0f]; // Out-of-range default
711 if (accuracy < -5.0f || accuracy > 10.0f)
712 {
713 accuracy = (randf() * 10.0)-5.0;
714
715 if (accuracy < 0.0f && (scanClass == CLASS_MILITARY || scanClass == CLASS_POLICE))
716 { // police and military pilots have a better average skill.
717 accuracy = -accuracy;
718 }
719 }
720 if (scanClass == CLASS_MISSILE)
721 { // missile accuracy range is 0 to 10
722 accuracy = OOClamp_0_max_f(accuracy, 10.0f);
723 }
724 [self setAccuracy:accuracy]; // set derived variables
725 _missed_shots = 0;
726
727 // escorts
728 _maxEscortCount = MIN([shipDict oo_unsignedCharForKey:@"escorts" defaultValue:0], (uint8_t)MAX_ESCORTS);
729 _pendingEscortCount = _maxEscortCount;
730 if (_pendingEscortCount == 0 && [shipDict oo_arrayForKey:@"escort_roles" defaultValue:nil] != nil)
731 {
732 // mostly ignored by setUpMixedEscorts, but needs to be high
733 // enough that it doesn't end up at zero (e.g. by governmental
734 // reductions in [Universe addShipAt]
735 _pendingEscortCount = MAX_ESCORTS;
736 }
737
738
739 // beacons
740 [self setBeaconCode:[shipDict oo_stringForKey:@"beacon"]];
741 [self setBeaconLabel:[shipDict oo_stringForKey:@"beacon_label" defaultValue:[shipDict oo_stringForKey:@"beacon"]]];
742
743
744 // contact tracking entities
745 [self setTrackCloseContacts:[shipDict oo_boolForKey:@"track_contacts" defaultValue:NO]];
746
747 // ship skin insulation factor (1.0 is normal)
748 [self setHeatInsulation:[shipDict oo_floatForKey:@"heat_insulation" defaultValue:[self hasHeatShield] ? 2.0 : 1.0]];
749
750 // unpiloted (like missiles asteroids etc.)
751 _explicitlyUnpiloted = [shipDict oo_fuzzyBooleanForKey:@"unpiloted"];
752 if (_explicitlyUnpiloted)
753 {
754 [self setCrew:nil];
755 }
756 else
757 {
758 // crew and passengers
759 NSDictionary *cdict = [[UNIVERSE characters] objectForKey:[shipDict oo_stringForKey:@"pilot"]];
760 if (cdict != nil)
761 {
763 [self setCrew:[NSArray arrayWithObject:pilot]];
764 }
765 }
766
767 [self setShipScript:[shipDict oo_stringForKey:@"script"]];
768
769 home_system = [UNIVERSE currentSystemID];
770 destination_system = [UNIVERSE currentSystemID];
771
772 reactionTime = [shipDict oo_floatForKey: @"reaction_time" defaultValue: COMBAT_AI_STANDARD_REACTION_TIME];
773
774 return YES;
775
777}
778
779
780- (void) setSubIdx:(NSUInteger)value
781{
782 _subIdx = value;
783}
784
785
786- (NSUInteger) subIdx
787{
788 return _subIdx;
789}
790
791
792- (NSUInteger) maxShipSubEntities
793{
794 return _maxShipSubIdx;
795}
796
797
798- (NSString *) repeatString:(NSString *)str times:(NSUInteger)times
799{
800 if (times == 0) return @"";
801
802 NSMutableString *result = [NSMutableString stringWithCapacity:[str length] * times];
803
804 for (NSUInteger i = 0; i < times; i++)
805 {
806 [result appendString:str];
807 }
808
809 return result;
810}
811
812
813- (NSString *) serializeShipSubEntities
814{
815 NSMutableString *result = [NSMutableString stringWithCapacity:4];
816 NSEnumerator *subEnum = nil;
817 ShipEntity *se = nil;
818 NSUInteger diff, i = 0;
819
820 for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); )
821 {
822 diff = [se subIdx] - i;
823 i += diff + 1;
824 [result appendString:[self repeatString:@"0" times:diff]];
825 [result appendString:@"1"];
826 }
827 // add trailing zeroes
828 [result appendString:[self repeatString:@"0" times:[self maxShipSubEntities] - i]];
829 return result;
830}
831
832
833- (void) deserializeShipSubEntitiesFrom:(NSString *)string
834{
835 NSArray *subEnts = [[self shipSubEntityEnumerator] allObjects];
836 NSInteger i,idx, start = [subEnts count] - 1;
837 NSInteger strMaxIdx = [string length] - 1;
838
839 ShipEntity *se = nil;
840
841 for (i = start; i >= 0; i--)
842 {
843 se = (ShipEntity *)[subEnts objectAtIndex:i];
844 idx = [se subIdx]; // should be identical to i, but better safe than sorry...
845 if (idx <= strMaxIdx && [[string substringWithRange:NSMakeRange(idx, 1)] isEqualToString:@"0"])
846 {
847 [se setSuppressExplosion:NO];
848 [se setEnergy:1];
849 [se takeEnergyDamage:500000000.0 from:nil becauseOf:nil weaponIdentifier:@""];
850 }
851 }
852}
853
854
855- (BOOL) setUpSubEntities
856{
858
859 unsigned int i;
860 NSDictionary *shipDict = [self shipInfoDictionary];
861 NSArray *plumes = [shipDict oo_arrayForKey:@"exhaust"];
862
863 _profileRadius = collision_radius;
864 _maxShipSubIdx = 0;
865
866 for (i = 0; i < [plumes count]; i++)
867 {
868 NSArray *definition = ScanTokensFromString([plumes oo_stringAtIndex:i]);
869 OOExhaustPlumeEntity *exhaust = [OOExhaustPlumeEntity exhaustForShip:self withDefinition:definition andScale:_scaleFactor];
870 [self addSubEntity:exhaust];
871 }
872
873 NSArray *subs = [shipDict oo_arrayForKey:@"subentities"];
874
875 totalBoundingBox = boundingBox;
876
877 for (i = 0; i < [subs count]; i++)
878 {
879 [self setUpOneSubentity:[subs oo_dictionaryAtIndex:i]];
880 }
881
882 no_draw_distance = _profileRadius * _profileRadius * NO_DRAW_DISTANCE_FACTOR * NO_DRAW_DISTANCE_FACTOR * 2.0;
883
884 return YES;
885
887}
888
889
890- (GLfloat) frustumRadius
891{
892 OOScalar exhaust_length = 0;
893 NSEnumerator *exEnum = nil;
894 OOExhaustPlumeEntity *exEnt = nil;
895 for (exEnum = [self exhaustEnumerator]; (exEnt = [exEnum nextObject]); )
896 {
897 if ([exEnt findCollisionRadius] > exhaust_length)
898 {
899 exhaust_length = [exEnt findCollisionRadius];
900 }
901 }
902 return _profileRadius + exhaust_length;
903}
904
905
906- (BOOL) setUpOneSubentity:(NSDictionary *) subentDict
907{
909
910 NSString *type = nil;
911
912 type = [subentDict oo_stringForKey:@"type"];
913 if ([type isEqualToString:@"flasher"])
914 {
915 return [self setUpOneFlasher:subentDict];
916 }
917 else
918 {
919 return [self setUpOneStandardSubentity:subentDict asTurret:[type isEqualToString:@"ball_turret"]];
920 }
921
923}
924
925
926- (BOOL) setUpOneFlasher:(NSDictionary *) subentDict
927{
929 [flasher setPosition:HPvector_multiply_scalar([subentDict oo_hpvectorForKey:@"position"],_scaleFactor)];
930 [flasher rescaleBy:_scaleFactor];
931 [self addSubEntity:flasher];
932 return YES;
933}
934
935
936- (BOOL) setUpOneStandardSubentity:(NSDictionary *)subentDict asTurret:(BOOL)asTurret
937{
938 ShipEntity *subentity = nil;
939 NSString *subentKey = nil;
940 HPVector subPosition;
941 Quaternion subOrientation;
942
943 subentKey = [subentDict oo_stringForKey:@"subentity_key"];
944 if (subentKey == nil) {
945 OOLog(@"setup.ship.badEntry.subentities",@"Failed to set up entity - no subentKey in %@",subentDict);
946 return NO;
947 }
948
949 if (!asTurret && [self isStation] && [subentDict oo_boolForKey:@"is_dock"])
950 {
951 subentity = [UNIVERSE newDockWithName:subentKey andScaleFactor:_scaleFactor];
952 }
953 else
954 {
955 subentity = [UNIVERSE newSubentityWithName:subentKey andScaleFactor:_scaleFactor];
956 }
957 if (subentity == nil) {
958 OOLog(@"setup.ship.badEntry.subentities",@"Failed to set up entity %@",subentKey);
959 return NO;
960 }
961
962 subPosition = HPvector_multiply_scalar([subentDict oo_hpvectorForKey:@"position"],_scaleFactor);
963 subOrientation = [subentDict oo_quaternionForKey:@"orientation"];
964
965 [subentity setPosition:subPosition];
966 [subentity setOrientation:subOrientation];
967 [subentity setReference:vector_forward_from_quaternion(subOrientation)];
968 // subentities inherit parent personality
969 [subentity setEntityPersonalityInt:[self entityPersonalityInt]];
970
971 if (asTurret)
972 {
973 [subentity setBehaviour:BEHAVIOUR_TRACK_AS_TURRET];
974 [subentity setWeaponRechargeRate:[subentDict oo_floatForKey:@"fire_rate" defaultValue:TURRET_SHOT_FREQUENCY]];
975 [subentity setWeaponEnergy:[subentDict oo_floatForKey:@"weapon_energy" defaultValue:TURRET_TYPICAL_ENERGY]];
976 [subentity setWeaponRange:[subentDict oo_floatForKey:@"weapon_range" defaultValue:TURRET_SHOT_RANGE]];
977 [subentity setStatus: STATUS_ACTIVE];
978 }
979 else
980 {
981 [subentity setStatus:STATUS_INACTIVE];
982 }
983
984 [subentity overrideScriptInfo:[subentDict oo_dictionaryForKey:@"script_info"]];
985
986 [self addSubEntity:subentity];
987 [subentity setSubIdx:_maxShipSubIdx];
988 _maxShipSubIdx++;
989
990 // update subentities
991 BoundingBox sebb = [subentity findSubentityBoundingBox];
992 bounding_box_add_vector(&totalBoundingBox, sebb.max);
993 bounding_box_add_vector(&totalBoundingBox, sebb.min);
994
995 if (!asTurret && [self isStation] && [subentDict oo_boolForKey:@"is_dock"])
996 {
997 BOOL allow_docking = [subentDict oo_boolForKey:@"allow_docking" defaultValue:YES];
998 BOOL ddc = [subentDict oo_boolForKey:@"disallowed_docking_collides" defaultValue:NO];
999 BOOL allow_launching = [subentDict oo_boolForKey:@"allow_launching" defaultValue:YES];
1000 // do not include this key in OOShipRegistry; should never be set by shipdata
1001 BOOL virtual_dock = [subentDict oo_boolForKey:@"_is_virtual_dock" defaultValue:NO];
1002 if (virtual_dock)
1003 {
1004 [(DockEntity *)subentity setVirtual];
1005 }
1006
1007 [(DockEntity *)subentity setDimensionsAndCorridor:allow_docking:ddc:allow_launching];
1008 [subentity setDisplayName:[subentDict oo_stringForKey:@"dock_label" defaultValue:@"the docking bay"]];
1009 }
1010
1011 [subentity release];
1012
1013 return YES;
1014}
1015
1016
1017- (BOOL) isTemplateCargoPod
1018{
1019 return [[self primaryRole] isEqualToString:@"oolite-template-cargopod"];
1020}
1021
1022
1023- (void) setUpCargoType:(NSString *) cargoString
1024{
1025 cargo_type = StringToCargoType(cargoString);
1026
1027 switch (cargo_type)
1028 {
1029 case CARGO_SLAVES:
1030 commodity_amount = 1;
1031 DESTROY(commodity_type);
1032 commodity_type = @"slaves";
1033 cargo_type = CARGO_RANDOM; // not realy random, but it tells that cargo is selected.
1034 break;
1035
1036 case CARGO_ALLOY:
1037 commodity_amount = 1;
1038 DESTROY(commodity_type);
1039 commodity_type = @"alloys";
1040 cargo_type = CARGO_RANDOM;
1041 break;
1042
1043 case CARGO_MINERALS:
1044 commodity_amount = 1;
1045 DESTROY(commodity_type);
1046 commodity_type = @"minerals";
1047 cargo_type = CARGO_RANDOM;
1048 break;
1049
1050 case CARGO_THARGOID:
1051 commodity_amount = 1;
1052 DESTROY(commodity_type);
1053 commodity_type = @"alien_items";
1054 cargo_type = CARGO_RANDOM;
1055 break;
1056
1058 commodity_amount = 1; // value > 0 is needed to be recognised as cargo by scripts;
1059 DESTROY(commodity_type); // will be defined elsewhere when needed.
1060 break;
1061
1062 case CARGO_RANDOM:
1063 // Could already be set by the cargo_carried key. If not, ensure at least one.
1064 if (commodity_amount == 0) commodity_amount = 1;
1065 break;
1066
1067 default:
1068 break;
1069 }
1070}
1071
1072
1073- (void) dealloc
1074{
1075 /* NOTE: we guarantee that entityDestroyed is sent immediately after the
1076 JS ship becomes invalid (as a result of dropping the weakref), i.e.
1077 with no intervening script activity.
1078 It has to be after the invalidation so that scripts can't directly or
1079 indirectly cause the ship to become strong-referenced. (Actually, we
1080 could handle that situation by breaking out of dealloc, but that's a
1081 nasty abuse of framework semantics and would require special-casing in
1082 subclasses.)
1083 -- Ahruman 2011-02-27
1084 */
1085 [weakSelf weakRefDrop];
1086 weakSelf = nil;
1087 ShipScriptEventNoCx(self, "entityDestroyed");
1088
1089 [self setTrackCloseContacts:NO]; // deallocs tracking dictionary
1090 [[self parentEntity] subEntityReallyDied:self]; // Will do nothing if we're not really a subentity
1091 [self clearSubEntities];
1092
1093 DESTROY(_shipKey);
1094 DESTROY(shipinfoDictionary);
1095 DESTROY(shipAI);
1096 DESTROY(cargo);
1097 DESTROY(name);
1098 DESTROY(shipUniqueName);
1099 DESTROY(shipClassName);
1100 DESTROY(displayName);
1101 DESTROY(scan_description);
1102 DESTROY(roleSet);
1103 DESTROY(primaryRole);
1104 DESTROY(laser_color);
1105 DESTROY(default_laser_color);
1106 DESTROY(exhaust_emissive_color);
1107 DESTROY(scanner_display_color1);
1108 DESTROY(scanner_display_color2);
1109 DESTROY(scanner_display_color_hostile1);
1110 DESTROY(scanner_display_color_hostile2);
1111 DESTROY(script);
1112 DESTROY(aiScript);
1113 DESTROY(previousCondition);
1114 DESTROY(dockingInstructions);
1115 DESTROY(crew);
1116 DESTROY(lastRadioMessage);
1117 DESTROY(octree);
1118 DESTROY(_defenseTargets);
1119 DESTROY(_collisionExceptions);
1120
1121 DESTROY(forwardWeaponOffset);
1122 DESTROY(aftWeaponOffset);
1123 DESTROY(portWeaponOffset);
1124 DESTROY(starboardWeaponOffset);
1125
1126 DESTROY(commodity_type);
1127
1128 [self setSubEntityTakingDamage:nil];
1129 [self removeAllEquipment];
1130
1131 [_group removeShip:self];
1132 DESTROY(_group);
1133 [_escortGroup removeShip:self];
1134 DESTROY(_escortGroup);
1135
1136 DESTROY(_lastAegisLock);
1137
1138 DESTROY(_beaconCode);
1139 DESTROY(_beaconLabel);
1140 DESTROY(_beaconDrawable);
1141
1142 DESTROY(explosionType);
1143
1144 [super dealloc];
1145}
1146
1147
1148- (void) removeScript
1149{
1150 [script autorelease];
1151 script = nil;
1152}
1153
1154
1155- (void) clearSubEntities
1156{
1157 [subEntities makeObjectsPerformSelector:@selector(setOwner:) withObject:nil]; // Ensure backlinks are broken
1158 [subEntities release];
1159 subEntities = nil;
1160
1161 // reset size & mass!
1162 collision_radius = [self findCollisionRadius];
1163 _profileRadius = collision_radius;
1164 float density = [[self shipInfoDictionary] oo_floatForKey:@"density" defaultValue:1.0f];
1165 if (octree) mass = (GLfloat)(density * 20.0f * [octree volume]);
1166}
1167
1168
1169- (Quaternion) subEntityRotationalVelocity
1170{
1171 return subentityRotationalVelocity;
1172}
1173
1174
1175- (void) setSubEntityRotationalVelocity:(Quaternion)rv
1176{
1177 subentityRotationalVelocity = rv;
1178}
1179
1180
1181- (NSString *)descriptionComponents
1182{
1183 if (![self isSubEntity])
1184 {
1185 return [NSString stringWithFormat:@"\"%@\" %@", [self name], [super descriptionComponents]];
1186 }
1187 else
1188 {
1189 // ID, scanClass and status are of no interest for subentities.
1190 NSString *subtype = nil;
1191 if ([self behaviour] == BEHAVIOUR_TRACK_AS_TURRET) subtype = @"(turret)";
1192 else subtype = @"(subentity)";
1193
1194 return [NSString stringWithFormat:@"\"%@\" position: %@ %@", [self name], HPVectorDescription([self position]), subtype];
1195 }
1196}
1197
1198
1199- (NSString *) shortDescriptionComponents
1200{
1201 return [NSString stringWithFormat:@"\"%@\"", [self name]];
1202}
1203
1204
1205- (GLfloat) sunGlareFilter
1206{
1207 return sunGlareFilter;
1208}
1209
1210
1211- (void) setSunGlareFilter:(GLfloat)newValue
1212{
1213 sunGlareFilter = OOClamp_0_1_f(newValue);
1214}
1215
1216
1217- (GLfloat) accuracy
1218{
1219 return accuracy;
1220}
1221
1222
1223- (void) setAccuracy:(GLfloat) new_accuracy
1224{
1225 if (new_accuracy < 0.0f && scanClass == CLASS_MISSILE)
1226 {
1227 new_accuracy = 0.0;
1228 }
1229 else if (new_accuracy < -5.0f)
1230 {
1231 new_accuracy = -5.0;
1232 }
1233 else if (new_accuracy > 10.0f)
1234 {
1235 new_accuracy = 10.0;
1236 }
1237 accuracy = new_accuracy;
1238 pitch_tolerance = 0.01 * (85.0f + accuracy);
1239// especially against small targets, less good pilots will waste some shots
1240 aim_tolerance = 240.0 - (18.0f * accuracy);
1241
1242 if (accuracy >= COMBAT_AI_ISNT_AWFUL && missile_load_time < 0.1)
1243 {
1244 missile_load_time = 2.0; // smart enough not to waste all missiles on 1 ECM!
1245 }
1246}
1247
1248- (OOMesh *)mesh
1249{
1250 return (OOMesh *)[self drawable];
1251}
1252
1253
1254- (void)setMesh:(OOMesh *)mesh
1255{
1256 if (mesh != [self mesh])
1257 {
1258 [self setDrawable:mesh];
1259 [octree autorelease];
1260 octree = [[mesh octree] retain];
1261 }
1262}
1263
1264
1265- (BoundingBox) totalBoundingBox
1266{
1267 return totalBoundingBox;
1268}
1269
1270
1271- (Vector) forwardVector
1272{
1273 return v_forward;
1274}
1275
1276
1277- (Vector) upVector
1278{
1279 return v_up;
1280}
1281
1282
1283- (Vector) rightVector
1284{
1285 return v_right;
1286}
1287
1288
1289- (BOOL) scriptedMisjump
1290{
1291 return scripted_misjump;
1292}
1293
1294
1295- (void) setScriptedMisjump:(BOOL)newValue
1296{
1297 scripted_misjump = !!newValue;
1298}
1299
1300
1301- (GLfloat) scriptedMisjumpRange
1302{
1303 return _scriptedMisjumpRange;
1304}
1305
1306
1307- (void) setScriptedMisjumpRange:(GLfloat)newValue
1308{
1309 _scriptedMisjumpRange = newValue;
1310}
1311
1312
1313- (NSArray *) subEntities
1314{
1315 return [[subEntities copy] autorelease];
1316}
1317
1318
1319- (NSUInteger) subEntityCount
1320{
1321 return [subEntities count];
1322}
1323
1324
1325- (BOOL) hasSubEntity:(Entity<OOSubEntity> *)sub
1326{
1327 return [subEntities containsObject:sub];
1328}
1329
1330
1331- (NSEnumerator *)subEntityEnumerator
1332{
1333 return [[self subEntities] objectEnumerator];
1334}
1335
1336
1337- (NSEnumerator *)shipSubEntityEnumerator
1338{
1339 return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isShip)];
1340}
1341
1342
1343- (NSEnumerator *)flasherEnumerator
1344{
1345 return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isFlasher)];
1346}
1347
1348
1349- (NSEnumerator *)exhaustEnumerator
1350{
1351 return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isExhaust)];
1352}
1353
1354
1355- (ShipEntity *) subEntityTakingDamage
1356{
1357 ShipEntity *result = [_subEntityTakingDamage weakRefUnderlyingObject];
1358
1359#ifndef NDEBUG
1360 // Sanity check - there have been problems here, see fireLaserShotInDirection:
1361 // -parentEntity will take care of reporting insanity.
1362 if ([result parentEntity] != self) result = nil;
1363#endif
1364
1365 // Clear the weakref if the subentity is dead.
1366 if (result == nil) [self setSubEntityTakingDamage:nil];
1367
1368 return result;
1369}
1370
1371
1372- (void) setSubEntityTakingDamage:(ShipEntity *)sub
1373{
1374#ifndef NDEBUG
1375 // Sanity checks: sub must be a ship subentity of self, or nil.
1376 if (sub != nil)
1377 {
1378 if (![self hasSubEntity:sub])
1379 {
1380 OOLog(@"ship.subentity.sanityCheck.failed.details", @"Attempt to set subentity taking damage of %@ to %@, which is not a subentity.", [self shortDescription], sub);
1381 sub = nil;
1382 }
1383 else if (![sub isShip])
1384 {
1385 OOLog(@"ship.subentity.sanityCheck.failed", @"Attempt to set subentity taking damage of %@ to %@, which is not a ship.", [self shortDescription], sub);
1386 sub = nil;
1387 }
1388 }
1389#endif
1390
1391 [_subEntityTakingDamage release];
1392 _subEntityTakingDamage = [sub weakRetain];
1393}
1394
1395
1396- (OOScript *)shipScript
1397{
1398 return script;
1399}
1400
1401
1402- (OOScript *)shipAIScript
1403{
1404 return aiScript;
1405}
1406
1407
1408- (OOTimeAbsolute) shipAIScriptWakeTime
1409{
1410 return aiScriptWakeTime;
1411}
1412
1413
1414- (void) setAIScriptWakeTime:(OOTimeAbsolute) t
1415{
1416 aiScriptWakeTime = t;
1417}
1418
1419
1420- (BoundingBox)findBoundingBoxRelativeToPosition:(HPVector)opv InVectors:(Vector) _i :(Vector) _j :(Vector) _k
1421{
1422 // HPVect: check that this conversion doesn't lose needed precision
1423 return [[self mesh] findBoundingBoxRelativeToPosition:HPVectorToVector(opv)
1424 basis:_i :_j :_k
1425 selfPosition:HPVectorToVector(position)
1426 selfBasis:v_right :v_up :v_forward];
1427}
1428
1429
1430- (Octree *) octree
1431{
1432 return octree;
1433}
1434
1435
1436- (float) volume
1437{
1438 return [octree volume];
1439}
1440
1441
1442- (GLfloat) doesHitLine:(HPVector)v0 :(HPVector)v1
1443{
1444 Vector u0 = HPVectorToVector(HPvector_between(position, v0)); // relative to origin of model / octree
1445 Vector u1 = HPVectorToVector(HPvector_between(position, v1));
1446 Vector w0 = make_vector(dot_product(u0, v_right), dot_product(u0, v_up), dot_product(u0, v_forward)); // in ijk vectors
1447 Vector w1 = make_vector(dot_product(u1, v_right), dot_product(u1, v_up), dot_product(u1, v_forward));
1448 return [octree isHitByLine:w0 :w1];
1449}
1450
1451
1452- (GLfloat) doesHitLine:(HPVector)v0 :(HPVector)v1 :(ShipEntity **)hitEntity
1453{
1454 if (hitEntity)
1455 hitEntity[0] = (ShipEntity*)nil;
1456 Vector u0 = HPVectorToVector(HPvector_between(position, v0)); // relative to origin of model / octree
1457 Vector u1 = HPVectorToVector(HPvector_between(position, v1));
1458 Vector w0 = make_vector(dot_product(u0, v_right), dot_product(u0, v_up), dot_product(u0, v_forward)); // in ijk vectors
1459 Vector w1 = make_vector(dot_product(u1, v_right), dot_product(u1, v_up), dot_product(u1, v_forward));
1460 GLfloat hit_distance = [octree isHitByLine:w0 :w1];
1461 if (hit_distance)
1462 {
1463 if (hitEntity)
1464 hitEntity[0] = self;
1465 }
1466
1467 NSEnumerator *subEnum = nil;
1468 ShipEntity *se = nil;
1469 for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); )
1470 {
1471 HPVector p0 = [se absolutePositionForSubentity];
1472 Triangle ijk = [se absoluteIJKForSubentity];
1473 u0 = HPVectorToVector(HPvector_between(p0, v0));
1474 u1 = HPVectorToVector(HPvector_between(p0, v1));
1475 w0 = resolveVectorInIJK(u0, ijk);
1476 w1 = resolveVectorInIJK(u1, ijk);
1477
1478 GLfloat hitSub = [se->octree isHitByLine:w0 :w1];
1479 if (hitSub && (hit_distance == 0 || hit_distance > hitSub))
1480 {
1481 hit_distance = hitSub;
1482 if (hitEntity)
1483 {
1484 *hitEntity = se;
1485 }
1486 }
1487 }
1488
1489 return hit_distance;
1490}
1491
1492
1493- (GLfloat)doesHitLine:(HPVector)v0 :(HPVector)v1 withPosition:(HPVector)o andIJK:(Vector)i :(Vector)j :(Vector)k
1494{
1495 Vector u0 = HPVectorToVector(HPvector_between(o, v0)); // relative to origin of model / octree
1496 Vector u1 = HPVectorToVector(HPvector_between(o, v1));
1497 Vector w0 = make_vector(dot_product(u0, i), dot_product(u0, j), dot_product(u0, k)); // in ijk vectors
1498 Vector w1 = make_vector(dot_product(u1, j), dot_product(u1, j), dot_product(u1, k));
1499 return [octree isHitByLine:w0 :w1];
1500}
1501
1502
1503- (void) wasAddedToUniverse
1504{
1505 [super wasAddedToUniverse];
1506
1507 // if we have a universal id then we can proceed to set up any
1508 // stuff that happens when we get added to the UNIVERSE
1509 if (universalID != NO_TARGET)
1510 {
1511 // set up escorts
1512 if (([self status] == STATUS_IN_FLIGHT || [self status] == STATUS_LAUNCHING) && _pendingEscortCount != 0) // just popped into existence
1513 {
1514 [self setUpEscorts];
1515 }
1516 else
1517 {
1518 /* Earlier there was a silly log message here because I thought
1519 this would never happen, but wasn't entirely sure. Turns out
1520 it did!
1521 -- Ahruman 2009-09-13
1522 */
1523 _pendingEscortCount = 0;
1524 }
1525 }
1526
1527 // Tell subentities, too
1528 [subEntities makeObjectsPerformSelector:@selector(wasAddedToUniverse)];
1529
1530 [self resetExhaustPlumes];
1531}
1532
1533
1534- (void)wasRemovedFromUniverse
1535{
1536 [subEntities makeObjectsPerformSelector:@selector(wasRemovedFromUniverse)];
1537}
1538
1539
1540- (HPVector)absoluteTractorPosition
1541{
1542 return HPvector_add(position, vectorToHPVector(quaternion_rotate_vector([self normalOrientation], tractor_position)));
1543}
1544
1545
1546- (NSString *) beaconCode
1547{
1548 return _beaconCode;
1549}
1550
1551
1552- (void) setBeaconCode:(NSString *)bcode
1553{
1554 if ([bcode length] == 0) bcode = nil;
1555
1556 if (_beaconCode != bcode)
1557 {
1558 [_beaconCode release];
1559 _beaconCode = [bcode copy];
1560
1561 DESTROY(_beaconDrawable);
1562 }
1563 // if not blanking code and label is currently blank, default label to code
1564 if (bcode != nil && (_beaconLabel == nil || [_beaconLabel length] == 0))
1565 {
1566 [self setBeaconLabel:bcode];
1567 }
1568}
1569
1570
1571- (NSString *) beaconLabel
1572{
1573 return _beaconLabel;
1574}
1575
1576
1577- (void) setBeaconLabel:(NSString *)blabel
1578{
1579 if ([blabel length] == 0) blabel = nil;
1580
1581 if (_beaconLabel != blabel)
1582 {
1583 [_beaconLabel release];
1584 _beaconLabel = [OOExpand(blabel) retain];
1585 }
1586}
1587
1588
1589- (BOOL) isVisible
1590{
1591 return cam_zero_distance <= no_draw_distance;
1592}
1593
1594
1595- (BOOL) isBeacon
1596{
1597 return [self beaconCode] != nil;
1598}
1599
1600
1601- (id <OOHUDBeaconIcon>) beaconDrawable
1602{
1603 if (_beaconDrawable == nil)
1604 {
1605 NSString *beaconCode = [self beaconCode];
1606 NSUInteger length = [beaconCode length];
1607
1608 if (length > 1)
1609 {
1610 NSArray *iconData = [[UNIVERSE descriptions] oo_arrayForKey:beaconCode];
1611 if (iconData != nil) _beaconDrawable = [[OOPolygonSprite alloc] initWithDataArray:iconData outlineWidth:0.5 name:beaconCode];
1612 }
1613
1614 if (_beaconDrawable == nil)
1615 {
1616 if (length > 0) _beaconDrawable = [[beaconCode substringToIndex:1] retain];
1617 else _beaconDrawable = @"";
1618 }
1619 }
1620
1621 return _beaconDrawable;
1622}
1623
1624
1625- (Entity <OOBeaconEntity> *) prevBeacon
1626{
1627 return [_prevBeacon weakRefUnderlyingObject];
1628}
1629
1630
1631- (Entity <OOBeaconEntity> *) nextBeacon
1632{
1633 return [_nextBeacon weakRefUnderlyingObject];
1634}
1635
1636
1637- (void) setPrevBeacon:(Entity <OOBeaconEntity> *)beaconShip
1638{
1639 if (beaconShip != [self prevBeacon])
1640 {
1641 [_prevBeacon release];
1642 _prevBeacon = [beaconShip weakRetain];
1643 }
1644}
1645
1646
1647- (void) setNextBeacon:(Entity <OOBeaconEntity> *)beaconShip
1648{
1649 if (beaconShip != [self nextBeacon])
1650 {
1651 [_nextBeacon release];
1652 _nextBeacon = [beaconShip weakRetain];
1653 }
1654}
1655
1656
1657#define kBoulderRole (@"boulder")
1658
1659- (void) setIsBoulder:(BOOL)flag
1660{
1661 if (flag) [self addRole:kBoulderRole];
1662 else [self removeRole:kBoulderRole];
1663}
1664
1665
1666- (BOOL) isBoulder
1667{
1668 return [roleSet hasRole:kBoulderRole];
1669}
1670
1671
1672- (BOOL) isMinable
1673{
1674 if ([self hasRole:@"asteroid"] || [self isBoulder])
1675 {
1676 if (!noRocks)
1677 {
1678 return YES;
1679 }
1680 }
1681 return NO;
1682}
1683
1684
1685- (BOOL) countsAsKill
1686{
1687 return [[self shipInfoDictionary] oo_boolForKey:@"counts_as_kill" defaultValue:YES];
1688}
1689
1690
1691- (void) setUpEscorts
1692{
1693 // Ensure that we do not try to create escorts if we are an escort ship ourselves.
1694 // This could lead to circular reference memory overflows (e.g. "boa-mk2" trying to create 4 "boa-mk2"
1695 // escorts or the case of two ships specifying eachother as escorts) - Nikos 20090510
1696 if ([self isEscort])
1697 {
1698 OOLogWARN(@"ship.setUp.escortShipCircularReference",
1699 @"Ship %@ requested escorts, when it is an escort ship itself. Avoiding possible circular reference overflow by ignoring escort setup.", self);
1700 return;
1701 }
1702
1703 if ([shipinfoDictionary objectForKey:@"escort_roles"] != nil)
1704 {
1705 [self setUpMixedEscorts];
1706 return;
1707 }
1708
1709 NSString *defaultRole = @"escort";
1710 NSString *escortRole = nil;
1711 NSString *escortShipKey = nil;
1712
1713 if (_pendingEscortCount == 0) return;
1714
1715 if (_maxEscortCount < _pendingEscortCount)
1716 {
1717 if ([self hasPrimaryRole:@"police"] || [self hasPrimaryRole:@"hunter"])
1718 {
1719 _maxEscortCount = MAX_ESCORTS; // police and hunters get up to MAX_ESCORTS, overriding the 'escorts' key.
1720 [self updateEscortFormation];
1721 }
1722 else
1723 {
1724 _pendingEscortCount = _maxEscortCount; // other ships can only get what's defined inside their 'escorts' key.
1725 }
1726 }
1727
1728 if ([self isPolice]) defaultRole = @"wingman";
1729
1730 escortRole = [shipinfoDictionary oo_stringForKey:@"escort_role" defaultValue:nil];
1731 if (escortRole == nil)
1732 escortRole = [shipinfoDictionary oo_stringForKey:@"escort-role" defaultValue:defaultRole];
1733 if (![escortRole isEqualToString: defaultRole])
1734 {
1735 if (![[UNIVERSE newShipWithRole:escortRole] autorelease])
1736 {
1737 escortRole = defaultRole;
1738 }
1739 }
1740
1741 escortShipKey = [shipinfoDictionary oo_stringForKey:@"escort_ship" defaultValue:nil];
1742 if (escortShipKey == nil)
1743 escortShipKey = [shipinfoDictionary oo_stringForKey:@"escort-ship"];
1744
1745 if (escortShipKey != nil)
1746 {
1747 if (![[UNIVERSE newShipWithName:escortShipKey] autorelease])
1748 {
1749 escortShipKey = nil;
1750 }
1751 else
1752 {
1753 escortRole = [NSString stringWithFormat:@"[%@]",escortShipKey];
1754 }
1755 }
1756
1757 OOShipGroup *escortGroup = [self escortGroup];
1758 if ([self group] == nil)
1759 {
1760 [self setGroup:escortGroup]; // should probably become a copy of the escortGroup post NMSR.
1761 }
1762 [escortGroup setLeader:self];
1763
1764 [self refreshEscortPositions];
1765
1766 uint8_t currentEscortCount = [escortGroup count] - 1; // always at least 0.
1767
1768 while (_pendingEscortCount > 0 && ([self isThargoid] || currentEscortCount < _maxEscortCount))
1769 {
1770 // The following line adds escort 1 in position 1, etc... up to MAX_ESCORTS.
1771 HPVector ex_pos = [self coordinatesForEscortPosition:currentEscortCount];
1772
1773 ShipEntity *escorter = nil;
1774
1775 escorter = [UNIVERSE newShipWithRole:escortRole]; // retained
1776
1777 if (escorter == nil) break;
1778 [self setUpOneEscort:escorter inGroup:escortGroup withRole:escortRole atPosition:ex_pos andCount:currentEscortCount];
1779
1780 [escorter release];
1781
1782 _pendingEscortCount--;
1783 currentEscortCount = [escortGroup count] - 1;
1784 }
1785 // done assigning escorts
1786 _pendingEscortCount = 0;
1787}
1788
1789
1790- (void) setUpMixedEscorts
1791{
1792 NSArray *escortRoles = [shipinfoDictionary oo_arrayForKey:@"escort_roles" defaultValue:nil];
1793 if (escortRoles == nil)
1794 {
1795 OOLogWARN(@"eship.setUp.escortShipRoles",
1796 @"Ship %@ has bad escort_roles definition.", self);
1797 return;
1798 }
1799 NSEnumerator *edefEnumerator = nil;
1800 NSDictionary *escortDefinition = nil;
1801 NSDictionary *systeminfo = nil;
1802 OOGovernmentID government;
1803
1804 systeminfo = [UNIVERSE currentSystemData];
1805 government = [systeminfo oo_unsignedCharForKey:KEY_GOVERNMENT];
1806
1807 OOShipGroup *escortGroup = [self escortGroup];
1808 if ([self group] == nil)
1809 {
1810 [self setGroup:escortGroup]; // should probably become a copy of the escortGroup post NMSR.
1811 }
1812 [escortGroup setLeader:self];
1813 _maxEscortCount = MAX_ESCORTS;
1814 [self refreshEscortPositions];
1815
1816 uint8_t currentEscortCount = [escortGroup count] - 1; // always at least 0
1817
1818 _maxEscortCount = 0;
1819 int8_t i = 0;
1820 for (edefEnumerator = [escortRoles objectEnumerator]; (escortDefinition = [edefEnumerator nextObject]); )
1821 {
1822 if (currentEscortCount >= MAX_ESCORTS)
1823 {
1824 break;
1825 }
1826 // int rather than uint because, at least for min, there is a
1827 // use to giving a negative value
1828 int8_t min = [escortDefinition oo_intForKey:@"min" defaultValue:0];
1829 int8_t max = [escortDefinition oo_intForKey:@"max" defaultValue:2];
1830 NSString *escortRole = [escortDefinition oo_stringForKey:@"role" defaultValue:@"escort"];
1831 int8_t desired = max;
1832 if (min < desired)
1833 {
1834 for (i = min ; i < max ; i++)
1835 {
1836 if (Ranrot()%11 < government+2)
1837 {
1838 desired--;
1839 }
1840 }
1841 }
1842 for (i = 0; i < desired; i++)
1843 {
1844 if (currentEscortCount >= MAX_ESCORTS)
1845 {
1846 break;
1847 }
1848 if (![escortRole isEqualToString:@""])
1849 {
1850 HPVector ex_pos = [self coordinatesForEscortPosition:currentEscortCount];
1851 ShipEntity *escorter = [UNIVERSE newShipWithRole:escortRole]; // retained
1852 if (escorter == nil)
1853 {
1854 break;
1855 }
1856 [self setUpOneEscort:escorter inGroup:escortGroup withRole:escortRole atPosition:ex_pos andCount:currentEscortCount];
1857 [escorter release];
1858 }
1859 currentEscortCount++;
1860 _maxEscortCount++;
1861 }
1862 }
1863 // done assigning escorts
1864 _pendingEscortCount = 0;
1865}
1866
1867
1868- (void) setUpOneEscort:(ShipEntity *)escorter inGroup:(OOShipGroup *)escortGroup withRole:(NSString *)escortRole atPosition:(HPVector)ex_pos andCount:(uint8_t)currentEscortCount
1869{
1870 NSString *autoAI = nil;
1871 NSString *pilotRole = nil;
1872 NSDictionary *autoAIMap = nil;
1873 NSDictionary *escortShipDict = nil;
1874 AI *escortAI = nil;
1875 NSString *defaultRole = @"escort";
1876
1877 if ([self isPolice])
1878 {
1879 defaultRole = @"wingman";
1880 pilotRole = @"police"; // police are always insured.
1881 }
1882 else
1883 {
1884 pilotRole = bounty ? @"pirate" : @"hunter"; // hunters have insurancies, pirates not.
1885 }
1886
1887 double dd = escorter->collision_radius;
1888
1889 if (EXPECT(currentEscortCount < (uint8_t)MAX_ESCORTS))
1890 {
1891 // spread them around a little randomly
1892 ex_pos.x += dd * 6.0 * (randf() - 0.5);
1893 ex_pos.y += dd * 6.0 * (randf() - 0.5);
1894 ex_pos.z += dd * 6.0 * (randf() - 0.5);
1895 }
1896 else
1897 {
1898 // Thargoid armada(!) Add more distance between the 'escorts'.
1899 ex_pos.x += dd * 12.0 * (randf() - 0.5);
1900 ex_pos.y += dd * 12.0 * (randf() - 0.5);
1901 ex_pos.z += dd * 12.0 * (randf() - 0.5);
1902 }
1903
1904 [escorter setPosition:ex_pos]; // minimise lollipop flash
1905
1906 if ([escorter crew] == nil)
1907 {
1908 [escorter setSingleCrewWithRole:pilotRole];
1909 }
1910
1911 [escorter setPrimaryRole:defaultRole]; //for mothership
1912 // in case this hasn't yet been set, make sure escorts get a real scan class
1913 // shouldn't happen very often, but is possible
1914 if (scanClass == CLASS_NOT_SET)
1915 {
1916 scanClass = CLASS_NEUTRAL;
1917 }
1918 [escorter setScanClass:scanClass]; // you are the same as I
1919
1920 if ([self bounty] == 0) [escorter setBounty:0 withReason:kOOLegalStatusReasonSetup]; // Avoid dirty escorts for clean mothers
1921
1922 // find the right autoAI.
1923 escortShipDict = [escorter shipInfoDictionary];
1924 autoAIMap = [ResourceManager dictionaryFromFilesNamed:@"autoAImap.plist" inFolder:@"Config" andMerge:YES];
1925 autoAI = [autoAIMap oo_stringForKey:defaultRole];
1926 if (autoAI==nil) // no 'wingman' defined in autoAImap?
1927 {
1928 autoAI = [autoAIMap oo_stringForKey:@"escort" defaultValue:@"nullAI.plist"];
1929 }
1930
1931 escortAI = [escorter getAI];
1932
1933 // Let the populator decide which AI to use, unless we have a working alternative AI & we specify auto_ai = NO !
1934 if ( (escortRole && [escortShipDict oo_fuzzyBooleanForKey:@"auto_ai" defaultValue:YES])
1935 || ([[escortAI name] isEqualToString: @"nullAI.plist"] && ![autoAI isEqualToString:@"nullAI.plist"]) )
1936 {
1937 [escorter switchAITo:autoAI];
1938 }
1939
1940 [escorter setGroup:escortGroup];
1941 [escorter setOwner:self]; // mark self as group leader
1942
1943
1944 if ([self status] == STATUS_DOCKED)
1945 {
1946 [[self owner] addShipToLaunchQueue:escorter withPriority:NO];
1947 }
1948 else
1949 {
1950 [UNIVERSE addEntity:escorter]; // STATUS_IN_FLIGHT, AI state GLOBAL
1951 [escortAI setState:@"FLYING_ESCORT"]; // Begin escort flight. (If the AI doesn't define FLYING_ESCORT, this has no effect.)
1952 [escorter doScriptEvent:OOJSID("spawnedAsEscort") withArgument:self];
1953 }
1954
1955 if([escorter heatInsulation] < [self heatInsulation]) [escorter setHeatInsulation:[self heatInsulation]]; // give escorts same protection as mother.
1956 if(([escorter maxFlightSpeed] < cruiseSpeed) && ([escorter maxFlightSpeed] > cruiseSpeed * 0.3))
1957 cruiseSpeed = [escorter maxFlightSpeed] * 0.99; // adapt patrolSpeed to the slowest escort but ignore the very slow ones.
1958
1959
1960 if (bounty)
1961 {
1962 int extra = 1 | (ranrot_rand() & 15);
1963 // if mothership is offender, make sure escorter is too.
1964 [escorter markAsOffender:extra withReason:kOOLegalStatusReasonSetup];
1965 }
1966 else
1967 {
1968 // otherwise force the escort to be clean
1969 [escorter setBounty:0 withReason:kOOLegalStatusReasonSetup];
1970 }
1971
1972}
1973
1974- (NSString *)shipDataKey
1975{
1976 return _shipKey;
1977}
1978
1979
1980- (NSString *)shipDataKeyAutoRole
1981{
1982 return [[[NSString alloc] initWithFormat:@"[%@]",[self shipDataKey]] autorelease];
1983}
1984
1985
1986- (void)setShipDataKey:(NSString *)key
1987{
1988 DESTROY(_shipKey);
1989 _shipKey = [key copy];
1990}
1991
1992
1993- (NSDictionary *)shipInfoDictionary
1994{
1995 return shipinfoDictionary;
1996}
1997
1998
1999#define MAKE_VECTOR_ARRAY(v) [[[OONativeVector alloc] initWithVector:v] autorelease]
2000
2001- (NSArray *) getWeaponOffsetFrom:(NSDictionary *)dict withKey:(NSString *)key inMode:(NSString *)mode
2002{
2003 Vector offset;
2004 if ([mode isEqualToString:@"single"])
2005 {
2006 offset = vector_multiply_scalar([dict oo_vectorForKey:key defaultValue:kZeroVector],_scaleFactor);
2007 return [NSArray arrayWithObject:MAKE_VECTOR_ARRAY(offset)];
2008 }
2009 else
2010 {
2011 NSArray *offsets = [dict oo_arrayForKey:key defaultValue:nil];
2012 if (offsets == nil) {
2014 return [NSArray arrayWithObject:MAKE_VECTOR_ARRAY(offset)];
2015 }
2016 NSMutableArray *output = [NSMutableArray arrayWithCapacity:[offsets count]];
2017 NSUInteger i;
2018 for (i=0;i<[offsets count];i++) {
2019 offset = vector_multiply_scalar([offsets oo_vectorAtIndex:i defaultValue:kZeroVector],_scaleFactor);
2020 [output addObject:MAKE_VECTOR_ARRAY(offset)];
2021 }
2022 return [NSArray arrayWithArray:output];
2023 }
2024}
2025
2026
2027- (NSArray *) aftWeaponOffset
2028{
2029 return aftWeaponOffset;
2030}
2031
2032
2033- (NSArray *) forwardWeaponOffset
2034{
2035 return forwardWeaponOffset;
2036}
2037
2038
2039- (NSArray *) portWeaponOffset
2040{
2041 return portWeaponOffset;
2042}
2043
2044
2045- (NSArray *) starboardWeaponOffset
2046{
2047 return starboardWeaponOffset;
2048}
2049
2050
2051- (BOOL)isFrangible
2052{
2053 return isFrangible;
2054}
2055
2056
2057- (BOOL) suppressFlightNotifications
2058{
2059 return suppressAegisMessages;
2060}
2061
2062
2063- (OOScanClass) scanClass
2064{
2065 if (cloaking_device_active) return CLASS_NO_DRAW;
2066 return scanClass;
2067}
2068
2070
2071- (BOOL) canCollide
2072{
2073 int status = [self status];
2074 if (status == STATUS_COCKPIT_DISPLAY || status == STATUS_DEAD || status == STATUS_BEING_SCOOPED)
2075 {
2076 return NO;
2077 }
2078
2079 if (isWreckage)
2080 {
2081 // wreckage won't collide
2082 return NO;
2083 }
2084
2085 if (isMissile && [self shotTime] < 0.25) // not yet fused
2086 {
2087 return NO;
2088 }
2089
2090 return YES;
2091}
2092
2094{
2095 // octree check
2096 Octree *prime_octree = prime->octree;
2097 Octree *other_octree = other->octree;
2098
2099 HPVector prime_position = [prime absolutePositionForSubentity];
2100 Triangle prime_ijk = [prime absoluteIJKForSubentity];
2101 HPVector other_position = [other absolutePositionForSubentity];
2102 Triangle other_ijk = [other absoluteIJKForSubentity];
2103
2104 Vector relative_position_of_other = resolveVectorInIJK(HPVectorToVector(HPvector_between(prime_position, other_position)), prime_ijk);
2105 Triangle relative_ijk_of_other;
2106 relative_ijk_of_other.v[0] = resolveVectorInIJK(other_ijk.v[0], prime_ijk);
2107 relative_ijk_of_other.v[1] = resolveVectorInIJK(other_ijk.v[1], prime_ijk);
2108 relative_ijk_of_other.v[2] = resolveVectorInIJK(other_ijk.v[2], prime_ijk);
2109
2110 // check hull octree against other hull octree
2111 if ([prime_octree isHitByOctree:other_octree
2112 withOrigin:relative_position_of_other
2113 andIJK:relative_ijk_of_other])
2114 {
2115 return other;
2116 }
2117
2118 // check prime subentities against the other's hull
2119 NSArray *prime_subs = prime->subEntities;
2120 if (prime_subs)
2121 {
2122 NSUInteger i, n_subs = [prime_subs count];
2123 for (i = 0; i < n_subs; i++)
2124 {
2125 Entity* se = [prime_subs objectAtIndex:i];
2126 if ([se isShip] && [se canCollide] && doOctreesCollide((ShipEntity*)se, other))
2127 return other;
2128 }
2129 }
2130
2131 // check prime hull against the other's subentities
2132 NSArray *other_subs = other->subEntities;
2133 if (other_subs)
2134 {
2135 NSUInteger i, n_subs = [other_subs count];
2136 for (i = 0; i < n_subs; i++)
2137 {
2138 Entity* se = [other_subs objectAtIndex:i];
2139 if ([se isShip] && [se canCollide] && doOctreesCollide(prime, (ShipEntity*)se))
2140 return (ShipEntity*)se;
2141 }
2142 }
2143
2144 // check prime subenties against the other's subentities
2145 if ((prime_subs)&&(other_subs))
2146 {
2147 NSUInteger i, n_osubs = [other_subs count];
2148 for (i = 0; i < n_osubs; i++)
2149 {
2150 Entity* oe = [other_subs objectAtIndex:i];
2151 if ([oe isShip] && [oe canCollide])
2152 {
2153 NSUInteger j, n_psubs = [prime_subs count];
2154 for (j = 0; j < n_psubs; j++)
2155 {
2156 Entity* pe = [prime_subs objectAtIndex:j];
2157 if ([pe isShip] && [pe canCollide] && doOctreesCollide((ShipEntity*)pe, (ShipEntity*)oe))
2158 return (ShipEntity*)oe;
2159 }
2160 }
2161 }
2162 }
2163
2164 // fall through => no collision
2165 return nil;
2166}
2167
2168
2169- (BOOL) checkCloseCollisionWith:(Entity *)other
2170{
2171 if (other == nil) return NO;
2172 if ([collidingEntities containsObject:other]) return NO; // we know about this already!
2173
2174 ShipEntity *otherShip = nil;
2175 if ([other isShip]) otherShip = (ShipEntity *)other;
2176
2177 if ([self canScoop:otherShip]) return YES; // quick test - could this improve scooping for small ships? I think so!
2178
2179 if (otherShip != nil && trackCloseContacts)
2180 {
2181 // in update we check if close contacts have gone out of touch range (origin within our collision_radius)
2182 // here we check if something has come within that range
2183 HPVector otherPos = [otherShip position];
2184 OOUniversalID otherID = [otherShip universalID];
2185 NSString *other_key = [NSString stringWithFormat:@"%d", otherID];
2186
2187 if (![closeContactsInfo objectForKey:other_key] &&
2188 HPdistance2(position, otherPos) < collision_radius * collision_radius)
2189 {
2190 // calculate position with respect to our own position and orientation
2191 Vector dpos = HPVectorToVector(HPvector_between(position, otherPos));
2192 Vector rpos = make_vector(dot_product(dpos, v_right), dot_product(dpos, v_up), dot_product(dpos, v_forward));
2193 [closeContactsInfo setObject:[NSString stringWithFormat:@"%f %f %f", rpos.x, rpos.y, rpos.z] forKey: other_key];
2194
2195 // send AI a message about the touch
2196 OOWeakReference *temp = _primaryTarget;
2197 _primaryTarget = [otherShip weakRetain];
2198 [self doScriptEvent:OOJSID("shipCloseContact") withArgument:otherShip andReactToAIMessage:@"CLOSE CONTACT"];
2199 _primaryTarget = temp;
2200 }
2201 }
2202
2203 /* This does not appear to save a significant amount of time in
2204 * most situations. No significant change in frame rate with a
2205 * 350-segment planetary ring at 1400 collision candidates, even
2206 * on old hardware. There are perhaps situations in which it could
2207 * be a significant optimisation, but those are likely to also be
2208 * the situations where the effect of adding hundreds of extra
2209 * false-positive collisions leaves the player returning to a
2210 * mess... So, commented out: CIM 21 Jan 2014
2211 if (zero_distance > CLOSE_COLLISION_CHECK_MAX_RANGE2) // don't work too hard on entities that are far from the player
2212 return YES;
2213 */
2214
2215 if (otherShip != nil)
2216 {
2217 // check hull octree versus other hull octree
2218 collider = doOctreesCollide(self, otherShip);
2219 return (collider != nil);
2220 }
2221
2222 // default at this stage is to say YES they've collided!
2223 collider = other;
2224 return YES;
2225}
2226
2227
2228- (BoundingBox)findSubentityBoundingBox
2229{
2230 return [[self mesh] findSubentityBoundingBoxWithPosition:HPVectorToVector(position) rotMatrix:rotMatrix];
2231}
2232
2233
2234- (Triangle) absoluteIJKForSubentity
2235{
2236 Triangle result = {{ kBasisXVector, kBasisYVector, kBasisZVector }};
2237 Entity *last = nil;
2238 Entity *father = self;
2239 OOMatrix r_mat;
2240
2241 while ((father)&&(father != last) && (father != NO_TARGET))
2242 {
2243 r_mat = [father drawRotationMatrix];
2244 result.v[0] = OOVectorMultiplyMatrix(result.v[0], r_mat);
2245 result.v[1] = OOVectorMultiplyMatrix(result.v[1], r_mat);
2246 result.v[2] = OOVectorMultiplyMatrix(result.v[2], r_mat);
2247
2248 last = father;
2249 if (![last isSubEntity]) break;
2250 father = [father owner];
2251 }
2252 return result;
2253}
2254
2255
2256- (void) addSubentityToCollisionRadius:(Entity<OOSubEntity> *)subent
2257{
2258 if (!subent) return;
2259
2260 double distance = HPmagnitude([subent position]) + [subent findCollisionRadius];
2261 if ([subent isKindOfClass:[ShipEntity class]]) // Solid subentity
2262 {
2263 if (distance > collision_radius)
2264 {
2265 collision_radius = distance;
2266 }
2267
2268 mass += [subent mass];
2269 }
2270 if (distance > _profileRadius)
2271 {
2272 _profileRadius = distance;
2273 }
2274}
2275
2276
2277- (ShipEntity *) launchPodWithCrew:(NSArray *)podCrew
2278{
2279 ShipEntity *pod = nil;
2280
2281 pod = [UNIVERSE newShipWithRole:[shipinfoDictionary oo_stringForKey:@"escape_pod_role"]]; // or nil
2282 if (!pod)
2283 {
2284 // _role not defined? it might have _model defined;
2285 pod = [UNIVERSE newShipWithRole:[shipinfoDictionary oo_stringForKey:@"escape_pod_model" defaultValue:@"escape-capsule"]];
2286 if (!pod)
2287 {
2288 pod = [UNIVERSE newShipWithRole:@"escape-capsule"];
2289 OOLog(@"shipEntity.noEscapePod", @"Ship %@ has no correct escape_pod_role defined. Now using default capsule.", self);
2290 }
2291 }
2292
2293 if (pod)
2294 {
2295 [pod setOwner:self];
2296 [pod setTemperature:[self randomEjectaTemperatureWithMaxFactor:0.9]];
2297 [pod setCommodity:@"slaves" andAmount:1];
2298 [pod setCrew:podCrew];
2299 [pod switchAITo:@"oolite-shuttleAI.js"];
2300 [self dumpItem:pod]; // CLASS_CARGO, STATUS_IN_FLIGHT, AI state GLOBAL
2301 [pod release]; //release
2302 }
2303
2304 return pod;
2305}
2306
2307
2308- (BOOL) validForAddToUniverse
2309{
2310 if (shipinfoDictionary == nil)
2311 {
2312 OOLog(@"shipEntity.notDict", @"Ship %@ was not set up from dictionary.", self);
2313 return NO;
2314 }
2315 return [super validForAddToUniverse];
2316}
2317
2318
2319- (void) update:(OOTimeDelta)delta_t
2320{
2321 if (shipinfoDictionary == nil)
2322 {
2323 OOLog(@"shipEntity.notDict", @"Ship %@ was not set up from dictionary.", self);
2324 [UNIVERSE removeEntity:self];
2325 return;
2326 }
2327
2328 if (!isfinite(maxFlightSpeed))
2329 {
2330 OOLog(@"ship.sanityCheck.failed", @"Ship %@ %@ infinite top speed, clamped to 300.", self, @"had");
2331 maxFlightSpeed = 300;
2332 }
2333
2334 bool isSubEnt = [self isSubEntity];
2335
2336 if (isDemoShip)
2337 {
2338 if (demoRate > 0)
2339 {
2340 OOScalar cos1 = cos(M_PI * ([UNIVERSE getTime] - demoStartTime) * demoRate / 11);
2341 OOScalar sin1 = sin(M_PI * ([UNIVERSE getTime] - demoStartTime) * demoRate / 11);
2342 OOScalar cos2 = cos(-M_PI * ([UNIVERSE getTime] - demoStartTime) * demoRate / 15);
2343 OOScalar sin2 = sin(-M_PI * ([UNIVERSE getTime] - demoStartTime) * demoRate / 15);
2344 Quaternion q1 = make_quaternion(cos1, sin1*sqrt(3)/2, sin1/2, 0);
2345 Quaternion q2 = make_quaternion(cos2, -sin2*sqrt(4)/sqrt(5), 0, sin2*sqrt(1)/sqrt(5));
2346 [self setOrientation: quaternion_multiply(q2, quaternion_multiply(q1, demoStartOrientation))];
2347 }
2348
2349 [super update:delta_t];
2350 if ([self subEntityCount] > 0)
2351 {
2352 // only copy the subent array if there are subentities
2353 ShipEntity *se = nil;
2354 foreach (se, [self subEntities])
2355 {
2356 [se update:delta_t];
2357 if ([se isShip])
2358 {
2359 BoundingBox sebb = [se findSubentityBoundingBox];
2360 bounding_box_add_vector(&totalBoundingBox, sebb.max);
2361 bounding_box_add_vector(&totalBoundingBox, sebb.min);
2362 }
2363 }
2364 }
2365 return;
2366 }
2367
2368
2369 if (!isSubEnt)
2370 {
2371 if (scanClass == CLASS_NOT_SET)
2372 {
2373 scanClass = CLASS_NEUTRAL;
2374 OOLog(@"ship.sanityCheck.failed", @"Ship %@ %@ with scanClass CLASS_NOT_SET; forced to CLASS_NEUTRAL.", self, [self primaryRole]);
2375 }
2376
2377 [self updateTrackingCurve];
2378
2379 //
2380 // deal with collisions
2381 //
2382 [self manageCollisions];
2383
2384 // subentity collisions managed via parent entity
2385
2386 //
2387 // reset any inadvertant legal mishaps
2388 //
2389 if (scanClass == CLASS_POLICE)
2390 {
2391 if (bounty > 0)
2392 {
2393 [self setBounty:0 withReason:kOOLegalStatusReasonPoliceAreClean];
2394 }
2395 ShipEntity* target = [self primaryTarget];
2396 if ((target)&&([target scanClass] == CLASS_POLICE))
2397 {
2398 [self noteLostTarget];
2399 }
2400 }
2401
2402 if (trackCloseContacts)
2403 {
2404 // in checkCloseCollisionWith: we check if some thing has come within touch range (origin within our collision_radius)
2405 // here we check if it has gone outside that range
2406 NSString *other_key = nil;
2407
2408 // create a temp copy to iterate over, since we may want to
2409 // change the original
2410 NSDictionary *closeContactsTemp = [[NSDictionary alloc] initWithDictionary:closeContactsInfo];
2411 foreachkey (other_key, closeContactsTemp)
2412 {
2413 ShipEntity* other = [UNIVERSE entityForUniversalID:[other_key intValue]];
2414 if ((other != nil) && (other->isShip))
2415 {
2416 if (HPdistance2(position, other->position) > collision_radius * collision_radius) // moved beyond our sphere!
2417 {
2418 // calculate position with respect to our own position and orientation
2419 Vector dpos = HPVectorToVector(HPvector_between(position, other->position));
2420 Vector pos1 = make_vector(dot_product(dpos, v_right), dot_product(dpos, v_up), dot_product(dpos, v_forward));
2421 Vector pos0 = {0, 0, 0};
2422 ScanVectorFromString([closeContactsInfo objectForKey: other_key], &pos0);
2423 // send AI messages about the contact
2424 OOWeakReference *temp = _primaryTarget;
2425 _primaryTarget = [other weakRetain];
2426 if ((pos0.x < 0.0)&&(pos1.x > 0.0))
2427 {
2428 [self doScriptEvent:OOJSID("shipTraversePositiveX") withArgument:other andReactToAIMessage:@"POSITIVE X TRAVERSE"];
2429 }
2430 if ((pos0.x > 0.0)&&(pos1.x < 0.0))
2431 {
2432 [self doScriptEvent:OOJSID("shipTraverseNegativeX") withArgument:other andReactToAIMessage:@"NEGATIVE X TRAVERSE"];
2433 }
2434 if ((pos0.y < 0.0)&&(pos1.y > 0.0))
2435 {
2436 [self doScriptEvent:OOJSID("shipTraversePositiveY") withArgument:other andReactToAIMessage:@"POSITIVE Y TRAVERSE"];
2437 }
2438 if ((pos0.y > 0.0)&&(pos1.y < 0.0))
2439 {
2440 [self doScriptEvent:OOJSID("shipTraverseNegativeY") withArgument:other andReactToAIMessage:@"NEGATIVE Y TRAVERSE"];
2441 }
2442 if ((pos0.z < 0.0)&&(pos1.z > 0.0))
2443 {
2444 [self doScriptEvent:OOJSID("shipTraversePositiveZ") withArgument:other andReactToAIMessage:@"POSITIVE Z TRAVERSE"];
2445 }
2446 if ((pos0.z > 0.0)&&(pos1.z < 0.0))
2447 {
2448 [self doScriptEvent:OOJSID("shipTraverseNegativeZ") withArgument:other andReactToAIMessage:@"NEGATIVE Z TRAVERSE"];
2449 }
2450 _primaryTarget = temp;
2451 [closeContactsInfo removeObjectForKey: other_key];
2452 }
2453 }
2454 else
2455 {
2456 [closeContactsInfo removeObjectForKey: other_key];
2457 }
2458 }
2459 [closeContactsTemp release];
2460 } // end if trackCloseContacts
2461
2462 } // end if !isSubEntity
2463
2464
2465#ifndef NDEBUG
2466 // DEBUGGING
2467 if (reportAIMessages && (debugLastBehaviour != behaviour))
2468 {
2469 OOLog(kOOLogEntityBehaviourChanged, @"%@ behaviour is now %@", self, OOStringFromBehaviour(behaviour));
2470 debugLastBehaviour = behaviour;
2471 }
2472#endif
2473
2474 // cool all weapons.
2475 weapon_temp = fmaxf(weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2476 forward_weapon_temp = fmaxf(forward_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2477 aft_weapon_temp = fmaxf(aft_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2478 port_weapon_temp = fmaxf(port_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2479 starboard_weapon_temp = fmaxf(starboard_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2480
2481 // update time between shots
2482 shot_time += delta_t;
2483
2484 // handle radio message effects
2485 if (messageTime > 0.0)
2486 {
2487 messageTime -= delta_t;
2488 if (messageTime < 0.0) messageTime = 0.0;
2489 }
2490
2491 // temperature factors
2492 if(!isSubEnt)
2493 {
2494 double external_temp = 0.0;
2495 OOSunEntity *sun = [UNIVERSE sun];
2496 if (sun != nil)
2497 {
2498 // set the ambient temperature here
2499 double sun_zd = HPdistance2(position, [sun position]); // square of distance
2500 double sun_cr = sun->collision_radius;
2501 double alt1 = sun_cr * sun_cr / sun_zd;
2502 external_temp = SUN_TEMPERATURE * alt1;
2503 if ([sun goneNova]) external_temp *= 100;
2504
2505 if ([self hasFuelScoop] && alt1 > 0.75 && [self fuel] < [self fuelCapacity])
2506 {
2507 fuel_accumulator += (float)(delta_t * flightSpeed * 0.010 / [self fuelChargeRate]);
2508 // are we fast enough to collect any fuel?
2509 while (fuel_accumulator > 1.0f)
2510 {
2511 [self setFuel:[self fuel] + 1];
2512 fuel_accumulator -= 1.0f;
2513 [self doScriptEvent:OOJSID("shipScoopedFuel")];
2514 }
2515 }
2516 }
2517
2518 // work on the ship temperature
2519 //
2520 float heatThreshold = [self heatInsulation] * 100.0f;
2521 if (external_temp > heatThreshold && external_temp > ship_temperature)
2522 ship_temperature += (external_temp - ship_temperature) * delta_t * SHIP_INSULATION_FACTOR / [self heatInsulation];
2523 else
2524 {
2525 if (ship_temperature > SHIP_MIN_CABIN_TEMP)
2526 {
2527 ship_temperature += (external_temp - heatThreshold - ship_temperature) * delta_t * SHIP_COOLING_FACTOR / [self heatInsulation];
2528 if (ship_temperature < SHIP_MIN_CABIN_TEMP) ship_temperature = SHIP_MIN_CABIN_TEMP;
2529 }
2530 }
2531 }
2532 else //subents
2533 {
2534 ship_temperature = [[self owner] temperature];
2535 }
2536
2537 if (ship_temperature > SHIP_MAX_CABIN_TEMP)
2538 [self takeHeatDamage: delta_t * ship_temperature];
2539
2540 // are we burning due to low energy
2541 if ((energy < maxEnergy * 0.20)&&_showDamage) // prevents asteroid etc. from burning
2542 throw_sparks = YES;
2543
2544 // burning effects
2545 if (throw_sparks)
2546 {
2547 next_spark_time -= delta_t;
2548 if (next_spark_time < 0.0)
2549 {
2550 [self throwSparks];
2551 throw_sparks = NO; // until triggered again
2552 }
2553 }
2554
2555 if (!isSubEnt)
2556 {
2557
2558 // cloaking device
2559 if ([self hasCloakingDevice])
2560 {
2561 if (cloaking_device_active)
2562 {
2563 energy -= delta_t * CLOAKING_DEVICE_ENERGY_RATE;
2564 if (energy < CLOAKING_DEVICE_MIN_ENERGY)
2565 {
2566 [self deactivateCloakingDevice];
2567 if (energy < 0) energy = 0;
2568 }
2569 }
2570 }
2571
2572 // military_jammer
2573 if ([self hasMilitaryJammer])
2574 {
2575 if (military_jammer_active)
2576 {
2577 energy -= delta_t * MILITARY_JAMMER_ENERGY_RATE;
2578 if (energy < MILITARY_JAMMER_MIN_ENERGY)
2579 {
2580 military_jammer_active = NO;
2581 if (energy < 0) energy = 0;
2582 }
2583 }
2584 else
2585 {
2586 if (energy > 1.5 * MILITARY_JAMMER_MIN_ENERGY)
2587 military_jammer_active = YES;
2588 }
2589 }
2590
2591 // check outside factors
2592 /* aegis checks are expensive, so only do them once every km or so of flight
2593 * unlikely to be important otherwise. (every 100m if already close to
2594 * planet, to watch for surface)
2595
2596 * if have non-zero inertial velocity, need to check every frame,
2597 * as distanceTravelled does not include this component - CIM */
2598 if (_nextAegisCheck < distanceTravelled || !vector_equal([super velocity],kZeroVector))
2599 {
2600 aegis_status = [self checkForAegis]; // is a station or something nearby??
2601 if (aegis_status == AEGIS_NONE)
2602 {
2603 // in open space: check every km
2604 _nextAegisCheck = distanceTravelled + 1000.0;
2605 }
2606 else
2607 {
2608 // near planets: check every 100m
2609 _nextAegisCheck = distanceTravelled + 100.0;
2610 }
2611 }
2612 } // end if !isSubEntity
2613
2614 // scripting
2615 if (!haveExecutedSpawnAction)
2616 {
2617 // When crashing into a boulder, STATUS_LAUNCHING is sometimes skipped on scooping the resulting splinters.
2618 OOEntityStatus status = [self status];
2619 if (script != nil && (status == STATUS_IN_FLIGHT ||
2620 status == STATUS_LAUNCHING ||
2621 status == STATUS_BEING_SCOOPED ||
2622 (status == STATUS_ACTIVE && self == [UNIVERSE station])
2623 ))
2624 {
2625 [PLAYER setScriptTarget:self];
2626 [self doScriptEvent:OOJSID("shipSpawned")];
2627 if ([self status] != STATUS_DEAD) [PLAYER doScriptEvent:OOJSID("shipSpawned") withArgument:self];
2628 }
2629 haveExecutedSpawnAction = YES;
2630 }
2631 /* No point in starting the AI if still launching */
2632 if (!haveStartedJSAI && [self status] != STATUS_LAUNCHING)
2633 {
2634 haveStartedJSAI = YES;
2635 [self doScriptEvent:OOJSID("aiStarted")];
2636 }
2637
2638 // behaviours according to status and behaviour
2639 //
2640 if ([self status] == STATUS_LAUNCHING)
2641 {
2642 if ([UNIVERSE getTime] > launch_time + launch_delay) // move for while before thinking
2643 {
2644 StationEntity *stationLaunchedFrom = [UNIVERSE nearestEntityMatchingPredicate:IsStationPredicate parameter:NULL relativeToEntity:self];
2645 [self setStatus:STATUS_IN_FLIGHT];
2646 // awaken JS-based AIs
2647 haveStartedJSAI = YES;
2648 [self doScriptEvent:OOJSID("aiStarted")];
2649 [self doScriptEvent:OOJSID("shipLaunchedFromStation") withArgument:stationLaunchedFrom];
2650 [shipAI reactToMessage:@"LAUNCHED OKAY" context:@"launched"];
2651 }
2652 else
2653 {
2654 // ignore behaviour just keep moving...
2655 flightYaw = 0.0;
2656 [self applyAttitudeChanges:delta_t];
2657 [self applyThrust:delta_t];
2658 if (energy < maxEnergy)
2659 {
2660 energy += energy_recharge_rate * delta_t;
2661 if (energy > maxEnergy)
2662 {
2663 energy = maxEnergy;
2664 [self doScriptEvent:OOJSID("shipEnergyBecameFull")];
2665 [shipAI message:@"ENERGY_FULL"];
2666 }
2667 }
2668
2669 if ([self subEntityCount] > 0)
2670 {
2671 // only copy the subent array if there are subentities
2672 ShipEntity *se = nil;
2673 foreach (se, [self subEntities])
2674 {
2675 [se update:delta_t];
2676 }
2677 }
2678 // super update
2679 [super update:delta_t];
2680
2681 return;
2682 }
2683 }
2684 //
2685 // double check scooped behaviour
2686 //
2687 if ([self status] == STATUS_BEING_SCOOPED)
2688 {
2689 //if we are being tractored, but we have no owner, then we have a problem
2690 if (behaviour != BEHAVIOUR_TRACTORED || [self owner] == nil || [self owner] == self || [self owner] == NO_TARGET)
2691 {
2692 // escaped tractor beam
2693 [self setStatus:STATUS_IN_FLIGHT]; // should correct 'uncollidable objects' bug
2694 behaviour = BEHAVIOUR_IDLE;
2695 frustration = 0.0;
2696 [self setOwner:self];
2697 [shipAI exitStateMachineWithMessage:nil]; // Escapepods and others should continue their old AI here.
2698 }
2699 }
2700
2701 if ([self status] == STATUS_COCKPIT_DISPLAY)
2702 {
2703 flightYaw = 0.0;
2704 [self applyAttitudeChanges:delta_t];
2705 GLfloat range2 = 0.1 * HPdistance2(position, _destination) / (collision_radius * collision_radius);
2706 if ((range2 > 1.0)||(velocity.z > 0.0)) range2 = 1.0;
2707 position = HPvector_add(position, vectorToHPVector(vector_multiply_scalar(velocity, range2 * delta_t)));
2708 }
2709 else
2710 {
2711 [self processBehaviour:delta_t];
2712
2713 // manage energy
2714 if (energy < maxEnergy)
2715 {
2716 energy += energy_recharge_rate * delta_t;
2717 if (energy > maxEnergy)
2718 {
2719 energy = maxEnergy;
2720 [self doScriptEvent:OOJSID("shipEnergyBecameFull")];
2721 [shipAI message:@"ENERGY_FULL"];
2722 }
2723 }
2724
2725 if (!isSubEnt)
2726 {
2727 // update destination position for escorts
2728 [self refreshEscortPositions];
2729 if ([self hasEscorts])
2730 {
2731 ShipEntity *escort = nil;
2732 unsigned i = 0;
2733 // Note: works on escortArray rather than escortEnumerator because escorts may be mutated.
2734 foreach(escort, [self escortArray])
2735 {
2736 [escort setEscortDestination:[self coordinatesForEscortPosition:i++]];
2737 }
2738
2739 ShipEntity *leader = [[self escortGroup] leader];
2740 if (leader != nil && ([leader scanClass] != [self scanClass])) {
2741 OOLog(@"ship.sanityCheck.failed", @"Ship %@ escorting %@ with wrong scanclass!", self, leader);
2742 [[self escortGroup] removeShip:self];
2743 [self setEscortGroup:nil];
2744 }
2745 }
2746 }
2747 }
2748
2749 // rotational velocity
2750 if (!quaternion_equal(subentityRotationalVelocity, kIdentityQuaternion) &&
2751 !quaternion_equal(subentityRotationalVelocity, kZeroQuaternion))
2752 {
2753 Quaternion qf = subentityRotationalVelocity;
2754 qf.w *= (1.0 - delta_t);
2755 qf.x *= delta_t;
2756 qf.y *= delta_t;
2757 qf.z *= delta_t;
2758 [self setOrientation:quaternion_multiply(qf, orientation)];
2759 }
2760
2761 // reset totalBoundingBox
2762 totalBoundingBox = boundingBox;
2763
2764 // super update
2765 [super update:delta_t];
2766
2767 // update subentities
2768
2769 if ([self subEntityCount] > 0)
2770 {
2771 // only copy the subent array if there are subentities
2772 ShipEntity *se = nil;
2773 foreach (se, [self subEntities])
2774 {
2775 [se update:delta_t];
2776 if ([se isShip])
2777 {
2778 BoundingBox sebb = [se findSubentityBoundingBox];
2779 bounding_box_add_vector(&totalBoundingBox, sebb.max);
2780 bounding_box_add_vector(&totalBoundingBox, sebb.min);
2781 }
2782 }
2783 }
2784
2785 if (aiScriptWakeTime > 0 && [PLAYER clockTimeAdjusted] > aiScriptWakeTime)
2786 {
2787 aiScriptWakeTime = 0;
2788 [self doScriptEvent:OOJSID("aiAwoken")];
2789 }
2790}
2791
2792
2793- (void) processBehaviour:(OOTimeDelta)delta_t
2794{
2795 BOOL applyThrust = YES;
2796 switch (behaviour)
2797 {
2798 case BEHAVIOUR_TUMBLE :
2799 [self behaviour_tumble: delta_t];
2800 break;
2801
2802 case BEHAVIOUR_STOP_STILL :
2803 case BEHAVIOUR_STATION_KEEPING :
2804 [self behaviour_stop_still: delta_t];
2805 break;
2806
2807 case BEHAVIOUR_IDLE :
2808 if ([self isSubEntity])
2809 {
2810 applyThrust = NO;
2811 }
2812 [self behaviour_idle: delta_t];
2813 break;
2814
2815 case BEHAVIOUR_TRACTORED :
2816 [self behaviour_tractored: delta_t];
2817 break;
2818
2819 case BEHAVIOUR_TRACK_TARGET :
2820 [self behaviour_track_target: delta_t];
2821 break;
2822
2823 case BEHAVIOUR_INTERCEPT_TARGET :
2824 case BEHAVIOUR_COLLECT_TARGET :
2825 [self behaviour_intercept_target: delta_t];
2826 break;
2827
2828 case BEHAVIOUR_ATTACK_TARGET :
2829 [self behaviour_attack_target: delta_t];
2830 break;
2831
2832 case BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX :
2833 case BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE :
2834 [self behaviour_fly_to_target_six: delta_t];
2835 break;
2836
2837 case BEHAVIOUR_ATTACK_MINING_TARGET :
2838 [self behaviour_attack_mining_target: delta_t];
2839 break;
2840
2841 case BEHAVIOUR_ATTACK_FLY_TO_TARGET :
2842 [self behaviour_attack_fly_to_target: delta_t];
2843 break;
2844
2845 case BEHAVIOUR_ATTACK_FLY_FROM_TARGET :
2846 [self behaviour_attack_fly_from_target: delta_t];
2847 break;
2848
2849 case BEHAVIOUR_ATTACK_BREAK_OFF_TARGET :
2850 [self behaviour_attack_break_off_target: delta_t];
2851 break;
2852
2853 case BEHAVIOUR_ATTACK_SLOW_DOGFIGHT :
2854 [self behaviour_attack_slow_dogfight: delta_t];
2855 break;
2856
2857 case BEHAVIOUR_RUNNING_DEFENSE :
2858 [self behaviour_running_defense: delta_t];
2859 break;
2860
2861 case BEHAVIOUR_ATTACK_BROADSIDE :
2862 [self behaviour_attack_broadside: delta_t];
2863 break;
2864
2865 case BEHAVIOUR_ATTACK_BROADSIDE_LEFT :
2866 [self behaviour_attack_broadside_left: delta_t];
2867 break;
2868
2869 case BEHAVIOUR_ATTACK_BROADSIDE_RIGHT :
2870 [self behaviour_attack_broadside_right: delta_t];
2871 break;
2872
2873 case BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE :
2874 [self behaviour_close_to_broadside_range: delta_t];
2875 break;
2876
2877 case BEHAVIOUR_CLOSE_WITH_TARGET :
2878 [self behaviour_close_with_target: delta_t];
2879 break;
2880
2881 case BEHAVIOUR_ATTACK_SNIPER :
2882 [self behaviour_attack_sniper: delta_t];
2883 break;
2884
2885 case BEHAVIOUR_EVASIVE_ACTION :
2886 case BEHAVIOUR_FLEE_EVASIVE_ACTION :
2887 [self behaviour_evasive_action: delta_t];
2888 break;
2889
2890 case BEHAVIOUR_FLEE_TARGET :
2891 [self behaviour_flee_target: delta_t];
2892 break;
2893
2894 case BEHAVIOUR_FLY_RANGE_FROM_DESTINATION :
2895 [self behaviour_fly_range_from_destination: delta_t];
2896 break;
2897
2898 case BEHAVIOUR_FACE_DESTINATION :
2899 [self behaviour_face_destination: delta_t];
2900 break;
2901
2902 case BEHAVIOUR_LAND_ON_PLANET :
2903 [self behaviour_land_on_planet: delta_t];
2904 break;
2905
2906 case BEHAVIOUR_FORMATION_FORM_UP :
2907 [self behaviour_formation_form_up: delta_t];
2908 break;
2909
2910 case BEHAVIOUR_FLY_TO_DESTINATION :
2911 [self behaviour_fly_to_destination: delta_t];
2912 break;
2913
2914 case BEHAVIOUR_FLY_FROM_DESTINATION :
2915 case BEHAVIOUR_FORMATION_BREAK :
2916 [self behaviour_fly_from_destination: delta_t];
2917 break;
2918
2919 case BEHAVIOUR_AVOID_COLLISION :
2920 [self behaviour_avoid_collision: delta_t];
2921 break;
2922
2923 case BEHAVIOUR_TRACK_AS_TURRET :
2924 applyThrust = NO;
2925 [self behaviour_track_as_turret: delta_t];
2926 break;
2927
2928 case BEHAVIOUR_FLY_THRU_NAVPOINTS :
2929 [self behaviour_fly_thru_navpoints: delta_t];
2930 break;
2931
2932 case BEHAVIOUR_SCRIPTED_AI:
2933 case BEHAVIOUR_SCRIPTED_ATTACK_AI:
2934 [self behaviour_scripted_ai: delta_t];
2935 break;
2936
2937 case BEHAVIOUR_ENERGY_BOMB_COUNTDOWN:
2938 applyThrust = NO;
2939 // Do nothing
2940 break;
2941 }
2942
2943 // generally the checks above should be turning this *off* for subents
2944 if (applyThrust)
2945 {
2946 [self applyAttitudeChanges:delta_t];
2947 [self applyThrust:delta_t];
2948 }
2949}
2950
2951
2952// called when behaviour is unable to improve position
2953- (void)noteFrustration:(NSString *)context
2954{
2955 [shipAI reactToMessage:@"FRUSTRATED" context:context];
2956 [self doScriptEvent:OOJSID("shipAIFrustrated") withArgument:context];
2957}
2958
2959
2960- (void)respondToAttackFrom:(Entity *)from becauseOf:(Entity *)other
2961{
2962 Entity *source = nil;
2963
2964 if ([other isKindOfClass:[ShipEntity class]])
2965 {
2966 source = other;
2967
2968 // JSAIs handle friendly fire themselves
2969 if (![self hasNewAI])
2970 {
2971
2972 ShipEntity *hunter = (ShipEntity *)other;
2973 //if we are in the same group, then we have to be careful about how we handle things
2974 if ([self isPolice] && [hunter isPolice])
2975 {
2976 //police never get into a fight with each other
2977 return;
2978 }
2979
2980 OOShipGroup *group = [self group];
2981
2982 if (group != nil && group == [hunter group])
2983 {
2984 //we are in the same group, do we forgive you?
2985 //criminals are less likely to forgive
2986 if (randf() < (0.8 - (bounty/100)))
2987 {
2988 //it was an honest mistake, lets get on with it
2989 return;
2990 }
2991
2992 ShipEntity *groupLeader = [group leader];
2993 if (hunter == groupLeader)
2994 {
2995 //oops we were attacked by our leader, desert him
2996 [group removeShip:self];
2997 }
2998 else
2999 {
3000 //evict them from our group
3001 [group removeShip:hunter];
3002
3003 [groupLeader setFoundTarget:other];
3004 [groupLeader setPrimaryAggressor:hunter];
3005 [groupLeader respondToAttackFrom:from becauseOf:other];
3006 }
3007 }
3008 }
3009 }
3010 else
3011 {
3012 source = from;
3013 }
3014
3015 [self doScriptEvent:OOJSID("shipBeingAttacked") withArgument:source andReactToAIMessage:@"ATTACKED"];
3016 if ([source isShip]) [(ShipEntity *)source doScriptEvent:OOJSID("shipAttackedOther") withArgument:self];
3017}
3018
3019
3020// Equipment
3021
3022- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeWeapons:(BOOL)includeWeapons whileLoading:(BOOL)loading
3023{
3024 if ([self hasOneEquipmentItem:itemKey includeMissiles:includeWeapons whileLoading:loading]) return YES;
3025
3026 if (loading)
3027 {
3028 NSString *damaged = [itemKey stringByAppendingString:@"_DAMAGED"];
3029 if ([_equipment containsObject:damaged]) return YES;
3030 }
3031
3032 if (includeWeapons)
3033 {
3034 // Check for primary weapon
3036 if (!isWeaponNone(weaponType))
3037 {
3038 if ([self hasPrimaryWeapon:weaponType]) return YES;
3039 }
3040 }
3041
3042 return NO;
3043}
3044
3045
3046- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeMissiles:(BOOL)includeMissiles whileLoading:(BOOL)loading
3047{
3048 if ([_equipment containsObject:itemKey]) return YES;
3049
3050 if (loading)
3051 {
3052 NSString *damaged = [itemKey stringByAppendingString:@"_DAMAGED"];
3053 if ([_equipment containsObject:damaged]) return YES;
3054 }
3055
3056 if (includeMissiles && missiles > 0)
3057 {
3058 unsigned i;
3059 if ([itemKey isEqualToString:@"thargon"]) itemKey = @"EQ_THARGON";
3060 for (i = 0; i < missiles; i++)
3061 {
3062 if (missile_list[i] != nil && [[missile_list[i] identifier] isEqualTo:itemKey]) return YES;
3063 }
3064 }
3065
3066 return NO;
3067}
3068
3069
3070- (BOOL) hasPrimaryWeapon:(OOWeaponType)weaponType
3071{
3072 NSEnumerator *subEntEnum = nil;
3073 ShipEntity *subEntity = nil;
3074
3075 if ([[forward_weapon_type identifier] isEqualToString:[weaponType identifier]] ||
3076 [[aft_weapon_type identifier] isEqualToString:[weaponType identifier]] ||
3077 [[port_weapon_type identifier] isEqualToString:[weaponType identifier]] ||
3078 [[starboard_weapon_type identifier] isEqualToString:[weaponType identifier]])
3079 {
3080 return YES;
3081 }
3082
3083 for (subEntEnum = [self shipSubEntityEnumerator]; (subEntity = [subEntEnum nextObject]); )
3084 {
3085 if ([subEntity hasPrimaryWeapon:weaponType]) return YES;
3086 }
3087
3088 return NO;
3089}
3090
3091
3092- (NSUInteger) countEquipmentItem:(NSString *)eqkey
3093{
3094 NSString *eq = nil;
3095 NSUInteger count = 0;
3096 foreach (eq, _equipment)
3097 {
3098 if ([eqkey isEqualToString:eq])
3099 {
3100 ++count;
3101 }
3102 }
3103 return count;
3104}
3105
3106
3107- (BOOL) hasEquipmentItem:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons whileLoading:(BOOL)loading
3108{
3109 // this method is also used internally to find out if an equipped item is undamaged.
3110 if ([equipmentKeys isKindOfClass:[NSString class]])
3111 {
3112 return [self hasOneEquipmentItem:equipmentKeys includeWeapons:includeWeapons whileLoading:loading];
3113 }
3114 else
3115 {
3116 NSParameterAssert([equipmentKeys isKindOfClass:[NSArray class]] || [equipmentKeys isKindOfClass:[NSSet class]]);
3117
3118 id key = nil;
3119 foreach (key, equipmentKeys)
3120 {
3121 if ([self hasOneEquipmentItem:key includeWeapons:includeWeapons whileLoading:loading]) return YES;
3122 }
3123 }
3124
3125 return NO;
3126}
3127
3128
3129- (BOOL) hasEquipmentItem:(id)equipmentKeys
3130{
3131 return [self hasEquipmentItem:equipmentKeys includeWeapons:NO whileLoading:NO];
3132}
3133
3134
3135/* allows OXP equipment to provide core functions (or indeed OXP
3136 * functions, potentially) */
3137- (BOOL) hasEquipmentItemProviding:(NSString *)equipmentType
3138{
3139 NSString *key = nil;
3140 foreach (key, _equipment) {
3141 if ([key isEqualToString:equipmentType])
3142 {
3143 // equipment always provides itself
3144 return YES;
3145 }
3146 else
3147 {
3149 if (et != nil && [et provides:equipmentType])
3150 {
3151 return YES;
3152 }
3153 }
3154 }
3155 return NO;
3156}
3157
3158
3159- (NSString *) equipmentItemProviding:(NSString *)equipmentType
3160{
3161 NSString *key = nil;
3162 foreach (key, _equipment) {
3163 if ([key isEqualToString:equipmentType])
3164 {
3165 // equipment always provides itself
3166 return [[key copy] autorelease];
3167 }
3168 else
3169 {
3171 if (et != nil && [et provides:equipmentType])
3172 {
3173 return [[key copy] autorelease];
3174 }
3175 }
3176 }
3177 return nil;
3178}
3179
3180
3181- (BOOL) hasAllEquipment:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons whileLoading:(BOOL)loading
3182{
3183 NSEnumerator *keyEnum = nil;
3184 id key = nil;
3185
3186 if (_equipment == nil) return NO;
3187
3188 // Make sure it's an array or set, using a single-object set if it's a string.
3189 if ([equipmentKeys isKindOfClass:[NSString class]]) equipmentKeys = [NSArray arrayWithObject:equipmentKeys];
3190 else if (![equipmentKeys isKindOfClass:[NSArray class]] && ![equipmentKeys isKindOfClass:[NSSet class]]) return NO;
3191
3192 for (keyEnum = [equipmentKeys objectEnumerator]; (key = [keyEnum nextObject]); )
3193 {
3194 if (![self hasOneEquipmentItem:key includeWeapons:includeWeapons whileLoading:loading]) return NO;
3195 }
3196
3197 return YES;
3198}
3199
3200
3201- (BOOL) hasAllEquipment:(id)equipmentKeys
3202{
3203 return [self hasAllEquipment:equipmentKeys includeWeapons:NO whileLoading:NO];
3204}
3205
3206
3207- (BOOL) hasHyperspaceMotor
3208{
3209 return hyperspaceMotorSpinTime >= 0;
3210}
3211
3212
3213- (float) hyperspaceSpinTime
3214{
3215 return hyperspaceMotorSpinTime;
3216}
3217
3218
3219- (void) setHyperspaceSpinTime:(float)new
3220{
3221 hyperspaceMotorSpinTime = new;
3222}
3223
3224
3225- (BOOL) canAddEquipment:(NSString *)equipmentKey inContext:(NSString *)context
3226{
3227 if ([equipmentKey hasSuffix:@"_DAMAGED"])
3228 {
3229 equipmentKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
3230 }
3231
3232 NSString * lcEquipmentKey = [equipmentKey lowercaseString];
3233 if ([equipmentKey hasSuffix:@"MISSILE"]||[equipmentKey hasSuffix:@"MINE"]||([self isThargoid] && ([lcEquipmentKey hasPrefix:@"thargon"] || [lcEquipmentKey hasSuffix:@"thargon"])))
3234 {
3235 if (missiles >= max_missiles) return NO;
3236 }
3237
3239
3240 if (![eqType canCarryMultiple] && [self hasEquipmentItem:equipmentKey]) return NO;
3241 if (![self equipmentValidToAdd:equipmentKey inContext:context]) return NO;
3242
3243 return YES;
3244}
3245
3246
3247- (OOWeaponFacingSet) weaponFacings
3248{
3249 return weapon_facings;
3250}
3251
3252
3253- (OOWeaponType) weaponTypeIDForFacing:(OOWeaponFacing)facing strict:(BOOL)strict
3254{
3255 OOWeaponType weaponType = nil;
3256
3257 if (facing & weapon_facings)
3258 {
3259 switch (facing)
3260 {
3262 weaponType = forward_weapon_type;
3263 // if no forward weapon, and not carrying out a strict check, see if subentities have forward weapons, return the first one found.
3264 if (isWeaponNone(weaponType) && !strict)
3265 {
3266 NSEnumerator *subEntEnum = [self shipSubEntityEnumerator];
3267 ShipEntity *subEntity = nil;
3268 while (isWeaponNone(weaponType) && (subEntity = [subEntEnum nextObject]))
3269 {
3270 weaponType = subEntity->forward_weapon_type;
3271 }
3272 }
3273 break;
3274
3275 case WEAPON_FACING_AFT:
3276 weaponType = aft_weapon_type;
3277 break;
3278
3279 case WEAPON_FACING_PORT:
3280 weaponType = port_weapon_type;
3281 break;
3282
3284 weaponType = starboard_weapon_type;
3285 break;
3286
3287 case WEAPON_FACING_NONE:
3288 break;
3289 }
3290 }
3291 return weaponType;
3292}
3293
3294- (OOEquipmentType *) weaponTypeForFacing:(OOWeaponFacing)facing strict:(BOOL)strict
3295{
3296// OOWeaponType weaponType = [self weaponTypeIDForFacing:facing strict:strict];
3297// return [OOEquipmentType equipmentTypeWithIdentifier:OOEquipmentIdentifierFromWeaponType(weaponType)];
3298 return [self weaponTypeIDForFacing:facing strict:strict];
3299}
3300
3301
3302- (NSArray *) missilesList
3303{
3304 // if missile_list is empty, avoid exception and return empty NSArray instead
3305 return missile_list[0] != nil ? [NSArray arrayWithObjects:missile_list count:missiles] :
3306 [NSArray array];
3307}
3308
3309
3310- (NSArray *) passengerListForScripting
3311{
3312 return [NSArray array];
3313}
3314
3315
3316- (NSArray *) parcelListForScripting
3317{
3318 return [NSArray array];
3319}
3320
3321
3322- (NSArray *) contractListForScripting
3323{
3324 return [NSArray array];
3325}
3326
3327
3328- (OOEquipmentType *) generateMissileEquipmentTypeFrom:(NSString *)role
3329{
3330 /* The generated missile equipment type provides for backward compatibility with pre-1.74 OXPs missile_roles
3331 and follows this template:
3332
3333 //NPC equipment, incompatible with player ship. Not buyable because of its TL.
3334 (
3335 100, 100000, "Missile",
3336 "EQ_X_MISSILE",
3337 "Unidentified missile type.",
3338 {
3339 is_external_store = true;
3340 }
3341 )
3342 */
3343 NSArray *itemInfo = [NSArray arrayWithObjects:@"100", @"100000", @"Missile", role, @"Unidentified missile type.",
3344 [NSDictionary dictionaryWithObjectsAndKeys: @"true", @"is_external_store", nil], nil];
3345
3348}
3349
3350
3351- (NSArray *) equipmentListForScripting
3352{
3353 NSArray *eqTypes = [OOEquipmentType allEquipmentTypes];
3354 NSMutableArray *quip = [NSMutableArray arrayWithCapacity:[eqTypes count]];
3355 NSEnumerator *eqTypeEnum = nil;
3356 OOEquipmentType *eqType = nil;
3357 BOOL isDamaged;
3358
3359 for (eqTypeEnum = [eqTypes objectEnumerator]; (eqType = [eqTypeEnum nextObject]); )
3360 {
3361 // Equipment list, consistent with the rest of the API - Kaks
3362 if ([eqType canCarryMultiple])
3363 {
3364 NSString *damagedIdentifier = [[eqType identifier] stringByAppendingString:@"_DAMAGED"];
3365 NSUInteger i, count = 0;
3366 count += [self countEquipmentItem:[eqType identifier]];
3367 count += [self countEquipmentItem:damagedIdentifier];
3368 for (i=0;i<count;i++)
3369 {
3370 [quip addObject:eqType];
3371 }
3372 }
3373 else
3374 {
3375 isDamaged = [self hasEquipmentItem:[[eqType identifier] stringByAppendingString:@"_DAMAGED"]];
3376 if ([self hasEquipmentItem:[eqType identifier]] || isDamaged)
3377 {
3378 [quip addObject:eqType];
3379 }
3380 }
3381 }
3382
3383 // Passengers - not supported yet for NPCs, but it's here for genericity.
3384 if ([self passengerCapacity] > 0)
3385 {
3386 eqType = [OOEquipmentType equipmentTypeWithIdentifier:@"EQ_PASSENGER_BERTH"];
3387 //[quip addObject:[self eqDictionaryWithType:eqType isDamaged:NO]];
3388 [quip addObject:eqType];
3389 }
3390
3391 return [[quip copy] autorelease];
3392}
3393
3394
3395- (BOOL) equipmentValidToAdd:(NSString *)equipmentKey inContext:(NSString *)context
3396{
3397 return [self equipmentValidToAdd:equipmentKey whileLoading:NO inContext:context];
3398}
3399
3400
3401- (BOOL) equipmentValidToAdd:(NSString *)equipmentKey whileLoading:(BOOL)loading inContext:(NSString *)context
3402{
3403 OOEquipmentType *eqType = nil;
3404 BOOL validationForDamagedEquipment = NO;
3405
3406 if ([equipmentKey hasSuffix:@"_DAMAGED"])
3407 {
3408 equipmentKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
3409 }
3410
3411 eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey];
3412 if (eqType == nil) return NO;
3413
3414 // need to know if we are trying to add a Repair version of the equipment. In some cases
3415 // (e.g. available cargo space required), it makes sense to deny installation of equipment
3416 // if the condition is not satisfied, but it doesn't make sense to deny repair when the
3417 // equipment is already installed. For now, we are checking only the cargo space condition,
3418 // but other conditions might need to be revised too. - Nikos, 20151115
3419 if ([self hasEquipmentItem:[eqType damagedIdentifier]])
3420 {
3421 validationForDamagedEquipment = YES;
3422 }
3423
3424 // not all conditions make sence checking while loading a game with already purchaged equipment.
3425 // while loading, we mainly need to catch changes when the installed oxps set has changed since saving.
3426 if ([eqType requiresEmptyPylon] && [self missileCount] >= [self missileCapacity] && !loading) return NO;
3427 if ([eqType requiresMountedPylon] && [self missileCount] == 0 && !loading) return NO;
3428 if ([self availableCargoSpace] < [eqType requiredCargoSpace] && !validationForDamagedEquipment && !loading) return NO;
3429 if ([eqType requiresEquipment] != nil && ![self hasAllEquipment:[eqType requiresEquipment] includeWeapons:YES whileLoading:loading]) return NO;
3430 if ([eqType requiresAnyEquipment] != nil && ![self hasEquipmentItem:[eqType requiresAnyEquipment] includeWeapons:YES whileLoading:loading]) return NO;
3431 if ([eqType incompatibleEquipment] != nil && [self hasEquipmentItem:[eqType incompatibleEquipment] includeWeapons:YES whileLoading:loading]) return NO;
3432 if ([eqType requiresCleanLegalRecord] && [self legalStatus] != 0 && !loading) return NO;
3433 if ([eqType requiresNonCleanLegalRecord] && [self legalStatus] == 0 && !loading) return NO;
3434 if ([eqType requiresFreePassengerBerth] && [self passengerCount] >= [self passengerCapacity]) return NO;
3435 if ([eqType requiresFullFuel] && [self fuel] < [self fuelCapacity] && !loading) return NO;
3436 if ([eqType requiresNonFullFuel] && [self fuel] >= [self fuelCapacity] && !loading) return NO;
3437
3438 if (!loading)
3439 {
3440 NSString *condition_script = [eqType conditionScript];
3441 if (condition_script != nil)
3442 {
3443 OOJSScript *condScript = [UNIVERSE getConditionScript:condition_script];
3444 if (condScript != nil) // should always be non-nil, but just in case
3445 {
3446 JSContext *JScontext = OOJSAcquireContext();
3447 BOOL OK;
3448 JSBool allow_addition = false;
3449 jsval result;
3450 jsval args[] = { OOJSValueFromNativeObject(JScontext, equipmentKey) , OOJSValueFromNativeObject(JScontext, self) , OOJSValueFromNativeObject(JScontext, context)};
3451
3452 OK = [condScript callMethod:OOJSID("allowAwardEquipment")
3453 inContext:JScontext
3454 withArguments:args count:sizeof args / sizeof *args
3455 result:&result];
3456
3457 if (OK) OK = JS_ValueToBoolean(JScontext, result, &allow_addition);
3458
3459 OOJSRelinquishContext(JScontext);
3460
3461 if (OK && !allow_addition)
3462 {
3463 /* if the script exists, the function exists, the function
3464 * returns a bool, and that bool is false, block
3465 * addition. Otherwise allow it as default */
3466 return NO;
3467 }
3468 }
3469 }
3470 }
3471
3472 if ([self isPlayer])
3473 {
3474 if (![eqType isAvailableToPlayer]) return NO;
3475 if (![eqType isAvailableToAll])
3476 {
3477 // find options that agree with this ship. Only player ships have these options.
3479 NSDictionary *shipyardInfo = [registry shipyardInfoForKey:[self shipDataKey]];
3480 NSMutableSet *options = [NSMutableSet setWithArray:[shipyardInfo oo_arrayForKey:KEY_OPTIONAL_EQUIPMENT]];
3481 [options addObjectsFromArray:[[shipyardInfo oo_dictionaryForKey:KEY_STANDARD_EQUIPMENT] oo_arrayForKey:KEY_EQUIPMENT_EXTRAS]];
3482 if (![options containsObject:equipmentKey]) return NO;
3483 }
3484 }
3485 else
3486 {
3487 if (![eqType isAvailableToNPCs]) return NO;
3488 }
3489
3490 return YES;
3491}
3492
3493
3494- (BOOL) setWeaponMount:(OOWeaponFacing)facing toWeapon:(NSString *)eqKey
3495{
3496 // sets WEAPON_NONE if not recognised
3497 if (weapon_facings & facing)
3498 {
3500 switch (facing)
3501 {
3503 forward_weapon_type = chosen_weapon;
3504 break;
3505
3506 case WEAPON_FACING_AFT:
3507 aft_weapon_type = chosen_weapon;
3508 break;
3509
3510 case WEAPON_FACING_PORT:
3511 port_weapon_type = chosen_weapon;
3512 break;
3513
3515 starboard_weapon_type = chosen_weapon;
3516 break;
3517
3518 case WEAPON_FACING_NONE:
3519 break;
3520 }
3521
3522 return YES;
3523 }
3524 else
3525 {
3526 return NO;
3527 }
3528}
3529
3530
3531- (BOOL) addEquipmentItem:(NSString *)equipmentKey inContext:(NSString *)context
3532{
3533 return [self addEquipmentItem:equipmentKey withValidation:YES inContext:context];
3534}
3535
3536
3537- (BOOL) addEquipmentItem:(NSString *)equipmentKey withValidation:(BOOL)validateAddition inContext:(NSString *)context
3538{
3539 OOEquipmentType *eqType = nil;
3540 NSString *lcEquipmentKey = [equipmentKey lowercaseString];
3541 NSString *damagedKey;
3542 BOOL isEqThargon = [lcEquipmentKey hasSuffix:@"thargon"] || [lcEquipmentKey hasPrefix:@"thargon"];
3543 BOOL isRepairedEquipment = NO;
3544
3545 if([lcEquipmentKey isEqualToString:@"thargon"]) equipmentKey = @"EQ_THARGON";
3546
3547 // canAddEquipment always checks if the undamaged version is equipped.
3548 if (validateAddition == YES && ![self canAddEquipment:equipmentKey inContext:context]) return NO;
3549
3550 if ([equipmentKey hasSuffix:@"_DAMAGED"])
3551 {
3552 eqType = [OOEquipmentType equipmentTypeWithIdentifier:[equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]]];
3553 }
3554 else
3555 {
3556 eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey];
3557 // in case we have the damaged version!
3558 if (![eqType canCarryMultiple])
3559 {
3560 damagedKey = [equipmentKey stringByAppendingString:@"_DAMAGED"];
3561 if ([_equipment containsObject:damagedKey])
3562 {
3563 [_equipment removeObject:damagedKey];
3564 isRepairedEquipment = YES;
3565 }
3566 }
3567 }
3568
3569 // does this equipment actually exist?
3570 if (eqType == nil) return NO;
3571
3572 // special cases
3573 if ([eqType isMissileOrMine] || ([self isThargoid] && isEqThargon))
3574 {
3575 if (missiles >= max_missiles) return NO;
3576
3577 missile_list[missiles] = eqType;
3578 missiles++;
3579 return YES;
3580 }
3581
3582 // don't add any thargons to non-thargoid ships.
3583 if(isEqThargon) return NO;
3584
3585 // we can theoretically add a damaged weapon, but not a working one.
3586 if([equipmentKey hasPrefix:@"EQ_WEAPON"] && ![equipmentKey hasSuffix:@"_DAMAGED"])
3587 {
3588 return NO;
3589 }
3590 // end special cases
3591
3592 if (_equipment == nil) _equipment = [[NSMutableArray alloc] init];
3593
3594 if (![equipmentKey isEqualToString:@"EQ_PASSENGER_BERTH"] && !isRepairedEquipment)
3595 {
3596 // Add to equipment_weight with all other equipment.
3597 equipment_weight += [eqType requiredCargoSpace];
3598 if (equipment_weight > max_cargo)
3599 {
3600 // should not even happen with old save games. Reject equipment now.
3601 equipment_weight -= [eqType requiredCargoSpace];
3602 return NO;
3603 }
3604 }
3605
3606
3607 if (!isPlayer)
3608 {
3609 if ([equipmentKey isEqual:@"EQ_CARGO_BAY"])
3610 {
3611 max_cargo += extra_cargo;
3612 }
3613 else if([equipmentKey isEqualToString:@"EQ_SHIELD_BOOSTER"])
3614 {
3615 maxEnergy += 256.0f;
3616 }
3617 if([equipmentKey isEqualToString:@"EQ_SHIELD_ENHANCER"])
3618 {
3619 maxEnergy += 256.0f;
3620 energy_recharge_rate *= 1.5;
3621 }
3622 }
3623 // add the equipment
3624 [_equipment addObject:equipmentKey];
3625 [self doScriptEvent:OOJSID("equipmentAdded") withArgument:equipmentKey];
3626 return YES;
3627}
3628
3629
3630- (NSEnumerator *) equipmentEnumerator
3631{
3632 return [_equipment objectEnumerator];
3633}
3634
3635
3636- (NSUInteger) equipmentCount
3637{
3638 return [_equipment count];
3639}
3640
3641
3642- (void) removeEquipmentItem:(NSString *)equipmentKey
3643{
3644 NSString *equipmentTypeCheckKey = equipmentKey;
3645 NSString *lcEquipmentKey = [equipmentKey lowercaseString];
3646 NSUInteger equipmentIndex = NSNotFound;
3647 // determine the equipment type and make sure it works also in the case of damaged equipment
3648 if ([equipmentKey hasSuffix:@"_DAMAGED"])
3649 {
3650 equipmentTypeCheckKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
3651 }
3652 OOEquipmentType *eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentTypeCheckKey];
3653 if (eqType == nil) return;
3654
3655 if ([eqType isMissileOrMine] || ([self isThargoid] && ([lcEquipmentKey hasSuffix:@"thargon"] || [lcEquipmentKey hasPrefix:@"thargon"])))
3656 {
3657 [self removeExternalStore:eqType];
3658 }
3659 else
3660 {
3661 if ([_equipment containsObject:equipmentKey])
3662 {
3663 if (![equipmentKey isEqualToString:@"EQ_PASSENGER_BERTH"])
3664 {
3665 equipment_weight -= [eqType requiredCargoSpace]; // all other cases;
3666 }
3667
3668 if ([equipmentKey isEqualToString:@"EQ_CLOAKING_DEVICE"])
3669 {
3670 if ([self isCloaked]) [self setCloaked:NO];
3671 }
3672
3673 if (!isPlayer)
3674 {
3675 if([equipmentKey isEqualToString:@"EQ_SHIELD_BOOSTER"])
3676 {
3677 maxEnergy -= 256.0f;
3678 if (maxEnergy < energy) energy = maxEnergy;
3679 }
3680 else if([equipmentKey isEqualToString:@"EQ_SHIELD_ENHANCER"])
3681 {
3682 maxEnergy -= 256.0f;
3683 energy_recharge_rate /= 1.5;
3684 if (maxEnergy < energy) energy = maxEnergy;
3685 }
3686 else if ([equipmentKey isEqual:@"EQ_CARGO_BAY"])
3687 {
3688 max_cargo -= extra_cargo;
3689 }
3690 }
3691 }
3692
3693 if (![equipmentKey hasSuffix:@"_DAMAGED"] && ![eqType canCarryMultiple])
3694 {
3695 NSString *damagedKey = [equipmentKey stringByAppendingString:@"_DAMAGED"];
3696 if ([_equipment containsObject:damagedKey])
3697 {
3698 equipmentIndex = [_equipment indexOfObject:damagedKey];
3699 if (equipmentIndex != NSNotFound)
3700 {
3701 // remove damaged counterpart
3702 [_equipment removeObjectAtIndex:equipmentIndex];
3703 }
3704 equipment_weight -= [eqType requiredCargoSpace];
3705 }
3706 }
3707 equipmentIndex = [_equipment indexOfObject:equipmentKey];
3708 if (equipmentIndex != NSNotFound)
3709 {
3710 [_equipment removeObjectAtIndex:equipmentIndex];
3711 }
3712 // this event must come after the item is actually removed
3713 [self doScriptEvent:OOJSID("equipmentRemoved") withArgument:equipmentKey];
3714
3715 // if all docking computers are damaged while active
3716 if ([self isPlayer] && [self status] == STATUS_AUTOPILOT_ENGAGED && ![self hasDockingComputer])
3717 {
3718 [(PlayerEntity *)self disengageAutopilot];
3719 }
3720
3721
3722 if ([_equipment count] == 0) [self removeAllEquipment];
3723 }
3724}
3725
3726
3727- (BOOL) removeExternalStore:(OOEquipmentType *)eqType
3728{
3729 NSString *identifier = [eqType identifier];
3730 unsigned i;
3731
3732 for (i = 0; i < missiles; i++)
3733 {
3734 if ([[missile_list[i] identifier] isEqualTo:identifier])
3735 {
3736 // now 'delete' [i] by compacting the array
3737 while ( ++i < missiles ) missile_list[i - 1] = missile_list[i];
3738
3739 missiles--;
3740 return YES;
3741 }
3742 }
3743 return NO;
3744}
3745
3746
3747- (OOEquipmentType *) verifiedMissileTypeFromRole:(NSString *)role
3748{
3749 NSString *eqRole = nil;
3750 NSString *shipKey = nil;
3751 ShipEntity *missile = nil;
3752 OOEquipmentType *missileType = nil;
3753 BOOL isRandomMissile = [role isEqualToString:@"missile"];
3754
3755 if (isRandomMissile)
3756 {
3757 while (!shipKey)
3758 {
3759 shipKey = [UNIVERSE randomShipKeyForRoleRespectingConditions:role];
3760 if (!shipKey)
3761 {
3762 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", @"random missile", shipKey, [self name], @"shipdata", @"Trying another missile.");
3763 }
3764 }
3765 }
3766 else
3767 {
3768 shipKey = [UNIVERSE randomShipKeyForRoleRespectingConditions:role];
3769 if (!shipKey)
3770 {
3771 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", @"missile_role", role, [self name], @"shipdata", @" Using defaults instead.");
3772 return nil;
3773 }
3774 }
3775
3776 eqRole = [OOEquipmentType getMissileRegistryRoleForShip:shipKey]; // eqRole != role for generic missiles.
3777
3778 if (eqRole == nil)
3779 {
3780 missile = [UNIVERSE newShipWithName:shipKey];
3781 if (!missile)
3782 {
3783 if (isRandomMissile)
3784 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", @"random missile", shipKey, [self name], @"shipdata", @"Trying another missile.");
3785 else
3786 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", @"missile_role", role, [self name], @"shipdata", @" Using defaults instead.");
3787
3788 [OOEquipmentType setMissileRegistryRole:@"" forShip:shipKey]; // no valid role for this shipKey
3789 if (isRandomMissile) return [self verifiedMissileTypeFromRole:role];
3790 else return nil;
3791 }
3792
3793 if(isRandomMissile)
3794 {
3795 id value;
3796 NSEnumerator *enumerator = [[[missile roleSet] roles] objectEnumerator];
3797
3798 while ((value = [enumerator nextObject]))
3799 {
3800 role = (NSString *)value;
3801 missileType = [OOEquipmentType equipmentTypeWithIdentifier:role];
3802 // ensure that we have a missile or mine
3803 if ([missileType isMissileOrMine]) break;
3804 }
3805
3806 if (![missileType isMissileOrMine])
3807 {
3808 role = shipKey; // unique identifier to use in lieu of a valid equipment type if none are defined inside the generic missile roleset.
3809 }
3810 }
3811
3812 missileType = [OOEquipmentType equipmentTypeWithIdentifier:role];
3813
3814 if (!missileType)
3815 {
3816 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", (isRandomMissile ? @"random missile" : @"missile_role"), role, [self name], @"equipment", @" Enabling compatibility mode.");
3817 missileType = [self generateMissileEquipmentTypeFrom:role];
3818 }
3819
3821 [missile release];
3822 }
3823 else
3824 {
3825 if ([eqRole isEqualToString:@""])
3826 {
3827 // wrong ship definition, already written to the log in a previous call.
3828 if (isRandomMissile) return [self verifiedMissileTypeFromRole:role]; // try and find a valid missile with role 'missile'.
3829 return nil;
3830 }
3831 missileType = [OOEquipmentType equipmentTypeWithIdentifier:eqRole];
3832 }
3833
3834 return missileType;
3835}
3836
3837
3838- (OOEquipmentType *) selectMissile
3839{
3840 OOEquipmentType *missileType = nil;
3841 NSString *role = nil;
3842 double chance = randf();
3843 BOOL thargoidMissile = NO;
3844
3845 if ([self isThargoid])
3846 {
3847 if (_missileRole != nil) missileType = [self verifiedMissileTypeFromRole:_missileRole];
3848 if (missileType == nil) {
3849 _missileRole = @"EQ_THARGON"; // no valid missile_role defined, use thargoid fallback from now on.
3850 missileType = [self verifiedMissileTypeFromRole:_missileRole];
3851 }
3852 }
3853 else
3854 {
3855 // All other ships: random role 10% of the cases when auto weapons is set, if a missile_role is defined.
3856 // Without auto weapons, never random.
3857 float randomSelectionChance = chance;
3858 if(![self hasAutoWeapons]) randomSelectionChance = 0.0f;
3859 if (randomSelectionChance < 0.9f && _missileRole != nil)
3860 {
3861 missileType = [self verifiedMissileTypeFromRole:_missileRole];
3862 }
3863
3864 if (missileType == nil) // the random 10% , or no valid missile_role defined
3865 {
3866 if (chance < 0.9f && _missileRole != nil) // no valid missile_role defined?
3867 {
3868 _missileRole = nil; // use generic ship fallback from now on.
3869 }
3870
3871 // assign random missiles 20% of the time without missile_role (or 10% with valid missile_role)
3872 if (chance > 0.8f) role = @"missile";
3873 // otherwise use the standard role
3874 else role = @"EQ_MISSILE";
3875
3876 missileType = [self verifiedMissileTypeFromRole:role];
3877 }
3878 }
3879
3880 if (missileType == nil) OOLogERR(@"ship.setUp.missiles", @"could not resolve missile / mine type for ship \"%@\". Original missile role:\"%@\".", [self name],_missileRole);
3881
3882 role = [[missileType identifier] lowercaseString];
3883 thargoidMissile = [self isThargoid] && ([role hasSuffix:@"thargon"] || [role hasPrefix:@"thargon"]);
3884
3885 if (thargoidMissile || (!thargoidMissile && [missileType isMissileOrMine]))
3886 {
3887 return missileType;
3888 }
3889 else
3890 {
3891 OOLogWARN(@"ship.setUp.missiles", @"missile_role \"%@\" is not a valid missile / mine type for ship \"%@\".%@", [missileType identifier] , [self name],@" No missile selected.");
3892 return nil;
3893 }
3894}
3895
3896
3897- (void) removeAllEquipment
3898{
3899 [_equipment release];
3900 _equipment = nil;
3901}
3902
3903
3904- (OOCreditsQuantity) removeMissiles
3905{
3906 missiles = 0;
3907 return 0;
3908}
3909
3910
3911- (NSUInteger) parcelCount
3912{
3913 return 0;
3914}
3915
3916
3917- (NSUInteger) passengerCount
3918{
3919 return 0;
3920}
3921
3922
3923- (NSUInteger) passengerCapacity
3924{
3925 return 0;
3926}
3927
3928
3929- (NSUInteger) missileCount
3930{
3931 return missiles;
3932}
3933
3934
3935- (NSUInteger) missileCapacity
3936{
3937 return max_missiles;
3938}
3939
3940
3941- (NSUInteger) extraCargo
3942{
3943 return extra_cargo;
3944}
3945
3946
3947/* This is used for e.g. displaying the HUD icon */
3948- (BOOL) hasScoop
3949{
3950 return [self hasEquipmentItemProviding:@"EQ_FUEL_SCOOPS"] || [self hasEquipmentItemProviding:@"EQ_CARGO_SCOOPS"];
3951}
3952
3953
3954- (BOOL) hasFuelScoop
3955{
3956 return [self hasEquipmentItemProviding:@"EQ_FUEL_SCOOPS"];
3957}
3958
3959
3960/* No such core equipment item, but EQ_FUEL_SCOOPS provides it */
3961- (BOOL) hasCargoScoop
3962{
3963 return [self hasEquipmentItemProviding:@"EQ_CARGO_SCOOPS"];
3964}
3965
3966
3967- (BOOL) hasECM
3968{
3969 return [self hasEquipmentItemProviding:@"EQ_ECM"];
3970}
3971
3972
3973- (BOOL) hasCloakingDevice
3974{
3975 /* TODO: Checks above stop this being 'providing'. */
3976 return [self hasEquipmentItem:@"EQ_CLOAKING_DEVICE"];
3977}
3978
3979
3980- (BOOL) hasMilitaryScannerFilter
3981{
3982#if USEMASC
3983 return [self hasEquipmentItemProviding:@"EQ_MILITARY_SCANNER_FILTER"];
3984#else
3985 return NO;
3986#endif
3987}
3988
3989
3990- (BOOL) hasMilitaryJammer
3991{
3992#if USEMASC
3993 return [self hasEquipmentItemProviding:@"EQ_MILITARY_JAMMER"];
3994#else
3995 return NO;
3996#endif
3997}
3998
3999
4000- (BOOL) hasExpandedCargoBay
4001{
4002 /* Not 'providing' - controlled through scripts */
4003 return [self hasEquipmentItem:@"EQ_CARGO_BAY"];
4004}
4005
4006
4007- (BOOL) hasShieldBooster
4008{
4009 /* Not 'providing' - controlled through scripts */
4010 return [self hasEquipmentItem:@"EQ_SHIELD_BOOSTER"];
4011}
4012
4013
4014- (BOOL) hasMilitaryShieldEnhancer
4015{
4016 /* Not 'providing' - controlled through scripts */
4017 return [self hasEquipmentItem:@"EQ_NAVAL_SHIELD_BOOSTER"];
4018}
4019
4020
4021- (BOOL) hasHeatShield
4022{
4023 return [self hasEquipmentItemProviding:@"EQ_HEAT_SHIELD"];
4024}
4025
4026
4027- (BOOL) hasFuelInjection
4028{
4029 return [self hasEquipmentItemProviding:@"EQ_FUEL_INJECTION"];
4030}
4031
4032
4033- (BOOL) hasCascadeMine
4034{
4035 /* TODO: this could be providing since theoretically OXP
4036 * deployable mines could also do cascade effects, but there are
4037 * probably better ways to manage OXP pylon AI */
4038 return [self hasEquipmentItem:@"EQ_QC_MINE" includeWeapons:YES whileLoading:NO];
4039}
4040
4041
4042- (BOOL) hasEscapePod
4043{
4044 return [self hasEquipmentItemProviding:@"EQ_ESCAPE_POD"];
4045}
4046
4047
4048- (BOOL) hasDockingComputer
4049{
4050 return [self hasEquipmentItemProviding:@"EQ_DOCK_COMP"];
4051}
4052
4053
4054- (BOOL) hasGalacticHyperdrive
4055{
4056 return [self hasEquipmentItemProviding:@"EQ_GAL_DRIVE"];
4057}
4058
4059
4060- (float) shieldBoostFactor
4061{
4062 float boostFactor = 1.0f;
4063 if ([self hasShieldBooster]) boostFactor += 1.0f;
4064 if ([self hasMilitaryShieldEnhancer]) boostFactor += 1.0f;
4065
4066 return boostFactor;
4067}
4068
4069
4070/* These next three are never called as of 12/12/2014, as NPCs don't
4071 * have shields and PlayerEntity overrides these. */
4072- (float) maxForwardShieldLevel
4073{
4074 return BASELINE_SHIELD_LEVEL * [self shieldBoostFactor];
4075}
4076
4077
4078- (float) maxAftShieldLevel
4079{
4080 return BASELINE_SHIELD_LEVEL * [self shieldBoostFactor];
4081}
4082
4083
4084- (float) shieldRechargeRate
4085{
4086 return [self hasMilitaryShieldEnhancer] ? 3.0f : 2.0f;
4087}
4088
4089
4090- (double) maxHyperspaceDistance
4091{
4092 return MAX_JUMP_RANGE;
4093}
4094
4095- (float) afterburnerFactor
4096{
4097 return afterburner_speed_factor;
4098}
4099
4100
4101- (float) afterburnerRate
4102{
4103 return afterburner_rate;
4104}
4105
4106
4107- (void) setAfterburnerFactor:(GLfloat)new
4108{
4109 afterburner_speed_factor = new;
4110}
4111
4112
4113- (void) setAfterburnerRate:(GLfloat)new
4114{
4115 afterburner_rate = new;
4116}
4117
4118
4119- (float) maxThrust
4120{
4121 return max_thrust;
4122}
4123
4124
4125- (void) setMaxThrust:(GLfloat)new
4126{
4127 max_thrust = new;
4128}
4129
4130
4131- (float) thrust
4132{
4133 return thrust;
4134}
4135
4136
4138// //
4139// behaviours //
4140// //
4141- (void) behaviour_stop_still:(double) delta_t
4142{
4143 stick_roll = 0.0;
4144 stick_pitch = 0.0;
4145 stick_yaw = 0.0;
4146 [self applySticks:delta_t];
4147
4148
4149
4150}
4151
4152
4153- (void) behaviour_idle:(double) delta_t
4154{
4155 stick_yaw = 0.0;
4156 if ((!isStation)&&(scanClass != CLASS_BUOY))
4157 {
4158 stick_roll = 0.0;
4159 }
4160 else
4161 {
4162 stick_roll = flightRoll;
4163 }
4164 if (scanClass != CLASS_BUOY)
4165 {
4166 stick_pitch = 0.0;
4167 }
4168 else
4169 {
4170 stick_pitch = flightPitch;
4171 }
4172 [self applySticks:delta_t];
4173
4174
4175}
4176
4177
4178- (void) behaviour_tumble:(double) delta_t
4179{
4180 [self applySticks:delta_t];
4181
4182
4183}
4184
4185
4186- (void) behaviour_tractored:(double) delta_t
4187{
4188 desired_range = collision_radius * 2.0;
4189 ShipEntity* hauler = (ShipEntity*)[self owner];
4190 if ((hauler)&&([hauler isShip]))
4191 {
4192 _destination = [hauler absoluteTractorPosition];
4193 double distance = [self rangeToDestination];
4194 if (distance < desired_range)
4195 {
4196 [self performTumble];
4197 [self setStatus:STATUS_IN_FLIGHT];
4198 [hauler scoopUp:self];
4199 return;
4200 }
4201 GLfloat tf = TRACTOR_FORCE / mass;
4202 // adjust for difference in velocity (spring rule)
4203 Vector dv = vector_between([self velocity], [hauler velocity]);
4204 GLfloat moment = delta_t * 0.25 * tf;
4205 velocity.x += moment * dv.x;
4206 velocity.y += moment * dv.y;
4207 velocity.z += moment * dv.z;
4208 // acceleration = force / mass
4209 // force proportional to distance (spring rule)
4210 HPVector dp = HPvector_between(position, _destination);
4211 moment = delta_t * 0.5 * tf;
4212 velocity.x += moment * dp.x;
4213 velocity.y += moment * dp.y;
4214 velocity.z += moment * dp.z;
4215 // force inversely proportional to distance
4216 GLfloat d2 = HPmagnitude2(dp);
4217 moment = (d2 > 0.0)? delta_t * 5.0 * tf / d2 : 0.0;
4218 if (d2 > 0.0)
4219 {
4220 velocity.x += moment * dp.x;
4221 velocity.y += moment * dp.y;
4222 velocity.z += moment * dp.z;
4223 }
4224 //
4225 if ([self status] == STATUS_BEING_SCOOPED)
4226 {
4227 BOOL lost_contact = (distance > hauler->collision_radius + collision_radius + 250.0f); // 250m range for tractor beam
4228 if ([hauler isPlayer])
4229 {
4230 switch ([(PlayerEntity*)hauler dialFuelScoopStatus])
4231 {
4234 lost_contact = YES; // don't draw
4235 break;
4236
4237 case SCOOP_STATUS_OKAY:
4239 break;
4240 }
4241 }
4242
4243 if (lost_contact) // 250m range for tractor beam
4244 {
4245 // escaped tractor beam
4246 [self setStatus:STATUS_IN_FLIGHT];
4247 behaviour = BEHAVIOUR_IDLE;
4248 [self setThrust:[self maxThrust]]; // restore old thrust.
4249 frustration = 0.0;
4250 [self setOwner:self];
4251 [shipAI exitStateMachineWithMessage:nil]; // exit nullAI.plist
4252 return;
4253 }
4254 else if ([hauler isPlayer])
4255 {
4256 [(PlayerEntity*)hauler setScoopsActive];
4257 }
4258 }
4259 }
4260
4261// being tractored; sticks ignored - CIM
4262 flightYaw = 0.0;
4263
4264 desired_speed = 0.0;
4265 thrust = 25.0; // used to damp velocity (must be less than hauler thrust)
4266
4267 thrust = 0.0; // must reset thrust now
4268}
4269
4270
4271- (void) behaviour_track_target:(double) delta_t
4272{
4273 if ([self primaryTarget] == nil)
4274 {
4275 [self noteLostTargetAndGoIdle];
4276 return;
4277 }
4278 [self trackPrimaryTarget:delta_t:NO]; // applies sticks
4279 if ([self hasProximityAlertIgnoringTarget:YES])
4280 {
4281 [self avoidCollision];
4282 }
4283
4284}
4285
4286
4287- (void) behaviour_intercept_target:(double) delta_t
4288{
4289 double range = [self rangeToPrimaryTarget];
4290 if (behaviour == BEHAVIOUR_INTERCEPT_TARGET)
4291 {
4292 desired_speed = maxFlightSpeed;
4293 if (range < desired_range)
4294 {
4295 [shipAI reactToMessage:@"DESIRED_RANGE_ACHIEVED" context:@"BEHAVIOUR_INTERCEPT_TARGET"];
4296 [self doScriptEvent:OOJSID("shipAchievedDesiredRange")];
4297
4298 }
4299 desired_speed = maxFlightSpeed * [self trackPrimaryTarget:delta_t:NO];
4300 }
4301 else
4302 {
4303 // = BEHAVIOUR_COLLECT_TARGET
4304 ShipEntity* target = [self primaryTarget];
4305// if somehow ended up in this state but target is not cargo, stop
4306// trying to scoop it
4307 if (!target || [target scanClass] != CLASS_CARGO || [target cargoType] == CARGO_NOT_CARGO)
4308 {
4309 [self noteLostTargetAndGoIdle];
4310 return;
4311 }
4312 double target_speed = [target speed];
4313 double eta = range / (flightSpeed - target_speed);
4314 double last_success_factor = success_factor;
4315 double last_distance = last_success_factor;
4316 double distance = [self rangeToDestination];
4317 success_factor = distance;
4318 //
4319 double slowdownTime = 96.0 / (thrust*SHIP_THRUST_FACTOR); // more thrust implies better slowing
4320 double minTurnSpeedFactor = 0.005 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds
4321
4322 if ((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor))
4323 desired_speed = flightSpeed * 0.75; // cut speed by 50% to a minimum minTurnSpeedFactor of speed
4324 else
4325 desired_speed = maxFlightSpeed;
4326
4327 if (desired_speed < target_speed)
4328 {
4329 desired_speed += target_speed;
4330 if (target_speed > maxFlightSpeed)
4331 {
4332 [self noteLostTargetAndGoIdle];
4333 return;
4334 }
4335 }
4336 if (desired_speed > maxFlightSpeed)
4337 { // never use injectors for scooping
4338 desired_speed = maxFlightSpeed;
4339 }
4340
4341 _destination = target->position;
4342 desired_range = 0.5 * target->collision_radius;
4343 [self trackDestination: delta_t : NO];
4344
4345 //
4346 if (distance < last_distance) // improvement
4347 {
4348 frustration -= delta_t;
4349 if (frustration < 0.0)
4350 frustration = 0.0;
4351 }
4352 else
4353 {
4354 frustration += delta_t * 0.9;
4355 if (frustration > 10.0) // 10s of frustration
4356 {
4357 [self noteFrustration:@"BEHAVIOUR_INTERCEPT_TARGET"];
4358 frustration -= 5.0; //repeat after another five seconds' frustration
4359 }
4360 }
4361 }
4362 if ([self hasProximityAlertIgnoringTarget:YES])
4363 {
4364 [self avoidCollision];
4365 }
4366
4367
4368}
4369
4370
4371- (void) behaviour_attack_break_off_target:(double) delta_t
4372{
4373 if (![self canStillTrackPrimaryTarget])
4374 {
4375 [self noteLostTargetAndGoIdle];
4376 return;
4377 }
4378 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
4379 float max_available_speed = maxFlightSpeed;
4380 double range = [self rangeToPrimaryTarget];
4381 if (canBurn) max_available_speed *= [self afterburnerFactor];
4382
4383 desired_speed = max_available_speed;
4384
4385 Entity* target = [self primaryTarget];
4386
4387 if (desired_speed > maxFlightSpeed)
4388 {
4389 double target_speed = [target speed];
4390 if (desired_speed > target_speed * 3.0)
4391 {
4392 desired_speed = maxFlightSpeed; // don't overuse the injectors
4393 }
4394 }
4395
4396 if (cloakAutomatic) [self activateCloakingDevice];
4397 if ([self hasProximityAlertIgnoringTarget:NO])
4398 {
4399 [self avoidCollision];
4400 return;
4401 }
4402
4403 frustration += delta_t;
4404 if (frustration > 15.0 && accuracy >= COMBAT_AI_DOGFIGHTER && !canBurn)
4405 {
4406 desired_speed = maxFlightSpeed / 2.0;
4407 }
4408 double aspect = [self approachAspectToPrimaryTarget];
4409 if (range > 3000.0 || ([target isShip] && [(ShipEntity*)target primaryTarget] != self) || frustration - floor(frustration) > fmin(1.6/max_flight_roll,aspect))
4410 {
4411 [self trackPrimaryTarget:delta_t:YES];
4412 }
4413 else
4414 {
4415// less useful at long range if not under direct fire
4416 [self evasiveAction:delta_t];
4417 }
4418
4419 if (range > COMBAT_OUT_RANGE_FACTOR * weaponRange)
4420 {
4421 behaviour = BEHAVIOUR_ATTACK_TARGET;
4422 }
4423 else if (aspect < -0.75 && accuracy >= COMBAT_AI_DOGFIGHTER)
4424 {
4425 behaviour = BEHAVIOUR_ATTACK_SLOW_DOGFIGHT;
4426 }
4427 else if (frustration > 10.0 && [self approachAspectToPrimaryTarget] < 0.85 && forward_weapon_temp < COMBAT_AI_WEAPON_TEMP_READY)
4428 {
4429 frustration = 0.0;
4430 if (accuracy >= COMBAT_AI_DOGFIGHTER)
4431 {
4432 behaviour = BEHAVIOUR_ATTACK_SLOW_DOGFIGHT;
4433 }
4434 else
4435 {
4436 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4437 }
4438 }
4439
4440 flightYaw = 0.0;
4441}
4442
4443
4444- (void) behaviour_attack_slow_dogfight:(double) delta_t
4445{
4446 if (![self canStillTrackPrimaryTarget])
4447 {
4448 [self noteLostTargetAndGoIdle];
4449 return;
4450 }
4451 if ([self hasProximityAlertIgnoringTarget:YES])
4452 {
4453 [self avoidCollision];
4454 return;
4455 }
4456 double range = [self rangeToPrimaryTarget];
4457 ShipEntity* target = [self primaryTarget];
4458 double aspect = [self approachAspectToPrimaryTarget];
4459 if (range < 2.5*(collision_radius+target->collision_radius) && [self proximityAlert] == target && aspect > 0) {
4460 desired_speed = maxFlightSpeed;
4461 [self avoidCollision];
4462 return;
4463 }
4464 if (aspect < -0.5 && range > COMBAT_IN_RANGE_FACTOR * weaponRange * 2.0)
4465 {
4466 behaviour = BEHAVIOUR_ATTACK_TARGET;
4467 }
4468 else if (aspect < -0.5)
4469 {
4470// mostly behind target - try to stay there and keep up
4471 desired_speed = fmin(maxFlightSpeed * 0.5,[target speed]*0.5);
4472 }
4473 else if (aspect < 0.3)
4474 {
4475// to side of target - slow right down
4476 desired_speed = maxFlightSpeed * 0.1;
4477 }
4478 else
4479 {
4480// coming to front of target - accelerate for a quick getaway
4481 desired_speed = maxFlightSpeed * fmin(aspect*2.5,1.0);
4482 }
4483 if (aspect > 0.85)
4484 {
4485 behaviour = BEHAVIOUR_ATTACK_BREAK_OFF_TARGET;
4486 }
4487 if (aspect > 0.0)
4488 {
4489 frustration += delta_t;
4490 }
4491 else
4492 {
4493 frustration -= delta_t;
4494 }
4495 if (frustration > 10.0)
4496 {
4497 desired_speed /= 2.0;
4498 }
4499 else if (frustration < 0.0)
4500 frustration = 0.0;
4501
4502 [self trackPrimaryTarget:delta_t:NO];
4503
4504 if (missiles) [self considerFiringMissile:delta_t];
4505
4506 if (cloakAutomatic) [self activateCloakingDevice];
4507
4508}
4509
4510
4511- (void) behaviour_evasive_action:(double) delta_t
4512{
4513 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
4514 float max_available_speed = maxFlightSpeed;
4515// double range = [self rangeToPrimaryTarget];
4516 if (canBurn) max_available_speed *= [self afterburnerFactor];
4517 desired_speed = max_available_speed;
4518 if (desired_speed > maxFlightSpeed)
4519 {
4520 ShipEntity* target = [self primaryTarget];
4521 double target_speed = [target speed];
4522 if (desired_speed > target_speed)
4523 {
4524 desired_speed = maxFlightSpeed; // don't overuse the injectors
4525 }
4526 }
4527
4528 if (cloakAutomatic) [self activateCloakingDevice];
4529 if ([self proximityAlert] != nil)
4530 {
4531 [self avoidCollision];
4532 return;
4533 }
4534
4535 [self evasiveAction:delta_t];
4536
4537 frustration += delta_t;
4538
4539 if (frustration > 0.5)
4540 {
4541 if (behaviour == BEHAVIOUR_FLEE_EVASIVE_ACTION)
4542 {
4543 [self setEvasiveJink:400.0];
4544 behaviour = BEHAVIOUR_FLEE_TARGET;
4545 }
4546 else
4547 {
4548 behaviour = BEHAVIOUR_ATTACK_TARGET;
4549 }
4550 }
4551
4552 flightYaw = 0.0;
4553
4554 // probably only useful for Thargoids, except for the occasional opportunist
4555 [self fireMainWeapon:[self rangeToPrimaryTarget]];
4556
4557}
4558
4559
4560- (void) behaviour_attack_target:(double) delta_t
4561{
4562 double range = [self rangeToPrimaryTarget];
4563
4564 if (cloakAutomatic) [self activateCloakingDevice];
4565
4566/* Start of behaviour selection:
4567 * Anything beyond the basics should require accuracy >= COMBAT_AI_ISNT_AWFUL
4568 * Anything fancy should require accuracy >= COMBAT_AI_IS_SMART
4569 * If precise aim is required, behaviour should have accuracy >= COMBAT_AI_TRACKS_CLOSER
4570 * - CIM
4571 */
4572
4573 OOWeaponType forward_weapon_real_type = forward_weapon_type;
4574 GLfloat forward_weapon_real_temp = forward_weapon_temp;
4575
4576// if forward weapon is actually on a subent
4577 if (isWeaponNone(forward_weapon_real_type))
4578 {
4579 BOOL hasTurrets = NO;
4580 NSEnumerator *subEnum = [self shipSubEntityEnumerator];
4581 ShipEntity *se = nil;
4582 while (isWeaponNone(forward_weapon_real_type) && (se = [subEnum nextObject]))
4583 {
4584 forward_weapon_real_type = se->forward_weapon_type;
4585 forward_weapon_real_temp = se->forward_weapon_temp;
4586 if (se->behaviour == BEHAVIOUR_TRACK_AS_TURRET)
4587 {
4588 hasTurrets = YES;
4589 }
4590 }
4591 if (isWeaponNone(forward_weapon_real_type) && hasTurrets)
4592 { // safety for ships only equipped with turrets
4593 forward_weapon_real_type = OOWeaponTypeFromEquipmentIdentifierSloppy(@"EQ_WEAPON_PULSE_LASER");
4594 forward_weapon_real_temp = COMBAT_AI_WEAPON_TEMP_USABLE * 0.9;
4595 }
4596 }
4597
4598 if ([forward_weapon_real_type isTurretLaser])
4599 {
4600 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
4601 }
4602 else
4603 {
4604 BOOL in_good_range = aim_tolerance*range < COMBAT_AI_CONFIDENCE_FACTOR;
4605
4606 BOOL aft_weapon_ready = !isWeaponNone(aft_weapon_type) && (aft_weapon_temp < COMBAT_AI_WEAPON_TEMP_READY) && in_good_range;
4607 BOOL forward_weapon_ready = !isWeaponNone(forward_weapon_real_type) && (forward_weapon_real_temp < COMBAT_AI_WEAPON_TEMP_READY); // does not require in_good_range
4608 BOOL port_weapon_ready = !isWeaponNone(port_weapon_type) && (port_weapon_temp < COMBAT_AI_WEAPON_TEMP_READY) && in_good_range;
4609 BOOL starboard_weapon_ready = !isWeaponNone(starboard_weapon_type) && (starboard_weapon_temp < COMBAT_AI_WEAPON_TEMP_READY) && in_good_range;
4610// if no weapons cool enough to be good choices, be less picky
4611 BOOL weapons_heating = NO;
4612 if (!forward_weapon_ready && !aft_weapon_ready && !port_weapon_ready && !starboard_weapon_ready)
4613 {
4614 weapons_heating = YES;
4615 aft_weapon_ready = !isWeaponNone(aft_weapon_type) && (aft_weapon_temp < COMBAT_AI_WEAPON_TEMP_USABLE) && in_good_range;
4616 forward_weapon_ready = !isWeaponNone(forward_weapon_real_type) && (forward_weapon_real_temp < COMBAT_AI_WEAPON_TEMP_USABLE); // does not require in_good_range
4617 port_weapon_ready = !isWeaponNone(port_weapon_type) && (port_weapon_temp < COMBAT_AI_WEAPON_TEMP_USABLE) && in_good_range;
4618 starboard_weapon_ready = !isWeaponNone(starboard_weapon_type) && (starboard_weapon_temp < COMBAT_AI_WEAPON_TEMP_USABLE) && in_good_range;
4619 }
4620
4621 Entity* target = [self primaryTarget];
4622 double aspect = [self approachAspectToPrimaryTarget];
4623
4624 if (!forward_weapon_ready && !aft_weapon_ready && !port_weapon_ready && !starboard_weapon_ready)
4625 { // no usable weapons! Either not fitted or overheated
4626
4627 // if unarmed
4628 if (isWeaponNone(forward_weapon_real_type) &&
4629 isWeaponNone(aft_weapon_type) &&
4630 isWeaponNone(port_weapon_type) &&
4631 isWeaponNone(starboard_weapon_type))
4632 {
4633 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4634 }
4635 else if (aspect > 0)
4636 {
4637 if (in_good_range)
4638 {
4639 if (accuracy >= COMBAT_AI_IS_SMART && randf() < 0.75)
4640 {
4641 behaviour = BEHAVIOUR_EVASIVE_ACTION;
4642 }
4643 else
4644 {
4645 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4646 }
4647 }
4648 else
4649 {
4650 // ready to get more accurate shots later
4651 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4652 }
4653 }
4654 else
4655 {
4656 // if target is running away, stay on target
4657 // unless too close for safety
4658 if (range < COMBAT_IN_RANGE_FACTOR * weaponRange) {
4659 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4660 } else {
4661 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4662 }
4663 }
4664 }
4665// if our current target isn't targeting us, and we have some idea of how to fight, and our weapons are running hot, and we're fairly nearby
4666 else if (weapons_heating && accuracy >= COMBAT_AI_ISNT_AWFUL && [target isShip] && [(ShipEntity *)target primaryTarget] != self && range < COMBAT_OUT_RANGE_FACTOR * weaponRange)
4667 {
4668// then back off a bit for weapons to cool so we get a good attack run later, rather than weaving closer
4669 float relativeSpeed = magnitude(vector_subtract([self velocity], [target velocity]));
4670 [self setEvasiveJink:(range + COMBAT_JINK_OFFSET - relativeSpeed / max_flight_pitch)];
4671 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4672 }
4673 else
4674 {
4675 BOOL nearby = range < COMBAT_IN_RANGE_FACTOR * getWeaponRangeFromType(forward_weapon_type);
4676 BOOL midrange = range < COMBAT_OUT_RANGE_FACTOR * getWeaponRangeFromType(aft_weapon_type);
4677
4678
4679 if (nearby && aft_weapon_ready)
4680 {
4681 jink = kZeroVector; // almost all behaviours
4682 behaviour = BEHAVIOUR_RUNNING_DEFENSE;
4683 }
4684 else if (nearby && (port_weapon_ready || starboard_weapon_ready))
4685 {
4686 jink = kZeroVector; // almost all behaviours
4687 behaviour = BEHAVIOUR_ATTACK_BROADSIDE;
4688 }
4689 else if (nearby)
4690 {
4691 if (!pitching_over) // don't change jink in the middle of a sharp turn.
4692 {
4693 /*
4694 For most AIs, is behaviour_attack_target called as starting behaviour on every hit.
4695 Target can both fly towards or away from ourselves here. Both situations
4696 need a different jink.z for optimal collision avoidance at high speed approach and low speed dogfighting.
4697 The COMBAT_JINK_OFFSET intentionally over-compensates the range for collision radii to send ships towards
4698 the target at low speeds.
4699 */
4700 float relativeSpeed = magnitude(vector_subtract([self velocity], [target velocity]));
4701 [self setEvasiveJink:(range + COMBAT_JINK_OFFSET - relativeSpeed / max_flight_pitch)];
4702 }
4703 // good pilots use behaviour_attack_break_off_target instead
4704 if (accuracy >= COMBAT_AI_FLEES_BETTER)
4705 {
4706 behaviour = BEHAVIOUR_ATTACK_BREAK_OFF_TARGET;
4707 }
4708 else
4709 {
4710 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4711 }
4712 }
4713 else if (forward_weapon_ready)
4714 {
4715 jink = kZeroVector; // almost all behaviours
4716
4717 // TODO: good pilots use behaviour_attack_sniper sometimes
4718 if (getWeaponRangeFromType(forward_weapon_real_type) > 12500 && range > 12500)
4719 {
4720 behaviour = BEHAVIOUR_ATTACK_SNIPER;
4721 }
4722// generally not good tactics the next two
4723 else if (accuracy < COMBAT_AI_ISNT_AWFUL && aspect < 0)
4724 {
4725 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX;
4726 }
4727 else if (accuracy < COMBAT_AI_ISNT_AWFUL)
4728 {
4729 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
4730 }
4731 else
4732 {
4733 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4734 }
4735 }
4736 else if (port_weapon_ready || starboard_weapon_ready)
4737 {
4738 jink = kZeroVector; // almost all behaviours
4739 behaviour = BEHAVIOUR_ATTACK_BROADSIDE;
4740 }
4741 else if (aft_weapon_ready && midrange)
4742 {
4743 jink = kZeroVector; // almost all behaviours
4744 behaviour = BEHAVIOUR_RUNNING_DEFENSE;
4745 }
4746 else
4747 {
4748 jink = kZeroVector; // almost all behaviours
4749 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4750 }
4751 }
4752 }
4753
4754 frustration = 0.0; // behaviour changed, so reset frustration
4755
4756}
4757
4758
4759- (void) behaviour_attack_broadside:(double) delta_t
4760{
4761 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
4762 float max_available_speed = maxFlightSpeed;
4763 double range = [self rangeToPrimaryTarget];
4764 if (canBurn) max_available_speed *= [self afterburnerFactor];
4765
4766 if (cloakAutomatic) [self activateCloakingDevice];
4767
4768 if (![self canStillTrackPrimaryTarget])
4769 {
4770 [self noteLostTargetAndGoIdle];
4771 return;
4772 }
4773
4774 desired_speed = max_available_speed;
4775 if (range < COMBAT_BROADSIDE_IN_RANGE_FACTOR * weaponRange)
4776 {
4777 behaviour = BEHAVIOUR_ATTACK_TARGET;
4778 }
4779 else
4780 {
4781 if (port_weapon_temp < starboard_weapon_temp)
4782 {
4783 if (isWeaponNone(port_weapon_type))
4784 {
4785 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_RIGHT;
4786 [self setWeaponDataFromType:starboard_weapon_type];
4787 }
4788 else
4789 {
4790 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_LEFT;
4791 [self setWeaponDataFromType:port_weapon_type];
4792 }
4793 }
4794 else
4795 {
4796 if (isWeaponNone(starboard_weapon_type))
4797 {
4798 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_RIGHT;
4799 [self setWeaponDataFromType:starboard_weapon_type];
4800 }
4801 else
4802 {
4803 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_LEFT;
4804 [self setWeaponDataFromType:port_weapon_type];
4805 }
4806 }
4807 jink = kZeroVector;
4808 if (weapon_damage == 0.0)
4809 { // safety in case side lasers no longer exist
4810 behaviour = BEHAVIOUR_ATTACK_TARGET;
4811 }
4812 else if (range > 0.9 * weaponRange)
4813 {
4814 behaviour = BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE;
4815 }
4816 }
4817
4818 frustration = 0.0; // behaviour changed, so reset frustration
4819
4820
4821
4822}
4823
4824
4825- (void) behaviour_attack_broadside_left:(double) delta_t
4826{
4827 [self behaviour_attack_broadside_target:delta_t leftside:YES];
4828}
4829
4830
4831- (void) behaviour_attack_broadside_right:(double) delta_t
4832{
4833 [self behaviour_attack_broadside_target:delta_t leftside:NO];
4834}
4835
4836
4837- (void) behaviour_attack_broadside_target:(double) delta_t leftside:(BOOL) leftside
4838{
4839 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
4840 float max_available_speed = maxFlightSpeed;
4841 double range = [self rangeToPrimaryTarget];
4842 if (canBurn) max_available_speed *= [self afterburnerFactor];
4843 if ([self primaryTarget] == nil)
4844 {
4845 [self noteLostTargetAndGoIdle];
4846 return;
4847 }
4848 GLfloat currentWeaponRange = getWeaponRangeFromType(leftside?port_weapon_type:starboard_weapon_type);
4849 if (range > COMBAT_BROADSIDE_RANGE_FACTOR * currentWeaponRange)
4850 {
4851 behaviour = BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE;
4852 return;
4853 }
4854
4855// can get closer on broadsides since there's less risk of a collision
4856 if ((range < COMBAT_BROADSIDE_IN_RANGE_FACTOR * currentWeaponRange)||([self proximityAlert] != nil))
4857 {
4858 if (![self hasProximityAlertIgnoringTarget:YES])
4859 {
4860 behaviour = BEHAVIOUR_ATTACK_TARGET;
4861 }
4862 else
4863 {
4864 [self avoidCollision];
4865 return;
4866 }
4867 }
4868 else
4869 {
4870 if (![self canStillTrackPrimaryTarget])
4871 {
4872 [self noteLostTargetAndGoIdle];
4873 return;
4874 }
4875 }
4876 // control speed
4877 //
4878 BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed);
4879 double slow_down_range = currentWeaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
4880// double target_speed = [target speed];
4881 if (range <= slow_down_range)
4882 desired_speed = fmin(0.8 * maxFlightSpeed, fmax((2.0-frustration)*maxFlightSpeed, 0.1 * maxFlightSpeed)); // within the weapon's range slow down to aim
4883 else
4884 desired_speed = max_available_speed; // use afterburner to approach
4885
4886 double last_success_factor = success_factor;
4887 success_factor = [self trackSideTarget:delta_t:leftside]; // do the actual piloting
4888 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE)
4889 { // will probably have more luck with the other laser or picking a different attack method
4890 if (leftside)
4891 {
4892 if (!isWeaponNone(starboard_weapon_type))
4893 {
4894 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_RIGHT;
4895 }
4896 else
4897 {
4898 behaviour = BEHAVIOUR_ATTACK_TARGET;
4899 }
4900 }
4901 else
4902 {
4903 if (!isWeaponNone(port_weapon_type))
4904 {
4905 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_LEFT;
4906 }
4907 else
4908 {
4909 behaviour = BEHAVIOUR_ATTACK_TARGET;
4910 }
4911 }
4912 }
4913
4914/* FIXME: again, basically all of this next bit common with standard attack */
4915 if ((success_factor > 0.999)||(success_factor > last_success_factor))
4916 {
4917 frustration -= delta_t;
4918 if (frustration < 0.0)
4919 frustration = 0.0;
4920 }
4921 else
4922 {
4923 frustration += delta_t;
4924 if (frustration > 3.0) // 3s of frustration
4925 {
4926
4927 [self noteFrustration:@"BEHAVIOUR_ATTACK_BROADSIDE"];
4928 [self setEvasiveJink:1000.0];
4929 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4930 frustration = 0.0;
4931 desired_speed = maxFlightSpeed;
4932 }
4933 }
4934
4935 if (missiles) [self considerFiringMissile:delta_t];
4936
4937 if (cloakAutomatic) [self activateCloakingDevice];
4938 if (leftside)
4939 {
4940 [self firePortWeapon:range];
4941 }
4942 else
4943 {
4944 [self fireStarboardWeapon:range];
4945 }
4946
4947
4948
4949 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE)
4950 {
4951 behaviour = BEHAVIOUR_ATTACK_TARGET;
4952 }
4953}
4954
4955
4956- (void) behaviour_close_to_broadside_range:(double) delta_t
4957{
4958 double range = [self rangeToPrimaryTarget];
4959 if ([self proximityAlert] != nil)
4960 {
4961 if ([self proximityAlert] == [self primaryTarget])
4962 {
4963 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; // this behaviour will handle proximity_alert.
4964 [self behaviour_attack_fly_from_target: delta_t]; // do it now.
4965 }
4966 else
4967 {
4968 [self avoidCollision];
4969 }
4970 return;
4971 }
4972 if (![self canStillTrackPrimaryTarget])
4973 {
4974 [self noteLostTargetAndGoIdle];
4975 return;
4976 }
4977
4978 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
4979 [self behaviour_fly_to_target_six:delta_t];
4980 if (!isWeaponNone(port_weapon_type))
4981 {
4982 [self setWeaponDataFromType:port_weapon_type];
4983 }
4984 else
4985 {
4986 [self setWeaponDataFromType:starboard_weapon_type];
4987 }
4988 if (range <= COMBAT_BROADSIDE_RANGE_FACTOR * weaponRange)
4989 {
4990 behaviour = BEHAVIOUR_ATTACK_BROADSIDE;
4991 }
4992 else
4993 {
4994 behaviour = BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE;
4995 }
4996}
4997
4998
4999- (void) behaviour_close_with_target:(double) delta_t
5000{
5001 double range = [self rangeToPrimaryTarget];
5002 if ([self proximityAlert] != nil)
5003 {
5004 if ([self proximityAlert] == [self primaryTarget])
5005 {
5006 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; // this behaviour will handle proximity_alert.
5007 [self behaviour_attack_fly_from_target: delta_t]; // do it now.
5008 }
5009 else
5010 {
5011 [self avoidCollision];
5012 }
5013 return;
5014 }
5015 if (![self canStillTrackPrimaryTarget])
5016 {
5017 [self noteLostTargetAndGoIdle];
5018 return;
5019 }
5020 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
5021 double saved_frustration = frustration;
5022 [self behaviour_fly_to_target_six:delta_t];
5023 frustration = saved_frustration; // ignore fly-to-12 frustration
5024 frustration += delta_t;
5025 if (range <= COMBAT_IN_RANGE_FACTOR * weaponRange || frustration > 5.0)
5026 {
5027 behaviour = BEHAVIOUR_ATTACK_TARGET;
5028 }
5029 else
5030 {
5031 behaviour = BEHAVIOUR_CLOSE_WITH_TARGET;
5032 }
5033
5034
5035}
5036
5037
5038- (void) behaviour_attack_sniper:(double) delta_t
5039{
5040 if (![self canStillTrackPrimaryTarget])
5041 {
5042 [self noteLostTargetAndGoIdle];
5043 return;
5044 }
5045 Entity* rawTarget = [self primaryTarget];
5046 if (![rawTarget isShip])
5047 {
5048 // can't attack a wormhole
5049 [self noteLostTargetAndGoIdle];
5050 return;
5051 }
5052 ShipEntity *target = (ShipEntity *)rawTarget;
5053
5054 double range = [self rangeToPrimaryTarget];
5055 float max_available_speed = maxFlightSpeed;
5056
5057 if (range < 15000)
5058 {
5059 behaviour = BEHAVIOUR_ATTACK_TARGET;
5060 }
5061 else
5062 {
5063 if (range > weaponRange || range > scannerRange * 0.8)
5064 {
5065 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
5066 if (canBurn && [target weaponRange] > weaponRange && range > weaponRange)
5067 {
5068 // if outside maximum weapon range, but inside target weapon range
5069 // close to fight ASAP!
5070 max_available_speed *= [self afterburnerFactor];
5071 }
5072 desired_speed = max_available_speed;
5073 }
5074 else
5075 {
5076 desired_speed = max_available_speed / 10.0f;
5077 }
5078
5079 double last_success_factor = success_factor;
5080 success_factor = [self trackPrimaryTarget:delta_t:NO];
5081
5082 if ((success_factor > 0.999)||(success_factor > last_success_factor))
5083 {
5084 frustration -= delta_t;
5085 if (frustration < 0.0)
5086 frustration = 0.0;
5087 }
5088 else
5089 {
5090 frustration += delta_t;
5091 if (frustration > 3.0) // 3s of frustration
5092 {
5093 [self noteFrustration:@"BEHAVIOUR_ATTACK_SNIPER"];
5094 [self setEvasiveJink:1000.0];
5095 behaviour = BEHAVIOUR_ATTACK_TARGET;
5096 frustration = 0.0;
5097 desired_speed = maxFlightSpeed;
5098 }
5099 }
5100
5101 }
5102
5103 if (missiles) [self considerFiringMissile:delta_t];
5104
5105 if (cloakAutomatic) [self activateCloakingDevice];
5106 [self fireMainWeapon:range];
5107
5108 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE && accuracy >= COMBAT_AI_ISNT_AWFUL)
5109 {
5110 behaviour = BEHAVIOUR_ATTACK_TARGET;
5111 }
5112
5113}
5114
5115
5116- (void) behaviour_fly_to_target_six:(double) delta_t
5117{
5118 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
5119 float max_available_speed = maxFlightSpeed;
5120 double range = [self rangeToPrimaryTarget];
5121 if (canBurn) max_available_speed *= [self afterburnerFactor];
5122
5123 // deal with collisions and lost targets
5124 if ([self proximityAlert] != nil)
5125 {
5126 if ([self proximityAlert] == [self primaryTarget])
5127 {
5128 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; // this behaviour will handle proximity_alert.
5129 [self behaviour_attack_fly_from_target: delta_t]; // do it now.
5130 }
5131 else
5132 {
5133 [self avoidCollision];
5134 }
5135 return;
5136 }
5137 if (![self canStillTrackPrimaryTarget])
5138 {
5139 [self noteLostTargetAndGoIdle];
5140 return;
5141 }
5142
5143 // control speed
5144 BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed);
5145 BOOL closeQuickly = (canBurn && range > weaponRange);
5146 double slow_down_range = weaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
5147 if (closeQuickly)
5148 {
5149 slow_down_range = weaponRange * COMBAT_OUT_RANGE_FACTOR;
5150 }
5151 double back_off_range = weaponRange * COMBAT_OUT_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
5152 Entity* rawTarget = [self primaryTarget];
5153 if (![rawTarget isShip])
5154 {
5155 [self noteLostTargetAndGoIdle];
5156 return;
5157 }
5158 ShipEntity* target = (ShipEntity *)rawTarget;
5159 double target_speed = [target speed];
5160 double last_success_factor = success_factor;
5161 double distance = [self rangeToDestination];
5162 success_factor = distance;
5163
5164 if (range < slow_down_range && (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX))
5165 {
5166 if (range < back_off_range)
5167 {
5168 desired_speed = fmax(0.9 * target_speed, 0.4 * maxFlightSpeed);
5169 }
5170 else
5171 {
5172 desired_speed = fmax(target_speed * 1.2, maxFlightSpeed);
5173 }
5174
5175 // avoid head-on collision
5176 if ((range < 0.5 * distance)&&(behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX))
5177 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
5178 }
5179 else
5180 {
5181 if (range < back_off_range)
5182 {
5183 desired_speed = fmax(0.9 * target_speed, 0.8 * maxFlightSpeed);
5184 }
5185 else
5186 {
5187 desired_speed = max_available_speed; // use afterburner to approach
5188 }
5189 }
5190
5191
5192 // if within 0.75km of the target's six or twelve, or if target almost at standstill for 62.5% of non-thargoid ships (!),
5193 // then vector in attack.
5194 if (distance < 750.0 || (target_speed < 0.2 && ![self isThargoid] && ([self universalID] & 14) > 4))
5195 {
5196 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
5197 frustration = 0.0;
5198 desired_speed = fmax(target_speed, 0.4 * maxFlightSpeed); // within the weapon's range don't use afterburner
5199 }
5200
5201 // target-six
5202 if (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX)
5203 {
5204 // head for a point weapon-range * 0.5 to the six of the target
5205 //
5206 _destination = [target distance_six:0.5 * weaponRange];
5207 }
5208 // target-twelve
5209 if (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE)
5210 {
5211 if ([forward_weapon_type isTurretLaser])
5212 {
5213 // head for a point near the target, avoiding common Galcop weapon mount locations
5214 // TODO: this should account for weapon ranges
5215 GLfloat offset = 1000.0;
5216 GLfloat spacing = 2000.0;
5217 if (accuracy > 0.0)
5218 {
5219 offset = accuracy * 750.0;
5220 spacing = 2000.0 + (accuracy * 500.0);
5221 }
5222 if (entity_personality & 1)
5223 { // half at random
5224 offset = -offset;
5225 }
5226 _destination = [target distance_twelve:spacing withOffset:offset];
5227 }
5228 else
5229 {
5230 // head for a point 1.25km above the target
5231 _destination = [target distance_twelve:1250 withOffset:0];
5232 }
5233 }
5234
5235 pitching_over = NO; // in case it's set from elsewhere
5236 double confidenceFactor = [self trackDestination:delta_t :NO];
5237
5238 if(success_factor > last_success_factor || confidenceFactor < 0.85) frustration += delta_t;
5239 else if(frustration > 0.0) frustration -= delta_t * 0.75;
5240
5241 double aspect = [self approachAspectToPrimaryTarget];
5242 if(![forward_weapon_type isTurretLaser] && (frustration > 10 || aspect > 0.75))
5243 {
5244 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
5245 }
5246
5247 // use weaponry
5248 if (missiles) [self considerFiringMissile:delta_t];
5249
5250 if (cloakAutomatic) [self activateCloakingDevice];
5251 [self fireMainWeapon:range];
5252
5253
5254
5255 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE)
5256 {
5257 behaviour = BEHAVIOUR_ATTACK_TARGET;
5258 }
5259}
5260
5261
5262- (void) behaviour_attack_mining_target:(double) delta_t
5263{
5264 double range = [self rangeToPrimaryTarget];
5265 if (![self canStillTrackPrimaryTarget])
5266 {
5267 [self noteLostTargetAndGoIdle];
5268 desired_speed = maxFlightSpeed * 0.375;
5269 return;
5270 }
5271 else if ((range < 650) || ([self proximityAlert] != nil))
5272 {
5273 if ([self proximityAlert] == NO_TARGET)
5274 {
5275 desired_speed = range * maxFlightSpeed / (650.0 * 16.0);
5276 }
5277 else
5278 {
5279 [self avoidCollision];
5280 }
5281 }
5282 else
5283 {
5284 //we have a target, its within scanner range, and outside 650
5285 desired_speed = maxFlightSpeed * 0.875;
5286 }
5287
5288 [self trackPrimaryTarget:delta_t:NO];
5289
5290 /* Don't open fire until within 3km - it doesn't take many mining
5291 * laser shots to destroy an asteroid, but some of these mining
5292 * ships are way too slow to effectively chase down the debris:
5293 * wait until reasonably close before trying to split it. */
5294 if (range < 3000)
5295 {
5296 [self fireMainWeapon:range];
5297 }
5298
5299
5300}
5301
5302
5303- (void) behaviour_attack_fly_to_target:(double) delta_t
5304{
5305 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
5306 float max_available_speed = maxFlightSpeed;
5307 double range = [self rangeToPrimaryTarget];
5308 if (canBurn) max_available_speed *= [self afterburnerFactor];
5309 if ([self primaryTarget] == nil)
5310 {
5311 [self noteLostTargetAndGoIdle];
5312 return;
5313 }
5314 Entity* rawTarget = [self primaryTarget];
5315 if (![rawTarget isShip])
5316 {
5317 // can't attack a wormhole
5318 [self noteLostTargetAndGoIdle];
5319 return;
5320 }
5321
5322 ShipEntity *target = (ShipEntity *)rawTarget;
5323 if ((range < COMBAT_IN_RANGE_FACTOR * weaponRange)||([self proximityAlert] != nil))
5324 {
5325 if (![self hasProximityAlertIgnoringTarget:YES])
5326 {
5327 behaviour = BEHAVIOUR_ATTACK_TARGET;
5328 }
5329 else
5330 {
5331 [self avoidCollision];
5332 return;
5333 }
5334 }
5335 else
5336 {
5337 if (![self canStillTrackPrimaryTarget])
5338 {
5339 [self noteLostTargetAndGoIdle];
5340 return;
5341 }
5342 }
5343
5344 // control speed
5345 //
5346 BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed);
5347 BOOL closeQuickly = (canBurn && [target weaponRange] > weaponRange && range > weaponRange);
5348 double slow_down_range = weaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
5349 if (closeQuickly)
5350 {
5351 slow_down_range = weaponRange * COMBAT_OUT_RANGE_FACTOR;
5352 }
5353 double back_off_range = 10000 * COMBAT_OUT_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
5354 double target_speed = [target speed];
5355 double aspect = [self approachAspectToPrimaryTarget];
5356
5357 if (range <= slow_down_range)
5358 {
5359 if (range < back_off_range)
5360 {
5361 if (accuracy < COMBAT_AI_IS_SMART || ([target primaryTarget] == self && aspect > 0.8) || aim_tolerance*range > COMBAT_AI_CONFIDENCE_FACTOR)
5362 {
5363 if (accuracy >= COMBAT_AI_FLEES_BETTER && aspect > 0.8)
5364 {
5365 desired_speed = fmax(target_speed * 1.25, 0.8 * maxFlightSpeed);
5366 // stay at high speed if might be taking return fire
5367 }
5368 else
5369 {
5370 desired_speed = fmax(target_speed * 1.05, 0.25 * maxFlightSpeed); // within the weapon's range match speed
5371
5372 }
5373 }
5374 else
5375 { // smart, and not being shot at right now - slow down to attack
5376 desired_speed = fmax(0.1 * target_speed, 0.1 * maxFlightSpeed);
5377 }
5378 }
5379 else
5380 {
5381 if (accuracy < COMBAT_AI_IS_SMART || ([target isShip] && [(ShipEntity *)target primaryTarget] == self) || range > weaponRange / 2.0)
5382 {
5383 desired_speed = fmax(target_speed * 1.5, maxFlightSpeed);
5384 }
5385 else
5386 { // smart, and not being shot at right now - slow down to attack
5387 if (aspect > -0.25)
5388 {
5389 desired_speed = fmax(0.5 * target_speed, 0.5 * maxFlightSpeed);
5390 }
5391 else
5392 {
5393 desired_speed = fmax(1.25 * target_speed, 0.5 * maxFlightSpeed);
5394 }
5395 }
5396 }
5397 }
5398 else
5399 {
5400 if (closeQuickly)
5401 {
5402 desired_speed = max_available_speed; // use afterburner to approach
5403 }
5404 else
5405 {
5406 desired_speed = fmax(maxFlightSpeed,fmin(3.0 * target_speed, max_available_speed)); // possibly use afterburner to approach
5407 }
5408 }
5409
5410
5411 double last_success_factor = success_factor;
5412 success_factor = [self trackPrimaryTarget:delta_t:NO]; // do the actual piloting
5413
5414 if ((success_factor > 0.999)||(success_factor > last_success_factor))
5415 {
5416 frustration -= delta_t;
5417 if (frustration < 0.0)
5418 frustration = 0.0;
5419 }
5420 else
5421 {
5422 frustration += delta_t;
5423 if (frustration > 3.0) // 3s of frustration
5424 {
5425 [self noteFrustration:@"BEHAVIOUR_ATTACK_FLY_TO_TARGET"];
5426 [self setEvasiveJink:1000.0];
5427 behaviour = BEHAVIOUR_ATTACK_TARGET;
5428 frustration = 0.0;
5429 desired_speed = maxFlightSpeed;
5430 }
5431 }
5432
5433 if (missiles) [self considerFiringMissile:delta_t];
5434
5435 if (cloakAutomatic) [self activateCloakingDevice];
5436 [self fireMainWeapon:range];
5437
5438
5439
5440 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE && accuracy >= COMBAT_AI_ISNT_AWFUL && aim_tolerance * range < COMBAT_AI_CONFIDENCE_FACTOR)
5441 {
5442 // don't do this if the target is fleeing and the front laser is
5443 // the only weapon, or if we're too far away to use non-front
5444 // lasers effectively
5445 if (aspect < 0 ||
5446 !isWeaponNone(aft_weapon_type) ||
5447 !isWeaponNone(port_weapon_type) ||
5448 !isWeaponNone(starboard_weapon_type))
5449 {
5450 frustration = 0.0;
5451 behaviour = BEHAVIOUR_ATTACK_TARGET;
5452 }
5453 }
5454 else if (accuracy >= COMBAT_AI_FLEES_BETTER_2)
5455 {
5456 // if we're right in their gunsights, dodge!
5457 // need to dodge sooner if in aft sights
5458 if ([target behaviour] != BEHAVIOUR_FLEE_TARGET && [target behaviour] != BEHAVIOUR_FLEE_EVASIVE_ACTION)
5459 {
5460 if ((aspect > 0.99999 && !isWeaponNone([target weaponTypeForFacing:WEAPON_FACING_FORWARD strict:NO])) || (aspect < -0.999 && !isWeaponNone([target weaponTypeForFacing:WEAPON_FACING_AFT strict:NO])))
5461 {
5462 frustration = 0.0;
5463 behaviour = BEHAVIOUR_EVASIVE_ACTION;
5464 }
5465 }
5466 }
5467}
5468
5469
5470- (void) behaviour_attack_fly_from_target:(double) delta_t
5471{
5472 double range = [self rangeToPrimaryTarget];
5473 double last_success_factor = success_factor;
5474 success_factor = range;
5475
5476 if ([self primaryTarget] == nil)
5477 {
5478 [self noteLostTargetAndGoIdle];
5479 return;
5480 }
5481 if (last_success_factor > success_factor) // our target is closing in.
5482 {
5483 frustration += delta_t;
5484 }
5485 else
5486 { // not getting away fast enough?
5487 frustration += delta_t / 4.0 ;
5488 }
5489
5490 if (frustration > 10.0)
5491 {
5492 if (randf() < 0.3) {
5493 desired_speed = maxFlightSpeed * (([self hasFuelInjection] && (fuel > MIN_FUEL)) ? [self afterburnerFactor] : 1);
5494 }
5495 else if (range > COMBAT_IN_RANGE_FACTOR * weaponRange && randf() < 0.3)
5496 {
5497 behaviour = BEHAVIOUR_ATTACK_TARGET;
5498 }
5499 GLfloat z = jink.z;
5500 if (randf() < 0.3)
5501 {
5502 z /= 2; // move the z-offset closer to the target to let him fly away from the target.
5503 desired_speed = flightSpeed * 2; // increase speed a bit.
5504 }
5505 [self setEvasiveJink:z];
5506
5507 frustration /= 2.0;
5508 }
5509 if (desired_speed > maxFlightSpeed)
5510 {
5511 ShipEntity* target = [self primaryTarget];
5512 double target_speed = [target speed];
5513 if (desired_speed > target_speed * 2.0)
5514 {
5515 desired_speed = maxFlightSpeed; // don't overuse the injectors
5516 }
5517 }
5518 else if (desired_speed < maxFlightSpeed * 0.5)
5519 {
5520 desired_speed = maxFlightSpeed;
5521 }
5522
5523 if (range > COMBAT_OUT_RANGE_FACTOR * weaponRange + 15.0 * jink.x ||
5524 flightSpeed > (scannerRange - range) * max_flight_pitch / 6.28)
5525 {
5526 jink = kZeroVector;
5527 behaviour = BEHAVIOUR_ATTACK_TARGET;
5528 frustration = 0.0;
5529 }
5530 [self trackPrimaryTarget:delta_t:YES];
5531
5532 if (missiles) [self considerFiringMissile:delta_t];
5533
5534 if (cloakAutomatic) [self activateCloakingDevice];
5535 if ([self hasProximityAlertIgnoringTarget:YES])
5536 [self avoidCollision];
5537
5538 if (accuracy >= COMBAT_AI_FLEES_BETTER_2)
5539 {
5540 double aspect = [self approachAspectToPrimaryTarget];
5541 // if we're right in their gunsights, dodge!
5542 // need to dodge sooner if in aft sights
5543 if (aspect > 0.99999 || aspect < -0.999)
5544 {
5545 frustration = 0.0;
5546 behaviour = BEHAVIOUR_EVASIVE_ACTION;
5547 }
5548 }
5549
5550}
5551
5552
5553- (void) behaviour_running_defense:(double) delta_t
5554{
5555 if (![self canStillTrackPrimaryTarget])
5556 {
5557 [self noteLostTargetAndGoIdle];
5558 return;
5559 }
5560
5561 double range = [self rangeToPrimaryTarget];
5562 desired_speed = maxFlightSpeed; // not injectors
5563 jink = kZeroVector;
5564 if (range > weaponRange || range > 0.8 * scannerRange || range == 0)
5565 {
5566 behaviour = BEHAVIOUR_CLOSE_WITH_TARGET;
5567 if ([forward_weapon_type isTurretLaser])
5568 {
5569 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
5570 }
5571 frustration = 0.0;
5572 }
5573 [self trackPrimaryTarget:delta_t:YES];
5574 if ([forward_weapon_type isTurretLaser])
5575 {
5576 // most Thargoids will only have the forward weapon
5577 [self fireMainWeapon:range];
5578 }
5579 else
5580 {
5581 [self fireAftWeapon:range];
5582 }
5583 if (cloakAutomatic) [self activateCloakingDevice];
5584 if ([self hasProximityAlertIgnoringTarget:YES])
5585 [self avoidCollision];
5586
5587 if (behaviour != BEHAVIOUR_CLOSE_WITH_TARGET && weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE)
5588 {
5589 behaviour = BEHAVIOUR_ATTACK_TARGET;
5590 }
5591
5592 // remember to look where you're going?
5593 if (accuracy >= COMBAT_AI_ISNT_AWFUL && [self hasProximityAlertIgnoringTarget:YES])
5594 {
5595 [self avoidCollision];
5596 }
5597
5598
5599}
5600
5601
5602- (void) behaviour_flee_target:(double) delta_t
5603{
5604 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
5605 float max_available_speed = maxFlightSpeed;
5606 double range = [self rangeToPrimaryTarget];
5607 if ([self primaryTarget] == nil)
5608 {
5609 [self noteLostTargetAndGoIdle];
5610 return;
5611 }
5612 if (canBurn) max_available_speed *= [self afterburnerFactor];
5613
5614 double last_range = success_factor;
5615 success_factor = range;
5616
5617 if (range > desired_range || range == 0)
5618 [shipAI message:@"REACHED_SAFETY"];
5619 else
5620 desired_speed = max_available_speed;
5621
5622 if (range > last_range) // improvement
5623 {
5624 frustration -= 0.25 * delta_t;
5625 if (frustration < 0.0)
5626 frustration = 0.0;
5627 }
5628 else
5629 {
5630 frustration += delta_t;
5631 if (frustration > 15.0) // 15s of frustration
5632 {
5633 [self noteFrustration:@"BEHAVIOUR_FLEE_TARGET"];
5634 frustration = 0.0;
5635 }
5636 }
5637
5638 [self trackPrimaryTarget:delta_t:YES];
5639
5640 Entity *target = [self primaryTarget];
5641
5642 if (missiles && [target isShip] && [(ShipEntity *)target primaryTarget] == self)
5643 {
5644 [self considerFiringMissile:delta_t];
5645 }
5646
5647 if (([self hasCascadeMine]) && (range < 10000.0) && canBurn)
5648 {
5649 float qbomb_chance = 0.01 * delta_t;
5650 if (randf() < qbomb_chance)
5651 {
5652 [self launchCascadeMine];
5653 }
5654 }
5655
5656// thargoids won't normally be fleeing, but if they do, they can still shoot
5657 if ([forward_weapon_type isTurretLaser])
5658 {
5659 [self fireMainWeapon:range];
5660 }
5661
5662 if (cloakAutomatic) [self activateCloakingDevice];
5663
5664 // remember to look where you're going?
5665 if (accuracy >= COMBAT_AI_ISNT_AWFUL && [self hasProximityAlertIgnoringTarget:YES])
5666 {
5667 [self avoidCollision];
5668 }
5669
5670}
5671
5672
5673- (void) behaviour_fly_range_from_destination:(double) delta_t
5674{
5675 double distance = [self rangeToDestination];
5676 if (distance < desired_range)
5677 {
5678 behaviour = BEHAVIOUR_FLY_FROM_DESTINATION;
5679 if (desired_speed < maxFlightSpeed)
5680 {
5681 desired_speed = maxFlightSpeed; // Not all AI define speed when flying away. Start with max speed to stay compatible with such AI's, but allow faster flight if it's (e.g.) used to flee from coordinates rather than entity
5682 }
5683 }
5684 else
5685 {
5686 behaviour = BEHAVIOUR_FLY_TO_DESTINATION;
5687 }
5688 if ([self hasProximityAlertIgnoringTarget:YES])
5689 {
5690 [self avoidCollision];
5691 }
5692 frustration = 0.0;
5693
5694
5695
5696}
5697
5698
5699- (void) behaviour_face_destination:(double) delta_t
5700{
5701 double max_cos = MAX_COS;
5702 double distance = [self rangeToDestination];
5703 double old_pitch = flightPitch;
5704 desired_speed = 0.0;
5705 if (desired_range > 1.0 && distance > desired_range)
5706 {
5707 max_cos = sqrt(1 - 0.90 * desired_range*desired_range/(distance * distance)); // Head for a point within 95% of desired_range (must match the value in trackDestination)
5708 }
5709 double confidenceFactor = [self trackDestination:delta_t:NO];
5710 if (confidenceFactor >= max_cos && flightPitch == 0.0)
5711 {
5712 // desired facing achieved and movement stabilised.
5713 [shipAI message:@"FACING_DESTINATION"];
5714 [self doScriptEvent:OOJSID("shipNowFacingDestination")];
5715 frustration = 0.0;
5716 if(docking_match_rotation) // IDLE stops rotating while docking
5717 {
5718 behaviour = BEHAVIOUR_FLY_TO_DESTINATION;
5719 }
5720 else
5721 {
5722 behaviour = BEHAVIOUR_IDLE;
5723 }
5724 }
5725
5726 if(flightSpeed == 0) frustration += delta_t;
5727 if (frustration > 15.0 / max_flight_pitch) // allow more time for slow ships.
5728 {
5729 frustration = 0.0;
5730 [self noteFrustration:@"BEHAVIOUR_FACE_DESTINATION"];
5731 if(flightPitch == old_pitch) flightPitch = 0.5 * max_flight_pitch; // hack to get out of frustration.
5732 }
5733
5734 /* 2009-7-18 Eric: the condition check below is intended to eliminate the flippering between two positions for fast turning ships
5735 during low FPS conditions. This flippering is particular frustrating on slow computers during docking. But with my current computer I can't
5736 induce those low FPS conditions so I can't properly test if it helps.
5737 I did try with the TAF time acceleration that also generated larger frame jumps and than it seemed to help.
5738 */
5739 if(flightSpeed == 0 && frustration > 5 && confidenceFactor > 0.5 && ((flightPitch > 0 && old_pitch < 0) || (flightPitch < 0 && old_pitch > 0)))
5740 {
5741 flightPitch += 0.5 * old_pitch; // damping with last pitch value.
5742 }
5743
5744 if ([self hasProximityAlertIgnoringTarget:YES])
5745 {
5746 [self avoidCollision];
5747 }
5748
5749
5750}
5751
5752
5753- (void) behaviour_land_on_planet:(double) delta_t
5754{
5755 double max_cos = MAX_COS2; // trackDestination returns the squared confidence in reverse mode.
5756 desired_speed = 0.0;
5757
5758 OOPlanetEntity* planet = [UNIVERSE entityForUniversalID:planetForLanding];
5759
5760 if (![planet isPlanet])
5761 {
5762 behaviour = BEHAVIOUR_IDLE;
5763 aiScriptWakeTime = 1; // reconsider JSAI
5764 [shipAI message:@"NO_PLANET_NEARBY"];
5765 return;
5766 }
5767
5768 if (HPdistance(position, [planet position]) + [self collisionRadius] < [planet radius])
5769 {
5770 // we have landed. (completely disappeared inside planet)
5771 [self landOnPlanet:planet];
5772 return;
5773 }
5774
5775 double confidenceFactor = [self trackDestination:delta_t:YES]; // turn away from destination
5776
5777 if (confidenceFactor >= max_cos && flightSpeed == 0.0)
5778 {
5779 // We are now turned away from planet. Start landing by flying backward.
5780 thrust = 0.0; // stop forward acceleration.
5781 if (magnitude2(velocity) < MAX_LANDING_SPEED2)
5782 {
5783 [self adjustVelocity:vector_multiply_scalar([self forwardVector], -max_thrust * delta_t)];
5784 }
5785 }
5786
5787
5788 if ([self hasProximityAlertIgnoringTarget:YES])
5789 {
5790 [self avoidCollision];
5791 }
5792
5793
5794}
5795
5796
5797- (void) behaviour_formation_form_up:(double) delta_t
5798{
5799 // destination for each escort is set in update() from owner.
5800 ShipEntity* leadShip = [self owner];
5801 double distance = [self rangeToDestination];
5802 double eta = (distance - desired_range) / flightSpeed;
5803 if(eta < 0) eta = 0;
5804 if ((eta < 5.0)&&(leadShip)&&(leadShip->isShip))
5805 desired_speed = [leadShip flightSpeed] * (1 + eta * 0.05);
5806 else
5807 desired_speed = maxFlightSpeed;
5808
5809 double last_distance = success_factor;
5810 success_factor = distance;
5811
5812 // do the actual piloting!!
5813 [self trackDestination:delta_t: NO];
5814
5815 eta = eta / 0.51; // 2% safety margin assuming an average of half current speed
5816 GLfloat slowdownTime = (thrust > 0.0)? flightSpeed / (thrust) : 4.0;
5817 GLfloat minTurnSpeedFactor = 0.05 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds
5818
5819 if ((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor))
5820 desired_speed = flightSpeed * 0.50; // cut speed by 50% to a minimum minTurnSpeedFactor of speed
5821
5822 if (distance < last_distance) // improvement
5823 {
5824 frustration -= 0.25 * delta_t;
5825 if (frustration < 0.0)
5826 frustration = 0.0;
5827 }
5828 else
5829 {
5830 frustration += delta_t;
5831 if (frustration > 15.0)
5832 {
5833 if (!leadShip) [self noteFrustration:@"BEHAVIOUR_FORMATION_FORM_UP"]; // escorts never reach their destination when following leader.
5834 else if (distance > 0.5 * scannerRange && !pitching_over)
5835 {
5836 pitching_over = YES; // Force the ship in a 180 degree turn. Do it here to allow escorts to break out formation for some seconds.
5837 }
5838 frustration = 0;
5839 }
5840 }
5841 if ([self hasProximityAlertIgnoringTarget:YES])
5842 {
5843 [self avoidCollision];
5844 }
5845
5846
5847}
5848
5849
5850- (void) behaviour_fly_to_destination:(double) delta_t
5851{
5852 double distance = [self rangeToDestination];
5853 // double desiredRange = (dockingInstructions != nil) ? 1.2 * desired_range : desired_range; // stop a bit earlyer when docking.
5854 if (distance < desired_range) // + collision_radius)
5855 {
5856 // desired range achieved
5857 [shipAI message:@"DESIRED_RANGE_ACHIEVED"];
5858 [self doScriptEvent:OOJSID("shipAchievedDesiredRange")];
5859
5860 if(!docking_match_rotation) // IDLE stops rotating while docking
5861 {
5862 behaviour = BEHAVIOUR_IDLE;
5863 desired_speed = 0.0;
5864 }
5865 frustration = 0.0;
5866 }
5867 else
5868 {
5869 double last_distance = success_factor;
5870 success_factor = distance;
5871
5872 // do the actual piloting!!
5873 double confidenceFactor = [self trackDestination:delta_t: NO];
5874 if(confidenceFactor < 0.2) confidenceFactor = 0.2; // don't allow small or negative values.
5875
5876 /* 2009-07-19 Eric: Estimated Time of Arrival (eta) should also take the "angle to target" into account (confidenceFactor = cos(angle to target))
5877 and should not fuss about that last meter and use "distance + 1" instead of just "distance".
5878 trackDestination already did pitch regulation, use confidence here only for cutting down to high speeds.
5879 This should prevent ships crawling to their destination when they try to pull up close to their destination.
5880
5881 To prevent ships circling around their target without reaching destination I added a limitation based on turnrate,
5882 speed and distance to target. Formula based on satelite orbit:
5883 orbitspeed = turnrate (rad/sec) * radius (m) or flightSpeed = max_flight_pitch * 2 Pi * distance
5884 Speed must be significant lower when not flying in direction of target (low confidenceFactor) or it can never approach its destination
5885 and the ships runs the risk flying in circles around the target. (exclude active escorts)
5886 */
5887 GLfloat eta = ((distance + 1) - desired_range) / (0.51 * flightSpeed * confidenceFactor); // 2% safety margin assuming an average of half current speed
5888 GLfloat slowdownTime = (thrust > 0.0)? flightSpeed / (thrust) : 4.0;
5889 GLfloat minTurnSpeedFactor = 0.05 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds
5890 if (dockingInstructions != nil)
5891 {
5892 minTurnSpeedFactor /= 10.0;
5893 if (minTurnSpeedFactor * maxFlightSpeed > 20.0)
5894 {
5895 minTurnSpeedFactor /= 10.0;
5896 }
5897 }
5898
5899
5900 if (((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor)) || (flightSpeed > max_flight_pitch * 5 * confidenceFactor * distance))
5901 {
5902 desired_speed = flightSpeed * 0.50; // cut speed by 50% to a minimum minTurnSpeedFactor of speed
5903 }
5904
5905 /* Flight correction block to prevent one possible form of
5906 * crashes in late docking process */
5907 if (docking_match_rotation && confidenceFactor >= MAX_COS && dockingInstructions != nil && [dockingInstructions oo_intForKey:@"docking_stage"] >= 7)
5908 {
5909 // then at this point should be rotating to match the station
5910 StationEntity* station_for_docking = (StationEntity*)[self targetStation];
5911
5912 if ((station_for_docking)&&(station_for_docking->isStation))
5913 {
5914 float rollMatch = dot_product([station_for_docking portUpVectorForShip:self],[self upVector]);
5915 if (rollMatch < MAX_COS && rollMatch > -MAX_COS)
5916 {
5917 // not matching rotating - stop until corrected
5918 desired_speed = 0.1;
5919 }
5920 else if (desired_speed <= 0.2)
5921 {
5922 // had previously paused, so return to normal speed
5923 desired_speed = [dockingInstructions oo_floatForKey:@"speed"];
5924 }
5925 }
5926 }
5927
5928
5929
5930 if (distance < last_distance) // improvement
5931 {
5932 frustration -= 0.25 * delta_t;
5933 if (frustration < 0.0)
5934 frustration = 0.0;
5935 }
5936 else
5937 {
5938 frustration += delta_t;
5939 if ((frustration > slowdownTime * 10.0 && slowdownTime > 0)||(frustration > 15.0)) // 10x slowdownTime or 15s of frustration
5940 {
5941 [self noteFrustration:@"BEHAVIOUR_FLY_TO_DESTINATION"];
5942 frustration -= slowdownTime * 5.0; //repeat after another five units of frustration
5943 }
5944 }
5945 }
5946 if ([self hasProximityAlertIgnoringTarget:YES])
5947 {
5948 [self avoidCollision];
5949 }
5950
5951
5952}
5953
5954
5955- (void) behaviour_fly_from_destination:(double) delta_t
5956{
5957 double distance = [self rangeToDestination];
5958 if (distance > desired_range)
5959 {
5960 // desired range achieved
5961 [shipAI message:@"DESIRED_RANGE_ACHIEVED"];
5962 [self doScriptEvent:OOJSID("shipAchievedDesiredRange")];
5963
5964 behaviour = BEHAVIOUR_IDLE;
5965 frustration = 0.0;
5966 desired_speed = 0.0;
5967 }
5968
5969 [self trackDestination:delta_t:YES];
5970 if ([self hasProximityAlertIgnoringTarget:YES])
5971 {
5972 [self avoidCollision];
5973 }
5974
5975
5976}
5977
5978
5979- (void) behaviour_avoid_collision:(double) delta_t
5980{
5981 double distance = [self rangeToDestination];
5982 if (distance > desired_range)
5983 {
5984 [self resumePostProximityAlert];
5985 }
5986 else
5987 {
5988 ShipEntity* prox_ship = (ShipEntity*)[self proximityAlert];
5989 if (prox_ship)
5990 {
5991 desired_range = prox_ship->collision_radius * PROXIMITY_AVOID_DISTANCE_FACTOR;
5992 _destination = prox_ship->position;
5993 }
5994 double dq = [self trackDestination:delta_t:YES]; // returns 0 when heading towards prox_ship
5995 // Heading towards target with desired_speed > 0, avoids collisions better than setting desired_speed to zero.
5996 // (tested with boa class cruiser on collisioncourse with buoy)
5997 desired_speed = maxFlightSpeed * (0.5 * dq + 0.5);
5998 }
5999
6000
6001
6002}
6003
6004
6005- (void) behaviour_track_as_turret:(double) delta_t
6006{
6007 double aim = -2.0;
6008 ShipEntity *turret_owner = (ShipEntity *)[self owner];
6009 ShipEntity *turret_target = (ShipEntity *)[turret_owner primaryTarget];
6010 if (turret_owner && turret_target && [turret_owner hasHostileTarget])
6011 {
6012 aim = [self ballTrackLeadingTarget:delta_t atTarget:turret_target];
6013 if (aim > -1.0) // potential target
6014 {
6015 HPVector p = HPvector_subtract([turret_target position], [turret_owner position]);
6016 double cr = [turret_owner collisionRadius];
6017
6018 if (aim > .95)
6019 {
6020 [self fireTurretCannon:HPmagnitude(p) - cr];
6021 }
6022 return;
6023 }
6024 }
6025
6026 // can't fire on primary target; track secondary targets instead
6027 NSEnumerator *targetEnum = [turret_owner defenseTargetEnumerator];
6028 Entity *target = nil;
6029 while ((target = [[targetEnum nextObject] weakRefUnderlyingObject]))
6030 {
6031 // defense targets cannot be tracked while cloaked
6032 if ([target scanClass] == CLASS_NO_DRAW || [(ShipEntity *)target isCloaked] || [target energy] <= 0.0)
6033 {
6034 [turret_owner removeDefenseTarget:target];
6035 }
6036 else
6037 {
6038 double range = [turret_owner rangeToSecondaryTarget:target];
6039 if (range < weaponRange)
6040 {
6041 aim = [self ballTrackLeadingTarget:delta_t atTarget:target];
6042 if (aim > -1.0)
6043 { // tracking...
6044 HPVector p = HPvector_subtract([target position], [turret_owner position]);
6045 double cr = [turret_owner collisionRadius];
6046
6047 if (aim > .95)
6048 { // fire!
6049 [self fireTurretCannon:HPmagnitude(p) - cr];
6050 }
6051 return;
6052 }
6053 // else that target is out of range, try the next priority defense target
6054 }
6055 else if (range > scannerRange)
6056 {
6057 [turret_owner removeDefenseTarget:target];
6058 }
6059 }
6060 }
6061
6062 // turrets now don't return to neutral facing if no suitable target
6063 // better for shooting at targets that are on edge of fire arc
6064}
6065
6066
6067- (void) behaviour_fly_thru_navpoints:(double) delta_t
6068{
6069 int navpoint_plus_index = (next_navpoint_index + 1) % number_of_navpoints;
6070 HPVector d1 = navpoints[next_navpoint_index]; // head for this one
6071 HPVector d2 = navpoints[navpoint_plus_index]; // but be facing this one
6072
6073 HPVector rel = HPvector_between(d1, position); // vector from d1 to position
6074 HPVector ref = HPvector_between(d2, d1); // vector from d2 to d1
6075 ref = HPvector_normal(ref);
6076
6077 HPVector xp = make_HPvector(ref.y * rel.z - ref.z * rel.y, ref.z * rel.x - ref.x * rel.z, ref.x * rel.y - ref.y * rel.x);
6078
6079 GLfloat v0 = 0.0;
6080
6081 GLfloat r0 = HPdot_product(rel, ref); // proportion of rel in direction ref
6082
6083 // if r0 is negative then we're the wrong side of things
6084
6085 GLfloat r1 = HPmagnitude(xp); // distance of position from line
6086
6087 BOOL in_cone = (r0 > 0.5 * r1);
6088
6089 if (!in_cone) // are we in the approach cone ?
6090 r1 = 25.0 * flightSpeed; // aim a few km out!
6091 else
6092 r1 *= 2.0;
6093
6094 GLfloat dist2 = HPmagnitude2(rel);
6095
6096 if (dist2 < desired_range * desired_range)
6097 {
6098 // desired range achieved
6099 [self doScriptEvent:OOJSID("shipReachedNavPoint") andReactToAIMessage:@"NAVPOINT_REACHED"];
6100 if (navpoint_plus_index == 0)
6101 {
6102 [self doScriptEvent:OOJSID("shipReachedEndPoint") andReactToAIMessage:@"ENDPOINT_REACHED"];
6103 behaviour = BEHAVIOUR_IDLE;
6104 }
6105 next_navpoint_index = navpoint_plus_index; // loop as required
6106 }
6107 else
6108 {
6109 double last_success_factor = success_factor;
6110 double last_dist2 = last_success_factor;
6111 success_factor = dist2;
6112
6113 // set destination spline point from r1 and ref
6114 _destination = make_HPvector(d1.x + r1 * ref.x, d1.y + r1 * ref.y, d1.z + r1 * ref.z);
6115
6116 // do the actual piloting!!
6117 //
6118 // aim to within 1m
6119 GLfloat temp = desired_range;
6120 if (in_cone)
6121 desired_range = 1.0;
6122 else
6123 desired_range = 100.0;
6124 v0 = [self trackDestination:delta_t: NO];
6125 desired_range = temp;
6126
6127 if (dist2 < last_dist2) // improvement
6128 {
6129 frustration -= 0.25 * delta_t;
6130 if (frustration < 0.0)
6131 frustration = 0.0;
6132 }
6133 else
6134 {
6135 frustration += delta_t;
6136 if (frustration > 15.0) // 15s of frustration
6137 {
6138 [self noteFrustration:@"BEHAVIOUR_FLY_THRU_NAVPOINTS"];
6139 frustration -= 15.0; //repeat after another 15s of frustration
6140 }
6141 }
6142 }
6143
6144
6145
6146 GLfloat temp = desired_speed;
6147 desired_speed *= v0 * v0;
6148
6149 desired_speed = temp;
6150}
6151
6152
6153- (void) behaviour_scripted_ai:(double) delta_t
6154{
6155
6156 JSContext *context = OOJSAcquireContext();
6157 jsval rval = JSVAL_VOID;
6158 jsval deltaJS = JSVAL_VOID;
6159 NSDictionary *result = nil;
6160
6161 BOOL OK = JS_NewNumberValue(context, delta_t, &deltaJS);
6162 if (OK)
6163 {
6164 OK = [[self script] callMethod:OOJSID("scriptedAI")
6165 inContext:context
6166 withArguments:&deltaJS
6167 count:1
6168 result:&rval];
6169 }
6170
6171 if (!OK)
6172 {
6173 OOLog(@"ai.error",@"Could not call scriptedAI in ship script of %@, reverting to idle",self);
6174 behaviour = BEHAVIOUR_IDLE;
6175 OOJSRelinquishContext(context);
6176 return;
6177 }
6178
6179 if (!JSVAL_IS_OBJECT(rval))
6180 {
6181 OOLog(@"ai.error",@"Invalid return value of scriptedAI in ship script of %@, reverting to idle",self);
6182 behaviour = BEHAVIOUR_IDLE;
6183 OOJSRelinquishContext(context);
6184 return;
6185 }
6186
6187 result = OOJSNativeObjectFromJSObject(context, JSVAL_TO_OBJECT(rval));
6188 OOJSRelinquishContext(context);
6189
6190 // roll or roll factor
6191 if ([result objectForKey:@"stickRollFactor"] != nil)
6192 {
6193 stick_roll = [result oo_floatForKey:@"stickRollFactor"] * max_flight_roll;
6194 }
6195 else
6196 {
6197 stick_roll = [result oo_floatForKey:@"stickRoll"];
6198 }
6199 if (stick_roll > max_flight_roll)
6200 {
6201 stick_roll = max_flight_roll;
6202 }
6203 else if (stick_roll < -max_flight_roll)
6204 {
6205 stick_roll = -max_flight_roll;
6206 }
6207
6208 // pitch or pitch factor
6209 if ([result objectForKey:@"stickPitchFactor"] != nil)
6210 {
6211 stick_pitch = [result oo_floatForKey:@"stickPitchFactor"] * max_flight_pitch;
6212 }
6213 else
6214 {
6215 stick_pitch = [result oo_floatForKey:@"stickPitch"];
6216 }
6217 if (stick_pitch > max_flight_pitch)
6218 {
6219 stick_pitch = max_flight_pitch;
6220 }
6221 else if (stick_pitch < -max_flight_pitch)
6222 {
6223 stick_pitch = -max_flight_pitch;
6224 }
6225
6226 // yaw or yaw factor
6227 if ([result objectForKey:@"stickYawFactor"] != nil)
6228 {
6229 stick_yaw = [result oo_floatForKey:@"stickYawFactor"] * max_flight_yaw;
6230 }
6231 else
6232 {
6233 stick_yaw = [result oo_floatForKey:@"stickYaw"];
6234 }
6235 if (stick_yaw > max_flight_yaw)
6236 {
6237 stick_yaw = max_flight_yaw;
6238 }
6239 else if (stick_yaw < -max_flight_yaw)
6240 {
6241 stick_yaw = -max_flight_yaw;
6242 }
6243
6244 // apply sticks to current flight profile
6245 [self applySticks:delta_t];
6246
6247 // desired speed
6248 if ([result objectForKey:@"desiredSpeedFactor"] != nil)
6249 {
6250 desired_speed = [result oo_floatForKey:@"desiredSpeedFactor"] * maxFlightSpeed;
6251 }
6252 else
6253 {
6254 desired_speed = [result oo_floatForKey:@"desiredSpeed"];
6255 }
6256
6257 if (desired_speed < 0.0)
6258 {
6259 desired_speed = 0.0;
6260 }
6261 // overspeed and injector use is handled by applyThrust
6262
6263 if (behaviour == BEHAVIOUR_SCRIPTED_ATTACK_AI)
6264 {
6265 NSString* chosen_weapon = [result oo_stringForKey:@"chosenWeapon" defaultValue:@"FORWARD"];
6266 double range = [self rangeToPrimaryTarget];
6267
6268 if ([chosen_weapon isEqualToString:@"FORWARD"])
6269 {
6270 [self fireMainWeapon:range];
6271 }
6272 else if ([chosen_weapon isEqualToString:@"AFT"])
6273 {
6274 [self fireAftWeapon:range];
6275 }
6276 else if ([chosen_weapon isEqualToString:@"PORT"])
6277 {
6278 [self firePortWeapon:range];
6279 }
6280 else if ([chosen_weapon isEqualToString:@"STARBOARD"])
6281 {
6282 [self fireStarboardWeapon:range];
6283 }
6284 }
6285}
6286
6287- (float) reactionTime
6288{
6289 return reactionTime;
6290}
6291
6292
6293- (void) setReactionTime: (float) newReactionTime
6294{
6295 reactionTime = newReactionTime;
6296}
6297
6298
6299- (HPVector) calculateTargetPosition
6300{
6301 Entity *target = [self primaryTarget];
6302 if (target == nil)
6303 {
6304 return kZeroHPVector;
6305 }
6306 if (reactionTime <= 0.0)
6307 {
6308 return [target position];
6309 }
6310 double t = [UNIVERSE getTime] - trackingCurveTimes[1];
6311 return HPvector_add(HPvector_add(trackingCurveCoeffs[0], HPvector_multiply_scalar(trackingCurveCoeffs[1],t)), HPvector_multiply_scalar(trackingCurveCoeffs[2],t*t));
6312}
6313
6314
6315- (void) startTrackingCurve
6316{
6317 Entity *target = [self primaryTarget];
6318 if (target == nil)
6319 {
6320 return;
6321 }
6322 OOTimeAbsolute now = [UNIVERSE getTime];
6323 trackingCurvePositions[0] = [target position];
6324 trackingCurvePositions[1] = [target position];
6325 trackingCurvePositions[2] = [target position];
6326 trackingCurvePositions[3] = [target position];
6327 trackingCurveTimes[0] = now;
6328 trackingCurveTimes[1] = now - reactionTime/3.0;
6329 trackingCurveTimes[2] = now - reactionTime*2.0/3.0;
6330 trackingCurveTimes[3] = now - reactionTime;
6331 [self calculateTrackingCurve];
6332 return;
6333}
6334
6335
6336- (void) updateTrackingCurve
6337{
6338 Entity *target = [self primaryTarget];
6339 OOTimeAbsolute now = [UNIVERSE getTime];
6340 if (target == nil || reactionTime <= 0.0 || trackingCurveTimes[0] + reactionTime/3.0 > now) return;
6341 trackingCurvePositions[3] = trackingCurvePositions[2];
6342 trackingCurvePositions[2] = trackingCurvePositions[1];
6343 trackingCurvePositions[1] = trackingCurvePositions[0];
6344 if (EXPECT_NOT([target isShip] && [(ShipEntity *)target isCloaked]))
6345 {
6346 // if target is cloaked, introduce some more inaccuracy
6347 // 0.02 seems to be enough to give them slight difficulty on
6348 // a straight-line target and real trouble on anything better
6349 trackingCurvePositions[0] = HPvector_add([target position],OOHPVectorRandomSpatial([(ShipEntity *)target flightSpeed]*reactionTime*0.02));
6350 }
6351 else
6352 {
6353 trackingCurvePositions[0] = [target position];
6354 }
6355 trackingCurveTimes[3] = trackingCurveTimes[2];
6356 trackingCurveTimes[2] = trackingCurveTimes[1];
6357 trackingCurveTimes[1] = trackingCurveTimes[0];
6358 trackingCurveTimes[0] = now;
6359 [self calculateTrackingCurve];
6360 return;
6361}
6362
6363- (void) calculateTrackingCurve
6364{
6365 if (reactionTime <= 0.0)
6366 {
6367 trackingCurveCoeffs[0] = trackingCurvePositions[0];
6368 trackingCurveCoeffs[1] = kZeroHPVector;
6369 trackingCurveCoeffs[2] = kZeroHPVector;
6370 return;
6371 }
6372 double t1 = trackingCurveTimes[2] - trackingCurveTimes[1],
6373 t2 = trackingCurveTimes[3] - trackingCurveTimes[1];
6374 trackingCurveCoeffs[0] = trackingCurvePositions[1];
6375 trackingCurveCoeffs[1] = HPvector_add(HPvector_add(
6376 HPvector_multiply_scalar(trackingCurvePositions[1], -(t1+t2)/(t1*t2)),
6377 HPvector_multiply_scalar(trackingCurvePositions[2], -t2/(t1*(t1-t2)))),
6378 HPvector_multiply_scalar(trackingCurvePositions[3], t1/(t2*(t1-t2))));
6379 trackingCurveCoeffs[2] = HPvector_add(HPvector_add(
6380 HPvector_multiply_scalar(trackingCurvePositions[1], 1/(t1*t2)),
6381 HPvector_multiply_scalar(trackingCurvePositions[2], 1/(t1*(t1-t2)))),
6382 HPvector_multiply_scalar(trackingCurvePositions[3], -1/(t2*(t1-t2))));
6383 return;
6384}
6385
6386- (void) drawImmediate:(bool)immediate translucent:(bool)translucent
6387{
6388 if ((no_draw_distance < cam_zero_distance) || // Done redundantly to skip subentities
6389 (cloaking_device_active && randf() > 0.10))
6390 {
6391 // Don't draw.
6392 return;
6393 }
6394
6395 // Draw self.
6396 [super drawImmediate:immediate translucent:translucent];
6397
6398#ifndef NDEBUG
6399 // Draw bounding boxes if we have to before going for the subentities.
6400 // TODO: the translucent flag here makes very little sense. Something's wrong with the matrices.
6401 if (translucent) [self drawDebugStuff];
6402 else if (gDebugFlags & DEBUG_BOUNDING_BOXES && ![self isSubEntity])
6403 {
6404 OODebugDrawBoundingBox([self boundingBox]);
6405 OODebugDrawColoredBoundingBox(totalBoundingBox, [OOColor purpleColor]);
6406 }
6407#endif
6408
6409 // Draw subentities.
6410 if (!immediate) // TODO: is this relevant any longer?
6411 {
6412 // save time by not copying the subentity array if it's empty - CIM
6413 if ([self subEntityCount] > 0)
6414 {
6415 Entity<OOSubEntity> *subEntity = nil;
6416 foreach (subEntity, [self subEntities])
6417 {
6418 NSAssert3([subEntity owner] == self, @"Subentity ownership broke - %@ should be owned by %@ but is owned by %@.", subEntity, self, [subEntity owner]);
6419 [subEntity drawSubEntityImmediate:immediate translucent:translucent];
6420 }
6421 }
6422 }
6423}
6424
6425
6426#ifndef NDEBUG
6427- (void) drawDebugStuff
6428{
6429 // HPVect: imprecise here - needs camera relative
6430 if (0 && reportAIMessages)
6431 {
6432 OODebugDrawPoint(HPVectorToVector(_destination), [OOColor blueColor]);
6433 OODebugDrawColoredLine(HPVectorToVector([self position]), HPVectorToVector(_destination), [OOColor colorWithWhite:0.15 alpha:1.0]);
6434
6435 Entity *pTarget = [self primaryTarget];
6436 if (pTarget != nil)
6437 {
6438 OODebugDrawPoint(HPVectorToVector([pTarget position]), [OOColor redColor]);
6439 OODebugDrawColoredLine(HPVectorToVector([self position]), HPVectorToVector([pTarget position]), [OOColor colorWithRed:0.2 green:0.0 blue:0.0 alpha:1.0]);
6440 }
6441
6442 Entity *sTarget = [self targetStation];
6443 if (sTarget != pTarget && [sTarget isStation])
6444 {
6445 OODebugDrawPoint(HPVectorToVector([sTarget position]), [OOColor cyanColor]);
6446 }
6447
6448 Entity *fTarget = [self foundTarget];
6449 if (fTarget != nil && fTarget != pTarget && fTarget != sTarget)
6450 {
6451 OODebugDrawPoint(HPVectorToVector([fTarget position]), [OOColor magentaColor]);
6452 }
6453 }
6454}
6455#endif
6456
6457
6458- (void) drawSubEntityImmediate:(bool)immediate translucent:(bool)translucent
6459{
6461
6462 if (cam_zero_distance > no_draw_distance) // this test provides an opportunity to do simple LoD culling
6463 {
6464 return; // TOO FAR AWAY
6465 }
6467
6468 // HPVect: need to make camera-relative
6469 OOGLTranslateModelView(HPVectorToVector(position));
6470 OOGLMultModelView(rotMatrix);
6471 [self drawImmediate:immediate translucent:translucent];
6472
6473#ifndef NDEBUG
6475 {
6476 OODebugDrawBoundingBox([self boundingBox]);
6477 }
6478#endif
6479
6481
6483}
6484
6485
6486static GLfloat cargo_color[4] = { 0.9, 0.9, 0.9, 1.0}; // gray
6487static GLfloat hostile_color[4] = { 1.0, 0.25, 0.0, 1.0}; // red/orange
6488static GLfloat neutral_color[4] = { 1.0, 1.0, 0.0, 1.0}; // yellow
6489static GLfloat friendly_color[4] = { 0.0, 1.0, 0.0, 1.0}; // green
6490static GLfloat missile_color[4] = { 0.0, 1.0, 1.0, 1.0}; // cyan
6491static GLfloat police_color1[4] = { 0.5, 0.0, 1.0, 1.0}; // purpley-blue
6492static GLfloat police_color2[4] = { 1.0, 0.0, 0.5, 1.0}; // purpley-red
6493static GLfloat jammed_color[4] = { 0.0, 0.0, 0.0, 0.0}; // clear black
6494static GLfloat mascem_color1[4] = { 0.3, 0.3, 0.3, 1.0}; // dark gray
6495static GLfloat mascem_color2[4] = { 0.4, 0.1, 0.4, 1.0}; // purple
6496static GLfloat scripted_color[4] = { 0.0, 0.0, 0.0, 0.0}; // to be defined by script
6497
6498- (GLfloat *) scannerDisplayColorForShip:(ShipEntity*)otherShip :(BOOL)isHostile :(BOOL)flash :(OOColor *)scannerDisplayColor1 :(OOColor *)scannerDisplayColor2 :(OOColor *)scannerDisplayColorH1 :(OOColor *)scannerDisplayColorH2
6499{
6500 if (isHostile)
6501 {
6502 /* if there are any scripted scanner hostile display colours
6503 * for the ship, use them - otherwise fall through to the
6504 * normal scripted colours, then the scan class colours */
6505 if (scannerDisplayColorH1 || scannerDisplayColorH2)
6506 {
6507 if (scannerDisplayColorH1 && !scannerDisplayColorH2)
6508 {
6509 [scannerDisplayColorH1 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6510 }
6511
6512 if (!scannerDisplayColorH1 && scannerDisplayColorH2)
6513 {
6514 [scannerDisplayColorH2 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6515 }
6516
6517 if (scannerDisplayColorH1 && scannerDisplayColorH2)
6518 {
6519 if (flash)
6520 [scannerDisplayColorH1 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6521 else
6522 [scannerDisplayColorH2 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6523 }
6524
6525 return scripted_color;
6526 }
6527 }
6528
6529 // if there are any scripted scanner display colors for the ship, use them
6530 if (scannerDisplayColor1 || scannerDisplayColor2)
6531 {
6532 if (scannerDisplayColor1 && !scannerDisplayColor2)
6533 {
6534 [scannerDisplayColor1 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6535 }
6536
6537 if (!scannerDisplayColor1 && scannerDisplayColor2)
6538 {
6539 [scannerDisplayColor2 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6540 }
6541
6542 if (scannerDisplayColor1 && scannerDisplayColor2)
6543 {
6544 if (flash)
6545 [scannerDisplayColor1 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6546 else
6547 [scannerDisplayColor2 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6548 }
6549
6550 return scripted_color;
6551 }
6552
6553 // no scripted scanner display colors defined, proceed as per standard
6554 if ([self isJammingScanning])
6555 {
6556 if (![otherShip hasMilitaryScannerFilter])
6557 return jammed_color;
6558 else
6559 {
6560 if (flash)
6561 return mascem_color1;
6562 else
6563 {
6564 if (isHostile)
6565 return hostile_color;
6566 else
6567 return mascem_color2;
6568 }
6569 }
6570 }
6571
6572 switch (scanClass)
6573 {
6574 case CLASS_ROCK :
6575 case CLASS_CARGO :
6576 return cargo_color;
6577 case CLASS_THARGOID :
6578 if (flash)
6579 return hostile_color;
6580 else
6581 return friendly_color;
6582 case CLASS_MISSILE :
6583 return missile_color;
6584 case CLASS_STATION :
6585 return friendly_color;
6586 case CLASS_BUOY :
6587 if (flash)
6588 return friendly_color;
6589 else
6590 return neutral_color;
6591 case CLASS_POLICE :
6592 case CLASS_MILITARY :
6593 if ((isHostile)&&(flash))
6594 return police_color2;
6595 else
6596 return police_color1;
6597 case CLASS_MINE :
6598 if (flash)
6599 return neutral_color;
6600 else
6601 return hostile_color;
6602 default :
6603 if (isHostile)
6604 return hostile_color;
6605 }
6606 return neutral_color;
6607}
6608
6609
6610- (void)setScannerDisplayColor1:(OOColor *)color
6611{
6612 DESTROY(scanner_display_color1);
6613
6614 if (color == nil) color = [OOColor colorWithDescription:[[self shipInfoDictionary] objectForKey:@"scanner_display_color1"]];
6615 scanner_display_color1 = [color retain];
6616}
6617
6618
6619- (void)setScannerDisplayColor2:(OOColor *)color
6620{
6621 DESTROY(scanner_display_color2);
6622
6623 if (color == nil) color = [OOColor colorWithDescription:[[self shipInfoDictionary] objectForKey:@"scanner_display_color2"]];
6624 scanner_display_color2 = [color retain];
6625}
6626
6627
6628- (OOColor *)scannerDisplayColor1
6629{
6630 return [[scanner_display_color1 retain] autorelease];
6631}
6632
6633
6634- (OOColor *)scannerDisplayColor2
6635{
6636 return [[scanner_display_color2 retain] autorelease];
6637}
6638
6639
6640- (void)setScannerDisplayColorHostile1:(OOColor *)color
6641{
6642 DESTROY(scanner_display_color_hostile1);
6643
6644 if (color == nil) color = [OOColor colorWithDescription:[[self shipInfoDictionary] objectForKey:@"scanner_hostile_display_color1"]];
6645 scanner_display_color_hostile1 = [color retain];
6646}
6647
6648
6649- (void)setScannerDisplayColorHostile2:(OOColor *)color
6650{
6651 DESTROY(scanner_display_color_hostile2);
6652
6653 if (color == nil) color = [OOColor colorWithDescription:[[self shipInfoDictionary] objectForKey:@"scanner_hostile_display_color2"]];
6654 scanner_display_color_hostile2 = [color retain];
6655}
6656
6657
6658- (OOColor *)scannerDisplayColorHostile1
6659{
6660 return [[scanner_display_color_hostile1 retain] autorelease];
6661}
6662
6663
6664- (OOColor *)scannerDisplayColorHostile2
6665{
6666 return [[scanner_display_color_hostile2 retain] autorelease];
6667}
6668
6669
6670- (BOOL)isCloaked
6671{
6672 return cloaking_device_active;
6673}
6674
6675
6676- (BOOL) cloakPassive
6677{
6678 return cloakPassive;
6679}
6680
6681
6682- (void)setCloaked:(BOOL)cloak
6683{
6684 if (cloak) [self activateCloakingDevice];
6685 else [self deactivateCloakingDevice];
6686}
6687
6688
6689- (BOOL)hasAutoCloak
6690{
6691 return cloakAutomatic;
6692}
6693
6694
6695- (void)setAutoCloak:(BOOL)automatic
6696{
6697 cloakAutomatic = !!automatic;
6698}
6699
6700
6701- (BOOL) isJammingScanning
6702{
6703 return ([self hasMilitaryJammer] && military_jammer_active);
6704}
6705
6706
6707- (void) addSubEntity:(Entity<OOSubEntity> *)sub
6708{
6709 if (sub == nil) return;
6710
6711 if (subEntities == nil) subEntities = [[NSMutableArray alloc] init];
6712 sub->isSubEntity = YES;
6713 // Order matters - need consistent state in setOwner:. -- Ahruman 2008-04-20
6714 [subEntities addObject:sub];
6715 [sub setOwner:self];
6716
6717 [self addSubentityToCollisionRadius:sub];
6718}
6719
6720
6721- (void) setOwner:(Entity *)who_owns_entity
6722{
6723 [super setOwner:who_owns_entity];
6724
6725 /* Reset shader binding target so that bind-to-super works.
6726 This is necessary since we don't know about the owner in
6727 setUpShipFromDictionary:, when the mesh is initially set up.
6728 -- Ahruman 2008-04-19
6729 */
6730 if (isSubEntity)
6731 {
6732 [[self drawable] setBindingTarget:self];
6733 }
6734}
6735
6736
6737- (void) applyThrust:(double) delta_t
6738{
6739 GLfloat dt_thrust = SHIP_THRUST_FACTOR * thrust * delta_t;
6740 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
6741 BOOL isUsingAfterburner = (canBurn && (flightSpeed > maxFlightSpeed) && (desired_speed >= flightSpeed));
6742 float max_available_speed = maxFlightSpeed;
6743 if (canBurn) max_available_speed *= [self afterburnerFactor];
6744
6745 if (thrust)
6746 {
6747 // If we have Newtonian (non-thrust) velocity, brake it.
6748 GLfloat velmag = magnitude(velocity);
6749 if (velmag)
6750 {
6751 GLfloat vscale = fmaxf((velmag - dt_thrust) / velmag, 0.0f);
6752 scale_vector(&velocity, vscale);
6753 }
6754 }
6755
6756 if (behaviour == BEHAVIOUR_TUMBLE) return;
6757
6758 // check for speed
6759 if (desired_speed > max_available_speed)
6760 desired_speed = max_available_speed;
6761
6762 if (flightSpeed > desired_speed)
6763 {
6764 [self decrease_flight_speed: dt_thrust];
6765 if (flightSpeed < desired_speed) flightSpeed = desired_speed;
6766 }
6767 if (flightSpeed < desired_speed)
6768 {
6769 [self increase_flight_speed: dt_thrust];
6770 if (flightSpeed > desired_speed) flightSpeed = desired_speed;
6771 }
6772 [self moveForward: delta_t*flightSpeed];
6773
6774 // burn fuel at the appropriate rate
6775 if (isUsingAfterburner) // no fuelconsumption on slowdown
6776 {
6777 fuel_accumulator -= delta_t * afterburner_rate;
6778 while (fuel_accumulator < 0.0)
6779 {
6780 fuel--;
6781 fuel_accumulator += 1.0;
6782 }
6783 }
6784}
6785
6786
6787- (void) orientationChanged
6788{
6789 [super orientationChanged];
6790
6791 v_forward = vector_forward_from_quaternion(orientation);
6792 v_up = vector_up_from_quaternion(orientation);
6793 v_right = vector_right_from_quaternion(orientation);
6794}
6795
6796
6797- (void) applyRoll:(GLfloat) roll1 andClimb:(GLfloat) climb1
6798{
6799 Quaternion q1 = kIdentityQuaternion;
6800
6801 if (!roll1 && !climb1 && !hasRotated) return;
6802
6803 if (roll1) quaternion_rotate_about_z(&q1, -roll1);
6804 if (climb1) quaternion_rotate_about_x(&q1, -climb1);
6805
6806 orientation = quaternion_multiply(q1, orientation);
6807 [self orientationChanged];
6808}
6809
6810
6811- (void) applyRoll:(GLfloat) roll1 climb:(GLfloat) climb1 andYaw:(GLfloat) yaw1
6812{
6813 if ((roll1 == 0.0)&&(climb1 == 0.0)&&(yaw1 == 0.0)&&(!hasRotated))
6814 return;
6815
6816 Quaternion q1 = kIdentityQuaternion;
6817
6818 if (roll1)
6819 quaternion_rotate_about_z(&q1, -roll1);
6820 if (climb1)
6821 quaternion_rotate_about_x(&q1, -climb1);
6822 if (yaw1)
6823 quaternion_rotate_about_y(&q1, -yaw1);
6824
6825 orientation = quaternion_multiply(q1, orientation);
6826 [self orientationChanged];
6827}
6828
6829
6830- (void) applyAttitudeChanges:(double) delta_t
6831{
6832 [self applyRoll:flightRoll*delta_t climb:flightPitch*delta_t andYaw:flightYaw*delta_t];
6833}
6834
6835
6836- (void) avoidCollision
6837{
6838 if (scanClass == CLASS_MISSILE)
6839 return; // missiles are SUPPOSED to collide!
6840
6841 ShipEntity* prox_ship = (ShipEntity*)[self proximityAlert];
6842
6843 if (prox_ship)
6844 {
6845 if (previousCondition)
6846 {
6847 [previousCondition release];
6848 previousCondition = nil;
6849 }
6850
6851 previousCondition = [[NSMutableDictionary dictionaryWithCapacity:5] retain];
6852
6853 [previousCondition oo_setInteger:behaviour forKey:@"behaviour"];
6854 if ([self primaryTarget] != nil)
6855 {
6856 // must use the weak ref here to prevent potential over-retention
6857 [previousCondition setObject:[[self primaryTarget] weakSelf] forKey:@"primaryTarget"];
6858 }
6859 [previousCondition oo_setFloat:desired_range forKey:@"desired_range"];
6860 [previousCondition oo_setFloat:desired_speed forKey:@"desired_speed"];
6861 [previousCondition oo_setHPVector:_destination forKey:@"destination"];
6862
6863 _destination = [prox_ship position];
6864 _destination = OOHPVectorInterpolate(position, [prox_ship position], 0.5); // point between us and them
6865
6866 desired_range = prox_ship->collision_radius * PROXIMITY_AVOID_DISTANCE_FACTOR;
6867
6868 behaviour = BEHAVIOUR_AVOID_COLLISION;
6869 pitching_over = YES;
6870 }
6871}
6872
6873
6874- (void) resumePostProximityAlert
6875{
6876 if (!previousCondition) return;
6877
6878 behaviour = [previousCondition oo_intForKey:@"behaviour"];
6879 [_primaryTarget release];
6880 _primaryTarget = [[previousCondition objectForKey:@"primaryTarget"] weakRetain];
6881 [self startTrackingCurve];
6882 desired_range = [previousCondition oo_floatForKey:@"desired_range"];
6883 desired_speed = [previousCondition oo_floatForKey:@"desired_speed"];
6884 _destination = [previousCondition oo_hpvectorForKey:@"destination"];
6885
6886 [previousCondition release];
6887 previousCondition = nil;
6888 frustration = 0.0;
6889
6890 DESTROY(_proximityAlert);
6891
6892 //[shipAI message:@"RESTART_DOCKING"]; // if docking, start over, other AIs will ignore this message
6893}
6894
6895
6896- (double) messageTime
6897{
6898 return messageTime;
6899}
6900
6901
6902- (void) setMessageTime:(double) value
6903{
6904 messageTime = value;
6905}
6906
6907
6908- (OOShipGroup *) group
6909{
6910 return _group;
6911}
6912
6913
6914- (void) setGroup:(OOShipGroup *)group
6915{
6916 if (group != _group)
6917 {
6918 if (_escortGroup != _group)
6919 {
6920 if (self == [_group leader]) [_group setLeader:nil];
6921 [_group removeShip:self];
6922 }
6923 [_group release];
6924 [group addShip:self];
6925 _group = [group retain];
6926
6927 [[group leader] updateEscortFormation];
6928 }
6929}
6930
6931
6932- (OOShipGroup *) escortGroup
6933{
6934 if (_escortGroup == nil)
6935 {
6936 _escortGroup = [[OOShipGroup alloc] initWithName:@"escort group"];
6937 [_escortGroup setLeader:self];
6938 }
6939
6940 return _escortGroup;
6941}
6942
6943
6944- (void) setEscortGroup:(OOShipGroup *)group
6945{
6946 if (group != _escortGroup)
6947 {
6948 [_escortGroup release];
6949 _escortGroup = [group retain];
6950 [group setLeader:self]; // A ship is always leader of its own escort group.
6951 [self updateEscortFormation];
6952 }
6953}
6954
6955
6956#ifndef NDEBUG
6957- (OOShipGroup *) rawEscortGroup
6958{
6959 return _escortGroup;
6960}
6961#endif
6962
6963
6964- (OOShipGroup *) stationGroup
6965{
6966 if (_group == nil)
6967 {
6968 _group = [[OOShipGroup alloc] initWithName:@"station group"];
6969 [_group setLeader:self];
6970 }
6971
6972 return _group;
6973}
6974
6975
6976- (BOOL) hasEscorts
6977{
6978 if (_escortGroup == nil) return NO;
6979 return [_escortGroup count] > 1; // If only one member, it's self.
6980}
6981
6982
6983- (NSEnumerator *) escortEnumerator
6984{
6985 if (_escortGroup == nil) return [[NSArray array] objectEnumerator];
6986 return [[_escortGroup mutationSafeEnumerator] ooExcludingObject:self];
6987}
6988
6989
6990- (NSArray *) escortArray
6991{
6992 if (_escortGroup == nil) return [NSArray array];
6993 return [[self escortEnumerator] allObjects];
6994}
6995
6996
6997- (uint8_t) escortCount
6998{
6999 if (_escortGroup == nil) return 0;
7000 return [_escortGroup count] - 1;
7001}
7002
7003
7004- (uint8_t) pendingEscortCount
7005{
7006 return _pendingEscortCount;
7007}
7008
7009
7010- (void) setPendingEscortCount:(uint8_t)count
7011{
7012 _pendingEscortCount = MIN(count, _maxEscortCount);
7013}
7014
7015
7016- (uint8_t) maxEscortCount
7017{
7018 return _maxEscortCount;
7019}
7020
7021
7022- (void) setMaxEscortCount:(uint8_t)newCount
7023{
7024 _maxEscortCount = newCount;
7025}
7026
7027
7028- (NSUInteger) turretCount
7029{
7030 NSUInteger count = 0;
7031 NSEnumerator *subEnum = [self shipSubEntityEnumerator];
7032 ShipEntity *se = nil;
7033 while ((se = [subEnum nextObject]))
7034 {
7035 if ([se isTurret])
7036 {
7037 count ++;
7038 }
7039 }
7040 return count;
7041}
7042
7043
7044- (Entity*) proximityAlert
7045{
7046 Entity* prox = [_proximityAlert weakRefUnderlyingObject];
7047 if (prox == nil)
7048 {
7049 DESTROY(_proximityAlert);
7050 }
7051 return prox;
7052}
7053
7054
7055- (void) setProximityAlert:(ShipEntity*) other
7056{
7057 if (!other)
7058 {
7059 DESTROY(_proximityAlert);
7060 return;
7061 }
7062
7063 if ([other mass] < 2000) // we are not alerted by small objects. (a cargopod has a mass of about 1000)
7064 return;
7065
7066
7067 if (isStation) // stations don't worry about colliding with things
7068 return;
7069
7070 /* Ignore station collision warnings if launching or docking */
7071 if ((other->isStation) && ([self status] == STATUS_LAUNCHING ||
7072 dockingInstructions != nil))
7073 {
7074 return;
7075 }
7076
7077 if (!crew) // Ships without pilot (cargo, rocks, missiles, buoys etc) will not get alarmed. (escape-pods have pilots)
7078 return;
7079
7080 // check vectors
7081 Vector vdiff = HPVectorToVector(HPvector_between(position, other->position));
7082 GLfloat d_forward = dot_product(vdiff, v_forward);
7083 GLfloat d_up = dot_product(vdiff, v_up);
7084 GLfloat d_right = dot_product(vdiff, v_right);
7085 if ((d_forward > 0.0)&&(flightSpeed > 0.0)) // it's ahead of us and we're moving forward
7086 d_forward *= 0.25 * maxFlightSpeed / flightSpeed; // extend the collision zone forward up to 400%
7087 double d2 = d_forward * d_forward + d_up * d_up + d_right * d_right;
7088 double cr2 = collision_radius * 2.0 + other->collision_radius; cr2 *= cr2; // check with twice the combined radius
7089
7090 if (d2 > cr2) // we're okay
7091 return;
7092
7093 if (behaviour == BEHAVIOUR_AVOID_COLLISION) // already avoiding something
7094 {
7095 ShipEntity* prox = (ShipEntity*)[self proximityAlert];
7096 if ((prox)&&(prox != other))
7097 {
7098 // check which subtends the greatest angle
7099 GLfloat sa_prox = prox->collision_radius * prox->collision_radius / HPdistance2(position, prox->position);
7100 GLfloat sa_other = other->collision_radius * other->collision_radius / HPdistance2(position, other->position);
7101 if (sa_prox < sa_other) return;
7102 }
7103 }
7104 [_proximityAlert release];
7105 _proximityAlert = [other weakRetain];
7106}
7107
7108
7109- (NSString *) name
7110{
7111 return name;
7112}
7113
7114
7115- (NSString *) shipUniqueName
7116{
7117 return shipUniqueName;
7118}
7119
7120
7121- (NSString *) shipClassName
7122{
7123 return shipClassName;
7124}
7125
7126
7127- (NSString *) displayName
7128{
7129 if (displayName == nil || [displayName length] == 0)
7130 {
7131 if ([shipUniqueName length] == 0)
7132 {
7133 if (shipClassName == nil)
7134 {
7135 return name;
7136 }
7137 else
7138 {
7139 return shipClassName;
7140 }
7141 }
7142 else
7143 {
7144 if (shipClassName == nil)
7145 {
7146 return [NSString stringWithFormat:@"%@: %@",name,shipUniqueName];
7147 }
7148 else
7149 {
7150 return [NSString stringWithFormat:@"%@: %@",shipClassName,shipUniqueName];
7151 }
7152 }
7153 }
7154 return displayName;
7155}
7156
7157
7158// needed so that scan_description = scan_description doesn't have odd effects
7159- (NSString *)scanDescriptionForScripting
7160{
7161 return scan_description;
7162}
7163
7164
7165- (NSString *)scanDescription
7166{
7167 if (scan_description != nil)
7168 {
7169 return scan_description;
7170 }
7171 else
7172 {
7173 NSString *desc = nil;
7174 switch ([self scanClass])
7175 {
7176 case CLASS_NEUTRAL:
7177 {
7178 int legal = [self legalStatus];
7179 int legal_i = 0;
7180 if (legal > 0)
7181 {
7182 legal_i = (legal <= 50) ? 1 : 2;
7183 }
7184 desc = [[[UNIVERSE descriptions] oo_arrayForKey:@"legal_status"] oo_stringAtIndex:legal_i];
7185 }
7186 break;
7187
7188 case CLASS_THARGOID:
7189 desc = DESC(@"legal-desc-alien");
7190 break;
7191
7192 case CLASS_POLICE:
7193 desc = DESC(@"legal-desc-system-vessel");
7194 break;
7195
7196 case CLASS_MILITARY:
7197 desc = DESC(@"legal-desc-military-vessel");
7198 break;
7199
7200 default:
7201 break;
7202 }
7203 return desc;
7204 }
7205}
7206
7207
7208- (void) setName:(NSString *)inName
7209{
7210 [name release];
7211 name = [inName copy];
7212}
7213
7214
7215- (void) setShipUniqueName:(NSString *)inName
7216{
7217 [shipUniqueName release];
7218 shipUniqueName = [inName copy];
7219}
7220
7221
7222- (void) setShipClassName:(NSString *)inName
7223{
7224 [shipClassName release];
7225 shipClassName = [inName copy];
7226}
7227
7228
7229- (void) setDisplayName:(NSString *)inName
7230{
7231 [displayName release];
7232 displayName = [inName copy];
7233}
7234
7235
7236- (void) setScanDescription:(NSString *)inName
7237{
7238 [scan_description release];
7239 scan_description = [inName copy];
7240}
7241
7242
7243- (NSString *) identFromShip:(ShipEntity*) otherShip
7244{
7245 if ([self isJammingScanning] && ![otherShip hasMilitaryScannerFilter])
7246 {
7247 return DESC(@"unknown-target");
7248 }
7249 return [self displayName];
7250}
7251
7252
7253- (BOOL) hasRole:(NSString *)role
7254{
7255 return [roleSet hasRole:role] || [role isEqual:primaryRole] || [role isEqual:[self shipDataKeyAutoRole]];
7256}
7257
7258
7259- (OORoleSet *)roleSet
7260{
7261 if (roleSet == nil) roleSet = [[OORoleSet alloc] initWithRoleString:primaryRole];
7262 return [[roleSet roleSetWithAddedRoleIfNotSet:primaryRole probability:1.0] roleSetWithAddedRoleIfNotSet:[self shipDataKeyAutoRole] probability:1.0];
7263}
7264
7265
7266- (void) addRole:(NSString *)role
7267{
7268 [self addRole:role withProbability:0.0f];
7269}
7270
7271
7272- (void) addRole:(NSString *)role withProbability:(float)probability
7273{
7274 if (![self hasRole:role])
7275 {
7276 OORoleSet *newRoles = nil;
7277 if (roleSet != nil) newRoles = [roleSet roleSetWithAddedRole:role probability:probability];
7278 else newRoles = [OORoleSet roleSetWithRole:role probability:probability];
7279 if (newRoles != nil)
7280 {
7281 [roleSet release];
7282 roleSet = [newRoles retain];
7283 }
7284 }
7285}
7286
7287
7288- (void) removeRole:(NSString *)role
7289{
7290 if ([self hasRole:role])
7291 {
7292 OORoleSet *newRoles = [roleSet roleSetWithRemovedRole:role];
7293 if (newRoles != nil)
7294 {
7295 [roleSet release];
7296 roleSet = [newRoles retain];
7297 }
7298 }
7299}
7300
7301
7302- (NSString *)primaryRole
7303{
7304 if (primaryRole == nil)
7305 {
7306 primaryRole = [roleSet anyRole];
7307 if (primaryRole == nil) primaryRole = @"trader";
7308 [primaryRole retain];
7309 OOLog(@"ship.noPrimaryRole", @"%@ had no primary role, randomly selected \"%@\".", [self name], primaryRole);
7310 }
7311
7312 return primaryRole;
7313}
7314
7315
7316// Exposed to AI.
7317- (void)setPrimaryRole:(NSString *)role
7318{
7319 if (![role isEqual:primaryRole])
7320 {
7321 [primaryRole release];
7322 primaryRole = [role copy];
7323 }
7324}
7325
7326
7327- (BOOL)hasPrimaryRole:(NSString *)role
7328{
7329 return [[self primaryRole] isEqual:role];
7330}
7331
7332
7333- (BOOL)isPolice
7334{
7335 //bounty hunters have a police role, but are not police, so we must test by scan class, not by role
7336 return [self scanClass] == CLASS_POLICE;
7337}
7338
7339- (BOOL)isThargoid
7340{
7341 return [self scanClass] == CLASS_THARGOID;
7342}
7343
7344
7345- (BOOL)isTrader
7346{
7347 return [UNIVERSE role:[self primaryRole] isInCategory:@"oolite-trader"];
7348}
7349
7350
7351- (BOOL)isPirate
7352{
7353 return [UNIVERSE role:[self primaryRole] isInCategory:@"oolite-pirate"];
7354}
7355
7356
7357- (BOOL)isMissile
7358{
7359 return ([[self primaryRole] hasSuffix:@"MISSILE"] || [self hasPrimaryRole:@"missile"]);
7360}
7361
7362
7363- (BOOL)isMine
7364{
7365 return [[self primaryRole] hasSuffix:@"MINE"];
7366}
7367
7368
7369- (BOOL)isWeapon
7370{
7371 return [self isMissile] || [self isMine];
7372}
7373
7374
7375- (BOOL)isEscort
7376{
7377 return [UNIVERSE role:[self primaryRole] isInCategory:@"oolite-escort"];
7378}
7379
7380
7381- (BOOL)isShuttle
7382{
7383 return [UNIVERSE role:[self primaryRole] isInCategory:@"oolite-shuttle"];
7384}
7385
7386
7387- (BOOL)isTurret
7388{
7389 return behaviour == BEHAVIOUR_TRACK_AS_TURRET;
7390}
7391
7392
7393- (BOOL)isPirateVictim
7394{
7395 return [UNIVERSE roleIsPirateVictim:[self primaryRole]];
7396}
7397
7398
7399- (BOOL)isExplicitlyUnpiloted
7400{
7401 return _explicitlyUnpiloted;
7402}
7403
7404
7405- (BOOL)isUnpiloted
7406{
7407 return [self isExplicitlyUnpiloted] || [self isHulk] || [self scanClass] == CLASS_ROCK || [self scanClass] == CLASS_CARGO;
7408}
7409
7410
7411static BOOL IsBehaviourHostile(OOBehaviour behaviour)
7412{
7413 switch (behaviour)
7414 {
7415 case BEHAVIOUR_ATTACK_TARGET:
7416 case BEHAVIOUR_ATTACK_FLY_TO_TARGET:
7417 case BEHAVIOUR_ATTACK_FLY_FROM_TARGET:
7418 case BEHAVIOUR_RUNNING_DEFENSE:
7419 case BEHAVIOUR_FLEE_TARGET:
7420 case BEHAVIOUR_ATTACK_BREAK_OFF_TARGET:
7421 case BEHAVIOUR_ATTACK_SLOW_DOGFIGHT:
7422 case BEHAVIOUR_EVASIVE_ACTION:
7423 case BEHAVIOUR_FLEE_EVASIVE_ACTION:
7424 case BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX:
7425 // case BEHAVIOUR_ATTACK_MINING_TARGET:
7426 case BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE:
7427 case BEHAVIOUR_ATTACK_BROADSIDE:
7428 case BEHAVIOUR_ATTACK_BROADSIDE_LEFT:
7429 case BEHAVIOUR_ATTACK_BROADSIDE_RIGHT:
7430 case BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE:
7431 case BEHAVIOUR_CLOSE_WITH_TARGET:
7432 case BEHAVIOUR_ATTACK_SNIPER:
7433 case BEHAVIOUR_SCRIPTED_ATTACK_AI:
7434 return YES;
7435
7436 default:
7437 return NO;
7438 }
7439
7440 return 100 < behaviour && behaviour < 120;
7441}
7442
7443
7444// Exposed to shaders.
7445- (BOOL) hasHostileTarget
7446{
7447 Entity *t = [self primaryTarget];
7448 if (t == nil || ![t isShip])
7449 {
7450 return NO;
7451 }
7452 if ([self isMissile])
7453 {
7454 return YES; // missiles are always fired against a hostile target
7455 }
7456 if ((behaviour == BEHAVIOUR_AVOID_COLLISION)&&(previousCondition))
7457 {
7458 int old_behaviour = [previousCondition oo_intForKey:@"behaviour"];
7459 return IsBehaviourHostile(old_behaviour);
7460 }
7461 return IsBehaviourHostile(behaviour);
7462}
7463
7464
7465- (BOOL) isHostileTo:(Entity *)entity
7466{
7467 return ([self hasHostileTarget] && [self primaryTarget] == entity);
7468}
7469
7470- (GLfloat) weaponRange
7471{
7472 return weaponRange;
7473}
7474
7475
7476- (void) setWeaponRange: (GLfloat) value
7477{
7478 weaponRange = value;
7479}
7480
7481
7482- (void) setWeaponDataFromType: (OOWeaponType) weapon_type
7483{
7484 weaponRange = getWeaponRangeFromType(weapon_type);
7485 weapon_energy_use = [weapon_type weaponEnergyUse];
7486 weapon_recharge_rate = [weapon_type weaponRechargeRate];
7487 weapon_shot_temperature = [weapon_type weaponShotTemperature];
7488 weapon_damage = [weapon_type weaponDamage];
7489
7490 if (default_laser_color == nil)
7491 {
7492 OOColor *wcol = [weapon_type weaponColor];
7493 if (wcol != nil)
7494 {
7495 [self setLaserColor:wcol];
7496 }
7497 }
7498
7499}
7500
7501
7502- (float) energyRechargeRate
7503{
7504 return energy_recharge_rate;
7505}
7506
7507
7508- (void) setEnergyRechargeRate:(GLfloat)new
7509{
7510 energy_recharge_rate = new;
7511}
7512
7513
7514- (float) weaponRechargeRate
7515{
7516 return weapon_recharge_rate;
7517}
7518
7519
7520- (void) setWeaponRechargeRate:(float)value
7521{
7522 weapon_recharge_rate = value;
7523}
7524
7525
7526- (void) setWeaponEnergy:(float)value
7527{
7528 weapon_damage = value;
7529}
7530
7531
7532- (OOWeaponFacing) currentWeaponFacing
7533{
7534 return currentWeaponFacing;
7535}
7536
7537
7538- (GLfloat) scannerRange
7539{
7540 return scannerRange;
7541}
7542
7543
7544- (void) setScannerRange: (GLfloat) value
7545{
7546 scannerRange = value;
7547}
7548
7549
7550- (Vector) reference
7551{
7552 return reference;
7553}
7554
7555
7556- (void) setReference:(Vector) v
7557{
7558 reference = v;
7559}
7560
7561
7562- (BOOL) reportAIMessages
7563{
7564 return reportAIMessages;
7565}
7566
7567
7568- (void) setReportAIMessages:(BOOL) yn
7569{
7570 reportAIMessages = yn;
7571}
7572
7573
7574- (void) transitionToAegisNone
7575{
7576 if (!suppressAegisMessages && aegis_status != AEGIS_NONE)
7577 {
7578 Entity<OOStellarBody> *lastAegisLock = [self lastAegisLock];
7579 if (lastAegisLock != nil)
7580 {
7581 [self doScriptEvent:OOJSID("shipExitedPlanetaryVicinity") withArgument:lastAegisLock];
7582
7583 if (lastAegisLock == [UNIVERSE sun])
7584 {
7585 [shipAI message:@"AWAY_FROM_SUN"];
7586 }
7587 else
7588 {
7589 [shipAI message:@"AWAY_FROM_PLANET"];
7590 }
7591 }
7592
7593 if (aegis_status != AEGIS_CLOSE_TO_ANY_PLANET)
7594 {
7595 [shipAI message:@"AEGIS_NONE"];
7596 }
7597 }
7598 aegis_status = AEGIS_NONE;
7599}
7600
7601
7602static float SurfaceDistanceSqaredV(HPVector reference, Entity<OOStellarBody> *stellar)
7603{
7604 float centerDistance = HPmagnitude2(HPvector_subtract([stellar position], reference));
7605 float r = [stellar radius];
7606 /* 1.35: empirical value used to help determine proximity when non-nested
7607 planets are close to each other
7608 */
7609 return centerDistance - 1.35 * r * r;
7610}
7611
7612
7613static float SurfaceDistanceSqared(Entity *reference, Entity<OOStellarBody> *stellar)
7614{
7615 return SurfaceDistanceSqaredV([reference position], stellar);
7616}
7617
7618
7619NSComparisonResult ComparePlanetsBySurfaceDistance(id i1, id i2, void* context)
7620{
7621 HPVector p = [(ShipEntity*) context position];
7622 OOPlanetEntity* e1 = i1;
7623 OOPlanetEntity* e2 = i2;
7624
7625 float p1 = SurfaceDistanceSqaredV(p, e1);
7626 float p2 = SurfaceDistanceSqaredV(p, e2);
7627
7628 if (p1 < p2) return NSOrderedAscending;
7629 if (p1 > p2) return NSOrderedDescending;
7630
7631 return NSOrderedSame;
7632}
7633
7634
7635- (OOPlanetEntity *) findNearestPlanet
7636{
7637 /*
7638 Performance note: this method is called every frame by every ship, and
7639 has a significant profiler presence.
7640 -- Ahruman 2012-09-13
7641 */
7642 OOPlanetEntity *planet = nil, *bestPlanet = nil;
7643 float bestRange = INFINITY;
7644 HPVector myPosition = [self position];
7645
7646 // valgrind complains about this line here. Might be compiler/GNUstep bug?
7647 // should we go back to a traditional enumerator? - CIM
7648 // similar complaints about the other foreach() in this file
7649 foreach (planet, [UNIVERSE planets])
7650 {
7651 // Ignore miniature planets.
7652 if ([planet planetType] == STELLAR_TYPE_MINIATURE) continue;
7653
7654 float range = SurfaceDistanceSqaredV(myPosition, planet);
7655 if (range < bestRange)
7656 {
7657 bestPlanet = planet;
7658 bestRange = range;
7659 }
7660 }
7661
7662 return bestPlanet;
7663}
7664
7665
7666- (Entity<OOStellarBody> *) findNearestStellarBody
7667{
7668 Entity<OOStellarBody> *match = [self findNearestPlanet];
7669 OOSunEntity *sun = [UNIVERSE sun];
7670
7671 if (sun != nil)
7672 {
7673 if (match == nil ||
7674 SurfaceDistanceSqared(self, sun) < SurfaceDistanceSqared(self, match))
7675 {
7676 match = sun;
7677 }
7678 }
7679
7680 return match;
7681}
7682
7683
7684- (OOPlanetEntity *) findNearestPlanetExcludingMoons
7685{
7686 OOPlanetEntity *result = nil;
7687 OOPlanetEntity *planet = nil;
7688 NSArray *bodies = nil;
7689 NSArray *planets = nil;
7690 unsigned i;
7691
7692 bodies = [UNIVERSE planets];
7693 planets = [NSMutableArray arrayWithCapacity:[bodies count]];
7694
7695 for (i=0; i < [bodies count]; i++)
7696 {
7697 planet = [bodies objectAtIndex:i];
7698 if([planet planetType] == STELLAR_TYPE_NORMAL_PLANET)
7699 planets = [planets arrayByAddingObject:planet];
7700 }
7701
7702 if ([planets count] == 0) return nil;
7703
7704 planets = [planets sortedArrayUsingFunction:ComparePlanetsBySurfaceDistance context:self];
7705 result = [planets objectAtIndex:0];
7706
7707 return result;
7708}
7709
7710
7711- (OOAegisStatus) checkForAegis
7712{
7713 Entity<OOStellarBody> *nearest = [self findNearestStellarBody];
7714 BOOL sunGoneNova = [[UNIVERSE sun] goneNova];
7715
7716 if (nearest == nil)
7717 {
7718 if (aegis_status != AEGIS_NONE)
7719 {
7720 // Planet disappeared!
7721 [self transitionToAegisNone];
7722 }
7723 return AEGIS_NONE;
7724 }
7725 // check planet
7726 float cr = [nearest radius];
7727 float cr2 = cr * cr;
7728 OOAegisStatus result = AEGIS_NONE;
7729 float d2 = HPmagnitude2(HPvector_subtract([nearest position], [self position]));
7730 // not scannerRange: aegis shouldn't depend on that
7731 float sd2 = SCANNER_MAX_RANGE2 * 10.0f;
7732
7733 // check if nearing a surface
7734 unsigned wasNearPlanetSurface = isNearPlanetSurface; // isNearPlanetSurface is a bit flag, not an actual BOOL
7735 isNearPlanetSurface = (d2 - cr2) < (250000.0f + 1000.0f * cr); //less than 500m from the surface: (a+b)*(a+b) = a*a+b*b +2*a*b
7736
7737
7738 if (EXPECT_NOT((wasNearPlanetSurface != isNearPlanetSurface) && !suppressAegisMessages))
7739 {
7740 if (isNearPlanetSurface)
7741 {
7742 [self doScriptEvent:OOJSID("shipApproachingPlanetSurface") withArgument:nearest];
7743 [shipAI reactToMessage:@"APPROACHING_SURFACE" context:@"flight update"];
7744 }
7745 else
7746 {
7747 [self doScriptEvent:OOJSID("shipLeavingPlanetSurface") withArgument:nearest];
7748 [shipAI reactToMessage:@"LEAVING_SURFACE" context:@"flight update"];
7749 }
7750 }
7751
7752 // being close to the station takes precedence over planets
7753 StationEntity *the_station = [UNIVERSE station];
7754 if (the_station)
7755 {
7756 sd2 = HPmagnitude2(HPvector_subtract([the_station position], [self position]));
7757 }
7758 // again, notional scanner range is intentional
7759 if (sd2 < SCANNER_MAX_RANGE2 * 4.0f) // double scanner range
7760 {
7761 result = AEGIS_IN_DOCKING_RANGE;
7762 }
7763 else if (EXPECT_NOT(isNearPlanetSurface || d2 < cr2 * 9.0f)) // to 3x radius of any planet/moon - or 500m of tiny ones,
7764 {
7766 if (EXPECT((OOPlanetEntity *)nearest == [UNIVERSE planet]))
7767 {
7769 }
7770 }
7771 // need to do this check separately from above case to avoid oddity where
7772 // main planet and small moon are at just the wrong distance. - CIM
7773 if (result != AEGIS_CLOSE_TO_MAIN_PLANET && result != AEGIS_IN_DOCKING_RANGE && !sunGoneNova)
7774 {
7775 // are we also close to the main planet?
7776 OOPlanetEntity *mainPlanet = [UNIVERSE planet];
7777 d2 = HPmagnitude2(HPvector_subtract([mainPlanet position], [self position]));
7778 cr2 = [mainPlanet radius];
7779 cr2 *= cr2;
7780 if (d2 < cr2 * 9.0f)
7781 {
7782 nearest = mainPlanet;
7784 }
7785 }
7786
7787
7788 /* Rewrote aegis stuff and tested it against redux.oxp that adds multiple planets and moons.
7789 Made sure AI scripts can differentiate between MAIN and NON-MAIN planets so they can decide
7790 if they can dock at the systemStation or just any station.
7791 Added sun detection so route2Patrol can turn before they heat up in the sun.
7792 -- Eric 2009-07-11
7793
7794 More rewriting of the aegis stuff, it's now a bit faster and works properly when moving
7795 from one secondary planet/moon vicinity to another one. -- Kaks 20120917
7796 */
7797 if (EXPECT(!suppressAegisMessages))
7798 {
7799 // script/AI messages on change in status
7800 if (EXPECT_NOT(aegis_status == AEGIS_IN_DOCKING_RANGE && result != aegis_status))
7801 {
7802 [self doScriptEvent:OOJSID("shipExitedStationAegis") withArgument:the_station];
7803 [shipAI message:@"AEGIS_LEAVING_DOCKING_RANGE"];
7804 }
7805
7806 if (EXPECT_NOT(result == AEGIS_IN_DOCKING_RANGE && aegis_status != result))
7807 {
7808 [self doScriptEvent:OOJSID("shipEnteredStationAegis") withArgument:the_station];
7809 [shipAI message:@"AEGIS_IN_DOCKING_RANGE"];
7810
7811 if([self lastAegisLock] == nil && !sunGoneNova) // With small main planets the station aegis can come before planet aegis
7812 {
7813 [self doScriptEvent:OOJSID("shipEnteredPlanetaryVicinity") withArgument:[UNIVERSE planet]];
7814 [self setLastAegisLock:[UNIVERSE planet]];
7815 }
7816 }
7817 else if (EXPECT_NOT(result == AEGIS_NONE && aegis_status != result))
7818 {
7819 if([self lastAegisLock] == nil && !sunGoneNova)
7820 {
7821 [self setLastAegisLock:[UNIVERSE planet]]; // in case of a first launch from a near-planet station.
7822 }
7823 [self transitionToAegisNone];
7824 }
7825 // approaching..
7826 else if (EXPECT_NOT((result == AEGIS_CLOSE_TO_ANY_PLANET || result == AEGIS_CLOSE_TO_MAIN_PLANET) && [self lastAegisLock] != nearest))
7827 {
7828 if(aegis_status != AEGIS_NONE && [self lastAegisLock] != nil) // we were close to another stellar body
7829 {
7830 [self doScriptEvent:OOJSID("shipExitedPlanetaryVicinity") withArgument:[self lastAegisLock]];
7831 [shipAI message:@"AWAY_FROM_PLANET"]; // fires for suns, planets and moons.
7832 }
7833 [self doScriptEvent:OOJSID("shipEnteredPlanetaryVicinity") withArgument:nearest];
7834 [self setLastAegisLock:nearest];
7835
7836 if (EXPECT_NOT([nearest isSun]))
7837 {
7838 [shipAI message:@"CLOSE_TO_SUN"];
7839 }
7840 else
7841 {
7842 [shipAI message:@"CLOSE_TO_PLANET"];
7843
7844 if (EXPECT(result == AEGIS_CLOSE_TO_MAIN_PLANET))
7845 {
7846 // It's been years since 1.71 - it should be safe enough to comment out the line below for 1.77/1.78 -- Kaks 20120917
7847 //[shipAI message:@"AEGIS_CLOSE_TO_PLANET"]; // fires only for main planets, kept for compatibility with pre-1.72 AI plists.
7848 [shipAI message:@"AEGIS_CLOSE_TO_MAIN_PLANET"]; // fires only for main planet.
7849 }
7850 else if (EXPECT_NOT([nearest planetType] == STELLAR_TYPE_MOON))
7851 {
7852 [shipAI message:@"CLOSE_TO_MOON"];
7853 }
7854 else
7855 {
7856 [shipAI message:@"CLOSE_TO_SECONDARY_PLANET"];
7857 }
7858 }
7859 }
7860
7861
7862 }
7863 if (result == AEGIS_NONE)
7864 {
7865 [self setLastAegisLock:nil];
7866 }
7867
7868 aegis_status = result; // put this here
7869 return result;
7870}
7871
7872
7873- (void) forceAegisCheck
7874{
7875 _nextAegisCheck = -1.0f;
7876}
7877
7878
7879- (BOOL) withinStationAegis
7880{
7881 return aegis_status == AEGIS_IN_DOCKING_RANGE;
7882}
7883
7884
7885- (Entity<OOStellarBody> *) lastAegisLock
7886{
7887 Entity<OOStellarBody> *stellar = [_lastAegisLock weakRefUnderlyingObject];
7888 if (stellar == nil)
7889 {
7890 [_lastAegisLock release];
7891 _lastAegisLock = nil;
7892 }
7893
7894 return stellar;
7895}
7896
7897
7898- (void) setLastAegisLock:(Entity<OOStellarBody> *)lastAegisLock
7899{
7900 [_lastAegisLock release];
7901 _lastAegisLock = [lastAegisLock weakRetain];
7902}
7903
7904
7905- (OOSystemID) homeSystem
7906{
7907 return home_system;
7908}
7909
7910
7911- (OOSystemID) destinationSystem
7912{
7913 return destination_system;
7914}
7915
7916
7917- (void) setHomeSystem:(OOSystemID)s
7918{
7919 home_system = s;
7920}
7921
7922
7923- (void) setDestinationSystem:(OOSystemID)s
7924{
7925 destination_system = s;
7926}
7927
7928
7929- (void) setStatus:(OOEntityStatus) stat
7930{
7931 if ([self status] == stat) return;
7932 [super setStatus:stat];
7933 if (stat == STATUS_LAUNCHING)
7934 {
7935 launch_time = [UNIVERSE getTime];
7936 }
7937}
7938
7939- (void) setLaunchDelay:(double)delay
7940{
7941 launch_delay = delay;
7942}
7943
7944
7945- (NSArray *) crew
7946{
7947 return crew;
7948}
7949
7950
7951- (void) setCrew:(NSArray *)crewArray
7952{
7953 if ([self isExplicitlyUnpiloted])
7954 {
7955 //unpiloted ships cannot have crew
7956 // but may have crew before isExplicitlyUnpiloted set, so force *that* to clear too
7957 [crew autorelease];
7958 crew = nil;
7959 return;
7960 }
7961 //do not set to hulk here when crew is nil (or 0). Some things like missiles have no crew.
7962 [crew autorelease];
7963 crew = [crewArray copy];
7964}
7965
7966
7967- (void) setSingleCrewWithRole:(NSString *)crewRole
7968{
7969 if (![self isUnpiloted])
7970 {
7971 OOCharacter *crewMember = [OOCharacter randomCharacterWithRole:crewRole
7972 andOriginalSystem:[self homeSystem]];
7973 [self setCrew:[NSArray arrayWithObject:crewMember]];
7974 }
7975}
7976
7977
7978- (NSArray *) crewForScripting
7979{
7980 if (crew == nil)
7981 {
7982 return nil;
7983 }
7984 OOCharacter *crewMember = nil;
7985 NSMutableArray *result = [NSMutableArray arrayWithCapacity:4];
7986 foreach (crewMember, crew)
7987 {
7988 NSDictionary *crewDict = [crewMember infoForScripting];
7989 [result addObject:crewDict];
7990 }
7991 return result;
7992}
7993
7994
7995
7996- (void) setStateMachine:(NSString *)smName
7997{
7998 [self setAITo:smName];
7999}
8000
8001
8002- (void) setAI:(AI *)ai
8003{
8004 [ai retain];
8005 if (shipAI)
8006 {
8007 [shipAI clearAllData];
8008 [shipAI autorelease];
8009 }
8010 shipAI = ai;
8011}
8012
8013
8014- (AI *) getAI
8015{
8016 return shipAI;
8017}
8018
8019
8020- (BOOL) hasAutoAI
8021{
8022 return [[self shipInfoDictionary] oo_fuzzyBooleanForKey:@"auto_ai" defaultValue:YES];
8023}
8024
8025
8026- (BOOL) hasNewAI
8027{
8028 return [[[self getAI] name] isEqualToString:@"nullAI.plist"];
8029}
8030
8031
8032- (BOOL) hasAutoWeapons
8033{
8034 return [[self shipInfoDictionary] oo_fuzzyBooleanForKey:@"auto_weapons" defaultValue:NO];
8035}
8036
8037
8038- (void) setShipScript:(NSString *)script_name
8039{
8040 NSMutableDictionary *properties = nil;
8041 NSArray *actions = nil;
8042
8043 properties = [NSMutableDictionary dictionary];
8044 [properties setObject:self forKey:@"ship"];
8045
8046 [script autorelease];
8047 script = [OOScript jsScriptFromFileNamed:script_name properties:properties];
8048
8049 if (script == nil)
8050 {
8051 actions = [shipinfoDictionary oo_arrayForKey:@"launch_actions"];
8052 if (actions)
8053 {
8054 OOStandardsDeprecated([NSString stringWithFormat:@"The launch_actions ship key is deprecated on %@.",[self displayName]]);
8055 if (!OOEnforceStandards())
8056 {
8057 [properties setObject:actions forKey:@"legacy_launchActions"];
8058 }
8059 }
8060
8061 actions = [shipinfoDictionary oo_arrayForKey:@"script_actions"];
8062 if (actions)
8063 {
8064 OOStandardsDeprecated([NSString stringWithFormat:@"The script_actions ship key is deprecated on %@.",[self displayName]]);
8065 if (!OOEnforceStandards())
8066 {
8067 [properties setObject:actions forKey:@"legacy_scriptActions"];
8068 }
8069 }
8070
8071 actions = [shipinfoDictionary oo_arrayForKey:@"death_actions"];
8072 if (actions)
8073 {
8074 OOStandardsDeprecated([NSString stringWithFormat:@"The death_actions ship key is deprecated on %@.",[self displayName]]);
8075 if (!OOEnforceStandards())
8076 {
8077 [properties setObject:actions forKey:@"legacy_deathActions"];
8078 }
8079 }
8080
8081 actions = [shipinfoDictionary oo_arrayForKey:@"setup_actions"];
8082 if (actions)
8083 {
8084 OOStandardsDeprecated([NSString stringWithFormat:@"The setup_actions ship key is deprecated on %@.",[self displayName]]);
8085 if (!OOEnforceStandards())
8086 {
8087 [properties setObject:actions forKey:@"legacy_setupActions"];
8088 }
8089 }
8090
8091 script = [OOScript jsScriptFromFileNamed:@"oolite-default-ship-script.js"
8092 properties:properties];
8093 }
8094 [script retain];
8095}
8096
8097
8098- (double)frustration
8099{
8100 return frustration;
8101}
8102
8103
8104- (OOFuelQuantity) fuel
8105{
8106 return fuel;
8107}
8108
8109
8110- (void) setFuel:(OOFuelQuantity) amount
8111{
8112 if (amount > [self fuelCapacity]) amount = [self fuelCapacity];
8113
8114 fuel = amount;
8115}
8116
8117
8118- (OOFuelQuantity) fuelCapacity
8119{
8120 // FIXME: shipdata.plist can allow greater fuel quantities (without extending hyperspace range). Need some consistency here.
8121 return PLAYER_MAX_FUEL;
8122}
8123
8124
8125- (GLfloat) fuelChargeRate
8126{
8127 GLfloat rate = 1.0; // Standard (& strict play) charge rate.
8128
8129#if MASS_DEPENDENT_FUEL_PRICES
8130
8131 if (EXPECT(PLAYER != nil && mass> 0 && mass != [PLAYER baseMass]))
8132 {
8133 rate = calcFuelChargeRate(mass);
8134 }
8135
8136 OOLog(@"fuelPrices", @"\"%@\" fuel charge rate: %.2f (mass ratio: %.2f/%.2f)", [self shipDataKey], rate, mass, [PLAYER baseMass]);
8137#endif
8138
8139 return rate;
8140}
8141
8142
8143- (void) applySticks:(double)delta_t
8144{
8145
8146 double rate1 = 2.0 * delta_t; //roll
8147 double rate2 = 4.0 * delta_t; //pitch
8148 double rate3 = 4.0 * delta_t; //yaw
8149
8150 if (((stick_roll > 0.0)&&(flightRoll < 0.0))||((stick_roll < 0.0)&&(flightRoll > 0.0)))
8151 rate1 *= 4.0; // much faster correction
8152 if (((stick_pitch > 0.0)&&(flightPitch < 0.0))||((stick_pitch < 0.0)&&(flightPitch > 0.0)))
8153 rate2 *= 4.0; // much faster correction
8154 if (((stick_yaw > 0.0)&&(flightYaw < 0.0))||((stick_yaw < 0.0)&&(flightYaw > 0.0)))
8155 rate3 *= 4.0; // much faster correction
8156
8157 if (accuracy >= COMBAT_AI_TRACKS_CLOSER)
8158 {
8159 if (stick_roll == 0.0)
8160 rate1 *= 2.0; // faster correction
8161 if (stick_pitch == 0.0)
8162 rate2 *= 2.0; // faster correction
8163 if (stick_yaw == 0.0)
8164 rate3 *= 2.0; // faster correction
8165 }
8166
8167 // apply stick movement limits
8168 if (flightRoll < stick_roll - rate1)
8169 {
8170 flightRoll = flightRoll + rate1;
8171 }
8172 else if (flightRoll > stick_roll + rate1)
8173 {
8174 flightRoll = flightRoll - rate1;
8175 }
8176 else
8177 {
8178 flightRoll = stick_roll;
8179 }
8180
8181 if (flightPitch < stick_pitch - rate2)
8182 {
8183 flightPitch = flightPitch + rate2;
8184 }
8185 else if (flightPitch > stick_pitch + rate2)
8186 {
8187 flightPitch = flightPitch - rate2;
8188 }
8189 else
8190 {
8191 flightPitch = stick_pitch;
8192 }
8193
8194 if (flightYaw < stick_yaw - rate3)
8195 {
8196 flightYaw = flightYaw + rate3;
8197 }
8198 else if (flightYaw > stick_yaw + rate3)
8199 {
8200 flightYaw = flightYaw - rate3;
8201 }
8202 else
8203 {
8204 flightYaw = stick_yaw;
8205 }
8206
8207}
8208
8209
8210- (void) setRoll:(double) amount
8211{
8212 flightRoll = amount * M_PI / 2.0;
8213}
8214
8215
8216- (void) setRawRoll:(double) amount
8217{
8218 flightRoll = amount;
8219}
8220
8221
8222- (void) setPitch:(double) amount
8223{
8224 flightPitch = amount * M_PI / 2.0;
8225}
8226
8227
8228- (void) setYaw:(double) amount
8229{
8230 flightYaw = amount * M_PI / 2.0;
8231}
8232
8233
8234- (void) setThrust:(double) amount
8235{
8236 thrust = amount;
8237}
8238
8239
8240- (void) setThrustForDemo:(float) factor
8241{
8242 flightSpeed = factor * maxFlightSpeed;
8243}
8244
8245
8246- (void) setBounty:(OOCreditsQuantity) amount
8247{
8248 [self setBounty:amount withReason:kOOLegalStatusReasonUnknown];
8249}
8250
8251
8252- (void) setBounty:(OOCreditsQuantity) amount withReason:(OOLegalStatusReason)reason
8253{
8254 if ([self isSubEntity])
8255 {
8256 [[self parentEntity] setBounty:amount withReason:reason];
8257 }
8258 else
8259 {
8260 if ((scanClass == CLASS_THARGOID || scanClass == CLASS_STATION) && reason != kOOLegalStatusReasonSetup && reason != kOOLegalStatusReasonByScript)
8261 {
8262 return; // no standard bounties for Thargoids / Stations
8263 }
8264 if (scanClass == CLASS_POLICE && amount != 0)
8265 {
8266 return; // police never have bounties
8267 }
8268 NSString* nReason = OOStringFromLegalStatusReason(reason);
8269 [self setBounty:amount withReasonAsString:nReason];
8270 }
8271}
8272
8273- (void) setBounty:(OOCreditsQuantity) amount withReasonAsString:(NSString*)reason
8274{
8275 if ([self isSubEntity])
8276 {
8277 [[self parentEntity] setBounty:amount withReasonAsString:reason];
8278 }
8279 else
8280 {
8281 JSContext *context = OOJSAcquireContext();
8282
8283 jsval amountVal = JSVAL_VOID;
8284 JS_NewNumberValue(context, (int)amount-(int)bounty, &amountVal);
8285
8286 bounty = amount; // can't set the new bounty until the size of the change is known
8287
8288 jsval reasonVal = OOJSValueFromNativeObject(context,reason);
8289
8290 ShipScriptEvent(context, self, "shipBountyChanged", amountVal, reasonVal);
8291
8292 OOJSRelinquishContext(context);
8293
8294 }
8295}
8296
8297
8298
8299- (OOCreditsQuantity) bounty
8300{
8301 if ([self isSubEntity])
8302 {
8303 return [[self parentEntity] bounty];
8304 }
8305 else
8306 {
8307 return bounty;
8308 }
8309}
8310
8311
8312- (int) legalStatus
8313{
8314 if (scanClass == CLASS_THARGOID)
8315 return 5 * collision_radius;
8316 if (scanClass == CLASS_ROCK)
8317 return 0;
8318 return (int)[self bounty];
8319}
8320
8321
8322- (void) setCommodity:(OOCommodityType)co_type andAmount:(OOCargoQuantity)co_amount
8323{
8324 if (co_type != nil)
8325 {
8326 /* The tmp variable is needed as scoopUp can cause the method
8327 * to be passed a reference to self.commodity_type, so DESTROY
8328 * then copying the parameter segfaults */
8329 NSString *tmp = [co_type copy];
8330 DESTROY(commodity_type);
8331 commodity_type = tmp;
8332 commodity_amount = co_amount;
8333 }
8334}
8335
8336
8337- (void) setCommodityForPod:(OOCommodityType)co_type andAmount:(OOCargoQuantity)co_amount
8338{
8339 // can be nil for pods
8340 if (co_type == nil)
8341 {
8342 DESTROY(commodity_type);
8343 commodity_amount = 0;
8344 return;
8345 }
8346 // pod content should never be greater than 1 ton or this will give cargo counting problems elsewhere in the code.
8347 // so do first a mass check for cargo added by script/plist.
8348 OOMassUnit unit = [[UNIVERSE commodityMarket] massUnitForGood:co_type];
8349 if (unit == UNITS_TONS && co_amount > 1) co_amount = 1;
8350 else if (unit == UNITS_KILOGRAMS && co_amount > 1000) co_amount = 1000;
8351 else if (unit == UNITS_GRAMS && co_amount > 1000000) co_amount = 1000000;
8352 [self setCommodity:co_type andAmount:co_amount];
8353}
8354
8355
8356- (OOCommodityType) commodityType
8357{
8358 return commodity_type;
8359}
8360
8361
8362- (OOCargoQuantity) commodityAmount
8363{
8364 return commodity_amount;
8365}
8366
8367
8368- (OOCargoQuantity) maxAvailableCargoSpace
8369{
8370 return max_cargo - equipment_weight;
8371}
8372
8373
8374- (void) setMaxAvailableCargoSpace:(OOCargoQuantity)new
8375{
8376 max_cargo = new + equipment_weight;
8377}
8378
8379
8380- (OOCargoQuantity) availableCargoSpace
8381{
8382 // OOCargoQuantity is unsigned, we need to check for underflows.
8383 if (EXPECT_NOT([self cargoQuantityOnBoard] + equipment_weight >= max_cargo)) return 0;
8384 return [self maxAvailableCargoSpace] - [self cargoQuantityOnBoard];
8385}
8386
8387
8388- (OOCargoQuantity) cargoQuantityOnBoard
8389{
8390 NSUInteger result = [[self cargo] count];
8391 NSAssert(result < UINT32_MAX, @"Cargo quantity out of bounds.");
8392 return (OOCargoQuantity)result;
8393}
8394
8395
8396
8397- (OOCargoType) cargoType
8398{
8399 return cargo_type;
8400}
8401
8402
8403/* Note: this array probably contains some template cargo pods. Do not
8404 * pass it to Javascript without reifying them first. */
8405- (NSMutableArray*) cargo
8406{
8407 return cargo;
8408}
8409
8410
8411- (NSArray *) cargoListForScripting
8412{
8413 NSMutableArray *list = [NSMutableArray array];
8414
8415 OOCommodityType good = nil;
8416 NSArray *goods = [[UNIVERSE commodityMarket] goods];
8417 NSUInteger i, j, commodityCount = [goods count];
8418 OOCargoQuantity quantityInHold[commodityCount];
8419
8420 for (i = 0; i < commodityCount; i++)
8421 {
8422 quantityInHold[i] = 0;
8423 }
8424 for (i = 0; i < [cargo count]; i++)
8425 {
8426 ShipEntity *container = [cargo objectAtIndex:i];
8427 j = [goods indexOfObject:[container commodityType]];
8428 quantityInHold[j] += [container commodityAmount];
8429 }
8430
8431 for (i = 0; i < commodityCount; i++)
8432 {
8433 if (quantityInHold[i] > 0)
8434 {
8435 NSMutableDictionary *commodity = [NSMutableDictionary dictionaryWithCapacity:4];
8436 good = [goods objectAtIndex:i];
8437 // commodity, quantity - keep consistency between .manifest and .contracts
8438 [commodity setObject:good forKey:@"commodity"];
8439 [commodity setObject:[NSNumber numberWithUnsignedInt:quantityInHold[i]] forKey:@"quantity"];
8440 [commodity setObject:[[UNIVERSE commodityMarket] nameForGood:good] forKey:@"displayName"];
8441 [commodity setObject:DisplayStringForMassUnitForCommodity(good) forKey:@"unit"];
8442 [list addObject:commodity];
8443 }
8444 }
8445
8446 return [[list copy] autorelease]; // return an immutable copy
8447}
8448
8449- (void) setCargo:(NSArray *) some_cargo
8450{
8451 [cargo removeAllObjects];
8452 [cargo addObjectsFromArray:some_cargo];
8453}
8454
8455
8456- (BOOL) addCargo:(NSArray *) some_cargo
8457{
8458 if ([cargo count] + [some_cargo count] > [self maxAvailableCargoSpace])
8459 {
8460 return NO;
8461 }
8462 else
8463 {
8464 [cargo addObjectsFromArray:some_cargo];
8465 return YES;
8466 }
8467}
8468
8469
8470- (BOOL) removeCargo:(OOCommodityType)commodity amount:(OOCargoQuantity) amount
8471{
8472 OOCargoQuantity found = 0;
8473 ShipEntity *pod = nil;
8474 foreach (pod, cargo)
8475 {
8476 if ([[pod commodityType] isEqualToString:commodity])
8477 {
8478 found++;
8479 }
8480 }
8481 if (found < amount)
8482 {
8483 // don't remove any if there aren't enough to remove the full amount
8484 return NO;
8485 }
8486
8487 NSUInteger i = [cargo count] - 1;
8488 // iterate downwards to be safe removing during iteration
8489 while (amount > 0)
8490 {
8491 if ([[[cargo objectAtIndex:i] commodityType] isEqualToString:commodity])
8492 {
8493 amount--;
8494 [cargo removeObjectAtIndex:i];
8495 }
8496 // check above means array index can't underflow here
8497 i--;
8498 }
8499
8500 return YES;
8501}
8502
8503
8504
8505- (BOOL) showScoopMessage
8506{
8507 return hasScoopMessage;
8508}
8509
8510
8511- (OOCargoFlag) cargoFlag
8512{
8513 return cargo_flag;
8514}
8515
8516
8517- (void) setCargoFlag:(OOCargoFlag) flag
8518{
8519 if (cargo_flag != flag)
8520 {
8521 cargo_flag = flag;
8522 NSArray *newCargo = nil;
8523 unsigned num = 0;
8524 if (likely_cargo > 0)
8525 {
8526 num = likely_cargo * (0.5+randf());
8527 if (num > [self maxAvailableCargoSpace])
8528 {
8529 num = [self maxAvailableCargoSpace];
8530 }
8531 }
8532 else
8533 {
8534 num = [self maxAvailableCargoSpace];
8535 }
8536 if (num > 200)
8537 {
8538 num = 200;
8539 /* no core NPC ship carries this much when generated (the
8540 * Anaconda could, but doesn't): let's not waste time generating
8541 * thousands of pods - even if they are semi-virtual - for some
8542 * massive OXP ship */
8543 }
8544 if (num > 0)
8545 {
8546 switch (cargo_flag)
8547 {
8549 newCargo = [UNIVERSE getContainersOfCommodity:[shipinfoDictionary oo_stringForKey:@"cargo_carried"] :num];
8550 break;
8552 newCargo = [UNIVERSE getContainersOfGoods:num scarce:NO legal:YES];
8553 break;
8555 newCargo = [UNIVERSE getContainersOfGoods:num scarce:YES legal:YES];
8556 break;
8558 newCargo = [UNIVERSE getContainersOfCommodity:@"Narcotics" :num];
8559 break;
8561 newCargo = [UNIVERSE getContainersOfGoods:num scarce:YES legal:NO];
8562 break;
8563 case CARGO_FLAG_PIRATE:
8564 newCargo = [UNIVERSE getContainersOfGoods:(Ranrot() % (1+num/2)) scarce:YES legal:NO];
8565 break;
8567 // TODO: allow passengers to survive
8568 case CARGO_FLAG_NONE:
8569 default:
8570 break;
8571 }
8572 }
8573 [self setCargo:newCargo];
8574 }
8575}
8576
8577
8578- (void) setSpeed:(double) amount
8579{
8580 flightSpeed = amount;
8581}
8582
8583
8584- (void) setDesiredSpeed:(double) amount
8585{
8586 desired_speed = amount;
8587}
8588
8589
8590- (double) desiredSpeed
8591{
8592 return desired_speed;
8593}
8594
8595
8596- (double) desiredRange
8597{
8598 return desired_range;
8599}
8600
8601
8602- (void) setDesiredRange:(double) amount
8603{
8604 desired_range = amount;
8605}
8606
8607
8608- (double) cruiseSpeed
8609{
8610 return cruiseSpeed;
8611}
8612
8613
8614- (void) increase_flight_speed:(double) delta
8615{
8616 double factor = 1.0;
8617 if (desired_speed > maxFlightSpeed && [self hasFuelInjection] && fuel > MIN_FUEL) factor = [self afterburnerFactor];
8618
8619 if (flightSpeed < maxFlightSpeed * factor)
8620 flightSpeed += delta * factor;
8621 else
8622 flightSpeed = maxFlightSpeed * factor;
8623}
8624
8625
8626- (void) decrease_flight_speed:(double) delta
8627{
8628 double factor = 1.0;
8629 if (flightSpeed > maxFlightSpeed)
8630 {
8631 factor = MIN_HYPERSPEED_FACTOR;
8632 }
8633
8634 if (flightSpeed > factor * delta)
8635 {
8636 flightSpeed -= factor * delta;
8637 }
8638 else
8639 {
8640 flightSpeed = 0;
8641 }
8642}
8643
8644
8645- (void) increase_flight_roll:(double) delta
8646{
8647 flightRoll += delta;
8648 if (flightRoll > max_flight_roll)
8649 flightRoll = max_flight_roll;
8650 else if (flightRoll < -max_flight_roll)
8651 flightRoll = -max_flight_roll;
8652}
8653
8654
8655- (void) decrease_flight_roll:(double) delta
8656{
8657 flightRoll -= delta;
8658 if (flightRoll > max_flight_roll)
8659 flightRoll = max_flight_roll;
8660 else if (flightRoll < -max_flight_roll)
8661 flightRoll = -max_flight_roll;
8662}
8663
8664
8665- (void) increase_flight_pitch:(double) delta
8666{
8667 flightPitch += delta;
8668 if (flightPitch > max_flight_pitch)
8669 flightPitch = max_flight_pitch;
8670 else if (flightPitch < -max_flight_pitch)
8671 flightPitch = -max_flight_pitch;
8672}
8673
8674
8675- (void) decrease_flight_pitch:(double) delta
8676{
8677 flightPitch -= delta;
8678 if (flightPitch > max_flight_pitch)
8679 flightPitch = max_flight_pitch;
8680 else if (flightPitch < -max_flight_pitch)
8681 flightPitch = -max_flight_pitch;
8682}
8683
8684
8685- (void) increase_flight_yaw:(double) delta
8686{
8687 flightYaw += delta;
8688 if (flightYaw > max_flight_yaw)
8689 flightYaw = max_flight_yaw;
8690 else if (flightYaw < -max_flight_yaw)
8691 flightYaw = -max_flight_yaw;
8692}
8693
8694
8695- (void) decrease_flight_yaw:(double) delta
8696{
8697 flightYaw -= delta;
8698 if (flightYaw > max_flight_yaw)
8699 flightYaw = max_flight_yaw;
8700 else if (flightYaw < -max_flight_yaw)
8701 flightYaw = -max_flight_yaw;
8702}
8703
8704
8705- (GLfloat) flightRoll
8706{
8707 return flightRoll;
8708}
8709
8710
8711- (GLfloat) flightPitch
8712{
8713 return flightPitch;
8714}
8715
8716
8717- (GLfloat) flightYaw
8718{
8719 return flightYaw;
8720}
8721
8722
8723- (GLfloat) flightSpeed
8724{
8725 return flightSpeed;
8726}
8727
8728
8729- (GLfloat) maxFlightPitch
8730{
8731 return max_flight_pitch;
8732}
8733
8734
8735- (GLfloat) maxFlightSpeed
8736{
8737 return maxFlightSpeed;
8738}
8739
8740
8741- (GLfloat) maxFlightRoll
8742{
8743 return max_flight_roll;
8744}
8745
8746
8747- (GLfloat) maxFlightYaw
8748{
8749 return max_flight_yaw;
8750}
8751
8752
8753- (void) setMaxFlightPitch:(GLfloat)new
8754{
8755 max_flight_pitch = new;
8756}
8757
8758
8759- (void) setMaxFlightSpeed:(GLfloat)new
8760{
8761 maxFlightSpeed = new;
8762}
8763
8764
8765- (void) setMaxFlightRoll:(GLfloat)new
8766{
8767 max_flight_roll = new;
8768}
8769
8770
8771- (void) setMaxFlightYaw:(GLfloat)new
8772{
8773 max_flight_yaw = new;
8774}
8775
8776
8777- (GLfloat) speedFactor
8778{
8779 if (maxFlightSpeed <= 0.0) return 0.0;
8780 return flightSpeed / maxFlightSpeed;
8781}
8782
8783
8784- (GLfloat) temperature
8785{
8786 return ship_temperature;
8787}
8788
8789
8790- (void) setTemperature:(GLfloat) value
8791{
8792 ship_temperature = value;
8793}
8794
8795
8796- (float) randomEjectaTemperature
8797{
8798 return [self randomEjectaTemperatureWithMaxFactor:0.99f];
8799}
8800
8801
8802- (float) randomEjectaTemperatureWithMaxFactor:(float)factor
8803{
8804 const float kRange = 0.02f;
8805 factor -= kRange;
8806
8807 float parentTemp = [self temperature];
8808 float adjusted = parentTemp * (bellf(5) * (kRange * 2.0f) - kRange + factor);
8809 if (adjusted > SHIP_MAX_CABIN_TEMP)
8810 {
8811 adjusted = SHIP_MAX_CABIN_TEMP;
8812 }
8813
8814 // Interpolate so that result == parentTemp when parentTemp is SHIP_MIN_CABIN_TEMP
8815 float interp = OOClamp_0_1_f((parentTemp - SHIP_MIN_CABIN_TEMP) / (SHIP_MAX_CABIN_TEMP - SHIP_MIN_CABIN_TEMP));
8816
8817 return OOLerp(SHIP_MIN_CABIN_TEMP, adjusted, interp);
8818}
8819
8820
8821- (GLfloat) heatInsulation
8822{
8823 return _heatInsulation;
8824}
8825
8826
8827- (void) setHeatInsulation:(GLfloat) value
8828{
8829 _heatInsulation = value;
8830}
8831
8832
8833- (int) damage
8834{
8835 return (int)(100 - (100 * energy / maxEnergy));
8836}
8837
8838
8839- (void) dealEnergyDamage:(GLfloat) baseDamage atRange:(GLfloat) range withBias:(GLfloat) velocityBias
8840{
8841 // this is limited to the player's scanner range
8842 GLfloat maxRange = fmin(range * sqrt(baseDamage), SCANNER_MAX_RANGE);
8843
8844 OOLog(@"missile.damage.calc", @"Range: %f | Damage: %f | MaxRange: %f",range,baseDamage,maxRange);
8845
8846 NSArray *targets = [UNIVERSE entitiesWithinRange:maxRange ofEntity:self];
8847 if ([targets count] > 0)
8848 {
8849 unsigned i;
8850 for (i = 0; i < [targets count]; i++)
8851 {
8852 Entity *e2 = [targets objectAtIndex:i];
8853 Vector p2 = [self vectorTo:e2];
8854 double ecr = [e2 collisionRadius];
8855 double d = (magnitude(p2) - ecr) / range;
8856 // base damage within defined range, inverse-square falloff outside
8857 double localDamage = baseDamage;
8858 OOLog(@"missile.damage.calc", @"Base damage: %f",baseDamage);
8859 if (velocityBias > 0)
8860 {
8861 Vector v2 = vector_subtract([self velocity], [e2 velocity]);
8862 double vSign = dot_product(vector_normal([self velocity]), vector_normal(p2));
8863 // vSign should always be positive for the missile's actual target
8864 // but might be negative for other nearby ships which are
8865 // actually moving further away from the missile
8866// double vMag = vSign > 0.0 ? magnitude(v2) : -magnitude(v2);
8867 double vMag = vSign * magnitude(v2);
8868 if (vMag > 1000.0) {
8869 vMag = 1000.0;
8870// cap effective closing speed to 1.0LM or injector-collisions can still do
8871// ridiculous damage
8872 }
8873
8874 localDamage += vMag * velocityBias;
8875 OOLog(@"missile.damage.calc",@"Velocity magnitude + sign: %f , %f",magnitude(v2),vSign);
8876 OOLog(@"missile.damage.calc",@"Velocity magnitude factor: %f",vMag);
8877 OOLog(@"missile.damage.calc",@"Velocity corrected damage: %f",localDamage);
8878 }
8879 double damage = (d > 1) ? localDamage / (d * d) : localDamage;
8880 OOLog(@"missile.damage.calc",@"%f at range %f (d=%f)",damage,magnitude(p2)-ecr,d);
8881 if (damage > 0.0)
8882 {
8883 if ([self owner])
8884 {
8885 [e2 takeEnergyDamage:damage from:self becauseOf:[self owner] weaponIdentifier:[self primaryRole]];
8886 }
8887 else
8888 {
8889 [e2 takeEnergyDamage:damage from:self becauseOf:self weaponIdentifier:[self primaryRole]];
8890 }
8891 }
8892 }
8893 }
8894
8895 /* the actual damage can't go more than S_M_R, so cap the range
8896 * for exploding purposes so that the visual appearance isn't
8897 * larger than that */
8898 if (range > SCANNER_MAX_RANGE / 4.0)
8899 {
8900 range = SCANNER_MAX_RANGE / 4.0;
8901 }
8902 // and a visual sign of the explosion
8903 // "fireball" explosion effect
8904 NSDictionary *explosion = [UNIVERSE explosionSetting:@"oolite-default-ship-explosion"];
8905 [UNIVERSE addEntity:[OOExplosionCloudEntity explosionCloudFromEntity:self withSize:range*3.0 andSettings:explosion]];
8906
8907}
8908
8909
8910// dealEnergyDamage preferred
8911// Exposed to AI
8912- (void) dealEnergyDamageWithinDesiredRange
8913{
8914 OOStandardsDeprecated([NSString stringWithFormat:@"dealEnergyDamageWithinDesiredRange is deprecated for %@",self]);
8915 // not over scannerRange
8916 NSArray* targets = [UNIVERSE entitiesWithinRange:(desired_range < SCANNER_MAX_RANGE ? desired_range : SCANNER_MAX_RANGE) ofEntity:self];
8917 if ([targets count] > 0)
8918 {
8919 unsigned i;
8920 for (i = 0; i < [targets count]; i++)
8921 {
8922 Entity *e2 = [targets objectAtIndex:i];
8923 Vector p2 = [self vectorTo:e2];
8924 double ecr = [e2 collisionRadius];
8925 double d = (magnitude(p2) - ecr) * 2.6; // 2.6 is a correction constant to stay in limits of the old code.
8926 double damage = (d > 0) ? weapon_damage * desired_range / (d * d) : weapon_damage;
8927 [e2 takeEnergyDamage:damage from:self becauseOf:[self owner] weaponIdentifier:[self primaryRole]];
8928 }
8929 }
8930}
8931
8932
8933- (void) dealMomentumWithinDesiredRange:(double)amount
8934{
8935 NSArray* targets = [UNIVERSE entitiesWithinRange:desired_range ofEntity:self];
8936 if ([targets count] > 0)
8937 {
8938 unsigned i;
8939 for (i = 0; i < [targets count]; i++)
8940 {
8941 ShipEntity *e2 = (ShipEntity*)[targets objectAtIndex:i];
8942 if ([e2 isShip] && [e2 isInSpace])
8943 {
8944 Vector p2 = [self vectorTo:e2];
8945 double ecr = [e2 collisionRadius];
8946 double d2 = magnitude2(p2) - ecr * ecr;
8947 // limit momentum transfer to relatively sensible levels
8948 if (d2 < 0.1)
8949 {
8950 d2 = 0.1;
8951 }
8952 double moment = amount*desired_range/d2;
8953 [e2 addImpactMoment:vector_normal(p2) fraction:moment];
8954 }
8955 }
8956 }
8957}
8958
8959
8960- (BOOL) isHulk
8961{
8962 return isHulk;
8963}
8964
8965
8966- (void) setHulk:(BOOL)isNowHulk
8967{
8968 if (![self isSubEntity])
8969 {
8970 isHulk = isNowHulk;
8971 }
8972}
8973
8974
8975- (void) noteTakingDamage:(double)amount from:(Entity *)entity type:(OOShipDamageType)type
8976{
8977 if (amount < 0 || (amount == 0 && [[UNIVERSE gameController] isGamePaused])) return;
8978
8979 JSContext *context = OOJSAcquireContext();
8980
8981 jsval amountVal = JSVAL_VOID;
8982 JS_NewNumberValue(context, amount, &amountVal);
8983 jsval entityVal = OOJSValueFromNativeObject(context, entity);
8984 jsval typeVal = OOJSValueFromShipDamageType(context, type);
8985
8986 ShipScriptEvent(context, self, "shipTakingDamage", amountVal, entityVal, typeVal);
8987 OOJSRelinquishContext(context);
8988
8989 if ([entity isShip]) {
8990// ShipEntity* attacker = (ShipEntity *)entity;
8991 if ([self hasHostileTarget] && accuracy >= COMBAT_AI_IS_SMART && (randf()*10.0 < accuracy || desired_speed < 0.5 * maxFlightSpeed) && behaviour != BEHAVIOUR_EVASIVE_ACTION && behaviour != BEHAVIOUR_FLEE_EVASIVE_ACTION && behaviour != BEHAVIOUR_SCRIPTED_ATTACK_AI)
8992 {
8993 if (behaviour == BEHAVIOUR_FLEE_TARGET)
8994 {
8995// jink should be sufficient to avoid being hit most of the time
8996// if not, this will make a sharp turn and then select a new jink position
8997 behaviour = BEHAVIOUR_FLEE_EVASIVE_ACTION;
8998 }
8999 else
9000 {
9001 behaviour = BEHAVIOUR_EVASIVE_ACTION;
9002 }
9003 frustration = 0.0;
9004 }
9005 }
9006
9007}
9008
9009
9010- (void) noteKilledBy:(Entity *)whom damageType:(OOShipDamageType)type
9011{
9012 if ([self status] == STATUS_DEAD) return;
9013
9014 [PLAYER setScriptTarget:self];
9015
9016 JSContext *context = OOJSAcquireContext();
9017
9018 jsval whomVal = OOJSValueFromNativeObject(context, whom);
9019 jsval typeVal = OOJSValueFromShipDamageType(context, type);
9020 OOEntityStatus originalStatus = [self status];
9021 [self setStatus:STATUS_DEAD];
9022
9023 ShipScriptEvent(context, self, "shipDied", whomVal, typeVal);
9024 if ([whom isShip])
9025 {
9026 jsval selfVal = OOJSValueFromNativeObject(context, self);
9027 ShipScriptEvent(context, (ShipEntity *)whom, "shipKilledOther", selfVal, typeVal);
9028 }
9029
9030 [self setStatus:originalStatus];
9031 OOJSRelinquishContext(context);
9032}
9033
9034
9035- (void) getDestroyedBy:(Entity *)whom damageType:(OOShipDamageType)type
9036{
9037 [self noteKilledBy:whom damageType:type];
9038 [self abortDocking];
9039 [self becomeExplosion];
9040}
9041
9042
9043- (void) rescaleBy:(GLfloat)factor
9044{
9045 [self rescaleBy:factor writeToCache:YES];
9046}
9047
9048
9049- (void) rescaleBy:(GLfloat)factor writeToCache:(BOOL)writeToCache
9050{
9051 _scaleFactor *= factor;
9052 OOMesh *mesh = nil;
9053
9054 NSDictionary *shipDict = [self shipInfoDictionary];
9055 NSString *modelName = [shipDict oo_stringForKey:@"model"];
9056 if (modelName != nil)
9057 {
9058 mesh = [OOMesh meshWithName:modelName
9059 cacheKey:[NSString stringWithFormat:@"%@-%.3f",_shipKey,_scaleFactor]
9060 materialDictionary:[shipDict oo_dictionaryForKey:@"materials"]
9061 shadersDictionary:[shipDict oo_dictionaryForKey:@"shaders"]
9062 smooth:[shipDict oo_boolForKey:@"smooth" defaultValue:NO]
9063 shaderMacros:OODefaultShipShaderMacros()
9065 scaleFactor:factor
9066 cacheWriteable:writeToCache];
9067
9068 if (mesh == nil) return;
9069 [self setMesh:mesh];
9070 }
9071
9072 // rescale subentities
9074 foreach (se, [self subEntities])
9075 {
9076 [se setPosition:HPvector_multiply_scalar([se position], factor)];
9077 [se rescaleBy:factor writeToCache:writeToCache];
9078 }
9079
9080 // rescale mass
9081 mass *= factor * factor * factor;
9082}
9083
9084
9085- (void) releaseCargoPodsDebris
9086{
9087 HPVector xposition = position;
9088 NSUInteger i;
9089 Vector v;
9090 Quaternion q;
9091 int speed_low = 200;
9092
9093 NSArray *jetsam = nil; // this will contain the stuff to get thrown out
9094 unsigned cargo_chance = 70;
9095 jetsam = [NSArray arrayWithArray:cargo]; // what the ship is carrying
9096 [cargo removeAllObjects]; // dispense with it!
9097 unsigned limit = 15;
9098 // Throw out cargo
9099 NSUInteger n_jetsam = [jetsam count];
9100
9101 for (i = 0; i < n_jetsam; i++)
9102 {
9103 if (Ranrot() % 100 < cargo_chance) // chance of any given piece of cargo surviving decompression
9104 {
9105 // a higher chance of getting at least a couple of bits of cargo out
9106 if (cargo_chance > 10)
9107 {
9108 if (EXPECT_NOT([self isPlayer]))
9109 {
9110 cargo_chance -= 20;
9111 }
9112 else
9113 {
9114 cargo_chance -= 30;
9115 }
9116 }
9117 limit--;
9118 ShipEntity* cargoObj = [jetsam objectAtIndex:i];
9119 ShipEntity* container = [UNIVERSE reifyCargoPod:cargoObj];
9120 /* TODO: this debris position/velocity setting code is
9121 * duplicated - sometimes not very cleanly - all over the
9122 * place. Unify to a single function - CIM */
9123 HPVector rpos = xposition;
9124 Vector rrand = OORandomPositionInBoundingBox(boundingBox);
9125 rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z;
9126 rpos.x += (ranrot_rand() % 7) - 3;
9127 rpos.y += (ranrot_rand() % 7) - 3;
9128 rpos.z += (ranrot_rand() % 7) - 3;
9129 [container setPosition:rpos];
9130 v.x = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9131 v.y = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9132 v.z = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9133 [container setVelocity:vector_add(v,[self velocity])];
9135 [container setOrientation:q];
9136
9137 [container setTemperature:[self randomEjectaTemperature]];
9138 [container setScanClass: CLASS_CARGO];
9139 [UNIVERSE addEntity:container]; // STATUS_IN_FLIGHT, AI state GLOBAL
9140
9141 AI *containerAI = [container getAI];
9142 if ([containerAI hasSuspendedStateMachines]) // check if new or recycled cargo.
9143 {
9144 [containerAI exitStateMachineWithMessage:nil];
9145 [container setThrust:[container maxThrust]]; // restore old value. Was set to zero on previous scooping.
9146 [container setOwner:container];
9147 }
9148 }
9149 if (limit <= 0)
9150 {
9151 break; // even really big ships won't have too much cargo survive an explosion
9152 }
9153 }
9154
9155}
9156
9157
9158- (void) setIsWreckage:(BOOL)isw
9159{
9160 isWreckage = isw;
9161}
9162
9163
9164- (BOOL) showDamage
9165{
9166 return _showDamage;
9167}
9168
9169
9170- (void) becomeExplosion
9171{
9172
9173 // check if we're destroying a subentity
9174 ShipEntity *parent = [self parentEntity];
9175 if (parent != nil)
9176 {
9177 ShipEntity *this_ship = [self retain];
9178 HPVector this_pos = [self absolutePositionForSubentity];
9179
9180 // remove this ship from its parent's subentity list
9181 [parent subEntityDied:self];
9182 [UNIVERSE addEntity:this_ship];
9183 [this_ship setPosition:this_pos];
9184 [this_ship release];
9185 if ([parent isPlayer])
9186 {
9187 // make the parent ship less reliable.
9188 [(PlayerEntity *)parent adjustTradeInFactorBy:-PLAYER_SHIP_SUBENTITY_TRADE_IN_VALUE];
9189 }
9190 }
9191
9192 HPVector xposition = position;
9193 NSUInteger i;
9194 Vector v;
9195 Quaternion q;
9196 int speed_low = 200;
9197 GLfloat n_alloys = sqrtf(sqrtf(mass / 6000.0f));
9198 NSUInteger numAlloys = 0;
9199 BOOL canReleaseSubWreckage = isWreckage && ([UNIVERSE detailLevel] >= DETAIL_LEVEL_EXTRAS);
9200
9201 if ([self status] == STATUS_DEAD)
9202 {
9203 [UNIVERSE removeEntity:self];
9204 return;
9205 }
9206 [self setStatus:STATUS_DEAD];
9207
9208 @try
9209 {
9210 if ([self isThargoid] && [roleSet hasRole:@"thargoid-mothership"]) [self broadcastThargoidDestroyed];
9211
9212 if (!suppressExplosion && ([self isVisible] || HPdistance2([self position], [PLAYER position]) < SCANNER_MAX_RANGE2))
9213 {
9214 if (!isWreckage && mass > 500000.0f && randf() < 0.25f) // big!
9215 {
9216 // draw an expanding ring
9218 [ring setVelocity:vector_multiply_scalar([self velocity], 0.25f)];
9219 [UNIVERSE addEntity:ring];
9220 }
9221
9222 BOOL add_debris = (UNIVERSE->n_entities < 0.95 * UNIVERSE_MAX_ENTITIES) &&
9223 ([UNIVERSE getTimeDelta] < 0.125); // FPS > 8
9224
9225
9226 // There are several parts to explosions, show only the main
9227 // explosion effect if UNIVERSE is almost full.
9228
9229 if (add_debris)
9230 {
9231 if ([UNIVERSE reducedDetail])
9232 {
9233 // Quick explosion effects for reduced detail mode
9234
9235 // 1. fast sparks
9236 [UNIVERSE addEntity:[OOSmallFragmentBurstEntity fragmentBurstFromEntity:self]];
9237 // 2. slow clouds
9238 [UNIVERSE addEntity:[OOBigFragmentBurstEntity fragmentBurstFromEntity:self]];
9239 // 3. flash
9240 [UNIVERSE addEntity:[OOFlashEffectEntity explosionFlashFromEntity:self]];
9241 /* This mode used to be the default for
9242 * cargo/munitions but this now must be explicitly
9243 * specified. */
9244 }
9245 else
9246 {
9247 NSString *explosionKey = @"oolite-default-ship-explosion";
9248 NSDictionary *explosion = nil;
9249 if (explosionType == nil)
9250 {
9251 explosion = [UNIVERSE explosionSetting:explosionKey];
9252 [UNIVERSE addEntity:[OOExplosionCloudEntity explosionCloudFromEntity:self withSettings:explosion]];
9253 // 3. flash
9254 [UNIVERSE addEntity:[OOFlashEffectEntity explosionFlashFromEntity:self]];
9255 }
9256 for (NSUInteger i=0;i<[explosionType count];i++)
9257 {
9258 explosionKey = [explosionType oo_stringAtIndex:i defaultValue:nil];
9259 if (explosionKey != nil)
9260 {
9261 // three special-case builtins
9262 if ([explosionKey isEqualToString:@"oolite-builtin-flash"])
9263 {
9264 [UNIVERSE addEntity:[OOFlashEffectEntity explosionFlashFromEntity:self]];
9265 }
9266 else if ([explosionKey isEqualToString:@"oolite-builtin-slowcloud"])
9267 {
9268 [UNIVERSE addEntity:[OOBigFragmentBurstEntity fragmentBurstFromEntity:self]];
9269 }
9270 else if ([explosionKey isEqualToString:@"oolite-builtin-fastspark"])
9271 {
9272 [UNIVERSE addEntity:[OOSmallFragmentBurstEntity fragmentBurstFromEntity:self]];
9273 }
9274 else
9275 {
9276 explosion = [UNIVERSE explosionSetting:explosionKey];
9277 [UNIVERSE addEntity:[OOExplosionCloudEntity explosionCloudFromEntity:self withSettings:explosion]];
9278 }
9279 }
9280 }
9281 // "fireball" explosion effect
9282
9283 }
9284 }
9285
9286 // If UNIVERSE is nearing limit for entities don't add to it!
9287 if (add_debris)
9288 {
9289 // we need to throw out cargo at this point.
9290 [self releaseCargoPodsDebris];
9291
9292 // Throw out rocks and alloys to be scooped up
9293 if ([self hasRole:@"asteroid"] || [self isBoulder])
9294 {
9295 if (!noRocks && (being_mined || randf() < 0.20))
9296 {
9297 NSString *defaultRole = @"boulder";
9298 float defaultSpeed = 50.0;
9299 if ([self isBoulder])
9300 {
9301 defaultRole = @"splinter";
9302 defaultSpeed = 20.0;
9303 if (likely_cargo == 0)
9304 {
9305 likely_cargo = 4; // compatibility with older boulders
9306 }
9307 }
9308 else if ([[self primaryAggressor] isPlayer])
9309 {
9310 [PLAYER addRoleForMining];
9311 }
9312 NSUInteger n_rocks = 2 + (Ranrot() % (likely_cargo + 1));
9313
9314 NSString *debrisRole = [[self shipInfoDictionary] oo_stringForKey:@"debris_role" defaultValue:defaultRole];
9315 for (i = 0; i < n_rocks; i++)
9316 {
9317 ShipEntity* rock = [UNIVERSE newShipWithRole:debrisRole]; // retain count = 1
9318 if (rock)
9319 {
9320 float r_speed = [rock maxFlightSpeed] > 0 ? 2.0 * [rock maxFlightSpeed] : defaultSpeed;
9321 float cr = (collision_radius < rock->collision_radius) ? collision_radius : 2 * rock->collision_radius;
9322 v.x = ((randf() * r_speed) - r_speed / 2);
9323 v.y = ((randf() * r_speed) - r_speed / 2);
9324 v.z = ((randf() * r_speed) - r_speed / 2);
9325 [rock setVelocity:vector_add(v,[self velocity])];
9326 HPVector rpos = HPvector_add(xposition,vectorToHPVector(vector_multiply_scalar(vector_normal(v),cr)));
9327 [rock setPosition:rpos];
9328
9330 [rock setOrientation:q];
9331
9332 [rock setTemperature:[self randomEjectaTemperature]];
9333 if ([self isBoulder])
9334 {
9335 [rock setScanClass: CLASS_CARGO];
9336 [rock setBounty: 0 withReason:kOOLegalStatusReasonSetup];
9337 // only make the rock have minerals if something isn't already defined for the rock
9338 if ([[rock shipInfoDictionary] oo_stringForKey:@"cargo_carried"] == nil)
9339 [rock setCommodity:@"minerals" andAmount: 1];
9340 }
9341 else
9342 {
9343 [rock setScanClass:CLASS_ROCK];
9344 [rock setIsBoulder:YES];
9345 }
9346 [UNIVERSE addEntity:rock]; // STATUS_IN_FLIGHT, AI state GLOBAL
9347 [rock release];
9348 }
9349 }
9350 }
9351 return;
9352 }
9353
9354 // throw out burning chunks of wreckage
9355 //
9356 if ((n_alloys && canFragment) || canReleaseSubWreckage)
9357 {
9358 NSUInteger n_wreckage = 0;
9359
9360 if (UNIVERSE->n_entities < 0.50 * UNIVERSE_MAX_ENTITIES)
9361 {
9362 // Create wreckage only when UNIVERSE is less than half full.
9363 // (condition set in r906 - was < 0.75 before) --Kaks 2011.10.17
9364 NSUInteger maxWrecks = 3;
9365 if (n_alloys == 0)
9366 {
9367 // must be sub-wreckage here
9368 n_wreckage = (mass > 600.0 && randf() < 0.2)?2:0;
9369 }
9370 else
9371 {
9372 n_wreckage = (n_alloys < maxWrecks)? floorf(randf()*(n_alloys+2)) : maxWrecks;
9373 }
9374 }
9375
9376 for (i = 0; i < n_wreckage; i++)
9377 {
9378 Vector r1 = [octree randomPoint];
9379 Vector dir = quaternion_rotate_vector([self normalOrientation], r1);
9380 HPVector rpos = HPvector_add(vectorToHPVector(dir), xposition);
9381 GLfloat lifetime = 750.0 * randf() + 250.0 * i + 100.0;
9382 ShipEntity *wreck = [UNIVERSE addWreckageFrom:self withRole:@"wreckage" at:rpos scale:1.0 lifetime:lifetime/2];
9383
9384 [wreck setVelocity:vector_add([wreck velocity],vector_multiply_scalar(vector_normal(dir),randf()*[wreck collisionRadius]))];
9385
9386 }
9387 n_alloys = randf() * n_alloys;
9388 }
9389 }
9390
9391 if (!canFragment)
9392 {
9393 n_alloys = 0.0;
9394 }
9395 // If UNIVERSE is almost full, don't create more than 1 piece of scrap metal.
9396 else if (!add_debris)
9397 {
9398 n_alloys = (n_alloys > 1.0) ? 1.0 : 0.0;
9399 }
9400
9401 // now convert to uint
9402 numAlloys = floorf(n_alloys);
9403
9404 // Throw out scrap metal
9405 //
9406 for (i = 0; i < numAlloys; i++)
9407 {
9408 ShipEntity* plate = [UNIVERSE newShipWithRole:@"alloy"]; // retain count = 1
9409 if (plate)
9410 {
9411 HPVector rpos = xposition;
9412 Vector rrand = OORandomPositionInBoundingBox(boundingBox);
9413 rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z;
9414 rpos.x += (ranrot_rand() % 7) - 3;
9415 rpos.y += (ranrot_rand() % 7) - 3;
9416 rpos.z += (ranrot_rand() % 7) - 3;
9417 [plate setPosition:rpos];
9418 v.x = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9419 v.y = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9420 v.z = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9421 [plate setVelocity:vector_add(v,[self velocity])];
9423 [plate setOrientation:q];
9424
9425 [plate setTemperature:[self randomEjectaTemperature]];
9426 [plate setScanClass: CLASS_CARGO];
9427 [plate setCommodity:@"alloys" andAmount:1];
9428 [UNIVERSE addEntity:plate]; // STATUS_IN_FLIGHT, AI state GLOBAL
9429
9430 [plate release];
9431 }
9432 }
9433 }
9434
9435 // Explode subentities.
9436 NSEnumerator *subEnum = nil;
9437 ShipEntity *se = nil;
9438 for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); )
9439 {
9440 [se setSuppressExplosion:suppressExplosion];
9441 [se becomeExplosion];
9442 }
9443 [self clearSubEntities];
9444
9445 // momentum from explosions
9446 if (!suppressExplosion)
9447 {
9448 desired_range = collision_radius * 2.5f;
9449 [self dealMomentumWithinDesiredRange:0.125f * mass];
9450 }
9451
9452 if (self != PLAYER) // was if !isPlayer - but I think this may cause ghosts (Who's "I"? -- Ahruman)
9453 {
9454 if (isPlayer)
9455 {
9456 #ifndef NDEBUG
9457 OOLog(@"becomeExplosion.suspectedGhost.confirm", @"%@", @"Ship spotted with isPlayer set when not actually the player.");
9458 #endif
9459 isPlayer = NO;
9460 }
9461 }
9462 }
9463 @finally
9464 {
9465 if (self != PLAYER)
9466 {
9467 [UNIVERSE removeEntity:self];
9468 }
9469 }
9470}
9471
9472
9473// Exposed to AI
9474- (void) becomeEnergyBlast
9475{
9476 [UNIVERSE addEntity:[OOQuiriumCascadeEntity quiriumCascadeFromShip:self]];
9477 [self broadcastEnergyBlastImminent];
9478 [self noteKilledBy:nil damageType:kOODamageTypeCascadeWeapon];
9479 [UNIVERSE removeEntity:self];
9480}
9481
9482
9483// Exposed to AI
9484- (void) broadcastEnergyBlastImminent
9485{
9486 // anyone further away than typical scanner range probably doesn't need to hear
9487 NSArray* targets = [UNIVERSE entitiesWithinRange:SCANNER_MAX_RANGE ofEntity:self];
9488 if ([targets count] > 0)
9489 {
9490 unsigned i;
9491 for (i = 0; i < [targets count]; i++)
9492 {
9493 Entity *e2 = [targets objectAtIndex:i];
9494 if ([e2 isShip])
9495 {
9496 ShipEntity *se = (ShipEntity *)e2;
9497 [se setFoundTarget:self];
9498 [se reactToAIMessage:@"CASCADE_WEAPON_DETECTED" context:@"nearby Q-mine"];
9499 [se doScriptEvent:OOJSID("cascadeWeaponDetected") withArgument:self];
9500 }
9501 }
9502 }
9503}
9504
9505
9506- (void) removeExhaust:(OOExhaustPlumeEntity *)exhaust
9507{
9508 [subEntities removeObject:exhaust];
9509 [exhaust setOwner:nil];
9510}
9511
9512
9513- (void) removeFlasher:(OOFlasherEntity *)flasher
9514{
9515 [subEntities removeObject:flasher];
9516 [flasher setOwner:nil];
9517}
9518
9519
9520- (void)subEntityDied:(ShipEntity *)sub
9521{
9522 if ([self subEntityTakingDamage] == sub) [self setSubEntityTakingDamage:nil];
9523
9524 [sub setOwner:nil];
9525 // TODO? Recalculating collision radius should increase collision testing efficiency,
9526 // but for most ship models the difference would be marginal. -- Kaks 20110429
9527 mass -= [sub mass]; // missing subents affect fuel charge rate, etc..
9528 [subEntities removeObject:sub];
9529}
9530
9531
9532- (void)subEntityReallyDied:(ShipEntity *)sub
9533{
9534 if ([self subEntityTakingDamage] == sub) [self setSubEntityTakingDamage:nil];
9535
9536 if ([self hasSubEntity:sub])
9537 {
9538 OOLogERR(@"shipEntity.bug.subEntityRetainUnderflow", @"Subentity of %@ died while still in subentity list! This is bad. Leaking subentity list to avoid crash. %@", self, @"This is an internal error, please report it.");
9539
9540 // Leak subentity list.
9541 subEntities = nil;
9542 }
9543}
9544
9545
9546- (Vector) positionOffsetForAlignment:(NSString*) align
9547{
9548 NSString* padAlign = [NSString stringWithFormat:@"%@---", align];
9549 Vector result = kZeroVector;
9550 switch ([padAlign characterAtIndex:0])
9551 {
9552 case (unichar)'c':
9553 case (unichar)'C':
9554 result.x = 0.5 * (boundingBox.min.x + boundingBox.max.x);
9555 break;
9556 case (unichar)'M':
9557 result.x = boundingBox.max.x;
9558 break;
9559 case (unichar)'m':
9560 result.x = boundingBox.min.x;
9561 break;
9562 }
9563 switch ([padAlign characterAtIndex:1])
9564 {
9565 case (unichar)'c':
9566 case (unichar)'C':
9567 result.y = 0.5 * (boundingBox.min.y + boundingBox.max.y);
9568 break;
9569 case (unichar)'M':
9570 result.y = boundingBox.max.y;
9571 break;
9572 case (unichar)'m':
9573 result.y = boundingBox.min.y;
9574 break;
9575 }
9576 switch ([padAlign characterAtIndex:2])
9577 {
9578 case (unichar)'c':
9579 case (unichar)'C':
9580 result.z = 0.5 * (boundingBox.min.z + boundingBox.max.z);
9581 break;
9582 case (unichar)'M':
9583 result.z = boundingBox.max.z;
9584 break;
9585 case (unichar)'m':
9586 result.z = boundingBox.min.z;
9587 break;
9588 }
9589 return result;
9590}
9591
9592
9593Vector positionOffsetForShipInRotationToAlignment(ShipEntity* ship, Quaternion q, NSString* align)
9594{
9595 NSString* padAlign = [NSString stringWithFormat:@"%@---", align];
9596 Vector i = vector_right_from_quaternion(q);
9597 Vector j = vector_up_from_quaternion(q);
9598 Vector k = vector_forward_from_quaternion(q);
9599 BoundingBox arbb = [ship findBoundingBoxRelativeToPosition:kZeroHPVector InVectors:i :j :k];
9600 Vector result = kZeroVector;
9601 switch ([padAlign characterAtIndex:0])
9602 {
9603 case (unichar)'c':
9604 case (unichar)'C':
9605 result.x = 0.5 * (arbb.min.x + arbb.max.x);
9606 break;
9607 case (unichar)'M':
9608 result.x = arbb.max.x;
9609 break;
9610 case (unichar)'m':
9611 result.x = arbb.min.x;
9612 break;
9613 }
9614 switch ([padAlign characterAtIndex:1])
9615 {
9616 case (unichar)'c':
9617 case (unichar)'C':
9618 result.y = 0.5 * (arbb.min.y + arbb.max.y);
9619 break;
9620 case (unichar)'M':
9621 result.y = arbb.max.y;
9622 break;
9623 case (unichar)'m':
9624 result.y = arbb.min.y;
9625 break;
9626 }
9627 switch ([padAlign characterAtIndex:2])
9628 {
9629 case (unichar)'c':
9630 case (unichar)'C':
9631 result.z = 0.5 * (arbb.min.z + arbb.max.z);
9632 break;
9633 case (unichar)'M':
9634 result.z = arbb.max.z;
9635 break;
9636 case (unichar)'m':
9637 result.z = arbb.min.z;
9638 break;
9639 }
9640 return result;
9641}
9642
9643
9644- (void) becomeLargeExplosion:(double)factor
9645{
9646
9647 if ([self status] == STATUS_DEAD) return;
9648 [self setStatus:STATUS_DEAD];
9649
9650 @try
9651 {
9652 // two parts to the explosion:
9653 // 1. fast sparks
9654 float how_many = factor;
9655 while (how_many > 0.5f)
9656 {
9657 [UNIVERSE addEntity:[OOSmallFragmentBurstEntity fragmentBurstFromEntity:self]];
9658 how_many -= 1.0f;
9659 }
9660 // 2. slow clouds
9661 how_many = factor;
9662 while (how_many > 0.5f)
9663 {
9664 [UNIVERSE addEntity:[OOBigFragmentBurstEntity fragmentBurstFromEntity:self]];
9665 how_many -= 1.0f;
9666 }
9667
9668 [self releaseCargoPodsDebris];
9669
9670 NSEnumerator *subEnum = nil;
9671 ShipEntity *se = nil;
9672 for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); )
9673 {
9674 [se setSuppressExplosion:suppressExplosion];
9675 [se becomeExplosion];
9676 }
9677 [self clearSubEntities];
9678
9679 }
9680 @finally
9681 {
9682 if (!isPlayer) [UNIVERSE removeEntity:self];
9683 }
9684}
9685
9686
9687- (void) collectBountyFor:(ShipEntity *)other
9688{
9689 if ([other isPolice]) // oops, we shot a copper!
9690 {
9691 [self markAsOffender:64 withReason:kOOLegalStatusReasonAttackedPolice];
9692 }
9693}
9694
9695
9696- (NSComparisonResult) compareBeaconCodeWith:(Entity<OOBeaconEntity> *) other
9697{
9698 return [[self beaconCode] compare:[other beaconCode] options: NSCaseInsensitiveSearch];
9699}
9700
9701
9702// for shaders, equivalent to 1.76's NPC laserHeatLevel
9703- (GLfloat) weaponRecoveryTime
9704{
9705 float result = (weapon_recharge_rate - [self shotTime]) / weapon_recharge_rate;
9706 return OOClamp_0_1_f(result);
9707}
9708
9709
9710- (GLfloat)laserHeatLevel
9711{
9712 GLfloat result = weapon_temp / NPC_MAX_WEAPON_TEMP;
9713 return OOClamp_0_1_f(result);
9714}
9715
9716
9717- (GLfloat)laserHeatLevelAft
9718{
9719 GLfloat result = aft_weapon_temp / NPC_MAX_WEAPON_TEMP;
9720 return OOClamp_0_1_f(result);
9721}
9722
9723
9724- (GLfloat)laserHeatLevelForward
9725{
9726 GLfloat result = forward_weapon_temp / NPC_MAX_WEAPON_TEMP;
9727 if (isWeaponNone(forward_weapon_type))
9728 { // must check subents
9729 OOWeaponType forward_weapon_real_type = nil;
9730 NSEnumerator *subEnum = [self shipSubEntityEnumerator];
9731 ShipEntity *se = nil;
9732 while (isWeaponNone(forward_weapon_real_type) && (se = [subEnum nextObject]))
9733 {
9735 {
9736 forward_weapon_real_type = se->forward_weapon_type;
9738 }
9739 }
9740 }
9741 return OOClamp_0_1_f(result);
9742}
9743
9744
9745- (GLfloat)laserHeatLevelPort
9746{
9747 GLfloat result = port_weapon_temp / NPC_MAX_WEAPON_TEMP;
9748 return OOClamp_0_1_f(result);
9749}
9750
9751
9752- (GLfloat)laserHeatLevelStarboard
9753{
9754 GLfloat result = starboard_weapon_temp / NPC_MAX_WEAPON_TEMP;
9755 return OOClamp_0_1_f(result);
9756}
9757
9758
9759- (GLfloat)hullHeatLevel
9760{
9761 GLfloat result = (GLfloat)ship_temperature / (GLfloat)SHIP_MAX_CABIN_TEMP;
9762 return OOClamp_0_1_f(result);
9763}
9764
9765
9766- (GLfloat)entityPersonality
9767{
9768 return entity_personality / (float)ENTITY_PERSONALITY_MAX;
9769}
9770
9771
9772- (GLint)entityPersonalityInt
9773{
9774 return entity_personality;
9775}
9776
9777
9778- (uint32_t) randomSeedForShaders
9779{
9780 return entity_personality * 0x00010001;
9781}
9782
9783
9784- (void) setEntityPersonalityInt:(uint16_t)value
9785{
9786 if (value <= ENTITY_PERSONALITY_MAX)
9787 {
9788 entity_personality = value;
9789 [[self mesh] rebindMaterials];
9790 }
9791}
9792
9793
9794- (void)setSuppressExplosion:(BOOL)suppress
9795{
9796 suppressExplosion = !!suppress;
9797}
9798
9799
9800- (void) resetExhaustPlumes
9801{
9802 NSEnumerator *exEnum = nil;
9803 OOExhaustPlumeEntity *exEnt = nil;
9804
9805 for (exEnum = [self exhaustEnumerator]; (exEnt = [exEnum nextObject]); )
9806 {
9807 [exEnt resetPlume];
9808 }
9809}
9810
9811
9812/*-----------------------------------------
9813
9814 AI piloting methods
9815
9816-----------------------------------------*/
9817
9818
9819- (void) checkScanner
9820{
9821 Entity* scan;
9822 n_scanned_ships = 0;
9823 //
9824 scan = z_previous; while ((scan)&&(scan->isShip == NO)) scan = scan->z_previous; // skip non-ships
9825 GLfloat scannerRange2 = scannerRange * scannerRange;
9826 while ((scan)&&(scan->position.z > position.z - scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
9827 {
9828 // can't scan cloaked ships
9829 if (scan->isShip && ![(ShipEntity*)scan isCloaked] && [self isValidTarget:scan])
9830 {
9831 distance2_scanned_ships[n_scanned_ships] = HPdistance2(position, scan->position);
9832 if (distance2_scanned_ships[n_scanned_ships] < scannerRange2)
9833 scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
9834 }
9835 scan = scan->z_previous; while ((scan)&&(scan->isShip == NO)) scan = scan->z_previous;
9836 }
9837 //
9838 scan = z_next; while ((scan)&&(scan->isShip == NO)) scan = scan->z_next; // skip non-ships
9839 while ((scan)&&(scan->position.z < position.z + scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
9840 {
9841 if (scan->isShip && ![(ShipEntity*)scan isCloaked] && [self isValidTarget:scan])
9842 {
9843 distance2_scanned_ships[n_scanned_ships] = HPdistance2(position, scan->position);
9844 if (distance2_scanned_ships[n_scanned_ships] < scannerRange2)
9845 scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
9846 }
9847 scan = scan->z_next; while ((scan)&&(scan->isShip == NO)) scan = scan->z_next; // skip non-ships
9848 }
9849 //
9850 scanned_ships[n_scanned_ships] = nil; // terminate array
9851}
9852
9853
9854- (void) checkScannerIgnoringUnpowered
9855{
9856 Entity* scan;
9857 n_scanned_ships = 0;
9858 //
9859 GLfloat scannerRange2 = scannerRange * scannerRange;
9860 scan = z_previous;
9861 while ((scan)&&((scan->isShip == NO)||(scan->scanClass==CLASS_ROCK)||(scan->scanClass==CLASS_CARGO)))
9862 {
9863 scan = scan->z_previous; // skip non-ships
9864 }
9865 while ((scan)&&(scan->position.z > position.z - scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
9866 {
9867 if (scan->isShip && ![(ShipEntity*)scan isCloaked])
9868 {
9869 distance2_scanned_ships[n_scanned_ships] = HPdistance2(position, scan->position);
9870 if (distance2_scanned_ships[n_scanned_ships] < scannerRange2)
9871 scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
9872 }
9873 scan = scan->z_previous;
9874 while ((scan)&&((scan->isShip == NO)||(scan->scanClass==CLASS_ROCK)||(scan->scanClass==CLASS_CARGO)))
9875 {
9876 scan = scan->z_previous; // skip non-ships
9877 }
9878 }
9879 //
9880 scan = z_next;
9881 while ((scan)&&((scan->isShip == NO)||(scan->scanClass==CLASS_ROCK)||(scan->scanClass==CLASS_CARGO)))
9882 {
9883 scan = scan->z_next; // skip non-ships
9884 }
9885
9886 while ((scan)&&(scan->position.z < position.z + scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
9887 {
9888 if (scan->isShip && ![(ShipEntity*)scan isCloaked])
9889 {
9890 distance2_scanned_ships[n_scanned_ships] = HPdistance2(position, scan->position);
9891 if (distance2_scanned_ships[n_scanned_ships] < scannerRange2)
9892 scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
9893 }
9894 scan = scan->z_next;
9895 while ((scan)&&((scan->isShip == NO)||(scan->scanClass==CLASS_ROCK)||(scan->scanClass==CLASS_CARGO)))
9896 {
9897 scan = scan->z_next; // skip non-ships
9898 }
9899 }
9900 //
9901 scanned_ships[n_scanned_ships] = nil; // terminate array
9902}
9903
9904
9905- (ShipEntity**) scannedShips
9906{
9907 scanned_ships[n_scanned_ships] = nil; // terminate array
9908 return scanned_ships;
9909}
9910
9911
9912- (int) numberOfScannedShips
9913{
9914 return n_scanned_ships;
9915}
9916
9917
9918- (Entity *) foundTarget
9919{
9920 Entity *result = [_foundTarget weakRefUnderlyingObject];
9921 if (result == nil || ![self isValidTarget:result])
9922 {
9923 DESTROY(_foundTarget);
9924 return nil;
9925 }
9926 return result;
9927}
9928
9929
9930- (void) setFoundTarget:(Entity *) targetEntity
9931{
9932 [_foundTarget release];
9933 _foundTarget = [targetEntity weakRetain];
9934}
9935
9936
9937- (Entity *) primaryAggressor
9938{
9939 Entity *result = [_primaryAggressor weakRefUnderlyingObject];
9940 if (result == nil || ![self isValidTarget:result])
9941 {
9942 DESTROY(_primaryAggressor);
9943 return nil;
9944 }
9945 return result;
9946}
9947
9948
9949- (void) setPrimaryAggressor:(Entity *) targetEntity
9950{
9951 [_primaryAggressor release];
9952 _primaryAggressor = [targetEntity weakRetain];
9953}
9954
9955
9956- (Entity *) lastEscortTarget
9957{
9958 Entity *result = [_lastEscortTarget weakRefUnderlyingObject];
9959 if (result == nil || ![self isValidTarget:result])
9960 {
9961 DESTROY(_lastEscortTarget);
9962 return nil;
9963 }
9964 return result;
9965}
9966
9967
9968- (void) setLastEscortTarget:(Entity *) targetEntity
9969{
9970 [_lastEscortTarget release];
9971 _lastEscortTarget = [targetEntity weakRetain];
9972}
9973
9974
9975- (Entity *) thankedShip
9976{
9977 Entity *result = [_thankedShip weakRefUnderlyingObject];
9978 if (result == nil || ![self isValidTarget:result])
9979 {
9980 DESTROY(_thankedShip);
9981 return nil;
9982 }
9983 return result;
9984}
9985
9986
9987- (void) setThankedShip:(Entity *) targetEntity
9988{
9989 [_thankedShip release];
9990 _thankedShip = [targetEntity weakRetain];
9991}
9992
9993
9994- (Entity *) rememberedShip
9995{
9996 Entity *result = [_rememberedShip weakRefUnderlyingObject];
9997 if (result == nil || ![self isValidTarget:result])
9998 {
9999 DESTROY(_rememberedShip);
10000 return nil;
10001 }
10002 return result;
10003}
10004
10005
10006- (void) setRememberedShip:(Entity *) targetEntity
10007{
10008 [_rememberedShip release];
10009 _rememberedShip = [targetEntity weakRetain];
10010}
10011
10012
10013- (StationEntity *) targetStation
10014{
10015 StationEntity *result = [_targetStation weakRefUnderlyingObject];
10016 if (result == nil || ![self isValidTarget:result])
10017 {
10018 DESTROY(_targetStation);
10019 return nil;
10020 }
10021 return result;
10022}
10023
10024
10025- (void) setTargetStation:(Entity *) targetEntity
10026{
10027 [_targetStation release];
10028 _targetStation = [targetEntity weakRetain];
10029}
10030
10031/* Now we use weakrefs rather than universal ID this function checks
10032 * for targets which may have a valid reference but are not currently
10033 * targetable. */
10034- (BOOL) isValidTarget:(Entity *)target
10035{
10036 if (target == nil)
10037 {
10038 return NO;
10039 }
10040 if ([target isShip])
10041 {
10042 OOEntityStatus tstatus = [target status];
10043 if (tstatus == STATUS_ENTERING_WITCHSPACE || tstatus == STATUS_IN_HOLD || tstatus == STATUS_DOCKED || tstatus == STATUS_DEAD)
10044 // 2013-01-13, Eric: added STATUS_DEAD because I keep seeing ships locked on dead ships in attack mode.
10045 {
10046 return NO;
10047 }
10048 return YES;
10049 }
10050 if ([target isWormhole] && [target scanClass] != CLASS_NO_DRAW)
10051 {
10052 return YES;
10053 }
10054 return NO;
10055}
10056
10057
10058- (void) addTarget:(Entity *) targetEntity
10059{
10060 if (targetEntity == self) return;
10061 if (targetEntity != nil)
10062 {
10063 DESTROY(_primaryTarget);
10064 _primaryTarget = [targetEntity weakRetain];
10065 [self startTrackingCurve];
10066 }
10067
10068 [[self shipSubEntityEnumerator] makeObjectsPerformSelector:@selector(addTarget:) withObject:targetEntity];
10069 if (![self isSubEntity]) [self doScriptEvent:OOJSID("shipTargetAcquired") withArgument:targetEntity];
10070}
10071
10072
10073- (void) removeTarget:(Entity *) targetEntity
10074{
10075 if(targetEntity != nil) [self noteLostTarget];
10076 else DESTROY(_primaryTarget);
10077 // targetEntity == nil is currently only true for mounted player missiles.
10078 // we don't want to send lostTarget messages while the missile is mounted.
10079
10080 [[self shipSubEntityEnumerator] makeObjectsPerformSelector:@selector(removeTarget:) withObject:targetEntity];
10081}
10082
10083
10084/* Checks if the primary target is still trackable.
10085 *
10086 * 1.80 behaviour: still exists, is in scanner range, is not cloaked
10087 *
10088 * 1.81 (planned) behaviour:
10089 * - track cloaked ships once primary targeted (but no missiles, and can't keep as a mere defense target, and probably some other penalties)
10090 * - track ships at 120% scanner range *if* they are also primary aggressor
10091 *
10092 * But first, just switch over to this method on 1.80 behaviour and check
10093 * that things still work.
10094 */
10095- (BOOL) canStillTrackPrimaryTarget
10096{
10097 Entity *target = (Entity *)[self primaryTargetWithoutValidityCheck];
10098 if (target == nil)
10099 {
10100 return NO;
10101 }
10102 if (![self isValidTarget:target])
10103 {
10104 return NO;
10105 }
10106 double range2 = HPmagnitude2(HPvector_subtract([target position], position));
10107 if (range2 > scannerRange * scannerRange * 1.5625)
10108 {
10109 // 1.5625 = 1.25*1.25
10110 return NO;
10111 }
10112 // 1.81: can retain cloaked ships as a *primary* target now
10113/* if ([target isShip] && [(ShipEntity*)target isCloaked])
10114 {
10115 return NO;
10116 } */
10117 return YES;
10118}
10119
10120
10121- (id) primaryTarget
10122{
10123 id result = [_primaryTarget weakRefUnderlyingObject];
10124 if ((result == nil && _primaryTarget != nil)
10125 || ![self isValidTarget:result])
10126 {
10127 DESTROY(_primaryTarget);
10128 return nil;
10129 }
10130 else if (EXPECT_NOT(result == self))
10131 {
10132 /* Added in response to a crash report showing recursion in
10133 [PlayerEntity hasHostileTarget].
10134 -- Ahruman 2009-12-17
10135 */
10136 DESTROY(_primaryTarget);
10137 }
10138 return result;
10139}
10140
10141
10142// used when we need to check the target - perhaps for a potential
10143// noteTargetLost - without invalidating the target first
10144- (id) primaryTargetWithoutValidityCheck
10145{
10146 id result = [_primaryTarget weakRefUnderlyingObject];
10147 if (EXPECT_NOT(result == self))
10148 {
10149 // just in case
10150 DESTROY(_primaryTarget);
10151 return nil;
10152 }
10153 return result;
10154}
10155
10156
10157- (BOOL) isFriendlyTo:(ShipEntity *)otherShip
10158{
10159 BOOL isFriendly = NO;
10160 OOShipGroup *myGroup = [self group];
10161 OOShipGroup *otherGroup = [otherShip group];
10162
10163 if ((otherShip == self) ||
10164 ([self isPolice] && [otherShip isPolice]) ||
10165 ([self isThargoid] && [otherShip isThargoid]) ||
10166 (myGroup != nil && otherGroup != nil && (myGroup == otherGroup || [otherGroup leader] == self)) ||
10167 ([self scanClass] == CLASS_MILITARY && [otherShip scanClass] == CLASS_MILITARY))
10168 {
10169 isFriendly = YES;
10170 }
10171
10172 return isFriendly;
10173}
10174
10175
10176- (ShipEntity *) shipHitByLaser
10177{
10178 return [_shipHitByLaser weakRefUnderlyingObject];
10179}
10180
10181
10182- (void) setShipHitByLaser:(ShipEntity *)ship
10183{
10184 if (ship != [self shipHitByLaser])
10185 {
10186 [_shipHitByLaser release];
10187 _shipHitByLaser = [ship weakRetain];
10188 }
10189}
10190
10191
10192- (void) noteLostTarget
10193{
10194 id target = nil;
10195 if ([self primaryTarget] != nil)
10196 {
10197 ShipEntity* ship = [self primaryTarget];
10198 if ([self isDefenseTarget:ship])
10199 {
10200 [self removeDefenseTarget:ship];
10201 }
10202 // for compatibility with 1.76 behaviour of this function, only pass
10203 // the target as a function parameter if the target is still a potential
10204 // valid target (e.g. not scooped, docked, hyperspaced, etc.)
10205 target = (ship && ship->isShip && [self isValidTarget:ship]) ? (id)ship : nil;
10206 if ([self primaryAggressor] == ship)
10207 {
10208 DESTROY(_primaryAggressor);
10209 }
10210 DESTROY(_primaryTarget);
10211 }
10212 // always do target lost
10213 [self doScriptEvent:OOJSID("shipTargetLost") withArgument:target];
10214 if (target == nil) [shipAI message:@"TARGET_LOST"]; // stale target? no major urgency.
10215 else [shipAI reactToMessage:@"TARGET_LOST" context:@"flight updates"]; // execute immediately otherwise.
10216}
10217
10218
10219- (void) noteLostTargetAndGoIdle
10220{
10221 behaviour = BEHAVIOUR_IDLE;
10222 frustration = 0.0;
10223 [self noteLostTarget];
10224}
10225
10226- (void) noteTargetDestroyed:(ShipEntity *)target
10227{
10228 [self collectBountyFor:(ShipEntity *)target];
10229 if ([self primaryTarget] == target)
10230 {
10231 [self removeTarget:target];
10232 [self doScriptEvent:OOJSID("shipTargetDestroyed") withArgument:target];
10233 [shipAI message:@"TARGET_DESTROYED"];
10234 }
10235 if ([self isDefenseTarget:target])
10236 {
10237 [self removeDefenseTarget:target];
10238 [shipAI message:@"DEFENSE_TARGET_DESTROYED"];
10239 [self doScriptEvent:OOJSID("defenseTargetDestroyed") withArgument:target];
10240 }
10241}
10242
10243
10244- (OOBehaviour) behaviour
10245{
10246 return behaviour;
10247}
10248
10249
10250- (void) setBehaviour:(OOBehaviour) cond
10251{
10252 if (cond != behaviour)
10253 {
10254 frustration = 0.0; // change is a GOOD thing
10255 behaviour = cond;
10256 }
10257}
10258
10259
10260- (HPVector) destination
10261{
10262 return _destination;
10263}
10264
10265- (HPVector) coordinates
10266{
10267 return coordinates;
10268}
10269
10270- (void) setCoordinate:(HPVector) coord // The name "setCoordinates" is already used by AI scripting.
10271{
10272 coordinates = coord;
10273}
10274
10275- (HPVector) distance_six: (GLfloat) dist
10276{
10277 HPVector six = position;
10278 six.x -= dist * v_forward.x; six.y -= dist * v_forward.y; six.z -= dist * v_forward.z;
10279 return six;
10280}
10281
10282
10283- (HPVector) distance_twelve: (GLfloat) dist withOffset:(GLfloat)offset
10284{
10285 HPVector twelve = position;
10286 twelve.x += dist * v_up.x; twelve.y += dist * v_up.y; twelve.z += dist * v_up.z;
10287 twelve.x += offset * v_right.x; twelve.y += offset * v_right.y; twelve.z += offset * v_right.z;
10288 return twelve;
10289}
10290
10291
10292- (void) trackOntoTarget:(double) delta_t withDForward: (GLfloat) dp
10293{
10294 Vector vector_to_target;
10295 Quaternion q_minarc;
10296 //
10297 Entity* target = [self primaryTarget];
10298 //
10299 if (!target)
10300 return;
10301
10302 vector_to_target = [self vectorTo:target];
10303 //
10304 GLfloat range2 = magnitude2(vector_to_target);
10305 GLfloat targetRadius = 0.75 * target->collision_radius;
10306 GLfloat max_cos = sqrt(1 - targetRadius*targetRadius/range2);
10307
10308 if (dp > max_cos)
10309 return; // ON TARGET!
10310
10311 if (vector_to_target.x||vector_to_target.y||vector_to_target.z)
10312 vector_to_target = vector_normal(vector_to_target);
10313 else
10314 vector_to_target.z = 1.0;
10315
10316 q_minarc = quaternion_rotation_between(v_forward, vector_to_target);
10317
10318 orientation = quaternion_multiply(q_minarc, orientation);
10319 [self orientationChanged];
10320
10321 flightRoll = 0.0;
10322 flightPitch = 0.0;
10323 flightYaw = 0.0;
10324 stick_roll = 0.0;
10325 stick_pitch = 0.0;
10326 stick_yaw = 0.0;
10327}
10328
10329
10330- (double) ballTrackLeadingTarget:(double) delta_t atTarget:(Entity *)target
10331{
10332 if (!target)
10333 {
10334 return -2.0; // no target
10335 }
10336
10337 Vector vector_to_target;
10338 Vector axis_to_track_by;
10339 Vector my_aim = vector_forward_from_quaternion(orientation);
10340 Vector my_ref = reference;
10341 double aim_cos, ref_cos;
10342 Vector leading = [target velocity];
10343
10344 // need to get vector to target in terms of this entities coordinate system
10345 HPVector my_position = [self absolutePositionForSubentity];
10346 vector_to_target = HPVectorToVector(HPvector_subtract([target position], my_position));
10347 // this is in absolute coordinates, so now rotate it
10348
10349 Entity *last = nil;
10350 Entity *father = [self parentEntity];
10351
10352 Quaternion q = kIdentityQuaternion;
10353 while ((father)&&(father != last) && (father != NO_TARGET))
10354 {
10355 /* Fix orientation */
10356 Quaternion fo = [father normalOrientation];
10357 fo.w = -fo.w;
10358 /* The below code works for player turrets where the
10359 * orientation is different, but not for NPC turrets. Taking
10360 * the normal orientation with -w works: there is probably a
10361 * neater way which someone who understands quaternions can
10362 * find, but this works well enough for 1.82 - CIM */
10363 q = quaternion_multiply(q,quaternion_conjugate(fo));
10364 last = father;
10365 if (![last isSubEntity]) break;
10366 father = [father owner];
10367 }
10368 q = quaternion_conjugate(q);
10369 // q now contains the rotation to the turret's reference system
10370
10371 vector_to_target = quaternion_rotate_vector(q,vector_to_target);
10372
10373 leading = quaternion_rotate_vector(q,leading);
10374 // rotate the vector to target and its velocity
10375
10376 if (magnitude(vector_to_target) > weaponRange * 1.01)
10377 {
10378 return -2.0; // out of range
10379 }
10380
10381 float lead = magnitude(vector_to_target) / TURRET_SHOT_SPEED;
10382
10383 vector_to_target = vector_add(vector_to_target, vector_multiply_scalar(leading, lead));
10384 vector_to_target = vector_normal_or_fallback(vector_to_target, kBasisZVector);
10385
10386 // do the tracking!
10387 aim_cos = dot_product(vector_to_target, my_aim);
10388 ref_cos = dot_product(vector_to_target, my_ref);
10389
10390
10391 if (ref_cos > TURRET_MINIMUM_COS) // target is forward of self
10392 {
10393 axis_to_track_by = cross_product(vector_to_target, my_aim);
10394 }
10395 else
10396 {
10397 return -2.0; // target is out of fire arc
10398 }
10399
10400 quaternion_rotate_about_axis(&orientation, axis_to_track_by, thrust * delta_t);
10401 [self orientationChanged];
10402
10403 [self setStatus:STATUS_ACTIVE];
10404
10405 return aim_cos;
10406}
10407
10408
10409- (void) setEvasiveJink:(GLfloat) z
10410{
10411 if (accuracy < COMBAT_AI_ISNT_AWFUL)
10412 {
10413 jink = kZeroVector;
10414 }
10415 else
10416 {
10417 jink.x = (ranrot_rand() % 256) - 128.0;
10418 jink.y = (ranrot_rand() % 256) - 128.0;
10419 jink.z = z;
10420
10421 // make sure we don't accidentally have near-zero jink
10422 if (jink.x < 0.0)
10423 {
10424 jink.x -= 128.0;
10425 }
10426 else
10427 {
10428 jink.x += 128.0;
10429 }
10430 if (jink.y < 0)
10431 {
10432 jink.y -= 128.0;
10433 }
10434 else
10435 {
10436 jink.y += 128.0;
10437 }
10438 }
10439}
10440
10441
10442- (void) evasiveAction:(double) delta_t
10443{
10444 stick_roll = flightRoll; //desired roll and pitch
10445 stick_pitch = flightPitch;
10446
10447 ShipEntity* target = [self primaryTarget];
10448 if (!target) // leave now!
10449 {
10450 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10451 return;
10452 }
10453
10454 double agreement = dot_product(v_right,target->v_right);
10455 if (agreement > -0.3 && agreement < 0.3)
10456 {
10457 stick_roll = 0.0;
10458 }
10459 else
10460 {
10461 if (stick_roll >= 0.0) {
10462 stick_roll = max_flight_roll;
10463 } else {
10464 stick_roll = -max_flight_roll;
10465 }
10466 }
10467 if (stick_pitch >= 0.0) {
10468 stick_pitch = max_flight_pitch;
10469 } else {
10470 stick_pitch = -max_flight_pitch;
10471 }
10472
10473 [self applySticks:delta_t];
10474}
10475
10476
10477- (double) trackPrimaryTarget:(double) delta_t :(BOOL) retreat
10478{
10479 Entity* target = [self primaryTarget];
10480
10481 if (!target) // leave now!
10482 {
10483 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10484 return 0.0;
10485 }
10486
10487 if (![self canStillTrackPrimaryTarget])
10488 {
10489 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10490 return 0.0;
10491 }
10492
10493 /* 1.81 change: do the above check first: if a missile can't be
10494 * fired outside scanner range it should self-destruct if the
10495 * target gets far enough away (it's going to miss anyway) -
10496 * CIM */
10497 if (scanClass == CLASS_MISSILE)
10498 return [self missileTrackPrimaryTarget: delta_t];
10499
10500 GLfloat d_forward, d_up, d_right;
10501
10502 Vector relPos = HPVectorToVector(HPvector_subtract([self calculateTargetPosition], position));
10503
10504 double range2 = HPmagnitude2(HPvector_subtract([target position], position));
10505
10506 //jink if retreating
10507 if (retreat) // calculate jink position when flying away from target.
10508 {
10509 Vector vx, vy, vz;
10510 if (target->isShip)
10511 {
10512 ShipEntity* targetShip = (ShipEntity*)target;
10513 vx = targetShip->v_right;
10514 vy = targetShip->v_up;
10515 vz = targetShip->v_forward;
10516 }
10517 else
10518 {
10519 Quaternion q = target->orientation;
10523 }
10524
10525 BOOL avoidCollision = NO;
10526 if (range2 < collision_radius * target->collision_radius * 100.0) // Check direction within 10 * collision radius.
10527 {
10528 Vector targetDirection = kBasisZVector;
10529 if (!vector_equal(relPos, kZeroVector)) targetDirection = vector_normal(relPos);
10530 avoidCollision = (dot_product(targetDirection, v_forward) > -0.1); // is flying toward target or only slightly outward.
10531 }
10532
10533 GLfloat dist_adjust_factor = 1.0;
10534 if (accuracy >= COMBAT_AI_FLEES_BETTER)
10535 {
10536 double range = magnitude(relPos);
10537 if (range > 2000.0)
10538 {
10539 dist_adjust_factor = range / 2000.0;
10540 if (accuracy >= COMBAT_AI_FLEES_BETTER_2)
10541 {
10542 dist_adjust_factor *= 3;
10543 }
10544 }
10545 if (jink.x == 0.0 && behaviour != BEHAVIOUR_RUNNING_DEFENSE)
10546 { // test for zero jink and correct
10547 [self setEvasiveJink:400.0];
10548 }
10549 }
10550
10551 if (!avoidCollision) // it is safe to jink
10552 {
10553 relPos.x += (jink.x * vx.x + jink.y * vy.x + jink.z * vz.x) * dist_adjust_factor;
10554 relPos.y += (jink.x * vx.y + jink.y * vy.y + jink.z * vz.y) * dist_adjust_factor;
10555 relPos.z += (jink.x * vx.z + jink.y * vy.z + jink.z * vz.z);
10556 }
10557
10558 }
10559
10560 if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
10561 else relPos.z = 1.0;
10562
10563 double max_cos = [self currentAimTolerance];
10564
10565 stick_roll = 0.0; //desired roll and pitch
10566 stick_pitch = 0.0;
10567
10568 double reverse = (retreat)? -1.0: 1.0;
10569
10570 double min_d = 0.004; // ~= 40m at 10km
10571 int max_factor = 8;
10572 double r_max_factor = 0.125;
10573 if (!retreat)
10574 {
10575 if (accuracy >= COMBAT_AI_TRACKS_CLOSER)
10576 {
10577 // much greater precision in combat
10578 if (max_flight_pitch > 1.0)
10579 {
10580 max_factor = floor(max_flight_pitch/0.125);
10581 r_max_factor = 1.0/max_factor;
10582 }
10583 min_d = 0.0004; // 10 times more precision ~= 4m at 10km
10584 max_factor *= 3;
10585 r_max_factor /= 3.0;
10586 }
10587 else if (accuracy >= COMBAT_AI_ISNT_AWFUL)
10588 {
10589 // slowly improve precision to target, but only if missing
10590 min_d -= 0.0001 * [self missedShots];
10591 if (min_d < 0.001)
10592 {
10593 min_d = 0.001;
10594 max_factor *= 2;
10595 r_max_factor /= 2.0;
10596 }
10597 }
10598 }
10599
10600 d_right = dot_product(relPos, v_right);
10601 d_up = dot_product(relPos, v_up);
10602 d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target
10603
10604 if (d_forward * reverse > max_cos) // on_target!
10605 {
10606 return d_forward;
10607 }
10608
10609 // begin rule-of-thumb manoeuvres
10610 stick_pitch = 0.0;
10611 stick_roll = 0.0;
10612
10613
10614 if ((reverse * d_forward < -0.5) && !pitching_over) // we're going the wrong way!
10615 pitching_over = YES;
10616
10617 if (pitching_over)
10618 {
10619 if (reverse * d_up > 0) // pitch up
10620 stick_pitch = -max_flight_pitch;
10621 else
10622 stick_pitch = max_flight_pitch;
10623 pitching_over = (reverse * d_forward < 0.707);
10624 }
10625
10626 // check if we are flying toward the destination..
10627 if ((d_forward < max_cos)||(retreat)) // not on course so we must adjust controls..
10628 {
10629 if (d_forward < -max_cos) // hack to avoid just flying away from the destination
10630 {
10631 d_up = min_d * 2.0;
10632 }
10633
10634 if (d_up > min_d)
10635 {
10636 int factor = sqrt(fabs(d_right) / fabs(min_d));
10637 if (factor > max_factor)
10638 factor = max_factor;
10639 if (d_right > min_d)
10640 stick_roll = - max_flight_roll * r_max_factor * factor; // note#
10641 if (d_right < -min_d)
10642 stick_roll = + max_flight_roll * r_max_factor * factor; // note#
10643 }
10644 if (d_up < -min_d)
10645 {
10646 int factor = sqrt(fabs(d_right) / fabs(min_d));
10647 if (factor > max_factor)
10648 factor = max_factor;
10649 if (d_right > min_d)
10650 stick_roll = + max_flight_roll * r_max_factor * factor; // note#
10651 if (d_right < -min_d)
10652 stick_roll = - max_flight_roll * r_max_factor * factor; // note#
10653 }
10654
10655 if (stick_roll == 0.0)
10656 {
10657 int factor = sqrt(fabs(d_up) / fabs(min_d));
10658 if (factor > max_factor)
10659 factor = max_factor;
10660 if (d_up > min_d)
10661 stick_pitch = - max_flight_pitch * reverse * r_max_factor * factor;
10662 if (d_up < -min_d)
10663 stick_pitch = + max_flight_pitch * reverse * r_max_factor * factor;
10664 }
10665
10666 if (accuracy >= COMBAT_AI_ISNT_AWFUL)
10667 {
10668 // don't overshoot target (helps accuracy at low frame rates)
10669 if (fabs(d_right) < fabs(stick_roll) * delta_t)
10670 {
10671 stick_roll = fabs(d_right) / delta_t * (stick_roll<0 ? -1 : 1);
10672 }
10673 if (fabs(d_up) < fabs(stick_pitch) * delta_t)
10674 {
10675 stick_pitch = fabs(d_up) / delta_t * (stick_pitch<0 ? -0.9 : 0.9);
10676 }
10677 }
10678
10679 }
10680 /* # note
10681 Eric 9-9-2010: Removed the "reverse" variable from the stick_roll calculation. This was mathematical wrong and
10682 made the ship roll in the wrong direction, preventing the ship to fly away in a straight line from the target.
10683 This means all the places were a jink was set, this jink never worked correctly. The main reason a ship still
10684 managed to turn at close range was probably by the fail-safe mechanisme with the "pitching_over" variable.
10685 The jink was programmed to do nothing within 500 meters of the ship and just fly away in direct line from the target
10686 in that range. Because of the bug the ships always rolled to the wrong side needed to fly away in direct line
10687 resulting in making it a difficult target.
10688 After fixing the bug, the ship realy flew away in direct line during the first 500 meters, making it a easy target
10689 for the player. All jink settings are retested and changed to give a turning behaviour that felt like the old
10690 situation, but now more deliberately set.
10691 */
10692
10693 // end rule-of-thumb manoeuvres
10694 stick_yaw = 0.0;
10695
10696 [self applySticks:delta_t];
10697
10698 if (retreat)
10699 d_forward *= d_forward; // make positive AND decrease granularity
10700
10701 if (d_forward < 0.0)
10702 return 0.0;
10703
10704 if ((!flightRoll)&&(!flightPitch)) // no correction
10705 return 1.0;
10706
10707 return d_forward;
10708}
10709
10710
10711- (double) trackSideTarget:(double) delta_t :(BOOL) leftside
10712{
10713 Entity* target = [self primaryTarget];
10714
10715 if (!target) // leave now!
10716 {
10717 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10718 return 0.0;
10719 }
10720
10721 if (![self canStillTrackPrimaryTarget])
10722 {
10723 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10724 return 0.0;
10725 }
10726
10727
10728 if (scanClass == CLASS_MISSILE) // never?
10729 return [self missileTrackPrimaryTarget: delta_t];
10730
10731 GLfloat d_forward, d_up, d_right;
10732
10733 Vector relPos = HPVectorToVector(HPvector_subtract([self calculateTargetPosition], position));
10734
10735
10736 if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
10737 else relPos.z = 1.0;
10738
10739// worse shots with side lasers than fore/aft, in general
10740
10741 double max_cos = [self currentAimTolerance];
10742
10743 stick_roll = 0.0; //desired roll and pitch
10744 stick_pitch = 0.0;
10745 stick_yaw = 0.0;
10746
10747 double reverse = (leftside)? -1.0: 1.0;
10748
10749 double min_d = 0.004;
10750 if (accuracy >= COMBAT_AI_TRACKS_CLOSER)
10751 {
10752 min_d = 0.002;
10753 }
10754 int max_factor = 8;
10755 double r_max_factor = 0.125;
10756
10757 d_right = dot_product(relPos, v_right);
10758 d_up = dot_product(relPos, v_up);
10759 d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target
10760
10761 if (d_right * reverse > max_cos) // on_target!
10762 {
10763 return d_right * reverse;
10764 }
10765
10766 // begin rule-of-thumb manoeuvres
10767 stick_pitch = 0.0;
10768 stick_roll = 0.0;
10769 stick_yaw = 0.0;
10770
10771 // check if we are flying toward the destination..
10772 if ((d_right * reverse < max_cos)) // not on course so we must adjust controls..
10773 {
10774 if (d_right < -max_cos) // hack to avoid just pointing away from the destination
10775 {
10776 d_forward = min_d * 2.0;
10777 }
10778
10779 if (d_forward > min_d)
10780 {
10781 int factor = sqrt(fabs(d_up) / fabs(min_d));
10782 if (factor > max_factor)
10783 factor = max_factor;
10784 if (d_up > min_d)
10785 stick_pitch = + max_flight_pitch * r_max_factor * factor; // note#
10786 if (d_up < -min_d)
10787 stick_pitch = - max_flight_pitch * r_max_factor * factor; // note#
10788 }
10789 if (d_forward < -min_d)
10790 {
10791 int factor = sqrt(fabs(d_up) / fabs(min_d));
10792 if (factor > max_factor)
10793 factor = max_factor;
10794 if (d_up > min_d)
10795 stick_pitch = + max_flight_pitch * r_max_factor * factor; // note#
10796 if (d_up < -min_d)
10797 stick_pitch = - max_flight_pitch * r_max_factor * factor; // note#
10798 }
10799
10800 if (fabs(stick_pitch) == 0.0 || fabs(d_forward) > 0.5)
10801 {
10802 stick_pitch = 0.0;
10803 int factor = sqrt(fabs(d_forward) / fabs(min_d));
10804 if (factor > max_factor)
10805 factor = max_factor;
10806 if (d_forward > min_d)
10807 stick_yaw = - max_flight_yaw * reverse * r_max_factor * factor;
10808 if (d_forward < -min_d)
10809 {
10810 if (factor < max_factor/2.0) // compensate for forward thrust
10811 factor *= 2.0;
10812 stick_yaw = + max_flight_yaw * reverse * r_max_factor * factor;
10813 }
10814 }
10815 }
10816
10817
10818 // end rule-of-thumb manoeuvres
10819
10820 [self applySticks:delta_t];
10821
10822 if ((!flightPitch)&&(!flightYaw)) // no correction
10823 return 1.0;
10824
10825 return d_right * reverse;
10826}
10827
10828
10829
10830- (double) missileTrackPrimaryTarget:(double) delta_t
10831{
10832 Vector relPos;
10833 GLfloat d_forward, d_up, d_right;
10834 ShipEntity *target = [self primaryTarget];
10835 BOOL inPursuit = YES;
10836
10837 if (!target || ![target isShip]) // leave now!
10838 return 0.0;
10839
10840 double damping = 0.5 * delta_t;
10841
10842 stick_roll = 0.0; //desired roll and pitch
10843 stick_pitch = 0.0;
10844 stick_yaw = 0.0;
10845
10846 relPos = [self vectorTo:target];
10847
10848 // Adjust missile course by taking into account target's velocity and missile
10849 // accuracy. Modification on original code contributed by Cmdr James.
10850
10851 float missileSpeed = (float)[self speed];
10852
10853 // Avoid getting ourselves in a divide by zero situation by setting a missileSpeed
10854 // low threshold. Arbitrarily chosen 0.01, since it seems to work quite well.
10855 // Missile accuracy is already clamped within the 0.0 to 10.0 range at initialization,
10856 // but doing these calculations every frame when accuracy equals 0.0 just wastes cycles.
10857 if (missileSpeed > 0.01f && accuracy > 0.0f)
10858 {
10859 inPursuit = (dot_product([target forwardVector], v_forward) > 0.0f);
10860 if (inPursuit)
10861 {
10862 Vector leading = [target velocity];
10863 float lead = magnitude(relPos) / missileSpeed;
10864
10865 // Adjust where we are going to take into account target's velocity.
10866 // Use accuracy value to determine how well missile will track target.
10867 relPos.x += (lead * leading.x * (accuracy / 10.0f));
10868 relPos.y += (lead * leading.y * (accuracy / 10.0f));
10869 relPos.z += (lead * leading.z * (accuracy / 10.0f));
10870 }
10871 }
10872
10873 if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
10874 else relPos.z = 1.0;
10875
10876 d_right = dot_product(relPos, v_right); // = cosine of angle between angle to target and v_right
10877 d_up = dot_product(relPos, v_up); // = cosine of angle between angle to target and v_up
10878 d_forward = dot_product(relPos, v_forward); // = cosine of angle between angle to target and v_forward
10879
10880 // begin rule-of-thumb manoeuvres
10881
10882 stick_roll = 0.0;
10883
10884 if (pitching_over)
10885 pitching_over = (stick_pitch != 0.0);
10886
10887 if ((d_forward < -pitch_tolerance) && (!pitching_over))
10888 {
10889 pitching_over = YES;
10890 if (d_up >= 0)
10891 stick_pitch = -max_flight_pitch;
10892 if (d_up < 0)
10893 stick_pitch = max_flight_pitch;
10894 }
10895
10896 if (pitching_over)
10897 {
10898 pitching_over = (d_forward < 0.5);
10899 }
10900 else
10901 {
10902 stick_pitch = -max_flight_pitch * d_up;
10903 stick_roll = -max_flight_roll * d_right;
10904 }
10905
10906 // end rule-of-thumb manoeuvres
10907
10908 // apply damping
10909 if (flightRoll < 0)
10910 flightRoll += (flightRoll < -damping) ? damping : -flightRoll;
10911 if (flightRoll > 0)
10912 flightRoll -= (flightRoll > damping) ? damping : flightRoll;
10913 if (flightPitch < 0)
10914 flightPitch += (flightPitch < -damping) ? damping : -flightPitch;
10915 if (flightPitch > 0)
10916 flightPitch -= (flightPitch > damping) ? damping : flightPitch;
10917
10918
10919 [self applySticks:delta_t];
10920
10921 //
10922 // return target confidence 0.0 .. 1.0
10923 //
10924 if (d_forward < 0.0)
10925 return 0.0;
10926 return d_forward;
10927}
10928
10929
10930- (double) trackDestination:(double) delta_t :(BOOL) retreat
10931{
10932 Vector relPos;
10933 GLfloat d_forward, d_up, d_right;
10934
10935 BOOL we_are_docking = (nil != dockingInstructions);
10936
10937 stick_roll = 0.0; //desired roll and pitch
10938 stick_pitch = 0.0;
10939 stick_yaw = 0.0;
10940
10941 double reverse = 1.0;
10942 double reversePlayer = 1.0;
10943
10944 double min_d = 0.004;
10945 double max_cos = MAX_COS; // should match default value of max_cos in behaviour_fly_to_destination!
10946 double precision = we_are_docking ? 0.25 : 0.9025; // lower values force a direction closer to the target. (resp. 50% and 95% within range)
10947
10948 if (retreat)
10949 reverse = -reverse;
10950
10951 if (isPlayer)
10952 {
10953 reverse = -reverse;
10954 reversePlayer = -1;
10955 }
10956
10957 relPos = HPVectorToVector(HPvector_subtract(_destination, position));
10958 double range2 = magnitude2(relPos);
10959 double desired_range2 = desired_range*desired_range;
10960
10961 /* 2009-7-18 Eric: We need to aim well inide the desired_range sphere round the target and not at the surface of the sphere.
10962 Because of the framerate most ships normally overshoot the target and they end up flying clearly on a path
10963 through the sphere. Those ships give no problems, but ships with a very low turnrate will aim close to the surface and will than
10964 have large trouble with reaching their destination. When those ships enter the slowdown range, they have almost no speed vector
10965 in the direction of the target. I now used 95% of desired_range to aim at, but a smaller value might even be better.
10966 */
10967 if (range2 > desired_range2)
10968 {
10969 max_cos = sqrt(1 - precision * desired_range2/range2); // Head for a point within 95% of desired_range.
10970 if (max_cos >= 0.99999)
10971 {
10972 max_cos = 0.99999;
10973 }
10974 }
10975
10976 if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
10977 else relPos.z = 1.0;
10978
10979 d_right = dot_product(relPos, v_right);
10980 d_up = dot_product(relPos, v_up);
10981 d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target
10982
10983 // begin rule-of-thumb manoeuvres
10984 stick_pitch = 0.0;
10985 stick_roll = 0.0;
10986
10987 // pitching_over is currently only set in behaviour_formation_form_up, for escorts and in avoidCollision.
10988 // This allows for immediate pitch corrections instead of first waiting untill roll has completed.
10989 if (pitching_over)
10990 {
10991 if (reverse * d_up > 0) // pitch up
10992 stick_pitch = -max_flight_pitch;
10993 else
10994 stick_pitch = max_flight_pitch;
10995 pitching_over = (reverse * d_forward < 0.707);
10996 }
10997
10998 // check if we are flying toward (or away from) the destination..
10999 if ((d_forward < max_cos)||(retreat)) // not on course so we must adjust controls..
11000 {
11001
11002 if (d_forward <= -max_cos || (retreat && d_forward >= max_cos)) // hack to avoid just flying away from the destination
11003 {
11004 d_up = min_d * 2.0;
11005 }
11006
11007 if (d_up > min_d)
11008 {
11009 int factor = sqrt(fabs(d_right) / fabs(min_d));
11010 if (factor > 8)
11011 factor = 8;
11012 if (d_right > min_d)
11013 stick_roll = - max_flight_roll * reversePlayer * 0.125 * factor; // only reverse sign for the player;
11014 if (d_right < -min_d)
11015 stick_roll = + max_flight_roll * reversePlayer * 0.125 * factor;
11016 if (fabs(d_right) < fabs(stick_roll) * delta_t)
11017 stick_roll = fabs(d_right) / delta_t * (stick_roll<0 ? -1 : 1); // don't overshoot heading
11018 }
11019
11020 if (d_up < -min_d)
11021 {
11022 int factor = sqrt(fabs(d_right) / fabs(min_d));
11023 if (factor > 8)
11024 factor = 8;
11025 if (d_right > min_d)
11026 stick_roll = + max_flight_roll * reversePlayer * 0.125 * factor; // only reverse sign for the player;
11027 if (d_right < -min_d)
11028 stick_roll = - max_flight_roll * reversePlayer * 0.125 * factor;
11029 if (fabs(d_right) < fabs(stick_roll) * delta_t)
11030 stick_roll = fabs(d_right) / delta_t * (stick_roll<0 ? -1 : 1); // don't overshoot heading
11031 }
11032
11033 if (stick_roll == 0.0)
11034 {
11035 int factor = sqrt(fabs(d_up) / fabs(min_d));
11036 if (factor > 8)
11037 factor = 8;
11038 if (d_up > min_d)
11039 stick_pitch = - max_flight_pitch * reverse * 0.125 * factor; //pitch_pitch * reverse;
11040 if (d_up < -min_d)
11041 stick_pitch = + max_flight_pitch * reverse * 0.125 * factor;
11042 if (fabs(d_up) < fabs(stick_pitch) * delta_t)
11043 stick_pitch = fabs(d_up) / delta_t * (stick_pitch<0 ? -1 : 1); // don't overshoot heading
11044 }
11045
11046 if (stick_pitch == 0.0)
11047 {
11048 // not sufficiently on course yet, but min_d is too high
11049 // turn anyway slightly to adjust
11050 stick_pitch = 0.01;
11051 }
11052 }
11053
11054 if (we_are_docking && docking_match_rotation && (d_forward > max_cos))
11055 {
11056 /* we are docking and need to consider the rotation/orientation of the docking port */
11057 StationEntity* station_for_docking = (StationEntity*)[self targetStation];
11058
11059 if ((station_for_docking)&&(station_for_docking->isStation))
11060 {
11061 stick_roll = [self rollToMatchUp:[station_for_docking portUpVectorForShip:self] rotating:[station_for_docking flightRoll]];
11062 }
11063 }
11064
11065 // end rule-of-thumb manoeuvres
11066
11067 [self applySticks:delta_t];
11068
11069 if (retreat)
11070 d_forward *= d_forward; // make positive AND decrease granularity
11071
11072 if (d_forward < 0.0)
11073 return 0.0;
11074
11075 if ((!flightRoll)&&(!flightPitch)) // no correction
11076 return 1.0;
11077
11078 return d_forward;
11079}
11080
11081
11082- (GLfloat) rollToMatchUp:(Vector)up_vec rotating:(GLfloat)match_roll
11083{
11084 GLfloat cosTheta = dot_product(up_vec, v_up); // == cos of angle between up vectors
11085 GLfloat sinTheta = dot_product(up_vec, v_right);
11086
11087 if (!isPlayer)
11088 {
11089 match_roll = -match_roll; // make necessary corrections for a different viewpoint
11090 sinTheta = -sinTheta;
11091 }
11092
11093 if (cosTheta < 0.0f)
11094 {
11095 cosTheta = -cosTheta;
11096 sinTheta = -sinTheta;
11097 }
11098
11099 if (sinTheta > 0.0f)
11100 {
11101 // increase roll rate
11102 return cosTheta * cosTheta * match_roll + sinTheta * sinTheta * max_flight_roll;
11103 }
11104 else
11105 {
11106 // decrease roll rate
11107 return cosTheta * cosTheta * match_roll - sinTheta * sinTheta * max_flight_roll;
11108 }
11109}
11110
11111
11112- (GLfloat) rangeToDestination
11113{
11114 return HPdistance(position, _destination);
11115}
11116
11117
11118- (NSArray *) collisionExceptions
11119{
11120 if (_collisionExceptions == nil)
11121 {
11122 return [NSArray array];
11123 }
11124 return [_collisionExceptions allObjects];
11125}
11126
11127
11128- (void) addCollisionException:(ShipEntity *)ship
11129{
11130 if (_collisionExceptions == nil)
11131 {
11132 // Allocate lazily for the benefit of the ships that never need this.
11133 _collisionExceptions = [[OOWeakSet alloc] init];
11134 }
11135 [_collisionExceptions addObject:ship];
11136}
11137
11138
11139- (void) removeCollisionException:(ShipEntity *)ship
11140{
11141 if (_collisionExceptions != nil)
11142 {
11143 [_collisionExceptions removeObject:ship];
11144 }
11145}
11146
11147
11148- (BOOL) collisionExceptedFor:(ShipEntity *)ship
11149{
11150 if (_collisionExceptions == nil)
11151 {
11152 return NO;
11153 }
11154 return [_collisionExceptions containsObject:ship];
11155}
11156
11157
11158
11159- (NSUInteger) defenseTargetCount
11160{
11161 return [_defenseTargets count];
11162}
11163
11164
11165- (NSArray *) allDefenseTargets
11166{
11167 return [_defenseTargets allObjects];
11168}
11169
11170
11171- (NSEnumerator *) defenseTargetEnumerator
11172{
11173 return [_defenseTargets objectEnumerator];
11174}
11175
11176
11177- (BOOL) addDefenseTarget:(Entity *)target
11178{
11179 if ([self defenseTargetCount] >= MAX_TARGETS)
11180 {
11181 return NO;
11182 }
11183 // primary target can be a wormhole, defense targets shouldn't be
11184 if (target == nil || [self isDefenseTarget:target] || ![target isShip])
11185 {
11186 return NO;
11187 }
11188 if (_defenseTargets == nil)
11189 {
11190 // Allocate lazily for the benefit of the ships that never get in fights.
11191 _defenseTargets = [[OOWeakSet alloc] init];
11192 }
11193
11194 [_defenseTargets addObject:target];
11195 return YES;
11196}
11197
11198
11199- (void) validateDefenseTargets
11200{
11201 if (_defenseTargets == nil)
11202 {
11203 return;
11204 }
11205 // get enumerator from array as we'll be modifying original during enumeration
11206 NSEnumerator *defTargets = [[self allDefenseTargets] objectEnumerator];
11207 Entity *target = nil;
11208 while ((target = [[defTargets nextObject] weakRefUnderlyingObject]))
11209 {
11210 if ([target status] == STATUS_DEAD)
11211 {
11212 [self removeDefenseTarget:target];
11213 }
11214 }
11215}
11216
11217
11218- (BOOL) isDefenseTarget:(Entity *)target
11219{
11220 return [_defenseTargets containsObject:target];
11221}
11222
11223
11224// exposed to AI (as alias of clearDefenseTargets)
11225- (void) removeAllDefenseTargets
11226{
11227 [_defenseTargets removeAllObjects];
11228}
11229
11230
11231- (void) removeDefenseTarget:(Entity *)target
11232{
11233 [_defenseTargets removeObject:target];
11234}
11235
11236
11237- (double) rangeToPrimaryTarget
11238{
11239 return [self rangeToSecondaryTarget:[self primaryTarget]];
11240}
11241
11242
11243- (double) rangeToSecondaryTarget:(Entity *)target
11244{
11245 double dist;
11246 Vector delta;
11247 if (target == nil) // leave now!
11248 return 0.0;
11249 delta = HPVectorToVector(HPvector_subtract(target->position, position));
11250 dist = magnitude(delta);
11251 dist -= target->collision_radius;
11252 dist -= collision_radius;
11253 return dist;
11254}
11255
11256
11257- (double) approachAspectToPrimaryTarget
11258{
11259 Vector delta;
11260 Entity *target = [self primaryTarget];
11261 if (target == nil || ![target isShip]) // leave now!
11262 {
11263 return 0.0;
11264 }
11265 ShipEntity *ship_target = (ShipEntity *)target;
11266
11267 delta = HPVectorToVector(HPvector_subtract(position, target->position));
11268
11269 return dot_product(vector_normal(delta), ship_target->v_forward);
11270}
11271
11272
11273- (BOOL) hasProximityAlertIgnoringTarget:(BOOL)ignore_target
11274{
11275 if (([self proximityAlert] != nil)&&(!ignore_target || ([self proximityAlert] != [self primaryTarget])))
11276 {
11277 return YES;
11278 }
11279 return NO;
11280}
11281
11282
11283// lower is better. Defines angular size of circle in which ship
11284// thinks is on target
11285- (GLfloat) currentAimTolerance
11286{
11287 GLfloat basic_aim = aim_tolerance;
11288 GLfloat best_cos = 0.99999; // ~45m in 10km (track won't go better than 40)
11289 if (accuracy >= COMBAT_AI_ISNT_AWFUL)
11290 {
11291 // better general targeting
11292 best_cos = 0.999999; // ~14m in 10km (track won't go better than 10)
11293 // if missing, aim better!
11294 basic_aim /= 1.0 + ((GLfloat)[self missedShots] / 4.0);
11295 }
11296 if (accuracy >= COMBAT_AI_TRACKS_CLOSER)
11297 {
11298 // deadly shots
11299 best_cos = 0.9999999; // ~4m in 10km (track won't go better than 4)
11300 // and start with extremely good aim circle
11301 basic_aim /= 5.0;
11302 }
11303 if (currentWeaponFacing == WEAPON_FACING_AFT && accuracy < COMBAT_AI_ISNT_AWFUL)
11304 { // bad shots with aft lasers
11305 basic_aim *= 1.3;
11306 }
11307 else if (currentWeaponFacing == WEAPON_FACING_PORT || currentWeaponFacing == WEAPON_FACING_STARBOARD)
11308 { // everyone a bit worse with side lasers
11309 if (accuracy < COMBAT_AI_ISNT_AWFUL)
11310 { // especially these
11311 basic_aim *= 1.3 + randf();
11312 }
11313 else
11314 {
11315 basic_aim *= 1.3;
11316 }
11317 }
11318 // only apply glare if ship is not shadowed
11319 if (isSunlit) {
11320 OOSunEntity *sun = [UNIVERSE sun];
11321 if (sun)
11322 {
11323 GLfloat sunGlareAngularSize = atan([sun radius]/HPdistance([self position], [sun position])) * SUN_GLARE_MULT_FACTOR + (SUN_GLARE_ADD_FACTOR);
11324 GLfloat glareLevel = [self lookingAtSunWithThresholdAngleCos:cos(sunGlareAngularSize)] * (1.0f - [self sunGlareFilter]);
11325 if (glareLevel > 0.1f)
11326 {
11327 // looking towards sun can seriously mess up aim (glareLevel 0..1)
11328 basic_aim *= (1.0 + glareLevel*3.0);
11329// OOLog(@"aim.debug",@"Sun glare affecting aim: %f for %@",glareLevel,self);
11330 if (glareLevel > 0.5f)
11331 {
11332 // strong glare makes precise targeting impossible
11333 best_cos = 0.99999;
11334 }
11335 }
11336 }
11337 }
11338
11339
11340 GLfloat max_cos = sqrt(1-(basic_aim * basic_aim / 100000000.0));
11341
11342 if (max_cos < best_cos)
11343 {
11344 return max_cos;
11345 }
11346 return best_cos;
11347}
11348
11349
11350// much simpler than player version
11351- (GLfloat) lookingAtSunWithThresholdAngleCos:(GLfloat) thresholdAngleCos
11352{
11353 OOSunEntity *sun = [UNIVERSE sun];
11354 GLfloat measuredCos = 999.0f, measuredCosAbs;
11355 GLfloat sunBrightness = 0.0f;
11356 Vector relativePosition, unitRelativePosition;
11357
11358 if (EXPECT_NOT(!sun)) return 0.0f;
11359
11360 relativePosition = HPVectorToVector(HPvector_subtract([self position], [sun position]));
11361 unitRelativePosition = vector_normal_or_zbasis(relativePosition);
11362 switch (currentWeaponFacing)
11363 {
11365 measuredCos = -dot_product(unitRelativePosition, v_forward);
11366 break;
11367 case WEAPON_FACING_AFT:
11368 measuredCos = +dot_product(unitRelativePosition, v_forward);
11369 break;
11370 case WEAPON_FACING_PORT:
11371 measuredCos = +dot_product(unitRelativePosition, v_right);
11372 break;
11374 measuredCos = -dot_product(unitRelativePosition, v_right);
11375 break;
11376 default:
11377 break;
11378 }
11379 measuredCosAbs = fabs(measuredCos);
11380 if (thresholdAngleCos <= measuredCosAbs && measuredCosAbs <= 1.0f) // angle from viewpoint to sun <= desired threshold
11381 {
11382 sunBrightness = (measuredCos - thresholdAngleCos) / (1.0f - thresholdAngleCos);
11383 if (sunBrightness < 0.0f) sunBrightness = 0.0f;
11384 }
11385 return sunBrightness * sunBrightness * sunBrightness;
11386}
11387
11388
11389- (BOOL) onTarget:(OOWeaponFacing)direction withWeapon:(OOWeaponType)weapon_type
11390{
11391 // initialize dq to a value that would normally return NO; dq is handled inside the defaultless switch(direction) statement
11392 // and should alaways be recalculated anyway. Initialization here needed to silence compiler warning - Nikos 20120526
11393 GLfloat dq = -1.0f;
11394 GLfloat d2, radius, astq;
11395 Vector rel_pos, urp;
11396 if ([weapon_type isTurretLaser])
11397 {
11398 return YES;
11399 }
11400
11401 Entity *target = [self primaryTarget];
11402 if (target == nil) return NO;
11403 if ([target status] == STATUS_DEAD) return NO;
11404
11405 if (isSunlit && (target->isSunlit == NO) && (randf() < 0.75))
11406 {
11407 return NO; // 3/4 of the time you can't see from a lit place into a darker place
11408 }
11409 radius = target->collision_radius;
11410 rel_pos = HPVectorToVector(HPvector_subtract([self calculateTargetPosition], position));
11411 d2 = magnitude2(rel_pos);
11412 urp = vector_normal_or_zbasis(rel_pos);
11413
11414 switch (direction)
11415 {
11417 dq = +dot_product(urp, v_forward); // cosine of angle between v_forward and unit relative position
11418 break;
11419
11420 case WEAPON_FACING_AFT:
11421 dq = -dot_product(urp, v_forward); // cosine of angle between v_forward and unit relative position
11422 break;
11423
11424 case WEAPON_FACING_PORT:
11425 dq = -dot_product(urp, v_right); // cosine of angle between v_right and unit relative position
11426 break;
11427
11429 dq = +dot_product(urp, v_right); // cosine of angle between v_right and unit relative position
11430 break;
11431
11432 case WEAPON_FACING_NONE:
11433 break;
11434 }
11435
11436 if (dq < 0.0) return NO;
11437
11438 GLfloat aim = [self currentAimTolerance];
11439 if (dq > aim*aim) return YES;
11440
11441 // cosine of 1/3 of half angle subtended by target (mostly they'll
11442 // fire sooner anyway due to currentAimTolerance, but this should
11443 // almost always be a solid hit)
11444 astq = sqrt(1.0 - radius * radius / (d2 * 9));
11445
11446 return (fabs(dq) >= astq);
11447}
11448
11449
11450- (BOOL) fireWeapon:(OOWeaponType)weapon_type direction:(OOWeaponFacing)direction range:(double)range
11451{
11452 weapon_temp = 0.0;
11453 switch (direction)
11454 {
11456 weapon_temp = forward_weapon_temp;
11457 break;
11458
11459 case WEAPON_FACING_AFT:
11460 weapon_temp = aft_weapon_temp;
11461 break;
11462
11463 case WEAPON_FACING_PORT:
11464 weapon_temp = port_weapon_temp;
11465 break;
11466
11468 weapon_temp = starboard_weapon_temp;
11469 break;
11470
11471 case WEAPON_FACING_NONE:
11472 break;
11473 }
11474 if (weapon_temp / NPC_MAX_WEAPON_TEMP >= WEAPON_COOLING_CUTOUT) return NO;
11475
11476 NSUInteger multiplier = 1;
11477 if (_multiplyWeapons)
11478 {
11479 // multiple fitted
11480 multiplier = [[self laserPortOffset:direction] count];
11481 }
11482
11483 if (energy <= weapon_energy_use * multiplier) return NO;
11484 if ([self shotTime] < weapon_recharge_rate) return NO;
11485 if (![weapon_type isTurretLaser])
11486 { // thargoid laser may just pick secondary target in this case
11487 if (range > randf() * weaponRange * (accuracy+7.5)) return NO;
11488 if (range > weaponRange) return NO;
11489 }
11490 if (![self onTarget:direction withWeapon:weapon_type]) return NO;
11491
11492 BOOL fired = NO;
11493 if (!isWeaponNone(weapon_type))
11494 {
11495 if ([weapon_type isTurretLaser])
11496 {
11497 [self fireDirectLaserShot:range];
11498 fired = YES;
11499 }
11500 else
11501 {
11502 [self fireLaserShotInDirection:direction weaponIdentifier:[weapon_type identifier]];
11503 fired = YES;
11504 }
11505 }
11506
11507 if (fired)
11508 {
11509 energy -= weapon_energy_use * multiplier;
11510 switch (direction)
11511 {
11513 forward_weapon_temp += weapon_shot_temperature * multiplier;
11514 break;
11515
11516 case WEAPON_FACING_AFT:
11517 aft_weapon_temp += weapon_shot_temperature * multiplier;
11518 break;
11519
11520 case WEAPON_FACING_PORT:
11521 port_weapon_temp += weapon_shot_temperature * multiplier;
11522 break;
11523
11525 starboard_weapon_temp += weapon_shot_temperature * multiplier;
11526 break;
11527
11528 case WEAPON_FACING_NONE:
11529 break;
11530 }
11531 }
11532
11533 if (direction == WEAPON_FACING_FORWARD)
11534 {
11535 //can we fire lasers from our subentities?
11536 NSEnumerator *subEnum = nil;
11537 ShipEntity *se = nil;
11538 for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); )
11539 {
11540 if ([se fireSubentityLaserShot:range])
11541 {
11542 fired = YES;
11543 }
11544 }
11545 }
11546
11547 if (fired && cloaking_device_active && cloakPassive)
11548 {
11549 [self deactivateCloakingDevice];
11550 }
11551
11552 return fired;
11553}
11554
11555
11556- (BOOL) fireMainWeapon:(double)range
11557{
11558 // set the values from forward_weapon_type.
11559 // OXPs can override the default front laser energy damage.
11560 currentWeaponFacing = WEAPON_FACING_FORWARD;
11561 [self setWeaponDataFromType:forward_weapon_type];
11562
11563// weapon damage override no longer effective
11564// weapon_damage = weapon_damage_override;
11565
11566 BOOL result = [self fireWeapon:forward_weapon_type direction:WEAPON_FACING_FORWARD range:range];
11567 if (isWeaponNone(forward_weapon_type))
11568 {
11569 // need to check subentities to avoid AI oddities
11570 // will already have fired them by now, though
11571 NSEnumerator *subEnum = [self shipSubEntityEnumerator];
11572 ShipEntity *se = nil;
11573 OOWeaponType weapon_type = nil;
11574 BOOL hasTurrets = NO;
11575 while (isWeaponNone(weapon_type) && (se = [subEnum nextObject]))
11576 {
11577 weapon_type = se->forward_weapon_type;
11578 weapon_temp = se->forward_weapon_temp;
11579 if (se->behaviour == BEHAVIOUR_TRACK_AS_TURRET)
11580 {
11581 hasTurrets = YES;
11582 }
11583 }
11584 if (isWeaponNone(weapon_type) && hasTurrets)
11585 { /* no forward weapon but has turrets, so set up range calculations accordingly
11586 note: this was hard-coded to 10000.0, although turrets have a notably
11587 shorter range. We are using a multiplier of 1.667 in order to not change
11588 something that already works, but probably it would be best to use
11589 TURRET_SHOT_RANGE * COMBAT_WEAPON_RANGE_FACTOR here
11590 */
11591 weaponRange = TURRET_SHOT_RANGE * 1.667;
11592 }
11593 else
11594 {
11595 [self setWeaponDataFromType:weapon_type];
11596 }
11597 }
11598 return result;
11599}
11600
11601
11602- (BOOL) fireAftWeapon:(double)range
11603{
11604 // set the values from aft_weapon_type.
11605 currentWeaponFacing = WEAPON_FACING_AFT;
11606 [self setWeaponDataFromType:aft_weapon_type];
11607
11608 return [self fireWeapon:aft_weapon_type direction:WEAPON_FACING_AFT range:range];
11609}
11610
11611
11612- (BOOL) firePortWeapon:(double)range
11613{
11614 // set the values from port_weapon_type.
11615 currentWeaponFacing = WEAPON_FACING_PORT;
11616 [self setWeaponDataFromType:port_weapon_type];
11617
11618 return [self fireWeapon:port_weapon_type direction:WEAPON_FACING_PORT range:range];
11619}
11620
11621
11622- (BOOL) fireStarboardWeapon:(double)range
11623{
11624 // set the values from starboard_weapon_type.
11625 currentWeaponFacing = WEAPON_FACING_STARBOARD;
11626 [self setWeaponDataFromType:starboard_weapon_type];
11627
11628 return [self fireWeapon:starboard_weapon_type direction:WEAPON_FACING_STARBOARD range:range];
11629}
11630
11631
11632- (OOTimeDelta) shotTime
11633{
11634 return shot_time;
11635}
11636
11637
11638- (void) resetShotTime
11639{
11640 shot_time = 0.0;
11641}
11642
11643
11644- (BOOL) fireTurretCannon:(double) range
11645{
11646 if ([self shotTime] < weapon_recharge_rate)
11647 return NO;
11648 if (range > weaponRange * 1.01) // 1% more than max range - open up just slightly early
11649 return NO;
11650 ShipEntity *root = [self rootShipEntity];
11651 if ([root isPlayer] && ![PLAYER weaponsOnline])
11652 return NO;
11653
11654 if ([root isCloaked] && [root cloakPassive])
11655 {
11656 // can't fire turrets while cloaked
11657 return NO;
11658 }
11659
11660 Vector vel;
11661 HPVector origin = [self position];
11662
11663 Entity *last = nil;
11664 Entity *father = [self parentEntity];
11665 OOMatrix r_mat;
11666
11667 vel = vector_forward_from_quaternion(orientation); // Facing
11668 // adjust velocity and position vectors to absolute coordinates
11669 while ((father)&&(father != last) && (father != NO_TARGET))
11670 {
11671 r_mat = [father drawRotationMatrix];
11672 origin = HPvector_add(OOHPVectorMultiplyMatrix(origin, r_mat), [father position]);
11673 vel = OOVectorMultiplyMatrix(vel, r_mat);
11674 last = father;
11675 if (![last isSubEntity]) break;
11676 father = [father owner];
11677 }
11678
11679 origin = HPvector_add(origin, vectorToHPVector(vector_multiply_scalar(vel, collision_radius + 0.5))); // Start just outside collision sphere
11680 vel = vector_multiply_scalar(vel, TURRET_SHOT_SPEED); // Shot velocity
11681
11682 OOPlasmaShotEntity *shot = [[OOPlasmaShotEntity alloc] initWithPosition:origin
11683 velocity:vel
11684 energy:weapon_damage
11685 duration:weaponRange/TURRET_SHOT_SPEED
11686 color:laser_color];
11687
11688 [shot autorelease];
11689 [UNIVERSE addEntity:shot];
11690 [shot setOwner:[self rootShipEntity]]; // has to be done AFTER adding shot to the UNIVERSE
11691
11692 [self resetShotTime];
11693 return YES;
11694}
11695
11696
11697- (void) setLaserColor:(OOColor *) color
11698{
11699 if (color)
11700 {
11701 [laser_color release];
11702 laser_color = [color retain];
11703 }
11704}
11705
11706
11707- (void) setExhaustEmissiveColor:(OOColor *) color
11708{
11709 if (color)
11710 {
11711 [exhaust_emissive_color release];
11712 exhaust_emissive_color = [color retain];
11713 }
11714}
11715
11716
11717- (OOColor *)laserColor
11718{
11719 return [[laser_color retain] autorelease];
11720}
11721
11722
11723- (OOColor *)exhaustEmissiveColor
11724{
11725 return [[exhaust_emissive_color retain] autorelease];
11726}
11727
11728
11729- (BOOL) fireSubentityLaserShot:(double)range
11730{
11731 [self setShipHitByLaser:nil];
11732
11733 if (isWeaponNone(forward_weapon_type)) return NO;
11734 [self setWeaponDataFromType:forward_weapon_type];
11735
11736 ShipEntity *parent = [self owner];
11737 NSAssert([parent isShipWithSubEntityShip:self], @"-fireSubentityLaserShot: called on ship which is not a subentity.");
11738
11739 // subentity lasers still draw power from the main entity
11740 if ([parent energy] <= weapon_energy_use) return NO;
11741 if ([self shotTime] < weapon_recharge_rate) return NO;
11742 if (forward_weapon_temp > WEAPON_COOLING_CUTOUT * NPC_MAX_WEAPON_TEMP) return NO;
11743 if (range > weaponRange) return NO;
11744
11745 forward_weapon_temp += weapon_shot_temperature;
11746 [parent setEnergy:([parent energy] - weapon_energy_use)];
11747
11748 GLfloat hitAtRange = weaponRange;
11750 ShipEntity *victim = [UNIVERSE firstShipHitByLaserFromShip:self inDirection:direction offset:kZeroVector gettingRangeFound:&hitAtRange];
11751 [self setShipHitByLaser:victim];
11752
11753 OOLaserShotEntity *shot = [OOLaserShotEntity laserFromShip:self direction:direction offset:kZeroVector];
11754 [shot setColor:laser_color];
11755 [shot setScanClass:CLASS_NO_DRAW];
11756
11757 if (victim != nil)
11758 {
11759 [self adjustMissedShots:-1];
11760
11761 if ([self isPlayer])
11762 {
11763 [PLAYER addRoleForAggression:victim];
11764 }
11765
11766 ShipEntity *subent = [victim subEntityTakingDamage];
11767 if (subent != nil && [victim isFrangible])
11768 {
11769 // do 1% bleed-through damage...
11770 [victim takeEnergyDamage:0.01 * weapon_damage from:self becauseOf:parent weaponIdentifier:[[self weaponTypeForFacing:WEAPON_FACING_FORWARD strict:YES] identifier]];
11771 victim = subent;
11772 }
11773
11774 if (hitAtRange < weaponRange)
11775 {
11776 [victim takeEnergyDamage:weapon_damage from:self becauseOf:parent weaponIdentifier:[[self weaponTypeForFacing:WEAPON_FACING_FORWARD strict:YES] identifier]]; // a very palpable hit
11777
11778 [shot setRange:hitAtRange];
11779 Vector vd = vector_forward_from_quaternion([shot orientation]);
11780 HPVector flash_pos = HPvector_add([shot position], vectorToHPVector(vector_multiply_scalar(vd, hitAtRange)));
11781 [UNIVERSE addLaserHitEffectsAt:flash_pos against:victim damage:weapon_damage color:laser_color];
11782 }
11783 }
11784 else
11785 {
11786 [self adjustMissedShots:+1];
11787
11788 // see ATTACKER_MISSED section of main entity laser routine
11789 if (![parent isCloaked])
11790 {
11791 victim = [parent primaryTarget];
11792
11793 Vector shotDirection = vector_forward_from_quaternion([shot orientation]);
11794 Vector victimDirection = vector_normal(HPVectorToVector(HPvector_subtract([victim position], [parent position])));
11795 if (dot_product(shotDirection, victimDirection) > 0.995) // Within 84.26 degrees
11796 {
11797 if ([self isPlayer])
11798 {
11799 [PLAYER addRoleForAggression:victim];
11800 }
11801 [victim setPrimaryAggressor:parent];
11802 [victim setFoundTarget:parent];
11803 [victim reactToAIMessage:@"ATTACKER_MISSED" context:@"attacker narrowly misses"];
11804 [victim doScriptEvent:OOJSID("shipBeingAttackedUnsuccessfully") withArgument:parent];
11805
11806 }
11807 }
11808 }
11809
11810 [UNIVERSE addEntity:shot];
11811 [self resetShotTime];
11812
11813 return YES;
11814}
11815
11816
11817- (BOOL) fireDirectLaserShot:(double)range
11818{
11819 Entity *my_target = [self primaryTarget];
11820 if (my_target == nil) return [self fireDirectLaserDefensiveShot];
11821 if (range > randf() * weaponRange * (accuracy+5.5)) return [self fireDirectLaserDefensiveShot];
11822 if (range > weaponRange) return [self fireDirectLaserDefensiveShot];
11823 return [self fireDirectLaserShotAt:my_target];
11824}
11825
11826
11827- (BOOL) fireDirectLaserDefensiveShot
11828{
11829 NSEnumerator *targetEnum = [self defenseTargetEnumerator];
11830 Entity *target = nil;
11831 while ((target = [[targetEnum nextObject] weakRefUnderlyingObject]))
11832 {
11833 // can't fire defensively at cloaked ships
11834 if ([target scanClass] == CLASS_NO_DRAW || [(ShipEntity *)target isCloaked] || [target energy] <= 0.0)
11835 {
11836 [self removeDefenseTarget:target];
11837 }
11838 else
11839 {
11840 double range = [self rangeToSecondaryTarget:target];
11841 if (range < weaponRange)
11842 {
11843 return [self fireDirectLaserShotAt:target];
11844 }
11845 else if (range > scannerRange)
11846 {
11847 [self removeDefenseTarget:target];
11848 }
11849 }
11850 }
11851 return NO;
11852}
11853
11854
11855- (BOOL) fireDirectLaserShotAt:(Entity *)my_target
11856{
11857 GLfloat hit_at_range;
11858 double range_limit2 = weaponRange*weaponRange;
11859 Vector r_pos;
11860
11861 r_pos = vector_normal_or_zbasis([self vectorTo:my_target]);
11862
11863 Quaternion q_laser = quaternion_rotation_between(r_pos, kBasisZVector);
11864
11865 GLfloat acc_factor = (10.0 - accuracy) * 0.001;
11866
11867 q_laser.x += acc_factor * (randf() - 0.5); // randomise aim a little (+/- 0.005 at accuracy 0, never miss at accuracy 10)
11868 q_laser.y += acc_factor * (randf() - 0.5);
11869 q_laser.z += acc_factor * (randf() - 0.5);
11870 quaternion_normalize(&q_laser);
11871
11872 Quaternion q_save = orientation; // save rotation
11873 orientation = q_laser; // face in direction of laser
11874 // weapon offset for thargoid lasers is always zero
11875 ShipEntity *victim = [UNIVERSE firstShipHitByLaserFromShip:self inDirection:WEAPON_FACING_FORWARD offset:kZeroVector gettingRangeFound:&hit_at_range];
11876 [self setShipHitByLaser:victim];
11877 orientation = q_save; // restore rotation
11878
11879 Vector vel = vector_multiply_scalar(v_forward, flightSpeed);
11880
11881 // do special effects laser line
11882 OOLaserShotEntity *shot = [OOLaserShotEntity laserFromShip:self direction:WEAPON_FACING_FORWARD offset:kZeroVector];
11883 [shot setColor:laser_color];
11884 [shot setScanClass: CLASS_NO_DRAW];
11885 [shot setPosition: position];
11886 [shot setOrientation: q_laser];
11887 [shot setVelocity: vel];
11888
11889 if (victim != nil)
11890 {
11891 ShipEntity *subent = [victim subEntityTakingDamage];
11892 if (subent != nil && [victim isFrangible])
11893 {
11894 // do 1% bleed-through damage...
11895 [victim takeEnergyDamage: 0.01 * weapon_damage from:self becauseOf:self weaponIdentifier:[[self weaponTypeForFacing:WEAPON_FACING_FORWARD strict:YES] identifier]];
11896 victim = subent;
11897 }
11898
11899 if (hit_at_range * hit_at_range < range_limit2)
11900 {
11901 [victim takeEnergyDamage:weapon_damage from:self becauseOf:self weaponIdentifier:[[self weaponTypeForFacing:WEAPON_FACING_FORWARD strict:YES] identifier]]; // a very palpable hit
11902
11903 [shot setRange:hit_at_range];
11904 Vector vd = vector_forward_from_quaternion([shot orientation]);
11905 HPVector flash_pos = HPvector_add([shot position], vectorToHPVector(vector_multiply_scalar(vd, hit_at_range)));
11906 [UNIVERSE addLaserHitEffectsAt:flash_pos against:victim damage:weapon_damage color:laser_color];
11907 }
11908 }
11909
11910 [UNIVERSE addEntity:shot];
11911
11912 [self resetShotTime];
11913
11914 return YES;
11915}
11916
11917
11918- (NSArray *) laserPortOffset:(OOWeaponFacing)direction
11919{
11920 NSArray *laserPortOffset = nil;
11921 switch (direction)
11922 {
11924 case WEAPON_FACING_NONE:
11925 laserPortOffset = forwardWeaponOffset;
11926 break;
11927
11928 case WEAPON_FACING_AFT:
11929 laserPortOffset = aftWeaponOffset;
11930 break;
11931
11932 case WEAPON_FACING_PORT:
11933 laserPortOffset = portWeaponOffset;
11934 break;
11935
11937 laserPortOffset = starboardWeaponOffset;
11938 break;
11939 }
11940 return laserPortOffset;
11941}
11942
11943
11944- (BOOL) fireLaserShotInDirection:(OOWeaponFacing)direction weaponIdentifier:(NSString *)weaponIdentifier
11945{
11946 double range_limit2 = weaponRange * weaponRange;
11947 GLfloat hit_at_range;
11948 NSUInteger i, barrels;
11949 Vector vel = vector_multiply_scalar(v_forward, flightSpeed);
11950 NSArray *laserPortOffsets = [self laserPortOffset:direction];
11951 OOLaserShotEntity *shot = nil;
11952
11953 barrels = [laserPortOffsets count];
11954 NSMutableArray *shotEntities = [NSMutableArray arrayWithCapacity:barrels];
11955
11956
11957 GLfloat effective_damage = weapon_damage;
11958 if (barrels > 1 && !_multiplyWeapons)
11959 {
11960 // then divide the shot power between the shots
11961 effective_damage /= (GLfloat)barrels;
11962 }
11963
11964 for (i=0;i<barrels;i++)
11965 {
11966 Vector laserPortOffset = [laserPortOffsets oo_vectorAtIndex:i];
11967
11968 last_shot_time = [UNIVERSE getTime];
11969
11970 ShipEntity *victim = [UNIVERSE firstShipHitByLaserFromShip:self inDirection:direction offset:laserPortOffset gettingRangeFound:&hit_at_range];
11971 [self setShipHitByLaser:victim];
11972
11973 shot = [OOLaserShotEntity laserFromShip:self direction:direction offset:laserPortOffset];
11974 if ([self isPlayer])
11975 {
11976 [shotEntities addObject:shot];
11977 }
11978
11979 [shot setColor:laser_color];
11980 [shot setScanClass: CLASS_NO_DRAW];
11981 [shot setVelocity: vel];
11982
11983 if (victim != nil)
11984 {
11985 [self adjustMissedShots:-1];
11986 if ([self isPlayer])
11987 {
11988 [PLAYER addRoleForAggression:victim];
11989 }
11990
11991 /* CRASH in [victim->sub_entities containsObject:subent] here (1.69, OS X/x86).
11992 Analysis: Crash is in _freedHandler called from CFEqual, indicating either a dead
11993 object in victim->sub_entities or dead victim->subentity_taking_damage. I suspect
11994 the latter. Probable solution: dying subentities must cause parent to clean up
11995 properly. This was probably obscured by the entity recycling scheme in the past.
11996 Fix: made subentity_taking_damage a weak reference accessed via a method.
11997 -- Ahruman 20070706, 20080304
11998 */
11999 ShipEntity *subent = [victim subEntityTakingDamage];
12000 if (subent != nil && [victim isFrangible])
12001 {
12002 // do 1% bleed-through damage...
12003 [victim takeEnergyDamage: 0.01 * effective_damage from:self becauseOf:self weaponIdentifier:weaponIdentifier];
12004 victim = subent;
12005 }
12006
12007 if (hit_at_range * hit_at_range < range_limit2)
12008 {
12009 [victim takeEnergyDamage:effective_damage from:self becauseOf:self weaponIdentifier:weaponIdentifier]; // a very palpable hit
12010
12011 [shot setRange:hit_at_range];
12012 Vector vd = vector_forward_from_quaternion([shot orientation]);
12013 HPVector flash_pos = HPvector_add([shot position], vectorToHPVector(vector_multiply_scalar(vd, hit_at_range)));
12014 [UNIVERSE addLaserHitEffectsAt:flash_pos against:victim damage:effective_damage color:laser_color];
12015 }
12016 }
12017 else
12018 {
12019 [self adjustMissedShots:+1];
12020
12021 // shot missed
12022 if (![self isCloaked])
12023 {
12024 victim = [self primaryTarget];
12025 if ([victim isShip]) // it might not be - fixes crash bug
12026 {
12027
12028 /* player currently gets a bit of an advantage here if
12029 * they ambush without having their target actually
12030 * targeted. Though in those circumstances they
12031 * shouldn't be missing their first shot anyway. */
12032 if (dot_product(vector_forward_from_quaternion([shot orientation]),vector_normal([self vectorTo:victim])) > 0.995)
12033 {
12034 /* plausibly aimed at target. Allows reaction
12035 * before attacker actually hits. But we need to
12036 * be able to distinguish in AI from ATTACKED so
12037 * that ships in combat aren't bothered by
12038 * amateurs. So should only respond to
12039 * ATTACKER_MISSED if not already fighting */
12040 if ([self isPlayer])
12041 {
12042 [PLAYER addRoleForAggression:victim];
12043 }
12044 [victim setPrimaryAggressor:self];
12045 [victim setFoundTarget:self];
12046 [victim reactToAIMessage:@"ATTACKER_MISSED" context:@"attacker narrowly misses"];
12047 [victim doScriptEvent:OOJSID("shipBeingAttackedUnsuccessfully") withArgument:self];
12048 }
12049 }
12050 }
12051 }
12052
12053 [UNIVERSE addEntity:shot];
12054
12055 }
12056
12057 if ([self isPlayer])
12058 {
12059 [(PlayerEntity *)self setLastShot:shotEntities];
12060 }
12061
12062 [self resetShotTime];
12063
12064 return YES;
12065}
12066
12067
12068- (void) adjustMissedShots:(int) delta
12069{
12070 if ([self isSubEntity])
12071 {
12072 [[self owner] adjustMissedShots:delta];
12073 }
12074 else
12075 {
12076 _missed_shots += delta;
12077 if (_missed_shots < 0)
12078 {
12079 _missed_shots = 0;
12080 }
12081 }
12082}
12083
12084
12085- (int) missedShots
12086{
12087 if ([self isSubEntity])
12088 {
12089 return [[self owner] missedShots];
12090 }
12091 else
12092 {
12093 return _missed_shots;
12094 }
12095}
12096
12097
12098- (void) throwSparks
12099{
12100 Vector offset =
12101 {
12102 randf() * (boundingBox.max.x - boundingBox.min.x) + boundingBox.min.x,
12103 randf() * (boundingBox.max.y - boundingBox.min.y) + boundingBox.min.y,
12104 randf() * boundingBox.max.z + boundingBox.min.z // rear section only
12105 };
12106 HPVector origin = HPvector_add(position, vectorToHPVector(quaternion_rotate_vector([self normalOrientation], offset)));
12107
12108 float w = boundingBox.max.x - boundingBox.min.x;
12109 float h = boundingBox.max.y - boundingBox.min.y;
12110 float m = (w < h) ? 0.25 * w: 0.25 * h;
12111
12112 float sz = m * (1 + randf() + randf()); // half minimum dimension on average
12113
12114 Vector vel = vector_multiply_scalar(HPVectorToVector(HPvector_subtract(origin, position)), 2.0);
12115
12116 OOColor *color = [OOColor colorWithHue:0.08 + 0.17 * randf() saturation:1.0 brightness:1.0 alpha:1.0];
12117
12118 OOSparkEntity *spark = [[OOSparkEntity alloc] initWithPosition:origin
12119 velocity:vel
12120 duration:2.0 + 3.0 * randf()
12121 size:sz
12122 color:color];
12123
12124 [spark setOwner:self];
12125 [UNIVERSE addEntity:spark];
12126 [spark release];
12127
12128 next_spark_time = randf();
12129}
12130
12131
12132- (void) considerFiringMissile:(double)delta_t
12133{
12134 int missile_chance = 0;
12135 int rhs = 3.2 / delta_t;
12136 if (rhs) missile_chance = 1 + (ranrot_rand() % rhs);
12137
12138 double hurt_factor = 16 * pow(energy/maxEnergy, 4.0);
12139 if (missiles > missile_chance * hurt_factor)
12140 {
12141 [self fireMissile];
12142 }
12143}
12144
12145
12146- (Vector) missileLaunchPosition
12147{
12148 Vector start;
12149 // default launching position
12150 start.x = 0.0f; // in the middle
12151 start.y = boundingBox.min.y - 4.0f; // 4m below bounding box
12152 start.z = boundingBox.max.z + 1.0f; // 1m ahead of bounding box
12153
12154 // custom launching position
12155 start = [shipinfoDictionary oo_vectorForKey:@"missile_launch_position" defaultValue:start];
12156 if (EXPECT_NOT(_scaleFactor != 1.0))
12157 {
12158 start = vector_multiply_scalar(start,_scaleFactor);
12159 }
12160
12161 if (start.x == 0.0f && start.y == 0.0f && start.z <= 0.0f) // The kZeroVector as start is illegal also.
12162 {
12163 OOLog(@"ship.missileLaunch.invalidPosition", @"***** ERROR: The missile_launch_position defines a position %@ behind the %@. In future versions such missiles may explode on launch because they have to travel through the ship.", VectorDescription(start), self);
12164 start.x = 0.0f;
12165 start.y = boundingBox.min.y - 4.0f;
12166 start.z = boundingBox.max.z + 1.0f;
12167 }
12168 return start;
12169}
12170
12171
12172- (ShipEntity *) fireMissile
12173{
12174 return [self fireMissileWithIdentifier:nil andTarget:[self primaryTarget]];
12175}
12176
12177
12178- (ShipEntity *) fireMissileWithIdentifier:(NSString *) identifier andTarget:(Entity *) target
12179{
12180 // both players and NPCs!
12181 //
12182 ShipEntity *missile = nil;
12183 ShipEntity *target_ship = nil;
12184
12185 Vector vel;
12186 Vector start, v_eject;
12187
12188 if ([UNIVERSE getTime] < missile_launch_time) return nil;
12189
12190 start = [self missileLaunchPosition];
12191
12192 double throw_speed = 250.0f;
12193
12194 if ((missiles <= 0)||(target == nil)||([target scanClass] == CLASS_NO_DRAW)) // no missile lock!
12195 return nil;
12196
12197 if ([target isShip])
12198 {
12199 target_ship = (ShipEntity*)target;
12200 if ([target_ship isCloaked])
12201 {
12202 return nil;
12203 }
12204 // missile fire requires being in scanner range
12205 if (HPmagnitude2(HPvector_subtract([target_ship position], position)) > scannerRange * scannerRange)
12206 {
12207 return nil;
12208 }
12209 if (![self hasMilitaryScannerFilter] && [target_ship isJammingScanning])
12210 {
12211 return nil;
12212 }
12213 }
12214
12215 unsigned i;
12216 if (identifier == nil)
12217 {
12218 // use a random missile from the list
12219 i = floor(randf()*(double)missiles);
12220 identifier = [missile_list[i] identifier];
12221 missile = [UNIVERSE newShipWithRole:identifier];
12222 if (EXPECT_NOT(missile == nil)) // invalid missile role.
12223 {
12224 // remove that invalid missile role from the missiles list.
12225 while ( ++i < missiles ) missile_list[i - 1] = missile_list[i];
12226 missiles--;
12227 }
12228 }
12229 else
12230 missile = [UNIVERSE newShipWithRole:identifier];
12231
12232 if (EXPECT_NOT(missile == nil)) return nil;
12233
12234 // By definition, the player will always have the specified missile.
12235 // What if the NPC didn't actually have the specified missile to begin with?
12236 if (!isPlayer && ![self removeExternalStore:[OOEquipmentType equipmentTypeWithIdentifier:identifier]])
12237 {
12238 [missile release];
12239 return nil;
12240 }
12241
12242 double mcr = missile->collision_radius;
12243 v_eject = vector_normal(start);
12244 vel = kZeroVector; // starting velocity
12245
12246 // check if start is within bounding box...
12247 while ( (start.x > boundingBox.min.x - mcr)&&(start.x < boundingBox.max.x + mcr)&&
12248 (start.y > boundingBox.min.y - mcr)&&(start.y < boundingBox.max.y + mcr)&&
12249 (start.z > boundingBox.min.z - mcr)&&(start.z < boundingBox.max.z + mcr) )
12250 {
12251 start = vector_add(start, vector_multiply_scalar(v_eject, mcr));
12252 }
12253
12254 vel = vector_add(vel, vector_multiply_scalar(v_forward, flightSpeed + throw_speed));
12255
12256 Quaternion q1 = [self normalOrientation];
12257 HPVector origin = HPvector_add(position, vectorToHPVector(quaternion_rotate_vector(q1, start)));
12258
12259 if (isPlayer) [missile setScanClass: CLASS_MISSILE];
12260
12261// special cases
12262
12263 //We don't want real missiles in a group. Missiles could become escorts when the group is also used as escortGroup.
12264 if ([missile scanClass] == CLASS_THARGOID)
12265 {
12266 if([self group] == nil) [self setGroup:[OOShipGroup groupWithName:@"thargoid group"]];
12267
12268 ShipEntity *thisGroupLeader = [_group leader];
12269
12270 if ([thisGroupLeader escortGroup] != _group) // avoid adding tharons to escort groups
12271 {
12272 [missile setGroup:[self group]];
12273 }
12274 }
12275
12276 // is this a submunition?
12277 if (![self isMissileFlagSet]) [missile setOwner:self];
12278 else [missile setOwner:[self owner]];
12279
12280// end special cases
12281
12282 [missile setPosition:origin];
12283 [missile addTarget:target];
12284 [missile setOrientation:q1];
12285 [missile setIsMissileFlag:YES];
12286 [missile setVelocity:vel];
12287 [missile setSpeed:150.0f];
12288 [missile setDistanceTravelled:0.0f];
12289 [missile resetShotTime];
12290 missile_launch_time = [UNIVERSE getTime] + missile_load_time; // set minimum launchtime for the next missile.
12291
12292 [UNIVERSE addEntity:missile]; // STATUS_IN_FLIGHT, AI state GLOBAL
12293 [missile release]; //release
12294
12295 // missile lives on after UNIVERSE addEntity
12296 if ([missile isMissile] && [target isShip])
12297 {
12298 [self doScriptEvent:OOJSID("shipFiredMissile") withArgument:missile andArgument:target_ship];
12299 [target_ship setPrimaryAggressor:self];
12300 [target_ship doScriptEvent:OOJSID("shipAttackedWithMissile") withArgument:missile andArgument:self];
12301 [target_ship reactToAIMessage:@"INCOMING_MISSILE" context:@"someone's shooting at me!"];
12302 if (cloaking_device_active && cloakPassive)
12303 {
12304 // parity between player &NPCs, only deactivate cloak for missiles
12305 [self deactivateCloakingDevice];
12306 }
12307 }
12308 else
12309 {
12310 [self doScriptEvent:OOJSID("shipReleasedEquipment") withArgument:missile];
12311 }
12312
12313 return missile;
12314}
12315
12316
12317- (BOOL) isMissileFlagSet
12318{
12319 return isMissile; // were we created using fireMissile? (for tracking submunitions and preventing collisions at launch)
12320}
12321
12322
12323- (void) setIsMissileFlag:(BOOL)newValue
12324{
12325 isMissile = !!newValue; // set the isMissile flag, used for tracking submunitions and preventing collisions at launch.
12326}
12327
12328
12329- (OOTimeDelta) missileLoadTime
12330{
12331 return missile_load_time;
12332}
12333
12334
12335- (void) setMissileLoadTime:(OOTimeDelta)newMissileLoadTime
12336{
12337 missile_load_time = fmax(0.0, newMissileLoadTime);
12338}
12339
12340
12341// reactions to ECM that are not dependent on current AI state here
12342- (void) noticeECM
12343{
12344 if (accuracy >= COMBAT_AI_ISNT_AWFUL && missiles > 0 && [[missile_list[0] identifier] isEqualTo:@"EQ_MISSILE"])
12345 {
12346// if we're being ECMd, and our missiles appear to be standard, and we
12347// have some combat sense, wait a bit before firing the next one!
12348 missile_launch_time = [UNIVERSE getTime] + fmax(2.0,missile_load_time); // set minimum launchtime for the next missile.
12349 }
12350}
12351
12352
12353// Exposed to AI
12354- (BOOL) fireECM
12355{
12356 if (![self hasECM]) return NO;
12357
12358 OOECMBlastEntity *ecmDevice = [[OOECMBlastEntity alloc] initFromShip:self];
12359 [UNIVERSE addEntity:ecmDevice];
12360 [ecmDevice release];
12361 return YES;
12362}
12363
12364
12365- (BOOL) activateCloakingDevice
12366{
12367 if (![self hasCloakingDevice] || cloaking_device_active) return cloaking_device_active; // no changes.
12368
12369 if (!cloaking_device_active) cloaking_device_active = (energy > CLOAKING_DEVICE_START_ENERGY * maxEnergy);
12370 if (cloaking_device_active) [self doScriptEvent:OOJSID("shipCloakActivated")];
12371 return cloaking_device_active;
12372}
12373
12374
12375- (void) deactivateCloakingDevice
12376{
12377 if ([self hasCloakingDevice] && cloaking_device_active)
12378 {
12379 cloaking_device_active = NO;
12380 [self doScriptEvent:OOJSID("shipCloakDeactivated")];
12381 }
12382}
12383
12384
12385- (BOOL) launchCascadeMine
12386{
12387 if (![self hasCascadeMine]) return NO;
12388 [self setSpeed: maxFlightSpeed + 300];
12389 ShipEntity* bomb = [UNIVERSE newShipWithRole:@"energy-bomb"];
12390 if (bomb == nil) return NO;
12391
12392 [self removeEquipmentItem:@"EQ_QC_MINE"];
12393
12394 double start = collision_radius + bomb->collision_radius;
12395 Quaternion random_direction;
12396 Vector vel;
12397 HPVector rpos;
12398 double random_roll = randf() - 0.5; // -0.5 to +0.5
12399 double random_pitch = randf() - 0.5; // -0.5 to +0.5
12400 quaternion_set_random(&random_direction);
12401
12402 rpos = HPvector_subtract([self position], vectorToHPVector(vector_multiply_scalar(v_forward, start)));
12403
12404 double eject_speed = -800.0;
12405 vel = vector_multiply_scalar(v_forward, [self flightSpeed] + eject_speed);
12406 eject_speed *= 0.5 * (randf() - 0.5); // -0.25x .. +0.25x
12407 vel = vector_add(vel, vector_multiply_scalar(v_up, eject_speed));
12408 eject_speed *= 0.5 * (randf() - 0.5); // -0.0625x .. +0.0625x
12409 vel = vector_add(vel, vector_multiply_scalar(v_right, eject_speed));
12410
12411 [bomb setPosition:rpos];
12412 [bomb setOrientation:random_direction];
12413 [bomb setRoll:random_roll];
12414 [bomb setPitch:random_pitch];
12415 [bomb setVelocity:vel];
12416 [bomb setScanClass:CLASS_MINE];
12417 [bomb setEnergy:5.0]; // 5 second countdown
12418 [bomb setBehaviour:BEHAVIOUR_ENERGY_BOMB_COUNTDOWN];
12419 [bomb setOwner:self];
12420 [UNIVERSE addEntity:bomb]; // STATUS_IN_FLIGHT, AI state GLOBAL
12421 [bomb release];
12422
12423 if (cloaking_device_active && cloakPassive)
12424 {
12425 [self deactivateCloakingDevice];
12426 }
12427
12428 if (self != PLAYER) // get the heck out of here
12429 {
12430 [self addTarget:bomb];
12431 [self setBehaviour:BEHAVIOUR_FLEE_TARGET];
12432 frustration = 0.0;
12433 }
12434 return YES;
12435}
12436
12437
12438- (ShipEntity*)launchEscapeCapsule
12439{
12440 ShipEntity *result = nil;
12441 ShipEntity *mainPod = nil;
12442 unsigned n_pods, i;
12443 NSMutableArray *passengers = nil;
12444
12445 /*
12446 CHANGE: both player & NPCs can now launch escape pods in interstellar
12447 space. -- Kaks 20101113
12448 */
12449
12450 // check number of pods aboard -- require at least one.
12451 n_pods = [shipinfoDictionary oo_unsignedIntForKey:@"has_escape_pod"];
12452 if (n_pods > 65) n_pods = 65; // maximum of 64 passengers.
12453 if (n_pods > 1) passengers = [NSMutableArray arrayWithCapacity:n_pods-1];
12454
12455 if (crew) // transfer crew
12456 {
12457 // make sure crew inherit any legalStatus
12458 for (i = 0; i < [crew count]; i++)
12459 {
12460 OOCharacter *ch = (OOCharacter*)[crew objectAtIndex:i];
12461 [ch setLegalStatus: [self legalStatus] | [ch legalStatus]];
12462 }
12463 mainPod = [self launchPodWithCrew:crew];
12464 if (mainPod)
12465 {
12466 result = mainPod;
12467 [self setCrew:nil];
12468 [self setHulk:YES]; // we are without crew now.
12469 }
12470 }
12471
12472 // launch other pods (passengers)
12473 for (i = 1; i < n_pods; i++)
12474 {
12475 ShipEntity *passenger = nil;
12476 passenger = [self launchPodWithCrew:[NSArray arrayWithObject:[OOCharacter randomCharacterWithRole:@"passenger" andOriginalSystem:gen_rnd_number()]]];
12477 [passengers addObject:passenger];
12478 }
12479
12480 if (mainPod) [self doScriptEvent:OOJSID("shipLaunchedEscapePod") withArgument:mainPod andArgument:passengers];
12481
12482 return result;
12483}
12484
12485
12486// This is a documented AI method; do not change semantics. (Note: AIs don't have access to the return value.)
12487- (OOCommodityType) dumpCargo
12488{
12489 ShipEntity *jetto = [self dumpCargoItem:nil];
12490 if (jetto != nil)
12491 {
12492 return [jetto commodityType];
12493 }
12494 else
12495 {
12496 return nil;
12497 }
12498}
12499
12500
12501- (ShipEntity *) dumpCargoItem:(OOCommodityType)preferred
12502{
12503 ShipEntity *jetto = nil;
12504 NSUInteger i = 0;
12505
12506 if (([cargo count] > 0)&&([UNIVERSE getTime] - cargo_dump_time > 0.5)) // space them 0.5s or 10m apart
12507 {
12508 if (preferred == nil)
12509 {
12510 jetto = [[[cargo objectAtIndex:0] retain] autorelease];
12511 }
12512 else
12513 {
12514 BOOL found = NO;
12515 for (i=0;i<[cargo count];i++)
12516 {
12517 if ([[[cargo objectAtIndex:i] commodityType] isEqualToString:preferred])
12518 {
12519 jetto = [[[cargo objectAtIndex:i] retain] autorelease];
12520 found = YES;
12521 break;
12522 }
12523 }
12524 if (found == NO)
12525 {
12526 // dump anything
12527 jetto = [[[cargo objectAtIndex:0] retain] autorelease];
12528 i = 0;
12529 }
12530 }
12531 if (jetto != nil)
12532 {
12533 [self dumpItem:jetto]; // CLASS_CARGO, STATUS_IN_FLIGHT, AI state GLOBAL
12534 [cargo removeObjectAtIndex:i];
12535 [self broadcastAIMessage:@"CARGO_DUMPED"]; // goes only to 16 nearby ships in range, but that should be enough.
12536 unsigned i;
12537 // only send script event to powered entities
12538 [self checkScannerIgnoringUnpowered];
12539 for (i = 0; i < n_scanned_ships ; i++)
12540 {
12541 ShipEntity* other = scanned_ships[i];
12542 [other doScriptEvent:OOJSID("cargoDumpedNearby") withArgument:jetto andArgument:self];
12543
12544 }
12545 }
12546 }
12547
12548 return jetto;
12549}
12550
12551
12552- (OOCargoType) dumpItem: (ShipEntity*) cargoObj
12553{
12554 if (!cargoObj)
12555 return 0;
12556
12557 ShipEntity* jetto = [UNIVERSE reifyCargoPod:cargoObj];
12558
12559 int result = [jetto cargoType];
12560 AI *jettoAI = nil;
12561 Vector start;
12562
12563 // players get to see their old ship sailing forth, while NPCs run away more efficiently!
12564 // cargo is ejected at higher speed from any ship
12565 double eject_speed = EXPECT_NOT([jetto crew] && [jetto isPlayer]) ? 20.0 : 100.0;
12566 double eject_reaction = -eject_speed * [jetto mass] / [self mass];
12567 double jcr = jetto->collision_radius;
12568
12569 Quaternion jetto_orientation = kIdentityQuaternion;
12570 Vector vel, v_eject, v_eject_normal;
12571 HPVector rpos = [self absolutePositionForSubentity];
12572 double jetto_roll = 0;
12573 double jetto_pitch = 0;
12574
12575 // default launching position
12576 start.x = 0.0; // in the middle
12577 start.y = 0.0; //
12578 start.z = boundingBox.min.z - jcr; // 1m behind of bounding box
12579
12580 // custom launching position
12581 start = [shipinfoDictionary oo_vectorForKey:@"aft_eject_position" defaultValue:start];
12582
12583 v_eject = vector_normal(start);
12584
12585 // check if start is within bounding box...
12586 while ( (start.x > boundingBox.min.x - jcr)&&(start.x < boundingBox.max.x + jcr)&&
12587 (start.y > boundingBox.min.y - jcr)&&(start.y < boundingBox.max.y + jcr)&&
12588 (start.z > boundingBox.min.z - jcr)&&(start.z < boundingBox.max.z + jcr))
12589 {
12590 start = vector_add(start, vector_multiply_scalar(v_eject, jcr));
12591 }
12592
12593 v_eject = quaternion_rotate_vector([self normalOrientation], start);
12594 rpos = HPvector_add(rpos, vectorToHPVector(v_eject));
12595 v_eject = vector_normal(v_eject);
12596 v_eject_normal = v_eject;
12597
12598 v_eject.x += (randf() - randf())/eject_speed;
12599 v_eject.y += (randf() - randf())/eject_speed;
12600 v_eject.z += (randf() - randf())/eject_speed;
12601
12602 vel = vector_add(vector_multiply_scalar(v_forward, flightSpeed), vector_multiply_scalar(v_eject, eject_speed));
12603 velocity = vector_add(velocity, vector_multiply_scalar(v_eject, eject_reaction));
12604
12605 [jetto setPosition:rpos];
12606 if ([jetto crew]) // jetto has a crew, so assume it is an escape pod.
12607 {
12608 // orient the pod away from the ship to avoid colliding with it.
12609 jetto_orientation = quaternion_rotation_between(v_eject_normal, kBasisZVector);
12610 }
12611 else
12612 {
12613 // It is true cargo, let it tumble.
12614 jetto_roll = ((ranrot_rand() % 1024) - 512.0)/1024.0; // -0.5 to +0.5
12615 jetto_pitch = ((ranrot_rand() % 1024) - 512.0)/1024.0; // -0.5 to +0.5
12616 quaternion_set_random(&jetto_orientation);
12617 }
12618
12619 [jetto setOrientation:jetto_orientation];
12620 [jetto setRoll:jetto_roll];
12621 [jetto setPitch:jetto_pitch];
12622 [jetto setVelocity:vel];
12623 [jetto setScanClass: CLASS_CARGO];
12624 [jetto setTemperature:[self randomEjectaTemperature]];
12625 [UNIVERSE addEntity:jetto]; // STATUS_IN_FLIGHT, AI state GLOBAL
12626
12627 jettoAI = [jetto getAI];
12628 if ([jettoAI hasSuspendedStateMachines]) // check if this was previous scooped cargo.
12629 {
12630 [jetto setThrust:[jetto maxThrust]]; // restore old thrust.
12631 [jetto setOwner:jetto];
12632 [jettoAI exitStateMachineWithMessage:nil]; // exit nullAI.
12633 }
12634 [jetto doScriptEvent:OOJSID("shipWasDumped") withArgument:self];
12635 [self doScriptEvent:OOJSID("shipDumpedCargo") withArgument:jetto];
12636
12637 cargo_dump_time = [UNIVERSE getTime];
12638 return result;
12639}
12640
12641
12642- (void) manageCollisions
12643{
12644 // deal with collisions
12645 //
12646 Entity* ent;
12647 ShipEntity* other_ship;
12648
12649 while ([collidingEntities count] > 0)
12650 {
12651 // EMMSTRAN: investigate if doing this backwards would be more efficient. (Not entirely obvious, NSArray is kinda funky.) -- Ahruman 2011-02-12
12652 ent = [[[collidingEntities objectAtIndex:0] retain] autorelease];
12653 [collidingEntities removeObjectAtIndex:0];
12654 if (ent)
12655 {
12656 if ([ent isShip])
12657 {
12658 other_ship = (ShipEntity *)ent;
12659 [self collideWithShip:other_ship];
12660 }
12661 else if ([ent isStellarObject])
12662 {
12663 [self getDestroyedBy:ent damageType:[ent isSun] ? kOODamageTypeHitASun : kOODamageTypeHitAPlanet];
12664 if (self == PLAYER) [self retain];
12665 }
12666 else if ([ent isWormhole])
12667 {
12668 if( [self isPlayer] ) [self enterWormhole:(WormholeEntity*)ent];
12669 else [self enterWormhole:(WormholeEntity*)ent replacing:NO];
12670 }
12671 }
12672 }
12673}
12674
12675
12676- (BOOL) collideWithShip:(ShipEntity *)other
12677{
12678 HPVector hploc;
12679 Vector loc;
12680 double dam1, dam2;
12681
12682 if (!other)
12683 return NO;
12684
12685 ShipEntity* otherParent = [other parentEntity];
12686 BOOL otherIsStation = other == [UNIVERSE station];
12687 // calculate line of centers using centres
12688 hploc = HPvector_normal_or_zbasis(HPvector_subtract([other absolutePositionForSubentity], position));
12689 loc = HPVectorToVector(hploc);
12690
12691
12692 if ([self canScoop:other])
12693 {
12694 [self scoopIn:other];
12695 return NO;
12696 }
12697 if ([other canScoop:self])
12698 {
12699 [other scoopIn:self];
12700 return NO;
12701 }
12702 if (universalID == NO_TARGET)
12703 return NO;
12704 if (other->universalID == NO_TARGET)
12705 return NO;
12706
12707 // find velocity along line of centers
12708 //
12709 // momentum = mass x velocity
12710 // ke = mass x velocity x velocity
12711 //
12712 GLfloat m1 = mass; // mass of self
12713 GLfloat m2 = [other mass]; // mass of other
12714
12715 // starting velocities:
12716 Vector v, vel1b = [self velocity];
12717
12718 if (otherParent != nil)
12719 {
12720 // Subentity
12721 /* TODO: if the subentity is rotating (subentityRotationalVelocity is
12722 not 1 0 0 0) we should calculate the tangential velocity from the
12723 other's position relative to our absolute position and add that in.
12724 */
12725 v = [otherParent velocity];
12726 }
12727 else
12728 {
12729 v = [other velocity];
12730 }
12731
12732 v = vector_subtract(vel1b, v);
12733
12734 GLfloat v2b = dot_product(v, loc); // velocity of other along loc before collision
12735
12736 GLfloat v1a = sqrt(v2b * v2b * m2 / m1); // velocity of self along loc after elastic collision
12737 if (v2b < 0.0f) v1a = -v1a; // in same direction as v2b
12738
12739 // are they moving apart at over 1m/s already?
12740 if (v2b < 0.0f)
12741 {
12742 if (v2b < -1.0f) return NO;
12743 else
12744 {
12745 position = HPvector_subtract(position, hploc); // adjust self position
12746 v = kZeroVector; // go for the 1m/s solution
12747 }
12748 }
12749
12750 // convert change in velocity into damage energy (KE)
12751 dam1 = m2 * v2b * v2b / 50000000;
12752 dam2 = m1 * v2b * v2b / 50000000;
12753
12754 // calculate adjustments to velocity after collision
12755 Vector vel1a = vector_multiply_scalar(loc, -v1a);
12756 Vector vel2a = vector_multiply_scalar(loc, v2b);
12757
12758 if (magnitude2(v) <= 0.1) // virtually no relative velocity - we must provide at least 1m/s to avoid conjoined objects
12759 {
12760 vel1a = vector_multiply_scalar(loc, -1);
12761 vel2a = loc;
12762 }
12763
12764 // apply change in velocity
12765 if (otherParent != nil)
12766 {
12767 [otherParent adjustVelocity:vel2a]; // move the otherParent not the subentity
12768 }
12769 else
12770 {
12771 [other adjustVelocity:vel2a];
12772 }
12773
12774 [self adjustVelocity:vel1a];
12775
12776 BOOL selfDestroyed = (dam1 > energy);
12777 BOOL otherDestroyed = (dam2 > [other energy]) && !otherIsStation;
12778
12779 if (dam1 > 0.05)
12780 {
12781 [self takeScrapeDamage: dam1 from:other];
12782 if (selfDestroyed) // inelastic! - take xplosion velocity damage instead
12783 {
12784 vel2a = vector_multiply_scalar(vel2a, -1);
12785 [other adjustVelocity:vel2a];
12786 }
12787 }
12788
12789 if (dam2 > 0.05)
12790 {
12791 if (otherParent != nil && ![otherParent isFrangible])
12792 {
12793 [otherParent takeScrapeDamage: dam2 from:self];
12794 }
12795 else
12796 {
12797 [other takeScrapeDamage: dam2 from:self];
12798 }
12799
12800 if (otherDestroyed) // inelastic! - take explosion velocity damage instead
12801 {
12802 vel1a = vector_multiply_scalar(vel1a, -1);
12803 [self adjustVelocity:vel1a];
12804 }
12805 }
12806
12807 if (!selfDestroyed && !otherDestroyed)
12808 {
12809 float t = 10.0 * [UNIVERSE getTimeDelta]; // 10 ticks
12810
12811 HPVector pos1a = HPvector_add([self position], vectorToHPVector(vector_multiply_scalar(loc, t * v1a)));
12812 [self setPosition:pos1a];
12813
12814 if (!otherIsStation)
12815 {
12816 HPVector pos2a = HPvector_add([other position], vectorToHPVector(vector_multiply_scalar(loc, t * v2b)));
12817 [other setPosition:pos2a];
12818 }
12819 }
12820
12821 // remove self from other's collision list
12822 [[other collisionArray] removeObject:self];
12823
12824 [self doScriptEvent:OOJSID("shipCollided") withArgument:other andReactToAIMessage:@"COLLISION"];
12825 [other doScriptEvent:OOJSID("shipCollided") withArgument:self andReactToAIMessage:@"COLLISION"];
12826
12827 return YES;
12828}
12829
12830
12831- (Vector) thrustVector
12832{
12833 return vector_multiply_scalar(v_forward, flightSpeed);
12834}
12835
12836
12837- (Vector) velocity
12838{
12839 return vector_add([super velocity], [self thrustVector]);
12840}
12841
12842
12843- (void) setTotalVelocity:(Vector)vel
12844{
12845 [self setVelocity:vector_subtract(vel, [self thrustVector])];
12846}
12847
12848
12849- (void) adjustVelocity:(Vector) xVel
12850{
12851 velocity = vector_add(velocity, xVel);
12852}
12853
12854
12855- (void) addImpactMoment:(Vector) moment fraction:(GLfloat) howmuch
12856{
12857 velocity = vector_add(velocity, vector_multiply_scalar(moment, howmuch / mass));
12858}
12859
12860
12861- (BOOL) canScoop:(ShipEntity*)other
12862{
12863 if (other == nil) return NO;
12864 if (![self hasCargoScoop]) return NO;
12865 if ([cargo count] >= [self maxAvailableCargoSpace]) return NO;
12866 if (scanClass == CLASS_CARGO) return NO; // we have no power so we can't scoop
12867 if ([other scanClass] != CLASS_CARGO) return NO;
12868 if ([other cargoType] == CARGO_NOT_CARGO) return NO;
12869
12870 if ([other isStation]) return NO;
12871
12872 HPVector loc = HPvector_between(position, [other position]);
12873
12874 if (dot_product(v_forward, HPVectorToVector(loc)) < 0.0f) return NO; // Must be in front of us
12875 if ([self isPlayer] && dot_product(v_up, HPVectorToVector(loc)) > 0.0f) return NO; // player has to scoop on underside, give more flexibility to NPCs
12876
12877 return YES;
12878}
12879
12880
12881- (void) getTractoredBy:(ShipEntity *)other
12882{
12883 if([self status] == STATUS_BEING_SCOOPED) return; // both cargo and ship call this. Act only once.
12884 desired_speed = 0.0;
12885 [self setAITo:@"nullAI.plist"]; // prevent AI from changing status or behaviour.
12886 behaviour = BEHAVIOUR_TRACTORED;
12887 [self setStatus:STATUS_BEING_SCOOPED];
12888 [self addTarget:other];
12889 [self setOwner:other];
12890 // should we make this an all rather than first 16? - CIM
12891 // made it ignore other cargopods and similar at least. - CIM 28/7/2013
12892 [self checkScannerIgnoringUnpowered];
12893 unsigned i;
12894 ShipEntity *scooper;
12895 for (i = 0; i < n_scanned_ships ; i++)
12896 {
12897 scooper = (ShipEntity *)scanned_ships[i];
12898 // 'Dibs!' - Stops other ships from trying to scoop/shoot this cargo.
12899 if (other != scooper && (id) self == [scooper primaryTarget])
12900 {
12901 [scooper noteLostTarget];
12902 }
12903 }
12904}
12905
12906
12907- (void) scoopIn:(ShipEntity *)other
12908{
12909 [other getTractoredBy:self];
12910}
12911
12912
12913- (void) suppressTargetLost
12914{
12915
12916}
12917
12918
12919- (void) scoopUp:(ShipEntity *)other
12920{
12921 [self scoopUpProcess:other processEvents:YES processMessages:YES];
12922}
12923
12924
12925- (void) scoopUpProcess:(ShipEntity *)other processEvents:(BOOL) procEvents processMessages:(BOOL) procMessages
12926{
12927 if (other == nil) return;
12928
12929 OOCommodityType co_type = nil;
12930 OOCargoQuantity co_amount;
12931
12932 // don't even think of trying to scoop if the cargo hold is already full
12933 if (max_cargo && [cargo count] >= [self maxAvailableCargoSpace])
12934 {
12935 [other setStatus:STATUS_IN_FLIGHT];
12936 return;
12937 }
12938
12939 switch ([other cargoType])
12940 {
12941 case CARGO_RANDOM:
12942 co_type = [other commodityType];
12943 co_amount = [other commodityAmount];
12944 break;
12945
12947 {
12948 //scripting
12949 PlayerEntity *player = PLAYER;
12950 [player setScriptTarget:self];
12951 if (procEvents)
12952 {
12953 [other doScriptEvent:OOJSID("shipWasScooped") withArgument:self];
12954 }
12955
12956 if ([other commodityType] != nil)
12957 {
12958 co_type = [other commodityType];
12959 co_amount = [other commodityAmount];
12960 // don't show scoop message now, will happen later.
12961 }
12962 else
12963 {
12964 if (isPlayer && [other showScoopMessage] && procMessages)
12965 {
12966 [UNIVERSE clearPreviousMessage];
12967 NSString *shipName = [other displayName];
12968 [UNIVERSE addMessage:OOExpandKey(@"scripted-item-scooped", shipName) forCount:4];
12969 }
12970 [other setCommodityForPod:nil andAmount:0];
12971 co_amount = 0;
12972 co_type = nil;
12973 }
12974 }
12975 break;
12976
12977 default :
12978 co_amount = 0;
12979 co_type = nil;
12980 break;
12981 }
12982
12983 /* Bug: docking failed due to NSRangeException while looking for element
12984 NSNotFound of cargo mainfest in -[PlayerEntity unloadCargoPods].
12985 Analysis: bad cargo pods being generated due to
12986 -[Universe commodityForName:] looking in wrong place for names.
12987 Fix 1: fix -[Universe commodityForName:].
12988 Fix 2: catch NSNotFound here and substitute random cargo type.
12989 -- Ahruman 20070714
12990 */
12991 if (co_type == nil && co_amount > 0)
12992 {
12993 co_type = [UNIVERSE getRandomCommodity];
12994 co_amount = [UNIVERSE getRandomAmountOfCommodity:co_type];
12995 }
12996
12997 if (co_amount > 0)
12998 {
12999 [other setCommodity:co_type andAmount:co_amount]; // belt and braces setting this!
13000 cargo_flag = CARGO_FLAG_CANISTERS;
13001
13002 if (isPlayer)
13003 {
13004 if ([other crew])
13005 {
13006 if ([other showScoopMessage] && procMessages)
13007 {
13008 [UNIVERSE clearPreviousMessage];
13009 unsigned i;
13010 for (i = 0; i < [[other crew] count]; i++)
13011 {
13012 OOCharacter *rescuee = [[other crew] objectAtIndex:i];
13013 NSString *characterName = [rescuee name];
13014 if ([rescuee legalStatus])
13015 {
13016 [UNIVERSE addMessage:OOExpandKey(@"scoop-captured-character", characterName) forCount: 4.5];
13017 }
13018 else if ([rescuee insuranceCredits])
13019 {
13020 [UNIVERSE addMessage:OOExpandKey(@"scoop-rescued-character", characterName) forCount: 4.5];
13021 }
13022 else
13023 {
13024 [UNIVERSE addMessage: DESC(@"scoop-got-slave") forCount: 4.5];
13025 }
13026 }
13027 }
13028 if (procEvents)
13029 {
13030 [(PlayerEntity *)self playEscapePodScooped];
13031 }
13032 }
13033 else
13034 {
13035 if ([other showScoopMessage] && procMessages)
13036 {
13037 [UNIVERSE clearPreviousMessage];
13038 [UNIVERSE addMessage:[UNIVERSE describeCommodity:co_type amount:co_amount] forCount:4.5];
13039 }
13040 }
13041 }
13042 [cargo insertObject:other atIndex:0]; // places most recently scooped object at eject position
13043 [other setStatus:STATUS_IN_HOLD];
13044 [other performTumble];
13045 [shipAI message:@"CARGO_SCOOPED"];
13046 if (max_cargo && [cargo count] >= [self maxAvailableCargoSpace]) [shipAI message:@"HOLD_FULL"];
13047 }
13048 if (procEvents)
13049 {
13050 [self doScriptEvent:OOJSID("shipScoopedOther") withArgument:other]; // always fire, even without commodity.
13051 }
13052
13053 // if shipScoopedOther does something strange to the object, we must
13054 // then remove it from the hold, or it will be over-retained
13055 if ([other status] != STATUS_IN_HOLD)
13056 {
13057 if ([cargo containsObject:other])
13058 {
13059 [cargo removeObject:other];
13060 }
13061 }
13062
13063 [[other collisionArray] removeObject:self]; // so it can't be scooped twice!
13064 // make sure other ships trying to scoop it lose it
13065 // probably already happened, but some may have acquired it
13066 // after the scooping started, and they might get stuck in a scooping
13067 // attempt as a result
13068 [self checkScannerIgnoringUnpowered];
13069 unsigned i;
13070 ShipEntity *scooper;
13071 for (i = 0; i < n_scanned_ships ; i++)
13072 {
13073 scooper = (ShipEntity *)scanned_ships[i];
13074 if (self != scooper && (id) other == [scooper primaryTargetWithoutValidityCheck])
13075 {
13076 [scooper noteLostTarget];
13077 }
13078 }
13079
13080 [self suppressTargetLost];
13081 [UNIVERSE removeEntity:other];
13082}
13083
13084
13085- (BOOL) cascadeIfAppropriateWithDamageAmount:(double)amount cascadeOwner:(Entity *)owner
13086{
13087 BOOL cascade = NO;
13088 switch ([self scanClass])
13089 {
13090 case CLASS_WORMHOLE:
13091 case CLASS_ROCK:
13092 case CLASS_CARGO:
13093 case CLASS_VISUAL_EFFECT:
13094 case CLASS_BUOY:
13095 // does not normally cascade
13096 if ((fuel > MIN_FUEL) || isStation)
13097 {
13098 //we have fuel onboard so we can still go pop, or we are a station which can
13099 }
13100 else break;
13101
13102 case CLASS_STATION:
13103 case CLASS_MINE:
13104 case CLASS_PLAYER:
13105 case CLASS_POLICE:
13106 case CLASS_MILITARY:
13107 case CLASS_THARGOID:
13108 case CLASS_MISSILE:
13109 case CLASS_NOT_SET:
13110 case CLASS_NO_DRAW:
13111 case CLASS_NEUTRAL:
13112 case CLASS_TARGET:
13113 // ...start a chain reaction, if we're dying and have a non-trivial amount of energy.
13114 if (energy < amount && energy > 10 && [self countsAsKill])
13115 {
13116 cascade = YES; // confirm we're cascading, then try to add our cascade to UNIVERSE.
13117 [UNIVERSE addEntity:[OOQuiriumCascadeEntity quiriumCascadeFromShip:self]];
13118 }
13119 break;
13120 //no default thanks, we want the compiler to tell us if we missed a case.
13121 }
13122 return cascade;
13123}
13124
13125
13126- (void) takeEnergyDamage:(double)amount from:(Entity *)ent becauseOf:(Entity *)other weaponIdentifier:(NSString *)weaponIdentifier
13127{
13128 if ([self status] == STATUS_DEAD) return;
13129 if (amount <= 0.0) return;
13130
13131 BOOL energyMine = [ent isCascadeWeapon];
13132 BOOL cascade = NO;
13133 if (energyMine)
13134 {
13135 cascade = [self cascadeIfAppropriateWithDamageAmount:amount cascadeOwner:[ent owner]];
13136 }
13137
13138 energy -= amount;
13139 /* Heat increase from energy impacts will never directly cause
13140 * overheating - too easy for missile hits to cause an uncredited
13141 * death by overheating - CIM */
13142 if (ship_temperature < SHIP_MAX_CABIN_TEMP)
13143 {
13144 ship_temperature += amount * SHIP_ENERGY_DAMAGE_TO_HEAT_FACTOR / [self heatInsulation];
13145 if (ship_temperature > SHIP_MAX_CABIN_TEMP)
13146 {
13147 ship_temperature = SHIP_MAX_CABIN_TEMP;
13148 }
13149 }
13150
13151
13152 being_mined = NO;
13153 ShipEntity *hunter = nil;
13154
13155 hunter = [other rootShipEntity];
13156 if (hunter == nil && [other isShip]) hunter = (ShipEntity *)other;
13157
13158 // must check for this before potentially deleting 'other' for cloaking
13159 if ((other)&&([other isShip]))
13160 {
13161 being_mined = [(ShipEntity *)other isMining];
13162 }
13163
13164 if (hunter !=nil && [self owner] != hunter) // our owner could be the same entity as the one responsible for our taking damage in the case of submunitions
13165 {
13166 if ([hunter isCloaked])
13167 {
13168 [self doScriptEvent:OOJSID("shipBeingAttackedByCloaked") andReactToAIMessage:@"ATTACKED_BY_CLOAKED"];
13169
13170 // lose it!
13171 other = nil;
13172 hunter = nil;
13173 }
13174 }
13175 else
13176 {
13177 hunter = nil;
13178 }
13179
13180 // if the other entity is a ship note it as an aggressor
13181 if (hunter != nil)
13182 {
13183 BOOL iAmTheLaw = [self isPolice];
13184 BOOL uAreTheLaw = [hunter isPolice];
13185
13186 DESTROY(_lastEscortTarget); // we're being attacked, escorts can scramble!
13187
13188 [self setPrimaryAggressor:hunter];
13189 [self setFoundTarget:hunter];
13190
13191 // firing on an innocent ship is an offence
13192 [self broadcastHitByLaserFrom: hunter];
13193
13194 // tell ourselves we've been attacked
13195 if (energy > 0)
13196 {
13197 [self respondToAttackFrom:ent becauseOf:hunter];
13198 }
13199
13200 OOShipGroup *group = [self group];
13201 // JSAIs manage group notifications themselves
13202 if (![self hasNewAI])
13203 {
13204 // additionally, tell our group we've been attacked
13205 if (group != nil && group != [hunter group] && !(iAmTheLaw || uAreTheLaw))
13206 {
13207 if ([self isTrader] || [self isEscort])
13208 {
13209 ShipEntity *groupLeader = [group leader];
13210 if (groupLeader != self)
13211 {
13212 [groupLeader setFoundTarget:hunter];
13213 [groupLeader setPrimaryAggressor:hunter];
13214 [groupLeader respondToAttackFrom:ent becauseOf:hunter];
13215 //unsetting group leader for carriers can break stuff
13216 }
13217 }
13218 if ([self isPirate])
13219 {
13220 NSEnumerator *groupEnum = nil;
13221 ShipEntity *otherPirate = nil;
13222
13223 for (groupEnum = [group mutationSafeEnumerator]; (otherPirate = [groupEnum nextObject]); )
13224 {
13225 if (otherPirate != self && randf() < 0.5) // 50% chance they'll help
13226 {
13227 [otherPirate setFoundTarget:hunter];
13228 [otherPirate setPrimaryAggressor:hunter];
13229 [otherPirate respondToAttackFrom:ent becauseOf:hunter];
13230 }
13231 }
13232 }
13233 else if (iAmTheLaw)
13234 {
13235 NSEnumerator *groupEnum = nil;
13236 ShipEntity *otherPolice = nil;
13237
13238 for (groupEnum = [group mutationSafeEnumerator]; (otherPolice = [groupEnum nextObject]); )
13239 {
13240 if (otherPolice != self)
13241 {
13242 [otherPolice setFoundTarget:hunter];
13243 [otherPolice setPrimaryAggressor:hunter];
13244 [otherPolice respondToAttackFrom:ent becauseOf:hunter];
13245 }
13246 }
13247 }
13248 }
13249 }
13250
13251 // if I'm a copper and you're not, then mark the other as an offender!
13252 if (iAmTheLaw && !uAreTheLaw)
13253 {
13254 // JSAI's can choose not to do this for friendly fire purposes
13255 if (![self hasNewAI])
13256 {
13257 [hunter markAsOffender:64 withReason:kOOLegalStatusReasonAttackedPolice];
13258 }
13259 }
13260
13261 if ((group != nil && [hunter group] == group) || (iAmTheLaw && uAreTheLaw))
13262 {
13263 // avoid shooting each other
13264 if ([hunter behaviour] == BEHAVIOUR_ATTACK_FLY_TO_TARGET) // avoid me please!
13265 {
13266 [hunter setBehaviour:BEHAVIOUR_ATTACK_FLY_FROM_TARGET];
13267 [hunter setDesiredSpeed:[hunter maxFlightSpeed]];
13268 }
13269 }
13270
13271 }
13272
13273 OOShipDamageType damageType = kOODamageTypeEnergy;
13274 if (suppressExplosion) damageType = kOODamageTypeRemoved;
13275 else if (energyMine) damageType = kOODamageTypeCascadeWeapon;
13276
13277 if (!suppressExplosion)
13278 {
13279 [self noteTakingDamage:amount from:other type:damageType];
13280 if (cascade) energy = 0.0; // explicit set energy to zero in case an oxp raised the energy in previous line.
13281 }
13282
13283 // die if I'm out of energy
13284 if (energy <= 0.0)
13285 {
13286 // backup check just in case scripts have reduced energy
13287 if (self != [UNIVERSE station])
13288 {
13289 if (hunter != nil) [hunter noteTargetDestroyed:self];
13290 [self getDestroyedBy:other damageType:damageType];
13291 }
13292 }
13293 else
13294 {
13295 // warn if I'm low on energy
13296 if (energy < maxEnergy * 0.25)
13297 {
13298 [self doScriptEvent:OOJSID("shipEnergyIsLow") andReactToAIMessage:@"ENERGY_LOW"];
13299 }
13300 if ((energy < maxEnergy *0.125 || (energy < 64 && energy < amount*2)) && [self hasEscapePod] && (ranrot_rand() & 3) == 0) // 25% chance he gets to an escape pod
13301 {
13302 [self abandonShip];
13303 }
13304 }
13305}
13306
13307
13308- (BOOL) abandonShip
13309{
13310 BOOL OK = NO;
13311 if ([self isPlayer] && [(PlayerEntity *)self isDocked])
13312 {
13313 OOLog(@"ShipEntity.abandonShip.failed", @"%@", @"Player cannot abandon ship while docked.");
13314 return OK;
13315 }
13316
13317 if (![self hasEscapePod])
13318 {
13319 OOLog(@"ShipEntity.abandonShip.failed", @"Ship abandonment was requested for %@, but this ship does not carry escape pod(s).", self);
13320 return OK;
13321 }
13322
13323 if (EXPECT([self launchEscapeCapsule] != NO_TARGET)) // -launchEscapeCapsule takes care of everything for the player
13324 {
13325 if (![self isPlayer])
13326 {
13327 OK = YES;
13328 // if multiple items providing escape pod, remove all of them (NPC process)
13329 while ([self hasEquipmentItemProviding:@"EQ_ESCAPE_POD"])
13330 {
13331 [self removeEquipmentItem:[self equipmentItemProviding:@"EQ_ESCAPE_POD"]];
13332 }
13333 [self setAITo:@"nullAI.plist"];
13334 behaviour = BEHAVIOUR_IDLE;
13335 frustration = 0.0;
13336 [self setScanClass: CLASS_CARGO]; // we're unmanned now!
13337 thrust = thrust * 0.5;
13338 if (thrust > 5) thrust = 5; // 5 is the thrust of an escape-capsule
13339 desired_speed = 0.0;
13340 if ([self group]) [self setGroup:nil]; // remove self from group.
13341 if (![self isSubEntity] && [self owner]) [self setOwner:nil]; //unset owner, but not if we are a subent
13342 if ([self hasEscorts])
13343 {
13344 OOShipGroup *escortGroup = [self escortGroup];
13345 NSEnumerator *escortEnum = nil;
13346 ShipEntity *escort = nil;
13347 // Note: works on escortArray rather than escortEnumerator because escorts may be mutated.
13348 for (escortEnum = [[self escortArray] objectEnumerator]; (escort = [escortEnum nextObject]); )
13349 {
13350 // act individually now!
13351 if ([escort group] == escortGroup) [escort setGroup:nil];
13352 if ([escort owner] == self) [escort setOwner:escort];
13353 }
13354
13355 // We now have no escorts.
13356 [_escortGroup release];
13357 _escortGroup = nil;
13358 }
13359 }
13360 }
13361 else if (EXPECT([self isSubEntity]))
13362 {
13363 // may still have launched passenger pods even if no crew
13364 // if multiple items providing escape pod, remove all of them (NPC process)
13365 while ([self hasEquipmentItemProviding:@"EQ_ESCAPE_POD"])
13366 {
13367 [self removeEquipmentItem:[self equipmentItemProviding:@"EQ_ESCAPE_POD"]];
13368 }
13369
13370 }
13371 else
13372 {
13373 // this shouldn't happen any more!
13374 OOLog(@"ShipEntity.abandonShip.notPossible", @"Ship %@ cannot be abandoned at this time.", self);
13375 }
13376 return OK;
13377}
13378
13379
13380- (void) takeScrapeDamage:(double) amount from:(Entity *)ent
13381{
13382 if ([self status] == STATUS_DEAD) return;
13383
13384 if ([self status] == STATUS_LAUNCHING|| [ent status] == STATUS_LAUNCHING)
13385 {
13386 // no collisions during launches please
13387 return;
13388 }
13389
13390 energy -= amount;
13391 [self noteTakingDamage:amount from:ent type:kOODamageTypeScrape];
13392
13393 // oops we hit too hard!!!
13394 if (energy <= 0.0)
13395 {
13396 float frag_chance = [ent mass]*10/[self mass];
13397 /* impacts from heavier entities produce fragments
13398 * impacts from lighter entities might do but not always
13399 * asteroid-asteroid impacts likely to fragment
13400 * ship-asteroid impacts might, or might just vaporise it
13401 * projectile weapons just get the default chance
13402 */
13403 if (randf() < frag_chance)
13404 {
13405 being_mined = YES; // same as using a mining laser
13406 }
13407 if ([ent isShip])
13408 {
13409 [(ShipEntity *)ent noteTargetDestroyed:self];
13410 }
13411 [self getDestroyedBy:ent damageType:kOODamageTypeScrape];
13412 }
13413 else
13414 {
13415 // warn if I'm low on energy
13416 if (energy < maxEnergy * 0.25)
13417 {
13418 [self doScriptEvent:OOJSID("shipEnergyIsLow") andReactToAIMessage:@"ENERGY_LOW"];
13419 }
13420 }
13421}
13422
13423
13424- (void) takeHeatDamage:(double)amount
13425{
13426 if ([self status] == STATUS_DEAD) return;
13427
13428 if ([self isSubEntity])
13429 {
13430 ShipEntity* owner = [self owner];
13431 if (![owner isFrangible])
13432 {
13433 return;
13434 }
13435 }
13436
13437 energy -= amount;
13438 throw_sparks = YES;
13439
13440 [self noteTakingDamage:amount from:nil type:kOODamageTypeHeat];
13441
13442 // oops we're burning up!
13443 if (energy <= 0.0)
13444 {
13445 [self getDestroyedBy:nil damageType:kOODamageTypeHeat];
13446 }
13447 else
13448 {
13449 // warn if I'm low on energy
13450 if (energy < maxEnergy * 0.25)
13451 {
13452 [self doScriptEvent:OOJSID("shipEnergyIsLow") andReactToAIMessage:@"ENERGY_LOW"];
13453 }
13454 }
13455}
13456
13457
13458- (void) enterDock:(StationEntity *)station
13459{
13460 // throw these away now we're docked...
13461 if (dockingInstructions != nil)
13462 {
13463 [dockingInstructions autorelease];
13464 dockingInstructions = nil;
13465 }
13466
13467 [self doScriptEvent:OOJSID("shipWillDockWithStation") withArgument:station];
13468 [self doScriptEvent:OOJSID("shipDockedWithStation") withArgument:station];
13469 [shipAI message:@"DOCKED"];
13470 [station noteDockedShip:self];
13471 [UNIVERSE removeEntity:self];
13472}
13473
13474
13475- (void) leaveDock:(StationEntity *)station
13476{
13477 // This code is never used. Currently npc ships are only launched from the stations launch queue.
13478 if (station == nil) return;
13479
13480 [station launchShip:self];
13481
13482}
13483
13484
13485- (void) enterWormhole:(WormholeEntity *) w_hole
13486{
13487 [self enterWormhole:w_hole replacing:YES];
13488}
13489
13490
13491- (void) enterWormhole:(WormholeEntity *) w_hole replacing:(BOOL)replacing
13492{
13493 if (w_hole == nil) return;
13494 if ([self status] == STATUS_ENTERING_WITCHSPACE)
13495 {
13496 return; // has already entered a different wormhole
13497 }
13498 // Replacement ships now handled by system repopulator
13499
13500 // MKW 2011.02.27 - Moved here from ShipEntityAI so escorts reliably follow
13501 // mother in all wormhole cases, not just when the ship
13502 // creates the wormhole.
13503 [self addTarget:w_hole];
13504 [self setFoundTarget:w_hole];
13505 [shipAI reactToMessage:@"WITCHSPACE OKAY" context:@"performHyperSpaceExit"]; // must be a reaction, the ship is about to disappear
13506
13507 // CIM 2012.07.22 above only covers those cases where ship expected to leave
13508 if ([[self escortArray] count] > 1)
13509 {
13510 // so wormhole escorts anyway if it leaves unexpectedly.
13511 [self wormholeEscorts];
13512 }
13513
13514 if ([self scriptedMisjump])
13515 {
13516 [self setScriptedMisjump:NO];
13517 [w_hole setMisjumpWithRange:[self scriptedMisjumpRange]];
13518 [self setScriptedMisjumpRange:0.5];
13519 }
13520 [w_hole suckInShip: self]; // removes ship from universe
13521}
13522
13523
13524- (void) enterWitchspace
13525{
13526 [UNIVERSE addWitchspaceJumpEffectForShip:self];
13527 [shipAI message:@"ENTERED_WITCHSPACE"];
13528
13529 if (![[UNIVERSE sun] willGoNova])
13530 {
13531 // if the sun's not going nova, add a new ship like this one leaving.
13532 [UNIVERSE witchspaceShipWithPrimaryRole:[self primaryRole]];
13533 }
13534
13535 [UNIVERSE removeEntity:self];
13536}
13537
13538
13539- (void) leaveWitchspace
13540{
13541 Quaternion q1;
13543 Vector v1 = vector_forward_from_quaternion(q1);
13544 double d1 = 0.0;
13545
13546 GLfloat min_d1 = [UNIVERSE safeWitchspaceExitDistance];
13547
13548 while (fabs(d1) < min_d1)
13549 {
13550 // not scannerRange - has no effect on witchspace exit
13551 d1 = SCANNER_MAX_RANGE * (randf() - randf());
13552 }
13553
13554 HPVector exitposition = [UNIVERSE getWitchspaceExitPosition];
13555 exitposition.x += v1.x * d1; // randomise exit position
13556 exitposition.y += v1.y * d1;
13557 exitposition.z += v1.z * d1;
13558 [self setPosition:exitposition];
13559 [self witchspaceLeavingEffects];
13560}
13561
13562
13563- (BOOL) witchspaceLeavingEffects
13564{
13565 // all ships exiting witchspace will share the same orientation.
13566 orientation = [UNIVERSE getWitchspaceExitRotation];
13567 flightRoll = 0.0;
13568 stick_roll = 0.0;
13569 flightPitch = 0.0;
13570 stick_pitch = 0.0;
13571 flightYaw = 0.0;
13572 stick_yaw = 0.0;
13573 flightSpeed = 50.0; // constant speed same for all ships
13574// was a quarter of max speed, so the Anaconda speeds up and most
13575// others slow down - CIM
13576// will be overridden if left witchspace via a genuine wormhole
13577 velocity = kZeroVector;
13578 if (![UNIVERSE addEntity:self]) // AI and status get initialised here
13579 {
13580 return NO;
13581 }
13582 [self setStatus:STATUS_EXITING_WITCHSPACE];
13583 [shipAI message:@"EXITED_WITCHSPACE"];
13584
13585 [UNIVERSE addWitchspaceJumpEffectForShip:self];
13586 [self setStatus:STATUS_IN_FLIGHT];
13587 return YES;
13588}
13589
13590
13591- (void) markAsOffender:(int)offence_value
13592{
13593 [self markAsOffender:offence_value withReason:kOOLegalStatusReasonUnknown];
13594}
13595
13596
13597- (void) markAsOffender:(int)offence_value withReason:(OOLegalStatusReason)reason
13598{
13599 if (![self isPolice] && ![self isCloaked] && self != [UNIVERSE station])
13600 {
13601 if ([self isSubEntity])
13602 {
13603 [[self parentEntity] markAsOffender:offence_value withReason:reason];
13604 }
13605 else
13606 {
13607 if ((scanClass == CLASS_THARGOID || scanClass == CLASS_STATION) && reason != kOOLegalStatusReasonSetup && reason != kOOLegalStatusReasonByScript)
13608 {
13609 return; // no non-scripted bounties for thargoids and stations
13610 }
13611
13612 JSContext *context = OOJSAcquireContext();
13613
13614 jsval amountVal = JSVAL_VOID;
13615 JS_NewNumberValue(context, (bounty | offence_value)-bounty, &amountVal);
13616
13617 bounty |= offence_value; // can't set the new bounty until the size of the change is known
13618
13619 jsval reasonVal = OOJSValueFromLegalStatusReason(context, reason);
13620
13621 ShipScriptEvent(context, self, "shipBountyChanged", amountVal, reasonVal);
13622
13623 OOJSRelinquishContext(context);
13624
13625 }
13626 }
13627}
13628
13629
13630// Exposed to AI
13631- (void) switchLightsOn
13632{
13633 NSEnumerator *subEnum = nil;
13634 OOFlasherEntity *se = nil;
13635 ShipEntity *sub = nil;
13636
13637 _lightsActive = YES;
13638
13639 for (subEnum = [self flasherEnumerator]; (se = [subEnum nextObject]); )
13640 {
13641 [se setActive:YES];
13642 }
13643 for (subEnum = [self shipSubEntityEnumerator]; (sub = [subEnum nextObject]); )
13644 {
13645 [sub switchLightsOn];
13646 }
13647}
13648
13649// Exposed to AI
13650- (void) switchLightsOff
13651{
13652 NSEnumerator *subEnum = nil;
13653 OOFlasherEntity *se = nil;
13654 ShipEntity *sub = nil;
13655
13656 _lightsActive = NO;
13657
13658 for (subEnum = [self flasherEnumerator]; (se = [subEnum nextObject]); )
13659 {
13660 [se setActive:NO];
13661 }
13662 for (subEnum = [self shipSubEntityEnumerator]; (sub = [subEnum nextObject]); )
13663 {
13664 [sub switchLightsOff];
13665 }
13666}
13667
13668
13669- (BOOL) lightsActive
13670{
13671 return _lightsActive;
13672}
13673
13674
13675- (void) setDestination:(HPVector) dest
13676{
13677 _destination = dest;
13678 frustration = 0.0; // new destination => no frustration!
13679}
13680
13681
13682- (void) setEscortDestination:(HPVector) dest
13683{
13684 _destination = dest; // don't reset frustration for escorts.
13685}
13686
13687
13688- (BOOL) canAcceptEscort:(ShipEntity *)potentialEscort
13689{
13690 if (dockingInstructions) // we are busy with docking.
13691 {
13692 return NO;
13693 }
13694 if (scanClass != [potentialEscort scanClass]) // this makes sure that wingman can only select police, thargons only thargoids.
13695 {
13696 return NO;
13697 }
13698 if ([self bounty] == 0 && [potentialEscort bounty] != 0) // clean mothers can only accept clean escorts
13699 {
13700 return NO;
13701 }
13702 if (![self isEscort]) // self is NOT wingman or escort or thargon
13703 {
13704 return [potentialEscort isEscort]; // is wingman or escort or thargon
13705 }
13706 return NO;
13707}
13708
13709
13710- (BOOL) acceptAsEscort:(ShipEntity *) other_ship
13711{
13712 // can't pair with self
13713 if (self == other_ship) return NO;
13714
13715 // no longer in flight, probably entered wormhole without telling escorts.
13716 if ([self status] != STATUS_IN_FLIGHT) return NO;
13717
13718 //increased stack depth at which it can accept escorts to avoid rejections at this stage.
13719 //doesn't seem to have any adverse effect for now. - Kaks.
13720 if ([shipAI stackDepth] > 3)
13721 {
13722 OOLog(@"ship.escort.reject", @"%@ rejecting escort %@ because AI stack depth is %lu.",self, other_ship, [shipAI stackDepth]);
13723 return NO;
13724 }
13725
13726 if ([self canAcceptEscort:other_ship])
13727 {
13728 OOShipGroup *escortGroup = [self escortGroup];
13729
13730 if ([escortGroup containsShip:other_ship]) return YES;
13731
13732 // check total number acceptable
13733 // the system's patrols don't have escorts set inside their dictionary, but accept max escorts.
13734 if (_maxEscortCount == 0 && ([self hasPrimaryRole:@"police"] || [self hasPrimaryRole:@"hunter"] || [self hasRole:@"thargoid-mothership"]))
13735 {
13736 _maxEscortCount = MAX_ESCORTS;
13737 }
13738
13739 NSUInteger maxEscorts = _maxEscortCount; // never bigger than MAX_ESCORTS.
13740 NSUInteger escortCount = [escortGroup count] - 1; // always 0 or higher.
13741
13742 if (escortCount < maxEscorts)
13743 {
13744 [other_ship setGroup:escortGroup];
13745 if ([self group] == nil)
13746 {
13747 [self setGroup:escortGroup];
13748 }
13749 else if ([self group] != escortGroup) [[self group] addShip:other_ship];
13750
13751 if (([other_ship maxFlightSpeed] < cruiseSpeed) && ([other_ship maxFlightSpeed] > cruiseSpeed * 0.3))
13752 {
13753 cruiseSpeed = [other_ship maxFlightSpeed] * 0.99;
13754 }
13755
13756 OOLog(@"ship.escort.accept", @"%@ accepting escort %@.", self, other_ship);
13757
13758 [self doScriptEvent:OOJSID("shipAcceptedEscort") withArgument:other_ship];
13759 [other_ship doScriptEvent:OOJSID("escortAccepted") withArgument:self];
13760 [shipAI message:@"ACCEPTED_ESCORT"];
13761 return YES;
13762 }
13763 else
13764 {
13765 OOLog(@"ship.escort.reject", @"%@ already got max escorts(%ld). Escort rejected: %@.", self, escortCount, other_ship);
13766 }
13767 }
13768 else
13769 {
13770 OOLog(@"ship.escort.reject", @"%@ failed canAcceptEscort for escort %@.", self, other_ship);
13771 }
13772
13773
13774 return NO;
13775}
13776
13777
13778// Exposed to AI
13779- (void) updateEscortFormation
13780{
13781 _escortPositionsValid = NO;
13782}
13783
13784
13785/*
13786 NOTE: it's tempting to call refreshEscortPositions from coordinatesForEscortPosition:
13787 as needed, but that would cause unnecessary extra work if the formation
13788 callback itself calls updateEscortFormation.
13789*/
13790- (void) refreshEscortPositions
13791{
13792 if (!_escortPositionsValid)
13793 {
13794 JSContext *context = OOJSAcquireContext();
13795 jsval result;
13796 jsval args[] = { INT_TO_JSVAL(0), INT_TO_JSVAL(_maxEscortCount) };
13797 BOOL OK;
13798
13799 // Reset validity first so updateEscortFormation can be called from the update callback.
13800 _escortPositionsValid = YES;
13801
13802 uint8_t i;
13803 for (i = 0; i < _maxEscortCount; i++)
13804 {
13805 args[0] = INT_TO_JSVAL(i);
13806 OK = [script callMethod:OOJSID("coordinatesForEscortPosition")
13807 inContext:context
13808 withArguments:args count:sizeof args / sizeof *args
13809 result:&result];
13810
13811 if (OK) OK = JSValueToVector(context, result, &_escortPositions[i]);
13812
13813 if (!OK) _escortPositions[i] = kZeroVector;
13814 }
13815
13816 OOJSRelinquishContext(context);
13817 }
13818}
13819
13820
13821- (HPVector) coordinatesForEscortPosition:(unsigned)idx
13822{
13823 /*
13824 This function causes problems with Thargoids: their missiles (aka Thargons) are automatically
13825 added to the escorts group, and when a mother ship dies all thargons will attach themselves
13826 as escorts to the surviving battleships. This can lead to huge escort groups.
13827 TODO: better handling of Thargoid groups:
13828 - put thargons (& all other thargon missiles) in their own non-escort group perhaps?
13829 */
13830
13831 // The _escortPositions array is always MAX_ESCORTS long.
13832 // Kludge: return the same last escort position if we have escorts above MAX_ESCORTS...
13833 idx = MIN(idx, (unsigned)(MAX_ESCORTS - 1));
13834
13835 return HPvector_add(self->position, vectorToHPVector(quaternion_rotate_vector([self normalOrientation], _escortPositions[idx])));
13836}
13837
13838
13839// Exposed to AI
13840- (void) deployEscorts
13841{
13842 NSEnumerator *escortEnum = nil;
13843 ShipEntity *escort = nil;
13844 ShipEntity *target = nil;
13845 NSMutableSet *idleEscorts = nil;
13846 unsigned deployCount;
13847
13848 if ([self primaryTarget] == nil || _escortGroup == nil) return;
13849
13850 OOShipGroup *escortGroup = [self escortGroup];
13851 NSUInteger escortCount = [escortGroup count] - 1; // escorts minus leader.
13852 if (escortCount == 0) return;
13853
13854 if ([self group] == nil) [self setGroup:escortGroup];
13855
13856 if ([self primaryTarget] == [self lastEscortTarget])
13857 {
13858 // already deployed escorts onto this target!
13859 return;
13860 }
13861
13862 [self setLastEscortTarget:[self primaryTarget]];
13863
13864 // Find idle escorts
13865 idleEscorts = [NSMutableSet set];
13866 for (escortEnum = [self escortEnumerator]; (escort = [escortEnum nextObject]); )
13867 {
13868 if (![[[escort getAI] name] isEqualToString:@"interceptAI.plist"] && ![escort hasNewAI])
13869 {
13870 [idleEscorts addObject:escort];
13871 }
13872 else if ([escort hasNewAI])
13873 {
13874 // JS-based escorts get a help request
13875 [escort doScriptEvent:OOJSID("helpRequestReceived") withArgument:self andArgument:[self primaryTarget]];
13876 }
13877 }
13878
13879 escortCount = [idleEscorts count];
13880 if (escortCount == 0) return;
13881
13882 deployCount = ranrot_rand() % escortCount + 1;
13883
13884 // Deploy deployCount idle escorts.
13885 target = [self primaryTarget];
13886 for (escortEnum = [idleEscorts objectEnumerator]; (escort = [escortEnum nextObject]); )
13887 {
13888 [escort addTarget:target];
13889 [escort setAITo:@"interceptAI.plist"];
13890 [escort doScriptEvent:OOJSID("escortAttack") withArgument:target];
13891
13892 if (--deployCount == 0) break;
13893 }
13894
13895 [self updateEscortFormation];
13896}
13897
13898
13899// Exposed to AI
13900- (void) dockEscorts
13901{
13902 if (![self hasEscorts]) return;
13903
13904 OOShipGroup *escortGroup = [self escortGroup];
13905 NSEnumerator *escortEnum = nil;
13906 ShipEntity *escort = nil;
13907 ShipEntity *target = [self primaryTarget];
13908 unsigned i = 0;
13909 // Note: works on escortArray rather than escortEnumerator because escorts may be mutated.
13910 for (escortEnum = [[self escortArray] objectEnumerator]; (escort = [escortEnum nextObject]); )
13911 {
13912 float delay = i++ * 3.0 + 1.5; // send them off at three second intervals
13913 AI *ai = [escort getAI];
13914
13915 // act individually now!
13916 if ([escort group] == escortGroup) [escort setGroup:nil];
13917 if ([escort owner] == self) [escort setOwner:escort];
13918 if(target && [target isStation]) [escort setTargetStation:target];
13919 // JSAI: handles own delay
13920 if (![escort hasNewAI])
13921 {
13922 [escort setAITo:@"dockingAI.plist"];
13923 [ai setState:@"ABORT" afterDelay:delay + 0.25];
13924 }
13925 [escort doScriptEvent:OOJSID("escortDock") withArgument:[NSNumber numberWithFloat:delay]];
13926 }
13927
13928 // We now have no escorts.
13929 [_escortGroup release];
13930 _escortGroup = nil;
13931}
13932
13933
13934- (void) setTargetToNearestStationIncludingHostiles:(BOOL) includeHostiles
13935{
13936 // check if the groupID (parent ship) points to a station...
13937 Entity *mother = [[self group] leader];
13938 if ([mother isStation])
13939 {
13940 [self addTarget:mother];
13941 [self setTargetStation:mother];
13942 return; // head for mother!
13943 }
13944
13945 /*- selects the nearest station it can find -*/
13946 if (!UNIVERSE)
13947 return;
13948 int ent_count = UNIVERSE->n_entities;
13949 Entity **uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list
13950 Entity *my_entities[ent_count];
13951 int i;
13952 int station_count = 0;
13953 for (i = 0; i < ent_count; i++)
13954 if (uni_entities[i]->isStation)
13955 my_entities[station_count++] = [uni_entities[i] retain]; // retained
13956 //
13957 StationEntity *thing = nil, *station = nil;
13958 double range2, nearest2 = SCANNER_MAX_RANGE2 * 1000000.0; // 1000x typical scanner range (25600 km), squared.
13959 for (i = 0; i < station_count; i++)
13960 {
13961 thing = (StationEntity *)my_entities[i];
13962 range2 = HPdistance2(position, thing->position);
13963 if (range2 < nearest2 && (includeHostiles || ![thing isHostileTo:self]))
13964 {
13965 station = thing;
13966 nearest2 = range2;
13967 }
13968 }
13969 for (i = 0; i < station_count; i++)
13970 [my_entities[i] release]; // released
13971 //
13972 if (station)
13973 {
13974 [self addTarget:station];
13975 [self setTargetStation:station];
13976 }
13977 else
13978 {
13979 [shipAI message:@"NO_STATION_FOUND"];
13980 }
13981}
13982
13983
13984// Exposed to AI
13985- (void) setTargetToNearestFriendlyStation
13986{
13987 [self setTargetToNearestStationIncludingHostiles:NO];
13988}
13989
13990
13991// Exposed to AI
13992- (void) setTargetToNearestStation
13993{
13994 [self setTargetToNearestStationIncludingHostiles:YES];
13995}
13996
13997
13998// Exposed to AI
13999- (void) setTargetToSystemStation
14000{
14001 StationEntity* system_station = [UNIVERSE station];
14002
14003 if (!system_station)
14004 {
14005 [shipAI message:@"NOTHING_FOUND"];
14006 [shipAI message:@"NO_STATION_FOUND"];
14007 DESTROY(_primaryTarget);
14008 [self setTargetStation:nil];
14009 return;
14010 }
14011
14012 if (!system_station->isStation)
14013 {
14014 [shipAI message:@"NOTHING_FOUND"];
14015 [shipAI message:@"NO_STATION_FOUND"];
14016 DESTROY(_primaryTarget);
14017 [self setTargetStation:nil];
14018 return;
14019 }
14020
14021 [self addTarget:system_station];
14022 [self setTargetStation:system_station];
14023 return;
14024}
14025
14026
14027- (void) landOnPlanet:(OOPlanetEntity *)planet
14028{
14029 if (planet && [self isShuttle])
14030 {
14031 [planet welcomeShuttle:self];
14032 }
14033 [self doScriptEvent:OOJSID("shipLandedOnPlanet") withArgument:planet andReactToAIMessage:@"LANDED_ON_PLANET"];
14034
14035#ifndef NDEBUG
14036 if ([self reportAIMessages])
14037 {
14038 OOLog(@"planet.collide.shuttleLanded", @"DEBUG: %@ landed on planet %@", self, planet);
14039 }
14040#endif
14041
14042 [UNIVERSE removeEntity:self];
14043}
14044
14045
14046// Exposed to AI
14047- (void) abortDocking
14048{
14049 [[UNIVERSE findEntitiesMatchingPredicate:IsStationPredicate
14050 parameter:nil
14051 inRange:-1
14052 ofEntity:nil]
14053 makeObjectsPerformSelector:@selector(abortDockingForShip:) withObject:self];
14054}
14055
14056
14057- (NSDictionary *) dockingInstructions
14058{
14059 return dockingInstructions;
14060}
14061
14062
14063- (void) broadcastThargoidDestroyed
14064{
14065 [[UNIVERSE findShipsMatchingPredicate:HasRolePredicate
14066 parameter:@"tharglet"
14067 inRange:SCANNER_MAX_RANGE
14068 ofEntity:self]
14069 makeObjectsPerformSelector:@selector(sendAIMessage:) withObject:@"THARGOID_DESTROYED"];
14070}
14071
14072
14073static BOOL AuthorityPredicate(Entity *entity, void *parameter)
14074{
14075 ShipEntity *victim = parameter;
14076
14077 // Select main station, if victim is in aegis
14078 if (entity == [UNIVERSE station] && [victim withinStationAegis])
14079 {
14080 return YES;
14081 }
14082
14083 // Select police units in typical scanner range
14084 if ([entity scanClass] == CLASS_POLICE &&
14085 HPdistance2([victim position], [entity position]) < SCANNER_MAX_RANGE2)
14086 {
14087 return YES;
14088 }
14089
14090 // Reject others
14091 return NO;
14092}
14093
14094
14095- (void) broadcastHitByLaserFrom:(ShipEntity *) aggressor_ship
14096{
14097 /*-- If you're clean, locates all police and stations in range and tells them OFFENCE_COMMITTED --*/
14098 if (!UNIVERSE) return;
14099 if ([self bounty]) return;
14100 if (!aggressor_ship) return;
14101
14102 if ( (scanClass == CLASS_NEUTRAL)||
14103 (scanClass == CLASS_STATION)||
14104 (scanClass == CLASS_BUOY)||
14105 (scanClass == CLASS_POLICE)||
14106 (scanClass == CLASS_MILITARY)||
14107 (scanClass == CLASS_PLAYER)) // only for active ships...
14108 {
14109 NSArray *authorities = nil;
14110 NSEnumerator *authEnum = nil;
14111 ShipEntity *auth = nil;
14112
14113 authorities = [UNIVERSE findShipsMatchingPredicate:AuthorityPredicate
14114 parameter:self
14115 inRange:-1
14116 ofEntity:nil];
14117 authEnum = [authorities objectEnumerator];
14118 while ((auth = [authEnum nextObject]))
14119 {
14120 [auth setFoundTarget:aggressor_ship];
14121 [auth doScriptEvent:OOJSID("offenceCommittedNearby") withArgument:aggressor_ship andArgument:self];
14122 [auth reactToAIMessage:@"OFFENCE_COMMITTED" context:@"combat update"];
14123 }
14124 }
14125}
14126
14127
14128- (void) sendMessage:(NSString *) message_text toShip:(ShipEntity*) other_ship withUnpilotedOverride:(BOOL)unpilotedOverride
14129{
14130 if (!other_ship || !message_text) return;
14131 if (!crew && !unpilotedOverride) return;
14132
14133 double d2 = HPdistance2(position, [other_ship position]);
14134 if (d2 > scannerRange * scannerRange)
14135 return; // out of comms range
14136
14137 NSString *expandedMessage = OOExpand(message_text); // consistent with broadcast message.
14138
14139 if (other_ship->isPlayer)
14140 {
14141 [self setCommsMessageColor];
14142 [(PlayerEntity *)other_ship receiveCommsMessage:expandedMessage from:self];
14143 messageTime = 6.0;
14144 [UNIVERSE resetCommsLogColor];
14145 }
14146 else
14147 [other_ship receiveCommsMessage:expandedMessage from:self];
14148}
14149
14150
14151- (void) sendExpandedMessage:(NSString *)message_text toShip:(ShipEntity *)other_ship
14152{
14153 if (!other_ship || !crew)
14154 return; // nobody to receive or send the signal
14155 if ((lastRadioMessage) && (messageTime > 0.0) && [message_text isEqual:lastRadioMessage])
14156 return; // don't send the same message too often
14157 [lastRadioMessage autorelease];
14158 lastRadioMessage = [message_text retain];
14159
14160 double d2 = HPdistance2(position, [other_ship position]);
14161 if (d2 > scannerRange * scannerRange)
14162 {
14163 // out of comms range
14164 return;
14165 }
14166
14167 Random_Seed very_random_seed;
14168 very_random_seed.a = rand() & 255;
14169 very_random_seed.b = rand() & 255;
14170 very_random_seed.c = rand() & 255;
14171 very_random_seed.d = rand() & 255;
14172 very_random_seed.e = rand() & 255;
14173 very_random_seed.f = rand() & 255;
14174 seed_RNG_only_for_planet_description(very_random_seed);
14175
14176 NSDictionary *specials = [NSDictionary dictionaryWithObjectsAndKeys:
14177 [self displayName], @"[self:name]",
14178 [other_ship identFromShip: self], @"[target:name]",
14179 nil];
14180 NSString *expandedMessage = OOExpandDescriptionString(OOStringExpanderDefaultRandomSeed(), message_text, specials, nil, nil, kOOExpandNoOptions);
14181
14182 [self sendMessage:expandedMessage toShip:other_ship withUnpilotedOverride:NO];
14183}
14184
14185
14186- (void) broadcastAIMessage:(NSString *) ai_message
14187{
14188 NSString *expandedMessage = OOExpand(ai_message);
14189
14190 [self checkScanner];
14191 unsigned i;
14192 for (i = 0; i < n_scanned_ships ; i++)
14193 {
14194 ShipEntity* ship = scanned_ships[i];
14195 [[ship getAI] message: expandedMessage];
14196 }
14197}
14198
14199
14200- (void) broadcastMessage:(NSString *) message_text withUnpilotedOverride:(BOOL) unpilotedOverride
14201{
14202 NSString *expandedMessage = OOExpand(message_text); // consistent with broadcast message.
14203
14204
14205 if (!crew && !unpilotedOverride)
14206 return; // nobody to send the signal and no override for unpiloted craft is set
14207
14208 [self checkScanner];
14209 unsigned i;
14210 for (i = 0; i < n_scanned_ships ; i++)
14211 {
14212 ShipEntity* ship = scanned_ships[i];
14213 if (![ship isPlayer]) [ship receiveCommsMessage:expandedMessage from:self];
14214 }
14215
14216 PlayerEntity *player = PLAYER; // make sure that the player always receives a message when in range
14217 // SCANNER_MAX_RANGE2 because it's the player's scanner range
14218 // which is important
14219 if (HPdistance2(position, [player position]) < SCANNER_MAX_RANGE2)
14220 {
14221 [self setCommsMessageColor];
14222 [player receiveCommsMessage:expandedMessage from:self];
14223 messageTime = 6.0;
14224 [UNIVERSE resetCommsLogColor];
14225 }
14226}
14227
14228
14229- (void) setCommsMessageColor
14230{
14231 float hue = 0.0625f * (universalID & 15);
14232 [[UNIVERSE commLogGUI] setTextColor:[OOColor colorWithHue:hue saturation:0.375f brightness:1.0f alpha:1.0f]];
14233 if (scanClass == CLASS_THARGOID)
14234 [[UNIVERSE commLogGUI] setTextColor:[OOColor greenColor]];
14235 if (scanClass == CLASS_POLICE)
14236 [[UNIVERSE commLogGUI] setTextColor:[OOColor cyanColor]];
14237}
14238
14239
14240- (void) receiveCommsMessage:(NSString *) message_text from:(ShipEntity *) other
14241{
14242 // Too complex for AI scripts to handle, JS event only.
14243 [self doScriptEvent:OOJSID("commsMessageReceived") withArgument:message_text andArgument:other];
14244}
14245
14246
14247- (void) commsMessage:(NSString *)valueString withUnpilotedOverride:(BOOL)unpilotedOverride
14248{
14249 Random_Seed very_random_seed;
14250 very_random_seed.a = rand() & 255;
14251 very_random_seed.b = rand() & 255;
14252 very_random_seed.c = rand() & 255;
14253 very_random_seed.d = rand() & 255;
14254 very_random_seed.e = rand() & 255;
14255 very_random_seed.f = rand() & 255;
14256 seed_RNG_only_for_planet_description(very_random_seed);
14257
14258 [self broadcastMessage:valueString withUnpilotedOverride:unpilotedOverride];
14259}
14260
14261
14262- (BOOL) markedForFines
14263{
14264 return being_fined;
14265}
14266
14267
14268- (BOOL) markForFines
14269{
14270 if (being_fined)
14271 return NO; // can't mark twice
14272 being_fined = ([self legalStatus] > 0);
14273 return being_fined;
14274}
14275
14276
14277- (BOOL) isMining
14278{
14279 return ((behaviour == BEHAVIOUR_ATTACK_MINING_TARGET)&&([forward_weapon_type isMiningLaser]));
14280}
14281
14282
14283- (void) interpretAIMessage:(NSString *)ms
14284{
14285 if ([ms hasPrefix:AIMS_AGGRESSOR_SWITCHED_TARGET])
14286 {
14287 // if I'm under attack send a thank-you message to the rescuer
14288 //
14289 NSArray* tokens = ScanTokensFromString(ms);
14290 int switcher_id = [(NSString*)[tokens objectAtIndex:1] intValue]; // Attacker that switched targets.
14291 Entity* switcher = [UNIVERSE entityForUniversalID:switcher_id];
14292 int rescuer_id = [(NSString*)[tokens objectAtIndex:2] intValue]; // New primary target of attacker.
14293 Entity* rescuer = [UNIVERSE entityForUniversalID:rescuer_id];
14294 if ((switcher == [self primaryAggressor])&&(switcher == [self primaryTarget])&&(switcher)&&(rescuer)&&(rescuer->isShip)&&([self thankedShip] != rescuer)&&(scanClass != CLASS_THARGOID))
14295 {
14296 ShipEntity* rescueShip = (ShipEntity*)rescuer;
14297// ShipEntity* switchingShip = (ShipEntity*)switcher;
14298 if (scanClass == CLASS_POLICE)
14299 {
14300 [self sendExpandedMessage:@"[police-thanks-for-assist]" toShip:rescueShip];
14301 [rescueShip setBounty:[rescueShip bounty] * 0.80 withReason:kOOLegalStatusReasonAssistingPolice]; // lower bounty by 20%
14302 }
14303 else
14304 {
14305 [self sendExpandedMessage:@"[thanks-for-assist]" toShip:rescueShip];
14306 }
14307 [self setThankedShip:rescuer];
14308 }
14309 }
14310}
14311
14312
14313- (BoundingBox) findBoundingBoxRelativeTo:(Entity *)other InVectors:(Vector) _i :(Vector) _j :(Vector) _k
14314{
14315 HPVector opv = other ? other->position : position;
14316 return [self findBoundingBoxRelativeToPosition:opv InVectors:_i :_j :_k];
14317}
14318
14319
14320// Exposed to AI and legacy scripts.
14321- (void) spawn:(NSString *)roles_number
14322{
14323 NSArray *tokens = ScanTokensFromString(roles_number);
14324 NSString *roleString = nil;
14325 NSString *numberString = nil;
14326 NSUInteger number;
14327
14328 if ([tokens count] != 2)
14329 {
14330 OOLog(kOOLogSyntaxAddShips, @"***** Could not spawn: \"%@\" (must be two tokens, role and number)",roles_number);
14331 return;
14332 }
14333
14334 roleString = [tokens oo_stringAtIndex:0];
14335 numberString = [tokens oo_stringAtIndex:1];
14336
14337 number = [numberString intValue];
14338
14339 [self spawnShipsWithRole:roleString count:number];
14340}
14341
14342
14343- (int) checkShipsInVicinityForWitchJumpExit
14344{
14345 // checks if there are any large masses close by
14346 // since we want to place the space station at least 10km away
14347 // the formula we'll use is K x m / d2 < 1.0
14348 // (m = mass, d2 = distance squared)
14349 // coriolis station is mass 455,223,200
14350 // 10km is 10,000m,
14351 // 10km squared is 100,000,000
14352 // therefore K is 0.22 (approx)
14353
14354 int result = NO_TARGET;
14355
14356 GLfloat k = 0.1;
14357
14358 int ent_count = UNIVERSE->n_entities;
14359 Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list
14360 ShipEntity* my_entities[ent_count];
14361 int i;
14362
14363 int ship_count = 0;
14364 for (i = 0; i < ent_count; i++)
14365 if ((uni_entities[i]->isShip)&&(uni_entities[i] != self))
14366 my_entities[ship_count++] = (ShipEntity*)[uni_entities[i] retain]; // retained
14367 //
14368 for (i = 0; (i < ship_count)&&(result == NO_TARGET) ; i++)
14369 {
14370 ShipEntity* ship = my_entities[i];
14371 HPVector delta = HPvector_between(position, ship->position);
14372 GLfloat d2 = HPmagnitude2(delta);
14373 if (![ship isPlayer] || ![PLAYER isDocked])
14374 { // player doesn't block if docked
14375 if ((k * [ship mass] > d2)&&(d2 < SCANNER_MAX_RANGE2)) // if you go off (typical) scanner from a blocker - it ceases to block
14376 result = [ship universalID];
14377 }
14378 }
14379 for (i = 0; i < ship_count; i++)
14380 [my_entities[i] release]; // released
14381
14382 return result;
14383}
14384
14385
14386- (BOOL) trackCloseContacts
14387{
14388 return trackCloseContacts;
14389}
14390
14391
14392- (void) setTrackCloseContacts:(BOOL) value
14393{
14394 if (value == (BOOL)trackCloseContacts) return;
14395
14396 trackCloseContacts = value;
14397 [closeContactsInfo release];
14398
14399 if (trackCloseContacts)
14400 {
14401 closeContactsInfo = [[NSMutableDictionary alloc] init];
14402 }
14403 else
14404 {
14405 closeContactsInfo = nil;
14406 }
14407}
14408
14409
14410#if OO_SALVAGE_SUPPORT
14411// Never used.
14412- (void) claimAsSalvage
14413{
14414 // Create a bouy and beacon where the hulk is.
14415 // Get the main GalCop station to launch a pilot boat to deliver a pilot to the hulk.
14416 OOLog(@"claimAsSalvage.called", @"claimAsSalvage called on %@ %@", [self name], [self roleSet]);
14417
14418 // Not an abandoned hulk, so don't allow the salvage
14419 if (![self isHulk])
14420 {
14421 OOLog(@"claimAsSalvage.failed.notHulk", @"claimAsSalvage failed because not a hulk");
14422 return;
14423 }
14424
14425 // Set target to main station, and return now if it can't be found
14426 [self setTargetToSystemStation];
14427 if ([self primaryTarget] == nil)
14428 {
14429 OOLog(@"claimAsSalvage.failed.noStation", @"claimAsSalvage failed because did not find a station");
14430 return;
14431 }
14432
14433 // Get the station to launch a pilot boat to bring a pilot out to the hulk (use a viper for now)
14434 StationEntity *station = (StationEntity *)[self primaryTarget];
14435 OOLog(@"claimAsSalvage.requestingPilot", @"claimAsSalvage asking station to launch a pilot boat");
14436 [station launchShipWithRole:@"pilot"];
14437 [self setReportAIMessages:YES];
14438 OOLog(@"claimAsSalvage.success", @"claimAsSalvage setting own state machine to capturedShipAI.plist");
14439 [self setAITo:@"capturedShipAI.plist"];
14440}
14441
14442
14443- (void) sendCoordinatesToPilot
14444{
14445 Entity *scan;
14446 ShipEntity *scanShip, *pilot;
14447
14448 n_scanned_ships = 0;
14449 scan = z_previous;
14450 OOLog(@"ship.pilotage", @"searching for pilot boat");
14451 while (scan &&(scan->isShip == NO))
14452 {
14453 scan = scan->z_previous; // skip non-ships
14454 }
14455
14456 pilot = nil;
14457 while (scan)
14458 {
14459 if (scan->isShip)
14460 {
14461 scanShip = (ShipEntity *)scan;
14462
14463 if ([self hasRole:@"pilot"] == YES)
14464 {
14465 if ([scanShip primaryTarget] == nil)
14466 {
14467 OOLog(@"ship.pilotage", @"found pilot boat with no target, will use this one");
14468 pilot = scanShip;
14469 [pilot setPrimaryRole:@"pilot"];
14470 break;
14471 }
14472 }
14473 }
14474 scan = scan->z_previous;
14475 while (scan && (scan->isShip == NO))
14476 {
14477 scan = scan->z_previous;
14478 }
14479 }
14480
14481 if (pilot != nil)
14482 {
14483 OOLog(@"ship.pilotage", @"becoming pilot target and setting AI");
14484 [pilot setReportAIMessages:YES];
14485 [pilot addTarget:self];
14486 [pilot setAITo:@"pilotAI.plist"];
14487 [self reactToAIMessage:@"FOUND_PILOT" context:@"flight update"];
14488 }
14489}
14490
14491
14492- (void) pilotArrived
14493{
14494 [self setHulk:NO];
14495 [self reactToAIMessage:@"PILOT_ARRIVED" context:@"flight update"];
14496}
14497#endif
14498
14499
14500#ifndef NDEBUG
14501- (void)dumpSelfState
14502{
14503 NSMutableArray *flags = nil;
14504 NSString *flagsString = nil;
14505
14506 [super dumpSelfState];
14507
14508 OOLog(@"dumpState.shipEntity", @"Type: %@", [self shipDataKey]);
14509 OOLog(@"dumpState.shipEntity", @"Name: %@", name);
14510 OOLog(@"dumpState.shipEntity", @"Display Name: %@", [self displayName]);
14511 OOLog(@"dumpState.shipEntity", @"Roles: %@", [self roleSet]);
14512 OOLog(@"dumpState.shipEntity", @"Primary role: %@", primaryRole);
14513 OOLog(@"dumpState.shipEntity", @"Script: %@", script);
14514 OOLog(@"dumpState.shipEntity", @"Subentity count: %lu", [self subEntityCount]);
14515 OOLog(@"dumpState.shipEntity", @"Behaviour: %@", OOStringFromBehaviour(behaviour));
14516 id target = [self primaryTarget];
14517 if (target == nil) target = @"<none>";
14518 OOLog(@"dumpState.shipEntity", @"Target: %@", target);
14519 OOLog(@"dumpState.shipEntity", @"Destination: %@", HPVectorDescription(_destination));
14520 OOLog(@"dumpState.shipEntity", @"Other destination: %@", HPVectorDescription(coordinates));
14521 OOLog(@"dumpState.shipEntity", @"Waypoint count: %u", number_of_navpoints);
14522 OOLog(@"dumpState.shipEntity", @"Desired speed: %g", desired_speed);
14523 OOLog(@"dumpState.shipEntity", @"Thrust: %g", thrust);
14524 if ([self escortCount] != 0) OOLog(@"dumpState.shipEntity", @"Escort count: %u", [self escortCount]);
14525 OOLog(@"dumpState.shipEntity", @"Fuel: %i", fuel);
14526 OOLog(@"dumpState.shipEntity", @"Fuel accumulator: %g", fuel_accumulator);
14527 OOLog(@"dumpState.shipEntity", @"Missile count: %u", missiles);
14528
14529 if (shipAI != nil && OOLogWillDisplayMessagesInClass(@"dumpState.shipEntity.ai"))
14530 {
14531 OOLog(@"dumpState.shipEntity.ai", @"%@", @"AI:");
14533 OOLogIndent();
14534 @try
14535 {
14536 [shipAI dumpState];
14537 }
14538 @catch (id exception) {}
14540 }
14541 OOLog(@"dumpState.shipEntity", @"Accuracy: %g", accuracy);
14542 OOLog(@"dumpState.shipEntity", @"Jink position: %@", VectorDescription(jink));
14543 OOLog(@"dumpState.shipEntity", @"Frustration: %g", frustration);
14544 OOLog(@"dumpState.shipEntity", @"Success factor: %g", success_factor);
14545 OOLog(@"dumpState.shipEntity", @"Shots fired: %u", shot_counter);
14546 OOLog(@"dumpState.shipEntity", @"Time since shot: %g", [self shotTime]);
14547 OOLog(@"dumpState.shipEntity", @"Spawn time: %g (%g seconds ago)", [self spawnTime], [self timeElapsedSinceSpawn]);
14548 if ([self isBeacon])
14549 {
14550 OOLog(@"dumpState.shipEntity", @"Beacon code: %@", [self beaconCode]);
14551 }
14552 OOLog(@"dumpState.shipEntity", @"Hull temperature: %g", ship_temperature);
14553 OOLog(@"dumpState.shipEntity", @"Heat insulation: %g", [self heatInsulation]);
14554
14555 flags = [NSMutableArray array];
14556 #define ADD_FLAG_IF_SET(x) if (x) { [flags addObject:@#x]; }
14557 ADD_FLAG_IF_SET(military_jammer_active);
14558 ADD_FLAG_IF_SET(docking_match_rotation);
14559 ADD_FLAG_IF_SET(pitching_over);
14560 ADD_FLAG_IF_SET(reportAIMessages);
14561 ADD_FLAG_IF_SET(being_mined);
14562 ADD_FLAG_IF_SET(being_fined);
14563 ADD_FLAG_IF_SET(isHulk);
14564 ADD_FLAG_IF_SET(trackCloseContacts);
14565 ADD_FLAG_IF_SET(isNearPlanetSurface);
14566 ADD_FLAG_IF_SET(isFrangible);
14567 ADD_FLAG_IF_SET(cloaking_device_active);
14568 ADD_FLAG_IF_SET(canFragment);
14569 ADD_FLAG_IF_SET([self proximityAlert] != nil);
14570 flagsString = [flags count] ? [flags componentsJoinedByString:@", "] : (NSString *)@"none";
14571 OOLog(@"dumpState.shipEntity", @"Flags: %@", flagsString);
14572}
14573#endif
14574
14575
14576- (OOJSScript *)script
14577{
14578 return script;
14579}
14580
14581
14582- (NSDictionary *)scriptInfo
14583{
14584 return (scriptInfo != nil) ? scriptInfo : (NSDictionary *)[NSDictionary dictionary];
14585}
14586
14587
14588- (void) overrideScriptInfo:(NSDictionary *)override
14589{
14590 if (scriptInfo == nil) scriptInfo = [override retain];
14591 else if (override != nil)
14592 {
14593 NSMutableDictionary *newInfo = [NSMutableDictionary dictionaryWithDictionary:scriptInfo];
14594 [newInfo addEntriesFromDictionary:override];
14595 [scriptInfo release];
14596 scriptInfo = [newInfo copy];
14597 }
14598}
14599
14600
14601- (Entity *)entityForShaderProperties
14602{
14603 return [self rootShipEntity];
14604}
14605
14606- (void) setDemoShip: (OOScalar) rate
14607{
14608 demoStartOrientation = orientation;
14609 demoRate = rate;
14610 isDemoShip = YES;
14611 [self setPitch: 0.0f];
14612 [self setRoll: 0.0f];
14613}
14614
14615- (BOOL) isDemoShip
14616{
14617 return isDemoShip;
14618}
14619
14620- (void) setDemoStartTime: (OOTimeAbsolute) time
14621{
14622 demoStartTime = time;
14623}
14624
14625- (OOTimeAbsolute) getDemoStartTime
14626{
14627 return demoStartTime;
14628}
14629
14630// *** Script event dispatch.
14631- (void) doScriptEvent:(jsid)message
14632{
14633 JSContext *context = OOJSAcquireContext();
14634 [self doScriptEvent:message inContext:context withArguments:NULL count:0];
14635 OOJSRelinquishContext(context);
14636}
14637
14638
14639- (void) doScriptEvent:(jsid)message withArgument:(id)argument
14640{
14641 JSContext *context = OOJSAcquireContext();
14642
14643 jsval value = OOJSValueFromNativeObject(context, argument);
14644 [self doScriptEvent:message inContext:context withArguments:&value count:1];
14645
14646 OOJSRelinquishContext(context);
14647}
14648
14649
14650- (void) doScriptEvent:(jsid)message
14651 withArgument:(id)argument1
14652 andArgument:(id)argument2
14653{
14654 JSContext *context = OOJSAcquireContext();
14655
14656 jsval argv[2] = { OOJSValueFromNativeObject(context, argument1), OOJSValueFromNativeObject(context, argument2) };
14657 [self doScriptEvent:message inContext:context withArguments:argv count:2];
14658
14659 OOJSRelinquishContext(context);
14660}
14661
14662
14663- (void) doScriptEvent:(jsid)message withArguments:(NSArray *)arguments
14664{
14665 JSContext *context = OOJSAcquireContext();
14666 uintN i, argc;
14667 jsval *argv = NULL;
14668
14669 // Convert arguments to JS values and make them temporarily un-garbage-collectable.
14670 argc = (uintN)[arguments count];
14671 if (argc != 0)
14672 {
14673 argv = malloc(sizeof *argv * argc);
14674 if (argv != NULL)
14675 {
14676 for (i = 0; i != argc; ++i)
14677 {
14678 argv[i] = [[arguments objectAtIndex:i] oo_jsValueInContext:context];
14679 OOJSAddGCValueRoot(context, &argv[i], "event parameter");
14680 }
14681 }
14682 else argc = 0;
14683 }
14684
14685 [self doScriptEvent:message inContext:context withArguments:argv count:argc];
14686
14687 // Re-garbage-collectibalize the arguments and free the array.
14688 if (argv != NULL)
14689 {
14690 for (i = 0; i != argc; ++i)
14691 {
14692 JS_RemoveValueRoot(context, &argv[i]);
14693 }
14694 free(argv);
14695 }
14696
14697 OOJSRelinquishContext(context);
14698}
14699
14700
14701- (void) doScriptEvent:(jsid)message withArguments:(jsval *)argv count:(uintN)argc
14702{
14703 JSContext *context = OOJSAcquireContext();
14704 [self doScriptEvent:message inContext:context withArguments:argv count:argc];
14705 OOJSRelinquishContext(context);
14706}
14707
14708
14709- (void) doScriptEvent:(jsid)message inContext:(JSContext *)context withArguments:(jsval *)argv count:(uintN)argc
14710{
14711 // This method is a bottleneck so that PlayerEntity can override at one point.
14712 [script callMethod:message inContext:context withArguments:argv count:argc result:NULL];
14713 [aiScript callMethod:message inContext:context withArguments:argv count:argc result:NULL];
14714}
14715
14716
14717- (void) reactToAIMessage:(NSString *)message context:(NSString *)debugContext
14718{
14719 [shipAI reactToMessage:message context:debugContext];
14720}
14721
14722
14723- (void) sendAIMessage:(NSString *)message
14724{
14725 [shipAI message:message];
14726}
14727
14728
14729- (void) doScriptEvent:(jsid)scriptEvent andReactToAIMessage:(NSString *)aiMessage
14730{
14731 [self doScriptEvent:scriptEvent];
14732 [self reactToAIMessage:aiMessage context:nil];
14733}
14734
14735
14736- (void) doScriptEvent:(jsid)scriptEvent withArgument:(id)argument andReactToAIMessage:(NSString *)aiMessage
14737{
14738 [self doScriptEvent:scriptEvent withArgument:argument];
14739 [self reactToAIMessage:aiMessage context:nil];
14740}
14741
14742
14743// exposed for shaders; fake alert level
14744// since NPCs don't have torus drive, they're never at condition green
14745- (OOAlertCondition) alertCondition
14746{
14747 if ([self status] == STATUS_DOCKED)
14748 {
14750 }
14751 if ([self hasHostileTarget] || energy < maxEnergy / 4)
14752 {
14753 return ALERT_CONDITION_RED;
14754 }
14756}
14757
14758
14759- (OOAlertCondition) realAlertCondition
14760{
14761 if ([self status] == STATUS_DOCKED)
14762 {
14764 }
14765 if ([self hasHostileTarget])
14766 {
14767 return ALERT_CONDITION_RED;
14768 }
14769 else
14770 {
14771 NSEnumerator *sEnum = [_defenseTargets objectEnumerator];
14772 ShipEntity *ship = nil;
14773 double scanrange2 = scannerRange * scannerRange;
14774 while ((ship = [sEnum nextObject]))
14775 {
14776 if ([ship hasHostileTarget] || ([ship isPlayer] && [PLAYER weaponsOnline]))
14777 {
14778 if (HPdistance2([ship position],position) < scanrange2)
14779 {
14780 return ALERT_CONDITION_RED;
14781 }
14782 }
14783 }
14784 // also need to check primary target separately
14785 if ([self hasHostileTarget])
14786 {
14787 Entity *ptarget = [self primaryTargetWithoutValidityCheck];
14788 if (ptarget != nil && [ptarget isShip])
14789 {
14790 ship = (ShipEntity *)ptarget;
14791 if ([ship hasHostileTarget] || ([ship isPlayer] && [PLAYER weaponsOnline]))
14792 {
14793 if (HPdistance2([ship position],position) < scanrange2 * 1.5625)
14794 {
14795 return ALERT_CONDITION_RED;
14796 }
14797 }
14798 }
14799 }
14800 if (_group)
14801 {
14802 sEnum = [_group objectEnumerator];
14803 while ((ship = [sEnum nextObject]))
14804 {
14805 if ([ship hasHostileTarget] || ([ship isPlayer] && [PLAYER weaponsOnline]))
14806 {
14807 if (HPdistance2([ship position],position) < scanrange2)
14808 {
14809 return ALERT_CONDITION_RED;
14810 }
14811 }
14812 }
14813 }
14814 if (_escortGroup && _group != _escortGroup)
14815 {
14816 sEnum = [_escortGroup objectEnumerator];
14817 while ((ship = [sEnum nextObject]))
14818 {
14819 if ([ship hasHostileTarget] || ([ship isPlayer] && [PLAYER weaponsOnline]))
14820 {
14821 if (HPdistance2([ship position],position) < scanrange2)
14822 {
14823 return ALERT_CONDITION_RED;
14824 }
14825 }
14826 }
14827 }
14828 }
14830}
14831
14832
14833// Exposed to AI and scripts.
14834- (void) doNothing
14835{
14836
14837}
14838
14839
14840#ifndef NDEBUG
14841- (NSString *) descriptionForObjDump
14842{
14843 NSString *desc = [super descriptionForObjDump];
14844 desc = [NSString stringWithFormat:@"%@ mass %g", desc, [self mass]];
14845 if (![self isPlayer])
14846 {
14847 desc = [NSString stringWithFormat:@"%@ AI: %@", desc, [[self getAI] shortDescriptionComponents]];
14848 }
14849 return desc;
14850}
14851#endif
14852
14853@end
14854
14855
14856@implementation Entity (SubEntityRelationship)
14857
14858- (BOOL) isShipWithSubEntityShip:(Entity *)other
14859{
14860 return NO;
14861}
14862
14863
14864- (void) drawSubEntityImmediate:(bool)immediate translucent:(bool)translucent
14865{
14866 // Do nothing.
14867}
14868
14869@end
14870
14871
14872@implementation ShipEntity (SubEntityRelationship)
14873
14874- (BOOL) isShipWithSubEntityShip:(Entity *)other
14875{
14876 assert ([self isShip]);
14877
14878 if (![other isShip]) return NO;
14879 if (![other isSubEntity]) return NO;
14880 if ([other owner] != self) return NO;
14881
14882#ifndef NDEBUG
14883 // Sanity check; this should always be true.
14884 if (![self hasSubEntity:(ShipEntity *)other])
14885 {
14886 OOLogERR(@"ship.subentity.sanityCheck.failed", @"%@ thinks it's a subentity of %@, but the supposed parent does not agree. %@", [other shortDescription], [self shortDescription], @"This is an internal error, please report it.");
14887 [other setOwner:nil];
14888 return NO;
14889 }
14890#endif
14891
14892 return YES;
14893}
14894
14895@end
14896
14897
14898NSDictionary *OODefaultShipShaderMacros(void)
14899{
14900 static NSDictionary *macros = nil;
14901
14902 if (macros == nil)
14903 {
14904 macros = [[[ResourceManager materialDefaults] oo_dictionaryForKey:@"ship-prefix-macros" defaultValue:[NSDictionary dictionary]] retain];
14905 }
14906
14907 return macros;
14908}
14909
14910// is this the right place for this function now? - CIM
14911BOOL OOUniformBindingPermitted(NSString *propertyName, id bindingTarget)
14912{
14913 static NSSet *entityWhitelist = nil;
14914 static NSSet *shipWhitelist = nil;
14915 static NSSet *playerShipWhitelist = nil;
14916 static NSSet *visualEffectWhitelist = nil;
14917
14918 if (entityWhitelist == nil)
14919 {
14920 NSDictionary *wlDict = [ResourceManager whitelistDictionary];
14921 entityWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_entity_binding_methods"]];
14922 shipWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_ship_binding_methods"]];
14923 playerShipWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_player_ship_binding_methods"]];
14924 visualEffectWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_visual_effect_binding_methods"]];
14925 }
14926
14927 if ([bindingTarget isKindOfClass:[Entity class]])
14928 {
14929 if ([entityWhitelist containsObject:propertyName]) return YES;
14930 if ([bindingTarget isShip])
14931 {
14932 if ([shipWhitelist containsObject:propertyName]) return YES;
14933 }
14934 if ([bindingTarget isPlayerLikeShip])
14935 {
14936 if ([playerShipWhitelist containsObject:propertyName]) return YES;
14937 }
14938 if ([bindingTarget isVisualEffect])
14939 {
14940 if ([visualEffectWhitelist containsObject:propertyName]) return YES;
14941 }
14942 }
14943
14944 return NO;
14945}
14946
14947
14949{
14950 return [weapon_type weaponRange];
14951}
14952
14953
14955{
14956 return weapon == nil || [[weapon identifier] isEqualToString:@"EQ_WEAPON_NONE"];
14957}
NSUInteger gDebugFlags
Definition main.m:7
#define NO_DRAW_DISTANCE_FACTOR
Definition Entity.h:46
OOEntityStatus
Definition Entity.h:60
OOScanClass OOScanClassFromString(NSString *string) PURE_FUNC
#define SCANNER_MAX_RANGE
Definition Entity.h:51
OOScanClass
Definition Entity.h:71
#define SCANNER_MAX_RANGE2
Definition Entity.h:52
#define ADD_FLAG_IF_SET(x)
#define DESTROY(x)
Definition OOCocoa.h:77
#define foreachkey(VAR, DICT)
Definition OOCocoa.h:366
OOINLINE jsval OOJSValueFromLegalStatusReason(JSContext *context, OOLegalStatusReason value)
OOINLINE jsval OOJSValueFromShipDamageType(JSContext *context, OOShipDamageType value)
OOCargoType StringToCargoType(NSString *string) PURE_FUNC
NSString * OOStringFromLegalStatusReason(OOLegalStatusReason reason)
@ DEBUG_BOUNDING_BOXES
OOINLINE void OODebugDrawColoredBoundingBox(BoundingBox box, OOColor *color)
OOINLINE void OODebugDrawBoundingBox(BoundingBox box)
void OODebugDrawColoredLine(Vector start, Vector end, OOColor *color)
void OODebugDrawPoint(Vector position, OOColor *color)
void OOStandardsDeprecated(NSString *message)
BOOL OOEnforceStandards(void)
#define EXPECT_NOT(x)
#define EXPECT(x)
const HPVector kZeroHPVector
Definition OOHPVector.m:28
HPVector OOHPVectorRandomSpatial(OOHPScalar maxLength)
Definition OOHPVector.m:82
#define OOJS_PROFILE_EXIT
#define OOJS_PROFILE_ENTER
#define OOJSID(str)
Definition OOJSPropID.h:38
BOOL JSValueToVector(JSContext *context, jsval value, Vector *outVector) NONNULL_FUNC
Definition OOJSVector.m:259
OOINLINE jsval OOJSValueFromNativeObject(JSContext *context, id object)
id OOJSNativeObjectFromJSObject(JSContext *context, JSObject *object)
OOINLINE JSContext * OOJSAcquireContext(void)
OOINLINE void OOJSRelinquishContext(JSContext *context)
#define OOJSAddGCValueRoot(context, root, name)
void OOLogPushIndent(void)
Definition OOLogging.m:316
#define OOLogWARN(class, format,...)
Definition OOLogging.h:113
#define OOLogERR(class, format,...)
Definition OOLogging.h:112
void OOLogPopIndent(void)
Definition OOLogging.m:340
BOOL OOLogWillDisplayMessagesInClass(NSString *inMessageClass)
Definition OOLogging.m:144
#define OOLog(class, format,...)
Definition OOLogging.h:88
void OOLogIndent(void)
Definition OOLogging.m:366
#define MIN(A, B)
Definition OOMaths.h:111
GLfloat OOScalar
Definition OOMaths.h:64
#define M_PI
Definition OOMaths.h:73
HPVector OOHPVectorMultiplyMatrix(HPVector v, OOMatrix m)
Definition OOMatrix.m:145
const OOMatrix kIdentityMatrix
Definition OOMatrix.m:31
Vector OOVectorMultiplyMatrix(Vector v, OOMatrix m)
Definition OOMatrix.m:129
void OOGLPushModelView(void)
void OOGLTranslateModelView(Vector vector)
void OOGLMultModelView(OOMatrix matrix)
OOMatrix OOGLPopModelView(void)
#define OOVerifyOpenGLState()
Definition OOOpenGL.h:136
return self
unsigned count
return nil
Vector vector_up_from_quaternion(Quaternion quat)
void quaternion_rotate_about_x(Quaternion *quat, OOScalar angle)
Vector vector_right_from_quaternion(Quaternion quat)
Vector vector_forward_from_quaternion(Quaternion quat)
void quaternion_rotate_about_z(Quaternion *quat, OOScalar angle)
void quaternion_set_random(Quaternion *quat)
Vector quaternion_rotate_vector(Quaternion q, Vector v)
const Quaternion kIdentityQuaternion
Quaternion quaternion_rotation_between(Vector v0, Vector v1)
void quaternion_rotate_about_y(Quaternion *quat, OOScalar angle)
const Quaternion kZeroQuaternion
void quaternion_rotate_about_axis(Quaternion *quat, Vector axis, OOScalar angle)
Quaternion quaternion_multiply(Quaternion q1, Quaternion q2)
float y
float x
@ STELLAR_TYPE_MOON
@ STELLAR_TYPE_MINIATURE
@ STELLAR_TYPE_NORMAL_PLANET
@ kOOExpandNoOptions
Random_Seed OOStringExpanderDefaultRandomSeed(void)
NSString * OOExpandDescriptionString(Random_Seed seed, NSString *string, NSDictionary *overrides, NSDictionary *legacyLocals, NSString *systemName, OOExpandOptions options)
#define OOExpand(string,...)
NSMutableArray * ScanTokensFromString(NSString *values)
BOOL ScanVectorFromString(NSString *xyzString, Vector *outVector)
uint16_t OOFuelQuantity
Definition OOTypes.h:179
uint8_t OOWeaponFacingSet
Definition OOTypes.h:237
NSString * OOCommodityType
Definition OOTypes.h:106
OOAegisStatus
Definition OOTypes.h:60
@ AEGIS_IN_DOCKING_RANGE
Definition OOTypes.h:64
@ AEGIS_CLOSE_TO_MAIN_PLANET
Definition OOTypes.h:63
@ AEGIS_CLOSE_TO_ANY_PLANET
Definition OOTypes.h:62
@ AEGIS_NONE
Definition OOTypes.h:61
@ DETAIL_LEVEL_EXTRAS
Definition OOTypes.h:247
OOLegalStatusReason
Definition OOTypes.h:157
uint64_t OOCreditsQuantity
Definition OOTypes.h:182
uint16_t OOUniversalID
Definition OOTypes.h:189
#define VALID_WEAPON_FACINGS
Definition OOTypes.h:239
int16_t OOSystemID
Definition OOTypes.h:211
OOCargoType
Definition OOTypes.h:69
@ CARGO_RANDOM
Definition OOTypes.h:75
@ CARGO_NOT_CARGO
Definition OOTypes.h:70
@ CARGO_THARGOID
Definition OOTypes.h:74
@ CARGO_SLAVES
Definition OOTypes.h:71
@ CARGO_ALLOY
Definition OOTypes.h:72
@ CARGO_SCRIPTED_ITEM
Definition OOTypes.h:76
@ CARGO_MINERALS
Definition OOTypes.h:73
uint32_t OOCargoQuantity
Definition OOTypes.h:176
OOMassUnit
Definition OOTypes.h:123
@ UNITS_TONS
Definition OOTypes.h:124
@ UNITS_GRAMS
Definition OOTypes.h:126
@ UNITS_KILOGRAMS
Definition OOTypes.h:125
double OOTimeDelta
Definition OOTypes.h:224
OOCargoFlag
Definition OOTypes.h:109
@ CARGO_FLAG_FULL_CONTRABAND
Definition OOTypes.h:114
@ CARGO_FLAG_FULL_SCARCE
Definition OOTypes.h:112
@ CARGO_FLAG_FULL_PLENTIFUL
Definition OOTypes.h:111
@ CARGO_FLAG_PIRATE
Definition OOTypes.h:115
@ CARGO_FLAG_CANISTERS
Definition OOTypes.h:117
@ CARGO_FLAG_FULL_UNIFORM
Definition OOTypes.h:116
@ CARGO_FLAG_FULL_MEDICAL
Definition OOTypes.h:113
@ CARGO_FLAG_FULL_PASSENGERS
Definition OOTypes.h:118
@ CARGO_FLAG_NONE
Definition OOTypes.h:110
uint8_t OOGovernmentID
Definition OOTypes.h:206
double OOTimeAbsolute
Definition OOTypes.h:223
@ UNIVERSE_MAX_ENTITIES
Definition OOTypes.h:193
@ NO_TARGET
Definition OOTypes.h:194
OOWeaponFacing
Definition OOTypes.h:228
@ WEAPON_FACING_FORWARD
Definition OOTypes.h:229
@ WEAPON_FACING_NONE
Definition OOTypes.h:234
@ WEAPON_FACING_AFT
Definition OOTypes.h:230
@ WEAPON_FACING_PORT
Definition OOTypes.h:231
@ WEAPON_FACING_STARBOARD
Definition OOTypes.h:232
const Vector kZeroVector
Definition OOVector.m:28
const Vector kBasisYVector
Definition OOVector.m:30
const Vector kBasisZVector
Definition OOVector.m:31
Vector OORandomPositionInBoundingBox(BoundingBox bb)
Definition OOVector.m:121
const Vector kBasisXVector
Definition OOVector.m:29
static GLfloat scripted_color[4]
static BOOL isHitByOctree(Octree_details axialDetails, Octree_details otherDetails, Vector delta, Triangle other_ijk)
static NSString *const kOOLogSyntaxAddShips
#define HYPERSPEED_FACTOR
@ SCOOP_STATUS_FULL_HOLD
@ SCOOP_STATUS_NOT_INSTALLED
@ SCOOP_STATUS_ACTIVE
@ SCOOP_STATUS_OKAY
#define MIN_HYPERSPEED_FACTOR
#define PLAYER
#define PLAYER_MAX_FUEL
#define MIN_FUEL
Definition ShipEntity.h:102
#define INITIAL_SHOT_TIME
Definition ShipEntity.h:100
#define COMBAT_BROADSIDE_IN_RANGE_FACTOR
Definition ShipEntity.h:55
BOOL isWeaponNone(OOWeaponType weapon)
#define COMBAT_AI_TRACKS_CLOSER
Definition ShipEntity.h:131
#define SHIP_COOLING_FACTOR
Definition ShipEntity.h:61
#define CLOAKING_DEVICE_MIN_ENERGY
Definition ShipEntity.h:48
#define COMBAT_AI_WEAPON_TEMP_READY
Definition ShipEntity.h:118
OOAlertCondition
Definition ShipEntity.h:172
@ ALERT_CONDITION_RED
Definition ShipEntity.h:178
@ ALERT_CONDITION_YELLOW
Definition ShipEntity.h:177
@ ALERT_CONDITION_DOCKED
Definition ShipEntity.h:175
#define MAX_SCAN_NUMBER
Definition ShipEntity.h:97
OOWeaponType OOWeaponTypeFromEquipmentIdentifierStrict(NSString *string) PURE_FUNC
#define COMBAT_BROADSIDE_RANGE_FACTOR
Definition ShipEntity.h:57
#define COMBAT_AI_WEAPON_TEMP_USABLE
Definition ShipEntity.h:119
#define ShipScriptEventNoCx(ship, event,...)
#define TURRET_SHOT_RANGE
Definition ShipEntity.h:82
#define COMBAT_AI_ISNT_AWFUL
Definition ShipEntity.h:123
#define TURRET_MINIMUM_COS
Definition ShipEntity.h:42
NSDictionary * OODefaultShipShaderMacros(void)
#define SHIP_MIN_CABIN_TEMP
Definition ShipEntity.h:68
#define CLOAKING_DEVICE_START_ENERGY
Definition ShipEntity.h:49
#define MAX_COS
Definition ShipEntity.h:143
#define NPC_MAX_WEAPON_TEMP
Definition ShipEntity.h:115
#define COMBAT_AI_FLEES_BETTER_2
Definition ShipEntity.h:134
#define MILITARY_JAMMER_ENERGY_RATE
Definition ShipEntity.h:51
#define MAX_JUMP_RANGE
Definition ShipEntity.h:107
#define TRACTOR_FORCE
Definition ShipEntity.h:92
#define MAX_ESCORTS
Definition ShipEntity.h:74
#define CLOAKING_DEVICE_ENERGY_RATE
Definition ShipEntity.h:47
#define AIMS_AGGRESSOR_SWITCHED_TARGET
Definition ShipEntity.h:94
#define COMBAT_AI_DOGFIGHTER
Definition ShipEntity.h:129
#define WEAPON_COOLING_FACTOR
Definition ShipEntity.h:114
#define ENTITY_PERSONALITY_MAX
Definition ShipEntity.h:110
GLfloat getWeaponRangeFromType(OOWeaponType weapon_type)
#define SHIP_MAX_CABIN_TEMP
Definition ShipEntity.h:67
#define MILITARY_JAMMER_MIN_ENERGY
Definition ShipEntity.h:52
#define COMBAT_AI_FLEES_BETTER
Definition ShipEntity.h:127
#define TURRET_SHOT_SPEED
Definition ShipEntity.h:80
OOBehaviour
Definition ShipEntity.h:150
#define COMBAT_IN_RANGE_FACTOR
Definition ShipEntity.h:54
#define COMBAT_OUT_RANGE_FACTOR
Definition ShipEntity.h:56
#define SUN_TEMPERATURE
Definition ShipEntity.h:72
#define COMBAT_WEAPON_RANGE_FACTOR
Definition ShipEntity.h:58
#define MAX_TARGETS
Definition ShipEntity.h:36
#define COMBAT_AI_CONFIDENCE_FACTOR
Definition ShipEntity.h:122
#define COMBAT_AI_IS_SMART
Definition ShipEntity.h:125
NSString * OOStringFromBehaviour(OOBehaviour behaviour) CONST_FUNC
#define MAX_COS2
Definition ShipEntity.h:144
OOWeaponType OOWeaponTypeFromEquipmentIdentifierSloppy(NSString *string) PURE_FUNC
#define BASELINE_SHIELD_LEVEL
Definition ShipEntity.h:99
OOShipDamageType
Definition ShipEntity.h:183
#define SHIPENTITY_MAX_MISSILES
Definition ShipEntity.h:77
#define MAX_LANDING_SPEED2
Definition ShipEntity.h:141
#define SHIP_THRUST_FACTOR
Definition ShipEntity.h:44
OOWeaponType OOWeaponTypeFromString(NSString *string) PURE_FUNC
#define ShipScriptEvent(context, ship, event,...)
#define WEAPON_COOLING_CUTOUT
Definition ShipEntity.h:116
#define SHIP_INSULATION_FACTOR
Definition ShipEntity.h:66
#define SHIP_ENERGY_DAMAGE_TO_HEAT_FACTOR
Definition ShipEntity.h:65
static ShipEntity * doOctreesCollide(ShipEntity *prime, ShipEntity *other)
static GLfloat mascem_color1[4]
static GLfloat scripted_color[4]
static GLfloat neutral_color[4]
static GLfloat cargo_color[4]
static GLfloat hostile_color[4]
static NSString *const kOOLogEntityBehaviourChanged
Definition ShipEntity.m:101
static GLfloat missile_color[4]
static GLfloat police_color1[4]
BOOL OOUniformBindingPermitted(NSString *propertyName, id bindingTarget)
static GLfloat mascem_color2[4]
static GLfloat jammed_color[4]
static GLfloat friendly_color[4]
static GLfloat police_color2[4]
static NSString *const kOOLogSyntaxAddShips
Definition ShipEntity.m:99
#define UNIVERSE
Definition Universe.h:840
#define DESC(key)
Definition Universe.h:846
#define PROXIMITY_AVOID_DISTANCE_FACTOR
Definition Universe.h:110
Entity< OOStellarBody > * lastAegisLock()
Definition AI.h:38
void exitStateMachineWithMessage:(NSString *message)
Definition AI.m:296
void message:(NSString *ms)
Definition AI.m:600
void setState:afterDelay:(NSString *stateName,[afterDelay] NSTimeInterval delay)
Definition AI.m:358
void setState:(NSString *stateName)
Definition AI.m:334
GLfloat collision_radius
Definition Entity.h:111
OOUniversalID universalID
Definition Entity.h:89
HPVector absolutePositionForSubentity()
Definition Entity.m:669
Entity * z_next
Definition Entity.h:122
NSMutableArray * collisionArray()
Definition Entity.m:923
void setVelocity:(Vector vel)
Definition Entity.m:757
Entity * z_previous
Definition Entity.h:122
Quaternion orientation
Definition Entity.h:114
unsigned isSubEntity
Definition Entity.h:95
BOOL isSun()
Definition Entity.m:167
void setOrientation:(Quaternion quat)
Definition Entity.m:725
GLfloat energy
Definition Entity.h:142
unsigned isShip
Definition Entity.h:91
GLfloat collisionRadius()
Definition Entity.m:905
OOScanClass scanClass
Definition Entity.h:106
void setOwner:(Entity *ent)
Definition Entity.m:576
void setScanClass:(OOScanClass sClass)
Definition Entity.m:799
OOEntityStatus status()
Definition Entity.m:793
void drawSubEntityImmediate:translucent:(bool immediate, [translucent] bool translucent)
void setDistanceTravelled:(GLfloat value)
Definition Entity.m:781
ShipEntity * rootShipEntity()
Definition Entity.m:603
unsigned isStation
Definition Entity.h:92
unsigned isPlayer
Definition Entity.h:93
HPVector position
Definition Entity.h:112
void setEnergy:(GLfloat amount)
Definition Entity.m:811
Quaternion normalOrientation()
Definition Entity.m:738
Vector velocity
Definition Entity.h:140
ShipEntity * parentEntity()
Definition Entity.m:589
void takeEnergyDamage:from:becauseOf:weaponIdentifier:(double amount,[from] Entity *ent,[becauseOf] Entity *other,[weaponIdentifier] NSString *weaponIdentifier)
Definition Entity.m:990
double speed()
Definition Entity.m:769
id owner()
Definition Entity.m:583
unsigned isSunlit
Definition Entity.h:99
GLfloat mass
Definition Entity.h:146
void setPosition:(HPVector posn)
Definition Entity.m:647
OOMatrix drawRotationMatrix()
Definition Entity.m:879
id fragmentBurstFromEntity:(Entity *entity)
NSString * name()
OOCharacter * characterWithDictionary:(NSDictionary *c_dict)
void setLegalStatus:(int value)
NSDictionary * infoForScripting()
OOCharacter * randomCharacterWithRole:andOriginalSystem:(NSString *c_role,[andOriginalSystem] OOSystemID s)
OOColor * colorWithRGBAComponents:(OORGBAComponents components)
Definition OOColor.m:109
OOColor * brightColorWithDescription:(id description)
Definition OOColor.m:205
OOColor * colorWithDescription:(id description)
Definition OOColor.m:127
OOColor * redColor()
Definition OOColor.m:268
void getRed:green:blue:alpha:(float *red,[green] float *green,[blue] float *blue,[alpha] float *alpha)
Definition OOColor.m:368
OOColor * colorWithHue:saturation:brightness:alpha:(float hue,[saturation] float saturation,[brightness] float brightness,[alpha] float alpha)
Definition OOColor.m:87
NSString * conditionScript()
GLfloat weaponShotTemperature()
void addEquipmentWithInfo:(NSArray *itemInfo)
void setMissileRegistryRole:forShip:(NSString *roles,[forShip] NSString *shipKey)
OOColor * weaponColor()
NSString * getMissileRegistryRoleForShip:(NSString *shipKey)
OOEquipmentType * equipmentTypeWithIdentifier:(NSString *identifier)
NSArray * allEquipmentTypes()
NSString * identifier()
GLfloat weaponRechargeRate()
id exhaustForShip:withDefinition:andScale:(ShipEntity *ship,[withDefinition] NSArray *definition,[andScale] float scale)
instancetype explosionCloudFromEntity:withSettings:(Entity *entity,[withSettings] NSDictionary *settings)
instancetype explosionCloudFromEntity:withSize:andSettings:(Entity *entity,[withSize] float size,[andSettings] NSDictionary *settings)
instancetype explosionFlashFromEntity:(Entity *entity)
instancetype flasherWithDictionary:(NSDictionary *dictionary)
void setActive:(BOOL active)
void rescaleBy:(GLfloat factor)
BOOL callMethod:inContext:withArguments:count:result:(jsid methodID,[inContext] JSContext *context,[withArguments] jsval *argv,[count] intN argc,[result] jsval *outResult)
Definition OOJSScript.m:395
void setRange:(GLfloat range)
void setColor:(OOColor *color)
instancetype laserFromShip:direction:offset:(ShipEntity *ship,[direction] OOWeaponFacing direction,[offset] Vector offset)
Octree * octree
Definition OOMesh.h:122
instancetype meshWithName:cacheKey:materialDictionary:shadersDictionary:smooth:shaderMacros:shaderBindingTarget:scaleFactor:cacheWriteable:(NSString *name,[cacheKey] NSString *cacheKey,[materialDictionary] NSDictionary *materialDict,[shadersDictionary] NSDictionary *shadersDict,[smooth] BOOL smooth,[shaderMacros] NSDictionary *macros,[shaderBindingTarget] id< OOWeakReferenceSupport > object,[scaleFactor] float factor,[cacheWriteable] BOOL cacheWriteable)
Definition OOMesh.m:252
instancetype quiriumCascadeFromShip:(ShipEntity *ship)
instancetype ringFromEntity:(Entity *sourceEntity)
instancetype roleSetWithString:(NSString *roleString)
Definition OORoleSet.m:44
NSSet * roles()
Definition OORoleSet.m:171
instancetype roleSetWithRole:probability:(NSString *role,[probability] float probability)
Definition OORoleSet.m:50
id jsScriptFromFileNamed:properties:(NSString *fileName,[properties] NSDictionary *properties)
Definition OOScript.m:192
BOOL addShip:(ShipEntity *ship)
void setLeader:(ShipEntity *leader)
NSUInteger count()
instancetype groupWithName:(NSString *name)
ShipEntity * leader()
NSDictionary * shipyardInfoForKey:(NSString *key)
OOShipRegistry * sharedRegistry()
id fragmentBurstFromEntity:(Entity *entity)
void setScriptTarget:(ShipEntity *ship)
void receiveCommsMessage:from:(NSString *message_text, [from] ShipEntity *other)
NSDictionary * materialDefaults()
NSDictionary * whitelistDictionary()
NSDictionary * dictionaryFromFilesNamed:inFolder:andMerge:(NSString *fileName,[inFolder] NSString *folderName,[andMerge] BOOL mergeFiles)
void setWeaponEnergy:(float value)
void setSuppressExplosion:(BOOL suppress)
HPVector absoluteTractorPosition()
void setTargetStation:(Entity *targetEntity)
void noteLostTarget()
HPVector distance_twelve:withOffset:(GLfloat dist,[withOffset] GLfloat offset)
void setBounty:withReason:(OOCreditsQuantity amount,[withReason] OOLegalStatusReason reason)
float maxThrust()
void addTarget:(Entity *targetEntity)
void setPrimaryAggressor:(Entity *targetEntity)
Vector v_forward
Definition ShipEntity.h:200
void setDesiredSpeed:(double amount)
NSDictionary * shipInfoDictionary()
void takeEnergyDamage:from:becauseOf:weaponIdentifier:(double amount, [from] Entity *ent, [becauseOf] Entity *other, [weaponIdentifier] NSString *weaponIdentifier)
void setStatus:(OOEntityStatus stat)
GLfloat weaponRange
Definition ShipEntity.h:311
void setRoll:(double amount)
void setThrust:(double amount)
void performTumble()
ShipEntity * subEntityTakingDamage()
void setSpeed:(double amount)
void takeScrapeDamage:from:(double amount,[from] Entity *ent)
void setSingleCrewWithRole:(NSString *crewRole)
void switchLightsOn()
NSUInteger subIdx()
Definition ShipEntity.m:786
OORoleSet * roleSet
Definition ShipEntity.h:332
void setGroup:(OOShipGroup *group)
NSString * beaconCode()
void setEscortDestination:(HPVector dest)
OOShipGroup * group()
void addImpactMoment:fraction:(Vector moment,[fraction] GLfloat howmuch)
static float SurfaceDistanceSqared(Entity *reference, Entity< OOStellarBody > *stellar)
void receiveCommsMessage:from:(NSString *message_text,[from] ShipEntity *other)
void setDisplayName:(NSString *inName)
double rangeToSecondaryTarget:(Entity *target)
BOOL isPolice()
void setPrimaryRole:(NSString *role)
void subEntityDied:(ShipEntity *sub)
OOCargoType cargoType()
Vector v_up
Definition ShipEntity.h:200
NSEnumerator * defenseTargetEnumerator()
void setHeatInsulation:(GLfloat value)
OOCreditsQuantity bounty
Definition ShipEntity.h:300
void respondToAttackFrom:becauseOf:(Entity *from,[becauseOf] Entity *other)
void setPitch:(double amount)
HPVector distance_six:(GLfloat dist)
NSString * identFromShip:(ShipEntity *otherShip)
GLfloat forward_weapon_temp
Definition ShipEntity.h:315
void removeDefenseTarget:(Entity *target)
void setAITo:(NSString *aiString)
void switchLightsOff()
void noteTargetDestroyed:(ShipEntity *target)
void setWeaponRange:(GLfloat value)
void adjustVelocity:(Vector xVel)
void setWeaponRechargeRate:(float value)
OOBehaviour behaviour
Definition ShipEntity.h:211
Vector velocity()
void setEntityPersonalityInt:(uint16_t value)
void setReportAIMessages:(BOOL yn)
void updateEscortFormation()
void setSubIdx:(NSUInteger value)
Definition ShipEntity.m:780
OOCargoQuantity commodityAmount()
void overrideScriptInfo:(NSDictionary *override)
void update:(OOTimeDelta delta_t)
void setTemperature:(GLfloat value)
void resetShotTime()
void setIsBoulder:(BOOL flag)
void setCrew:(NSArray *crewArray)
void setCommodityForPod:andAmount:(OOCommodityType co_type,[andAmount] OOCargoQuantity co_amount)
void markAsOffender:withReason:(int offence_value,[withReason] OOLegalStatusReason reason)
void setBehaviour:(OOBehaviour cond)
void doScriptEvent:withArgument:(jsid message,[withArgument] id argument)
Octree * octree
Definition ShipEntity.h:423
void getTractoredBy:(ShipEntity *other)
void setIsMissileFlag:(BOOL newValue)
void scoopUp:(ShipEntity *other)
void switchAITo:(NSString *aiString)
Triangle absoluteIJKForSubentity()
void reactToAIMessage:context:(NSString *message,[context] NSString *debugContext)
void setFoundTarget:(Entity *targetEntity)
void becomeExplosion()
void setCommodity:andAmount:(OOCommodityType co_type,[andAmount] OOCargoQuantity co_amount)
OOWeaponType forward_weapon_type
Definition ShipEntity.h:305
NSArray * crew
Definition ShipEntity.h:399
GLfloat flightRoll
Definition ShipEntity.h:369
NSMutableArray * subEntities
Definition ShipEntity.h:433
GLfloat maxFlightSpeed
Definition ShipEntity.h:239
OOCommodityType commodityType()
void setOwner:(Entity *who_owns_entity)
BoundingBox findSubentityBoundingBox()
NSString * displayName
Definition ShipEntity.h:330
BoundingBox findBoundingBoxRelativeToPosition:InVectors:i:j:(HPVector opv,[InVectors] Vector,[i] Vector,[j] Vector k)
void setReference:(Vector v)
BOOL isEscort()
static float SurfaceDistanceSqaredV(HPVector reference, Entity< OOStellarBody > *stellar)
void doScriptEvent:withArgument:andArgument:(jsid message,[withArgument] id argument1,[andArgument] id argument2)
Vector v_right
Definition ShipEntity.h:200
void launchShip:(ShipEntity *ship)
Vector portUpVectorForShip:(ShipEntity *ship)
void launchShipWithRole:(NSString *role)
void noteDockedShip:(ShipEntity *ship)
BOOL suckInShip:(ShipEntity *ship)
void setMisjumpWithRange:(GLfloat range)
voidpf uLong int origin
Definition ioapi.h:140
voidpf uLong offset
Definition ioapi.h:140
typedef int(ZCALLBACK *close_file_func) OF((voidpf opaque
const char int mode
Definition ioapi.h:133
float randf(void)
void seed_RNG_only_for_planet_description(Random_Seed s_seed)
float bellf(int n)
unsigned Ranrot(void)
#define ranrot_rand()