git-svn-id: http://svn.berlios.de/svnroot/repos/oolite-linux/trunk@1733 127b21dd-08f5-0310-b4b7-95ae10353056
8190 lines
211 KiB
Objective-C
8190 lines
211 KiB
Objective-C
/*
|
|
|
|
ShipEntity.m
|
|
|
|
Oolite
|
|
Copyright (C) 2004-2008 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.
|
|
|
|
*/
|
|
|
|
#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 "OOCharacter.h"
|
|
#import "AI.h"
|
|
#ifdef OO_BRAIN_AI
|
|
#import "OOBrain.h"
|
|
#endif
|
|
|
|
#import "OOMesh.h"
|
|
#import "Geometry.h"
|
|
#import "Octree.h"
|
|
#import "OOColor.h"
|
|
|
|
#import "ParticleEntity.h"
|
|
#import "StationEntity.h"
|
|
#import "PlanetEntity.h"
|
|
#import "PlayerEntity.h"
|
|
#import "PlayerEntityLegacyScriptEngine.h"
|
|
#import "WormholeEntity.h"
|
|
#import "GuiDisplayGen.h"
|
|
#import "HeadUpDisplay.h"
|
|
#import "OOEntityFilterPredicate.h"
|
|
#import "OOEquipmentType.h"
|
|
|
|
#import "OODebugGLDrawing.h"
|
|
|
|
#import "OOScript.h"
|
|
|
|
|
|
#define kOOLogUnconvertedNSLog @"unclassified.ShipEntity"
|
|
|
|
|
|
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
|
|
|
|
@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;
|
|
status = 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 %@ generated with infinite top speed!", self);
|
|
maxFlightSpeed = 300;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
|
|
- (BOOL) setUpSubEntities: (NSDictionary *) shipDict
|
|
{
|
|
unsigned int i;
|
|
NSArray *plumes = [shipDict arrayForKey:@"exhaust"];
|
|
|
|
for (i = 0; i < [plumes count]; i++)
|
|
{
|
|
ParticleEntity *exhaust = [[ParticleEntity alloc] initExhaustFromShip:self details:[plumes objectAtIndex:i]];
|
|
[self addExhaust:exhaust];
|
|
[exhaust release];
|
|
}
|
|
|
|
NSArray *subs = [shipDict arrayForKey:@"subentities"];
|
|
|
|
for (i = 0; i < [subs count]; i++)
|
|
{
|
|
NSArray *details = ScanTokensFromString([subs objectAtIndex:i]);
|
|
|
|
if ([details count] == 8)
|
|
{
|
|
Vector sub_pos, ref;
|
|
Quaternion sub_q;
|
|
NSString* subdesc = [details stringAtIndex:0];
|
|
sub_pos.x = [details floatAtIndex:1];
|
|
sub_pos.y = [details floatAtIndex:2];
|
|
sub_pos.z = [details floatAtIndex:3];
|
|
sub_q.w = [details floatAtIndex:4];
|
|
sub_q.x = [details floatAtIndex:5];
|
|
sub_q.y = [details floatAtIndex:6];
|
|
sub_q.z = [details floatAtIndex:7];
|
|
|
|
if ([subdesc isEqual:@"*FLASHER*"])
|
|
{
|
|
ParticleEntity *flasher;
|
|
flasher = [[ParticleEntity alloc]
|
|
initFlasherWithSize:sub_q.z
|
|
frequency:sub_q.x
|
|
phase:2.0 * sub_q.y];
|
|
[flasher setColor:[OOColor colorWithCalibratedHue:sub_q.w/360.0
|
|
saturation:1.0
|
|
brightness:1.0
|
|
alpha:1.0]];
|
|
[flasher setPosition:sub_pos];
|
|
[self addFlasher:flasher];
|
|
[flasher release];
|
|
}
|
|
else
|
|
{
|
|
ShipEntity* subent;
|
|
quaternion_normalize(&sub_q);
|
|
|
|
subent = [UNIVERSE newShipWithName:subdesc]; // retained
|
|
if (subent == nil)
|
|
{
|
|
// Failing to find a subentity could result in a partial ship, which'd be, y'know, weird.
|
|
return NO;
|
|
}
|
|
|
|
if ((self->isStation)&&([subdesc rangeOfString:@"dock"].location != NSNotFound))
|
|
[(StationEntity*)self setDockingPortModel:(ShipEntity*)subent :sub_pos :sub_q];
|
|
|
|
[(ShipEntity*)subent setStatus:STATUS_INACTIVE];
|
|
|
|
ref = vector_forward_from_quaternion(sub_q); // VECTOR FORWARD
|
|
|
|
[(ShipEntity*)subent setReference: ref];
|
|
[(ShipEntity*)subent setPosition: sub_pos];
|
|
[(ShipEntity*)subent setOrientation: sub_q];
|
|
|
|
[self addSolidSubentityToCollisionRadius:(ShipEntity*)subent];
|
|
|
|
[self addSubEntity:subent];
|
|
[subent release];
|
|
}
|
|
}
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL) setUpShipFromDictionary:(NSDictionary *) dict
|
|
{
|
|
NSDictionary *shipDict = dict;
|
|
|
|
orientation = kIdentityQuaternion;
|
|
rotMatrix = kIdentityMatrix;
|
|
v_forward = kBasisZVector;
|
|
v_up = kBasisYVector;
|
|
v_right = kBasisXVector;
|
|
reference = v_forward; // reference vector for (* turrets *)
|
|
|
|
isShip = YES;
|
|
|
|
// 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)
|
|
{
|
|
OOLog(@"ship.setUp.like_ship", @"***** Error: like_ship found in ship dictionary in -[ShipEntity setUpShipFromDictionary:], 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 things from dictionary from here out
|
|
maxFlightSpeed = [shipDict floatForKey:@"max_flight_speed"];
|
|
max_flight_roll = [shipDict floatForKey:@"max_flight_roll"];
|
|
max_flight_pitch = [shipDict floatForKey:@"max_flight_pitch"];
|
|
max_flight_yaw = [shipDict floatForKey:@"max_flight_yaw" defaultValue:max_flight_pitch]; // Note by default yaw == pitch
|
|
|
|
thrust = [shipDict floatForKey:@"thrust"];
|
|
|
|
maxEnergy = [shipDict floatForKey:@"max_energy"];
|
|
energy_recharge_rate = [shipDict floatForKey:@"energy_recharge_rate"];
|
|
|
|
forward_weapon_type = StringToWeaponType([shipDict stringForKey:@"forward_weapon_type" defaultValue:@"WEAPON_NONE"]);
|
|
aft_weapon_type = StringToWeaponType([shipDict stringForKey:@"aft_weapon_type" defaultValue:@"WEAPON_NONE"]);
|
|
[self setWeaponDataFromType:forward_weapon_type];
|
|
|
|
weapon_energy = [shipDict floatForKey:@"weapon_energy"];
|
|
scannerRange = [shipDict floatForKey:@"scanner_range" defaultValue:25600.0];
|
|
missiles = [shipDict intForKey:@"missiles"];
|
|
|
|
// upgrades:
|
|
if ([shipDict fuzzyBooleanForKey:@"has_ecm"]) [self addEquipmentItem:@"EQ_ECM"];
|
|
if ([shipDict fuzzyBooleanForKey:@"has_scoop"]) [self addEquipmentItem:@"EQ_FUEL_SCOOPS"];
|
|
if ([shipDict fuzzyBooleanForKey:@"has_escape_pod"]) [self addEquipmentItem:@"EQ_ESCAPE_POD"];
|
|
if ([shipDict fuzzyBooleanForKey:@"has_energy_bomb"]) [self addEquipmentItem:@"EQ_ENERGY_BOMB"];
|
|
if ([shipDict fuzzyBooleanForKey:@"has_cloaking_device"]) [self addEquipmentItem:@"EQ_CLOAKING_DEVICE"];
|
|
if (![UNIVERSE strict])
|
|
{
|
|
// These items are not available in strict mode.
|
|
if ([shipDict fuzzyBooleanForKey:@"has_fuel_injection"]) [self addEquipmentItem:@"EQ_FUEL_INJECTION"];
|
|
if ([shipDict fuzzyBooleanForKey:@"has_military_jammer"]) [self addEquipmentItem:@"EQ_MILITARY_JAMMER"];
|
|
if ([shipDict fuzzyBooleanForKey:@"has_military_scanner_filter"]) [self addEquipmentItem:@"EQ_MILITARY_SCANNER_FILTER"];
|
|
}
|
|
|
|
canFragment = [shipDict fuzzyBooleanForKey:@"fragment_chance" defaultValue:0.9];
|
|
|
|
cloaking_device_active = NO;
|
|
military_jammer_active = NO;
|
|
|
|
// FIXME: give NPCs shields instead.
|
|
if ([shipDict fuzzyBooleanForKey:@"has_shield_booster"])
|
|
{
|
|
maxEnergy += 256.0f;
|
|
}
|
|
if ([shipDict fuzzyBooleanForKey:@"has_shield_enhancer"])
|
|
{
|
|
maxEnergy += 256.0f;
|
|
energy_recharge_rate *= 1.5;
|
|
}
|
|
|
|
// Moved here from above upgrade loading so that ships start with full energy banks. -- Ahruman
|
|
energy = maxEnergy;
|
|
|
|
fuel = [shipDict unsignedShortForKey:@"fuel"]; // Does it make sense that this defaults to 0? Should it not be 70? -- Ahruman
|
|
fuel_accumulator = 1.0;
|
|
|
|
if (![UNIVERSE strict])
|
|
{
|
|
hyperspaceMotorSpinTime = [shipDict floatForKey:@"hyperspace_motor_spin_time" defaultValue:DEFAULT_HYPERSPACE_SPIN_TIME];
|
|
}
|
|
else
|
|
{
|
|
hyperspaceMotorSpinTime = DEFAULT_HYPERSPACE_SPIN_TIME;
|
|
}
|
|
|
|
bounty = [shipDict unsignedIntForKey:@"bounty"];
|
|
|
|
[shipAI autorelease];
|
|
shipAI = [[AI alloc] init];
|
|
[shipAI setStateMachine:[shipDict stringForKey:@"ai_type" defaultValue:@"nullAI.plist"]];
|
|
|
|
max_cargo = [shipDict unsignedIntForKey:@"max_cargo"];
|
|
likely_cargo = [shipDict unsignedIntForKey:@"likely_cargo"];
|
|
extra_cargo = [shipDict unsignedIntForKey:@"extra_cargo" defaultValue:15];
|
|
if ([shipDict fuzzyBooleanForKey:@"no_boulders"]) noRocks = YES;
|
|
|
|
NSString *cargoString = [shipDict 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 stringForKey:@"cargo_carried"]];
|
|
}
|
|
|
|
if (c_commodity != CARGO_UNDEFINED) [self setCommodity:c_commodity andAmount:c_amount];
|
|
}
|
|
|
|
cargoString = [shipDict stringForKey:@"cargo_type"];
|
|
if (cargoString)
|
|
{
|
|
cargo_type = StringToCargoType(cargoString);
|
|
|
|
[cargo autorelease];
|
|
cargo = [[NSMutableArray alloc] initWithCapacity:max_cargo]; // alloc retains;
|
|
}
|
|
|
|
// Load the model (must be before subentities)
|
|
NSString *modelName = [shipDict stringForKey:@"model"];
|
|
if (modelName != nil)
|
|
{
|
|
OOMesh *mesh = [OOMesh meshWithName:modelName
|
|
materialDictionary:[shipDict dictionaryForKey:@"materials"]
|
|
shadersDictionary:[shipDict dictionaryForKey:@"shaders"]
|
|
smooth:[shipDict boolForKey:@"smooth"]
|
|
shaderMacros:DefaultShipShaderMacros()
|
|
shaderBindingTarget:self];
|
|
[self setMesh:mesh];
|
|
}
|
|
|
|
float density = [shipDict floatForKey:@"density" defaultValue:1.0];
|
|
if (octree) mass = density * 20.0 * [octree volume];
|
|
|
|
[name autorelease];
|
|
name = [[shipDict stringForKey:@"name" defaultValue:name] copy];
|
|
|
|
[displayName autorelease];
|
|
displayName = [[shipDict stringForKey:@"display_name" defaultValue:name] copy];
|
|
|
|
[roleSet release];
|
|
roleSet = [[[OORoleSet roleSetWithString:[shipDict stringForKey:@"roles"]] roleSetWithRemovedRole:@"player"] retain];
|
|
[primaryRole release];
|
|
primaryRole = nil;
|
|
|
|
[self setOwner:self];
|
|
|
|
[self setHulk:[shipDict boolForKey:@"is_hulk"]];
|
|
|
|
if (![self setUpSubEntities: shipDict])
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
isFrangible = [shipDict boolForKey:@"frangible" defaultValue:YES];
|
|
|
|
OOColor *color = [OOColor brightColorWithDescription:[shipDict objectForKey:@"laser_color"]];
|
|
if (color == nil) color = [OOColor redColor];
|
|
[self setLaserColor:color];
|
|
|
|
// scan class. NOTE: non-standard capitalization is documented and entrenched.
|
|
scanClass = StringToScanClass([shipDict objectForKey:@"scanClass"]);
|
|
|
|
// accuracy. Must come after scanClass, because we are using scanClass to determine if this is a missile.
|
|
accuracy = [shipDict 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
|
|
escortCount = [shipDict unsignedIntForKey:@"escorts"];
|
|
escortsAreSetUp = (escortCount == 0);
|
|
|
|
// beacons
|
|
[self setBeaconCode:[shipDict stringForKey:@"beacon"]];
|
|
|
|
// rotating subentities
|
|
subentityRotationalVelocity = kIdentityQuaternion;
|
|
ScanQuaternionFromString([shipDict objectForKey:@"rotational_velocity"], &subentityRotationalVelocity);
|
|
|
|
// contact tracking entities
|
|
if ([shipDict objectForKey:@"track_contacts"])
|
|
{
|
|
[self setTrackCloseContacts:[shipDict boolForKey:@"track_contacts"]];
|
|
}
|
|
else
|
|
{
|
|
[self setTrackCloseContacts:NO];
|
|
}
|
|
|
|
// 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);
|
|
|
|
// ship skin insulation factor (1.0 is normal)
|
|
[self setHeatInsulation:[shipDict floatForKey:@"heat_insulation" defaultValue:[self hasHeatShield] ? 2.0 : 1.0]];
|
|
|
|
// crew and passengers
|
|
NSDictionary* cdict = [[UNIVERSE characters] objectForKey:[shipDict stringForKey:@"pilot"]];
|
|
if (cdict != nil)
|
|
{
|
|
OOCharacter *pilot = [OOCharacter characterWithDictionary:cdict];
|
|
[self setCrew:[NSArray arrayWithObject:pilot]];
|
|
}
|
|
|
|
// unpiloted (like missiles asteroids etc.)
|
|
if ([shipDict fuzzyBooleanForKey:@"unpiloted"]) [self setCrew:nil];
|
|
|
|
// Get scriptInfo dictionary, containing arbitrary stuff scripts might be interested in.
|
|
scriptInfo = [[shipDict dictionaryForKey:@"script_info" defaultValue:nil] retain];
|
|
|
|
[self setShipScript:[shipDict stringForKey:@"script"]];
|
|
|
|
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];
|
|
[script release];
|
|
|
|
[previousCondition release];
|
|
|
|
[dockingInstructions release];
|
|
|
|
[crew release];
|
|
|
|
[lastRadioMessage autorelease];
|
|
|
|
[octree autorelease];
|
|
|
|
[self setSubEntityTakingDamage:nil];
|
|
[self removeAllEquipment];
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
|
|
- (void) clearSubEntities
|
|
{
|
|
[subEntities makeObjectsPerformSelector:@selector(setOwner:) withObject:nil]; // Ensure backlinks are broken
|
|
[subEntities release];
|
|
subEntities = nil;
|
|
}
|
|
|
|
|
|
- (NSString *)descriptionComponents
|
|
{
|
|
return [NSString stringWithFormat:@"\"%@\" %@", [self name], [super descriptionComponents]];
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
}
|
|
|
|
|
|
- (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 *)particleSubEntityEnumerator
|
|
{
|
|
return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isParticle)];
|
|
}
|
|
|
|
|
|
- (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
|
|
|
|
|
|
- (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 (status == STATUS_IN_FLIGHT) // just popped into existence
|
|
{
|
|
if ((!escortsAreSetUp) && (escortCount > 0)) [self setUpEscorts];
|
|
}
|
|
else
|
|
{
|
|
escortsAreSetUp = YES; // we don't do this ourself!
|
|
}
|
|
}
|
|
|
|
// Tell subentities, too
|
|
[subEntities makeObjectsPerformSelector:@selector(wasAddedToUniverse)];
|
|
|
|
[self resetTracking]; // resets stuff for tracking/exhausts
|
|
}
|
|
|
|
|
|
- (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;
|
|
|
|
if ([self isPolice]) defaultRole = @"wingman";
|
|
|
|
escortRole = [shipinfoDictionary stringForKey:@"escort-role" defaultValue:defaultRole];
|
|
if (![escortRole isEqualToString: defaultRole])
|
|
{
|
|
if (![[UNIVERSE newShipWithRole:escortRole] autorelease])
|
|
{
|
|
escortRole = defaultRole;
|
|
}
|
|
}
|
|
|
|
escortShipKey = [shipinfoDictionary stringForKey:@"escort-ship"];
|
|
if (escortShipKey != nil)
|
|
{
|
|
if (![[UNIVERSE newShipWithName:escortShipKey] autorelease])
|
|
{
|
|
escortShipKey = nil;
|
|
}
|
|
}
|
|
|
|
while (escortCount > 0)
|
|
{
|
|
Vector ex_pos = [self coordinatesForEscortPosition:escortCount - 1];
|
|
|
|
ShipEntity *escorter = nil;
|
|
|
|
if (escortShipKey)
|
|
escorter = [UNIVERSE newShipWithName:escortShipKey]; // retained
|
|
else
|
|
escorter = [UNIVERSE newShipWithRole:escortRole]; // retained
|
|
|
|
if (!escorter) break;
|
|
|
|
if (![escorter crew])
|
|
{
|
|
[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
|
|
|
|
[UNIVERSE addEntity:escorter];
|
|
|
|
escortShipDict = [escorter shipInfoDictionary];
|
|
autoAIMap = [ResourceManager dictionaryFromFilesNamed:@"autoAImap.plist" inFolder:@"Config" andMerge:YES];
|
|
autoAI = [autoAIMap stringForKey:defaultRole];
|
|
if (autoAI==nil) // no 'wingman' defined in autoAImap?
|
|
{
|
|
autoAI = [autoAIMap stringForKey:@"escort" defaultValue:@"nullAI.plist"];
|
|
}
|
|
if (escortShipKey && [escortShipDict fuzzyBooleanForKey:@"auto_ai" defaultValue:YES]) //setAITo only once!
|
|
{
|
|
[escorter setAITo:autoAI];
|
|
}
|
|
|
|
escortAI = [escorter getAI];
|
|
if ([[escortAI name] isEqualToString: @"nullAI.plist"] && ![autoAI isEqualToString:@"nullAI.plist"])
|
|
{
|
|
[escortAI setStateMachine:autoAI]; // must happen after adding to the UNIVERSE!
|
|
}
|
|
|
|
[escorter setGroupID:universalID];
|
|
[self setGroupID:universalID]; // make self part of same group
|
|
[escorter setOwner: self]; // make self group leader
|
|
|
|
[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];
|
|
escortCount--;
|
|
}
|
|
if (escortCount == 0) escortsAreSetUp = YES;
|
|
}
|
|
|
|
|
|
- (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->shot_time < 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 other_position = [prime absolutePositionForSubentity];
|
|
Triangle other_ijk = [prime absoluteIJKForSubentity];
|
|
Vector prime_position = [other absolutePositionForSubentity];
|
|
Triangle prime_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];
|
|
}
|
|
|
|
|
|
- (Vector) absolutePositionForSubentity
|
|
{
|
|
return [self absolutePositionForSubentityOffset:kZeroVector];
|
|
}
|
|
|
|
|
|
- (Vector) absolutePositionForSubentityOffset:(Vector) offset
|
|
{
|
|
Vector abspos = vector_add(position, OOVectorMultiplyMatrix(offset, rotMatrix));
|
|
Entity *last = nil;
|
|
Entity *father = [self parentEntity];
|
|
OOMatrix r_mat;
|
|
|
|
while ((father)&&(father != last))
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
abspos = vector_add(OOVectorMultiplyMatrix(abspos, r_mat), [father position]);
|
|
last = father;
|
|
father = [father owner];
|
|
}
|
|
return abspos;
|
|
}
|
|
|
|
|
|
- (Triangle) absoluteIJKForSubentity
|
|
{
|
|
Triangle result = {{ kBasisXVector, kBasisYVector, kBasisZVector, kZeroVector }};
|
|
Entity *last = nil;
|
|
Entity *father = self;
|
|
OOMatrix r_mat;
|
|
|
|
while ((father)&&(father != last))
|
|
{
|
|
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;
|
|
father = [father owner];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
- (void) addSolidSubentityToCollisionRadius:(ShipEntity*) subent
|
|
{
|
|
if (!subent)
|
|
return;
|
|
|
|
double distance = sqrt(magnitude2(subent->position)) + [subent findCollisionRadius];
|
|
if (distance > collision_radius)
|
|
collision_radius = distance;
|
|
|
|
mass += 20.0 * [subent->octree volume];
|
|
}
|
|
|
|
|
|
- (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 %@ has infinite top speed!", self);
|
|
maxFlightSpeed = 300;
|
|
}
|
|
|
|
//
|
|
// deal with collisions
|
|
//
|
|
[self manageCollisions];
|
|
[self saveToLastFrame];
|
|
|
|
//
|
|
// 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;
|
|
PlanetEntity *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
|
|
//
|
|
if (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 - 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 && status == STATUS_IN_FLIGHT)
|
|
{
|
|
[[PlayerEntity sharedPlayer] setScriptTarget:self];
|
|
[self doScriptEvent:@"shipSpawned"];
|
|
haveExecutedSpawnAction = YES;
|
|
}
|
|
|
|
// behaviours according to status and behaviour
|
|
//
|
|
if (status == STATUS_LAUNCHING)
|
|
{
|
|
if ([UNIVERSE getTime] > launch_time + LAUNCH_DELAY) // move for while before thinking
|
|
{
|
|
status = STATUS_IN_FLIGHT;
|
|
[self doScriptEvent:@"shipLaunchedFromStation"];
|
|
[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 (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)
|
|
{
|
|
// escaped tractor beam
|
|
status = STATUS_IN_FLIGHT; // should correct 'uncollidable objects' bug
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
}
|
|
}
|
|
|
|
if (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
|
|
{
|
|
double target_speed = maxFlightSpeed;
|
|
|
|
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];
|
|
}
|
|
else
|
|
{
|
|
target_speed = [target flightSpeed];
|
|
if (target_speed < maxFlightSpeed)
|
|
{
|
|
target_speed += maxFlightSpeed;
|
|
target_speed /= 2.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (escortCount > 0)
|
|
{
|
|
unsigned i;
|
|
for (i = 0; i < escortCount; i++)
|
|
{
|
|
ShipEntity *escorter = [UNIVERSE entityForUniversalID:escort_ids[i]];
|
|
// check it's still an escort ship
|
|
BOOL escorter_okay = (escorter != nil) && escorter->isShip;
|
|
|
|
if (escorter_okay)
|
|
[escorter setDestination:[self coordinatesForEscortPosition:i]]; // update its destination
|
|
else
|
|
escort_ids[i--] = escort_ids[--escortCount]; // remove the escort
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
if (groupID != NO_TARGET && groupID == [hunter groupID])
|
|
{
|
|
//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 *group_leader = [UNIVERSE entityForUniversalID:groupID];
|
|
if (hunter == group_leader)
|
|
{
|
|
//oops we were attacked by our leader, desert him
|
|
[self setGroupID:NO_TARGET];
|
|
}
|
|
else
|
|
{
|
|
//evict them from our group
|
|
[hunter setGroupID:NO_TARGET];
|
|
if ((group_leader)&&(group_leader->isShip))
|
|
{
|
|
[group_leader setFound_target:other];
|
|
[group_leader setPrimaryAggressor:hunter];
|
|
[group_leader respondToAttackFrom:from becauseOf:other];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
source = from;
|
|
}
|
|
[self doScriptEvent:@"shipBeingAttacked" withArgument:source andReactToAIMessage:@"ATTACKED"];
|
|
}
|
|
|
|
|
|
// Equipment
|
|
|
|
- (BOOL) hasEquipmentItem:(id)equipmentKeys
|
|
{
|
|
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 ([_equipment containsObject:key]) return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (BOOL) hasAllEquipment:(id)equipmentKeys
|
|
{
|
|
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 (![_equipment containsObject:key]) return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) canAddEquipment:(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;
|
|
|
|
// FIXME: deal with special handling of missiles and mines.
|
|
if ([self hasEquipmentItem:equipmentKey]) 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]]) return NO;
|
|
if ([eqType requiresAnyEquipment] != nil && ![self hasEquipmentItem:[eqType requiresAnyEquipment]]) return NO;
|
|
if ([eqType incompatibleEquipment] != nil && [self hasEquipmentItem:[eqType incompatibleEquipment]]) 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;
|
|
}
|
|
|
|
|
|
- (void) addEquipmentItem:(NSString *)equipmentKey
|
|
{
|
|
[self addEquipmentItem:equipmentKey withValidation:YES];
|
|
}
|
|
|
|
|
|
- (void) addEquipmentItem:(NSString *)equipmentKey withValidation:(BOOL)validateAddition
|
|
{
|
|
OOEquipmentType *eqType = nil;
|
|
|
|
if (validateAddition == YES && ![self canAddEquipment:equipmentKey]) return;
|
|
eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey];
|
|
|
|
// FIXME: deal with special handling of missiles and mines.
|
|
if ([eqType isMissileOrMine]) return;
|
|
|
|
if (_equipment == nil) _equipment = [[NSMutableSet alloc] init];
|
|
|
|
// if we heve one of these with a differen damage status - remove it first
|
|
NSString *alterKey = nil;
|
|
if ([equipmentKey hasSuffix:@"_DAMAGED"])
|
|
{
|
|
alterKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]];
|
|
}
|
|
else
|
|
{
|
|
alterKey = [equipmentKey stringByAppendingString:@"_DAMAGED"];
|
|
}
|
|
[_equipment removeObject:alterKey];
|
|
|
|
// add the equipment
|
|
[_equipment addObject:equipmentKey];
|
|
}
|
|
|
|
|
|
- (NSEnumerator *) equipmentEnumerator
|
|
{
|
|
return [_equipment objectEnumerator];
|
|
}
|
|
|
|
|
|
- (unsigned) equipmentCount
|
|
{
|
|
return [_equipment count];
|
|
}
|
|
|
|
|
|
- (void) removeEquipmentItem:(NSString *)equipmentKey
|
|
{
|
|
[_equipment removeObject:equipmentKey];
|
|
if ([_equipment count] == 0) [self removeAllEquipment];
|
|
}
|
|
|
|
|
|
- (void) removeAllEquipment
|
|
{
|
|
[_equipment release];
|
|
_equipment = nil;
|
|
}
|
|
- (unsigned) passengerCount
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
- (unsigned) passengerCapacity
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
|
|
- (unsigned) missileCount
|
|
{
|
|
return missiles;
|
|
}
|
|
|
|
|
|
- (unsigned) missileCapacity
|
|
{
|
|
// FIXME: need useful maximum from shipdata key max_missiles (or missiles if no max specified).
|
|
return missiles;
|
|
}
|
|
|
|
|
|
- (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
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_MILITARY_SCANNER_FILTER"];
|
|
}
|
|
|
|
|
|
- (BOOL) hasMilitaryJammer
|
|
{
|
|
return [self hasEquipmentItem:@"EQ_MILITARY_JAMMER"];
|
|
}
|
|
|
|
|
|
- (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.0;
|
|
if ([self hasShieldBooster]) boostFactor += 1.0;
|
|
if ([self hasMilitaryShieldEnhancer]) boostFactor += 1.0;
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
////////////////
|
|
// //
|
|
// 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
|
|
{
|
|
double distance = [self rangeToDestination];
|
|
desired_range = collision_radius * 2.0;
|
|
ShipEntity* hauler = (ShipEntity*)[self owner];
|
|
if ((hauler)&&([hauler isShip]))
|
|
{
|
|
if (distance < desired_range)
|
|
{
|
|
behaviour = BEHAVIOUR_TUMBLE;
|
|
status = STATUS_IN_FLIGHT;
|
|
[hauler scoopUp:self];
|
|
return;
|
|
}
|
|
GLfloat tf = TRACTOR_FORCE / mass;
|
|
destination = [hauler absoluteTractorPosition];
|
|
// 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 (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
|
|
status = STATUS_IN_FLIGHT;
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[shipAI exitStateMachine]; // 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
|
|
{
|
|
[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
|
|
{
|
|
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)
|
|
{
|
|
[self noteLostTarget];
|
|
}
|
|
}
|
|
//
|
|
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];
|
|
}
|
|
//
|
|
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)
|
|
{
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
[self noteLostTarget];
|
|
}
|
|
|
|
// 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 distance = [self rangeToDestination];
|
|
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];
|
|
}
|
|
|
|
[self trackDestination:delta_t :NO];
|
|
|
|
// 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 ((range < 650)||(proximity_alert != NO_TARGET))
|
|
{
|
|
if (proximity_alert == NO_TARGET)
|
|
{
|
|
desired_speed = range * maxFlightSpeed / (650.0 * 16.0);
|
|
}
|
|
else
|
|
{
|
|
[self avoidCollision];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (range > SCANNER_MAX_RANGE) [self noteLostTarget];
|
|
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 ((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];
|
|
}
|
|
}
|
|
|
|
// 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 (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)
|
|
{
|
|
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)
|
|
[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))
|
|
{
|
|
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;
|
|
}
|
|
else
|
|
{
|
|
behaviour = BEHAVIOUR_FLY_TO_DESTINATION;
|
|
}
|
|
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];
|
|
desired_speed = 0.0;
|
|
if (desired_range > 1.0)
|
|
max_cos = sqrt(1 - desired_range*desired_range/(distance * distance));
|
|
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"];
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.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_formation_form_up:(double) delta_t
|
|
{
|
|
// get updated destination from owner
|
|
ShipEntity* leadShip = [self owner];
|
|
double distance = [self rangeToDestination];
|
|
double eta = (distance - desired_range) / flightSpeed;
|
|
if ((eta < 5.0)&&(leadShip)&&(leadShip->isShip))
|
|
desired_speed = [leadShip flightSpeed] * 1.25;
|
|
else
|
|
desired_speed = maxFlightSpeed;
|
|
[self behaviour_fly_to_destination: 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"];
|
|
behaviour = BEHAVIOUR_IDLE;
|
|
frustration = 0.0;
|
|
desired_speed = 0.0;
|
|
}
|
|
else
|
|
{
|
|
double last_success_factor = success_factor;
|
|
double last_distance = last_success_factor;
|
|
success_factor = distance;
|
|
|
|
// do the actual piloting!!
|
|
[self trackDestination:delta_t: NO];
|
|
|
|
GLfloat eta = (distance - desired_range) / (0.51 * flightSpeed); // 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 > slowdownTime * 10.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;
|
|
}
|
|
else
|
|
{
|
|
desired_speed = maxFlightSpeed;
|
|
}
|
|
[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;
|
|
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 p1 = turret_target->position;
|
|
Vector p0 = turret_owner->position;
|
|
double cr = turret_owner->collision_radius;
|
|
p1.x -= p0.x; p1.y -= p0.y; p1.z -= p0.z;
|
|
if (aim > .95)
|
|
[self fireTurretCannon: sqrt(magnitude2(p1)) - 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 = unit_vector(&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 = sqrtf(magnitude2(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;
|
|
}
|
|
|
|
|
|
- (void) saveToLastFrame
|
|
{
|
|
double t_now = [UNIVERSE getTime];
|
|
|
|
if (t_now >= trackTime + 0.1) // update at most every 1/10 of a second
|
|
{
|
|
// save previous data
|
|
Quaternion qrot = [self normalOrientation];
|
|
trackTime = t_now;
|
|
track[trackIndex].position = position;
|
|
track[trackIndex].orientation = qrot;
|
|
track[trackIndex].timeframe = trackTime;
|
|
track[trackIndex].k = v_forward;
|
|
|
|
// Update exhaust
|
|
NSEnumerator *subEnum = nil;
|
|
ShipEntity *se = nil;
|
|
Frame thisFrame = { trackTime, kZeroVector, qrot, v_forward };
|
|
|
|
for (subEnum = [self exhaustEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
Vector sepos = [se position];
|
|
thisFrame.position = make_vector(
|
|
position.x + v_right.x * sepos.x + v_up.x * sepos.y + v_forward.x * sepos.z,
|
|
position.y + v_right.y * sepos.x + v_up.y * sepos.y + v_forward.y * sepos.z,
|
|
position.z + v_right.z * sepos.x + v_up.z * sepos.y + v_forward.z * sepos.z);
|
|
[se saveFrame:thisFrame atIndex:trackIndex]; // syncs subentity trackIndex to this entity
|
|
}
|
|
|
|
trackIndex = (trackIndex + 1 ) & 0xff;
|
|
}
|
|
}
|
|
|
|
|
|
// reset position tracking
|
|
- (void) resetTracking
|
|
{
|
|
Quaternion qrot = [self normalOrientation];
|
|
Vector vi = vector_right_from_quaternion(qrot);
|
|
Vector vj = vector_up_from_quaternion(qrot);
|
|
Vector vk = vector_forward_from_quaternion(qrot);
|
|
Frame resetFrame = { 0, position, qrot, vk };
|
|
|
|
Vector vel = vector_multiply_scalar(vk, flightSpeed);
|
|
|
|
[self resetFramesFromFrame:resetFrame withVelocity:vel];
|
|
|
|
NSEnumerator *subEnum = nil;
|
|
ShipEntity *se = nil;
|
|
|
|
for (subEnum = [self exhaustEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
Vector sepos = [se position];
|
|
resetFrame.position = make_vector(
|
|
position.x + vi.x * sepos.x + vj.x * sepos.y + vk.x * sepos.z,
|
|
position.y + vi.y * sepos.x + vj.y * sepos.y + vk.y * sepos.z,
|
|
position.z + vi.z * sepos.x + vj.z * sepos.y + vk.z * sepos.z);
|
|
[se resetFramesFromFrame:resetFrame withVelocity:vel];
|
|
}
|
|
}
|
|
|
|
|
|
- (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.
|
|
[super drawEntity:immediate :translucent];
|
|
|
|
#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 (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))
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
abspos = vector_add(OOVectorMultiplyMatrix(abspos, r_mat), [father position]);
|
|
last = father;
|
|
father = [father owner];
|
|
}
|
|
|
|
GLLoadOOMatrix([UNIVERSE viewMatrix]);
|
|
glPopMatrix();
|
|
glPushMatrix();
|
|
GLTranslateOOVector(abspos);
|
|
GLMultOOMatrix(rotMatrix);
|
|
|
|
[self drawEntity:immediate :translucent];
|
|
}
|
|
else
|
|
{
|
|
glPushMatrix();
|
|
|
|
GLTranslateOOVector(position);
|
|
GLMultOOMatrix(rotMatrix);
|
|
|
|
[self drawEntity:immediate :translucent];
|
|
|
|
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
|
|
|
|
- (GLfloat *) scannerDisplayColorForShip:(ShipEntity*)otherShip :(BOOL)isHostile :(BOOL)flash
|
|
{
|
|
|
|
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;
|
|
}
|
|
|
|
|
|
- (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];
|
|
}
|
|
|
|
|
|
- (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) addExhaust:(ParticleEntity *)exhaust
|
|
{
|
|
[self addSubEntity:exhaust];
|
|
}
|
|
|
|
|
|
- (void) addFlasher:(ParticleEntity *)flasher
|
|
{
|
|
[self addSubEntity:flasher];
|
|
}
|
|
|
|
|
|
- (void) applyThrust:(double) delta_t
|
|
{
|
|
GLfloat dt_thrust = thrust * delta_t;
|
|
BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL);
|
|
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 = sqrtf(magnitude2(velocity));
|
|
if (velmag)
|
|
{
|
|
GLfloat vscale = (velmag - dt_thrust) / velmag;
|
|
if (vscale < 0.0)
|
|
vscale = 0.0;
|
|
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 ((flightSpeed > maxFlightSpeed) && canBurn)
|
|
{
|
|
fuel_accumulator -= delta_t * AFTERBURNER_NPC_BURNRATE;
|
|
while (fuel_accumulator < 0.0)
|
|
{
|
|
if (fuel-- <= MIN_FUEL)
|
|
max_available_speed = maxFlightSpeed;
|
|
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 setInteger:behaviour forKey:@"behaviour"];
|
|
[previousCondition setInteger:primaryTarget forKey:@"primaryTarget"];
|
|
[previousCondition setFloat:desired_range forKey:@"desired_range"];
|
|
[previousCondition setFloat:desired_speed forKey:@"desired_speed"];
|
|
[previousCondition 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;
|
|
|
|
behaviour = BEHAVIOUR_AVOID_COLLISION;
|
|
}
|
|
}
|
|
|
|
|
|
- (void) resumePostProximityAlert
|
|
{
|
|
if (!previousCondition) return;
|
|
|
|
behaviour = [previousCondition intForKey:@"behaviour"];
|
|
primaryTarget = [previousCondition intForKey:@"primaryTarget"];
|
|
desired_range = [previousCondition floatForKey:@"desired_range"];
|
|
desired_speed = [previousCondition floatForKey:@"desired_speed"];
|
|
destination = [previousCondition 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;
|
|
}
|
|
|
|
|
|
- (int) groupID
|
|
{
|
|
return groupID;
|
|
}
|
|
|
|
|
|
- (void) setGroupID:(int) value
|
|
{
|
|
groupID = value;
|
|
}
|
|
|
|
|
|
- (unsigned) escortCount
|
|
{
|
|
return escortCount;
|
|
}
|
|
|
|
|
|
- (void) setEscortCount:(unsigned) value
|
|
{
|
|
escortCount = value;
|
|
escortsAreSetUp = (escortCount == 0);
|
|
}
|
|
|
|
|
|
- (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
|
|
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];
|
|
other->proximity_alert = 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;
|
|
}
|
|
|
|
|
|
- (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"];
|
|
}
|
|
|
|
|
|
- (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) hasHostileTarget
|
|
{
|
|
if (primaryTarget == NO_TARGET)
|
|
return NO;
|
|
if ((behaviour == BEHAVIOUR_AVOID_COLLISION)&&(previousCondition))
|
|
{
|
|
int old_behaviour = [previousCondition intForKey:@"behaviour"];
|
|
return IsBehaviourHostile(old_behaviour);
|
|
}
|
|
return IsBehaviourHostile(behaviour);
|
|
}
|
|
|
|
|
|
- (GLfloat) weaponRange
|
|
{
|
|
return weaponRange;
|
|
}
|
|
|
|
|
|
- (void) setWeaponRange: (GLfloat) value
|
|
{
|
|
weaponRange = value;
|
|
}
|
|
|
|
|
|
- (void) setWeaponDataFromType: (int) 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 :
|
|
weapon_energy = 0.0; // indicating no weapon!
|
|
weapon_recharge_rate = 0.20; // maximum rate
|
|
weaponRange = 32000;
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
- (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)
|
|
{
|
|
if (aegis_status == AEGIS_IN_DOCKING_RANGE)
|
|
{
|
|
[self doScriptEvent:@"shipExitedStationAegis"];
|
|
[shipAI message:@"AEGIS_LEAVING_DOCKING_RANGE"];
|
|
}
|
|
|
|
PlanetEntity* the_planet;
|
|
if (aegis_status == AEGIS_CLOSE_TO_ANY_PLANET)
|
|
{
|
|
the_planet = [self findPlanetNearestSurface];
|
|
}
|
|
else //must be the main planet!
|
|
{
|
|
the_planet = [UNIVERSE planet];
|
|
}
|
|
|
|
[self doScriptEvent:@"shipExitedPlanetaryVicinity" withArgument:the_planet];
|
|
[shipAI message:@"AWAY_FROM_PLANET"];
|
|
if (aegis_status != AEGIS_CLOSE_TO_ANY_PLANET)
|
|
{
|
|
[shipAI message:@"AEGIS_NONE"];
|
|
}
|
|
}
|
|
aegis_status = AEGIS_NONE;
|
|
}
|
|
|
|
|
|
NSComparisonResult planetSort(id i1, id i2, void* context)
|
|
{
|
|
Vector p = [(ShipEntity*) context position];
|
|
PlanetEntity* e1= i1;
|
|
PlanetEntity* e2= i2;
|
|
//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;
|
|
|
|
if (p1 < p2) return NSOrderedAscending;
|
|
if (p1 > p2) return NSOrderedDescending;
|
|
|
|
return NSOrderedSame;
|
|
}
|
|
|
|
|
|
- (PlanetEntity *) findPlanetNearestSurface
|
|
{
|
|
NSMutableArray *planets = nil;
|
|
NSArray *sortedPlanets = nil;
|
|
|
|
planets = [UNIVERSE planetsAndSun];
|
|
if ([planets count] == 0) return nil;
|
|
|
|
PlanetEntity* the_planet = [planets objectAtIndex:0];
|
|
if ([planets count] >1)
|
|
{
|
|
sortedPlanets = [planets sortedArrayUsingFunction:planetSort context:self];
|
|
the_planet = [sortedPlanets objectAtIndex:0];
|
|
}
|
|
return the_planet;
|
|
}
|
|
|
|
|
|
- (OOAegisStatus) checkForAegis
|
|
{
|
|
PlanetEntity *the_planet = [self findPlanetNearestSurface];
|
|
PlanetEntity *warnedPlanet = nil;
|
|
PlanetEntity *mainPlanet = nil;
|
|
|
|
if (the_planet == nil)
|
|
{
|
|
if (aegis_status != AEGIS_NONE)
|
|
{
|
|
// Planet disappeared!
|
|
[self transitionToAegisNone];
|
|
}
|
|
return AEGIS_NONE;
|
|
}
|
|
|
|
// check planet
|
|
float cr = [the_planet collisionRadius];
|
|
float cr2 = cr * cr;
|
|
OOAegisStatus result = AEGIS_NONE;
|
|
float d2;
|
|
|
|
d2 = magnitude2(vector_subtract([the_planet 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:the_planet];
|
|
[shipAI reactToMessage:@"APPROACHING_SURFACE"];
|
|
}
|
|
if (wasNearPlanetSurface && !isNearPlanetSurface)
|
|
{
|
|
[self doScriptEvent:@"shipLeavingPlanetSurface" withArgument:the_planet];
|
|
[shipAI reactToMessage:@"LEAVING_SURFACE"];
|
|
}
|
|
}
|
|
|
|
if (d2 < cr2 * 9.0f && [UNIVERSE sun] != the_planet) //to 3x radius of any planet/moon
|
|
{
|
|
result = AEGIS_CLOSE_TO_ANY_PLANET;
|
|
warnedPlanet = the_planet; // Avoid duplicate message
|
|
}
|
|
|
|
d2 = magnitude2(vector_subtract([[UNIVERSE planet] position], [self position]));
|
|
cr2 = [[UNIVERSE planet] collisionRadius];
|
|
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)
|
|
{
|
|
if (aegis_status != AEGIS_NONE)
|
|
{
|
|
// Station disappeared!
|
|
[self transitionToAegisNone];
|
|
}
|
|
return AEGIS_NONE;
|
|
}
|
|
|
|
d2 = magnitude2(vector_subtract([the_station position], [self position]));
|
|
if (d2 < SCANNER_MAX_RANGE2 * 4.0f) // double scanner range
|
|
{
|
|
result = AEGIS_IN_DOCKING_RANGE;
|
|
}
|
|
|
|
if (!suppressAegisMessages)
|
|
{
|
|
// script/AI messages on change in status
|
|
// approaching..
|
|
if ((aegis_status == AEGIS_NONE)&&(result == AEGIS_CLOSE_TO_MAIN_PLANET))
|
|
{
|
|
mainPlanet = [UNIVERSE planet];
|
|
if (warnedPlanet != mainPlanet)
|
|
{
|
|
[self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:mainPlanet];
|
|
}
|
|
[shipAI message:@"CLOSE_TO_PLANET"];
|
|
[shipAI message:@"AEGIS_CLOSE_TO_PLANET"]; //keep for compatibility with pre-1.72 AI plists
|
|
[shipAI message:@"AEGIS_CLOSE_TO_MAIN_PLANET"];
|
|
}
|
|
if ((aegis_status == AEGIS_NONE)&&(result == AEGIS_CLOSE_TO_ANY_PLANET))
|
|
{
|
|
[self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:the_planet];
|
|
[shipAI message:@"CLOSE_TO_PLANET"];
|
|
}
|
|
if ((aegis_status == AEGIS_CLOSE_TO_ANY_PLANET || result == AEGIS_IN_DOCKING_RANGE)&&(result == AEGIS_CLOSE_TO_MAIN_PLANET))
|
|
{
|
|
[self doScriptEvent:@"shipExitedPlanetaryVicinity"]; //needs work!
|
|
[self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:[UNIVERSE planet]];
|
|
[shipAI message:@"AWAY_FROM_PLANET"];
|
|
[shipAI message:@"CLOSE_TO_PLANET"];
|
|
[shipAI message:@"AEGIS_CLOSE_TO_PLANET"];
|
|
[shipAI message:@"AEGIS_CLOSE_TO_MAIN_PLANET"];
|
|
}
|
|
if ((aegis_status == AEGIS_CLOSE_TO_MAIN_PLANET || result == AEGIS_IN_DOCKING_RANGE)&&(result == AEGIS_CLOSE_TO_ANY_PLANET))
|
|
{
|
|
[self doScriptEvent:@"shipExitedPlanetaryVicinity" withArgument:[UNIVERSE planet]];
|
|
[self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:the_planet];
|
|
[shipAI message:@"AWAY_FROM_PLANET"];
|
|
[shipAI message:@"CLOSE_TO_PLANET"];
|
|
}
|
|
if (((aegis_status == AEGIS_CLOSE_TO_MAIN_PLANET)||(aegis_status == AEGIS_NONE))&&(result == AEGIS_IN_DOCKING_RANGE))
|
|
{
|
|
[self doScriptEvent:@"shipEnteredStationAegis" withArgument:the_station];
|
|
[shipAI message:@"AEGIS_IN_DOCKING_RANGE"];
|
|
}
|
|
// leaving..
|
|
if ((aegis_status == AEGIS_IN_DOCKING_RANGE)&&(result == AEGIS_CLOSE_TO_MAIN_PLANET))
|
|
{
|
|
[self doScriptEvent:@"shipExitedStationAegis"];
|
|
[shipAI message:@"AEGIS_LEAVING_DOCKING_RANGE"];
|
|
}
|
|
if ((aegis_status != AEGIS_NONE)&&(result == AEGIS_NONE))
|
|
{
|
|
[self transitionToAegisNone];
|
|
}
|
|
}
|
|
|
|
aegis_status = result; // put this here
|
|
return result;
|
|
}
|
|
|
|
|
|
- (BOOL) withinStationAegis
|
|
{
|
|
return aegis_status == AEGIS_IN_DOCKING_RANGE;
|
|
}
|
|
|
|
|
|
- (void) setStatus:(OOEntityStatus) stat
|
|
{
|
|
status = stat;
|
|
if ((status == STATUS_LAUNCHING)&&(UNIVERSE))
|
|
launch_time = [UNIVERSE getTime];
|
|
}
|
|
|
|
|
|
- (NSArray*) crew
|
|
{
|
|
return crew;
|
|
}
|
|
|
|
|
|
- (void) setCrew: (NSArray*) crewArray
|
|
{
|
|
//do not set to hulk here when crew is nill (or 0). Some things like missiles have no crew.
|
|
[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 arrayForKey:@"launch_actions"];
|
|
if (actions) [properties setObject:actions forKey:@"legacy_launchActions"];
|
|
actions = [shipinfoDictionary arrayForKey:@"script_actions"];
|
|
if (actions) [properties setObject:actions forKey:@"legacy_scriptActions"];
|
|
actions = [shipinfoDictionary arrayForKey:@"death_actions"];
|
|
if (actions) [properties setObject:actions forKey:@"legacy_deathActions"];
|
|
actions = [shipinfoDictionary arrayForKey:@"setup_actions"];
|
|
if (actions) [properties setObject:actions forKey:@"legacy_setupActions"];
|
|
|
|
script = [OOScript nonLegacyScriptFromFileNamed:@"oolite-default-ship-script.js"
|
|
properties:properties];
|
|
}
|
|
[script retain];
|
|
}
|
|
|
|
|
|
- (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)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 cargo] count];
|
|
}
|
|
|
|
|
|
- (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
|
|
{
|
|
if (flightSpeed > -maxFlightSpeed)
|
|
flightSpeed -= delta;
|
|
else
|
|
flightSpeed = -maxFlightSpeed;
|
|
}
|
|
|
|
|
|
- (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
|
|
{
|
|
suppressExplosion = NO; // Can only be set in death handler
|
|
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;
|
|
ParticleEntity *fragment;
|
|
int i;
|
|
Vector v;
|
|
Quaternion q;
|
|
int speed_low = 200;
|
|
int n_alloys = floor(sqrtf(sqrtf(mass / 25000.0)));
|
|
|
|
if (status == STATUS_DEAD)
|
|
{
|
|
[UNIVERSE removeEntity:self];
|
|
return;
|
|
}
|
|
status = STATUS_DEAD;
|
|
|
|
//scripting
|
|
// if (script != nil)
|
|
// {
|
|
// [[PlayerEntity sharedPlayer] setScriptTarget:self];
|
|
// [self doScriptEvent:@"shipDied"];
|
|
// }
|
|
|
|
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 = [[ParticleEntity alloc] initFlashSize:collision_radius fromPosition:xposition];
|
|
[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 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];
|
|
//
|
|
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];
|
|
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];
|
|
[[container getAI] setState:@"GLOBAL"];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Throw out rocks and alloys to be scooped up
|
|
if ([self hasPrimaryRole:@"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];
|
|
[[rock getAI] setState:@"GLOBAL"];
|
|
[rock release];
|
|
}
|
|
}
|
|
}
|
|
[UNIVERSE removeEntity:self];
|
|
return; // don't do anything more
|
|
}
|
|
|
|
if ([self hasPrimaryRole:@"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];
|
|
[[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];
|
|
[wreck performTumble];
|
|
[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:9 andAmount:1];
|
|
[UNIVERSE addEntity:plate];
|
|
[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];
|
|
}
|
|
}
|
|
|
|
|
|
- (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
|
|
{
|
|
NSMutableArray *newSubs = nil;
|
|
unsigned i, count;
|
|
id element;
|
|
|
|
if ([self subEntityTakingDamage] == sub) [self setSubEntityTakingDamage:nil];
|
|
|
|
if ([self hasSubEntity:sub])
|
|
{
|
|
OOLog(@"shipEntity.bug.subEntityRetainUnderflow", @"***** VALIDATION ERROR: Subentity died while still in subentity list! This is bad. Leaking subentity list to avoid crash. This is an internal error, please report it.");
|
|
|
|
count = [subEntities count];
|
|
if (count != 1)
|
|
{
|
|
newSubs = [[NSMutableArray alloc] initWithCapacity:count - 1];
|
|
for (i = 0; i != count; ++i)
|
|
{
|
|
element = [subEntities objectAtIndex:i];
|
|
if (element != sub)
|
|
{
|
|
[newSubs addObject:element];
|
|
[element release]; // Let it die later, even though there's a reference in the leaked array.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Leak old array, replace with new.
|
|
subEntities = newSubs;
|
|
}
|
|
}
|
|
|
|
|
|
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 (status == STATUS_DEAD) return;
|
|
status = STATUS_DEAD;
|
|
|
|
//scripting
|
|
if (script != nil)
|
|
{
|
|
[[PlayerEntity sharedPlayer] setScriptTarget:self];
|
|
[self doScriptEvent:@"shipDied"];
|
|
}
|
|
|
|
// 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_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:NO]];
|
|
cargo_chance = 100;
|
|
}
|
|
if (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:NO]];
|
|
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];
|
|
[[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 - shot_time) / 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
|
|
{
|
|
// I don't think this is used anywhere. -- Ahruman
|
|
#ifndef NDEBUG
|
|
if (suppress || ![self isSubEntity])
|
|
{
|
|
OOLog(@"method.undead", @"Believed-dead method %s called.", __FUNCTION__);
|
|
}
|
|
#endif
|
|
|
|
suppressExplosion = suppress != NO;
|
|
}
|
|
|
|
/*-----------------------------------------
|
|
|
|
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
|
|
{
|
|
[self noteLostTarget];
|
|
|
|
[[self shipSubEntityEnumerator] makeObjectsPerformSelector:@selector(removeTarget:) withObject:targetEntity];
|
|
}
|
|
|
|
|
|
- (id) primaryTarget
|
|
{
|
|
return [UNIVERSE entityForUniversalID:primaryTarget];
|
|
}
|
|
|
|
|
|
- (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"];
|
|
}
|
|
}
|
|
|
|
|
|
- (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) 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))
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
my_position = vector_add(OOVectorMultiplyMatrix(my_position, r_mat), [father position]);
|
|
my_ref = OOVectorMultiplyMatrix(my_ref, r_mat);
|
|
last = father;
|
|
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];
|
|
|
|
status = 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 = unit_vector(&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))
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
my_position = vector_add(OOVectorMultiplyMatrix(my_position, r_mat), [father position]);
|
|
my_ref = OOVectorMultiplyMatrix(my_ref, r_mat);
|
|
last = father;
|
|
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];
|
|
|
|
status = STATUS_ACTIVE;
|
|
|
|
return aim_cos;
|
|
}
|
|
|
|
|
|
- (double) trackPrimaryTarget:(double) delta_t :(BOOL) retreat
|
|
{
|
|
Entity* target = [self primaryTarget];
|
|
|
|
if (!target) // leave now!
|
|
{
|
|
[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)
|
|
{
|
|
[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, range2;
|
|
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));
|
|
}
|
|
|
|
|
|
range2 = magnitude2(relPos);
|
|
|
|
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.85;
|
|
|
|
if (retreat)
|
|
reverse = -reverse;
|
|
|
|
if (isPlayer)
|
|
reverse = -reverse;
|
|
|
|
relPos = vector_subtract(destination, position);
|
|
double range2 = magnitude2(relPos);
|
|
|
|
max_cos = sqrt(1 - desired_range*desired_range/range2);
|
|
|
|
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) // leave now!
|
|
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 = unit_vector(&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];
|
|
|
|
if (shot_time < 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 firePlasmaShot: 0.0: 1500.0: [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:
|
|
// 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;
|
|
}
|
|
|
|
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 (shot_time < 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_NONE:
|
|
// do nothing
|
|
break;
|
|
}
|
|
}
|
|
|
|
// restore previous values
|
|
weapon_energy = weapon_energy1;
|
|
weapon_recharge_rate = weapon_recharge_rate1;
|
|
weaponRange = weapon_range1;
|
|
//
|
|
return result;
|
|
}
|
|
|
|
|
|
- (BOOL) fireTurretCannon:(double) range
|
|
{
|
|
if (shot_time < weapon_recharge_rate)
|
|
return NO;
|
|
if (range > 5050) //50 more than max range - open up just slightly early
|
|
return NO;
|
|
|
|
ParticleEntity *shot = nil;
|
|
Vector origin = position;
|
|
Entity *last = nil;
|
|
Entity *father = [self parentEntity];
|
|
OOMatrix r_mat;
|
|
Vector vel;
|
|
|
|
while ((father)&&(father != last))
|
|
{
|
|
r_mat = [father drawRotationMatrix];
|
|
origin = vector_add(OOVectorMultiplyMatrix(origin, r_mat), [father position]);
|
|
last = father;
|
|
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
|
|
|
|
shot = [[ParticleEntity alloc] initPlasmaShotAt:origin
|
|
velocity:vel
|
|
energy:weapon_energy
|
|
duration:3.0
|
|
color:laser_color];
|
|
[shot autorelease];
|
|
[UNIVERSE addEntity:shot];
|
|
[shot setOwner:[self owner]]; // has to be done AFTER adding shot to the UNIVERSE
|
|
|
|
shot_time = 0.0;
|
|
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 (shot_time < 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 flash_pos = [shot position];
|
|
Vector vd = vector_forward_from_quaternion([shot orientation]);
|
|
flash_pos.x += vd.x * hit_at_range; flash_pos.y += vd.y * hit_at_range; flash_pos.z += vd.z * hit_at_range;
|
|
ParticleEntity* laserFlash = [[ParticleEntity alloc] initFlashSize:1.0 fromPosition: flash_pos color:laser_color];
|
|
[laserFlash setVelocity:[victim velocity]];
|
|
[UNIVERSE addEntity:laserFlash];
|
|
[laserFlash release];
|
|
}
|
|
}
|
|
[UNIVERSE addEntity:shot];
|
|
[shot release];
|
|
|
|
shot_time = 0.0;
|
|
|
|
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 = unit_vector(&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 flash_pos = shot->position;
|
|
Vector vd = vector_forward_from_quaternion(shot->orientation);
|
|
flash_pos.x += vd.x * hit_at_range; flash_pos.y += vd.y * hit_at_range; flash_pos.z += vd.z * hit_at_range;
|
|
ParticleEntity* laserFlash = [[ParticleEntity alloc] initFlashSize:1.0 fromPosition: flash_pos color:laser_color];
|
|
[laserFlash setVelocity:[victim velocity]];
|
|
[UNIVERSE addEntity:laserFlash];
|
|
[laserFlash release];
|
|
}
|
|
}
|
|
[UNIVERSE addEntity:shot];
|
|
[shot release];
|
|
|
|
shot_time = 0.0;
|
|
|
|
// 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])
|
|
{
|
|
/* FIXME 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 flash_pos = shot->position;
|
|
Vector vd = vector_forward_from_quaternion(shot->orientation);
|
|
flash_pos.x += vd.x * hit_at_range; flash_pos.y += vd.y * hit_at_range; flash_pos.z += vd.z * hit_at_range;
|
|
ParticleEntity* laserFlash = [[ParticleEntity alloc] initFlashSize:1.0 fromPosition: flash_pos color:laser_color];
|
|
[laserFlash setVelocity:[victim velocity]];
|
|
[UNIVERSE addEntity:laserFlash];
|
|
[laserFlash release];
|
|
}
|
|
}
|
|
[UNIVERSE addEntity:shot];
|
|
[shot release]; //release
|
|
|
|
shot_time = 0.0;
|
|
|
|
// 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
|
|
{
|
|
ParticleEntity* spark;
|
|
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 = [[ParticleEntity alloc] initSparkAt: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) firePlasmaShot:(double) offset :(double) speed :(OOColor *) color
|
|
{
|
|
ParticleEntity *shot;
|
|
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 = v_forward;
|
|
vel.x = -vel.x; vel.y = -vel.y; vel.z = -vel.z; // reverse
|
|
rt = v_right;
|
|
rt.x = -rt.x; rt.y = -rt.y; rt.z = -rt.z; // reverse
|
|
break;
|
|
case VIEW_STARBOARD :
|
|
vel = v_right;
|
|
rt = v_forward;
|
|
rt.x = -rt.x; rt.y = -rt.y; rt.z = -rt.z; // reverse
|
|
break;
|
|
case VIEW_PORT :
|
|
vel = v_right;
|
|
vel.x = -vel.x; vel.y = -vel.y; vel.z = -vel.z; // reverse
|
|
rt = v_forward;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
origin.x += vel.x * start;
|
|
origin.y += vel.y * start;
|
|
origin.z += vel.z * start;
|
|
|
|
origin.x += rt.x * offset;
|
|
origin.y += rt.y * offset;
|
|
origin.z += rt.z * offset;
|
|
|
|
vel.x *= speed;
|
|
vel.y *= speed;
|
|
vel.z *= speed;
|
|
|
|
shot = [[ParticleEntity alloc] initPlasmaShotAt:origin
|
|
velocity:vel
|
|
energy:weapon_energy
|
|
duration:5.0
|
|
color:color];
|
|
|
|
[shot setOwner:self];
|
|
[UNIVERSE addEntity:shot];
|
|
[shot release];
|
|
|
|
shot_time = 0.0;
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) fireMissile
|
|
{
|
|
NSString *missileRole = nil;
|
|
ShipEntity *missile = nil;
|
|
Vector vel;
|
|
Vector origin = position;
|
|
Vector start, v_eject;
|
|
Entity *target = nil;
|
|
ShipEntity *target_ship = nil;
|
|
|
|
// default launching position
|
|
start.x = 0.0; // in the middle
|
|
start.y = boundingBox.min.y - 4.0; // 4m below bounding box
|
|
start.z = boundingBox.max.z + 1.0; // 1m ahead of bounding box
|
|
// custom launching position
|
|
ScanVectorFromString([shipinfoDictionary objectForKey:@"missile_launch_position"], &start);
|
|
|
|
double throw_speed = 250.0;
|
|
Quaternion q1 = orientation;
|
|
target = [self primaryTarget];
|
|
|
|
if ((missiles <= 0)||(target == nil)||(target->scanClass == CLASS_NO_DRAW)) // no missile lock!
|
|
return NO;
|
|
|
|
if ([target isShip])
|
|
{
|
|
target_ship = (ShipEntity*)target;
|
|
if ([target_ship isCloaked]) return NO;
|
|
if (![self hasMilitaryScannerFilter] && [target_ship isJammingScanning]) return NO;
|
|
}
|
|
|
|
// custom missiles
|
|
missileRole = [shipinfoDictionary stringForKey:@"missile_role"];
|
|
if (missileRole != nil) missile = [UNIVERSE newShipWithRole:missileRole];
|
|
if (missile == nil) // no custom role
|
|
{
|
|
if (randf() < 0.90) // choose a standard missile 90% of the time
|
|
{
|
|
missile = [UNIVERSE newShipWithRole:@"EQ_MISSILE"]; // retained
|
|
}
|
|
else // otherwise choose any with the role 'missile' - which may include alternative weapons
|
|
{
|
|
missile = [UNIVERSE newShipWithRole:@"missile"]; // retained
|
|
}
|
|
}
|
|
|
|
if (missile == nil) return NO;
|
|
|
|
missiles--;
|
|
|
|
double mcr = missile->collision_radius;
|
|
|
|
v_eject = unit_vector(&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.x += mcr * v_eject.x; start.y += mcr * v_eject.y; start.z += mcr * v_eject.z;
|
|
vel.x += 10.0f * mcr * v_eject.x; vel.y += 10.0f * mcr * v_eject.y; vel.z += 10.0f * mcr * v_eject.z; // throw it outward a bit harder
|
|
}
|
|
|
|
if (isPlayer)
|
|
q1.w = -q1.w; // player view is reversed remember!
|
|
|
|
vel.x += (flightSpeed + throw_speed) * v_forward.x;
|
|
vel.y += (flightSpeed + throw_speed) * v_forward.y;
|
|
vel.z += (flightSpeed + throw_speed) * v_forward.z;
|
|
|
|
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 (!isMissile) [missile setOwner:self];
|
|
else [missile setOwner:[self owner]];
|
|
|
|
[missile addTarget:target];
|
|
[missile setGroupID:groupID];
|
|
[missile setPosition:origin];
|
|
[missile setOrientation:q1];
|
|
[missile setVelocity:vel];
|
|
[missile setSpeed:150.0];
|
|
[missile setDistanceTravelled:0.0];
|
|
[missile setStatus:STATUS_IN_FLIGHT]; // necessary to get it going!
|
|
missile->isMissile = YES;
|
|
|
|
[UNIVERSE addEntity:missile];
|
|
|
|
[missile release]; //release
|
|
|
|
if ([missile scanClass] == CLASS_MISSILE)
|
|
{
|
|
[target_ship setPrimaryAggressor:self];
|
|
[target_ship doScriptEvent:@"shipAttackedWithMissile" withArgument:missile andArgument:self];
|
|
[target_ship reactToAIMessage:@"INCOMING_MISSILE"];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL) fireECM
|
|
{
|
|
if (![self hasECM]) return NO;
|
|
|
|
ParticleEntity *ecmDevice = [[ParticleEntity alloc] initECMMineFromShip:self]; // retained
|
|
[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];
|
|
[[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, *pod = nil;
|
|
unsigned n_pods;
|
|
|
|
/* 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 unsignedIntForKey:@"has_escape_pod"];
|
|
|
|
pod = [UNIVERSE newShipWithRole:[shipinfoDictionary stringForKey:@"escape_pod_model" defaultValue:@"escape-capsule"]];
|
|
mainPod = pod;
|
|
|
|
if (pod)
|
|
{
|
|
[pod setOwner:self];
|
|
[pod setScanClass: CLASS_CARGO];
|
|
[pod setCommodity:[UNIVERSE commodityForName:@"Slaves"] andAmount:1];
|
|
if (crew) // transfer crew
|
|
{
|
|
// make sure crew inherit any legalStatus
|
|
unsigned i;
|
|
for (i = 0; i < [crew count]; i++)
|
|
{
|
|
OOCharacter *ch = (OOCharacter*)[crew objectAtIndex:i];
|
|
[ch setLegalStatus: [self legalStatus] | [ch legalStatus]];
|
|
}
|
|
[pod setCrew: crew];
|
|
[self setCrew: nil];
|
|
[self setHulk: true]; //CmdrJames experiment with fixing ejection behaviour
|
|
}
|
|
[[pod getAI] setStateMachine:@"homeAI.plist"];
|
|
[self dumpItem:pod];
|
|
[[pod getAI] setState:@"GLOBAL"];
|
|
[pod release]; //release
|
|
result = [pod universalID];
|
|
}
|
|
// launch other pods (passengers)
|
|
unsigned i;
|
|
for (i = 1; i < n_pods; i++)
|
|
{
|
|
pod = [UNIVERSE newShipWithRole:@"escape-capsule"];
|
|
if (pod)
|
|
{
|
|
Random_Seed orig = [UNIVERSE systemSeedForSystemNumber:gen_rnd_number()];
|
|
[pod setOwner:self];
|
|
[pod setScanClass: CLASS_CARGO];
|
|
[pod setCommodity:[UNIVERSE commodityForName:@"Slaves"] andAmount:1];
|
|
[pod setCrew:[NSArray arrayWithObject:[OOCharacter randomCharacterWithRole:@"passenger" andOriginalSystem:orig]]];
|
|
[[pod getAI] setStateMachine:@"homeAI.plist"];
|
|
[self dumpItem:pod];
|
|
[[pod getAI] setState:@"GLOBAL"];
|
|
[pod release]; //release
|
|
}
|
|
}
|
|
|
|
[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];
|
|
}
|
|
}
|
|
|
|
return jetto;
|
|
}
|
|
|
|
|
|
- (OOCargoType) dumpItem: (ShipEntity*) jetto
|
|
{
|
|
if (!jetto)
|
|
return 0;
|
|
int result = [jetto cargoType];
|
|
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 = unit_vector(&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];
|
|
[[jetto getAI] setState:@"GLOBAL"];
|
|
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];
|
|
}
|
|
if (ent->isPlanet)
|
|
{
|
|
[self getDestroyedBy:ent context:@"hit a planet"];
|
|
if (self == [PlayerEntity sharedPlayer]) [self retain];
|
|
}
|
|
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 inc1, 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));
|
|
|
|
inc1 = dot_product(v_forward, loc);
|
|
|
|
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
|
|
{
|
|
vel2a = 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
|
|
{
|
|
desired_speed = 0.0;
|
|
[self setAITo:@"nullAI.plist"]; // prevent AI from changing status or behaviour
|
|
behaviour = BEHAVIOUR_TRACTORED;
|
|
status = STATUS_BEING_SCOOPED;
|
|
[self addTarget:other];
|
|
[self setOwner:other];
|
|
}
|
|
|
|
|
|
- (void) scoopIn:(ShipEntity *)other
|
|
{
|
|
[other getTractoredBy:self];
|
|
}
|
|
|
|
|
|
- (void) scoopUp:(ShipEntity *)other
|
|
{
|
|
if (other == nil) return;
|
|
|
|
OOCargoType co_type;
|
|
OOCargoQuantity co_amount;
|
|
|
|
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];
|
|
}
|
|
[UNIVERSE playCustomSound:@"[escape-pod-scooped]"];
|
|
}
|
|
}
|
|
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 ([cargo count] == max_cargo) [shipAI message:@"HOLD_FULL"];
|
|
}
|
|
[[other collisionArray] removeObject:self]; // so it can't be scooped twice!
|
|
if (isPlayer) [(PlayerEntity*)self suppressTargetLost];
|
|
[UNIVERSE removeEntity:other];
|
|
}
|
|
|
|
|
|
- (void) takeEnergyDamage:(double)amount from:(Entity *)ent becauseOf:(Entity *)other
|
|
{
|
|
if (status == STATUS_DEAD) return;
|
|
if (amount <= 0.0) return;
|
|
|
|
// If it's an energy mine...
|
|
if (ent && ent->isParticle && ent->scanClass == CLASS_MINE)
|
|
{
|
|
// ...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];
|
|
}
|
|
}
|
|
|
|
energy -= amount;
|
|
being_mined = NO;
|
|
ShipEntity *hunter = nil;
|
|
|
|
if ([other isShip])
|
|
{
|
|
hunter = (ShipEntity *)other;
|
|
if ([hunter isCloaked])
|
|
{
|
|
[self doScriptEvent:@"shipBeingAttackedByCloaked" andReactToAIMessage:@"ATTACKED_BY_CLOAKED"];
|
|
|
|
// lose it!
|
|
other = nil;
|
|
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:other];
|
|
|
|
// tell our group we've been attacked
|
|
if (groupID != NO_TARGET && (groupID != [hunter groupID]) && (!iAmTheLaw && !uAreTheLaw))
|
|
{
|
|
if ([self isTrader] || [self isEscort])
|
|
{
|
|
ShipEntity *group_leader = [UNIVERSE entityForUniversalID:groupID];
|
|
if ((group_leader)&&(group_leader->isShip))
|
|
{
|
|
[group_leader setFound_target:hunter];
|
|
[group_leader setPrimaryAggressor:hunter];
|
|
[group_leader respondToAttackFrom:ent becauseOf:hunter];
|
|
}
|
|
else if (self != group_leader)
|
|
{
|
|
[self setGroupID:NO_TARGET];
|
|
}
|
|
//unsetting group leader for carriers can break stuff
|
|
}
|
|
if ([self isPirate])
|
|
{
|
|
NSArray *fellow_pirates = [self shipsInGroup:groupID];
|
|
unsigned int i;
|
|
for (i = 0; i < [fellow_pirates count]; i++)
|
|
{
|
|
ShipEntity *other_pirate = (ShipEntity *)[fellow_pirates objectAtIndex:i];
|
|
if (randf() < 0.5) // 50% chance they'll help
|
|
{
|
|
[other_pirate setFound_target:hunter];
|
|
[other_pirate setPrimaryAggressor:hunter];
|
|
[other_pirate respondToAttackFrom:ent becauseOf:hunter];
|
|
}
|
|
}
|
|
}
|
|
if (iAmTheLaw)
|
|
{
|
|
NSArray *fellow_police = [self shipsInGroup:groupID];
|
|
unsigned int i;
|
|
for (i = 0; i < [fellow_police count]; i++)
|
|
{
|
|
ShipEntity *other_police = (ShipEntity *)[fellow_police objectAtIndex:i];
|
|
[other_police setFound_target:hunter];
|
|
[other_police setPrimaryAggressor:hunter];
|
|
[other_police 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];
|
|
|
|
// avoid shooting each other
|
|
if (([hunter groupID] == groupID)||(iAmTheLaw && uAreTheLaw))
|
|
{
|
|
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)
|
|
{
|
|
[hunter noteTargetDestroyed:self];
|
|
[self getDestroyedBy:other context:@"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
|
|
{
|
|
// TODO: abandoning ship should be split out into a separate method.
|
|
if ([self launchEscapeCapsule] != NO_TARGET)
|
|
{
|
|
[self removeEquipmentItem:@"EQ_ESCAPE_POD"];
|
|
|
|
[shipAI setStateMachine:@"nullAI.plist"];
|
|
[shipAI setState:@"GLOBAL"];
|
|
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];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) takeScrapeDamage:(double) amount from:(Entity *) ent
|
|
{
|
|
if (status == STATUS_DEAD) return;
|
|
|
|
if (status == STATUS_LAUNCHING) // no collisions during launches please
|
|
return;
|
|
if (ent && 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 (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)
|
|
[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;
|
|
|
|
status = STATUS_LAUNCHING;
|
|
|
|
[self doScriptEvent:@"shipWillLaunchFromStation" withArgument:station];
|
|
[shipAI message:@"LAUNCHED"];
|
|
[UNIVERSE addEntity:self];
|
|
}
|
|
|
|
|
|
- (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
|
|
{
|
|
Vector pos = [UNIVERSE getWitchspaceExitPosition];
|
|
Quaternion q_rtn = [UNIVERSE getWitchspaceExitRotation];
|
|
|
|
// try to ensure healthy random numbers
|
|
//
|
|
ranrot_srand(w_space_seed);
|
|
w_space_seed = ranrot_rand();
|
|
|
|
position = pos;
|
|
double d1 = SCANNER_MAX_RANGE * (randf() - randf());
|
|
if (abs(d1) < 500.0) // no closer than 500m
|
|
d1 += ((d1 > 0.0)? 500.0: -500.0);
|
|
Quaternion q1 = q_rtn;
|
|
quaternion_set_random(&q1);
|
|
Vector v1 = vector_forward_from_quaternion(q1);
|
|
position.x += v1.x * d1; // randomise exit position
|
|
position.y += v1.y * d1;
|
|
position.z += v1.z * d1;
|
|
orientation = q_rtn;
|
|
flightRoll = 0.0;
|
|
flightPitch = 0.0;
|
|
flightSpeed = maxFlightSpeed * 0.25;
|
|
status = STATUS_LAUNCHING;
|
|
[shipAI message:@"EXITED_WITCHSPACE"];
|
|
[UNIVERSE addEntity:self];
|
|
|
|
// 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;
|
|
ParticleEntity *se = nil;
|
|
ShipEntity *sub = nil;
|
|
|
|
for (subEnum = [self flasherEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
[se setStatus:STATUS_EFFECT];
|
|
}
|
|
for (subEnum = [self shipSubEntityEnumerator]; (sub = [subEnum nextObject]); )
|
|
{
|
|
[sub switchLightsOn];
|
|
}
|
|
}
|
|
|
|
// Exposed to AI
|
|
- (void) switchLightsOff
|
|
{
|
|
NSEnumerator *subEnum = nil;
|
|
ParticleEntity *se = nil;
|
|
ShipEntity *sub = nil;
|
|
|
|
for (subEnum = [self flasherEnumerator]; (se = [subEnum nextObject]); )
|
|
{
|
|
[se setStatus:STATUS_INACTIVE];
|
|
}
|
|
for (subEnum = [self shipSubEntityEnumerator]; (sub = [subEnum nextObject]); )
|
|
{
|
|
[sub switchLightsOff];
|
|
}
|
|
}
|
|
|
|
|
|
- (void) setDestination:(Vector) dest
|
|
{
|
|
destination = dest;
|
|
frustration = 0.0; // new destination => no frustration!
|
|
}
|
|
|
|
|
|
- (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 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])
|
|
{
|
|
unsigned i;
|
|
// check it's not already been accepted
|
|
for (i = 0; i < escortCount; i++)
|
|
{
|
|
if (escort_ids[i] == [other_ship universalID])
|
|
{
|
|
//[other_ship setGroupID:universalID];
|
|
//[self setGroupID:universalID]; // make self part of same group
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
// check total number acceptable
|
|
unsigned max_escorts = [shipinfoDictionary unsignedIntForKey:@"escorts" defaultValue:0];
|
|
//however the system's patrols don't have escorts inside their dictionary
|
|
if (max_escorts == 0 && ([self hasRole:@"police"]||[self hasRole:@"interceptor"]||[self hasRole:@"hunter"]))
|
|
max_escorts = MAX_ESCORTS;
|
|
if ((escortCount < MAX_ESCORTS)&&(escortCount < max_escorts))
|
|
{
|
|
escort_ids[escortCount] = [other_ship universalID];
|
|
[other_ship setGroupID:universalID];
|
|
[self setGroupID:universalID]; // make self part of same group
|
|
escortCount++;
|
|
//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 (max_escorts > 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 };
|
|
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;
|
|
}
|
|
|
|
|
|
- (void) deployEscorts
|
|
{
|
|
if (escortCount < 1)
|
|
return;
|
|
|
|
if (!escortsAreSetUp) return;
|
|
|
|
if (![self primaryTarget])
|
|
return;
|
|
|
|
if ([self groupID] == NO_TARGET)
|
|
{
|
|
[self setGroupID:universalID];
|
|
}
|
|
if (primaryTarget == last_escort_target)
|
|
{
|
|
// already deployed escorts onto this target!
|
|
return;
|
|
}
|
|
|
|
last_escort_target = primaryTarget;
|
|
|
|
int n_deploy = ranrot_rand() % escortCount;
|
|
if (n_deploy == 0)
|
|
n_deploy = 1;
|
|
|
|
int i_deploy = escortCount - 1;
|
|
while ((n_deploy > 0)&&(escortCount > 0))
|
|
{
|
|
int escort_id = escort_ids[i_deploy];
|
|
ShipEntity *escorter = [UNIVERSE entityForUniversalID:escort_id];
|
|
// check it's still an escort ship
|
|
if (escorter != nil && escorter->isShip)
|
|
{
|
|
[escorter setGroupID:groupID]; //you still belong to me, see [Bug #14011 ] Carriers should have groupID set.
|
|
[escorter addTarget:[self primaryTarget]];
|
|
[[escorter getAI] setStateMachine:@"interceptAI.plist"];
|
|
[[escorter getAI] setState:@"GLOBAL"];
|
|
[escorter doScriptEvent:@"escortAttack" withArgument:[self primaryTarget]];
|
|
|
|
escort_ids[i_deploy] = NO_TARGET;
|
|
i_deploy--;
|
|
n_deploy--;
|
|
escortCount--;
|
|
}
|
|
else
|
|
{
|
|
escort_ids[i_deploy--] = escort_ids[--escortCount]; // remove the escort
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (void) dockEscorts
|
|
{
|
|
if (escortCount < 1)
|
|
return;
|
|
|
|
unsigned i;
|
|
for (i = 0; i < escortCount; i++)
|
|
{
|
|
int escort_id = escort_ids[i];
|
|
ShipEntity *escorter = [UNIVERSE entityForUniversalID:escort_id];
|
|
// check it's still an escort ship
|
|
BOOL escorter_okay = YES;
|
|
if (!escorter)
|
|
escorter_okay = NO;
|
|
else
|
|
escorter_okay = escorter->isShip;
|
|
if (escorter_okay)
|
|
{
|
|
float delay = i * 3.0 + 1.5; // send them off at three second intervals
|
|
AI *ai = [escorter getAI];
|
|
|
|
[escorter setGroupID:NO_TARGET]; // act individually now!
|
|
[ai setStateMachine:@"dockingAI.plist" afterDelay:delay];
|
|
[ai setState:@"ABORT" afterDelay:delay + 0.25];
|
|
[escorter doScriptEvent:@"escortDock" withArgument:[NSNumber numberWithFloat:delay]];
|
|
}
|
|
escort_ids[i] = NO_TARGET;
|
|
}
|
|
escortCount = 0;
|
|
|
|
}
|
|
|
|
|
|
- (void) setTargetToStation
|
|
{
|
|
// check if the groupID (parent ship) points to a station...
|
|
Entity* mother = [UNIVERSE entityForUniversalID:groupID];
|
|
if ((mother)&&(mother->isStation))
|
|
{
|
|
primaryTarget = groupID;
|
|
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;
|
|
}
|
|
}
|
|
|
|
|
|
- (void) setTargetToSystemStation
|
|
{
|
|
StationEntity* system_station = [UNIVERSE station];
|
|
|
|
if (!system_station)
|
|
{
|
|
[shipAI message:@"NOTHING_FOUND"];
|
|
primaryTarget = NO_TARGET;
|
|
targetStation = NO_TARGET;
|
|
return;
|
|
}
|
|
|
|
if (!system_station->isStation)
|
|
{
|
|
[shipAI message:@"NOTHING_FOUND"];
|
|
primaryTarget = NO_TARGET;
|
|
targetStation = NO_TARGET;
|
|
return;
|
|
}
|
|
|
|
primaryTarget = [system_station universalID];
|
|
targetStation = primaryTarget;
|
|
return;
|
|
}
|
|
|
|
|
|
- (PlanetEntity *) findNearestLargeBody
|
|
{
|
|
/*- selects the nearest planet it can find -*/
|
|
if (!UNIVERSE)
|
|
return nil;
|
|
int ent_count = UNIVERSE->n_entities;
|
|
Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list
|
|
Entity* my_entities[ent_count];
|
|
int i;
|
|
int planet_count = 0;
|
|
for (i = 0; i < ent_count; i++)
|
|
if (uni_entities[i]->isPlanet)
|
|
my_entities[planet_count++] = [uni_entities[i] retain]; // retained
|
|
//
|
|
PlanetEntity *the_planet = nil;
|
|
double nearest2 = SCANNER_MAX_RANGE2 * 10000000000.0; // 100 000x scanner range (2 560 000 km), squared.
|
|
for (i = 0; i < planet_count; i++)
|
|
{
|
|
PlanetEntity *thing = (PlanetEntity*)my_entities[i];
|
|
double range2 = distance2(position, thing->position);
|
|
if ((!the_planet)||(range2 < nearest2))
|
|
{
|
|
the_planet = (PlanetEntity *)thing;
|
|
nearest2 = range2;
|
|
}
|
|
}
|
|
for (i = 0; i < planet_count; i++)
|
|
[my_entities[i] release]; // released
|
|
//
|
|
return the_planet;
|
|
}
|
|
|
|
|
|
- (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"];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- (NSArray *) shipsInGroup:(int) ship_group_id
|
|
{
|
|
//-- Locates all the ships with this particular group id --//
|
|
NSMutableArray* result = [NSMutableArray array]; // is autoreleased
|
|
if (!UNIVERSE)
|
|
return (NSArray *)result;
|
|
int ent_count = UNIVERSE->n_entities;
|
|
Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list
|
|
int i;
|
|
for (i = 0; i < ent_count; i++)
|
|
{
|
|
if (uni_entities[i]->isShip)
|
|
{
|
|
ShipEntity* ship = (ShipEntity*)uni_entities[i];
|
|
if ([ship groupID] == ship_group_id)
|
|
[result addObject: ship];
|
|
}
|
|
}
|
|
return (NSArray *)result;
|
|
}
|
|
|
|
|
|
- (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
|
|
}
|
|
|
|
|
|
- (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];
|
|
}
|
|
|
|
|
|
- (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 stringAtIndex:0];
|
|
numberString = [tokens 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:false];
|
|
[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);
|
|
if (escortCount != 0) OOLog(@"dumpState.shipEntity", @"Escort count: %u", 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", shot_time);
|
|
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(escortsAreSetUp);
|
|
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) argument = [NSNull null];
|
|
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];
|
|
}
|
|
|
|
@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])
|
|
{
|
|
OOLog(@"ship.subentity.sanityCheck.failed", @"***** VALIDATION ERROR: %@ thinks it's a subentity of %@, but the supposed parent does not agree. This is an internal error, please report it.", [other shortDescription], [self shortDescription]);
|
|
[other setOwner:nil];
|
|
return NO;
|
|
}
|
|
#endif
|
|
|
|
return YES;
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
NSDictionary *DefaultShipShaderMacros(void)
|
|
{
|
|
static NSDictionary *macros = nil;
|
|
NSDictionary *materialDefaults = nil;
|
|
|
|
if (macros == nil)
|
|
{
|
|
materialDefaults = [ResourceManager dictionaryFromFilesNamed:@"material-defaults.plist" inFolder:@"Config" andMerge:YES];
|
|
macros = [[materialDefaults dictionaryForKey:@"ship-prefix-macros" defaultValue:[NSDictionary dictionary]] retain];
|
|
}
|
|
|
|
return macros;
|
|
}
|