git-svn-id: http://svn.berlios.de/svnroot/repos/oolite-linux/trunk@2894 127b21dd-08f5-0310-b4b7-95ae10353056
9382 lines
246 KiB
Objective-C
9382 lines
246 KiB
Objective-C
/*
|
|
|
|
ShipEntity.m
|
|
|
|
|
|
Oolite
|
|
Copyright (C) 2004-2009 Giles C Williams and contributors
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License
|
|
as published by the Free Software Foundation; either version 2
|
|
of the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
MA 02110-1301, USA.
|
|
|
|
*/
|
|
|
|
#define BALL_WORLD 0
|
|
|
|
|
|
#import "ShipEntity.h"
|
|
#import "ShipEntityAI.h"
|
|
#import "ShipEntityScriptMethods.h"
|
|
|
|
#import "OOMaths.h"
|
|
#import "Universe.h"
|
|
#import "OOShaderMaterial.h"
|
|
#import "OOOpenGLExtensionManager.h"
|
|
|
|
#import "ResourceManager.h"
|
|
#import "OOStringParsing.h"
|
|
#import "OOCollectionExtractors.h"
|
|
#import "OOConstToString.h"
|
|
#import "NSScannerOOExtensions.h"
|
|
#import "OOFilteringEnumerator.h"
|
|
#import "OORoleSet.h"
|
|
#import "OOShipGroup.h"
|
|
#import "OOExcludeObjectEnumerator.h"
|
|
|
|
#import "OOCharacter.h"
|
|
#import "AI.h"
|
|
#ifdef OO_BRAIN_AI
|
|
#import "OOBrain.h"
|
|
#endif
|
|
|
|
#import "OOMesh.h"
|
|
#if BALL_WORLD
|
|
#import "OOPlanetDrawable.h"
|
|
#endif
|
|
|
|
#import "Geometry.h"
|
|
#import "Octree.h"
|
|
#import "OOColor.h"
|
|
|
|
#import "ParticleEntity.h"
|
|
#import "StationEntity.h"
|
|
#import "OOSunEntity.h"
|
|
#import "OOPlanetEntity.h"
|
|
#import "PlayerEntity.h"
|
|
#import "WormholeEntity.h"
|
|
#import "OOFlasherEntity.h"
|
|
#import "OOExhaustPlumeEntity.h"
|
|
#import "OOSparkEntity.h"
|
|
#import "OOECMBlastEntity.h"
|
|
#import "OOPlasmaShotEntity.h"
|
|
#import "OOFlashEffectEntity.h"
|
|
|
|
#import "PlayerEntityLegacyScriptEngine.h"
|
|
#import "PlayerEntitySound.h"
|
|
#import "GuiDisplayGen.h"
|
|
#import "HeadUpDisplay.h"
|
|
#import "OOEntityFilterPredicate.h"
|
|
#import "OOEquipmentType.h"
|
|
|
|
#import "OODebugGLDrawing.h"
|
|
|
|
#import "OOScript.h"
|
|
|
|
|
|
#define kOOLogUnconvertedNSLog @"unclassified.ShipEntity"
|
|
|
|
#define USEMASC 1
|
|
|
|
|
|
extern NSString * const kOOLogSyntaxAddShips;
|
|
static NSString * const kOOLogEntityBehaviourChanged = @"entity.behaviour.changed";
|
|
|
|
|
|
@interface ShipEntity (Private)
|
|
|
|
- (void) drawSubEntity:(BOOL) immediate :(BOOL) translucent;
|
|
|
|
- (void)subEntityDied:(ShipEntity *)sub;
|
|
- (void)subEntityReallyDied:(ShipEntity *)sub;
|
|
|
|
#ifndef NDEBUG
|
|
- (void) drawDebugStuff;
|
|
#endif
|
|
|
|
- (void) rescaleBy:(GLfloat)factor;
|
|
|
|
- (BOOL) setUpOneSubentity:(NSDictionary *) subentDict;
|
|
- (BOOL) setUpOneFlasher:(NSDictionary *) subentDict;
|
|
- (BOOL) setUpOneStandardSubentity:(NSDictionary *) subentDict asTurret:(BOOL)asTurret;
|
|
|
|
- (Entity<OOStellarBody> *) lastAegisLock;
|
|
- (void) setLastAegisLock:(Entity<OOStellarBody> *)lastAegisLock;
|
|
|
|
- (void) addSubEntity:(Entity *) subent;
|
|
|
|
- (void) addSubentityToCollisionRadius:(Entity*) subent;
|
|
- (ShipEntity *) launchPodWithCrew:(NSArray *)podCrew;
|
|
|
|
// equipment
|
|
- (NSDictionary *) eqDictionaryWithType:(OOEquipmentType *) type isDamaged:(BOOL) isDamaged;
|
|
|
|
@end
|
|
|
|
|
|
@implementation ShipEntity
|
|
|
|
- (id) init
|
|
{
|
|
/* -init used to set up a bunch of defaults that were different from
|
|
those in -reinit and -setUpShipFromDictionary:. However, it seems that
|
|
no ships are ever used which are not -setUpShipFromDictionary: (which
|
|
is as it should be), so these different defaults were meaningless.
|
|
*/
|
|
return [self initWithDictionary:nil];
|
|
}
|
|
|
|
|
|
// Designated initializer
|
|
- (id) initWithDictionary:(NSDictionary *) dict
|
|
{
|
|
if (dict == nil && ![self isKindOfClass:[PlayerEntity class]])
|
|
{
|
|
// Is there any reason we should allow nil dictionary here? I think not. --Ahruman 2008-04-27
|
|
// Yes, the player ship uses -init. Any others? --Ahruman 2008-04-28
|
|
OOLog(@"ship.sanityCheck.nilDict", @"Ship created with nil dictionary!");
|
|
}
|
|
|
|
self = [super init];
|
|
|
|
isShip = YES;
|
|
entity_personality = ranrot_rand() & 0x7FFF;
|
|
[self setStatus:STATUS_IN_FLIGHT];
|
|
|
|
zero_distance = SCANNER_MAX_RANGE2 * 2.0;
|
|
weapon_recharge_rate = 6.0;
|
|
shot_time = 100000.0;
|
|
ship_temperature = 60.0;
|
|
|
|
if (![self setUpShipFromDictionary:dict])
|
|
{
|
|
[self release];
|
|
self = nil;
|
|
}
|
|
|
|
// Problem observed in testing -- Ahruman
|
|
if (self != nil && !isfinite(maxFlightSpeed))
|
|
{
|
|
OOLog(@"ship.sanityCheck.failed", @"Ship %@ %@ infinite top speed, clamped to 300.", self, @"generated with");
|
|
maxFlightSpeed = 300;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
|
|
- (BOOL) setUpFromDictionary:(NSDictionary *) shipDict
|
|
{
|
|
// Settings shared by players & NPCs.
|
|
//
|
|
// In order for default values to work and float values to not be junk,
|
|
// replace nil with empty dictionary. -- Ahruman 2008-04-28
|
|
if (shipDict == nil) shipDict = [NSDictionary dictionary];
|
|
|
|
// All like_ship references should have been resolved in -[Universe getDictionaryForShip:recursionLimit:]
|
|
if ([shipDict objectForKey:@"like_ship"] != nil)
|
|
{
|
|
OOLogERR(@"ship.setUp.like_ship", @"like_ship found inside ship dictionary in -[ShipEntity setUpFromDictionary:], when it should have been resolved already. %@", @"This is an internal error, please report it.");
|
|
return NO;
|
|
}
|
|
|
|
shipinfoDictionary = [shipDict copy];
|
|
shipDict = shipinfoDictionary; // TEMP: ensure no mutation
|
|
|
|
// set these flags explicitly.
|
|
haveExecutedSpawnAction = NO;
|
|
being_fined = NO;
|
|
isNearPlanetSurface = NO;
|
|
suppressAegisMessages = NO;
|
|
isMissile = NO;
|
|
suppressExplosion = NO;
|
|
|
|
// set things from dictionary from here out - default values might require adjustment -- Kaks 20091130
|
|
maxFlightSpeed = [shipDict oo_floatForKey:@"max_flight_speed" defaultValue:160.0f];
|
|
max_flight_roll = [shipDict oo_floatForKey:@"max_flight_roll" defaultValue:2.0f];
|
|
max_flight_pitch = [shipDict oo_floatForKey:@"max_flight_pitch" defaultValue:1.0f];
|
|
max_flight_yaw = [shipDict oo_floatForKey:@"max_flight_yaw" defaultValue:max_flight_pitch]; // Note by default yaw == pitch
|
|
cruiseSpeed = maxFlightSpeed*0.8f;
|
|
|
|
max_thrust = [shipDict oo_floatForKey:@"thrust" defaultValue:15.0f];
|
|
thrust = max_thrust;
|
|
maxEnergy = [shipDict oo_floatForKey:@"max_energy" defaultValue:200.0f];
|
|
energy_recharge_rate = [shipDict oo_floatForKey:@"energy_recharge_rate" defaultValue:1.0f];
|
|
|
|
// Each new ship should start in seemingly good operating condition, unless specifically told not to - this does not affect the ship's energy levels
|
|
[self setThrowSparks:[shipDict oo_boolForKey:@"throw_sparks" defaultValue:NO]];
|
|
|
|
forward_weapon_type = StringToWeaponType([shipDict oo_stringForKey:@"forward_weapon_type" defaultValue:@"WEAPON_NONE"]);
|
|
aft_weapon_type = StringToWeaponType([shipDict oo_stringForKey:@"aft_weapon_type" defaultValue:@"WEAPON_NONE"]);
|
|
[self setWeaponDataFromType:forward_weapon_type];
|
|
|
|
cloaking_device_active = NO;
|
|
military_jammer_active = NO;
|
|
cloakPassive = [shipDict oo_boolForKey:@"cloak_passive" defaultValue:NO];
|
|
|
|
missiles = [shipDict oo_intForKey:@"missiles" defaultValue:0];
|
|
max_missiles = [shipDict oo_intForKey:@"max_missiles" defaultValue:missiles];
|
|
if (max_missiles > SHIPENTITY_MAX_MISSILES) max_missiles = SHIPENTITY_MAX_MISSILES;
|
|
if (missiles > max_missiles) missiles = max_missiles;
|
|
missile_load_time = [shipDict oo_floatForKey:@"missile_load_time" defaultValue:0];
|
|
missile_launch_time = [UNIVERSE getTime] + missile_load_time;
|
|
|
|
// upgrades:
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_ecm"]) [self addEquipmentItem:@"EQ_ECM"];
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_scoop"]) [self addEquipmentItem:@"EQ_FUEL_SCOOPS"];
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_escape_pod"]) [self addEquipmentItem:@"EQ_ESCAPE_POD"];
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_energy_bomb"]) [self addEquipmentItem:@"EQ_ENERGY_BOMB"];
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_cloaking_device"]) [self addEquipmentItem:@"EQ_CLOAKING_DEVICE"];
|
|
if (![UNIVERSE strict])
|
|
{
|
|
// These items are not available in strict mode.
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_fuel_injection"]) [self addEquipmentItem:@"EQ_FUEL_INJECTION"];
|
|
#if USEMASC
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_military_jammer"]) [self addEquipmentItem:@"EQ_MILITARY_JAMMER"];
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_military_scanner_filter"]) [self addEquipmentItem:@"EQ_MILITARY_SCANNER_FILTER"];
|
|
#endif
|
|
}
|
|
|
|
// can it be 'mined' for alloys?
|
|
canFragment = [shipDict oo_fuzzyBooleanForKey:@"fragment_chance" defaultValue:0.9];
|
|
// can subentities be destroyed separately?
|
|
isFrangible = [shipDict oo_boolForKey:@"frangible" defaultValue:YES];
|
|
|
|
max_cargo = [shipDict oo_unsignedIntForKey:@"max_cargo"];
|
|
extra_cargo = [shipDict oo_unsignedIntForKey:@"extra_cargo" defaultValue:15];
|
|
|
|
if (![UNIVERSE strict])
|
|
{
|
|
hyperspaceMotorSpinTime = [shipDict oo_floatForKey:@"hyperspace_motor_spin_time" defaultValue:DEFAULT_HYPERSPACE_SPIN_TIME];
|
|
}
|
|
else
|
|
{
|
|
hyperspaceMotorSpinTime = DEFAULT_HYPERSPACE_SPIN_TIME;
|
|
}
|
|
|
|
if (name !=nil) [name autorelease];
|
|
name = [[shipDict oo_stringForKey:@"name" defaultValue:@"?"] copy];
|
|
|
|
if (displayName !=nil) [displayName autorelease];
|
|
displayName = [[shipDict oo_stringForKey:@"display_name" defaultValue:name] copy];
|
|
|
|
// Load the model (must be before subentities)
|
|
NSString *modelName = [shipDict oo_stringForKey:@"model"];
|
|
if (modelName != nil)
|
|
{
|
|
OOMesh *mesh = [OOMesh meshWithName:modelName
|
|
materialDictionary:[shipDict oo_dictionaryForKey:@"materials"]
|
|
shadersDictionary:[shipDict oo_dictionaryForKey:@"shaders"]
|
|
smooth:[shipDict oo_boolForKey:@"smooth" defaultValue:NO]
|
|
shaderMacros:DefaultShipShaderMacros()
|
|
shaderBindingTarget:self];
|
|
if (mesh == nil) return NO;
|
|
[self setMesh:mesh];
|
|
}
|
|
|
|
float density = [shipDict oo_floatForKey:@"density" defaultValue:1.0f];
|
|
if (octree) mass = (GLfloat)(density * 20.0 * [octree volume]);
|
|
|
|
OOColor *color = [OOColor brightColorWithDescription:[shipDict objectForKey:@"laser_color"]];
|
|
if (color == nil) color = [OOColor redColor];
|
|
[self setLaserColor:color];
|
|
|
|
[self clearSubEntities];
|
|
[self setUpSubEntities: shipDict];
|
|
|
|
// rotating subentities
|
|
subentityRotationalVelocity = kIdentityQuaternion;
|
|
ScanQuaternionFromString([shipDict objectForKey:@"rotational_velocity"], &subentityRotationalVelocity);
|
|
|
|
// set weapon offsets
|
|
[self setDefaultWeaponOffsets];
|
|
|
|
ScanVectorFromString([shipDict objectForKey:@"weapon_position_forward"], &forwardWeaponOffset);
|
|
ScanVectorFromString([shipDict objectForKey:@"weapon_position_aft"], &aftWeaponOffset);
|
|
ScanVectorFromString([shipDict objectForKey:@"weapon_position_port"], &portWeaponOffset);
|
|
ScanVectorFromString([shipDict objectForKey:@"weapon_position_starboard"], &starboardWeaponOffset);
|
|
|
|
// fuel scoop destination position (where cargo gets sucked into)
|
|
tractor_position = kZeroVector;
|
|
ScanVectorFromString([shipDict objectForKey:@"scoop_position"], &tractor_position);
|
|
|
|
// Get scriptInfo dictionary, containing arbitrary stuff scripts might be interested in.
|
|
scriptInfo = [[shipDict oo_dictionaryForKey:@"script_info" defaultValue:nil] retain];
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) setUpShipFromDictionary:(NSDictionary *) shipDict
|
|
{
|
|
if (![self setUpFromDictionary:shipDict]) return NO;
|
|
|
|
// NPC-only settings.
|
|
//
|
|
orientation = kIdentityQuaternion;
|
|
rotMatrix = kIdentityMatrix;
|
|
v_forward = kBasisZVector;
|
|
v_up = kBasisYVector;
|
|
v_right = kBasisXVector;
|
|
reference = v_forward; // reference vector for (* turrets *)
|
|
|
|
isShip = YES;
|
|
|
|
// FIXME: give NPCs shields instead.
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_shield_booster"])
|
|
{
|
|
maxEnergy += 256.0f;
|
|
}
|
|
if ([shipDict oo_fuzzyBooleanForKey:@"has_shield_enhancer"])
|
|
{
|
|
maxEnergy += 256.0f;
|
|
energy_recharge_rate *= 1.5;
|
|
}
|
|
|
|
// Start with full energy banks.
|
|
energy = maxEnergy;
|
|
|
|
// setWeaponDataFromType inside setUpFromDictionary should set weapon_energy from the front laser.
|
|
// no weapon_energy? It's a missile: set weapon_energy from shipdata!
|
|
if (weapon_energy == 0.0) weapon_energy = [shipDict oo_floatForKey:@"weapon_energy"];
|
|
|
|
scannerRange = [shipDict oo_floatForKey:@"scanner_range" defaultValue:(float)SCANNER_MAX_RANGE];
|
|
|
|
fuel = [shipDict oo_unsignedShortForKey:@"fuel"]; // Does it make sense that this defaults to 0? Should it not be 70? -- Ahruman
|
|
fuel_accumulator = 1.0;
|
|
|
|
bounty = [shipDict oo_unsignedIntForKey:@"bounty"];
|
|
|
|
[shipAI autorelease];
|
|
shipAI = [[AI alloc] init];
|
|
[shipAI setOwner:self];
|
|
[shipAI setStateMachine:[shipDict oo_stringForKey:@"ai_type" defaultValue:@"nullAI.plist"]];
|
|
|
|
likely_cargo = [shipDict oo_unsignedIntForKey:@"likely_cargo"];
|
|
noRocks = [shipDict oo_fuzzyBooleanForKey:@"no_boulders"];
|
|
|
|
NSString *cargoString = [shipDict oo_stringForKey:@"cargo_carried"];
|
|
if (cargoString != nil)
|
|
{
|
|
cargo_flag = CARGO_FLAG_FULL_UNIFORM;
|
|
|
|
OOCargoType c_commodity = CARGO_UNDEFINED;
|
|
int c_amount = 1;
|
|
NSScanner *scanner = [NSScanner scannerWithString:cargoString];
|
|
if ([scanner scanInt:&c_amount])
|
|
{
|
|
[scanner ooliteScanCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:NULL]; // skip whitespace
|
|
c_commodity = [UNIVERSE commodityForName: [[scanner string] substringFromIndex:[scanner scanLocation]]];
|
|
}
|
|
else
|
|
{
|
|
c_amount = 1;
|
|
c_commodity = [UNIVERSE commodityForName: [shipDict oo_stringForKey:@"cargo_carried"]];
|
|
}
|
|
|
|
if (c_commodity != CARGO_UNDEFINED) [self setCommodity:c_commodity andAmount:c_amount];
|
|
}
|
|
|
|
cargoString = [shipDict oo_stringForKey:@"cargo_type"];
|
|
if (cargoString)
|
|
{
|
|
cargo_type = StringToCargoType(cargoString);
|
|
|
|
if (cargo != nil) [cargo autorelease];
|
|
cargo = [[NSMutableArray alloc] initWithCapacity:max_cargo]; // alloc retains;
|
|
}
|
|
|
|
[roleSet release];
|
|
roleSet = [[[OORoleSet roleSetWithString:[shipDict oo_stringForKey:@"roles"]] roleSetWithRemovedRole:@"player"] retain];
|
|
[primaryRole release];
|
|
primaryRole = nil;
|
|
|
|
[self setOwner:self];
|
|
[self setHulk:[shipDict oo_boolForKey:@"is_hulk"]];
|
|
|
|
// these are the colors used for the "lollipop" of the ship. Any of the two (or both, for flash effect) can be defined
|
|
scanner_display_color1 = [[OOColor colorWithDescription:[shipDict objectForKey:@"scanner_display_color1"]] retain];
|
|
scanner_display_color2 = [[OOColor colorWithDescription:[shipDict objectForKey:@"scanner_display_color2"]] retain];
|
|
|
|
// scan class settings. 'scanClass' is in common usage, but we could also have a more standard 'scan_class' key with higher precedence. Kaks 20090810
|
|
// let's see if scan_class is set...
|
|
scanClass = StringToScanClass([shipDict oo_stringForKey:@"scan_class" defaultValue:@"CLASS_NOT_SET"]);
|
|
|
|
// if not, try 'scanClass'. NOTE: non-standard capitalization is documented and entrenched.
|
|
if (scanClass == CLASS_NOT_SET)
|
|
{
|
|
scanClass = StringToScanClass([shipDict oo_stringForKey:@"scanClass" defaultValue:@"CLASS_NOT_SET"]);
|
|
}
|
|
|
|
// Populate the missiles here. Must come after scanClass.
|
|
missileRole = [shipDict oo_stringForKey:@"missile_role"];
|
|
unsigned i;
|
|
for (i = 0; i < missiles; i++)
|
|
{
|
|
missile_list[i] = [self newMissile];
|
|
if (missile_list[i] == nil) i--;
|
|
}
|
|
|
|
// accuracy. Must come after scanClass, because we are using scanClass to determine if this is a missile.
|
|
accuracy = [shipDict oo_floatForKey:@"accuracy" defaultValue:-100.0f]; // Out-of-range default
|
|
if (accuracy >= -5.0f && accuracy <= 10.0f)
|
|
{
|
|
pitch_tolerance = 0.01 * (85.0f + accuracy);
|
|
}
|
|
else
|
|
{
|
|
pitch_tolerance = 0.01 * (80 + (randf() * 15.0f));
|
|
}
|
|
|
|
// If this entity is a missile, clamp its accuracy within range from 0.0 to 10.0.
|
|
// Otherwise, just make sure that the accuracy value does not fall below 1.0.
|
|
// Using a switch statement, in case accuracy for other scan classes need be considered in the future.
|
|
switch (scanClass)
|
|
{
|
|
case CLASS_MISSILE :
|
|
accuracy = OOClamp_0_max_f(accuracy, 10.0f);
|
|
break;
|
|
default :
|
|
if (accuracy < 1.0f) accuracy = 1.0f;
|
|
break;
|
|
}
|
|
|
|
// escorts
|
|
_pendingEscortCount = _maxEscortCount = [shipDict oo_unsignedIntForKey:@"escorts"];
|
|
|
|
// beacons
|
|
[self setBeaconCode:[shipDict oo_stringForKey:@"beacon"]];
|
|
|
|
// contact tracking entities
|
|
[self setTrackCloseContacts:[shipDict oo_boolForKey:@"track_contacts" defaultValue:NO]];
|
|
|
|
// ship skin insulation factor (1.0 is normal)
|
|
[self setHeatInsulation:[shipDict oo_floatForKey:@"heat_insulation" defaultValue:[self hasHeatShield] ? 2.0 : 1.0]];
|
|
|
|
// crew and passengers
|
|
NSDictionary* cdict = [[UNIVERSE characters] objectForKey:[shipDict oo_stringForKey:@"pilot"]];
|
|
if (cdict != nil)
|
|
{
|
|
OOCharacter *pilot = [OOCharacter characterWithDictionary:cdict];
|
|
[self setCrew:[NSArray arrayWithObject:pilot]];
|
|
}
|
|
|
|
// unpiloted (like missiles asteroids etc.)
|
|
if ((isUnpiloted = [shipDict oo_fuzzyBooleanForKey:@"unpiloted"])) [self setCrew:nil];
|
|
|
|
[self setShipScript:[shipDict oo_stringForKey:@"script"]];
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) setUpSubEntities: (NSDictionary *) shipDict
|
|
{
|
|
unsigned int i;
|
|
NSArray *plumes = [shipDict oo_arrayForKey:@"exhaust"];
|
|
|
|
_profileRadius = collision_radius;
|
|
|
|
for (i = 0; i < [plumes count]; i++)
|
|
{
|
|
NSArray *definition = ScanTokensFromString([plumes oo_stringAtIndex:i]);
|
|
OOExhaustPlumeEntity *exhaust = [OOExhaustPlumeEntity exhaustForShip:self withDefinition:definition];
|
|
[self addSubEntity:exhaust];
|
|
}
|
|
|
|
NSArray *subs = [shipDict oo_arrayForKey:@"subentities"];
|
|
|
|
for (i = 0; i < [subs count]; i++)
|
|
{
|
|
[self setUpOneSubentity:[subs oo_dictionaryAtIndex:i]];
|
|
}
|
|
|
|
no_draw_distance = _profileRadius * _profileRadius * NO_DRAW_DISTANCE_FACTOR * NO_DRAW_DISTANCE_FACTOR * 2.0;
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) setUpOneSubentity:(NSDictionary *) subentDict
|
|
{
|
|
NSString *type = nil;
|
|
|
|
type = [subentDict oo_stringForKey:@"type"];
|
|
if ([type isEqualToString:@"flasher"])
|
|
{
|
|
return [self setUpOneFlasher:subentDict];
|
|
}
|
|
else
|
|
{
|
|
return [self setUpOneStandardSubentity:subentDict asTurret:[type isEqualToString:@"ball_turret"]];
|
|
}
|
|
}
|
|
|
|
|
|
- (BOOL) setUpOneFlasher:(NSDictionary *) subentDict
|
|
{
|
|
OOFlasherEntity *flasher = [OOFlasherEntity flasherWithDictionary:subentDict];
|
|
[flasher setPosition:[subentDict oo_vectorForKey:@"position"]];
|
|
[self addSubEntity:flasher];
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) setUpOneStandardSubentity:(NSDictionary *) subentDict asTurret:(BOOL)asTurret
|
|
{
|
|
ShipEntity *subentity = nil;
|
|
NSString *subentKey = nil;
|
|
Vector subPosition;
|
|
Quaternion subOrientation;
|
|
|
|
subentKey = [subentDict oo_stringForKey:@"subentity_key"];
|
|
if (subentKey == nil) return NO;
|
|
|
|
subentity = [UNIVERSE newShipWithName:subentKey];
|
|
if (subentity == nil) return NO;
|
|
|
|
subPosition = [subentDict oo_vectorForKey:@"position"];
|
|
subOrientation = [subentDict oo_quaternionForKey:@"orientation"];
|
|
|
|
if (!asTurret && [self isStation] && [subentDict oo_boolForKey:@"is_dock"])
|
|
{
|
|
[(StationEntity *)self setDockingPortModel:subentity :subPosition :subOrientation];
|
|
}
|
|
|
|
[subentity setPosition:subPosition];
|
|
[subentity setOrientation:subOrientation];
|
|
[subentity setReference:vector_forward_from_quaternion(subOrientation)];
|
|
|
|
if (asTurret)
|
|
{
|
|
[subentity setBehaviour:BEHAVIOUR_TRACK_AS_TURRET];
|
|
[subentity setWeaponRechargeRate:[subentDict oo_floatForKey:@"fire_rate" defaultValue:TURRET_SHOT_FREQUENCY]];
|
|
[subentity setStatus: STATUS_ACTIVE];
|
|
}
|
|
else
|
|
{
|
|
[subentity setStatus:STATUS_INACTIVE];
|
|
}
|
|
|
|
[self addSubEntity:subentity];
|
|
[subentity release];
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (void) dealloc
|
|
{
|
|
[self setTrackCloseContacts:NO]; // deallocs tracking dictionary
|
|
[[self parentEntity] subEntityReallyDied:self]; // Will do nothing if we're not really a subentity
|
|
[self clearSubEntities];
|
|
|
|
[shipinfoDictionary release];
|
|
[shipAI release];
|
|
[cargo release];
|
|
[name release];
|
|
[displayName release];
|
|
[roleSet release];
|
|
[primaryRole release];
|
|
[laser_color release];
|
|
[scanner_display_color1 release];
|
|
scanner_display_color1 = nil;
|
|
[scanner_display_color2 release];
|
|
scanner_display_color2 = nil;
|
|
if (script != nil)
|
|
{
|
|
[script release];
|
|
script = nil;
|
|
}
|
|
|
|
[previousCondition release];
|
|
|
|
[dockingInstructions release];
|
|
|
|
[crew release];
|
|
|
|
[lastRadioMessage autorelease];
|
|
|
|
[octree autorelease];
|
|
|
|
[self setSubEntityTakingDamage:nil];
|
|
[self removeAllEquipment];
|
|
|
|
[_group removeShip:self];
|
|
[_group release];
|
|
[_escortGroup removeShip:self];
|
|
[_escortGroup release];
|
|
|
|
[_lastAegisLock release];
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
|
|
- (void) removeScript
|
|
{
|
|
[script autorelease];
|
|
script = nil;
|
|
}
|
|
|
|
|
|
- (void) clearSubEntities
|
|
{
|
|
[subEntities makeObjectsPerformSelector:@selector(setOwner:) withObject:nil]; // Ensure backlinks are broken
|
|
[subEntities release];
|
|
subEntities = nil;
|
|
}
|
|
|
|
|
|
- (NSString *)descriptionComponents
|
|
{
|
|
if (![self isSubEntity])
|
|
{
|
|
return [NSString stringWithFormat:@"\"%@\" %@", [self name], [super descriptionComponents]];
|
|
}
|
|
else
|
|
{
|
|
// ID, scanClass and status are of no interest for subentities.
|
|
NSString *subtype = nil;
|
|
if ([self behaviour] == BEHAVIOUR_TRACK_AS_TURRET) subtype = @"(turret)";
|
|
else subtype = @"(subentity)";
|
|
|
|
return [NSString stringWithFormat:@"\"%@\" position: %@ %@", [self name], VectorDescription([self position]), subtype];
|
|
}
|
|
}
|
|
|
|
|
|
- (NSString *) shortDescriptionComponents
|
|
{
|
|
return [NSString stringWithFormat:@"\"%@\"", [self name]];
|
|
}
|
|
|
|
|
|
- (OOMesh *)mesh
|
|
{
|
|
return (OOMesh *)[self drawable];
|
|
}
|
|
|
|
|
|
- (void)setMesh:(OOMesh *)mesh
|
|
{
|
|
if (mesh != [self mesh])
|
|
{
|
|
[self setDrawable:mesh];
|
|
[octree autorelease];
|
|
octree = [[mesh octree] retain];
|
|
}
|
|
}
|
|
|
|
|
|
- (Vector) forwardVector
|
|
{
|
|
return v_forward;
|
|
}
|
|
|
|
|
|
- (Vector) upVector
|
|
{
|
|
return v_up;
|
|
}
|
|
|
|
|
|
- (Vector) rightVector
|
|
{
|
|
return v_right;
|
|
}
|
|
|
|
|
|
- (NSArray *)subEntities
|
|
{
|
|
return [[subEntities copy] autorelease];
|
|
}
|
|
|
|
|
|
- (unsigned) subEntityCount
|
|
{
|
|
return [subEntities count];
|
|
}
|
|
|
|
|
|
- (BOOL) hasSubEntity:(ShipEntity *)sub
|
|
{
|
|
return [subEntities containsObject:sub];
|
|
}
|
|
|
|
- (NSEnumerator *)subEntityEnumerator
|
|
{
|
|
return [[self subEntities] objectEnumerator];
|
|
}
|
|
|
|
|
|
- (NSEnumerator *)shipSubEntityEnumerator
|
|
{
|
|
return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isShip)];
|
|
}
|
|
|
|
|
|
- (NSEnumerator *)flasherEnumerator
|
|
{
|
|
return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isFlasher)];
|
|
}
|
|
|
|
|
|
- (NSEnumerator *)exhaustEnumerator
|
|
{
|
|
return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isExhaust)];
|
|
}
|
|
|
|
|
|
- (ShipEntity *) subEntityTakingDamage
|
|
{
|
|
ShipEntity *result = [_subEntityTakingDamage weakRefUnderlyingObject];
|
|
|
|
#ifndef NDEBUG
|
|
// Sanity check - there have been problems here, see fireLaserShotInDirection:
|
|
// -parentEntity will take care of reporting insanity.
|
|
if ([result parentEntity] != self) result = nil;
|
|
#endif
|
|
|
|
// Clear the weakref if the subentity is dead.
|
|
if (result == nil) [self setSubEntityTakingDamage:nil];
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
- (void) setSubEntityTakingDamage:(ShipEntity *)sub
|
|
{
|
|
#ifndef NDEBUG
|
|
// Sanity checks: sub must be a ship subentity of self, or nil.
|
|
if (sub != nil)
|
|
{
|
|
if (![self hasSubEntity:sub])
|
|
{
|
|
OOLog(@"ship.subentity.sanityCheck.failed.details", @"Attempt to set subentity taking damage of %@ to %@, which is not a subentity.", [self shortDescription], sub);
|
|
sub = nil;
|
|
}
|
|
else if (![sub isShip])
|
|
{
|
|
OOLog(@"ship.subentity.sanityCheck.failed", @"Attempt to set subentity taking damage of %@ to %@, which is not a ship.", [self shortDescription], sub);
|
|
sub = nil;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
[_subEntityTakingDamage release];
|
|
_subEntityTakingDamage = [sub weakRetain];
|
|
}
|
|
|
|
|
|
- (OOScript *)shipScript
|
|
{
|
|
return script;
|
|
}
|
|
|
|
|
|
- (BoundingBox)findBoundingBoxRelativeToPosition:(Vector)opv InVectors:(Vector) _i :(Vector) _j :(Vector) _k
|
|
{
|
|
return [[self mesh] findBoundingBoxRelativeToPosition:opv
|
|
basis:_i :_j :_k
|
|
selfPosition:position
|
|
selfBasis:v_right :v_up :v_forward];
|
|
}
|
|
|
|
|
|
#ifdef OO_BRAIN_AI
|
|
// ship's brains!
|
|
- (OOBrain *)brain
|
|
{
|
|
return brain;
|
|
}
|
|
|
|
|
|
- (void)setBrain:(OOBrain *)aBrain
|
|
{
|
|
brain = aBrain;
|
|
}
|
|
#endif
|
|
|
|
|
|
- (Octree *) octree
|
|
{
|
|
return octree;
|
|
}
|
|
|
|
|
|
- (float) volume
|
|
{
|
|
return [octree volume];
|
|
}
|
|
|
|
|
|
- (GLfloat)doesHitLine:(Vector)v0: (Vector)v1;
|
|
{
|
|
Vector u0 = vector_between(position, v0); // relative to origin of model / octree
|
|
Vector u1 = vector_between(position, v1);
|
|
Vector w0 = make_vector(dot_product(u0, v_right), dot_product(u0, v_up), dot_product(u0, v_forward)); // in ijk vectors
|
|
Vector w1 = make_vector(dot_product(u1, v_right), dot_product(u1, v_up), dot_product(u1, v_forward));
|
|
return [octree isHitByLine:w0 :w1];
|
|
}
|
|
|
|
|
|
- (GLfloat) doesHitLine:(Vector)v0: (Vector)v1 :(ShipEntity **)hitEntity;
|
|
{
|
|
if (hitEntity)
|
|
hitEntity[0] = (ShipEntity*)nil;
|
|
Vector u0 = vector_between(position, v0); // relative to origin of model / octree
|
|
Vector u1 = vector_between(position, v1);
|
|
Vector w0 = make_vector(dot_product(u0, v_right), dot_product(u0, v_up), dot_product(u0, v_forward)); // in ijk vectors
|
|
Vector w1 = make_vector(dot_product(u1, v_right), dot_product(u1, v_up), dot_product(u1, v_forward));
|
|
GLfloat hit_distance = [octree isHitByLine:w0 :w1];
|
|
if (hit_distance)
|
|
{
|
|
if (hitEntity)
|
|
hitEntity[0] = self;
|
|
}
|
|
|
|
NSEnumerator *subEnum = nil;
|
|
ShipEntity *se = nil;
|
|
for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
Vector p0 = [se absolutePositionForSubentity];
|
|
Triangle ijk = [se absoluteIJKForSubentity];
|
|
u0 = vector_between(p0, v0);
|
|
u1 = vector_between(p0, v1);
|
|
w0 = resolveVectorInIJK(u0, ijk);
|
|
w1 = resolveVectorInIJK(u1, ijk);
|
|
|
|
GLfloat hitSub = [se->octree isHitByLine:w0 :w1];
|
|
if (hitSub && (hit_distance == 0 || hit_distance > hitSub))
|
|
{
|
|
hit_distance = hitSub;
|
|
if (hitEntity)
|
|
{
|
|
*hitEntity = se;
|
|
}
|
|
}
|
|
}
|
|
|
|
return hit_distance;
|
|
}
|
|
|
|
|
|
- (GLfloat)doesHitLine:(Vector)v0: (Vector)v1 withPosition:(Vector)o andIJK:(Vector)i :(Vector)j :(Vector)k;
|
|
{
|
|
Vector u0 = vector_between(o, v0); // relative to origin of model / octree
|
|
Vector u1 = vector_between(o, v1);
|
|
Vector w0 = make_vector(dot_product(u0, i), dot_product(u0, j), dot_product(u0, k)); // in ijk vectors
|
|
Vector w1 = make_vector(dot_product(u1, j), dot_product(u1, j), dot_product(u1, k));
|
|
return [octree isHitByLine:w0 :w1];
|
|
}
|
|
|
|
|
|
- (void) wasAddedToUniverse
|
|
{
|
|
[super wasAddedToUniverse];
|
|
|
|
// if we have a universal id then we can proceed to set up any
|
|
// stuff that happens when we get added to the UNIVERSE
|
|
if (universalID != NO_TARGET)
|
|
{
|
|
// set up escorts
|
|
if (([self status] == STATUS_IN_FLIGHT || [self status] == STATUS_LAUNCHING) && _pendingEscortCount != 0) // just popped into existence
|
|
{
|
|
[self setUpEscorts];
|
|
}
|
|
else
|
|
{
|
|
/* Earlier there was a silly log message here because I thought
|
|
this would never happen, but wasn't entirely sure. Turns out
|
|
it did!
|
|
-- Ahruman 2009-09-13
|
|
*/
|
|
_pendingEscortCount = 0;
|
|
}
|
|
}
|
|
|
|
// Tell subentities, too
|
|
[subEntities makeObjectsPerformSelector:@selector(wasAddedToUniverse)];
|
|
|
|
[self resetExhaustPlumes];
|
|
}
|
|
|
|
|
|
- (void)wasRemovedFromUniverse
|
|
{
|
|
[subEntities makeObjectsPerformSelector:@selector(wasRemovedFromUniverse)];
|
|
}
|
|
|
|
|
|
- (Vector)absoluteTractorPosition
|
|
{
|
|
Vector result = position;
|
|
result.x += v_right.x * tractor_position.x + v_up.x * tractor_position.y + v_forward.x * tractor_position.z;
|
|
result.y += v_right.y * tractor_position.x + v_up.y * tractor_position.y + v_forward.y * tractor_position.z;
|
|
result.z += v_right.z * tractor_position.x + v_up.z * tractor_position.y + v_forward.z * tractor_position.z;
|
|
return result;
|
|
}
|
|
|
|
|
|
- (NSString *)beaconCode
|
|
{
|
|
return beaconCode;
|
|
}
|
|
|
|
|
|
- (void)setBeaconCode:(NSString *)bcode
|
|
{
|
|
if ([beaconCode length] == 0) beaconCode = nil;
|
|
|
|
if (beaconCode != bcode)
|
|
{
|
|
[beaconCode release];
|
|
beaconCode = [bcode copy];
|
|
if (beaconCode != nil)
|
|
{
|
|
beaconChar = [bcode cStringUsingOoliteEncodingAndRemapping][0];
|
|
}
|
|
else
|
|
{
|
|
beaconChar = '\0';
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (BOOL)isBeacon
|
|
{
|
|
return (beaconChar != 0);
|
|
}
|
|
|
|
|
|
- (char)beaconChar
|
|
{
|
|
return beaconChar;
|
|
}
|
|
|
|
|
|
- (int)nextBeaconID
|
|
{
|
|
return nextBeaconID;
|
|
}
|
|
|
|
|
|
- (void)setNextBeacon:(ShipEntity *)beaconShip
|
|
{
|
|
if (beaconShip == nil) nextBeaconID = NO_TARGET;
|
|
else nextBeaconID = [beaconShip universalID];
|
|
}
|
|
|
|
|
|
- (void) setUpEscorts
|
|
{
|
|
NSString *defaultRole = @"escort";
|
|
NSString *escortRole = nil;
|
|
NSString *escortShipKey = nil;
|
|
NSString *autoAI = nil;
|
|
NSDictionary *autoAIMap = nil;
|
|
NSDictionary *escortShipDict = nil;
|
|
AI *escortAI = nil;
|
|
BOOL isSelfGroupmember = NO;
|
|
|
|
// Ensure that we do not try to create escorts if we are an escort ship ourselves.
|
|
// This could lead to circular reference memory overflows (e.g. "boa-mk2" trying to create 4 "boa-mk2"
|
|
// escorts or the case of two ships specifying eachother as escorts) - Nikos 20090510
|
|
if ([self isEscort])
|
|
{
|
|
OOLogWARN(@"shipEntity.setupEscorts.escortShipCircularReference",
|
|
@"Ship %@ requested escorts, when it is an escort ship itself. Avoiding possible circular reference overflow by ignoring escort setup.", self);
|
|
return;
|
|
}
|
|
|
|
if (_pendingEscortCount == 0) return;
|
|
|
|
if ([self isPolice]) defaultRole = @"wingman";
|
|
|
|
escortRole = [shipinfoDictionary oo_stringForKey:@"escort_role" defaultValue:nil];
|
|
if (escortRole == nil)
|
|
escortRole = [shipinfoDictionary oo_stringForKey:@"escort-role" defaultValue:defaultRole];
|
|
if (![escortRole isEqualToString: defaultRole])
|
|
{
|
|
if (![[UNIVERSE newShipWithRole:escortRole] autorelease])
|
|
{
|
|
escortRole = defaultRole;
|
|
}
|
|
}
|
|
|
|
escortShipKey = [shipinfoDictionary oo_stringForKey:@"escort_ship" defaultValue:nil];
|
|
if (escortShipKey == nil)
|
|
escortShipKey = [shipinfoDictionary oo_stringForKey:@"escort-ship"];
|
|
|
|
if (escortShipKey != nil)
|
|
{
|
|
if (![[UNIVERSE newShipWithName:escortShipKey] autorelease])
|
|
{
|
|
escortShipKey = nil;
|
|
}
|
|
}
|
|
|
|
OOShipGroup *escortGroup = [self escortGroup];
|
|
if ([self group] == nil)
|
|
{
|
|
[self setGroup:escortGroup];
|
|
}
|
|
else
|
|
{
|
|
isSelfGroupmember = YES;
|
|
}
|
|
[escortGroup setLeader:self];
|
|
|
|
while (_pendingEscortCount > 0)
|
|
{
|
|
Vector ex_pos = [self coordinatesForEscortPosition:_pendingEscortCount - 1];
|
|
|
|
ShipEntity *escorter = nil;
|
|
|
|
if (escortShipKey)
|
|
{
|
|
escorter = [UNIVERSE newShipWithName:escortShipKey]; // retained
|
|
}
|
|
else
|
|
{
|
|
escorter = [UNIVERSE newShipWithRole:escortRole]; // retained
|
|
}
|
|
|
|
if (escorter == nil) break;
|
|
|
|
if ([escorter crew] == nil)
|
|
{
|
|
[escorter setCrew:[NSArray arrayWithObject:
|
|
[OOCharacter randomCharacterWithRole: @"hunter"
|
|
andOriginalSystem: [UNIVERSE systemSeed]]]];
|
|
}
|
|
|
|
// spread them around a little randomly
|
|
double dd = escorter->collision_radius;
|
|
ex_pos.x += dd * 6.0 * (randf() - 0.5);
|
|
ex_pos.y += dd * 6.0 * (randf() - 0.5);
|
|
ex_pos.z += dd * 6.0 * (randf() - 0.5);
|
|
|
|
[escorter setPosition:ex_pos];
|
|
//[escorter setStatus:STATUS_IN_FLIGHT];
|
|
[escorter setPrimaryRole:defaultRole]; //for mothership
|
|
[escorter setScanClass:scanClass]; // you are the same as I
|
|
if ([self bounty] == 0) [escorter setBounty:0]; // Avoid dirty escorts for clean mothers
|
|
|
|
// find the right outoAI.
|
|
escortShipDict = [escorter shipInfoDictionary];
|
|
autoAIMap = [ResourceManager dictionaryFromFilesNamed:@"autoAImap.plist" inFolder:@"Config" andMerge:YES];
|
|
autoAI = [autoAIMap oo_stringForKey:defaultRole];
|
|
if (autoAI==nil) // no 'wingman' defined in autoAImap?
|
|
{
|
|
autoAI = [autoAIMap oo_stringForKey:@"escort" defaultValue:@"nullAI.plist"];
|
|
}
|
|
|
|
escortAI = [escorter getAI];
|
|
|
|
// Let the populator decide which AI to use, unless we have a working alternative AI & we specify auto_ai = NO !
|
|
if ( ((escortShipKey || escortRole) && [escortShipDict oo_fuzzyBooleanForKey:@"auto_ai" defaultValue:YES])
|
|
|| ([[escortAI name] isEqualToString: @"nullAI.plist"] && ![autoAI isEqualToString:@"nullAI.plist"]) )
|
|
{
|
|
[escorter switchAITo:autoAI];
|
|
}
|
|
|
|
[UNIVERSE addEntity:escorter]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
|
|
[escorter setGroup:escortGroup];
|
|
[escorter setOwner:self]; // make self group leader
|
|
if(isSelfGroupmember) [[self group] addShip:escorter]; // put escorts also in leaders group when present.
|
|
|
|
if([escorter heatInsulation] < [self heatInsulation]) [escorter setHeatInsulation:[self heatInsulation]]; // give escorts same protection as mother.
|
|
if(([escorter maxFlightSpeed] < cruiseSpeed) && ([escorter maxFlightSpeed] > cruiseSpeed * 0.3))
|
|
cruiseSpeed = [escorter maxFlightSpeed] * 0.99; // adapt patrolSpeed to the slowest escort but ignore the very slow ones.
|
|
|
|
[escortAI setState:@"FLYING_ESCORT"]; // Begin escort flight. (If the AI doesn't define FLYING_ESCORT, this has no effect.)
|
|
[escorter doScriptEvent:@"spawnedAsEscort" withArgument:self];
|
|
|
|
if (bounty)
|
|
{
|
|
int extra = 1 | (ranrot_rand() & 15);
|
|
bounty += extra; // obviously we're dodgier than we thought!
|
|
[escorter setBounty: extra];
|
|
}
|
|
else
|
|
{
|
|
[escorter setBounty:0];
|
|
}
|
|
[escorter release];
|
|
_pendingEscortCount--;
|
|
}
|
|
}
|
|
|
|
|
|
- (NSDictionary *)shipInfoDictionary
|
|
{
|
|
return shipinfoDictionary;
|
|
}
|
|
|
|
|
|
- (void) setDefaultWeaponOffsets
|
|
{
|
|
forwardWeaponOffset = kZeroVector;
|
|
aftWeaponOffset = kZeroVector;
|
|
portWeaponOffset = kZeroVector;
|
|
starboardWeaponOffset = kZeroVector;
|
|
}
|
|
|
|
|
|
- (BOOL)isFrangible
|
|
{
|
|
return isFrangible;
|
|
}
|
|
|
|
|
|
- (OOScanClass) scanClass
|
|
{
|
|
if (cloaking_device_active)
|
|
return CLASS_NO_DRAW;
|
|
else
|
|
return scanClass;
|
|
}
|
|
|
|
//////////////////////////////////////////////
|
|
|
|
BOOL ship_canCollide (ShipEntity* ship)
|
|
{
|
|
int s_status = [ship status];
|
|
int s_scan_class = ship->scanClass;
|
|
if ((s_status == STATUS_COCKPIT_DISPLAY)||(s_status == STATUS_DEAD)||(s_status == STATUS_BEING_SCOOPED))
|
|
return NO;
|
|
if ((s_scan_class == CLASS_MISSILE) && ([ship shotTime] < 0.25)) // not yet fused
|
|
return NO;
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) canCollide
|
|
{
|
|
return ship_canCollide(self);
|
|
}
|
|
|
|
ShipEntity* doOctreesCollide(ShipEntity* prime, ShipEntity* other)
|
|
{
|
|
// octree check
|
|
Octree *prime_octree = prime->octree;
|
|
Octree *other_octree = other->octree;
|
|
|
|
Vector prime_position = [prime absolutePositionForSubentity];
|
|
Triangle prime_ijk = [prime absoluteIJKForSubentity];
|
|
Vector other_position = [other absolutePositionForSubentity];
|
|
Triangle other_ijk = [other absoluteIJKForSubentity];
|
|
|
|
Vector relative_position_of_other = resolveVectorInIJK(vector_between(prime_position, other_position), prime_ijk);
|
|
Triangle relative_ijk_of_other;
|
|
relative_ijk_of_other.v[0] = resolveVectorInIJK(other_ijk.v[0], prime_ijk);
|
|
relative_ijk_of_other.v[1] = resolveVectorInIJK(other_ijk.v[1], prime_ijk);
|
|
relative_ijk_of_other.v[2] = resolveVectorInIJK(other_ijk.v[2], prime_ijk);
|
|
|
|
// check hull octree against other hull octree
|
|
if ([prime_octree isHitByOctree:other_octree
|
|
withOrigin:relative_position_of_other
|
|
andIJK:relative_ijk_of_other])
|
|
{
|
|
return other;
|
|
}
|
|
|
|
// check prime subentities against the other's hull
|
|
NSArray* prime_subs = prime->subEntities;
|
|
if (prime_subs)
|
|
{
|
|
int i;
|
|
int n_subs = [prime_subs count];
|
|
for (i = 0; i < n_subs; i++)
|
|
{
|
|
Entity* se = [prime_subs objectAtIndex:i];
|
|
if ([se isShip] && [se canCollide] && doOctreesCollide((ShipEntity*)se, other))
|
|
return other;
|
|
}
|
|
}
|
|
|
|
// check prime hull against the other's subentities
|
|
NSArray* other_subs = other->subEntities;
|
|
if (other_subs)
|
|
{
|
|
int i;
|
|
int n_subs = [other_subs count];
|
|
for (i = 0; i < n_subs; i++)
|
|
{
|
|
Entity* se = [other_subs objectAtIndex:i];
|
|
if ([se isShip] && [se canCollide] && doOctreesCollide(prime, (ShipEntity*)se))
|
|
return (ShipEntity*)se;
|
|
}
|
|
}
|
|
|
|
// check prime subenties against the other's subentities
|
|
if ((prime_subs)&&(other_subs))
|
|
{
|
|
int i;
|
|
int n_osubs = [other_subs count];
|
|
for (i = 0; i < n_osubs; i++)
|
|
{
|
|
Entity* oe = [other_subs objectAtIndex:i];
|
|
if ([oe isShip] && [oe canCollide])
|
|
{
|
|
int j;
|
|
int n_psubs = [prime_subs count];
|
|
for (j = 0; j < n_psubs; j++)
|
|
{
|
|
Entity* pe = [prime_subs objectAtIndex:j];
|
|
if ([pe isShip] && [pe canCollide] && doOctreesCollide((ShipEntity*)pe, (ShipEntity*)oe))
|
|
return (ShipEntity*)oe;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// fall through => no collision
|
|
return nil;
|
|
}
|
|
|
|
|
|
- (BOOL) checkCloseCollisionWith:(Entity *)other
|
|
{
|
|
if (other == nil) return NO;
|
|
if ([collidingEntities containsObject:other]) return NO; // we know about this already!
|
|
|
|
ShipEntity *otherShip = nil;
|
|
if ([other isShip]) otherShip = (ShipEntity *)other;
|
|
|
|
if ([self canScoop:otherShip]) return YES; // quick test - could this improve scooping for small ships? I think so!
|
|
|
|
if (otherShip != nil && trackCloseContacts)
|
|
{
|
|
// in update we check if close contacts have gone out of touch range (origin within our collision_radius)
|
|
// here we check if something has come within that range
|
|
Vector otherPos = [otherShip position];
|
|
OOUniversalID otherID = [otherShip universalID];
|
|
NSString *other_key = [NSString stringWithFormat:@"%d", otherID];
|
|
|
|
if (![closeContactsInfo objectForKey:other_key] &&
|
|
distance2(position, otherPos) < collision_radius * collision_radius)
|
|
{
|
|
// calculate position with respect to our own position and orientation
|
|
Vector dpos = vector_between(position, otherPos);
|
|
Vector rpos = make_vector(dot_product(dpos, v_right), dot_product(dpos, v_up), dot_product(dpos, v_forward));
|
|
[closeContactsInfo setObject:[NSString stringWithFormat:@"%f %f %f", rpos.x, rpos.y, rpos.z] forKey: other_key];
|
|
|
|
// send AI a message about the touch
|
|
OOUniversalID temp_id = primaryTarget;
|
|
primaryTarget = otherID;
|
|
[self doScriptEvent:@"shipCloseContact" withArgument:otherShip andReactToAIMessage:@"CLOSE CONTACT"];
|
|
primaryTarget = temp_id;
|
|
}
|
|
}
|
|
|
|
if (zero_distance > CLOSE_COLLISION_CHECK_MAX_RANGE2) // don't work too hard on entities that are far from the player
|
|
return YES;
|
|
|
|
if (otherShip != nil)
|
|
{
|
|
// check hull octree versus other hull octree
|
|
collider = doOctreesCollide(self, otherShip);
|
|
return (collider != nil);
|
|
}
|
|
|
|
// default at this stage is to say YES they've collided!
|
|
collider = other;
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BoundingBox)findSubentityBoundingBox
|
|
{
|
|
return [[self mesh] findSubentityBoundingBoxWithPosition:position rotMatrix:rotMatrix];
|
|
}
|
|
|
|
|
|
- (Triangle) absoluteIJKForSubentity
|
|
{
|
|
Triangle result = {{ kBasisXVector, kBasisYVector, kBasisZVector, kZeroVector }};
|
|
Entity *last = nil;
|
|
Entity *father = self;
|
|
OOMatrix r_mat;
|
|
|
|
while ((father)&&(father != last) && (father != NO_TARGET))
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
result.v[0] = OOVectorMultiplyMatrix(result.v[0], r_mat);
|
|
result.v[1] = OOVectorMultiplyMatrix(result.v[1], r_mat);
|
|
result.v[2] = OOVectorMultiplyMatrix(result.v[2], r_mat);
|
|
|
|
last = father;
|
|
if (![last isSubEntity]) break;
|
|
father = [father owner];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
- (void) addSubentityToCollisionRadius:(Entity*)subent
|
|
{
|
|
if (!subent) return;
|
|
|
|
double distance = magnitude([subent position]) + [subent findCollisionRadius];
|
|
if ([subent isKindOfClass:[ShipEntity class]]) // Solid subentity
|
|
{
|
|
if (distance > collision_radius)
|
|
{
|
|
collision_radius = distance;
|
|
}
|
|
|
|
mass += 20.0 * [(ShipEntity *)subent volume];
|
|
}
|
|
if (distance > _profileRadius)
|
|
{
|
|
_profileRadius = distance;
|
|
}
|
|
}
|
|
|
|
|
|
- (ShipEntity *) launchPodWithCrew:(NSArray *)podCrew
|
|
{
|
|
ShipEntity *pod = nil;
|
|
|
|
pod = [UNIVERSE newShipWithRole:[shipinfoDictionary oo_stringForKey:@"escape_pod_model" defaultValue:@"escape-capsule"]];
|
|
if (pod)
|
|
{
|
|
[pod setOwner:self];
|
|
//[pod setScanClass: CLASS_CARGO];
|
|
[pod setCommodity:[UNIVERSE commodityForName:@"Slaves"] andAmount:1];
|
|
[pod setCrew:podCrew];
|
|
[pod switchAITo:@"homeAI.plist"];
|
|
[self dumpItem:pod]; // includes UNIVERSE addEntity
|
|
//[[pod getAI] setState:@"GLOBAL"];
|
|
[pod release]; //release
|
|
}
|
|
|
|
return pod;
|
|
}
|
|
|
|
|
|
- (BOOL) validForAddToUniverse
|
|
{
|
|
if (shipinfoDictionary == nil)
|
|
{
|
|
OOLog(@"shipEntity.notDict", @"Ship %@ was not set up from dictionary.", self);
|
|
return NO;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (void) update:(OOTimeDelta)delta_t
|
|
{
|
|
if (shipinfoDictionary == nil)
|
|
{
|
|
OOLog(@"shipEntity.notDict", @"Ship %@ was not set up from dictionary.", self);
|
|
[UNIVERSE removeEntity:self];
|
|
return;
|
|
}
|
|
|
|
if (!isfinite(maxFlightSpeed))
|
|
{
|
|
OOLog(@"ship.sanityCheck.failed", @"Ship %@ %@ infinite top speed, clamped to 300.", self, @"had");
|
|
maxFlightSpeed = 300;
|
|
}
|
|
|
|
//
|
|
// deal with collisions
|
|
//
|
|
[self manageCollisions];
|
|
|
|
//
|
|
// reset any inadvertant legal mishaps
|
|
//
|
|
if (scanClass == CLASS_POLICE)
|
|
{
|
|
if (bounty > 0)
|
|
bounty = 0;
|
|
ShipEntity* target = [UNIVERSE entityForUniversalID:primaryTarget];
|
|
if ((target)&&(target->scanClass == CLASS_POLICE))
|
|
{
|
|
[self noteLostTarget];
|
|
}
|
|
}
|
|
|
|
if (trackCloseContacts)
|
|
{
|
|
// in checkCloseCollisionWith: we check if some thing has come within touch range (origin within our collision_radius)
|
|
// here we check if it has gone outside that range
|
|
NSEnumerator *contactEnum = nil;
|
|
NSString *other_key = nil;
|
|
|
|
for (contactEnum = [closeContactsInfo keyEnumerator]; (other_key = [contactEnum nextObject]); )
|
|
{
|
|
ShipEntity* other = [UNIVERSE entityForUniversalID:[other_key intValue]];
|
|
if ((other != nil) && (other->isShip))
|
|
{
|
|
if (distance2(position, other->position) > collision_radius * collision_radius) // moved beyond our sphere!
|
|
{
|
|
// calculate position with respect to our own position and orientation
|
|
Vector dpos = vector_between(position, other->position);
|
|
Vector pos1 = make_vector(dot_product(dpos, v_right), dot_product(dpos, v_up), dot_product(dpos, v_forward));
|
|
Vector pos0 = {0, 0, 0};
|
|
ScanVectorFromString([closeContactsInfo objectForKey: other_key], &pos0);
|
|
// send AI messages about the contact
|
|
int temp_id = primaryTarget;
|
|
primaryTarget = other->universalID;
|
|
if ((pos0.x < 0.0)&&(pos1.x > 0.0))
|
|
{
|
|
[self doScriptEvent:@"shipTraversePositiveX" withArgument:other andReactToAIMessage:@"POSITIVE X TRAVERSE"];
|
|
}
|
|
if ((pos0.x > 0.0)&&(pos1.x < 0.0))
|
|
{
|
|
[self doScriptEvent:@"shipTraverseNegativeX" withArgument:other andReactToAIMessage:@"NEGATIVE X TRAVERSE"];
|
|
}
|
|
if ((pos0.y < 0.0)&&(pos1.y > 0.0))
|
|
{
|
|
[self doScriptEvent:@"shipTraversePositiveY" withArgument:other andReactToAIMessage:@"POSITIVE Y TRAVERSE"];
|
|
}
|
|
if ((pos0.y > 0.0)&&(pos1.y < 0.0))
|
|
{
|
|
[self doScriptEvent:@"shipTraverseNegativeY" withArgument:other andReactToAIMessage:@"NEGATIVE Y TRAVERSE"];
|
|
}
|
|
if ((pos0.z < 0.0)&&(pos1.z > 0.0))
|
|
{
|
|
[self doScriptEvent:@"shipTraversePositiveZ" withArgument:other andReactToAIMessage:@"POSITIVE Z TRAVERSE"];
|
|
}
|
|
if ((pos0.z > 0.0)&&(pos1.z < 0.0))
|
|
{
|
|
[self doScriptEvent:@"shipTraverseNegativeZ" withArgument:other andReactToAIMessage:@"NEGATIVE Z TRAVERSE"];
|
|
}
|
|
primaryTarget = temp_id;
|
|
[closeContactsInfo removeObjectForKey: other_key];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[closeContactsInfo removeObjectForKey: other_key];
|
|
}
|
|
}
|
|
}
|
|
|
|
// think!
|
|
#ifdef OO_BRAIN_AI
|
|
[brain update:delta_t];
|
|
#endif
|
|
|
|
// super update
|
|
[super update:delta_t];
|
|
|
|
#ifndef NDEBUG
|
|
// DEBUGGING
|
|
if (reportAIMessages && (debugLastBehaviour != behaviour))
|
|
{
|
|
OOLog(kOOLogEntityBehaviourChanged, @"%@ behaviour is now %@", self, BehaviourToString(behaviour));
|
|
debugLastBehaviour = behaviour;
|
|
}
|
|
#endif
|
|
|
|
// update time between shots
|
|
shot_time += delta_t;
|
|
|
|
// handle radio message effects
|
|
if (messageTime > 0.0)
|
|
{
|
|
messageTime -= delta_t;
|
|
if (messageTime < 0.0)
|
|
messageTime = 0.0;
|
|
}
|
|
|
|
// temperature factors
|
|
double external_temp = 0.0;
|
|
OOSunEntity *sun = [UNIVERSE sun];
|
|
if (sun != nil)
|
|
{
|
|
// set the ambient temperature here
|
|
double sun_zd = magnitude2(vector_between(position, [sun position])); // square of distance
|
|
double sun_cr = sun->collision_radius;
|
|
double alt1 = sun_cr * sun_cr / sun_zd;
|
|
external_temp = SUN_TEMPERATURE * alt1;
|
|
if ([sun goneNova]) external_temp *= 100;
|
|
}
|
|
|
|
// work on the ship temperature
|
|
//
|
|
float heatThreshold = [self heatInsulation] * 100.0f;
|
|
if (external_temp > heatThreshold && external_temp > ship_temperature)
|
|
ship_temperature += (external_temp - ship_temperature) * delta_t * SHIP_INSULATION_FACTOR / [self heatInsulation];
|
|
else
|
|
{
|
|
if (ship_temperature > SHIP_MIN_CABIN_TEMP)
|
|
ship_temperature += (external_temp - heatThreshold - ship_temperature) * delta_t * SHIP_COOLING_FACTOR / [self heatInsulation];
|
|
}
|
|
|
|
if (ship_temperature > SHIP_MAX_CABIN_TEMP)
|
|
[self takeHeatDamage: delta_t * ship_temperature];
|
|
|
|
// are we burning due to low energy
|
|
if ((energy < maxEnergy * 0.20)&&(energy_recharge_rate > 0.0)) // prevents asteroid etc. from burning
|
|
throw_sparks = YES;
|
|
|
|
// burning effects
|
|
if (throw_sparks)
|
|
{
|
|
next_spark_time -= delta_t;
|
|
if (next_spark_time < 0.0)
|
|
{
|
|
[self throwSparks];
|
|
throw_sparks = NO; // until triggered again
|
|
}
|
|
}
|
|
|
|
// cloaking device
|
|
if ([self hasCloakingDevice])
|
|
{
|
|
if (cloaking_device_active)
|
|
{
|
|
energy -= delta_t * CLOAKING_DEVICE_ENERGY_RATE;
|
|
if (energy < CLOAKING_DEVICE_MIN_ENERGY)
|
|
[self deactivateCloakingDevice];
|
|
}
|
|
else
|
|
{
|
|
if (energy < maxEnergy)
|
|
{
|
|
energy += delta_t * CLOAKING_DEVICE_ENERGY_RATE;
|
|
if (energy > maxEnergy)
|
|
{
|
|
energy = maxEnergy;
|
|
[shipAI message:@"ENERGY_FULL"];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// military_jammer
|
|
if ([self hasMilitaryJammer])
|
|
{
|
|
if (military_jammer_active)
|
|
{
|
|
energy -= delta_t * MILITARY_JAMMER_ENERGY_RATE;
|
|
if (energy < MILITARY_JAMMER_MIN_ENERGY)
|
|
military_jammer_active = NO;
|
|
}
|
|
else
|
|
{
|
|
if (energy > 1.5 * MILITARY_JAMMER_MIN_ENERGY)
|
|
military_jammer_active = YES;
|
|
}
|
|
}
|
|
|
|
// check outside factors
|
|
//
|
|
aegis_status = [self checkForAegis]; // is a station or something nearby??
|
|
|
|
//scripting
|
|
if (!haveExecutedSpawnAction && script != nil && [self status] == STATUS_IN_FLIGHT)
|
|
{
|
|
PlayerEntity *player = [PlayerEntity sharedPlayer];
|
|
[player setScriptTarget:self];
|
|
[self doScriptEvent:@"shipSpawned"];
|
|
if ([self status] != STATUS_DEAD) [player doScriptEvent:@"shipSpawned" withArgument:self];
|
|
haveExecutedSpawnAction = YES;
|
|
}
|
|
|
|
// behaviours according to status and behaviour
|
|
//
|
|
if ([self status] == STATUS_LAUNCHING)
|
|
{
|
|
if ([UNIVERSE getTime] > launch_time + LAUNCH_DELAY) // move for while before thinking
|
|
{
|
|
StationEntity *stationLaunchedFrom = [UNIVERSE nearestEntityMatchingPredicate:IsStationPredicate parameter:NULL relativeToEntity:self];
|
|
[self setStatus:STATUS_IN_FLIGHT];
|
|
[self doScriptEvent:@"shipLaunchedFromStation" withArgument:stationLaunchedFrom];
|
|
[shipAI reactToMessage: @"LAUNCHED OKAY"];
|
|
}
|
|
else
|
|
{
|
|
// ignore behaviour just keep moving...
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
if (energy < maxEnergy)
|
|
{
|
|
energy += energy_recharge_rate * delta_t;
|
|
if (energy > maxEnergy)
|
|
{
|
|
energy = maxEnergy;
|
|
[self doScriptEvent:@"shipEnergyBecameFull"];
|
|
[shipAI message:@"ENERGY_FULL"];
|
|
}
|
|
}
|
|
|
|
NSEnumerator *subEnum = nil;
|
|
ShipEntity *se = nil;
|
|
for (subEnum = [self subEntityEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
[se update:delta_t];
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
//
|
|
// double check scooped behaviour
|
|
//
|
|
if ([self status] == STATUS_BEING_SCOOPED)
|
|
{
|
|
//if we are being tractored, but we have no owner, then we have a problem
|
|
if (behaviour != BEHAVIOUR_TRACTORED || [self owner] == nil || [self owner] == self || [self owner] == NO_TARGET)
|
|
{
|
|
// escaped tractor beam
|
|
[self setStatus:STATUS_IN_FLIGHT]; // should correct 'uncollidable objects' bug
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self setOwner:self];
|
|
[shipAI exitStateMachineWithMessage:nil]; // Escapepods and others should continue their old AI here.
|
|
}
|
|
}
|
|
|
|
if ([self status] == STATUS_COCKPIT_DISPLAY)
|
|
{
|
|
[self applyRoll: delta_t * flightRoll andClimb: delta_t * flightPitch];
|
|
GLfloat range2 = 0.1 * distance2(position, destination) / (collision_radius * collision_radius);
|
|
if ((range2 > 1.0)||(velocity.z > 0.0)) range2 = 1.0;
|
|
position = vector_add(position, vector_multiply_scalar(velocity, range2 * delta_t));
|
|
}
|
|
else
|
|
{
|
|
ShipEntity *target = [UNIVERSE entityForUniversalID:primaryTarget];
|
|
|
|
if (target == nil || [target scanClass] == CLASS_NO_DRAW || ![target isShip] || [target isCloaked])
|
|
{
|
|
// It's no longer a parrot, it has ceased to be, it has joined the choir invisible...
|
|
if (primaryTarget != NO_TARGET)
|
|
{
|
|
if ([target isShip] && [target isCloaked])
|
|
{
|
|
[self doScriptEvent:@"shipTargetCloaked" andReactToAIMessage:@"TARGET_CLOAKED"];
|
|
}
|
|
[self noteLostTarget];
|
|
}
|
|
}
|
|
|
|
switch (behaviour)
|
|
{
|
|
case BEHAVIOUR_TUMBLE :
|
|
[self behaviour_tumble: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_STOP_STILL :
|
|
case BEHAVIOUR_STATION_KEEPING :
|
|
[self behaviour_stop_still: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_IDLE :
|
|
[self behaviour_idle: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_TRACTORED :
|
|
[self behaviour_tractored: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_TRACK_TARGET :
|
|
[self behaviour_track_target: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_INTERCEPT_TARGET :
|
|
case BEHAVIOUR_COLLECT_TARGET :
|
|
[self behaviour_intercept_target: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_ATTACK_TARGET :
|
|
[self behaviour_attack_target: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX :
|
|
case BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE :
|
|
[self behaviour_fly_to_target_six: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_ATTACK_MINING_TARGET :
|
|
[self behaviour_attack_mining_target: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_ATTACK_FLY_TO_TARGET :
|
|
[self behaviour_attack_fly_to_target: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_ATTACK_FLY_FROM_TARGET :
|
|
[self behaviour_attack_fly_from_target: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_RUNNING_DEFENSE :
|
|
[self behaviour_running_defense: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_FLEE_TARGET :
|
|
[self behaviour_flee_target: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_FLY_RANGE_FROM_DESTINATION :
|
|
[self behaviour_fly_range_from_destination: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_FACE_DESTINATION :
|
|
[self behaviour_face_destination: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_FORMATION_FORM_UP :
|
|
[self behaviour_formation_form_up: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_FLY_TO_DESTINATION :
|
|
[self behaviour_fly_to_destination: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_FLY_FROM_DESTINATION :
|
|
case BEHAVIOUR_FORMATION_BREAK :
|
|
[self behaviour_fly_from_destination: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_AVOID_COLLISION :
|
|
[self behaviour_avoid_collision: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_TRACK_AS_TURRET :
|
|
[self behaviour_track_as_turret: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_FLY_THRU_NAVPOINTS :
|
|
[self behaviour_fly_thru_navpoints: delta_t];
|
|
break;
|
|
|
|
case BEHAVIOUR_ENERGY_BOMB_COUNTDOWN:
|
|
// Do nothing
|
|
break;
|
|
}
|
|
|
|
// manage energy
|
|
if (energy < maxEnergy)
|
|
{
|
|
energy += energy_recharge_rate * delta_t;
|
|
if (energy > maxEnergy)
|
|
{
|
|
energy = maxEnergy;
|
|
[shipAI message:@"ENERGY_FULL"];
|
|
}
|
|
}
|
|
|
|
// update destination position for escorts
|
|
if ([self hasEscorts])
|
|
{
|
|
NSEnumerator *escortEnum = nil;
|
|
ShipEntity *escort = nil;
|
|
unsigned i = 0;
|
|
|
|
// Note: works on escortArray rather than escortEnumerator because escorts may be mutated.
|
|
for (escortEnum = [[self escortArray] objectEnumerator]; (escort = [escortEnum nextObject]); )
|
|
{
|
|
[escort setEscortDestination:[self coordinatesForEscortPosition:i++]];
|
|
}
|
|
}
|
|
if ([self escortGroup] != nil)
|
|
{
|
|
ShipEntity *leader = [[self escortGroup] leader];
|
|
if (leader != nil && ([leader scanClass] != [self scanClass])) {
|
|
OOLog(@"ship.sanityCheck.failed", @"Ship %@ escorting %@ with wrong scanclass!", self, leader);
|
|
[[self escortGroup] removeShip:self];
|
|
[[self escortGroup] release];
|
|
}
|
|
}
|
|
}
|
|
|
|
// subentity rotation
|
|
if (!quaternion_equal(subentityRotationalVelocity, kIdentityQuaternion) &&
|
|
!quaternion_equal(subentityRotationalVelocity, kZeroQuaternion))
|
|
{
|
|
Quaternion qf = subentityRotationalVelocity;
|
|
qf.w *= (1.0 - delta_t);
|
|
qf.x *= delta_t;
|
|
qf.y *= delta_t;
|
|
qf.z *= delta_t;
|
|
orientation = quaternion_multiply(qf, orientation);
|
|
}
|
|
|
|
// reset totalBoundingBox
|
|
totalBoundingBox = boundingBox;
|
|
|
|
// update subentities
|
|
NSEnumerator *subEnum = nil;
|
|
ShipEntity *se = nil;
|
|
for (subEnum = [self subEntityEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
[se update:delta_t];
|
|
if ([se isShip])
|
|
{
|
|
BoundingBox sebb = [se findSubentityBoundingBox];
|
|
bounding_box_add_vector(&totalBoundingBox, sebb.max);
|
|
bounding_box_add_vector(&totalBoundingBox, sebb.min);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// override Entity version...
|
|
//
|
|
- (double) speed
|
|
{
|
|
return sqrt(velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z + flightSpeed * flightSpeed);
|
|
}
|
|
|
|
|
|
|
|
- (void)respondToAttackFrom:(Entity *)from becauseOf:(Entity *)other
|
|
{
|
|
Entity *source = nil;
|
|
|
|
if ([other isKindOfClass:[ShipEntity class]])
|
|
{
|
|
source = other;
|
|
|
|
ShipEntity *hunter = (ShipEntity *)other;
|
|
//if we are in the same group, then we have to be careful about how we handle things
|
|
if ([self isPolice] && [hunter isPolice])
|
|
{
|
|
//police never get into a fight with each other
|
|
return;
|
|
}
|
|
|
|
OOShipGroup *group = [self group];
|
|
|
|
if (group != nil && group == [hunter group])
|
|
{
|
|
//we are in the same group, do we forgive you?
|
|
//criminals are less likely to forgive
|
|
if (randf() < (0.8 - (bounty/100)))
|
|
{
|
|
//it was an honest mistake, lets get on with it
|
|
return;
|
|
}
|
|
|
|
ShipEntity *groupLeader = [group leader];
|
|
if (hunter == groupLeader)
|
|
{
|
|
//oops we were attacked by our leader, desert him
|
|
[self setGroup:nil];
|
|
}
|
|
else
|
|
{
|
|
//evict them from our group
|
|
[hunter setGroup:nil];
|
|
|
|
[groupLeader setFound_target:other];
|
|
[groupLeader setPrimaryAggressor:hunter];
|
|
[groupLeader respondToAttackFrom:from becauseOf:other];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
source = from;
|
|
}
|
|
[self doScriptEvent:@"shipBeingAttacked" withArgument:source andReactToAIMessage:@"ATTACKED"];
|
|
}
|
|
|
|
|
|
// Equipment
|
|
|
|
- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeWeapons:(BOOL)includeWeapons
|
|
{
|
|
if ([self hasOneEquipmentItem:itemKey includeMissiles:includeWeapons]) return YES;
|
|
if (includeWeapons)
|
|
{
|
|
// Check for primary weapon
|
|
OOWeaponType weaponType = EquipmentStringToWeaponTypeStrict(itemKey);
|
|
if (weaponType != WEAPON_NONE)
|
|
{
|
|
if ([self hasPrimaryWeapon:weaponType]) return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeMissiles:(BOOL)includeMissiles
|
|
{
|
|
if ([_equipment containsObject:itemKey]) return YES;
|
|
|
|
if (includeMissiles && missiles > 0)
|
|
{
|
|
unsigned i;
|
|
for (i = 0; i < missiles; i++)
|
|
{
|
|
if ([[missile_list[i] identifier] isEqualTo:itemKey]) return YES;
|
|
}
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (BOOL) hasPrimaryWeapon:(OOWeaponType)weaponType
|
|
{
|
|
NSEnumerator *subEntEnum = nil;
|
|
ShipEntity *subEntity = nil;
|
|
|
|
if (forward_weapon_type == weaponType || aft_weapon_type == weaponType) return YES;
|
|
|
|
for (subEntEnum = [self shipSubEntityEnumerator]; (subEntity = [subEntEnum nextObject]); )
|
|
{
|
|
if ([subEntity hasPrimaryWeapon:weaponType]) return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (BOOL) hasEquipmentItem:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons
|
|
{
|
|
// this function is also used internally to find out if an equipped item is undamaged.
|
|
NSEnumerator *keyEnum = nil;
|
|
id key = nil;
|
|
|
|
if (!includeWeapons && _equipment == nil) return NO;
|
|
|
|
// Make sure it's an array or set, using a single-object set if it's a string.
|
|
if ([equipmentKeys isKindOfClass:[NSString class]]) equipmentKeys = [NSArray arrayWithObject:equipmentKeys];
|
|
else if (![equipmentKeys isKindOfClass:[NSArray class]] && ![equipmentKeys isKindOfClass:[NSSet class]]) return NO;
|
|
|
|
for (keyEnum = [equipmentKeys objectEnumerator]; (key = [keyEnum nextObject]); )
|
|
{
|
|
if ([self hasOneEquipmentItem:key includeWeapons:includeWeapons]) return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (BOOL) hasEquipmentItem:(id)equipmentKeys
|
|
{
|
|
return [self hasEquipmentItem:equipmentKeys includeWeapons:NO];
|
|
}
|
|
|
|
|
|
- (BOOL) hasAllEquipment:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons
|
|
{
|
|
NSEnumerator *keyEnum = nil;
|
|
id key = nil;
|
|
|
|
if (_equipment == nil) return NO;
|
|
|
|
// Make sure it's an array or set, using a single-object set if it's a string.
|
|
if ([equipmentKeys isKindOfClass:[NSString class]]) equipmentKeys = [NSArray arrayWithObject:equipmentKeys];
|
|
else if (![equipmentKeys isKindOfClass:[NSArray class]] && ![equipmentKeys isKindOfClass:[NSSet class]]) return NO;
|
|
|
|
for (keyEnum = [equipmentKeys objectEnumerator]; (key = [keyEnum nextObject]); )
|
|
{
|
|
if (![self hasOneEquipmentItem:key includeWeapons:includeWeapons]) return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) hasAllEquipment:(id)equipmentKeys
|
|
{
|
|
return [self hasAllEquipment:equipmentKeys includeWeapons:NO];
|
|
}
|
|
|
|
|
|
- (BOOL) canAddEquipment:(NSString *)equipmentKey
|
|
{
|
|
if ([equipmentKey hasSuffix:@"_DAMAGED"])
|
|
{
|
|
equipmentKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
|
|
}
|
|
|
|
if ([equipmentKey hasSuffix:@"MISSILE"]||[equipmentKey hasSuffix:@"MINE"]||([self isThargoid] && [equipmentKey isEqualToString:@"thargon"]))
|
|
{
|
|
if (missiles >= max_missiles) return NO;
|
|
}
|
|
|
|
if ([self hasEquipmentItem:equipmentKey]) return NO;
|
|
if (![self equipmentValidToAdd:equipmentKey]) return NO;
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (OOEquipmentType *) weaponTypeForFacing:(int) facing
|
|
{
|
|
OOWeaponType weapon_type = WEAPON_NONE;
|
|
|
|
switch (facing)
|
|
{
|
|
case WEAPON_FACING_FORWARD:
|
|
weapon_type = forward_weapon_type;
|
|
break;
|
|
case WEAPON_FACING_AFT:
|
|
weapon_type = aft_weapon_type;
|
|
break;
|
|
// any other value is not a facing for NPC ships...
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return [OOEquipmentType equipmentTypeWithIdentifier:WeaponTypeToEquipmentString(weapon_type)];
|
|
}
|
|
|
|
|
|
- (NSArray *) missilesList
|
|
{
|
|
NSMutableArray *miss = [NSMutableArray arrayWithCapacity:missiles];
|
|
unsigned i;
|
|
|
|
for (i = 0; i < missiles; i++)
|
|
{
|
|
[miss addObject:missile_list[i]];
|
|
}
|
|
return [[miss copy] autorelease];
|
|
}
|
|
|
|
|
|
- (NSArray *) passengerListForScripting
|
|
{
|
|
return [NSArray array];
|
|
}
|
|
|
|
|
|
- (NSArray *) contractListForScripting
|
|
{
|
|
return [NSArray array];
|
|
}
|
|
|
|
|
|
- (NSDictionary *) eqDictionaryWithType:(OOEquipmentType *) type isDamaged:(BOOL) isDamaged
|
|
{
|
|
return [NSDictionary dictionaryWithObjects:[NSArray arrayWithObjects:type, [NSNumber numberWithBool:isDamaged], nil]
|
|
forKeys:[NSArray arrayWithObjects:@"type", @"isDamaged", nil]];
|
|
}
|
|
|
|
|
|
- (NSArray *) equipmentListForScripting
|
|
{
|
|
NSArray *eqTypes = [OOEquipmentType allEquipmentTypes];
|
|
NSMutableArray *quip = [NSMutableArray arrayWithCapacity:[eqTypes count]];
|
|
NSEnumerator *eqTypeEnum = nil;
|
|
OOEquipmentType *eqType = nil;
|
|
BOOL isDamaged;
|
|
|
|
for (eqTypeEnum = [eqTypes objectEnumerator]; (eqType = [eqTypeEnum nextObject]); )
|
|
{
|
|
/*
|
|
// comprehensive list, but at odds with the rest of the api - Kaks
|
|
if ([self hasEquipmentItem:[eqType identifier]])
|
|
{
|
|
[quip addObject:[self eqDictionaryWithType:eqType isDamaged:NO]];
|
|
}
|
|
else if (![UNIVERSE strict]) // Check for damaged version
|
|
{
|
|
if ([self hasEquipmentItem:[[eqType identifier] stringByAppendingString:@"_DAMAGED"]])
|
|
{
|
|
[quip addObject:[self eqDictionaryWithType:eqType isDamaged:YES]];
|
|
}
|
|
}
|
|
*/
|
|
// less comprehensive list, but consistent with the rest of the API - Kaks
|
|
isDamaged = ![UNIVERSE strict] && [self hasEquipmentItem:[[eqType identifier] stringByAppendingString:@"_DAMAGED"]];
|
|
if ([self hasEquipmentItem:[eqType identifier]] || isDamaged)
|
|
{
|
|
[quip addObject:eqType];
|
|
}
|
|
}
|
|
|
|
// Passengers - not supported for NPCs, but it's here for genericity.
|
|
if ([self passengerCapacity] > 0)
|
|
{
|
|
eqType = [OOEquipmentType equipmentTypeWithIdentifier:@"EQ_PASSENGER_BERTH"];
|
|
//[quip addObject:[self eqDictionaryWithType:eqType isDamaged:NO]];
|
|
[quip addObject:eqType];
|
|
}
|
|
|
|
return [[quip copy] autorelease];
|
|
}
|
|
|
|
|
|
- (BOOL) equipmentValidToAdd:(NSString *)equipmentKey
|
|
{
|
|
OOEquipmentType *eqType = nil;
|
|
|
|
if ([equipmentKey hasSuffix:@"_DAMAGED"])
|
|
{
|
|
equipmentKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
|
|
}
|
|
|
|
eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey];
|
|
if (eqType == nil) return NO;
|
|
|
|
if ([eqType requiresEmptyPylon] && [self missileCount] >= [self missileCapacity]) return NO;
|
|
if ([eqType requiresMountedPylon] && [self missileCount] == 0) return NO;
|
|
if ([self availableCargoSpace] < [eqType requiredCargoSpace]) return NO;
|
|
if ([eqType requiresEquipment] != nil && ![self hasAllEquipment:[eqType requiresEquipment] includeWeapons:YES]) return NO;
|
|
if ([eqType requiresAnyEquipment] != nil && ![self hasEquipmentItem:[eqType requiresAnyEquipment] includeWeapons:YES]) return NO;
|
|
if ([eqType incompatibleEquipment] != nil && [self hasEquipmentItem:[eqType incompatibleEquipment] includeWeapons:YES]) return NO;
|
|
if ([eqType requiresCleanLegalRecord] && [self legalStatus] != 0) return NO;
|
|
if ([eqType requiresNonCleanLegalRecord] && [self legalStatus] == 0) return NO;
|
|
if ([eqType requiresFreePassengerBerth] && [self passengerCount] >= [self passengerCapacity]) return NO;
|
|
if ([eqType requiresFullFuel] && [self fuel] < [self fuelCapacity]) return NO;
|
|
if ([eqType requiresNonFullFuel] && [self fuel] >= [self fuelCapacity]) return NO;
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) addEquipmentItem:(NSString *)equipmentKey
|
|
{
|
|
return [self addEquipmentItem:equipmentKey withValidation:YES];
|
|
}
|
|
|
|
|
|
- (BOOL) addEquipmentItem:(NSString *)equipmentKey withValidation:(BOOL)validateAddition
|
|
{
|
|
OOEquipmentType *eqType = nil;
|
|
|
|
// canAddEquipment always checks if the undamaged version is equipped.
|
|
if (validateAddition == YES && ![self canAddEquipment:equipmentKey]) return NO;
|
|
|
|
// special cases
|
|
|
|
if ([equipmentKey hasSuffix:@"MISSILE"]||[equipmentKey hasSuffix:@"MINE"]||([self isThargoid] && [equipmentKey isEqualToString:@"thargon"]))
|
|
{
|
|
if (missiles >= max_missiles) return NO;
|
|
|
|
missile_list[missiles] = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey];
|
|
missiles++;
|
|
return YES;
|
|
}
|
|
|
|
// don't add any thargons to non-thargoid ships.
|
|
if([equipmentKey isEqualToString:@"thargon"]) return NO;
|
|
|
|
// we can theoretically add a damaged weapon, but not a working one.
|
|
if([equipmentKey hasPrefix:@"EQ_WEAPON"] && ![equipmentKey hasSuffix:@"_DAMAGED"])
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
// end special cases
|
|
|
|
if ([equipmentKey hasSuffix:@"_DAMAGED"])
|
|
{
|
|
eqType = [OOEquipmentType equipmentTypeWithIdentifier:[equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]]];
|
|
}
|
|
else
|
|
{
|
|
eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey];
|
|
// in case we have the damaged version!
|
|
[_equipment removeObject:[equipmentKey stringByAppendingString:@"_DAMAGED"]];
|
|
}
|
|
|
|
// does this equipment actually exist?
|
|
if (eqType == nil) return NO;
|
|
|
|
if (_equipment == nil) _equipment = [[NSMutableSet alloc] init];
|
|
|
|
if ([equipmentKey isEqual:@"EQ_CARGO_BAY"])
|
|
{
|
|
max_cargo += extra_cargo;
|
|
}
|
|
|
|
// add the equipment
|
|
[_equipment addObject:equipmentKey];
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (NSEnumerator *) equipmentEnumerator
|
|
{
|
|
return [_equipment objectEnumerator];
|
|
}
|
|
|
|
|
|
- (unsigned) equipmentCount
|
|
{
|
|
return [_equipment count];
|
|
}
|
|
|
|
|
|
- (void) removeEquipmentItem:(NSString *)equipmentKey
|
|
{
|
|
NSString *equipmentTypeCheckKey = equipmentKey;
|
|
|
|
// determine the equipment type and make sure it works also in the case of damaged equipment
|
|
if ([equipmentKey hasSuffix:@"_DAMAGED"])
|
|
{
|
|
equipmentTypeCheckKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
|
|
}
|
|
OOEquipmentType *eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentTypeCheckKey];
|
|
if (eqType == nil) return;
|
|
|
|
if ([eqType isMissileOrMine] || ([self isThargoid] && [equipmentTypeCheckKey isEqualToString:@"thargon"]))
|
|
{
|
|
[self removeExternalStore:eqType];
|
|
}
|
|
else
|
|
{
|
|
if ([equipmentKey isEqual:@"EQ_CARGO_BAY"] && [_equipment containsObject:equipmentKey])
|
|
{
|
|
max_cargo -= extra_cargo;
|
|
}
|
|
|
|
[_equipment removeObject:equipmentKey];
|
|
if (![equipmentKey hasSuffix:@"_DAMAGED"])
|
|
{
|
|
[_equipment removeObject:[equipmentKey stringByAppendingString:@"_DAMAGED"]];
|
|
}
|
|
if ([_equipment count] == 0) [self removeAllEquipment];
|
|
if (isPlayer)
|
|
{
|
|
if([equipmentKey isEqualToString:@"EQ_ADVANCED_COMPASS"]) [(PlayerEntity*)self setCompassMode:COMPASS_MODE_BASIC];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (BOOL) removeExternalStore:(OOEquipmentType *)eqType
|
|
{
|
|
NSString *identifier = [eqType identifier];
|
|
unsigned i;
|
|
|
|
for (i = 0; i < missiles; i++)
|
|
{
|
|
if ([[missile_list[i] identifier] isEqualTo:identifier])
|
|
{
|
|
// now 'delete' [i] by compacting the array
|
|
while ( ++i < missiles ) missile_list[i - 1] = missile_list[i];
|
|
|
|
missiles--;
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (OOEquipmentType *) newMissile
|
|
{
|
|
ShipEntity *missile = nil;
|
|
OOEquipmentType *missileType = nil;
|
|
BOOL isMissileType = NO;
|
|
id value;
|
|
double chance = randf();
|
|
|
|
if ([self isThargoid])
|
|
{
|
|
if (missileRole != nil) missile = [UNIVERSE newShipWithRole:missileRole]; // retained
|
|
else missile = [UNIVERSE newShipWithRole:@"thargon"]; // retained
|
|
missileType = [OOEquipmentType equipmentTypeWithIdentifier:[missile primaryRole]];
|
|
return missileType;
|
|
}
|
|
|
|
// random role 10% of the cases, if a missile role is defined.
|
|
if (chance < 0.9f && missileRole != nil) missile = [UNIVERSE newShipWithRole:missileRole]; // retained
|
|
if (missile == nil) // no actual role defined?
|
|
{
|
|
// random role 20% of the cases. (the 10% above is included here)
|
|
if (chance > 0.8f) missile = [UNIVERSE newShipWithRole:@"missile"]; // retained
|
|
// otherwise use the standard role.
|
|
else missile = [UNIVERSE newShipWithRole:@"EQ_MISSILE"]; // retained
|
|
}
|
|
|
|
NSEnumerator *enumerator = [[[missile roleSet] roles] objectEnumerator];
|
|
|
|
while ((value = [enumerator nextObject])) {
|
|
missileType = [OOEquipmentType equipmentTypeWithIdentifier:(NSString *)value];
|
|
// ensure that we have a missile or mine
|
|
isMissileType = (missileType != nil && [missileType isMissileOrMine]);
|
|
if (isMissileType) break;
|
|
}
|
|
[missile release];
|
|
|
|
return (isMissileType ? missileType : nil);
|
|
}
|
|
|
|
|
|
- (void) removeAllEquipment
|
|
{
|
|
[_equipment release];
|
|
_equipment = nil;
|
|
}
|
|
|
|
|
|
- (int) removeMissiles
|
|
{
|
|
missiles = 0;
|
|
return 0;
|
|
}
|
|
|
|
|
|
- (unsigned) passengerCount
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
- (unsigned) passengerCapacity
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
- (unsigned) missileCount
|
|
{
|
|
return missiles;
|
|
}
|
|
|
|
|
|
- (unsigned) missileCapacity
|
|
{
|
|
return max_missiles;
|
|
}
|
|
|
|
|
|
- (unsigned) extraCargo
|
|
{
|
|
return extra_cargo;
|
|
}
|
|
|
|
|
|
- (BOOL) hasScoop
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_FUEL_SCOOPS"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasECM
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_ECM"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasCloakingDevice
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_CLOAKING_DEVICE"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasMilitaryScannerFilter
|
|
{
|
|
#if USEMASC
|
|
return [self hasEquipmentItem:@"EQ_MILITARY_SCANNER_FILTER"];
|
|
#else
|
|
return NO;
|
|
#endif
|
|
}
|
|
|
|
|
|
- (BOOL) hasMilitaryJammer
|
|
{
|
|
#if USEMASC
|
|
return [self hasEquipmentItem:@"EQ_MILITARY_JAMMER"];
|
|
#else
|
|
return NO;
|
|
#endif
|
|
}
|
|
|
|
|
|
- (BOOL) hasExpandedCargoBay
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_CARGO_BAY"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasShieldBooster
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_SHIELD_BOOSTER"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasMilitaryShieldEnhancer
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_NAVAL_SHIELD_BOOSTER"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasHeatShield
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_HEAT_SHIELD"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasFuelInjection
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_FUEL_INJECTION"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasEnergyBomb
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_ENERGY_BOMB"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasEscapePod
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_ESCAPE_POD"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasDockingComputer
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_DOCK_COMP"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasGalacticHyperdrive
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_GAL_DRIVE"];
|
|
}
|
|
|
|
|
|
- (float) shieldBoostFactor
|
|
{
|
|
float boostFactor = 1.0f;
|
|
if ([self hasShieldBooster]) boostFactor += 1.0f;
|
|
if ([self hasMilitaryShieldEnhancer]) boostFactor += 1.0f;
|
|
|
|
return boostFactor;
|
|
}
|
|
|
|
|
|
- (float) maxForwardShieldLevel
|
|
{
|
|
return BASELINE_SHIELD_LEVEL * [self shieldBoostFactor];
|
|
}
|
|
|
|
|
|
- (float) maxAftShieldLevel
|
|
{
|
|
return BASELINE_SHIELD_LEVEL * [self shieldBoostFactor];
|
|
}
|
|
|
|
|
|
- (float) shieldRechargeRate
|
|
{
|
|
return [self hasMilitaryShieldEnhancer] ? 3.0f : 2.0f;
|
|
}
|
|
|
|
|
|
- (float) afterburnerFactor
|
|
{
|
|
return 7.0f;
|
|
}
|
|
|
|
|
|
- (float) maxThrust
|
|
{
|
|
return max_thrust;
|
|
}
|
|
|
|
|
|
- (float) thrust
|
|
{
|
|
return thrust;
|
|
}
|
|
|
|
|
|
////////////////
|
|
// //
|
|
// behaviours //
|
|
// //
|
|
- (void) behaviour_stop_still:(double) delta_t
|
|
{
|
|
double damping = 0.5 * delta_t;
|
|
// damp roll
|
|
if (flightRoll < 0)
|
|
flightRoll += (flightRoll < -damping) ? damping : -flightRoll;
|
|
if (flightRoll > 0)
|
|
flightRoll -= (flightRoll > damping) ? damping : flightRoll;
|
|
// damp pitch
|
|
if (flightPitch < 0)
|
|
flightPitch += (flightPitch < -damping) ? damping : -flightPitch;
|
|
if (flightPitch > 0)
|
|
flightPitch -= (flightPitch > damping) ? damping : flightPitch;
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_idle:(double) delta_t
|
|
{
|
|
double damping = 0.5 * delta_t;
|
|
if ((!isStation)&&(scanClass != CLASS_BUOY))
|
|
{
|
|
// damp roll
|
|
if (flightRoll < 0)
|
|
flightRoll += (flightRoll < -damping) ? damping : -flightRoll;
|
|
if (flightRoll > 0)
|
|
flightRoll -= (flightRoll > damping) ? damping : flightRoll;
|
|
}
|
|
if (scanClass != CLASS_BUOY)
|
|
{
|
|
// damp pitch
|
|
if (flightPitch < 0)
|
|
flightPitch += (flightPitch < -damping) ? damping : -flightPitch;
|
|
if (flightPitch > 0)
|
|
flightPitch -= (flightPitch > damping) ? damping : flightPitch;
|
|
}
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_tumble:(double) delta_t
|
|
{
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_tractored:(double) delta_t
|
|
{
|
|
desired_range = collision_radius * 2.0;
|
|
ShipEntity* hauler = (ShipEntity*)[self owner];
|
|
if ((hauler)&&([hauler isShip]))
|
|
{
|
|
destination = [hauler absoluteTractorPosition];
|
|
double distance = [self rangeToDestination];
|
|
if (distance < desired_range)
|
|
{
|
|
behaviour = BEHAVIOUR_TUMBLE;
|
|
[self setStatus:STATUS_IN_FLIGHT];
|
|
[hauler scoopUp:self];
|
|
return;
|
|
}
|
|
GLfloat tf = TRACTOR_FORCE / mass;
|
|
// adjust for difference in velocity (spring rule)
|
|
Vector dv = vector_between([self velocity], [hauler velocity]);
|
|
GLfloat moment = delta_t * 0.25 * tf;
|
|
velocity.x += moment * dv.x;
|
|
velocity.y += moment * dv.y;
|
|
velocity.z += moment * dv.z;
|
|
// acceleration = force / mass
|
|
// force proportional to distance (spring rule)
|
|
Vector dp = vector_between(position, destination);
|
|
moment = delta_t * 0.5 * tf;
|
|
velocity.x += moment * dp.x;
|
|
velocity.y += moment * dp.y;
|
|
velocity.z += moment * dp.z;
|
|
// force inversely proportional to distance
|
|
GLfloat d2 = magnitude2(dp);
|
|
moment = (d2 > 0.0)? delta_t * 5.0 * tf / d2 : 0.0;
|
|
if (d2 > 0.0)
|
|
{
|
|
velocity.x += moment * dp.x;
|
|
velocity.y += moment * dp.y;
|
|
velocity.z += moment * dp.z;
|
|
}
|
|
//
|
|
if ([self status] == STATUS_BEING_SCOOPED)
|
|
{
|
|
BOOL lost_contact = (distance > hauler->collision_radius + collision_radius + 250.0f); // 250m range for tractor beam
|
|
if ([hauler isPlayer])
|
|
{
|
|
switch ([(PlayerEntity*)hauler dialFuelScoopStatus])
|
|
{
|
|
case SCOOP_STATUS_NOT_INSTALLED :
|
|
case SCOOP_STATUS_FULL_HOLD :
|
|
lost_contact = YES; // don't draw
|
|
break;
|
|
}
|
|
}
|
|
//
|
|
if (lost_contact) // 250m range for tractor beam
|
|
{
|
|
// escaped tractor beam
|
|
[self setStatus:STATUS_IN_FLIGHT];
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self setOwner:self];
|
|
[shipAI exitStateMachineWithMessage:nil]; // exit nullAI.plist
|
|
}
|
|
else if ([hauler isPlayer])
|
|
{
|
|
[(PlayerEntity*)hauler setScoopsActive];
|
|
}
|
|
}
|
|
}
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
desired_speed = 0.0;
|
|
thrust = 25.0; // used to damp velocity (must be less than hauler thrust)
|
|
[self applyThrust:delta_t];
|
|
thrust = 0.0; // must reset thrust now
|
|
}
|
|
|
|
|
|
- (void) behaviour_track_target:(double) delta_t
|
|
{
|
|
if (primaryTarget == NO_TARGET)
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget];
|
|
return;
|
|
}
|
|
[self trackPrimaryTarget:delta_t:NO];
|
|
if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget))
|
|
{
|
|
[self avoidCollision];
|
|
}
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_intercept_target:(double) delta_t
|
|
{
|
|
double range = [self rangeToPrimaryTarget];
|
|
if (behaviour == BEHAVIOUR_INTERCEPT_TARGET)
|
|
{
|
|
desired_speed = maxFlightSpeed;
|
|
if (range < desired_range)
|
|
{
|
|
[shipAI reactToMessage:@"DESIRED_RANGE_ACHIEVED"];
|
|
}
|
|
desired_speed = maxFlightSpeed * [self trackPrimaryTarget:delta_t:NO];
|
|
}
|
|
else
|
|
{
|
|
// = BEHAVIOUR_COLLECT_TARGET
|
|
ShipEntity* target = [self primaryTarget];
|
|
double target_speed = [target speed];
|
|
double eta = range / (flightSpeed - target_speed);
|
|
double last_success_factor = success_factor;
|
|
double last_distance = last_success_factor;
|
|
double distance = [self rangeToDestination];
|
|
success_factor = distance;
|
|
//
|
|
double slowdownTime = 96.0 / thrust; // more thrust implies better slowing
|
|
double minTurnSpeedFactor = 0.005 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds
|
|
|
|
if ((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor))
|
|
desired_speed = flightSpeed * 0.75; // cut speed by 50% to a minimum minTurnSpeedFactor of speed
|
|
else
|
|
desired_speed = maxFlightSpeed;
|
|
|
|
if (desired_speed < target_speed)
|
|
{
|
|
desired_speed += target_speed;
|
|
if (target_speed > maxFlightSpeed)
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget];
|
|
return;
|
|
}
|
|
}
|
|
//
|
|
if (target) // check introduced to stop crash at next line
|
|
{
|
|
destination = target->position;
|
|
desired_range = 0.5 * target->collision_radius;
|
|
[self trackDestination: delta_t : NO];
|
|
}
|
|
else
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget];
|
|
}
|
|
//
|
|
if (distance < last_distance) // improvement
|
|
{
|
|
frustration -= delta_t;
|
|
if (frustration < 0.0)
|
|
frustration = 0.0;
|
|
}
|
|
else
|
|
{
|
|
frustration += delta_t;
|
|
if (frustration > 10.0) // 10s of frustration
|
|
{
|
|
[shipAI reactToMessage:@"FRUSTRATED"];
|
|
frustration -= 5.0; //repeat after another five seconds' frustration
|
|
}
|
|
}
|
|
}
|
|
if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget))
|
|
[self avoidCollision];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_attack_target:(double) delta_t
|
|
{
|
|
BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
|
|
float max_available_speed = maxFlightSpeed;
|
|
double range = [self rangeToPrimaryTarget];
|
|
if (canBurn) max_available_speed *= [self afterburnerFactor];
|
|
|
|
[self activateCloakingDevice];
|
|
|
|
desired_speed = max_available_speed;
|
|
if (range < 0.035 * weaponRange)
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
|
|
else
|
|
if (universalID & 1) // 50% of ships are smart S.M.R.T. smart!
|
|
{
|
|
if (randf() < 0.75)
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX;
|
|
else
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
|
|
}
|
|
else
|
|
{
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
|
|
}
|
|
frustration = 0.0; // behaviour changed, so reset frustration
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_fly_to_target_six:(double) delta_t
|
|
{
|
|
BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
|
|
float max_available_speed = maxFlightSpeed;
|
|
double range = [self rangeToPrimaryTarget];
|
|
if (canBurn) max_available_speed *= [self afterburnerFactor];
|
|
|
|
// deal with collisions and lost targets
|
|
if (proximity_alert != NO_TARGET)
|
|
{
|
|
[self avoidCollision];
|
|
}
|
|
if (range > SCANNER_MAX_RANGE || primaryTarget == NO_TARGET)
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget];
|
|
return;
|
|
}
|
|
|
|
// control speed
|
|
BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed);
|
|
double slow_down_range = weaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
|
|
ShipEntity* target = [UNIVERSE entityForUniversalID:primaryTarget];
|
|
double target_speed = [target speed];
|
|
double last_success_factor = success_factor; // EW
|
|
double distance = [self rangeToDestination];
|
|
success_factor = distance; // EW
|
|
|
|
if (range < slow_down_range)
|
|
{
|
|
desired_speed = OOMax_d(target_speed, 0.4 * maxFlightSpeed);
|
|
|
|
// avoid head-on collision
|
|
if ((range < 0.5 * distance)&&(behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX))
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
|
|
}
|
|
else
|
|
desired_speed = max_available_speed; // use afterburner to approach
|
|
|
|
|
|
// if within 0.75km of the target's six or twelve then vector in attack
|
|
if (distance < 750.0)
|
|
{
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
|
|
frustration = 0.0;
|
|
desired_speed = OOMax_d(target_speed, 0.4 * maxFlightSpeed); // within the weapon's range don't use afterburner
|
|
}
|
|
|
|
// target-six
|
|
if (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX)
|
|
{
|
|
// head for a point weapon-range * 0.5 to the six of the target
|
|
//
|
|
destination = [target distance_six:0.5 * weaponRange];
|
|
}
|
|
// target-twelve
|
|
if (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE)
|
|
{
|
|
// head for a point 1.25km above the target
|
|
//
|
|
destination = [target distance_twelve:1250];
|
|
}
|
|
|
|
double confidenceFactor = [self trackDestination:delta_t :NO];
|
|
|
|
if(success_factor > last_success_factor || confidenceFactor < 0.85) frustration += delta_t; // EW
|
|
else if(frustration > 0.0) frustration -= delta_t;
|
|
if(frustration > 10) // EW
|
|
{
|
|
frustration = 0.0;
|
|
if (randf() < 0.4)
|
|
{
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
|
|
}
|
|
else
|
|
{
|
|
if (randf() < 0.5)
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX;
|
|
else
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE;
|
|
}
|
|
}
|
|
|
|
// use weaponry
|
|
int missile_chance = 0;
|
|
int rhs = 3.2 / delta_t;
|
|
if (rhs) missile_chance = 1 + (ranrot_rand() % rhs);
|
|
|
|
double hurt_factor = 16 * pow(energy/maxEnergy, 4.0);
|
|
if (missiles > missile_chance * hurt_factor)
|
|
{
|
|
[self fireMissile];
|
|
}
|
|
[self activateCloakingDevice];
|
|
[self fireMainWeapon:range];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_attack_mining_target:(double) delta_t
|
|
{
|
|
double range = [self rangeToPrimaryTarget];
|
|
if (primaryTarget == NO_TARGET || range > SCANNER_MAX_RANGE)
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget];
|
|
desired_speed = maxFlightSpeed * 0.375;
|
|
return;
|
|
}
|
|
else if ((range < 650) || (proximity_alert != NO_TARGET))
|
|
{
|
|
if (proximity_alert == NO_TARGET)
|
|
{
|
|
desired_speed = range * maxFlightSpeed / (650.0 * 16.0);
|
|
}
|
|
else
|
|
{
|
|
[self avoidCollision];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//we have a target, its within scanner range, and outside 650
|
|
desired_speed = maxFlightSpeed * 0.375;
|
|
}
|
|
|
|
[self trackPrimaryTarget:delta_t:NO];
|
|
[self fireMainWeapon:range];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_attack_fly_to_target:(double) delta_t
|
|
{
|
|
BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
|
|
float max_available_speed = maxFlightSpeed;
|
|
double range = [self rangeToPrimaryTarget];
|
|
if (canBurn) max_available_speed *= [self afterburnerFactor];
|
|
if (primaryTarget == NO_TARGET)
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget];
|
|
return;
|
|
}
|
|
if ((range < COMBAT_IN_RANGE_FACTOR * weaponRange)||(proximity_alert != NO_TARGET))
|
|
{
|
|
if (proximity_alert == NO_TARGET)
|
|
{
|
|
if (aft_weapon_type == WEAPON_NONE)
|
|
{
|
|
jink.x = (ranrot_rand() % 256) - 128.0;
|
|
jink.y = (ranrot_rand() % 256) - 128.0;
|
|
jink.z = 1000.0;
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
|
|
frustration = 0.0;
|
|
desired_speed = max_available_speed;
|
|
}
|
|
else
|
|
{
|
|
// entering running defense mode
|
|
jink = kZeroVector;
|
|
behaviour = BEHAVIOUR_RUNNING_DEFENSE;
|
|
frustration = 0.0;
|
|
desired_speed = maxFlightSpeed;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self avoidCollision];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (range > SCANNER_MAX_RANGE)
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget];
|
|
return;
|
|
}
|
|
}
|
|
|
|
// control speed
|
|
//
|
|
BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed);
|
|
double slow_down_range = weaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0);
|
|
ShipEntity* target = [UNIVERSE entityForUniversalID:primaryTarget];
|
|
double target_speed = [target speed];
|
|
if (range <= slow_down_range)
|
|
desired_speed = OOMax_d(target_speed, 0.25 * maxFlightSpeed); // within the weapon's range match speed
|
|
else
|
|
desired_speed = max_available_speed; // use afterburner to approach
|
|
|
|
double last_success_factor = success_factor;
|
|
success_factor = [self trackPrimaryTarget:delta_t:NO]; // do the actual piloting
|
|
if ((success_factor > 0.999)||(success_factor > last_success_factor))
|
|
{
|
|
frustration -= delta_t;
|
|
if (frustration < 0.0)
|
|
frustration = 0.0;
|
|
}
|
|
else
|
|
{
|
|
frustration += delta_t;
|
|
if (frustration > 3.0) // 3s of frustration
|
|
{
|
|
[shipAI reactToMessage:@"FRUSTRATED"];
|
|
// THIS IS HERE AS A TEST ONLY
|
|
// BREAK OFF
|
|
jink.x = (ranrot_rand() % 256) - 128.0;
|
|
jink.y = (ranrot_rand() % 256) - 128.0;
|
|
jink.z = 1000.0;
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET;
|
|
frustration = 0.0;
|
|
desired_speed = maxFlightSpeed;
|
|
}
|
|
}
|
|
|
|
int missile_chance = 0;
|
|
int rhs = 3.2 / delta_t;
|
|
if (rhs) missile_chance = 1 + (ranrot_rand() % rhs);
|
|
|
|
double hurt_factor = 16 * pow(energy/maxEnergy, 4.0);
|
|
if (missiles > missile_chance * hurt_factor)
|
|
{
|
|
[self fireMissile];
|
|
}
|
|
[self activateCloakingDevice];
|
|
[self fireMainWeapon:range];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_attack_fly_from_target:(double) delta_t
|
|
{
|
|
double range = [self rangeToPrimaryTarget];
|
|
if (primaryTarget == NO_TARGET)
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget];
|
|
return;
|
|
}
|
|
if (range > COMBAT_OUT_RANGE_FACTOR * weaponRange + 15.0 * jink.x)
|
|
{
|
|
jink.x = 0.0;
|
|
jink.y = 0.0;
|
|
jink.z = 0.0;
|
|
behaviour = BEHAVIOUR_ATTACK_TARGET;
|
|
frustration = 0.0;
|
|
}
|
|
[self trackPrimaryTarget:delta_t:YES];
|
|
|
|
int missile_chance = 0;
|
|
int rhs = 3.2 / delta_t;
|
|
if (rhs) missile_chance = 1 + (ranrot_rand() % rhs);
|
|
|
|
double hurt_factor = 16 * pow(energy/maxEnergy, 4.0);
|
|
if (missiles > missile_chance * hurt_factor)
|
|
{
|
|
[self fireMissile];
|
|
}
|
|
[self activateCloakingDevice];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_running_defense:(double) delta_t
|
|
{
|
|
double range = [self rangeToPrimaryTarget];
|
|
if (range > weaponRange || range > 0.8 * scannerRange || range == 0)
|
|
{
|
|
jink.x = 0.0;
|
|
jink.y = 0.0;
|
|
jink.z = 0.0;
|
|
behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET;
|
|
frustration = 0.0;
|
|
}
|
|
[self trackPrimaryTarget:delta_t:YES];
|
|
[self fireAftWeapon:range];
|
|
[self activateCloakingDevice];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_flee_target:(double) delta_t
|
|
{
|
|
BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
|
|
float max_available_speed = maxFlightSpeed;
|
|
double range = [self rangeToPrimaryTarget];
|
|
if (canBurn) max_available_speed *= [self afterburnerFactor];
|
|
|
|
if (range > desired_range || range == 0)
|
|
[shipAI message:@"REACHED_SAFETY"];
|
|
else
|
|
desired_speed = max_available_speed;
|
|
[self trackPrimaryTarget:delta_t:YES];
|
|
|
|
int missile_chance = 0;
|
|
int rhs = 3.2 / delta_t;
|
|
if (rhs) missile_chance = 1 + (ranrot_rand() % rhs);
|
|
|
|
if (([self hasEnergyBomb]) && (range < 10000.0) && canBurn)
|
|
{
|
|
float qbomb_chance = 0.01 * delta_t;
|
|
if (randf() < qbomb_chance)
|
|
{
|
|
[self launchEnergyBomb];
|
|
}
|
|
}
|
|
|
|
double hurt_factor = 16 * pow(energy/maxEnergy, 4.0);
|
|
if (([(ShipEntity *)[self primaryTarget] primaryTarget] == self)&&(missiles > missile_chance * hurt_factor))
|
|
[self fireMissile];
|
|
[self activateCloakingDevice];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_fly_range_from_destination:(double) delta_t
|
|
{
|
|
double distance = [self rangeToDestination];
|
|
if (distance < desired_range)
|
|
{
|
|
behaviour = BEHAVIOUR_FLY_FROM_DESTINATION;
|
|
desired_speed = maxFlightSpeed; // Not all AI define speed when flying away. Start with max speed to stay compatible with such AI's.
|
|
}
|
|
else
|
|
{
|
|
behaviour = BEHAVIOUR_FLY_TO_DESTINATION;
|
|
}
|
|
if (proximity_alert != NO_TARGET)
|
|
{
|
|
[self avoidCollision];
|
|
}
|
|
frustration = 0.0;
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_face_destination:(double) delta_t
|
|
{
|
|
double max_cos = 0.995;
|
|
double distance = [self rangeToDestination];
|
|
double old_pitch = flightPitch;
|
|
desired_speed = 0.0;
|
|
if (desired_range > 1.0 && distance > desired_range)
|
|
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)
|
|
else
|
|
max_cos = 0.995; // 0.995 - cos(5 degrees) is close enough
|
|
double confidenceFactor = [self trackDestination:delta_t:NO];
|
|
if (confidenceFactor >= max_cos)
|
|
{
|
|
// desired facing achieved
|
|
[shipAI message:@"FACING_DESTINATION"];
|
|
frustration = 0.0;
|
|
if(docking_match_rotation) // IDLE stops rotating while docking
|
|
{
|
|
behaviour = BEHAVIOUR_FLY_TO_DESTINATION;
|
|
}
|
|
else
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
}
|
|
}
|
|
|
|
if(flightSpeed == 0) frustration += delta_t;
|
|
if (frustration > 15.0 / max_flight_pitch) // allow more time for slow ships.
|
|
{
|
|
frustration = 0.0;
|
|
[shipAI reactToMessage:@"FRUSTRATED"];
|
|
if(flightPitch == old_pitch) flightPitch = 0.5 * max_flight_pitch; // hack to get out of frustration.
|
|
}
|
|
|
|
/* 2009-7-18 Eric: the condition check below is intended to eliminate the flippering between two positions for fast turning ships
|
|
during low FPS conditions. This flippering is particular frustrating on slow computers during docking. But with my current computer I can't
|
|
induce those low FPS conditions so I can't properly test if it helps.
|
|
I did try with the TAF time acceleration that also generated larger frame jumps and than it seemed to help.
|
|
*/
|
|
if(flightSpeed == 0 && frustration > 5 && confidenceFactor > 0.5 && ((flightPitch > 0 && old_pitch < 0) || (flightPitch < 0 && old_pitch > 0)))
|
|
{
|
|
flightPitch += 0.5 * old_pitch; // damping with last pitch value.
|
|
}
|
|
|
|
if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget))
|
|
[self avoidCollision];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_formation_form_up:(double) delta_t
|
|
{
|
|
// destination for each escort is set in update() from owner.
|
|
ShipEntity* leadShip = [self owner];
|
|
double distance = [self rangeToDestination];
|
|
double eta = (distance - desired_range) / flightSpeed;
|
|
if(eta < 0) eta = 0;
|
|
if ((eta < 5.0)&&(leadShip)&&(leadShip->isShip))
|
|
desired_speed = [leadShip flightSpeed] * (1 + eta * 0.05); // EW: This code works better, specialy at low speeds.
|
|
// desired_speed = [leadShip flightSpeed] * 1.25; // EW Original code, escorts always fly 25% to fast or drop speed 50% later on.
|
|
// this speed dropping does not work well at low speeds leading to escorts "waggling".
|
|
else
|
|
desired_speed = maxFlightSpeed;
|
|
|
|
double last_distance = success_factor;
|
|
success_factor = distance;
|
|
|
|
// do the actual piloting!!
|
|
[self trackDestination:delta_t: NO];
|
|
|
|
eta = eta / 0.51; // 2% safety margin assuming an average of half current speed
|
|
GLfloat slowdownTime = (thrust > 0.0)? flightSpeed / thrust : 4.0;
|
|
GLfloat minTurnSpeedFactor = 0.05 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds
|
|
|
|
if ((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor))
|
|
desired_speed = flightSpeed * 0.50; // cut speed by 50% to a minimum minTurnSpeedFactor of speed
|
|
|
|
if (distance < last_distance) // improvement
|
|
{
|
|
frustration -= 0.25 * delta_t;
|
|
if (frustration < 0.0)
|
|
frustration = 0.0;
|
|
}
|
|
else
|
|
{
|
|
frustration += delta_t;
|
|
if (frustration > 15.0)
|
|
{
|
|
if (!leadShip) [shipAI reactToMessage:@"FRUSTRATED"]; // escorts never reach their destination when following leader.
|
|
else if (distance > 0.5 * scannerRange)
|
|
{
|
|
flightPitch = max_flight_pitch; // hack to get out of frustration.
|
|
}
|
|
frustration = 0;
|
|
}
|
|
}
|
|
if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget))
|
|
[self avoidCollision];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_fly_to_destination:(double) delta_t
|
|
{
|
|
double distance = [self rangeToDestination];
|
|
if (distance < desired_range)// + collision_radius)
|
|
{
|
|
// desired range achieved
|
|
[shipAI message:@"DESIRED_RANGE_ACHIEVED"];
|
|
if(!docking_match_rotation) // IDLE stops rotating while docking
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
desired_speed = 0.0;
|
|
}
|
|
frustration = 0.0;
|
|
}
|
|
else
|
|
{
|
|
double last_distance = success_factor;
|
|
success_factor = distance;
|
|
|
|
// do the actual piloting!!
|
|
double confidenceFactor = [self trackDestination:delta_t: NO];
|
|
if(confidenceFactor < 0.2) confidenceFactor = 0.2; // don't allow small or negative values.
|
|
|
|
/* 2009-07-19 Eric: Estimated Time of Arrival (eta) should also take the "angle to target" into account (confidenceFactor = cos(angle to target))
|
|
and should not fuss about that last meter and use "distance + 1" instead of just "distance".
|
|
trackDestination already did pitch regulation, use confidence here only for cutting down to high speeds.
|
|
This should prevent ships crawling to their destination when they try to pull up close to their destination.
|
|
|
|
To prevent ships circling around their target without reaching destination I added a limitation based on turnrate,
|
|
speed and distance to target. Formula based on satelite orbit:
|
|
orbitspeed = turnrate (rad/sec) * radius (m) or flightSpeed = max_flight_pitch * 2 Pi * distance
|
|
Speed must be significant lower when not flying in direction of target (low confidenceFactor) or it can never approach its destination
|
|
and the ships runs the risk flying in circles around the target. (exclude active escorts)
|
|
*/
|
|
GLfloat eta = ((distance + 1) - desired_range) / (0.51 * flightSpeed * confidenceFactor); // 2% safety margin assuming an average of half current speed
|
|
GLfloat slowdownTime = (thrust > 0.0)? flightSpeed / thrust : 4.0;
|
|
GLfloat minTurnSpeedFactor = 0.05 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds
|
|
|
|
if (((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor)) || (flightSpeed > max_flight_pitch * 5 * confidenceFactor * distance))
|
|
desired_speed = flightSpeed * 0.50; // cut speed by 50% to a minimum minTurnSpeedFactor of speed
|
|
|
|
if (distance < last_distance) // improvement
|
|
{
|
|
frustration -= 0.25 * delta_t;
|
|
if (frustration < 0.0)
|
|
frustration = 0.0;
|
|
}
|
|
else
|
|
{
|
|
frustration += delta_t;
|
|
if ((frustration > slowdownTime * 10.0 && slowdownTime > 0)||(frustration > 15.0)) // 10x slowdownTime or 15s of frustration
|
|
{
|
|
[shipAI reactToMessage:@"FRUSTRATED"];
|
|
frustration -= slowdownTime * 5.0; //repeat after another five units of frustration
|
|
}
|
|
}
|
|
}
|
|
if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget))
|
|
[self avoidCollision];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_fly_from_destination:(double) delta_t
|
|
{
|
|
double distance = [self rangeToDestination];
|
|
if (distance > desired_range)
|
|
{
|
|
// desired range achieved
|
|
[shipAI message:@"DESIRED_RANGE_ACHIEVED"];
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
desired_speed = 0.0;
|
|
}
|
|
|
|
[self trackDestination:delta_t:YES];
|
|
if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget))
|
|
[self avoidCollision];
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_avoid_collision:(double) delta_t
|
|
{
|
|
double distance = [self rangeToDestination];
|
|
if (distance > desired_range)
|
|
{
|
|
[self resumePostProximityAlert];
|
|
}
|
|
else
|
|
{
|
|
ShipEntity* prox_ship = [self proximity_alert];
|
|
if (prox_ship)
|
|
{
|
|
desired_range = prox_ship->collision_radius * PROXIMITY_AVOID_DISTANCE_FACTOR;
|
|
destination = prox_ship->position;
|
|
}
|
|
double dq = [self trackDestination:delta_t:YES];
|
|
if (dq >= 0)
|
|
dq = 0.5 * dq + 0.5;
|
|
else
|
|
dq = 0.0;
|
|
desired_speed = maxFlightSpeed * dq;
|
|
}
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
[self applyThrust:delta_t];
|
|
}
|
|
|
|
|
|
- (void) behaviour_track_as_turret:(double) delta_t
|
|
{
|
|
double aim = [self ballTrackLeadingTarget:delta_t];
|
|
ShipEntity *turret_owner = (ShipEntity *)[self owner];
|
|
ShipEntity *turret_target = (ShipEntity *)[turret_owner primaryTarget];
|
|
|
|
if (turret_owner && turret_target && [turret_owner hasHostileTarget])
|
|
{
|
|
Vector p = vector_subtract([turret_target position], [turret_owner position]);
|
|
double cr = [turret_owner collisionRadius];
|
|
|
|
if (aim > .95)
|
|
{
|
|
[self fireTurretCannon:magnitude(p) - cr];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) behaviour_fly_thru_navpoints:(double) delta_t
|
|
{
|
|
int navpoint_plus_index = (next_navpoint_index + 1) % number_of_navpoints;
|
|
Vector d1 = navpoints[next_navpoint_index]; // head for this one
|
|
Vector d2 = navpoints[navpoint_plus_index]; // but be facing this one
|
|
|
|
Vector rel = vector_between(d1, position); // vector from d1 to position
|
|
Vector ref = vector_between(d2, d1); // vector from d2 to d1
|
|
ref = vector_normal(ref);
|
|
|
|
Vector xp = make_vector(ref.y * rel.z - ref.z * rel.y, ref.z * rel.x - ref.x * rel.z, ref.x * rel.y - ref.y * rel.x);
|
|
|
|
GLfloat v0 = 0.0;
|
|
|
|
GLfloat r0 = dot_product(rel, ref); // proportion of rel in direction ref
|
|
|
|
// if r0 is negative then we're the wrong side of things
|
|
|
|
GLfloat r1 = magnitude(xp); // distance of position from line
|
|
|
|
BOOL in_cone = (r0 > 0.5 * r1);
|
|
|
|
if (!in_cone) // are we in the approach cone ?
|
|
r1 = 25.0 * flightSpeed; // aim a few km out!
|
|
else
|
|
r1 *= 2.0;
|
|
|
|
GLfloat dist2 = magnitude2(rel);
|
|
|
|
if (dist2 < desired_range * desired_range)
|
|
{
|
|
// desired range achieved
|
|
[self doScriptEvent:@"shipReachedNavPoint" andReactToAIMessage:@"NAVPOINT_REACHED"];
|
|
if (navpoint_plus_index == 0)
|
|
{
|
|
[self doScriptEvent:@"shipReachedEndPoint" andReactToAIMessage:@"ENDPOINT_REACHED"];
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
}
|
|
next_navpoint_index = navpoint_plus_index; // loop as required
|
|
}
|
|
else
|
|
{
|
|
double last_success_factor = success_factor;
|
|
double last_dist2 = last_success_factor;
|
|
success_factor = dist2;
|
|
|
|
// set destination spline point from r1 and ref
|
|
destination = make_vector(d1.x + r1 * ref.x, d1.y + r1 * ref.y, d1.z + r1 * ref.z);
|
|
|
|
// do the actual piloting!!
|
|
//
|
|
// aim to within 1m
|
|
GLfloat temp = desired_range;
|
|
if (in_cone)
|
|
desired_range = 1.0;
|
|
else
|
|
desired_range = 100.0;
|
|
v0 = [self trackDestination:delta_t: NO];
|
|
desired_range = temp;
|
|
|
|
if (dist2 < last_dist2) // improvement
|
|
{
|
|
frustration -= 0.25 * delta_t;
|
|
if (frustration < 0.0)
|
|
frustration = 0.0;
|
|
}
|
|
else
|
|
{
|
|
frustration += delta_t;
|
|
if (frustration > 15.0) // 15s of frustration
|
|
{
|
|
[shipAI reactToMessage:@"FRUSTRATED"];
|
|
frustration -= 15.0; //repeat after another 15s of frustration
|
|
}
|
|
}
|
|
}
|
|
|
|
[self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch];
|
|
GLfloat temp = desired_speed;
|
|
desired_speed *= v0 * v0;
|
|
[self applyThrust:delta_t];
|
|
desired_speed = temp;
|
|
}
|
|
|
|
|
|
#if BALL_WORLD
|
|
- (void)drawEntity2:(BOOL)immediate :(BOOL)translucent
|
|
{
|
|
if (no_draw_distance < zero_distance)
|
|
{
|
|
// Don't draw.
|
|
return;
|
|
}
|
|
|
|
if ([UNIVERSE wireframeGraphics]) GLDebugWireframeModeOn();
|
|
|
|
OOPlanetDrawable *ball = [OOPlanetDrawable planetWithTextureName:@"oolite-planet-temp.png" radius:[[self mesh] collisionRadius]];
|
|
|
|
if (translucent) [ball renderTranslucentParts];
|
|
else [ball renderOpaqueParts];
|
|
|
|
if ([UNIVERSE wireframeGraphics]) GLDebugWireframeModeOff();
|
|
}
|
|
#endif
|
|
|
|
|
|
- (void)drawEntity:(BOOL)immediate :(BOOL)translucent
|
|
{
|
|
NSEnumerator *subEntityEnum = nil;
|
|
ShipEntity *subEntity = nil;
|
|
|
|
if ((no_draw_distance < zero_distance) || // Done redundantly to skip subentities
|
|
(cloaking_device_active && randf() > 0.10))
|
|
{
|
|
// Don't draw.
|
|
return;
|
|
}
|
|
|
|
// Draw self.
|
|
#if BALL_WORLD
|
|
[self drawEntity2:immediate :translucent];
|
|
#else
|
|
[super drawEntity:immediate :translucent];
|
|
#endif
|
|
|
|
#ifndef NDEBUG
|
|
// Draw bounding boxes if we have to before going for the subentities.
|
|
// TODO: the translucent flag here makes very little sense. Something's wrong with the matrices.
|
|
if (translucent) [self drawDebugStuff];
|
|
else if (gDebugFlags & DEBUG_BOUNDING_BOXES && ![self isSubEntity])
|
|
{
|
|
OODebugDrawBoundingBox([self boundingBox]);
|
|
OODebugDrawColoredBoundingBox(totalBoundingBox, [OOColor purpleColor]);
|
|
}
|
|
#endif
|
|
|
|
// Draw subentities.
|
|
if (!immediate) // TODO: is this relevant any longer?
|
|
{
|
|
for (subEntityEnum = [self subEntityEnumerator]; (subEntity = [subEntityEnum nextObject]); )
|
|
{
|
|
[subEntity setOwner:self]; // refresh ownership
|
|
[subEntity drawSubEntity:immediate :translucent];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#ifndef NDEBUG
|
|
- (void) drawDebugStuff
|
|
{
|
|
|
|
if (reportAIMessages)
|
|
{
|
|
OODebugDrawPoint(destination, [OOColor blueColor]);
|
|
OODebugDrawColoredLine([self position], destination, [OOColor colorWithCalibratedWhite:0.15 alpha:1.0]);
|
|
|
|
Entity *pTarget = [self primaryTarget];
|
|
if (pTarget != nil)
|
|
{
|
|
OODebugDrawPoint([pTarget position], [OOColor redColor]);
|
|
OODebugDrawColoredLine([self position], [pTarget position], [OOColor colorWithCalibratedRed:0.2 green:0.0 blue:0.0 alpha:1.0]);
|
|
}
|
|
|
|
Entity *sTarget = [UNIVERSE entityForUniversalID:targetStation];
|
|
if (sTarget != pTarget && [sTarget isStation])
|
|
{
|
|
OODebugDrawPoint([sTarget position], [OOColor cyanColor]);
|
|
}
|
|
|
|
Entity *fTarget = [UNIVERSE entityForUniversalID:found_target];
|
|
if (fTarget != nil && fTarget != pTarget && fTarget != sTarget)
|
|
{
|
|
OODebugDrawPoint([fTarget position], [OOColor magentaColor]);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
- (void) drawSubEntity:(BOOL) immediate :(BOOL) translucent
|
|
{
|
|
Entity* my_owner = [self owner];
|
|
if (my_owner)
|
|
{
|
|
// this test provides an opportunity to do simple LoD culling
|
|
zero_distance = [my_owner zeroDistance];
|
|
if (zero_distance > no_draw_distance)
|
|
{
|
|
return; // TOO FAR AWAY
|
|
}
|
|
}
|
|
|
|
if ([self status] == STATUS_ACTIVE)
|
|
{
|
|
Vector abspos = position; // STATUS_ACTIVE means it is in control of it's own orientation
|
|
Entity *last = nil;
|
|
Entity *father = my_owner;
|
|
OOMatrix r_mat;
|
|
|
|
while ((father)&&(father != last) &&father != NO_TARGET)
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
abspos = vector_add(OOVectorMultiplyMatrix(abspos, r_mat), [father position]);
|
|
last = father;
|
|
if (![last isSubEntity]) break;
|
|
father = [father owner];
|
|
}
|
|
|
|
GLLoadOOMatrix([UNIVERSE viewMatrix]);
|
|
OOGL(glPopMatrix());
|
|
OOGL(glPushMatrix());
|
|
GLTranslateOOVector(abspos);
|
|
GLMultOOMatrix(rotMatrix);
|
|
|
|
[self drawEntity:immediate :translucent];
|
|
}
|
|
else
|
|
{
|
|
OOGL(glPushMatrix());
|
|
|
|
GLTranslateOOVector(position);
|
|
GLMultOOMatrix(rotMatrix);
|
|
|
|
[self drawEntity:immediate :translucent];
|
|
|
|
OOGL(glPopMatrix());
|
|
}
|
|
|
|
#ifndef NDEBUG
|
|
if (gDebugFlags & DEBUG_BOUNDING_BOXES)
|
|
{
|
|
OODebugDrawBoundingBox([self boundingBox]);
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
static GLfloat cargo_color[4] = { 0.9, 0.9, 0.9, 1.0}; // gray
|
|
static GLfloat hostile_color[4] = { 1.0, 0.25, 0.0, 1.0}; // red/orange
|
|
static GLfloat neutral_color[4] = { 1.0, 1.0, 0.0, 1.0}; // yellow
|
|
static GLfloat friendly_color[4] = { 0.0, 1.0, 0.0, 1.0}; // green
|
|
static GLfloat missile_color[4] = { 0.0, 1.0, 1.0, 1.0}; // cyan
|
|
static GLfloat police_color1[4] = { 0.5, 0.0, 1.0, 1.0}; // purpley-blue
|
|
static GLfloat police_color2[4] = { 1.0, 0.0, 0.5, 1.0}; // purpley-red
|
|
static GLfloat jammed_color[4] = { 0.0, 0.0, 0.0, 0.0}; // clear black
|
|
static GLfloat mascem_color1[4] = { 0.3, 0.3, 0.3, 1.0}; // dark gray
|
|
static GLfloat mascem_color2[4] = { 0.4, 0.1, 0.4, 1.0}; // purple
|
|
static GLfloat scripted_color[4] = { 0.0, 0.0, 0.0, 0.0}; // to be defined by script
|
|
|
|
- (GLfloat *) scannerDisplayColorForShip:(ShipEntity*)otherShip :(BOOL)isHostile :(BOOL)flash :(OOColor *)scannerDisplayColor1 :(OOColor *)scannerDisplayColor2
|
|
{
|
|
// if there are any scripted scanner display colors for the ship, use them
|
|
if (scannerDisplayColor1 || scannerDisplayColor2)
|
|
{
|
|
if (scannerDisplayColor1 && !scannerDisplayColor2)
|
|
{
|
|
[scannerDisplayColor1 getGLRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
|
|
}
|
|
|
|
if (!scannerDisplayColor1 && scannerDisplayColor2)
|
|
{
|
|
[scannerDisplayColor2 getGLRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
|
|
}
|
|
|
|
if (scannerDisplayColor1 && scannerDisplayColor2)
|
|
{
|
|
if (flash)
|
|
[scannerDisplayColor1 getGLRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
|
|
else
|
|
[scannerDisplayColor2 getGLRed:&scripted_color[0] green:&scripted_color[1] blue:&scripted_color[2] alpha:&scripted_color[3]];
|
|
}
|
|
|
|
return scripted_color;
|
|
}
|
|
|
|
// no scripted scanner display colors defined, proceed as per standard
|
|
if ([self isJammingScanning])
|
|
{
|
|
if (![otherShip hasMilitaryScannerFilter])
|
|
return jammed_color;
|
|
else
|
|
{
|
|
if (flash)
|
|
return mascem_color1;
|
|
else
|
|
{
|
|
if (isHostile)
|
|
return hostile_color;
|
|
else
|
|
return mascem_color2;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (scanClass)
|
|
{
|
|
case CLASS_ROCK :
|
|
case CLASS_CARGO :
|
|
return cargo_color;
|
|
case CLASS_THARGOID :
|
|
if (flash)
|
|
return hostile_color;
|
|
else
|
|
return friendly_color;
|
|
case CLASS_MISSILE :
|
|
return missile_color;
|
|
case CLASS_STATION :
|
|
return friendly_color;
|
|
case CLASS_BUOY :
|
|
if (flash)
|
|
return friendly_color;
|
|
else
|
|
return neutral_color;
|
|
case CLASS_POLICE :
|
|
case CLASS_MILITARY :
|
|
if ((isHostile)&&(flash))
|
|
return police_color2;
|
|
else
|
|
return police_color1;
|
|
case CLASS_MINE :
|
|
if (flash)
|
|
return neutral_color;
|
|
else
|
|
return hostile_color;
|
|
default :
|
|
if (isHostile)
|
|
return hostile_color;
|
|
}
|
|
return neutral_color;
|
|
}
|
|
|
|
|
|
- (void)setScannerDisplayColor1:(OOColor *)color1
|
|
{
|
|
[scanner_display_color1 release];
|
|
scanner_display_color1 = nil;
|
|
if (color1)
|
|
{
|
|
scanner_display_color1 = [color1 retain];
|
|
}
|
|
}
|
|
|
|
|
|
- (void)setScannerDisplayColor2:(OOColor *)color2
|
|
{
|
|
[scanner_display_color2 release];
|
|
scanner_display_color2 = nil;
|
|
if (color2)
|
|
{
|
|
scanner_display_color2 = [color2 retain];
|
|
}
|
|
}
|
|
|
|
|
|
- (OOColor *)scannerDisplayColor1
|
|
{
|
|
return [[scanner_display_color1 retain] autorelease];
|
|
}
|
|
|
|
|
|
- (OOColor *)scannerDisplayColor2
|
|
{
|
|
return [[scanner_display_color2 retain] autorelease];
|
|
}
|
|
|
|
|
|
- (BOOL)isCloaked
|
|
{
|
|
return cloaking_device_active;
|
|
}
|
|
|
|
|
|
- (void)setCloaked:(BOOL)cloak
|
|
{
|
|
if (cloak) [self activateCloakingDevice];
|
|
else [self deactivateCloakingDevice];
|
|
}
|
|
|
|
|
|
- (BOOL) isJammingScanning
|
|
{
|
|
return ([self hasMilitaryJammer] && military_jammer_active);
|
|
}
|
|
|
|
|
|
- (void) addSubEntity:(Entity *)sub
|
|
{
|
|
if (sub == nil) return;
|
|
|
|
if (subEntities == nil) subEntities = [[NSMutableArray alloc] init];
|
|
sub->isSubEntity = YES;
|
|
// Order matters - need consistent state in setOwner:. -- Ahruman 2008-04-20
|
|
[subEntities addObject:sub];
|
|
[sub setOwner:self];
|
|
|
|
[self addSubentityToCollisionRadius:sub];
|
|
}
|
|
|
|
|
|
- (void) setOwner:(Entity *)who_owns_entity
|
|
{
|
|
[super setOwner:who_owns_entity];
|
|
|
|
/* Reset shader binding target so that bind-to-super works.
|
|
This is necessary since we don't know about the owner in
|
|
setUpShipFromDictionary:, when the mesh is initially set up.
|
|
-- Ahruman 2008-04-19
|
|
*/
|
|
OODrawable *drawable_ = [self drawable];
|
|
if (isSubEntity)
|
|
{
|
|
[drawable_ setBindingTarget:self];
|
|
}
|
|
}
|
|
|
|
|
|
- (void) applyThrust:(double) delta_t
|
|
{
|
|
GLfloat dt_thrust = thrust * delta_t;
|
|
BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
|
|
BOOL isUsingAfterburner = (canBurn && (flightSpeed > maxFlightSpeed) && (desired_speed >= flightSpeed));
|
|
float max_available_speed = maxFlightSpeed;
|
|
if (canBurn) max_available_speed *= [self afterburnerFactor];
|
|
|
|
position = vector_add(position, vector_multiply_scalar(velocity, delta_t));
|
|
|
|
if (thrust)
|
|
{
|
|
GLfloat velmag = magnitude(velocity);
|
|
if (velmag)
|
|
{
|
|
GLfloat vscale = fmaxf((velmag - dt_thrust) / velmag, 0.0f);
|
|
scale_vector(&velocity, vscale);
|
|
}
|
|
}
|
|
|
|
if (behaviour == BEHAVIOUR_TUMBLE) return;
|
|
|
|
// check for speed
|
|
if (desired_speed > max_available_speed)
|
|
desired_speed = max_available_speed;
|
|
|
|
if (flightSpeed > desired_speed)
|
|
{
|
|
[self decrease_flight_speed: dt_thrust];
|
|
if (flightSpeed < desired_speed) flightSpeed = desired_speed;
|
|
}
|
|
if (flightSpeed < desired_speed)
|
|
{
|
|
[self increase_flight_speed: dt_thrust];
|
|
if (flightSpeed > desired_speed) flightSpeed = desired_speed;
|
|
}
|
|
[self moveForward: delta_t*flightSpeed];
|
|
|
|
// burn fuel at the appropriate rate
|
|
if (isUsingAfterburner) // no fuelconsumption on slowdown
|
|
{
|
|
fuel_accumulator -= delta_t * AFTERBURNER_NPC_BURNRATE;
|
|
while (fuel_accumulator < 0.0)
|
|
{
|
|
fuel--;
|
|
fuel_accumulator += 1.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) orientationChanged
|
|
{
|
|
[super orientationChanged];
|
|
|
|
v_forward = vector_forward_from_quaternion(orientation);
|
|
v_up = vector_up_from_quaternion(orientation);
|
|
v_right = vector_right_from_quaternion(orientation);
|
|
}
|
|
|
|
|
|
- (void) applyRoll:(GLfloat) roll1 andClimb:(GLfloat) climb1
|
|
{
|
|
Quaternion q1 = kIdentityQuaternion;
|
|
|
|
if (!roll1 && !climb1 && !hasRotated) return;
|
|
|
|
if (roll1) quaternion_rotate_about_z(&q1, -roll1);
|
|
if (climb1) quaternion_rotate_about_x(&q1, -climb1);
|
|
|
|
orientation = quaternion_multiply(q1, orientation);
|
|
[self orientationChanged];
|
|
}
|
|
|
|
|
|
- (void) avoidCollision
|
|
{
|
|
if (scanClass == CLASS_MISSILE)
|
|
return; // missiles are SUPPOSED to collide!
|
|
|
|
ShipEntity* prox_ship = [self proximity_alert];
|
|
|
|
if (prox_ship)
|
|
{
|
|
if (previousCondition)
|
|
{
|
|
[previousCondition release];
|
|
previousCondition = nil;
|
|
}
|
|
|
|
previousCondition = [[NSMutableDictionary dictionaryWithCapacity:5] retain];
|
|
|
|
[previousCondition oo_setInteger:behaviour forKey:@"behaviour"];
|
|
[previousCondition oo_setInteger:primaryTarget forKey:@"primaryTarget"];
|
|
[previousCondition oo_setFloat:desired_range forKey:@"desired_range"];
|
|
[previousCondition oo_setFloat:desired_speed forKey:@"desired_speed"];
|
|
[previousCondition oo_setVector:destination forKey:@"destination"];
|
|
|
|
destination = [prox_ship position];
|
|
destination = OOVectorInterpolate(position, [prox_ship position], 0.5); // point between us and them
|
|
|
|
desired_range = prox_ship->collision_radius * PROXIMITY_AVOID_DISTANCE_FACTOR;
|
|
|
|
behaviour = BEHAVIOUR_AVOID_COLLISION;
|
|
}
|
|
}
|
|
|
|
|
|
- (void) resumePostProximityAlert
|
|
{
|
|
if (!previousCondition) return;
|
|
|
|
behaviour = [previousCondition oo_intForKey:@"behaviour"];
|
|
primaryTarget = [previousCondition oo_intForKey:@"primaryTarget"];
|
|
desired_range = [previousCondition oo_floatForKey:@"desired_range"];
|
|
desired_speed = [previousCondition oo_floatForKey:@"desired_speed"];
|
|
destination = [previousCondition oo_vectorForKey:@"destination"];
|
|
|
|
[previousCondition release];
|
|
previousCondition = nil;
|
|
frustration = 0.0;
|
|
|
|
proximity_alert = NO_TARGET;
|
|
|
|
//[shipAI message:@"RESTART_DOCKING"]; // if docking, start over, other AIs will ignore this message
|
|
}
|
|
|
|
|
|
- (double) messageTime
|
|
{
|
|
return messageTime;
|
|
}
|
|
|
|
|
|
- (void) setMessageTime:(double) value
|
|
{
|
|
messageTime = value;
|
|
}
|
|
|
|
|
|
- (OOShipGroup *) group
|
|
{
|
|
return _group;
|
|
}
|
|
|
|
|
|
- (void) setGroup:(OOShipGroup *)group
|
|
{
|
|
if (group != _group)
|
|
{
|
|
if (_escortGroup != _group)
|
|
{
|
|
if (self == [_group leader]) [_group setLeader:nil];
|
|
[_group removeShip:self];
|
|
}
|
|
[_group release];
|
|
[group addShip:self];
|
|
_group = [group retain];
|
|
}
|
|
}
|
|
|
|
|
|
- (OOShipGroup *) escortGroup
|
|
{
|
|
if (_escortGroup == nil)
|
|
{
|
|
_escortGroup = [[OOShipGroup alloc] initWithName:@"escort group"];
|
|
[_escortGroup setLeader:self];
|
|
}
|
|
|
|
return _escortGroup;
|
|
}
|
|
|
|
|
|
#ifndef NDEBUG
|
|
- (OOShipGroup *) rawEscortGroup
|
|
{
|
|
return _escortGroup;
|
|
}
|
|
#endif
|
|
|
|
|
|
- (OOShipGroup *) stationGroup
|
|
{
|
|
if (_group == nil)
|
|
{
|
|
_group = [[OOShipGroup alloc] initWithName:@"station group"];
|
|
[_group setLeader:self];
|
|
}
|
|
|
|
return _group;
|
|
}
|
|
|
|
|
|
- (BOOL) hasEscorts
|
|
{
|
|
if (_escortGroup == nil) return NO;
|
|
return [_escortGroup count] > 1; // If only one memeber, it's self.
|
|
}
|
|
|
|
|
|
- (NSEnumerator *) escortEnumerator
|
|
{
|
|
if (_escortGroup == nil) return [[NSArray array] objectEnumerator];
|
|
return [[_escortGroup objectEnumerator] ooExcludingObject:self];
|
|
}
|
|
|
|
|
|
- (NSArray *) escortArray
|
|
{
|
|
if (_escortGroup == nil) return [NSArray array];
|
|
return [[self escortEnumerator] allObjects];
|
|
}
|
|
|
|
|
|
- (uint8_t) escortCount
|
|
{
|
|
if (_escortGroup == nil) return 0;
|
|
return [_escortGroup count] - 1;
|
|
}
|
|
|
|
|
|
- (uint8_t) pendingEscortCount
|
|
{
|
|
return _pendingEscortCount;
|
|
}
|
|
|
|
|
|
- (void) setPendingEscortCount:(uint8_t)count
|
|
{
|
|
_pendingEscortCount = count;
|
|
}
|
|
|
|
|
|
- (ShipEntity*) proximity_alert
|
|
{
|
|
return [UNIVERSE entityForUniversalID:proximity_alert];
|
|
}
|
|
|
|
|
|
- (void) setProximity_alert:(ShipEntity*) other
|
|
{
|
|
if (!other)
|
|
{
|
|
proximity_alert = NO_TARGET;
|
|
return;
|
|
}
|
|
|
|
if (isStation || (other->isStation)) // don't be alarmed close to stations -- is this sensible? we dont mind crashing into carriers?
|
|
return;
|
|
|
|
if ((scanClass == CLASS_CARGO)||(scanClass == CLASS_BUOY)||(scanClass == CLASS_MISSILE)||(scanClass == CLASS_ROCK)) // rocks and stuff don't get alarmed easily
|
|
return;
|
|
|
|
// check vectors
|
|
Vector vdiff = vector_between(position, other->position);
|
|
GLfloat d_forward = dot_product(vdiff, v_forward);
|
|
GLfloat d_up = dot_product(vdiff, v_up);
|
|
GLfloat d_right = dot_product(vdiff, v_right);
|
|
if ((d_forward > 0.0)&&(flightSpeed > 0.0)) // it's ahead of us and we're moving forward
|
|
d_forward *= 0.25 * maxFlightSpeed / flightSpeed; // extend the collision zone forward up to 400%
|
|
double d2 = d_forward * d_forward + d_up * d_up + d_right * d_right;
|
|
double cr2 = collision_radius * 2.0 + other->collision_radius; cr2 *= cr2; // check with twice the combined radius
|
|
|
|
if (d2 > cr2) // we're okay
|
|
return;
|
|
|
|
if (behaviour == BEHAVIOUR_AVOID_COLLISION) // already avoiding something
|
|
{
|
|
ShipEntity* prox = [UNIVERSE entityForUniversalID:proximity_alert];
|
|
if ((prox)&&(prox != other))
|
|
{
|
|
// check which subtends the greatest angle
|
|
GLfloat sa_prox = prox->collision_radius * prox->collision_radius / distance2(position, prox->position);
|
|
GLfloat sa_other = other->collision_radius * other->collision_radius / distance2(position, other->position);
|
|
if (sa_prox < sa_other) return;
|
|
}
|
|
}
|
|
proximity_alert = [other universalID];
|
|
}
|
|
|
|
|
|
- (NSString *) name
|
|
{
|
|
return name;
|
|
}
|
|
|
|
|
|
- (NSString *) displayName
|
|
{
|
|
if (displayName == nil) return name;
|
|
return displayName;
|
|
}
|
|
|
|
|
|
- (void) setName:(NSString *)inName
|
|
{
|
|
[name release];
|
|
name = [inName copy];
|
|
}
|
|
|
|
|
|
- (void) setDisplayName:(NSString *)inName
|
|
{
|
|
[displayName release];
|
|
displayName = [inName copy];
|
|
}
|
|
|
|
|
|
- (NSString *) identFromShip:(ShipEntity*) otherShip
|
|
{
|
|
if ([self isJammingScanning] && ![otherShip hasMilitaryScannerFilter])
|
|
{
|
|
return DESC(@"unknown-target");
|
|
}
|
|
return displayName;
|
|
}
|
|
|
|
|
|
- (BOOL) hasRole:(NSString *)role
|
|
{
|
|
return [roleSet hasRole:role] || [role isEqual:primaryRole];
|
|
}
|
|
|
|
|
|
- (OORoleSet *)roleSet
|
|
{
|
|
if (roleSet == nil) roleSet = [[OORoleSet alloc] initWithRoleString:primaryRole];
|
|
return [roleSet roleSetWithAddedRoleIfNotSet:primaryRole probability:1.0];
|
|
}
|
|
|
|
|
|
- (NSString *)primaryRole
|
|
{
|
|
if (primaryRole == nil)
|
|
{
|
|
primaryRole = [roleSet anyRole];
|
|
if (primaryRole == nil) primaryRole = @"trader";
|
|
[primaryRole retain];
|
|
OOLog(@"ship.noPrimaryRole", @"%@ had no primary role, randomly selected \"%@\".", [self name], primaryRole);
|
|
}
|
|
|
|
return primaryRole;
|
|
}
|
|
|
|
|
|
// Exposed to AI.
|
|
- (void)setPrimaryRole:(NSString *)role
|
|
{
|
|
if (![role isEqual:primaryRole])
|
|
{
|
|
[primaryRole release];
|
|
primaryRole = [role copy];
|
|
}
|
|
}
|
|
|
|
|
|
- (BOOL)hasPrimaryRole:(NSString *)role
|
|
{
|
|
return [[self primaryRole] isEqual:role];
|
|
}
|
|
|
|
|
|
- (BOOL)isPolice
|
|
{
|
|
//bounty hunters have a police role, but are not police, so we must test by scan class, not by role
|
|
return [self scanClass] == CLASS_POLICE;
|
|
}
|
|
|
|
- (BOOL)isThargoid
|
|
{
|
|
return [self scanClass] == CLASS_THARGOID;
|
|
}
|
|
|
|
|
|
- (BOOL)isTrader
|
|
{
|
|
return isPlayer || [self hasPrimaryRole:@"trader"];
|
|
}
|
|
|
|
|
|
- (BOOL)isPirate
|
|
{
|
|
return [self hasPrimaryRole:@"pirate"];
|
|
}
|
|
|
|
|
|
- (BOOL)isMissile
|
|
{
|
|
return ([[self primaryRole] hasSuffix:@"MISSILE"] || [self hasPrimaryRole:@"missile"]);
|
|
}
|
|
|
|
|
|
- (BOOL)isMine
|
|
{
|
|
return [[self primaryRole] hasSuffix:@"MINE"];
|
|
}
|
|
|
|
|
|
- (BOOL)isWeapon
|
|
{
|
|
return [self isMissile] || [self isMine];
|
|
}
|
|
|
|
|
|
- (BOOL)isEscort
|
|
{
|
|
return [self hasPrimaryRole:@"escort"] || [self hasPrimaryRole:@"wingman"];
|
|
}
|
|
|
|
|
|
- (BOOL)isShuttle
|
|
{
|
|
return [self hasPrimaryRole:@"shuttle"];
|
|
}
|
|
|
|
|
|
- (BOOL)isPirateVictim
|
|
{
|
|
return [UNIVERSE roleIsPirateVictim:[self primaryRole]];
|
|
}
|
|
|
|
|
|
- (BOOL)isUnpiloted
|
|
{
|
|
return isUnpiloted;
|
|
}
|
|
|
|
|
|
// Exposed to shaders.
|
|
- (BOOL) hasHostileTarget
|
|
{
|
|
if (primaryTarget == NO_TARGET)
|
|
return NO;
|
|
if ([self isMissile])
|
|
return YES; // missiles are always fired against a hostile target
|
|
if ((behaviour == BEHAVIOUR_AVOID_COLLISION)&&(previousCondition))
|
|
{
|
|
int old_behaviour = [previousCondition oo_intForKey:@"behaviour"];
|
|
return IsBehaviourHostile(old_behaviour);
|
|
}
|
|
return IsBehaviourHostile(behaviour);
|
|
}
|
|
|
|
|
|
- (GLfloat) weaponRange
|
|
{
|
|
return weaponRange;
|
|
}
|
|
|
|
|
|
- (void) setWeaponRange: (GLfloat) value
|
|
{
|
|
weaponRange = value;
|
|
}
|
|
|
|
|
|
- (void) setWeaponDataFromType: (OOWeaponType) weapon_type
|
|
{
|
|
switch (weapon_type)
|
|
{
|
|
case WEAPON_PLASMA_CANNON:
|
|
weapon_energy = 6.0;
|
|
weapon_recharge_rate = 0.25;
|
|
weaponRange = 5000;
|
|
break;
|
|
case WEAPON_PULSE_LASER:
|
|
weapon_energy = 15.0;
|
|
weapon_recharge_rate = 0.33;
|
|
weaponRange = 12500;
|
|
break;
|
|
case WEAPON_BEAM_LASER:
|
|
weapon_energy = 15.0;
|
|
weapon_recharge_rate = 0.25;
|
|
weaponRange = 15000;
|
|
break;
|
|
case WEAPON_MINING_LASER:
|
|
weapon_energy = 50.0;
|
|
weapon_recharge_rate = 0.5;
|
|
weaponRange = 12500;
|
|
break;
|
|
case WEAPON_THARGOID_LASER: // omni directional lasers FRIGHTENING!
|
|
weapon_energy = 12.5;
|
|
weapon_recharge_rate = 0.5;
|
|
weaponRange = 17500;
|
|
break;
|
|
case WEAPON_MILITARY_LASER:
|
|
weapon_energy = 23.0;
|
|
weapon_recharge_rate = 0.20;
|
|
weaponRange = 30000;
|
|
break;
|
|
case WEAPON_NONE:
|
|
case WEAPON_UNDEFINED:
|
|
weapon_energy = 0.0; // indicating no weapon!
|
|
weapon_recharge_rate = 0.20; // maximum rate
|
|
weaponRange = 32000;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
- (float) weaponRechargeRate
|
|
{
|
|
return weapon_recharge_rate;
|
|
}
|
|
|
|
|
|
- (void) setWeaponRechargeRate:(float)value
|
|
{
|
|
weapon_recharge_rate = value;
|
|
}
|
|
|
|
|
|
- (GLfloat) scannerRange
|
|
{
|
|
return scannerRange;
|
|
}
|
|
|
|
|
|
- (void) setScannerRange: (GLfloat) value
|
|
{
|
|
scannerRange = value;
|
|
}
|
|
|
|
|
|
- (Vector) reference
|
|
{
|
|
return reference;
|
|
}
|
|
|
|
|
|
- (void) setReference:(Vector) v
|
|
{
|
|
reference = v;
|
|
}
|
|
|
|
|
|
- (BOOL) reportAIMessages
|
|
{
|
|
return reportAIMessages;
|
|
}
|
|
|
|
|
|
- (void) setReportAIMessages:(BOOL) yn
|
|
{
|
|
reportAIMessages = yn;
|
|
}
|
|
|
|
|
|
- (void) transitionToAegisNone
|
|
{
|
|
if (!suppressAegisMessages && aegis_status != AEGIS_NONE)
|
|
{
|
|
Entity<OOStellarBody> *lastAegisLock = [self lastAegisLock];
|
|
if (lastAegisLock != nil)
|
|
{
|
|
[self doScriptEvent:@"shipExitedPlanetaryVicinity" withArgument:lastAegisLock];
|
|
|
|
if (lastAegisLock == [UNIVERSE sun])
|
|
{
|
|
[shipAI message:@"AWAY_FROM_SUN"];
|
|
}
|
|
else
|
|
{
|
|
[shipAI message:@"AWAY_FROM_PLANET"];
|
|
}
|
|
[self setLastAegisLock:nil];
|
|
}
|
|
|
|
if (aegis_status != AEGIS_CLOSE_TO_ANY_PLANET)
|
|
{
|
|
[shipAI message:@"AEGIS_NONE"];
|
|
}
|
|
}
|
|
aegis_status = AEGIS_NONE;
|
|
}
|
|
|
|
|
|
static float SurfaceDistanceSqaredV(Vector reference, Entity<OOStellarBody> *stellar)
|
|
{
|
|
float centerDistance = magnitude2(vector_subtract([stellar position], reference));
|
|
float r = [(OOPlanetEntity *)stellar radius];
|
|
|
|
/* 1.35: empirical value used to help determine proximity when non-nested
|
|
planets are close to each other
|
|
*/
|
|
return centerDistance - 1.35 * r * r;
|
|
}
|
|
|
|
|
|
static float SurfaceDistanceSqared(Entity *reference, Entity<OOStellarBody> *stellar)
|
|
{
|
|
return SurfaceDistanceSqaredV([reference position], stellar);
|
|
}
|
|
|
|
|
|
NSComparisonResult ComparePlanetsBySurfaceDistance(id i1, id i2, void* context)
|
|
{
|
|
Vector p = [(ShipEntity*) context position];
|
|
OOPlanetEntity* e1 = i1;
|
|
OOPlanetEntity* e2 = i2;
|
|
|
|
#if OBSOLETE
|
|
//fx: empirical value used to help determine proximity when non-nested planets are close to each other
|
|
float fx=1.35;
|
|
float r;
|
|
|
|
float p1 = magnitude2(vector_subtract([e1 position], p));
|
|
float p2 = magnitude2(vector_subtract([e2 position], p));
|
|
r = [e1 radius];
|
|
p1 -= fx*r*r;
|
|
r = [e2 radius];
|
|
p2 -= fx*r*r;
|
|
#else
|
|
float p1 = SurfaceDistanceSqaredV(p, e1);
|
|
float p2 = SurfaceDistanceSqaredV(p, e2);
|
|
#endif
|
|
|
|
if (p1 < p2) return NSOrderedAscending;
|
|
if (p1 > p2) return NSOrderedDescending;
|
|
|
|
return NSOrderedSame;
|
|
}
|
|
|
|
|
|
- (OOPlanetEntity *) findNearestPlanet
|
|
{
|
|
OOPlanetEntity *result = nil;
|
|
NSArray *planets = nil;
|
|
|
|
planets = [UNIVERSE planets];
|
|
if ([planets count] == 0) return nil;
|
|
|
|
planets = [planets sortedArrayUsingFunction:ComparePlanetsBySurfaceDistance context:self];
|
|
result = [planets objectAtIndex:0];
|
|
|
|
// ignore miniature planets when determining nearest planet - Nikos 20090313
|
|
if ([result planetType] == STELLAR_TYPE_MINIATURE)
|
|
{
|
|
if ([UNIVERSE sun]) // if we are not in witchspace give us the next in the list, else nothing
|
|
{
|
|
result = [planets objectAtIndex:1];
|
|
}
|
|
else
|
|
{
|
|
result = nil;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
- (Entity<OOStellarBody> *) findNearestStellarBody
|
|
{
|
|
Entity<OOStellarBody> *match = [self findNearestPlanet];
|
|
OOSunEntity *sun = [UNIVERSE sun];
|
|
|
|
if (sun != nil)
|
|
{
|
|
if (match == nil ||
|
|
SurfaceDistanceSqared(self, sun) < SurfaceDistanceSqared(self, match))
|
|
{
|
|
match = sun;
|
|
}
|
|
}
|
|
|
|
return match;
|
|
}
|
|
|
|
|
|
- (OOPlanetEntity *) findNearestPlanetExcludingMoons
|
|
{
|
|
OOPlanetEntity *result = nil;
|
|
OOPlanetEntity *planet = nil;
|
|
NSArray *bodies = nil;
|
|
NSArray *planets = nil;
|
|
unsigned i;
|
|
|
|
bodies = [UNIVERSE planets];
|
|
planets = [NSMutableArray arrayWithCapacity:[bodies count]];
|
|
|
|
for (i=0; i < [bodies count]; i++)
|
|
{
|
|
planet = [bodies objectAtIndex:i];
|
|
if([planet planetType] == STELLAR_TYPE_NORMAL_PLANET)
|
|
planets = [planets arrayByAddingObject:planet];
|
|
}
|
|
|
|
if ([planets count] == 0) return nil;
|
|
|
|
planets = [planets sortedArrayUsingFunction:ComparePlanetsBySurfaceDistance context:self];
|
|
result = [planets objectAtIndex:0];
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
- (OOAegisStatus) checkForAegis
|
|
{
|
|
Entity<OOStellarBody> *nearest = [self findNearestStellarBody];
|
|
BOOL sunGoneNova = [[UNIVERSE sun] goneNova];
|
|
|
|
if (nearest == nil)
|
|
{
|
|
if (aegis_status != AEGIS_NONE)
|
|
{
|
|
// Planet disappeared!
|
|
[self transitionToAegisNone];
|
|
}
|
|
return AEGIS_NONE;
|
|
}
|
|
|
|
// check planet
|
|
float cr = [nearest radius];
|
|
float cr2 = cr * cr;
|
|
OOAegisStatus result = AEGIS_NONE;
|
|
float d2;
|
|
|
|
d2 = magnitude2(vector_subtract([nearest position], [self position]));
|
|
|
|
// check if nearing surface
|
|
BOOL wasNearPlanetSurface = isNearPlanetSurface;
|
|
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
|
|
if (!suppressAegisMessages)
|
|
{
|
|
if (!wasNearPlanetSurface && isNearPlanetSurface)
|
|
{
|
|
[self doScriptEvent:@"shipApproachingPlanetSurface" withArgument:nearest];
|
|
[shipAI reactToMessage:@"APPROACHING_SURFACE"];
|
|
}
|
|
if (wasNearPlanetSurface && !isNearPlanetSurface)
|
|
{
|
|
[self doScriptEvent:@"shipLeavingPlanetSurface" withArgument:nearest];
|
|
[shipAI reactToMessage:@"LEAVING_SURFACE"];
|
|
}
|
|
}
|
|
|
|
if (d2 < cr2 * 9.0f) //to 3x radius of any planet/moon
|
|
{
|
|
result = AEGIS_CLOSE_TO_ANY_PLANET;
|
|
}
|
|
|
|
if (!sunGoneNova)
|
|
{
|
|
d2 = magnitude2(vector_subtract([[UNIVERSE planet] position], [self position]));
|
|
cr2 = [[UNIVERSE planet] radius];
|
|
cr2 *= cr2;
|
|
if (d2 < cr2 * 9.0f) // to 3x radius of main planet
|
|
{
|
|
result = AEGIS_CLOSE_TO_MAIN_PLANET;
|
|
}
|
|
}
|
|
|
|
// check station
|
|
StationEntity *the_station = [UNIVERSE station];
|
|
if (the_station)
|
|
{
|
|
d2 = magnitude2(vector_subtract([the_station position], [self position]));
|
|
if (d2 < SCANNER_MAX_RANGE2 * 4.0f) // double scanner range
|
|
{
|
|
result = AEGIS_IN_DOCKING_RANGE;
|
|
}
|
|
}
|
|
|
|
/* Rewrote aegis stuff and tested it against redux.oxp that adds multiple planets and moons.
|
|
Made sure AI scripts can differentiate between MAIN and NON-MAIN planets so they can decide
|
|
if they can dock at the systemStation or just any station.
|
|
Added sun detection so route2Patrol can turn before they heat up in the sun.
|
|
-- Eric 2009-07-11
|
|
*/
|
|
if (!suppressAegisMessages)
|
|
{
|
|
// script/AI messages on change in status
|
|
// approaching..
|
|
if ((aegis_status == AEGIS_NONE || aegis_status == AEGIS_CLOSE_TO_ANY_PLANET)&&(result == AEGIS_CLOSE_TO_MAIN_PLANET))
|
|
{
|
|
if(aegis_status == AEGIS_CLOSE_TO_ANY_PLANET)
|
|
{
|
|
[self doScriptEvent:@"shipExitedPlanetaryVicinity" withArgument:[self lastAegisLock]];
|
|
[shipAI message:@"AWAY_FROM_PLANET"]; // fires for all planets and moons.
|
|
}
|
|
[self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:[UNIVERSE planet]];
|
|
[self setLastAegisLock:[UNIVERSE planet]];
|
|
[shipAI message:@"CLOSE_TO_PLANET"]; // fires for all planets and moons.
|
|
[shipAI message:@"AEGIS_CLOSE_TO_PLANET"]; // fires only for main planets, keep for compatibility with pre-1.72 AI plists.
|
|
[shipAI message:@"AEGIS_CLOSE_TO_MAIN_PLANET"]; // fires only for main planet.
|
|
}
|
|
if ((aegis_status == AEGIS_NONE || aegis_status == AEGIS_CLOSE_TO_MAIN_PLANET)&&(result == AEGIS_CLOSE_TO_ANY_PLANET))
|
|
{
|
|
if(aegis_status == AEGIS_CLOSE_TO_MAIN_PLANET)
|
|
{
|
|
[self doScriptEvent:@"shipExitedPlanetaryVicinity" withArgument:[UNIVERSE planet]];
|
|
[shipAI message:@"AWAY_FROM_PLANET"];
|
|
}
|
|
[self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:nearest];
|
|
[self setLastAegisLock:nearest];
|
|
if([nearest isSun])
|
|
{
|
|
[shipAI message:@"CLOSE_TO_SUN"];
|
|
}
|
|
else
|
|
{
|
|
[shipAI message:@"CLOSE_TO_PLANET"];
|
|
if ([nearest planetType] == STELLAR_TYPE_MOON)
|
|
{
|
|
[shipAI message:@"CLOSE_TO_MOON"];
|
|
}
|
|
else
|
|
{
|
|
[shipAI message:@"CLOSE_TO_SECONDARY_PLANET"];
|
|
}
|
|
}
|
|
}
|
|
if ((aegis_status != AEGIS_IN_DOCKING_RANGE)&&(result == AEGIS_IN_DOCKING_RANGE))
|
|
{
|
|
[self doScriptEvent:@"shipEnteredStationAegis" withArgument:the_station];
|
|
[shipAI message:@"AEGIS_IN_DOCKING_RANGE"];
|
|
|
|
if([self lastAegisLock] == nil && !sunGoneNova) // With small main planets the station aegis can come before planet aegis
|
|
{
|
|
[self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:[UNIVERSE planet]];
|
|
[self setLastAegisLock:[UNIVERSE planet]];
|
|
}
|
|
}
|
|
// leaving..
|
|
if ((aegis_status == AEGIS_IN_DOCKING_RANGE)&&(result != AEGIS_IN_DOCKING_RANGE))
|
|
{
|
|
[self doScriptEvent:@"shipExitedStationAegis" withArgument:the_station];
|
|
[shipAI message:@"AEGIS_LEAVING_DOCKING_RANGE"];
|
|
}
|
|
if ((aegis_status != AEGIS_NONE)&&(result == AEGIS_NONE))
|
|
{
|
|
if([self lastAegisLock] == nil && !sunGoneNova)
|
|
{
|
|
[self setLastAegisLock:[UNIVERSE planet]]; // in case of a first launch.
|
|
}
|
|
[self transitionToAegisNone];
|
|
}
|
|
}
|
|
|
|
aegis_status = result; // put this here
|
|
return result;
|
|
}
|
|
|
|
|
|
- (BOOL) withinStationAegis
|
|
{
|
|
return aegis_status == AEGIS_IN_DOCKING_RANGE;
|
|
}
|
|
|
|
|
|
- (Entity<OOStellarBody> *) lastAegisLock
|
|
{
|
|
Entity<OOStellarBody> *stellar = [_lastAegisLock weakRefUnderlyingObject];
|
|
if (stellar == nil)
|
|
{
|
|
[_lastAegisLock release];
|
|
_lastAegisLock = nil;
|
|
}
|
|
|
|
return stellar;
|
|
}
|
|
|
|
|
|
- (void) setLastAegisLock:(Entity<OOStellarBody> *)lastAegisLock
|
|
{
|
|
[_lastAegisLock release];
|
|
_lastAegisLock = [lastAegisLock weakRetain];
|
|
}
|
|
|
|
|
|
- (void) setStatus:(OOEntityStatus) stat
|
|
{
|
|
if ([self status] == stat) return;
|
|
[super setStatus:stat];
|
|
if (stat == STATUS_LAUNCHING)
|
|
{
|
|
launch_time = [UNIVERSE getTime];
|
|
}
|
|
}
|
|
|
|
|
|
- (NSArray*) crew
|
|
{
|
|
return crew;
|
|
}
|
|
|
|
|
|
- (void) setCrew: (NSArray*) crewArray
|
|
{
|
|
if (isUnpiloted)
|
|
{
|
|
//unpiloted ships cannot have crew
|
|
return;
|
|
}
|
|
//do not set to hulk here when crew is nill (or 0). Some things like missiles have no crew.
|
|
if (crew != nil) [crew autorelease];
|
|
crew = [crewArray copy];
|
|
}
|
|
|
|
|
|
- (void) setStateMachine:(NSString *) ai_desc
|
|
{
|
|
[shipAI setStateMachine: ai_desc];
|
|
}
|
|
|
|
|
|
- (void) setAI:(AI *) ai
|
|
{
|
|
[ai retain];
|
|
if (shipAI)
|
|
{
|
|
[shipAI clearAllData];
|
|
[shipAI autorelease];
|
|
}
|
|
shipAI = ai;
|
|
}
|
|
|
|
|
|
- (AI *) getAI
|
|
{
|
|
return shipAI;
|
|
}
|
|
|
|
|
|
- (void) setShipScript:(NSString *)script_name
|
|
{
|
|
NSMutableDictionary *properties = nil;
|
|
NSArray *actions = nil;
|
|
|
|
properties = [NSMutableDictionary dictionary];
|
|
[properties setObject:self forKey:@"ship"];
|
|
|
|
[script autorelease];
|
|
script = [OOScript nonLegacyScriptFromFileNamed:script_name properties:properties];
|
|
|
|
if (script == nil)
|
|
{
|
|
actions = [shipinfoDictionary oo_arrayForKey:@"launch_actions"];
|
|
if (actions) [properties setObject:actions forKey:@"legacy_launchActions"];
|
|
actions = [shipinfoDictionary oo_arrayForKey:@"script_actions"];
|
|
if (actions) [properties setObject:actions forKey:@"legacy_scriptActions"];
|
|
actions = [shipinfoDictionary oo_arrayForKey:@"death_actions"];
|
|
if (actions) [properties setObject:actions forKey:@"legacy_deathActions"];
|
|
actions = [shipinfoDictionary oo_arrayForKey:@"setup_actions"];
|
|
if (actions) [properties setObject:actions forKey:@"legacy_setupActions"];
|
|
|
|
script = [OOScript nonLegacyScriptFromFileNamed:@"oolite-default-ship-script.js"
|
|
properties:properties];
|
|
}
|
|
[script retain];
|
|
}
|
|
|
|
|
|
- (double)frustration
|
|
{
|
|
return frustration;
|
|
}
|
|
|
|
|
|
- (OOFuelQuantity) fuel
|
|
{
|
|
return fuel;
|
|
}
|
|
|
|
|
|
- (void) setFuel:(OOFuelQuantity) amount
|
|
{
|
|
if (amount > [self fuelCapacity]) amount = [self fuelCapacity];
|
|
|
|
fuel = amount;
|
|
}
|
|
|
|
|
|
- (OOFuelQuantity) fuelCapacity
|
|
{
|
|
// FIXME: shipdata.plist can allow greater fuel quantities (without extending hyperspace range). Need some consistency here.
|
|
return PLAYER_MAX_FUEL;
|
|
}
|
|
|
|
|
|
- (void) setRoll:(double) amount
|
|
{
|
|
flightRoll = amount * M_PI / 2.0;
|
|
}
|
|
|
|
|
|
- (void) setPitch:(double) amount
|
|
{
|
|
flightPitch = amount * M_PI / 2.0;
|
|
}
|
|
|
|
|
|
- (void) setThrust:(double) amount
|
|
{
|
|
thrust = amount;
|
|
}
|
|
|
|
|
|
- (void) setThrustForDemo:(float) factor
|
|
{
|
|
flightSpeed = factor * maxFlightSpeed;
|
|
}
|
|
|
|
|
|
- (void) setBounty:(OOCreditsQuantity) amount
|
|
{
|
|
bounty = amount;
|
|
}
|
|
|
|
|
|
- (OOCreditsQuantity) bounty
|
|
{
|
|
return bounty;
|
|
}
|
|
|
|
|
|
- (int) legalStatus
|
|
{
|
|
if (scanClass == CLASS_THARGOID)
|
|
return 5 * collision_radius;
|
|
if (scanClass == CLASS_ROCK)
|
|
return 0;
|
|
return bounty;
|
|
}
|
|
|
|
|
|
- (void) setCommodity:(OOCargoType)co_type andAmount:(OOCargoQuantity)co_amount
|
|
{
|
|
if (co_type != CARGO_UNDEFINED)
|
|
{
|
|
commodity_type = co_type;
|
|
commodity_amount = co_amount;
|
|
}
|
|
}
|
|
|
|
|
|
- (OOCargoType) commodityType
|
|
{
|
|
return commodity_type;
|
|
}
|
|
|
|
|
|
- (OOCargoQuantity) commodityAmount
|
|
{
|
|
return commodity_amount;
|
|
}
|
|
|
|
|
|
- (OOCargoQuantity) maxCargo
|
|
{
|
|
return max_cargo;
|
|
}
|
|
|
|
|
|
- (OOCargoQuantity) availableCargoSpace
|
|
{
|
|
return [self maxCargo] - [self cargoQuantityOnBoard];
|
|
}
|
|
|
|
|
|
- (OOCargoQuantity) cargoQuantityOnBoard
|
|
{
|
|
OOCargoQuantity cargoQtyOnBoard = 0;
|
|
int i;
|
|
|
|
if ([self isPlayer] && ([(PlayerEntity*)self specialCargo] != nil))
|
|
{
|
|
return [self maxCargo];
|
|
}
|
|
|
|
if ([self isPlayer]) // always use this method to avoid inconsistencies - Kaks 20091002
|
|
{
|
|
/*
|
|
The cargo array is nil when the player ship is docked, due to action in unloadCargopods. For
|
|
this reason, we must use a slightly more complex method to determine the quantity of cargo
|
|
carried in this case - Nikos 20090830
|
|
|
|
Optimised this method, to compensate for increased usage - Kaks 20091002
|
|
*/
|
|
PlayerEntity* player = [PlayerEntity sharedPlayer];
|
|
NSArray* manifest = [NSArray arrayWithArray:[player shipCommodityData]];
|
|
OOMassUnit commodityUnits = UNITS_TONS;
|
|
OOCargoQuantity quantity = 0;
|
|
for (i = [manifest count] - 1; i >= 0 ; i--)
|
|
{
|
|
NSArray* commodityInfo = [NSArray arrayWithArray:(NSArray *)[manifest objectAtIndex:i]];
|
|
quantity = [commodityInfo oo_intAtIndex:MARKET_QUANTITY];
|
|
// manifest contains entries for all 17 commodities, whether their quantity is 0 or more.
|
|
commodityUnits = [UNIVERSE unitsForCommodity:i];
|
|
if (commodityUnits != UNITS_TONS)
|
|
{
|
|
if (commodityUnits == UNITS_KILOGRAMS) quantity = (quantity + 500) / 1000;
|
|
else quantity = (quantity + 500000) / 1000000; // grams
|
|
}
|
|
cargoQtyOnBoard += quantity;
|
|
}
|
|
cargoQtyOnBoard += [[self cargo] count];
|
|
}
|
|
else // NPCs only method
|
|
{
|
|
cargoQtyOnBoard = [[self cargo] count];
|
|
}
|
|
|
|
return cargoQtyOnBoard;
|
|
}
|
|
|
|
|
|
|
|
- (OOCargoType) cargoType
|
|
{
|
|
return cargo_type;
|
|
}
|
|
|
|
|
|
- (NSMutableArray*) cargo
|
|
{
|
|
return cargo;
|
|
}
|
|
|
|
|
|
- (void) setCargo:(NSArray *) some_cargo
|
|
{
|
|
[cargo removeAllObjects];
|
|
[cargo addObjectsFromArray:some_cargo];
|
|
}
|
|
|
|
|
|
- (OOCargoFlag) cargoFlag
|
|
{
|
|
return cargo_flag;
|
|
}
|
|
|
|
|
|
- (void) setCargoFlag:(OOCargoFlag) flag
|
|
{
|
|
cargo_flag = flag;
|
|
}
|
|
|
|
|
|
- (void) setSpeed:(double) amount
|
|
{
|
|
flightSpeed = amount;
|
|
}
|
|
|
|
|
|
- (void) setDesiredSpeed:(double) amount
|
|
{
|
|
desired_speed = amount;
|
|
}
|
|
|
|
|
|
- (double) desiredSpeed
|
|
{
|
|
return desired_speed;
|
|
}
|
|
|
|
|
|
- (void) increase_flight_speed:(double) delta
|
|
{
|
|
double factor = 1.0;
|
|
if (desired_speed > maxFlightSpeed && [self hasFuelInjection] && fuel > MIN_FUEL) factor = [self afterburnerFactor];
|
|
|
|
if (flightSpeed < maxFlightSpeed * factor)
|
|
flightSpeed += delta * factor;
|
|
else
|
|
flightSpeed = maxFlightSpeed * factor;
|
|
}
|
|
|
|
|
|
- (void) decrease_flight_speed:(double) delta
|
|
{
|
|
double factor = 1.0;
|
|
if (flightSpeed > maxFlightSpeed) factor = [self afterburnerFactor] / 2;
|
|
|
|
if (flightSpeed > factor * delta)
|
|
flightSpeed -= factor * delta; // Player uses here: flightSpeed -= 5 * HYPERSPEED_FACTOR * delta (= 160 * delta);
|
|
else
|
|
flightSpeed = 0;
|
|
}
|
|
|
|
|
|
- (void) increase_flight_roll:(double) delta
|
|
{
|
|
if (flightRoll < max_flight_roll)
|
|
flightRoll += delta;
|
|
if (flightRoll > max_flight_roll)
|
|
flightRoll = max_flight_roll;
|
|
}
|
|
|
|
|
|
- (void) decrease_flight_roll:(double) delta
|
|
{
|
|
if (flightRoll > -max_flight_roll)
|
|
flightRoll -= delta;
|
|
if (flightRoll < -max_flight_roll)
|
|
flightRoll = -max_flight_roll;
|
|
}
|
|
|
|
|
|
- (void) increase_flight_pitch:(double) delta
|
|
{
|
|
if (flightPitch < max_flight_pitch)
|
|
flightPitch += delta;
|
|
if (flightPitch > max_flight_pitch)
|
|
flightPitch = max_flight_pitch;
|
|
}
|
|
|
|
|
|
- (void) decrease_flight_pitch:(double) delta
|
|
{
|
|
if (flightPitch > -max_flight_pitch)
|
|
flightPitch -= delta;
|
|
if (flightPitch < -max_flight_pitch)
|
|
flightPitch = -max_flight_pitch;
|
|
}
|
|
|
|
|
|
- (void) increase_flight_yaw:(double) delta
|
|
{
|
|
if (flightYaw < max_flight_yaw)
|
|
flightYaw += delta;
|
|
if (flightYaw > max_flight_yaw)
|
|
flightYaw = max_flight_yaw;
|
|
}
|
|
|
|
|
|
- (void) decrease_flight_yaw:(double) delta
|
|
{
|
|
if (flightYaw > -max_flight_yaw)
|
|
flightYaw -= delta;
|
|
if (flightYaw < -max_flight_yaw)
|
|
flightYaw = -max_flight_yaw;
|
|
}
|
|
|
|
|
|
- (GLfloat) flightRoll
|
|
{
|
|
return flightRoll;
|
|
}
|
|
|
|
|
|
- (GLfloat) flightPitch
|
|
{
|
|
return flightPitch;
|
|
}
|
|
|
|
|
|
- (GLfloat) flightYaw
|
|
{
|
|
return flightYaw;
|
|
}
|
|
|
|
|
|
- (GLfloat) flightSpeed
|
|
{
|
|
return flightSpeed;
|
|
}
|
|
|
|
|
|
- (GLfloat) maxFlightSpeed
|
|
{
|
|
return maxFlightSpeed;
|
|
}
|
|
|
|
|
|
- (GLfloat) speedFactor
|
|
{
|
|
if (maxFlightSpeed <= 0.0) return 0.0;
|
|
return flightSpeed / maxFlightSpeed;
|
|
}
|
|
|
|
|
|
- (GLfloat) temperature
|
|
{
|
|
return ship_temperature;
|
|
}
|
|
|
|
|
|
- (void) setTemperature:(GLfloat) value
|
|
{
|
|
ship_temperature = value;
|
|
}
|
|
|
|
|
|
- (GLfloat) heatInsulation
|
|
{
|
|
return _heatInsulation;
|
|
}
|
|
|
|
|
|
- (void) setHeatInsulation:(GLfloat) value
|
|
{
|
|
_heatInsulation = value;
|
|
}
|
|
|
|
|
|
- (int) damage
|
|
{
|
|
return (int)(100 - (100 * energy / maxEnergy));
|
|
}
|
|
|
|
|
|
// Exposed to AI
|
|
- (void) dealEnergyDamageWithinDesiredRange
|
|
{
|
|
NSArray* targets = [UNIVERSE getEntitiesWithinRange:desired_range ofEntity:self];
|
|
if ([targets count] > 0)
|
|
{
|
|
unsigned i;
|
|
for (i = 0; i < [targets count]; i++)
|
|
{
|
|
Entity *e2 = [targets objectAtIndex:i];
|
|
Vector p2 = vector_subtract([e2 position], position);
|
|
double ecr = [e2 collisionRadius];
|
|
double d2 = magnitude2(p2) - ecr * ecr;
|
|
double damage = weapon_energy*desired_range/d2;
|
|
[e2 takeEnergyDamage:damage from:self becauseOf:[self owner]];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) dealMomentumWithinDesiredRange:(double)amount
|
|
{
|
|
NSArray* targets = [UNIVERSE getEntitiesWithinRange:desired_range ofEntity:self];
|
|
if ([targets count] > 0)
|
|
{
|
|
unsigned i;
|
|
for (i = 0; i < [targets count]; i++)
|
|
{
|
|
ShipEntity *e2 = (ShipEntity*)[targets objectAtIndex:i];
|
|
if ([e2 isShip])
|
|
{
|
|
Vector p2 = vector_subtract([e2 position], position);
|
|
double ecr = [e2 collisionRadius];
|
|
double d2 = magnitude2(p2) - ecr * ecr;
|
|
while (d2 <= 0.0)
|
|
{
|
|
p2 = OOVectorRandomSpatial(1.0);
|
|
d2 = magnitude2(p2);
|
|
}
|
|
double moment = amount*desired_range/d2;
|
|
[e2 addImpactMoment:vector_normal(p2) fraction:moment];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (BOOL) isHulk
|
|
{
|
|
return isHulk;
|
|
}
|
|
|
|
- (void) setHulk:(BOOL)isNowHulk
|
|
{
|
|
isHulk = isNowHulk;
|
|
}
|
|
|
|
- (void) getDestroyedBy:(Entity *)whom context:(NSString *)context
|
|
{
|
|
if (whom == nil) whom = (id)[NSNull null];
|
|
|
|
// Is this safe to do here? The script actions will be executed before the status has been set to
|
|
// STATUS_DEAD, which is the opposite of what was happening inside becomeExplosion - Nikos.
|
|
if (script != nil)
|
|
{
|
|
[[PlayerEntity sharedPlayer] setScriptTarget:self];
|
|
[self doScriptEvent:@"shipDied" withArguments:[NSArray arrayWithObjects:whom, context, nil]];
|
|
}
|
|
|
|
[self becomeExplosion];
|
|
}
|
|
|
|
|
|
- (void) rescaleBy:(GLfloat)factor
|
|
{
|
|
// rescale mesh (and collision detection stuff)
|
|
[self setMesh:[[self mesh] meshRescaledBy:factor]];
|
|
|
|
// rescale positions of subentities
|
|
NSEnumerator *subEnum = nil;
|
|
Entity *se = nil;
|
|
for (subEnum = [self subEntityEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
se->position = vector_multiply_scalar([se position], factor);
|
|
|
|
// rescale ship subentities
|
|
if ([se isShip]) [(ShipEntity*)se rescaleBy:factor];
|
|
|
|
// rescale particle subentities
|
|
if ([se isParticle])
|
|
{
|
|
ParticleEntity* pe = (ParticleEntity*)se;
|
|
NSSize sz = [pe size];
|
|
sz.width *= factor;
|
|
sz.height *= factor;
|
|
[pe setSize: sz];
|
|
}
|
|
}
|
|
|
|
// rescale mass
|
|
mass *= factor * factor * factor;
|
|
}
|
|
|
|
|
|
- (void) becomeExplosion
|
|
{
|
|
OOCargoQuantity cargo_to_go;
|
|
|
|
// check if we're destroying a subentity
|
|
ShipEntity *parent = [self parentEntity];
|
|
if (parent != nil)
|
|
{
|
|
ShipEntity* this_ship = [self retain];
|
|
Vector this_pos = [self absolutePositionForSubentity];
|
|
|
|
// remove this ship from its parent's subentity list
|
|
[parent subEntityDied:self];
|
|
[UNIVERSE addEntity:this_ship];
|
|
[this_ship setPosition:this_pos];
|
|
[this_ship release];
|
|
}
|
|
|
|
Vector xposition = position;
|
|
Entity *fragment = nil;
|
|
int i;
|
|
Vector v;
|
|
Quaternion q;
|
|
int speed_low = 200;
|
|
int n_alloys = floor(sqrtf(sqrtf(mass / 25000.0)));
|
|
|
|
if ([self status] == STATUS_DEAD)
|
|
{
|
|
[UNIVERSE removeEntity:self];
|
|
return;
|
|
}
|
|
[self setStatus:STATUS_DEAD];
|
|
|
|
if ([self isThargoid]) [self broadcastThargoidDestroyed];
|
|
|
|
if (!suppressExplosion)
|
|
{
|
|
if ((mass > 500000.0f)&&(randf() < 0.25f)) // big!
|
|
{
|
|
// draw an expanding ring
|
|
ParticleEntity *ring = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained
|
|
Vector ring_vel = [self velocity];
|
|
ring_vel.x *= 0.25; ring_vel.y *= 0.25; ring_vel.z *= 0.25; // quarter velocity
|
|
[ring setVelocity:ring_vel];
|
|
[UNIVERSE addEntity:ring];
|
|
[ring release];
|
|
}
|
|
|
|
// several parts to the explosion:
|
|
// 1. fast sparks
|
|
fragment = [[ParticleEntity alloc] initFragburstSize:collision_radius fromPosition:xposition];
|
|
[UNIVERSE addEntity:fragment];
|
|
[fragment release];
|
|
// 2. slow clouds
|
|
fragment = [[ParticleEntity alloc] initBurst2Size:collision_radius fromPosition:xposition];
|
|
[UNIVERSE addEntity:fragment];
|
|
[fragment release];
|
|
// 3. flash
|
|
fragment = [[OOFlashEffectEntity alloc] initExplosionFlashWithPosition:position size:collision_radius];
|
|
[UNIVERSE addEntity:fragment];
|
|
[fragment release];
|
|
|
|
BOOL add_more_explosion = YES;
|
|
if (UNIVERSE)
|
|
{
|
|
add_more_explosion &= (UNIVERSE->n_entities < 0.95 * UNIVERSE_MAX_ENTITIES); //
|
|
add_more_explosion &= ([UNIVERSE getTimeDelta] < 0.125); // FPS > 8
|
|
}
|
|
// quick - check if UNIVERSE is nearing limit for entities - if it is don't add to it!
|
|
//
|
|
if (add_more_explosion)
|
|
{
|
|
// we need to throw out cargo at this point.
|
|
NSArray *jetsam = nil; // this will contain the stuff to get thrown out
|
|
unsigned cargo_chance = 10;
|
|
if ([[name lowercaseString] rangeOfString:@"medical"].location != NSNotFound)
|
|
{
|
|
cargo_to_go = max_cargo * cargo_chance / 100;
|
|
while (cargo_to_go > 15)
|
|
cargo_to_go = ranrot_rand() % cargo_to_go;
|
|
[self setCargo:[UNIVERSE getContainersOfDrugs:cargo_to_go]];
|
|
cargo_chance = 100; // chance of any given piece of cargo surviving decompression
|
|
cargo_flag = CARGO_FLAG_CANISTERS;
|
|
}
|
|
|
|
cargo_to_go = max_cargo * cargo_chance / 100;
|
|
while (cargo_to_go > 15)
|
|
cargo_to_go = ranrot_rand() % cargo_to_go;
|
|
cargo_chance = 100; // chance of any given piece of cargo surviving decompression
|
|
switch (cargo_flag)
|
|
{
|
|
case CARGO_FLAG_NONE:
|
|
case CARGO_FLAG_FULL_PASSENGERS:
|
|
break;
|
|
|
|
case CARGO_FLAG_FULL_UNIFORM :
|
|
{
|
|
NSString* commodity_name = [shipinfoDictionary oo_stringForKey:@"cargo_carried"];
|
|
jetsam = [UNIVERSE getContainersOfCommodity:commodity_name :cargo_to_go];
|
|
}
|
|
break;
|
|
|
|
case CARGO_FLAG_FULL_PLENTIFUL :
|
|
jetsam = [UNIVERSE getContainersOfGoods:cargo_to_go scarce:NO];
|
|
break;
|
|
|
|
case CARGO_FLAG_PIRATE :
|
|
cargo_to_go = likely_cargo;
|
|
while (cargo_to_go > 15)
|
|
cargo_to_go = ranrot_rand() % cargo_to_go;
|
|
cargo_chance = 65; // 35% chance of spoilage
|
|
jetsam = [UNIVERSE getContainersOfGoods:cargo_to_go scarce:YES];
|
|
break;
|
|
|
|
case CARGO_FLAG_FULL_SCARCE :
|
|
jetsam = [UNIVERSE getContainersOfGoods:cargo_to_go scarce:YES];
|
|
break;
|
|
|
|
case CARGO_FLAG_CANISTERS:
|
|
jetsam = [NSArray arrayWithArray:cargo]; // what the ship is carrying
|
|
[cargo removeAllObjects]; // dispense with it!
|
|
break;
|
|
}
|
|
|
|
// Throw out cargo
|
|
if (jetsam)
|
|
{
|
|
int n_jetsam = [jetsam count];
|
|
//NSDictionary *containerShipDict = nil;
|
|
//
|
|
for (i = 0; i < n_jetsam; i++)
|
|
{
|
|
if (Ranrot() % 100 < cargo_chance) // chance of any given piece of cargo surviving decompression
|
|
{
|
|
ShipEntity* container = [jetsam objectAtIndex:i];
|
|
AI *containerAI = nil;
|
|
Vector rpos = xposition;
|
|
Vector rrand = randomPositionInBoundingBox(boundingBox);
|
|
rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z;
|
|
rpos.x += (ranrot_rand() % 7) - 3;
|
|
rpos.y += (ranrot_rand() % 7) - 3;
|
|
rpos.z += (ranrot_rand() % 7) - 3;
|
|
[container setPosition:rpos];
|
|
v.x = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
|
|
v.y = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
|
|
v.z = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
|
|
[container setVelocity:v];
|
|
quaternion_set_random(&q);
|
|
[container setOrientation:q];
|
|
//[container setStatus:STATUS_IN_FLIGHT];
|
|
[container setScanClass: CLASS_CARGO];
|
|
[UNIVERSE addEntity:container]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
containerAI = [container getAI];
|
|
if ([containerAI hasSuspendedStateMachines]) // check if new or recycled cargo.
|
|
{
|
|
[containerAI exitStateMachineWithMessage:nil];
|
|
[container setThrust:[container maxThrust]]; // restore old value. Was set to zero on previous scooping.
|
|
[container setOwner:container];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Throw out rocks and alloys to be scooped up
|
|
if ([self hasRole:@"asteroid"])
|
|
{
|
|
if (!noRocks && (being_mined || randf() < 0.20))
|
|
{
|
|
int n_rocks = 2 + (Ranrot() % (likely_cargo + 1));
|
|
|
|
for (i = 0; i < n_rocks; i++)
|
|
{
|
|
ShipEntity* rock = [UNIVERSE newShipWithRole:@"boulder"]; // retain count = 1
|
|
if (rock)
|
|
{
|
|
Vector rpos = xposition;
|
|
int r_speed = 20.0 * [rock maxFlightSpeed];
|
|
int cr = 3 * rock->collision_radius;
|
|
rpos.x += (ranrot_rand() % cr) - cr/2;
|
|
rpos.y += (ranrot_rand() % cr) - cr/2;
|
|
rpos.z += (ranrot_rand() % cr) - cr/2;
|
|
[rock setPosition:rpos];
|
|
v.x = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2);
|
|
v.y = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2);
|
|
v.z = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2);
|
|
[rock setVelocity:v];
|
|
quaternion_set_random(&q);
|
|
[rock setOrientation:q];
|
|
//[rock setStatus:STATUS_IN_FLIGHT];
|
|
[rock setScanClass: CLASS_ROCK];
|
|
[UNIVERSE addEntity:rock]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
//[[rock getAI] setState:@"GLOBAL"];
|
|
[rock release];
|
|
}
|
|
}
|
|
}
|
|
[UNIVERSE removeEntity:self];
|
|
return; // don't do anything more
|
|
}
|
|
|
|
if ([self hasRole:@"boulder"])
|
|
{
|
|
if ((being_mined)||(ranrot_rand() % 100 < 20))
|
|
{
|
|
int n_rocks = 2 + (ranrot_rand() % 5);
|
|
//
|
|
for (i = 0; i < n_rocks; i++)
|
|
{
|
|
ShipEntity* rock = [UNIVERSE newShipWithRole:@"splinter"]; // retain count = 1
|
|
if (rock)
|
|
{
|
|
Vector rpos = xposition;
|
|
int r_speed = 20.0 * [rock maxFlightSpeed];
|
|
int cr = 3 * rock->collision_radius;
|
|
rpos.x += (ranrot_rand() % cr) - cr/2;
|
|
rpos.y += (ranrot_rand() % cr) - cr/2;
|
|
rpos.z += (ranrot_rand() % cr) - cr/2;
|
|
[rock setPosition:rpos];
|
|
v.x = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2);
|
|
v.y = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2);
|
|
v.z = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2);
|
|
[rock setBounty: 0];
|
|
[rock setCommodity:[UNIVERSE commodityForName:@"Minerals"] andAmount: 1];
|
|
[rock setVelocity:v];
|
|
quaternion_set_random(&q);
|
|
[rock setOrientation:q];
|
|
//[rock setStatus:STATUS_IN_FLIGHT];
|
|
[rock setScanClass: CLASS_CARGO];
|
|
[UNIVERSE addEntity:rock]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
//[[rock getAI] setState:@"GLOBAL"];
|
|
[rock release];
|
|
}
|
|
}
|
|
}
|
|
[UNIVERSE removeEntity:self];
|
|
return; // don't do anything more
|
|
}
|
|
|
|
// throw out burning chunks of wreckage
|
|
//
|
|
if (n_alloys && canFragment)
|
|
{
|
|
int n_wreckage = (n_alloys < 3)? n_alloys : 3;
|
|
|
|
// quick - check if UNIVERSE is nearing limit for entities - if it is don't make wreckage
|
|
//
|
|
add_more_explosion &= (UNIVERSE->n_entities < 0.50 * UNIVERSE_MAX_ENTITIES);
|
|
if (!add_more_explosion)
|
|
n_wreckage = 0;
|
|
//
|
|
////
|
|
|
|
for (i = 0; i < n_wreckage; i++)
|
|
{
|
|
ShipEntity* wreck = [UNIVERSE newShipWithRole:@"wreckage"]; // retain count = 1
|
|
if (wreck)
|
|
{
|
|
GLfloat expected_mass = 0.1f * mass * (0.75 + 0.5 * randf());
|
|
GLfloat wreck_mass = [wreck mass];
|
|
GLfloat scale_factor = powf(expected_mass / wreck_mass, 0.33333333f); // cube root of volume ratio
|
|
[wreck rescaleBy: scale_factor];
|
|
|
|
Vector r1 = randomFullNodeFrom([octree octreeDetails], kZeroVector);
|
|
Vector rpos = make_vector (v_right.x * r1.x + v_up.x * r1.y + v_forward.x * r1.z,
|
|
v_right.y * r1.x + v_up.y * r1.y + v_forward.y * r1.z,
|
|
v_right.z * r1.x + v_up.z * r1.y + v_forward.z * r1.z);
|
|
rpos.x += xposition.x;
|
|
rpos.y += xposition.y;
|
|
rpos.z += xposition.z;
|
|
[wreck setPosition:rpos];
|
|
|
|
[wreck setVelocity:[self velocity]];
|
|
|
|
quaternion_set_random(&q);
|
|
[wreck setOrientation:q];
|
|
|
|
[wreck setTemperature: 1000.0]; // take 1000e heat damage per second
|
|
[wreck setHeatInsulation: 1.0e7]; // very large! so it won't cool down
|
|
[wreck setEnergy: 750.0 * randf() + 250.0 * i + 100.0]; // burn for 0.25s -> 1.25s
|
|
|
|
//[wreck setStatus:STATUS_IN_FLIGHT];
|
|
[UNIVERSE addEntity:wreck]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
[wreck performTumble];
|
|
[wreck rescaleBy: 1.0/scale_factor];
|
|
[wreck release];
|
|
}
|
|
}
|
|
n_alloys = ranrot_rand() % n_alloys;
|
|
}
|
|
|
|
// Throw out scrap metal
|
|
//
|
|
for (i = 0; i < n_alloys; i++)
|
|
{
|
|
ShipEntity* plate = [UNIVERSE newShipWithRole:@"alloy"]; // retain count = 1
|
|
if (plate)
|
|
{
|
|
Vector rpos = xposition;
|
|
Vector rrand = randomPositionInBoundingBox(boundingBox);
|
|
rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z;
|
|
rpos.x += (ranrot_rand() % 7) - 3;
|
|
rpos.y += (ranrot_rand() % 7) - 3;
|
|
rpos.z += (ranrot_rand() % 7) - 3;
|
|
[plate setPosition:rpos];
|
|
v.x = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
|
|
v.y = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
|
|
v.z = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2);
|
|
[plate setVelocity:v];
|
|
quaternion_set_random(&q);
|
|
[plate setOrientation:q];
|
|
[plate setScanClass: CLASS_CARGO];
|
|
[plate setCommodity:[UNIVERSE commodityForName:@"Alloys"] andAmount:1];
|
|
[UNIVERSE addEntity:plate]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
//[plate setStatus:STATUS_IN_FLIGHT];
|
|
[plate setTemperature:[self temperature] * EJECTA_TEMP_FACTOR];
|
|
//[[plate getAI] setState:@"GLOBAL"];
|
|
[plate release];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
NSEnumerator *subEnum = nil;
|
|
ShipEntity *se = nil;
|
|
for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
[se setSuppressExplosion:suppressExplosion];
|
|
[se setPosition:[se absolutePositionForSubentity]];
|
|
[UNIVERSE addEntity:se];
|
|
[se becomeExplosion];
|
|
}
|
|
[self clearSubEntities];
|
|
|
|
// momentum from explosions
|
|
desired_range = collision_radius * 2.5;
|
|
[self dealMomentumWithinDesiredRange: 0.125 * mass];
|
|
|
|
if (self != [PlayerEntity sharedPlayer]) // was if !isPlayer - but I think this may cause ghosts (Who's "I"? -- Ahruman)
|
|
{
|
|
if (isPlayer)
|
|
{
|
|
#ifndef NDEBUG
|
|
OOLog(@"becomeExplosion.suspectedGhost.confirm", @"Ship spotted with isPlayer set when not actually the player.");
|
|
#endif
|
|
isPlayer = NO;
|
|
}
|
|
[UNIVERSE removeEntity:self];
|
|
}
|
|
}
|
|
|
|
|
|
// Exposed to AI
|
|
- (void) becomeEnergyBlast
|
|
{
|
|
ParticleEntity* blast = [[ParticleEntity alloc] initEnergyMineFromShip:self];
|
|
[UNIVERSE addEntity:blast];
|
|
[blast setOwner: [self owner]];
|
|
[blast release];
|
|
[UNIVERSE removeEntity:self];
|
|
}
|
|
|
|
|
|
- (void)subEntityDied:(ShipEntity *)sub
|
|
{
|
|
if ([self subEntityTakingDamage] == sub) [self setSubEntityTakingDamage:nil];
|
|
|
|
[sub setOwner:nil];
|
|
[subEntities removeObject:sub];
|
|
}
|
|
|
|
|
|
- (void)subEntityReallyDied:(ShipEntity *)sub
|
|
{
|
|
if ([self subEntityTakingDamage] == sub) [self setSubEntityTakingDamage:nil];
|
|
|
|
if ([self hasSubEntity:sub])
|
|
{
|
|
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.");
|
|
|
|
// Leak subentity list.
|
|
subEntities = nil;
|
|
}
|
|
}
|
|
|
|
|
|
Vector randomPositionInBoundingBox(BoundingBox bb)
|
|
{
|
|
Vector result;
|
|
result.x = bb.min.x + randf() * (bb.max.x - bb.min.x);
|
|
result.y = bb.min.y + randf() * (bb.max.y - bb.min.y);
|
|
result.z = bb.min.z + randf() * (bb.max.z - bb.min.z);
|
|
return result;
|
|
}
|
|
|
|
|
|
- (Vector) positionOffsetForAlignment:(NSString*) align
|
|
{
|
|
NSString* padAlign = [NSString stringWithFormat:@"%@---", align];
|
|
Vector result = kZeroVector;
|
|
switch ([padAlign characterAtIndex:0])
|
|
{
|
|
case (unichar)'c':
|
|
case (unichar)'C':
|
|
result.x = 0.5 * (boundingBox.min.x + boundingBox.max.x);
|
|
break;
|
|
case (unichar)'M':
|
|
result.x = boundingBox.max.x;
|
|
break;
|
|
case (unichar)'m':
|
|
result.x = boundingBox.min.x;
|
|
break;
|
|
}
|
|
switch ([padAlign characterAtIndex:1])
|
|
{
|
|
case (unichar)'c':
|
|
case (unichar)'C':
|
|
result.y = 0.5 * (boundingBox.min.y + boundingBox.max.y);
|
|
break;
|
|
case (unichar)'M':
|
|
result.y = boundingBox.max.y;
|
|
break;
|
|
case (unichar)'m':
|
|
result.y = boundingBox.min.y;
|
|
break;
|
|
}
|
|
switch ([padAlign characterAtIndex:2])
|
|
{
|
|
case (unichar)'c':
|
|
case (unichar)'C':
|
|
result.z = 0.5 * (boundingBox.min.z + boundingBox.max.z);
|
|
break;
|
|
case (unichar)'M':
|
|
result.z = boundingBox.max.z;
|
|
break;
|
|
case (unichar)'m':
|
|
result.z = boundingBox.min.z;
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
Vector positionOffsetForShipInRotationToAlignment(ShipEntity* ship, Quaternion q, NSString* align)
|
|
{
|
|
NSString* padAlign = [NSString stringWithFormat:@"%@---", align];
|
|
Vector i = vector_right_from_quaternion(q);
|
|
Vector j = vector_up_from_quaternion(q);
|
|
Vector k = vector_forward_from_quaternion(q);
|
|
BoundingBox arbb = [ship findBoundingBoxRelativeToPosition: make_vector(0,0,0) InVectors: i : j : k];
|
|
Vector result = kZeroVector;
|
|
switch ([padAlign characterAtIndex:0])
|
|
{
|
|
case (unichar)'c':
|
|
case (unichar)'C':
|
|
result.x = 0.5 * (arbb.min.x + arbb.max.x);
|
|
break;
|
|
case (unichar)'M':
|
|
result.x = arbb.max.x;
|
|
break;
|
|
case (unichar)'m':
|
|
result.x = arbb.min.x;
|
|
break;
|
|
}
|
|
switch ([padAlign characterAtIndex:1])
|
|
{
|
|
case (unichar)'c':
|
|
case (unichar)'C':
|
|
result.y = 0.5 * (arbb.min.y + arbb.max.y);
|
|
break;
|
|
case (unichar)'M':
|
|
result.y = arbb.max.y;
|
|
break;
|
|
case (unichar)'m':
|
|
result.y = arbb.min.y;
|
|
break;
|
|
}
|
|
switch ([padAlign characterAtIndex:2])
|
|
{
|
|
case (unichar)'c':
|
|
case (unichar)'C':
|
|
result.z = 0.5 * (arbb.min.z + arbb.max.z);
|
|
break;
|
|
case (unichar)'M':
|
|
result.z = arbb.max.z;
|
|
break;
|
|
case (unichar)'m':
|
|
result.z = arbb.min.z;
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
- (void) becomeLargeExplosion:(double) factor
|
|
{
|
|
Vector xposition = position;
|
|
ParticleEntity *fragment;
|
|
OOCargoQuantity n_cargo = (ranrot_rand() % (likely_cargo + 1));
|
|
OOCargoQuantity cargo_to_go;
|
|
|
|
if ([self status] == STATUS_DEAD) return;
|
|
[self setStatus:STATUS_DEAD];
|
|
|
|
//scripting
|
|
if (script != nil)
|
|
{
|
|
[[PlayerEntity sharedPlayer] setScriptTarget:self];
|
|
[self doScriptEvent:@"shipDied"]; // FIXME: params missing
|
|
}
|
|
|
|
// two parts to the explosion:
|
|
// 1. fast sparks
|
|
float how_many = factor;
|
|
while (how_many > 0.5f)
|
|
{
|
|
fragment = [[ParticleEntity alloc] initFragburstSize: collision_radius fromPosition:xposition];
|
|
[UNIVERSE addEntity:fragment];
|
|
[fragment release];
|
|
how_many -= 1.0f;
|
|
}
|
|
// 2. slow clouds
|
|
how_many = factor;
|
|
while (how_many > 0.5f)
|
|
{
|
|
fragment = [[ParticleEntity alloc] initBurst2Size: collision_radius fromPosition:xposition];
|
|
[UNIVERSE addEntity:fragment];
|
|
[fragment release];
|
|
how_many -= 1.0f;
|
|
}
|
|
|
|
|
|
// we need to throw out cargo at this point.
|
|
unsigned cargo_chance = 10;
|
|
if ([[name lowercaseString] rangeOfString:@"medical"].location != NSNotFound)
|
|
{
|
|
cargo_to_go = max_cargo * cargo_chance / 100;
|
|
while (cargo_to_go > 15)
|
|
cargo_to_go = ranrot_rand() % cargo_to_go;
|
|
[self setCargo:[UNIVERSE getContainersOfDrugs:cargo_to_go]];
|
|
cargo_chance = 100; // chance of any given piece of cargo surviving decompression
|
|
cargo_flag = CARGO_FLAG_CANISTERS;
|
|
}
|
|
if (cargo_flag == CARGO_FLAG_FULL_PLENTIFUL || cargo_flag == CARGO_FLAG_FULL_SCARCE)
|
|
{
|
|
cargo_to_go = max_cargo / 10;
|
|
while (cargo_to_go > 15)
|
|
cargo_to_go = ranrot_rand() % cargo_to_go;
|
|
[self setCargo:[UNIVERSE getContainersOfGoods:cargo_to_go scarce:(cargo_flag == CARGO_FLAG_FULL_SCARCE)]];
|
|
cargo_chance = 100;
|
|
}
|
|
while ([cargo count] > 0)
|
|
{
|
|
if (Ranrot() % 100 < cargo_chance) // 10% chance of any given piece of cargo surviving decompression
|
|
{
|
|
ShipEntity* container = [[cargo objectAtIndex:0] retain];
|
|
Vector rpos = xposition;
|
|
Vector rrand = randomPositionInBoundingBox(boundingBox);
|
|
rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z;
|
|
rpos.x += (ranrot_rand() % 7) - 3;
|
|
rpos.y += (ranrot_rand() % 7) - 3;
|
|
rpos.z += (ranrot_rand() % 7) - 3;
|
|
[container setPosition:rpos];
|
|
[container setScanClass: CLASS_CARGO];
|
|
[UNIVERSE addEntity:container]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
//[[container getAI] setState:@"GLOBAL"];
|
|
//[container setStatus:STATUS_IN_FLIGHT];
|
|
[container release];
|
|
if (n_cargo > 0)
|
|
n_cargo--; // count down extra cargo
|
|
}
|
|
[cargo removeObjectAtIndex:0];
|
|
}
|
|
|
|
NSEnumerator *subEnum = nil;
|
|
ShipEntity *se = nil;
|
|
for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
[se setSuppressExplosion:suppressExplosion];
|
|
[se setPosition:[se absolutePositionForSubentity]];
|
|
[UNIVERSE addEntity:se];
|
|
[se becomeExplosion];
|
|
}
|
|
[self clearSubEntities];
|
|
|
|
if (!isPlayer) [UNIVERSE removeEntity:self];
|
|
}
|
|
|
|
|
|
- (void) collectBountyFor:(ShipEntity *)other
|
|
{
|
|
if ([self isPirate]) bounty += [other bounty];
|
|
}
|
|
|
|
|
|
- (NSComparisonResult) compareBeaconCodeWith:(ShipEntity*) other
|
|
{
|
|
return [[self beaconCode] compare:[other beaconCode] options: NSCaseInsensitiveSearch];
|
|
}
|
|
|
|
|
|
- (GLfloat)laserHeatLevel
|
|
{
|
|
float result = (weapon_recharge_rate - [self shotTime]) / weapon_recharge_rate;
|
|
return OOClamp_0_1_f(result);
|
|
}
|
|
|
|
|
|
- (GLfloat)hullHeatLevel
|
|
{
|
|
return ship_temperature / (GLfloat)SHIP_MAX_CABIN_TEMP;
|
|
}
|
|
|
|
|
|
- (GLfloat)entityPersonality
|
|
{
|
|
return entity_personality / (float)0x7FFF;
|
|
}
|
|
|
|
|
|
- (GLint)entityPersonalityInt
|
|
{
|
|
return entity_personality;
|
|
}
|
|
|
|
|
|
- (void)setSuppressExplosion:(BOOL)suppress
|
|
{
|
|
suppressExplosion = !!suppress;
|
|
}
|
|
|
|
|
|
- (void) resetExhaustPlumes
|
|
{
|
|
NSEnumerator *exEnum = nil;
|
|
OOExhaustPlumeEntity *exEnt = nil;
|
|
|
|
for (exEnum = [self exhaustEnumerator]; (exEnt = [exEnum nextObject]); )
|
|
{
|
|
[exEnt resetPlume];
|
|
}
|
|
}
|
|
|
|
|
|
/*-----------------------------------------
|
|
|
|
AI piloting methods
|
|
|
|
-----------------------------------------*/
|
|
|
|
BOOL class_masslocks(int some_class)
|
|
{
|
|
switch (some_class)
|
|
{
|
|
case CLASS_BUOY:
|
|
case CLASS_ROCK:
|
|
case CLASS_CARGO:
|
|
case CLASS_MINE:
|
|
case CLASS_NO_DRAW:
|
|
return NO;
|
|
|
|
case CLASS_THARGOID:
|
|
case CLASS_MISSILE:
|
|
case CLASS_STATION:
|
|
case CLASS_POLICE:
|
|
case CLASS_MILITARY:
|
|
case CLASS_WORMHOLE:
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (BOOL) checkTorusJumpClear
|
|
{
|
|
Entity* scan;
|
|
//
|
|
scan = z_previous; while ((scan)&&(!class_masslocks(scan->scanClass))) scan = scan->z_previous; // skip non-mass-locking
|
|
while ((scan)&&(scan->position.z > position.z - scannerRange))
|
|
{
|
|
if (class_masslocks(scan->scanClass) && (distance2(position, scan->position) < SCANNER_MAX_RANGE2))
|
|
return NO;
|
|
scan = scan->z_previous; while ((scan)&&(!class_masslocks(scan->scanClass))) scan = scan->z_previous;
|
|
}
|
|
scan = z_next; while ((scan)&&(!class_masslocks(scan->scanClass))) scan = scan->z_next; // skip non-mass-locking
|
|
while ((scan)&&(scan->position.z < position.z + scannerRange))
|
|
{
|
|
if (class_masslocks(scan->scanClass) && (distance2(position, scan->position) < SCANNER_MAX_RANGE2))
|
|
return NO;
|
|
scan = scan->z_previous; while ((scan)&&(!class_masslocks(scan->scanClass))) scan = scan->z_previous;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (void) checkScanner
|
|
{
|
|
Entity* scan;
|
|
n_scanned_ships = 0;
|
|
//
|
|
scan = z_previous; while ((scan)&&(scan->isShip == NO)) scan = scan->z_previous; // skip non-ships
|
|
while ((scan)&&(scan->position.z > position.z - scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
|
|
{
|
|
if (scan->isShip)
|
|
{
|
|
distance2_scanned_ships[n_scanned_ships] = distance2(position, scan->position);
|
|
if (distance2_scanned_ships[n_scanned_ships] < SCANNER_MAX_RANGE2)
|
|
scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
|
|
}
|
|
scan = scan->z_previous; while ((scan)&&(scan->isShip == NO)) scan = scan->z_previous;
|
|
}
|
|
//
|
|
scan = z_next; while ((scan)&&(scan->isShip == NO)) scan = scan->z_next; // skip non-ships
|
|
while ((scan)&&(scan->position.z < position.z + scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER))
|
|
{
|
|
if (scan->isShip)
|
|
{
|
|
distance2_scanned_ships[n_scanned_ships] = distance2(position, scan->position);
|
|
if (distance2_scanned_ships[n_scanned_ships] < SCANNER_MAX_RANGE2)
|
|
scanned_ships[n_scanned_ships++] = (ShipEntity*)scan;
|
|
}
|
|
scan = scan->z_next; while ((scan)&&(scan->isShip == NO)) scan = scan->z_next; // skip non-ships
|
|
}
|
|
//
|
|
scanned_ships[n_scanned_ships] = nil; // terminate array
|
|
}
|
|
|
|
|
|
- (ShipEntity**) scannedShips
|
|
{
|
|
scanned_ships[n_scanned_ships] = nil; // terminate array
|
|
return scanned_ships;
|
|
}
|
|
|
|
|
|
- (int) numberOfScannedShips
|
|
{
|
|
return n_scanned_ships;
|
|
}
|
|
|
|
|
|
- (void) setFound_target:(Entity *) targetEntity
|
|
{
|
|
if (targetEntity)
|
|
found_target = [targetEntity universalID];
|
|
}
|
|
|
|
|
|
- (void) setPrimaryAggressor:(Entity *) targetEntity
|
|
{
|
|
if (targetEntity)
|
|
primaryAggressor = [targetEntity universalID];
|
|
}
|
|
|
|
|
|
- (void) addTarget:(Entity *) targetEntity
|
|
{
|
|
if (targetEntity == self) return;
|
|
if (targetEntity != nil) primaryTarget = [targetEntity universalID];
|
|
|
|
[[self shipSubEntityEnumerator] makeObjectsPerformSelector:@selector(addTarget:) withObject:targetEntity];
|
|
}
|
|
|
|
|
|
- (void) removeTarget:(Entity *) targetEntity
|
|
{
|
|
if(targetEntity != nil) [self noteLostTarget];
|
|
else primaryTarget = NO_TARGET;
|
|
// targetEntity == nil is currently only true for mounted player missiles.
|
|
// we don't want to send lostTarget messages while the missile is mounted.
|
|
|
|
[[self shipSubEntityEnumerator] makeObjectsPerformSelector:@selector(removeTarget:) withObject:targetEntity];
|
|
}
|
|
|
|
|
|
- (id) primaryTarget
|
|
{
|
|
id result = [UNIVERSE entityForUniversalID:primaryTarget];
|
|
if (EXPECT_NOT(result == self))
|
|
{
|
|
/* Added in response to a crash report showing recursion in
|
|
[PlayerEntity hasHostileTarget].
|
|
-- Ahruman 2009-12-17
|
|
*/
|
|
result = nil;
|
|
primaryTarget = NO_TARGET;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
- (int) primaryTargetID
|
|
{
|
|
return primaryTarget;
|
|
}
|
|
|
|
|
|
- (void) noteLostTarget
|
|
{
|
|
if (primaryTarget != NO_TARGET)
|
|
{
|
|
ShipEntity* target = [UNIVERSE entityForUniversalID:primaryTarget];
|
|
primaryTarget = NO_TARGET;
|
|
[self doScriptEvent:@"shipLostTarget" withArgument:(target && target->isShip) ? (id)target : nil];
|
|
[shipAI reactToMessage:@"TARGET_LOST"];
|
|
}
|
|
else
|
|
{
|
|
[self doScriptEvent:@"shipLostTarget" withArgument:nil];
|
|
[shipAI message:@"TARGET_LOST"]; // this often triggers on witchspace jumps, so make it a message.
|
|
}
|
|
}
|
|
|
|
|
|
- (void) noteTargetDestroyed:(ShipEntity *)target
|
|
{
|
|
[self collectBountyFor:(ShipEntity *)target];
|
|
if ([self primaryTarget] == target)
|
|
{
|
|
[self removeTarget:target];
|
|
[self doScriptEvent:@"shipDestroyedTarget" withArgument:target];
|
|
[shipAI message:@"TARGET_DESTROYED"];
|
|
}
|
|
}
|
|
|
|
|
|
- (OOBehaviour) behaviour
|
|
{
|
|
return behaviour;
|
|
}
|
|
|
|
|
|
- (void) setBehaviour:(OOBehaviour) cond
|
|
{
|
|
if (cond != behaviour)
|
|
{
|
|
frustration = 0.0; // change is a GOOD thing
|
|
behaviour = cond;
|
|
}
|
|
}
|
|
|
|
|
|
- (Vector) destination
|
|
{
|
|
return destination;
|
|
}
|
|
|
|
- (Vector) coordinates
|
|
{
|
|
return coordinates;
|
|
}
|
|
|
|
- (void) setCoordinate:(Vector) coord // The name "setCoordinates" is already used by AI scripting.
|
|
{
|
|
coordinates = coord;
|
|
}
|
|
|
|
- (Vector) distance_six: (GLfloat) dist
|
|
{
|
|
Vector six = position;
|
|
six.x -= dist * v_forward.x; six.y -= dist * v_forward.y; six.z -= dist * v_forward.z;
|
|
return six;
|
|
}
|
|
|
|
|
|
- (Vector) distance_twelve: (GLfloat) dist
|
|
{
|
|
Vector twelve = position;
|
|
twelve.x += dist * v_up.x; twelve.y += dist * v_up.y; twelve.z += dist * v_up.z;
|
|
return twelve;
|
|
}
|
|
|
|
|
|
- (double) ballTrackTarget:(double) delta_t
|
|
{
|
|
Vector vector_to_target;
|
|
Vector axis_to_track_by;
|
|
Vector my_position = position; // position relative to parent
|
|
Vector my_aim = vector_forward_from_quaternion(orientation);
|
|
Vector my_ref = reference;
|
|
double aim_cos, ref_cos;
|
|
|
|
Entity *target = [self primaryTarget];
|
|
|
|
Entity *last = nil;
|
|
Entity *father = [self parentEntity];
|
|
OOMatrix r_mat;
|
|
|
|
while ((father)&&(father != last) && father != NO_TARGET)
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
my_position = vector_add(OOVectorMultiplyMatrix(my_position, r_mat), [father position]);
|
|
my_ref = OOVectorMultiplyMatrix(my_ref, r_mat);
|
|
last = father;
|
|
if (![last isSubEntity]) break;
|
|
father = [father owner];
|
|
}
|
|
|
|
if (target)
|
|
{
|
|
vector_to_target = vector_subtract([target position], my_position);
|
|
vector_to_target = vector_normal_or_fallback(vector_to_target, kBasisZVector);
|
|
|
|
// do the tracking!
|
|
aim_cos = dot_product(vector_to_target, my_aim);
|
|
ref_cos = dot_product(vector_to_target, my_ref);
|
|
}
|
|
else
|
|
{
|
|
aim_cos = 0.0;
|
|
ref_cos = -1.0;
|
|
}
|
|
|
|
if (ref_cos > TURRET_MINIMUM_COS) // target is forward of self
|
|
{
|
|
axis_to_track_by = cross_product(vector_to_target, my_aim);
|
|
}
|
|
else
|
|
{
|
|
aim_cos = 0.0;
|
|
axis_to_track_by = cross_product(my_ref, my_aim); // return to center
|
|
}
|
|
|
|
quaternion_rotate_about_axis(&orientation, axis_to_track_by, thrust * delta_t);
|
|
[self orientationChanged];
|
|
|
|
[self setStatus:STATUS_ACTIVE];
|
|
|
|
return aim_cos;
|
|
}
|
|
|
|
|
|
- (void) trackOntoTarget:(double) delta_t withDForward: (GLfloat) dp
|
|
{
|
|
Vector vector_to_target;
|
|
Quaternion q_minarc;
|
|
//
|
|
Entity* target = [self primaryTarget];
|
|
//
|
|
if (!target)
|
|
return;
|
|
|
|
vector_to_target = target->position;
|
|
vector_to_target.x -= position.x; vector_to_target.y -= position.y; vector_to_target.z -= position.z;
|
|
//
|
|
GLfloat range2 = magnitude2(vector_to_target);
|
|
GLfloat targetRadius = 0.75 * target->collision_radius;
|
|
GLfloat max_cos = sqrt(1 - targetRadius*targetRadius/range2);
|
|
|
|
if (dp > max_cos)
|
|
return; // ON TARGET!
|
|
|
|
if (vector_to_target.x||vector_to_target.y||vector_to_target.z)
|
|
vector_to_target = vector_normal(vector_to_target);
|
|
else
|
|
vector_to_target.z = 1.0;
|
|
|
|
q_minarc = quaternion_rotation_between(v_forward, vector_to_target);
|
|
|
|
orientation = quaternion_multiply(q_minarc, orientation);
|
|
[self orientationChanged];
|
|
|
|
flightRoll = 0.0;
|
|
flightPitch = 0.0;
|
|
}
|
|
|
|
|
|
- (double) ballTrackLeadingTarget:(double) delta_t
|
|
{
|
|
Vector vector_to_target;
|
|
Vector axis_to_track_by;
|
|
Vector my_position = position; // position relative to parent
|
|
Vector my_aim = vector_forward_from_quaternion(orientation);
|
|
Vector my_ref = reference;
|
|
double aim_cos, ref_cos;
|
|
Entity *target = [self primaryTarget];
|
|
Vector leading = [target velocity];
|
|
Entity *last = nil;
|
|
Entity *father = [self parentEntity];
|
|
OOMatrix r_mat;
|
|
|
|
while ((father)&&(father != last) && (father != NO_TARGET))
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
my_position = vector_add(OOVectorMultiplyMatrix(my_position, r_mat), [father position]);
|
|
my_ref = OOVectorMultiplyMatrix(my_ref, r_mat);
|
|
last = father;
|
|
if (![last isSubEntity]) break;
|
|
father = [father owner];
|
|
}
|
|
|
|
if (target)
|
|
{
|
|
vector_to_target = vector_subtract([target position], my_position);
|
|
float lead = magnitude(vector_to_target) / TURRET_SHOT_SPEED;
|
|
|
|
vector_to_target = vector_add(vector_to_target, vector_multiply_scalar(leading, lead));
|
|
vector_to_target = vector_normal_or_fallback(vector_to_target, kBasisZVector);
|
|
|
|
// do the tracking!
|
|
aim_cos = dot_product(vector_to_target, my_aim);
|
|
ref_cos = dot_product(vector_to_target, my_ref);
|
|
}
|
|
else
|
|
{
|
|
aim_cos = 0.0;
|
|
ref_cos = -1.0;
|
|
}
|
|
|
|
if (ref_cos > TURRET_MINIMUM_COS) // target is forward of self
|
|
{
|
|
axis_to_track_by = cross_product(vector_to_target, my_aim);
|
|
}
|
|
else
|
|
{
|
|
aim_cos = 0.0;
|
|
axis_to_track_by = cross_product(my_ref, my_aim); // return to center
|
|
}
|
|
|
|
quaternion_rotate_about_axis(&orientation, axis_to_track_by, thrust * delta_t);
|
|
[self orientationChanged];
|
|
|
|
[self setStatus:STATUS_ACTIVE];
|
|
|
|
return aim_cos;
|
|
}
|
|
|
|
|
|
- (double) trackPrimaryTarget:(double) delta_t :(BOOL) retreat
|
|
{
|
|
Entity* target = [self primaryTarget];
|
|
|
|
if (!target) // leave now!
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget]; // NOTE: was AI message: rather than reactToMessage:
|
|
return 0.0;
|
|
}
|
|
|
|
if (scanClass == CLASS_MISSILE)
|
|
return [self missileTrackPrimaryTarget: delta_t];
|
|
|
|
GLfloat d_forward, d_up, d_right;
|
|
|
|
Vector relPos = vector_subtract(target->position, position);
|
|
double range2 = magnitude2(relPos);
|
|
|
|
if (range2 > SCANNER_MAX_RANGE2)
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget]; // NOTE: was AI message: rather than reactToMessage:
|
|
return 0.0;
|
|
}
|
|
|
|
//jink if retreating
|
|
if (retreat && (range2 > 250000.0)) // don't jink if closer than 500m - just RUN
|
|
{
|
|
Vector vx, vy, vz;
|
|
if (target->isShip)
|
|
{
|
|
ShipEntity* targetShip = (ShipEntity*)target;
|
|
vx = targetShip->v_right;
|
|
vy = targetShip->v_up;
|
|
vz = targetShip->v_forward;
|
|
}
|
|
else
|
|
{
|
|
Quaternion q = target->orientation;
|
|
vx = vector_right_from_quaternion(q);
|
|
vy = vector_up_from_quaternion(q);
|
|
vz = vector_forward_from_quaternion(q);
|
|
}
|
|
relPos.x += jink.x * vx.x + jink.y * vy.x + jink.z * vz.x;
|
|
relPos.y += jink.x * vx.y + jink.y * vy.y + jink.z * vz.y;
|
|
relPos.z += jink.x * vx.z + jink.y * vy.z + jink.z * vz.z;
|
|
}
|
|
|
|
if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
|
|
else relPos.z = 1.0;
|
|
|
|
double targetRadius = 0.75 * target->collision_radius;
|
|
|
|
double max_cos = sqrt(1 - targetRadius*targetRadius/range2);
|
|
|
|
double rate2 = 4.0 * delta_t;
|
|
double rate1 = 2.0 * delta_t;
|
|
|
|
double stick_roll = 0.0; //desired roll and pitch
|
|
double stick_pitch = 0.0;
|
|
|
|
double reverse = (retreat)? -1.0: 1.0;
|
|
|
|
double min_d = 0.004;
|
|
|
|
d_right = dot_product(relPos, v_right);
|
|
d_up = dot_product(relPos, v_up);
|
|
d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target
|
|
|
|
if (d_forward * reverse > max_cos) // on_target!
|
|
return d_forward;
|
|
|
|
// begin rule-of-thumb manoeuvres
|
|
stick_pitch = 0.0;
|
|
stick_roll = 0.0;
|
|
|
|
|
|
if ((reverse * d_forward < -0.5) && !pitching_over) // we're going the wrong way!
|
|
pitching_over = YES;
|
|
|
|
if (pitching_over)
|
|
{
|
|
if (reverse * d_up > 0) // pitch up
|
|
stick_pitch = -max_flight_pitch;
|
|
else
|
|
stick_pitch = max_flight_pitch;
|
|
pitching_over = (reverse * d_forward < 0.707);
|
|
}
|
|
|
|
// check if we are flying toward the destination..
|
|
if ((d_forward < max_cos)||(retreat)) // not on course so we must adjust controls..
|
|
{
|
|
if (d_forward < -max_cos) // hack to avoid just flying away from the destination
|
|
{
|
|
d_up = min_d * 2.0;
|
|
}
|
|
|
|
if (d_up > min_d)
|
|
{
|
|
int factor = sqrt(fabs(d_right) / fabs(min_d));
|
|
if (factor > 8)
|
|
factor = 8;
|
|
if (d_right > min_d)
|
|
stick_roll = - max_flight_roll * reverse * 0.125 * factor;
|
|
if (d_right < -min_d)
|
|
stick_roll = + max_flight_roll * reverse * 0.125 * factor;
|
|
}
|
|
if (d_up < -min_d)
|
|
{
|
|
int factor = sqrt(fabs(d_right) / fabs(min_d));
|
|
if (factor > 8)
|
|
factor = 8;
|
|
if (d_right > min_d)
|
|
stick_roll = + max_flight_roll * reverse * 0.125 * factor;
|
|
if (d_right < -min_d)
|
|
stick_roll = - max_flight_roll * reverse * 0.125 * factor;
|
|
}
|
|
|
|
if (stick_roll == 0.0)
|
|
{
|
|
int factor = sqrt(fabs(d_up) / fabs(min_d));
|
|
if (factor > 8)
|
|
factor = 8;
|
|
if (d_up > min_d)
|
|
stick_pitch = - max_flight_pitch * reverse * 0.125 * factor;
|
|
if (d_up < -min_d)
|
|
stick_pitch = + max_flight_pitch * reverse * 0.125 * factor;
|
|
}
|
|
}
|
|
|
|
// end rule-of-thumb manoeuvres
|
|
|
|
// apply 'quick-stop' to roll and pitch adjustments
|
|
if (((stick_roll > 0.0)&&(flightRoll < 0.0))||((stick_roll < 0.0)&&(flightRoll > 0.0)))
|
|
rate1 *= 4.0; // much faster correction
|
|
if (((stick_pitch > 0.0)&&(flightPitch < 0.0))||((stick_pitch < 0.0)&&(flightPitch > 0.0)))
|
|
rate2 *= 4.0; // much faster correction
|
|
|
|
// apply stick movement limits
|
|
if (flightRoll < stick_roll - rate1)
|
|
stick_roll = flightRoll + rate1;
|
|
if (flightRoll > stick_roll + rate1)
|
|
stick_roll = flightRoll - rate1;
|
|
if (flightPitch < stick_pitch - rate2)
|
|
stick_pitch = flightPitch + rate2;
|
|
if (flightPitch > stick_pitch + rate2)
|
|
stick_pitch = flightPitch - rate2;
|
|
|
|
// apply stick to attitude control
|
|
flightRoll = stick_roll;
|
|
flightPitch = stick_pitch;
|
|
|
|
if (retreat)
|
|
d_forward *= d_forward; // make positive AND decrease granularity
|
|
|
|
if (d_forward < 0.0)
|
|
return 0.0;
|
|
|
|
if ((!flightRoll)&&(!flightPitch)) // no correction
|
|
return 1.0;
|
|
|
|
return d_forward;
|
|
}
|
|
|
|
|
|
- (double) missileTrackPrimaryTarget:(double) delta_t
|
|
{
|
|
Vector relPos;
|
|
GLfloat d_forward, d_up, d_right;
|
|
Entity *target = [self primaryTarget];
|
|
|
|
if (!target) // leave now!
|
|
return 0.0;
|
|
|
|
double damping = 0.5 * delta_t;
|
|
double rate2 = 4.0 * delta_t;
|
|
double rate1 = 2.0 * delta_t;
|
|
|
|
double stick_roll = 0.0; //desired roll and pitch
|
|
double stick_pitch = 0.0;
|
|
|
|
relPos = vector_subtract(target->position, position);
|
|
|
|
|
|
// Adjust missile course by taking into account target's velocity and missile
|
|
// accuracy. Modification on original code contributed by Cmdr James.
|
|
|
|
float missileSpeed = (float)[self speed];
|
|
|
|
// Avoid getting ourselves in a divide by zero situation by setting a missileSpeed
|
|
// low threshold. Arbitrarily chosen 0.01, since it seems to work quite well.
|
|
// Missile accuracy is already clamped within the 0.0 to 10.0 range at initialization,
|
|
// but doing these calculations every frame when accuracy equals 0.0 just wastes cycles.
|
|
if (missileSpeed > 0.01f && accuracy > 0.0f)
|
|
{
|
|
Vector leading = [target velocity];
|
|
float lead = magnitude(relPos) / missileSpeed;
|
|
|
|
// Adjust where we are going to take into account target's velocity.
|
|
// Use accuracy value to determine how well missile will track target.
|
|
relPos.x += (lead * leading.x * (accuracy / 10.0f));
|
|
relPos.y += (lead * leading.y * (accuracy / 10.0f));
|
|
relPos.z += (lead * leading.z * (accuracy / 10.0f));
|
|
}
|
|
|
|
if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
|
|
else relPos.z = 1.0;
|
|
|
|
d_right = dot_product(relPos, v_right); // = cosine of angle between angle to target and v_right
|
|
d_up = dot_product(relPos, v_up); // = cosine of angle between angle to target and v_up
|
|
d_forward = dot_product(relPos, v_forward); // = cosine of angle between angle to target and v_forward
|
|
|
|
// begin rule-of-thumb manoeuvres
|
|
|
|
stick_roll = 0.0;
|
|
|
|
if (pitching_over)
|
|
pitching_over = (stick_pitch != 0.0);
|
|
|
|
if ((d_forward < -pitch_tolerance) && (!pitching_over))
|
|
{
|
|
pitching_over = YES;
|
|
if (d_up >= 0)
|
|
stick_pitch = -max_flight_pitch;
|
|
if (d_up < 0)
|
|
stick_pitch = max_flight_pitch;
|
|
}
|
|
|
|
if (pitching_over)
|
|
{
|
|
pitching_over = (d_forward < 0.5);
|
|
}
|
|
else
|
|
{
|
|
stick_pitch = -max_flight_pitch * d_up;
|
|
stick_roll = -max_flight_roll * d_right;
|
|
}
|
|
|
|
// end rule-of-thumb manoeuvres
|
|
|
|
// apply damping
|
|
if (flightRoll < 0)
|
|
flightRoll += (flightRoll < -damping) ? damping : -flightRoll;
|
|
if (flightRoll > 0)
|
|
flightRoll -= (flightRoll > damping) ? damping : flightRoll;
|
|
if (flightPitch < 0)
|
|
flightPitch += (flightPitch < -damping) ? damping : -flightPitch;
|
|
if (flightPitch > 0)
|
|
flightPitch -= (flightPitch > damping) ? damping : flightPitch;
|
|
|
|
// apply stick movement limits
|
|
if (flightRoll + rate1 < stick_roll)
|
|
stick_roll = flightRoll + rate1;
|
|
if (flightRoll - rate1 > stick_roll)
|
|
stick_roll = flightRoll - rate1;
|
|
if (flightPitch + rate2 < stick_pitch)
|
|
stick_pitch = flightPitch + rate2;
|
|
if (flightPitch - rate2 > stick_pitch)
|
|
stick_pitch = flightPitch - rate2;
|
|
|
|
// apply stick to attitude
|
|
flightRoll = stick_roll;
|
|
flightPitch = stick_pitch;
|
|
|
|
//
|
|
// return target confidence 0.0 .. 1.0
|
|
//
|
|
if (d_forward < 0.0)
|
|
return 0.0;
|
|
return d_forward;
|
|
}
|
|
|
|
|
|
- (double) trackDestination:(double) delta_t :(BOOL) retreat
|
|
{
|
|
Vector relPos;
|
|
GLfloat d_forward, d_up, d_right;
|
|
|
|
BOOL we_are_docking = (nil != dockingInstructions);
|
|
|
|
double rate2 = 4.0 * delta_t;
|
|
double rate1 = 2.0 * delta_t;
|
|
|
|
double stick_roll = 0.0; //desired roll and pitch
|
|
double stick_pitch = 0.0;
|
|
|
|
double reverse = 1.0;
|
|
|
|
double min_d = 0.004;
|
|
double max_cos = 0.995; // was 0.85; should match default value of max_cos in behaviour_fly_to_destination!
|
|
|
|
if (retreat)
|
|
reverse = -reverse;
|
|
|
|
if (isPlayer)
|
|
reverse = -reverse;
|
|
|
|
relPos = vector_subtract(destination, position);
|
|
double range2 = magnitude2(relPos);
|
|
double desired_range2 = desired_range*desired_range;
|
|
|
|
/* 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.
|
|
Because of the framerate most ships normally overshoot the target and they end up flying clearly on a path
|
|
through the sphere. Those ships give no problems, but ships with a very low turnrate will aim close to the surface and will than
|
|
have large trouble with reaching their destination. When those ships enter the slowdown range, they have almost no speed vector
|
|
in the direction of the target. I now used 95% of desired_range to aim at, but a smaller value might even be better.
|
|
*/
|
|
if (range2 > desired_range2) max_cos = sqrt(1 - 0.90 * desired_range2/range2); // Head for a point within 95% of desired_range.
|
|
|
|
if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos);
|
|
else relPos.z = 1.0;
|
|
|
|
d_right = dot_product(relPos, v_right);
|
|
d_up = dot_product(relPos, v_up);
|
|
d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target
|
|
|
|
// begin rule-of-thumb manoeuvres
|
|
stick_pitch = 0.0;
|
|
stick_roll = 0.0;
|
|
|
|
// check if we are flying toward the destination..
|
|
if ((d_forward < max_cos)||(retreat)) // not on course so we must adjust controls..
|
|
{
|
|
|
|
if (d_forward <= -max_cos) // hack to avoid just flying away from the destination
|
|
{
|
|
d_up = min_d * 2.0;
|
|
}
|
|
|
|
if (d_up > min_d)
|
|
{
|
|
int factor = sqrt(fabs(d_right) / fabs(min_d));
|
|
if (factor > 8)
|
|
factor = 8;
|
|
if (d_right > min_d)
|
|
stick_roll = - max_flight_roll * reverse * 0.125 * factor; //roll_roll * reverse;
|
|
if (d_right < -min_d)
|
|
stick_roll = + max_flight_roll * reverse * 0.125 * factor; //roll_roll * reverse;
|
|
}
|
|
if (d_up < -min_d)
|
|
{
|
|
int factor = sqrt(fabs(d_right) / fabs(min_d));
|
|
if (factor > 8)
|
|
factor = 8;
|
|
if (d_right > min_d)
|
|
stick_roll = + max_flight_roll * reverse * 0.125 * factor; //roll_roll * reverse;
|
|
if (d_right < -min_d)
|
|
stick_roll = - max_flight_roll * reverse * 0.125 * factor; //roll_roll * reverse;
|
|
}
|
|
|
|
if (stick_roll == 0.0)
|
|
{
|
|
int factor = sqrt(fabs(d_up) / fabs(min_d));
|
|
if (factor > 8)
|
|
factor = 8;
|
|
if (d_up > min_d)
|
|
stick_pitch = - max_flight_pitch * reverse * 0.125 * factor; //pitch_pitch * reverse;
|
|
if (d_up < -min_d)
|
|
stick_pitch = + max_flight_pitch * reverse * 0.125 * factor; //pitch_pitch * reverse;
|
|
}
|
|
}
|
|
|
|
if (we_are_docking && docking_match_rotation && (d_forward > max_cos))
|
|
{
|
|
/* we are docking and need to consider the rotation/orientation of the docking port */
|
|
StationEntity* station_for_docking = (StationEntity*)[UNIVERSE entityForUniversalID:targetStation];
|
|
|
|
if ((station_for_docking)&&(station_for_docking->isStation))
|
|
{
|
|
stick_roll = [self rollToMatchUp:[station_for_docking portUpVectorForShipsBoundingBox: boundingBox] rotating:[station_for_docking flightRoll]];
|
|
}
|
|
}
|
|
|
|
// end rule-of-thumb manoeuvres
|
|
|
|
// apply 'quick-stop' to roll and pitch adjustments
|
|
if (((stick_roll > 0.0)&&(flightRoll < 0.0))||((stick_roll < 0.0)&&(flightRoll > 0.0)))
|
|
rate1 *= 4.0; // much faster correction
|
|
if (((stick_pitch > 0.0)&&(flightPitch < 0.0))||((stick_pitch < 0.0)&&(flightPitch > 0.0)))
|
|
rate2 *= 4.0; // much faster correction
|
|
|
|
// apply stick movement limits
|
|
if (flightRoll < stick_roll - rate1)
|
|
stick_roll = flightRoll + rate1;
|
|
if (flightRoll > stick_roll + rate1)
|
|
stick_roll = flightRoll - rate1;
|
|
if (flightPitch < stick_pitch - rate2)
|
|
stick_pitch = flightPitch + rate2;
|
|
if (flightPitch > stick_pitch + rate2)
|
|
stick_pitch = flightPitch - rate2;
|
|
|
|
// apply stick to attitude control
|
|
flightRoll = stick_roll;
|
|
flightPitch = stick_pitch;
|
|
|
|
if (retreat)
|
|
d_forward *= d_forward; // make positive AND decrease granularity
|
|
|
|
if (d_forward < 0.0)
|
|
return 0.0;
|
|
|
|
if ((!flightRoll)&&(!flightPitch)) // no correction
|
|
return 1.0;
|
|
|
|
return d_forward;
|
|
}
|
|
|
|
|
|
- (GLfloat) rollToMatchUp:(Vector) up_vec rotating:(GLfloat) match_roll;
|
|
{
|
|
GLfloat cosTheta = dot_product(up_vec, v_up); // == cos of angle between up vectors
|
|
GLfloat sinTheta = dot_product(up_vec, v_right);
|
|
|
|
if (!isPlayer)
|
|
{
|
|
match_roll = -match_roll; // make necessary corrections for a different viewpoint
|
|
sinTheta = -sinTheta;
|
|
}
|
|
|
|
if (cosTheta < 0.0f)
|
|
{
|
|
cosTheta = -cosTheta;
|
|
sinTheta = -sinTheta;
|
|
}
|
|
|
|
if (sinTheta > 0.0f)
|
|
{
|
|
// increase roll rate
|
|
return cosTheta * cosTheta * match_roll + sinTheta * sinTheta * max_flight_roll;
|
|
}
|
|
else
|
|
{
|
|
// decrease roll rate
|
|
return cosTheta * cosTheta * match_roll - sinTheta * sinTheta * max_flight_roll;
|
|
}
|
|
}
|
|
|
|
|
|
- (GLfloat) rangeToDestination
|
|
{
|
|
return sqrtf(distance2(position, destination));
|
|
}
|
|
|
|
|
|
- (double) rangeToPrimaryTarget
|
|
{
|
|
double dist;
|
|
Vector delta;
|
|
Entity *target = [self primaryTarget];
|
|
if (target == nil) // leave now!
|
|
return 0.0;
|
|
delta = target->position;
|
|
delta.x -= position.x;
|
|
delta.y -= position.y;
|
|
delta.z -= position.z;
|
|
dist = sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z);
|
|
dist -= target->collision_radius;
|
|
dist -= collision_radius;
|
|
return dist;
|
|
}
|
|
|
|
|
|
- (BOOL) onTarget:(BOOL) fwd_weapon
|
|
{
|
|
GLfloat d2, radius, dq, astq;
|
|
Vector rel_pos, urp;
|
|
int weapon_type = (fwd_weapon)? forward_weapon_type : aft_weapon_type;
|
|
if (weapon_type == WEAPON_THARGOID_LASER)
|
|
{
|
|
return (randf() < 0.05); // one in twenty shots on target
|
|
}
|
|
|
|
Entity *target = [self primaryTarget];
|
|
if (target == nil) return NO;
|
|
if ([target status] == STATUS_DEAD) return NO;
|
|
|
|
if (isSunlit && (target->isSunlit == NO) && (randf() < 0.75))
|
|
return NO; // 3/4 of the time you can't see from a lit place into a darker place
|
|
radius = target->collision_radius;
|
|
rel_pos = target->position;
|
|
rel_pos.x -= position.x;
|
|
rel_pos.y -= position.y;
|
|
rel_pos.z -= position.z;
|
|
d2 = magnitude2(rel_pos);
|
|
if (d2)
|
|
urp = vector_normal(rel_pos);
|
|
else
|
|
urp = make_vector(0, 0, 1);
|
|
dq = dot_product(urp, v_forward); // cosine of angle between v_forward and unit relative position
|
|
if (((fwd_weapon)&&(dq < 0.0)) || ((!fwd_weapon)&&(dq > 0.0)))
|
|
return NO;
|
|
|
|
astq = sqrt(1.0 - radius * radius / d2); // cosine of half angle subtended by target
|
|
|
|
return (fabs(dq) >= astq);
|
|
}
|
|
|
|
|
|
- (BOOL) fireMainWeapon:(double) range
|
|
{
|
|
//
|
|
// set the values for the forward weapon
|
|
//
|
|
[self setWeaponDataFromType:forward_weapon_type];
|
|
weapon_energy = OOClamp_0_max_f([shipinfoDictionary oo_floatForKey:@"weapon_energy" defaultValue:weapon_energy],50.0);
|
|
|
|
if ([self shotTime] < weapon_recharge_rate)
|
|
return NO;
|
|
|
|
if (range > randf() * weaponRange * accuracy)
|
|
return NO;
|
|
if (range > weaponRange)
|
|
return NO;
|
|
if (![self onTarget:YES])
|
|
return NO;
|
|
|
|
//
|
|
|
|
BOOL fired = NO;
|
|
switch (forward_weapon_type)
|
|
{
|
|
case WEAPON_PLASMA_CANNON :
|
|
[self firePlasmaShotAtOffset:0.0 speed:NPC_PLASMA_SPEED color:[OOColor yellowColor]];
|
|
fired = YES;
|
|
break;
|
|
|
|
case WEAPON_PULSE_LASER :
|
|
case WEAPON_BEAM_LASER :
|
|
case WEAPON_MINING_LASER :
|
|
case WEAPON_MILITARY_LASER :
|
|
[self fireLaserShotInDirection: VIEW_FORWARD];
|
|
fired = YES;
|
|
break;
|
|
|
|
case WEAPON_THARGOID_LASER :
|
|
[self fireDirectLaserShot];
|
|
fired = YES;
|
|
break;
|
|
|
|
case WEAPON_NONE:
|
|
case WEAPON_UNDEFINED:
|
|
// Do nothing
|
|
break;
|
|
}
|
|
|
|
//can we fire lasers from our subentities?
|
|
NSEnumerator *subEnum = nil;
|
|
ShipEntity *se = nil;
|
|
for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
if ([se fireSubentityLaserShot:range]) fired = YES;
|
|
}
|
|
|
|
if (fired && cloaking_device_active && cloakPassive)
|
|
{
|
|
[self deactivateCloakingDevice];
|
|
}
|
|
|
|
return fired;
|
|
}
|
|
|
|
|
|
- (BOOL) fireAftWeapon:(double) range
|
|
{
|
|
BOOL result = YES;
|
|
//
|
|
// save the existing weapon values
|
|
//
|
|
double weapon_energy1 = weapon_energy;
|
|
double weapon_recharge_rate1 = weapon_recharge_rate;
|
|
double weapon_range1 = weaponRange;
|
|
//
|
|
// set new values from aft_weapon_type
|
|
//
|
|
[self setWeaponDataFromType:aft_weapon_type];
|
|
|
|
if ([self shotTime] < weapon_recharge_rate)
|
|
return NO;
|
|
if (![self onTarget:NO])
|
|
return NO;
|
|
if (range > randf() * weaponRange)
|
|
return NO;
|
|
|
|
if (result)
|
|
{
|
|
switch (aft_weapon_type)
|
|
{
|
|
case WEAPON_PULSE_LASER :
|
|
case WEAPON_BEAM_LASER :
|
|
case WEAPON_MINING_LASER :
|
|
case WEAPON_MILITARY_LASER :
|
|
[self fireLaserShotInDirection:VIEW_AFT];
|
|
break;
|
|
case WEAPON_THARGOID_LASER :
|
|
[self fireDirectLaserShot];
|
|
return YES;
|
|
break;
|
|
|
|
case WEAPON_PLASMA_CANNON: // FIXME: NPCs can't have rear plasma cannons, for no obvious reason.
|
|
case WEAPON_UNDEFINED:
|
|
case WEAPON_NONE:
|
|
// do nothing
|
|
break;
|
|
}
|
|
}
|
|
|
|
// restore previous values
|
|
weapon_energy = weapon_energy1;
|
|
weapon_recharge_rate = weapon_recharge_rate1;
|
|
weaponRange = weapon_range1;
|
|
//
|
|
return result;
|
|
}
|
|
|
|
|
|
- (OOTimeDelta) shotTime
|
|
{
|
|
return shot_time;
|
|
}
|
|
|
|
|
|
- (void) resetShotTime
|
|
{
|
|
shot_time = 0.0;
|
|
}
|
|
|
|
|
|
- (BOOL) fireTurretCannon:(double) range
|
|
{
|
|
if ([self shotTime] < weapon_recharge_rate)
|
|
return NO;
|
|
if (range > TURRET_SHOT_RANGE * 1.01) // 1% more than max range - open up just slightly early
|
|
return NO;
|
|
|
|
Vector origin = position;
|
|
Entity *last = nil;
|
|
Entity *father = [self parentEntity];
|
|
OOMatrix r_mat;
|
|
Vector vel;
|
|
|
|
while ((father)&&(father != last) && (father != NO_TARGET))
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
origin = vector_add(OOVectorMultiplyMatrix(origin, r_mat), [father position]);
|
|
last = father;
|
|
if (![last isSubEntity]) break;
|
|
father = [father owner];
|
|
}
|
|
|
|
vel = vector_forward_from_quaternion(orientation); // Facing
|
|
origin = vector_add(origin, vector_multiply_scalar(vel, collision_radius + 0.5)); // Start just outside collision sphere
|
|
vel = vector_multiply_scalar(vel, TURRET_SHOT_SPEED); // Shot velocity
|
|
|
|
OOPlasmaShotEntity *shot = [[OOPlasmaShotEntity alloc] initWithPosition:origin
|
|
velocity:vel
|
|
energy:weapon_energy
|
|
duration:TURRET_SHOT_DURATION
|
|
color:laser_color];
|
|
|
|
[shot autorelease];
|
|
[UNIVERSE addEntity:shot];
|
|
[shot setOwner:[self rootShipEntity]]; // has to be done AFTER adding shot to the UNIVERSE
|
|
|
|
[self resetShotTime];
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (void) setLaserColor:(OOColor *) color
|
|
{
|
|
if (color)
|
|
{
|
|
[laser_color release];
|
|
laser_color = [color retain];
|
|
}
|
|
}
|
|
|
|
|
|
- (OOColor *)laserColor
|
|
{
|
|
return [[laser_color retain] autorelease];
|
|
}
|
|
|
|
|
|
- (BOOL) fireSubentityLaserShot: (double) range
|
|
{
|
|
ParticleEntity *shot;
|
|
int direction = VIEW_FORWARD;
|
|
GLfloat hit_at_range;
|
|
target_laser_hit = NO_TARGET;
|
|
|
|
if (forward_weapon_type == WEAPON_NONE)
|
|
return NO;
|
|
[self setWeaponDataFromType:forward_weapon_type];
|
|
|
|
ShipEntity* parent = (ShipEntity*)[self owner];
|
|
|
|
if ([self shotTime] < weapon_recharge_rate)
|
|
return NO;
|
|
|
|
if (range > weaponRange)
|
|
return NO;
|
|
|
|
hit_at_range = weaponRange;
|
|
target_laser_hit = [UNIVERSE getFirstEntityHitByLaserFromEntity:self inView:direction offset: make_vector(0,0,0) rangeFound: &hit_at_range];
|
|
|
|
shot = [[ParticleEntity alloc] initLaserFromShip:self view:direction offset:kZeroVector];
|
|
[shot setColor:laser_color];
|
|
[shot setScanClass: CLASS_NO_DRAW];
|
|
ShipEntity *victim = [UNIVERSE entityForUniversalID:target_laser_hit];
|
|
if ([victim isShip])
|
|
{
|
|
ShipEntity *subent = [victim subEntityTakingDamage];
|
|
if (subent && [victim isFrangible])
|
|
{
|
|
// do 1% bleed-through damage...
|
|
[victim takeEnergyDamage: 0.01 * weapon_energy from:subent becauseOf: parent];
|
|
victim = subent;
|
|
}
|
|
|
|
if (hit_at_range < weaponRange)
|
|
{
|
|
[victim takeEnergyDamage:weapon_energy from:self becauseOf: parent]; // a very palpable hit
|
|
|
|
[shot setCollisionRadius: hit_at_range];
|
|
Vector vd = vector_forward_from_quaternion([shot orientation]);
|
|
Vector flash_pos = vector_add([shot position], vector_multiply_scalar(vd, hit_at_range));
|
|
OOFlashEffectEntity *laserFlash = [[OOFlashEffectEntity alloc] initLaserFlashWithPosition:flash_pos color:laser_color];
|
|
[laserFlash setVelocity:[victim velocity]];
|
|
[UNIVERSE addEntity:laserFlash];
|
|
[laserFlash release];
|
|
}
|
|
}
|
|
[UNIVERSE addEntity:shot];
|
|
[shot release];
|
|
|
|
[self resetShotTime];
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) fireDirectLaserShot
|
|
{
|
|
GLfloat hit_at_range;
|
|
Entity* my_target = [self primaryTarget];
|
|
if (!my_target)
|
|
return NO;
|
|
ParticleEntity* shot;
|
|
double range_limit2 = weaponRange*weaponRange;
|
|
Vector r_pos = my_target->position;
|
|
r_pos.x -= position.x; r_pos.y -= position.y; r_pos.z -= position.z;
|
|
if (r_pos.x||r_pos.y||r_pos.z)
|
|
r_pos = vector_normal(r_pos);
|
|
else
|
|
r_pos.z = 1.0;
|
|
|
|
Quaternion q_laser = quaternion_rotation_between(r_pos, make_vector(0.0f,0.0f,1.0f));
|
|
q_laser.x += 0.01 * (randf() - 0.5); // randomise aim a little (+/- 0.005)
|
|
q_laser.y += 0.01 * (randf() - 0.5);
|
|
q_laser.z += 0.01 * (randf() - 0.5);
|
|
quaternion_normalize(&q_laser);
|
|
|
|
Quaternion q_save = orientation; // save rotation
|
|
orientation = q_laser; // face in direction of laser
|
|
target_laser_hit = [UNIVERSE getFirstEntityHitByLaserFromEntity:self inView:VIEW_FORWARD offset: make_vector(0,0,0) rangeFound: &hit_at_range];
|
|
orientation = q_save; // restore rotation
|
|
|
|
Vector vel = make_vector(v_forward.x * flightSpeed, v_forward.y * flightSpeed, v_forward.z * flightSpeed);
|
|
|
|
// do special effects laser line
|
|
shot = [[ParticleEntity alloc] initLaserFromShip:self view:VIEW_FORWARD offset:kZeroVector];
|
|
[shot setColor:laser_color];
|
|
[shot setScanClass: CLASS_NO_DRAW];
|
|
[shot setPosition: position];
|
|
[shot setOrientation: q_laser];
|
|
[shot setVelocity: vel];
|
|
ShipEntity *victim = [UNIVERSE entityForUniversalID:target_laser_hit];
|
|
if ([victim isShip])
|
|
{
|
|
ShipEntity *subent = [victim subEntityTakingDamage];
|
|
if (subent != nil && [victim isFrangible])
|
|
{
|
|
// do 1% bleed-through damage...
|
|
[victim takeEnergyDamage: 0.01 * weapon_energy from:subent becauseOf:self];
|
|
victim = subent;
|
|
}
|
|
|
|
if (hit_at_range * hit_at_range < range_limit2)
|
|
{
|
|
[victim takeEnergyDamage:weapon_energy from:self becauseOf:self]; // a very palpable hit
|
|
|
|
[shot setCollisionRadius: hit_at_range];
|
|
Vector vd = vector_forward_from_quaternion([shot orientation]);
|
|
Vector flash_pos = vector_add([shot position], vector_multiply_scalar(vd, hit_at_range));
|
|
OOFlashEffectEntity *laserFlash = [[OOFlashEffectEntity alloc] initLaserFlashWithPosition:flash_pos color:laser_color];
|
|
[laserFlash setVelocity:[victim velocity]];
|
|
[UNIVERSE addEntity:laserFlash];
|
|
[laserFlash release];
|
|
}
|
|
}
|
|
[UNIVERSE addEntity:shot];
|
|
[shot release];
|
|
|
|
[self resetShotTime];
|
|
|
|
// random laser over-heating for AI ships
|
|
if ((!isPlayer)&&((ranrot_rand() & 255) < weapon_energy)&&(![self isMining]))
|
|
{
|
|
shot_time -= (randf() * weapon_energy);
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) fireLaserShotInDirection: (OOViewID) direction
|
|
{
|
|
ParticleEntity *shot;
|
|
double range_limit2 = weaponRange*weaponRange;
|
|
GLfloat hit_at_range;
|
|
Vector vel;
|
|
target_laser_hit = NO_TARGET;
|
|
|
|
vel.x = v_forward.x * flightSpeed;
|
|
vel.y = v_forward.y * flightSpeed;
|
|
vel.z = v_forward.z * flightSpeed;
|
|
|
|
Vector laserPortOffset;
|
|
|
|
switch(direction)
|
|
{
|
|
case VIEW_AFT:
|
|
laserPortOffset = aftWeaponOffset;
|
|
break;
|
|
case VIEW_PORT:
|
|
laserPortOffset = portWeaponOffset;
|
|
break;
|
|
case VIEW_STARBOARD:
|
|
laserPortOffset = starboardWeaponOffset;
|
|
break;
|
|
default:
|
|
laserPortOffset = forwardWeaponOffset;
|
|
}
|
|
|
|
target_laser_hit = [UNIVERSE getFirstEntityHitByLaserFromEntity:self inView:direction offset:laserPortOffset rangeFound: &hit_at_range];
|
|
|
|
shot = [[ParticleEntity alloc] initLaserFromShip:self view:direction offset:laserPortOffset]; // alloc retains!
|
|
|
|
[shot setColor:laser_color];
|
|
[shot setScanClass: CLASS_NO_DRAW];
|
|
[shot setVelocity: vel];
|
|
ShipEntity *victim = [UNIVERSE entityForUniversalID:target_laser_hit];
|
|
if ([victim isShip])
|
|
{
|
|
/* CRASH in [victim->sub_entities containsObject:subent] here (1.69, OS X/x86).
|
|
Analysis: Crash is in _freedHandler called from CFEqual, indicating either a dead
|
|
object in victim->sub_entities or dead victim->subentity_taking_damage. I suspect
|
|
the latter. Probable solution: dying subentities must cause parent to clean up
|
|
properly. This was probably obscured by the entity recycling scheme in the past.
|
|
Fix: made subentity_taking_damage a weak reference accessed via a method.
|
|
-- Ahruman 20070706, 20080304
|
|
*/
|
|
ShipEntity *subent = [victim subEntityTakingDamage];
|
|
if (subent != nil && [victim isFrangible])
|
|
{
|
|
// do 1% bleed-through damage...
|
|
[victim takeEnergyDamage: 0.01 * weapon_energy from:subent becauseOf:self];
|
|
victim = subent;
|
|
}
|
|
|
|
if (hit_at_range * hit_at_range < range_limit2)
|
|
{
|
|
[victim takeEnergyDamage:weapon_energy from:self becauseOf:self]; // a very palpable hit
|
|
|
|
[shot setCollisionRadius:hit_at_range];
|
|
Vector vd = vector_forward_from_quaternion([shot orientation]);
|
|
Vector flash_pos = vector_add([shot position], vector_multiply_scalar(vd, hit_at_range));
|
|
OOFlashEffectEntity *laserFlash = [[OOFlashEffectEntity alloc] initLaserFlashWithPosition:flash_pos color:laser_color];
|
|
[laserFlash setVelocity:[victim velocity]];
|
|
[UNIVERSE addEntity:laserFlash];
|
|
[laserFlash release];
|
|
}
|
|
}
|
|
[UNIVERSE addEntity:shot];
|
|
[shot release]; //release
|
|
|
|
[self resetShotTime];
|
|
|
|
// random laser over-heating for AI ships
|
|
if ((!isPlayer)&&((ranrot_rand() & 255) < weapon_energy)&&(![self isMining]))
|
|
{
|
|
shot_time -= (randf() * weapon_energy);
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void) throwSparks
|
|
{
|
|
OOSparkEntity *spark = nil;
|
|
Vector vel;
|
|
Vector origin = position;
|
|
|
|
GLfloat lr = randf() * (boundingBox.max.x - boundingBox.min.x) + boundingBox.min.x;
|
|
GLfloat ud = randf() * (boundingBox.max.y - boundingBox.min.y) + boundingBox.min.y;
|
|
GLfloat fb = randf() * boundingBox.max.z + boundingBox.min.z; // rear section only
|
|
|
|
origin.x += fb * v_forward.x;
|
|
origin.y += fb * v_forward.y;
|
|
origin.z += fb * v_forward.z;
|
|
|
|
origin.x += ud * v_up.x;
|
|
origin.y += ud * v_up.y;
|
|
origin.z += ud * v_up.z;
|
|
|
|
origin.x += lr * v_right.x;
|
|
origin.y += lr * v_right.y;
|
|
origin.z += lr * v_right.z;
|
|
|
|
float w = boundingBox.max.x - boundingBox.min.x;
|
|
float h = boundingBox.max.y - boundingBox.min.y;
|
|
float m = (w < h) ? 0.25 * w: 0.25 * h;
|
|
|
|
float sz = m * (1 + randf() + randf()); // half minimum dimension on average
|
|
|
|
vel = make_vector(2.0 * (origin.x - position.x), 2.0 * (origin.y - position.y), 2.0 * (origin.z - position.z));
|
|
|
|
OOColor *color = [OOColor colorWithCalibratedHue:0.08 + 0.17 * randf() saturation:1.0 brightness:1.0 alpha:1.0];
|
|
|
|
spark = [[OOSparkEntity alloc] initWithPosition:origin
|
|
velocity:vel
|
|
duration:2.0 + 3.0 * randf()
|
|
size:sz
|
|
color:color];
|
|
|
|
[spark setOwner:self];
|
|
[UNIVERSE addEntity:spark];
|
|
[spark release];
|
|
|
|
next_spark_time = randf();
|
|
}
|
|
|
|
|
|
- (BOOL) firePlasmaShotAtOffset:(double)offset speed:(double)speed color:(OOColor *)color
|
|
{
|
|
Vector vel, rt;
|
|
Vector origin = position;
|
|
double start = collision_radius + 0.5;
|
|
|
|
speed += flightSpeed;
|
|
|
|
if (++shot_counter % 2)
|
|
offset = -offset;
|
|
|
|
vel = v_forward;
|
|
rt = v_right;
|
|
|
|
if (isPlayer) // player can fire into multiple views!
|
|
{
|
|
switch ([UNIVERSE viewDirection])
|
|
{
|
|
case VIEW_AFT :
|
|
vel = vector_flip(v_forward);
|
|
rt = vector_flip(v_right);
|
|
break;
|
|
case VIEW_STARBOARD :
|
|
vel = v_right;
|
|
rt = vector_flip(v_forward);
|
|
break;
|
|
case VIEW_PORT :
|
|
vel = vector_flip(v_right);
|
|
rt = v_forward;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
origin = vector_add(origin, vector_multiply_scalar(vel, start));
|
|
origin = vector_add(origin, vector_multiply_scalar(rt, offset));
|
|
|
|
vel = vector_multiply_scalar(vel, speed);
|
|
|
|
OOPlasmaShotEntity *shot = [[OOPlasmaShotEntity alloc] initWithPosition:origin
|
|
velocity:vel
|
|
energy:weapon_energy
|
|
duration:MAIN_PLASMA_DURATION
|
|
color:color];
|
|
|
|
[UNIVERSE addEntity:shot];
|
|
[shot setOwner:[self rootShipEntity]];
|
|
[shot release];
|
|
|
|
[self resetShotTime];
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (ShipEntity *) fireMissile
|
|
{
|
|
return [self fireMissileWithIdentifier:nil andTarget:[self primaryTarget]];
|
|
}
|
|
|
|
|
|
- (ShipEntity *) fireMissileWithIdentifier:(NSString *) identifier andTarget:(Entity *) target
|
|
{
|
|
// both players and NPCs!
|
|
//
|
|
ShipEntity *missile = nil;
|
|
ShipEntity *target_ship = nil;
|
|
|
|
Vector vel;
|
|
Vector origin = position;
|
|
Vector start, v_eject;
|
|
|
|
if ([UNIVERSE getTime] < missile_launch_time) return nil;
|
|
|
|
// default launching position
|
|
start.x = 0.0f; // in the middle
|
|
start.y = boundingBox.min.y - 4.0f; // 4m below bounding box
|
|
start.z = boundingBox.max.z + 1.0f; // 1m ahead of bounding box
|
|
// custom launching position
|
|
ScanVectorFromString([shipinfoDictionary objectForKey:@"missile_launch_position"], &start);
|
|
|
|
double throw_speed = 250.0f;
|
|
Quaternion q1 = orientation;
|
|
if (isPlayer) q1.w = -q1.w; // player view is reversed!
|
|
|
|
if ((missiles <= 0)||(target == nil)||(target->scanClass == CLASS_NO_DRAW)) // no missile lock!
|
|
return nil;
|
|
|
|
if ([target isShip])
|
|
{
|
|
target_ship = (ShipEntity*)target;
|
|
if ([target_ship isCloaked]) return nil;
|
|
if (![self hasMilitaryScannerFilter] && [target_ship isJammingScanning]) return nil;
|
|
}
|
|
|
|
unsigned i;
|
|
if (identifier == nil)
|
|
{
|
|
// use a random missile from the list
|
|
i = floor(randf()*(double)missiles);
|
|
identifier = [missile_list[i] identifier];
|
|
missile = [UNIVERSE newShipWithRole:identifier];
|
|
if (missile == nil) // invalid missile role.
|
|
{
|
|
// remove that invalid missile role from the missiles list.
|
|
while ( ++i < missiles ) missile_list[i - 1] = missile_list[i];
|
|
missiles--;
|
|
}
|
|
}
|
|
else
|
|
missile = [UNIVERSE newShipWithRole:identifier];
|
|
|
|
if (missile == nil) return nil;
|
|
|
|
// By definition, the player will always have the specified missile.
|
|
// What if the NPC didn't actually have the specified missile to begin with?
|
|
if (!isPlayer && ![self removeExternalStore:[OOEquipmentType equipmentTypeWithIdentifier:identifier]])
|
|
{
|
|
[missile release];
|
|
return nil;
|
|
}
|
|
|
|
double mcr = missile->collision_radius;
|
|
v_eject = vector_normal(start);
|
|
vel = kZeroVector; // starting velocity
|
|
|
|
// check if start is within bounding box...
|
|
while ( (start.x > boundingBox.min.x - mcr)&&(start.x < boundingBox.max.x + mcr)&&
|
|
(start.y > boundingBox.min.y - mcr)&&(start.y < boundingBox.max.y + mcr)&&
|
|
(start.z > boundingBox.min.z - mcr)&&(start.z < boundingBox.max.z + mcr) )
|
|
{
|
|
start = vector_add(start, vector_multiply_scalar(v_eject, mcr));
|
|
|
|
// Kept different vel calculations for player & NPCs. Is there an acutal reason for that difference? - Kaks 20091204
|
|
if (!isPlayer) vel = vector_add(vel, vector_multiply_scalar(v_eject, 10.0f * mcr)); // throw it outward a bit harder
|
|
}
|
|
|
|
// Kept different vel calculations for player & NPCs. Is there an acutal reason for that difference? - Kaks 20091204
|
|
if (isPlayer) vel = vector_multiply_scalar(v_forward, flightSpeed + throw_speed);
|
|
else vel = vector_add(vel, vector_multiply_scalar(v_forward, flightSpeed + throw_speed));
|
|
|
|
|
|
origin.x = position.x + v_right.x * start.x + v_up.x * start.y + v_forward.x * start.z;
|
|
origin.y = position.y + v_right.y * start.x + v_up.y * start.y + v_forward.y * start.z;
|
|
origin.z = position.z + v_right.z * start.x + v_up.z * start.y + v_forward.z * start.z;
|
|
|
|
if (isPlayer) [missile setScanClass: CLASS_MISSILE];
|
|
|
|
// special cases
|
|
|
|
//We don't want real missiles in a group. Missiles could become escorts when the group is also used as escortGroup.
|
|
if ([missile scanClass] == CLASS_THARGOID)
|
|
{
|
|
if([self group] == nil) [self setGroup:[OOShipGroup groupWithName:@"thargoid group"]];
|
|
[missile setGroup:[self group]];
|
|
}
|
|
|
|
// is this a submunition?
|
|
if (![self isMissileFlagSet]) [missile setOwner:self];
|
|
else [missile setOwner:[self owner]];
|
|
|
|
// end special cases
|
|
|
|
[missile setPosition:origin];
|
|
[missile addTarget:target];
|
|
[missile setOrientation:q1];
|
|
//[missile setStatus:STATUS_IN_FLIGHT]; // necessary to get it going!
|
|
[missile setIsMissileFlag:YES];
|
|
[missile setVelocity:vel];
|
|
[missile setSpeed:150.0f];
|
|
[missile setDistanceTravelled:0.0f];
|
|
[missile resetShotTime];
|
|
missile_launch_time = [UNIVERSE getTime] + missile_load_time; // set minimum launchtime for the next missile.
|
|
|
|
[UNIVERSE addEntity:missile]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
[missile release]; //release
|
|
|
|
// missile lives on after UNIVERSE addEntity
|
|
if ([missile scanClass] == CLASS_MISSILE && [target isShip])
|
|
{
|
|
[self doScriptEvent:@"shipFiredMissile" withArgument:missile andArgument:target_ship];
|
|
[target_ship setPrimaryAggressor:self];
|
|
[target_ship doScriptEvent:@"shipAttackedWithMissile" withArgument:missile andArgument:self];
|
|
[target_ship reactToAIMessage:@"INCOMING_MISSILE"];
|
|
}
|
|
|
|
if (cloaking_device_active && cloakPassive)
|
|
{
|
|
[self deactivateCloakingDevice];
|
|
}
|
|
|
|
return missile;
|
|
}
|
|
|
|
|
|
- (BOOL) isMissileFlagSet
|
|
{
|
|
return isMissile; // were we created using fireMissile? (for tracking submunitions)
|
|
}
|
|
|
|
|
|
- (void) setIsMissileFlag:(BOOL)newValue
|
|
{
|
|
isMissile = !!newValue; // set the isMissile flag, used for tracking submunitions
|
|
}
|
|
|
|
|
|
// Exposed to AI
|
|
- (BOOL) fireECM
|
|
{
|
|
if (![self hasECM]) return NO;
|
|
|
|
OOECMBlastEntity *ecmDevice = [[OOECMBlastEntity alloc] initFromShip:self];
|
|
[UNIVERSE addEntity:ecmDevice];
|
|
[ecmDevice release];
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) activateCloakingDevice
|
|
{
|
|
if (![self hasCloakingDevice]) return NO;
|
|
|
|
if (!cloaking_device_active) cloaking_device_active = (energy > CLOAKING_DEVICE_START_ENERGY * maxEnergy);
|
|
return cloaking_device_active;
|
|
}
|
|
|
|
|
|
- (void) deactivateCloakingDevice
|
|
{
|
|
cloaking_device_active = NO;
|
|
}
|
|
|
|
|
|
- (BOOL) launchEnergyBomb
|
|
{
|
|
if (![self hasEnergyBomb]) return NO;
|
|
[self setSpeed: maxFlightSpeed + 300];
|
|
ShipEntity* bomb = [UNIVERSE newShipWithRole:@"energy-bomb"];
|
|
if (bomb == nil) return NO;
|
|
|
|
[self removeEquipmentItem:@"EQ_ENERGY_BOMB"];
|
|
|
|
double start = collision_radius + bomb->collision_radius;
|
|
Quaternion random_direction;
|
|
Vector vel;
|
|
Vector rpos;
|
|
double random_roll = randf() - 0.5; // -0.5 to +0.5
|
|
double random_pitch = randf() - 0.5; // -0.5 to +0.5
|
|
quaternion_set_random(&random_direction);
|
|
|
|
rpos = vector_subtract([self position], vector_multiply_scalar(v_forward, start));
|
|
|
|
double eject_speed = -800.0;
|
|
vel = vector_multiply_scalar(v_forward, [self flightSpeed] + eject_speed);
|
|
eject_speed *= 0.5 * (randf() - 0.5); // -0.25x .. +0.25x
|
|
vel = vector_add(vel, vector_multiply_scalar(v_up, eject_speed));
|
|
eject_speed *= 0.5 * (randf() - 0.5); // -0.0625x .. +0.0625x
|
|
vel = vector_add(vel, vector_multiply_scalar(v_right, eject_speed));
|
|
|
|
[bomb setPosition:rpos];
|
|
[bomb setOrientation:random_direction];
|
|
[bomb setRoll:random_roll];
|
|
[bomb setPitch:random_pitch];
|
|
[bomb setVelocity:vel];
|
|
[bomb setScanClass:CLASS_MINE]; // TODO should be CLASS_ENERGY_BOMB
|
|
//[bomb setStatus:STATUS_IN_FLIGHT];
|
|
[bomb setEnergy:5.0]; // 5 second countdown
|
|
[bomb setBehaviour:BEHAVIOUR_ENERGY_BOMB_COUNTDOWN];
|
|
[bomb setOwner:self];
|
|
[UNIVERSE addEntity:bomb]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
//[[bomb getAI] setState:@"GLOBAL"];
|
|
[bomb release];
|
|
|
|
if (self != [PlayerEntity sharedPlayer]) // get the heck out of here
|
|
{
|
|
[self addTarget:bomb];
|
|
[self setBehaviour:BEHAVIOUR_FLEE_TARGET];
|
|
frustration = 0.0;
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (OOUniversalID)launchEscapeCapsule
|
|
{
|
|
OOUniversalID result = NO_TARGET;
|
|
ShipEntity *mainPod = nil;
|
|
unsigned n_pods, i;
|
|
|
|
/* BUG: player can't launch escape pod in interstellar space (because
|
|
there is no standard place for ressurection), but NPCs can.
|
|
FIX: don't let NPCs do it either. Submitted by Cmdr James.
|
|
-- Ahruman 20070822
|
|
*/
|
|
if ([UNIVERSE station] == nil) return NO_TARGET;
|
|
|
|
// check number of pods aboard -- require at least one.
|
|
n_pods = [shipinfoDictionary oo_unsignedIntForKey:@"has_escape_pod"];
|
|
|
|
if (crew) // transfer crew
|
|
{
|
|
// make sure crew inherit any legalStatus
|
|
for (i = 0; i < [crew count]; i++)
|
|
{
|
|
OOCharacter *ch = (OOCharacter*)[crew objectAtIndex:i];
|
|
[ch setLegalStatus: [self legalStatus] | [ch legalStatus]];
|
|
}
|
|
mainPod = [self launchPodWithCrew:crew];
|
|
if (mainPod)
|
|
{
|
|
result = [mainPod universalID];
|
|
[self setCrew:nil];
|
|
[self setHulk:YES]; //CmdrJames experiment with fixing ejection behaviour
|
|
}
|
|
}
|
|
|
|
// launch other pods (passengers)
|
|
for (i = 1; i < n_pods; i++)
|
|
{
|
|
Random_Seed orig = [UNIVERSE systemSeedForSystemNumber:gen_rnd_number()];
|
|
[self launchPodWithCrew:[NSArray arrayWithObject:[OOCharacter randomCharacterWithRole:@"passenger" andOriginalSystem:orig]]];
|
|
}
|
|
|
|
if (mainPod) [self doScriptEvent:@"shipLaunchedEscapePod" withArgument:mainPod];
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
// This is a documented AI method; do not change semantics. (Note: AIs don't have access to the return value.)
|
|
- (OOCargoType) dumpCargo
|
|
{
|
|
ShipEntity *jetto = [self dumpCargoItem];
|
|
if (jetto != nil) return [jetto commodityType];
|
|
else return CARGO_NOT_CARGO;
|
|
}
|
|
|
|
|
|
- (ShipEntity *) dumpCargoItem
|
|
{
|
|
ShipEntity *jetto = nil;
|
|
|
|
if (([cargo count] > 0)&&([UNIVERSE getTime] - cargo_dump_time > 0.5)) // space them 0.5s or 10m apart
|
|
{
|
|
jetto = [[[cargo objectAtIndex:0] retain] autorelease];
|
|
if (jetto != nil)
|
|
{
|
|
[self dumpItem:jetto];
|
|
[cargo removeObjectAtIndex:0];
|
|
[[UNIVERSE findShipsMatchingPredicate:YESPredicate
|
|
parameter:nil
|
|
inRange:SCANNER_MAX_RANGE2
|
|
ofEntity:self]
|
|
makeObjectsPerformSelector:@selector(sendAIMessage:) withObject:@"CARGO_DUMPED"];
|
|
}
|
|
}
|
|
|
|
return jetto;
|
|
}
|
|
|
|
|
|
- (OOCargoType) dumpItem: (ShipEntity*) jetto
|
|
{
|
|
if (!jetto)
|
|
return 0;
|
|
int result = [jetto cargoType];
|
|
AI *jettoAI = nil;
|
|
Vector start;
|
|
|
|
double eject_speed = 20.0;
|
|
double eject_reaction = -eject_speed * [jetto mass] / [self mass];
|
|
double jcr = jetto->collision_radius;
|
|
|
|
Quaternion random_direction;
|
|
Vector vel, v_eject;
|
|
Vector rpos = position;
|
|
double random_roll = ((ranrot_rand() % 1024) - 512.0)/1024.0; // -0.5 to +0.5
|
|
double random_pitch = ((ranrot_rand() % 1024) - 512.0)/1024.0; // -0.5 to +0.5
|
|
quaternion_set_random(&random_direction);
|
|
|
|
// default launching position
|
|
start.x = 0.0; // in the middle
|
|
start.y = 0.0; //
|
|
start.z = boundingBox.min.z - jcr; // 1m behind of bounding box
|
|
|
|
// custom launching position
|
|
ScanVectorFromString([shipinfoDictionary objectForKey:@"aft_eject_position"], &start);
|
|
|
|
v_eject = vector_normal(start);
|
|
|
|
// check if start is within bounding box...
|
|
while ( (start.x > boundingBox.min.x - jcr)&&(start.x < boundingBox.max.x + jcr)&&
|
|
(start.y > boundingBox.min.y - jcr)&&(start.y < boundingBox.max.y + jcr)&&
|
|
(start.z > boundingBox.min.z - jcr)&&(start.z < boundingBox.max.z + jcr))
|
|
{
|
|
start = vector_add(start, vector_multiply_scalar(v_eject, jcr));
|
|
}
|
|
|
|
v_eject = make_vector( v_right.x * start.x + v_up.x * start.y + v_forward.x * start.z,
|
|
v_right.y * start.x + v_up.y * start.y + v_forward.y * start.z,
|
|
v_right.z * start.x + v_up.z * start.y + v_forward.z * start.z);
|
|
|
|
rpos = vector_add(rpos, v_eject);
|
|
v_eject = vector_normal(v_eject);
|
|
|
|
v_eject.x += (randf() - randf())/eject_speed;
|
|
v_eject.y += (randf() - randf())/eject_speed;
|
|
v_eject.z += (randf() - randf())/eject_speed;
|
|
|
|
vel = vector_add(vector_multiply_scalar(v_forward, flightSpeed), vector_multiply_scalar(v_eject, eject_speed));
|
|
velocity = vector_add(velocity, vector_multiply_scalar(v_eject, eject_reaction));
|
|
|
|
[jetto setPosition:rpos];
|
|
[jetto setOrientation:random_direction];
|
|
[jetto setRoll:random_roll];
|
|
[jetto setPitch:random_pitch];
|
|
[jetto setVelocity:vel];
|
|
[jetto setScanClass: CLASS_CARGO];
|
|
//[jetto setStatus: STATUS_IN_FLIGHT];
|
|
[jetto setTemperature:[self temperature] * EJECTA_TEMP_FACTOR];
|
|
[UNIVERSE addEntity:jetto]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
|
|
jettoAI = [jetto getAI];
|
|
if ([jettoAI hasSuspendedStateMachines]) // check if this was previos scooped cargo.
|
|
{
|
|
[jetto setThrust:[jetto maxThrust]]; // restore old thrust.
|
|
[jetto setOwner:jetto];
|
|
[jettoAI exitStateMachineWithMessage:nil]; // exit nullAI.
|
|
}
|
|
|
|
cargo_dump_time = [UNIVERSE getTime];
|
|
return result;
|
|
}
|
|
|
|
|
|
- (void) manageCollisions
|
|
{
|
|
// deal with collisions
|
|
//
|
|
Entity* ent;
|
|
ShipEntity* other_ship;
|
|
|
|
while ([collidingEntities count] > 0)
|
|
{
|
|
ent = [(Entity *)[collidingEntities objectAtIndex:0] retain];
|
|
[collidingEntities removeObjectAtIndex:0];
|
|
if (ent)
|
|
{
|
|
if ([ent isShip])
|
|
{
|
|
other_ship = (ShipEntity *)ent;
|
|
[self collideWithShip:other_ship];
|
|
}
|
|
else if ([ent isStellarObject])
|
|
{
|
|
[self getDestroyedBy:ent context:[ent isSun] ? @"hit a sun" : @"hit a planet"];
|
|
if (self == [PlayerEntity sharedPlayer]) [self retain];
|
|
}
|
|
else if ([ent isWormhole])
|
|
{
|
|
WormholeEntity* whole = (WormholeEntity*)ent;
|
|
if (isPlayer)
|
|
{
|
|
[(PlayerEntity*)self enterWormhole: whole];
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
[whole suckInShip: self];
|
|
}
|
|
}
|
|
[ent release];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (BOOL) collideWithShip:(ShipEntity *)other
|
|
{
|
|
Vector loc;
|
|
double dam1, dam2;
|
|
|
|
if (!other)
|
|
return NO;
|
|
|
|
ShipEntity* otherParent = [other parentEntity];
|
|
BOOL otherIsStation = other == [UNIVERSE station];
|
|
// calculate line of centers using centres
|
|
loc = vector_normal_or_zbasis(vector_subtract([other absolutePositionForSubentity], position));
|
|
|
|
if ([self canScoop:other])
|
|
{
|
|
[self scoopIn:other];
|
|
return NO;
|
|
}
|
|
if ([other canScoop:self])
|
|
{
|
|
[other scoopIn:self];
|
|
return NO;
|
|
}
|
|
if (universalID == NO_TARGET)
|
|
return NO;
|
|
if (other->universalID == NO_TARGET)
|
|
return NO;
|
|
|
|
// find velocity along line of centers
|
|
//
|
|
// momentum = mass x velocity
|
|
// ke = mass x velocity x velocity
|
|
//
|
|
GLfloat m1 = mass; // mass of self
|
|
GLfloat m2 = [other mass]; // mass of other
|
|
|
|
// starting velocities:
|
|
Vector v, vel1b = [self velocity];
|
|
|
|
if (otherParent != nil)
|
|
{
|
|
// Subentity
|
|
/* TODO: if the subentity is rotating (subentityRotationalVelocity is
|
|
not 1 0 0 0) we should calculate the tangential velocity from the
|
|
other's position relative to our absolute position and add that in.
|
|
*/
|
|
v = [otherParent velocity];
|
|
}
|
|
else
|
|
{
|
|
v = [other velocity];
|
|
}
|
|
|
|
v = vector_subtract(vel1b, v);
|
|
|
|
GLfloat v2b = dot_product(v, loc); // velocity of other along loc before collision
|
|
|
|
GLfloat v1a = sqrt(v2b * v2b * m2 / m1); // velocity of self along loc after elastic collision
|
|
if (v2b < 0.0f) v1a = -v1a; // in same direction as v2b
|
|
|
|
// are they moving apart at over 1m/s already?
|
|
if (v2b < 0.0f)
|
|
{
|
|
if (v2b < -1.0f) return NO;
|
|
else
|
|
{
|
|
position = vector_subtract(position, loc); // adjust self position
|
|
v = kZeroVector; // go for the 1m/s solution
|
|
}
|
|
}
|
|
|
|
// convert change in velocity into damage energy (KE)
|
|
dam1 = m2 * v2b * v2b / 50000000;
|
|
dam2 = m1 * v2b * v2b / 50000000;
|
|
|
|
// calculate adjustments to velocity after collision
|
|
Vector vel1a = vector_multiply_scalar(loc, -v1a);
|
|
Vector vel2a = vector_multiply_scalar(loc, v2b);
|
|
|
|
if (magnitude2(v) <= 0.1) // virtually no relative velocity - we must provide at least 1m/s to avoid conjoined objects
|
|
{
|
|
vel1a = vector_multiply_scalar(loc, -1);
|
|
vel2a = loc;
|
|
}
|
|
|
|
// apply change in velocity
|
|
if (otherParent != nil)
|
|
{
|
|
[otherParent adjustVelocity:vel2a]; // move the otherParent not the subentity
|
|
}
|
|
else
|
|
{
|
|
[other adjustVelocity:vel2a];
|
|
}
|
|
|
|
[self adjustVelocity:vel1a];
|
|
|
|
BOOL selfDestroyed = (dam1 > energy);
|
|
BOOL otherDestroyed = (dam2 > [other energy]) && !otherIsStation;
|
|
|
|
if (dam1 > 0.05)
|
|
{
|
|
[self takeScrapeDamage: dam1 from:other];
|
|
if (selfDestroyed) // inelastic! - take xplosion velocity damage instead
|
|
{
|
|
vel2a = vector_multiply_scalar(vel2a, -1);
|
|
[other adjustVelocity:vel2a];
|
|
}
|
|
}
|
|
|
|
if (dam2 > 0.05)
|
|
{
|
|
if (otherParent != nil && ![otherParent isFrangible])
|
|
{
|
|
[otherParent takeScrapeDamage: dam2 from:self];
|
|
}
|
|
else
|
|
{
|
|
[other takeScrapeDamage: dam2 from:self];
|
|
}
|
|
|
|
if (otherDestroyed) // inelastic! - take explosion velocity damage instead
|
|
{
|
|
vel1a = vector_multiply_scalar(vel1a, -1);
|
|
[self adjustVelocity:vel1a];
|
|
}
|
|
}
|
|
|
|
if (!selfDestroyed && !otherDestroyed)
|
|
{
|
|
float t = 10.0 * [UNIVERSE getTimeDelta]; // 10 ticks
|
|
|
|
Vector pos1a = vector_add([self position], vector_multiply_scalar(loc, t * v1a));
|
|
[self setPosition:pos1a];
|
|
|
|
if (!otherIsStation)
|
|
{
|
|
Vector pos2a = vector_add([other position], vector_multiply_scalar(loc, t * v2b));
|
|
[other setPosition:pos2a];
|
|
}
|
|
}
|
|
|
|
// remove self from other's collision list
|
|
[[other collisionArray] removeObject:self];
|
|
|
|
[self doScriptEvent:@"shipCollided" withArgument:other andReactToAIMessage:@"COLLISION"];
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (Vector) velocity // overrides Entity velocity
|
|
{
|
|
return vector_add(velocity, vector_multiply_scalar(v_forward, flightSpeed));
|
|
}
|
|
|
|
|
|
- (void) adjustVelocity:(Vector) xVel
|
|
{
|
|
velocity = vector_add(velocity, xVel);
|
|
}
|
|
|
|
|
|
- (void) addImpactMoment:(Vector) moment fraction:(GLfloat) howmuch
|
|
{
|
|
velocity = vector_add(velocity, vector_multiply_scalar(moment, howmuch / mass));
|
|
}
|
|
|
|
|
|
- (BOOL) canScoop:(ShipEntity*)other
|
|
{
|
|
if (other == nil) return NO;
|
|
if (![self hasScoop]) return NO;
|
|
if ([cargo count] >= max_cargo) return NO;
|
|
if (scanClass == CLASS_CARGO) return NO; // we have no power so we can't scoop
|
|
if ([other scanClass] != CLASS_CARGO) return NO;
|
|
if ([other cargoType] == CARGO_NOT_CARGO) return NO;
|
|
|
|
if ([other isStation]) return NO;
|
|
|
|
Vector loc = vector_between(position, [other position]);
|
|
|
|
if (dot_product(v_forward, loc) < 0.0f) return NO; // Must be in front of us
|
|
if ([self isPlayer] && dot_product(v_up, loc) > 0.0f) return NO; // player has to scoop on underside, give more flexibility to NPCs
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (void) getTractoredBy:(ShipEntity *)other
|
|
{
|
|
if([self status] == STATUS_BEING_SCOOPED) return; // both cargo and ship call this. Act only once.
|
|
desired_speed = 0.0;
|
|
[self setAITo:@"nullAI.plist"]; // prevent AI from changing status or behaviour.
|
|
behaviour = BEHAVIOUR_TRACTORED;
|
|
[self setStatus:STATUS_BEING_SCOOPED];
|
|
[self addTarget:other];
|
|
[self setOwner:other];
|
|
[self checkScanner];
|
|
unsigned i;
|
|
for (i = 0; i < n_scanned_ships ; i++)
|
|
{
|
|
ShipEntity *scooper = (ShipEntity *)scanned_ships[i];
|
|
// very specific. might break if the AI convention changes.
|
|
// if (other != scoopers && (id) self == [scoopers primaryTarget] && [[[scoopers getAI] state] isEqualToString: @"COLLECT_STUFF"])
|
|
// more generic, if other ships are trying to shoot the scooped item, they'll lose it too.
|
|
if (other != scooper && (id) self == [scooper primaryTarget])
|
|
{
|
|
[scooper noteLostTarget];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) scoopIn:(ShipEntity *)other
|
|
{
|
|
[other getTractoredBy:self];
|
|
}
|
|
|
|
|
|
- (void) suppressTargetLost
|
|
{
|
|
|
|
}
|
|
|
|
|
|
- (void) scoopUp:(ShipEntity *)other
|
|
{
|
|
if (other == nil) return;
|
|
|
|
OOCargoType co_type;
|
|
OOCargoQuantity co_amount;
|
|
|
|
// don't even think of trying to scoop if the cargo hold is already full
|
|
if (max_cargo && [cargo count] >= max_cargo)
|
|
{
|
|
[other setStatus:STATUS_IN_FLIGHT];
|
|
return;
|
|
}
|
|
|
|
switch ([other cargoType])
|
|
{
|
|
case CARGO_RANDOM:
|
|
co_type = [other commodityType];
|
|
co_amount = [other commodityAmount];
|
|
break;
|
|
|
|
case CARGO_SLAVES:
|
|
co_amount = 1;
|
|
co_type = [UNIVERSE commodityForName:@"Slaves"];
|
|
break;
|
|
|
|
case CARGO_ALLOY:
|
|
co_amount = 1;
|
|
co_type = [UNIVERSE commodityForName:@"Alloys"];
|
|
break;
|
|
|
|
case CARGO_MINERALS:
|
|
co_amount = 1;
|
|
co_type = [UNIVERSE commodityForName:@"Minerals"];
|
|
break;
|
|
|
|
case CARGO_THARGOID:
|
|
co_amount = 1;
|
|
co_type = [UNIVERSE commodityForName:@"Alien Items"];
|
|
break;
|
|
|
|
case CARGO_SCRIPTED_ITEM:
|
|
{
|
|
//scripting
|
|
PlayerEntity *player = [PlayerEntity sharedPlayer];
|
|
[player setScriptTarget:self];
|
|
[other doScriptEvent:@"shipWasScooped" withArgument:self];
|
|
[self doScriptEvent:@"shipScoopedOther" withArgument:other];
|
|
|
|
if (isPlayer)
|
|
{
|
|
NSString* scoopedMS = [NSString stringWithFormat:DESC(@"@-scooped"), [other displayName]];
|
|
[UNIVERSE clearPreviousMessage];
|
|
[UNIVERSE addMessage:scoopedMS forCount:4];
|
|
}
|
|
}
|
|
|
|
default :
|
|
co_amount = 0;
|
|
co_type = 0;
|
|
break;
|
|
}
|
|
|
|
/* Bug: docking failed due to NSRangeException while looking for element
|
|
NSNotFound of cargo mainfest in -[PlayerEntity unloadCargoPods].
|
|
Analysis: bad cargo pods being generated due to
|
|
-[Universe commodityForName:] looking in wrong place for names.
|
|
Fix 1: fix -[Universe commodityForName:].
|
|
Fix 2: catch NSNotFound here and substitute random cargo type.
|
|
-- Ahruman 20070714
|
|
*/
|
|
if (co_type == CARGO_UNDEFINED)
|
|
{
|
|
co_type = [UNIVERSE getRandomCommodity];
|
|
co_amount = [UNIVERSE getRandomAmountOfCommodity:co_type];
|
|
}
|
|
|
|
if (co_amount > 0)
|
|
{
|
|
[other setCommodity:co_type andAmount:co_amount]; // belt and braces setting this!
|
|
cargo_flag = CARGO_FLAG_CANISTERS;
|
|
|
|
if (isPlayer)
|
|
{
|
|
[UNIVERSE clearPreviousMessage];
|
|
if ([other crew])
|
|
{
|
|
unsigned i;
|
|
for (i = 0; i < [[other crew] count]; i++)
|
|
{
|
|
OOCharacter *rescuee = [[other crew] objectAtIndex:i];
|
|
if ([rescuee legalStatus])
|
|
{
|
|
[UNIVERSE addMessage: [NSString stringWithFormat:DESC(@"scoop-captured-@"), [rescuee name]] forCount: 4.5];
|
|
}
|
|
else if ([rescuee insuranceCredits])
|
|
{
|
|
[UNIVERSE addMessage: [NSString stringWithFormat:DESC(@"scoop-rescued-@"), [rescuee name]] forCount: 4.5];
|
|
}
|
|
else
|
|
{
|
|
[UNIVERSE addMessage: DESC(@"scoop-got-slave") forCount: 4.5];
|
|
}
|
|
[(PlayerEntity *)self playEscapePodScooped];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[UNIVERSE addMessage:[UNIVERSE describeCommodity:co_type amount:co_amount] forCount:4.5];
|
|
}
|
|
}
|
|
[cargo insertObject: other atIndex: 0]; // places most recently scooped object at eject position
|
|
[other setStatus:STATUS_IN_HOLD];
|
|
[other setBehaviour:BEHAVIOUR_TUMBLE];
|
|
[shipAI message:@"CARGO_SCOOPED"];
|
|
if (max_cargo && [cargo count] >= max_cargo) [shipAI message:@"HOLD_FULL"];
|
|
}
|
|
[[other collisionArray] removeObject:self]; // so it can't be scooped twice!
|
|
[self suppressTargetLost];
|
|
[UNIVERSE removeEntity:other];
|
|
}
|
|
|
|
|
|
- (void) takeEnergyDamage:(double)amount from:(Entity *)ent becauseOf:(Entity *)other
|
|
{
|
|
if ([self status] == STATUS_DEAD) return;
|
|
if (amount <= 0.0) return;
|
|
|
|
// If it's an energy mine...
|
|
BOOL energyMine = ([ent isParticle] && [ent scanClass] == CLASS_MINE);
|
|
if (energyMine)
|
|
{
|
|
switch (scanClass)
|
|
{
|
|
case CLASS_WORMHOLE :
|
|
case CLASS_ROCK :
|
|
case CLASS_CARGO :
|
|
case CLASS_BUOY :
|
|
// does not normally cascade
|
|
if ((fuel > MIN_FUEL) || isStation)
|
|
{
|
|
//we have fuel onboard so we can still go pop, or we are a station which can
|
|
}
|
|
else break;
|
|
case CLASS_STATION :
|
|
case CLASS_MINE :
|
|
case CLASS_PLAYER :
|
|
case CLASS_POLICE :
|
|
case CLASS_MILITARY :
|
|
case CLASS_THARGOID :
|
|
case CLASS_MISSILE :
|
|
case CLASS_NOT_SET :
|
|
case CLASS_NO_DRAW :
|
|
case CLASS_NEUTRAL :
|
|
case CLASS_TARGET :
|
|
// ...start a chain reaction, if we're dying and have a non-trivial amount of energy.
|
|
if (energy < amount && energy > 10)
|
|
{
|
|
ParticleEntity *chainReaction = [[ParticleEntity alloc] initEnergyMineFromShip:self];
|
|
[UNIVERSE addEntity:chainReaction];
|
|
[chainReaction setOwner:[ent owner]];
|
|
[chainReaction release];
|
|
}
|
|
break;
|
|
//no default thanks, we want the compiler to tell us if we missed a case.
|
|
}
|
|
}
|
|
|
|
energy -= amount;
|
|
being_mined = NO;
|
|
ShipEntity *hunter = nil;
|
|
|
|
hunter = [other rootShipEntity];
|
|
if (hunter == nil && [other isShip]) hunter = (ShipEntity *)other;
|
|
|
|
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
|
|
{
|
|
if ([hunter isCloaked])
|
|
{
|
|
[self doScriptEvent:@"shipBeingAttackedByCloaked" andReactToAIMessage:@"ATTACKED_BY_CLOAKED"];
|
|
|
|
// lose it!
|
|
other = nil;
|
|
hunter = nil;
|
|
}
|
|
}
|
|
else
|
|
hunter = nil;
|
|
|
|
// if the other entity is a ship note it as an aggressor
|
|
if (hunter != nil)
|
|
{
|
|
BOOL iAmTheLaw = [self isPolice];
|
|
BOOL uAreTheLaw = [hunter isPolice];
|
|
|
|
last_escort_target = NO_TARGET; // we're being attacked, escorts can scramble!
|
|
|
|
primaryAggressor = [hunter universalID];
|
|
found_target = primaryAggressor;
|
|
|
|
// firing on an innocent ship is an offence
|
|
[self broadcastHitByLaserFrom: hunter];
|
|
|
|
// tell ourselves we've been attacked
|
|
if (energy > 0)
|
|
{
|
|
[self respondToAttackFrom:ent becauseOf:hunter];
|
|
}
|
|
|
|
// additionally, tell our group we've been attacked
|
|
OOShipGroup *group = [self group];
|
|
if (group != nil && group != [hunter group] && !(iAmTheLaw || uAreTheLaw))
|
|
{
|
|
if ([self isTrader] || [self isEscort])
|
|
{
|
|
ShipEntity *groupLeader = [group leader];
|
|
if (groupLeader != self)
|
|
{
|
|
[groupLeader setFound_target:hunter];
|
|
[groupLeader setPrimaryAggressor:hunter];
|
|
[groupLeader respondToAttackFrom:ent becauseOf:hunter];
|
|
//unsetting group leader for carriers can break stuff
|
|
}
|
|
}
|
|
if ([self isPirate])
|
|
{
|
|
NSEnumerator *groupEnum = nil;
|
|
ShipEntity *otherPirate = nil;
|
|
|
|
for (groupEnum = [group objectEnumerator]; (otherPirate = [groupEnum nextObject]); )
|
|
{
|
|
if (otherPirate != self && randf() < 0.5) // 50% chance they'll help
|
|
{
|
|
[otherPirate setFound_target:hunter];
|
|
[otherPirate setPrimaryAggressor:hunter];
|
|
[otherPirate respondToAttackFrom:ent becauseOf:hunter];
|
|
}
|
|
}
|
|
}
|
|
else if (iAmTheLaw)
|
|
{
|
|
NSEnumerator *groupEnum = nil;
|
|
ShipEntity *otherPolice = nil;
|
|
|
|
for (groupEnum = [group objectEnumerator]; (otherPolice = [groupEnum nextObject]); )
|
|
{
|
|
if (otherPolice != self)
|
|
{
|
|
[otherPolice setFound_target:hunter];
|
|
[otherPolice setPrimaryAggressor:hunter];
|
|
[otherPolice respondToAttackFrom:ent becauseOf:hunter];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// if I'm a copper and you're not, then mark the other as an offender!
|
|
if (iAmTheLaw && !uAreTheLaw)
|
|
{
|
|
[hunter markAsOffender:64];
|
|
}
|
|
|
|
if ([hunter group] == group || (iAmTheLaw && uAreTheLaw))
|
|
{
|
|
// avoid shooting each other
|
|
if ([hunter behaviour] == BEHAVIOUR_ATTACK_FLY_TO_TARGET) // avoid me please!
|
|
{
|
|
[hunter setBehaviour:BEHAVIOUR_ATTACK_FLY_FROM_TARGET];
|
|
[hunter setDesiredSpeed:[hunter maxFlightSpeed]];
|
|
}
|
|
}
|
|
|
|
if ((other)&&([other isShip]))
|
|
being_mined = [(ShipEntity *)other isMining];
|
|
}
|
|
// die if I'm out of energy
|
|
if (energy <= 0.0)
|
|
{
|
|
if (hunter != nil) [hunter noteTargetDestroyed:self];
|
|
[self getDestroyedBy:other context:suppressExplosion ? @"removed" : (energyMine ? @"cascade weapon" : @"energy damage")];
|
|
}
|
|
else
|
|
{
|
|
// warn if I'm low on energy
|
|
if (energy < maxEnergy * 0.25)
|
|
{
|
|
[self doScriptEvent:@"shipEnergyIsLow" andReactToAIMessage:@"ENERGY_LOW"];
|
|
}
|
|
if (energy < maxEnergy *0.125 && [self hasEscapePod] && (ranrot_rand() & 3) == 0) // 25% chance he gets to an escape pod
|
|
{
|
|
[self abandonShip];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) abandonShip
|
|
{
|
|
if ([self isPlayer] && [(PlayerEntity *)self isDocked])
|
|
{
|
|
OOLog(@"ShipEntity.abandonShip.failed", @"Player cannot abandon ship while docked.");
|
|
return;
|
|
}
|
|
|
|
if (![self hasEscapePod])
|
|
{
|
|
OOLog(@"ShipEntity.abandonShip.failed", @"Ship abandonment was requested for %@, but this ship does not carry escape pod(s).", self);
|
|
return;
|
|
}
|
|
|
|
if ([self launchEscapeCapsule] != NO_TARGET) // -launchEscapeCapsule takes care of everyhing for the player
|
|
{
|
|
if (![self isPlayer])
|
|
{
|
|
[self removeEquipmentItem:@"EQ_ESCAPE_POD"];
|
|
[shipAI setStateMachine:@"nullAI.plist"];
|
|
//[shipAI setState:@"GLOBAL"]; // alreaday set inside setStateMachine
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self setScanClass: CLASS_CARGO]; // we're unmanned now!
|
|
thrust = thrust * 0.5;
|
|
desired_speed = 0.0;
|
|
//maxFlightSpeed = 0.0;
|
|
//[self setHulk:YES]; // already set inside launcEscapeCapsule
|
|
|
|
if ([self hasEscorts])
|
|
{
|
|
OOShipGroup *escortGroup = [self escortGroup];
|
|
NSEnumerator *escortEnum = nil;
|
|
ShipEntity *escort = nil;
|
|
// Note: works on escortArray rather than escortEnumerator because escorts may be mutated.
|
|
for (escortEnum = [[self escortArray] objectEnumerator]; (escort = [escortEnum nextObject]); )
|
|
{
|
|
// act individually now!
|
|
if ([escort group] == escortGroup) [escort setGroup:nil];
|
|
if ([escort owner] == self) [escort setOwner:nil];
|
|
}
|
|
|
|
// We now have no escorts.
|
|
[_escortGroup release];
|
|
_escortGroup = nil;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OOLog(@"ShipEntity.abandonShip.notPossible", @"Ship %@ cannot be abandoned at this time.", self);
|
|
}
|
|
}
|
|
|
|
|
|
- (void) takeScrapeDamage:(double) amount from:(Entity *) ent
|
|
{
|
|
if ([self status] == STATUS_DEAD) return;
|
|
|
|
if ([self status] == STATUS_LAUNCHING) // no collisions during launches please
|
|
return;
|
|
if ([ent status] == STATUS_LAUNCHING) // no collisions during launches please
|
|
return;
|
|
|
|
energy -= amount;
|
|
// oops we hit too hard!!!
|
|
if (energy <= 0.0)
|
|
{
|
|
being_mined = YES; // same as using a mining laser
|
|
if ([ent isShip])
|
|
{
|
|
[(ShipEntity *)ent noteTargetDestroyed:self];
|
|
}
|
|
[self getDestroyedBy:ent context:@"scrape damage"];
|
|
}
|
|
else
|
|
{
|
|
// warn if I'm low on energy
|
|
if (energy < maxEnergy * 0.25)
|
|
{
|
|
[self doScriptEvent:@"shipEnergyIsLow" andReactToAIMessage:@"ENERGY_LOW"];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) takeHeatDamage:(double) amount
|
|
{
|
|
if ([self status] == STATUS_DEAD) // it's too late for this one!
|
|
return;
|
|
|
|
if (amount < 0.0)
|
|
return;
|
|
|
|
energy -= amount;
|
|
|
|
throw_sparks = YES;
|
|
|
|
// oops we're burning up!
|
|
if (energy <= 0.0)
|
|
[self getDestroyedBy:nil context:@"heat damage"];
|
|
else
|
|
{
|
|
// warn if I'm low on energy
|
|
if (energy < maxEnergy * 0.25)
|
|
{
|
|
[self doScriptEvent:@"shipEnergyIsLow" andReactToAIMessage:@"ENERGY_LOW"];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) enterDock:(StationEntity *)station
|
|
{
|
|
// throw these away now we're docked...
|
|
if (dockingInstructions != nil)
|
|
{
|
|
[dockingInstructions autorelease];
|
|
dockingInstructions = nil;
|
|
}
|
|
|
|
[self doScriptEvent:@"shipWillDockWithStation" withArgument:station];
|
|
[self doScriptEvent:@"shipDockedWithStation" withArgument:station];
|
|
[shipAI message:@"DOCKED"];
|
|
[station noteDockedShip:self];
|
|
[UNIVERSE removeEntity:self];
|
|
}
|
|
|
|
|
|
- (void) leaveDock:(StationEntity *)station
|
|
{
|
|
if (station == nil) return;
|
|
|
|
Vector stat_f = vector_forward_from_quaternion([station orientation]);
|
|
[self setPosition:vector_add([station position], vector_multiply_scalar(stat_f, 500.0f))];
|
|
|
|
[self setOrientation:[station orientation]];
|
|
flightRoll = [station flightRoll];
|
|
flightPitch = 0.0;
|
|
flightSpeed = maxFlightSpeed * 0.5;
|
|
|
|
[self setStatus:STATUS_LAUNCHING];
|
|
|
|
[self doScriptEvent:@"shipWillLaunchFromStation" withArgument:station];
|
|
[UNIVERSE addEntity:self]; // STATUS_IN_FLIGHT, AI state GLOBAL
|
|
[shipAI message:@"LAUNCHED"];
|
|
}
|
|
|
|
|
|
- (void) enterWormhole:(WormholeEntity *) w_hole
|
|
{
|
|
[self enterWormhole:w_hole replacing:YES];
|
|
}
|
|
|
|
|
|
- (void) enterWormhole:(WormholeEntity *) w_hole replacing:(BOOL)replacing
|
|
{
|
|
if (replacing && ![[UNIVERSE sun] willGoNova] && [UNIVERSE sun] != nil)
|
|
{
|
|
/* Add a new ship to maintain quantities of standard ships, unless
|
|
there's a nova in the works, the AI asked us not to, or we're in
|
|
interstellar space.
|
|
*/
|
|
[UNIVERSE witchspaceShipWithPrimaryRole:[self primaryRole]];
|
|
}
|
|
|
|
[w_hole suckInShip: self]; // removes ship from universe
|
|
}
|
|
|
|
|
|
- (void) enterWitchspace
|
|
{
|
|
// witchspace entry effects here
|
|
ParticleEntity *ring1 = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained
|
|
[UNIVERSE addEntity:ring1];
|
|
[ring1 release];
|
|
ParticleEntity *ring2 = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained
|
|
[ring2 setSize:NSMakeSize([ring2 size].width * -2.5 ,[ring2 size].height * -2.0 )]; // shrinking!
|
|
[UNIVERSE addEntity:ring2];
|
|
[ring2 release];
|
|
|
|
[shipAI message:@"ENTERED_WITCHSPACE"];
|
|
|
|
if (![[UNIVERSE sun] willGoNova]) // if the sun's not going nova
|
|
[UNIVERSE witchspaceShipWithPrimaryRole:[self primaryRole]]; // then add a new ship like this one leaving!
|
|
|
|
[UNIVERSE removeEntity:self];
|
|
}
|
|
|
|
int w_space_seed = 1234567;
|
|
- (void) leaveWitchspace
|
|
{
|
|
Quaternion q1 = [UNIVERSE getWitchspaceExitRotation];
|
|
double d1 = SCANNER_MAX_RANGE * (randf() - randf());
|
|
|
|
// try to ensure healthy random numbers
|
|
//
|
|
ranrot_srand(w_space_seed);
|
|
w_space_seed = ranrot_rand();
|
|
|
|
if (abs(d1) < 500.0) // no closer than 500m
|
|
d1 += ((d1 > 0.0)? 500.0: -500.0);
|
|
quaternion_set_random(&q1);
|
|
Vector v1 = vector_forward_from_quaternion(q1);
|
|
position = [UNIVERSE getWitchspaceExitPosition];
|
|
position.x += v1.x * d1; // randomise exit position
|
|
position.y += v1.y * d1;
|
|
position.z += v1.z * d1;
|
|
[self witchspaceLeavingEffects];
|
|
}
|
|
|
|
|
|
- (void) witchspaceLeavingEffects
|
|
{
|
|
// all ships exiting witchspace will share the same orientation.
|
|
orientation = [UNIVERSE getWitchspaceExitRotation];
|
|
flightRoll = 0.0;
|
|
flightPitch = 0.0;
|
|
flightSpeed = maxFlightSpeed * 0.25;
|
|
[UNIVERSE addEntity:self]; // AI and status get initialised here
|
|
[self setStatus:STATUS_EXITING_WITCHSPACE];
|
|
[shipAI message:@"EXITED_WITCHSPACE"];
|
|
|
|
// witchspace exit effects here
|
|
ParticleEntity *ring1 = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained
|
|
[UNIVERSE addEntity:ring1];
|
|
[ring1 release];
|
|
ParticleEntity *ring2 = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained
|
|
[ring2 setSize:NSMakeSize([ring2 size].width * -2.5 ,[ring2 size].height * -2.0 )]; // shrinking!
|
|
[UNIVERSE addEntity:ring2];
|
|
[ring2 release];
|
|
}
|
|
|
|
|
|
- (void) markAsOffender:(int)offence_value
|
|
{
|
|
if (scanClass != CLASS_POLICE) bounty |= offence_value;
|
|
}
|
|
|
|
|
|
// Exposed to AI
|
|
- (void) switchLightsOn
|
|
{
|
|
NSEnumerator *subEnum = nil;
|
|
OOFlasherEntity *se = nil;
|
|
ShipEntity *sub = nil;
|
|
|
|
for (subEnum = [self flasherEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
[se setActive:YES];
|
|
}
|
|
for (subEnum = [self shipSubEntityEnumerator]; (sub = [subEnum nextObject]); )
|
|
{
|
|
[sub switchLightsOn];
|
|
}
|
|
}
|
|
|
|
// Exposed to AI
|
|
- (void) switchLightsOff
|
|
{
|
|
NSEnumerator *subEnum = nil;
|
|
OOFlasherEntity *se = nil;
|
|
ShipEntity *sub = nil;
|
|
|
|
for (subEnum = [self flasherEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
[se setActive:NO];
|
|
}
|
|
for (subEnum = [self shipSubEntityEnumerator]; (sub = [subEnum nextObject]); )
|
|
{
|
|
[sub switchLightsOff];
|
|
}
|
|
}
|
|
|
|
|
|
- (void) setDestination:(Vector) dest
|
|
{
|
|
destination = dest;
|
|
frustration = 0.0; // new destination => no frustration!
|
|
}
|
|
|
|
|
|
- (void) setEscortDestination:(Vector) dest
|
|
{
|
|
destination = dest; // don't reset frustration for escorts.
|
|
}
|
|
|
|
|
|
- (BOOL) canAcceptEscort:(ShipEntity *)potentialEscort
|
|
{
|
|
//this condition has to be checked first!
|
|
if (![self isEscort] && ([self hasRole:@"police"] || [self hasRole:@"interceptor"]))
|
|
{
|
|
return [potentialEscort hasRole:@"wingman"];
|
|
}
|
|
if ([self bounty] == 0 && [potentialEscort bounty] != 0) // clean mothers can only accept clean escorts
|
|
{
|
|
return NO;
|
|
}
|
|
if (![self isEscort])
|
|
{
|
|
return [potentialEscort hasRole:@"escort"];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (BOOL) acceptAsEscort:(ShipEntity *) other_ship
|
|
{
|
|
// can't pair with self
|
|
if (self == other_ship) return NO;
|
|
|
|
//increased stack depth at which it can accept escorts to avoid rejections at this stage.
|
|
//doesn't seem to have any adverse effect for now. - Kaks.
|
|
if ([shipAI stackDepth] > 3)
|
|
{
|
|
OOLog(@"ship.escort.reject", @"%@ rejecting escort %@ because AI stack depth is %u.",self, other_ship, [shipAI stackDepth]);
|
|
return NO;
|
|
}
|
|
|
|
if ([self canAcceptEscort:other_ship])
|
|
{
|
|
OOShipGroup *escortGroup = [self escortGroup];
|
|
|
|
if ([escortGroup containsShip:other_ship]) return YES;
|
|
|
|
// check total number acceptable
|
|
unsigned maxEscorts = _maxEscortCount;
|
|
unsigned escortCount = [escortGroup count] - 1;
|
|
|
|
//however the system's patrols don't have escorts inside their dictionary
|
|
if (maxEscorts == 0 && ([self hasRole:@"police"] || [self hasRole:@"interceptor"] || [self hasRole:@"hunter"]))
|
|
{
|
|
maxEscorts = MAX_ESCORTS;
|
|
}
|
|
else if (maxEscorts > MAX_ESCORTS)
|
|
{
|
|
maxEscorts = MAX_ESCORTS;
|
|
}
|
|
|
|
if (escortCount < maxEscorts)
|
|
{
|
|
[escortGroup addShip:other_ship];
|
|
[other_ship setGroup:escortGroup];
|
|
if ([self group] == nil)
|
|
{
|
|
[self setGroup:escortGroup];
|
|
}
|
|
else if ([self group] != escortGroup) [[self group] addShip:other_ship];
|
|
|
|
if(([other_ship maxFlightSpeed] < cruiseSpeed) && ([other_ship maxFlightSpeed] > cruiseSpeed * 0.3))
|
|
cruiseSpeed = [other_ship maxFlightSpeed] * 0.99;
|
|
|
|
OOLog(@"ship.escort.accept", @"Accepting existing escort %@.", other_ship);
|
|
|
|
[self doScriptEvent:@"shipAcceptedEscort" withArgument:other_ship];
|
|
[other_ship doScriptEvent:@"escortAccepted" withArgument:self];
|
|
return YES;
|
|
}
|
|
else if (maxEscorts > 0)
|
|
{
|
|
OOLog(@"ship.escort.reject", @" %@ already got max escorts(%d). Escort rejected: %@.",self, escortCount, other_ship);
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (Vector) coordinatesForEscortPosition:(int) f_pos
|
|
{
|
|
int f_hi = 1 + (f_pos >> 2);
|
|
int f_lo = f_pos & 3;
|
|
|
|
int fp = f_lo * 3;
|
|
int escort_positions[12] = { -2,0,-1, 2,0,-1, -3,0,-3, 3,0,-3 }; // V-shape escort pattern
|
|
// int escort_positions[12] = { -2,0,+2, 2,0,+2, -3,0,-3, 3,0,-3 }; // X-shape escort pattern
|
|
Vector pos = position;
|
|
double spacing = collision_radius * ESCORT_SPACING_FACTOR;
|
|
double xx = f_hi * spacing * escort_positions[fp++];
|
|
double yy = f_hi * spacing * escort_positions[fp++];
|
|
double zz = f_hi * spacing * escort_positions[fp];
|
|
pos.x += v_right.x * xx; pos.y += v_right.y * xx; pos.z += v_right.z * xx;
|
|
pos.x += v_up.x * yy; pos.y += v_up.y * yy; pos.z += v_up.z * yy;
|
|
pos.x += v_forward.x * zz; pos.y += v_forward.y * zz; pos.z += v_forward.z * zz;
|
|
|
|
return pos;
|
|
}
|
|
|
|
|
|
// Exposed to AI
|
|
- (void) deployEscorts
|
|
{
|
|
NSEnumerator *escortEnum = nil;
|
|
ShipEntity *escort = nil;
|
|
ShipEntity *target = nil;
|
|
NSMutableSet *idleEscorts = nil;
|
|
unsigned deployCount;
|
|
|
|
if ([self primaryTarget] == nil || _escortGroup == nil) return;
|
|
|
|
OOShipGroup *escortGroup = [self escortGroup];
|
|
unsigned escortCount = [escortGroup count] - 1; // escorts minus leader.
|
|
if (escortCount == 0) return;
|
|
|
|
if ([self group] == nil) [self setGroup:escortGroup];
|
|
|
|
if (primaryTarget == last_escort_target)
|
|
{
|
|
// already deployed escorts onto this target!
|
|
return;
|
|
}
|
|
|
|
last_escort_target = primaryTarget;
|
|
|
|
// Find idle escorts
|
|
idleEscorts = [NSMutableSet set];
|
|
for (escortEnum = [self escortEnumerator]; (escort = [escortEnum nextObject]); )
|
|
{
|
|
if (![[[escort getAI] name] isEqualToString:@"interceptAI.plist"])
|
|
{
|
|
[idleEscorts addObject:escort];
|
|
}
|
|
}
|
|
|
|
escortCount = [idleEscorts count];
|
|
if (escortCount == 0) return;
|
|
|
|
deployCount = ranrot_rand() % escortCount + 1;
|
|
|
|
// Deploy deployCount idle escorts.
|
|
target = [self primaryTarget];
|
|
for (escortEnum = [idleEscorts objectEnumerator]; (escort = [escortEnum nextObject]); )
|
|
{
|
|
[escort addTarget:target];
|
|
[escort setStateMachine:@"interceptAI.plist"];
|
|
[escort doScriptEvent:@"escortAttack" withArgument:target];
|
|
|
|
if (--deployCount == 0) break;
|
|
}
|
|
}
|
|
|
|
|
|
// Exposed to AI
|
|
- (void) dockEscorts
|
|
{
|
|
if (![self hasEscorts]) return;
|
|
|
|
OOShipGroup *escortGroup = [self escortGroup];
|
|
NSEnumerator *escortEnum = nil;
|
|
ShipEntity *escort = nil;
|
|
ShipEntity *target = [self primaryTarget];
|
|
unsigned i = 0;
|
|
// Note: works on escortArray rather than escortEnumerator because escorts may be mutated.
|
|
for (escortEnum = [[self escortArray] objectEnumerator]; (escort = [escortEnum nextObject]); )
|
|
{
|
|
float delay = i++ * 3.0 + 1.5; // send them off at three second intervals
|
|
AI *ai = [escort getAI];
|
|
|
|
// act individually now!
|
|
if ([escort group] == escortGroup) [escort setGroup:nil];
|
|
if ([escort owner] == self) [escort setOwner:nil];
|
|
if(target && [target isStation]) [escort addTarget:target];
|
|
|
|
[ai setStateMachine:@"dockingAI.plist" afterDelay:delay];
|
|
[ai setState:@"ABORT" afterDelay:delay + 0.25];
|
|
[escort doScriptEvent:@"escortDock" withArgument:[NSNumber numberWithFloat:delay]];
|
|
}
|
|
|
|
// We now have no escorts.
|
|
[_escortGroup release];
|
|
_escortGroup = nil;
|
|
}
|
|
|
|
|
|
// Exosed to AI
|
|
- (void) setTargetToNearestStation
|
|
{
|
|
// check if the groupID (parent ship) points to a station...
|
|
Entity* mother = [[self group] leader];
|
|
if ([mother isStation])
|
|
{
|
|
primaryTarget = [mother universalID];
|
|
targetStation = primaryTarget;
|
|
return; // head for mother!
|
|
}
|
|
|
|
/*- selects the nearest station it can find -*/
|
|
if (!UNIVERSE)
|
|
return;
|
|
int ent_count = UNIVERSE->n_entities;
|
|
Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list
|
|
Entity* my_entities[ent_count];
|
|
int i;
|
|
int station_count = 0;
|
|
for (i = 0; i < ent_count; i++)
|
|
if (uni_entities[i]->isStation)
|
|
my_entities[station_count++] = [uni_entities[i] retain]; // retained
|
|
//
|
|
StationEntity* station = nil;
|
|
double nearest2 = SCANNER_MAX_RANGE2 * 1000000.0; // 1000x scanner range (25600 km), squared.
|
|
for (i = 0; i < station_count; i++)
|
|
{
|
|
StationEntity* thing = (StationEntity*)my_entities[i];
|
|
double range2 = distance2(position, thing->position);
|
|
if (range2 < nearest2)
|
|
{
|
|
station = (StationEntity *)thing;
|
|
nearest2 = range2;
|
|
}
|
|
}
|
|
for (i = 0; i < station_count; i++)
|
|
[my_entities[i] release]; // released
|
|
//
|
|
if (station)
|
|
{
|
|
primaryTarget = [station universalID];
|
|
targetStation = primaryTarget;
|
|
}
|
|
else
|
|
{
|
|
[shipAI message:@"NO_STATION_FOUND"];
|
|
}
|
|
}
|
|
|
|
|
|
// Exosed to AI
|
|
- (void) setTargetToSystemStation
|
|
{
|
|
StationEntity* system_station = [UNIVERSE station];
|
|
|
|
if (!system_station)
|
|
{
|
|
[shipAI message:@"NOTHING_FOUND"];
|
|
[shipAI message:@"NO_STATION_FOUND"];
|
|
primaryTarget = NO_TARGET;
|
|
targetStation = NO_TARGET;
|
|
return;
|
|
}
|
|
|
|
if (!system_station->isStation)
|
|
{
|
|
[shipAI message:@"NOTHING_FOUND"];
|
|
[shipAI message:@"NO_STATION_FOUND"];
|
|
primaryTarget = NO_TARGET;
|
|
targetStation = NO_TARGET;
|
|
return;
|
|
}
|
|
|
|
primaryTarget = [system_station universalID];
|
|
targetStation = primaryTarget;
|
|
return;
|
|
}
|
|
|
|
|
|
- (void) landOnPlanet:(OOPlanetEntity *)planet
|
|
{
|
|
if (planet)
|
|
{
|
|
[planet welcomeShuttle:self]; // 10km from the surface
|
|
}
|
|
[shipAI message:@"LANDED_ON_PLANET"];
|
|
|
|
#ifndef NDEBUG
|
|
if ([self reportAIMessages])
|
|
{
|
|
OOLog(@"planet.collide.shuttleLanded", @"DEBUG: %@ landed on planet %@", self, planet);
|
|
}
|
|
#endif
|
|
|
|
[UNIVERSE removeEntity:self];
|
|
}
|
|
|
|
|
|
// Exposed to AI
|
|
- (void) abortDocking
|
|
{
|
|
[[UNIVERSE findEntitiesMatchingPredicate:IsStationPredicate
|
|
parameter:nil
|
|
inRange:-1
|
|
ofEntity:nil]
|
|
makeObjectsPerformSelector:@selector(abortDockingForShip:) withObject:self];
|
|
}
|
|
|
|
|
|
- (void) broadcastThargoidDestroyed
|
|
{
|
|
[[UNIVERSE findShipsMatchingPredicate:HasRolePredicate
|
|
parameter:@"tharglet"
|
|
inRange:SCANNER_MAX_RANGE2
|
|
ofEntity:self]
|
|
makeObjectsPerformSelector:@selector(sendAIMessage:) withObject:@"THARGOID_DESTROYED"];
|
|
}
|
|
|
|
|
|
static BOOL AuthorityPredicate(Entity *entity, void *parameter)
|
|
{
|
|
ShipEntity *victim = parameter;
|
|
|
|
// Select main station, if victim is in aegis
|
|
if (entity == [UNIVERSE station] && [victim withinStationAegis])
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
// Select police units in scanner range
|
|
if ([entity scanClass] == CLASS_POLICE &&
|
|
distance2([victim position], [entity position]) < SCANNER_MAX_RANGE2)
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
// Reject others
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (void) broadcastHitByLaserFrom:(ShipEntity *) aggressor_ship
|
|
{
|
|
/*-- If you're clean, locates all police and stations in range and tells them OFFENCE_COMMITTED --*/
|
|
if (!UNIVERSE) return;
|
|
if ([self bounty]) return;
|
|
if (!aggressor_ship) return;
|
|
|
|
if ( (scanClass == CLASS_NEUTRAL)||
|
|
(scanClass == CLASS_STATION)||
|
|
(scanClass == CLASS_BUOY)||
|
|
(scanClass == CLASS_POLICE)||
|
|
(scanClass == CLASS_MILITARY)||
|
|
(scanClass == CLASS_PLAYER)) // only for active ships...
|
|
{
|
|
NSArray *authorities = nil;
|
|
NSEnumerator *authEnum = nil;
|
|
ShipEntity *auth = nil;
|
|
|
|
authorities = [UNIVERSE findShipsMatchingPredicate:AuthorityPredicate
|
|
parameter:self
|
|
inRange:-1
|
|
ofEntity:nil];
|
|
authEnum = [authorities objectEnumerator];
|
|
while ((auth = [authEnum nextObject]))
|
|
{
|
|
[auth setFound_target:aggressor_ship];
|
|
[auth doScriptEvent:@"offenceCommittedNearby" withArgument:aggressor_ship andArgument:self];
|
|
[auth reactToAIMessage:@"OFFENCE_COMMITTED"];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) sendExpandedMessage:(NSString *) message_text toShip:(ShipEntity*) other_ship
|
|
{
|
|
if (!other_ship)
|
|
return;
|
|
if (!crew)
|
|
return; // nobody to send the signal
|
|
if ((lastRadioMessage) && (messageTime > 0.0) && [message_text isEqual:lastRadioMessage])
|
|
return; // don't send the same message too often
|
|
[lastRadioMessage autorelease];
|
|
lastRadioMessage = [message_text retain];
|
|
Vector delta = other_ship->position;
|
|
delta.x -= position.x; delta.y -= position.y; delta.z -= position.z;
|
|
double d2 = delta.x*delta.x + delta.y*delta.y + delta.z*delta.z;
|
|
if (d2 > scannerRange * scannerRange)
|
|
return; // out of comms range
|
|
if (!other_ship)
|
|
return;
|
|
NSMutableString *localExpandedMessage = [NSMutableString stringWithString:message_text];
|
|
[localExpandedMessage replaceOccurrencesOfString:@"[self:name]"
|
|
withString:[self displayName]
|
|
options:NSLiteralSearch range:NSMakeRange(0, [localExpandedMessage length])];
|
|
[localExpandedMessage replaceOccurrencesOfString:@"[target:name]"
|
|
withString:[other_ship identFromShip: self]
|
|
options:NSLiteralSearch range:NSMakeRange(0, [localExpandedMessage length])];
|
|
Random_Seed very_random_seed;
|
|
very_random_seed.a = rand() & 255;
|
|
very_random_seed.b = rand() & 255;
|
|
very_random_seed.c = rand() & 255;
|
|
very_random_seed.d = rand() & 255;
|
|
very_random_seed.e = rand() & 255;
|
|
very_random_seed.f = rand() & 255;
|
|
seed_RNG_only_for_planet_description(very_random_seed);
|
|
NSString* expandedMessage = ExpandDescriptionForCurrentSystem(localExpandedMessage);
|
|
[self setCommsMessageColor];
|
|
[other_ship receiveCommsMessage:[NSString stringWithFormat:@"%@:\n %@", displayName, expandedMessage]];
|
|
if (other_ship->isPlayer)
|
|
messageTime = 6.0;
|
|
[UNIVERSE resetCommsLogColor];
|
|
}
|
|
|
|
|
|
- (void) broadcastAIMessage:(NSString *) ai_message
|
|
{
|
|
NSString* expandedMessage = ExpandDescriptionForCurrentSystem(ai_message);
|
|
|
|
[self checkScanner];
|
|
unsigned i;
|
|
for (i = 0; i < n_scanned_ships ; i++)
|
|
{
|
|
ShipEntity* ship = scanned_ships[i];
|
|
[[ship getAI] message: expandedMessage];
|
|
}
|
|
}
|
|
|
|
|
|
- (void) broadcastMessage:(NSString *) message_text withUnpilotedOverride:(BOOL) unpilotedOverride
|
|
{
|
|
NSString* expandedMessage = [NSString stringWithFormat:@"%@:\n %@", displayName, ExpandDescriptionForCurrentSystem(message_text)];
|
|
|
|
if (!crew && !unpilotedOverride)
|
|
return; // nobody to send the signal and no override for unpiloted craft is set
|
|
|
|
[self setCommsMessageColor];
|
|
[self checkScanner];
|
|
unsigned i;
|
|
for (i = 0; i < n_scanned_ships ; i++)
|
|
{
|
|
ShipEntity* ship = scanned_ships[i];
|
|
[ship receiveCommsMessage: expandedMessage];
|
|
if ([ship isPlayer])
|
|
messageTime = 6.0;
|
|
}
|
|
[UNIVERSE resetCommsLogColor];
|
|
}
|
|
|
|
|
|
- (void) setCommsMessageColor
|
|
{
|
|
float hue = 0.0625 * (universalID & 15);
|
|
[[UNIVERSE comm_log_gui] setTextColor:[OOColor colorWithCalibratedHue:hue saturation:0.375 brightness:1.0 alpha:1.0]];
|
|
if (scanClass == CLASS_THARGOID)
|
|
[[UNIVERSE comm_log_gui] setTextColor:[OOColor greenColor]];
|
|
if (scanClass == CLASS_POLICE)
|
|
[[UNIVERSE comm_log_gui] setTextColor:[OOColor cyanColor]];
|
|
}
|
|
|
|
|
|
- (void) receiveCommsMessage:(NSString *) message_text
|
|
{
|
|
// ignore messages for now
|
|
}
|
|
|
|
|
|
- (void) commsMessage:(NSString *)valueString withUnpilotedOverride:(BOOL)unpilotedOverride
|
|
{
|
|
Random_Seed very_random_seed;
|
|
very_random_seed.a = rand() & 255;
|
|
very_random_seed.b = rand() & 255;
|
|
very_random_seed.c = rand() & 255;
|
|
very_random_seed.d = rand() & 255;
|
|
very_random_seed.e = rand() & 255;
|
|
very_random_seed.f = rand() & 255;
|
|
seed_RNG_only_for_planet_description(very_random_seed);
|
|
|
|
[self broadcastMessage:valueString withUnpilotedOverride:unpilotedOverride];
|
|
}
|
|
|
|
|
|
- (BOOL) markForFines
|
|
{
|
|
if (being_fined)
|
|
return NO; // can't mark twice
|
|
being_fined = ([self legalStatus] > 0);
|
|
return being_fined;
|
|
}
|
|
|
|
|
|
- (BOOL) isMining
|
|
{
|
|
return ((behaviour == BEHAVIOUR_ATTACK_MINING_TARGET)&&(forward_weapon_type == WEAPON_MINING_LASER));
|
|
}
|
|
|
|
|
|
- (void) interpretAIMessage:(NSString *)ms
|
|
{
|
|
if ([ms hasPrefix:AIMS_AGGRESSOR_SWITCHED_TARGET])
|
|
{
|
|
// if I'm under attack send a thank-you message to the rescuer
|
|
//
|
|
NSArray* tokens = ScanTokensFromString(ms);
|
|
int switcher_id = [(NSString*)[tokens objectAtIndex:1] intValue];
|
|
Entity* switcher = [UNIVERSE entityForUniversalID:switcher_id];
|
|
int rescuer_id = [(NSString*)[tokens objectAtIndex:2] intValue];
|
|
Entity* rescuer = [UNIVERSE entityForUniversalID:rescuer_id];
|
|
if ((switcher_id == primaryAggressor)&&(switcher_id == primaryTarget)&&(switcher)&&(rescuer)&&(rescuer->isShip)&&(thanked_ship_id != rescuer_id)&&(scanClass != CLASS_THARGOID))
|
|
{
|
|
if (scanClass == CLASS_POLICE)
|
|
[self sendExpandedMessage:@"[police-thanks-for-assist]" toShip:(ShipEntity*)rescuer];
|
|
else
|
|
[self sendExpandedMessage:@"[thanks-for-assist]" toShip:(ShipEntity*)rescuer];
|
|
thanked_ship_id = rescuer_id;
|
|
[(ShipEntity*)switcher setBounty:[(ShipEntity*)switcher bounty] + 5 + (ranrot_rand() & 15)]; // reward
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (BoundingBox) findBoundingBoxRelativeTo:(Entity *)other InVectors:(Vector) _i :(Vector) _j :(Vector) _k
|
|
{
|
|
Vector opv = other ? other->position : position;
|
|
return [self findBoundingBoxRelativeToPosition:opv InVectors:_i :_j :_k];
|
|
}
|
|
|
|
|
|
// Exposed to AI and legacy scripts.
|
|
- (void) spawn:(NSString *)roles_number
|
|
{
|
|
NSArray *tokens = ScanTokensFromString(roles_number);
|
|
NSString *roleString = nil;
|
|
NSString *numberString = nil;
|
|
OOUInteger number;
|
|
|
|
if ([tokens count] != 2)
|
|
{
|
|
OOLog(kOOLogSyntaxAddShips, @"***** Could not spawn: '%@' (must be two tokens, role and number)",roles_number);
|
|
return;
|
|
}
|
|
|
|
roleString = [tokens oo_stringAtIndex:0];
|
|
numberString = [tokens oo_stringAtIndex:1];
|
|
|
|
number = [numberString intValue];
|
|
|
|
[self spawnShipsWithRole:roleString count:number];
|
|
}
|
|
|
|
|
|
- (int) checkShipsInVicinityForWitchJumpExit
|
|
{
|
|
// checks if there are any large masses close by
|
|
// since we want to place the space station at least 10km away
|
|
// the formula we'll use is K x m / d2 < 1.0
|
|
// (m = mass, d2 = distance squared)
|
|
// coriolis station is mass 455,223,200
|
|
// 10km is 10,000m,
|
|
// 10km squared is 100,000,000
|
|
// therefore K is 0.22 (approx)
|
|
|
|
int result = NO_TARGET;
|
|
|
|
GLfloat k = 0.1;
|
|
|
|
int ent_count = UNIVERSE->n_entities;
|
|
Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list
|
|
ShipEntity* my_entities[ent_count];
|
|
int i;
|
|
|
|
int ship_count = 0;
|
|
for (i = 0; i < ent_count; i++)
|
|
if ((uni_entities[i]->isShip)&&(uni_entities[i] != self))
|
|
my_entities[ship_count++] = (ShipEntity*)[uni_entities[i] retain]; // retained
|
|
//
|
|
for (i = 0; (i < ship_count)&&(result == NO_TARGET) ; i++)
|
|
{
|
|
ShipEntity* ship = my_entities[i];
|
|
Vector delta = vector_between(position, ship->position);
|
|
GLfloat d2 = magnitude2(delta);
|
|
if ((k * [ship mass] > d2)&&(d2 < SCANNER_MAX_RANGE2)) // if you go off scanner from a blocker - it ceases to block
|
|
result = [ship universalID];
|
|
}
|
|
for (i = 0; i < ship_count; i++)
|
|
[my_entities[i] release]; // released
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
- (BOOL) trackCloseContacts
|
|
{
|
|
return trackCloseContacts;
|
|
}
|
|
|
|
|
|
- (void) setTrackCloseContacts:(BOOL) value
|
|
{
|
|
if (value == trackCloseContacts) return;
|
|
|
|
trackCloseContacts = value;
|
|
[closeContactsInfo release];
|
|
|
|
if (trackCloseContacts)
|
|
{
|
|
closeContactsInfo = [[NSMutableDictionary alloc] init];
|
|
}
|
|
else
|
|
{
|
|
closeContactsInfo = nil;
|
|
}
|
|
}
|
|
|
|
|
|
- (void) claimAsSalvage
|
|
{
|
|
// Create a bouy and beacon where the hulk is.
|
|
// Get the main GalCop station to launch a pilot boat to deliver a pilot to the hulk.
|
|
OOLog(@"claimAsSalvage.called", @"claimAsSalvage called on %@ %@", [self name], [self roleSet]);
|
|
|
|
// Not an abandoned hulk, so don't allow the salvage
|
|
if (![self isHulk])
|
|
{
|
|
OOLog(@"claimAsSalvage.failed.notHulk", @"claimAsSalvage failed because not a hulk");
|
|
return;
|
|
}
|
|
|
|
// Set target to main station, and return now if it can't be found
|
|
[self setTargetToSystemStation];
|
|
if (primaryTarget == NO_TARGET)
|
|
{
|
|
OOLog(@"claimAsSalvage.failed.noStation", @"claimAsSalvage failed because did not find a station");
|
|
return;
|
|
}
|
|
|
|
// Get the station to launch a pilot boat to bring a pilot out to the hulk (use a viper for now)
|
|
StationEntity *station = (StationEntity *)[UNIVERSE entityForUniversalID:primaryTarget];
|
|
OOLog(@"claimAsSalvage.requestingPilot", @"claimAsSalvage asking station to launch a pilot boat");
|
|
[station launchShipWithRole:@"pilot"];
|
|
[self setReportAIMessages:YES];
|
|
OOLog(@"claimAsSalvage.success", @"claimAsSalvage setting own state machine to capturedShipAI.plist");
|
|
[self setStateMachine:@"capturedShipAI.plist"];
|
|
}
|
|
|
|
|
|
- (void) sendCoordinatesToPilot
|
|
{
|
|
Entity *scan;
|
|
ShipEntity *scanShip, *pilot;
|
|
|
|
n_scanned_ships = 0;
|
|
scan = z_previous;
|
|
OOLog(@"ship.pilotage", @"searching for pilot boat");
|
|
while (scan &&(scan->isShip == NO))
|
|
{
|
|
scan = scan->z_previous; // skip non-ships
|
|
}
|
|
|
|
pilot = nil;
|
|
while (scan)
|
|
{
|
|
if (scan->isShip)
|
|
{
|
|
scanShip = (ShipEntity *)scan;
|
|
|
|
if ([self hasRole:@"pilot"] == YES)
|
|
{
|
|
if ([scanShip primaryTargetID] == NO_TARGET)
|
|
{
|
|
OOLog(@"ship.pilotage", @"found pilot boat with no target, will use this one");
|
|
pilot = scanShip;
|
|
[pilot setPrimaryRole:@"pilot"];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
scan = scan->z_previous;
|
|
while (scan && (scan->isShip == NO))
|
|
{
|
|
scan = scan->z_previous;
|
|
}
|
|
}
|
|
|
|
if (pilot != nil)
|
|
{
|
|
OOLog(@"ship.pilotage", @"becoming pilot target and setting AI");
|
|
[pilot setReportAIMessages:YES];
|
|
[pilot addTarget:self];
|
|
[pilot setStateMachine:@"pilotAI.plist"];
|
|
[self reactToAIMessage:@"FOUND_PILOT"];
|
|
}
|
|
}
|
|
|
|
|
|
- (void) pilotArrived
|
|
{
|
|
[self setHulk:NO];
|
|
[self reactToAIMessage:@"PILOT_ARRIVED"];
|
|
}
|
|
|
|
|
|
#ifndef NDEBUG
|
|
- (void)dumpSelfState
|
|
{
|
|
NSMutableArray *flags = nil;
|
|
NSString *flagsString = nil;
|
|
|
|
[super dumpSelfState];
|
|
|
|
OOLog(@"dumpState.shipEntity", @"Name: %@", name);
|
|
OOLog(@"dumpState.shipEntity", @"Display Name: %@", displayName);
|
|
OOLog(@"dumpState.shipEntity", @"Roles: %@", [self roleSet]);
|
|
OOLog(@"dumpState.shipEntity", @"Primary role: %@", primaryRole);
|
|
OOLog(@"dumpState.shipEntity", @"Script: %@", script);
|
|
OOLog(@"dumpState.shipEntity", @"Subentity count: %u", [self subEntityCount]);
|
|
OOLog(@"dumpState.shipEntity", @"Behaviour: %@", BehaviourToString(behaviour));
|
|
if (primaryTarget != NO_TARGET) OOLog(@"dumpState.shipEntity", @"Target: %@", [self primaryTarget]);
|
|
OOLog(@"dumpState.shipEntity", @"Destination: %@", VectorDescription(destination));
|
|
OOLog(@"dumpState.shipEntity", @"Other destination: %@", VectorDescription(coordinates));
|
|
OOLog(@"dumpState.shipEntity", @"Waypoint count: %u", number_of_navpoints);
|
|
OOLog(@"dumpState.shipEntity", @"Desired speed: %g", desired_speed);
|
|
OOLog(@"dumpState.shipEntity", @"Thrust: %g", thrust);
|
|
if ([self escortCount] != 0) OOLog(@"dumpState.shipEntity", @"Escort count: %u", [self escortCount]);
|
|
OOLog(@"dumpState.shipEntity", @"Fuel: %i", fuel);
|
|
OOLog(@"dumpState.shipEntity", @"Fuel accumulator: %g", fuel_accumulator);
|
|
OOLog(@"dumpState.shipEntity", @"Missile count: %u", missiles);
|
|
|
|
#ifdef OO_BRAIN_AI
|
|
if (brain != nil && OOLogWillDisplayMessagesInClass(@"dumpState.shipEntity.brain"))
|
|
{
|
|
OOLog(@"dumpState.shipEntity.brain", @"Brain:");
|
|
OOLogPushIndent();
|
|
OOLogIndent();
|
|
NS_DURING
|
|
[brain dumpState];
|
|
NS_HANDLER
|
|
NS_ENDHANDLER
|
|
OOLogPopIndent();
|
|
}
|
|
#endif
|
|
|
|
if (shipAI != nil && OOLogWillDisplayMessagesInClass(@"dumpState.shipEntity.ai"))
|
|
{
|
|
OOLog(@"dumpState.shipEntity.ai", @"AI:");
|
|
OOLogPushIndent();
|
|
OOLogIndent();
|
|
NS_DURING
|
|
[shipAI dumpState];
|
|
NS_HANDLER
|
|
NS_ENDHANDLER
|
|
OOLogPopIndent();
|
|
}
|
|
OOLog(@"dumpState.shipEntity", @"Frustration: %g", frustration);
|
|
OOLog(@"dumpState.shipEntity", @"Success factor: %g", success_factor);
|
|
OOLog(@"dumpState.shipEntity", @"Shots fired: %u", shot_counter);
|
|
OOLog(@"dumpState.shipEntity", @"Time since shot: %g", [self shotTime]);
|
|
OOLog(@"dumpState.shipEntity", @"Spawn time: %g (%g seconds ago)", [self spawnTime], [self timeElapsedSinceSpawn]);
|
|
if (beaconChar != '\0')
|
|
{
|
|
OOLog(@"dumpState.shipEntity", @"Beacon character: '%c'", beaconChar);
|
|
}
|
|
OOLog(@"dumpState.shipEntity", @"Hull temperature: %g", ship_temperature);
|
|
OOLog(@"dumpState.shipEntity", @"Heat insulation: %g", [self heatInsulation]);
|
|
|
|
flags = [NSMutableArray array];
|
|
#define ADD_FLAG_IF_SET(x) if (x) { [flags addObject:@#x]; }
|
|
ADD_FLAG_IF_SET(military_jammer_active);
|
|
ADD_FLAG_IF_SET(docking_match_rotation);
|
|
ADD_FLAG_IF_SET(pitching_over);
|
|
ADD_FLAG_IF_SET(reportAIMessages);
|
|
ADD_FLAG_IF_SET(being_mined);
|
|
ADD_FLAG_IF_SET(being_fined);
|
|
ADD_FLAG_IF_SET(isHulk);
|
|
ADD_FLAG_IF_SET(trackCloseContacts);
|
|
ADD_FLAG_IF_SET(isNearPlanetSurface);
|
|
ADD_FLAG_IF_SET(isFrangible);
|
|
ADD_FLAG_IF_SET(cloaking_device_active);
|
|
ADD_FLAG_IF_SET(canFragment);
|
|
ADD_FLAG_IF_SET(proximity_alert);
|
|
flagsString = [flags count] ? [flags componentsJoinedByString:@", "] : (NSString *)@"none";
|
|
OOLog(@"dumpState.shipEntity", @"Flags: %@", flagsString);
|
|
}
|
|
#endif
|
|
|
|
|
|
- (OOScript *)script
|
|
{
|
|
return script;
|
|
}
|
|
|
|
|
|
- (NSDictionary *)scriptInfo
|
|
{
|
|
return (scriptInfo != nil) ? scriptInfo : (NSDictionary *)[NSDictionary dictionary];
|
|
}
|
|
|
|
|
|
- (Entity *)entityForShaderProperties
|
|
{
|
|
return [self rootShipEntity];
|
|
}
|
|
|
|
|
|
// *** Script event dispatch.
|
|
// For ease of overriding, these all go through doScriptEvent:withArguments:.
|
|
- (void) doScriptEvent:(NSString *)message
|
|
{
|
|
[self doScriptEvent:message withArguments:nil];
|
|
}
|
|
|
|
|
|
- (void) doScriptEvent:(NSString *)message withArgument:(id)argument
|
|
{
|
|
NSArray *arguments = nil;
|
|
|
|
if (argument == nil)
|
|
[self doScriptEvent:message];
|
|
else
|
|
{
|
|
arguments = [NSArray arrayWithObject:argument];
|
|
[self doScriptEvent:message withArguments:arguments];
|
|
}
|
|
}
|
|
|
|
|
|
- (void) doScriptEvent:(NSString *)message
|
|
withArgument:(id)argument1
|
|
andArgument:(id)argument2
|
|
{
|
|
NSArray *arguments = nil;
|
|
|
|
if (argument1 == nil) argument1 = [NSNull null];
|
|
if (argument2 == nil) argument2 = [NSNull null];
|
|
arguments = [NSArray arrayWithObjects:argument1, argument2, nil];
|
|
|
|
NS_DURING
|
|
[self doScriptEvent:message withArguments:arguments];
|
|
NS_HANDLER
|
|
OOLog(kOOLogException, @"***** Exception while performing script event %@ for %@: %@ : %@", message, [self shortDescription], [localException name], [localException reason]);
|
|
NS_ENDHANDLER
|
|
}
|
|
|
|
|
|
- (void) doScriptEvent:(NSString *)message withArguments:(NSArray *)arguments
|
|
{
|
|
[script doEvent:message withArguments:arguments];
|
|
}
|
|
|
|
|
|
- (void) reactToAIMessage:(NSString *)message
|
|
{
|
|
[shipAI reactToMessage:message];
|
|
}
|
|
|
|
|
|
- (void) sendAIMessage:(NSString *)message
|
|
{
|
|
[shipAI message:message];
|
|
}
|
|
|
|
|
|
- (void) doScriptEvent:(NSString *)scriptEvent andReactToAIMessage:(NSString *)aiMessage
|
|
{
|
|
[self doScriptEvent:scriptEvent];
|
|
[self reactToAIMessage:aiMessage];
|
|
}
|
|
|
|
|
|
- (void) doScriptEvent:(NSString *)scriptEvent withArgument:(id)argument andReactToAIMessage:(NSString *)aiMessage
|
|
{
|
|
[self doScriptEvent:scriptEvent withArgument:argument];
|
|
[self reactToAIMessage:aiMessage];
|
|
}
|
|
|
|
|
|
// Exposed to AI and scripts.
|
|
- (void) doNothing
|
|
{
|
|
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation Entity (SubEntityRelationship)
|
|
|
|
- (BOOL) isShipWithSubEntityShip:(Entity *)other
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation ShipEntity (SubEntityRelationship)
|
|
|
|
- (BOOL) isShipWithSubEntityShip:(Entity *)other
|
|
{
|
|
assert ([self isShip]);
|
|
|
|
if (![other isShip]) return NO;
|
|
if (![other isSubEntity]) return NO;
|
|
if ([other owner] != self) return NO;
|
|
|
|
#ifndef NDEBUG
|
|
// Sanity check; this should always be true.
|
|
if (![self hasSubEntity:(ShipEntity *)other])
|
|
{
|
|
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.");
|
|
[other setOwner:nil];
|
|
return NO;
|
|
}
|
|
#endif
|
|
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
NSDictionary *DefaultShipShaderMacros(void)
|
|
{
|
|
static NSDictionary *macros = nil;
|
|
|
|
if (macros == nil)
|
|
{
|
|
macros = [[[ResourceManager materialDefaults] oo_dictionaryForKey:@"ship-prefix-macros" defaultValue:[NSDictionary dictionary]] retain];
|
|
}
|
|
|
|
return macros;
|
|
}
|
|
|
|
|
|
BOOL OOUniformBindingPermitted(NSString *propertyName, id bindingTarget)
|
|
{
|
|
static NSSet *entityWhitelist = nil;
|
|
static NSSet *shipWhitelist = nil;
|
|
static NSSet *playerShipWhitelist = nil;
|
|
|
|
if (entityWhitelist == nil)
|
|
{
|
|
NSDictionary *wlDict = [ResourceManager whitelistDictionary];
|
|
entityWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_entity_binding_methods"]];
|
|
shipWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_ship_binding_methods"]];
|
|
playerShipWhitelist = [[NSSet alloc] initWithArray:[wlDict oo_arrayForKey:@"shader_player_ship_binding_methods"]];
|
|
}
|
|
|
|
if ([bindingTarget isKindOfClass:[Entity class]])
|
|
{
|
|
if ([entityWhitelist containsObject:propertyName]) return YES;
|
|
}
|
|
if ([bindingTarget isKindOfClass:[ShipEntity class]])
|
|
{
|
|
if ([shipWhitelist containsObject:propertyName]) return YES;
|
|
}
|
|
if ([bindingTarget isKindOfClass:[PlayerEntity class]])
|
|
{
|
|
if ([playerShipWhitelist containsObject:propertyName]) return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|