Files
source/Tactical/Campaign.cpp
T
MaddMugsy 49a590ed34 1. PItems are now variable, from 3-20 and can be set in JA2Options.ini via NUM_P_ITEMS
2. USE_XML_TILESETS moved from JA2.ini to JA2Options.ini
3. New Feature - Ctrl+Click on an item with an item in hand to auto-attach/merge them.  Attach works with stacks.  Merge only works on single items, for now at least.
4. New Feature - To accompany Quiet Training, there are now Quiet Repairing and Quiet Doctoring options.
5. Bugfix - inseparable attachments of attachments were getting lost again when using ctrl+f/the map screen button to remove attachments
6. Bugfix - INT8 overflow error causing vehicle repair to actually lower its status
7. GasMask tag in items.xml can now be applied to helmets as well as face items.
8. Bugfix - AI code was referencing hard coded GASMASK item number instead of looking for the tag
9. Bugfix - "break;" was removed in Explosion Control.cpp, thereby causing mustard gas to deal out fire damage. (Not sure if this was deliberate?  But the new implementation differs from the JA2 standard, so it should be added to JA2Options.ini at least)

git-svn-id: https://ja2svn.mooo.com/source/ja2/trunk/GameSource/ja2_v1.13/Build@5320 3b4a5df2-a311-0410-b5c6-a8a6f20db521
2012-05-30 10:19:53 +00:00

2060 lines
60 KiB
C++

