This is Unit.m in view mode; [Download] [Up]
//
// $Id: Unit.m,v 1.9 1997/10/31 04:52:11 nygard Exp $
//
//
// This file is a part of Empire, a game of exploration and conquest.
// Copyright (C) 1996 Steve Nygard
//
// 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., 675 Mass Ave, Cambridge, MA 02139, USA.
//
// You may contact the author by:
// e-mail: nygard@telusplanet.net
//
#import "Empire.h"
RCSID ("$Id: Unit.m,v 1.9 1997/10/31 04:52:11 nygard Exp $");
#import "Unit.h"
#import "Map.h"
#import "City.h"
#import "EmpireImageVendor.h"
#import "EmPlayer.h"
#import "Orders.subproj/Orders.h"
#import "GameManager.h" // For berserk
//----------------------------------------------------------------------
// hit points - damage sustained before unit is destroyed.
// speed - number of moves per turn the unit can take. An attack counts
// as a move.
// fuel size - fighters have a limited fuel capacity and must be
// refueled in cities or carriers.
// cargo capacity - number of units this type can hold, or -1 if it
// can't carry other units. Note: There are further
// restrictions on what types can be carried.
// damage per hit - damage to enemy for each hit landed.
// moves on land - can this unit move on (or over) land?
// moves on water - can this unit move on (or over) water?
// attack factor - adjusts the probability or a successful attack based
// on the unit type.
// defense factor - adjusts the probability or a successful defense
// based on the unit type.
// defense factor - adjusts the probability or a successful defense
// against bombardment, based on the unit type.
//----------------------------------------------------------------------
typedef struct
{
int hitPoints;
int speed; // per turn
int fuel_size;
int cargo_capacity;
int damage_per_hit;
BOOL moves_on_land;
BOOL moves_on_water;
float attack_factor;
float defense_factor;
float defense_factor_against_bombardment;
} UnitTypeAttributes;
static UnitTypeAttributes unitTypeAttributes[UNIT_TYPE_COUNT] =
{
{ 0, 0, -1, 0, 0, NO, NO , 0, 0, 0 }, // u_unknown
{ 1, 1, -1, 0, 1, YES, NO , 1, 1, 0.5 }, // u_army
{ 1, 5, 20, 0, 1, YES, YES, 1, 1, 1 }, // u_fighter
{ 3, 2, -1, 6, 1, NO, YES, 0.5, 0.5, 0.5 }, // u_transport
{ 2, 2, -1, 0, 3, NO, YES, 1, 0.5, 0.5 }, // u_submarine
{ 3, 3, -1, 0, 1, NO, YES, 1, 1, 1 }, // u_destroyer
{ 8, 2, -1, 0, 2, NO, YES, 1, 1, 1 }, // u_cruiser
{ 8, 2, -1, 8, 1, NO, YES, 0.5, 1, 1 }, // u_carrier
{ 12, 2, -1, 0, 3, NO, YES, 1, 1, 1 }, // u_battleship
{ 1, 5, -1, 0, 1, YES, YES, 0, 0, 0 }, // u_hovercraft
};
//----------------------------------------------------------------------
// This simply keeps track of the number of units of each type produced,
// so that we can give them unique names to each unit. (i.e. Army #7).
//
// Ultimately, there should be a more intelligent name assignment. For
// example, use lists of names from a text file for ships, and rules
// for armies and fighters. (i.e. 57th Army, 72nd Fighter).
//----------------------------------------------------------------------
static int _typeCount[UNIT_TYPE_COUNT];
//======================================================================
// Most of the attributes of a unit can be parameterized, so there is
// no need for separate subclasses for each type of unit.
//======================================================================
#define Unit_VERSION 1
@implementation Unit
+ (void) initialize
{
if (self == [Unit class])
{
[self setVersion:Unit_VERSION];
}
}
//----------------------------------------------------------------------
- initWithUnitType:(UnitType)aUnitType inCity:(City *)producingCity
{
[super init];
NSAssert (aUnitType >= u_army && aUnitType <= u_hovercraft, @"Invalid unit type.");
unitType = aUnitType;
// We don't "own" the owner, so it is not retained.
owner = [producingCity owner];
unitName = [[NSString stringWithFormat:@"%@ #%d", EMUnitTypeName (unitType), ++_typeCount[unitType]] retain];
unitLocation = [producingCity cityLocation];
currentHitPoints = unitTypeAttributes[unitType].hitPoints;
currentRange = unitTypeAttributes[unitType].speed;
fuel = unitTypeAttributes[unitType].fuel_size;
order = nil;
// Hmm. With more than one game manager, how is the Berserk On/Off menu item handled?
if ([[owner gameManager] isBerserk] == YES)
{
order = [[OExplore alloc] initForUnit:self explore:[owner map]];
}
isDestroyed = NO;
isDoneTurn = NO;
if (unitTypeAttributes[unitType].cargo_capacity > 0)
cargo = [[NSMutableArray array] retain];
else
cargo = nil;
onBoard = nil;
withinCity = producingCity;
[producingCity unitDidEnter:self];
return self;
}
//----------------------------------------------------------------------
- (void) dealloc
{
// Need to notify anything that might be keeping a reference...
if (withinCity != nil)
[withinCity unitDidExit:self];
if (onBoard != nil)
[onBoard unitDidExit:self];
SNRelease (unitName);
SNRelease (order);
SNRelease (cargo);
[super dealloc];
}
//----------------------------------------------------------------------
- (NSString *) description
{
return [NSString stringWithFormat:@"<%@>", [self statusLine]];
}
//======================================================================
// General static values
//======================================================================
- (UnitType) unitType
{
return unitType;
}
//----------------------------------------------------------------------
- (EmPlayer *) owner
{
return owner;
}
//----------------------------------------------------------------------
- (Player) playerNumber
{
return [owner playerNumber];
}
//----------------------------------------------------------------------
- (NSString *) unitName
{
return unitName;
}
//----------------------------------------------------------------------
- (BOOL) isAShip
{
return unitType >= u_transport && unitType <= u_battleship;
}
//======================================================================
// Combat information
//======================================================================
- (int) currentHitPoints
{
return currentHitPoints;
}
//----------------------------------------------------------------------
- (int) maxHitPoints
{
return unitTypeAttributes[unitType].hitPoints;
}
//----------------------------------------------------------------------
- (int) damagePerHit
{
return unitTypeAttributes[unitType].damage_per_hit;
}
//----------------------------------------------------------------------
- (float) attackFactor
{
return unitTypeAttributes[unitType].attack_factor;
}
//----------------------------------------------------------------------
- (float) defenseFactor:(BOOL)bombardment
{
if (bombardment == YES)
return unitTypeAttributes[unitType].defense_factor;
return unitTypeAttributes[unitType].defense_factor_against_bombardment;
}
//----------------------------------------------------------------------
- (BOOL) canBombard
{
return (unitType == u_cruiser) || (unitType == u_battleship);
}
//======================================================================
// Movement information
//======================================================================
- (int) currentRange
{
return currentRange;
}
//----------------------------------------------------------------------
- (int) remainingFuel
{
return fuel;
}
//----------------------------------------------------------------------
- (BOOL) canMoveOnTerrain:(Terrain)terrain
{
BOOL canMove = YES;
switch (terrain)
{
case t_land:
canMove = unitTypeAttributes[unitType].moves_on_land;
break;
case t_water:
canMove = unitTypeAttributes[unitType].moves_on_water;
break;
case t_city:
canMove = NO;
break;
default:
break;
}
return canMove;
}
//----------------------------------------------------------------------
- (MovementType) movementType
{
int movement = 0;
if (unitTypeAttributes[unitType].moves_on_land == YES)
movement |= LAND_UNIT;
if (unitTypeAttributes[unitType].moves_on_water == YES)
movement |= WATER_UNIT;
return movement;
}
//----------------------------------------------------------------------
- (EMMapLocation) unitLocation
{
return unitLocation;
}
//----------------------------------------------------------------------
// Set the new location of a unit. This takes a look at the map to
// determine if it's entering a city or boarding a ship.
//----------------------------------------------------------------------
- (void) setLocation:(EMMapLocation)newLocation
{
MapToken mapToken = [[owner map] tokenAtLocation:newLocation];
Terrain terrain;
Icon icon;
UnitType targetUnitType;
unitLocation = newLocation;
// We need to update the cargo locations before we set the city.
if (cargo != nil)
{
NSEnumerator *unitEnumerator = [cargo objectEnumerator];
Unit *unit;
while (unit = [unitEnumerator nextObject])
{
[unit moved];
[unit takeLocationFromTransport];
}
}
// Assuming that the player is us.
EMMapTokenComponents (mapToken, &terrain, NULL, &icon, NULL);
targetUnitType = EMConvertIconToUnitType (icon);
if (terrain == t_city)
{
[self setCity:[owner cityAtLocation:newLocation]];
}
else
[self setCity:nil];
if ((unitType == u_army && targetUnitType == u_transport)
|| (unitType == u_fighter && targetUnitType == u_carrier) )
{
// What happens if it doesn't fit? Or is it just assumed to fit by this point?
[[owner unitWithType:targetUnitType atLocation:newLocation] loadUnit:self];
//[self setOnBoardShip:[owner unitWithType:targetUnitType atLocation:newLocation]];
}
else
{
[self setOnBoardShip:nil];
}
}
//----------------------------------------------------------------------
// If a ship moves, it notifies all of its cargo so they can update
// their location to match that of the ship.
//----------------------------------------------------------------------
- (void) takeLocationFromTransport
{
if (onBoard != nil)
unitLocation = [onBoard unitLocation];
}
//======================================================================
// Display
//======================================================================
- (Icon) icon
{
static const Icon baseIconsForUnitTypes[] = {i_none, i_army, i_fighter, i_unloaded_transport, i_submarine,
i_destroyer, i_cruiser, i_unloaded_carrier, i_battleship, i_hovercraft};
Icon icon = baseIconsForUnitTypes[unitType];
if (unitType == u_army && [self isSentried] == YES)
{
icon = i_sentry;
}
else if (unitType == u_transport && cargo != nil && [cargo count] > 0)
{
icon = i_loaded_transport;
}
else if (unitType == u_carrier && cargo != nil && [cargo count] > 0)
{
icon = i_loaded_carrier;
}
return icon;
}
//----------------------------------------------------------------------
- (NSImage *) iconImage
{
NSImage *aCity, **onLand, **onWater, **onCity;
EmpireImageVendor *vendor = [EmpireImageVendor instance];
NSImage *image = nil;
[vendor player:[owner playerNumber]:&aCity:&onLand:&onWater:&onCity:NULL];
switch ([self icon])
{
case i_army:
image = onLand[0];
break;
case i_sentry:
image = onLand[1];
break;
case i_fighter:
image = onLand[2];
break;
case i_unloaded_transport:
image = onWater[1];
break;
case i_loaded_transport:
image = onWater[2];
break;
case i_submarine:
image = onWater[3];
break;
case i_destroyer:
image = onWater[4];
break;
case i_cruiser:
image = onWater[5];
break;
case i_unloaded_carrier:
image = onWater[6];
break;
case i_loaded_carrier:
image = onWater[7];
break;
case i_battleship:
image = onWater[8];
break;
case i_hovercraft:
image = onWater[9];
break;
default:
image = nil;
}
return image;
}
//======================================================================
// Order support
//======================================================================
- (void) setOrder:(Order *)newOrder
{
BOOL s1, s2;
s1 = s2 = NO;
if (order != nil)
s1 = [order isSentried];
if (newOrder != nil)
s2 = [newOrder isSentried];
SNRelease (order);
order = [newOrder retain];
if ((s1 == YES && s2 == NO) || (s1 == NO && s2 == YES))
{
[owner unitIconHasChanged:self];
[withinCity unitIconHasChanged:self];
}
}
//----------------------------------------------------------------------
- (Order *) currentOrder
{
return order;
}
//----------------------------------------------------------------------
- (BOOL) isSentried
{
return (order != nil) && [order isSentried] == YES;
}
//----------------------------------------------------------------------
// Damaged ships have a reduced cargo capacity.
//----------------------------------------------------------------------
- (int) remainingCargoCapacity
{
if (cargo == nil)
{
return 0;
}
else if (unitType == u_transport)
{
return 2 * currentHitPoints - [cargo count];
}
else if (unitType == u_carrier)
{
return currentHitPoints - [cargo count];
}
return unitTypeAttributes[unitType].cargo_capacity - [cargo count];
}
//----------------------------------------------------------------------
- (BOOL) isFull
{
return [self remainingCargoCapacity] == 0;
}
//======================================================================
// Turn handling
//======================================================================
//----------------------------------------------------------------------
// Only ships can repair (other units only have 1 hit point)
//----------------------------------------------------------------------
- (void) repairDamage
{
if (currentHitPoints < unitTypeAttributes[unitType].hitPoints)
currentHitPoints++;
}
//----------------------------------------------------------------------
// Only fighters need to refuel... But only in city/carrier.
//----------------------------------------------------------------------
- (void) refuel
{
if (unitTypeAttributes[unitType].fuel_size > 0 && (withinCity != nil || onBoard != nil))
fuel = unitTypeAttributes[unitType].fuel_size;
}
//----------------------------------------------------------------------
- (void) resetRange
{
if (currentHitPoints <= unitTypeAttributes[unitType].hitPoints / 2)
currentRange = unitTypeAttributes[unitType].speed / 2;
else
currentRange = unitTypeAttributes[unitType].speed;
isDoneTurn = NO;
}
//----------------------------------------------------------------------
- (void) skipMove
{
isDoneTurn = YES;
}
//----------------------------------------------------------------------
- (void) useSkippedFuel
{
fuel -= currentRange;
currentRange = 0;
}
//----------------------------------------------------------------------
- (void) moved
{
if (currentRange > 0)
currentRange--;
if (fuel > 0)
fuel--;
if (currentRange == 0)
isDoneTurn = YES;
if (order != nil)
[order unblocked];
}
//----------------------------------------------------------------------
- (BOOL) isFinishedMoving
{
return isDoneTurn;
}
//----------------------------------------------------------------------
- (void) setUnitFinishedMoving:(BOOL)flag
{
if (currentRange > 0 && flag == NO)
isDoneTurn = NO;
else
isDoneTurn = YES;
}
//----------------------------------------------------------------------
// If this unit has orders, it will try to execute them.
// Returns YES if it had orders and was able to move, NO otherwise;
//----------------------------------------------------------------------
- (BOOL) tryToMove
{
BOOL hasMoved = NO;
if (order != nil)
{
Direction dir;
[order aboutToMove];
if ([self isFinishedMoving] == NO)
{
int count = 9;
dir = [order nextMove];
while (dir != d_none && hasMoved == NO && count > 0)
{
count--;
hasMoved = [owner tryToMoveSelectedUnitInDirection:dir];
if (hasMoved == NO)
{
BOOL tryAgain = [order wasBlocked];
if (tryAgain == YES)
dir = [order nextMove];
else
break;
}
else
{
[order unblocked];
}
}
}
else
{
isDoneTurn = YES;
hasMoved = YES;
}
if ([order isOrderComplete] == YES)
[self setOrder:nil];
}
if (isDoneTurn == YES)
hasMoved = YES;
return hasMoved;
}
//======================================================================
// Human UI support
//======================================================================
- (NSString *) statusLine
{
NSString *orderText;
int count;
if (order == nil)
orderText = @"None";
else
orderText = [order orderText];
count = (cargo == nil) ? 0 : [cargo count];
if (unitType == u_fighter)
{
return [NSString stringWithFormat:@"Loc: %d,%d - %@ %@, Hits: %d/%d, %d/%d%@, Orders: %@",
unitLocation.row, unitLocation.column, EMUnitTypeName (unitType), unitName, currentHitPoints,
[self maxHitPoints], currentRange, fuel, (isDoneTurn == YES) ? @"*" : @"", orderText];
}
else if (unitType == u_transport && count > 0)
{
return [NSString stringWithFormat:@"Loc: %d,%d - %@ %@ w/%d %@, Hits: %d/%d, %d, Orders: %@",
unitLocation.row, unitLocation.column,
EMUnitTypeName (unitType), unitName, count, (count == 1) ? @"army" : @"armies",
currentHitPoints, [self maxHitPoints], currentRange, orderText];
}
else if (unitType == u_carrier && count > 0)
{
return [NSString stringWithFormat:@"Loc: %d,%d - %@ %@ w/%d fighter%@, Hits: %d/%d, %d, Orders: %@",
unitLocation.row, unitLocation.column,
EMUnitTypeName (unitType), unitName, [cargo count], (count == 1) ? @"" : @"s",
currentHitPoints, [self maxHitPoints], currentRange, orderText];
}
return [NSString stringWithFormat:@"Loc: %d,%d - %@ %@, Hits: %d/%d, %d, Orders: %@",
unitLocation.row, unitLocation.column, EMUnitTypeName (unitType), unitName, currentHitPoints,
[self maxHitPoints], currentRange, orderText];
}
//======================================================================
// Commands
//======================================================================
//----------------------------------------------------------------------
// Need to set orders to nil, make the ship wait, and then add any
// unloaded cargo that can move to the units awaing movement list, being
// careful not to add duplicates...
//----------------------------------------------------------------------
- (void) unloadShip
{
if (cargo != nil)
{
int count = [cargo count];
int l;
Unit *unit;
for (l = 0; l < count; l++)
{
unit = [cargo objectAtIndex:l];
[unit setOrder:nil];
[owner activateThisUnit:unit];
}
}
}
//----------------------------------------------------------------------
- (void) destroyUnit:(BOOL)disbanded
{
Unit *unit;
currentHitPoints = 0;
isDestroyed = YES;
// Disbanded to be YES when disbanding an army to create city garrison.
[self setCity:nil];
[self setOnBoardShip:nil];
if (cargo != nil)
{
unit = [cargo lastObject];
while (unit != nil)
{
[owner destroyUnit:unit wasDisbanded:NO];
unit = [cargo lastObject];
}
}
}
//----------------------------------------------------------------------
// Returns YES is the unit was loaded aboard this unit, NO otherwise.
//----------------------------------------------------------------------
- (BOOL) loadUnit:(Unit *)aUnit
{
UnitType cargo_ut = [aUnit unitType];
Order *old_order;
if ([self isFull] == NO
&& ( (unitType == u_transport && cargo_ut == u_army)
|| (unitType == u_carrier && cargo_ut == u_fighter) ))
{
[aUnit setOnBoardShip:self];
old_order = [aUnit currentOrder];
if (old_order == nil || [old_order isOrderComplete] == YES)
{
[aUnit setOrder:[[OSentry alloc] initForUnit:aUnit]];
}
return YES;
}
return NO;
}
//======================================================================
// Combat Support
//======================================================================
- (BOOL) hitForDamage:(int)damage
{
BOOL wasDestroyed = NO;
currentHitPoints -= damage;
if (currentHitPoints <= 0)
{
[[self retain] autorelease];
[owner destroyUnit:self wasDisbanded:NO];
wasDestroyed = YES;
}
else if (cargo != nil) // Make sure cargo still fits!
{
int count = [cargo count];
int over = 0;
int l;
if (unitType == u_transport)
over = count - 2 * currentHitPoints;
else if (unitType == u_carrier)
over = count - currentHitPoints;
for (l = 0; l < over; l++)
[owner destroyUnit:[cargo lastObject] wasDisbanded:NO];
}
return wasDestroyed;
}
//----------------------------------------------------------------------
- (CombatProfile) combatProfileAgainstBombardment:(BOOL)bombarding
{
CombatProfile profile;
int efficiency;
efficiency = [owner combatEfficiency];
profile.location = unitLocation;
profile.player = [owner playerNumber];
profile.attacking_efficiency = efficiency * [self attackFactor];
profile.defending_efficiency = efficiency * [self defenseFactor:bombarding];
profile.damage_per_hit = [self damagePerHit];
profile.current_hit_points = currentHitPoints;
profile.adjacent_players = 0; // filled in by someone else
profile.is_a_city = NO;
return profile;
}
//======================================================================
// Other
//======================================================================
- (BOOL) isOnBoardShip
{
return onBoard != nil;
}
//----------------------------------------------------------------------
- (BOOL) isInCity
{
return withinCity != nil;
}
//----------------------------------------------------------------------
// The current management of releasing destroyed units has to be
// looked at a bit more closely and handled more carefully.
//----------------------------------------------------------------------
- (BOOL) hasBeenDestroyed
{
return NO;
}
//----------------------------------------------------------------------
- (City *) city
{
return withinCity;
}
//----------------------------------------------------------------------
// A unit may be within a city, or (possibly) on board a ship, but it
// can't be on both.
//
// When a unit enters a city, all the cargo has it's city set, and therefore
// leaves its transport.
//----------------------------------------------------------------------
- (void) setCity:(City *)aCity
{
if (withinCity != nil)
{
[withinCity unitDidExit:self];
withinCity = nil;
}
if (aCity != nil && onBoard != nil)
{
[onBoard unitDidExit:self];
onBoard = nil;
}
if (aCity != nil && cargo != nil)
{
Unit *unit = [cargo lastObject];
while (unit != nil)
{
[unit setCity:aCity];
// Carge removed when the new city is set.
//[cargo removeLastObject];
unit = [cargo lastObject];
}
}
withinCity = aCity;
if (withinCity != nil)
[withinCity unitDidEnter:self];
}
//----------------------------------------------------------------------
- (Unit *) shipOnBoard
{
return onBoard;
}
//----------------------------------------------------------------------
- (void) setOnBoardShip:(Unit *)ship
{
if (onBoard != nil)
{
[onBoard unitDidExit:self];
onBoard = nil;
}
if (ship != nil && withinCity != nil)
{
[withinCity unitDidExit:self];
withinCity = nil;
}
onBoard = ship;
if (ship != nil)
[ship unitDidEnter:self];
}
//----------------------------------------------------------------------
- (void) unitDidExit:(Unit *)aUnit
{
[cargo removeObject:aUnit];
if (cargo != nil && [cargo count] == 0)
[owner unitIconHasChanged:self];
}
//----------------------------------------------------------------------
// Assumed that the movement method has already checked that the unit
// will fit.
//----------------------------------------------------------------------
- (void) unitDidEnter:(Unit *)aUnit
{
BOOL wasEmpty = [cargo count] == 0;
[cargo addObject:aUnit];
if (wasEmpty == YES)
[owner unitIconHasChanged:self];
[aUnit setOrder:[[OSentry alloc] initForUnit:aUnit]];
if (order != nil)
{
[order unitDidEnter:aUnit];
}
}
@end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.