Oolite
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 NSDictionary *escortDefinition = nil;
1800 NSDictionary *systeminfo = nil;
1801 OOGovernmentID government;
1802
1803 systeminfo = [UNIVERSE currentSystemData];
1804 government = [systeminfo oo_unsignedCharForKey:KEY_GOVERNMENT];
1805
1806 OOShipGroup *escortGroup = [self escortGroup];
1807 if ([self group] == nil)
1808 {
1809 [self setGroup:escortGroup]; // should probably become a copy of the escortGroup post NMSR.
1810 }
1811 [escortGroup setLeader:self];
1812 _maxEscortCount = MAX_ESCORTS;
1813 [self refreshEscortPositions];
1814
1815 uint8_t currentEscortCount = [escortGroup count] - 1; // always at least 0
1816
1817 _maxEscortCount = 0;
1818 int8_t i = 0;
1819 foreach (escortDefinition, escortRoles)
1820 {
1821 if (currentEscortCount >= MAX_ESCORTS)
1822 {
1823 break;
1824 }
1825 // int rather than uint because, at least for min, there is a
1826 // use to giving a negative value
1827 int8_t min = [escortDefinition oo_intForKey:@"min" defaultValue:0];
1828 int8_t max = [escortDefinition oo_intForKey:@"max" defaultValue:2];
1829 NSString *escortRole = [escortDefinition oo_stringForKey:@"role" defaultValue:@"escort"];
1830 int8_t desired = max;
1831 if (min < desired)
1832 {
1833 for (i = min ; i < max ; i++)
1834 {
1835 if (Ranrot()%11 < government+2)
1836 {
1837 desired--;
1838 }
1839 }
1840 }
1841 for (i = 0; i < desired; i++)
1842 {
1843 if (currentEscortCount >= MAX_ESCORTS)
1844 {
1845 break;
1846 }
1847 if (![escortRole isEqualToString:@""])
1848 {
1849 HPVector ex_pos = [self coordinatesForEscortPosition:currentEscortCount];
1850 ShipEntity *escorter = [UNIVERSE newShipWithRole:escortRole]; // retained
1851 if (escorter == nil)
1852 {
1853 break;
1854 }
1855 [self setUpOneEscort:escorter inGroup:escortGroup withRole:escortRole atPosition:ex_pos andCount:currentEscortCount];
1856 [escorter release];
1857 }
1858 currentEscortCount++;
1859 _maxEscortCount++;
1860 }
1861 }
1862 // done assigning escorts
1863 _pendingEscortCount = 0;
1864}
1865
1866
1867- (void) setUpOneEscort:(ShipEntity *)escorter inGroup:(OOShipGroup *)escortGroup withRole:(NSString *)escortRole atPosition:(HPVector)ex_pos andCount:(uint8_t)currentEscortCount
1868{
1869 NSString *autoAI = nil;
1870 NSString *pilotRole = nil;
1871 NSDictionary *autoAIMap = nil;
1872 NSDictionary *escortShipDict = nil;
1873 AI *escortAI = nil;
1874 NSString *defaultRole = @"escort";
1875
1876 if ([self isPolice])
1877 {
1878 defaultRole = @"wingman";
1879 pilotRole = @"police"; // police are always insured.
1880 }
1881 else
1882 {
1883 pilotRole = bounty ? @"pirate" : @"hunter"; // hunters have insurancies, pirates not.
1884 }
1885
1886 double dd = escorter->collision_radius;
1887
1888 if (EXPECT(currentEscortCount < (uint8_t)MAX_ESCORTS))
1889 {
1890 // spread them around a little randomly
1891 ex_pos.x += dd * 6.0 * (randf() - 0.5);
1892 ex_pos.y += dd * 6.0 * (randf() - 0.5);
1893 ex_pos.z += dd * 6.0 * (randf() - 0.5);
1894 }
1895 else
1896 {
1897 // Thargoid armada(!) Add more distance between the 'escorts'.
1898 ex_pos.x += dd * 12.0 * (randf() - 0.5);
1899 ex_pos.y += dd * 12.0 * (randf() - 0.5);
1900 ex_pos.z += dd * 12.0 * (randf() - 0.5);
1901 }
1902
1903 [escorter setPosition:ex_pos]; // minimise lollipop flash
1904
1905 if ([escorter crew] == nil)
1906 {
1907 [escorter setSingleCrewWithRole:pilotRole];
1908 }
1909
1910 [escorter setPrimaryRole:defaultRole]; //for mothership
1911 // in case this hasn't yet been set, make sure escorts get a real scan class
1912 // shouldn't happen very often, but is possible
1913 if (scanClass == CLASS_NOT_SET)
1914 {
1915 scanClass = CLASS_NEUTRAL;
1916 }
1917 [escorter setScanClass:scanClass]; // you are the same as I
1918
1919 if ([self bounty] == 0) [escorter setBounty:0 withReason:kOOLegalStatusReasonSetup]; // Avoid dirty escorts for clean mothers
1920
1921 // find the right autoAI.
1922 escortShipDict = [escorter shipInfoDictionary];
1923 autoAIMap = [ResourceManager dictionaryFromFilesNamed:@"autoAImap.plist" inFolder:@"Config" andMerge:YES];
1924 autoAI = [autoAIMap oo_stringForKey:defaultRole];
1925 if (autoAI==nil) // no 'wingman' defined in autoAImap?
1926 {
1927 autoAI = [autoAIMap oo_stringForKey:@"escort" defaultValue:@"nullAI.plist"];
1928 }
1929
1930 escortAI = [escorter getAI];
1931
1932 // Let the populator decide which AI to use, unless we have a working alternative AI & we specify auto_ai = NO !
1933 if ( (escortRole && [escortShipDict oo_fuzzyBooleanForKey:@"auto_ai" defaultValue:YES])
1934 || ([[escortAI name] isEqualToString: @"nullAI.plist"] && ![autoAI isEqualToString:@"nullAI.plist"]) )
1935 {
1936 [escorter switchAITo:autoAI];
1937 }
1938
1939 [escorter setGroup:escortGroup];
1940 [escorter setOwner:self]; // mark self as group leader
1941
1942
1943 if ([self status] == STATUS_DOCKED)
1944 {
1945 [[self owner] addShipToLaunchQueue:escorter withPriority:NO];
1946 }
1947 else
1948 {
1949 [UNIVERSE addEntity:escorter]; // STATUS_IN_FLIGHT, AI state GLOBAL
1950 [escortAI setState:@"FLYING_ESCORT"]; // Begin escort flight. (If the AI doesn't define FLYING_ESCORT, this has no effect.)
1951 [escorter doScriptEvent:OOJSID("spawnedAsEscort") withArgument:self];
1952 }
1953
1954 if([escorter heatInsulation] < [self heatInsulation]) [escorter setHeatInsulation:[self heatInsulation]]; // give escorts same protection as mother.
1955 if(([escorter maxFlightSpeed] < cruiseSpeed) && ([escorter maxFlightSpeed] > cruiseSpeed * 0.3))
1956 cruiseSpeed = [escorter maxFlightSpeed] * 0.99; // adapt patrolSpeed to the slowest escort but ignore the very slow ones.
1957
1958
1959 if (bounty)
1960 {
1961 int extra = 1 | (ranrot_rand() & 15);
1962 // if mothership is offender, make sure escorter is too.
1963 [escorter markAsOffender:extra withReason:kOOLegalStatusReasonSetup];
1964 }
1965 else
1966 {
1967 // otherwise force the escort to be clean
1968 [escorter setBounty:0 withReason:kOOLegalStatusReasonSetup];
1969 }
1970
1971}
1972
1973- (NSString *)shipDataKey
1974{
1975 return _shipKey;
1976}
1977
1978
1979- (NSString *)shipDataKeyAutoRole
1980{
1981 return [[[NSString alloc] initWithFormat:@"[%@]",[self shipDataKey]] autorelease];
1982}
1983
1984
1985- (void)setShipDataKey:(NSString *)key
1986{
1987 DESTROY(_shipKey);
1988 _shipKey = [key copy];
1989}
1990
1991
1992- (NSDictionary *)shipInfoDictionary
1993{
1994 return shipinfoDictionary;
1995}
1996
1997
1998#define MAKE_VECTOR_ARRAY(v) [[[OONativeVector alloc] initWithVector:v] autorelease]
1999
2000- (NSArray *) getWeaponOffsetFrom:(NSDictionary *)dict withKey:(NSString *)key inMode:(NSString *)mode
2001{
2002 Vector offset;
2003 if ([mode isEqualToString:@"single"])
2004 {
2005 offset = vector_multiply_scalar([dict oo_vectorForKey:key defaultValue:kZeroVector],_scaleFactor);
2006 return [NSArray arrayWithObject:MAKE_VECTOR_ARRAY(offset)];
2007 }
2008 else
2009 {
2010 NSArray *offsets = [dict oo_arrayForKey:key defaultValue:nil];
2011 if (offsets == nil) {
2013 return [NSArray arrayWithObject:MAKE_VECTOR_ARRAY(offset)];
2014 }
2015 NSMutableArray *output = [NSMutableArray arrayWithCapacity:[offsets count]];
2016 NSUInteger i;
2017 for (i=0;i<[offsets count];i++) {
2018 offset = vector_multiply_scalar([offsets oo_vectorAtIndex:i defaultValue:kZeroVector],_scaleFactor);
2019 [output addObject:MAKE_VECTOR_ARRAY(offset)];
2020 }
2021 return [NSArray arrayWithArray:output];
2022 }
2023}
2024
2025
2026- (NSArray *) aftWeaponOffset
2027{
2028 return aftWeaponOffset;
2029}
2030
2031
2032- (NSArray *) forwardWeaponOffset
2033{
2034 return forwardWeaponOffset;
2035}
2036
2037
2038- (NSArray *) portWeaponOffset
2039{
2040 return portWeaponOffset;
2041}
2042
2043
2044- (NSArray *) starboardWeaponOffset
2045{
2046 return starboardWeaponOffset;
2047}
2048
2049
2050- (BOOL)isFrangible
2051{
2052 return isFrangible;
2053}
2054
2055
2056- (BOOL) suppressFlightNotifications
2057{
2058 return suppressAegisMessages;
2059}
2060
2061
2062- (OOScanClass) scanClass
2063{
2064 if (cloaking_device_active) return CLASS_NO_DRAW;
2065 return scanClass;
2066}
2067
2069
2070- (BOOL) canCollide
2071{
2072 int status = [self status];
2073 if (status == STATUS_COCKPIT_DISPLAY || status == STATUS_DEAD || status == STATUS_BEING_SCOOPED)
2074 {
2075 return NO;
2076 }
2077
2078 if (isWreckage)
2079 {
2080 // wreckage won't collide
2081 return NO;
2082 }
2083
2084 if (isMissile && [self shotTime] < 0.25) // not yet fused
2085 {
2086 return NO;
2087 }
2088
2089 return YES;
2090}
2091
2093{
2094 // octree check
2095 Octree *prime_octree = prime->octree;
2096 Octree *other_octree = other->octree;
2097
2098 HPVector prime_position = [prime absolutePositionForSubentity];
2099 Triangle prime_ijk = [prime absoluteIJKForSubentity];
2100 HPVector other_position = [other absolutePositionForSubentity];
2101 Triangle other_ijk = [other absoluteIJKForSubentity];
2102
2103 Vector relative_position_of_other = resolveVectorInIJK(HPVectorToVector(HPvector_between(prime_position, other_position)), prime_ijk);
2104 Triangle relative_ijk_of_other;
2105 relative_ijk_of_other.v[0] = resolveVectorInIJK(other_ijk.v[0], prime_ijk);
2106 relative_ijk_of_other.v[1] = resolveVectorInIJK(other_ijk.v[1], prime_ijk);
2107 relative_ijk_of_other.v[2] = resolveVectorInIJK(other_ijk.v[2], prime_ijk);
2108
2109 // check hull octree against other hull octree
2110 if ([prime_octree isHitByOctree:other_octree
2111 withOrigin:relative_position_of_other
2112 andIJK:relative_ijk_of_other])
2113 {
2114 return other;
2115 }
2116
2117 // check prime subentities against the other's hull
2118 NSArray *prime_subs = prime->subEntities;
2119 if (prime_subs)
2120 {
2121 NSUInteger i, n_subs = [prime_subs count];
2122 for (i = 0; i < n_subs; i++)
2123 {
2124 Entity* se = [prime_subs objectAtIndex:i];
2125 if ([se isShip] && [se canCollide] && doOctreesCollide((ShipEntity*)se, other))
2126 return other;
2127 }
2128 }
2129
2130 // check prime hull against the other's subentities
2131 NSArray *other_subs = other->subEntities;
2132 if (other_subs)
2133 {
2134 NSUInteger i, n_subs = [other_subs count];
2135 for (i = 0; i < n_subs; i++)
2136 {
2137 Entity* se = [other_subs objectAtIndex:i];
2138 if ([se isShip] && [se canCollide] && doOctreesCollide(prime, (ShipEntity*)se))
2139 return (ShipEntity*)se;
2140 }
2141 }
2142
2143 // check prime subenties against the other's subentities
2144 if ((prime_subs)&&(other_subs))
2145 {
2146 NSUInteger i, n_osubs = [other_subs count];
2147 for (i = 0; i < n_osubs; i++)
2148 {
2149 Entity* oe = [other_subs objectAtIndex:i];
2150 if ([oe isShip] && [oe canCollide])
2151 {
2152 NSUInteger j, n_psubs = [prime_subs count];
2153 for (j = 0; j < n_psubs; j++)
2154 {
2155 Entity* pe = [prime_subs objectAtIndex:j];
2156 if ([pe isShip] && [pe canCollide] && doOctreesCollide((ShipEntity*)pe, (ShipEntity*)oe))
2157 return (ShipEntity*)oe;
2158 }
2159 }
2160 }
2161 }
2162
2163 // fall through => no collision
2164 return nil;
2165}
2166
2167
2168- (BOOL) checkCloseCollisionWith:(Entity *)other
2169{
2170 if (other == nil) return NO;
2171 if ([collidingEntities containsObject:other]) return NO; // we know about this already!
2172
2173 ShipEntity *otherShip = nil;
2174 if ([other isShip]) otherShip = (ShipEntity *)other;
2175
2176 if ([self canScoop:otherShip]) return YES; // quick test - could this improve scooping for small ships? I think so!
2177
2178 if (otherShip != nil && trackCloseContacts)
2179 {
2180 // in update we check if close contacts have gone out of touch range (origin within our collision_radius)
2181 // here we check if something has come within that range
2182 HPVector otherPos = [otherShip position];
2183 OOUniversalID otherID = [otherShip universalID];
2184 NSString *other_key = [NSString stringWithFormat:@"%d", otherID];
2185
2186 if (![closeContactsInfo objectForKey:other_key] &&
2187 HPdistance2(position, otherPos) < collision_radius * collision_radius)
2188 {
2189 // calculate position with respect to our own position and orientation
2190 Vector dpos = HPVectorToVector(HPvector_between(position, otherPos));
2191 Vector rpos = make_vector(dot_product(dpos, v_right), dot_product(dpos, v_up), dot_product(dpos, v_forward));
2192 [closeContactsInfo setObject:[NSString stringWithFormat:@"%f %f %f", rpos.x, rpos.y, rpos.z] forKey: other_key];
2193
2194 // send AI a message about the touch
2195 OOWeakReference *temp = _primaryTarget;
2196 _primaryTarget = [otherShip weakRetain];
2197 [self doScriptEvent:OOJSID("shipCloseContact") withArgument:otherShip andReactToAIMessage:@"CLOSE CONTACT"];
2198 _primaryTarget = temp;
2199 }
2200 }
2201
2202 /* This does not appear to save a significant amount of time in
2203 * most situations. No significant change in frame rate with a
2204 * 350-segment planetary ring at 1400 collision candidates, even
2205 * on old hardware. There are perhaps situations in which it could
2206 * be a significant optimisation, but those are likely to also be
2207 * the situations where the effect of adding hundreds of extra
2208 * false-positive collisions leaves the player returning to a
2209 * mess... So, commented out: CIM 21 Jan 2014
2210 if (zero_distance > CLOSE_COLLISION_CHECK_MAX_RANGE2) // don't work too hard on entities that are far from the player
2211 return YES;
2212 */
2213
2214 if (otherShip != nil)
2215 {
2216 // check hull octree versus other hull octree
2217 collider = doOctreesCollide(self, otherShip);
2218 return (collider != nil);
2219 }
2220
2221 // default at this stage is to say YES they've collided!
2222 collider = other;
2223 return YES;
2224}
2225
2226
2227- (BoundingBox)findSubentityBoundingBox
2228{
2229 return [[self mesh] findSubentityBoundingBoxWithPosition:HPVectorToVector(position) rotMatrix:rotMatrix];
2230}
2231
2232
2233- (Triangle) absoluteIJKForSubentity
2234{
2235 Triangle result = {{ kBasisXVector, kBasisYVector, kBasisZVector }};
2236 Entity *last = nil;
2237 Entity *father = self;
2238 OOMatrix r_mat;
2239
2240 while ((father)&&(father != last) && (father != NO_TARGET))
2241 {
2242 r_mat = [father drawRotationMatrix];
2243 result.v[0] = OOVectorMultiplyMatrix(result.v[0], r_mat);
2244 result.v[1] = OOVectorMultiplyMatrix(result.v[1], r_mat);
2245 result.v[2] = OOVectorMultiplyMatrix(result.v[2], r_mat);
2246
2247 last = father;
2248 if (![last isSubEntity]) break;
2249 father = [father owner];
2250 }
2251 return result;
2252}
2253
2254
2255- (void) addSubentityToCollisionRadius:(Entity<OOSubEntity> *)subent
2256{
2257 if (!subent) return;
2258
2259 double distance = HPmagnitude([subent position]) + [subent findCollisionRadius];
2260 if ([subent isKindOfClass:[ShipEntity class]]) // Solid subentity
2261 {
2262 if (distance > collision_radius)
2263 {
2264 collision_radius = distance;
2265 }
2266
2267 mass += [subent mass];
2268 }
2269 if (distance > _profileRadius)
2270 {
2271 _profileRadius = distance;
2272 }
2273}
2274
2275
2276- (ShipEntity *) launchPodWithCrew:(NSArray *)podCrew
2277{
2278 ShipEntity *pod = nil;
2279
2280 pod = [UNIVERSE newShipWithRole:[shipinfoDictionary oo_stringForKey:@"escape_pod_role"]]; // or nil
2281 if (!pod)
2282 {
2283 // _role not defined? it might have _model defined;
2284 pod = [UNIVERSE newShipWithRole:[shipinfoDictionary oo_stringForKey:@"escape_pod_model" defaultValue:@"escape-capsule"]];
2285 if (!pod)
2286 {
2287 pod = [UNIVERSE newShipWithRole:@"escape-capsule"];
2288 OOLog(@"shipEntity.noEscapePod", @"Ship %@ has no correct escape_pod_role defined. Now using default capsule.", self);
2289 }
2290 }
2291
2292 if (pod)
2293 {
2294 [pod setOwner:self];
2295 [pod setTemperature:[self randomEjectaTemperatureWithMaxFactor:0.9]];
2296 [pod setCommodity:@"slaves" andAmount:1];
2297 [pod setCrew:podCrew];
2298 [pod switchAITo:@"oolite-shuttleAI.js"];
2299 [self dumpItem:pod]; // CLASS_CARGO, STATUS_IN_FLIGHT, AI state GLOBAL
2300 [pod release]; //release
2301 }
2302
2303 return pod;
2304}
2305
2306
2307- (BOOL) validForAddToUniverse
2308{
2309 if (shipinfoDictionary == nil)
2310 {
2311 OOLog(@"shipEntity.notDict", @"Ship %@ was not set up from dictionary.", self);
2312 return NO;
2313 }
2314 return [super validForAddToUniverse];
2315}
2316
2317
2318- (void) update:(OOTimeDelta)delta_t
2319{
2320 if (shipinfoDictionary == nil)
2321 {
2322 OOLog(@"shipEntity.notDict", @"Ship %@ was not set up from dictionary.", self);
2323 [UNIVERSE removeEntity:self];
2324 return;
2325 }
2326
2327 if (!isfinite(maxFlightSpeed))
2328 {
2329 OOLog(@"ship.sanityCheck.failed", @"Ship %@ %@ infinite top speed, clamped to 300.", self, @"had");
2330 maxFlightSpeed = 300;
2331 }
2332
2333 bool isSubEnt = [self isSubEntity];
2334
2335 if (isDemoShip)
2336 {
2337 if (demoRate > 0)
2338 {
2339 OOScalar cos1 = cos(M_PI * ([UNIVERSE getTime] - demoStartTime) * demoRate / 11);
2340 OOScalar sin1 = sin(M_PI * ([UNIVERSE getTime] - demoStartTime) * demoRate / 11);
2341 OOScalar cos2 = cos(-M_PI * ([UNIVERSE getTime] - demoStartTime) * demoRate / 15);
2342 OOScalar sin2 = sin(-M_PI * ([UNIVERSE getTime] - demoStartTime) * demoRate / 15);
2343 Quaternion q1 = make_quaternion(cos1, sin1*sqrt(3)/2, sin1/2, 0);
2344 Quaternion q2 = make_quaternion(cos2, -sin2*sqrt(4)/sqrt(5), 0, sin2*sqrt(1)/sqrt(5));
2345 [self setOrientation: quaternion_multiply(q2, quaternion_multiply(q1, demoStartOrientation))];
2346 }
2347
2348 [super update:delta_t];
2349 if ([self subEntityCount] > 0)
2350 {
2351 // only copy the subent array if there are subentities
2352 ShipEntity *se = nil;
2353 foreach (se, [self subEntities])
2354 {
2355 [se update:delta_t];
2356 if ([se isShip])
2357 {
2358 BoundingBox sebb = [se findSubentityBoundingBox];
2359 bounding_box_add_vector(&totalBoundingBox, sebb.max);
2360 bounding_box_add_vector(&totalBoundingBox, sebb.min);
2361 }
2362 }
2363 }
2364 return;
2365 }
2366
2367
2368 if (!isSubEnt)
2369 {
2370 if (scanClass == CLASS_NOT_SET)
2371 {
2372 scanClass = CLASS_NEUTRAL;
2373 OOLog(@"ship.sanityCheck.failed", @"Ship %@ %@ with scanClass CLASS_NOT_SET; forced to CLASS_NEUTRAL.", self, [self primaryRole]);
2374 }
2375
2376 [self updateTrackingCurve];
2377
2378 //
2379 // deal with collisions
2380 //
2381 [self manageCollisions];
2382
2383 // subentity collisions managed via parent entity
2384
2385 //
2386 // reset any inadvertant legal mishaps
2387 //
2388 if (scanClass == CLASS_POLICE)
2389 {
2390 if (bounty > 0)
2391 {
2392 [self setBounty:0 withReason:kOOLegalStatusReasonPoliceAreClean];
2393 }
2394 ShipEntity* target = [self primaryTarget];
2395 if ((target)&&([target scanClass] == CLASS_POLICE))
2396 {
2397 [self noteLostTarget];
2398 }
2399 }
2400
2401 if (trackCloseContacts)
2402 {
2403 // in checkCloseCollisionWith: we check if some thing has come within touch range (origin within our collision_radius)
2404 // here we check if it has gone outside that range
2405 NSString *other_key = nil;
2406
2407 // create a temp copy to iterate over, since we may want to
2408 // change the original
2409 NSDictionary *closeContactsTemp = [[NSDictionary alloc] initWithDictionary:closeContactsInfo];
2410 foreachkey (other_key, closeContactsTemp)
2411 {
2412 ShipEntity* other = [UNIVERSE entityForUniversalID:[other_key intValue]];
2413 if ((other != nil) && (other->isShip))
2414 {
2415 if (HPdistance2(position, other->position) > collision_radius * collision_radius) // moved beyond our sphere!
2416 {
2417 // calculate position with respect to our own position and orientation
2418 Vector dpos = HPVectorToVector(HPvector_between(position, other->position));
2419 Vector pos1 = make_vector(dot_product(dpos, v_right), dot_product(dpos, v_up), dot_product(dpos, v_forward));
2420 Vector pos0 = {0, 0, 0};
2421 ScanVectorFromString([closeContactsInfo objectForKey: other_key], &pos0);
2422 // send AI messages about the contact
2423 OOWeakReference *temp = _primaryTarget;
2424 _primaryTarget = [other weakRetain];
2425 if ((pos0.x < 0.0)&&(pos1.x > 0.0))
2426 {
2427 [self doScriptEvent:OOJSID("shipTraversePositiveX") withArgument:other andReactToAIMessage:@"POSITIVE X TRAVERSE"];
2428 }
2429 if ((pos0.x > 0.0)&&(pos1.x < 0.0))
2430 {
2431 [self doScriptEvent:OOJSID("shipTraverseNegativeX") withArgument:other andReactToAIMessage:@"NEGATIVE X TRAVERSE"];
2432 }
2433 if ((pos0.y < 0.0)&&(pos1.y > 0.0))
2434 {
2435 [self doScriptEvent:OOJSID("shipTraversePositiveY") withArgument:other andReactToAIMessage:@"POSITIVE Y TRAVERSE"];
2436 }
2437 if ((pos0.y > 0.0)&&(pos1.y < 0.0))
2438 {
2439 [self doScriptEvent:OOJSID("shipTraverseNegativeY") withArgument:other andReactToAIMessage:@"NEGATIVE Y TRAVERSE"];
2440 }
2441 if ((pos0.z < 0.0)&&(pos1.z > 0.0))
2442 {
2443 [self doScriptEvent:OOJSID("shipTraversePositiveZ") withArgument:other andReactToAIMessage:@"POSITIVE Z TRAVERSE"];
2444 }
2445 if ((pos0.z > 0.0)&&(pos1.z < 0.0))
2446 {
2447 [self doScriptEvent:OOJSID("shipTraverseNegativeZ") withArgument:other andReactToAIMessage:@"NEGATIVE Z TRAVERSE"];
2448 }
2449 _primaryTarget = temp;
2450 [closeContactsInfo removeObjectForKey: other_key];
2451 }
2452 }
2453 else
2454 {
2455 [closeContactsInfo removeObjectForKey: other_key];
2456 }
2457 }
2458 [closeContactsTemp release];
2459 } // end if trackCloseContacts
2460
2461 } // end if !isSubEntity
2462
2463
2464#ifndef NDEBUG
2465 // DEBUGGING
2466 if (reportAIMessages && (debugLastBehaviour != behaviour))
2467 {
2468 OOLog(kOOLogEntityBehaviourChanged, @"%@ behaviour is now %@", self, OOStringFromBehaviour(behaviour));
2469 debugLastBehaviour = behaviour;
2470 }
2471#endif
2472
2473 // cool all weapons.
2474 weapon_temp = fmaxf(weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2475 forward_weapon_temp = fmaxf(forward_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2476 aft_weapon_temp = fmaxf(aft_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2477 port_weapon_temp = fmaxf(port_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2478 starboard_weapon_temp = fmaxf(starboard_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f);
2479
2480 // update time between shots
2481 shot_time += delta_t;
2482
2483 // handle radio message effects
2484 if (messageTime > 0.0)
2485 {
2486 messageTime -= delta_t;
2487 if (messageTime < 0.0) messageTime = 0.0;
2488 }
2489
2490 // temperature factors
2491 if(!isSubEnt)
2492 {
2493 double external_temp = 0.0;
2494 OOSunEntity *sun = [UNIVERSE sun];
2495 if (sun != nil)
2496 {
2497 // set the ambient temperature here
2498 double sun_zd = HPdistance2(position, [sun position]); // square of distance
2499 double sun_cr = sun->collision_radius;
2500 double alt1 = sun_cr * sun_cr / sun_zd;
2501 external_temp = SUN_TEMPERATURE * alt1;
2502 if ([sun goneNova]) external_temp *= 100;
2503
2504 if ([self hasFuelScoop] && alt1 > 0.75 && [self fuel] < [self fuelCapacity])
2505 {
2506 fuel_accumulator += (float)(delta_t * flightSpeed * 0.010 / [self fuelChargeRate]);
2507 // are we fast enough to collect any fuel?
2508 while (fuel_accumulator > 1.0f)
2509 {
2510 [self setFuel:[self fuel] + 1];
2511 fuel_accumulator -= 1.0f;
2512 [self doScriptEvent:OOJSID("shipScoopedFuel")];
2513 }
2514 }
2515 }
2516
2517 // work on the ship temperature
2518 //
2519 float heatThreshold = [self heatInsulation] * 100.0f;
2520 if (external_temp > heatThreshold && external_temp > ship_temperature)
2521 ship_temperature += (external_temp - ship_temperature) * delta_t * SHIP_INSULATION_FACTOR / [self heatInsulation];
2522 else
2523 {
2524 if (ship_temperature > SHIP_MIN_CABIN_TEMP)
2525 {
2526 ship_temperature += (external_temp - heatThreshold - ship_temperature) * delta_t * SHIP_COOLING_FACTOR / [self heatInsulation];
2527 if (ship_temperature < SHIP_MIN_CABIN_TEMP) ship_temperature = SHIP_MIN_CABIN_TEMP;
2528 }
2529 }
2530 }
2531 else //subents
2532 {
2533 ship_temperature = [[self owner] temperature];
2534 }
2535
2536 if (ship_temperature > SHIP_MAX_CABIN_TEMP)
2537 [self takeHeatDamage: delta_t * ship_temperature];
2538
2539 // are we burning due to low energy
2540 if ((energy < maxEnergy * 0.20)&&_showDamage) // prevents asteroid etc. from burning
2541 throw_sparks = YES;
2542
2543 // burning effects
2544 if (throw_sparks)
2545 {
2546 next_spark_time -= delta_t;
2547 if (next_spark_time < 0.0)
2548 {
2549 [self throwSparks];
2550 throw_sparks = NO; // until triggered again
2551 }
2552 }
2553
2554 if (!isSubEnt)
2555 {
2556
2557 // cloaking device
2558 if ([self hasCloakingDevice])
2559 {
2560 if (cloaking_device_active)
2561 {
2562 energy -= delta_t * CLOAKING_DEVICE_ENERGY_RATE;
2563 if (energy < CLOAKING_DEVICE_MIN_ENERGY)
2564 {
2565 [self deactivateCloakingDevice];
2566 if (energy < 0) energy = 0;
2567 }
2568 }
2569 }
2570
2571 // military_jammer
2572 if ([self hasMilitaryJammer])
2573 {
2574 if (military_jammer_active)
2575 {
2576 energy -= delta_t * MILITARY_JAMMER_ENERGY_RATE;
2577 if (energy < MILITARY_JAMMER_MIN_ENERGY)
2578 {
2579 military_jammer_active = NO;
2580 if (energy < 0) energy = 0;
2581 }
2582 }
2583 else
2584 {
2585 if (energy > 1.5 * MILITARY_JAMMER_MIN_ENERGY)
2586 military_jammer_active = YES;
2587 }
2588 }
2589
2590 // check outside factors
2591 /* aegis checks are expensive, so only do them once every km or so of flight
2592 * unlikely to be important otherwise. (every 100m if already close to
2593 * planet, to watch for surface)
2594
2595 * if have non-zero inertial velocity, need to check every frame,
2596 * as distanceTravelled does not include this component - CIM */
2597 if (_nextAegisCheck < distanceTravelled || !vector_equal([super velocity],kZeroVector))
2598 {
2599 aegis_status = [self checkForAegis]; // is a station or something nearby??
2600 if (aegis_status == AEGIS_NONE)
2601 {
2602 // in open space: check every km
2603 _nextAegisCheck = distanceTravelled + 1000.0;
2604 }
2605 else
2606 {
2607 // near planets: check every 100m
2608 _nextAegisCheck = distanceTravelled + 100.0;
2609 }
2610 }
2611 } // end if !isSubEntity
2612
2613 // scripting
2614 if (!haveExecutedSpawnAction)
2615 {
2616 // When crashing into a boulder, STATUS_LAUNCHING is sometimes skipped on scooping the resulting splinters.
2617 OOEntityStatus status = [self status];
2618 if (script != nil && (status == STATUS_IN_FLIGHT ||
2619 status == STATUS_LAUNCHING ||
2620 status == STATUS_BEING_SCOOPED ||
2621 (status == STATUS_ACTIVE && self == [UNIVERSE station])
2622 ))
2623 {
2624 [PLAYER setScriptTarget:self];
2625 [self doScriptEvent:OOJSID("shipSpawned")];
2626 if ([self status] != STATUS_DEAD) [PLAYER doScriptEvent:OOJSID("shipSpawned") withArgument:self];
2627 }
2628 haveExecutedSpawnAction = YES;
2629 }
2630 /* No point in starting the AI if still launching */
2631 if (!haveStartedJSAI && [self status] != STATUS_LAUNCHING)
2632 {
2633 haveStartedJSAI = YES;
2634 [self doScriptEvent:OOJSID("aiStarted")];
2635 }
2636
2637 // behaviours according to status and behaviour
2638 //
2639 if ([self status] == STATUS_LAUNCHING)
2640 {
2641 if ([UNIVERSE getTime] > launch_time + launch_delay) // move for while before thinking
2642 {
2643 StationEntity *stationLaunchedFrom = [UNIVERSE nearestEntityMatchingPredicate:IsStationPredicate parameter:NULL relativeToEntity:self];
2644 [self setStatus:STATUS_IN_FLIGHT];
2645 // awaken JS-based AIs
2646 haveStartedJSAI = YES;
2647 [self doScriptEvent:OOJSID("aiStarted")];
2648 [self doScriptEvent:OOJSID("shipLaunchedFromStation") withArgument:stationLaunchedFrom];
2649 [shipAI reactToMessage:@"LAUNCHED OKAY" context:@"launched"];
2650 }
2651 else
2652 {
2653 // ignore behaviour just keep moving...
2654 flightYaw = 0.0;
2655 [self applyAttitudeChanges:delta_t];
2656 [self applyThrust:delta_t];
2657 if (energy < maxEnergy)
2658 {
2659 energy += energy_recharge_rate * delta_t;
2660 if (energy > maxEnergy)
2661 {
2662 energy = maxEnergy;
2663 [self doScriptEvent:OOJSID("shipEnergyBecameFull")];
2664 [shipAI message:@"ENERGY_FULL"];
2665 }
2666 }
2667
2668 if ([self subEntityCount] > 0)
2669 {
2670 // only copy the subent array if there are subentities
2671 ShipEntity *se = nil;
2672 foreach (se, [self subEntities])
2673 {
2674 [se update:delta_t];
2675 }
2676 }
2677 // super update
2678 [super update:delta_t];
2679
2680 return;
2681 }
2682 }
2683 //
2684 // double check scooped behaviour
2685 //
2686 if ([self status] == STATUS_BEING_SCOOPED)
2687 {
2688 //if we are being tractored, but we have no owner, then we have a problem
2689 if (behaviour != BEHAVIOUR_TRACTORED || [self owner] == nil || [self owner] == self || [self owner] == NO_TARGET)
2690 {
2691 // escaped tractor beam
2692 [self setStatus:STATUS_IN_FLIGHT]; // should correct 'uncollidable objects' bug
2693 behaviour = BEHAVIOUR_IDLE;
2694 frustration = 0.0;
2695 [self setOwner:self];
2696 [shipAI exitStateMachineWithMessage:nil]; // Escapepods and others should continue their old AI here.
2697 }
2698 }
2699
2700 if ([self status] == STATUS_COCKPIT_DISPLAY)
2701 {
2702 flightYaw = 0.0;
2703 [self applyAttitudeChanges:delta_t];
2704 GLfloat range2 = 0.1 * HPdistance2(position, _destination) / (collision_radius * collision_radius);
2705 if ((range2 > 1.0)||(velocity.z > 0.0)) range2 = 1.0;
2706 position = HPvector_add(position, vectorToHPVector(vector_multiply_scalar(velocity, range2 * delta_t)));
2707 }
2708 else
2709 {
2710 [self processBehaviour:delta_t];
2711
2712 // manage energy
2713 if (energy < maxEnergy)
2714 {
2715 energy += energy_recharge_rate * delta_t;
2716 if (energy > maxEnergy)
2717 {
2718 energy = maxEnergy;
2719 [self doScriptEvent:OOJSID("shipEnergyBecameFull")];
2720 [shipAI message:@"ENERGY_FULL"];
2721 }
2722 }
2723
2724 if (!isSubEnt)
2725 {
2726 // update destination position for escorts
2727 [self refreshEscortPositions];
2728 if ([self hasEscorts])
2729 {
2730 ShipEntity *escort = nil;
2731 unsigned i = 0;
2732 // Note: works on escortArray rather than escortEnumerator because escorts may be mutated.
2733 foreach (escort, [self escortArray])
2734 {
2735 [escort setEscortDestination:[self coordinatesForEscortPosition:i++]];
2736 }
2737
2738 ShipEntity *leader = [[self escortGroup] leader];
2739 if (leader != nil && ([leader scanClass] != [self scanClass])) {
2740 OOLog(@"ship.sanityCheck.failed", @"Ship %@ escorting %@ with wrong scanclass!", self, leader);
2741 [[self escortGroup] removeShip:self];
2742 [self setEscortGroup:nil];
2743 }
2744 }
2745 }
2746 }
2747
2748 // rotational velocity
2749 if (!quaternion_equal(subentityRotationalVelocity, kIdentityQuaternion) &&
2750 !quaternion_equal(subentityRotationalVelocity, kZeroQuaternion))
2751 {
2752 Quaternion qf = subentityRotationalVelocity;
2753 qf.w *= (1.0 - delta_t);
2754 qf.x *= delta_t;
2755 qf.y *= delta_t;
2756 qf.z *= delta_t;
2757 [self setOrientation:quaternion_multiply(qf, orientation)];
2758 }
2759
2760 // reset totalBoundingBox
2761 totalBoundingBox = boundingBox;
2762
2763 // super update
2764 [super update:delta_t];
2765
2766 // update subentities
2767
2768 if ([self subEntityCount] > 0)
2769 {
2770 // only copy the subent array if there are subentities
2771 ShipEntity *se = nil;
2772 foreach (se, [self subEntities])
2773 {
2774 [se update:delta_t];
2775 if ([se isShip])
2776 {
2777 BoundingBox sebb = [se findSubentityBoundingBox];
2778 bounding_box_add_vector(&totalBoundingBox, sebb.max);
2779 bounding_box_add_vector(&totalBoundingBox, sebb.min);
2780 }
2781 }
2782 }
2783
2784 if (aiScriptWakeTime > 0 && [PLAYER clockTimeAdjusted] > aiScriptWakeTime)
2785 {
2786 aiScriptWakeTime = 0;
2787 [self doScriptEvent:OOJSID("aiAwoken")];
2788 }
2789}
2790
2791
2792- (void) processBehaviour:(OOTimeDelta)delta_t
2793{
2794 BOOL applyThrust = YES;
2795 switch (behaviour)
2796 {
2797 case BEHAVIOUR_TUMBLE :
2798 [self behaviour_tumble: delta_t];
2799 break;
2800
2801 case BEHAVIOUR_STOP_STILL :
2802 case BEHAVIOUR_STATION_KEEPING :
2803 [self behaviour_stop_still: delta_t];
2804 break;
2805
2806 case BEHAVIOUR_IDLE :
2807 if ([self isSubEntity])
2808 {
2809 applyThrust = NO;
2810 }
2811 [self behaviour_idle: delta_t];
2812 break;
2813
2814 case BEHAVIOUR_TRACTORED :
2815 [self behaviour_tractored: delta_t];
2816 break;
2817
2818 case BEHAVIOUR_TRACK_TARGET :
2819 [self behaviour_track_target: delta_t];
2820 break;
2821
2822 case BEHAVIOUR_INTERCEPT_TARGET :
2823 case BEHAVIOUR_COLLECT_TARGET :
2824 [self behaviour_intercept_target: delta_t];
2825 break;
2826
2827 case BEHAVIOUR_ATTACK_TARGET :
2828 [self behaviour_attack_target: delta_t];
2829 break;
2830
2831 case BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX :
2832 case BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE :
2833 [self behaviour_fly_to_target_six: delta_t];
2834 break;
2835
2836 case BEHAVIOUR_ATTACK_MINING_TARGET :
2837 [self behaviour_attack_mining_target: delta_t];
2838 break;
2839
2840 case BEHAVIOUR_ATTACK_FLY_TO_TARGET :
2841 [self behaviour_attack_fly_to_target: delta_t];
2842 break;
2843
2844 case BEHAVIOUR_ATTACK_FLY_FROM_TARGET :
2845 [self behaviour_attack_fly_from_target: delta_t];
2846 break;
2847
2848 case BEHAVIOUR_ATTACK_BREAK_OFF_TARGET :
2849 [self behaviour_attack_break_off_target: delta_t];
2850 break;
2851
2852 case BEHAVIOUR_ATTACK_SLOW_DOGFIGHT :
2853 [self behaviour_attack_slow_dogfight: delta_t];
2854 break;
2855
2856 case BEHAVIOUR_RUNNING_DEFENSE :
2857 [self behaviour_running_defense: delta_t];
2858 break;
2859
2860 case BEHAVIOUR_ATTACK_BROADSIDE :
2861 [self behaviour_attack_broadside: delta_t];
2862 break;
2863
2864 case BEHAVIOUR_ATTACK_BROADSIDE_LEFT :
2865 [self behaviour_attack_broadside_left: delta_t];
2866 break;
2867
2868 case BEHAVIOUR_ATTACK_BROADSIDE_RIGHT :
2869 [self behaviour_attack_broadside_right: delta_t];
2870 break;
2871
2872 case BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE :
2873 [self behaviour_close_to_broadside_range: delta_t];
2874 break;
2875
2876 case BEHAVIOUR_CLOSE_WITH_TARGET :
2877 [self behaviour_close_with_target: delta_t];
2878 break;
2879
2880 case BEHAVIOUR_ATTACK_SNIPER :
2881 [self behaviour_attack_sniper: delta_t];
2882 break;
2883
2884 case BEHAVIOUR_EVASIVE_ACTION :
2885 case BEHAVIOUR_FLEE_EVASIVE_ACTION :
2886 [self behaviour_evasive_action: delta_t];
2887 break;
2888
2889 case BEHAVIOUR_FLEE_TARGET :
2890 [self behaviour_flee_target: delta_t];
2891 break;
2892
2893 case BEHAVIOUR_FLY_RANGE_FROM_DESTINATION :
2894 [self behaviour_fly_range_from_destination: delta_t];
2895 break;
2896
2897 case BEHAVIOUR_FACE_DESTINATION :
2898 [self behaviour_face_destination: delta_t];
2899 break;
2900
2901 case BEHAVIOUR_LAND_ON_PLANET :
2902 [self behaviour_land_on_planet: delta_t];
2903 break;
2904
2905 case BEHAVIOUR_FORMATION_FORM_UP :
2906 [self behaviour_formation_form_up: delta_t];
2907 break;
2908
2909 case BEHAVIOUR_FLY_TO_DESTINATION :
2910 [self behaviour_fly_to_destination: delta_t];
2911 break;
2912
2913 case BEHAVIOUR_FLY_FROM_DESTINATION :
2914 case BEHAVIOUR_FORMATION_BREAK :
2915 [self behaviour_fly_from_destination: delta_t];
2916 break;
2917
2918 case BEHAVIOUR_AVOID_COLLISION :
2919 [self behaviour_avoid_collision: delta_t];
2920 break;
2921
2922 case BEHAVIOUR_TRACK_AS_TURRET :
2923 applyThrust = NO;
2924 [self behaviour_track_as_turret: delta_t];
2925 break;
2926
2927 case BEHAVIOUR_FLY_THRU_NAVPOINTS :
2928 [self behaviour_fly_thru_navpoints: delta_t];
2929 break;
2930
2931 case BEHAVIOUR_SCRIPTED_AI:
2932 case BEHAVIOUR_SCRIPTED_ATTACK_AI:
2933 [self behaviour_scripted_ai: delta_t];
2934 break;
2935
2936 case BEHAVIOUR_ENERGY_BOMB_COUNTDOWN:
2937 applyThrust = NO;
2938 // Do nothing
2939 break;
2940 }
2941
2942 // generally the checks above should be turning this *off* for subents
2943 if (applyThrust)
2944 {
2945 [self applyAttitudeChanges:delta_t];
2946 [self applyThrust:delta_t];
2947 }
2948}
2949
2950
2951// called when behaviour is unable to improve position
2952- (void)noteFrustration:(NSString *)context
2953{
2954 [shipAI reactToMessage:@"FRUSTRATED" context:context];
2955 [self doScriptEvent:OOJSID("shipAIFrustrated") withArgument:context];
2956}
2957
2958
2959- (void)respondToAttackFrom:(Entity *)from becauseOf:(Entity *)other
2960{
2961 Entity *source = nil;
2962
2963 if ([other isKindOfClass:[ShipEntity class]])
2964 {
2965 source = other;
2966
2967 // JSAIs handle friendly fire themselves
2968 if (![self hasNewAI])
2969 {
2970
2971 ShipEntity *hunter = (ShipEntity *)other;
2972 //if we are in the same group, then we have to be careful about how we handle things
2973 if ([self isPolice] && [hunter isPolice])
2974 {
2975 //police never get into a fight with each other
2976 return;
2977 }
2978
2979 OOShipGroup *group = [self group];
2980
2981 if (group != nil && group == [hunter group])
2982 {
2983 //we are in the same group, do we forgive you?
2984 //criminals are less likely to forgive
2985 if (randf() < (0.8 - (bounty/100)))
2986 {
2987 //it was an honest mistake, lets get on with it
2988 return;
2989 }
2990
2991 ShipEntity *groupLeader = [group leader];
2992 if (hunter == groupLeader)
2993 {
2994 //oops we were attacked by our leader, desert him
2995 [group removeShip:self];
2996 }
2997 else
2998 {
2999 //evict them from our group
3000 [group removeShip:hunter];
3001
3002 [groupLeader setFoundTarget:other];
3003 [groupLeader setPrimaryAggressor:hunter];
3004 [groupLeader respondToAttackFrom:from becauseOf:other];
3005 }
3006 }
3007 }
3008 }
3009 else
3010 {
3011 source = from;
3012 }
3013
3014 [self doScriptEvent:OOJSID("shipBeingAttacked") withArgument:source andReactToAIMessage:@"ATTACKED"];
3015 if ([source isShip]) [(ShipEntity *)source doScriptEvent:OOJSID("shipAttackedOther") withArgument:self];
3016}
3017
3018
3019// Equipment
3020
3021- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeWeapons:(BOOL)includeWeapons whileLoading:(BOOL)loading
3022{
3023 if ([self hasOneEquipmentItem:itemKey includeMissiles:includeWeapons whileLoading:loading]) return YES;
3024
3025 if (loading)
3026 {
3027 NSString *damaged = [itemKey stringByAppendingString:@"_DAMAGED"];
3028 if ([_equipment containsObject:damaged]) return YES;
3029 }
3030
3031 if (includeWeapons)
3032 {
3033 // Check for primary weapon
3035 if (!isWeaponNone(weaponType))
3036 {
3037 if ([self hasPrimaryWeapon:weaponType]) return YES;
3038 }
3039 }
3040
3041 return NO;
3042}
3043
3044
3045- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeMissiles:(BOOL)includeMissiles whileLoading:(BOOL)loading
3046{
3047 if ([_equipment containsObject:itemKey]) return YES;
3048
3049 if (loading)
3050 {
3051 NSString *damaged = [itemKey stringByAppendingString:@"_DAMAGED"];
3052 if ([_equipment containsObject:damaged]) return YES;
3053 }
3054
3055 if (includeMissiles && missiles > 0)
3056 {
3057 unsigned i;
3058 if ([itemKey isEqualToString:@"thargon"]) itemKey = @"EQ_THARGON";
3059 for (i = 0; i < missiles; i++)
3060 {
3061 if (missile_list[i] != nil && [[missile_list[i] identifier] isEqualTo:itemKey]) return YES;
3062 }
3063 }
3064
3065 return NO;
3066}
3067
3068
3069- (BOOL) hasPrimaryWeapon:(OOWeaponType)weaponType
3070{
3071 NSEnumerator *subEntEnum = nil;
3072 ShipEntity *subEntity = nil;
3073
3074 if ([[forward_weapon_type identifier] isEqualToString:[weaponType identifier]] ||
3075 [[aft_weapon_type identifier] isEqualToString:[weaponType identifier]] ||
3076 [[port_weapon_type identifier] isEqualToString:[weaponType identifier]] ||
3077 [[starboard_weapon_type identifier] isEqualToString:[weaponType identifier]])
3078 {
3079 return YES;
3080 }
3081
3082 for (subEntEnum = [self shipSubEntityEnumerator]; (subEntity = [subEntEnum nextObject]); )
3083 {
3084 if ([subEntity hasPrimaryWeapon:weaponType]) return YES;
3085 }
3086
3087 return NO;
3088}
3089
3090
3091- (NSUInteger) countEquipmentItem:(NSString *)eqkey
3092{
3093 NSString *eq = nil;
3094 NSUInteger count = 0;
3095 foreach (eq, _equipment)
3096 {
3097 if ([eqkey isEqualToString:eq])
3098 {
3099 ++count;
3100 }
3101 }
3102 return count;
3103}
3104
3105
3106- (BOOL) hasEquipmentItem:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons whileLoading:(BOOL)loading
3107{
3108 // this method is also used internally to find out if an equipped item is undamaged.
3109 if ([equipmentKeys isKindOfClass:[NSString class]])
3110 {
3111 return [self hasOneEquipmentItem:equipmentKeys includeWeapons:includeWeapons whileLoading:loading];
3112 }
3113 else
3114 {
3115 NSParameterAssert([equipmentKeys isKindOfClass:[NSArray class]] || [equipmentKeys isKindOfClass:[NSSet class]]);
3116
3117 id key = nil;
3118 foreach (key, equipmentKeys)
3119 {
3120 if ([self hasOneEquipmentItem:key includeWeapons:includeWeapons whileLoading:loading]) return YES;
3121 }
3122 }
3123
3124 return NO;
3125}
3126
3127
3128- (BOOL) hasEquipmentItem:(id)equipmentKeys
3129{
3130 return [self hasEquipmentItem:equipmentKeys includeWeapons:NO whileLoading:NO];
3131}
3132
3133
3134/* allows OXP equipment to provide core functions (or indeed OXP
3135 * functions, potentially) */
3136- (BOOL) hasEquipmentItemProviding:(NSString *)equipmentType
3137{
3138 NSString *key = nil;
3139 foreach (key, _equipment) {
3140 if ([key isEqualToString:equipmentType])
3141 {
3142 // equipment always provides itself
3143 return YES;
3144 }
3145 else
3146 {
3148 if (et != nil && [et provides:equipmentType])
3149 {
3150 return YES;
3151 }
3152 }
3153 }
3154 return NO;
3155}
3156
3157
3158- (NSString *) equipmentItemProviding:(NSString *)equipmentType
3159{
3160 NSString *key = nil;
3161 foreach (key, _equipment) {
3162 if ([key isEqualToString:equipmentType])
3163 {
3164 // equipment always provides itself
3165 return [[key copy] autorelease];
3166 }
3167 else
3168 {
3170 if (et != nil && [et provides:equipmentType])
3171 {
3172 return [[key copy] autorelease];
3173 }
3174 }
3175 }
3176 return nil;
3177}
3178
3179
3180- (BOOL) hasAllEquipment:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons whileLoading:(BOOL)loading
3181{
3182 NSEnumerator *keyEnum = nil;
3183 id key = nil;
3184
3185 if (_equipment == nil) return NO;
3186
3187 // Make sure it's an array or set, using a single-object set if it's a string.
3188 if ([equipmentKeys isKindOfClass:[NSString class]]) equipmentKeys = [NSArray arrayWithObject:equipmentKeys];
3189 else if (![equipmentKeys isKindOfClass:[NSArray class]] && ![equipmentKeys isKindOfClass:[NSSet class]]) return NO;
3190
3191 for (keyEnum = [equipmentKeys objectEnumerator]; (key = [keyEnum nextObject]); )
3192 {
3193 if (![self hasOneEquipmentItem:key includeWeapons:includeWeapons whileLoading:loading]) return NO;
3194 }
3195
3196 return YES;
3197}
3198
3199
3200- (BOOL) hasAllEquipment:(id)equipmentKeys
3201{
3202 return [self hasAllEquipment:equipmentKeys includeWeapons:NO whileLoading:NO];
3203}
3204
3205
3206- (BOOL) hasHyperspaceMotor
3207{
3208 return hyperspaceMotorSpinTime >= 0;
3209}
3210
3211
3212- (float) hyperspaceSpinTime
3213{
3214 return hyperspaceMotorSpinTime;
3215}
3216
3217
3218- (void) setHyperspaceSpinTime:(float)new
3219{
3220 hyperspaceMotorSpinTime = new;
3221}
3222
3223
3224- (BOOL) canAddEquipment:(NSString *)equipmentKey inContext:(NSString *)context
3225{
3226 if ([equipmentKey hasSuffix:@"_DAMAGED"])
3227 {
3228 equipmentKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
3229 }
3230
3231 NSString * lcEquipmentKey = [equipmentKey lowercaseString];
3232 if ([equipmentKey hasSuffix:@"MISSILE"]||[equipmentKey hasSuffix:@"MINE"]||([self isThargoid] && ([lcEquipmentKey hasPrefix:@"thargon"] || [lcEquipmentKey hasSuffix:@"thargon"])))
3233 {
3234 if (missiles >= max_missiles) return NO;
3235 }
3236
3238
3239 if (![eqType canCarryMultiple] && [self hasEquipmentItem:equipmentKey]) return NO;
3240 if (![self equipmentValidToAdd:equipmentKey inContext:context]) return NO;
3241
3242 return YES;
3243}
3244
3245
3246- (OOWeaponFacingSet) weaponFacings
3247{
3248 return weapon_facings;
3249}
3250
3251
3252- (OOWeaponType) weaponTypeIDForFacing:(OOWeaponFacing)facing strict:(BOOL)strict
3253{
3254 OOWeaponType weaponType = nil;
3255
3256 if (facing & weapon_facings)
3257 {
3258 switch (facing)
3259 {
3261 weaponType = forward_weapon_type;
3262 // if no forward weapon, and not carrying out a strict check, see if subentities have forward weapons, return the first one found.
3263 if (isWeaponNone(weaponType) && !strict)
3264 {
3265 NSEnumerator *subEntEnum = [self shipSubEntityEnumerator];
3266 ShipEntity *subEntity = nil;
3267 while (isWeaponNone(weaponType) && (subEntity = [subEntEnum nextObject]))
3268 {
3269 weaponType = subEntity->forward_weapon_type;
3270 }
3271 }
3272 break;
3273
3274 case WEAPON_FACING_AFT:
3275 weaponType = aft_weapon_type;
3276 break;
3277
3278 case WEAPON_FACING_PORT:
3279 weaponType = port_weapon_type;
3280 break;
3281
3283 weaponType = starboard_weapon_type;
3284 break;
3285
3286 case WEAPON_FACING_NONE:
3287 break;
3288 }
3289 }
3290 return weaponType;
3291}
3292
3293- (OOEquipmentType *) weaponTypeForFacing:(OOWeaponFacing)facing strict:(BOOL)strict
3294{
3295// OOWeaponType weaponType = [self weaponTypeIDForFacing:facing strict:strict];
3296// return [OOEquipmentType equipmentTypeWithIdentifier:OOEquipmentIdentifierFromWeaponType(weaponType)];
3297 return [self weaponTypeIDForFacing:facing strict:strict];
3298}
3299
3300
3301- (NSArray *) missilesList
3302{
3303 // if missile_list is empty, avoid exception and return empty NSArray instead
3304 return missile_list[0] != nil ? [NSArray arrayWithObjects:missile_list count:missiles] :
3305 [NSArray array];
3306}
3307
3308
3309- (NSArray *) passengerListForScripting
3310{
3311 return [NSArray array];
3312}
3313
3314
3315- (NSArray *) parcelListForScripting
3316{
3317 return [NSArray array];
3318}
3319
3320
3321- (NSArray *) contractListForScripting
3322{
3323 return [NSArray array];
3324}
3325
3326
3327- (OOEquipmentType *) generateMissileEquipmentTypeFrom:(NSString *)role
3328{
3329 /* The generated missile equipment type provides for backward compatibility with pre-1.74 OXPs missile_roles
3330 and follows this template:
3331
3332 //NPC equipment, incompatible with player ship. Not buyable because of its TL.
3333 (
3334 100, 100000, "Missile",
3335 "EQ_X_MISSILE",
3336 "Unidentified missile type.",
3337 {
3338 is_external_store = true;
3339 }
3340 )
3341 */
3342 NSArray *itemInfo = [NSArray arrayWithObjects:@"100", @"100000", @"Missile", role, @"Unidentified missile type.",
3343 [NSDictionary dictionaryWithObjectsAndKeys: @"true", @"is_external_store", nil], nil];
3344
3347}
3348
3349
3350- (NSArray *) equipmentListForScripting
3351{
3352 NSArray *eqTypes = [OOEquipmentType allEquipmentTypes];
3353 NSMutableArray *quip = [NSMutableArray arrayWithCapacity:[eqTypes count]];
3354 OOEquipmentType *eqType = nil;
3355 BOOL isDamaged;
3356
3357 foreach (eqType, eqTypes)
3358 {
3359 // Equipment list, consistent with the rest of the API - Kaks
3360 if ([eqType canCarryMultiple])
3361 {
3362 NSString *damagedIdentifier = [[eqType identifier] stringByAppendingString:@"_DAMAGED"];
3363 NSUInteger i, count = 0;
3364 count += [self countEquipmentItem:[eqType identifier]];
3365 count += [self countEquipmentItem:damagedIdentifier];
3366 for (i=0;i<count;i++)
3367 {
3368 [quip addObject:eqType];
3369 }
3370 }
3371 else
3372 {
3373 isDamaged = [self hasEquipmentItem:[[eqType identifier] stringByAppendingString:@"_DAMAGED"]];
3374 if ([self hasEquipmentItem:[eqType identifier]] || isDamaged)
3375 {
3376 [quip addObject:eqType];
3377 }
3378 }
3379 }
3380
3381 // Passengers - not supported yet for NPCs, but it's here for genericity.
3382 if ([self passengerCapacity] > 0)
3383 {
3384 eqType = [OOEquipmentType equipmentTypeWithIdentifier:@"EQ_PASSENGER_BERTH"];
3385 //[quip addObject:[self eqDictionaryWithType:eqType isDamaged:NO]];
3386 [quip addObject:eqType];
3387 }
3388
3389 return [[quip copy] autorelease];
3390}
3391
3392
3393- (BOOL) equipmentValidToAdd:(NSString *)equipmentKey inContext:(NSString *)context
3394{
3395 return [self equipmentValidToAdd:equipmentKey whileLoading:NO inContext:context];
3396}
3397
3398
3399- (BOOL) equipmentValidToAdd:(NSString *)equipmentKey whileLoading:(BOOL)loading inContext:(NSString *)context
3400{
3401 OOEquipmentType *eqType = nil;
3402 BOOL validationForDamagedEquipment = NO;
3403
3404 if ([equipmentKey hasSuffix:@"_DAMAGED"])
3405 {
3406 equipmentKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
3407 }
3408
3409 eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey];
3410 if (eqType == nil) return NO;
3411
3412 // need to know if we are trying to add a Repair version of the equipment. In some cases
3413 // (e.g. available cargo space required), it makes sense to deny installation of equipment
3414 // if the condition is not satisfied, but it doesn't make sense to deny repair when the
3415 // equipment is already installed. For now, we are checking only the cargo space condition,
3416 // but other conditions might need to be revised too. - Nikos, 20151115
3417 if ([self hasEquipmentItem:[eqType damagedIdentifier]])
3418 {
3419 validationForDamagedEquipment = YES;
3420 }
3421
3422 // not all conditions make sence checking while loading a game with already purchaged equipment.
3423 // while loading, we mainly need to catch changes when the installed oxps set has changed since saving.
3424 if ([eqType requiresEmptyPylon] && [self missileCount] >= [self missileCapacity] && !loading) return NO;
3425 if ([eqType requiresMountedPylon] && [self missileCount] == 0 && !loading) return NO;
3426 if ([self availableCargoSpace] < [eqType requiredCargoSpace] && !validationForDamagedEquipment && !loading) return NO;
3427 if ([eqType requiresEquipment] != nil && ![self hasAllEquipment:[eqType requiresEquipment] includeWeapons:YES whileLoading:loading]) return NO;
3428 if ([eqType requiresAnyEquipment] != nil && ![self hasEquipmentItem:[eqType requiresAnyEquipment] includeWeapons:YES whileLoading:loading]) return NO;
3429 if ([eqType incompatibleEquipment] != nil && [self hasEquipmentItem:[eqType incompatibleEquipment] includeWeapons:YES whileLoading:loading]) return NO;
3430 if ([eqType requiresCleanLegalRecord] && [self legalStatus] != 0 && !loading) return NO;
3431 if ([eqType requiresNonCleanLegalRecord] && [self legalStatus] == 0 && !loading) return NO;
3432 if ([eqType requiresFreePassengerBerth] && [self passengerCount] >= [self passengerCapacity]) return NO;
3433 if ([eqType requiresFullFuel] && [self fuel] < [self fuelCapacity] && !loading) return NO;
3434 if ([eqType requiresNonFullFuel] && [self fuel] >= [self fuelCapacity] && !loading) return NO;
3435
3436 if (!loading)
3437 {
3438 NSString *condition_script = [eqType conditionScript];
3439 if (condition_script != nil)
3440 {
3441 OOJSScript *condScript = [UNIVERSE getConditionScript:condition_script];
3442 if (condScript != nil) // should always be non-nil, but just in case
3443 {
3444 JSContext *JScontext = OOJSAcquireContext();
3445 BOOL OK;
3446 JSBool allow_addition = false;
3447 jsval result;
3448 jsval args[] = { OOJSValueFromNativeObject(JScontext, equipmentKey) , OOJSValueFromNativeObject(JScontext, self) , OOJSValueFromNativeObject(JScontext, context)};
3449
3450 OK = [condScript callMethod:OOJSID("allowAwardEquipment")
3451 inContext:JScontext
3452 withArguments:args count:sizeof args / sizeof *args
3453 result:&result];
3454
3455 if (OK) OK = JS_ValueToBoolean(JScontext, result, &allow_addition);
3456
3457 OOJSRelinquishContext(JScontext);
3458
3459 if (OK && !allow_addition)
3460 {
3461 /* if the script exists, the function exists, the function
3462 * returns a bool, and that bool is false, block
3463 * addition. Otherwise allow it as default */
3464 return NO;
3465 }
3466 }
3467 }
3468 }
3469
3470 if ([self isPlayer])
3471 {
3472 if (![eqType isAvailableToPlayer]) return NO;
3473 if (![eqType isAvailableToAll])
3474 {
3475 // find options that agree with this ship. Only player ships have these options.
3477 NSDictionary *shipyardInfo = [registry shipyardInfoForKey:[self shipDataKey]];
3478 NSMutableSet *options = [NSMutableSet setWithArray:[shipyardInfo oo_arrayForKey:KEY_OPTIONAL_EQUIPMENT]];
3479 [options addObjectsFromArray:[[shipyardInfo oo_dictionaryForKey:KEY_STANDARD_EQUIPMENT] oo_arrayForKey:KEY_EQUIPMENT_EXTRAS]];
3480 if (![options containsObject:equipmentKey]) return NO;
3481 }
3482 }
3483 else
3484 {
3485 if (![eqType isAvailableToNPCs]) return NO;
3486 }
3487
3488 return YES;
3489}
3490
3491
3492- (BOOL) setWeaponMount:(OOWeaponFacing)facing toWeapon:(NSString *)eqKey
3493{
3494 // sets WEAPON_NONE if not recognised
3495 if (weapon_facings & facing)
3496 {
3498 switch (facing)
3499 {
3501 forward_weapon_type = chosen_weapon;
3502 break;
3503
3504 case WEAPON_FACING_AFT:
3505 aft_weapon_type = chosen_weapon;
3506 break;
3507
3508 case WEAPON_FACING_PORT:
3509 port_weapon_type = chosen_weapon;
3510 break;
3511
3513 starboard_weapon_type = chosen_weapon;
3514 break;
3515
3516 case WEAPON_FACING_NONE:
3517 break;
3518 }
3519
3520 return YES;
3521 }
3522 else
3523 {
3524 return NO;
3525 }
3526}
3527
3528
3529- (BOOL) addEquipmentItem:(NSString *)equipmentKey inContext:(NSString *)context
3530{
3531 return [self addEquipmentItem:equipmentKey withValidation:YES inContext:context];
3532}
3533
3534
3535- (BOOL) addEquipmentItem:(NSString *)equipmentKey withValidation:(BOOL)validateAddition inContext:(NSString *)context
3536{
3537 OOEquipmentType *eqType = nil;
3538 NSString *lcEquipmentKey = [equipmentKey lowercaseString];
3539 NSString *damagedKey;
3540 BOOL isEqThargon = [lcEquipmentKey hasSuffix:@"thargon"] || [lcEquipmentKey hasPrefix:@"thargon"];
3541 BOOL isRepairedEquipment = NO;
3542
3543 if([lcEquipmentKey isEqualToString:@"thargon"]) equipmentKey = @"EQ_THARGON";
3544
3545 // canAddEquipment always checks if the undamaged version is equipped.
3546 if (validateAddition == YES && ![self canAddEquipment:equipmentKey inContext:context]) return NO;
3547
3548 if ([equipmentKey hasSuffix:@"_DAMAGED"])
3549 {
3550 eqType = [OOEquipmentType equipmentTypeWithIdentifier:[equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]]];
3551 }
3552 else
3553 {
3554 eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey];
3555 // in case we have the damaged version!
3556 if (![eqType canCarryMultiple])
3557 {
3558 damagedKey = [equipmentKey stringByAppendingString:@"_DAMAGED"];
3559 if ([_equipment containsObject:damagedKey])
3560 {
3561 [_equipment removeObject:damagedKey];
3562 isRepairedEquipment = YES;
3563 }
3564 }
3565 }
3566
3567 // does this equipment actually exist?
3568 if (eqType == nil) return NO;
3569
3570 // special cases
3571 if ([eqType isMissileOrMine] || ([self isThargoid] && isEqThargon))
3572 {
3573 if (missiles >= max_missiles) return NO;
3574
3575 missile_list[missiles] = eqType;
3576 missiles++;
3577 return YES;
3578 }
3579
3580 // don't add any thargons to non-thargoid ships.
3581 if(isEqThargon) return NO;
3582
3583 // we can theoretically add a damaged weapon, but not a working one.
3584 if([equipmentKey hasPrefix:@"EQ_WEAPON"] && ![equipmentKey hasSuffix:@"_DAMAGED"])
3585 {
3586 return NO;
3587 }
3588 // end special cases
3589
3590 if (_equipment == nil) _equipment = [[NSMutableArray alloc] init];
3591
3592 if (![equipmentKey isEqualToString:@"EQ_PASSENGER_BERTH"] && !isRepairedEquipment)
3593 {
3594 // Add to equipment_weight with all other equipment.
3595 equipment_weight += [eqType requiredCargoSpace];
3596 if (equipment_weight > max_cargo)
3597 {
3598 // should not even happen with old save games. Reject equipment now.
3599 equipment_weight -= [eqType requiredCargoSpace];
3600 return NO;
3601 }
3602 }
3603
3604
3605 if (!isPlayer)
3606 {
3607 if ([equipmentKey isEqual:@"EQ_CARGO_BAY"])
3608 {
3609 max_cargo += extra_cargo;
3610 }
3611 else if([equipmentKey isEqualToString:@"EQ_SHIELD_BOOSTER"])
3612 {
3613 maxEnergy += 256.0f;
3614 }
3615 if([equipmentKey isEqualToString:@"EQ_SHIELD_ENHANCER"])
3616 {
3617 maxEnergy += 256.0f;
3618 energy_recharge_rate *= 1.5;
3619 }
3620 }
3621 // add the equipment
3622 [_equipment addObject:equipmentKey];
3623 [self doScriptEvent:OOJSID("equipmentAdded") withArgument:equipmentKey];
3624 return YES;
3625}
3626
3627
3628- (NSEnumerator *) equipmentEnumerator
3629{
3630 return [_equipment objectEnumerator];
3631}
3632
3633
3634- (NSUInteger) equipmentCount
3635{
3636 return [_equipment count];
3637}
3638
3639
3640- (void) removeEquipmentItem:(NSString *)equipmentKey
3641{
3642 NSString *equipmentTypeCheckKey = equipmentKey;
3643 NSString *lcEquipmentKey = [equipmentKey lowercaseString];
3644 NSUInteger equipmentIndex = NSNotFound;
3645 // determine the equipment type and make sure it works also in the case of damaged equipment
3646 if ([equipmentKey hasSuffix:@"_DAMAGED"])
3647 {
3648 equipmentTypeCheckKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
3649 }
3650 OOEquipmentType *eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentTypeCheckKey];
3651 if (eqType == nil) return;
3652
3653 if ([eqType isMissileOrMine] || ([self isThargoid] && ([lcEquipmentKey hasSuffix:@"thargon"] || [lcEquipmentKey hasPrefix:@"thargon"])))
3654 {
3655 [self removeExternalStore:eqType];
3656 }
3657 else
3658 {
3659 if ([_equipment containsObject:equipmentKey])
3660 {
3661 if (![equipmentKey isEqualToString:@"EQ_PASSENGER_BERTH"])
3662 {
3663 equipment_weight -= [eqType requiredCargoSpace]; // all other cases;
3664 }
3665
3666 if ([equipmentKey isEqualToString:@"EQ_CLOAKING_DEVICE"])
3667 {
3668 if ([self isCloaked]) [self setCloaked:NO];
3669 }
3670
3671 if (!isPlayer)
3672 {
3673 if([equipmentKey isEqualToString:@"EQ_SHIELD_BOOSTER"])
3674 {
3675 maxEnergy -= 256.0f;
3676 if (maxEnergy < energy) energy = maxEnergy;
3677 }
3678 else if([equipmentKey isEqualToString:@"EQ_SHIELD_ENHANCER"])
3679 {
3680 maxEnergy -= 256.0f;
3681 energy_recharge_rate /= 1.5;
3682 if (maxEnergy < energy) energy = maxEnergy;
3683 }
3684 else if ([equipmentKey isEqual:@"EQ_CARGO_BAY"])
3685 {
3686 max_cargo -= extra_cargo;
3687 }
3688 }
3689 }
3690
3691 if (![equipmentKey hasSuffix:@"_DAMAGED"] && ![eqType canCarryMultiple])
3692 {
3693 NSString *damagedKey = [equipmentKey stringByAppendingString:@"_DAMAGED"];
3694 if ([_equipment containsObject:damagedKey])
3695 {
3696 equipmentIndex = [_equipment indexOfObject:damagedKey];
3697 if (equipmentIndex != NSNotFound)
3698 {
3699 // remove damaged counterpart
3700 [_equipment removeObjectAtIndex:equipmentIndex];
3701 }
3702 equipment_weight -= [eqType requiredCargoSpace];
3703 }
3704 }
3705 equipmentIndex = [_equipment indexOfObject:equipmentKey];
3706 if (equipmentIndex != NSNotFound)
3707 {
3708 [_equipment removeObjectAtIndex:equipmentIndex];
3709 }
3710 // this event must come after the item is actually removed
3711 [self doScriptEvent:OOJSID("equipmentRemoved") withArgument:equipmentKey];
3712
3713 // if all docking computers are damaged while active
3714 if ([self isPlayer] && [self status] == STATUS_AUTOPILOT_ENGAGED && ![self hasDockingComputer])
3715 {
3716 [(PlayerEntity *)self disengageAutopilot];
3717 }
3718
3719
3720 if ([_equipment count] == 0) [self removeAllEquipment];
3721 }
3722}
3723
3724
3725- (BOOL) removeExternalStore:(OOEquipmentType *)eqType
3726{
3727 NSString *identifier = [eqType identifier];
3728 unsigned i;
3729
3730 for (i = 0; i < missiles; i++)
3731 {
3732 if ([[missile_list[i] identifier] isEqualTo:identifier])
3733 {
3734 // now 'delete' [i] by compacting the array
3735 while ( ++i < missiles ) missile_list[i - 1] = missile_list[i];
3736
3737 missiles--;
3738 return YES;
3739 }
3740 }
3741 return NO;
3742}
3743
3744
3745- (OOEquipmentType *) verifiedMissileTypeFromRole:(NSString *)role
3746{
3747 NSString *eqRole = nil;
3748 NSString *shipKey = nil;
3749 ShipEntity *missile = nil;
3750 OOEquipmentType *missileType = nil;
3751 BOOL isRandomMissile = [role isEqualToString:@"missile"];
3752
3753 if (isRandomMissile)
3754 {
3755 while (!shipKey)
3756 {
3757 shipKey = [UNIVERSE randomShipKeyForRoleRespectingConditions:role];
3758 if (!shipKey)
3759 {
3760 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", @"random missile", shipKey, [self name], @"shipdata", @"Trying another missile.");
3761 }
3762 }
3763 }
3764 else
3765 {
3766 shipKey = [UNIVERSE randomShipKeyForRoleRespectingConditions:role];
3767 if (!shipKey)
3768 {
3769 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", @"missile_role", role, [self name], @"shipdata", @" Using defaults instead.");
3770 return nil;
3771 }
3772 }
3773
3774 eqRole = [OOEquipmentType getMissileRegistryRoleForShip:shipKey]; // eqRole != role for generic missiles.
3775
3776 if (eqRole == nil)
3777 {
3778 missile = [UNIVERSE newShipWithName:shipKey];
3779 if (!missile)
3780 {
3781 if (isRandomMissile)
3782 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", @"random missile", shipKey, [self name], @"shipdata", @"Trying another missile.");
3783 else
3784 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", @"missile_role", role, [self name], @"shipdata", @" Using defaults instead.");
3785
3786 [OOEquipmentType setMissileRegistryRole:@"" forShip:shipKey]; // no valid role for this shipKey
3787 if (isRandomMissile) return [self verifiedMissileTypeFromRole:role];
3788 else return nil;
3789 }
3790
3791 if(isRandomMissile)
3792 {
3793 id value;
3794
3795 foreach (value, [[missile roleSet] roles])
3796 {
3797 role = (NSString *)value;
3798 missileType = [OOEquipmentType equipmentTypeWithIdentifier:role];
3799 // ensure that we have a missile or mine
3800 if ([missileType isMissileOrMine]) break;
3801 }
3802
3803 if (![missileType isMissileOrMine])
3804 {
3805 role = shipKey; // unique identifier to use in lieu of a valid equipment type if none are defined inside the generic missile roleset.
3806 }
3807 }
3808
3809 missileType = [OOEquipmentType equipmentTypeWithIdentifier:role];
3810
3811 if (!missileType)
3812 {
3813 OOLogWARN(@"ship.setUp.missiles", @"%@ \"%@\" used in ship \"%@\" needs a valid %@.plist entry.%@", (isRandomMissile ? @"random missile" : @"missile_role"), role, [self name], @"equipment", @" Enabling compatibility mode.");
3814 missileType = [self generateMissileEquipmentTypeFrom:role];
3815 }
3816
3818 [missile release];
3819 }
3820 else
3821 {
3822 if ([eqRole isEqualToString:@""])
3823 {
3824 // wrong ship definition, already written to the log in a previous call.
3825 if (isRandomMissile) return [self verifiedMissileTypeFromRole:role]; // try and find a valid missile with role 'missile'.
3826 return nil;
3827 }
3828 missileType = [OOEquipmentType equipmentTypeWithIdentifier:eqRole];
3829 }
3830
3831 return missileType;
3832}
3833
3834
3835- (OOEquipmentType *) selectMissile
3836{
3837 OOEquipmentType *missileType = nil;
3838 NSString *role = nil;
3839 double chance = randf();
3840 BOOL thargoidMissile = NO;
3841
3842 if ([self isThargoid])
3843 {
3844 if (_missileRole != nil) missileType = [self verifiedMissileTypeFromRole:_missileRole];
3845 if (missileType == nil) {
3846 _missileRole = @"EQ_THARGON"; // no valid missile_role defined, use thargoid fallback from now on.
3847 missileType = [self verifiedMissileTypeFromRole:_missileRole];
3848 }
3849 }
3850 else
3851 {
3852 // All other ships: random role 10% of the cases when auto weapons is set, if a missile_role is defined.
3853 // Without auto weapons, never random.
3854 float randomSelectionChance = chance;
3855 if(![self hasAutoWeapons]) randomSelectionChance = 0.0f;
3856 if (randomSelectionChance < 0.9f && _missileRole != nil)
3857 {
3858 missileType = [self verifiedMissileTypeFromRole:_missileRole];
3859 }
3860
3861 if (missileType == nil) // the random 10% , or no valid missile_role defined
3862 {
3863 if (chance < 0.9f && _missileRole != nil) // no valid missile_role defined?
3864 {
3865 _missileRole = nil; // use generic ship fallback from now on.
3866 }
3867
3868 // assign random missiles 20% of the time without missile_role (or 10% with valid missile_role)
3869 if (chance > 0.8f) role = @"missile";
3870 // otherwise use the standard role
3871 else role = @"EQ_MISSILE";
3872
3873 missileType = [self verifiedMissileTypeFromRole:role];
3874 }
3875 }
3876
3877 if (missileType == nil) OOLogERR(@"ship.setUp.missiles", @"could not resolve missile / mine type for ship \"%@\". Original missile role:\"%@\".", [self name],_missileRole);
3878
3879 role = [[missileType identifier] lowercaseString];
3880 thargoidMissile = [self isThargoid] && ([role hasSuffix:@"thargon"] || [role hasPrefix:@"thargon"]);
3881
3882 if (thargoidMissile || (!thargoidMissile && [missileType isMissileOrMine]))
3883 {
3884 return missileType;
3885 }
3886 else
3887 {
3888 OOLogWARN(@"ship.setUp.missiles", @"missile_role \"%@\" is not a valid missile / mine type for ship \"%@\".%@", [missileType identifier] , [self name],@" No missile selected.");
3889 return nil;
3890 }
3891}
3892
3893
3894- (void) removeAllEquipment
3895{
3896 [_equipment release];
3897 _equipment = nil;
3898}
3899
3900
3901- (OOCreditsQuantity) removeMissiles
3902{
3903 missiles = 0;
3904 return 0;
3905}
3906
3907
3908- (NSUInteger) parcelCount
3909{
3910 return 0;
3911}
3912
3913
3914- (NSUInteger) passengerCount
3915{
3916 return 0;
3917}
3918
3919
3920- (NSUInteger) passengerCapacity
3921{
3922 return 0;
3923}
3924
3925
3926- (NSUInteger) missileCount
3927{
3928 return missiles;
3929}
3930
3931
3932- (NSUInteger) missileCapacity
3933{
3934 return max_missiles;
3935}
3936
3937
3938- (NSUInteger) extraCargo
3939{
3940 return extra_cargo;
3941}
3942
3943
3944/* This is used for e.g. displaying the HUD icon */
3945- (BOOL) hasScoop
3946{
3947 return [self hasEquipmentItemProviding:@"EQ_FUEL_SCOOPS"] || [self hasEquipmentItemProviding:@"EQ_CARGO_SCOOPS"];
3948}
3949
3950
3951- (BOOL) hasFuelScoop
3952{
3953 return [self hasEquipmentItemProviding:@"EQ_FUEL_SCOOPS"];
3954}
3955
3956
3957/* No such core equipment item, but EQ_FUEL_SCOOPS provides it */
3958- (BOOL) hasCargoScoop
3959{
3960 return [self hasEquipmentItemProviding:@"EQ_CARGO_SCOOPS"];
3961}
3962
3963
3964- (BOOL) hasECM
3965{
3966 return [self hasEquipmentItemProviding:@"EQ_ECM"];
3967}
3968
3969
3970- (BOOL) hasCloakingDevice
3971{
3972 /* TODO: Checks above stop this being 'providing'. */
3973 return [self hasEquipmentItem:@"EQ_CLOAKING_DEVICE"];
3974}
3975
3976
3977- (BOOL) hasMilitaryScannerFilter
3978{
3979#if USEMASC
3980 return [self hasEquipmentItemProviding:@"EQ_MILITARY_SCANNER_FILTER"];
3981#else
3982 return NO;
3983#endif
3984}
3985
3986
3987- (BOOL) hasMilitaryJammer
3988{
3989#if USEMASC
3990 return [self hasEquipmentItemProviding:@"EQ_MILITARY_JAMMER"];
3991#else
3992 return NO;
3993#endif
3994}
3995
3996
3997- (BOOL) hasExpandedCargoBay
3998{
3999 /* Not 'providing' - controlled through scripts */
4000 return [self hasEquipmentItem:@"EQ_CARGO_BAY"];
4001}
4002
4003
4004- (BOOL) hasShieldBooster
4005{
4006 /* Not 'providing' - controlled through scripts */
4007 return [self hasEquipmentItem:@"EQ_SHIELD_BOOSTER"];
4008}
4009
4010
4011- (BOOL) hasMilitaryShieldEnhancer
4012{
4013 /* Not 'providing' - controlled through scripts */
4014 return [self hasEquipmentItem:@"EQ_NAVAL_SHIELD_BOOSTER"];
4015}
4016
4017
4018- (BOOL) hasHeatShield
4019{
4020 return [self hasEquipmentItemProviding:@"EQ_HEAT_SHIELD"];
4021}
4022
4023
4024- (BOOL) hasFuelInjection
4025{
4026 return [self hasEquipmentItemProviding:@"EQ_FUEL_INJECTION"];
4027}
4028
4029
4030- (BOOL) hasCascadeMine
4031{
4032 /* TODO: this could be providing since theoretically OXP
4033 * deployable mines could also do cascade effects, but there are
4034 * probably better ways to manage OXP pylon AI */
4035 return [self hasEquipmentItem:@"EQ_QC_MINE" includeWeapons:YES whileLoading:NO];
4036}
4037
4038
4039- (BOOL) hasEscapePod
4040{
4041 return [self hasEquipmentItemProviding:@"EQ_ESCAPE_POD"];
4042}
4043
4044
4045- (BOOL) hasDockingComputer
4046{
4047 return [self hasEquipmentItemProviding:@"EQ_DOCK_COMP"];
4048}
4049
4050
4051- (BOOL) hasGalacticHyperdrive
4052{
4053 return [self hasEquipmentItemProviding:@"EQ_GAL_DRIVE"];
4054}
4055
4056
4057- (float) shieldBoostFactor
4058{
4059 float boostFactor = 1.0f;
4060 if ([self hasShieldBooster]) boostFactor += 1.0f;
4061 if ([self hasMilitaryShieldEnhancer]) boostFactor += 1.0f;
4062
4063 return boostFactor;
4064}
4065
4066
4067/* These next three are never called as of 12/12/2014, as NPCs don't
4068 * have shields and PlayerEntity overrides these. */
4069- (float) maxForwardShieldLevel
4070{
4071 return BASELINE_SHIELD_LEVEL * [self shieldBoostFactor];
4072}
4073
4074
4075- (float) maxAftShieldLevel
4076{
4077 return BASELINE_SHIELD_LEVEL * [self shieldBoostFactor];
4078}
4079
4080
4081- (float) shieldRechargeRate
4082{
4083 return [self hasMilitaryShieldEnhancer] ? 3.0f : 2.0f;
4084}
4085
4086
4087- (double) maxHyperspaceDistance
4088{
4089 return MAX_JUMP_RANGE;
4090}
4091
4092- (float) afterburnerFactor
4093{
4094 return afterburner_speed_factor;
4095}
4096
4097
4098- (float) afterburnerRate
4099{
4100 return afterburner_rate;
4101}
4102
4103
4104- (void) setAfterburnerFactor:(GLfloat)new
4105{
4106 afterburner_speed_factor = new;
4107}
4108
4109
4110- (void) setAfterburnerRate:(GLfloat)new
4111{
4112 afterburner_rate = new;
4113}
4114
4115
4116- (float) maxThrust
4117{
4118 return max_thrust;
4119}
4120
4121
4122- (void) setMaxThrust:(GLfloat)new
4123{
4124 max_thrust = new;
4125}
4126
4127
4128- (float) thrust
4129{
4130 return thrust;
4131}
4132
4133
4135// //
4136// behaviours //
4137// //
4138- (void) behaviour_stop_still:(double) delta_t
4139{
4140 stick_roll = 0.0;
4141 stick_pitch = 0.0;
4142 stick_yaw = 0.0;
4143 [self applySticks:delta_t];
4144
4145
4146
4147}
4148
4149
4150- (void) behaviour_idle:(double) delta_t
4151{
4152 stick_yaw = 0.0;
4153 if ((!isStation)&&(scanClass != CLASS_BUOY))
4154 {
4155 stick_roll = 0.0;
4156 }
4157 else
4158 {
4159 stick_roll = flightRoll;
4160 }
4161 if (scanClass != CLASS_BUOY)
4162 {
4163 stick_pitch = 0.0;
4164 }
4165 else
4166 {
4167 stick_pitch = flightPitch;
4168 }
4169 [self applySticks:delta_t];
4170
4171
4172}
4173
4174
4175- (void) behaviour_tumble:(double) delta_t
4176{
4177 [self applySticks:delta_t];
4178
4179
4180}
4181
4182
4183- (void) behaviour_tractored:(double) delta_t
4184{
4185 desired_range = collision_radius * 2.0;
4186 ShipEntity* hauler = (ShipEntity*)[self owner];
4187 if ((hauler)&&([hauler isShip]))
4188 {
4189 _destination = [hauler absoluteTractorPosition];
4190 double distance = [self rangeToDestination];
4191 if (distance < desired_range)
4192 {
4193 [self performTumble];
4194 [self setStatus:STATUS_IN_FLIGHT];
4195 [hauler scoopUp:self];
4196 return;
4197 }
4198 GLfloat tf = TRACTOR_FORCE / mass;
4199 // adjust for difference in velocity (spring rule)
4200 Vector dv = vector_between([self velocity], [hauler velocity]);
4201 GLfloat moment = delta_t * 0.25 * tf;
4202 velocity.x += moment * dv.x;
4203 velocity.y += moment * dv.y;
4204 velocity.z += moment * dv.z;
4205 // acceleration = force / mass
4206 // force proportional to distance (spring rule)
4207 HPVector dp = HPvector_between(position, _destination);
4208 moment = delta_t * 0.5 * tf;
4209 velocity.x += moment * dp.x;
4210 velocity.y += moment * dp.y;
4211 velocity.z += moment * dp.z;
4212 // force inversely proportional to distance
4213 GLfloat d2 = HPmagnitude2(dp);
4214 moment = (d2 > 0.0)? delta_t * 5.0 * tf / d2 : 0.0;
4215 if (d2 > 0.0)
4216 {
4217 velocity.x += moment * dp.x;
4218 velocity.y += moment * dp.y;
4219 velocity.z += moment * dp.z;
4220 }
4221 //
4222 if ([self status] == STATUS_BEING_SCOOPED)
4223 {
4224 BOOL lost_contact = (distance > hauler->collision_radius + collision_radius + 250.0f); // 250m range for tractor beam
4225 if ([hauler isPlayer])
4226 {
4227 switch ([(PlayerEntity*)hauler dialFuelScoopStatus])
4228 {
4231 lost_contact = YES; // don't draw
4232 break;
4233
4234 case SCOOP_STATUS_OKAY:
4236 break;
4237 }
4238 }
4239
4240 if (lost_contact) // 250m range for tractor beam
4241 {
4242 // escaped tractor beam
4243 [self setStatus:STATUS_IN_FLIGHT];
4244 behaviour = BEHAVIOUR_IDLE;
4245 [self setThrust:[self maxThrust]]; // restore old thrust.
4246 frustration = 0.0;
4247 [self setOwner:self];
4248 [shipAI exitStateMachineWithMessage:nil]; // exit nullAI.plist
4249 return;
4250 }
4251 else if ([hauler isPlayer])
4252 {
4253 [(PlayerEntity*)hauler setScoopsActive];
4254 }
4255 }
4256 }
4257
4258// being tractored; sticks ignored - CIM
4259 flightYaw = 0.0;
4260
4261 desired_speed = 0.0;
4262 thrust = 25.0; // used to damp velocity (must be less than hauler thrust)
4263
4264 thrust = 0.0; // must reset thrust now
4265}
4266
4267
4268- (void) behaviour_track_target:(double) delta_t
4269{
4270 if ([self primaryTarget] == nil)
4271 {
4272 [self noteLostTargetAndGoIdle];
4273 return;
4274 }
4275 [self trackPrimaryTarget:delta_t:NO]; // applies sticks
4276 if ([self hasProximityAlertIgnoringTarget:YES])
4277 {
4278 [self avoidCollision];
4279 }
4280
4281}
4282
4283
4284- (void) behaviour_intercept_target:(double) delta_t
4285{
4286 double range = [self rangeToPrimaryTarget];
4287 if (behaviour == BEHAVIOUR_INTERCEPT_TARGET)
4288 {
4289 desired_speed = maxFlightSpeed;
4290 if (range < desired_range)
4291 {
4292 [shipAI reactToMessage:@"DESIRED_RANGE_ACHIEVED" context:@"BEHAVIOUR_INTERCEPT_TARGET"];
4293 [self doScriptEvent:OOJSID("shipAchievedDesiredRange")];
4294
4295 }
4296 desired_speed = maxFlightSpeed * [self trackPrimaryTarget:delta_t:NO];
4297 }
4298 else
4299 {
4300 // = BEHAVIOUR_COLLECT_TARGET
4301 ShipEntity* target = [self primaryTarget];
4302// if somehow ended up in this state but target is not cargo, stop
4303// trying to scoop it
4304 if (!target || [target scanClass] != CLASS_CARGO || [target cargoType] == CARGO_NOT_CARGO)
4305 {
4306 [self noteLostTargetAndGoIdle];
4307 return;
4308 }
4309 double target_speed = [target speed];
4310 double eta = range / (flightSpeed - target_speed);
4311 double last_success_factor = success_factor;
4312 double last_distance = last_success_factor;
4313 double distance = [self rangeToDestination];
4314 success_factor = distance;
4315 //
4316 double slowdownTime = 96.0 / (thrust*SHIP_THRUST_FACTOR); // more thrust implies better slowing
4317 double minTurnSpeedFactor = 0.005 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds
4318
4319 if ((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor))
4320 desired_speed = flightSpeed * 0.75; // cut speed by 50% to a minimum minTurnSpeedFactor of speed
4321 else
4322 desired_speed = maxFlightSpeed;
4323
4324 if (desired_speed < target_speed)
4325 {
4326 desired_speed += target_speed;
4327 if (target_speed > maxFlightSpeed)
4328 {
4329 [self noteLostTargetAndGoIdle];
4330 return;
4331 }
4332 }
4333 if (desired_speed > maxFlightSpeed)
4334 { // never use injectors for scooping
4335 desired_speed = maxFlightSpeed;
4336 }
4337
4338 _destination = target->position;
4339 desired_range = 0.5 * target->collision_radius;
4340 [self trackDestination: delta_t : NO];
4341
4342 //
4343 if (distance < last_distance) // improvement
4344 {
4345 frustration -= delta_t;
4346 if (frustration < 0.0)
4347 frustration = 0.0;
4348 }
4349 else
4350 {
4351 frustration += delta_t * 0.9;
4352 if (frustration > 10.0) // 10s of frustration
4353 {
4354 [self noteFrustration:@"BEHAVIOUR_INTERCEPT_TARGET"];
4355 frustration -= 5.0; //repeat after another five seconds' frustration
4356 }
4357 }
4358 }
4359 if ([self hasProximityAlertIgnoringTarget:YES])
4360 {
4361 [self avoidCollision];
4362 }
4363
4364
4365}
4366
4367
4368- (void) behaviour_attack_break_off_target:(double) delta_t
4369{
4370 if (![self canStillTrackPrimaryTarget])
4371 {
4372 [self noteLostTargetAndGoIdle];
4373 return;
4374 }
4375 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
4376 float max_available_speed = maxFlightSpeed;
4377 double range = [self rangeToPrimaryTarget];
4378 if (canBurn) max_available_speed *= [self afterburnerFactor];
4379
4380 desired_speed = max_available_speed;
4381
4382 Entity* target = [self primaryTarget];
4383
4384 if (desired_speed > maxFlightSpeed)
4385 {
4386 double target_speed = [target speed];
4387 if (desired_speed > target_speed * 3.0)
4388 {
4389 desired_speed = maxFlightSpeed; // don't overuse the injectors
4390 }
4391 }
4392
4393 if (cloakAutomatic) [self activateCloakingDevice];
4394 if ([self hasProximityAlertIgnoringTarget:NO])
4395 {
4396 [self avoidCollision];
4397 return;
4398 }
4399
4400 frustration += delta_t;
4401 if (frustration > 15.0 && accuracy >= COMBAT_AI_DOGFIGHTER && !canBurn)
4402 {
4403 desired_speed = maxFlightSpeed / 2.0;
4404 }
4405 double aspect = [self approachAspectToPrimaryTarget];
4406 if (range > 3000.0 || ([target isShip] && [(ShipEntity*)target primaryTarget] != self) || frustration - floor(frustration) > fmin(1.6/max_flight_roll,aspect))
4407 {
4408 [self trackPrimaryTarget:delta_t:YES];
4409 }
4410 else
4411 {
4412// less useful at long range if not under direct fire
4413 [self evasiveAction:delta_t];
4414 }
4415
4416 if (range > COMBAT_OUT_RANGE_FACTOR * weaponRange)
4417 {
4418 behaviour = BEHAVIOUR_ATTACK_TARGET;
4419 }
4420 else if (aspect < -0.75 && accuracy >= COMBAT_AI_DOGFIGHTER)
4421 {
4422 behaviour = BEHAVIOUR_ATTACK_SLOW_DOGFIGHT;
4423 }
4424 else if (frustration > 10.0 && [self approachAspectToPrimaryTarget] < 0.85 && forward_weapon_temp < COMBAT_AI_WEAPON_TEMP_READY)
4425 {
4426 frustration = 0.0;
4427 if (accuracy >= COMBAT_AI_DOGFIGHTER)
4428 {
4429 behaviour = BEHAVIOUR_ATTACK_SLOW_DOGFIGHT;
4430 }
4431 else
4432 {
4433 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4434 }
4435 }
4436
4437 flightYaw = 0.0;
4438}
4439
4440
4441- (void) behaviour_attack_slow_dogfight:(double) delta_t
4442{
4443 if (![self canStillTrackPrimaryTarget])
4444 {
4445 [self noteLostTargetAndGoIdle];
4446 return;
4447 }
4448 if ([self hasProximityAlertIgnoringTarget:YES])
4449 {
4450 [self avoidCollision];
4451 return;
4452 }
4453 double range = [self rangeToPrimaryTarget];
4454 ShipEntity* target = [self primaryTarget];
4455 double aspect = [self approachAspectToPrimaryTarget];
4456 if (range < 2.5*(collision_radius+target->collision_radius) && [self proximityAlert] == target && aspect > 0) {
4457 desired_speed = maxFlightSpeed;
4458 [self avoidCollision];
4459 return;
4460 }
4461 if (aspect < -0.5 && range > COMBAT_IN_RANGE_FACTOR * weaponRange * 2.0)
4462 {
4463 behaviour = BEHAVIOUR_ATTACK_TARGET;
4464 }
4465 else if (aspect < -0.5)
4466 {
4467// mostly behind target - try to stay there and keep up
4468 desired_speed = fmin(maxFlightSpeed * 0.5,[target speed]*0.5);
4469 }
4470 else if (aspect < 0.3)
4471 {
4472// to side of target - slow right down
4473 desired_speed = maxFlightSpeed * 0.1;
4474 }
4475 else
4476 {
4477// coming to front of target - accelerate for a quick getaway
4478 desired_speed = maxFlightSpeed * fmin(aspect*2.5,1.0);
4479 }
4480 if (aspect > 0.85)
4481 {
4482 behaviour = BEHAVIOUR_ATTACK_BREAK_OFF_TARGET;
4483 }
4484 if (aspect > 0.0)
4485 {
4486 frustration += delta_t;
4487 }
4488 else
4489 {
4490 frustration -= delta_t;
4491 }
4492 if (frustration > 10.0)
4493 {
4494 desired_speed /= 2.0;
4495 }
4496 else if (frustration < 0.0)
4497 frustration = 0.0;
4498
4499 [self trackPrimaryTarget:delta_t:NO];
4500
4501 if (missiles) [self considerFiringMissile:delta_t];
4502
4503 if (cloakAutomatic) [self activateCloakingDevice];
4504
4505}
4506
4507
4508- (void) behaviour_evasive_action:(double) delta_t
4509{
4510 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
4511 float max_available_speed = maxFlightSpeed;
4512// double range = [self rangeToPrimaryTarget];
4513 if (canBurn) max_available_speed *= [self afterburnerFactor];
4514 desired_speed = max_available_speed;
4515 if (desired_speed > maxFlightSpeed)
4516 {
4517 ShipEntity* target = [self primaryTarget];
4518 double target_speed = [target speed];
4519 if (desired_speed > target_speed)
4520 {
4521 desired_speed = maxFlightSpeed; // don't overuse the injectors
4522 }
4523 }
4524
4525 if (cloakAutomatic) [self activateCloakingDevice];
4526 if ([self proximityAlert] != nil)
4527 {
4528 [self avoidCollision];
4529 return;
4530 }
4531
4532 [self evasiveAction:delta_t];
4533
4534 frustration += delta_t;
4535
4536 if (frustration > 0.5)
4537 {
4538 if (behaviour == BEHAVIOUR_FLEE_EVASIVE_ACTION)
4539 {
4540 [self setEvasiveJink:400.0];
4541 behaviour = BEHAVIOUR_FLEE_TARGET;
4542 }
4543 else
4544 {
4545 behaviour = BEHAVIOUR_ATTACK_TARGET;
4546 }
4547 }
4548
4549 flightYaw = 0.0;
4550
4551 // probably only useful for Thargoids, except for the occasional opportunist
4552 [self fireMainWeapon:[self rangeToPrimaryTarget]];
4553
4554}
4555
4556
4557- (void) behaviour_attack_target:(double) delta_t
4558{
4559 double range = [self rangeToPrimaryTarget];
4560
4561 if (cloakAutomatic) [self activateCloakingDevice];
4562
4563/* Start of behaviour selection:
4564 * Anything beyond the basics should require accuracy >= COMBAT_AI_ISNT_AWFUL
4565 * Anything fancy should require accuracy >= COMBAT_AI_IS_SMART
4566 * If precise aim is required, behaviour should have accuracy >= COMBAT_AI_TRACKS_CLOSER
4567 * - CIM
4568 */
4569
4570 OOWeaponType forward_weapon_real_type = forward_weapon_type;
4571 GLfloat forward_weapon_real_temp = forward_weapon_temp;
4572
4573// if forward weapon is actually on a subent
4574 if (isWeaponNone(forward_weapon_real_type))
4575 {
4576 BOOL hasTurrets = NO;
4577 NSEnumerator *subEnum = [self shipSubEntityEnumerator];
4578 ShipEntity *se = nil;
4579 while (isWeaponNone(forward_weapon_real_type) && (se = [subEnum nextObject]))
4580 {
4581 forward_weapon_real_type = se->forward_weapon_type;
4582 forward_weapon_real_temp = se->forward_weapon_temp;
4583 if (se->behaviour == BEHAVIOUR_TRACK_AS_TURRET)
4584 {
4585 hasTurrets = YES;
4586 }
4587 }
4588 if (isWeaponNone(forward_weapon_real_type) && hasTurrets)
4589 { // safety for ships only equipped with turrets
4590 forward_weapon_real_type = OOWeaponTypeFromEquipmentIdentifierSloppy(@"EQ_WEAPON_PULSE_LASER");
4591 forward_weapon_real_temp = COMBAT_AI_WEAPON_TEMP_USABLE * 0.9;
4592 }
4593 }
4594
4595 if ([forward_weapon_real_type isTurretLaser])
4596 {
4597 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
4598 }
4599 else
4600 {
4601 BOOL in_good_range = aim_tolerance*range < COMBAT_AI_CONFIDENCE_FACTOR;
4602
4603 BOOL aft_weapon_ready = !isWeaponNone(aft_weapon_type) && (aft_weapon_temp < COMBAT_AI_WEAPON_TEMP_READY) && in_good_range;
4604 BOOL forward_weapon_ready = !isWeaponNone(forward_weapon_real_type) && (forward_weapon_real_temp < COMBAT_AI_WEAPON_TEMP_READY); // does not require in_good_range
4605 BOOL port_weapon_ready = !isWeaponNone(port_weapon_type) && (port_weapon_temp < COMBAT_AI_WEAPON_TEMP_READY) && in_good_range;
4606 BOOL starboard_weapon_ready = !isWeaponNone(starboard_weapon_type) && (starboard_weapon_temp < COMBAT_AI_WEAPON_TEMP_READY) && in_good_range;
4607// if no weapons cool enough to be good choices, be less picky
4608 BOOL weapons_heating = NO;
4609 if (!forward_weapon_ready && !aft_weapon_ready && !port_weapon_ready && !starboard_weapon_ready)
4610 {
4611 weapons_heating = YES;
4612 aft_weapon_ready = !isWeaponNone(aft_weapon_type) && (aft_weapon_temp < COMBAT_AI_WEAPON_TEMP_USABLE) && in_good_range;
4613 forward_weapon_ready = !isWeaponNone(forward_weapon_real_type) && (forward_weapon_real_temp < COMBAT_AI_WEAPON_TEMP_USABLE); // does not require in_good_range
4614 port_weapon_ready = !isWeaponNone(port_weapon_type) && (port_weapon_temp < COMBAT_AI_WEAPON_TEMP_USABLE) && in_good_range;
4615 starboard_weapon_ready = !isWeaponNone(starboard_weapon_type) && (starboard_weapon_temp < COMBAT_AI_WEAPON_TEMP_USABLE) && in_good_range;
4616 }
4617
4618 Entity* target = [self primaryTarget];
4619 double aspect = [self approachAspectToPrimaryTarget];
4620
4621 if (!forward_weapon_ready && !aft_weapon_ready && !port_weapon_ready && !starboard_weapon_ready)
4622 { // no usable weapons! Either not fitted or overheated
4623
4624 // if unarmed
4625 if (isWeaponNone(forward_weapon_real_type) &&
4626 isWeaponNone(aft_weapon_type) &&
4627 isWeaponNone(port_weapon_type) &&
4628 isWeaponNone(starboard_weapon_type))
4629 {
4630 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4631 }
4632 else if (aspect > 0)
4633 {
4634 if (in_good_range)
4635 {
4636 if (accuracy >= COMBAT_AI_IS_SMART && randf() < 0.75)
4637 {
4638 behaviour = BEHAVIOUR_EVASIVE_ACTION;
4639 }
4640 else
4641 {
4642 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4643 }
4644 }
4645 else
4646 {
4647 // ready to get more accurate shots later
4648 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4649 }
4650 }
4651 else
4652 {
4653 // if target is running away, stay on target
4654 // unless too close for safety
4655 if (range < COMBAT_IN_RANGE_FACTOR * weaponRange) {
4656 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4657 } else {
4658 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4659 }
4660 }
4661 }
4662// 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
4663 else if (weapons_heating && accuracy >= COMBAT_AI_ISNT_AWFUL && [target isShip] && [(ShipEntity *)target primaryTarget] != self && range < COMBAT_OUT_RANGE_FACTOR * weaponRange)
4664 {
4665// then back off a bit for weapons to cool so we get a good attack run later, rather than weaving closer
4666 float relativeSpeed = magnitude(vector_subtract([self velocity], [target velocity]));
4667 [self setEvasiveJink:(range + COMBAT_JINK_OFFSET - relativeSpeed / max_flight_pitch)];
4668 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4669 }
4670 else
4671 {
4672 BOOL nearby = range < COMBAT_IN_RANGE_FACTOR * getWeaponRangeFromType(forward_weapon_type);
4673 BOOL midrange = range < COMBAT_OUT_RANGE_FACTOR * getWeaponRangeFromType(aft_weapon_type);
4674
4675
4676 if (nearby && aft_weapon_ready)
4677 {
4678 jink = kZeroVector; // almost all behaviours
4679 behaviour = BEHAVIOUR_RUNNING_DEFENSE;
4680 }
4681 else if (nearby && (port_weapon_ready || starboard_weapon_ready))
4682 {
4683 jink = kZeroVector; // almost all behaviours
4684 behaviour = BEHAVIOUR_ATTACK_BROADSIDE;
4685 }
4686 else if (nearby)
4687 {
4688 if (!pitching_over) // don't change jink in the middle of a sharp turn.
4689 {
4690 /*
4691 For most AIs, is behaviour_attack_target called as starting behaviour on every hit.
4692 Target can both fly towards or away from ourselves here. Both situations
4693 need a different jink.z for optimal collision avoidance at high speed approach and low speed dogfighting.
4694 The COMBAT_JINK_OFFSET intentionally over-compensates the range for collision radii to send ships towards
4695 the target at low speeds.
4696 */
4697 float relativeSpeed = magnitude(vector_subtract([self velocity], [target velocity]));
4698 [self setEvasiveJink:(range + COMBAT_JINK_OFFSET - relativeSpeed / max_flight_pitch)];
4699 }
4700 // good pilots use behaviour_attack_break_off_target instead
4701 if (accuracy >= COMBAT_AI_FLEES_BETTER)
4702 {
4703 behaviour = BEHAVIOUR_ATTACK_BREAK_OFF_TARGET;
4704 }
4705 else
4706 {
4707 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4708 }
4709 }
4710 else if (forward_weapon_ready)
4711 {
4712 jink = kZeroVector; // almost all behaviours
4713
4714 // TODO: good pilots use behaviour_attack_sniper sometimes
4715 if (getWeaponRangeFromType(forward_weapon_real_type) > 12500 && range > 12500)
4716 {
4717 behaviour = BEHAVIOUR_ATTACK_SNIPER;
4718 }
4719// generally not good tactics the next two
4720 else if (accuracy < COMBAT_AI_ISNT_AWFUL && aspect < 0)
4721 {
4722 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX;
4723 }
4724 else if (accuracy < COMBAT_AI_ISNT_AWFUL)
4725 {
4726 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
4727 }
4728 else
4729 {
4730 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4731 }
4732 }
4733 else if (port_weapon_ready || starboard_weapon_ready)
4734 {
4735 jink = kZeroVector; // almost all behaviours
4736 behaviour = BEHAVIOUR_ATTACK_BROADSIDE;
4737 }
4738 else if (aft_weapon_ready && midrange)
4739 {
4740 jink = kZeroVector; // almost all behaviours
4741 behaviour = BEHAVIOUR_RUNNING_DEFENSE;
4742 }
4743 else
4744 {
4745 jink = kZeroVector; // almost all behaviours
4746 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
4747 }
4748 }
4749 }
4750
4751 frustration = 0.0; // behaviour changed, so reset frustration
4752
4753}
4754
4755
4756- (void) behaviour_attack_broadside:(double) delta_t
4757{
4758 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
4759 float max_available_speed = maxFlightSpeed;
4760 double range = [self rangeToPrimaryTarget];
4761 if (canBurn) max_available_speed *= [self afterburnerFactor];
4762
4763 if (cloakAutomatic) [self activateCloakingDevice];
4764
4765 if (![self canStillTrackPrimaryTarget])
4766 {
4767 [self noteLostTargetAndGoIdle];
4768 return;
4769 }
4770
4771 desired_speed = max_available_speed;
4772 if (range < COMBAT_BROADSIDE_IN_RANGE_FACTOR * weaponRange)
4773 {
4774 behaviour = BEHAVIOUR_ATTACK_TARGET;
4775 }
4776 else
4777 {
4778 if (port_weapon_temp < starboard_weapon_temp)
4779 {
4780 if (isWeaponNone(port_weapon_type))
4781 {
4782 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_RIGHT;
4783 [self setWeaponDataFromType:starboard_weapon_type];
4784 }
4785 else
4786 {
4787 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_LEFT;
4788 [self setWeaponDataFromType:port_weapon_type];
4789 }
4790 }
4791 else
4792 {
4793 if (isWeaponNone(starboard_weapon_type))
4794 {
4795 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_RIGHT;
4796 [self setWeaponDataFromType:starboard_weapon_type];
4797 }
4798 else
4799 {
4800 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_LEFT;
4801 [self setWeaponDataFromType:port_weapon_type];
4802 }
4803 }
4804 jink = kZeroVector;
4805 if (weapon_damage == 0.0)
4806 { // safety in case side lasers no longer exist
4807 behaviour = BEHAVIOUR_ATTACK_TARGET;
4808 }
4809 else if (range > 0.9 * weaponRange)
4810 {
4811 behaviour = BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE;
4812 }
4813 }
4814
4815 frustration = 0.0; // behaviour changed, so reset frustration
4816
4817
4818
4819}
4820
4821
4822- (void) behaviour_attack_broadside_left:(double) delta_t
4823{
4824 [self behaviour_attack_broadside_target:delta_t leftside:YES];
4825}
4826
4827
4828- (void) behaviour_attack_broadside_right:(double) delta_t
4829{
4830 [self behaviour_attack_broadside_target:delta_t leftside:NO];
4831}
4832
4833
4834- (void) behaviour_attack_broadside_target:(double) delta_t leftside:(BOOL) leftside
4835{
4836 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
4837 float max_available_speed = maxFlightSpeed;
4838 double range = [self rangeToPrimaryTarget];
4839 if (canBurn) max_available_speed *= [self afterburnerFactor];
4840 if ([self primaryTarget] == nil)
4841 {
4842 [self noteLostTargetAndGoIdle];
4843 return;
4844 }
4845 GLfloat currentWeaponRange = getWeaponRangeFromType(leftside?port_weapon_type:starboard_weapon_type);
4846 if (range > COMBAT_BROADSIDE_RANGE_FACTOR * currentWeaponRange)
4847 {
4848 behaviour = BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE;
4849 return;
4850 }
4851
4852// can get closer on broadsides since there's less risk of a collision
4853 if ((range < COMBAT_BROADSIDE_IN_RANGE_FACTOR * currentWeaponRange)||([self proximityAlert] != nil))
4854 {
4855 if (![self hasProximityAlertIgnoringTarget:YES])
4856 {
4857 behaviour = BEHAVIOUR_ATTACK_TARGET;
4858 }
4859 else
4860 {
4861 [self avoidCollision];
4862 return;
4863 }
4864 }
4865 else
4866 {
4867 if (![self canStillTrackPrimaryTarget])
4868 {
4869 [self noteLostTargetAndGoIdle];
4870 return;
4871 }
4872 }
4873 // control speed
4874 //
4875 BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed);
4876 double slow_down_range = currentWeaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
4877// double target_speed = [target speed];
4878 if (range <= slow_down_range)
4879 desired_speed = fmin(0.8 * maxFlightSpeed, fmax((2.0-frustration)*maxFlightSpeed, 0.1 * maxFlightSpeed)); // within the weapon's range slow down to aim
4880 else
4881 desired_speed = max_available_speed; // use afterburner to approach
4882
4883 double last_success_factor = success_factor;
4884 success_factor = [self trackSideTarget:delta_t:leftside]; // do the actual piloting
4885 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE)
4886 { // will probably have more luck with the other laser or picking a different attack method
4887 if (leftside)
4888 {
4889 if (!isWeaponNone(starboard_weapon_type))
4890 {
4891 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_RIGHT;
4892 }
4893 else
4894 {
4895 behaviour = BEHAVIOUR_ATTACK_TARGET;
4896 }
4897 }
4898 else
4899 {
4900 if (!isWeaponNone(port_weapon_type))
4901 {
4902 behaviour = BEHAVIOUR_ATTACK_BROADSIDE_LEFT;
4903 }
4904 else
4905 {
4906 behaviour = BEHAVIOUR_ATTACK_TARGET;
4907 }
4908 }
4909 }
4910
4911/* FIXME: again, basically all of this next bit common with standard attack */
4912 if ((success_factor > 0.999)||(success_factor > last_success_factor))
4913 {
4914 frustration -= delta_t;
4915 if (frustration < 0.0)
4916 frustration = 0.0;
4917 }
4918 else
4919 {
4920 frustration += delta_t;
4921 if (frustration > 3.0) // 3s of frustration
4922 {
4923
4924 [self noteFrustration:@"BEHAVIOUR_ATTACK_BROADSIDE"];
4925 [self setEvasiveJink:1000.0];
4926 behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
4927 frustration = 0.0;
4928 desired_speed = maxFlightSpeed;
4929 }
4930 }
4931
4932 if (missiles) [self considerFiringMissile:delta_t];
4933
4934 if (cloakAutomatic) [self activateCloakingDevice];
4935 if (leftside)
4936 {
4937 [self firePortWeapon:range];
4938 }
4939 else
4940 {
4941 [self fireStarboardWeapon:range];
4942 }
4943
4944
4945
4946 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE)
4947 {
4948 behaviour = BEHAVIOUR_ATTACK_TARGET;
4949 }
4950}
4951
4952
4953- (void) behaviour_close_to_broadside_range:(double) delta_t
4954{
4955 double range = [self rangeToPrimaryTarget];
4956 if ([self proximityAlert] != nil)
4957 {
4958 if ([self proximityAlert] == [self primaryTarget])
4959 {
4960 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; // this behaviour will handle proximity_alert.
4961 [self behaviour_attack_fly_from_target: delta_t]; // do it now.
4962 }
4963 else
4964 {
4965 [self avoidCollision];
4966 }
4967 return;
4968 }
4969 if (![self canStillTrackPrimaryTarget])
4970 {
4971 [self noteLostTargetAndGoIdle];
4972 return;
4973 }
4974
4975 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
4976 [self behaviour_fly_to_target_six:delta_t];
4977 if (!isWeaponNone(port_weapon_type))
4978 {
4979 [self setWeaponDataFromType:port_weapon_type];
4980 }
4981 else
4982 {
4983 [self setWeaponDataFromType:starboard_weapon_type];
4984 }
4985 if (range <= COMBAT_BROADSIDE_RANGE_FACTOR * weaponRange)
4986 {
4987 behaviour = BEHAVIOUR_ATTACK_BROADSIDE;
4988 }
4989 else
4990 {
4991 behaviour = BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE;
4992 }
4993}
4994
4995
4996- (void) behaviour_close_with_target:(double) delta_t
4997{
4998 double range = [self rangeToPrimaryTarget];
4999 if ([self proximityAlert] != nil)
5000 {
5001 if ([self proximityAlert] == [self primaryTarget])
5002 {
5003 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; // this behaviour will handle proximity_alert.
5004 [self behaviour_attack_fly_from_target: delta_t]; // do it now.
5005 }
5006 else
5007 {
5008 [self avoidCollision];
5009 }
5010 return;
5011 }
5012 if (![self canStillTrackPrimaryTarget])
5013 {
5014 [self noteLostTargetAndGoIdle];
5015 return;
5016 }
5017 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
5018 double saved_frustration = frustration;
5019 [self behaviour_fly_to_target_six:delta_t];
5020 frustration = saved_frustration; // ignore fly-to-12 frustration
5021 frustration += delta_t;
5022 if (range <= COMBAT_IN_RANGE_FACTOR * weaponRange || frustration > 5.0)
5023 {
5024 behaviour = BEHAVIOUR_ATTACK_TARGET;
5025 }
5026 else
5027 {
5028 behaviour = BEHAVIOUR_CLOSE_WITH_TARGET;
5029 }
5030
5031
5032}
5033
5034
5035- (void) behaviour_attack_sniper:(double) delta_t
5036{
5037 if (![self canStillTrackPrimaryTarget])
5038 {
5039 [self noteLostTargetAndGoIdle];
5040 return;
5041 }
5042 Entity* rawTarget = [self primaryTarget];
5043 if (![rawTarget isShip])
5044 {
5045 // can't attack a wormhole
5046 [self noteLostTargetAndGoIdle];
5047 return;
5048 }
5049 ShipEntity *target = (ShipEntity *)rawTarget;
5050
5051 double range = [self rangeToPrimaryTarget];
5052 float max_available_speed = maxFlightSpeed;
5053
5054 if (range < 15000)
5055 {
5056 behaviour = BEHAVIOUR_ATTACK_TARGET;
5057 }
5058 else
5059 {
5060 if (range > weaponRange || range > scannerRange * 0.8)
5061 {
5062 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
5063 if (canBurn && [target weaponRange] > weaponRange && range > weaponRange)
5064 {
5065 // if outside maximum weapon range, but inside target weapon range
5066 // close to fight ASAP!
5067 max_available_speed *= [self afterburnerFactor];
5068 }
5069 desired_speed = max_available_speed;
5070 }
5071 else
5072 {
5073 desired_speed = max_available_speed / 10.0f;
5074 }
5075
5076 double last_success_factor = success_factor;
5077 success_factor = [self trackPrimaryTarget:delta_t:NO];
5078
5079 if ((success_factor > 0.999)||(success_factor > last_success_factor))
5080 {
5081 frustration -= delta_t;
5082 if (frustration < 0.0)
5083 frustration = 0.0;
5084 }
5085 else
5086 {
5087 frustration += delta_t;
5088 if (frustration > 3.0) // 3s of frustration
5089 {
5090 [self noteFrustration:@"BEHAVIOUR_ATTACK_SNIPER"];
5091 [self setEvasiveJink:1000.0];
5092 behaviour = BEHAVIOUR_ATTACK_TARGET;
5093 frustration = 0.0;
5094 desired_speed = maxFlightSpeed;
5095 }
5096 }
5097
5098 }
5099
5100 if (missiles) [self considerFiringMissile:delta_t];
5101
5102 if (cloakAutomatic) [self activateCloakingDevice];
5103 [self fireMainWeapon:range];
5104
5105 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE && accuracy >= COMBAT_AI_ISNT_AWFUL)
5106 {
5107 behaviour = BEHAVIOUR_ATTACK_TARGET;
5108 }
5109
5110}
5111
5112
5113- (void) behaviour_fly_to_target_six:(double) delta_t
5114{
5115 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
5116 float max_available_speed = maxFlightSpeed;
5117 double range = [self rangeToPrimaryTarget];
5118 if (canBurn) max_available_speed *= [self afterburnerFactor];
5119
5120 // deal with collisions and lost targets
5121 if ([self proximityAlert] != nil)
5122 {
5123 if ([self proximityAlert] == [self primaryTarget])
5124 {
5125 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; // this behaviour will handle proximity_alert.
5126 [self behaviour_attack_fly_from_target: delta_t]; // do it now.
5127 }
5128 else
5129 {
5130 [self avoidCollision];
5131 }
5132 return;
5133 }
5134 if (![self canStillTrackPrimaryTarget])
5135 {
5136 [self noteLostTargetAndGoIdle];
5137 return;
5138 }
5139
5140 // control speed
5141 BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed);
5142 BOOL closeQuickly = (canBurn && range > weaponRange);
5143 double slow_down_range = weaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
5144 if (closeQuickly)
5145 {
5146 slow_down_range = weaponRange * COMBAT_OUT_RANGE_FACTOR;
5147 }
5148 double back_off_range = weaponRange * COMBAT_OUT_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
5149 Entity* rawTarget = [self primaryTarget];
5150 if (![rawTarget isShip])
5151 {
5152 [self noteLostTargetAndGoIdle];
5153 return;
5154 }
5155 ShipEntity* target = (ShipEntity *)rawTarget;
5156 double target_speed = [target speed];
5157 double last_success_factor = success_factor;
5158 double distance = [self rangeToDestination];
5159 success_factor = distance;
5160
5161 if (range < slow_down_range && (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX))
5162 {
5163 if (range < back_off_range)
5164 {
5165 desired_speed = fmax(0.9 * target_speed, 0.4 * maxFlightSpeed);
5166 }
5167 else
5168 {
5169 desired_speed = fmax(target_speed * 1.2, maxFlightSpeed);
5170 }
5171
5172 // avoid head-on collision
5173 if ((range < 0.5 * distance)&&(behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX))
5174 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
5175 }
5176 else
5177 {
5178 if (range < back_off_range)
5179 {
5180 desired_speed = fmax(0.9 * target_speed, 0.8 * maxFlightSpeed);
5181 }
5182 else
5183 {
5184 desired_speed = max_available_speed; // use afterburner to approach
5185 }
5186 }
5187
5188
5189 // if within 0.75km of the target's six or twelve, or if target almost at standstill for 62.5% of non-thargoid ships (!),
5190 // then vector in attack.
5191 if (distance < 750.0 || (target_speed < 0.2 && ![self isThargoid] && ([self universalID] & 14) > 4))
5192 {
5193 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
5194 frustration = 0.0;
5195 desired_speed = fmax(target_speed, 0.4 * maxFlightSpeed); // within the weapon's range don't use afterburner
5196 }
5197
5198 // target-six
5199 if (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX)
5200 {
5201 // head for a point weapon-range * 0.5 to the six of the target
5202 //
5203 _destination = [target distance_six:0.5 * weaponRange];
5204 }
5205 // target-twelve
5206 if (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE)
5207 {
5208 if ([forward_weapon_type isTurretLaser])
5209 {
5210 // head for a point near the target, avoiding common Galcop weapon mount locations
5211 // TODO: this should account for weapon ranges
5212 GLfloat offset = 1000.0;
5213 GLfloat spacing = 2000.0;
5214 if (accuracy > 0.0)
5215 {
5216 offset = accuracy * 750.0;
5217 spacing = 2000.0 + (accuracy * 500.0);
5218 }
5219 if (entity_personality & 1)
5220 { // half at random
5221 offset = -offset;
5222 }
5223 _destination = [target distance_twelve:spacing withOffset:offset];
5224 }
5225 else
5226 {
5227 // head for a point 1.25km above the target
5228 _destination = [target distance_twelve:1250 withOffset:0];
5229 }
5230 }
5231
5232 pitching_over = NO; // in case it's set from elsewhere
5233 double confidenceFactor = [self trackDestination:delta_t :NO];
5234
5235 if(success_factor > last_success_factor || confidenceFactor < 0.85) frustration += delta_t;
5236 else if(frustration > 0.0) frustration -= delta_t * 0.75;
5237
5238 double aspect = [self approachAspectToPrimaryTarget];
5239 if(![forward_weapon_type isTurretLaser] && (frustration > 10 || aspect > 0.75))
5240 {
5241 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
5242 }
5243
5244 // use weaponry
5245 if (missiles) [self considerFiringMissile:delta_t];
5246
5247 if (cloakAutomatic) [self activateCloakingDevice];
5248 [self fireMainWeapon:range];
5249
5250
5251
5252 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE)
5253 {
5254 behaviour = BEHAVIOUR_ATTACK_TARGET;
5255 }
5256}
5257
5258
5259- (void) behaviour_attack_mining_target:(double) delta_t
5260{
5261 double range = [self rangeToPrimaryTarget];
5262 if (![self canStillTrackPrimaryTarget])
5263 {
5264 [self noteLostTargetAndGoIdle];
5265 desired_speed = maxFlightSpeed * 0.375;
5266 return;
5267 }
5268 else if ((range < 650) || ([self proximityAlert] != nil))
5269 {
5270 if ([self proximityAlert] == NO_TARGET)
5271 {
5272 desired_speed = range * maxFlightSpeed / (650.0 * 16.0);
5273 }
5274 else
5275 {
5276 [self avoidCollision];
5277 }
5278 }
5279 else
5280 {
5281 //we have a target, its within scanner range, and outside 650
5282 desired_speed = maxFlightSpeed * 0.875;
5283 }
5284
5285 [self trackPrimaryTarget:delta_t:NO];
5286
5287 /* Don't open fire until within 3km - it doesn't take many mining
5288 * laser shots to destroy an asteroid, but some of these mining
5289 * ships are way too slow to effectively chase down the debris:
5290 * wait until reasonably close before trying to split it. */
5291 if (range < 3000)
5292 {
5293 [self fireMainWeapon:range];
5294 }
5295
5296
5297}
5298
5299
5300- (void) behaviour_attack_fly_to_target:(double) delta_t
5301{
5302 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
5303 float max_available_speed = maxFlightSpeed;
5304 double range = [self rangeToPrimaryTarget];
5305 if (canBurn) max_available_speed *= [self afterburnerFactor];
5306 if ([self primaryTarget] == nil)
5307 {
5308 [self noteLostTargetAndGoIdle];
5309 return;
5310 }
5311 Entity* rawTarget = [self primaryTarget];
5312 if (![rawTarget isShip])
5313 {
5314 // can't attack a wormhole
5315 [self noteLostTargetAndGoIdle];
5316 return;
5317 }
5318
5319 ShipEntity *target = (ShipEntity *)rawTarget;
5320 if ((range < COMBAT_IN_RANGE_FACTOR * weaponRange)||([self proximityAlert] != nil))
5321 {
5322 if (![self hasProximityAlertIgnoringTarget:YES])
5323 {
5324 behaviour = BEHAVIOUR_ATTACK_TARGET;
5325 }
5326 else
5327 {
5328 [self avoidCollision];
5329 return;
5330 }
5331 }
5332 else
5333 {
5334 if (![self canStillTrackPrimaryTarget])
5335 {
5336 [self noteLostTargetAndGoIdle];
5337 return;
5338 }
5339 }
5340
5341 // control speed
5342 //
5343 BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed);
5344 BOOL closeQuickly = (canBurn && [target weaponRange] > weaponRange && range > weaponRange);
5345 double slow_down_range = weaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
5346 if (closeQuickly)
5347 {
5348 slow_down_range = weaponRange * COMBAT_OUT_RANGE_FACTOR;
5349 }
5350 double back_off_range = 10000 * COMBAT_OUT_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
5351 double target_speed = [target speed];
5352 double aspect = [self approachAspectToPrimaryTarget];
5353
5354 if (range <= slow_down_range)
5355 {
5356 if (range < back_off_range)
5357 {
5358 if (accuracy < COMBAT_AI_IS_SMART || ([target primaryTarget] == self && aspect > 0.8) || aim_tolerance*range > COMBAT_AI_CONFIDENCE_FACTOR)
5359 {
5360 if (accuracy >= COMBAT_AI_FLEES_BETTER && aspect > 0.8)
5361 {
5362 desired_speed = fmax(target_speed * 1.25, 0.8 * maxFlightSpeed);
5363 // stay at high speed if might be taking return fire
5364 }
5365 else
5366 {
5367 desired_speed = fmax(target_speed * 1.05, 0.25 * maxFlightSpeed); // within the weapon's range match speed
5368
5369 }
5370 }
5371 else
5372 { // smart, and not being shot at right now - slow down to attack
5373 desired_speed = fmax(0.1 * target_speed, 0.1 * maxFlightSpeed);
5374 }
5375 }
5376 else
5377 {
5378 if (accuracy < COMBAT_AI_IS_SMART || ([target isShip] && [(ShipEntity *)target primaryTarget] == self) || range > weaponRange / 2.0)
5379 {
5380 desired_speed = fmax(target_speed * 1.5, maxFlightSpeed);
5381 }
5382 else
5383 { // smart, and not being shot at right now - slow down to attack
5384 if (aspect > -0.25)
5385 {
5386 desired_speed = fmax(0.5 * target_speed, 0.5 * maxFlightSpeed);
5387 }
5388 else
5389 {
5390 desired_speed = fmax(1.25 * target_speed, 0.5 * maxFlightSpeed);
5391 }
5392 }
5393 }
5394 }
5395 else
5396 {
5397 if (closeQuickly)
5398 {
5399 desired_speed = max_available_speed; // use afterburner to approach
5400 }
5401 else
5402 {
5403 desired_speed = fmax(maxFlightSpeed,fmin(3.0 * target_speed, max_available_speed)); // possibly use afterburner to approach
5404 }
5405 }
5406
5407
5408 double last_success_factor = success_factor;
5409 success_factor = [self trackPrimaryTarget:delta_t:NO]; // do the actual piloting
5410
5411 if ((success_factor > 0.999)||(success_factor > last_success_factor))
5412 {
5413 frustration -= delta_t;
5414 if (frustration < 0.0)
5415 frustration = 0.0;
5416 }
5417 else
5418 {
5419 frustration += delta_t;
5420 if (frustration > 3.0) // 3s of frustration
5421 {
5422 [self noteFrustration:@"BEHAVIOUR_ATTACK_FLY_TO_TARGET"];
5423 [self setEvasiveJink:1000.0];
5424 behaviour = BEHAVIOUR_ATTACK_TARGET;
5425 frustration = 0.0;
5426 desired_speed = maxFlightSpeed;
5427 }
5428 }
5429
5430 if (missiles) [self considerFiringMissile:delta_t];
5431
5432 if (cloakAutomatic) [self activateCloakingDevice];
5433 [self fireMainWeapon:range];
5434
5435
5436
5437 if (weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE && accuracy >= COMBAT_AI_ISNT_AWFUL && aim_tolerance * range < COMBAT_AI_CONFIDENCE_FACTOR)
5438 {
5439 // don't do this if the target is fleeing and the front laser is
5440 // the only weapon, or if we're too far away to use non-front
5441 // lasers effectively
5442 if (aspect < 0 ||
5443 !isWeaponNone(aft_weapon_type) ||
5444 !isWeaponNone(port_weapon_type) ||
5445 !isWeaponNone(starboard_weapon_type))
5446 {
5447 frustration = 0.0;
5448 behaviour = BEHAVIOUR_ATTACK_TARGET;
5449 }
5450 }
5451 else if (accuracy >= COMBAT_AI_FLEES_BETTER_2)
5452 {
5453 // if we're right in their gunsights, dodge!
5454 // need to dodge sooner if in aft sights
5455 if ([target behaviour] != BEHAVIOUR_FLEE_TARGET && [target behaviour] != BEHAVIOUR_FLEE_EVASIVE_ACTION)
5456 {
5457 if ((aspect > 0.99999 && !isWeaponNone([target weaponTypeForFacing:WEAPON_FACING_FORWARD strict:NO])) || (aspect < -0.999 && !isWeaponNone([target weaponTypeForFacing:WEAPON_FACING_AFT strict:NO])))
5458 {
5459 frustration = 0.0;
5460 behaviour = BEHAVIOUR_EVASIVE_ACTION;
5461 }
5462 }
5463 }
5464}
5465
5466
5467- (void) behaviour_attack_fly_from_target:(double) delta_t
5468{
5469 double range = [self rangeToPrimaryTarget];
5470 double last_success_factor = success_factor;
5471 success_factor = range;
5472
5473 if ([self primaryTarget] == nil)
5474 {
5475 [self noteLostTargetAndGoIdle];
5476 return;
5477 }
5478 if (last_success_factor > success_factor) // our target is closing in.
5479 {
5480 frustration += delta_t;
5481 }
5482 else
5483 { // not getting away fast enough?
5484 frustration += delta_t / 4.0 ;
5485 }
5486
5487 if (frustration > 10.0)
5488 {
5489 if (randf() < 0.3) {
5490 desired_speed = maxFlightSpeed * (([self hasFuelInjection] && (fuel > MIN_FUEL)) ? [self afterburnerFactor] : 1);
5491 }
5492 else if (range > COMBAT_IN_RANGE_FACTOR * weaponRange && randf() < 0.3)
5493 {
5494 behaviour = BEHAVIOUR_ATTACK_TARGET;
5495 }
5496 GLfloat z = jink.z;
5497 if (randf() < 0.3)
5498 {
5499 z /= 2; // move the z-offset closer to the target to let him fly away from the target.
5500 desired_speed = flightSpeed * 2; // increase speed a bit.
5501 }
5502 [self setEvasiveJink:z];
5503
5504 frustration /= 2.0;
5505 }
5506 if (desired_speed > maxFlightSpeed)
5507 {
5508 ShipEntity* target = [self primaryTarget];
5509 double target_speed = [target speed];
5510 if (desired_speed > target_speed * 2.0)
5511 {
5512 desired_speed = maxFlightSpeed; // don't overuse the injectors
5513 }
5514 }
5515 else if (desired_speed < maxFlightSpeed * 0.5)
5516 {
5517 desired_speed = maxFlightSpeed;
5518 }
5519
5520 if (range > COMBAT_OUT_RANGE_FACTOR * weaponRange + 15.0 * jink.x ||
5521 flightSpeed > (scannerRange - range) * max_flight_pitch / 6.28)
5522 {
5523 jink = kZeroVector;
5524 behaviour = BEHAVIOUR_ATTACK_TARGET;
5525 frustration = 0.0;
5526 }
5527 [self trackPrimaryTarget:delta_t:YES];
5528
5529 if (missiles) [self considerFiringMissile:delta_t];
5530
5531 if (cloakAutomatic) [self activateCloakingDevice];
5532 if ([self hasProximityAlertIgnoringTarget:YES])
5533 [self avoidCollision];
5534
5535 if (accuracy >= COMBAT_AI_FLEES_BETTER_2)
5536 {
5537 double aspect = [self approachAspectToPrimaryTarget];
5538 // if we're right in their gunsights, dodge!
5539 // need to dodge sooner if in aft sights
5540 if (aspect > 0.99999 || aspect < -0.999)
5541 {
5542 frustration = 0.0;
5543 behaviour = BEHAVIOUR_EVASIVE_ACTION;
5544 }
5545 }
5546
5547}
5548
5549
5550- (void) behaviour_running_defense:(double) delta_t
5551{
5552 if (![self canStillTrackPrimaryTarget])
5553 {
5554 [self noteLostTargetAndGoIdle];
5555 return;
5556 }
5557
5558 double range = [self rangeToPrimaryTarget];
5559 desired_speed = maxFlightSpeed; // not injectors
5560 jink = kZeroVector;
5561 if (range > weaponRange || range > 0.8 * scannerRange || range == 0)
5562 {
5563 behaviour = BEHAVIOUR_CLOSE_WITH_TARGET;
5564 if ([forward_weapon_type isTurretLaser])
5565 {
5566 behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
5567 }
5568 frustration = 0.0;
5569 }
5570 [self trackPrimaryTarget:delta_t:YES];
5571 if ([forward_weapon_type isTurretLaser])
5572 {
5573 // most Thargoids will only have the forward weapon
5574 [self fireMainWeapon:range];
5575 }
5576 else
5577 {
5578 [self fireAftWeapon:range];
5579 }
5580 if (cloakAutomatic) [self activateCloakingDevice];
5581 if ([self hasProximityAlertIgnoringTarget:YES])
5582 [self avoidCollision];
5583
5584 if (behaviour != BEHAVIOUR_CLOSE_WITH_TARGET && weapon_temp > COMBAT_AI_WEAPON_TEMP_USABLE)
5585 {
5586 behaviour = BEHAVIOUR_ATTACK_TARGET;
5587 }
5588
5589 // remember to look where you're going?
5590 if (accuracy >= COMBAT_AI_ISNT_AWFUL && [self hasProximityAlertIgnoringTarget:YES])
5591 {
5592 [self avoidCollision];
5593 }
5594
5595
5596}
5597
5598
5599- (void) behaviour_flee_target:(double) delta_t
5600{
5601 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
5602 float max_available_speed = maxFlightSpeed;
5603 double range = [self rangeToPrimaryTarget];
5604 if ([self primaryTarget] == nil)
5605 {
5606 [self noteLostTargetAndGoIdle];
5607 return;
5608 }
5609 if (canBurn) max_available_speed *= [self afterburnerFactor];
5610
5611 double last_range = success_factor;
5612 success_factor = range;
5613
5614 if (range > desired_range || range == 0)
5615 [shipAI message:@"REACHED_SAFETY"];
5616 else
5617 desired_speed = max_available_speed;
5618
5619 if (range > last_range) // improvement
5620 {
5621 frustration -= 0.25 * delta_t;
5622 if (frustration < 0.0)
5623 frustration = 0.0;
5624 }
5625 else
5626 {
5627 frustration += delta_t;
5628 if (frustration > 15.0) // 15s of frustration
5629 {
5630 [self noteFrustration:@"BEHAVIOUR_FLEE_TARGET"];
5631 frustration = 0.0;
5632 }
5633 }
5634
5635 [self trackPrimaryTarget:delta_t:YES];
5636
5637 Entity *target = [self primaryTarget];
5638
5639 if (missiles && [target isShip] && [(ShipEntity *)target primaryTarget] == self)
5640 {
5641 [self considerFiringMissile:delta_t];
5642 }
5643
5644 if (([self hasCascadeMine]) && (range < 10000.0) && canBurn)
5645 {
5646 float qbomb_chance = 0.01 * delta_t;
5647 if (randf() < qbomb_chance)
5648 {
5649 [self launchCascadeMine];
5650 }
5651 }
5652
5653// thargoids won't normally be fleeing, but if they do, they can still shoot
5654 if ([forward_weapon_type isTurretLaser])
5655 {
5656 [self fireMainWeapon:range];
5657 }
5658
5659 if (cloakAutomatic) [self activateCloakingDevice];
5660
5661 // remember to look where you're going?
5662 if (accuracy >= COMBAT_AI_ISNT_AWFUL && [self hasProximityAlertIgnoringTarget:YES])
5663 {
5664 [self avoidCollision];
5665 }
5666
5667}
5668
5669
5670- (void) behaviour_fly_range_from_destination:(double) delta_t
5671{
5672 double distance = [self rangeToDestination];
5673 if (distance < desired_range)
5674 {
5675 behaviour = BEHAVIOUR_FLY_FROM_DESTINATION;
5676 if (desired_speed < maxFlightSpeed)
5677 {
5678 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
5679 }
5680 }
5681 else
5682 {
5683 behaviour = BEHAVIOUR_FLY_TO_DESTINATION;
5684 }
5685 if ([self hasProximityAlertIgnoringTarget:YES])
5686 {
5687 [self avoidCollision];
5688 }
5689 frustration = 0.0;
5690
5691
5692
5693}
5694
5695
5696- (void) behaviour_face_destination:(double) delta_t
5697{
5698 double max_cos = MAX_COS;
5699 double distance = [self rangeToDestination];
5700 double old_pitch = flightPitch;
5701 desired_speed = 0.0;
5702 if (desired_range > 1.0 && distance > desired_range)
5703 {
5704 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)
5705 }
5706 double confidenceFactor = [self trackDestination:delta_t:NO];
5707 if (confidenceFactor >= max_cos && flightPitch == 0.0)
5708 {
5709 // desired facing achieved and movement stabilised.
5710 [shipAI message:@"FACING_DESTINATION"];
5711 [self doScriptEvent:OOJSID("shipNowFacingDestination")];
5712 frustration = 0.0;
5713 if(docking_match_rotation) // IDLE stops rotating while docking
5714 {
5715 behaviour = BEHAVIOUR_FLY_TO_DESTINATION;
5716 }
5717 else
5718 {
5719 behaviour = BEHAVIOUR_IDLE;
5720 }
5721 }
5722
5723 if(flightSpeed == 0) frustration += delta_t;
5724 if (frustration > 15.0 / max_flight_pitch) // allow more time for slow ships.
5725 {
5726 frustration = 0.0;
5727 [self noteFrustration:@"BEHAVIOUR_FACE_DESTINATION"];
5728 if(flightPitch == old_pitch) flightPitch = 0.5 * max_flight_pitch; // hack to get out of frustration.
5729 }
5730
5731 /* 2009-7-18 Eric: the condition check below is intended to eliminate the flippering between two positions for fast turning ships
5732 during low FPS conditions. This flippering is particular frustrating on slow computers during docking. But with my current computer I can't
5733 induce those low FPS conditions so I can't properly test if it helps.
5734 I did try with the TAF time acceleration that also generated larger frame jumps and than it seemed to help.
5735 */
5736 if(flightSpeed == 0 && frustration > 5 && confidenceFactor > 0.5 && ((flightPitch > 0 && old_pitch < 0) || (flightPitch < 0 && old_pitch > 0)))
5737 {
5738 flightPitch += 0.5 * old_pitch; // damping with last pitch value.
5739 }
5740
5741 if ([self hasProximityAlertIgnoringTarget:YES])
5742 {
5743 [self avoidCollision];
5744 }
5745
5746
5747}
5748
5749
5750- (void) behaviour_land_on_planet:(double) delta_t
5751{
5752 double max_cos = MAX_COS2; // trackDestination returns the squared confidence in reverse mode.
5753 desired_speed = 0.0;
5754
5755 OOPlanetEntity* planet = [UNIVERSE entityForUniversalID:planetForLanding];
5756
5757 if (![planet isPlanet])
5758 {
5759 behaviour = BEHAVIOUR_IDLE;
5760 aiScriptWakeTime = 1; // reconsider JSAI
5761 [shipAI message:@"NO_PLANET_NEARBY"];
5762 return;
5763 }
5764
5765 if (HPdistance(position, [planet position]) + [self collisionRadius] < [planet radius])
5766 {
5767 // we have landed. (completely disappeared inside planet)
5768 [self landOnPlanet:planet];
5769 return;
5770 }
5771
5772 double confidenceFactor = [self trackDestination:delta_t:YES]; // turn away from destination
5773
5774 if (confidenceFactor >= max_cos && flightSpeed == 0.0)
5775 {
5776 // We are now turned away from planet. Start landing by flying backward.
5777 thrust = 0.0; // stop forward acceleration.
5778 if (magnitude2(velocity) < MAX_LANDING_SPEED2)
5779 {
5780 [self adjustVelocity:vector_multiply_scalar([self forwardVector], -max_thrust * delta_t)];
5781 }
5782 }
5783
5784
5785 if ([self hasProximityAlertIgnoringTarget:YES])
5786 {
5787 [self avoidCollision];
5788 }
5789
5790
5791}
5792
5793
5794- (void) behaviour_formation_form_up:(double) delta_t
5795{
5796 // destination for each escort is set in update() from owner.
5797 ShipEntity* leadShip = [self owner];
5798 double distance = [self rangeToDestination];
5799 double eta = (distance - desired_range) / flightSpeed;
5800 if(eta < 0) eta = 0;
5801 if ((eta < 5.0)&&(leadShip)&&(leadShip->isShip))
5802 desired_speed = [leadShip flightSpeed] * (1 + eta * 0.05);
5803 else
5804 desired_speed = maxFlightSpeed;
5805
5806 double last_distance = success_factor;
5807 success_factor = distance;
5808
5809 // do the actual piloting!!
5810 [self trackDestination:delta_t: NO];
5811
5812 eta = eta / 0.51; // 2% safety margin assuming an average of half current speed
5813 GLfloat slowdownTime = (thrust > 0.0)? flightSpeed / (thrust) : 4.0;
5814 GLfloat minTurnSpeedFactor = 0.05 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds
5815
5816 if ((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor))
5817 desired_speed = flightSpeed * 0.50; // cut speed by 50% to a minimum minTurnSpeedFactor of speed
5818
5819 if (distance < last_distance) // improvement
5820 {
5821 frustration -= 0.25 * delta_t;
5822 if (frustration < 0.0)
5823 frustration = 0.0;
5824 }
5825 else
5826 {
5827 frustration += delta_t;
5828 if (frustration > 15.0)
5829 {
5830 if (!leadShip) [self noteFrustration:@"BEHAVIOUR_FORMATION_FORM_UP"]; // escorts never reach their destination when following leader.
5831 else if (distance > 0.5 * scannerRange && !pitching_over)
5832 {
5833 pitching_over = YES; // Force the ship in a 180 degree turn. Do it here to allow escorts to break out formation for some seconds.
5834 }
5835 frustration = 0;
5836 }
5837 }
5838 if ([self hasProximityAlertIgnoringTarget:YES])
5839 {
5840 [self avoidCollision];
5841 }
5842
5843
5844}
5845
5846
5847- (void) behaviour_fly_to_destination:(double) delta_t
5848{
5849 double distance = [self rangeToDestination];
5850 // double desiredRange = (dockingInstructions != nil) ? 1.2 * desired_range : desired_range; // stop a bit earlyer when docking.
5851 if (distance < desired_range) // + collision_radius)
5852 {
5853 // desired range achieved
5854 [shipAI message:@"DESIRED_RANGE_ACHIEVED"];
5855 [self doScriptEvent:OOJSID("shipAchievedDesiredRange")];
5856
5857 if(!docking_match_rotation) // IDLE stops rotating while docking
5858 {
5859 behaviour = BEHAVIOUR_IDLE;
5860 desired_speed = 0.0;
5861 }
5862 frustration = 0.0;
5863 }
5864 else
5865 {
5866 double last_distance = success_factor;
5867 success_factor = distance;
5868
5869 // do the actual piloting!!
5870 double confidenceFactor = [self trackDestination:delta_t: NO];
5871 if(confidenceFactor < 0.2) confidenceFactor = 0.2; // don't allow small or negative values.
5872
5873 /* 2009-07-19 Eric: Estimated Time of Arrival (eta) should also take the "angle to target" into account (confidenceFactor = cos(angle to target))
5874 and should not fuss about that last meter and use "distance + 1" instead of just "distance".
5875 trackDestination already did pitch regulation, use confidence here only for cutting down to high speeds.
5876 This should prevent ships crawling to their destination when they try to pull up close to their destination.
5877
5878 To prevent ships circling around their target without reaching destination I added a limitation based on turnrate,
5879 speed and distance to target. Formula based on satelite orbit:
5880 orbitspeed = turnrate (rad/sec) * radius (m) or flightSpeed = max_flight_pitch * 2 Pi * distance
5881 Speed must be significant lower when not flying in direction of target (low confidenceFactor) or it can never approach its destination
5882 and the ships runs the risk flying in circles around the target. (exclude active escorts)
5883 */
5884 GLfloat eta = ((distance + 1) - desired_range) / (0.51 * flightSpeed * confidenceFactor); // 2% safety margin assuming an average of half current speed
5885 GLfloat slowdownTime = (thrust > 0.0)? flightSpeed / (thrust) : 4.0;
5886 GLfloat minTurnSpeedFactor = 0.05 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds
5887 if (dockingInstructions != nil)
5888 {
5889 minTurnSpeedFactor /= 10.0;
5890 if (minTurnSpeedFactor * maxFlightSpeed > 20.0)
5891 {
5892 minTurnSpeedFactor /= 10.0;
5893 }
5894 }
5895
5896
5897 if (((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor)) || (flightSpeed > max_flight_pitch * 5 * confidenceFactor * distance))
5898 {
5899 desired_speed = flightSpeed * 0.50; // cut speed by 50% to a minimum minTurnSpeedFactor of speed
5900 }
5901
5902 /* Flight correction block to prevent one possible form of
5903 * crashes in late docking process */
5904 if (docking_match_rotation && confidenceFactor >= MAX_COS && dockingInstructions != nil && [dockingInstructions oo_intForKey:@"docking_stage"] >= 7)
5905 {
5906 // then at this point should be rotating to match the station
5907 StationEntity* station_for_docking = (StationEntity*)[self targetStation];
5908
5909 if ((station_for_docking)&&(station_for_docking->isStation))
5910 {
5911 float rollMatch = dot_product([station_for_docking portUpVectorForShip:self],[self upVector]);
5912 if (rollMatch < MAX_COS && rollMatch > -MAX_COS)
5913 {
5914 // not matching rotating - stop until corrected
5915 desired_speed = 0.1;
5916 }
5917 else if (desired_speed <= 0.2)
5918 {
5919 // had previously paused, so return to normal speed
5920 desired_speed = [dockingInstructions oo_floatForKey:@"speed"];
5921 }
5922 }
5923 }
5924
5925
5926
5927 if (distance < last_distance) // improvement
5928 {
5929 frustration -= 0.25 * delta_t;
5930 if (frustration < 0.0)
5931 frustration = 0.0;
5932 }
5933 else
5934 {
5935 frustration += delta_t;
5936 if ((frustration > slowdownTime * 10.0 && slowdownTime > 0)||(frustration > 15.0)) // 10x slowdownTime or 15s of frustration
5937 {
5938 [self noteFrustration:@"BEHAVIOUR_FLY_TO_DESTINATION"];
5939 frustration -= slowdownTime * 5.0; //repeat after another five units of frustration
5940 }
5941 }
5942 }
5943 if ([self hasProximityAlertIgnoringTarget:YES])
5944 {
5945 [self avoidCollision];
5946 }
5947
5948
5949}
5950
5951
5952- (void) behaviour_fly_from_destination:(double) delta_t
5953{
5954 double distance = [self rangeToDestination];
5955 if (distance > desired_range)
5956 {
5957 // desired range achieved
5958 [shipAI message:@"DESIRED_RANGE_ACHIEVED"];
5959 [self doScriptEvent:OOJSID("shipAchievedDesiredRange")];
5960
5961 behaviour = BEHAVIOUR_IDLE;
5962 frustration = 0.0;
5963 desired_speed = 0.0;
5964 }
5965
5966 [self trackDestination:delta_t:YES];
5967 if ([self hasProximityAlertIgnoringTarget:YES])
5968 {
5969 [self avoidCollision];
5970 }
5971
5972
5973}
5974
5975
5976- (void) behaviour_avoid_collision:(double) delta_t
5977{
5978 double distance = [self rangeToDestination];
5979 if (distance > desired_range)
5980 {
5981 [self resumePostProximityAlert];
5982 }
5983 else
5984 {
5985 ShipEntity* prox_ship = (ShipEntity*)[self proximityAlert];
5986 if (prox_ship)
5987 {
5988 desired_range = prox_ship->collision_radius * PROXIMITY_AVOID_DISTANCE_FACTOR;
5989 _destination = prox_ship->position;
5990 }
5991 double dq = [self trackDestination:delta_t:YES]; // returns 0 when heading towards prox_ship
5992 // Heading towards target with desired_speed > 0, avoids collisions better than setting desired_speed to zero.
5993 // (tested with boa class cruiser on collisioncourse with buoy)
5994 desired_speed = maxFlightSpeed * (0.5 * dq + 0.5);
5995 }
5996
5997
5998
5999}
6000
6001
6002- (void) behaviour_track_as_turret:(double) delta_t
6003{
6004 double aim = -2.0;
6005 ShipEntity *turret_owner = (ShipEntity *)[self owner];
6006 ShipEntity *turret_target = (ShipEntity *)[turret_owner primaryTarget];
6007 if (turret_owner && turret_target && [turret_owner hasHostileTarget])
6008 {
6009 aim = [self ballTrackLeadingTarget:delta_t atTarget:turret_target];
6010 if (aim > -1.0) // potential target
6011 {
6012 HPVector p = HPvector_subtract([turret_target position], [turret_owner position]);
6013 double cr = [turret_owner collisionRadius];
6014
6015 if (aim > .95)
6016 {
6017 [self fireTurretCannon:HPmagnitude(p) - cr];
6018 }
6019 return;
6020 }
6021 }
6022
6023 // can't fire on primary target; track secondary targets instead
6024 NSEnumerator *targetEnum = [turret_owner defenseTargetEnumerator];
6025 Entity *target = nil;
6026 while ((target = [[targetEnum nextObject] weakRefUnderlyingObject]))
6027 {
6028 // defense targets cannot be tracked while cloaked
6029 if ([target scanClass] == CLASS_NO_DRAW || [(ShipEntity *)target isCloaked] || [target energy] <= 0.0)
6030 {
6031 [turret_owner removeDefenseTarget:target];
6032 }
6033 else
6034 {
6035 double range = [turret_owner rangeToSecondaryTarget:target];
6036 if (range < weaponRange)
6037 {
6038 aim = [self ballTrackLeadingTarget:delta_t atTarget:target];
6039 if (aim > -1.0)
6040 { // tracking...
6041 HPVector p = HPvector_subtract([target position], [turret_owner position]);
6042 double cr = [turret_owner collisionRadius];
6043
6044 if (aim > .95)
6045 { // fire!
6046 [self fireTurretCannon:HPmagnitude(p) - cr];
6047 }
6048 return;
6049 }
6050 // else that target is out of range, try the next priority defense target
6051 }
6052 else if (range > scannerRange)
6053 {
6054 [turret_owner removeDefenseTarget:target];
6055 }
6056 }
6057 }
6058
6059 // turrets now don't return to neutral facing if no suitable target
6060 // better for shooting at targets that are on edge of fire arc
6061}
6062
6063
6064- (void) behaviour_fly_thru_navpoints:(double) delta_t
6065{
6066 int navpoint_plus_index = (next_navpoint_index + 1) % number_of_navpoints;
6067 HPVector d1 = navpoints[next_navpoint_index]; // head for this one
6068 HPVector d2 = navpoints[navpoint_plus_index]; // but be facing this one
6069
6070 HPVector rel = HPvector_between(d1, position); // vector from d1 to position
6071 HPVector ref = HPvector_between(d2, d1); // vector from d2 to d1
6072 ref = HPvector_normal(ref);
6073
6074 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);
6075
6076 GLfloat v0 = 0.0;
6077
6078 GLfloat r0 = HPdot_product(rel, ref); // proportion of rel in direction ref
6079
6080 // if r0 is negative then we're the wrong side of things
6081
6082 GLfloat r1 = HPmagnitude(xp); // distance of position from line
6083
6084 BOOL in_cone = (r0 > 0.5 * r1);
6085
6086 if (!in_cone) // are we in the approach cone ?
6087 r1 = 25.0 * flightSpeed; // aim a few km out!
6088 else
6089 r1 *= 2.0;
6090
6091 GLfloat dist2 = HPmagnitude2(rel);
6092
6093 if (dist2 < desired_range * desired_range)
6094 {
6095 // desired range achieved
6096 [self doScriptEvent:OOJSID("shipReachedNavPoint") andReactToAIMessage:@"NAVPOINT_REACHED"];
6097 if (navpoint_plus_index == 0)
6098 {
6099 [self doScriptEvent:OOJSID("shipReachedEndPoint") andReactToAIMessage:@"ENDPOINT_REACHED"];
6100 behaviour = BEHAVIOUR_IDLE;
6101 }
6102 next_navpoint_index = navpoint_plus_index; // loop as required
6103 }
6104 else
6105 {
6106 double last_success_factor = success_factor;
6107 double last_dist2 = last_success_factor;
6108 success_factor = dist2;
6109
6110 // set destination spline point from r1 and ref
6111 _destination = make_HPvector(d1.x + r1 * ref.x, d1.y + r1 * ref.y, d1.z + r1 * ref.z);
6112
6113 // do the actual piloting!!
6114 //
6115 // aim to within 1m
6116 GLfloat temp = desired_range;
6117 if (in_cone)
6118 desired_range = 1.0;
6119 else
6120 desired_range = 100.0;
6121 v0 = [self trackDestination:delta_t: NO];
6122 desired_range = temp;
6123
6124 if (dist2 < last_dist2) // improvement
6125 {
6126 frustration -= 0.25 * delta_t;
6127 if (frustration < 0.0)
6128 frustration = 0.0;
6129 }
6130 else
6131 {
6132 frustration += delta_t;
6133 if (frustration > 15.0) // 15s of frustration
6134 {
6135 [self noteFrustration:@"BEHAVIOUR_FLY_THRU_NAVPOINTS"];
6136 frustration -= 15.0; //repeat after another 15s of frustration
6137 }
6138 }
6139 }
6140
6141
6142
6143 GLfloat temp = desired_speed;
6144 desired_speed *= v0 * v0;
6145
6146 desired_speed = temp;
6147}
6148
6149
6150- (void) behaviour_scripted_ai:(double) delta_t
6151{
6152
6153 JSContext *context = OOJSAcquireContext();
6154 jsval rval = JSVAL_VOID;
6155 jsval deltaJS = JSVAL_VOID;
6156 NSDictionary *result = nil;
6157
6158 BOOL OK = JS_NewNumberValue(context, delta_t, &deltaJS);
6159 if (OK)
6160 {
6161 OK = [[self script] callMethod:OOJSID("scriptedAI")
6162 inContext:context
6163 withArguments:&deltaJS
6164 count:1
6165 result:&rval];
6166 }
6167
6168 if (!OK)
6169 {
6170 OOLog(@"ai.error",@"Could not call scriptedAI in ship script of %@, reverting to idle",self);
6171 behaviour = BEHAVIOUR_IDLE;
6172 OOJSRelinquishContext(context);
6173 return;
6174 }
6175
6176 if (!JSVAL_IS_OBJECT(rval))
6177 {
6178 OOLog(@"ai.error",@"Invalid return value of scriptedAI in ship script of %@, reverting to idle",self);
6179 behaviour = BEHAVIOUR_IDLE;
6180 OOJSRelinquishContext(context);
6181 return;
6182 }
6183
6184 result = OOJSNativeObjectFromJSObject(context, JSVAL_TO_OBJECT(rval));
6185 OOJSRelinquishContext(context);
6186
6187 // roll or roll factor
6188 if ([result objectForKey:@"stickRollFactor"] != nil)
6189 {
6190 stick_roll = [result oo_floatForKey:@"stickRollFactor"] * max_flight_roll;
6191 }
6192 else
6193 {
6194 stick_roll = [result oo_floatForKey:@"stickRoll"];
6195 }
6196 if (stick_roll > max_flight_roll)
6197 {
6198 stick_roll = max_flight_roll;
6199 }
6200 else if (stick_roll < -max_flight_roll)
6201 {
6202 stick_roll = -max_flight_roll;
6203 }
6204
6205 // pitch or pitch factor
6206 if ([result objectForKey:@"stickPitchFactor"] != nil)
6207 {
6208 stick_pitch = [result oo_floatForKey:@"stickPitchFactor"] * max_flight_pitch;
6209 }
6210 else
6211 {
6212 stick_pitch = [result oo_floatForKey:@"stickPitch"];
6213 }
6214 if (stick_pitch > max_flight_pitch)
6215 {
6216 stick_pitch = max_flight_pitch;
6217 }
6218 else if (stick_pitch < -max_flight_pitch)
6219 {
6220 stick_pitch = -max_flight_pitch;
6221 }
6222
6223 // yaw or yaw factor
6224 if ([result objectForKey:@"stickYawFactor"] != nil)
6225 {
6226 stick_yaw = [result oo_floatForKey:@"stickYawFactor"] * max_flight_yaw;
6227 }
6228 else
6229 {
6230 stick_yaw = [result oo_floatForKey:@"stickYaw"];
6231 }
6232 if (stick_yaw > max_flight_yaw)
6233 {
6234 stick_yaw = max_flight_yaw;
6235 }
6236 else if (stick_yaw < -max_flight_yaw)
6237 {
6238 stick_yaw = -max_flight_yaw;
6239 }
6240
6241 // apply sticks to current flight profile
6242 [self applySticks:delta_t];
6243
6244 // desired speed
6245 if ([result objectForKey:@"desiredSpeedFactor"] != nil)
6246 {
6247 desired_speed = [result oo_floatForKey:@"desiredSpeedFactor"] * maxFlightSpeed;
6248 }
6249 else
6250 {
6251 desired_speed = [result oo_floatForKey:@"desiredSpeed"];
6252 }
6253
6254 if (desired_speed < 0.0)
6255 {
6256 desired_speed = 0.0;
6257 }
6258 // overspeed and injector use is handled by applyThrust
6259
6260 if (behaviour == BEHAVIOUR_SCRIPTED_ATTACK_AI)
6261 {
6262 NSString* chosen_weapon = [result oo_stringForKey:@"chosenWeapon" defaultValue:@"FORWARD"];
6263 double range = [self rangeToPrimaryTarget];
6264
6265 if ([chosen_weapon isEqualToString:@"FORWARD"])
6266 {
6267 [self fireMainWeapon:range];
6268 }
6269 else if ([chosen_weapon isEqualToString:@"AFT"])
6270 {
6271 [self fireAftWeapon:range];
6272 }
6273 else if ([chosen_weapon isEqualToString:@"PORT"])
6274 {
6275 [self firePortWeapon:range];
6276 }
6277 else if ([chosen_weapon isEqualToString:@"STARBOARD"])
6278 {
6279 [self fireStarboardWeapon:range];
6280 }
6281 }
6282}
6283
6284- (float) reactionTime
6285{
6286 return reactionTime;
6287}
6288
6289
6290- (void) setReactionTime: (float) newReactionTime
6291{
6292 reactionTime = newReactionTime;
6293}
6294
6295
6296- (HPVector) calculateTargetPosition
6297{
6298 Entity *target = [self primaryTarget];
6299 if (target == nil)
6300 {
6301 return kZeroHPVector;
6302 }
6303 if (reactionTime <= 0.0)
6304 {
6305 return [target position];
6306 }
6307 double t = [UNIVERSE getTime] - trackingCurveTimes[1];
6308 return HPvector_add(HPvector_add(trackingCurveCoeffs[0], HPvector_multiply_scalar(trackingCurveCoeffs[1],t)), HPvector_multiply_scalar(trackingCurveCoeffs[2],t*t));
6309}
6310
6311
6312- (void) startTrackingCurve
6313{
6314 Entity *target = [self primaryTarget];
6315 if (target == nil)
6316 {
6317 return;
6318 }
6319 OOTimeAbsolute now = [UNIVERSE getTime];
6320 trackingCurvePositions[0] = [target position];
6321 trackingCurvePositions[1] = [target position];
6322 trackingCurvePositions[2] = [target position];
6323 trackingCurvePositions[3] = [target position];
6324 trackingCurveTimes[0] = now;
6325 trackingCurveTimes[1] = now - reactionTime/3.0;
6326 trackingCurveTimes[2] = now - reactionTime*2.0/3.0;
6327 trackingCurveTimes[3] = now - reactionTime;
6328 [self calculateTrackingCurve];
6329 return;
6330}
6331
6332
6333- (void) updateTrackingCurve
6334{
6335 Entity *target = [self primaryTarget];
6336 OOTimeAbsolute now = [UNIVERSE getTime];
6337 if (target == nil || reactionTime <= 0.0 || trackingCurveTimes[0] + reactionTime/3.0 > now) return;
6338 trackingCurvePositions[3] = trackingCurvePositions[2];
6339 trackingCurvePositions[2] = trackingCurvePositions[1];
6340 trackingCurvePositions[1] = trackingCurvePositions[0];
6341 if (EXPECT_NOT([target isShip] && [(ShipEntity *)target isCloaked]))
6342 {
6343 // if target is cloaked, introduce some more inaccuracy
6344 // 0.02 seems to be enough to give them slight difficulty on
6345 // a straight-line target and real trouble on anything better
6346 trackingCurvePositions[0] = HPvector_add([target position],OOHPVectorRandomSpatial([(ShipEntity *)target flightSpeed]*reactionTime*0.02));
6347 }
6348 else
6349 {
6350 trackingCurvePositions[0] = [target position];
6351 }
6352 trackingCurveTimes[3] = trackingCurveTimes[2];
6353 trackingCurveTimes[2] = trackingCurveTimes[1];
6354 trackingCurveTimes[1] = trackingCurveTimes[0];
6355 trackingCurveTimes[0] = now;
6356 [self calculateTrackingCurve];
6357 return;
6358}
6359
6360- (void) calculateTrackingCurve
6361{
6362 if (reactionTime <= 0.0)
6363 {
6364 trackingCurveCoeffs[0] = trackingCurvePositions[0];
6365 trackingCurveCoeffs[1] = kZeroHPVector;
6366 trackingCurveCoeffs[2] = kZeroHPVector;
6367 return;
6368 }
6369 double t1 = trackingCurveTimes[2] - trackingCurveTimes[1],
6370 t2 = trackingCurveTimes[3] - trackingCurveTimes[1];
6371 trackingCurveCoeffs[0] = trackingCurvePositions[1];
6372 trackingCurveCoeffs[1] = HPvector_add(HPvector_add(
6373 HPvector_multiply_scalar(trackingCurvePositions[1], -(t1+t2)/(t1*t2)),
6374 HPvector_multiply_scalar(trackingCurvePositions[2], -t2/(t1*(t1-t2)))),
6375 HPvector_multiply_scalar(trackingCurvePositions[3], t1/(t2*(t1-t2))));
6376 trackingCurveCoeffs[2] = HPvector_add(HPvector_add(
6377 HPvector_multiply_scalar(trackingCurvePositions[1], 1/(t1*t2)),
6378 HPvector_multiply_scalar(trackingCurvePositions[2], 1/(t1*(t1-t2)))),
6379 HPvector_multiply_scalar(trackingCurvePositions[3], -1/(t2*(t1-t2))));
6380 return;
6381}
6382
6383- (void) drawImmediate:(bool)immediate translucent:(bool)translucent
6384{
6385 if ((no_draw_distance < cam_zero_distance) || // Done redundantly to skip subentities
6386 (cloaking_device_active && randf() > 0.10))
6387 {
6388 // Don't draw.
6389 return;
6390 }
6391
6392 // Draw self.
6393 [super drawImmediate:immediate translucent:translucent];
6394
6395#ifndef NDEBUG
6396 // Draw bounding boxes if we have to before going for the subentities.
6397 // TODO: the translucent flag here makes very little sense. Something's wrong with the matrices.
6398 if (translucent) [self drawDebugStuff];
6399 else if (gDebugFlags & DEBUG_BOUNDING_BOXES && ![self isSubEntity])
6400 {
6401 OODebugDrawBoundingBox([self boundingBox]);
6402 OODebugDrawColoredBoundingBox(totalBoundingBox, [OOColor purpleColor]);
6403 }
6404#endif
6405
6406 // Draw subentities.
6407 if (!immediate) // TODO: is this relevant any longer?
6408 {
6409 // save time by not copying the subentity array if it's empty - CIM
6410 if ([self subEntityCount] > 0)
6411 {
6412 Entity<OOSubEntity> *subEntity = nil;
6413 foreach (subEntity, [self subEntities])
6414 {
6415 NSAssert3([subEntity owner] == self, @"Subentity ownership broke - %@ should be owned by %@ but is owned by %@.", subEntity, self, [subEntity owner]);
6416 [subEntity drawSubEntityImmediate:immediate translucent:translucent];
6417 }
6418 }
6419 }
6420}
6421
6422
6423#ifndef NDEBUG
6424- (void) drawDebugStuff
6425{
6426 // HPVect: imprecise here - needs camera relative
6427 if (0 && reportAIMessages)
6428 {
6429 OODebugDrawPoint(HPVectorToVector(_destination), [OOColor blueColor]);
6430 OODebugDrawColoredLine(HPVectorToVector([self position]), HPVectorToVector(_destination), [OOColor colorWithWhite:0.15 alpha:1.0]);
6431
6432 Entity *pTarget = [self primaryTarget];
6433 if (pTarget != nil)
6434 {
6435 OODebugDrawPoint(HPVectorToVector([pTarget position]), [OOColor redColor]);
6436 OODebugDrawColoredLine(HPVectorToVector([self position]), HPVectorToVector([pTarget position]), [OOColor colorWithRed:0.2 green:0.0 blue:0.0 alpha:1.0]);
6437 }
6438
6439 Entity *sTarget = [self targetStation];
6440 if (sTarget != pTarget && [sTarget isStation])
6441 {
6442 OODebugDrawPoint(HPVectorToVector([sTarget position]), [OOColor cyanColor]);
6443 }
6444
6445 Entity *fTarget = [self foundTarget];
6446 if (fTarget != nil && fTarget != pTarget && fTarget != sTarget)
6447 {
6448 OODebugDrawPoint(HPVectorToVector([fTarget position]), [OOColor magentaColor]);
6449 }
6450 }
6451}
6452#endif
6453
6454
6455- (void) drawSubEntityImmediate:(bool)immediate translucent:(bool)translucent
6456{
6458
6459 if (cam_zero_distance > no_draw_distance) // this test provides an opportunity to do simple LoD culling
6460 {
6461 return; // TOO FAR AWAY
6462 }
6464
6465 // HPVect: need to make camera-relative
6466 OOGLTranslateModelView(HPVectorToVector(position));
6467 OOGLMultModelView(rotMatrix);
6468 [self drawImmediate:immediate translucent:translucent];
6469
6470#ifndef NDEBUG
6472 {
6473 OODebugDrawBoundingBox([self boundingBox]);
6474 }
6475#endif
6476
6478
6480}
6481
6482
6483static GLfloat cargo_color[4] = { 0.9, 0.9, 0.9, 1.0}; // gray
6484static GLfloat hostile_color[4] = { 1.0, 0.25, 0.0, 1.0}; // red/orange
6485static GLfloat neutral_color[4] = { 1.0, 1.0, 0.0, 1.0}; // yellow
6486static GLfloat friendly_color[4] = { 0.0, 1.0, 0.0, 1.0}; // green
6487static GLfloat missile_color[4] = { 0.0, 1.0, 1.0, 1.0}; // cyan
6488static GLfloat police_color1[4] = { 0.5, 0.0, 1.0, 1.0}; // purpley-blue
6489static GLfloat police_color2[4] = { 1.0, 0.0, 0.5, 1.0}; // purpley-red
6490static GLfloat jammed_color[4] = { 0.0, 0.0, 0.0, 0.0}; // clear black
6491static GLfloat mascem_color1[4] = { 0.3, 0.3, 0.3, 1.0}; // dark gray
6492static GLfloat mascem_color2[4] = { 0.4, 0.1, 0.4, 1.0}; // purple
6493static GLfloat scripted_color[4] = { 0.0, 0.0, 0.0, 0.0}; // to be defined by script
6494
6495- (GLfloat *) scannerDisplayColorForShip:(ShipEntity*)otherShip :(BOOL)isHostile :(BOOL)flash :(OOColor *)scannerDisplayColor1 :(OOColor *)scannerDisplayColor2 :(OOColor *)scannerDisplayColorH1 :(OOColor *)scannerDisplayColorH2
6496{
6497 if (isHostile)
6498 {
6499 /* if there are any scripted scanner hostile display colours
6500 * for the ship, use them - otherwise fall through to the
6501 * normal scripted colours, then the scan class colours */
6502 if (scannerDisplayColorH1 || scannerDisplayColorH2)
6503 {
6504 if (scannerDisplayColorH1 && !scannerDisplayColorH2)
6505 {
6506 [scannerDisplayColorH1 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6507 }
6508
6509 if (!scannerDisplayColorH1 && scannerDisplayColorH2)
6510 {
6511 [scannerDisplayColorH2 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6512 }
6513
6514 if (scannerDisplayColorH1 && scannerDisplayColorH2)
6515 {
6516 if (flash)
6517 [scannerDisplayColorH1 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6518 else
6519 [scannerDisplayColorH2 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6520 }
6521
6522 return scripted_color;
6523 }
6524 }
6525
6526 // if there are any scripted scanner display colors for the ship, use them
6527 if (scannerDisplayColor1 || scannerDisplayColor2)
6528 {
6529 if (scannerDisplayColor1 && !scannerDisplayColor2)
6530 {
6531 [scannerDisplayColor1 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6532 }
6533
6534 if (!scannerDisplayColor1 && scannerDisplayColor2)
6535 {
6536 [scannerDisplayColor2 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6537 }
6538
6539 if (scannerDisplayColor1 && scannerDisplayColor2)
6540 {
6541 if (flash)
6542 [scannerDisplayColor1 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6543 else
6544 [scannerDisplayColor2 getRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
6545 }
6546
6547 return scripted_color;
6548 }
6549
6550 // no scripted scanner display colors defined, proceed as per standard
6551 if ([self isJammingScanning])
6552 {
6553 if (![otherShip hasMilitaryScannerFilter])
6554 return jammed_color;
6555 else
6556 {
6557 if (flash)
6558 return mascem_color1;
6559 else
6560 {
6561 if (isHostile)
6562 return hostile_color;
6563 else
6564 return mascem_color2;
6565 }
6566 }
6567 }
6568
6569 switch (scanClass)
6570 {
6571 case CLASS_ROCK :
6572 case CLASS_CARGO :
6573 return cargo_color;
6574 case CLASS_THARGOID :
6575 if (flash)
6576 return hostile_color;
6577 else
6578 return friendly_color;
6579 case CLASS_MISSILE :
6580 return missile_color;
6581 case CLASS_STATION :
6582 return friendly_color;
6583 case CLASS_BUOY :
6584 if (flash)
6585 return friendly_color;
6586 else
6587 return neutral_color;
6588 case CLASS_POLICE :
6589 case CLASS_MILITARY :
6590 if ((isHostile)&&(flash))
6591 return police_color2;
6592 else
6593 return police_color1;
6594 case CLASS_MINE :
6595 if (flash)
6596 return neutral_color;
6597 else
6598 return hostile_color;
6599 default :
6600 if (isHostile)
6601 return hostile_color;
6602 }
6603 return neutral_color;
6604}
6605
6606
6607- (void)setScannerDisplayColor1:(OOColor *)color
6608{
6609 DESTROY(scanner_display_color1);
6610
6611 if (color == nil) color = [OOColor colorWithDescription:[[self shipInfoDictionary] objectForKey:@"scanner_display_color1"]];
6612 scanner_display_color1 = [color retain];
6613}
6614
6615
6616- (void)setScannerDisplayColor2:(OOColor *)color
6617{
6618 DESTROY(scanner_display_color2);
6619
6620 if (color == nil) color = [OOColor colorWithDescription:[[self shipInfoDictionary] objectForKey:@"scanner_display_color2"]];
6621 scanner_display_color2 = [color retain];
6622}
6623
6624
6625- (OOColor *)scannerDisplayColor1
6626{
6627 return [[scanner_display_color1 retain] autorelease];
6628}
6629
6630
6631- (OOColor *)scannerDisplayColor2
6632{
6633 return [[scanner_display_color2 retain] autorelease];
6634}
6635
6636
6637- (void)setScannerDisplayColorHostile1:(OOColor *)color
6638{
6639 DESTROY(scanner_display_color_hostile1);
6640
6641 if (color == nil) color = [OOColor colorWithDescription:[[self shipInfoDictionary] objectForKey:@"scanner_hostile_display_color1"]];
6642 scanner_display_color_hostile1 = [color retain];
6643}
6644
6645
6646- (void)setScannerDisplayColorHostile2:(OOColor *)color
6647{
6648 DESTROY(scanner_display_color_hostile2);
6649
6650 if (color == nil) color = [OOColor colorWithDescription:[[self shipInfoDictionary] objectForKey:@"scanner_hostile_display_color2"]];
6651 scanner_display_color_hostile2 = [color retain];
6652}
6653
6654
6655- (OOColor *)scannerDisplayColorHostile1
6656{
6657 return [[scanner_display_color_hostile1 retain] autorelease];
6658}
6659
6660
6661- (OOColor *)scannerDisplayColorHostile2
6662{
6663 return [[scanner_display_color_hostile2 retain] autorelease];
6664}
6665
6666
6667- (BOOL)isCloaked
6668{
6669 return cloaking_device_active;
6670}
6671
6672
6673- (BOOL) cloakPassive
6674{
6675 return cloakPassive;
6676}
6677
6678
6679- (void)setCloaked:(BOOL)cloak
6680{
6681 if (cloak) [self activateCloakingDevice];
6682 else [self deactivateCloakingDevice];
6683}
6684
6685
6686- (BOOL)hasAutoCloak
6687{
6688 return cloakAutomatic;
6689}
6690
6691
6692- (void)setAutoCloak:(BOOL)automatic
6693{
6694 cloakAutomatic = !!automatic;
6695}
6696
6697
6698- (BOOL) isJammingScanning
6699{
6700 return ([self hasMilitaryJammer] && military_jammer_active);
6701}
6702
6703
6704- (void) addSubEntity:(Entity<OOSubEntity> *)sub
6705{
6706 if (sub == nil) return;
6707
6708 if (subEntities == nil) subEntities = [[NSMutableArray alloc] init];
6709 sub->isSubEntity = YES;
6710 // Order matters - need consistent state in setOwner:. -- Ahruman 2008-04-20
6711 [subEntities addObject:sub];
6712 [sub setOwner:self];
6713
6714 [self addSubentityToCollisionRadius:sub];
6715}
6716
6717
6718- (void) setOwner:(Entity *)who_owns_entity
6719{
6720 [super setOwner:who_owns_entity];
6721
6722 /* Reset shader binding target so that bind-to-super works.
6723 This is necessary since we don't know about the owner in
6724 setUpShipFromDictionary:, when the mesh is initially set up.
6725 -- Ahruman 2008-04-19
6726 */
6727 if (isSubEntity)
6728 {
6729 [[self drawable] setBindingTarget:self];
6730 }
6731}
6732
6733
6734- (void) applyThrust:(double) delta_t
6735{
6736 GLfloat dt_thrust = SHIP_THRUST_FACTOR * thrust * delta_t;
6737 BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
6738 BOOL isUsingAfterburner = (canBurn && (flightSpeed > maxFlightSpeed) && (desired_speed >= flightSpeed));
6739 float max_available_speed = maxFlightSpeed;
6740 if (canBurn) max_available_speed *= [self afterburnerFactor];
6741
6742 if (thrust)
6743 {
6744 // If we have Newtonian (non-thrust) velocity, brake it.
6745 GLfloat velmag = magnitude(velocity);
6746 if (velmag)
6747 {
6748 GLfloat vscale = fmaxf((velmag - dt_thrust) / velmag, 0.0f);
6749 scale_vector(&velocity, vscale);
6750 }
6751 }
6752
6753 if (behaviour == BEHAVIOUR_TUMBLE) return;
6754
6755 // check for speed
6756 if (desired_speed > max_available_speed)
6757 desired_speed = max_available_speed;
6758
6759 if (flightSpeed > desired_speed)
6760 {
6761 [self decrease_flight_speed: dt_thrust];
6762 if (flightSpeed < desired_speed) flightSpeed = desired_speed;
6763 }
6764 if (flightSpeed < desired_speed)
6765 {
6766 [self increase_flight_speed: dt_thrust];
6767 if (flightSpeed > desired_speed) flightSpeed = desired_speed;
6768 }
6769 [self moveForward: delta_t*flightSpeed];
6770
6771 // burn fuel at the appropriate rate
6772 if (isUsingAfterburner) // no fuelconsumption on slowdown
6773 {
6774 fuel_accumulator -= delta_t * afterburner_rate;
6775 while (fuel_accumulator < 0.0)
6776 {
6777 fuel--;
6778 fuel_accumulator += 1.0;
6779 }
6780 }
6781}
6782
6783
6784- (void) orientationChanged
6785{
6786 [super orientationChanged];
6787
6788 v_forward = vector_forward_from_quaternion(orientation);
6789 v_up = vector_up_from_quaternion(orientation);
6790 v_right = vector_right_from_quaternion(orientation);
6791}
6792
6793
6794- (void) applyRoll:(GLfloat) roll1 andClimb:(GLfloat) climb1
6795{
6796 Quaternion q1 = kIdentityQuaternion;
6797
6798 if (!roll1 && !climb1 && !hasRotated) return;
6799
6800 if (roll1) quaternion_rotate_about_z(&q1, -roll1);
6801 if (climb1) quaternion_rotate_about_x(&q1, -climb1);
6802
6803 orientation = quaternion_multiply(q1, orientation);
6804 [self orientationChanged];
6805}
6806
6807
6808- (void) applyRoll:(GLfloat) roll1 climb:(GLfloat) climb1 andYaw:(GLfloat) yaw1
6809{
6810 if ((roll1 == 0.0)&&(climb1 == 0.0)&&(yaw1 == 0.0)&&(!hasRotated))
6811 return;
6812
6813 Quaternion q1 = kIdentityQuaternion;
6814
6815 if (roll1)
6816 quaternion_rotate_about_z(&q1, -roll1);
6817 if (climb1)
6818 quaternion_rotate_about_x(&q1, -climb1);
6819 if (yaw1)
6820 quaternion_rotate_about_y(&q1, -yaw1);
6821
6822 orientation = quaternion_multiply(q1, orientation);
6823 [self orientationChanged];
6824}
6825
6826
6827- (void) applyAttitudeChanges:(double) delta_t
6828{
6829 [self applyRoll:flightRoll*delta_t climb:flightPitch*delta_t andYaw:flightYaw*delta_t];
6830}
6831
6832
6833- (void) avoidCollision
6834{
6835 if (scanClass == CLASS_MISSILE)
6836 return; // missiles are SUPPOSED to collide!
6837
6838 ShipEntity* prox_ship = (ShipEntity*)[self proximityAlert];
6839
6840 if (prox_ship)
6841 {
6842 if (previousCondition)
6843 {
6844 [previousCondition release];
6845 previousCondition = nil;
6846 }
6847
6848 previousCondition = [[NSMutableDictionary dictionaryWithCapacity:5] retain];
6849
6850 [previousCondition oo_setInteger:behaviour forKey:@"behaviour"];
6851 if ([self primaryTarget] != nil)
6852 {
6853 // must use the weak ref here to prevent potential over-retention
6854 [previousCondition setObject:[[self primaryTarget] weakSelf] forKey:@"primaryTarget"];
6855 }
6856 [previousCondition oo_setFloat:desired_range forKey:@"desired_range"];
6857 [previousCondition oo_setFloat:desired_speed forKey:@"desired_speed"];
6858 [previousCondition oo_setHPVector:_destination forKey:@"destination"];
6859
6860 _destination = [prox_ship position];
6861 _destination = OOHPVectorInterpolate(position, [prox_ship position], 0.5); // point between us and them
6862
6863 desired_range = prox_ship->collision_radius * PROXIMITY_AVOID_DISTANCE_FACTOR;
6864
6865 behaviour = BEHAVIOUR_AVOID_COLLISION;
6866 pitching_over = YES;
6867 }
6868}
6869
6870
6871- (void) resumePostProximityAlert
6872{
6873 if (!previousCondition) return;
6874
6875 behaviour = [previousCondition oo_intForKey:@"behaviour"];
6876 [_primaryTarget release];
6877 _primaryTarget = [[previousCondition objectForKey:@"primaryTarget"] weakRetain];
6878 [self startTrackingCurve];
6879 desired_range = [previousCondition oo_floatForKey:@"desired_range"];
6880 desired_speed = [previousCondition oo_floatForKey:@"desired_speed"];
6881 _destination = [previousCondition oo_hpvectorForKey:@"destination"];
6882
6883 [previousCondition release];
6884 previousCondition = nil;
6885 frustration = 0.0;
6886
6887 DESTROY(_proximityAlert);
6888
6889 //[shipAI message:@"RESTART_DOCKING"]; // if docking, start over, other AIs will ignore this message
6890}
6891
6892
6893- (double) messageTime
6894{
6895 return messageTime;
6896}
6897
6898
6899- (void) setMessageTime:(double) value
6900{
6901 messageTime = value;
6902}
6903
6904
6905- (OOShipGroup *) group
6906{
6907 return _group;
6908}
6909
6910
6911- (void) setGroup:(OOShipGroup *)group
6912{
6913 if (group != _group)
6914 {
6915 if (_escortGroup != _group)
6916 {
6917 if (self == [_group leader]) [_group setLeader:nil];
6918 [_group removeShip:self];
6919 }
6920 [_group release];
6921 [group addShip:self];
6922 _group = [group retain];
6923
6924 [[group leader] updateEscortFormation];
6925 }
6926}
6927
6928
6929- (OOShipGroup *) escortGroup
6930{
6931 if (_escortGroup == nil)
6932 {
6933 _escortGroup = [[OOShipGroup alloc] initWithName:@"escort group"];
6934 [_escortGroup setLeader:self];
6935 }
6936
6937 return _escortGroup;
6938}
6939
6940
6941- (void) setEscortGroup:(OOShipGroup *)group
6942{
6943 if (group != _escortGroup)
6944 {
6945 [_escortGroup release];
6946 _escortGroup = [group retain];
6947 [group setLeader:self]; // A ship is always leader of its own escort group.
6948 [self updateEscortFormation];
6949 }
6950}
6951
6952
6953#ifndef NDEBUG
6954- (OOShipGroup *) rawEscortGroup
6955{
6956 return _escortGroup;
6957}
6958#endif
6959
6960
6961- (OOShipGroup *) stationGroup
6962{
6963 if (_group == nil)
6964 {
6965 _group = [[OOShipGroup alloc] initWithName:@"station group"];
6966 [_group setLeader:self];
6967 }
6968
6969 return _group;
6970}
6971
6972
6973- (BOOL) hasEscorts
6974{
6975 if (_escortGroup == nil) return NO;
6976 return [_escortGroup count] > 1; // If only one member, it's self.
6977}
6978
6979
6980- (NSEnumerator *) escortEnumerator
6981{
6982 if (_escortGroup == nil) return [[NSArray array] objectEnumerator];
6983 return [[_escortGroup mutationSafeEnumerator] ooExcludingObject:self];
6984}
6985
6986
6987- (NSArray *) escortArray
6988{
6989 if (_escortGroup == nil) return [NSArray array];
6990 return [[self escortEnumerator] allObjects];
6991}
6992
6993
6994- (uint8_t) escortCount
6995{
6996 if (_escortGroup == nil) return 0;
6997 return [_escortGroup count] - 1;
6998}
6999
7000
7001- (uint8_t) pendingEscortCount
7002{
7003 return _pendingEscortCount;
7004}
7005
7006
7007- (void) setPendingEscortCount:(uint8_t)count
7008{
7009 _pendingEscortCount = MIN(count, _maxEscortCount);
7010}
7011
7012
7013- (uint8_t) maxEscortCount
7014{
7015 return _maxEscortCount;
7016}
7017
7018
7019- (void) setMaxEscortCount:(uint8_t)newCount
7020{
7021 _maxEscortCount = newCount;
7022}
7023
7024
7025- (NSUInteger) turretCount
7026{
7027 NSUInteger count = 0;
7028 NSEnumerator *subEnum = [self shipSubEntityEnumerator];
7029 ShipEntity *se = nil;
7030 while ((se = [subEnum nextObject]))
7031 {
7032 if ([se isTurret])
7033 {
7034 count ++;
7035 }
7036 }
7037 return count;
7038}
7039
7040
7041- (Entity*) proximityAlert
7042{
7043 Entity* prox = [_proximityAlert weakRefUnderlyingObject];
7044 if (prox == nil)
7045 {
7046 DESTROY(_proximityAlert);
7047 }
7048 return prox;
7049}
7050
7051
7052- (void) setProximityAlert:(ShipEntity*) other
7053{
7054 if (!other)
7055 {
7056 DESTROY(_proximityAlert);
7057 return;
7058 }
7059
7060 if ([other mass] < 2000) // we are not alerted by small objects. (a cargopod has a mass of about 1000)
7061 return;
7062
7063
7064 if (isStation) // stations don't worry about colliding with things
7065 return;
7066
7067 /* Ignore station collision warnings if launching or docking */
7068 if ((other->isStation) && ([self status] == STATUS_LAUNCHING ||
7069 dockingInstructions != nil))
7070 {
7071 return;
7072 }
7073
7074 if (!crew) // Ships without pilot (cargo, rocks, missiles, buoys etc) will not get alarmed. (escape-pods have pilots)
7075 return;
7076
7077 // check vectors
7078 Vector vdiff = HPVectorToVector(HPvector_between(position, other->position));
7079 GLfloat d_forward = dot_product(vdiff, v_forward);
7080 GLfloat d_up = dot_product(vdiff, v_up);
7081 GLfloat d_right = dot_product(vdiff, v_right);
7082 if ((d_forward > 0.0)&&(flightSpeed > 0.0)) // it's ahead of us and we're moving forward
7083 d_forward *= 0.25 * maxFlightSpeed / flightSpeed; // extend the collision zone forward up to 400%
7084 double d2 = d_forward * d_forward + d_up * d_up + d_right * d_right;
7085 double cr2 = collision_radius * 2.0 + other->collision_radius; cr2 *= cr2; // check with twice the combined radius
7086
7087 if (d2 > cr2) // we're okay
7088 return;
7089
7090 if (behaviour == BEHAVIOUR_AVOID_COLLISION) // already avoiding something
7091 {
7092 ShipEntity* prox = (ShipEntity*)[self proximityAlert];
7093 if ((prox)&&(prox != other))
7094 {
7095 // check which subtends the greatest angle
7096 GLfloat sa_prox = prox->collision_radius * prox->collision_radius / HPdistance2(position, prox->position);
7097 GLfloat sa_other = other->collision_radius * other->collision_radius / HPdistance2(position, other->position);
7098 if (sa_prox < sa_other) return;
7099 }
7100 }
7101 [_proximityAlert release];
7102 _proximityAlert = [other weakRetain];
7103}
7104
7105
7106- (NSString *) name
7107{
7108 return name;
7109}
7110
7111
7112- (NSString *) shipUniqueName
7113{
7114 return shipUniqueName;
7115}
7116
7117
7118- (NSString *) shipClassName
7119{
7120 return shipClassName;
7121}
7122
7123
7124- (NSString *) displayName
7125{
7126 if (displayName == nil || [displayName length] == 0)
7127 {
7128 if ([shipUniqueName length] == 0)
7129 {
7130 if (shipClassName == nil)
7131 {
7132 return name;
7133 }
7134 else
7135 {
7136 return shipClassName;
7137 }
7138 }
7139 else
7140 {
7141 if (shipClassName == nil)
7142 {
7143 return [NSString stringWithFormat:@"%@: %@",name,shipUniqueName];
7144 }
7145 else
7146 {
7147 return [NSString stringWithFormat:@"%@: %@",shipClassName,shipUniqueName];
7148 }
7149 }
7150 }
7151 return displayName;
7152}
7153
7154
7155// needed so that scan_description = scan_description doesn't have odd effects
7156- (NSString *)scanDescriptionForScripting
7157{
7158 return scan_description;
7159}
7160
7161
7162- (NSString *)scanDescription
7163{
7164 if (scan_description != nil)
7165 {
7166 return scan_description;
7167 }
7168 else
7169 {
7170 NSString *desc = nil;
7171 switch ([self scanClass])
7172 {
7173 case CLASS_NEUTRAL:
7174 {
7175 int legal = [self legalStatus];
7176 int legal_i = 0;
7177 if (legal > 0)
7178 {
7179 legal_i = (legal <= 50) ? 1 : 2;
7180 }
7181 desc = [[[UNIVERSE descriptions] oo_arrayForKey:@"legal_status"] oo_stringAtIndex:legal_i];
7182 }
7183 break;
7184
7185 case CLASS_THARGOID:
7186 desc = DESC(@"legal-desc-alien");
7187 break;
7188
7189 case CLASS_POLICE:
7190 desc = DESC(@"legal-desc-system-vessel");
7191 break;
7192
7193 case CLASS_MILITARY:
7194 desc = DESC(@"legal-desc-military-vessel");
7195 break;
7196
7197 default:
7198 break;
7199 }
7200 return desc;
7201 }
7202}
7203
7204
7205- (void) setName:(NSString *)inName
7206{
7207 [name release];
7208 name = [inName copy];
7209}
7210
7211
7212- (void) setShipUniqueName:(NSString *)inName
7213{
7214 [shipUniqueName release];
7215 shipUniqueName = [inName copy];
7216}
7217
7218
7219- (void) setShipClassName:(NSString *)inName
7220{
7221 [shipClassName release];
7222 shipClassName = [inName copy];
7223}
7224
7225
7226- (void) setDisplayName:(NSString *)inName
7227{
7228 [displayName release];
7229 displayName = [inName copy];
7230}
7231
7232
7233- (void) setScanDescription:(NSString *)inName
7234{
7235 [scan_description release];
7236 scan_description = [inName copy];
7237}
7238
7239
7240- (NSString *) identFromShip:(ShipEntity*) otherShip
7241{
7242 if ([self isJammingScanning] && ![otherShip hasMilitaryScannerFilter])
7243 {
7244 return DESC(@"unknown-target");
7245 }
7246 return [self displayName];
7247}
7248
7249
7250- (BOOL) hasRole:(NSString *)role
7251{
7252 return [roleSet hasRole:role] || [role isEqual:primaryRole] || [role isEqual:[self shipDataKeyAutoRole]];
7253}
7254
7255
7256- (OORoleSet *)roleSet
7257{
7258 if (roleSet == nil) roleSet = [[OORoleSet alloc] initWithRoleString:primaryRole];
7259 return [[roleSet roleSetWithAddedRoleIfNotSet:primaryRole probability:1.0] roleSetWithAddedRoleIfNotSet:[self shipDataKeyAutoRole] probability:1.0];
7260}
7261
7262
7263- (void) addRole:(NSString *)role
7264{
7265 [self addRole:role withProbability:0.0f];
7266}
7267
7268
7269- (void) addRole:(NSString *)role withProbability:(float)probability
7270{
7271 if (![self hasRole:role])
7272 {
7273 OORoleSet *newRoles = nil;
7274 if (roleSet != nil) newRoles = [roleSet roleSetWithAddedRole:role probability:probability];
7275 else newRoles = [OORoleSet roleSetWithRole:role probability:probability];
7276 if (newRoles != nil)
7277 {
7278 [roleSet release];
7279 roleSet = [newRoles retain];
7280 }
7281 }
7282}
7283
7284
7285- (void) removeRole:(NSString *)role
7286{
7287 if ([self hasRole:role])
7288 {
7289 OORoleSet *newRoles = [roleSet roleSetWithRemovedRole:role];
7290 if (newRoles != nil)
7291 {
7292 [roleSet release];
7293 roleSet = [newRoles retain];
7294 }
7295 }
7296}
7297
7298
7299- (NSString *)primaryRole
7300{
7301 if (primaryRole == nil)
7302 {
7303 primaryRole = [roleSet anyRole];
7304 if (primaryRole == nil) primaryRole = @"trader";
7305 [primaryRole retain];
7306 OOLog(@"ship.noPrimaryRole", @"%@ had no primary role, randomly selected \"%@\".", [self name], primaryRole);
7307 }
7308
7309 return primaryRole;
7310}
7311
7312
7313// Exposed to AI.
7314- (void)setPrimaryRole:(NSString *)role
7315{
7316 if (![role isEqual:primaryRole])
7317 {
7318 [primaryRole release];
7319 primaryRole = [role copy];
7320 }
7321}
7322
7323
7324- (BOOL)hasPrimaryRole:(NSString *)role
7325{
7326 return [[self primaryRole] isEqual:role];
7327}
7328
7329
7330- (BOOL)isPolice
7331{
7332 //bounty hunters have a police role, but are not police, so we must test by scan class, not by role
7333 return [self scanClass] == CLASS_POLICE;
7334}
7335
7336- (BOOL)isThargoid
7337{
7338 return [self scanClass] == CLASS_THARGOID;
7339}
7340
7341
7342- (BOOL)isTrader
7343{
7344 return [UNIVERSE role:[self primaryRole] isInCategory:@"oolite-trader"];
7345}
7346
7347
7348- (BOOL)isPirate
7349{
7350 return [UNIVERSE role:[self primaryRole] isInCategory:@"oolite-pirate"];
7351}
7352
7353
7354- (BOOL)isMissile
7355{
7356 return ([[self primaryRole] hasSuffix:@"MISSILE"] || [self hasPrimaryRole:@"missile"]);
7357}
7358
7359
7360- (BOOL)isMine
7361{
7362 return [[self primaryRole] hasSuffix:@"MINE"];
7363}
7364
7365
7366- (BOOL)isWeapon
7367{
7368 return [self isMissile] || [self isMine];
7369}
7370
7371
7372- (BOOL)isEscort
7373{
7374 return [UNIVERSE role:[self primaryRole] isInCategory:@"oolite-escort"];
7375}
7376
7377
7378- (BOOL)isShuttle
7379{
7380 return [UNIVERSE role:[self primaryRole] isInCategory:@"oolite-shuttle"];
7381}
7382
7383
7384- (BOOL)isTurret
7385{
7386 return behaviour == BEHAVIOUR_TRACK_AS_TURRET;
7387}
7388
7389
7390- (BOOL)isPirateVictim
7391{
7392 return [UNIVERSE roleIsPirateVictim:[self primaryRole]];
7393}
7394
7395
7396- (BOOL)isExplicitlyUnpiloted
7397{
7398 return _explicitlyUnpiloted;
7399}
7400
7401
7402- (BOOL)isUnpiloted
7403{
7404 return [self isExplicitlyUnpiloted] || [self isHulk] || [self scanClass] == CLASS_ROCK || [self scanClass] == CLASS_CARGO;
7405}
7406
7407
7408static BOOL IsBehaviourHostile(OOBehaviour behaviour)
7409{
7410 switch (behaviour)
7411 {
7412 case BEHAVIOUR_ATTACK_TARGET:
7413 case BEHAVIOUR_ATTACK_FLY_TO_TARGET:
7414 case BEHAVIOUR_ATTACK_FLY_FROM_TARGET:
7415 case BEHAVIOUR_RUNNING_DEFENSE:
7416 case BEHAVIOUR_FLEE_TARGET:
7417 case BEHAVIOUR_ATTACK_BREAK_OFF_TARGET:
7418 case BEHAVIOUR_ATTACK_SLOW_DOGFIGHT:
7419 case BEHAVIOUR_EVASIVE_ACTION:
7420 case BEHAVIOUR_FLEE_EVASIVE_ACTION:
7421 case BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX:
7422 // case BEHAVIOUR_ATTACK_MINING_TARGET:
7423 case BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE:
7424 case BEHAVIOUR_ATTACK_BROADSIDE:
7425 case BEHAVIOUR_ATTACK_BROADSIDE_LEFT:
7426 case BEHAVIOUR_ATTACK_BROADSIDE_RIGHT:
7427 case BEHAVIOUR_CLOSE_TO_BROADSIDE_RANGE:
7428 case BEHAVIOUR_CLOSE_WITH_TARGET:
7429 case BEHAVIOUR_ATTACK_SNIPER:
7430 case BEHAVIOUR_SCRIPTED_ATTACK_AI:
7431 return YES;
7432
7433 default:
7434 return NO;
7435 }
7436
7437 return 100 < behaviour && behaviour < 120;
7438}
7439
7440
7441// Exposed to shaders.
7442- (BOOL) hasHostileTarget
7443{
7444 Entity *t = [self primaryTarget];
7445 if (t == nil || ![t isShip])
7446 {
7447 return NO;
7448 }
7449 if ([self isMissile])
7450 {
7451 return YES; // missiles are always fired against a hostile target
7452 }
7453 if ((behaviour == BEHAVIOUR_AVOID_COLLISION)&&(previousCondition))
7454 {
7455 int old_behaviour = [previousCondition oo_intForKey:@"behaviour"];
7456 return IsBehaviourHostile(old_behaviour);
7457 }
7458 return IsBehaviourHostile(behaviour);
7459}
7460
7461
7462- (BOOL) isHostileTo:(Entity *)entity
7463{
7464 return ([self hasHostileTarget] && [self primaryTarget] == entity);
7465}
7466
7467- (GLfloat) weaponRange
7468{
7469 return weaponRange;
7470}
7471
7472
7473- (void) setWeaponRange: (GLfloat) value
7474{
7475 weaponRange = value;
7476}
7477
7478
7479- (void) setWeaponDataFromType: (OOWeaponType) weapon_type
7480{
7481 weaponRange = getWeaponRangeFromType(weapon_type);
7482 weapon_energy_use = [weapon_type weaponEnergyUse];
7483 weapon_recharge_rate = [weapon_type weaponRechargeRate];
7484 weapon_shot_temperature = [weapon_type weaponShotTemperature];
7485 weapon_damage = [weapon_type weaponDamage];
7486
7487 if (default_laser_color == nil)
7488 {
7489 OOColor *wcol = [weapon_type weaponColor];
7490 if (wcol != nil)
7491 {
7492 [self setLaserColor:wcol];
7493 }
7494 }
7495
7496}
7497
7498
7499- (float) energyRechargeRate
7500{
7501 return energy_recharge_rate;
7502}
7503
7504
7505- (void) setEnergyRechargeRate:(GLfloat)new
7506{
7507 energy_recharge_rate = new;
7508}
7509
7510
7511- (float) weaponRechargeRate
7512{
7513 return weapon_recharge_rate;
7514}
7515
7516
7517- (void) setWeaponRechargeRate:(float)value
7518{
7519 weapon_recharge_rate = value;
7520}
7521
7522
7523- (void) setWeaponEnergy:(float)value
7524{
7525 weapon_damage = value;
7526}
7527
7528
7529- (OOWeaponFacing) currentWeaponFacing
7530{
7531 return currentWeaponFacing;
7532}
7533
7534
7535- (GLfloat) scannerRange
7536{
7537 return scannerRange;
7538}
7539
7540
7541- (void) setScannerRange: (GLfloat) value
7542{
7543 scannerRange = value;
7544}
7545
7546
7547- (Vector) reference
7548{
7549 return reference;
7550}
7551
7552
7553- (void) setReference:(Vector) v
7554{
7555 reference = v;
7556}
7557
7558
7559- (BOOL) reportAIMessages
7560{
7561 return reportAIMessages;
7562}
7563
7564
7565- (void) setReportAIMessages:(BOOL) yn
7566{
7567 reportAIMessages = yn;
7568}
7569
7570
7571- (void) transitionToAegisNone
7572{
7573 if (!suppressAegisMessages && aegis_status != AEGIS_NONE)
7574 {
7575 Entity<OOStellarBody> *lastAegisLock = [self lastAegisLock];
7576 if (lastAegisLock != nil)
7577 {
7578 [self doScriptEvent:OOJSID("shipExitedPlanetaryVicinity") withArgument:lastAegisLock];
7579
7580 if (lastAegisLock == [UNIVERSE sun])
7581 {
7582 [shipAI message:@"AWAY_FROM_SUN"];
7583 }
7584 else
7585 {
7586 [shipAI message:@"AWAY_FROM_PLANET"];
7587 }
7588 }
7589
7590 if (aegis_status != AEGIS_CLOSE_TO_ANY_PLANET)
7591 {
7592 [shipAI message:@"AEGIS_NONE"];
7593 }
7594 }
7595 aegis_status = AEGIS_NONE;
7596}
7597
7598
7599static float SurfaceDistanceSqaredV(HPVector reference, Entity<OOStellarBody> *stellar)
7600{
7601 float centerDistance = HPmagnitude2(HPvector_subtract([stellar position], reference));
7602 float r = [stellar radius];
7603 /* 1.35: empirical value used to help determine proximity when non-nested
7604 planets are close to each other
7605 */
7606 return centerDistance - 1.35 * r * r;
7607}
7608
7609
7610static float SurfaceDistanceSqared(Entity *reference, Entity<OOStellarBody> *stellar)
7611{
7612 return SurfaceDistanceSqaredV([reference position], stellar);
7613}
7614
7615
7616NSComparisonResult ComparePlanetsBySurfaceDistance(id i1, id i2, void* context)
7617{
7618 HPVector p = [(ShipEntity*) context position];
7619 OOPlanetEntity* e1 = i1;
7620 OOPlanetEntity* e2 = i2;
7621
7622 float p1 = SurfaceDistanceSqaredV(p, e1);
7623 float p2 = SurfaceDistanceSqaredV(p, e2);
7624
7625 if (p1 < p2) return NSOrderedAscending;
7626 if (p1 > p2) return NSOrderedDescending;
7627
7628 return NSOrderedSame;
7629}
7630
7631
7632- (OOPlanetEntity *) findNearestPlanet
7633{
7634 /*
7635 Performance note: this method is called every frame by every ship, and
7636 has a significant profiler presence.
7637 -- Ahruman 2012-09-13
7638 */
7639 OOPlanetEntity *planet = nil, *bestPlanet = nil;
7640 float bestRange = INFINITY;
7641 HPVector myPosition = [self position];
7642
7643 // valgrind complains about this line here. Might be compiler/GNUstep bug?
7644 // should we go back to a traditional enumerator? - CIM
7645 // similar complaints about the other foreach() in this file
7646 foreach (planet, [UNIVERSE planets])
7647 {
7648 // Ignore miniature planets.
7649 if ([planet planetType] == STELLAR_TYPE_MINIATURE) continue;
7650
7651 float range = SurfaceDistanceSqaredV(myPosition, planet);
7652 if (range < bestRange)
7653 {
7654 bestPlanet = planet;
7655 bestRange = range;
7656 }
7657 }
7658
7659 return bestPlanet;
7660}
7661
7662
7663- (Entity<OOStellarBody> *) findNearestStellarBody
7664{
7665 Entity<OOStellarBody> *match = [self findNearestPlanet];
7666 OOSunEntity *sun = [UNIVERSE sun];
7667
7668 if (sun != nil)
7669 {
7670 if (match == nil ||
7671 SurfaceDistanceSqared(self, sun) < SurfaceDistanceSqared(self, match))
7672 {
7673 match = sun;
7674 }
7675 }
7676
7677 return match;
7678}
7679
7680
7681- (OOPlanetEntity *) findNearestPlanetExcludingMoons
7682{
7683 OOPlanetEntity *result = nil;
7684 OOPlanetEntity *planet = nil;
7685 NSArray *bodies = nil;
7686 NSArray *planets = nil;
7687 unsigned i;
7688
7689 bodies = [UNIVERSE planets];
7690 planets = [NSMutableArray arrayWithCapacity:[bodies count]];
7691
7692 for (i=0; i < [bodies count]; i++)
7693 {
7694 planet = [bodies objectAtIndex:i];
7695 if([planet planetType] == STELLAR_TYPE_NORMAL_PLANET)
7696 planets = [planets arrayByAddingObject:planet];
7697 }
7698
7699 if ([planets count] == 0) return nil;
7700
7701 planets = [planets sortedArrayUsingFunction:ComparePlanetsBySurfaceDistance context:self];
7702 result = [planets objectAtIndex:0];
7703
7704 return result;
7705}
7706
7707
7708- (OOAegisStatus) checkForAegis
7709{
7710 Entity<OOStellarBody> *nearest = [self findNearestStellarBody];
7711 BOOL sunGoneNova = [[UNIVERSE sun] goneNova];
7712
7713 if (nearest == nil)
7714 {
7715 if (aegis_status != AEGIS_NONE)
7716 {
7717 // Planet disappeared!
7718 [self transitionToAegisNone];
7719 }
7720 return AEGIS_NONE;
7721 }
7722 // check planet
7723 float cr = [nearest radius];
7724 float cr2 = cr * cr;
7725 OOAegisStatus result = AEGIS_NONE;
7726 float d2 = HPmagnitude2(HPvector_subtract([nearest position], [self position]));
7727 // not scannerRange: aegis shouldn't depend on that
7728 float sd2 = SCANNER_MAX_RANGE2 * 10.0f;
7729
7730 // check if nearing a surface
7731 unsigned wasNearPlanetSurface = isNearPlanetSurface; // isNearPlanetSurface is a bit flag, not an actual BOOL
7732 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
7733
7734
7735 if (EXPECT_NOT((wasNearPlanetSurface != isNearPlanetSurface) && !suppressAegisMessages))
7736 {
7737 if (isNearPlanetSurface)
7738 {
7739 [self doScriptEvent:OOJSID("shipApproachingPlanetSurface") withArgument:nearest];
7740 [shipAI reactToMessage:@"APPROACHING_SURFACE" context:@"flight update"];
7741 }
7742 else
7743 {
7744 [self doScriptEvent:OOJSID("shipLeavingPlanetSurface") withArgument:nearest];
7745 [shipAI reactToMessage:@"LEAVING_SURFACE" context:@"flight update"];
7746 }
7747 }
7748
7749 // being close to the station takes precedence over planets
7750 StationEntity *the_station = [UNIVERSE station];
7751 if (the_station)
7752 {
7753 sd2 = HPmagnitude2(HPvector_subtract([the_station position], [self position]));
7754 }
7755 // again, notional scanner range is intentional
7756 if (sd2 < SCANNER_MAX_RANGE2 * 4.0f) // double scanner range
7757 {
7758 result = AEGIS_IN_DOCKING_RANGE;
7759 }
7760 else if (EXPECT_NOT(isNearPlanetSurface || d2 < cr2 * 9.0f)) // to 3x radius of any planet/moon - or 500m of tiny ones,
7761 {
7763 if (EXPECT((OOPlanetEntity *)nearest == [UNIVERSE planet]))
7764 {
7766 }
7767 }
7768 // need to do this check separately from above case to avoid oddity where
7769 // main planet and small moon are at just the wrong distance. - CIM
7770 if (result != AEGIS_CLOSE_TO_MAIN_PLANET && result != AEGIS_IN_DOCKING_RANGE && !sunGoneNova)
7771 {
7772 // are we also close to the main planet?
7773 OOPlanetEntity *mainPlanet = [UNIVERSE planet];
7774 d2 = HPmagnitude2(HPvector_subtract([mainPlanet position], [self position]));
7775 cr2 = [mainPlanet radius];
7776 cr2 *= cr2;
7777 if (d2 < cr2 * 9.0f)
7778 {
7779 nearest = mainPlanet;
7781 }
7782 }
7783
7784
7785 /* Rewrote aegis stuff and tested it against redux.oxp that adds multiple planets and moons.
7786 Made sure AI scripts can differentiate between MAIN and NON-MAIN planets so they can decide
7787 if they can dock at the systemStation or just any station.
7788 Added sun detection so route2Patrol can turn before they heat up in the sun.
7789 -- Eric 2009-07-11
7790
7791 More rewriting of the aegis stuff, it's now a bit faster and works properly when moving
7792 from one secondary planet/moon vicinity to another one. -- Kaks 20120917
7793 */
7794 if (EXPECT(!suppressAegisMessages))
7795 {
7796 // script/AI messages on change in status
7797 if (EXPECT_NOT(aegis_status == AEGIS_IN_DOCKING_RANGE && result != aegis_status))
7798 {
7799 [self doScriptEvent:OOJSID("shipExitedStationAegis") withArgument:the_station];
7800 [shipAI message:@"AEGIS_LEAVING_DOCKING_RANGE"];
7801 }
7802
7803 if (EXPECT_NOT(result == AEGIS_IN_DOCKING_RANGE && aegis_status != result))
7804 {
7805 [self doScriptEvent:OOJSID("shipEnteredStationAegis") withArgument:the_station];
7806 [shipAI message:@"AEGIS_IN_DOCKING_RANGE"];
7807
7808 if([self lastAegisLock] == nil && !sunGoneNova) // With small main planets the station aegis can come before planet aegis
7809 {
7810 [self doScriptEvent:OOJSID("shipEnteredPlanetaryVicinity") withArgument:[UNIVERSE planet]];
7811 [self setLastAegisLock:[UNIVERSE planet]];
7812 }
7813 }
7814 else if (EXPECT_NOT(result == AEGIS_NONE && aegis_status != result))
7815 {
7816 if([self lastAegisLock] == nil && !sunGoneNova)
7817 {
7818 [self setLastAegisLock:[UNIVERSE planet]]; // in case of a first launch from a near-planet station.
7819 }
7820 [self transitionToAegisNone];
7821 }
7822 // approaching..
7823 else if (EXPECT_NOT((result == AEGIS_CLOSE_TO_ANY_PLANET || result == AEGIS_CLOSE_TO_MAIN_PLANET) && [self lastAegisLock] != nearest))
7824 {
7825 if(aegis_status != AEGIS_NONE && [self lastAegisLock] != nil) // we were close to another stellar body
7826 {
7827 [self doScriptEvent:OOJSID("shipExitedPlanetaryVicinity") withArgument:[self lastAegisLock]];
7828 [shipAI message:@"AWAY_FROM_PLANET"]; // fires for suns, planets and moons.
7829 }
7830 [self doScriptEvent:OOJSID("shipEnteredPlanetaryVicinity") withArgument:nearest];
7831 [self setLastAegisLock:nearest];
7832
7833 if (EXPECT_NOT([nearest isSun]))
7834 {
7835 [shipAI message:@"CLOSE_TO_SUN"];
7836 }
7837 else
7838 {
7839 [shipAI message:@"CLOSE_TO_PLANET"];
7840
7841 if (EXPECT(result == AEGIS_CLOSE_TO_MAIN_PLANET))
7842 {
7843 // 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
7844 //[shipAI message:@"AEGIS_CLOSE_TO_PLANET"]; // fires only for main planets, kept for compatibility with pre-1.72 AI plists.
7845 [shipAI message:@"AEGIS_CLOSE_TO_MAIN_PLANET"]; // fires only for main planet.
7846 }
7847 else if (EXPECT_NOT([nearest planetType] == STELLAR_TYPE_MOON))
7848 {
7849 [shipAI message:@"CLOSE_TO_MOON"];
7850 }
7851 else
7852 {
7853 [shipAI message:@"CLOSE_TO_SECONDARY_PLANET"];
7854 }
7855 }
7856 }
7857
7858
7859 }
7860 if (result == AEGIS_NONE)
7861 {
7862 [self setLastAegisLock:nil];
7863 }
7864
7865 aegis_status = result; // put this here
7866 return result;
7867}
7868
7869
7870- (void) forceAegisCheck
7871{
7872 _nextAegisCheck = -1.0f;
7873}
7874
7875
7876- (BOOL) withinStationAegis
7877{
7878 return aegis_status == AEGIS_IN_DOCKING_RANGE;
7879}
7880
7881
7882- (Entity<OOStellarBody> *) lastAegisLock
7883{
7884 Entity<OOStellarBody> *stellar = [_lastAegisLock weakRefUnderlyingObject];
7885 if (stellar == nil)
7886 {
7887 [_lastAegisLock release];
7888 _lastAegisLock = nil;
7889 }
7890
7891 return stellar;
7892}
7893
7894
7895- (void) setLastAegisLock:(Entity<OOStellarBody> *)lastAegisLock
7896{
7897 [_lastAegisLock release];
7898 _lastAegisLock = [lastAegisLock weakRetain];
7899}
7900
7901
7902- (OOSystemID) homeSystem
7903{
7904 return home_system;
7905}
7906
7907
7908- (OOSystemID) destinationSystem
7909{
7910 return destination_system;
7911}
7912
7913
7914- (void) setHomeSystem:(OOSystemID)s
7915{
7916 home_system = s;
7917}
7918
7919
7920- (void) setDestinationSystem:(OOSystemID)s
7921{
7922 destination_system = s;
7923}
7924
7925
7926- (void) setStatus:(OOEntityStatus) stat
7927{
7928 if ([self status] == stat) return;
7929 [super setStatus:stat];
7930 if (stat == STATUS_LAUNCHING)
7931 {
7932 launch_time = [UNIVERSE getTime];
7933 }
7934}
7935
7936- (void) setLaunchDelay:(double)delay
7937{
7938 launch_delay = delay;
7939}
7940
7941
7942- (NSArray *) crew
7943{
7944 return crew;
7945}
7946
7947
7948- (void) setCrew:(NSArray *)crewArray
7949{
7950 if ([self isExplicitlyUnpiloted])
7951 {
7952 //unpiloted ships cannot have crew
7953 // but may have crew before isExplicitlyUnpiloted set, so force *that* to clear too
7954 [crew autorelease];
7955 crew = nil;
7956 return;
7957 }
7958 //do not set to hulk here when crew is nil (or 0). Some things like missiles have no crew.
7959 [crew autorelease];
7960 crew = [crewArray copy];
7961}
7962
7963
7964- (void) setSingleCrewWithRole:(NSString *)crewRole
7965{
7966 if (![self isUnpiloted])
7967 {
7968 OOCharacter *crewMember = [OOCharacter randomCharacterWithRole:crewRole
7969 andOriginalSystem:[self homeSystem]];
7970 [self setCrew:[NSArray arrayWithObject:crewMember]];
7971 }
7972}
7973
7974
7975- (NSArray *) crewForScripting
7976{
7977 if (crew == nil)
7978 {
7979 return nil;
7980 }
7981 OOCharacter *crewMember = nil;
7982 NSMutableArray *result = [NSMutableArray arrayWithCapacity:4];
7983 foreach (crewMember, crew)
7984 {
7985 NSDictionary *crewDict = [crewMember infoForScripting];
7986 [result addObject:crewDict];
7987 }
7988 return result;
7989}
7990
7991
7992
7993- (void) setStateMachine:(NSString *)smName
7994{
7995 [self setAITo:smName];
7996}
7997
7998
7999- (void) setAI:(AI *)ai
8000{
8001 [ai retain];
8002 if (shipAI)
8003 {
8004 [shipAI clearAllData];
8005 [shipAI autorelease];
8006 }
8007 shipAI = ai;
8008}
8009
8010
8011- (AI *) getAI
8012{
8013 return shipAI;
8014}
8015
8016
8017- (BOOL) hasAutoAI
8018{
8019 return [[self shipInfoDictionary] oo_fuzzyBooleanForKey:@"auto_ai" defaultValue:YES];
8020}
8021
8022
8023- (BOOL) hasNewAI
8024{
8025 return [[[self getAI] name] isEqualToString:@"nullAI.plist"];
8026}
8027
8028
8029- (BOOL) hasAutoWeapons
8030{
8031 return [[self shipInfoDictionary] oo_fuzzyBooleanForKey:@"auto_weapons" defaultValue:NO];
8032}
8033
8034
8035- (void) setShipScript:(NSString *)script_name
8036{
8037 NSMutableDictionary *properties = nil;
8038 NSArray *actions = nil;
8039
8040 properties = [NSMutableDictionary dictionary];
8041 [properties setObject:self forKey:@"ship"];
8042
8043 [script autorelease];
8044 script = [OOScript jsScriptFromFileNamed:script_name properties:properties];
8045
8046 if (script == nil)
8047 {
8048 actions = [shipinfoDictionary oo_arrayForKey:@"launch_actions"];
8049 if (actions)
8050 {
8051 OOStandardsDeprecated([NSString stringWithFormat:@"The launch_actions ship key is deprecated on %@.",[self displayName]]);
8052 if (!OOEnforceStandards())
8053 {
8054 [properties setObject:actions forKey:@"legacy_launchActions"];
8055 }
8056 }
8057
8058 actions = [shipinfoDictionary oo_arrayForKey:@"script_actions"];
8059 if (actions)
8060 {
8061 OOStandardsDeprecated([NSString stringWithFormat:@"The script_actions ship key is deprecated on %@.",[self displayName]]);
8062 if (!OOEnforceStandards())
8063 {
8064 [properties setObject:actions forKey:@"legacy_scriptActions"];
8065 }
8066 }
8067
8068 actions = [shipinfoDictionary oo_arrayForKey:@"death_actions"];
8069 if (actions)
8070 {
8071 OOStandardsDeprecated([NSString stringWithFormat:@"The death_actions ship key is deprecated on %@.",[self displayName]]);
8072 if (!OOEnforceStandards())
8073 {
8074 [properties setObject:actions forKey:@"legacy_deathActions"];
8075 }
8076 }
8077
8078 actions = [shipinfoDictionary oo_arrayForKey:@"setup_actions"];
8079 if (actions)
8080 {
8081 OOStandardsDeprecated([NSString stringWithFormat:@"The setup_actions ship key is deprecated on %@.",[self displayName]]);
8082 if (!OOEnforceStandards())
8083 {
8084 [properties setObject:actions forKey:@"legacy_setupActions"];
8085 }
8086 }
8087
8088 script = [OOScript jsScriptFromFileNamed:@"oolite-default-ship-script.js"
8089 properties:properties];
8090 }
8091 [script retain];
8092}
8093
8094
8095- (double)frustration
8096{
8097 return frustration;
8098}
8099
8100
8101- (OOFuelQuantity) fuel
8102{
8103 return fuel;
8104}
8105
8106
8107- (void) setFuel:(OOFuelQuantity) amount
8108{
8109 if (amount > [self fuelCapacity]) amount = [self fuelCapacity];
8110
8111 fuel = amount;
8112}
8113
8114
8115- (OOFuelQuantity) fuelCapacity
8116{
8117 // FIXME: shipdata.plist can allow greater fuel quantities (without extending hyperspace range). Need some consistency here.
8118 return PLAYER_MAX_FUEL;
8119}
8120
8121
8122- (GLfloat) fuelChargeRate
8123{
8124 GLfloat rate = 1.0; // Standard (& strict play) charge rate.
8125
8126#if MASS_DEPENDENT_FUEL_PRICES
8127
8128 if (EXPECT(PLAYER != nil && mass> 0 && mass != [PLAYER baseMass]))
8129 {
8130 rate = calcFuelChargeRate(mass);
8131 }
8132
8133 OOLog(@"fuelPrices", @"\"%@\" fuel charge rate: %.2f (mass ratio: %.2f/%.2f)", [self shipDataKey], rate, mass, [PLAYER baseMass]);
8134#endif
8135
8136 return rate;
8137}
8138
8139
8140- (void) applySticks:(double)delta_t
8141{
8142
8143 double rate1 = 2.0 * delta_t; //roll
8144 double rate2 = 4.0 * delta_t; //pitch
8145 double rate3 = 4.0 * delta_t; //yaw
8146
8147 if (((stick_roll > 0.0)&&(flightRoll < 0.0))||((stick_roll < 0.0)&&(flightRoll > 0.0)))
8148 rate1 *= 4.0; // much faster correction
8149 if (((stick_pitch > 0.0)&&(flightPitch < 0.0))||((stick_pitch < 0.0)&&(flightPitch > 0.0)))
8150 rate2 *= 4.0; // much faster correction
8151 if (((stick_yaw > 0.0)&&(flightYaw < 0.0))||((stick_yaw < 0.0)&&(flightYaw > 0.0)))
8152 rate3 *= 4.0; // much faster correction
8153
8154 if (accuracy >= COMBAT_AI_TRACKS_CLOSER)
8155 {
8156 if (stick_roll == 0.0)
8157 rate1 *= 2.0; // faster correction
8158 if (stick_pitch == 0.0)
8159 rate2 *= 2.0; // faster correction
8160 if (stick_yaw == 0.0)
8161 rate3 *= 2.0; // faster correction
8162 }
8163
8164 // apply stick movement limits
8165 if (flightRoll < stick_roll - rate1)
8166 {
8167 flightRoll = flightRoll + rate1;
8168 }
8169 else if (flightRoll > stick_roll + rate1)
8170 {
8171 flightRoll = flightRoll - rate1;
8172 }
8173 else
8174 {
8175 flightRoll = stick_roll;
8176 }
8177
8178 if (flightPitch < stick_pitch - rate2)
8179 {
8180 flightPitch = flightPitch + rate2;
8181 }
8182 else if (flightPitch > stick_pitch + rate2)
8183 {
8184 flightPitch = flightPitch - rate2;
8185 }
8186 else
8187 {
8188 flightPitch = stick_pitch;
8189 }
8190
8191 if (flightYaw < stick_yaw - rate3)
8192 {
8193 flightYaw = flightYaw + rate3;
8194 }
8195 else if (flightYaw > stick_yaw + rate3)
8196 {
8197 flightYaw = flightYaw - rate3;
8198 }
8199 else
8200 {
8201 flightYaw = stick_yaw;
8202 }
8203
8204}
8205
8206
8207- (void) setRoll:(double) amount
8208{
8209 flightRoll = amount * M_PI / 2.0;
8210}
8211
8212
8213- (void) setRawRoll:(double) amount
8214{
8215 flightRoll = amount;
8216}
8217
8218
8219- (void) setPitch:(double) amount
8220{
8221 flightPitch = amount * M_PI / 2.0;
8222}
8223
8224
8225- (void) setYaw:(double) amount
8226{
8227 flightYaw = amount * M_PI / 2.0;
8228}
8229
8230
8231- (void) setThrust:(double) amount
8232{
8233 thrust = amount;
8234}
8235
8236
8237- (void) setThrustForDemo:(float) factor
8238{
8239 flightSpeed = factor * maxFlightSpeed;
8240}
8241
8242
8243- (void) setBounty:(OOCreditsQuantity) amount
8244{
8245 [self setBounty:amount withReason:kOOLegalStatusReasonUnknown];
8246}
8247
8248
8249- (void) setBounty:(OOCreditsQuantity) amount withReason:(OOLegalStatusReason)reason
8250{
8251 if ([self isSubEntity])
8252 {
8253 [[self parentEntity] setBounty:amount withReason:reason];
8254 }
8255 else
8256 {
8257 if ((scanClass == CLASS_THARGOID || scanClass == CLASS_STATION) && reason != kOOLegalStatusReasonSetup && reason != kOOLegalStatusReasonByScript)
8258 {
8259 return; // no standard bounties for Thargoids / Stations
8260 }
8261 if (scanClass == CLASS_POLICE && amount != 0)
8262 {
8263 return; // police never have bounties
8264 }
8265 NSString* nReason = OOStringFromLegalStatusReason(reason);
8266 [self setBounty:amount withReasonAsString:nReason];
8267 }
8268}
8269
8270- (void) setBounty:(OOCreditsQuantity) amount withReasonAsString:(NSString*)reason
8271{
8272 if ([self isSubEntity])
8273 {
8274 [[self parentEntity] setBounty:amount withReasonAsString:reason];
8275 }
8276 else
8277 {
8278 JSContext *context = OOJSAcquireContext();
8279
8280 jsval amountVal = JSVAL_VOID;
8281 JS_NewNumberValue(context, (int)amount-(int)bounty, &amountVal);
8282
8283 bounty = amount; // can't set the new bounty until the size of the change is known
8284
8285 jsval reasonVal = OOJSValueFromNativeObject(context,reason);
8286
8287 ShipScriptEvent(context, self, "shipBountyChanged", amountVal, reasonVal);
8288
8289 OOJSRelinquishContext(context);
8290
8291 }
8292}
8293
8294
8295
8296- (OOCreditsQuantity) bounty
8297{
8298 if ([self isSubEntity])
8299 {
8300 return [[self parentEntity] bounty];
8301 }
8302 else
8303 {
8304 return bounty;
8305 }
8306}
8307
8308
8309- (int) legalStatus
8310{
8311 if (scanClass == CLASS_THARGOID)
8312 return 5 * collision_radius;
8313 if (scanClass == CLASS_ROCK)
8314 return 0;
8315 return (int)[self bounty];
8316}
8317
8318
8319- (void) setCommodity:(OOCommodityType)co_type andAmount:(OOCargoQuantity)co_amount
8320{
8321 if (co_type != nil)
8322 {
8323 /* The tmp variable is needed as scoopUp can cause the method
8324 * to be passed a reference to self.commodity_type, so DESTROY
8325 * then copying the parameter segfaults */
8326 NSString *tmp = [co_type copy];
8327 DESTROY(commodity_type);
8328 commodity_type = tmp;
8329 commodity_amount = co_amount;
8330 }
8331}
8332
8333
8334- (void) setCommodityForPod:(OOCommodityType)co_type andAmount:(OOCargoQuantity)co_amount
8335{
8336 // can be nil for pods
8337 if (co_type == nil)
8338 {
8339 DESTROY(commodity_type);
8340 commodity_amount = 0;
8341 return;
8342 }
8343 // pod content should never be greater than 1 ton or this will give cargo counting problems elsewhere in the code.
8344 // so do first a mass check for cargo added by script/plist.
8345 OOMassUnit unit = [[UNIVERSE commodityMarket] massUnitForGood:co_type];
8346 if (unit == UNITS_TONS && co_amount > 1) co_amount = 1;
8347 else if (unit == UNITS_KILOGRAMS && co_amount > 1000) co_amount = 1000;
8348 else if (unit == UNITS_GRAMS && co_amount > 1000000) co_amount = 1000000;
8349 [self setCommodity:co_type andAmount:co_amount];
8350}
8351
8352
8353- (OOCommodityType) commodityType
8354{
8355 return commodity_type;
8356}
8357
8358
8359- (OOCargoQuantity) commodityAmount
8360{
8361 return commodity_amount;
8362}
8363
8364
8365- (OOCargoQuantity) maxAvailableCargoSpace
8366{
8367 return max_cargo - equipment_weight;
8368}
8369
8370
8371- (void) setMaxAvailableCargoSpace:(OOCargoQuantity)new
8372{
8373 max_cargo = new + equipment_weight;
8374}
8375
8376
8377- (OOCargoQuantity) availableCargoSpace
8378{
8379 // OOCargoQuantity is unsigned, we need to check for underflows.
8380 if (EXPECT_NOT([self cargoQuantityOnBoard] + equipment_weight >= max_cargo)) return 0;
8381 return [self maxAvailableCargoSpace] - [self cargoQuantityOnBoard];
8382}
8383
8384
8385- (OOCargoQuantity) cargoQuantityOnBoard
8386{
8387 NSUInteger result = [[self cargo] count];
8388 NSAssert(result < UINT32_MAX, @"Cargo quantity out of bounds.");
8389 return (OOCargoQuantity)result;
8390}
8391
8392
8393
8394- (OOCargoType) cargoType
8395{
8396 return cargo_type;
8397}
8398
8399
8400/* Note: this array probably contains some template cargo pods. Do not
8401 * pass it to Javascript without reifying them first. */
8402- (NSMutableArray*) cargo
8403{
8404 return cargo;
8405}
8406
8407
8408- (NSArray *) cargoListForScripting
8409{
8410 NSMutableArray *list = [NSMutableArray array];
8411
8412 OOCommodityType good = nil;
8413 NSArray *goods = [[UNIVERSE commodityMarket] goods];
8414 NSUInteger i, j, commodityCount = [goods count];
8415 OOCargoQuantity quantityInHold[commodityCount];
8416
8417 for (i = 0; i < commodityCount; i++)
8418 {
8419 quantityInHold[i] = 0;
8420 }
8421 for (i = 0; i < [cargo count]; i++)
8422 {
8423 ShipEntity *container = [cargo objectAtIndex:i];
8424 j = [goods indexOfObject:[container commodityType]];
8425 quantityInHold[j] += [container commodityAmount];
8426 }
8427
8428 for (i = 0; i < commodityCount; i++)
8429 {
8430 if (quantityInHold[i] > 0)
8431 {
8432 NSMutableDictionary *commodity = [NSMutableDictionary dictionaryWithCapacity:4];
8433 good = [goods objectAtIndex:i];
8434 // commodity, quantity - keep consistency between .manifest and .contracts
8435 [commodity setObject:good forKey:@"commodity"];
8436 [commodity setObject:[NSNumber numberWithUnsignedInt:quantityInHold[i]] forKey:@"quantity"];
8437 [commodity setObject:[[UNIVERSE commodityMarket] nameForGood:good] forKey:@"displayName"];
8438 [commodity setObject:DisplayStringForMassUnitForCommodity(good) forKey:@"unit"];
8439 [list addObject:commodity];
8440 }
8441 }
8442
8443 return [[list copy] autorelease]; // return an immutable copy
8444}
8445
8446- (void) setCargo:(NSArray *) some_cargo
8447{
8448 [cargo removeAllObjects];
8449 [cargo addObjectsFromArray:some_cargo];
8450}
8451
8452
8453- (BOOL) addCargo:(NSArray *) some_cargo
8454{
8455 if ([cargo count] + [some_cargo count] > [self maxAvailableCargoSpace])
8456 {
8457 return NO;
8458 }
8459 else
8460 {
8461 [cargo addObjectsFromArray:some_cargo];
8462 return YES;
8463 }
8464}
8465
8466
8467- (BOOL) removeCargo:(OOCommodityType)commodity amount:(OOCargoQuantity) amount
8468{
8469 OOCargoQuantity found = 0;
8470 ShipEntity *pod = nil;
8471 foreach (pod, cargo)
8472 {
8473 if ([[pod commodityType] isEqualToString:commodity])
8474 {
8475 found++;
8476 }
8477 }
8478 if (found < amount)
8479 {
8480 // don't remove any if there aren't enough to remove the full amount
8481 return NO;
8482 }
8483
8484 NSUInteger i = [cargo count] - 1;
8485 // iterate downwards to be safe removing during iteration
8486 while (amount > 0)
8487 {
8488 if ([[[cargo objectAtIndex:i] commodityType] isEqualToString:commodity])
8489 {
8490 amount--;
8491 [cargo removeObjectAtIndex:i];
8492 }
8493 // check above means array index can't underflow here
8494 i--;
8495 }
8496
8497 return YES;
8498}
8499
8500
8501
8502- (BOOL) showScoopMessage
8503{
8504 return hasScoopMessage;
8505}
8506
8507
8508- (OOCargoFlag) cargoFlag
8509{
8510 return cargo_flag;
8511}
8512
8513
8514- (void) setCargoFlag:(OOCargoFlag) flag
8515{
8516 if (cargo_flag != flag)
8517 {
8518 cargo_flag = flag;
8519 NSArray *newCargo = nil;
8520 unsigned num = 0;
8521 if (likely_cargo > 0)
8522 {
8523 num = likely_cargo * (0.5+randf());
8524 if (num > [self maxAvailableCargoSpace])
8525 {
8526 num = [self maxAvailableCargoSpace];
8527 }
8528 }
8529 else
8530 {
8531 num = [self maxAvailableCargoSpace];
8532 }
8533 if (num > 200)
8534 {
8535 num = 200;
8536 /* no core NPC ship carries this much when generated (the
8537 * Anaconda could, but doesn't): let's not waste time generating
8538 * thousands of pods - even if they are semi-virtual - for some
8539 * massive OXP ship */
8540 }
8541 if (num > 0)
8542 {
8543 switch (cargo_flag)
8544 {
8546 newCargo = [UNIVERSE getContainersOfCommodity:[shipinfoDictionary oo_stringForKey:@"cargo_carried"] :num];
8547 break;
8549 newCargo = [UNIVERSE getContainersOfGoods:num scarce:NO legal:YES];
8550 break;
8552 newCargo = [UNIVERSE getContainersOfGoods:num scarce:YES legal:YES];
8553 break;
8555 newCargo = [UNIVERSE getContainersOfCommodity:@"Narcotics" :num];
8556 break;
8558 newCargo = [UNIVERSE getContainersOfGoods:num scarce:YES legal:NO];
8559 break;
8560 case CARGO_FLAG_PIRATE:
8561 newCargo = [UNIVERSE getContainersOfGoods:(Ranrot() % (1+num/2)) scarce:YES legal:NO];
8562 break;
8564 // TODO: allow passengers to survive
8565 case CARGO_FLAG_NONE:
8566 default:
8567 break;
8568 }
8569 }
8570 [self setCargo:newCargo];
8571 }
8572}
8573
8574
8575- (void) setSpeed:(double) amount
8576{
8577 flightSpeed = amount;
8578}
8579
8580
8581- (void) setDesiredSpeed:(double) amount
8582{
8583 desired_speed = amount;
8584}
8585
8586
8587- (double) desiredSpeed
8588{
8589 return desired_speed;
8590}
8591
8592
8593- (double) desiredRange
8594{
8595 return desired_range;
8596}
8597
8598
8599- (void) setDesiredRange:(double) amount
8600{
8601 desired_range = amount;
8602}
8603
8604
8605- (double) cruiseSpeed
8606{
8607 return cruiseSpeed;
8608}
8609
8610
8611- (void) increase_flight_speed:(double) delta
8612{
8613 double factor = 1.0;
8614 if (desired_speed > maxFlightSpeed && [self hasFuelInjection] && fuel > MIN_FUEL) factor = [self afterburnerFactor];
8615
8616 if (flightSpeed < maxFlightSpeed * factor)
8617 flightSpeed += delta * factor;
8618 else
8619 flightSpeed = maxFlightSpeed * factor;
8620}
8621
8622
8623- (void) decrease_flight_speed:(double) delta
8624{
8625 double factor = 1.0;
8626 if (flightSpeed > maxFlightSpeed)
8627 {
8628 factor = MIN_HYPERSPEED_FACTOR;
8629 }
8630
8631 if (flightSpeed > factor * delta)
8632 {
8633 flightSpeed -= factor * delta;
8634 }
8635 else
8636 {
8637 flightSpeed = 0;
8638 }
8639}
8640
8641
8642- (void) increase_flight_roll:(double) delta
8643{
8644 flightRoll += delta;
8645 if (flightRoll > max_flight_roll)
8646 flightRoll = max_flight_roll;
8647 else if (flightRoll < -max_flight_roll)
8648 flightRoll = -max_flight_roll;
8649}
8650
8651
8652- (void) decrease_flight_roll:(double) delta
8653{
8654 flightRoll -= delta;
8655 if (flightRoll > max_flight_roll)
8656 flightRoll = max_flight_roll;
8657 else if (flightRoll < -max_flight_roll)
8658 flightRoll = -max_flight_roll;
8659}
8660
8661
8662- (void) increase_flight_pitch:(double) delta
8663{
8664 flightPitch += delta;
8665 if (flightPitch > max_flight_pitch)
8666 flightPitch = max_flight_pitch;
8667 else if (flightPitch < -max_flight_pitch)
8668 flightPitch = -max_flight_pitch;
8669}
8670
8671
8672- (void) decrease_flight_pitch:(double) delta
8673{
8674 flightPitch -= delta;
8675 if (flightPitch > max_flight_pitch)
8676 flightPitch = max_flight_pitch;
8677 else if (flightPitch < -max_flight_pitch)
8678 flightPitch = -max_flight_pitch;
8679}
8680
8681
8682- (void) increase_flight_yaw:(double) delta
8683{
8684 flightYaw += delta;
8685 if (flightYaw > max_flight_yaw)
8686 flightYaw = max_flight_yaw;
8687 else if (flightYaw < -max_flight_yaw)
8688 flightYaw = -max_flight_yaw;
8689}
8690
8691
8692- (void) decrease_flight_yaw:(double) delta
8693{
8694 flightYaw -= delta;
8695 if (flightYaw > max_flight_yaw)
8696 flightYaw = max_flight_yaw;
8697 else if (flightYaw < -max_flight_yaw)
8698 flightYaw = -max_flight_yaw;
8699}
8700
8701
8702- (GLfloat) flightRoll
8703{
8704 return flightRoll;
8705}
8706
8707
8708- (GLfloat) flightPitch
8709{
8710 return flightPitch;
8711}
8712
8713
8714- (GLfloat) flightYaw
8715{
8716 return flightYaw;
8717}
8718
8719
8720- (GLfloat) flightSpeed
8721{
8722 return flightSpeed;
8723}
8724
8725
8726- (GLfloat) maxFlightPitch
8727{
8728 return max_flight_pitch;
8729}
8730
8731
8732- (GLfloat) maxFlightSpeed
8733{
8734 return maxFlightSpeed;
8735}
8736
8737
8738- (GLfloat) maxFlightRoll
8739{
8740 return max_flight_roll;
8741}
8742
8743
8744- (GLfloat) maxFlightYaw
8745{
8746 return max_flight_yaw;
8747}
8748
8749
8750- (void) setMaxFlightPitch:(GLfloat)new
8751{
8752 max_flight_pitch = new;
8753}
8754
8755
8756- (void) setMaxFlightSpeed:(GLfloat)new
8757{
8758 maxFlightSpeed = new;
8759}
8760
8761
8762- (void) setMaxFlightRoll:(GLfloat)new
8763{
8764 max_flight_roll = new;
8765}
8766
8767
8768- (void) setMaxFlightYaw:(GLfloat)new
8769{
8770 max_flight_yaw = new;
8771}
8772
8773
8774- (GLfloat) speedFactor
8775{
8776 if (maxFlightSpeed <= 0.0) return 0.0;
8777 return flightSpeed / maxFlightSpeed;
8778}
8779
8780
8781- (GLfloat) temperature
8782{
8783 return ship_temperature;
8784}
8785
8786
8787- (void) setTemperature:(GLfloat) value
8788{
8789 ship_temperature = value;
8790}
8791
8792
8793- (float) randomEjectaTemperature
8794{
8795 return [self randomEjectaTemperatureWithMaxFactor:0.99f];
8796}
8797
8798
8799- (float) randomEjectaTemperatureWithMaxFactor:(float)factor
8800{
8801 const float kRange = 0.02f;
8802 factor -= kRange;
8803
8804 float parentTemp = [self temperature];
8805 float adjusted = parentTemp * (bellf(5) * (kRange * 2.0f) - kRange + factor);
8806 if (adjusted > SHIP_MAX_CABIN_TEMP)
8807 {
8808 adjusted = SHIP_MAX_CABIN_TEMP;
8809 }
8810
8811 // Interpolate so that result == parentTemp when parentTemp is SHIP_MIN_CABIN_TEMP
8812 float interp = OOClamp_0_1_f((parentTemp - SHIP_MIN_CABIN_TEMP) / (SHIP_MAX_CABIN_TEMP - SHIP_MIN_CABIN_TEMP));
8813
8814 return OOLerp(SHIP_MIN_CABIN_TEMP, adjusted, interp);
8815}
8816
8817
8818- (GLfloat) heatInsulation
8819{
8820 return _heatInsulation;
8821}
8822
8823
8824- (void) setHeatInsulation:(GLfloat) value
8825{
8826 _heatInsulation = value;
8827}
8828
8829
8830- (int) damage
8831{
8832 return (int)(100 - (100 * energy / maxEnergy));
8833}
8834
8835
8836- (void) dealEnergyDamage:(GLfloat) baseDamage atRange:(GLfloat) range withBias:(GLfloat) velocityBias
8837{
8838 // this is limited to the player's scanner range
8839 GLfloat maxRange = fmin(range * sqrt(baseDamage), SCANNER_MAX_RANGE);
8840
8841 OOLog(@"missile.damage.calc", @"Range: %f | Damage: %f | MaxRange: %f",range,baseDamage,maxRange);
8842
8843 NSArray *targets = [UNIVERSE entitiesWithinRange:maxRange ofEntity:self];
8844 if ([targets count] > 0)
8845 {
8846 unsigned i;
8847 for (i = 0; i < [targets count]; i++)
8848 {
8849 Entity *e2 = [targets objectAtIndex:i];
8850 Vector p2 = [self vectorTo:e2];
8851 double ecr = [e2 collisionRadius];
8852 double d = (magnitude(p2) - ecr) / range;
8853 // base damage within defined range, inverse-square falloff outside
8854 double localDamage = baseDamage;
8855 OOLog(@"missile.damage.calc", @"Base damage: %f",baseDamage);
8856 if (velocityBias > 0)
8857 {
8858 Vector v2 = vector_subtract([self velocity], [e2 velocity]);
8859 double vSign = dot_product(vector_normal([self velocity]), vector_normal(p2));
8860 // vSign should always be positive for the missile's actual target
8861 // but might be negative for other nearby ships which are
8862 // actually moving further away from the missile
8863// double vMag = vSign > 0.0 ? magnitude(v2) : -magnitude(v2);
8864 double vMag = vSign * magnitude(v2);
8865 if (vMag > 1000.0) {
8866 vMag = 1000.0;
8867// cap effective closing speed to 1.0LM or injector-collisions can still do
8868// ridiculous damage
8869 }
8870
8871 localDamage += vMag * velocityBias;
8872 OOLog(@"missile.damage.calc",@"Velocity magnitude + sign: %f , %f",magnitude(v2),vSign);
8873 OOLog(@"missile.damage.calc",@"Velocity magnitude factor: %f",vMag);
8874 OOLog(@"missile.damage.calc",@"Velocity corrected damage: %f",localDamage);
8875 }
8876 double damage = (d > 1) ? localDamage / (d * d) : localDamage;
8877 OOLog(@"missile.damage.calc",@"%f at range %f (d=%f)",damage,magnitude(p2)-ecr,d);
8878 if (damage > 0.0)
8879 {
8880 if ([self owner])
8881 {
8882 [e2 takeEnergyDamage:damage from:self becauseOf:[self owner] weaponIdentifier:[self primaryRole]];
8883 }
8884 else
8885 {
8886 [e2 takeEnergyDamage:damage from:self becauseOf:self weaponIdentifier:[self primaryRole]];
8887 }
8888 }
8889 }
8890 }
8891
8892 /* the actual damage can't go more than S_M_R, so cap the range
8893 * for exploding purposes so that the visual appearance isn't
8894 * larger than that */
8895 if (range > SCANNER_MAX_RANGE / 4.0)
8896 {
8897 range = SCANNER_MAX_RANGE / 4.0;
8898 }
8899 // and a visual sign of the explosion
8900 // "fireball" explosion effect
8901 NSDictionary *explosion = [UNIVERSE explosionSetting:@"oolite-default-ship-explosion"];
8902 [UNIVERSE addEntity:[OOExplosionCloudEntity explosionCloudFromEntity:self withSize:range*3.0 andSettings:explosion]];
8903
8904}
8905
8906
8907// dealEnergyDamage preferred
8908// Exposed to AI
8909- (void) dealEnergyDamageWithinDesiredRange
8910{
8911 OOStandardsDeprecated([NSString stringWithFormat:@"dealEnergyDamageWithinDesiredRange is deprecated for %@",self]);
8912 // not over scannerRange
8913 NSArray* targets = [UNIVERSE entitiesWithinRange:(desired_range < SCANNER_MAX_RANGE ? desired_range : SCANNER_MAX_RANGE) ofEntity:self];
8914 if ([targets count] > 0)
8915 {
8916 unsigned i;
8917 for (i = 0; i < [targets count]; i++)
8918 {
8919 Entity *e2 = [targets objectAtIndex:i];
8920 Vector p2 = [self vectorTo:e2];
8921 double ecr = [e2 collisionRadius];
8922 double d = (magnitude(p2) - ecr) * 2.6; // 2.6 is a correction constant to stay in limits of the old code.
8923 double damage = (d > 0) ? weapon_damage * desired_range / (d * d) : weapon_damage;
8924 [e2 takeEnergyDamage:damage from:self becauseOf:[self owner] weaponIdentifier:[self primaryRole]];
8925 }
8926 }
8927}
8928
8929
8930- (void) dealMomentumWithinDesiredRange:(double)amount
8931{
8932 NSArray* targets = [UNIVERSE entitiesWithinRange:desired_range ofEntity:self];
8933 if ([targets count] > 0)
8934 {
8935 unsigned i;
8936 for (i = 0; i < [targets count]; i++)
8937 {
8938 ShipEntity *e2 = (ShipEntity*)[targets objectAtIndex:i];
8939 if ([e2 isShip] && [e2 isInSpace])
8940 {
8941 Vector p2 = [self vectorTo:e2];
8942 double ecr = [e2 collisionRadius];
8943 double d2 = magnitude2(p2) - ecr * ecr;
8944 // limit momentum transfer to relatively sensible levels
8945 if (d2 < 0.1)
8946 {
8947 d2 = 0.1;
8948 }
8949 double moment = amount*desired_range/d2;
8950 [e2 addImpactMoment:vector_normal(p2) fraction:moment];
8951 }
8952 }
8953 }
8954}
8955
8956
8957- (BOOL) isHulk
8958{
8959 return isHulk;
8960}
8961
8962
8963- (void) setHulk:(BOOL)isNowHulk
8964{
8965 if (![self isSubEntity])
8966 {
8967 isHulk = isNowHulk;
8968 }
8969}
8970
8971
8972- (void) noteTakingDamage:(double)amount from:(Entity *)entity type:(OOShipDamageType)type
8973{
8974 if (amount < 0 || (amount == 0 && [[UNIVERSE gameController] isGamePaused])) return;
8975
8976 JSContext *context = OOJSAcquireContext();
8977
8978 jsval amountVal = JSVAL_VOID;
8979 JS_NewNumberValue(context, amount, &amountVal);
8980 jsval entityVal = OOJSValueFromNativeObject(context, entity);
8981 jsval typeVal = OOJSValueFromShipDamageType(context, type);
8982
8983 ShipScriptEvent(context, self, "shipTakingDamage", amountVal, entityVal, typeVal);
8984 OOJSRelinquishContext(context);
8985
8986 if ([entity isShip]) {
8987// ShipEntity* attacker = (ShipEntity *)entity;
8988 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)
8989 {
8990 if (behaviour == BEHAVIOUR_FLEE_TARGET)
8991 {
8992// jink should be sufficient to avoid being hit most of the time
8993// if not, this will make a sharp turn and then select a new jink position
8994 behaviour = BEHAVIOUR_FLEE_EVASIVE_ACTION;
8995 }
8996 else
8997 {
8998 behaviour = BEHAVIOUR_EVASIVE_ACTION;
8999 }
9000 frustration = 0.0;
9001 }
9002 }
9003
9004}
9005
9006
9007- (void) noteKilledBy:(Entity *)whom damageType:(OOShipDamageType)type
9008{
9009 if ([self status] == STATUS_DEAD) return;
9010
9011 [PLAYER setScriptTarget:self];
9012
9013 JSContext *context = OOJSAcquireContext();
9014
9015 jsval whomVal = OOJSValueFromNativeObject(context, whom);
9016 jsval typeVal = OOJSValueFromShipDamageType(context, type);
9017 OOEntityStatus originalStatus = [self status];
9018 [self setStatus:STATUS_DEAD];
9019
9020 ShipScriptEvent(context, self, "shipDied", whomVal, typeVal);
9021 if ([whom isShip])
9022 {
9023 jsval selfVal = OOJSValueFromNativeObject(context, self);
9024 ShipScriptEvent(context, (ShipEntity *)whom, "shipKilledOther", selfVal, typeVal);
9025 }
9026
9027 [self setStatus:originalStatus];
9028 OOJSRelinquishContext(context);
9029}
9030
9031
9032- (void) getDestroyedBy:(Entity *)whom damageType:(OOShipDamageType)type
9033{
9034 [self noteKilledBy:whom damageType:type];
9035 [self abortDocking];
9036 [self becomeExplosion];
9037}
9038
9039
9040- (void) rescaleBy:(GLfloat)factor
9041{
9042 [self rescaleBy:factor writeToCache:YES];
9043}
9044
9045
9046- (void) rescaleBy:(GLfloat)factor writeToCache:(BOOL)writeToCache
9047{
9048 _scaleFactor *= factor;
9049 OOMesh *mesh = nil;
9050
9051 NSDictionary *shipDict = [self shipInfoDictionary];
9052 NSString *modelName = [shipDict oo_stringForKey:@"model"];
9053 if (modelName != nil)
9054 {
9055 mesh = [OOMesh meshWithName:modelName
9056 cacheKey:[NSString stringWithFormat:@"%@-%.3f",_shipKey,_scaleFactor]
9057 materialDictionary:[shipDict oo_dictionaryForKey:@"materials"]
9058 shadersDictionary:[shipDict oo_dictionaryForKey:@"shaders"]
9059 smooth:[shipDict oo_boolForKey:@"smooth" defaultValue:NO]
9060 shaderMacros:OODefaultShipShaderMacros()
9062 scaleFactor:factor
9063 cacheWriteable:writeToCache];
9064
9065 if (mesh == nil) return;
9066 [self setMesh:mesh];
9067 }
9068
9069 // rescale subentities
9071 foreach (se, [self subEntities])
9072 {
9073 [se setPosition:HPvector_multiply_scalar([se position], factor)];
9074 [se rescaleBy:factor writeToCache:writeToCache];
9075 }
9076
9077 // rescale mass
9078 mass *= factor * factor * factor;
9079}
9080
9081
9082- (void) releaseCargoPodsDebris
9083{
9084 HPVector xposition = position;
9085 NSUInteger i;
9086 Vector v;
9087 Quaternion q;
9088 int speed_low = 200;
9089
9090 NSArray *jetsam = nil; // this will contain the stuff to get thrown out
9091 unsigned cargo_chance = 70;
9092 jetsam = [NSArray arrayWithArray:cargo]; // what the ship is carrying
9093 [cargo removeAllObjects]; // dispense with it!
9094 unsigned limit = 15;
9095 // Throw out cargo
9096 NSUInteger n_jetsam = [jetsam count];
9097
9098 for (i = 0; i < n_jetsam; i++)
9099 {
9100 if (Ranrot() % 100 < cargo_chance) // chance of any given piece of cargo surviving decompression
9101 {
9102 // a higher chance of getting at least a couple of bits of cargo out
9103 if (cargo_chance > 10)
9104 {
9105 if (EXPECT_NOT([self isPlayer]))
9106 {
9107 cargo_chance -= 20;
9108 }
9109 else
9110 {
9111 cargo_chance -= 30;
9112 }
9113 }
9114 limit--;
9115 ShipEntity* cargoObj = [jetsam objectAtIndex:i];
9116 ShipEntity* container = [UNIVERSE reifyCargoPod:cargoObj];
9117 /* TODO: this debris position/velocity setting code is
9118 * duplicated - sometimes not very cleanly - all over the
9119 * place. Unify to a single function - CIM */
9120 HPVector rpos = xposition;
9121 Vector rrand = OORandomPositionInBoundingBox(boundingBox);
9122 rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z;
9123 rpos.x += (ranrot_rand() % 7) - 3;
9124 rpos.y += (ranrot_rand() % 7) - 3;
9125 rpos.z += (ranrot_rand() % 7) - 3;
9126 [container setPosition:rpos];
9127 v.x = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9128 v.y = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9129 v.z = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9130 [container setVelocity:vector_add(v,[self velocity])];
9132 [container setOrientation:q];
9133
9134 [container setTemperature:[self randomEjectaTemperature]];
9135 [container setScanClass: CLASS_CARGO];
9136 [UNIVERSE addEntity:container]; // STATUS_IN_FLIGHT, AI state GLOBAL
9137
9138 AI *containerAI = [container getAI];
9139 if ([containerAI hasSuspendedStateMachines]) // check if new or recycled cargo.
9140 {
9141 [containerAI exitStateMachineWithMessage:nil];
9142 [container setThrust:[container maxThrust]]; // restore old value. Was set to zero on previous scooping.
9143 [container setOwner:container];
9144 }
9145 }
9146 if (limit <= 0)
9147 {
9148 break; // even really big ships won't have too much cargo survive an explosion
9149 }
9150 }
9151
9152}
9153
9154
9155- (void) setIsWreckage:(BOOL)isw
9156{
9157 isWreckage = isw;
9158}
9159
9160
9161- (BOOL) showDamage
9162{
9163 return _showDamage;
9164}
9165
9166
9167- (void) becomeExplosion
9168{
9169
9170 // check if we're destroying a subentity
9171 ShipEntity *parent = [self parentEntity];
9172 if (parent != nil)
9173 {
9174 ShipEntity *this_ship = [self retain];
9175 HPVector this_pos = [self absolutePositionForSubentity];
9176
9177 // remove this ship from its parent's subentity list
9178 [parent subEntityDied:self];
9179 [UNIVERSE addEntity:this_ship];
9180 [this_ship setPosition:this_pos];
9181 [this_ship release];
9182 if ([parent isPlayer])
9183 {
9184 // make the parent ship less reliable.
9185 [(PlayerEntity *)parent adjustTradeInFactorBy:-PLAYER_SHIP_SUBENTITY_TRADE_IN_VALUE];
9186 }
9187 }
9188
9189 HPVector xposition = position;
9190 NSUInteger i;
9191 Vector v;
9192 Quaternion q;
9193 int speed_low = 200;
9194 GLfloat n_alloys = sqrtf(sqrtf(mass / 6000.0f));
9195 NSUInteger numAlloys = 0;
9196 BOOL canReleaseSubWreckage = isWreckage && ([UNIVERSE detailLevel] >= DETAIL_LEVEL_EXTRAS);
9197
9198 if ([self status] == STATUS_DEAD)
9199 {
9200 [UNIVERSE removeEntity:self];
9201 return;
9202 }
9203 [self setStatus:STATUS_DEAD];
9204
9205 @try
9206 {
9207 if ([self isThargoid] && [roleSet hasRole:@"thargoid-mothership"]) [self broadcastThargoidDestroyed];
9208
9209 if (!suppressExplosion && ([self isVisible] || HPdistance2([self position], [PLAYER position]) < SCANNER_MAX_RANGE2))
9210 {
9211 if (!isWreckage && mass > 500000.0f && randf() < 0.25f) // big!
9212 {
9213 // draw an expanding ring
9215 [ring setVelocity:vector_multiply_scalar([self velocity], 0.25f)];
9216 [UNIVERSE addEntity:ring];
9217 }
9218
9219 BOOL add_debris = (UNIVERSE->n_entities < 0.95 * UNIVERSE_MAX_ENTITIES) &&
9220 ([UNIVERSE getTimeDelta] < 0.125); // FPS > 8
9221
9222
9223 // There are several parts to explosions, show only the main
9224 // explosion effect if UNIVERSE is almost full.
9225
9226 if (add_debris)
9227 {
9228 if ([UNIVERSE reducedDetail])
9229 {
9230 // Quick explosion effects for reduced detail mode
9231
9232 // 1. fast sparks
9233 [UNIVERSE addEntity:[OOSmallFragmentBurstEntity fragmentBurstFromEntity:self]];
9234 // 2. slow clouds
9235 [UNIVERSE addEntity:[OOBigFragmentBurstEntity fragmentBurstFromEntity:self]];
9236 // 3. flash
9237 [UNIVERSE addEntity:[OOFlashEffectEntity explosionFlashFromEntity:self]];
9238 /* This mode used to be the default for
9239 * cargo/munitions but this now must be explicitly
9240 * specified. */
9241 }
9242 else
9243 {
9244 NSString *explosionKey = @"oolite-default-ship-explosion";
9245 NSDictionary *explosion = nil;
9246 if (explosionType == nil)
9247 {
9248 explosion = [UNIVERSE explosionSetting:explosionKey];
9249 [UNIVERSE addEntity:[OOExplosionCloudEntity explosionCloudFromEntity:self withSettings:explosion]];
9250 // 3. flash
9251 [UNIVERSE addEntity:[OOFlashEffectEntity explosionFlashFromEntity:self]];
9252 }
9253 for (NSUInteger i=0;i<[explosionType count];i++)
9254 {
9255 explosionKey = [explosionType oo_stringAtIndex:i defaultValue:nil];
9256 if (explosionKey != nil)
9257 {
9258 // three special-case builtins
9259 if ([explosionKey isEqualToString:@"oolite-builtin-flash"])
9260 {
9261 [UNIVERSE addEntity:[OOFlashEffectEntity explosionFlashFromEntity:self]];
9262 }
9263 else if ([explosionKey isEqualToString:@"oolite-builtin-slowcloud"])
9264 {
9265 [UNIVERSE addEntity:[OOBigFragmentBurstEntity fragmentBurstFromEntity:self]];
9266 }
9267 else if ([explosionKey isEqualToString:@"oolite-builtin-fastspark"])
9268 {
9269 [UNIVERSE addEntity:[OOSmallFragmentBurstEntity fragmentBurstFromEntity:self]];
9270 }
9271 else
9272 {
9273 explosion = [UNIVERSE explosionSetting:explosionKey];
9274 [UNIVERSE addEntity:[OOExplosionCloudEntity explosionCloudFromEntity:self withSettings:explosion]];
9275 }
9276 }
9277 }
9278 // "fireball" explosion effect
9279
9280 }
9281 }
9282
9283 // If UNIVERSE is nearing limit for entities don't add to it!
9284 if (add_debris)
9285 {
9286 // we need to throw out cargo at this point.
9287 [self releaseCargoPodsDebris];
9288
9289 // Throw out rocks and alloys to be scooped up
9290 if ([self hasRole:@"asteroid"] || [self isBoulder])
9291 {
9292 if (!noRocks && (being_mined || randf() < 0.20))
9293 {
9294 NSString *defaultRole = @"boulder";
9295 float defaultSpeed = 50.0;
9296 if ([self isBoulder])
9297 {
9298 defaultRole = @"splinter";
9299 defaultSpeed = 20.0;
9300 if (likely_cargo == 0)
9301 {
9302 likely_cargo = 4; // compatibility with older boulders
9303 }
9304 }
9305 else if ([[self primaryAggressor] isPlayer])
9306 {
9307 [PLAYER addRoleForMining];
9308 }
9309 NSUInteger n_rocks = 2 + (Ranrot() % (likely_cargo + 1));
9310
9311 NSString *debrisRole = [[self shipInfoDictionary] oo_stringForKey:@"debris_role" defaultValue:defaultRole];
9312 for (i = 0; i < n_rocks; i++)
9313 {
9314 ShipEntity* rock = [UNIVERSE newShipWithRole:debrisRole]; // retain count = 1
9315 if (rock)
9316 {
9317 float r_speed = [rock maxFlightSpeed] > 0 ? 2.0 * [rock maxFlightSpeed] : defaultSpeed;
9318 float cr = (collision_radius < rock->collision_radius) ? collision_radius : 2 * rock->collision_radius;
9319 v.x = ((randf() * r_speed) - r_speed / 2);
9320 v.y = ((randf() * r_speed) - r_speed / 2);
9321 v.z = ((randf() * r_speed) - r_speed / 2);
9322 [rock setVelocity:vector_add(v,[self velocity])];
9323 HPVector rpos = HPvector_add(xposition,vectorToHPVector(vector_multiply_scalar(vector_normal(v),cr)));
9324 [rock setPosition:rpos];
9325
9327 [rock setOrientation:q];
9328
9329 [rock setTemperature:[self randomEjectaTemperature]];
9330 if ([self isBoulder])
9331 {
9332 [rock setScanClass: CLASS_CARGO];
9333 [rock setBounty: 0 withReason:kOOLegalStatusReasonSetup];
9334 // only make the rock have minerals if something isn't already defined for the rock
9335 if ([[rock shipInfoDictionary] oo_stringForKey:@"cargo_carried"] == nil)
9336 [rock setCommodity:@"minerals" andAmount: 1];
9337 }
9338 else
9339 {
9340 [rock setScanClass:CLASS_ROCK];
9341 [rock setIsBoulder:YES];
9342 }
9343 [UNIVERSE addEntity:rock]; // STATUS_IN_FLIGHT, AI state GLOBAL
9344 [rock release];
9345 }
9346 }
9347 }
9348 return;
9349 }
9350
9351 // throw out burning chunks of wreckage
9352 //
9353 if ((n_alloys && canFragment) || canReleaseSubWreckage)
9354 {
9355 NSUInteger n_wreckage = 0;
9356
9357 if (UNIVERSE->n_entities < 0.50 * UNIVERSE_MAX_ENTITIES)
9358 {
9359 // Create wreckage only when UNIVERSE is less than half full.
9360 // (condition set in r906 - was < 0.75 before) --Kaks 2011.10.17
9361 NSUInteger maxWrecks = 3;
9362 if (n_alloys == 0)
9363 {
9364 // must be sub-wreckage here
9365 n_wreckage = (mass > 600.0 && randf() < 0.2)?2:0;
9366 }
9367 else
9368 {
9369 n_wreckage = (n_alloys < maxWrecks)? floorf(randf()*(n_alloys+2)) : maxWrecks;
9370 }
9371 }
9372
9373 for (i = 0; i < n_wreckage; i++)
9374 {
9375 Vector r1 = [octree randomPoint];
9376 Vector dir = quaternion_rotate_vector([self normalOrientation], r1);
9377 HPVector rpos = HPvector_add(vectorToHPVector(dir), xposition);
9378 GLfloat lifetime = 750.0 * randf() + 250.0 * i + 100.0;
9379 ShipEntity *wreck = [UNIVERSE addWreckageFrom:self withRole:@"wreckage" at:rpos scale:1.0 lifetime:lifetime/2];
9380
9381 [wreck setVelocity:vector_add([wreck velocity],vector_multiply_scalar(vector_normal(dir),randf()*[wreck collisionRadius]))];
9382
9383 }
9384 n_alloys = randf() * n_alloys;
9385 }
9386 }
9387
9388 if (!canFragment)
9389 {
9390 n_alloys = 0.0;
9391 }
9392 // If UNIVERSE is almost full, don't create more than 1 piece of scrap metal.
9393 else if (!add_debris)
9394 {
9395 n_alloys = (n_alloys > 1.0) ? 1.0 : 0.0;
9396 }
9397
9398 // now convert to uint
9399 numAlloys = floorf(n_alloys);
9400
9401 // Throw out scrap metal
9402 //
9403 for (i = 0; i < numAlloys; i++)
9404 {
9405 ShipEntity* plate = [UNIVERSE newShipWithRole:@"alloy"]; // retain count = 1
9406 if (plate)
9407 {
9408 HPVector rpos = xposition;
9409 Vector rrand = OORandomPositionInBoundingBox(boundingBox);
9410 rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z;
9411 rpos.x += (ranrot_rand() % 7) - 3;
9412 rpos.y += (ranrot_rand() % 7) - 3;
9413 rpos.z += (ranrot_rand() % 7) - 3;
9414 [plate setPosition:rpos];
9415 v.x = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9416 v.y = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9417 v.z = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
9418 [plate setVelocity:vector_add(v,[self velocity])];
9420 [plate setOrientation:q];
9421
9422 [plate setTemperature:[self randomEjectaTemperature]];
9423 [plate setScanClass: CLASS_CARGO];
9424 [plate setCommodity:@"alloys" andAmount:1];
9425 [UNIVERSE addEntity:plate]; // STATUS_IN_FLIGHT, AI state GLOBAL
9426
9427 [plate release];
9428 }
9429 }
9430 }
9431
9432 // Explode subentities.
9433 ShipEntity *se = nil;
9434 foreach (se, [self shipSubEntityEnumerator])
9435 {
9436 [se setSuppressExplosion:suppressExplosion];
9437 [se becomeExplosion];
9438 }
9439 [self clearSubEntities];
9440
9441 // momentum from explosions
9442 if (!suppressExplosion)
9443 {
9444 desired_range = collision_radius * 2.5f;
9445 [self dealMomentumWithinDesiredRange:0.125f * mass];
9446 }
9447
9448 if (self != PLAYER) // was if !isPlayer - but I think this may cause ghosts (Who's "I"? -- Ahruman)
9449 {
9450 if (isPlayer)
9451 {
9452 #ifndef NDEBUG
9453 OOLog(@"becomeExplosion.suspectedGhost.confirm", @"%@", @"Ship spotted with isPlayer set when not actually the player.");
9454 #endif
9455 isPlayer = NO;
9456 }
9457 }
9458 }
9459 @finally
9460 {
9461 if (self != PLAYER)
9462 {
9463 [UNIVERSE removeEntity:self];
9464 }
9465 }
9466}
9467
9468
9469// Exposed to AI
9470- (void) becomeEnergyBlast
9471{
9472 [UNIVERSE addEntity:[OOQuiriumCascadeEntity quiriumCascadeFromShip:self]];
9473 [self broadcastEnergyBlastImminent];
9474 [self noteKilledBy:nil damageType:kOODamageTypeCascadeWeapon];
9475 [UNIVERSE removeEntity:self];
9476}
9477
9478
9479// Exposed to AI
9480- (void) broadcastEnergyBlastImminent
9481{
9482 // anyone further away than typical scanner range probably doesn't need to hear
9483 NSArray* targets = [UNIVERSE entitiesWithinRange:SCANNER_MAX_RANGE ofEntity:self];
9484 if ([targets count] > 0)
9485 {
9486 unsigned i;
9487 for (i = 0; i < [targets count]; i++)
9488 {
9489 Entity *e2 = [targets objectAtIndex:i];
9490 if ([e2 isShip])
9491 {
9492 ShipEntity *se = (ShipEntity *)e2;
9493 [se setFoundTarget:self];
9494 [se reactToAIMessage:@"CASCADE_WEAPON_DETECTED" context:@"nearby Q-mine"];
9495 [se doScriptEvent:OOJSID("cascadeWeaponDetected") withArgument:self];
9496 }
9497 }
9498 }
9499}
9500
9501
9502- (void) removeExhaust:(OOExhaustPlumeEntity *)exhaust
9503{
9504 [subEntities removeObject:exhaust];
9505 [exhaust setOwner:nil];
9506}
9507
9508
9509- (void) removeFlasher:(OOFlasherEntity *)flasher
9510{
9511 [subEntities removeObject:flasher];
9512 [flasher setOwner:nil];
9513}
9514
9515
9516- (void)subEntityDied:(ShipEntity *)sub
9517{
9518 if ([self subEntityTakingDamage] == sub) [self setSubEntityTakingDamage:nil];
9519
9520 [sub setOwner:nil];
9521 // TODO? Recalculating collision radius should increase collision testing efficiency,
9522 // but for most ship models the difference would be marginal. -- Kaks 20110429
9523 mass -= [sub mass]; // missing subents affect fuel charge rate, etc..
9524 [subEntities removeObject:sub];
9525}
9526
9527
9528- (void)subEntityReallyDied:(ShipEntity *)sub
9529{
9530 if ([self subEntityTakingDamage] == sub) [self setSubEntityTakingDamage:nil];
9531
9532 if ([self hasSubEntity:sub])
9533 {
9534 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.");
9535
9536 // Leak subentity list.
9537 subEntities = nil;
9538 }
9539}
9540
9541
9542- (Vector) positionOffsetForAlignment:(NSString*) align
9543{
9544 NSString* padAlign = [NSString stringWithFormat:@"%@---", align];
9545 Vector result = kZeroVector;
9546 switch ([padAlign characterAtIndex:0])
9547 {
9548 case (unichar)'c':
9549 case (unichar)'C':
9550 result.x = 0.5 * (boundingBox.min.x + boundingBox.max.x);
9551 break;
9552 case (unichar)'M':
9553 result.x = boundingBox.max.x;
9554 break;
9555 case (unichar)'m':
9556 result.x = boundingBox.min.x;
9557 break;
9558 }
9559 switch ([padAlign characterAtIndex:1])
9560 {
9561 case (unichar)'c':
9562 case (unichar)'C':
9563 result.y = 0.5 * (boundingBox.min.y + boundingBox.max.y);
9564 break;
9565 case (unichar)'M':
9566 result.y = boundingBox.max.y;
9567 break;
9568 case (unichar)'m':
9569 result.y = boundingBox.min.y;
9570 break;
9571 }
9572 switch ([padAlign characterAtIndex:2])
9573 {
9574 case (unichar)'c':
9575 case (unichar)'C':
9576 result.z = 0.5 * (boundingBox.min.z + boundingBox.max.z);
9577 break;
9578 case (unichar)'M':
9579 result.z = boundingBox.max.z;
9580 break;
9581 case (unichar)'m':
9582 result.z = boundingBox.min.z;
9583 break;
9584 }
9585 return result;
9586}
9587
9588
9589Vector positionOffsetForShipInRotationToAlignment(ShipEntity* ship, Quaternion q, NSString* align)
9590{
9591 NSString* padAlign = [NSString stringWithFormat:@"%@---", align];
9592 Vector i = vector_right_from_quaternion(q);
9593 Vector j = vector_up_from_quaternion(q);
9594 Vector k = vector_forward_from_quaternion(q);
9595 BoundingBox arbb = [ship findBoundingBoxRelativeToPosition:kZeroHPVector InVectors:i :j :k];
9596 Vector result = kZeroVector;
9597 switch ([padAlign characterAtIndex:0])
9598 {
9599 case (unichar)'c':
9600 case (unichar)'C':
9601 result.x = 0.5 * (arbb.min.x + arbb.max.x);
9602 break;
9603 case (unichar)'M':
9604 result.x = arbb.max.x;
9605 break;
9606 case (unichar)'m':
9607 result.x = arbb.min.x;
9608 break;
9609 }
9610 switch ([padAlign characterAtIndex:1])
9611 {
9612 case (unichar)'c':
9613 case (unichar)'C':
9614 result.y = 0.5 * (arbb.min.y + arbb.max.y);
9615 break;
9616 case (unichar)'M':
9617 result.y = arbb.max.y;
9618 break;
9619 case (unichar)'m':
9620 result.y = arbb.min.y;
9621 break;
9622 }
9623 switch ([padAlign characterAtIndex:2])
9624 {
9625 case (unichar)'c':
9626 case (unichar)'C':
9627 result.z = 0.5 * (arbb.min.z + arbb.max.z);
9628 break;
9629 case (unichar)'M':
9630 result.z = arbb.max.z;
9631 break;
9632 case (unichar)'m':
9633 result.z = arbb.min.z;
9634 break;
9635 }
9636 return result;
9637}
9638
9639
9640- (void) becomeLargeExplosion:(double)factor
9641{
9642
9643 if ([self status] == STATUS_DEAD) return;
9644 [self setStatus:STATUS_DEAD];
9645
9646 @try
9647 {
9648 // two parts to the explosion:
9649 // 1. fast sparks
9650 float how_many = factor;
9651 while (how_many > 0.5f)
9652 {
9653 [UNIVERSE addEntity:[OOSmallFragmentBurstEntity fragmentBurstFromEntity:self]];
9654 how_many -= 1.0f;
9655 }
9656 // 2. slow clouds
9657 how_many = factor;
9658 while (how_many > 0.5f)
9659 {
9660 [UNIVERSE addEntity:[OOBigFragmentBurstEntity fragmentBurstFromEntity:self]];
9661 how_many -= 1.0f;
9662 }
9663
9664 [self releaseCargoPodsDebris];
9665
9666 ShipEntity *se = nil;
9667 foreach (se, [self shipSubEntityEnumerator])
9668 {
9669 [se setSuppressExplosion:suppressExplosion];
9670 [se becomeExplosion];
9671 }
9672 [self clearSubEntities];
9673
9674 }
9675 @finally
9676 {
9677 if (!isPlayer) [UNIVERSE removeEntity:self];
9678 }
9679}
9680
9681
9682- (void) collectBountyFor:(ShipEntity *)other
9683{
9684 if ([other isPolice]) // oops, we shot a copper!
9685 {
9686 [self markAsOffender:64 withReason:kOOLegalStatusReasonAttackedPolice];
9687 }
9688}
9689
9690
9691- (NSComparisonResult) compareBeaconCodeWith:(Entity<OOBeaconEntity> *) other
9692{
9693 return [[self beaconCode] compare:[other beaconCode] options: NSCaseInsensitiveSearch];
9694}
9695
9696
9697// for shaders, equivalent to 1.76's NPC laserHeatLevel
9698- (GLfloat) weaponRecoveryTime
9699{
9700 float result = (weapon_recharge_rate - [self shotTime]) / weapon_recharge_rate;
9701 return OOClamp_0_1_f(result);
9702}
9703
9704
9705- (GLfloat)laserHeatLevel
9706{
9707 GLfloat result = weapon_temp / NPC_MAX_WEAPON_TEMP;
9708 return OOClamp_0_1_f(result);
9709}
9710
9711
9712- (GLfloat)laserHeatLevelAft
9713{
9714 GLfloat result = aft_weapon_temp / NPC_MAX_WEAPON_TEMP;
9715 return OOClamp_0_1_f(result);
9716}
9717
9718
9719- (GLfloat)laserHeatLevelForward
9720{
9721 GLfloat result = forward_weapon_temp / NPC_MAX_WEAPON_TEMP;
9722 if (isWeaponNone(forward_weapon_type))
9723 { // must check subents
9724 OOWeaponType forward_weapon_real_type = nil;
9725 NSEnumerator *subEnum = [self shipSubEntityEnumerator];
9726 ShipEntity *se = nil;
9727 while (isWeaponNone(forward_weapon_real_type) && (se = [subEnum nextObject]))
9728 {
9730 {
9731 forward_weapon_real_type = se->forward_weapon_type;
9733 }
9734 }
9735 }
9736 return OOClamp_0_1_f(result);
9737}
9738
9739
9740- (GLfloat)laserHeatLevelPort
9741{
9742 GLfloat result = port_weapon_temp / NPC_MAX_WEAPON_TEMP;
9743 return OOClamp_0_1_f(result);
9744}
9745
9746
9747- (GLfloat)laserHeatLevelStarboard
9748{
9749 GLfloat result = starboard_weapon_temp / NPC_MAX_WEAPON_TEMP;
9750 return OOClamp_0_1_f(result);
9751}
9752
9753
9754- (GLfloat)hullHeatLevel
9755{
9756 GLfloat result = (GLfloat)ship_temperature / (GLfloat)SHIP_MAX_CABIN_TEMP;
9757 return OOClamp_0_1_f(result);
9758}
9759
9760
9761- (GLfloat)entityPersonality
9762{
9763 return entity_personality / (float)ENTITY_PERSONALITY_MAX;
9764}
9765
9766
9767- (GLint)entityPersonalityInt
9768{
9769 return entity_personality;
9770}
9771
9772
9773- (uint32_t) randomSeedForShaders
9774{
9775 return entity_personality * 0x00010001;
9776}
9777
9778
9779- (void) setEntityPersonalityInt:(uint16_t)value
9780{
9781 if (value <= ENTITY_PERSONALITY_MAX)
9782 {
9783 entity_personality = value;
9784 [[self mesh] rebindMaterials];
9785 }
9786}
9787
9788
9789- (void)setSuppressExplosion:(BOOL)suppress
9790{
9791 suppressExplosion = !!suppress;
9792}
9793
9794
9795- (void) resetExhaustPlumes
9796{
9797 OOExhaustPlumeEntity *exEnt = nil;
9798
9799 foreach (exEnt, [self exhaustEnumerator])
9800 {
9801 [exEnt resetPlume];
9802 }
9803}
9804
9805
9806/*-----------------------------------------
9807
9808 AI piloting methods
9809
9810-----------------------------------------*/
9811
9812
9813- (void) checkScanner
9814{
9815 Entity* scan;
9816 n_scanned_ships = 0;
9817 //
9818 scan = z_previous; while ((scan)&&(scan->isShip == NO)) scan = scan->z_previous; // skip non-ships
9819 GLfloat scannerRange2 = scannerRange * scannerRange;
9820 while ((scan)&&(scan->position.z > position.z - scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
9821 {
9822 // can't scan cloaked ships
9823 if (scan->isShip && ![(ShipEntity*)scan isCloaked] && [self isValidTarget:scan])
9824 {
9825 distance2_scanned_ships[n_scanned_ships] = HPdistance2(position, scan->position);
9826 if (distance2_scanned_ships[n_scanned_ships] < scannerRange2)
9827 scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
9828 }
9829 scan = scan->z_previous; while ((scan)&&(scan->isShip == NO)) scan = scan->z_previous;
9830 }
9831 //
9832 scan = z_next; while ((scan)&&(scan->isShip == NO)) scan = scan->z_next; // skip non-ships
9833 while ((scan)&&(scan->position.z < position.z + scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
9834 {
9835 if (scan->isShip && ![(ShipEntity*)scan isCloaked] && [self isValidTarget:scan])
9836 {
9837 distance2_scanned_ships[n_scanned_ships] = HPdistance2(position, scan->position);
9838 if (distance2_scanned_ships[n_scanned_ships] < scannerRange2)
9839 scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
9840 }
9841 scan = scan->z_next; while ((scan)&&(scan->isShip == NO)) scan = scan->z_next; // skip non-ships
9842 }
9843 //
9844 scanned_ships[n_scanned_ships] = nil; // terminate array
9845}
9846
9847
9848- (void) checkScannerIgnoringUnpowered
9849{
9850 Entity* scan;
9851 n_scanned_ships = 0;
9852 //
9853 GLfloat scannerRange2 = scannerRange * scannerRange;
9854 scan = z_previous;
9855 while ((scan)&&((scan->isShip == NO)||(scan->scanClass==CLASS_ROCK)||(scan->scanClass==CLASS_CARGO)))
9856 {
9857 scan = scan->z_previous; // skip non-ships
9858 }
9859 while ((scan)&&(scan->position.z > position.z - scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
9860 {
9861 if (scan->isShip && ![(ShipEntity*)scan isCloaked])
9862 {
9863 distance2_scanned_ships[n_scanned_ships] = HPdistance2(position, scan->position);
9864 if (distance2_scanned_ships[n_scanned_ships] < scannerRange2)
9865 scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
9866 }
9867 scan = scan->z_previous;
9868 while ((scan)&&((scan->isShip == NO)||(scan->scanClass==CLASS_ROCK)||(scan->scanClass==CLASS_CARGO)))
9869 {
9870 scan = scan->z_previous; // skip non-ships
9871 }
9872 }
9873 //
9874 scan = z_next;
9875 while ((scan)&&((scan->isShip == NO)||(scan->scanClass==CLASS_ROCK)||(scan->scanClass==CLASS_CARGO)))
9876 {
9877 scan = scan->z_next; // skip non-ships
9878 }
9879
9880 while ((scan)&&(scan->position.z < position.z + scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
9881 {
9882 if (scan->isShip && ![(ShipEntity*)scan isCloaked])
9883 {
9884 distance2_scanned_ships[n_scanned_ships] = HPdistance2(position, scan->position);
9885 if (distance2_scanned_ships[n_scanned_ships] < scannerRange2)
9886 scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
9887 }
9888 scan = scan->z_next;
9889 while ((scan)&&((scan->isShip == NO)||(scan->scanClass==CLASS_ROCK)||(scan->scanClass==CLASS_CARGO)))
9890 {
9891 scan = scan->z_next; // skip non-ships
9892 }
9893 }
9894 //
9895 scanned_ships[n_scanned_ships] = nil; // terminate array
9896}
9897
9898
9899- (ShipEntity**) scannedShips
9900{
9901 scanned_ships[n_scanned_ships] = nil; // terminate array
9902 return scanned_ships;
9903}
9904
9905
9906- (int) numberOfScannedShips
9907{
9908 return n_scanned_ships;
9909}
9910
9911
9912- (Entity *) foundTarget
9913{
9914 Entity *result = [_foundTarget weakRefUnderlyingObject];
9915 if (result == nil || ![self isValidTarget:result])
9916 {
9917 DESTROY(_foundTarget);
9918 return nil;
9919 }
9920 return result;
9921}
9922
9923
9924- (void) setFoundTarget:(Entity *) targetEntity
9925{
9926 [_foundTarget release];
9927 _foundTarget = [targetEntity weakRetain];
9928}
9929
9930
9931- (Entity *) primaryAggressor
9932{
9933 Entity *result = [_primaryAggressor weakRefUnderlyingObject];
9934 if (result == nil || ![self isValidTarget:result])
9935 {
9936 DESTROY(_primaryAggressor);
9937 return nil;
9938 }
9939 return result;
9940}
9941
9942
9943- (void) setPrimaryAggressor:(Entity *) targetEntity
9944{
9945 [_primaryAggressor release];
9946 _primaryAggressor = [targetEntity weakRetain];
9947}
9948
9949
9950- (Entity *) lastEscortTarget
9951{
9952 Entity *result = [_lastEscortTarget weakRefUnderlyingObject];
9953 if (result == nil || ![self isValidTarget:result])
9954 {
9955 DESTROY(_lastEscortTarget);
9956 return nil;
9957 }
9958 return result;
9959}
9960
9961
9962- (void) setLastEscortTarget:(Entity *) targetEntity
9963{
9964 [_lastEscortTarget release];
9965 _lastEscortTarget = [targetEntity weakRetain];
9966}
9967
9968
9969- (Entity *) thankedShip
9970{
9971 Entity *result = [_thankedShip weakRefUnderlyingObject];
9972 if (result == nil || ![self isValidTarget:result])
9973 {
9974 DESTROY(_thankedShip);
9975 return nil;
9976 }
9977 return result;
9978}
9979
9980
9981- (void) setThankedShip:(Entity *) targetEntity
9982{
9983 [_thankedShip release];
9984 _thankedShip = [targetEntity weakRetain];
9985}
9986
9987
9988- (Entity *) rememberedShip
9989{
9990 Entity *result = [_rememberedShip weakRefUnderlyingObject];
9991 if (result == nil || ![self isValidTarget:result])
9992 {
9993 DESTROY(_rememberedShip);
9994 return nil;
9995 }
9996 return result;
9997}
9998
9999
10000- (void) setRememberedShip:(Entity *) targetEntity
10001{
10002 [_rememberedShip release];
10003 _rememberedShip = [targetEntity weakRetain];
10004}
10005
10006
10007- (StationEntity *) targetStation
10008{
10009 StationEntity *result = [_targetStation weakRefUnderlyingObject];
10010 if (result == nil || ![self isValidTarget:result])
10011 {
10012 DESTROY(_targetStation);
10013 return nil;
10014 }
10015 return result;
10016}
10017
10018
10019- (void) setTargetStation:(Entity *) targetEntity
10020{
10021 [_targetStation release];
10022 _targetStation = [targetEntity weakRetain];
10023}
10024
10025/* Now we use weakrefs rather than universal ID this function checks
10026 * for targets which may have a valid reference but are not currently
10027 * targetable. */
10028- (BOOL) isValidTarget:(Entity *)target
10029{
10030 if (target == nil)
10031 {
10032 return NO;
10033 }
10034 if ([target isShip])
10035 {
10036 OOEntityStatus tstatus = [target status];
10037 if (tstatus == STATUS_ENTERING_WITCHSPACE || tstatus == STATUS_IN_HOLD || tstatus == STATUS_DOCKED || tstatus == STATUS_DEAD)
10038 // 2013-01-13, Eric: added STATUS_DEAD because I keep seeing ships locked on dead ships in attack mode.
10039 {
10040 return NO;
10041 }
10042 return YES;
10043 }
10044 if ([target isWormhole] && [target scanClass] != CLASS_NO_DRAW)
10045 {
10046 return YES;
10047 }
10048 return NO;
10049}
10050
10051
10052- (void) addTarget:(Entity *) targetEntity
10053{
10054 if (targetEntity == self) return;
10055 if (targetEntity != nil)
10056 {
10057 DESTROY(_primaryTarget);
10058 _primaryTarget = [targetEntity weakRetain];
10059 [self startTrackingCurve];
10060 }
10061
10062 [[self shipSubEntityEnumerator] makeObjectsPerformSelector:@selector(addTarget:) withObject:targetEntity];
10063 if (![self isSubEntity]) [self doScriptEvent:OOJSID("shipTargetAcquired") withArgument:targetEntity];
10064}
10065
10066
10067- (void) removeTarget:(Entity *) targetEntity
10068{
10069 if(targetEntity != nil) [self noteLostTarget];
10070 else DESTROY(_primaryTarget);
10071 // targetEntity == nil is currently only true for mounted player missiles.
10072 // we don't want to send lostTarget messages while the missile is mounted.
10073
10074 [[self shipSubEntityEnumerator] makeObjectsPerformSelector:@selector(removeTarget:) withObject:targetEntity];
10075}
10076
10077
10078/* Checks if the primary target is still trackable.
10079 *
10080 * 1.80 behaviour: still exists, is in scanner range, is not cloaked
10081 *
10082 * 1.81 (planned) behaviour:
10083 * - track cloaked ships once primary targeted (but no missiles, and can't keep as a mere defense target, and probably some other penalties)
10084 * - track ships at 120% scanner range *if* they are also primary aggressor
10085 *
10086 * But first, just switch over to this method on 1.80 behaviour and check
10087 * that things still work.
10088 */
10089- (BOOL) canStillTrackPrimaryTarget
10090{
10091 Entity *target = (Entity *)[self primaryTargetWithoutValidityCheck];
10092 if (target == nil)
10093 {
10094 return NO;
10095 }
10096 if (![self isValidTarget:target])
10097 {
10098 return NO;
10099 }
10100 double range2 = HPmagnitude2(HPvector_subtract([target position], position));
10101 if (range2 > scannerRange * scannerRange * 1.5625)
10102 {
10103 // 1.5625 = 1.25*1.25
10104 return NO;
10105 }
10106 // 1.81: can retain cloaked ships as a *primary* target now
10107/* if ([target isShip] && [(ShipEntity*)target isCloaked])
10108 {
10109 return NO;
10110 } */
10111 return YES;
10112}
10113
10114
10115- (id) primaryTarget
10116{
10117 id result = [_primaryTarget weakRefUnderlyingObject];
10118 if ((result == nil && _primaryTarget != nil)
10119 || ![self isValidTarget:result])
10120 {
10121 DESTROY(_primaryTarget);
10122 return nil;
10123 }
10124 else if (EXPECT_NOT(result == self))
10125 {
10126 /* Added in response to a crash report showing recursion in
10127 [PlayerEntity hasHostileTarget].
10128 -- Ahruman 2009-12-17
10129 */
10130 DESTROY(_primaryTarget);
10131 }
10132 return result;
10133}
10134
10135
10136// used when we need to check the target - perhaps for a potential
10137// noteTargetLost - without invalidating the target first
10138- (id) primaryTargetWithoutValidityCheck
10139{
10140 id result = [_primaryTarget weakRefUnderlyingObject];
10141 if (EXPECT_NOT(result == self))
10142 {
10143 // just in case
10144 DESTROY(_primaryTarget);
10145 return nil;
10146 }
10147 return result;
10148}
10149
10150
10151- (BOOL) isFriendlyTo:(ShipEntity *)otherShip
10152{
10153 BOOL isFriendly = NO;
10154 OOShipGroup *myGroup = [self group];
10155 OOShipGroup *otherGroup = [otherShip group];
10156
10157 if ((otherShip == self) ||
10158 ([self isPolice] && [otherShip isPolice]) ||
10159 ([self isThargoid] && [otherShip isThargoid]) ||
10160 (myGroup != nil && otherGroup != nil && (myGroup == otherGroup || [otherGroup leader] == self)) ||
10161 ([self scanClass] == CLASS_MILITARY && [otherShip scanClass] == CLASS_MILITARY))
10162 {
10163 isFriendly = YES;
10164 }
10165
10166 return isFriendly;
10167}
10168
10169
10170- (ShipEntity *) shipHitByLaser
10171{
10172 return [_shipHitByLaser weakRefUnderlyingObject];
10173}
10174
10175
10176- (void) setShipHitByLaser:(ShipEntity *)ship
10177{
10178 if (ship != [self shipHitByLaser])
10179 {
10180 [_shipHitByLaser release];
10181 _shipHitByLaser = [ship weakRetain];
10182 }
10183}
10184
10185
10186- (void) noteLostTarget
10187{
10188 id target = nil;
10189 if ([self primaryTarget] != nil)
10190 {
10191 ShipEntity* ship = [self primaryTarget];
10192 if ([self isDefenseTarget:ship])
10193 {
10194 [self removeDefenseTarget:ship];
10195 }
10196 // for compatibility with 1.76 behaviour of this function, only pass
10197 // the target as a function parameter if the target is still a potential
10198 // valid target (e.g. not scooped, docked, hyperspaced, etc.)
10199 target = (ship && ship->isShip && [self isValidTarget:ship]) ? (id)ship : nil;
10200 if ([self primaryAggressor] == ship)
10201 {
10202 DESTROY(_primaryAggressor);
10203 }
10204 DESTROY(_primaryTarget);
10205 }
10206 // always do target lost
10207 [self doScriptEvent:OOJSID("shipTargetLost") withArgument:target];
10208 if (target == nil) [shipAI message:@"TARGET_LOST"]; // stale target? no major urgency.
10209 else [shipAI reactToMessage:@"TARGET_LOST" context:@"flight updates"]; // execute immediately otherwise.
10210}
10211
10212
10213- (void) noteLostTargetAndGoIdle
10214{
10215 behaviour = BEHAVIOUR_IDLE;
10216 frustration = 0.0;
10217 [self noteLostTarget];
10218}
10219
10220- (void) noteTargetDestroyed:(ShipEntity *)target
10221{
10222 [self collectBountyFor:(ShipEntity *)target];
10223 if ([self primaryTarget] == target)
10224 {
10225 [self removeTarget:target];
10226 [self doScriptEvent:OOJSID("shipTargetDestroyed") withArgument:target];
10227 [shipAI message:@"TARGET_DESTROYED"];
10228 }
10229 if ([self isDefenseTarget:target])
10230 {
10231 [self removeDefenseTarget:target];
10232 [shipAI message:@"DEFENSE_TARGET_DESTROYED"];
10233 [self doScriptEvent:OOJSID("defenseTargetDestroyed") withArgument:target];
10234 }
10235}
10236
10237
10238- (OOBehaviour) behaviour
10239{
10240 return behaviour;
10241}
10242
10243
10244- (void) setBehaviour:(OOBehaviour) cond
10245{
10246 if (cond != behaviour)
10247 {
10248 frustration = 0.0; // change is a GOOD thing
10249 behaviour = cond;
10250 }
10251}
10252
10253
10254- (HPVector) destination
10255{
10256 return _destination;
10257}
10258
10259- (HPVector) coordinates
10260{
10261 return coordinates;
10262}
10263
10264- (void) setCoordinate:(HPVector) coord // The name "setCoordinates" is already used by AI scripting.
10265{
10266 coordinates = coord;
10267}
10268
10269- (HPVector) distance_six: (GLfloat) dist
10270{
10271 HPVector six = position;
10272 six.x -= dist * v_forward.x; six.y -= dist * v_forward.y; six.z -= dist * v_forward.z;
10273 return six;
10274}
10275
10276
10277- (HPVector) distance_twelve: (GLfloat) dist withOffset:(GLfloat)offset
10278{
10279 HPVector twelve = position;
10280 twelve.x += dist * v_up.x; twelve.y += dist * v_up.y; twelve.z += dist * v_up.z;
10281 twelve.x += offset * v_right.x; twelve.y += offset * v_right.y; twelve.z += offset * v_right.z;
10282 return twelve;
10283}
10284
10285
10286- (void) trackOntoTarget:(double) delta_t withDForward: (GLfloat) dp
10287{
10288 Vector vector_to_target;
10289 Quaternion q_minarc;
10290 //
10291 Entity* target = [self primaryTarget];
10292 //
10293 if (!target)
10294 return;
10295
10296 vector_to_target = [self vectorTo:target];
10297 //
10298 GLfloat range2 = magnitude2(vector_to_target);
10299 GLfloat targetRadius = 0.75 * target->collision_radius;
10300 GLfloat max_cos = sqrt(1 - targetRadius*targetRadius/range2);
10301
10302 if (dp > max_cos)
10303 return; // ON TARGET!
10304
10305 if (vector_to_target.x||vector_to_target.y||vector_to_target.z)
10306 vector_to_target = vector_normal(vector_to_target);
10307 else
10308 vector_to_target.z = 1.0;
10309
10310 q_minarc = quaternion_rotation_between(v_forward, vector_to_target);
10311
10312 orientation = quaternion_multiply(q_minarc, orientation);
10313 [self orientationChanged];
10314
10315 flightRoll = 0.0;
10316 flightPitch = 0.0;
10317 flightYaw = 0.0;
10318 stick_roll = 0.0;
10319 stick_pitch = 0.0;
10320 stick_yaw = 0.0;
10321}
10322
10323
10324- (double) ballTrackLeadingTarget:(double) delta_t atTarget:(Entity *)target
10325{
10326 if (!target)
10327 {
10328 return -2.0; // no target
10329 }
10330
10331 Vector vector_to_target;
10332 Vector axis_to_track_by;
10333 Vector my_aim = vector_forward_from_quaternion(orientation);
10334 Vector my_ref = reference;
10335 double aim_cos, ref_cos;
10336 Vector leading = [target velocity];
10337
10338 // need to get vector to target in terms of this entities coordinate system
10339 HPVector my_position = [self absolutePositionForSubentity];
10340 vector_to_target = HPVectorToVector(HPvector_subtract([target position], my_position));
10341 // this is in absolute coordinates, so now rotate it
10342
10343 Entity *last = nil;
10344 Entity *father = [self parentEntity];
10345
10346 Quaternion q = kIdentityQuaternion;
10347 while ((father)&&(father != last) && (father != NO_TARGET))
10348 {
10349 /* Fix orientation */
10350 Quaternion fo = [father normalOrientation];
10351 fo.w = -fo.w;
10352 /* The below code works for player turrets where the
10353 * orientation is different, but not for NPC turrets. Taking
10354 * the normal orientation with -w works: there is probably a
10355 * neater way which someone who understands quaternions can
10356 * find, but this works well enough for 1.82 - CIM */
10357 q = quaternion_multiply(q,quaternion_conjugate(fo));
10358 last = father;
10359 if (![last isSubEntity]) break;
10360 father = [father owner];
10361 }
10362 q = quaternion_conjugate(q);
10363 // q now contains the rotation to the turret's reference system
10364
10365 vector_to_target = quaternion_rotate_vector(q,vector_to_target);
10366
10367 leading = quaternion_rotate_vector(q,leading);
10368 // rotate the vector to target and its velocity
10369
10370 if (magnitude(vector_to_target) > weaponRange * 1.01)
10371 {
10372 return -2.0; // out of range
10373 }
10374
10375 float lead = magnitude(vector_to_target) / TURRET_SHOT_SPEED;
10376
10377 vector_to_target = vector_add(vector_to_target, vector_multiply_scalar(leading, lead));
10378 vector_to_target = vector_normal_or_fallback(vector_to_target, kBasisZVector);
10379
10380 // do the tracking!
10381 aim_cos = dot_product(vector_to_target, my_aim);
10382 ref_cos = dot_product(vector_to_target, my_ref);
10383
10384
10385 if (ref_cos > TURRET_MINIMUM_COS) // target is forward of self
10386 {
10387 axis_to_track_by = cross_product(vector_to_target, my_aim);
10388 }
10389 else
10390 {
10391 return -2.0; // target is out of fire arc
10392 }
10393
10394 quaternion_rotate_about_axis(&orientation, axis_to_track_by, thrust * delta_t);
10395 [self orientationChanged];
10396
10397 [self setStatus:STATUS_ACTIVE];
10398
10399 return aim_cos;
10400}
10401
10402
10403- (void) setEvasiveJink:(GLfloat) z
10404{
10405 if (accuracy < COMBAT_AI_ISNT_AWFUL)
10406 {
10407 jink = kZeroVector;
10408 }
10409 else
10410 {
10411 jink.x = (ranrot_rand() % 256) - 128.0;
10412 jink.y = (ranrot_rand() % 256) - 128.0;
10413 jink.z = z;
10414
10415 // make sure we don't accidentally have near-zero jink
10416 if (jink.x < 0.0)
10417 {
10418 jink.x -= 128.0;
10419 }
10420 else
10421 {
10422 jink.x += 128.0;
10423 }
10424 if (jink.y < 0)
10425 {
10426 jink.y -= 128.0;
10427 }
10428 else
10429 {
10430 jink.y += 128.0;
10431 }
10432 }
10433}
10434
10435
10436- (void) evasiveAction:(double) delta_t
10437{
10438 stick_roll = flightRoll; //desired roll and pitch
10439 stick_pitch = flightPitch;
10440
10441 ShipEntity* target = [self primaryTarget];
10442 if (!target) // leave now!
10443 {
10444 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10445 return;
10446 }
10447
10448 double agreement = dot_product(v_right,target->v_right);
10449 if (agreement > -0.3 && agreement < 0.3)
10450 {
10451 stick_roll = 0.0;
10452 }
10453 else
10454 {
10455 if (stick_roll >= 0.0) {
10456 stick_roll = max_flight_roll;
10457 } else {
10458 stick_roll = -max_flight_roll;
10459 }
10460 }
10461 if (stick_pitch >= 0.0) {
10462 stick_pitch = max_flight_pitch;
10463 } else {
10464 stick_pitch = -max_flight_pitch;
10465 }
10466
10467 [self applySticks:delta_t];
10468}
10469
10470
10471- (double) trackPrimaryTarget:(double) delta_t :(BOOL) retreat
10472{
10473 Entity* target = [self primaryTarget];
10474
10475 if (!target) // leave now!
10476 {
10477 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10478 return 0.0;
10479 }
10480
10481 if (![self canStillTrackPrimaryTarget])
10482 {
10483 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10484 return 0.0;
10485 }
10486
10487 /* 1.81 change: do the above check first: if a missile can't be
10488 * fired outside scanner range it should self-destruct if the
10489 * target gets far enough away (it's going to miss anyway) -
10490 * CIM */
10491 if (scanClass == CLASS_MISSILE)
10492 return [self missileTrackPrimaryTarget: delta_t];
10493
10494 GLfloat d_forward, d_up, d_right;
10495
10496 Vector relPos = HPVectorToVector(HPvector_subtract([self calculateTargetPosition], position));
10497
10498 double range2 = HPmagnitude2(HPvector_subtract([target position], position));
10499
10500 //jink if retreating
10501 if (retreat) // calculate jink position when flying away from target.
10502 {
10503 Vector vx, vy, vz;
10504 if (target->isShip)
10505 {
10506 ShipEntity* targetShip = (ShipEntity*)target;
10507 vx = targetShip->v_right;
10508 vy = targetShip->v_up;
10509 vz = targetShip->v_forward;
10510 }
10511 else
10512 {
10513 Quaternion q = target->orientation;
10517 }
10518
10519 BOOL avoidCollision = NO;
10520 if (range2 < collision_radius * target->collision_radius * 100.0) // Check direction within 10 * collision radius.
10521 {
10522 Vector targetDirection = kBasisZVector;
10523 if (!vector_equal(relPos, kZeroVector)) targetDirection = vector_normal(relPos);
10524 avoidCollision = (dot_product(targetDirection, v_forward) > -0.1); // is flying toward target or only slightly outward.
10525 }
10526
10527 GLfloat dist_adjust_factor = 1.0;
10528 if (accuracy >= COMBAT_AI_FLEES_BETTER)
10529 {
10530 double range = magnitude(relPos);
10531 if (range > 2000.0)
10532 {
10533 dist_adjust_factor = range / 2000.0;
10534 if (accuracy >= COMBAT_AI_FLEES_BETTER_2)
10535 {
10536 dist_adjust_factor *= 3;
10537 }
10538 }
10539 if (jink.x == 0.0 && behaviour != BEHAVIOUR_RUNNING_DEFENSE)
10540 { // test for zero jink and correct
10541 [self setEvasiveJink:400.0];
10542 }
10543 }
10544
10545 if (!avoidCollision) // it is safe to jink
10546 {
10547 relPos.x += (jink.x * vx.x + jink.y * vy.x + jink.z * vz.x) * dist_adjust_factor;
10548 relPos.y += (jink.x * vx.y + jink.y * vy.y + jink.z * vz.y) * dist_adjust_factor;
10549 relPos.z += (jink.x * vx.z + jink.y * vy.z + jink.z * vz.z);
10550 }
10551
10552 }
10553
10554 if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
10555 else relPos.z = 1.0;
10556
10557 double max_cos = [self currentAimTolerance];
10558
10559 stick_roll = 0.0; //desired roll and pitch
10560 stick_pitch = 0.0;
10561
10562 double reverse = (retreat)? -1.0: 1.0;
10563
10564 double min_d = 0.004; // ~= 40m at 10km
10565 int max_factor = 8;
10566 double r_max_factor = 0.125;
10567 if (!retreat)
10568 {
10569 if (accuracy >= COMBAT_AI_TRACKS_CLOSER)
10570 {
10571 // much greater precision in combat
10572 if (max_flight_pitch > 1.0)
10573 {
10574 max_factor = floor(max_flight_pitch/0.125);
10575 r_max_factor = 1.0/max_factor;
10576 }
10577 min_d = 0.0004; // 10 times more precision ~= 4m at 10km
10578 max_factor *= 3;
10579 r_max_factor /= 3.0;
10580 }
10581 else if (accuracy >= COMBAT_AI_ISNT_AWFUL)
10582 {
10583 // slowly improve precision to target, but only if missing
10584 min_d -= 0.0001 * [self missedShots];
10585 if (min_d < 0.001)
10586 {
10587 min_d = 0.001;
10588 max_factor *= 2;
10589 r_max_factor /= 2.0;
10590 }
10591 }
10592 }
10593
10594 d_right = dot_product(relPos, v_right);
10595 d_up = dot_product(relPos, v_up);
10596 d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target
10597
10598 if (d_forward * reverse > max_cos) // on_target!
10599 {
10600 return d_forward;
10601 }
10602
10603 // begin rule-of-thumb manoeuvres
10604 stick_pitch = 0.0;
10605 stick_roll = 0.0;
10606
10607
10608 if ((reverse * d_forward < -0.5) && !pitching_over) // we're going the wrong way!
10609 pitching_over = YES;
10610
10611 if (pitching_over)
10612 {
10613 if (reverse * d_up > 0) // pitch up
10614 stick_pitch = -max_flight_pitch;
10615 else
10616 stick_pitch = max_flight_pitch;
10617 pitching_over = (reverse * d_forward < 0.707);
10618 }
10619
10620 // check if we are flying toward the destination..
10621 if ((d_forward < max_cos)||(retreat)) // not on course so we must adjust controls..
10622 {
10623 if (d_forward < -max_cos) // hack to avoid just flying away from the destination
10624 {
10625 d_up = min_d * 2.0;
10626 }
10627
10628 if (d_up > min_d)
10629 {
10630 int factor = sqrt(fabs(d_right) / fabs(min_d));
10631 if (factor > max_factor)
10632 factor = max_factor;
10633 if (d_right > min_d)
10634 stick_roll = - max_flight_roll * r_max_factor * factor; // note#
10635 if (d_right < -min_d)
10636 stick_roll = + max_flight_roll * r_max_factor * factor; // note#
10637 }
10638 if (d_up < -min_d)
10639 {
10640 int factor = sqrt(fabs(d_right) / fabs(min_d));
10641 if (factor > max_factor)
10642 factor = max_factor;
10643 if (d_right > min_d)
10644 stick_roll = + max_flight_roll * r_max_factor * factor; // note#
10645 if (d_right < -min_d)
10646 stick_roll = - max_flight_roll * r_max_factor * factor; // note#
10647 }
10648
10649 if (stick_roll == 0.0)
10650 {
10651 int factor = sqrt(fabs(d_up) / fabs(min_d));
10652 if (factor > max_factor)
10653 factor = max_factor;
10654 if (d_up > min_d)
10655 stick_pitch = - max_flight_pitch * reverse * r_max_factor * factor;
10656 if (d_up < -min_d)
10657 stick_pitch = + max_flight_pitch * reverse * r_max_factor * factor;
10658 }
10659
10660 if (accuracy >= COMBAT_AI_ISNT_AWFUL)
10661 {
10662 // don't overshoot target (helps accuracy at low frame rates)
10663 if (fabs(d_right) < fabs(stick_roll) * delta_t)
10664 {
10665 stick_roll = fabs(d_right) / delta_t * (stick_roll<0 ? -1 : 1);
10666 }
10667 if (fabs(d_up) < fabs(stick_pitch) * delta_t)
10668 {
10669 stick_pitch = fabs(d_up) / delta_t * (stick_pitch<0 ? -0.9 : 0.9);
10670 }
10671 }
10672
10673 }
10674 /* # note
10675 Eric 9-9-2010: Removed the "reverse" variable from the stick_roll calculation. This was mathematical wrong and
10676 made the ship roll in the wrong direction, preventing the ship to fly away in a straight line from the target.
10677 This means all the places were a jink was set, this jink never worked correctly. The main reason a ship still
10678 managed to turn at close range was probably by the fail-safe mechanisme with the "pitching_over" variable.
10679 The jink was programmed to do nothing within 500 meters of the ship and just fly away in direct line from the target
10680 in that range. Because of the bug the ships always rolled to the wrong side needed to fly away in direct line
10681 resulting in making it a difficult target.
10682 After fixing the bug, the ship realy flew away in direct line during the first 500 meters, making it a easy target
10683 for the player. All jink settings are retested and changed to give a turning behaviour that felt like the old
10684 situation, but now more deliberately set.
10685 */
10686
10687 // end rule-of-thumb manoeuvres
10688 stick_yaw = 0.0;
10689
10690 [self applySticks:delta_t];
10691
10692 if (retreat)
10693 d_forward *= d_forward; // make positive AND decrease granularity
10694
10695 if (d_forward < 0.0)
10696 return 0.0;
10697
10698 if ((!flightRoll)&&(!flightPitch)) // no correction
10699 return 1.0;
10700
10701 return d_forward;
10702}
10703
10704
10705- (double) trackSideTarget:(double) delta_t :(BOOL) leftside
10706{
10707 Entity* target = [self primaryTarget];
10708
10709 if (!target) // leave now!
10710 {
10711 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10712 return 0.0;
10713 }
10714
10715 if (![self canStillTrackPrimaryTarget])
10716 {
10717 [self noteLostTargetAndGoIdle]; // NOTE: was AI message: rather than reactToMessage:
10718 return 0.0;
10719 }
10720
10721
10722 if (scanClass == CLASS_MISSILE) // never?
10723 return [self missileTrackPrimaryTarget: delta_t];
10724
10725 GLfloat d_forward, d_up, d_right;
10726
10727 Vector relPos = HPVectorToVector(HPvector_subtract([self calculateTargetPosition], position));
10728
10729
10730 if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
10731 else relPos.z = 1.0;
10732
10733// worse shots with side lasers than fore/aft, in general
10734
10735 double max_cos = [self currentAimTolerance];
10736
10737 stick_roll = 0.0; //desired roll and pitch
10738 stick_pitch = 0.0;
10739 stick_yaw = 0.0;
10740
10741 double reverse = (leftside)? -1.0: 1.0;
10742
10743 double min_d = 0.004;
10744 if (accuracy >= COMBAT_AI_TRACKS_CLOSER)
10745 {
10746 min_d = 0.002;
10747 }
10748 int max_factor = 8;
10749 double r_max_factor = 0.125;
10750
10751 d_right = dot_product(relPos, v_right);
10752 d_up = dot_product(relPos, v_up);
10753 d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target
10754
10755 if (d_right * reverse > max_cos) // on_target!
10756 {
10757 return d_right * reverse;
10758 }
10759
10760 // begin rule-of-thumb manoeuvres
10761 stick_pitch = 0.0;
10762 stick_roll = 0.0;
10763 stick_yaw = 0.0;
10764
10765 // check if we are flying toward the destination..
10766 if ((d_right * reverse < max_cos)) // not on course so we must adjust controls..
10767 {
10768 if (d_right < -max_cos) // hack to avoid just pointing away from the destination
10769 {
10770 d_forward = min_d * 2.0;
10771 }
10772
10773 if (d_forward > min_d)
10774 {
10775 int factor = sqrt(fabs(d_up) / fabs(min_d));
10776 if (factor > max_factor)
10777 factor = max_factor;
10778 if (d_up > min_d)
10779 stick_pitch = + max_flight_pitch * r_max_factor * factor; // note#
10780 if (d_up < -min_d)
10781 stick_pitch = - max_flight_pitch * r_max_factor * factor; // note#
10782 }
10783 if (d_forward < -min_d)
10784 {
10785 int factor = sqrt(fabs(d_up) / fabs(min_d));
10786 if (factor > max_factor)
10787 factor = max_factor;
10788 if (d_up > min_d)
10789 stick_pitch = + max_flight_pitch * r_max_factor * factor; // note#
10790 if (d_up < -min_d)
10791 stick_pitch = - max_flight_pitch * r_max_factor * factor; // note#
10792 }
10793
10794 if (fabs(stick_pitch) == 0.0 || fabs(d_forward) > 0.5)
10795 {
10796 stick_pitch = 0.0;
10797 int factor = sqrt(fabs(d_forward) / fabs(min_d));
10798 if (factor > max_factor)
10799 factor = max_factor;
10800 if (d_forward > min_d)
10801 stick_yaw = - max_flight_yaw * reverse * r_max_factor * factor;
10802 if (d_forward < -min_d)
10803 {
10804 if (factor < max_factor/2.0) // compensate for forward thrust
10805 factor *= 2.0;
10806 stick_yaw = + max_flight_yaw * reverse * r_max_factor * factor;
10807 }
10808 }
10809 }
10810
10811
10812 // end rule-of-thumb manoeuvres
10813
10814 [self applySticks:delta_t];
10815
10816 if ((!flightPitch)&&(!flightYaw)) // no correction
10817 return 1.0;
10818
10819 return d_right * reverse;
10820}
10821
10822
10823
10824- (double) missileTrackPrimaryTarget:(double) delta_t
10825{
10826 Vector relPos;
10827 GLfloat d_forward, d_up, d_right;
10828 ShipEntity *target = [self primaryTarget];
10829 BOOL inPursuit = YES;
10830
10831 if (!target || ![target isShip]) // leave now!
10832 return 0.0;
10833
10834 double damping = 0.5 * delta_t;
10835
10836 stick_roll = 0.0; //desired roll and pitch
10837 stick_pitch = 0.0;
10838 stick_yaw = 0.0;
10839
10840 relPos = [self vectorTo:target];
10841
10842 // Adjust missile course by taking into account target's velocity and missile
10843 // accuracy. Modification on original code contributed by Cmdr James.
10844
10845 float missileSpeed = (float)[self speed];
10846
10847 // Avoid getting ourselves in a divide by zero situation by setting a missileSpeed
10848 // low threshold. Arbitrarily chosen 0.01, since it seems to work quite well.
10849 // Missile accuracy is already clamped within the 0.0 to 10.0 range at initialization,
10850 // but doing these calculations every frame when accuracy equals 0.0 just wastes cycles.
10851 if (missileSpeed > 0.01f && accuracy > 0.0f)
10852 {
10853 inPursuit = (dot_product([target forwardVector], v_forward) > 0.0f);
10854 if (inPursuit)
10855 {
10856 Vector leading = [target velocity];
10857 float lead = magnitude(relPos) / missileSpeed;
10858
10859 // Adjust where we are going to take into account target's velocity.
10860 // Use accuracy value to determine how well missile will track target.
10861 relPos.x += (lead * leading.x * (accuracy / 10.0f));
10862 relPos.y += (lead * leading.y * (accuracy / 10.0f));
10863 relPos.z += (lead * leading.z * (accuracy / 10.0f));
10864 }
10865 }
10866
10867 if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
10868 else relPos.z = 1.0;
10869
10870 d_right = dot_product(relPos, v_right); // = cosine of angle between angle to target and v_right
10871 d_up = dot_product(relPos, v_up); // = cosine of angle between angle to target and v_up
10872 d_forward = dot_product(relPos, v_forward); // = cosine of angle between angle to target and v_forward
10873
10874 // begin rule-of-thumb manoeuvres
10875
10876 stick_roll = 0.0;
10877
10878 if (pitching_over)
10879 pitching_over = (stick_pitch != 0.0);
10880
10881 if ((d_forward < -pitch_tolerance) && (!pitching_over))
10882 {
10883 pitching_over = YES;
10884 if (d_up >= 0)
10885 stick_pitch = -max_flight_pitch;
10886 if (d_up < 0)
10887 stick_pitch = max_flight_pitch;
10888 }
10889
10890 if (pitching_over)
10891 {
10892 pitching_over = (d_forward < 0.5);
10893 }
10894 else
10895 {
10896 stick_pitch = -max_flight_pitch * d_up;
10897 stick_roll = -max_flight_roll * d_right;
10898 }
10899
10900 // end rule-of-thumb manoeuvres
10901
10902 // apply damping
10903 if (flightRoll < 0)
10904 flightRoll += (flightRoll < -damping) ? damping : -flightRoll;
10905 if (flightRoll > 0)
10906 flightRoll -= (flightRoll > damping) ? damping : flightRoll;
10907 if (flightPitch < 0)
10908 flightPitch += (flightPitch < -damping) ? damping : -flightPitch;
10909 if (flightPitch > 0)
10910 flightPitch -= (flightPitch > damping) ? damping : flightPitch;
10911
10912
10913 [self applySticks:delta_t];
10914
10915 //
10916 // return target confidence 0.0 .. 1.0
10917 //
10918 if (d_forward < 0.0)
10919 return 0.0;
10920 return d_forward;
10921}
10922
10923
10924- (double) trackDestination:(double) delta_t :(BOOL) retreat
10925{
10926 Vector relPos;
10927 GLfloat d_forward, d_up, d_right;
10928
10929 BOOL we_are_docking = (nil != dockingInstructions);
10930
10931 stick_roll = 0.0; //desired roll and pitch
10932 stick_pitch = 0.0;
10933 stick_yaw = 0.0;
10934
10935 double reverse = 1.0;
10936 double reversePlayer = 1.0;
10937
10938 double min_d = 0.004;
10939 double max_cos = MAX_COS; // should match default value of max_cos in behaviour_fly_to_destination!
10940 double precision = we_are_docking ? 0.25 : 0.9025; // lower values force a direction closer to the target. (resp. 50% and 95% within range)
10941
10942 if (retreat)
10943 reverse = -reverse;
10944
10945 if (isPlayer)
10946 {
10947 reverse = -reverse;
10948 reversePlayer = -1;
10949 }
10950
10951 relPos = HPVectorToVector(HPvector_subtract(_destination, position));
10952 double range2 = magnitude2(relPos);
10953 double desired_range2 = desired_range*desired_range;
10954
10955 /* 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.
10956 Because of the framerate most ships normally overshoot the target and they end up flying clearly on a path
10957 through the sphere. Those ships give no problems, but ships with a very low turnrate will aim close to the surface and will than
10958 have large trouble with reaching their destination. When those ships enter the slowdown range, they have almost no speed vector
10959 in the direction of the target. I now used 95% of desired_range to aim at, but a smaller value might even be better.
10960 */
10961 if (range2 > desired_range2)
10962 {
10963 max_cos = sqrt(1 - precision * desired_range2/range2); // Head for a point within 95% of desired_range.
10964 if (max_cos >= 0.99999)
10965 {
10966 max_cos = 0.99999;
10967 }
10968 }
10969
10970 if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
10971 else relPos.z = 1.0;
10972
10973 d_right = dot_product(relPos, v_right);
10974 d_up = dot_product(relPos, v_up);
10975 d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target
10976
10977 // begin rule-of-thumb manoeuvres
10978 stick_pitch = 0.0;
10979 stick_roll = 0.0;
10980
10981 // pitching_over is currently only set in behaviour_formation_form_up, for escorts and in avoidCollision.
10982 // This allows for immediate pitch corrections instead of first waiting untill roll has completed.
10983 if (pitching_over)
10984 {
10985 if (reverse * d_up > 0) // pitch up
10986 stick_pitch = -max_flight_pitch;
10987 else
10988 stick_pitch = max_flight_pitch;
10989 pitching_over = (reverse * d_forward < 0.707);
10990 }
10991
10992 // check if we are flying toward (or away from) the destination..
10993 if ((d_forward < max_cos)||(retreat)) // not on course so we must adjust controls..
10994 {
10995
10996 if (d_forward <= -max_cos || (retreat && d_forward >= max_cos)) // hack to avoid just flying away from the destination
10997 {
10998 d_up = min_d * 2.0;
10999 }
11000
11001 if (d_up > min_d)
11002 {
11003 int factor = sqrt(fabs(d_right) / fabs(min_d));
11004 if (factor > 8)
11005 factor = 8;
11006 if (d_right > min_d)
11007 stick_roll = - max_flight_roll * reversePlayer * 0.125 * factor; // only reverse sign for the player;
11008 if (d_right < -min_d)
11009 stick_roll = + max_flight_roll * reversePlayer * 0.125 * factor;
11010 if (fabs(d_right) < fabs(stick_roll) * delta_t)
11011 stick_roll = fabs(d_right) / delta_t * (stick_roll<0 ? -1 : 1); // don't overshoot heading
11012 }
11013
11014 if (d_up < -min_d)
11015 {
11016 int factor = sqrt(fabs(d_right) / fabs(min_d));
11017 if (factor > 8)
11018 factor = 8;
11019 if (d_right > min_d)
11020 stick_roll = + max_flight_roll * reversePlayer * 0.125 * factor; // only reverse sign for the player;
11021 if (d_right < -min_d)
11022 stick_roll = - max_flight_roll * reversePlayer * 0.125 * factor;
11023 if (fabs(d_right) < fabs(stick_roll) * delta_t)
11024 stick_roll = fabs(d_right) / delta_t * (stick_roll<0 ? -1 : 1); // don't overshoot heading
11025 }
11026
11027 if (stick_roll == 0.0)
11028 {
11029 int factor = sqrt(fabs(d_up) / fabs(min_d));
11030 if (factor > 8)
11031 factor = 8;
11032 if (d_up > min_d)
11033 stick_pitch = - max_flight_pitch * reverse * 0.125 * factor; //pitch_pitch * reverse;
11034 if (d_up < -min_d)
11035 stick_pitch = + max_flight_pitch * reverse * 0.125 * factor;
11036 if (fabs(d_up) < fabs(stick_pitch) * delta_t)
11037 stick_pitch = fabs(d_up) / delta_t * (stick_pitch<0 ? -1 : 1); // don't overshoot heading
11038 }
11039
11040 if (stick_pitch == 0.0)
11041 {
11042 // not sufficiently on course yet, but min_d is too high
11043 // turn anyway slightly to adjust
11044 stick_pitch = 0.01;
11045 }
11046 }
11047
11048 if (we_are_docking && docking_match_rotation && (d_forward > max_cos))
11049 {
11050 /* we are docking and need to consider the rotation/orientation of the docking port */
11051 StationEntity* station_for_docking = (StationEntity*)[self targetStation];
11052
11053 if ((station_for_docking)&&(station_for_docking->isStation))
11054 {
11055 stick_roll = [self rollToMatchUp:[station_for_docking portUpVectorForShip:self] rotating:[station_for_docking flightRoll]];
11056 }
11057 }
11058
11059 // end rule-of-thumb manoeuvres
11060
11061 [self applySticks:delta_t];
11062
11063 if (retreat)
11064 d_forward *= d_forward; // make positive AND decrease granularity
11065
11066 if (d_forward < 0.0)
11067 return 0.0;
11068
11069 if ((!flightRoll)&&(!flightPitch)) // no correction
11070 return 1.0;
11071
11072 return d_forward;
11073}
11074
11075
11076- (GLfloat) rollToMatchUp:(Vector)up_vec rotating:(GLfloat)match_roll
11077{
11078 GLfloat cosTheta = dot_product(up_vec, v_up); // == cos of angle between up vectors
11079 GLfloat sinTheta = dot_product(up_vec, v_right);
11080
11081 if (!isPlayer)
11082 {
11083 match_roll = -match_roll; // make necessary corrections for a different viewpoint
11084 sinTheta = -sinTheta;
11085 }
11086
11087 if (cosTheta < 0.0f)
11088 {
11089 cosTheta = -cosTheta;
11090 sinTheta = -sinTheta;
11091 }
11092
11093 if (sinTheta > 0.0f)
11094 {
11095 // increase roll rate
11096 return cosTheta * cosTheta * match_roll + sinTheta * sinTheta * max_flight_roll;
11097 }
11098 else
11099 {
11100 // decrease roll rate
11101 return cosTheta * cosTheta * match_roll - sinTheta * sinTheta * max_flight_roll;
11102 }
11103}
11104
11105
11106- (GLfloat) rangeToDestination
11107{
11108 return HPdistance(position, _destination);
11109}
11110
11111
11112- (NSArray *) collisionExceptions
11113{
11114 if (_collisionExceptions == nil)
11115 {
11116 return [NSArray array];
11117 }
11118 return [_collisionExceptions allObjects];
11119}
11120
11121
11122- (void) addCollisionException:(ShipEntity *)ship
11123{
11124 if (_collisionExceptions == nil)
11125 {
11126 // Allocate lazily for the benefit of the ships that never need this.
11127 _collisionExceptions = [[OOWeakSet alloc] init];
11128 }
11129 [_collisionExceptions addObject:ship];
11130}
11131
11132
11133- (void) removeCollisionException:(ShipEntity *)ship
11134{
11135 if (_collisionExceptions != nil)
11136 {
11137 [_collisionExceptions removeObject:ship];
11138 }
11139}
11140
11141
11142- (BOOL) collisionExceptedFor:(ShipEntity *)ship
11143{
11144 if (_collisionExceptions == nil)
11145 {
11146 return NO;
11147 }
11148 return [_collisionExceptions containsObject:ship];
11149}
11150
11151
11152
11153- (NSUInteger) defenseTargetCount
11154{
11155 return [_defenseTargets count];
11156}
11157
11158
11159- (NSArray *) allDefenseTargets
11160{
11161 return [_defenseTargets allObjects];
11162}
11163
11164
11165- (NSEnumerator *) defenseTargetEnumerator
11166{
11167 return [_defenseTargets objectEnumerator];
11168}
11169
11170
11171- (BOOL) addDefenseTarget:(Entity *)target
11172{
11173 if ([self defenseTargetCount] >= MAX_TARGETS)
11174 {
11175 return NO;
11176 }
11177 // primary target can be a wormhole, defense targets shouldn't be
11178 if (target == nil || [self isDefenseTarget:target] || ![target isShip])
11179 {
11180 return NO;
11181 }
11182 if (_defenseTargets == nil)
11183 {
11184 // Allocate lazily for the benefit of the ships that never get in fights.
11185 _defenseTargets = [[OOWeakSet alloc] init];
11186 }
11187
11188 [_defenseTargets addObject:target];
11189 return YES;
11190}
11191
11192
11193- (void) validateDefenseTargets
11194{
11195 if (_defenseTargets == nil)
11196 {
11197 return;
11198 }
11199 // get enumerator from array as we'll be modifying original during enumeration
11200 NSEnumerator *defTargets = [[self allDefenseTargets] objectEnumerator];
11201 Entity *target = nil;
11202 while ((target = [[defTargets nextObject] weakRefUnderlyingObject]))
11203 {
11204 if ([target status] == STATUS_DEAD)
11205 {
11206 [self removeDefenseTarget:target];
11207 }
11208 }
11209}
11210
11211
11212- (BOOL) isDefenseTarget:(Entity *)target
11213{
11214 return [_defenseTargets containsObject:target];
11215}
11216
11217
11218// exposed to AI (as alias of clearDefenseTargets)
11219- (void) removeAllDefenseTargets
11220{
11221 [_defenseTargets removeAllObjects];
11222}
11223
11224
11225- (void) removeDefenseTarget:(Entity *)target
11226{
11227 [_defenseTargets removeObject:target];
11228}
11229
11230
11231- (double) rangeToPrimaryTarget
11232{
11233 return [self rangeToSecondaryTarget:[self primaryTarget]];
11234}
11235
11236
11237- (double) rangeToSecondaryTarget:(Entity *)target
11238{
11239 double dist;
11240 Vector delta;
11241 if (target == nil) // leave now!
11242 return 0.0;
11243 delta = HPVectorToVector(HPvector_subtract(target->position, position));
11244 dist = magnitude(delta);
11245 dist -= target->collision_radius;
11246 dist -= collision_radius;
11247 return dist;
11248}
11249
11250
11251- (double) approachAspectToPrimaryTarget
11252{
11253 Vector delta;
11254 Entity *target = [self primaryTarget];
11255 if (target == nil || ![target isShip]) // leave now!
11256 {
11257 return 0.0;
11258 }
11259 ShipEntity *ship_target = (ShipEntity *)target;
11260
11261 delta = HPVectorToVector(HPvector_subtract(position, target->position));
11262
11263 return dot_product(vector_normal(delta), ship_target->v_forward);
11264}
11265
11266
11267- (BOOL) hasProximityAlertIgnoringTarget:(BOOL)ignore_target
11268{
11269 if (([self proximityAlert] != nil)&&(!ignore_target || ([self proximityAlert] != [self primaryTarget])))
11270 {
11271 return YES;
11272 }
11273 return NO;
11274}
11275
11276
11277// lower is better. Defines angular size of circle in which ship
11278// thinks is on target
11279- (GLfloat) currentAimTolerance
11280{
11281 GLfloat basic_aim = aim_tolerance;
11282 GLfloat best_cos = 0.99999; // ~45m in 10km (track won't go better than 40)
11283 if (accuracy >= COMBAT_AI_ISNT_AWFUL)
11284 {
11285 // better general targeting
11286 best_cos = 0.999999; // ~14m in 10km (track won't go better than 10)
11287 // if missing, aim better!
11288 basic_aim /= 1.0 + ((GLfloat)[self missedShots] / 4.0);
11289 }
11290 if (accuracy >= COMBAT_AI_TRACKS_CLOSER)
11291 {
11292 // deadly shots
11293 best_cos = 0.9999999; // ~4m in 10km (track won't go better than 4)
11294 // and start with extremely good aim circle
11295 basic_aim /= 5.0;
11296 }
11297 if (currentWeaponFacing == WEAPON_FACING_AFT && accuracy < COMBAT_AI_ISNT_AWFUL)
11298 { // bad shots with aft lasers
11299 basic_aim *= 1.3;
11300 }
11301 else if (currentWeaponFacing == WEAPON_FACING_PORT || currentWeaponFacing == WEAPON_FACING_STARBOARD)
11302 { // everyone a bit worse with side lasers
11303 if (accuracy < COMBAT_AI_ISNT_AWFUL)
11304 { // especially these
11305 basic_aim *= 1.3 + randf();
11306 }
11307 else
11308 {
11309 basic_aim *= 1.3;
11310 }
11311 }
11312 // only apply glare if ship is not shadowed
11313 if (isSunlit) {
11314 OOSunEntity *sun = [UNIVERSE sun];
11315 if (sun)
11316 {
11317 GLfloat sunGlareAngularSize = atan([sun radius]/HPdistance([self position], [sun position])) * SUN_GLARE_MULT_FACTOR + (SUN_GLARE_ADD_FACTOR);
11318 GLfloat glareLevel = [self lookingAtSunWithThresholdAngleCos:cos(sunGlareAngularSize)] * (1.0f - [self sunGlareFilter]);
11319 if (glareLevel > 0.1f)
11320 {
11321 // looking towards sun can seriously mess up aim (glareLevel 0..1)
11322 basic_aim *= (1.0 + glareLevel*3.0);
11323// OOLog(@"aim.debug",@"Sun glare affecting aim: %f for %@",glareLevel,self);
11324 if (glareLevel > 0.5f)
11325 {
11326 // strong glare makes precise targeting impossible
11327 best_cos = 0.99999;
11328 }
11329 }
11330 }
11331 }
11332
11333
11334 GLfloat max_cos = sqrt(1-(basic_aim * basic_aim / 100000000.0));
11335
11336 if (max_cos < best_cos)
11337 {
11338 return max_cos;
11339 }
11340 return best_cos;
11341}
11342
11343
11344// much simpler than player version
11345- (GLfloat) lookingAtSunWithThresholdAngleCos:(GLfloat) thresholdAngleCos
11346{
11347 OOSunEntity *sun = [UNIVERSE sun];
11348 GLfloat measuredCos = 999.0f, measuredCosAbs;
11349 GLfloat sunBrightness = 0.0f;
11350 Vector relativePosition, unitRelativePosition;
11351
11352 if (EXPECT_NOT(!sun)) return 0.0f;
11353
11354 relativePosition = HPVectorToVector(HPvector_subtract([self position], [sun position]));
11355 unitRelativePosition = vector_normal_or_zbasis(relativePosition);
11356 switch (currentWeaponFacing)
11357 {
11359 measuredCos = -dot_product(unitRelativePosition, v_forward);
11360 break;
11361 case WEAPON_FACING_AFT:
11362 measuredCos = +dot_product(unitRelativePosition, v_forward);
11363 break;
11364 case WEAPON_FACING_PORT:
11365 measuredCos = +dot_product(unitRelativePosition, v_right);
11366 break;
11368 measuredCos = -dot_product(unitRelativePosition, v_right);
11369 break;
11370 default:
11371 break;
11372 }
11373 measuredCosAbs = fabs(measuredCos);
11374 if (thresholdAngleCos <= measuredCosAbs && measuredCosAbs <= 1.0f) // angle from viewpoint to sun <= desired threshold
11375 {
11376 sunBrightness = (measuredCos - thresholdAngleCos) / (1.0f - thresholdAngleCos);
11377 if (sunBrightness < 0.0f) sunBrightness = 0.0f;
11378 }
11379 return sunBrightness * sunBrightness * sunBrightness;
11380}
11381
11382
11383- (BOOL) onTarget:(OOWeaponFacing)direction withWeapon:(OOWeaponType)weapon_type
11384{
11385 // initialize dq to a value that would normally return NO; dq is handled inside the defaultless switch(direction) statement
11386 // and should alaways be recalculated anyway. Initialization here needed to silence compiler warning - Nikos 20120526
11387 GLfloat dq = -1.0f;
11388 GLfloat d2, radius, astq;
11389 Vector rel_pos, urp;
11390 if ([weapon_type isTurretLaser])
11391 {
11392 return YES;
11393 }
11394
11395 Entity *target = [self primaryTarget];
11396 if (target == nil) return NO;
11397 if ([target status] == STATUS_DEAD) return NO;
11398
11399 if (isSunlit && (target->isSunlit == NO) && (randf() < 0.75))
11400 {
11401 return NO; // 3/4 of the time you can't see from a lit place into a darker place
11402 }
11403 radius = target->collision_radius;
11404 rel_pos = HPVectorToVector(HPvector_subtract([self calculateTargetPosition], position));
11405 d2 = magnitude2(rel_pos);
11406 urp = vector_normal_or_zbasis(rel_pos);
11407
11408 switch (direction)
11409 {
11411 dq = +dot_product(urp, v_forward); // cosine of angle between v_forward and unit relative position
11412 break;
11413
11414 case WEAPON_FACING_AFT:
11415 dq = -dot_product(urp, v_forward); // cosine of angle between v_forward and unit relative position
11416 break;
11417
11418 case WEAPON_FACING_PORT:
11419 dq = -dot_product(urp, v_right); // cosine of angle between v_right and unit relative position
11420 break;
11421
11423 dq = +dot_product(urp, v_right); // cosine of angle between v_right and unit relative position
11424 break;
11425
11426 case WEAPON_FACING_NONE:
11427 break;
11428 }
11429
11430 if (dq < 0.0) return NO;
11431
11432 GLfloat aim = [self currentAimTolerance];
11433 if (dq > aim*aim) return YES;
11434
11435 // cosine of 1/3 of half angle subtended by target (mostly they'll
11436 // fire sooner anyway due to currentAimTolerance, but this should
11437 // almost always be a solid hit)
11438 astq = sqrt(1.0 - radius * radius / (d2 * 9));
11439
11440 return (fabs(dq) >= astq);
11441}
11442
11443
11444- (BOOL) fireWeapon:(OOWeaponType)weapon_type direction:(OOWeaponFacing)direction range:(double)range
11445{
11446 weapon_temp = 0.0;
11447 switch (direction)
11448 {
11450 weapon_temp = forward_weapon_temp;
11451 break;
11452
11453 case WEAPON_FACING_AFT:
11454 weapon_temp = aft_weapon_temp;
11455 break;
11456
11457 case WEAPON_FACING_PORT:
11458 weapon_temp = port_weapon_temp;
11459 break;
11460
11462 weapon_temp = starboard_weapon_temp;
11463 break;
11464
11465 case WEAPON_FACING_NONE:
11466 break;
11467 }
11468 if (weapon_temp / NPC_MAX_WEAPON_TEMP >= WEAPON_COOLING_CUTOUT) return NO;
11469
11470 NSUInteger multiplier = 1;
11471 if (_multiplyWeapons)
11472 {
11473 // multiple fitted
11474 multiplier = [[self laserPortOffset:direction] count];
11475 }
11476
11477 if (energy <= weapon_energy_use * multiplier) return NO;
11478 if ([self shotTime] < weapon_recharge_rate) return NO;
11479 if (![weapon_type isTurretLaser])
11480 { // thargoid laser may just pick secondary target in this case
11481 if (range > randf() * weaponRange * (accuracy+7.5)) return NO;
11482 if (range > weaponRange) return NO;
11483 }
11484 if (![self onTarget:direction withWeapon:weapon_type]) return NO;
11485
11486 BOOL fired = NO;
11487 if (!isWeaponNone(weapon_type))
11488 {
11489 if ([weapon_type isTurretLaser])
11490 {
11491 [self fireDirectLaserShot:range];
11492 fired = YES;
11493 }
11494 else
11495 {
11496 [self fireLaserShotInDirection:direction weaponIdentifier:[weapon_type identifier]];
11497 fired = YES;
11498 }
11499 }
11500
11501 if (fired)
11502 {
11503 energy -= weapon_energy_use * multiplier;
11504 switch (direction)
11505 {
11507 forward_weapon_temp += weapon_shot_temperature * multiplier;
11508 break;
11509
11510 case WEAPON_FACING_AFT:
11511 aft_weapon_temp += weapon_shot_temperature * multiplier;
11512 break;
11513
11514 case WEAPON_FACING_PORT:
11515 port_weapon_temp += weapon_shot_temperature * multiplier;
11516 break;
11517
11519 starboard_weapon_temp += weapon_shot_temperature * multiplier;
11520 break;
11521
11522 case WEAPON_FACING_NONE:
11523 break;
11524 }
11525 }
11526
11527 if (direction == WEAPON_FACING_FORWARD)
11528 {
11529 //can we fire lasers from our subentities?
11530 ShipEntity *se = nil;
11531 foreach (se, [self shipSubEntityEnumerator])
11532 {
11533 if ([se fireSubentityLaserShot:range])
11534 {
11535 fired = YES;
11536 }
11537 }
11538 }
11539
11540 if (fired && cloaking_device_active && cloakPassive)
11541 {
11542 [self deactivateCloakingDevice];
11543 }
11544
11545 return fired;
11546}
11547
11548
11549- (BOOL) fireMainWeapon:(double)range
11550{
11551 // set the values from forward_weapon_type.
11552 // OXPs can override the default front laser energy damage.
11553 currentWeaponFacing = WEAPON_FACING_FORWARD;
11554 [self setWeaponDataFromType:forward_weapon_type];
11555
11556// weapon damage override no longer effective
11557// weapon_damage = weapon_damage_override;
11558
11559 BOOL result = [self fireWeapon:forward_weapon_type direction:WEAPON_FACING_FORWARD range:range];
11560 if (isWeaponNone(forward_weapon_type))
11561 {
11562 // need to check subentities to avoid AI oddities
11563 // will already have fired them by now, though
11564 NSEnumerator *subEnum = [self shipSubEntityEnumerator];
11565 ShipEntity *se = nil;
11566 OOWeaponType weapon_type = nil;
11567 BOOL hasTurrets = NO;
11568 while (isWeaponNone(weapon_type) && (se = [subEnum nextObject]))
11569 {
11570 weapon_type = se->forward_weapon_type;
11571 weapon_temp = se->forward_weapon_temp;
11572 if (se->behaviour == BEHAVIOUR_TRACK_AS_TURRET)
11573 {
11574 hasTurrets = YES;
11575 }
11576 }
11577 if (isWeaponNone(weapon_type) && hasTurrets)
11578 { /* no forward weapon but has turrets, so set up range calculations accordingly
11579 note: this was hard-coded to 10000.0, although turrets have a notably
11580 shorter range. We are using a multiplier of 1.667 in order to not change
11581 something that already works, but probably it would be best to use
11582 TURRET_SHOT_RANGE * COMBAT_WEAPON_RANGE_FACTOR here
11583 */
11584 weaponRange = TURRET_SHOT_RANGE * 1.667;
11585 }
11586 else
11587 {
11588 [self setWeaponDataFromType:weapon_type];
11589 }
11590 }
11591 return result;
11592}
11593
11594
11595- (BOOL) fireAftWeapon:(double)range
11596{
11597 // set the values from aft_weapon_type.
11598 currentWeaponFacing = WEAPON_FACING_AFT;
11599 [self setWeaponDataFromType:aft_weapon_type];
11600
11601 return [self fireWeapon:aft_weapon_type direction:WEAPON_FACING_AFT range:range];
11602}
11603
11604
11605- (BOOL) firePortWeapon:(double)range
11606{
11607 // set the values from port_weapon_type.
11608 currentWeaponFacing = WEAPON_FACING_PORT;
11609 [self setWeaponDataFromType:port_weapon_type];
11610
11611 return [self fireWeapon:port_weapon_type direction:WEAPON_FACING_PORT range:range];
11612}
11613
11614
11615- (BOOL) fireStarboardWeapon:(double)range
11616{
11617 // set the values from starboard_weapon_type.
11618 currentWeaponFacing = WEAPON_FACING_STARBOARD;
11619 [self setWeaponDataFromType:starboard_weapon_type];
11620
11621 return [self fireWeapon:starboard_weapon_type direction:WEAPON_FACING_STARBOARD range:range];
11622}
11623
11624
11625- (OOTimeDelta) shotTime
11626{
11627 return shot_time;
11628}
11629
11630
11631- (void) resetShotTime
11632{
11633 shot_time = 0.0;
11634}
11635
11636
11637- (BOOL) fireTurretCannon:(double) range
11638{
11639 if ([self shotTime] < weapon_recharge_rate)
11640 return NO;
11641 if (range > weaponRange * 1.01) // 1% more than max range - open up just slightly early
11642 return NO;
11643 ShipEntity *root = [self rootShipEntity];
11644 if ([root isPlayer] && ![PLAYER weaponsOnline])
11645 return NO;
11646
11647 if ([root isCloaked] && [root cloakPassive])
11648 {
11649 // can't fire turrets while cloaked
11650 return NO;
11651 }
11652
11653 Vector vel;
11654 HPVector origin = [self position];
11655
11656 Entity *last = nil;
11657 Entity *father = [self parentEntity];
11658 OOMatrix r_mat;
11659
11660 vel = vector_forward_from_quaternion(orientation); // Facing
11661 // adjust velocity and position vectors to absolute coordinates
11662 while ((father)&&(father != last) && (father != NO_TARGET))
11663 {
11664 r_mat = [father drawRotationMatrix];
11665 origin = HPvector_add(OOHPVectorMultiplyMatrix(origin, r_mat), [father position]);
11666 vel = OOVectorMultiplyMatrix(vel, r_mat);
11667 last = father;
11668 if (![last isSubEntity]) break;
11669 father = [father owner];
11670 }
11671
11672 origin = HPvector_add(origin, vectorToHPVector(vector_multiply_scalar(vel, collision_radius + 0.5))); // Start just outside collision sphere
11673 vel = vector_multiply_scalar(vel, TURRET_SHOT_SPEED); // Shot velocity
11674
11675 OOPlasmaShotEntity *shot = [[OOPlasmaShotEntity alloc] initWithPosition:origin
11676 velocity:vel
11677 energy:weapon_damage
11678 duration:weaponRange/TURRET_SHOT_SPEED
11679 color:laser_color];
11680
11681 [shot autorelease];
11682 [UNIVERSE addEntity:shot];
11683 [shot setOwner:[self rootShipEntity]]; // has to be done AFTER adding shot to the UNIVERSE
11684
11685 [self resetShotTime];
11686 return YES;
11687}
11688
11689
11690- (void) setLaserColor:(OOColor *) color
11691{
11692 if (color)
11693 {
11694 [laser_color release];
11695 laser_color = [color retain];
11696 }
11697}
11698
11699
11700- (void) setExhaustEmissiveColor:(OOColor *) color
11701{
11702 if (color)
11703 {
11704 [exhaust_emissive_color release];
11705 exhaust_emissive_color = [color retain];
11706 }
11707}
11708
11709
11710- (OOColor *)laserColor
11711{
11712 return [[laser_color retain] autorelease];
11713}
11714
11715
11716- (OOColor *)exhaustEmissiveColor
11717{
11718 return [[exhaust_emissive_color retain] autorelease];
11719}
11720
11721
11722- (BOOL) fireSubentityLaserShot:(double)range
11723{
11724 [self setShipHitByLaser:nil];
11725
11726 if (isWeaponNone(forward_weapon_type)) return NO;
11727 [self setWeaponDataFromType:forward_weapon_type];
11728
11729 ShipEntity *parent = [self owner];
11730 NSAssert([parent isShipWithSubEntityShip:self], @"-fireSubentityLaserShot: called on ship which is not a subentity.");
11731
11732 // subentity lasers still draw power from the main entity
11733 if ([parent energy] <= weapon_energy_use) return NO;
11734 if ([self shotTime] < weapon_recharge_rate) return NO;
11735 if (forward_weapon_temp > WEAPON_COOLING_CUTOUT * NPC_MAX_WEAPON_TEMP) return NO;
11736 if (range > weaponRange) return NO;
11737
11738 forward_weapon_temp += weapon_shot_temperature;
11739 [parent setEnergy:([parent energy] - weapon_energy_use)];
11740
11741 GLfloat hitAtRange = weaponRange;
11743 ShipEntity *victim = [UNIVERSE firstShipHitByLaserFromShip:self inDirection:direction offset:kZeroVector gettingRangeFound:&hitAtRange];
11744 [self setShipHitByLaser:victim];
11745
11746 OOLaserShotEntity *shot = [OOLaserShotEntity laserFromShip:self direction:direction offset:kZeroVector];
11747 [shot setColor:laser_color];
11748 [shot setScanClass:CLASS_NO_DRAW];
11749
11750 if (victim != nil)
11751 {
11752 [self adjustMissedShots:-1];
11753
11754 if ([self isPlayer])
11755 {
11756 [PLAYER addRoleForAggression:victim];
11757 }
11758
11759 ShipEntity *subent = [victim subEntityTakingDamage];
11760 if (subent != nil && [victim isFrangible])
11761 {
11762 // do 1% bleed-through damage...
11763 [victim takeEnergyDamage:0.01 * weapon_damage from:self becauseOf:parent weaponIdentifier:[[self weaponTypeForFacing:WEAPON_FACING_FORWARD strict:YES] identifier]];
11764 victim = subent;
11765 }
11766
11767 if (hitAtRange < weaponRange)
11768 {
11769 [victim takeEnergyDamage:weapon_damage from:self becauseOf:parent weaponIdentifier:[[self weaponTypeForFacing:WEAPON_FACING_FORWARD strict:YES] identifier]]; // a very palpable hit
11770
11771 [shot setRange:hitAtRange];
11772 Vector vd = vector_forward_from_quaternion([shot orientation]);
11773 HPVector flash_pos = HPvector_add([shot position], vectorToHPVector(vector_multiply_scalar(vd, hitAtRange)));
11774 [UNIVERSE addLaserHitEffectsAt:flash_pos against:victim damage:weapon_damage color:laser_color];
11775 }
11776 }
11777 else
11778 {
11779 [self adjustMissedShots:+1];
11780
11781 // see ATTACKER_MISSED section of main entity laser routine
11782 if (![parent isCloaked])
11783 {
11784 victim = [parent primaryTarget];
11785
11786 Vector shotDirection = vector_forward_from_quaternion([shot orientation]);
11787 Vector victimDirection = vector_normal(HPVectorToVector(HPvector_subtract([victim position], [parent position])));
11788 if (dot_product(shotDirection, victimDirection) > 0.995) // Within 84.26 degrees
11789 {
11790 if ([self isPlayer])
11791 {
11792 [PLAYER addRoleForAggression:victim];
11793 }
11794 [victim setPrimaryAggressor:parent];
11795 [victim setFoundTarget:parent];
11796 [victim reactToAIMessage:@"ATTACKER_MISSED" context:@"attacker narrowly misses"];
11797 [victim doScriptEvent:OOJSID("shipBeingAttackedUnsuccessfully") withArgument:parent];
11798
11799 }
11800 }
11801 }
11802
11803 [UNIVERSE addEntity:shot];
11804 [self resetShotTime];
11805
11806 return YES;
11807}
11808
11809
11810- (BOOL) fireDirectLaserShot:(double)range
11811{
11812 Entity *my_target = [self primaryTarget];
11813 if (my_target == nil) return [self fireDirectLaserDefensiveShot];
11814 if (range > randf() * weaponRange * (accuracy+5.5)) return [self fireDirectLaserDefensiveShot];
11815 if (range > weaponRange) return [self fireDirectLaserDefensiveShot];
11816 return [self fireDirectLaserShotAt:my_target];
11817}
11818
11819
11820- (BOOL) fireDirectLaserDefensiveShot
11821{
11822 NSEnumerator *targetEnum = [self defenseTargetEnumerator];
11823 Entity *target = nil;
11824 while ((target = [[targetEnum nextObject] weakRefUnderlyingObject]))
11825 {
11826 // can't fire defensively at cloaked ships
11827 if ([target scanClass] == CLASS_NO_DRAW || [(ShipEntity *)target isCloaked] || [target energy] <= 0.0)
11828 {
11829 [self removeDefenseTarget:target];
11830 }
11831 else
11832 {
11833 double range = [self rangeToSecondaryTarget:target];
11834 if (range < weaponRange)
11835 {
11836 return [self fireDirectLaserShotAt:target];
11837 }
11838 else if (range > scannerRange)
11839 {
11840 [self removeDefenseTarget:target];
11841 }
11842 }
11843 }
11844 return NO;
11845}
11846
11847
11848- (BOOL) fireDirectLaserShotAt:(Entity *)my_target
11849{
11850 GLfloat hit_at_range;
11851 double range_limit2 = weaponRange*weaponRange;
11852 Vector r_pos;
11853
11854 r_pos = vector_normal_or_zbasis([self vectorTo:my_target]);
11855
11856 Quaternion q_laser = quaternion_rotation_between(r_pos, kBasisZVector);
11857
11858 GLfloat acc_factor = (10.0 - accuracy) * 0.001;
11859
11860 q_laser.x += acc_factor * (randf() - 0.5); // randomise aim a little (+/- 0.005 at accuracy 0, never miss at accuracy 10)
11861 q_laser.y += acc_factor * (randf() - 0.5);
11862 q_laser.z += acc_factor * (randf() - 0.5);
11863 quaternion_normalize(&q_laser);
11864
11865 Quaternion q_save = orientation; // save rotation
11866 orientation = q_laser; // face in direction of laser
11867 // weapon offset for thargoid lasers is always zero
11868 ShipEntity *victim = [UNIVERSE firstShipHitByLaserFromShip:self inDirection:WEAPON_FACING_FORWARD offset:kZeroVector gettingRangeFound:&hit_at_range];
11869 [self setShipHitByLaser:victim];
11870 orientation = q_save; // restore rotation
11871
11872 Vector vel = vector_multiply_scalar(v_forward, flightSpeed);
11873
11874 // do special effects laser line
11875 OOLaserShotEntity *shot = [OOLaserShotEntity laserFromShip:self direction:WEAPON_FACING_FORWARD offset:kZeroVector];
11876 [shot setColor:laser_color];
11877 [shot setScanClass: CLASS_NO_DRAW];
11878 [shot setPosition: position];
11879 [shot setOrientation: q_laser];
11880 [shot setVelocity: vel];
11881
11882 if (victim != nil)
11883 {
11884 ShipEntity *subent = [victim subEntityTakingDamage];
11885 if (subent != nil && [victim isFrangible])
11886 {
11887 // do 1% bleed-through damage...
11888 [victim takeEnergyDamage: 0.01 * weapon_damage from:self becauseOf:self weaponIdentifier:[[self weaponTypeForFacing:WEAPON_FACING_FORWARD strict:YES] identifier]];
11889 victim = subent;
11890 }
11891
11892 if (hit_at_range * hit_at_range < range_limit2)
11893 {
11894 [victim takeEnergyDamage:weapon_damage from:self becauseOf:self weaponIdentifier:[[self weaponTypeForFacing:WEAPON_FACING_FORWARD strict:YES] identifier]]; // a very palpable hit
11895
11896 [shot setRange:hit_at_range];
11897 Vector vd = vector_forward_from_quaternion([shot orientation]);
11898 HPVector flash_pos = HPvector_add([shot position], vectorToHPVector(vector_multiply_scalar(vd, hit_at_range)));
11899 [UNIVERSE addLaserHitEffectsAt:flash_pos against:victim damage:weapon_damage color:laser_color];
11900 }
11901 }
11902
11903 [UNIVERSE addEntity:shot];
11904
11905 [self resetShotTime];
11906
11907 return YES;
11908}
11909
11910
11911- (NSArray *) laserPortOffset:(OOWeaponFacing)direction
11912{
11913 NSArray *laserPortOffset = nil;
11914 switch (direction)
11915 {
11917 case WEAPON_FACING_NONE:
11918 laserPortOffset = forwardWeaponOffset;
11919 break;
11920
11921 case WEAPON_FACING_AFT:
11922 laserPortOffset = aftWeaponOffset;
11923 break;
11924
11925 case WEAPON_FACING_PORT:
11926 laserPortOffset = portWeaponOffset;
11927 break;
11928
11930 laserPortOffset = starboardWeaponOffset;
11931 break;
11932 }
11933 return laserPortOffset;
11934}
11935
11936
11937- (BOOL) fireLaserShotInDirection:(OOWeaponFacing)direction weaponIdentifier:(NSString *)weaponIdentifier
11938{
11939 double range_limit2 = weaponRange * weaponRange;
11940 GLfloat hit_at_range;
11941 NSUInteger i, barrels;
11942 Vector vel = vector_multiply_scalar(v_forward, flightSpeed);
11943 NSArray *laserPortOffsets = [self laserPortOffset:direction];
11944 OOLaserShotEntity *shot = nil;
11945
11946 barrels = [laserPortOffsets count];
11947 NSMutableArray *shotEntities = [NSMutableArray arrayWithCapacity:barrels];
11948
11949
11950 GLfloat effective_damage = weapon_damage;
11951 if (barrels > 1 && !_multiplyWeapons)
11952 {
11953 // then divide the shot power between the shots
11954 effective_damage /= (GLfloat)barrels;
11955 }
11956
11957 for (i=0;i<barrels;i++)
11958 {
11959 Vector laserPortOffset = [laserPortOffsets oo_vectorAtIndex:i];
11960
11961 last_shot_time = [UNIVERSE getTime];
11962
11963 ShipEntity *victim = [UNIVERSE firstShipHitByLaserFromShip:self inDirection:direction offset:laserPortOffset gettingRangeFound:&hit_at_range];
11964 [self setShipHitByLaser:victim];
11965
11966 shot = [OOLaserShotEntity laserFromShip:self direction:direction offset:laserPortOffset];
11967 if ([self isPlayer])
11968 {
11969 [shotEntities addObject:shot];
11970 }
11971
11972 [shot setColor:laser_color];
11973 [shot setScanClass: CLASS_NO_DRAW];
11974 [shot setVelocity: vel];
11975
11976 if (victim != nil)
11977 {
11978 [self adjustMissedShots:-1];
11979 if ([self isPlayer])
11980 {
11981 [PLAYER addRoleForAggression:victim];
11982 }
11983
11984 /* CRASH in [victim->sub_entities containsObject:subent] here (1.69, OS X/x86).
11985 Analysis: Crash is in _freedHandler called from CFEqual, indicating either a dead
11986 object in victim->sub_entities or dead victim->subentity_taking_damage. I suspect
11987 the latter. Probable solution: dying subentities must cause parent to clean up
11988 properly. This was probably obscured by the entity recycling scheme in the past.
11989 Fix: made subentity_taking_damage a weak reference accessed via a method.
11990 -- Ahruman 20070706, 20080304
11991 */
11992 ShipEntity *subent = [victim subEntityTakingDamage];
11993 if (subent != nil && [victim isFrangible])
11994 {
11995 // do 1% bleed-through damage...
11996 [victim takeEnergyDamage: 0.01 * effective_damage from:self becauseOf:self weaponIdentifier:weaponIdentifier];
11997 victim = subent;
11998 }
11999
12000 if (hit_at_range * hit_at_range < range_limit2)
12001 {
12002 [victim takeEnergyDamage:effective_damage from:self becauseOf:self weaponIdentifier:weaponIdentifier]; // a very palpable hit
12003
12004 [shot setRange:hit_at_range];
12005 Vector vd = vector_forward_from_quaternion([shot orientation]);
12006 HPVector flash_pos = HPvector_add([shot position], vectorToHPVector(vector_multiply_scalar(vd, hit_at_range)));
12007 [UNIVERSE addLaserHitEffectsAt:flash_pos against:victim damage:effective_damage color:laser_color];
12008 }
12009 }
12010 else
12011 {
12012 [self adjustMissedShots:+1];
12013
12014 // shot missed
12015 if (![self isCloaked])
12016 {
12017 victim = [self primaryTarget];
12018 if ([victim isShip]) // it might not be - fixes crash bug
12019 {
12020
12021 /* player currently gets a bit of an advantage here if
12022 * they ambush without having their target actually
12023 * targeted. Though in those circumstances they
12024 * shouldn't be missing their first shot anyway. */
12025 if (dot_product(vector_forward_from_quaternion([shot orientation]),vector_normal([self vectorTo:victim])) > 0.995)
12026 {
12027 /* plausibly aimed at target. Allows reaction
12028 * before attacker actually hits. But we need to
12029 * be able to distinguish in AI from ATTACKED so
12030 * that ships in combat aren't bothered by
12031 * amateurs. So should only respond to
12032 * ATTACKER_MISSED if not already fighting */
12033 if ([self isPlayer])
12034 {
12035 [PLAYER addRoleForAggression:victim];
12036 }
12037 [victim setPrimaryAggressor:self];
12038 [victim setFoundTarget:self];
12039 [victim reactToAIMessage:@"ATTACKER_MISSED" context:@"attacker narrowly misses"];
12040 [victim doScriptEvent:OOJSID("shipBeingAttackedUnsuccessfully") withArgument:self];
12041 }
12042 }
12043 }
12044 }
12045
12046 [UNIVERSE addEntity:shot];
12047
12048 }
12049
12050 if ([self isPlayer])
12051 {
12052 [(PlayerEntity *)self setLastShot:shotEntities];
12053 }
12054
12055 [self resetShotTime];
12056
12057 return YES;
12058}
12059
12060
12061- (void) adjustMissedShots:(int) delta
12062{
12063 if ([self isSubEntity])
12064 {
12065 [[self owner] adjustMissedShots:delta];
12066 }
12067 else
12068 {
12069 _missed_shots += delta;
12070 if (_missed_shots < 0)
12071 {
12072 _missed_shots = 0;
12073 }
12074 }
12075}
12076
12077
12078- (int) missedShots
12079{
12080 if ([self isSubEntity])
12081 {
12082 return [[self owner] missedShots];
12083 }
12084 else
12085 {
12086 return _missed_shots;
12087 }
12088}
12089
12090
12091- (void) throwSparks
12092{
12093 Vector offset =
12094 {
12095 randf() * (boundingBox.max.x - boundingBox.min.x) + boundingBox.min.x,
12096 randf() * (boundingBox.max.y - boundingBox.min.y) + boundingBox.min.y,
12097 randf() * boundingBox.max.z + boundingBox.min.z // rear section only
12098 };
12099 HPVector origin = HPvector_add(position, vectorToHPVector(quaternion_rotate_vector([self normalOrientation], offset)));
12100
12101 float w = boundingBox.max.x - boundingBox.min.x;
12102 float h = boundingBox.max.y - boundingBox.min.y;
12103 float m = (w < h) ? 0.25 * w: 0.25 * h;
12104
12105 float sz = m * (1 + randf() + randf()); // half minimum dimension on average
12106
12107 Vector vel = vector_multiply_scalar(HPVectorToVector(HPvector_subtract(origin, position)), 2.0);
12108
12109 OOColor *color = [OOColor colorWithHue:0.08 + 0.17 * randf() saturation:1.0 brightness:1.0 alpha:1.0];
12110
12111 OOSparkEntity *spark = [[OOSparkEntity alloc] initWithPosition:origin
12112 velocity:vel
12113 duration:2.0 + 3.0 * randf()
12114 size:sz
12115 color:color];
12116
12117 [spark setOwner:self];
12118 [UNIVERSE addEntity:spark];
12119 [spark release];
12120
12121 next_spark_time = randf();
12122}
12123
12124
12125- (void) considerFiringMissile:(double)delta_t
12126{
12127 int missile_chance = 0;
12128 int rhs = 3.2 / delta_t;
12129 if (rhs) missile_chance = 1 + (ranrot_rand() % rhs);
12130
12131 double hurt_factor = 16 * pow(energy/maxEnergy, 4.0);
12132 if (missiles > missile_chance * hurt_factor)
12133 {
12134 [self fireMissile];
12135 }
12136}
12137
12138
12139- (Vector) missileLaunchPosition
12140{
12141 Vector start;
12142 // default launching position
12143 start.x = 0.0f; // in the middle
12144 start.y = boundingBox.min.y - 4.0f; // 4m below bounding box
12145 start.z = boundingBox.max.z + 1.0f; // 1m ahead of bounding box
12146
12147 // custom launching position
12148 start = [shipinfoDictionary oo_vectorForKey:@"missile_launch_position" defaultValue:start];
12149 if (EXPECT_NOT(_scaleFactor != 1.0))
12150 {
12151 start = vector_multiply_scalar(start,_scaleFactor);
12152 }
12153
12154 if (start.x == 0.0f && start.y == 0.0f && start.z <= 0.0f) // The kZeroVector as start is illegal also.
12155 {
12156 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);
12157 start.x = 0.0f;
12158 start.y = boundingBox.min.y - 4.0f;
12159 start.z = boundingBox.max.z + 1.0f;
12160 }
12161 return start;
12162}
12163
12164
12165- (ShipEntity *) fireMissile
12166{
12167 return [self fireMissileWithIdentifier:nil andTarget:[self primaryTarget]];
12168}
12169
12170
12171- (ShipEntity *) fireMissileWithIdentifier:(NSString *) identifier andTarget:(Entity *) target
12172{
12173 // both players and NPCs!
12174 //
12175 ShipEntity *missile = nil;
12176 ShipEntity *target_ship = nil;
12177
12178 Vector vel;
12179 Vector start, v_eject;
12180
12181 if ([UNIVERSE getTime] < missile_launch_time) return nil;
12182
12183 start = [self missileLaunchPosition];
12184
12185 double throw_speed = 250.0f;
12186
12187 if ((missiles <= 0)||(target == nil)||([target scanClass] == CLASS_NO_DRAW)) // no missile lock!
12188 return nil;
12189
12190 if ([target isShip])
12191 {
12192 target_ship = (ShipEntity*)target;
12193 if ([target_ship isCloaked])
12194 {
12195 return nil;
12196 }
12197 // missile fire requires being in scanner range
12198 if (HPmagnitude2(HPvector_subtract([target_ship position], position)) > scannerRange * scannerRange)
12199 {
12200 return nil;
12201 }
12202 if (![self hasMilitaryScannerFilter] && [target_ship isJammingScanning])
12203 {
12204 return nil;
12205 }
12206 }
12207
12208 unsigned i;
12209 if (identifier == nil)
12210 {
12211 // use a random missile from the list
12212 i = floor(randf()*(double)missiles);
12213 identifier = [missile_list[i] identifier];
12214 missile = [UNIVERSE newShipWithRole:identifier];
12215 if (EXPECT_NOT(missile == nil)) // invalid missile role.
12216 {
12217 // remove that invalid missile role from the missiles list.
12218 while ( ++i < missiles ) missile_list[i - 1] = missile_list[i];
12219 missiles--;
12220 }
12221 }
12222 else
12223 missile = [UNIVERSE newShipWithRole:identifier];
12224
12225 if (EXPECT_NOT(missile == nil)) return nil;
12226
12227 // By definition, the player will always have the specified missile.
12228 // What if the NPC didn't actually have the specified missile to begin with?
12229 if (!isPlayer && ![self removeExternalStore:[OOEquipmentType equipmentTypeWithIdentifier:identifier]])
12230 {
12231 [missile release];
12232 return nil;
12233 }
12234
12235 double mcr = missile->collision_radius;
12236 v_eject = vector_normal(start);
12237 vel = kZeroVector; // starting velocity
12238
12239 // check if start is within bounding box...
12240 while ( (start.x > boundingBox.min.x - mcr)&&(start.x < boundingBox.max.x + mcr)&&
12241 (start.y > boundingBox.min.y - mcr)&&(start.y < boundingBox.max.y + mcr)&&
12242 (start.z > boundingBox.min.z - mcr)&&(start.z < boundingBox.max.z + mcr) )
12243 {
12244 start = vector_add(start, vector_multiply_scalar(v_eject, mcr));
12245 }
12246
12247 vel = vector_add(vel, vector_multiply_scalar(v_forward, flightSpeed + throw_speed));
12248
12249 Quaternion q1 = [self normalOrientation];
12250 HPVector origin = HPvector_add(position, vectorToHPVector(quaternion_rotate_vector(q1, start)));
12251
12252 if (isPlayer) [missile setScanClass: CLASS_MISSILE];
12253
12254// special cases
12255
12256 //We don't want real missiles in a group. Missiles could become escorts when the group is also used as escortGroup.
12257 if ([missile scanClass] == CLASS_THARGOID)
12258 {
12259 if([self group] == nil) [self setGroup:[OOShipGroup groupWithName:@"thargoid group"]];
12260
12261 ShipEntity *thisGroupLeader = [_group leader];
12262
12263 if ([thisGroupLeader escortGroup] != _group) // avoid adding tharons to escort groups
12264 {
12265 [missile setGroup:[self group]];
12266 }
12267 }
12268
12269 // is this a submunition?
12270 if (![self isMissileFlagSet]) [missile setOwner:self];
12271 else [missile setOwner:[self owner]];
12272
12273// end special cases
12274
12275 [missile setPosition:origin];
12276 [missile addTarget:target];
12277 [missile setOrientation:q1];
12278 [missile setIsMissileFlag:YES];
12279 [missile setVelocity:vel];
12280 [missile setSpeed:150.0f];
12281 [missile setDistanceTravelled:0.0f];
12282 [missile resetShotTime];
12283 missile_launch_time = [UNIVERSE getTime] + missile_load_time; // set minimum launchtime for the next missile.
12284
12285 [UNIVERSE addEntity:missile]; // STATUS_IN_FLIGHT, AI state GLOBAL
12286 [missile release]; //release
12287
12288 // missile lives on after UNIVERSE addEntity
12289 if ([missile isMissile] && [target isShip])
12290 {
12291 [self doScriptEvent:OOJSID("shipFiredMissile") withArgument:missile andArgument:target_ship];
12292 [target_ship setPrimaryAggressor:self];
12293 [target_ship doScriptEvent:OOJSID("shipAttackedWithMissile") withArgument:missile andArgument:self];
12294 [target_ship reactToAIMessage:@"INCOMING_MISSILE" context:@"someone's shooting at me!"];
12295 if (cloaking_device_active && cloakPassive)
12296 {
12297 // parity between player &NPCs, only deactivate cloak for missiles
12298 [self deactivateCloakingDevice];
12299 }
12300 }
12301 else
12302 {
12303 [self doScriptEvent:OOJSID("shipReleasedEquipment") withArgument:missile];
12304 }
12305
12306 return missile;
12307}
12308
12309
12310- (BOOL) isMissileFlagSet
12311{
12312 return isMissile; // were we created using fireMissile? (for tracking submunitions and preventing collisions at launch)
12313}
12314
12315
12316- (void) setIsMissileFlag:(BOOL)newValue
12317{
12318 isMissile = !!newValue; // set the isMissile flag, used for tracking submunitions and preventing collisions at launch.
12319}
12320
12321
12322- (OOTimeDelta) missileLoadTime
12323{
12324 return missile_load_time;
12325}
12326
12327
12328- (void) setMissileLoadTime:(OOTimeDelta)newMissileLoadTime
12329{
12330 missile_load_time = fmax(0.0, newMissileLoadTime);
12331}
12332
12333
12334// reactions to ECM that are not dependent on current AI state here
12335- (void) noticeECM
12336{
12337 if (accuracy >= COMBAT_AI_ISNT_AWFUL && missiles > 0 && [[missile_list[0] identifier] isEqualTo:@"EQ_MISSILE"])
12338 {
12339// if we're being ECMd, and our missiles appear to be standard, and we
12340// have some combat sense, wait a bit before firing the next one!
12341 missile_launch_time = [UNIVERSE getTime] + fmax(2.0,missile_load_time); // set minimum launchtime for the next missile.
12342 }
12343}
12344
12345
12346// Exposed to AI
12347- (BOOL) fireECM
12348{
12349 if (![self hasECM]) return NO;
12350
12351 OOECMBlastEntity *ecmDevice = [[OOECMBlastEntity alloc] initFromShip:self];
12352 [UNIVERSE addEntity:ecmDevice];
12353 [ecmDevice release];
12354 return YES;
12355}
12356
12357
12358- (BOOL) activateCloakingDevice
12359{
12360 if (![self hasCloakingDevice] || cloaking_device_active) return cloaking_device_active; // no changes.
12361
12362 if (!cloaking_device_active) cloaking_device_active = (energy > CLOAKING_DEVICE_START_ENERGY * maxEnergy);
12363 if (cloaking_device_active) [self doScriptEvent:OOJSID("shipCloakActivated")];
12364 return cloaking_device_active;
12365}
12366
12367
12368- (void) deactivateCloakingDevice
12369{
12370 if ([self hasCloakingDevice] && cloaking_device_active)
12371 {
12372 cloaking_device_active = NO;
12373 [self doScriptEvent:OOJSID("shipCloakDeactivated")];
12374 }
12375}
12376
12377
12378- (BOOL) launchCascadeMine
12379{
12380 if (![self hasCascadeMine]) return NO;
12381 [self setSpeed: maxFlightSpeed + 300];
12382 ShipEntity* bomb = [UNIVERSE newShipWithRole:@"energy-bomb"];
12383 if (bomb == nil) return NO;
12384
12385 [self removeEquipmentItem:@"EQ_QC_MINE"];
12386
12387 double start = collision_radius + bomb->collision_radius;
12388 Quaternion random_direction;
12389 Vector vel;
12390 HPVector rpos;
12391 double random_roll = randf() - 0.5; // -0.5 to +0.5
12392 double random_pitch = randf() - 0.5; // -0.5 to +0.5
12393 quaternion_set_random(&random_direction);
12394
12395 rpos = HPvector_subtract([self position], vectorToHPVector(vector_multiply_scalar(v_forward, start)));
12396
12397 double eject_speed = -800.0;
12398 vel = vector_multiply_scalar(v_forward, [self flightSpeed] + eject_speed);
12399 eject_speed *= 0.5 * (randf() - 0.5); // -0.25x .. +0.25x
12400 vel = vector_add(vel, vector_multiply_scalar(v_up, eject_speed));
12401 eject_speed *= 0.5 * (randf() - 0.5); // -0.0625x .. +0.0625x
12402 vel = vector_add(vel, vector_multiply_scalar(v_right, eject_speed));
12403
12404 [bomb setPosition:rpos];
12405 [bomb setOrientation:random_direction];
12406 [bomb setRoll:random_roll];
12407 [bomb setPitch:random_pitch];
12408 [bomb setVelocity:vel];
12409 [bomb setScanClass:CLASS_MINE];
12410 [bomb setEnergy:5.0]; // 5 second countdown
12411 [bomb setBehaviour:BEHAVIOUR_ENERGY_BOMB_COUNTDOWN];
12412 [bomb setOwner:self];
12413 [UNIVERSE addEntity:bomb]; // STATUS_IN_FLIGHT, AI state GLOBAL
12414 [bomb release];
12415
12416 if (cloaking_device_active && cloakPassive)
12417 {
12418 [self deactivateCloakingDevice];
12419 }
12420
12421 if (self != PLAYER) // get the heck out of here
12422 {
12423 [self addTarget:bomb];
12424 [self setBehaviour:BEHAVIOUR_FLEE_TARGET];
12425 frustration = 0.0;
12426 }
12427 return YES;
12428}
12429
12430
12431- (ShipEntity*)launchEscapeCapsule
12432{
12433 ShipEntity *result = nil;
12434 ShipEntity *mainPod = nil;
12435 unsigned n_pods, i;
12436 NSMutableArray *passengers = nil;
12437
12438 /*
12439 CHANGE: both player & NPCs can now launch escape pods in interstellar
12440 space. -- Kaks 20101113
12441 */
12442
12443 // check number of pods aboard -- require at least one.
12444 n_pods = [shipinfoDictionary oo_unsignedIntForKey:@"has_escape_pod"];
12445 if (n_pods > 65) n_pods = 65; // maximum of 64 passengers.
12446 if (n_pods > 1) passengers = [NSMutableArray arrayWithCapacity:n_pods-1];
12447
12448 if (crew) // transfer crew
12449 {
12450 // make sure crew inherit any legalStatus
12451 for (i = 0; i < [crew count]; i++)
12452 {
12453 OOCharacter *ch = (OOCharacter*)[crew objectAtIndex:i];
12454 [ch setLegalStatus: [self legalStatus] | [ch legalStatus]];
12455 }
12456 mainPod = [self launchPodWithCrew:crew];
12457 if (mainPod)
12458 {
12459 result = mainPod;
12460 [self setCrew:nil];
12461 [self setHulk:YES]; // we are without crew now.
12462 }
12463 }
12464
12465 // launch other pods (passengers)
12466 for (i = 1; i < n_pods; i++)
12467 {
12468 ShipEntity *passenger = nil;
12469 passenger = [self launchPodWithCrew:[NSArray arrayWithObject:[OOCharacter randomCharacterWithRole:@"passenger" andOriginalSystem:gen_rnd_number()]]];
12470 [passengers addObject:passenger];
12471 }
12472
12473 if (mainPod) [self doScriptEvent:OOJSID("shipLaunchedEscapePod") withArgument:mainPod andArgument:passengers];
12474
12475 return result;
12476}
12477
12478
12479// This is a documented AI method; do not change semantics. (Note: AIs don't have access to the return value.)
12480- (OOCommodityType) dumpCargo
12481{
12482 ShipEntity *jetto = [self dumpCargoItem:nil];
12483 if (jetto != nil)
12484 {
12485 return [jetto commodityType];
12486 }
12487 else
12488 {
12489 return nil;
12490 }
12491}
12492
12493
12494- (ShipEntity *) dumpCargoItem:(OOCommodityType)preferred
12495{
12496 ShipEntity *jetto = nil;
12497 NSUInteger i = 0;
12498
12499 if (([cargo count] > 0)&&([UNIVERSE getTime] - cargo_dump_time > 0.5)) // space them 0.5s or 10m apart
12500 {
12501 if (preferred == nil)
12502 {
12503 jetto = [[[cargo objectAtIndex:0] retain] autorelease];
12504 }
12505 else
12506 {
12507 BOOL found = NO;
12508 for (i=0;i<[cargo count];i++)
12509 {
12510 if ([[[cargo objectAtIndex:i] commodityType] isEqualToString:preferred])
12511 {
12512 jetto = [[[cargo objectAtIndex:i] retain] autorelease];
12513 found = YES;
12514 break;
12515 }
12516 }
12517 if (found == NO)
12518 {
12519 // dump anything
12520 jetto = [[[cargo objectAtIndex:0] retain] autorelease];
12521 i = 0;
12522 }
12523 }
12524 if (jetto != nil)
12525 {
12526 [self dumpItem:jetto]; // CLASS_CARGO, STATUS_IN_FLIGHT, AI state GLOBAL
12527 [cargo removeObjectAtIndex:i];
12528 [self broadcastAIMessage:@"CARGO_DUMPED"]; // goes only to 16 nearby ships in range, but that should be enough.
12529 unsigned i;
12530 // only send script event to powered entities
12531 [self checkScannerIgnoringUnpowered];
12532 for (i = 0; i < n_scanned_ships ; i++)
12533 {
12534 ShipEntity* other = scanned_ships[i];
12535 [other doScriptEvent:OOJSID("cargoDumpedNearby") withArgument:jetto andArgument:self];
12536
12537 }
12538 }
12539 }
12540
12541 return jetto;
12542}
12543
12544
12545- (OOCargoType) dumpItem: (ShipEntity*) cargoObj
12546{
12547 if (!cargoObj)
12548 return 0;
12549
12550 ShipEntity* jetto = [UNIVERSE reifyCargoPod:cargoObj];
12551
12552 int result = [jetto cargoType];
12553 AI *jettoAI = nil;
12554 Vector start;
12555
12556 // players get to see their old ship sailing forth, while NPCs run away more efficiently!
12557 // cargo is ejected at higher speed from any ship
12558 double eject_speed = EXPECT_NOT([jetto crew] && [jetto isPlayer]) ? 20.0 : 100.0;
12559 double eject_reaction = -eject_speed * [jetto mass] / [self mass];
12560 double jcr = jetto->collision_radius;
12561
12562 Quaternion jetto_orientation = kIdentityQuaternion;
12563 Vector vel, v_eject, v_eject_normal;
12564 HPVector rpos = [self absolutePositionForSubentity];
12565 double jetto_roll = 0;
12566 double jetto_pitch = 0;
12567
12568 // default launching position
12569 start.x = 0.0; // in the middle
12570 start.y = 0.0; //
12571 start.z = boundingBox.min.z - jcr; // 1m behind of bounding box
12572
12573 // custom launching position
12574 start = [shipinfoDictionary oo_vectorForKey:@"aft_eject_position" defaultValue:start];
12575
12576 v_eject = vector_normal(start);
12577
12578 // check if start is within bounding box...
12579 while ( (start.x > boundingBox.min.x - jcr)&&(start.x < boundingBox.max.x + jcr)&&
12580 (start.y > boundingBox.min.y - jcr)&&(start.y < boundingBox.max.y + jcr)&&
12581 (start.z > boundingBox.min.z - jcr)&&(start.z < boundingBox.max.z + jcr))
12582 {
12583 start = vector_add(start, vector_multiply_scalar(v_eject, jcr));
12584 }
12585
12586 v_eject = quaternion_rotate_vector([self normalOrientation], start);
12587 rpos = HPvector_add(rpos, vectorToHPVector(v_eject));
12588 v_eject = vector_normal(v_eject);
12589 v_eject_normal = v_eject;
12590
12591 v_eject.x += (randf() - randf())/eject_speed;
12592 v_eject.y += (randf() - randf())/eject_speed;
12593 v_eject.z += (randf() - randf())/eject_speed;
12594
12595 vel = vector_add(vector_multiply_scalar(v_forward, flightSpeed), vector_multiply_scalar(v_eject, eject_speed));
12596 velocity = vector_add(velocity, vector_multiply_scalar(v_eject, eject_reaction));
12597
12598 [jetto setPosition:rpos];
12599 if ([jetto crew]) // jetto has a crew, so assume it is an escape pod.
12600 {
12601 // orient the pod away from the ship to avoid colliding with it.
12602 jetto_orientation = quaternion_rotation_between(v_eject_normal, kBasisZVector);
12603 }
12604 else
12605 {
12606 // It is true cargo, let it tumble.
12607 jetto_roll = ((ranrot_rand() % 1024) - 512.0)/1024.0; // -0.5 to +0.5
12608 jetto_pitch = ((ranrot_rand() % 1024) - 512.0)/1024.0; // -0.5 to +0.5
12609 quaternion_set_random(&jetto_orientation);
12610 }
12611
12612 [jetto setOrientation:jetto_orientation];
12613 [jetto setRoll:jetto_roll];
12614 [jetto setPitch:jetto_pitch];
12615 [jetto setVelocity:vel];
12616 [jetto setScanClass: CLASS_CARGO];
12617 [jetto setTemperature:[self randomEjectaTemperature]];
12618 [UNIVERSE addEntity:jetto]; // STATUS_IN_FLIGHT, AI state GLOBAL
12619
12620 jettoAI = [jetto getAI];
12621 if ([jettoAI hasSuspendedStateMachines]) // check if this was previous scooped cargo.
12622 {
12623 [jetto setThrust:[jetto maxThrust]]; // restore old thrust.
12624 [jetto setOwner:jetto];
12625 [jettoAI exitStateMachineWithMessage:nil]; // exit nullAI.
12626 }
12627 [jetto doScriptEvent:OOJSID("shipWasDumped") withArgument:self];
12628 [self doScriptEvent:OOJSID("shipDumpedCargo") withArgument:jetto];
12629
12630 cargo_dump_time = [UNIVERSE getTime];
12631 return result;
12632}
12633
12634
12635- (void) manageCollisions
12636{
12637 // deal with collisions
12638 //
12639 Entity* ent;
12640 ShipEntity* other_ship;
12641
12642 while ([collidingEntities count] > 0)
12643 {
12644 // EMMSTRAN: investigate if doing this backwards would be more efficient. (Not entirely obvious, NSArray is kinda funky.) -- Ahruman 2011-02-12
12645 ent = [[[collidingEntities objectAtIndex:0] retain] autorelease];
12646 [collidingEntities removeObjectAtIndex:0];
12647 if (ent)
12648 {
12649 if ([ent isShip])
12650 {
12651 other_ship = (ShipEntity *)ent;
12652 [self collideWithShip:other_ship];
12653 }
12654 else if ([ent isStellarObject])
12655 {
12656 [self getDestroyedBy:ent damageType:[ent isSun] ? kOODamageTypeHitASun : kOODamageTypeHitAPlanet];
12657 if (self == PLAYER) [self retain];
12658 }
12659 else if ([ent isWormhole])
12660 {
12661 if( [self isPlayer] ) [self enterWormhole:(WormholeEntity*)ent];
12662 else [self enterWormhole:(WormholeEntity*)ent replacing:NO];
12663 }
12664 }
12665 }
12666}
12667
12668
12669- (BOOL) collideWithShip:(ShipEntity *)other
12670{
12671 HPVector hploc;
12672 Vector loc;
12673 double dam1, dam2;
12674
12675 if (!other)
12676 return NO;
12677
12678 ShipEntity* otherParent = [other parentEntity];
12679 BOOL otherIsStation = other == [UNIVERSE station];
12680 // calculate line of centers using centres
12681 hploc = HPvector_normal_or_zbasis(HPvector_subtract([other absolutePositionForSubentity], position));
12682 loc = HPVectorToVector(hploc);
12683
12684
12685 if ([self canScoop:other])
12686 {
12687 [self scoopIn:other];
12688 return NO;
12689 }
12690 if ([other canScoop:self])
12691 {
12692 [other scoopIn:self];
12693 return NO;
12694 }
12695 if (universalID == NO_TARGET)
12696 return NO;
12697 if (other->universalID == NO_TARGET)
12698 return NO;
12699
12700 // find velocity along line of centers
12701 //
12702 // momentum = mass x velocity
12703 // ke = mass x velocity x velocity
12704 //
12705 GLfloat m1 = mass; // mass of self
12706 GLfloat m2 = [other mass]; // mass of other
12707
12708 // starting velocities:
12709 Vector v, vel1b = [self velocity];
12710
12711 if (otherParent != nil)
12712 {
12713 // Subentity
12714 /* TODO: if the subentity is rotating (subentityRotationalVelocity is
12715 not 1 0 0 0) we should calculate the tangential velocity from the
12716 other's position relative to our absolute position and add that in.
12717 */
12718 v = [otherParent velocity];
12719 }
12720 else
12721 {
12722 v = [other velocity];
12723 }
12724
12725 v = vector_subtract(vel1b, v);
12726
12727 GLfloat v2b = dot_product(v, loc); // velocity of other along loc before collision
12728
12729 GLfloat v1a = sqrt(v2b * v2b * m2 / m1); // velocity of self along loc after elastic collision
12730 if (v2b < 0.0f) v1a = -v1a; // in same direction as v2b
12731
12732 // are they moving apart at over 1m/s already?
12733 if (v2b < 0.0f)
12734 {
12735 if (v2b < -1.0f) return NO;
12736 else
12737 {
12738 position = HPvector_subtract(position, hploc); // adjust self position
12739 v = kZeroVector; // go for the 1m/s solution
12740 }
12741 }
12742
12743 // convert change in velocity into damage energy (KE)
12744 dam1 = m2 * v2b * v2b / 50000000;
12745 dam2 = m1 * v2b * v2b / 50000000;
12746
12747 // calculate adjustments to velocity after collision
12748 Vector vel1a = vector_multiply_scalar(loc, -v1a);
12749 Vector vel2a = vector_multiply_scalar(loc, v2b);
12750
12751 if (magnitude2(v) <= 0.1) // virtually no relative velocity - we must provide at least 1m/s to avoid conjoined objects
12752 {
12753 vel1a = vector_multiply_scalar(loc, -1);
12754 vel2a = loc;
12755 }
12756
12757 // apply change in velocity
12758 if (otherParent != nil)
12759 {
12760 [otherParent adjustVelocity:vel2a]; // move the otherParent not the subentity
12761 }
12762 else
12763 {
12764 [other adjustVelocity:vel2a];
12765 }
12766
12767 [self adjustVelocity:vel1a];
12768
12769 BOOL selfDestroyed = (dam1 > energy);
12770 BOOL otherDestroyed = (dam2 > [other energy]) && !otherIsStation;
12771
12772 if (dam1 > 0.05)
12773 {
12774 [self takeScrapeDamage: dam1 from:other];
12775 if (selfDestroyed) // inelastic! - take xplosion velocity damage instead
12776 {
12777 vel2a = vector_multiply_scalar(vel2a, -1);
12778 [other adjustVelocity:vel2a];
12779 }
12780 }
12781
12782 if (dam2 > 0.05)
12783 {
12784 if (otherParent != nil && ![otherParent isFrangible])
12785 {
12786 [otherParent takeScrapeDamage: dam2 from:self];
12787 }
12788 else
12789 {
12790 [other takeScrapeDamage: dam2 from:self];
12791 }
12792
12793 if (otherDestroyed) // inelastic! - take explosion velocity damage instead
12794 {
12795 vel1a = vector_multiply_scalar(vel1a, -1);
12796 [self adjustVelocity:vel1a];
12797 }
12798 }
12799
12800 if (!selfDestroyed && !otherDestroyed)
12801 {
12802 float t = 10.0 * [UNIVERSE getTimeDelta]; // 10 ticks
12803
12804 HPVector pos1a = HPvector_add([self position], vectorToHPVector(vector_multiply_scalar(loc, t * v1a)));
12805 [self setPosition:pos1a];
12806
12807 if (!otherIsStation)
12808 {
12809 HPVector pos2a = HPvector_add([other position], vectorToHPVector(vector_multiply_scalar(loc, t * v2b)));
12810 [other setPosition:pos2a];
12811 }
12812 }
12813
12814 // remove self from other's collision list
12815 [[other collisionArray] removeObject:self];
12816
12817 [self doScriptEvent:OOJSID("shipCollided") withArgument:other andReactToAIMessage:@"COLLISION"];
12818 [other doScriptEvent:OOJSID("shipCollided") withArgument:self andReactToAIMessage:@"COLLISION"];
12819
12820 return YES;
12821}
12822
12823
12824- (Vector) thrustVector
12825{
12826 return vector_multiply_scalar(v_forward, flightSpeed);
12827}
12828
12829
12830- (Vector) velocity
12831{
12832 return vector_add([super velocity], [self thrustVector]);
12833}
12834
12835
12836- (void) setTotalVelocity:(Vector)vel
12837{
12838 [self setVelocity:vector_subtract(vel, [self thrustVector])];
12839}
12840
12841
12842- (void) adjustVelocity:(Vector) xVel
12843{
12844 velocity = vector_add(velocity, xVel);
12845}
12846
12847
12848- (void) addImpactMoment:(Vector) moment fraction:(GLfloat) howmuch
12849{
12850 velocity = vector_add(velocity, vector_multiply_scalar(moment, howmuch / mass));
12851}
12852
12853
12854- (BOOL) canScoop:(ShipEntity*)other
12855{
12856 if (other == nil) return NO;
12857 if (![self hasCargoScoop]) return NO;
12858 if ([cargo count] >= [self maxAvailableCargoSpace]) return NO;
12859 if (scanClass == CLASS_CARGO) return NO; // we have no power so we can't scoop
12860 if ([other scanClass] != CLASS_CARGO) return NO;
12861 if ([other cargoType] == CARGO_NOT_CARGO) return NO;
12862
12863 if ([other isStation]) return NO;
12864
12865 HPVector loc = HPvector_between(position, [other position]);
12866
12867 if (dot_product(v_forward, HPVectorToVector(loc)) < 0.0f) return NO; // Must be in front of us
12868 if ([self isPlayer] && dot_product(v_up, HPVectorToVector(loc)) > 0.0f) return NO; // player has to scoop on underside, give more flexibility to NPCs
12869
12870 return YES;
12871}
12872
12873
12874- (void) getTractoredBy:(ShipEntity *)other
12875{
12876 if([self status] == STATUS_BEING_SCOOPED) return; // both cargo and ship call this. Act only once.
12877 desired_speed = 0.0;
12878 [self setAITo:@"nullAI.plist"]; // prevent AI from changing status or behaviour.
12879 behaviour = BEHAVIOUR_TRACTORED;
12880 [self setStatus:STATUS_BEING_SCOOPED];
12881 [self addTarget:other];
12882 [self setOwner:other];
12883 // should we make this an all rather than first 16? - CIM
12884 // made it ignore other cargopods and similar at least. - CIM 28/7/2013
12885 [self checkScannerIgnoringUnpowered];
12886 unsigned i;
12887 ShipEntity *scooper;
12888 for (i = 0; i < n_scanned_ships ; i++)
12889 {
12890 scooper = (ShipEntity *)scanned_ships[i];
12891 // 'Dibs!' - Stops other ships from trying to scoop/shoot this cargo.
12892 if (other != scooper && (id) self == [scooper primaryTarget])
12893 {
12894 [scooper noteLostTarget];
12895 }
12896 }
12897}
12898
12899
12900- (void) scoopIn:(ShipEntity *)other
12901{
12902 [other getTractoredBy:self];
12903}
12904
12905
12906- (void) suppressTargetLost
12907{
12908
12909}
12910
12911
12912- (void) scoopUp:(ShipEntity *)other
12913{
12914 [self scoopUpProcess:other processEvents:YES processMessages:YES];
12915}
12916
12917
12918- (void) scoopUpProcess:(ShipEntity *)other processEvents:(BOOL) procEvents processMessages:(BOOL) procMessages
12919{
12920 if (other == nil) return;
12921
12922 OOCommodityType co_type = nil;
12923 OOCargoQuantity co_amount;
12924
12925 // don't even think of trying to scoop if the cargo hold is already full
12926 if (max_cargo && [cargo count] >= [self maxAvailableCargoSpace])
12927 {
12928 [other setStatus:STATUS_IN_FLIGHT];
12929 return;
12930 }
12931
12932 switch ([other cargoType])
12933 {
12934 case CARGO_RANDOM:
12935 co_type = [other commodityType];
12936 co_amount = [other commodityAmount];
12937 break;
12938
12940 {
12941 //scripting
12942 PlayerEntity *player = PLAYER;
12943 [player setScriptTarget:self];
12944 if (procEvents)
12945 {
12946 [other doScriptEvent:OOJSID("shipWasScooped") withArgument:self];
12947 }
12948
12949 if ([other commodityType] != nil)
12950 {
12951 co_type = [other commodityType];
12952 co_amount = [other commodityAmount];
12953 // don't show scoop message now, will happen later.
12954 }
12955 else
12956 {
12957 if (isPlayer && [other showScoopMessage] && procMessages)
12958 {
12959 [UNIVERSE clearPreviousMessage];
12960 NSString *shipName = [other displayName];
12961 [UNIVERSE addMessage:OOExpandKey(@"scripted-item-scooped", shipName) forCount:4];
12962 }
12963 [other setCommodityForPod:nil andAmount:0];
12964 co_amount = 0;
12965 co_type = nil;
12966 }
12967 }
12968 break;
12969
12970 default :
12971 co_amount = 0;
12972 co_type = nil;
12973 break;
12974 }
12975
12976 /* Bug: docking failed due to NSRangeException while looking for element
12977 NSNotFound of cargo mainfest in -[PlayerEntity unloadCargoPods].
12978 Analysis: bad cargo pods being generated due to
12979 -[Universe commodityForName:] looking in wrong place for names.
12980 Fix 1: fix -[Universe commodityForName:].
12981 Fix 2: catch NSNotFound here and substitute random cargo type.
12982 -- Ahruman 20070714
12983 */
12984 if (co_type == nil && co_amount > 0)
12985 {
12986 co_type = [UNIVERSE getRandomCommodity];
12987 co_amount = [UNIVERSE getRandomAmountOfCommodity:co_type];
12988 }
12989
12990 if (co_amount > 0)
12991 {
12992 [other setCommodity:co_type andAmount:co_amount]; // belt and braces setting this!
12993 cargo_flag = CARGO_FLAG_CANISTERS;
12994
12995 if (isPlayer)
12996 {
12997 if ([other crew])
12998 {
12999 if ([other showScoopMessage] && procMessages)
13000 {
13001 [UNIVERSE clearPreviousMessage];
13002 unsigned i;
13003 for (i = 0; i < [[other crew] count]; i++)
13004 {
13005 OOCharacter *rescuee = [[other crew] objectAtIndex:i];
13006 NSString *characterName = [rescuee name];
13007 if ([rescuee legalStatus])
13008 {
13009 [UNIVERSE addMessage:OOExpandKey(@"scoop-captured-character", characterName) forCount: 4.5];
13010 }
13011 else if ([rescuee insuranceCredits])
13012 {
13013 [UNIVERSE addMessage:OOExpandKey(@"scoop-rescued-character", characterName) forCount: 4.5];
13014 }
13015 else
13016 {
13017 [UNIVERSE addMessage: DESC(@"scoop-got-slave") forCount: 4.5];
13018 }
13019 }
13020 }
13021 if (procEvents)
13022 {
13023 [(PlayerEntity *)self playEscapePodScooped];
13024 }
13025 }
13026 else
13027 {
13028 if ([other showScoopMessage] && procMessages)
13029 {
13030 [UNIVERSE clearPreviousMessage];
13031 [UNIVERSE addMessage:[UNIVERSE describeCommodity:co_type amount:co_amount] forCount:4.5];
13032 }
13033 }
13034 }
13035 [cargo insertObject:other atIndex:0]; // places most recently scooped object at eject position
13036 [other setStatus:STATUS_IN_HOLD];
13037 [other performTumble];
13038 [shipAI message:@"CARGO_SCOOPED"];
13039 if (max_cargo && [cargo count] >= [self maxAvailableCargoSpace]) [shipAI message:@"HOLD_FULL"];
13040 }
13041 if (procEvents)
13042 {
13043 [self doScriptEvent:OOJSID("shipScoopedOther") withArgument:other]; // always fire, even without commodity.
13044 }
13045
13046 // if shipScoopedOther does something strange to the object, we must
13047 // then remove it from the hold, or it will be over-retained
13048 if ([other status] != STATUS_IN_HOLD)
13049 {
13050 if ([cargo containsObject:other])
13051 {
13052 [cargo removeObject:other];
13053 }
13054 }
13055
13056 [[other collisionArray] removeObject:self]; // so it can't be scooped twice!
13057 // make sure other ships trying to scoop it lose it
13058 // probably already happened, but some may have acquired it
13059 // after the scooping started, and they might get stuck in a scooping
13060 // attempt as a result
13061 [self checkScannerIgnoringUnpowered];
13062 unsigned i;
13063 ShipEntity *scooper;
13064 for (i = 0; i < n_scanned_ships ; i++)
13065 {
13066 scooper = (ShipEntity *)scanned_ships[i];
13067 if (self != scooper && (id) other == [scooper primaryTargetWithoutValidityCheck])
13068 {
13069 [scooper noteLostTarget];
13070 }
13071 }
13072
13073 [self suppressTargetLost];
13074 [UNIVERSE removeEntity:other];
13075}
13076
13077
13078- (BOOL) cascadeIfAppropriateWithDamageAmount:(double)amount cascadeOwner:(Entity *)owner
13079{
13080 BOOL cascade = NO;
13081 switch ([self scanClass])
13082 {
13083 case CLASS_WORMHOLE:
13084 case CLASS_ROCK:
13085 case CLASS_CARGO:
13086 case CLASS_VISUAL_EFFECT:
13087 case CLASS_BUOY:
13088 // does not normally cascade
13089 if ((fuel > MIN_FUEL) || isStation)
13090 {
13091 //we have fuel onboard so we can still go pop, or we are a station which can
13092 }
13093 else break;
13094
13095 case CLASS_STATION:
13096 case CLASS_MINE:
13097 case CLASS_PLAYER:
13098 case CLASS_POLICE:
13099 case CLASS_MILITARY:
13100 case CLASS_THARGOID:
13101 case CLASS_MISSILE:
13102 case CLASS_NOT_SET:
13103 case CLASS_NO_DRAW:
13104 case CLASS_NEUTRAL:
13105 case CLASS_TARGET:
13106 // ...start a chain reaction, if we're dying and have a non-trivial amount of energy.
13107 if (energy < amount && energy > 10 && [self countsAsKill])
13108 {
13109 cascade = YES; // confirm we're cascading, then try to add our cascade to UNIVERSE.
13110 [UNIVERSE addEntity:[OOQuiriumCascadeEntity quiriumCascadeFromShip:self]];
13111 }
13112 break;
13113 //no default thanks, we want the compiler to tell us if we missed a case.
13114 }
13115 return cascade;
13116}
13117
13118
13119- (void) takeEnergyDamage:(double)amount from:(Entity *)ent becauseOf:(Entity *)other weaponIdentifier:(NSString *)weaponIdentifier
13120{
13121 if ([self status] == STATUS_DEAD) return;
13122 if (amount <= 0.0) return;
13123
13124 BOOL energyMine = [ent isCascadeWeapon];
13125 BOOL cascade = NO;
13126 if (energyMine)
13127 {
13128 cascade = [self cascadeIfAppropriateWithDamageAmount:amount cascadeOwner:[ent owner]];
13129 }
13130
13131 energy -= amount;
13132 /* Heat increase from energy impacts will never directly cause
13133 * overheating - too easy for missile hits to cause an uncredited
13134 * death by overheating - CIM */
13135 if (ship_temperature < SHIP_MAX_CABIN_TEMP)
13136 {
13137 ship_temperature += amount * SHIP_ENERGY_DAMAGE_TO_HEAT_FACTOR / [self heatInsulation];
13138 if (ship_temperature > SHIP_MAX_CABIN_TEMP)
13139 {
13140 ship_temperature = SHIP_MAX_CABIN_TEMP;
13141 }
13142 }
13143
13144
13145 being_mined = NO;
13146 ShipEntity *hunter = nil;
13147
13148 hunter = [other rootShipEntity];
13149 if (hunter == nil && [other isShip]) hunter = (ShipEntity *)other;
13150
13151 // must check for this before potentially deleting 'other' for cloaking
13152 if ((other)&&([other isShip]))
13153 {
13154 being_mined = [(ShipEntity *)other isMining];
13155 }
13156
13157 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
13158 {
13159 if ([hunter isCloaked])
13160 {
13161 [self doScriptEvent:OOJSID("shipBeingAttackedByCloaked") andReactToAIMessage:@"ATTACKED_BY_CLOAKED"];
13162
13163 // lose it!
13164 other = nil;
13165 hunter = nil;
13166 }
13167 }
13168 else
13169 {
13170 hunter = nil;
13171 }
13172
13173 // if the other entity is a ship note it as an aggressor
13174 if (hunter != nil)
13175 {
13176 BOOL iAmTheLaw = [self isPolice];
13177 BOOL uAreTheLaw = [hunter isPolice];
13178
13179 DESTROY(_lastEscortTarget); // we're being attacked, escorts can scramble!
13180
13181 [self setPrimaryAggressor:hunter];
13182 [self setFoundTarget:hunter];
13183
13184 // firing on an innocent ship is an offence
13185 [self broadcastHitByLaserFrom: hunter];
13186
13187 // tell ourselves we've been attacked
13188 if (energy > 0)
13189 {
13190 [self respondToAttackFrom:ent becauseOf:hunter];
13191 }
13192
13193 OOShipGroup *group = [self group];
13194 // JSAIs manage group notifications themselves
13195 if (![self hasNewAI])
13196 {
13197 // additionally, tell our group we've been attacked
13198 if (group != nil && group != [hunter group] && !(iAmTheLaw || uAreTheLaw))
13199 {
13200 if ([self isTrader] || [self isEscort])
13201 {
13202 ShipEntity *groupLeader = [group leader];
13203 if (groupLeader != self)
13204 {
13205 [groupLeader setFoundTarget:hunter];
13206 [groupLeader setPrimaryAggressor:hunter];
13207 [groupLeader respondToAttackFrom:ent becauseOf:hunter];
13208 //unsetting group leader for carriers can break stuff
13209 }
13210 }
13211 if ([self isPirate])
13212 {
13213 NSEnumerator *groupEnum = nil;
13214 ShipEntity *otherPirate = nil;
13215
13216 for (groupEnum = [group mutationSafeEnumerator]; (otherPirate = [groupEnum nextObject]); )
13217 {
13218 if (otherPirate != self && randf() < 0.5) // 50% chance they'll help
13219 {
13220 [otherPirate setFoundTarget:hunter];
13221 [otherPirate setPrimaryAggressor:hunter];
13222 [otherPirate respondToAttackFrom:ent becauseOf:hunter];
13223 }
13224 }
13225 }
13226 else if (iAmTheLaw)
13227 {
13228 NSEnumerator *groupEnum = nil;
13229 ShipEntity *otherPolice = nil;
13230
13231 for (groupEnum = [group mutationSafeEnumerator]; (otherPolice = [groupEnum nextObject]); )
13232 {
13233 if (otherPolice != self)
13234 {
13235 [otherPolice setFoundTarget:hunter];
13236 [otherPolice setPrimaryAggressor:hunter];
13237 [otherPolice respondToAttackFrom:ent becauseOf:hunter];
13238 }
13239 }
13240 }
13241 }
13242 }
13243
13244 // if I'm a copper and you're not, then mark the other as an offender!
13245 if (iAmTheLaw && !uAreTheLaw)
13246 {
13247 // JSAI's can choose not to do this for friendly fire purposes
13248 if (![self hasNewAI])
13249 {
13250 [hunter markAsOffender:64 withReason:kOOLegalStatusReasonAttackedPolice];
13251 }
13252 }
13253
13254 if ((group != nil && [hunter group] == group) || (iAmTheLaw && uAreTheLaw))
13255 {
13256 // avoid shooting each other
13257 if ([hunter behaviour] == BEHAVIOUR_ATTACK_FLY_TO_TARGET) // avoid me please!
13258 {
13259 [hunter setBehaviour:BEHAVIOUR_ATTACK_FLY_FROM_TARGET];
13260 [hunter setDesiredSpeed:[hunter maxFlightSpeed]];
13261 }
13262 }
13263
13264 }
13265
13266 OOShipDamageType damageType = kOODamageTypeEnergy;
13267 if (suppressExplosion) damageType = kOODamageTypeRemoved;
13268 else if (energyMine) damageType = kOODamageTypeCascadeWeapon;
13269
13270 if (!suppressExplosion)
13271 {
13272 [self noteTakingDamage:amount from:other type:damageType];
13273 if (cascade) energy = 0.0; // explicit set energy to zero in case an oxp raised the energy in previous line.
13274 }
13275
13276 // die if I'm out of energy
13277 if (energy <= 0.0)
13278 {
13279 // backup check just in case scripts have reduced energy
13280 if (self != [UNIVERSE station])
13281 {
13282 if (hunter != nil) [hunter noteTargetDestroyed:self];
13283 [self getDestroyedBy:other damageType:damageType];
13284 }
13285 }
13286 else
13287 {
13288 // warn if I'm low on energy
13289 if (energy < maxEnergy * 0.25)
13290 {
13291 [self doScriptEvent:OOJSID("shipEnergyIsLow") andReactToAIMessage:@"ENERGY_LOW"];
13292 }
13293 if ((energy < maxEnergy *0.125 || (energy < 64 && energy < amount*2)) && [self hasEscapePod] && (ranrot_rand() & 3) == 0) // 25% chance he gets to an escape pod
13294 {
13295 [self abandonShip];
13296 }
13297 }
13298}
13299
13300
13301- (BOOL) abandonShip
13302{
13303 BOOL OK = NO;
13304 if ([self isPlayer] && [(PlayerEntity *)self isDocked])
13305 {
13306 OOLog(@"ShipEntity.abandonShip.failed", @"%@", @"Player cannot abandon ship while docked.");
13307 return OK;
13308 }
13309
13310 if (![self hasEscapePod])
13311 {
13312 OOLog(@"ShipEntity.abandonShip.failed", @"Ship abandonment was requested for %@, but this ship does not carry escape pod(s).", self);
13313 return OK;
13314 }
13315
13316 if (EXPECT([self launchEscapeCapsule] != NO_TARGET)) // -launchEscapeCapsule takes care of everything for the player
13317 {
13318 if (![self isPlayer])
13319 {
13320 OK = YES;
13321 // if multiple items providing escape pod, remove all of them (NPC process)
13322 while ([self hasEquipmentItemProviding:@"EQ_ESCAPE_POD"])
13323 {
13324 [self removeEquipmentItem:[self equipmentItemProviding:@"EQ_ESCAPE_POD"]];
13325 }
13326 [self setAITo:@"nullAI.plist"];
13327 behaviour = BEHAVIOUR_IDLE;
13328 frustration = 0.0;
13329 [self setScanClass: CLASS_CARGO]; // we're unmanned now!
13330 thrust = thrust * 0.5;
13331 if (thrust > 5) thrust = 5; // 5 is the thrust of an escape-capsule
13332 desired_speed = 0.0;
13333 if ([self group]) [self setGroup:nil]; // remove self from group.
13334 if (![self isSubEntity] && [self owner]) [self setOwner:nil]; //unset owner, but not if we are a subent
13335 if ([self hasEscorts])
13336 {
13337 OOShipGroup *escortGroup = [self escortGroup];
13338 NSEnumerator *escortEnum = nil;
13339 ShipEntity *escort = nil;
13340 // Note: works on escortArray rather than escortEnumerator because escorts may be mutated.
13341 for (escortEnum = [[self escortArray] objectEnumerator]; (escort = [escortEnum nextObject]); )
13342 {
13343 // act individually now!
13344 if ([escort group] == escortGroup) [escort setGroup:nil];
13345 if ([escort owner] == self) [escort setOwner:escort];
13346 }
13347
13348 // We now have no escorts.
13349 [_escortGroup release];
13350 _escortGroup = nil;
13351 }
13352 }
13353 }
13354 else if (EXPECT([self isSubEntity]))
13355 {
13356 // may still have launched passenger pods even if no crew
13357 // if multiple items providing escape pod, remove all of them (NPC process)
13358 while ([self hasEquipmentItemProviding:@"EQ_ESCAPE_POD"])
13359 {
13360 [self removeEquipmentItem:[self equipmentItemProviding:@"EQ_ESCAPE_POD"]];
13361 }
13362
13363 }
13364 else
13365 {
13366 // this shouldn't happen any more!
13367 OOLog(@"ShipEntity.abandonShip.notPossible", @"Ship %@ cannot be abandoned at this time.", self);
13368 }
13369 return OK;
13370}
13371
13372
13373- (void) takeScrapeDamage:(double) amount from:(Entity *)ent
13374{
13375 if ([self status] == STATUS_DEAD) return;
13376
13377 if ([self status] == STATUS_LAUNCHING|| [ent status] == STATUS_LAUNCHING)
13378 {
13379 // no collisions during launches please
13380 return;
13381 }
13382
13383 energy -= amount;
13384 [self noteTakingDamage:amount from:ent type:kOODamageTypeScrape];
13385
13386 // oops we hit too hard!!!
13387 if (energy <= 0.0)
13388 {
13389 float frag_chance = [ent mass]*10/[self mass];
13390 /* impacts from heavier entities produce fragments
13391 * impacts from lighter entities might do but not always
13392 * asteroid-asteroid impacts likely to fragment
13393 * ship-asteroid impacts might, or might just vaporise it
13394 * projectile weapons just get the default chance
13395 */
13396 if (randf() < frag_chance)
13397 {
13398 being_mined = YES; // same as using a mining laser
13399 }
13400 if ([ent isShip])
13401 {
13402 [(ShipEntity *)ent noteTargetDestroyed:self];
13403 }
13404 [self getDestroyedBy:ent damageType:kOODamageTypeScrape];
13405 }
13406 else
13407 {
13408 // warn if I'm low on energy
13409 if (energy < maxEnergy * 0.25)
13410 {
13411 [self doScriptEvent:OOJSID("shipEnergyIsLow") andReactToAIMessage:@"ENERGY_LOW"];
13412 }
13413 }
13414}
13415
13416
13417- (void) takeHeatDamage:(double)amount
13418{
13419 if ([self status] == STATUS_DEAD) return;
13420
13421 if ([self isSubEntity])
13422 {
13423 ShipEntity* owner = [self owner];
13424 if (![owner isFrangible])
13425 {
13426 return;
13427 }
13428 }
13429
13430 energy -= amount;
13431 throw_sparks = YES;
13432
13433 [self noteTakingDamage:amount from:nil type:kOODamageTypeHeat];
13434
13435 // oops we're burning up!
13436 if (energy <= 0.0)
13437 {
13438 [self getDestroyedBy:nil damageType:kOODamageTypeHeat];
13439 }
13440 else
13441 {
13442 // warn if I'm low on energy
13443 if (energy < maxEnergy * 0.25)
13444 {
13445 [self doScriptEvent:OOJSID("shipEnergyIsLow") andReactToAIMessage:@"ENERGY_LOW"];
13446 }
13447 }
13448}
13449
13450
13451- (void) enterDock:(StationEntity *)station
13452{
13453 // throw these away now we're docked...
13454 if (dockingInstructions != nil)
13455 {
13456 [dockingInstructions autorelease];
13457 dockingInstructions = nil;
13458 }
13459
13460 [self doScriptEvent:OOJSID("shipWillDockWithStation") withArgument:station];
13461 [self doScriptEvent:OOJSID("shipDockedWithStation") withArgument:station];
13462 [shipAI message:@"DOCKED"];
13463 [station noteDockedShip:self];
13464 [UNIVERSE removeEntity:self];
13465}
13466
13467
13468- (void) leaveDock:(StationEntity *)station
13469{
13470 // This code is never used. Currently npc ships are only launched from the stations launch queue.
13471 if (station == nil) return;
13472
13473 [station launchShip:self];
13474
13475}
13476
13477
13478- (void) enterWormhole:(WormholeEntity *) w_hole
13479{
13480 [self enterWormhole:w_hole replacing:YES];
13481}
13482
13483
13484- (void) enterWormhole:(WormholeEntity *) w_hole replacing:(BOOL)replacing
13485{
13486 if (w_hole == nil) return;
13487 if ([self status] == STATUS_ENTERING_WITCHSPACE)
13488 {
13489 return; // has already entered a different wormhole
13490 }
13491 // Replacement ships now handled by system repopulator
13492
13493 // MKW 2011.02.27 - Moved here from ShipEntityAI so escorts reliably follow
13494 // mother in all wormhole cases, not just when the ship
13495 // creates the wormhole.
13496 [self addTarget:w_hole];
13497 [self setFoundTarget:w_hole];
13498 [shipAI reactToMessage:@"WITCHSPACE OKAY" context:@"performHyperSpaceExit"]; // must be a reaction, the ship is about to disappear
13499
13500 // CIM 2012.07.22 above only covers those cases where ship expected to leave
13501 if ([[self escortArray] count] > 1)
13502 {
13503 // so wormhole escorts anyway if it leaves unexpectedly.
13504 [self wormholeEscorts];
13505 }
13506
13507 if ([self scriptedMisjump])
13508 {
13509 [self setScriptedMisjump:NO];
13510 [w_hole setMisjumpWithRange:[self scriptedMisjumpRange]];
13511 [self setScriptedMisjumpRange:0.5];
13512 }
13513 [w_hole suckInShip: self]; // removes ship from universe
13514}
13515
13516
13517- (void) enterWitchspace
13518{
13519 [UNIVERSE addWitchspaceJumpEffectForShip:self];
13520 [shipAI message:@"ENTERED_WITCHSPACE"];
13521
13522 if (![[UNIVERSE sun] willGoNova])
13523 {
13524 // if the sun's not going nova, add a new ship like this one leaving.
13525 [UNIVERSE witchspaceShipWithPrimaryRole:[self primaryRole]];
13526 }
13527
13528 [UNIVERSE removeEntity:self];
13529}
13530
13531
13532- (void) leaveWitchspace
13533{
13534 Quaternion q1;
13536 Vector v1 = vector_forward_from_quaternion(q1);
13537 double d1 = 0.0;
13538
13539 GLfloat min_d1 = [UNIVERSE safeWitchspaceExitDistance];
13540
13541 while (fabs(d1) < min_d1)
13542 {
13543 // not scannerRange - has no effect on witchspace exit
13544 d1 = SCANNER_MAX_RANGE * (randf() - randf());
13545 }
13546
13547 HPVector exitposition = [UNIVERSE getWitchspaceExitPosition];
13548 exitposition.x += v1.x * d1; // randomise exit position
13549 exitposition.y += v1.y * d1;
13550 exitposition.z += v1.z * d1;
13551 [self setPosition:exitposition];
13552 [self witchspaceLeavingEffects];
13553}
13554
13555
13556- (BOOL) witchspaceLeavingEffects
13557{
13558 // all ships exiting witchspace will share the same orientation.
13559 orientation = [UNIVERSE getWitchspaceExitRotation];
13560 flightRoll = 0.0;
13561 stick_roll = 0.0;
13562 flightPitch = 0.0;
13563 stick_pitch = 0.0;
13564 flightYaw = 0.0;
13565 stick_yaw = 0.0;
13566 flightSpeed = 50.0; // constant speed same for all ships
13567// was a quarter of max speed, so the Anaconda speeds up and most
13568// others slow down - CIM
13569// will be overridden if left witchspace via a genuine wormhole
13570 velocity = kZeroVector;
13571 if (![UNIVERSE addEntity:self]) // AI and status get initialised here
13572 {
13573 return NO;
13574 }
13575 [self setStatus:STATUS_EXITING_WITCHSPACE];
13576 [shipAI message:@"EXITED_WITCHSPACE"];
13577
13578 [UNIVERSE addWitchspaceJumpEffectForShip:self];
13579 [self setStatus:STATUS_IN_FLIGHT];
13580 return YES;
13581}
13582
13583
13584- (void) markAsOffender:(int)offence_value
13585{
13586 [self markAsOffender:offence_value withReason:kOOLegalStatusReasonUnknown];
13587}
13588
13589
13590- (void) markAsOffender:(int)offence_value withReason:(OOLegalStatusReason)reason
13591{
13592 if (![self isPolice] && ![self isCloaked] && self != [UNIVERSE station])
13593 {
13594 if ([self isSubEntity])
13595 {
13596 [[self parentEntity] markAsOffender:offence_value withReason:reason];
13597 }
13598 else
13599 {
13600 if ((scanClass == CLASS_THARGOID || scanClass == CLASS_STATION) && reason != kOOLegalStatusReasonSetup && reason != kOOLegalStatusReasonByScript)
13601 {
13602 return; // no non-scripted bounties for thargoids and stations
13603 }
13604
13605 JSContext *context = OOJSAcquireContext();
13606
13607 jsval amountVal = JSVAL_VOID;
13608 JS_NewNumberValue(context, (bounty | offence_value)-bounty, &amountVal);
13609
13610 bounty |= offence_value; // can't set the new bounty until the size of the change is known
13611
13612 jsval reasonVal = OOJSValueFromLegalStatusReason(context, reason);
13613
13614 ShipScriptEvent(context, self, "shipBountyChanged", amountVal, reasonVal);
13615
13616 OOJSRelinquishContext(context);
13617
13618 }
13619 }
13620}
13621
13622
13623// Exposed to AI
13624- (void) switchLightsOn
13625{
13626 OOFlasherEntity *se = nil;
13627 ShipEntity *sub = nil;
13628
13629 _lightsActive = YES;
13630
13631 foreach (se, [self flasherEnumerator])
13632 {
13633 [se setActive:YES];
13634 }
13635 foreach (sub, [self shipSubEntityEnumerator])
13636 {
13637 [sub switchLightsOn];
13638 }
13639}
13640
13641// Exposed to AI
13642- (void) switchLightsOff
13643{
13644 OOFlasherEntity *se = nil;
13645 ShipEntity *sub = nil;
13646
13647 _lightsActive = NO;
13648
13649 foreach (se, [self flasherEnumerator])
13650 {
13651 [se setActive:NO];
13652 }
13653 foreach (sub, [self shipSubEntityEnumerator])
13654 {
13655 [sub switchLightsOff];
13656 }
13657}
13658
13659
13660- (BOOL) lightsActive
13661{
13662 return _lightsActive;
13663}
13664
13665
13666- (void) setDestination:(HPVector) dest
13667{
13668 _destination = dest;
13669 frustration = 0.0; // new destination => no frustration!
13670}
13671
13672
13673- (void) setEscortDestination:(HPVector) dest
13674{
13675 _destination = dest; // don't reset frustration for escorts.
13676}
13677
13678
13679- (BOOL) canAcceptEscort:(ShipEntity *)potentialEscort
13680{
13681 if (dockingInstructions) // we are busy with docking.
13682 {
13683 return NO;
13684 }
13685 if (scanClass != [potentialEscort scanClass]) // this makes sure that wingman can only select police, thargons only thargoids.
13686 {
13687 return NO;
13688 }
13689 if ([self bounty] == 0 && [potentialEscort bounty] != 0) // clean mothers can only accept clean escorts
13690 {
13691 return NO;
13692 }
13693 if (![self isEscort]) // self is NOT wingman or escort or thargon
13694 {
13695 return [potentialEscort isEscort]; // is wingman or escort or thargon
13696 }
13697 return NO;
13698}
13699
13700
13701- (BOOL) acceptAsEscort:(ShipEntity *) other_ship
13702{
13703 // can't pair with self
13704 if (self == other_ship) return NO;
13705
13706 // no longer in flight, probably entered wormhole without telling escorts.
13707 if ([self status] != STATUS_IN_FLIGHT) return NO;
13708
13709 //increased stack depth at which it can accept escorts to avoid rejections at this stage.
13710 //doesn't seem to have any adverse effect for now. - Kaks.
13711 if ([shipAI stackDepth] > 3)
13712 {
13713 OOLog(@"ship.escort.reject", @"%@ rejecting escort %@ because AI stack depth is %llu.",self, other_ship, [shipAI stackDepth]);
13714 return NO;
13715 }
13716
13717 if ([self canAcceptEscort:other_ship])
13718 {
13719 OOShipGroup *escortGroup = [self escortGroup];
13720
13721 if ([escortGroup containsShip:other_ship]) return YES;
13722
13723 // check total number acceptable
13724 // the system's patrols don't have escorts set inside their dictionary, but accept max escorts.
13725 if (_maxEscortCount == 0 && ([self hasPrimaryRole:@"police"] || [self hasPrimaryRole:@"hunter"] || [self hasRole:@"thargoid-mothership"]))
13726 {
13727 _maxEscortCount = MAX_ESCORTS;
13728 }
13729
13730 NSUInteger maxEscorts = _maxEscortCount; // never bigger than MAX_ESCORTS.
13731 NSUInteger escortCount = [escortGroup count] - 1; // always 0 or higher.
13732
13733 if (escortCount < maxEscorts)
13734 {
13735 [other_ship setGroup:escortGroup];
13736 if ([self group] == nil)
13737 {
13738 [self setGroup:escortGroup];
13739 }
13740 else if ([self group] != escortGroup) [[self group] addShip:other_ship];
13741
13742 if (([other_ship maxFlightSpeed] < cruiseSpeed) && ([other_ship maxFlightSpeed] > cruiseSpeed * 0.3))
13743 {
13744 cruiseSpeed = [other_ship maxFlightSpeed] * 0.99;
13745 }
13746
13747 OOLog(@"ship.escort.accept", @"%@ accepting escort %@.", self, other_ship);
13748
13749 [self doScriptEvent:OOJSID("shipAcceptedEscort") withArgument:other_ship];
13750 [other_ship doScriptEvent:OOJSID("escortAccepted") withArgument:self];
13751 [shipAI message:@"ACCEPTED_ESCORT"];
13752 return YES;
13753 }
13754 else
13755 {
13756 OOLog(@"ship.escort.reject", @"%@ already got max escorts(%llu). Escort rejected: %@.", self, escortCount, other_ship);
13757 }
13758 }
13759 else
13760 {
13761 OOLog(@"ship.escort.reject", @"%@ failed canAcceptEscort for escort %@.", self, other_ship);
13762 }
13763
13764
13765 return NO;
13766}
13767
13768
13769// Exposed to AI
13770- (void) updateEscortFormation
13771{
13772 _escortPositionsValid = NO;
13773}
13774
13775
13776/*
13777 NOTE: it's tempting to call refreshEscortPositions from coordinatesForEscortPosition:
13778 as needed, but that would cause unnecessary extra work if the formation
13779 callback itself calls updateEscortFormation.
13780*/
13781- (void) refreshEscortPositions
13782{
13783 if (!_escortPositionsValid)
13784 {
13785 JSContext *context = OOJSAcquireContext();
13786 jsval result;
13787 jsval args[] = { INT_TO_JSVAL(0), INT_TO_JSVAL(_maxEscortCount) };
13788 BOOL OK;
13789
13790 // Reset validity first so updateEscortFormation can be called from the update callback.
13791 _escortPositionsValid = YES;
13792
13793 uint8_t i;
13794 for (i = 0; i < _maxEscortCount; i++)
13795 {
13796 args[0] = INT_TO_JSVAL(i);
13797 OK = [script callMethod:OOJSID("coordinatesForEscortPosition")
13798 inContext:context
13799 withArguments:args count:sizeof args / sizeof *args
13800 result:&result];
13801
13802 if (OK) OK = JSValueToVector(context, result, &_escortPositions[i]);
13803
13804 if (!OK) _escortPositions[i] = kZeroVector;
13805 }
13806
13807 OOJSRelinquishContext(context);
13808 }
13809}
13810
13811
13812- (HPVector) coordinatesForEscortPosition:(unsigned)idx
13813{
13814 /*
13815 This function causes problems with Thargoids: their missiles (aka Thargons) are automatically
13816 added to the escorts group, and when a mother ship dies all thargons will attach themselves
13817 as escorts to the surviving battleships. This can lead to huge escort groups.
13818 TODO: better handling of Thargoid groups:
13819 - put thargons (& all other thargon missiles) in their own non-escort group perhaps?
13820 */
13821
13822 // The _escortPositions array is always MAX_ESCORTS long.
13823 // Kludge: return the same last escort position if we have escorts above MAX_ESCORTS...
13824 idx = MIN(idx, (unsigned)(MAX_ESCORTS - 1));
13825
13826 return HPvector_add(self->position, vectorToHPVector(quaternion_rotate_vector([self normalOrientation], _escortPositions[idx])));
13827}
13828
13829
13830// Exposed to AI
13831- (void) deployEscorts
13832{
13833 ShipEntity *escort = nil;
13834 ShipEntity *target = nil;
13835 NSMutableSet *idleEscorts = nil;
13836 unsigned deployCount;
13837
13838 if ([self primaryTarget] == nil || _escortGroup == nil) return;
13839
13840 OOShipGroup *escortGroup = [self escortGroup];
13841 NSUInteger escortCount = [escortGroup count] - 1; // escorts minus leader.
13842 if (escortCount == 0) return;
13843
13844 if ([self group] == nil) [self setGroup:escortGroup];
13845
13846 if ([self primaryTarget] == [self lastEscortTarget])
13847 {
13848 // already deployed escorts onto this target!
13849 return;
13850 }
13851
13852 [self setLastEscortTarget:[self primaryTarget]];
13853
13854 // Find idle escorts
13855 idleEscorts = [NSMutableSet set];
13856 foreach (escort, [self escortEnumerator])
13857 {
13858 if (![[[escort getAI] name] isEqualToString:@"interceptAI.plist"] && ![escort hasNewAI])
13859 {
13860 [idleEscorts addObject:escort];
13861 }
13862 else if ([escort hasNewAI])
13863 {
13864 // JS-based escorts get a help request
13865 [escort doScriptEvent:OOJSID("helpRequestReceived") withArgument:self andArgument:[self primaryTarget]];
13866 }
13867 }
13868
13869 escortCount = [idleEscorts count];
13870 if (escortCount == 0) return;
13871
13872 deployCount = ranrot_rand() % escortCount + 1;
13873
13874 // Deploy deployCount idle escorts.
13875 target = [self primaryTarget];
13876 foreach (escort, idleEscorts)
13877 {
13878 [escort addTarget:target];
13879 [escort setAITo:@"interceptAI.plist"];
13880 [escort doScriptEvent:OOJSID("escortAttack") withArgument:target];
13881
13882 if (--deployCount == 0) break;
13883 }
13884
13885 [self updateEscortFormation];
13886}
13887
13888
13889// Exposed to AI
13890- (void) dockEscorts
13891{
13892 if (![self hasEscorts]) return;
13893
13894 OOShipGroup *escortGroup = [self escortGroup];
13895 ShipEntity *escort = nil;
13896 ShipEntity *target = [self primaryTarget];
13897 unsigned i = 0;
13898 // Note: works on escortArray rather than escortEnumerator because escorts may be mutated.
13899 foreach (escort, [self escortArray])
13900 {
13901 float delay = i++ * 3.0 + 1.5; // send them off at three second intervals
13902 AI *ai = [escort getAI];
13903
13904 // act individually now!
13905 if ([escort group] == escortGroup) [escort setGroup:nil];
13906 if ([escort owner] == self) [escort setOwner:escort];
13907 if(target && [target isStation]) [escort setTargetStation:target];
13908 // JSAI: handles own delay
13909 if (![escort hasNewAI])
13910 {
13911 [escort setAITo:@"dockingAI.plist"];
13912 [ai setState:@"ABORT" afterDelay:delay + 0.25];
13913 }
13914 [escort doScriptEvent:OOJSID("escortDock") withArgument:[NSNumber numberWithFloat:delay]];
13915 }
13916
13917 // We now have no escorts.
13918 [_escortGroup release];
13919 _escortGroup = nil;
13920}
13921
13922
13923- (void) setTargetToNearestStationIncludingHostiles:(BOOL) includeHostiles
13924{
13925 // check if the groupID (parent ship) points to a station...
13926 Entity *mother = [[self group] leader];
13927 if ([mother isStation])
13928 {
13929 [self addTarget:mother];
13930 [self setTargetStation:mother];
13931 return; // head for mother!
13932 }
13933
13934 /*- selects the nearest station it can find -*/
13935 if (!UNIVERSE)
13936 return;
13937 int ent_count = UNIVERSE->n_entities;
13938 Entity **uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list
13939 Entity *my_entities[ent_count];
13940 int i;
13941 int station_count = 0;
13942 for (i = 0; i < ent_count; i++)
13943 if (uni_entities[i]->isStation)
13944 my_entities[station_count++] = [uni_entities[i] retain]; // retained
13945 //
13946 StationEntity *thing = nil, *station = nil;
13947 double range2, nearest2 = SCANNER_MAX_RANGE2 * 1000000.0; // 1000x typical scanner range (25600 km), squared.
13948 for (i = 0; i < station_count; i++)
13949 {
13950 thing = (StationEntity *)my_entities[i];
13951 range2 = HPdistance2(position, thing->position);
13952 if (range2 < nearest2 && (includeHostiles || ![thing isHostileTo:self]))
13953 {
13954 station = thing;
13955 nearest2 = range2;
13956 }
13957 }
13958 for (i = 0; i < station_count; i++)
13959 [my_entities[i] release]; // released
13960 //
13961 if (station)
13962 {
13963 [self addTarget:station];
13964 [self setTargetStation:station];
13965 }
13966 else
13967 {
13968 [shipAI message:@"NO_STATION_FOUND"];
13969 }
13970}
13971
13972
13973// Exposed to AI
13974- (void) setTargetToNearestFriendlyStation
13975{
13976 [self setTargetToNearestStationIncludingHostiles:NO];
13977}
13978
13979
13980// Exposed to AI
13981- (void) setTargetToNearestStation
13982{
13983 [self setTargetToNearestStationIncludingHostiles:YES];
13984}
13985
13986
13987// Exposed to AI
13988- (void) setTargetToSystemStation
13989{
13990 StationEntity* system_station = [UNIVERSE station];
13991
13992 if (!system_station)
13993 {
13994 [shipAI message:@"NOTHING_FOUND"];
13995 [shipAI message:@"NO_STATION_FOUND"];
13996 DESTROY(_primaryTarget);
13997 [self setTargetStation:nil];
13998 return;
13999 }
14000
14001 if (!system_station->isStation)
14002 {
14003 [shipAI message:@"NOTHING_FOUND"];
14004 [shipAI message:@"NO_STATION_FOUND"];
14005 DESTROY(_primaryTarget);
14006 [self setTargetStation:nil];
14007 return;
14008 }
14009
14010 [self addTarget:system_station];
14011 [self setTargetStation:system_station];
14012 return;
14013}
14014
14015
14016- (void) landOnPlanet:(OOPlanetEntity *)planet
14017{
14018 if (planet && [self isShuttle])
14019 {
14020 [planet welcomeShuttle:self];
14021 }
14022 [self doScriptEvent:OOJSID("shipLandedOnPlanet") withArgument:planet andReactToAIMessage:@"LANDED_ON_PLANET"];
14023
14024#ifndef NDEBUG
14025 if ([self reportAIMessages])
14026 {
14027 OOLog(@"planet.collide.shuttleLanded", @"DEBUG: %@ landed on planet %@", self, planet);
14028 }
14029#endif
14030
14031 [UNIVERSE removeEntity:self];
14032}
14033
14034
14035// Exposed to AI
14036- (void) abortDocking
14037{
14038 [[UNIVERSE findEntitiesMatchingPredicate:IsStationPredicate
14039 parameter:nil
14040 inRange:-1
14041 ofEntity:nil]
14042 makeObjectsPerformSelector:@selector(abortDockingForShip:) withObject:self];
14043}
14044
14045
14046- (NSDictionary *) dockingInstructions
14047{
14048 return dockingInstructions;
14049}
14050
14051
14052- (void) broadcastThargoidDestroyed
14053{
14054 [[UNIVERSE findShipsMatchingPredicate:HasRolePredicate
14055 parameter:@"tharglet"
14056 inRange:SCANNER_MAX_RANGE
14057 ofEntity:self]
14058 makeObjectsPerformSelector:@selector(sendAIMessage:) withObject:@"THARGOID_DESTROYED"];
14059}
14060
14061
14062static BOOL AuthorityPredicate(Entity *entity, void *parameter)
14063{
14064 ShipEntity *victim = parameter;
14065
14066 // Select main station, if victim is in aegis
14067 if (entity == [UNIVERSE station] && [victim withinStationAegis])
14068 {
14069 return YES;
14070 }
14071
14072 // Select police units in typical scanner range
14073 if ([entity scanClass] == CLASS_POLICE &&
14074 HPdistance2([victim position], [entity position]) < SCANNER_MAX_RANGE2)
14075 {
14076 return YES;
14077 }
14078
14079 // Reject others
14080 return NO;
14081}
14082
14083
14084- (void) broadcastHitByLaserFrom:(ShipEntity *) aggressor_ship
14085{
14086 /*-- If you're clean, locates all police and stations in range and tells them OFFENCE_COMMITTED --*/
14087 if (!UNIVERSE) return;
14088 if ([self bounty]) return;
14089 if (!aggressor_ship) return;
14090
14091 if ( (scanClass == CLASS_NEUTRAL)||
14092 (scanClass == CLASS_STATION)||
14093 (scanClass == CLASS_BUOY)||
14094 (scanClass == CLASS_POLICE)||
14095 (scanClass == CLASS_MILITARY)||
14096 (scanClass == CLASS_PLAYER)) // only for active ships...
14097 {
14098 NSArray *authorities = nil;
14099 ShipEntity *auth = nil;
14100
14101 authorities = [UNIVERSE findShipsMatchingPredicate:AuthorityPredicate
14102 parameter:self
14103 inRange:-1
14104 ofEntity:nil];
14105 foreach (auth, authorities)
14106 {
14107 [auth setFoundTarget:aggressor_ship];
14108 [auth doScriptEvent:OOJSID("offenceCommittedNearby") withArgument:aggressor_ship andArgument:self];
14109 [auth reactToAIMessage:@"OFFENCE_COMMITTED" context:@"combat update"];
14110 }
14111 }
14112}
14113
14114
14115- (void) sendMessage:(NSString *) message_text toShip:(ShipEntity*) other_ship withUnpilotedOverride:(BOOL)unpilotedOverride
14116{
14117 if (!other_ship || !message_text) return;
14118 if (!crew && !unpilotedOverride) return;
14119
14120 double d2 = HPdistance2(position, [other_ship position]);
14121 if (d2 > scannerRange * scannerRange)
14122 return; // out of comms range
14123
14124 NSString *expandedMessage = OOExpand(message_text); // consistent with broadcast message.
14125
14126 if (other_ship->isPlayer)
14127 {
14128 [self setCommsMessageColor];
14129 [(PlayerEntity *)other_ship receiveCommsMessage:expandedMessage from:self];
14130 messageTime = 6.0;
14131 [UNIVERSE resetCommsLogColor];
14132 }
14133 else
14134 [other_ship receiveCommsMessage:expandedMessage from:self];
14135}
14136
14137
14138- (void) sendExpandedMessage:(NSString *)message_text toShip:(ShipEntity *)other_ship
14139{
14140 if (!other_ship || !crew)
14141 return; // nobody to receive or send the signal
14142 if ((lastRadioMessage) && (messageTime > 0.0) && [message_text isEqual:lastRadioMessage])
14143 return; // don't send the same message too often
14144 [lastRadioMessage autorelease];
14145 lastRadioMessage = [message_text retain];
14146
14147 double d2 = HPdistance2(position, [other_ship position]);
14148 if (d2 > scannerRange * scannerRange)
14149 {
14150 // out of comms range
14151 return;
14152 }
14153
14154 Random_Seed very_random_seed;
14155 very_random_seed.a = rand() & 255;
14156 very_random_seed.b = rand() & 255;
14157 very_random_seed.c = rand() & 255;
14158 very_random_seed.d = rand() & 255;
14159 very_random_seed.e = rand() & 255;
14160 very_random_seed.f = rand() & 255;
14161 seed_RNG_only_for_planet_description(very_random_seed);
14162
14163 NSDictionary *specials = [NSDictionary dictionaryWithObjectsAndKeys:
14164 [self displayName], @"[self:name]",
14165 [other_ship identFromShip: self], @"[target:name]",
14166 nil];
14167 NSString *expandedMessage = OOExpandDescriptionString(OOStringExpanderDefaultRandomSeed(), message_text, specials, nil, nil, kOOExpandNoOptions);
14168
14169 [self sendMessage:expandedMessage toShip:other_ship withUnpilotedOverride:NO];
14170}
14171
14172
14173- (void) broadcastAIMessage:(NSString *) ai_message
14174{
14175 NSString *expandedMessage = OOExpand(ai_message);
14176
14177 [self checkScanner];
14178 unsigned i;
14179 for (i = 0; i < n_scanned_ships ; i++)
14180 {
14181 ShipEntity* ship = scanned_ships[i];
14182 [[ship getAI] message: expandedMessage];
14183 }
14184}
14185
14186
14187- (void) broadcastMessage:(NSString *) message_text withUnpilotedOverride:(BOOL) unpilotedOverride
14188{
14189 NSString *expandedMessage = OOExpand(message_text); // consistent with broadcast message.
14190
14191
14192 if (!crew && !unpilotedOverride)
14193 return; // nobody to send the signal and no override for unpiloted craft is set
14194
14195 [self checkScanner];
14196 unsigned i;
14197 for (i = 0; i < n_scanned_ships ; i++)
14198 {
14199 ShipEntity* ship = scanned_ships[i];
14200 if (![ship isPlayer]) [ship receiveCommsMessage:expandedMessage from:self];
14201 }
14202
14203 PlayerEntity *player = PLAYER; // make sure that the player always receives a message when in range
14204 // SCANNER_MAX_RANGE2 because it's the player's scanner range
14205 // which is important
14206 if (HPdistance2(position, [player position]) < SCANNER_MAX_RANGE2)
14207 {
14208 [self setCommsMessageColor];
14209 [player receiveCommsMessage:expandedMessage from:self];
14210 messageTime = 6.0;
14211 [UNIVERSE resetCommsLogColor];
14212 }
14213}
14214
14215
14216- (void) setCommsMessageColor
14217{
14218 float hue = 0.0625f * (universalID & 15);
14219 [[UNIVERSE commLogGUI] setTextColor:[OOColor colorWithHue:hue saturation:0.375f brightness:1.0f alpha:1.0f]];
14220 if (scanClass == CLASS_THARGOID)
14221 [[UNIVERSE commLogGUI] setTextColor:[OOColor greenColor]];
14222 if (scanClass == CLASS_POLICE)
14223 [[UNIVERSE commLogGUI] setTextColor:[OOColor cyanColor]];
14224}
14225
14226
14227- (void) receiveCommsMessage:(NSString *) message_text from:(ShipEntity *) other
14228{
14229 // Too complex for AI scripts to handle, JS event only.
14230 [self doScriptEvent:OOJSID("commsMessageReceived") withArgument:message_text andArgument:other];
14231}
14232
14233
14234- (void) commsMessage:(NSString *)valueString withUnpilotedOverride:(BOOL)unpilotedOverride
14235{
14236 Random_Seed very_random_seed;
14237 very_random_seed.a = rand() & 255;
14238 very_random_seed.b = rand() & 255;
14239 very_random_seed.c = rand() & 255;
14240 very_random_seed.d = rand() & 255;
14241 very_random_seed.e = rand() & 255;
14242 very_random_seed.f = rand() & 255;
14243 seed_RNG_only_for_planet_description(very_random_seed);
14244
14245 [self broadcastMessage:valueString withUnpilotedOverride:unpilotedOverride];
14246}
14247
14248
14249- (BOOL) markedForFines
14250{
14251 return being_fined;
14252}
14253
14254
14255- (BOOL) markForFines
14256{
14257 if (being_fined)
14258 return NO; // can't mark twice
14259 being_fined = ([self legalStatus] > 0);
14260 return being_fined;
14261}
14262
14263
14264- (BOOL) isMining
14265{
14266 return ((behaviour == BEHAVIOUR_ATTACK_MINING_TARGET)&&([forward_weapon_type isMiningLaser]));
14267}
14268
14269
14270- (void) interpretAIMessage:(NSString *)ms
14271{
14272 if ([ms hasPrefix:AIMS_AGGRESSOR_SWITCHED_TARGET])
14273 {
14274 // if I'm under attack send a thank-you message to the rescuer
14275 //
14276 NSArray* tokens = ScanTokensFromString(ms);
14277 int switcher_id = [(NSString*)[tokens objectAtIndex:1] intValue]; // Attacker that switched targets.
14278 Entity* switcher = [UNIVERSE entityForUniversalID:switcher_id];
14279 int rescuer_id = [(NSString*)[tokens objectAtIndex:2] intValue]; // New primary target of attacker.
14280 Entity* rescuer = [UNIVERSE entityForUniversalID:rescuer_id];
14281 if ((switcher == [self primaryAggressor])&&(switcher == [self primaryTarget])&&(switcher)&&(rescuer)&&(rescuer->isShip)&&([self thankedShip] != rescuer)&&(scanClass != CLASS_THARGOID))
14282 {
14283 ShipEntity* rescueShip = (ShipEntity*)rescuer;
14284// ShipEntity* switchingShip = (ShipEntity*)switcher;
14285 if (scanClass == CLASS_POLICE)
14286 {
14287 [self sendExpandedMessage:@"[police-thanks-for-assist]" toShip:rescueShip];
14288 [rescueShip setBounty:[rescueShip bounty] * 0.80 withReason:kOOLegalStatusReasonAssistingPolice]; // lower bounty by 20%
14289 }
14290 else
14291 {
14292 [self sendExpandedMessage:@"[thanks-for-assist]" toShip:rescueShip];
14293 }
14294 [self setThankedShip:rescuer];
14295 }
14296 }
14297}
14298
14299
14300- (BoundingBox) findBoundingBoxRelativeTo:(Entity *)other InVectors:(Vector) _i :(Vector) _j :(Vector) _k
14301{
14302 HPVector opv = other ? other->position : position;
14303 return [self findBoundingBoxRelativeToPosition:opv InVectors:_i :_j :_k];
14304}
14305
14306
14307// Exposed to AI and legacy scripts.
14308- (void) spawn:(NSString *)roles_number
14309{
14310 NSArray *tokens = ScanTokensFromString(roles_number);
14311 NSString *roleString = nil;
14312 NSString *numberString = nil;
14313 NSUInteger number;
14314
14315 if ([tokens count] != 2)
14316 {
14317 OOLog(kOOLogSyntaxAddShips, @"***** Could not spawn: \"%@\" (must be two tokens, role and number)",roles_number);
14318 return;
14319 }
14320
14321 roleString = [tokens oo_stringAtIndex:0];
14322 numberString = [tokens oo_stringAtIndex:1];
14323
14324 number = [numberString intValue];
14325
14326 [self spawnShipsWithRole:roleString count:number];
14327}
14328
14329
14330- (int) checkShipsInVicinityForWitchJumpExit
14331{
14332 // checks if there are any large masses close by
14333 // since we want to place the space station at least 10km away
14334 // the formula we'll use is K x m / d2 < 1.0
14335 // (m = mass, d2 = distance squared)
14336 // coriolis station is mass 455,223,200
14337 // 10km is 10,000m,
14338 // 10km squared is 100,000,000
14339 // therefore K is 0.22 (approx)
14340
14341 int result = NO_TARGET;
14342
14343 GLfloat k = 0.1;
14344
14345 int ent_count = UNIVERSE->n_entities;
14346 Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list
14347 ShipEntity* my_entities[ent_count];
14348 int i;
14349
14350 int ship_count = 0;
14351 for (i = 0; i < ent_count; i++)
14352 if ((uni_entities[i]->isShip)&&(uni_entities[i] != self))
14353 my_entities[ship_count++] = (ShipEntity*)[uni_entities[i] retain]; // retained
14354 //
14355 for (i = 0; (i < ship_count)&&(result == NO_TARGET) ; i++)
14356 {
14357 ShipEntity* ship = my_entities[i];
14358 HPVector delta = HPvector_between(position, ship->position);
14359 GLfloat d2 = HPmagnitude2(delta);
14360 if (![ship isPlayer] || ![PLAYER isDocked])
14361 { // player doesn't block if docked
14362 if ((k * [ship mass] > d2)&&(d2 < SCANNER_MAX_RANGE2)) // if you go off (typical) scanner from a blocker - it ceases to block
14363 result = [ship universalID];
14364 }
14365 }
14366 for (i = 0; i < ship_count; i++)
14367 [my_entities[i] release]; // released
14368
14369 return result;
14370}
14371
14372
14373- (BOOL) trackCloseContacts
14374{
14375 return trackCloseContacts;
14376}
14377
14378
14379- (void) setTrackCloseContacts:(BOOL) value
14380{
14381 if (value == (BOOL)trackCloseContacts) return;
14382
14383 trackCloseContacts = value;
14384 [closeContactsInfo release];
14385
14386 if (trackCloseContacts)
14387 {
14388 closeContactsInfo = [[NSMutableDictionary alloc] init];
14389 }
14390 else
14391 {
14392 closeContactsInfo = nil;
14393 }
14394}
14395
14396
14397#if OO_SALVAGE_SUPPORT
14398// Never used.
14399- (void) claimAsSalvage
14400{
14401 // Create a bouy and beacon where the hulk is.
14402 // Get the main GalCop station to launch a pilot boat to deliver a pilot to the hulk.
14403 OOLog(@"claimAsSalvage.called", @"claimAsSalvage called on %@ %@", [self name], [self roleSet]);
14404
14405 // Not an abandoned hulk, so don't allow the salvage
14406 if (![self isHulk])
14407 {
14408 OOLog(@"claimAsSalvage.failed.notHulk", @"claimAsSalvage failed because not a hulk");
14409 return;
14410 }
14411
14412 // Set target to main station, and return now if it can't be found
14413 [self setTargetToSystemStation];
14414 if ([self primaryTarget] == nil)
14415 {
14416 OOLog(@"claimAsSalvage.failed.noStation", @"claimAsSalvage failed because did not find a station");
14417 return;
14418 }
14419
14420 // Get the station to launch a pilot boat to bring a pilot out to the hulk (use a viper for now)
14421 StationEntity *station = (StationEntity *)[self primaryTarget];
14422 OOLog(@"claimAsSalvage.requestingPilot", @"claimAsSalvage asking station to launch a pilot boat");
14423 [station launchShipWithRole:@"pilot"];
14424 [self setReportAIMessages:YES];
14425 OOLog(@"claimAsSalvage.success", @"claimAsSalvage setting own state machine to capturedShipAI.plist");
14426 [self setAITo:@"capturedShipAI.plist"];
14427}
14428
14429
14430- (void) sendCoordinatesToPilot
14431{
14432 Entity *scan;
14433 ShipEntity *scanShip, *pilot;
14434
14435 n_scanned_ships = 0;
14436 scan = z_previous;
14437 OOLog(@"ship.pilotage", @"searching for pilot boat");
14438 while (scan &&(scan->isShip == NO))
14439 {
14440 scan = scan->z_previous; // skip non-ships
14441 }
14442
14443 pilot = nil;
14444 while (scan)
14445 {
14446 if (scan->isShip)
14447 {
14448 scanShip = (ShipEntity *)scan;
14449
14450 if ([self hasRole:@"pilot"] == YES)
14451 {
14452 if ([scanShip primaryTarget] == nil)
14453 {
14454 OOLog(@"ship.pilotage", @"found pilot boat with no target, will use this one");
14455 pilot = scanShip;
14456 [pilot setPrimaryRole:@"pilot"];
14457 break;
14458 }
14459 }
14460 }
14461 scan = scan->z_previous;
14462 while (scan && (scan->isShip == NO))
14463 {
14464 scan = scan->z_previous;
14465 }
14466 }
14467
14468 if (pilot != nil)
14469 {
14470 OOLog(@"ship.pilotage", @"becoming pilot target and setting AI");
14471 [pilot setReportAIMessages:YES];
14472 [pilot addTarget:self];
14473 [pilot setAITo:@"pilotAI.plist"];
14474 [self reactToAIMessage:@"FOUND_PILOT" context:@"flight update"];
14475 }
14476}
14477
14478
14479- (void) pilotArrived
14480{
14481 [self setHulk:NO];
14482 [self reactToAIMessage:@"PILOT_ARRIVED" context:@"flight update"];
14483}
14484#endif
14485
14486
14487#ifndef NDEBUG
14488- (void)dumpSelfState
14489{
14490 NSMutableArray *flags = nil;
14491 NSString *flagsString = nil;
14492
14493 [super dumpSelfState];
14494
14495 OOLog(@"dumpState.shipEntity", @"Type: %@", [self shipDataKey]);
14496 OOLog(@"dumpState.shipEntity", @"Name: %@", name);
14497 OOLog(@"dumpState.shipEntity", @"Display Name: %@", [self displayName]);
14498 OOLog(@"dumpState.shipEntity", @"Roles: %@", [self roleSet]);
14499 OOLog(@"dumpState.shipEntity", @"Primary role: %@", primaryRole);
14500 OOLog(@"dumpState.shipEntity", @"Script: %@", script);
14501 OOLog(@"dumpState.shipEntity", @"Subentity count: %llu", [self subEntityCount]);
14502 OOLog(@"dumpState.shipEntity", @"Behaviour: %@", OOStringFromBehaviour(behaviour));
14503 id target = [self primaryTarget];
14504 if (target == nil) target = @"<none>";
14505 OOLog(@"dumpState.shipEntity", @"Target: %@", target);
14506 OOLog(@"dumpState.shipEntity", @"Destination: %@", HPVectorDescription(_destination));
14507 OOLog(@"dumpState.shipEntity", @"Other destination: %@", HPVectorDescription(coordinates));
14508 OOLog(@"dumpState.shipEntity", @"Waypoint count: %u", number_of_navpoints);
14509 OOLog(@"dumpState.shipEntity", @"Desired speed: %g", desired_speed);
14510 OOLog(@"dumpState.shipEntity", @"Thrust: %g", thrust);
14511 if ([self escortCount] != 0) OOLog(@"dumpState.shipEntity", @"Escort count: %u", [self escortCount]);
14512 OOLog(@"dumpState.shipEntity", @"Fuel: %i", fuel);
14513 OOLog(@"dumpState.shipEntity", @"Fuel accumulator: %g", fuel_accumulator);
14514 OOLog(@"dumpState.shipEntity", @"Missile count: %u", missiles);
14515
14516 if (shipAI != nil && OOLogWillDisplayMessagesInClass(@"dumpState.shipEntity.ai"))
14517 {
14518 OOLog(@"dumpState.shipEntity.ai", @"%@", @"AI:");
14520 OOLogIndent();
14521 @try
14522 {
14523 [shipAI dumpState];
14524 }
14525 @catch (id exception) {}
14527 }
14528 OOLog(@"dumpState.shipEntity", @"Accuracy: %g", accuracy);
14529 OOLog(@"dumpState.shipEntity", @"Jink position: %@", VectorDescription(jink));
14530 OOLog(@"dumpState.shipEntity", @"Frustration: %g", frustration);
14531 OOLog(@"dumpState.shipEntity", @"Success factor: %g", success_factor);
14532 OOLog(@"dumpState.shipEntity", @"Shots fired: %u", shot_counter);
14533 OOLog(@"dumpState.shipEntity", @"Time since shot: %g", [self shotTime]);
14534 OOLog(@"dumpState.shipEntity", @"Spawn time: %g (%g seconds ago)", [self spawnTime], [self timeElapsedSinceSpawn]);
14535 if ([self isBeacon])
14536 {
14537 OOLog(@"dumpState.shipEntity", @"Beacon code: %@", [self beaconCode]);
14538 }
14539 OOLog(@"dumpState.shipEntity", @"Hull temperature: %g", ship_temperature);
14540 OOLog(@"dumpState.shipEntity", @"Heat insulation: %g", [self heatInsulation]);
14541
14542 flags = [NSMutableArray array];
14543 #define ADD_FLAG_IF_SET(x) if (x) { [flags addObject:@#x]; }
14544 ADD_FLAG_IF_SET(military_jammer_active);
14545 ADD_FLAG_IF_SET(docking_match_rotation);
14546 ADD_FLAG_IF_SET(pitching_over);
14547 ADD_FLAG_IF_SET(reportAIMessages);
14548 ADD_FLAG_IF_SET(being_mined);
14549 ADD_FLAG_IF_SET(being_fined);
14550 ADD_FLAG_IF_SET(isHulk);
14551 ADD_FLAG_IF_SET(trackCloseContacts);
14552 ADD_FLAG_IF_SET(isNearPlanetSurface);
14553 ADD_FLAG_IF_SET(isFrangible);
14554 ADD_FLAG_IF_SET(cloaking_device_active);
14555 ADD_FLAG_IF_SET(canFragment);
14556 ADD_FLAG_IF_SET([self proximityAlert] != nil);
14557 flagsString = [flags count] ? [flags componentsJoinedByString:@", "] : (NSString *)@"none";
14558 OOLog(@"dumpState.shipEntity", @"Flags: %@", flagsString);
14559}
14560#endif
14561
14562
14563- (OOJSScript *)script
14564{
14565 return script;
14566}
14567
14568
14569- (NSDictionary *)scriptInfo
14570{
14571 return (scriptInfo != nil) ? scriptInfo : (NSDictionary *)[NSDictionary dictionary];
14572}
14573
14574
14575- (void) overrideScriptInfo:(NSDictionary *)override
14576{
14577 if (scriptInfo == nil) scriptInfo = [override retain];
14578 else if (override != nil)
14579 {
14580 NSMutableDictionary *newInfo = [NSMutableDictionary dictionaryWithDictionary:scriptInfo];
14581 [newInfo addEntriesFromDictionary:override];
14582 [scriptInfo release];
14583 scriptInfo = [newInfo copy];
14584 }
14585}
14586
14587
14588- (Entity *)entityForShaderProperties
14589{
14590 return [self rootShipEntity];
14591}
14592
14593- (void) setDemoShip: (OOScalar) rate
14594{
14595 demoStartOrientation = orientation;
14596 demoRate = rate;
14597 isDemoShip = YES;
14598 [self setPitch: 0.0f];
14599 [self setRoll: 0.0f];
14600}
14601
14602- (BOOL) isDemoShip
14603{
14604 return isDemoShip;
14605}
14606
14607- (void) setDemoStartTime: (OOTimeAbsolute) time
14608{
14609 demoStartTime = time;
14610}
14611
14612- (OOTimeAbsolute) getDemoStartTime
14613{
14614 return demoStartTime;
14615}
14616
14617// *** Script event dispatch.
14618- (void) doScriptEvent:(jsid)message
14619{
14620 JSContext *context = OOJSAcquireContext();
14621 [self doScriptEvent:message inContext:context withArguments:NULL count:0];
14622 OOJSRelinquishContext(context);
14623}
14624
14625
14626- (void) doScriptEvent:(jsid)message withArgument:(id)argument
14627{
14628 JSContext *context = OOJSAcquireContext();
14629
14630 jsval value = OOJSValueFromNativeObject(context, argument);
14631 [self doScriptEvent:message inContext:context withArguments:&value count:1];
14632
14633 OOJSRelinquishContext(context);
14634}
14635
14636
14637- (void) doScriptEvent:(jsid)message
14638 withArgument:(id)argument1
14639 andArgument:(id)argument2
14640{
14641 JSContext *context = OOJSAcquireContext();
14642
14643 jsval argv[2] = { OOJSValueFromNativeObject(context, argument1), OOJSValueFromNativeObject(context, argument2) };
14644 [self doScriptEvent:message inContext:context withArguments:argv count:2];
14645
14646 OOJSRelinquishContext(context);
14647}
14648
14649
14650- (void) doScriptEvent:(jsid)message withArguments:(NSArray *)arguments
14651{
14652 JSContext *context = OOJSAcquireContext();
14653 uintN i, argc;
14654 jsval *argv = NULL;
14655
14656 // Convert arguments to JS values and make them temporarily un-garbage-collectable.
14657 argc = (uintN)[arguments count];
14658 if (argc != 0)
14659 {
14660 argv = malloc(sizeof *argv * argc);
14661 if (argv != NULL)
14662 {
14663 for (i = 0; i != argc; ++i)
14664 {
14665 argv[i] = [[arguments objectAtIndex:i] oo_jsValueInContext:context];
14666 OOJSAddGCValueRoot(context, &argv[i], "event parameter");
14667 }
14668 }
14669 else argc = 0;
14670 }
14671
14672 [self doScriptEvent:message inContext:context withArguments:argv count:argc];
14673
14674 // Re-garbage-collectibalize the arguments and free the array.
14675 if (argv != NULL)
14676 {
14677 for (i = 0; i != argc; ++i)
14678 {
14679 JS_RemoveValueRoot(context, &argv[i]);
14680 }
14681 free(argv);
14682 }
14683
14684 OOJSRelinquishContext(context);
14685}
14686
14687
14688- (void) doScriptEvent:(jsid)message withArguments:(jsval *)argv count:(uintN)argc
14689{
14690 JSContext *context = OOJSAcquireContext();
14691 [self doScriptEvent:message inContext:context withArguments:argv count:argc];
14692 OOJSRelinquishContext(context);
14693}
14694
14695
14696- (void) doScriptEvent:(jsid)message inContext:(JSContext *)context withArguments:(jsval *)argv count:(uintN)argc
14697{
14698 // This method is a bottleneck so that PlayerEntity can override at one point.
14699 [script callMethod:message inContext:context withArguments:argv count:argc result:NULL];
14700 [aiScript callMethod:message inContext:context withArguments:argv count:argc result:NULL];
14701}
14702
14703
14704- (void) reactToAIMessage:(NSString *)message context:(NSString *)debugContext
14705{
14706 [shipAI reactToMessage:message context:debugContext];
14707}
14708
14709
14710- (void) sendAIMessage:(NSString *)message
14711{
14712 [shipAI message:message];
14713}
14714
14715
14716- (void) doScriptEvent:(jsid)scriptEvent andReactToAIMessage:(NSString *)aiMessage
14717{
14718 [self doScriptEvent:scriptEvent];
14719 [self reactToAIMessage:aiMessage context:nil];
14720}
14721
14722
14723- (void) doScriptEvent:(jsid)scriptEvent withArgument:(id)argument andReactToAIMessage:(NSString *)aiMessage
14724{
14725 [self doScriptEvent:scriptEvent withArgument:argument];
14726 [self reactToAIMessage:aiMessage context:nil];
14727}
14728
14729
14730// exposed for shaders; fake alert level
14731// since NPCs don't have torus drive, they're never at condition green
14732- (OOAlertCondition) alertCondition
14733{
14734 if ([self status] == STATUS_DOCKED)
14735 {
14737 }
14738 if ([self hasHostileTarget] || energy < maxEnergy / 4)
14739 {
14740 return ALERT_CONDITION_RED;
14741 }
14743}
14744
14745
14746- (OOAlertCondition) realAlertCondition
14747{
14748 if ([self status] == STATUS_DOCKED)
14749 {
14751 }
14752 if ([self hasHostileTarget])
14753 {
14754 return ALERT_CONDITION_RED;
14755 }
14756 else
14757 {
14758 NSEnumerator *sEnum = [_defenseTargets objectEnumerator];
14759 ShipEntity *ship = nil;
14760 double scanrange2 = scannerRange * scannerRange;
14761 // FIXME: OOWeakSet doesn't implement NSFastEnumeration protocol.
14762 foreach (ship, sEnum)
14763 {
14764 if ([ship hasHostileTarget] || ([ship isPlayer] && [PLAYER weaponsOnline]))
14765 {
14766 if (HPdistance2([ship position],position) < scanrange2)
14767 {
14768 return ALERT_CONDITION_RED;
14769 }
14770 }
14771 }
14772 // also need to check primary target separately
14773 if ([self hasHostileTarget])
14774 {
14775 Entity *ptarget = [self primaryTargetWithoutValidityCheck];
14776 if (ptarget != nil && [ptarget isShip])
14777 {
14778 ship = (ShipEntity *)ptarget;
14779 if ([ship hasHostileTarget] || ([ship isPlayer] && [PLAYER weaponsOnline]))
14780 {
14781 if (HPdistance2([ship position],position) < scanrange2 * 1.5625)
14782 {
14783 return ALERT_CONDITION_RED;
14784 }
14785 }
14786 }
14787 }
14788 if (_group)
14789 {
14790 sEnum = [_group objectEnumerator];
14791 while ((ship = [sEnum nextObject]))
14792 {
14793 if ([ship hasHostileTarget] || ([ship isPlayer] && [PLAYER weaponsOnline]))
14794 {
14795 if (HPdistance2([ship position],position) < scanrange2)
14796 {
14797 return ALERT_CONDITION_RED;
14798 }
14799 }
14800 }
14801 }
14802 if (_escortGroup && _group != _escortGroup)
14803 {
14804 sEnum = [_escortGroup objectEnumerator];
14805 while ((ship = [sEnum nextObject]))
14806 {
14807 if ([ship hasHostileTarget] || ([ship isPlayer] && [PLAYER weaponsOnline]))
14808 {
14809 if (HPdistance2([ship position],position) < scanrange2)
14810 {
14811 return ALERT_CONDITION_RED;
14812 }
14813 }
14814 }
14815 }
14816 }
14818}
14819
14820
14821// Exposed to AI and scripts.
14822- (void) doNothing
14823{
14824
14825}
14826
14827
14828#ifndef NDEBUG
14829- (NSString *) descriptionForObjDump
14830{
14831 NSString *desc = [super descriptionForObjDump];
14832 desc = [NSString stringWithFormat:@"%@ mass %g", desc, [self mass]];
14833 if (![self isPlayer])
14834 {
14835 desc = [NSString stringWithFormat:@"%@ AI: %@", desc, [[self getAI] shortDescriptionComponents]];
14836 }
14837 return desc;
14838}
14839#endif
14840
14841@end
14842
14843
14844@implementation Entity (SubEntityRelationship)
14845
14846- (BOOL) isShipWithSubEntityShip:(Entity *)other
14847{
14848 return NO;
14849}
14850
14851
14852- (void) drawSubEntityImmediate:(bool)immediate translucent:(bool)translucent
14853{
14854 // Do nothing.
14855}
14856
14857@end
14858
14859
14860@implementation ShipEntity (SubEntityRelationship)
14861
14862- (BOOL) isShipWithSubEntityShip:(Entity *)other
14863{
14864 assert ([self isShip]);
14865
14866 if (![other isShip]) return NO;
14867 if (![other isSubEntity]) return NO;
14868 if ([other owner] != self) return NO;
14869
14870#ifndef NDEBUG
14871 // Sanity check; this should always be true.
14872 if (![self hasSubEntity:(ShipEntity *)other])
14873 {
14874 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.");
14875 [other setOwner:nil];
14876 return NO;
14877 }
14878#endif
14879
14880 return YES;
14881}
14882
14883@end
14884
14885
14886NSDictionary *OODefaultShipShaderMacros(void)
14887{
14888 static NSDictionary *macros = nil;
14889
14890 if (macros == nil)
14891 {
14892 macros = [[[ResourceManager materialDefaults] oo_dictionaryForKey:@"ship-prefix-macros" defaultValue:[NSDictionary dictionary]] retain];
14893 }
14894
14895 return macros;
14896}
14897
14898// is this the right place for this function now? - CIM
14899BOOL OOUniformBindingPermitted(NSString *propertyName, id bindingTarget)
14900{
14901 static NSSet *entityWhitelist = nil;
14902 static NSSet *shipWhitelist = nil;
14903 static NSSet *playerShipWhitelist = nil;
14904 static NSSet *visualEffectWhitelist = nil;
14905
14906 if (entityWhitelist == nil)
14907 {
14908 NSDictionary *wlDict = [ResourceManager whitelistDictionary];
14909 entityWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_entity_binding_methods"]];
14910 shipWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_ship_binding_methods"]];
14911 playerShipWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_player_ship_binding_methods"]];
14912 visualEffectWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_visual_effect_binding_methods"]];
14913 }
14914
14915 if ([bindingTarget isKindOfClass:[Entity class]])
14916 {
14917 if ([entityWhitelist containsObject:propertyName]) return YES;
14918 if ([bindingTarget isShip])
14919 {
14920 if ([shipWhitelist containsObject:propertyName]) return YES;
14921 }
14922 if ([bindingTarget isPlayerLikeShip])
14923 {
14924 if ([playerShipWhitelist containsObject:propertyName]) return YES;
14925 }
14926 if ([bindingTarget isVisualEffect])
14927 {
14928 if ([visualEffectWhitelist containsObject:propertyName]) return YES;
14929 }
14930 }
14931
14932 return NO;
14933}
14934
14935
14937{
14938 return [weapon_type weaponRange];
14939}
14940
14941
14943{
14944 return weapon == nil || [[weapon identifier] isEqualToString:@"EQ_WEAPON_NONE"];
14945}
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
OOScanClass
Definition Entity.h:71
#define SCANNER_MAX_RANGE
Definition Entity.h:51
#define SCANNER_MAX_RANGE2
Definition Entity.h:52
#define ADD_FLAG_IF_SET(x)
#define DESTROY(x)
Definition OOCocoa.h:75
#define foreachkey(VAR, DICT)
Definition OOCocoa.h:353
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:842
#define DESC(key)
Definition Universe.h:848
#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
void drawSubEntityImmediate:translucent:(bool immediate, [translucent] bool translucent)
GLfloat collision_radius
Definition Entity.h:111
OOUniversalID universalID
Definition Entity.h:89
HPVector absolutePositionForSubentity()
Definition Entity.m:670
Entity * z_next
Definition Entity.h:122
NSMutableArray * collisionArray()
Definition Entity.m:924
void setVelocity:(Vector vel)
Definition Entity.m:758
Entity * z_previous
Definition Entity.h:122
Quaternion orientation
Definition Entity.h:114
unsigned isSubEntity
Definition Entity.h:95
BOOL isSun()
Definition Entity.m:168
void setOrientation:(Quaternion quat)
Definition Entity.m:726
GLfloat energy
Definition Entity.h:142
unsigned isShip
Definition Entity.h:91
GLfloat collisionRadius()
Definition Entity.m:906
OOScanClass scanClass
Definition Entity.h:106
void setOwner:(Entity *ent)
Definition Entity.m:577
void setScanClass:(OOScanClass sClass)
Definition Entity.m:800
OOEntityStatus status()
Definition Entity.m:794
void setDistanceTravelled:(GLfloat value)
Definition Entity.m:782
ShipEntity * rootShipEntity()
Definition Entity.m:604
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:812
Quaternion normalOrientation()
Definition Entity.m:739
Vector velocity
Definition Entity.h:140
ShipEntity * parentEntity()
Definition Entity.m:590
void takeEnergyDamage:from:becauseOf:weaponIdentifier:(double amount,[from] Entity *ent,[becauseOf] Entity *other,[weaponIdentifier] NSString *weaponIdentifier)
Definition Entity.m:991
double speed()
Definition Entity.m:770
id owner()
Definition Entity.m:584
unsigned isSunlit
Definition Entity.h:99
GLfloat mass
Definition Entity.h:146
void setPosition:(HPVector posn)
Definition Entity.m:648
OOMatrix drawRotationMatrix()
Definition Entity.m:880
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 * cyanColor()
Definition OOColor.m:286
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
OOColor * greenColor()
Definition OOColor.m:274
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:394
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
void welcomeShuttle:(ShipEntity *shuttle)
instancetype quiriumCascadeFromShip:(ShipEntity *ship)
instancetype ringFromEntity:(Entity *sourceEntity)
instancetype roleSetWithString:(NSString *roleString)
Definition OORoleSet.m:44
instancetype roleSetWithRole:probability:(NSString *role,[probability] float probability)
Definition OORoleSet.m:50
id jsScriptFromFileNamed:properties:(NSString *fileName,[properties] NSDictionary *properties)
Definition OOScript.m:191
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()
void doScriptEvent:withArgument:andReactToAIMessage:(jsid scriptEvent,[withArgument] id argument,[andReactToAIMessage] NSString *aiMessage)
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
void setGroup:(OOShipGroup *group)
void scoopIn:(ShipEntity *other)
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)
GLfloat flightSpeed
Definition ShipEntity.h:368
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()