#ifdef PRECOMPILEDHEADERS
#include "Tactical All.h"
#else
#include "builddefines.h"
#include <wchar.h>
#include <stdio.h>
#include <string.h>
#include "wcheck.h"
#include "stdlib.h"
#include "debug.h"
#include "MemMan.h"
#include "Overhead Types.h"
#include "random.h"
#include "campaign.h"
#include "dialogue control.h"
#include "Map Screen Interface.h"
#include "Message.h"
#include "Game Clock.h"
#include "Strategic Mines.h"
#include "Strategic Status.h"
#include "Sys Globals.h"
#include "Text.h"
#include "GameSettings.h"
#include "Assignments.h"
#include "Interface.h"
#include "Game Event Hook.h"
#include "Overhead.h"
#include "meanwhile.h"
#include "Quests.h"
#include "Squads.h"
#include "Soldier macros.h"
#include "strategic.h"
#include "strategicmap.h"
#include "Town Militia.h"
#include "Campaign Types.h"
#include "Tactical Save.h"
#endif
#ifdef JA2UB
#include "Explosion Control.h"
#include "Ja25_Tactical.h"
#include "Ja25 Strategic Ai.h"
#include "MapScreen Quotes.h"
#include "email.h"
#include "interface Dialogue.h"
#include "mercs.h"
#include "ub_config.h"
#endif
#include "email.h"
//forward declarations of common classes to eliminate includes
class OBJECTTYPE;
class SOLDIERTYPE;
extern UINT8 gbPlayerNum;
#ifdef JA2TESTVERSION
// comment out to get rid of stat change msgs
//#define STAT_CHANGE_DEBUG
#endif
#ifdef STAT_CHANGE_DEBUG
STR16 wDebugStatStrings[]={
L"",
L"Life (Max)",
L"Agility",
L"Dexterity",
L"Wisdom",
L"Medical",
L"Explosives",
L"Mechanical",
L"Marksmanship",
L"Experience Level",
L"Strength",
L"Leadership",
};
#endif
// local prototypes
UINT8 CalcImportantSectorControl( void );
// HEADROCK HAM B1: New function to calculate how many important sectors there are in the game at all.
UINT8 CalcTotalImportantSectors( void );
UINT16 CountSurfaceSectorsVisited( void );
// HAEDROCK HAM B1: New function to calculate how many sectors CAN be visited, on the surface.
UINT16 TotalVisitableSurfaceSectors( void );
// give pSoldier usNumChances to improve ubStat. If it's from training, it doesn't count towards experience level gain
void StatChange(SOLDIERTYPE *pSoldier, UINT8 ubStat, UINT16 usNumChances, UINT8 ubReason)
{
if (pSoldier == NULL || pSoldier->bActive == FALSE)
return; // THIS SHOULD NEVER HAPPEN
Assert(pSoldier != NULL);
Assert(pSoldier->bActive);
// ignore non-player soldiers
if (!PTR_OURTEAM)
return;
// ignore anything without a profile
if (pSoldier->ubProfile == NO_PROFILE)
return;
// ignore vehicles and robots
if( ( pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE ) || ( pSoldier->flags.uiStatusFlags & SOLDIER_ROBOT ) )
return;
if( pSoldier->bAssignment == ASSIGNMENT_POW )
{
ScreenMsg( FONT_ORANGE, MSG_BETAVERSION, L"ERROR: StatChange: %s improving stats while POW! ubStat %d", pSoldier->name, ubStat );
return;
}
// no points earned while somebody is unconscious (for assist XPs, and such)
if ( pSoldier->stats.bLife < CONSCIOUSNESS )
return;
#ifdef TESTVERSION
if ( gTacticalStatus.fStatChangeCheatOn )
{
usNumChances = 100;
}
#endif
ProcessStatChange( &( gMercProfiles[ pSoldier->ubProfile ] ), ubStat, usNumChances, ubReason );
// Update stats....right away... ATE
UpdateStats( pSoldier, ubReason );
}
// this is the equivalent of StatChange(), but for use with mercs not currently on player's team
// give pProfile usNumChances to improve ubStat. If it's from training, it doesn't count towards experience level gain
void ProfileStatChange(MERCPROFILESTRUCT *pProfile, UINT8 ubStat, UINT16 usNumChances, UINT8 ubReason)
{
// dead guys don't do nuthin' !
if ( pProfile->bMercStatus == MERC_IS_DEAD )
return;
if ( pProfile->bLife < OKLIFE )
return;
ProcessStatChange( pProfile, ubStat, usNumChances, ubReason );
// Update stats....right away... ATE
ProfileUpdateStats( pProfile, ubReason );
}
void ProcessStatChange(MERCPROFILESTRUCT *pProfile, UINT8 ubStat, UINT16 usNumChances, UINT8 ubReason)
{
UINT32 uiCnt,uiEffLevel;
INT16 sSubPointChange = 0;
UINT16 usChance=0;
UINT16 usSubpointsPerPoint;
UINT16 usSubpointsPerLevel;
INT8 bCurrentRating;
UINT16 *psStatGainPtr;
BOOLEAN fAffectedByWisdom = TRUE;
Assert(pProfile != NULL);
if ( pProfile->bEvolution == NO_EVOLUTION )
return; // No change possible, quit right away
// if this is a Reverse-Evolving merc who attempting to train
if ( ( ubReason == FROM_TRAINING ) && ( pProfile->bEvolution == DEVOLVE ) )
return; // he doesn't get any benefit, but isn't penalized either
if (usNumChances == 0)
return;
usSubpointsPerPoint = SubpointsPerPoint(ubStat, pProfile->bExpLevel);
usSubpointsPerLevel = SubpointsPerPoint(EXPERAMT, pProfile->bExpLevel);
switch (ubStat)
{
case HEALTHAMT:
bCurrentRating = pProfile->bLifeMax;
psStatGainPtr = (UINT16 *)&(pProfile->sLifeGain);
// NB physical stat checks not affected by wisdom, unless training is going on
fAffectedByWisdom = FALSE;
break;
case AGILAMT:
bCurrentRating = pProfile->bAgility;
psStatGainPtr = (UINT16 *)&(pProfile->sAgilityGain);
fAffectedByWisdom = FALSE;
break;
case DEXTAMT:
bCurrentRating = pProfile->bDexterity;
psStatGainPtr = (UINT16 *)&(pProfile->sDexterityGain);
fAffectedByWisdom = FALSE;
break;
case WISDOMAMT:
bCurrentRating = pProfile->bWisdom;
psStatGainPtr = (UINT16 *)&(pProfile->sWisdomGain);
break;
case MEDICALAMT:
bCurrentRating = pProfile->bMedical;
psStatGainPtr = (UINT16 *)&(pProfile->sMedicalGain);
break;
case EXPLODEAMT:
bCurrentRating = pProfile->bExplosive;
psStatGainPtr = (UINT16 *)&(pProfile->sExplosivesGain);
break;
case MECHANAMT:
bCurrentRating = pProfile->bMechanical;
psStatGainPtr = (UINT16 *)&(pProfile->sMechanicGain);
break;
case MARKAMT:
bCurrentRating = pProfile->bMarksmanship;
psStatGainPtr = (UINT16 *)&(pProfile->sMarksmanshipGain);
break;
case EXPERAMT:
bCurrentRating = pProfile->bExpLevel;
psStatGainPtr = (UINT16 *)&(pProfile->sExpLevelGain);
break;
case STRAMT:
bCurrentRating = pProfile->bStrength;
psStatGainPtr = (UINT16 *)&(pProfile->sStrengthGain);
fAffectedByWisdom = FALSE;
break;
case LDRAMT:
bCurrentRating = pProfile->bLeadership;
psStatGainPtr = (UINT16 *)&(pProfile->sLeadershipGain);
break;
default:
// BETA message
ScreenMsg( FONT_ORANGE, MSG_BETAVERSION, L"ERROR: ProcessStatChange: Rcvd unknown ubStat %d", ubStat);
return;
}
if (ubReason == FROM_TRAINING)
{
// training always affected by wisdom
fAffectedByWisdom = TRUE;
}
// stats/skills of 0 can NEVER be improved!
if ( bCurrentRating < 0 )
{
return;
}
// loop once for each chance to improve
for (uiCnt = 0; uiCnt < usNumChances; uiCnt++)
{
if (pProfile->bEvolution == NORMAL_EVOLUTION) // Evolves!
{
// if this is improving from a failure, and a successful roll would give us enough to go up a point
if ((ubReason == FROM_FAILURE) && ((*psStatGainPtr + 1) >= usSubpointsPerPoint))
{
// can't improve any more from this statchange, because Ian don't want failures causin increases!
break;
}
if (ubStat != EXPERAMT)
{
// NON-experience level changes, actual usChance depends on bCurrentRating
// Base usChance is '100 - bCurrentRating'
usChance = 100 - (bCurrentRating + (*psStatGainPtr / usSubpointsPerPoint));
// prevent training beyond the training cap
if ((ubReason == FROM_TRAINING) && (bCurrentRating + (*psStatGainPtr / usSubpointsPerPoint) >= gGameExternalOptions.ubTrainingSkillMax))
{
usChance = 0;
}
}
else
{
// Experience level changes, actual usChance depends on level
// Base usChance is '100 - (10 * current level)'
usChance = 100 - 10 * (bCurrentRating + (*psStatGainPtr / usSubpointsPerPoint));
}
// if there IS a usChance, adjust it for high or low wisdom (50 is avg)
if (usChance > 0 && fAffectedByWisdom)
{
usChance += (usChance * (pProfile->bWisdom + (pProfile->sWisdomGain / SubpointsPerPoint(WISDOMAMT, pProfile->bExpLevel)) - 50)) / 100;
}
/*
// if the stat is Marksmanship, and the guy is a hopeless shot
if ((ubStat == MARKAMT) && (pProfile->bSpecialTrait == HOPELESS_SHOT))
{
usChance /= 5; // MUCH slower to improve, divide usChance by 5
}
*/
// SANDRO - penalty for primitive people, they get lesser chance to gain point for certain skills
if ( gGameOptions.fNewTraitSystem && (usChance > 10) && (ubStat != EXPERAMT) && (pProfile->bCharacterTrait == CHAR_TRAIT_PRIMITIVE) )
{
switch (ubStat)
{
case WISDOMAMT:
case MEDICALAMT:
case EXPLODEAMT:
case MECHANAMT:
case LDRAMT:
usChance = max(1, (usChance - 10)); // -10% chance to gain the point
break;
}
}
// maximum possible usChance is 99%
if (usChance > 99)
{
usChance = 99;
}
if (PreRandom(100) < usChance )
{
(*psStatGainPtr)++;
sSubPointChange++;
// as long as we're not dealing with exp_level changes (already added above!)
// and it's not from training, and the exp level isn't max'ed out already
if ((ubStat != EXPERAMT) && (ubReason != FROM_TRAINING))
{
uiEffLevel = pProfile->bExpLevel + (pProfile->sExpLevelGain / usSubpointsPerLevel);
// if level is not at maximum
if (uiEffLevel < MAXEXPLEVEL)
{
// if this is NOT improving from a failure, OR it would NOT give us enough to go up a level
if ((ubReason != FROM_FAILURE) || ((pProfile->sExpLevelGain + 1) < usSubpointsPerLevel))
{
// all other stat changes count towards experience level changes (1 for 1 basis)
pProfile->sExpLevelGain++;
}
}
}
}
}
else // Regresses!
{
// regression can happen from both failures and successes (but not training, checked above)
if (ubStat != EXPERAMT)
{
// NON-experience level changes, actual usChance depends on bCurrentRating
switch (ubStat)
{
case HEALTHAMT:
case AGILAMT:
case DEXTAMT:
case WISDOMAMT:
case STRAMT:
// Base usChance is 'bCurrentRating - 1', since these must remain at 1-100
usChance = bCurrentRating + (*psStatGainPtr / usSubpointsPerPoint) - 1;
break;
case MEDICALAMT:
case EXPLODEAMT:
case MECHANAMT:
case MARKAMT:
case LDRAMT:
// Base usChance is 'bCurrentRating', these can drop to 0
usChance = bCurrentRating + (*psStatGainPtr / usSubpointsPerPoint);
break;
}
}
else
{
// Experience level changes, actual usChance depends on level
// Base usChance is '10 * (current level - 1)'
usChance = 10 * (bCurrentRating + (*psStatGainPtr / usSubpointsPerPoint) - 1);
// if there IS a usChance, adjust it for high or low wisdom (50 is avg)
if (usChance > 0 && fAffectedByWisdom)
{
usChance -= (usChance * (pProfile->bWisdom + (pProfile->sWisdomGain / SubpointsPerPoint(WISDOMAMT, pProfile->bExpLevel)) - 50)) / 100;
}
// if there's ANY usChance, minimum usChance is 1% regardless of wisdom
if (usChance < 1)
{
usChance = 1;
}
}
if (usChance <= 0)
usChance = 1;
if (PreRandom(100) < usChance )
{
(*psStatGainPtr)--;
sSubPointChange--;
// as long as we're not dealing with exp_level changes (already added above!)
// and it's not from training, and the exp level isn't max'ed out already
if ((ubStat != EXPERAMT) && (ubReason != FROM_TRAINING))
{
uiEffLevel = pProfile->bExpLevel + (pProfile->sExpLevelGain / usSubpointsPerLevel );
// if level is not at minimum
if (uiEffLevel > 1)
{
// all other stat changes count towards experience level changes (1 for 1 basis)
pProfile->sExpLevelGain--;
}
}
}
}
}
#ifdef STAT_CHANGE_DEBUG
if (sSubPointChange != 0)
{
// debug message
ScreenMsg( MSG_FONT_RED, MSG_DEBUG, L"%s's %s changed by %d", pProfile->zNickname, wDebugStatStrings[ubStat], sSubPointChange );
}
#endif
// exclude training, that's not under our control
if (ubReason != FROM_TRAINING)
{
// increment counters that track how often stat changes are being awarded
pProfile->usStatChangeChances[ ubStat ] += usNumChances;
pProfile->usStatChangeSuccesses[ ubStat ] += abs( sSubPointChange );
}
}
// convert hired mercs' stats subpoint changes into actual point changes where warranted
void UpdateStats( SOLDIERTYPE *pSoldier, UINT8 ubReason )
{
ProcessUpdateStats( &( gMercProfiles[ pSoldier->ubProfile ] ), pSoldier, ubReason );
}
// UpdateStats version for mercs not currently on player's team
void ProfileUpdateStats( MERCPROFILESTRUCT *pProfile, UINT8 ubReason )
{
ProcessUpdateStats( pProfile, NULL, ubReason );
}
void ChangeStat( MERCPROFILESTRUCT *pProfile, SOLDIERTYPE *pSoldier, UINT8 ubStat, INT16 sPtsChanged, UINT8 ubReason = 0 )
{
// this function changes the stat a given amount...
INT16 *psStatGainPtr = NULL;
INT8 *pbStatPtr = NULL;
INT8 *pbSoldierStatPtr = NULL;
INT8 *pbStatDeltaPtr = NULL;
UINT32 *puiStatTimerPtr = NULL;
BOOLEAN fChangeTypeIncrease;
BOOLEAN fChangeSalary;
UINT32 uiLevelCnt;
UINT8 ubMercMercIdValue = 0;
UINT16 usIncreaseValue = 0;
UINT16 usSubpointsPerPoint;
INT8 bDamagedStatToRaise = -1; // added by SANDRO
usSubpointsPerPoint = SubpointsPerPoint(ubStat, pProfile->bExpLevel );
// build ptrs to appropriate profiletype stat fields
switch( ubStat )
{
case HEALTHAMT:
psStatGainPtr = &(pProfile->sLifeGain);
pbStatDeltaPtr = &(pProfile->bLifeDelta);
pbStatPtr = &(pProfile->bLifeMax);
break;
case AGILAMT:
psStatGainPtr = &(pProfile->sAgilityGain);
pbStatDeltaPtr = &(pProfile->bAgilityDelta);
pbStatPtr = &(pProfile->bAgility);
break;
case DEXTAMT:
psStatGainPtr = &(pProfile->sDexterityGain);
pbStatDeltaPtr = &(pProfile->bDexterityDelta);
pbStatPtr = &(pProfile->bDexterity);
break;
case WISDOMAMT:
psStatGainPtr = &(pProfile->sWisdomGain);
pbStatDeltaPtr = &(pProfile->bWisdomDelta);
pbStatPtr = &(pProfile->bWisdom);
break;
case MEDICALAMT:
psStatGainPtr = &(pProfile->sMedicalGain);
pbStatDeltaPtr = &(pProfile->bMedicalDelta);
pbStatPtr = &(pProfile->bMedical);
break;
case EXPLODEAMT:
psStatGainPtr = &(pProfile->sExplosivesGain);
pbStatDeltaPtr = &(pProfile->bExplosivesDelta);
pbStatPtr = &(pProfile->bExplosive);
break;
case MECHANAMT:
psStatGainPtr = &(pProfile->sMechanicGain);
pbStatDeltaPtr = &(pProfile->bMechanicDelta);
pbStatPtr = &(pProfile->bMechanical);
break;
case MARKAMT:
psStatGainPtr = &(pProfile->sMarksmanshipGain);
pbStatDeltaPtr = &(pProfile->bMarksmanshipDelta);
pbStatPtr = &(pProfile->bMarksmanship);
break;
case EXPERAMT:
psStatGainPtr = &(pProfile->sExpLevelGain);
pbStatDeltaPtr = &(pProfile->bExpLevelDelta);
pbStatPtr = &(pProfile->bExpLevel);
break;
case STRAMT:
psStatGainPtr = &(pProfile->sStrengthGain);
pbStatDeltaPtr = &(pProfile->bStrengthDelta);
pbStatPtr = &(pProfile->bStrength);
break;
case LDRAMT:
psStatGainPtr = &(pProfile->sLeadershipGain);
pbStatDeltaPtr = &(pProfile->bLeadershipDelta);
pbStatPtr = &(pProfile->bLeadership);
break;
}
// if this merc is currently on the player's team
if (pSoldier != NULL)
{
// build ptrs to appropriate soldiertype stat fields
// SANDRO - added a variable for stat repair
switch( ubStat )
{
case HEALTHAMT:
pbSoldierStatPtr = &( pSoldier->stats.bLifeMax );
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeHealthTime);
usIncreaseValue = HEALTH_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_HEALTH;
break;
case AGILAMT:
pbSoldierStatPtr = &( pSoldier->stats.bAgility );
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeAgilityTime);
usIncreaseValue = AGIL_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_AGILITY;
break;
case DEXTAMT:
pbSoldierStatPtr = &( pSoldier->stats.bDexterity );
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeDexterityTime);
usIncreaseValue = DEX_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_DEXTERITY;
break;
case WISDOMAMT:
pbSoldierStatPtr = &( pSoldier->stats.bWisdom );
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeWisdomTime);
usIncreaseValue = WIS_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_WISDOM;
break;
case MEDICALAMT:
pbSoldierStatPtr = &( pSoldier->stats.bMedical );
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeMedicalTime);
usIncreaseValue = MED_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_MEDICAL;
break;
case EXPLODEAMT:
pbSoldierStatPtr = &( pSoldier->stats.bExplosive );
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeExplosivesTime);
usIncreaseValue = EXP_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_EXPLOSIVES;
break;
case MECHANAMT:
pbSoldierStatPtr = &( pSoldier->stats.bMechanical );
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeMechanicalTime);
usIncreaseValue = MECH_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_MECHANICAL;
break;
case MARKAMT:
pbSoldierStatPtr = &( pSoldier->stats.bMarksmanship );
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeMarksmanshipTime);
usIncreaseValue = MRK_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_MARKSMANSHIP;
break;
case EXPERAMT:
pbSoldierStatPtr = &(pSoldier->stats.bExpLevel);
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeLevelTime );
usIncreaseValue = LVL_INCREASE;
break;
case STRAMT:
pbSoldierStatPtr = &(pSoldier->stats.bStrength);
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeStrengthTime);
usIncreaseValue = STRENGTH_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_STRENGTH;
break;
case LDRAMT:
pbSoldierStatPtr = &( pSoldier->stats.bLeadership);
puiStatTimerPtr = &( pSoldier->timeChanges.uiChangeLeadershipTime);
usIncreaseValue = LDR_INCREASE;
bDamagedStatToRaise = DAMAGED_STAT_LEADERSHIP;
break;
}
}
// ptrs set up, now handle
// if the stat needs to change
if ( sPtsChanged != 0 )
{
// if a stat improved
if ( sPtsChanged > 0 )
{
fChangeTypeIncrease = TRUE;
}
else
{
fChangeTypeIncrease = FALSE;
}
// update merc profile stat
*pbStatPtr += sPtsChanged;
// if this merc is currently on the player's team (DON'T count increases earned outside the player's employ)
if (pSoldier != NULL)
{
// also note the delta (how much this stat has changed since start of game)
*pbStatDeltaPtr += sPtsChanged;
}
// reduce gain to the unused subpts only
*psStatGainPtr = ( *psStatGainPtr ) % usSubpointsPerPoint;
// if the guy is employed by player
if (pSoldier != NULL)
{
// transfer over change to soldiertype structure
*pbSoldierStatPtr = *pbStatPtr;
// SANDRO - reduce damaged stat if this stat was increased normally
if ( fChangeTypeIncrease && (bDamagedStatToRaise != -1) )
{
if (pSoldier->ubCriticalStatDamage[ bDamagedStatToRaise ] > 0)
pSoldier->ubCriticalStatDamage[ bDamagedStatToRaise ] = max( 0, (pSoldier->ubCriticalStatDamage[ bDamagedStatToRaise ] - sPtsChanged));
}
// if it's a level gain, or sometimes for other stats
// ( except health; not only will it sound silly, but
// also we give points for health on sector traversal and this would
// probaby mess up battle handling too )
if ( (ubStat != HEALTHAMT) && ( (ubStat == EXPERAMT) || Random( 100 ) < 25 ) )
//if ( (ubStat != EXPERAMT) && (ubStat != HEALTHAMT) && ( Random( 100 ) < 25 ) )
{
// Pipe up with "I'm getting better at this!"
TacticalCharacterDialogueWithSpecialEventEx( pSoldier, 0, DIALOGUE_SPECIAL_EVENT_DISPLAY_STAT_CHANGE, fChangeTypeIncrease, sPtsChanged, ubStat );
//Madd: option to make mercs quiet during training / doctoring / repairing
if ( ((pSoldier->bAssignment == TRAIN_BY_OTHER || pSoldier->bAssignment == TRAIN_TEAMMATE || pSoldier->bAssignment == TRAIN_SELF ||
pSoldier->bAssignment == FACILITY_STAFF || pSoldier->bAssignment == TRAIN_TOWN || pSoldier->bAssignment == TRAIN_MOBILE )
&& !gGameSettings.fOptions[TOPTION_QUIET_TRAINING]) ||
(pSoldier->bAssignment == REPAIR && !gGameSettings.fOptions[TOPTION_QUIET_REPAIRING]) ||
(pSoldier->bAssignment == DOCTOR && !gGameSettings.fOptions[TOPTION_QUIET_DOCTORING]))
TacticalCharacterDialogue( pSoldier, QUOTE_EXPERIENCE_GAIN );
}
else
{
CHAR16 wTempString[ 128 ];
// tell player about it
BuildStatChangeString( wTempString, pSoldier->name, fChangeTypeIncrease, sPtsChanged, ubStat );
ScreenMsg( FONT_MCOLOR_LTYELLOW, MSG_INTERFACE, wTempString );
}
// update mapscreen soldier info panel
fCharacterInfoPanelDirty = TRUE;
// remember what time it changed at, it's displayed in a different color for a while afterwards
*puiStatTimerPtr = GetJA2Clock();
if( fChangeTypeIncrease )
{
pSoldier->usValueGoneUp |= usIncreaseValue;
}
else
{
pSoldier->usValueGoneUp &= ~( usIncreaseValue );
}
fInterfacePanelDirty = DIRTYLEVEL2;
}
// special handling for LIFEMAX
if( ubStat == HEALTHAMT )
{
// adjust current health by the same amount as max health
pProfile->bLife += sPtsChanged;
// don't let this kill a guy or knock him out!!!
if (pProfile->bLife < OKLIFE)
{
pProfile->bLife = OKLIFE;
}
// if the guy is employed by player
if (pSoldier != NULL)
{
// adjust current health by the same amount as max health
pSoldier->stats.bLife += sPtsChanged;
// don't let this kill a guy or knock him out!!!
if (pSoldier->stats.bLife < OKLIFE)
{
pSoldier->stats.bLife = OKLIFE;
}
}
}
// special handling for EXPERIENCE LEVEL
// merc salaries increase if level goes up (but they don't go down if level drops!)
if( (ubStat == EXPERAMT) && fChangeTypeIncrease)
{
// if the guy is employed by player
if (pSoldier != NULL)
{
switch (pSoldier->ubWhatKindOfMercAmI)
{
case MERC_TYPE__AIM_MERC:
// A.I.M.
pSoldier->flags.fContractPriceHasIncreased = TRUE;
fChangeSalary = TRUE;
break;
case MERC_TYPE__MERC:
// M.E.R.C.
ubMercMercIdValue = pSoldier->ubProfile;
/*
// Biff's profile id ( 40 ) is the base
ubMercMercIdValue -= BIFF;
// offset for the 2 profiles of Larry (we only have one email for Larry..but 2 profile entries
if( ubMercMercIdValue >= ( LARRY_DRUNK - BIFF ) )
{
ubMercMercIdValue--;
}
*/
//
// Send special E-mail
//
// DEF: 03/06/99 Now sets an event that will be processed later in the day
// ubEmailOffset = MERC_UP_LEVEL_BIFF + MERC_UP_LEVEL_LENGTH_BIFF * ( ubMercMercIdValue );
// AddEmail( ubEmailOffset, MERC_UP_LEVEL_LENGTH_BIFF, SPECK_FROM_MERC, GetWorldTotalMin() );
AddStrategicEvent( EVENT_MERC_MERC_WENT_UP_LEVEL_EMAIL_DELAY, GetWorldTotalMin( ) + 60 + Random( 60 ), ubMercMercIdValue );
fChangeSalary = TRUE;
break;
default:
// others don't increase salary
fChangeSalary = FALSE;
break;
}
}
else // not employed by player
{
// only AIM and M.E.R.C.s update stats when not on player's team, and both of them DO change salary
fChangeSalary = TRUE;
}
if (fChangeSalary)
{
// increase all salaries and medical deposits, once for each level gained
for (uiLevelCnt = 0; uiLevelCnt < (UINT32) sPtsChanged; uiLevelCnt++)
{
pProfile->sSalary = (INT16) CalcNewSalary(pProfile->sSalary, fChangeTypeIncrease, MAX_DAILY_SALARY);
pProfile->uiWeeklySalary = CalcNewSalary(pProfile->uiWeeklySalary, fChangeTypeIncrease, MAX_LARGE_SALARY);
pProfile->uiBiWeeklySalary = CalcNewSalary(pProfile->uiBiWeeklySalary, fChangeTypeIncrease, MAX_LARGE_SALARY);
pProfile->sTrueSalary = (INT16) CalcNewSalary(pProfile->sTrueSalary, fChangeTypeIncrease, MAX_DAILY_SALARY);
pProfile->sMedicalDepositAmount = (INT16) CalcNewSalary(pProfile->sMedicalDepositAmount, fChangeTypeIncrease, MAX_DAILY_SALARY);
//if (pSoldier != NULL)
// DON'T increase the *effective* medical deposit, it's already been paid out
// pSoldier->usMedicalDeposit = pProfile->sMedicalDepositAmount;
}
}
}
}
return;
}
// pSoldier may be NULL!
void ProcessUpdateStats( MERCPROFILESTRUCT *pProfile, SOLDIERTYPE *pSoldier, UINT8 ubReason )
{
// this function will run through the soldier's profile and update their stats based on any accumulated gain pts.
UINT8 ubStat = 0;
INT16 *psStatGainPtr = NULL;
INT8 *pbStatPtr = NULL;
INT8 *pbSoldierStatPtr = NULL;
INT8 bMinStatValue;
INT8 bMaxStatValue;
UINT16 usSubpointsPerPoint;
INT16 sPtsChanged;
// if hired, not back at AIM
if ( pSoldier != NULL )
{
// ATE: if in the midst of an attack, if in the field, delay all stat changes until the check made after the 'attack'...
if ( ( gTacticalStatus.ubAttackBusyCount > 0 ) && pSoldier->bInSector && ( gTacticalStatus.uiFlags & INCOMBAT ) )
return;
// ignore non-player soldiers
if (!PTR_OURTEAM)
return;
// ignore anything without a profile
if (pSoldier->ubProfile == NO_PROFILE)
return;
// ignore vehicles and robots
if( ( pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE ) || ( pSoldier->flags.uiStatusFlags & SOLDIER_ROBOT ) )
return;
// delay increases while merc is dying
if (pSoldier->stats.bLife < OKLIFE)
return;
// ignore POWs - shouldn't ever be getting this far
if( pSoldier->bAssignment == ASSIGNMENT_POW )
{
return;
}
}
else
{
// dead guys don't do nuthin' !
if ( pProfile->bMercStatus == MERC_IS_DEAD )
return;
if ( pProfile->bLife < OKLIFE )
return;
}
// check every attribute, skill, and exp.level, too
for( ubStat = FIRST_CHANGEABLE_STAT; ubStat <= LAST_CHANGEABLE_STAT; ubStat++ )
{
// set default min & max, subpoints/pt.
bMinStatValue = 1;
bMaxStatValue = MAX_STAT_VALUE;
usSubpointsPerPoint = SubpointsPerPoint(ubStat, pProfile->bExpLevel);
// build ptrs to appropriate profiletype stat fields
switch( ubStat )
{
case HEALTHAMT:
psStatGainPtr = &(pProfile->sLifeGain);
pbStatPtr = &(pProfile->bLifeMax);
bMinStatValue = OKLIFE;
break;
case AGILAMT:
psStatGainPtr = &(pProfile->sAgilityGain);
pbStatPtr = &(pProfile->bAgility);
break;
case DEXTAMT:
psStatGainPtr = &(pProfile->sDexterityGain);
pbStatPtr = &(pProfile->bDexterity);
break;
case WISDOMAMT:
psStatGainPtr = &(pProfile->sWisdomGain);
pbStatPtr = &(pProfile->bWisdom);
break;
case MEDICALAMT:
psStatGainPtr = &(pProfile->sMedicalGain);
pbStatPtr = &(pProfile->bMedical);
bMinStatValue = 0;
break;
case EXPLODEAMT:
psStatGainPtr = &(pProfile->sExplosivesGain);
pbStatPtr = &(pProfile->bExplosive);
bMinStatValue = 0;
break;
case MECHANAMT:
psStatGainPtr = &(pProfile->sMechanicGain);
pbStatPtr = &(pProfile->bMechanical);
bMinStatValue = 0;
break;
case MARKAMT:
psStatGainPtr = &(pProfile->sMarksmanshipGain);
pbStatPtr = &(pProfile->bMarksmanship);
bMinStatValue = 0;
break;
case EXPERAMT:
psStatGainPtr = &(pProfile->sExpLevelGain);
pbStatPtr = &(pProfile->bExpLevel);
bMaxStatValue = MAXEXPLEVEL;
break;
case STRAMT:
psStatGainPtr = &(pProfile->sStrengthGain);
pbStatPtr = &(pProfile->bStrength);
break;
case LDRAMT:
psStatGainPtr = &(pProfile->sLeadershipGain);
pbStatPtr = &(pProfile->bLeadership);
bMinStatValue = 0; // Snap: Since leadership can now be 0...
break;
}
// if this merc is currently on the player's team
if (pSoldier != NULL)
{
// build ptrs to appropriate soldiertype stat fields
switch( ubStat )
{
case HEALTHAMT:
pbSoldierStatPtr = &( pSoldier->stats.bLifeMax );
break;
case AGILAMT:
pbSoldierStatPtr = &( pSoldier->stats.bAgility );
break;
case DEXTAMT:
pbSoldierStatPtr = &( pSoldier->stats.bDexterity );
break;
case WISDOMAMT:
pbSoldierStatPtr = &( pSoldier->stats.bWisdom );
break;
case MEDICALAMT:
pbSoldierStatPtr = &( pSoldier->stats.bMedical );
break;
case EXPLODEAMT:
pbSoldierStatPtr = &( pSoldier->stats.bExplosive );
break;
case MECHANAMT:
pbSoldierStatPtr = &( pSoldier->stats.bMechanical );
break;
case MARKAMT:
pbSoldierStatPtr = &( pSoldier->stats.bMarksmanship );
break;
case EXPERAMT:
pbSoldierStatPtr = &(pSoldier->stats.bExpLevel);
break;
case STRAMT:
pbSoldierStatPtr = &(pSoldier->stats.bStrength);
break;
case LDRAMT:
pbSoldierStatPtr = &( pSoldier->stats.bLeadership);
break;
}
}
// ptrs set up, now handle
// Calc how many full points worth of stat changes we have accumulated in this stat (positive OR negative!)
// NOTE: for simplicity, this hopes nobody will go up more than one level at once, which would change the subpoints/pt
sPtsChanged = ( *psStatGainPtr ) / usSubpointsPerPoint;
// gone too high or too low?..handle the fact
if( (*pbStatPtr + sPtsChanged) > bMaxStatValue )
{
// reduce change to reach max value and reset stat gain ptr
sPtsChanged = bMaxStatValue - *pbStatPtr;
*psStatGainPtr = 0;
}
else
if( (*pbStatPtr + sPtsChanged) < bMinStatValue )
{
// reduce change to reach min value and reset stat gain ptr
sPtsChanged = bMinStatValue - *pbStatPtr;
*psStatGainPtr = 0;
}
// if the stat needs to change
if ( sPtsChanged != 0 )
{
// Otherwise, use normal stat increase stuff...
ChangeStat( pProfile, pSoldier, ubStat, sPtsChanged, ubReason );
}
}
return;
}
void HandleAnyStatChangesAfterAttack( void )
{
INT32 cnt;
SOLDIERTYPE *pSoldier;
// must check everyone on player's team, not just the shooter
for ( cnt = 0, pSoldier = MercPtrs[ 0 ]; cnt <= gTacticalStatus.Team[ MercPtrs[ 0 ]->bTeam ].bLastID; cnt++,pSoldier++)
{
if (pSoldier->bActive)
{
ProcessUpdateStats( &( gMercProfiles[ pSoldier->ubProfile ] ), pSoldier );
}
}
}
UINT32 CalcNewSalary(UINT32 uiOldSalary, BOOLEAN fIncrease, UINT32 uiMaxLimit)
{
UINT32 uiNewSalary;
FLOAT SalaryMultiplier = (FLOAT) ((gGameExternalOptions.gMercLevelUpSalaryIncreasePercentage / 100) + 1);
// if he was working for free, it's still free!
if (uiOldSalary == 0)
{
return(0);
}
if (fIncrease)
{
uiNewSalary = (UINT32) (uiOldSalary * SalaryMultiplier);
}
else
{
uiNewSalary = (UINT32) (uiOldSalary / SalaryMultiplier);
}
// round it off to a reasonable multiple
uiNewSalary = RoundOffSalary(uiNewSalary);
// let's set some reasonable limits here, lest it get silly one day
if (uiNewSalary > uiMaxLimit)
uiNewSalary = uiMaxLimit;
if (uiNewSalary < 5)
uiNewSalary = 5;
return(uiNewSalary);
}
UINT32 RoundOffSalary(UINT32 uiSalary)
{
UINT32 uiMultiple;
// determine what multiple value the salary should be rounded off to
if (uiSalary <= 250) uiMultiple = 5;
else if (uiSalary <= 500) uiMultiple = 10;
else if (uiSalary <= 1000) uiMultiple = 25;
else if (uiSalary <= 2000) uiMultiple = 50;
else if (uiSalary <= 5000) uiMultiple = 100;
else if (uiSalary <= 10000) uiMultiple = 500;
else if (uiSalary <= 25000) uiMultiple = 1000;
else if (uiSalary <= 50000) uiMultiple = 2000;
else uiMultiple = 5000;
// if the salary doesn't divide evenly by the multiple
if (uiSalary % uiMultiple)
{
// then we have to make it so, as Picard would say <- We have to wonder how much Alex gets out
// and while we're at it, we round up to next higher multiple if halfway
uiSalary = ((uiSalary + (uiMultiple / 2)) / uiMultiple) * uiMultiple;
}
return(uiSalary);
}
UINT16 SubpointsPerPoint(UINT8 ubStat, INT8 bExpLevel)
{
UINT16 usSubpointsPerPoint;
UINT16 HEALTH_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usHealthSubpointsToImprove;
UINT16 STRENGTH_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usStrengthSubpointsToImprove;
UINT16 DEXTERITY_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usDexteritySubpointsToImprove;
UINT16 AGILITY_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usAgilitySubpointsToImprove;
UINT16 WISDOM_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usWisdomSubpointsToImprove;
UINT16 MARKSMANSHIP_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usMarksmanshipSubpointsToImprove;
UINT16 MEDICAL_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usMedicalSubpointsToImprove;
UINT16 MECHANICAL_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usMechanicalSubpointsToImprove;
UINT16 LEADERSHIP_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usLeadershipSubpointsToImprove;
UINT16 EXPLOSIVES_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usExplosivesSubpointsToImprove;
UINT16 LEVEL_SUBPOINTS_TO_IMPROVE = gGameExternalOptions.usLevelSubpointsToImprove;
// figure out how many subpoints this type of stat needs to change
switch (ubStat)
{
// Attributes
case HEALTHAMT:
usSubpointsPerPoint = HEALTH_SUBPOINTS_TO_IMPROVE;
break;
case AGILAMT:
usSubpointsPerPoint = AGILITY_SUBPOINTS_TO_IMPROVE;
break;
case DEXTAMT:
usSubpointsPerPoint = DEXTERITY_SUBPOINTS_TO_IMPROVE;
break;
case WISDOMAMT:
usSubpointsPerPoint = WISDOM_SUBPOINTS_TO_IMPROVE;
break;
case STRAMT:
usSubpointsPerPoint = STRENGTH_SUBPOINTS_TO_IMPROVE;
break;
// Skills
case MEDICALAMT:
usSubpointsPerPoint = MEDICAL_SUBPOINTS_TO_IMPROVE;
break;
case EXPLODEAMT:
usSubpointsPerPoint = EXPLOSIVES_SUBPOINTS_TO_IMPROVE;
break;
case MECHANAMT:
usSubpointsPerPoint = MECHANICAL_SUBPOINTS_TO_IMPROVE;
break;
case MARKAMT:
usSubpointsPerPoint = MARKSMANSHIP_SUBPOINTS_TO_IMPROVE;
break;
case LDRAMT:
usSubpointsPerPoint = LEADERSHIP_SUBPOINTS_TO_IMPROVE;
break;
// Experience
case EXPERAMT:
usSubpointsPerPoint = LEVEL_SUBPOINTS_TO_IMPROVE * bExpLevel;
break;
default:
// BETA message
ScreenMsg( FONT_ORANGE, MSG_BETAVERSION, L"SubpointsPerPoint: ERROR - Unknown ubStat %d", ubStat);
return(100);
}
return(usSubpointsPerPoint);
}
// handles stat changes for mercs not currently working for the player
void HandleUnhiredMercImprovement( MERCPROFILESTRUCT *pProfile )
{
UINT8 ubNumStats;
UINT8 ubStat;
UINT16 usNumChances;
ubNumStats = LAST_CHANGEABLE_STAT - FIRST_CHANGEABLE_STAT + 1;
// if he's working on another job
if (pProfile->bMercStatus == MERC_WORKING_ELSEWHERE)
{
// Lesh: changed chance from 20 to 70
// if he did't do anything interesting today
if (Random(100) < 70)
{
// no chance to change today
return;
}
// it's real on the job experience, counts towards experience
// all stats (including experience itself) get an equal chance to improve
// 80 wisdom gives 8 rolls per stat per day, 10 stats, avg success rate 40% = 32pts per day,
// so about 10 working days to hit lvl 2. This seems high, but mercs don't actually "work" that often, and it's twice
// as long to hit level 3. If we go lower, attribs & skills will barely move.
// Lesh: changed wisdom divided by 7 insead of 10
usNumChances = ( pProfile->bWisdom / 7 );
for (ubStat = FIRST_CHANGEABLE_STAT; ubStat <= LAST_CHANGEABLE_STAT; ubStat++)
{
ProfileStatChange( pProfile, ubStat, usNumChances, FALSE );
}
}
else
{
// if the merc just takes it easy (high level or stupid mercs are more likely to)
if (((INT8) Random(10) < pProfile->bExpLevel) || ((INT8) Random(100) > pProfile->bWisdom))
{
// no chance to change today
return;
}
// it's just practise/training back home
do
{
// pick ONE stat at random to focus on (it may be beyond training cap, but so what, too hard to weed those out)
ubStat = (UINT8) (FIRST_CHANGEABLE_STAT + Random(ubNumStats));
// except experience - can't practise that!
} while (ubStat == EXPERAMT);
// try to improve that one stat
ProfileStatChange( pProfile, ubStat, ( UINT16 ) ( pProfile->bWisdom / 2 ), FROM_TRAINING );
}
ProfileUpdateStats( pProfile );
}
// handles possible death of mercs not currently working for the player
void HandleUnhiredMercDeaths( INT32 iProfileID )
{
UINT8 ubMaxDeaths;
INT16 sChance;
MERCPROFILESTRUCT *pProfile = &(gMercProfiles[ iProfileID ]);
// if the player has never yet had the chance to hire this merc
if ( !( pProfile->ubMiscFlags3 & PROFILE_MISC_FLAG3_PLAYER_HAD_CHANCE_TO_HIRE) )
{
// then we're not allowed to kill him (to avoid really pissing off player by killing his very favorite merc)
return;
}
// how many in total can be killed like this depends on player's difficulty setting
switch( gGameOptions.ubDifficultyLevel )
{
case DIF_LEVEL_EASY: //Kaiden Externalized ubMaxDeaths Values
ubMaxDeaths = gGameExternalOptions.giEasyMercDeaths;
break;
case DIF_LEVEL_MEDIUM:
ubMaxDeaths = gGameExternalOptions.giExperiencedMercDeaths;
break;
case DIF_LEVEL_HARD:
ubMaxDeaths = gGameExternalOptions.giExpertMercDeaths;
break;
case DIF_LEVEL_INSANE:
ubMaxDeaths = gGameExternalOptions.giInsaneMercDeaths;
break;
default:
Assert(FALSE);
ubMaxDeaths = 0;
break;
}
// if we've already hit the limit in this game, skip these checks
if (gStrategicStatus.ubUnhiredMercDeaths >= ubMaxDeaths)
{
return;
}
// calculate this merc's (small) chance to get killed today (out of 1000)
sChance = 10 - pProfile->bExpLevel;
// SANDRO - certain traits makes us less likely to get killed
if ( gGameOptions.fNewTraitSystem )
{
if (pProfile->bDisability != NO_DISABILITY)
{
sChance += 1;
}
if ( ProfileHasSkillTrait( iProfileID, SQUADLEADER_NT ) > 0 )
sChance -= ProfileHasSkillTrait( iProfileID, SQUADLEADER_NT );
if ( ProfileHasSkillTrait( iProfileID, TECHNICIAN_NT ) > 0 )
sChance -= ProfileHasSkillTrait( iProfileID, TECHNICIAN_NT );
if ( ProfileHasSkillTrait( iProfileID, DOCTOR_NT ) > 0 )
sChance -= ProfileHasSkillTrait( iProfileID, DOCTOR_NT );
if ( ProfileHasSkillTrait( iProfileID, STEALTHY_NT ) > 0 )
sChance -= ProfileHasSkillTrait( iProfileID, STEALTHY_NT );
if ( ProfileHasSkillTrait( iProfileID, BODYBUILDING_NT ) > 0 )
sChance -= ProfileHasSkillTrait( iProfileID, BODYBUILDING_NT );
if ( ProfileHasSkillTrait( iProfileID, SCOUTING_NT ) > 0 )
sChance -= ProfileHasSkillTrait( iProfileID, SCOUTING_NT );
}
else
{
switch (pProfile->bDisability)
{
case FORGETFUL:
case NERVOUS:
case PSYCHO:
// these guys are somewhat more likely to get killed (they have "problems")
sChance += 2;
break;
}
// stealthy guys are slightly less likely to get killed (they're careful)
if ( ProfileHasSkillTrait( iProfileID, STEALTHY_OT ) > 0 )
sChance -= ProfileHasSkillTrait( iProfileID, STEALTHY_OT );
}
if ((INT16) PreRandom(1000) < sChance)
{
// this merc gets Killed In Action!!!
pProfile->bMercStatus = MERC_IS_DEAD;
pProfile->uiDayBecomesAvailable = 0;
// keep count of how many there have been
gStrategicStatus.ubUnhiredMercDeaths++;
//send an email as long as the merc is from aim
#ifdef JA2UB
//ja25 ub
if( gubQuest[ QUEST_FIX_LAPTOP ] == QUESTDONE || gGameUBOptions.LaptopQuestEnabled == FALSE )
{
if ( gProfilesAIM[ iProfileID ].ProfilId == iProfileID && gGameUBOptions.fDeadMerc == TRUE ) //new profiles by Jazz
//send an email to the player telling the player that a merc died
AddEmailWithSpecialData(206, MERC_DIED_ON_OTHER_ASSIGNMENT_LENGTH, AIM_SITE, GetWorldTotalMin(), 0, iProfileID, TYPE_EMAIL_DEAD_MERC_AIM_SITE_EMAIL_JA2_EDT, TYPE_E_AIM_L1 );
}
#else
// if( iProfileID < BIFF )
if ( gProfilesAIM[ iProfileID ].ProfilId == iProfileID ) //new profiles by Jazz
{
//send an email to the player telling the player that a merc died
AddEmailWithSpecialData(MERC_DIED_ON_OTHER_ASSIGNMENT, MERC_DIED_ON_OTHER_ASSIGNMENT_LENGTH, AIM_SITE, GetWorldTotalMin(), 0, iProfileID, TYPE_EMAIL_EMAIL_EDT, TYPE_E_NONE );
}
#endif
}
}
// These HAVE to total 100% at all times!!!
//#define PROGRESS_PORTION_KILLS 25
//#define PROGRESS_PORTION_CONTROL 25
//#define PROGRESS_PORTION_INCOME 50
//Lalien: Removed due to externalization
// returns a number between 0-100, this is an estimate of how far a player has progressed through the game
UINT8 CurrentPlayerProgressPercentage(void)
{
#ifdef JA2UB
INT8 bFurthestSectorPlayerOwns=-1; //JA25 UB
UINT8 ubCurrentProgress;
#else
UINT32 uiCurrentIncome;
UINT32 uiPossibleIncome;
UINT16 usCurrentProgress;
UINT16 ubKillsPerPoint;
UINT16 usKillsProgress;
UINT16 usControlProgress;
UINT16 usVisitProgress;
// HEADROCK HAM 3: Added a separate variable for Income Progress,
// to enable comparing the results from each progress aspect SEPARATELY.
UINT16 usIncomeProgress;
// HEADROCK HAM 3: And another variable to contain the highest result so far.
UINT16 usHighestProgress;
// HEADROCK HAM 3: Four variables to hold the maximum attainable progress from each aspect.
UINT16 usMaxKillsProgress;
UINT16 usMaxIncomeProgress;
UINT16 usMaxControlProgress;
UINT16 usMaxVisitProgress;
#endif
#ifdef JA2UB
//Get the furthest sector the player owns
bFurthestSectorPlayerOwns = GetTheFurthestSectorPlayerOwns();
//JA25 UB
switch( bFurthestSectorPlayerOwns )
{
//initial sector
case SEC_H7:
ubCurrentProgress = 44;
break;
case SEC_H8:
ubCurrentProgress = 45;
break;
//guard post
case SEC_H9:
ubCurrentProgress = 55;
break;
//field
case SEC_H10:
ubCurrentProgress = 58;
break;
//field
case SEC_I9:
ubCurrentProgress = 60;
break;
//first part of town
case SEC_I10:
ubCurrentProgress = 63;
break;
//second part of town
case SEC_I11:
ubCurrentProgress = 65;
break;
//field
case SEC_I12:
ubCurrentProgress = 68;
break;
//Abondoned mine
case SEC_I13:
ubCurrentProgress = 70;
break;
// cave under abondoned mine
/* case SEC_I13_1:
ubCurrentProgress = 72;
break;
*/
//field
case SEC_J11:
ubCurrentProgress = 70;
break;
//field
case SEC_J12:
ubCurrentProgress = 70;
break;
//power gen plant
case SEC_J13:
ubCurrentProgress = 75;
break;
/*
//power gen plant, sub level
case JA25_J13_1:
ubCurrentProgress = 75;
break;
//first part of tunnel
case JA25_J14_1:
ubCurrentProgress = 80;
break;
//second part of tunnel
case JA25_K14_1:
ubCurrentProgress = 82;
break;
//ground level of complex
case JA25_K15:
ubCurrentProgress = 90;
break;
//initial sector of complex
case JA25_K15_1:
ubCurrentProgress = 85;
break;
// 2nd level down of complex
case JA25_K15_2:
ubCurrentProgress = 95;
break;
//2nd last sector
case JA25_L15_2:
ubCurrentProgress = 98;
break;
//last sector
case JA25_L15_3:
ubCurrentProgress = 100;
break;
*/
default:
// OK, use percentage complete from map...
//Assert( 0 );
//ubCurrentProgress = SectorInfo[ bFurthestSectorPlayerOwns ].ubCurrentProgressValue;
ubCurrentProgress = 50;
break;
}
return(ubCurrentProgress);
#else
if( gfEditMode )
return 0;
// HEADROCK HAM 3: If the alternate progress calculation is used, all four INI settings should be set to 100,
// otherwise progress cannot ever reach 100...
if (gGameExternalOptions.fAlternateProgressCalculation)
{
usMaxKillsProgress = 100;
usMaxIncomeProgress = 100;
usMaxControlProgress = 100;
usMaxVisitProgress = 100;
}
// Else, set to INI-read values. Note that the rest of the function now reads these instead of referring
// to the long variable names.
else
{
usMaxKillsProgress = gGameExternalOptions.ubGameProgressPortionKills;
usMaxIncomeProgress = gGameExternalOptions.ubGameProgressPortionIncome;
usMaxControlProgress = gGameExternalOptions.ubGameProgressPortionControl;
usMaxVisitProgress = gGameExternalOptions.ubGameProgressPortionVisited;
}
// figure out the player's current mine income
uiCurrentIncome = PredictIncomeFromPlayerMines(FALSE);
// figure out the player's potential mine income
uiPossibleIncome = CalcMaxPlayerIncomeFromMines();
// either of these indicates a critical failure of some sort
// HEADROCK HAM 3.6: No need to assert this. Max Income can potentially be 0 in modded games.
// Assert(uiPossibleIncome > 0);
// HEADROCK HAM 3.6: It is now possible, with the help of facilities,
// to make more money from mines than normally possible.
// This assertion check is now obsolete.
//Assert(uiCurrentIncome <= uiPossibleIncome);
uiCurrentIncome = __min(uiPossibleIncome, uiCurrentIncome);
// for a rough guess as to how well the player is doing,
// we'll take the current mine income / potential mine income as a percentage
/////////////////////////////////////////////////////////////////////////////////////////////
//
// HEADROCK HAM 3
// Several changes have been made here to accomodate a new type of progress generation.
// With this alternate system, the program determines the current progress from each of the
// four aspects SEPARATELY. It then compares all of them, and only the HIGHEST one determines
// our current progress. Please note that while some changes have been made, the ORIGINAL
// progress control works EXACTLY THE SAME AS IT ALWAYS DID.
//
/////////////////////////////////////////////////////////////////////////////////////////////
//Kris: Make sure you don't divide by zero!!!
if( uiPossibleIncome > 0)
{
usIncomeProgress = (UINT8) ((uiCurrentIncome * usMaxIncomeProgress) / uiPossibleIncome);
}
else
{
usIncomeProgress = 0;
}
// kills per point depends on difficulty, and should match the ratios of starting enemy populations (730/1050/1500)
// HEADROCK HAM 3: Externalized all four Kills-per-point ratios.
switch( gGameOptions.ubDifficultyLevel )
{
case DIF_LEVEL_EASY:
ubKillsPerPoint = gGameExternalOptions.usNumKillsPerProgressPointNovice;
break;
case DIF_LEVEL_MEDIUM:
ubKillsPerPoint = gGameExternalOptions.usNumKillsPerProgressPointExperienced;
break;
case DIF_LEVEL_HARD:
ubKillsPerPoint = gGameExternalOptions.usNumKillsPerProgressPointExpert;
break;
case DIF_LEVEL_INSANE:
ubKillsPerPoint = gGameExternalOptions.usNumKillsPerProgressPointInsane;
break;
default:
Assert(FALSE);
ubKillsPerPoint = 10;
break;
}
usKillsProgress = gStrategicStatus.usPlayerKills / ubKillsPerPoint;
if (usKillsProgress > usMaxKillsProgress)
{
usKillsProgress = usMaxKillsProgress;
}
// 19 sectors in mining towns + 3 wilderness SAMs each count double. Balime & Meduna are extra and not required
// HEADROCK HAM B1: Changed the next line, adding a call to a new function. This allows the weight of Sector
// Control to be altered in JA2_OPTIONS.INI (Previously damaged the game's progress if set over 25... So I've
// made this MOD-Friendly :D )
// BTW, Balime and Meduna _ARE_ required. The function doesn't differentiate! Such carelessness. Tsk tsk tsk.
usControlProgress = usMaxControlProgress * CalcImportantSectorControl() / CalcTotalImportantSectors();
if (usControlProgress > usMaxControlProgress)
{
usControlProgress = usMaxControlProgress;
}
// WDS: Adding more ways to progress in the game
// Get a ratio of sectors visited to the total number of sectors
// HEADROCK HAM B1: Fixed this so it doesn't count sectors that can't be visited. Allows progress to go to
// 100 even if the map has some unvisitable sectors (heh, doesn't it always?)
usVisitProgress = CountSurfaceSectorsVisited() * usMaxVisitProgress / TotalVisitableSurfaceSectors();
// HEADROCK HAM 3: This bit is ugly for now, unless someone can optimize it for me.
// When the "Alternate Progress Calculation" is activated, the program selects only the HIGHEST of the
// progress controls, and sets that to be the current progress, disregarding any advances in the three
// other fields.
if (gGameExternalOptions.fAlternateProgressCalculation)
{
usHighestProgress = __max(usKillsProgress, usControlProgress);
usHighestProgress = __max(usHighestProgress, usIncomeProgress);
usHighestProgress = __max(usHighestProgress, usVisitProgress);
usCurrentProgress = usHighestProgress;
}
// Else, add them all up as normal (original progress calculation). This replaces all those lines I removed
// above where progress was summed along the way.
else
{
usCurrentProgress = usKillsProgress + usControlProgress + usIncomeProgress + usVisitProgress;
}
// Add a static amount of points, as declared in the INI file.
// HEADROCK HAM 3.6: Allow negative modifiers!
usCurrentProgress = __max(0, (INT16)(usCurrentProgress + gGameExternalOptions.bGameProgressModifier));
// And failsafes here. I'm not 100% sure about these though: I've never personally seen progress
// values go over 100, and I don't think they SHOULD... Can the game handle values > 100? Should it?
usCurrentProgress = __min(100, usCurrentProgress);
usCurrentProgress = __max(0, usCurrentProgress);
// No less than 0, or the minimum set in the INI file.
UINT32 uiMinProgress = __max(gGameExternalOptions.ubGameProgressMinimum, 0);
usCurrentProgress = __max(uiMinProgress, usCurrentProgress);
return((UINT8)usCurrentProgress);
#endif
}
UINT8 HighestPlayerProgressPercentage(void)
{
if( gfEditMode )
return 0;
return(gStrategicStatus.ubHighestProgress);
}
// monitors the highest level of progress that player has achieved so far (checking hourly),
// as opposed to his immediate situation (which may be worse if he's suffered a setback).
void HourlyProgressUpdate(void)
{
UINT8 ubCurrentProgress;
ubCurrentProgress = CurrentPlayerProgressPercentage();
// if this is new high, remember it as that
if (ubCurrentProgress > gStrategicStatus.ubHighestProgress)
{
// CJC: note when progress goes above certain values for the first time
// at 35% start the Madlab quest
#ifdef JA2UB
// no UB
#else
if ( ubCurrentProgress >= gGameExternalOptions.ubGameProgressStartMadlabQuest && gStrategicStatus.ubHighestProgress < gGameExternalOptions.ubGameProgressStartMadlabQuest )
{
HandleScientistAWOLMeanwhileScene();
}
#endif
// at 50% make Mike available to the strategic AI
if ( ubCurrentProgress >= gGameExternalOptions.ubGameProgressMikeAvailable && gStrategicStatus.ubHighestProgress <= gGameExternalOptions.ubGameProgressMikeAvailable )
{
SetFactTrue( FACT_MIKE_AVAILABLE_TO_ARMY );
}
// at 70% add Iggy to the world
if ( ubCurrentProgress >= gGameExternalOptions.ubGameProgressIggyAvaliable && gStrategicStatus.ubHighestProgress <= gGameExternalOptions.ubGameProgressIggyAvaliable )
{
gMercProfiles[ IGGY ].sSectorX = 5;
gMercProfiles[ IGGY ].sSectorY = MAP_ROW_C;
}
gStrategicStatus.ubHighestProgress = ubCurrentProgress;
// debug message
ScreenMsg( MSG_FONT_RED, MSG_DEBUG, L"New player progress record: %d%%", gStrategicStatus.ubHighestProgress );
}
}
#ifdef JA2TESTVERSION
void TestDumpStatChanges(void)
{
UINT32 uiProfileId;
UINT8 ubStat;
CHAR8 zPrintFileName[60];
FILE *FDump;
MERCPROFILESTRUCT *pProfile;
BOOLEAN fMercUsed;
CHAR8 cEvolutionChars[3] = { '+', '=', '-' };
UINT32 uiTotalSuccesses[ 12 ];
UINT32 uiTotalChances[ 12 ];
// clear totals
memset( uiTotalSuccesses, 0, sizeof( uiTotalSuccesses ) );
memset( uiTotalChances, 0, sizeof( uiTotalChances ) );
// open output file
strcpy(zPrintFileName, "C:\\Temp\\StatChanges.TXT");
FDump = fopen(zPrintFileName, "wt");
if (FDump == NULL)
return;
// print headings
fprintf(FDump, " NAME SRV EVL ");
fprintf(FDump, "---HEALTH-- --AGILITY-- -DEXTERITY- ---WISDOM-- --MEDICAL-- --EXPLOSIV- --MECHANIC- --MARKSMAN- -EXP.LEVEL- --STRENGTH- -LEADERSHIP");
fprintf(FDump, "\n");
// loop through profiles
for (uiProfileId = 0; uiProfileId < NUM_PROFILES; uiProfileId++)
{
pProfile = &(gMercProfiles[uiProfileId]);
fMercUsed = FALSE;
// see if this guy should be printed at all (only mercs actually used are dumped)
for( ubStat = FIRST_CHANGEABLE_STAT; ubStat <= LAST_CHANGEABLE_STAT; ubStat++ )
{
if ( pProfile->usStatChangeChances[ ubStat ] > 0 )
{
fMercUsed = TRUE;
break;
}
}
if (fMercUsed)
{
// print nickname
fprintf(FDump, "%-10ls ", pProfile->zNickname);
// print days served
fprintf(FDump, "%3d ", pProfile->usTotalDaysServed);
// print evolution type
fprintf(FDump, "%c ", cEvolutionChars[ pProfile->bEvolution ]);
// now print all non-zero stats
for( ubStat = FIRST_CHANGEABLE_STAT; ubStat <= LAST_CHANGEABLE_STAT; ubStat++ )
{
if ( pProfile->usStatChangeChances[ ubStat ] > 0 )
{
// print successes/chances
fprintf(FDump, " %5d/%-5d", pProfile->usStatChangeSuccesses[ ubStat ], pProfile->usStatChangeChances[ ubStat ]);
}
else
{
//
fprintf(FDump, " ");
}
uiTotalSuccesses[ ubStat ] += pProfile->usStatChangeSuccesses[ ubStat ];
uiTotalChances[ ubStat ] += pProfile->usStatChangeChances[ ubStat ];
}
// newline
fprintf(FDump, "\n");
}
}
// print totals:
fprintf(FDump, "TOTAL ");
for( ubStat = FIRST_CHANGEABLE_STAT; ubStat <= LAST_CHANGEABLE_STAT; ubStat++ )
{
fprintf(FDump, " %5d/%-5d", uiTotalSuccesses[ ubStat ], uiTotalChances[ ubStat ]);
}
fprintf(FDump, "\n");
fclose(FDump);
}
#endif
void AwardExperienceBonusToActiveSquad( UINT8 ubExpBonusType )
{
UINT16 usXPs = 0;
UINT8 ubGuynum;
SOLDIERTYPE *pSoldier;
Assert ( ubExpBonusType < NUM_EXP_BONUS_TYPES );
switch ( ubExpBonusType )
{
case EXP_BONUS_MINIMUM: usXPs = 25; break;
case EXP_BONUS_SMALL: usXPs = 50; break;
case EXP_BONUS_AVERAGE: usXPs = 100; break;
case EXP_BONUS_LARGE: usXPs = 200; break;
case EXP_BONUS_MAXIMUM: usXPs = 400; break;
}
// to do: find guys in sector on the currently active squad, those that are conscious get this amount in XPs
for ( ubGuynum = gTacticalStatus.Team[ gbPlayerNum ].bFirstID, pSoldier = MercPtrs[ ubGuynum ];
ubGuynum <= gTacticalStatus.Team[ gbPlayerNum ].bLastID;
ubGuynum++, pSoldier++ )
{
if ( pSoldier->bActive && pSoldier->bInSector && IsMercOnCurrentSquad( pSoldier ) && ( pSoldier->stats.bLife >= CONSCIOUSNESS ) &&
!( pSoldier->flags.uiStatusFlags & SOLDIER_VEHICLE ) && !AM_A_ROBOT( pSoldier ) )
{
StatChange( pSoldier, EXPERAMT, usXPs, FALSE );
}
}
}
void BuildStatChangeString( STR16 wString, STR16 wName, BOOLEAN fIncrease, INT16 sPtsChanged, UINT8 ubStat )
{
UINT8 ubStringIndex;
Assert( sPtsChanged != 0 );
Assert( ubStat >= FIRST_CHANGEABLE_STAT );
Assert( ubStat <= LAST_CHANGEABLE_STAT );
// if just a 1 point change
if ( abs( sPtsChanged ) == 1 )
{
// use singular
ubStringIndex = 2;
}
else
{
ubStringIndex = 3;
// use plural
}
if ( ubStat == EXPERAMT )
{
// use "level/levels instead of point/points
ubStringIndex += 2;
}
swprintf( wString, L"%s %s %d %s %s", wName, sPreStatBuildString[ fIncrease ? 1 : 0 ], abs( sPtsChanged ),
sPreStatBuildString[ ubStringIndex ], sStatGainStrings[ ubStat - FIRST_CHANGEABLE_STAT ] );
}
UINT8 CalcImportantSectorControl( void )
{
UINT8 ubMapX, ubMapY;
UINT8 ubSectorControlPts = 0;
for ( ubMapX = 1; ubMapX < MAP_WORLD_X - 1; ubMapX++ )
{
for ( ubMapY = 1; ubMapY < MAP_WORLD_Y - 1; ubMapY++ )
{
// if player controlled
if ( StrategicMap[ CALCULATE_STRATEGIC_INDEX( ubMapX, ubMapY ) ].fEnemyControlled == FALSE )
{
// towns where militia can be trained and SAM sites are important sectors
if ( MilitiaTrainingAllowedInSector( ubMapX, ubMapY, 0 ) )
{
ubSectorControlPts++;
// SAM sites count double - they have no income, but have significant air control value
if ( IsThisSectorASAMSector( ubMapX, ubMapY, 0 ) )
{
ubSectorControlPts++;
}
}
}
}
}
return( ubSectorControlPts );
}
// HEADROCK HAM B1: function to calculate how many "important sectors" there actually are in the game. This is
// used to fix the weight of strategic control importance on game progress, which was until now not really
// moddable.
UINT8 CalcTotalImportantSectors( void )
{
UINT8 ubMapX, ubMapY;
UINT8 ubSectorControlPts = 0;
for ( ubMapX = 1; ubMapX < MAP_WORLD_X - 1; ubMapX++ )
{
for ( ubMapY = 1; ubMapY < MAP_WORLD_Y - 1; ubMapY++ )
{
// towns where militia can be trained and SAM sites are important sectors
if ( MilitiaTrainingAllowedInSector( ubMapX, ubMapY, 0 ) )
{
ubSectorControlPts++;
// SAM sites count double - they have no income, but have significant air control value
if ( IsThisSectorASAMSector( ubMapX, ubMapY, 0 ) )
{
ubSectorControlPts++;
}
}
}
}
return( ubSectorControlPts );
}
// WDS: Adding more ways to progress in the game
// Count how many surface sectors the player has visited
UINT16 CountSurfaceSectorsVisited( void )
{
UINT8 ubMapX, ubMapY;
UINT16 ubSectorsVisited = 0;
for ( ubMapX = 1; ubMapX < MAP_WORLD_X - 1; ubMapX++ )
{
for ( ubMapY = 1; ubMapY < MAP_WORLD_Y - 1; ubMapY++ )
{
if( GetSectorFlagStatus( ubMapX, ubMapY, 0, SF_ALREADY_VISITED ) == TRUE )
++ubSectorsVisited;
}
}
return( ubSectorsVisited );
}
// HEADROCK: Count number of VISITABLE sectors
UINT16 TotalVisitableSurfaceSectors( void )
{
UINT8 ubMapX, ubMapY;
UINT16 ubVisitableSectors = 0;
for ( ubMapX = 1; ubMapX < MAP_WORLD_X - 1; ubMapX++ )
{
for ( ubMapY = 1; ubMapY < MAP_WORLD_Y - 1; ubMapY++ )
{
if( SectorInfo[ SECTOR(ubMapX,ubMapY) ].ubTraversability[ THROUGH_STRATEGIC_MOVE ] != GROUNDBARRIER && SectorInfo[ SECTOR(ubMapX,ubMapY) ].ubTraversability[ THROUGH_STRATEGIC_MOVE ] != EDGEOFWORLD )
++ubVisitableSectors;
}
}
return( ubVisitableSectors );
}
void MERCMercWentUpALevelSendEmail( UINT8 ubMercMercIdValue )
{
#ifdef JA2UB
//JA25 UB
#else
UINT8 ubEmailOffset = 0;
int iMsgLength = 0;
UINT8 pMerc = 0;
UINT8 iMerc = 0;
UINT8 oMerc = 0;
// Read from EmailMercAvailable.xml
if ( ReadXMLEmail == TRUE )
{
oMerc = ubMercMercIdValue;
iMerc = oMerc * 1;
if ( oMerc != 0 )
pMerc = oMerc + 1;
else
pMerc = 0;
if ( gProfilesMERC[ubMercMercIdValue].ProfilId == ubMercMercIdValue )
AddEmailTypeXML( pMerc, iMerc, iMerc, GetWorldTotalMin(), -1 , TYPE_EMAIL_MERC_LEVEL_UP);
}
else
{
// Read from Email.edt and sender (nickname) from MercProfiles.xml
// WANNE: TODO: Tex, Biggins, Stoggy and Gaston have special handling because they are the new MERC merc in 1.13
// There is no letter template in Email.edt. We have them hardcoded in the source code.
if (ubMercMercIdValue == 124 || ubMercMercIdValue == 125 || ubMercMercIdValue == 126 || ubMercMercIdValue == 127)
{
// Gaston
if (ubMercMercIdValue == 124)
{
ubEmailOffset = MERC_UP_LEVEL_BIFF;
iMsgLength = MERC_UP_LEVEL_GASTON;
}
// Stogie
else if (ubMercMercIdValue == 125)
{
ubEmailOffset = MERC_UP_LEVEL_BIFF;
iMsgLength = MERC_UP_LEVEL_STOGIE;
}
// Tex
else if (ubMercMercIdValue == 126)
{
ubEmailOffset = MERC_UP_LEVEL_BIFF;
iMsgLength = MERC_UP_LEVEL_TEX;
}
// Biggens
else if (ubMercMercIdValue == 127)
{
ubEmailOffset = MERC_UP_LEVEL_BIFF;
iMsgLength = MERC_UP_LEVEL_BIGGENS;
}
}
else
{
iMsgLength = MERC_UP_LEVEL_LENGTH_BIFF;
ubEmailOffset = MERC_UP_LEVEL_BIFF + MERC_UP_LEVEL_LENGTH_BIFF * ( ubMercMercIdValue );
}
AddEmail( ubEmailOffset, iMsgLength, SPECK_FROM_MERC, GetWorldTotalMin(), -1, -1, TYPE_EMAIL_EMAIL_EDT_NAME_MERC);
}
#endif
}