2022-05-19 01:51:26 -07:00
|
|
|
#include "stat.h"
|
|
|
|
|
|
|
|
#include "combat.h"
|
|
|
|
#include "core.h"
|
|
|
|
#include "critter.h"
|
|
|
|
#include "display_monitor.h"
|
|
|
|
#include "game.h"
|
|
|
|
#include "game_sound.h"
|
|
|
|
#include "interface.h"
|
|
|
|
#include "item.h"
|
|
|
|
#include "memory.h"
|
|
|
|
#include "object.h"
|
|
|
|
#include "perk.h"
|
2022-05-28 02:34:49 -07:00
|
|
|
#include "platform_compat.h"
|
2022-05-19 01:51:26 -07:00
|
|
|
#include "proto.h"
|
|
|
|
#include "random.h"
|
|
|
|
#include "scripts.h"
|
|
|
|
#include "skill.h"
|
|
|
|
#include "tile.h"
|
|
|
|
#include "trait.h"
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
2022-05-28 04:45:48 -07:00
|
|
|
#include <algorithm>
|
|
|
|
|
2022-05-19 01:51:26 -07:00
|
|
|
// 0x51D53C
|
|
|
|
StatDescription gStatDescriptions[STAT_COUNT] = {
|
|
|
|
{ NULL, NULL, 0, PRIMARY_STAT_MIN, PRIMARY_STAT_MAX, 5 },
|
|
|
|
{ NULL, NULL, 1, PRIMARY_STAT_MIN, PRIMARY_STAT_MAX, 5 },
|
|
|
|
{ NULL, NULL, 2, PRIMARY_STAT_MIN, PRIMARY_STAT_MAX, 5 },
|
|
|
|
{ NULL, NULL, 3, PRIMARY_STAT_MIN, PRIMARY_STAT_MAX, 5 },
|
|
|
|
{ NULL, NULL, 4, PRIMARY_STAT_MIN, PRIMARY_STAT_MAX, 5 },
|
|
|
|
{ NULL, NULL, 5, PRIMARY_STAT_MIN, PRIMARY_STAT_MAX, 5 },
|
|
|
|
{ NULL, NULL, 6, PRIMARY_STAT_MIN, PRIMARY_STAT_MAX, 5 },
|
|
|
|
{ NULL, NULL, 10, 0, 999, 0 },
|
|
|
|
{ NULL, NULL, 75, 1, 99, 0 },
|
|
|
|
{ NULL, NULL, 18, 0, 999, 0 },
|
|
|
|
{ NULL, NULL, 31, 0, INT_MAX, 0 },
|
|
|
|
{ NULL, NULL, 32, 0, 500, 0 },
|
|
|
|
{ NULL, NULL, 20, 0, 999, 0 },
|
|
|
|
{ NULL, NULL, 24, 0, 60, 0 },
|
|
|
|
{ NULL, NULL, 25, 0, 30, 0 },
|
|
|
|
{ NULL, NULL, 26, 0, 100, 0 },
|
|
|
|
{ NULL, NULL, 94, -60, 100, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 100, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 100, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 100, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 100, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 100, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 100, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 100, 0 },
|
|
|
|
{ NULL, NULL, 22, 0, 90, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 90, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 90, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 90, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 90, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 100, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, 90, 0 },
|
|
|
|
{ NULL, NULL, 83, 0, 95, 0 },
|
|
|
|
{ NULL, NULL, 23, 0, 95, 0 },
|
|
|
|
{ NULL, NULL, 0, 16, 101, 25 },
|
|
|
|
{ NULL, NULL, 0, 0, 1, 0 },
|
|
|
|
{ NULL, NULL, 10, 0, 2000, 0 },
|
|
|
|
{ NULL, NULL, 11, 0, 2000, 0 },
|
|
|
|
{ NULL, NULL, 12, 0, 2000, 0 },
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x51D8CC
|
|
|
|
StatDescription gPcStatDescriptions[PC_STAT_COUNT] = {
|
|
|
|
{ NULL, NULL, 0, 0, INT_MAX, 0 },
|
|
|
|
{ NULL, NULL, 0, 1, PC_LEVEL_MAX, 1 },
|
|
|
|
{ NULL, NULL, 0, 0, INT_MAX, 0 },
|
|
|
|
{ NULL, NULL, 0, -20, 20, 0 },
|
|
|
|
{ NULL, NULL, 0, 0, INT_MAX, 0 },
|
|
|
|
};
|
|
|
|
|
|
|
|
// 0x66817C
|
|
|
|
MessageList gStatsMessageList;
|
|
|
|
|
|
|
|
// 0x668184
|
|
|
|
char* gStatValueDescriptions[PRIMARY_STAT_RANGE];
|
|
|
|
|
|
|
|
// 0x6681AC
|
|
|
|
int gPcStatValues[PC_STAT_COUNT];
|
|
|
|
|
|
|
|
// 0x4AED70
|
|
|
|
int statsInit()
|
|
|
|
{
|
|
|
|
MessageListItem messageListItem;
|
|
|
|
|
|
|
|
// NOTE: Uninline.
|
|
|
|
pcStatsReset();
|
|
|
|
|
|
|
|
if (!messageListInit(&gStatsMessageList)) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
2022-05-28 02:34:49 -07:00
|
|
|
char path[COMPAT_MAX_PATH];
|
2022-05-19 01:51:26 -07:00
|
|
|
sprintf(path, "%s%s", asc_5186C8, "stat.msg");
|
|
|
|
|
|
|
|
if (!messageListLoad(&gStatsMessageList, path)) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int stat = 0; stat < STAT_COUNT; stat++) {
|
|
|
|
gStatDescriptions[stat].name = getmsg(&gStatsMessageList, &messageListItem, 100 + stat);
|
|
|
|
gStatDescriptions[stat].description = getmsg(&gStatsMessageList, &messageListItem, 200 + stat);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int pcStat = 0; pcStat < PC_STAT_COUNT; pcStat++) {
|
|
|
|
gPcStatDescriptions[pcStat].name = getmsg(&gStatsMessageList, &messageListItem, 400 + pcStat);
|
|
|
|
gPcStatDescriptions[pcStat].description = getmsg(&gStatsMessageList, &messageListItem, 500 + pcStat);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int index = 0; index < PRIMARY_STAT_RANGE; index++) {
|
|
|
|
gStatValueDescriptions[index] = getmsg(&gStatsMessageList, &messageListItem, 301 + index);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AEEC0
|
|
|
|
int statsReset()
|
|
|
|
{
|
|
|
|
// NOTE: Uninline.
|
|
|
|
pcStatsReset();
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AEEE4
|
|
|
|
int statsExit()
|
|
|
|
{
|
|
|
|
messageListFree(&gStatsMessageList);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AEEF4
|
|
|
|
int statsLoad(File* stream)
|
|
|
|
{
|
|
|
|
for (int index = 0; index < PC_STAT_COUNT; index++) {
|
|
|
|
if (fileReadInt32(stream, &(gPcStatValues[index])) == -1) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AEF20
|
|
|
|
int statsSave(File* stream)
|
|
|
|
{
|
|
|
|
for (int index = 0; index < PC_STAT_COUNT; index++) {
|
|
|
|
if (fileWriteInt32(stream, gPcStatValues[index]) == -1) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AEF48
|
|
|
|
int critterGetStat(Object* critter, int stat)
|
|
|
|
{
|
|
|
|
int value;
|
|
|
|
if (stat >= 0 && stat < SAVEABLE_STAT_COUNT) {
|
|
|
|
value = critterGetBaseStatWithTraitModifier(critter, stat);
|
|
|
|
value += critterGetBonusStat(critter, stat);
|
|
|
|
|
|
|
|
switch (stat) {
|
|
|
|
case STAT_PERCEPTION:
|
|
|
|
if ((critter->data.critter.combat.results & DAM_BLIND) != 0) {
|
|
|
|
value -= 5;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_MAXIMUM_ACTION_POINTS:
|
|
|
|
if (1) {
|
|
|
|
int remainingCarryWeight = critterGetStat(critter, STAT_CARRY_WEIGHT) - objectGetInventoryWeight(critter);
|
|
|
|
if (remainingCarryWeight < 0) {
|
|
|
|
value -= -remainingCarryWeight / 40 + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_ARMOR_CLASS:
|
|
|
|
if (isInCombat()) {
|
|
|
|
if (_combat_whose_turn() != critter) {
|
|
|
|
int actionPointsMultiplier = 1;
|
|
|
|
int hthEvadeBonus = 0;
|
|
|
|
|
|
|
|
if (critter == gDude) {
|
|
|
|
if (perkHasRank(gDude, PERK_HTH_EVADE)) {
|
|
|
|
bool hasWeapon = false;
|
|
|
|
|
|
|
|
Object* item2 = critterGetItem2(gDude);
|
|
|
|
if (item2 != NULL) {
|
|
|
|
if (itemGetType(item2) == ITEM_TYPE_WEAPON) {
|
|
|
|
if (weaponGetAnimationCode(item2) != WEAPON_ANIMATION_NONE) {
|
|
|
|
hasWeapon = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!hasWeapon) {
|
|
|
|
Object* item1 = critterGetItem1(gDude);
|
|
|
|
if (item1 != NULL) {
|
|
|
|
if (itemGetType(item1) == ITEM_TYPE_WEAPON) {
|
|
|
|
if (weaponGetAnimationCode(item1) != WEAPON_ANIMATION_NONE) {
|
|
|
|
hasWeapon = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!hasWeapon) {
|
|
|
|
actionPointsMultiplier = 2;
|
|
|
|
hthEvadeBonus = skillGetValue(gDude, SKILL_UNARMED) / 12;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
value += hthEvadeBonus;
|
|
|
|
value += critter->data.critter.combat.ap * actionPointsMultiplier;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_AGE:
|
|
|
|
value += gameTimeGetTime() / GAME_TIME_TICKS_PER_YEAR;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (critter == gDude) {
|
|
|
|
switch (stat) {
|
|
|
|
case STAT_STRENGTH:
|
|
|
|
if (perkGetRank(critter, PERK_GAIN_STRENGTH)) {
|
|
|
|
value++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (perkGetRank(critter, PERK_ADRENALINE_RUSH)) {
|
|
|
|
if (critterGetStat(critter, STAT_CURRENT_HIT_POINTS) < (critterGetStat(critter, STAT_MAXIMUM_HIT_POINTS) / 2)) {
|
|
|
|
value++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_PERCEPTION:
|
|
|
|
if (perkGetRank(critter, PERK_GAIN_PERCEPTION)) {
|
|
|
|
value++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_ENDURANCE:
|
|
|
|
if (perkGetRank(critter, PERK_GAIN_ENDURANCE)) {
|
|
|
|
value++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_CHARISMA:
|
|
|
|
if (1) {
|
|
|
|
if (perkGetRank(critter, PERK_GAIN_CHARISMA)) {
|
|
|
|
value++;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool hasMirrorShades = false;
|
|
|
|
|
|
|
|
Object* item2 = critterGetItem2(critter);
|
|
|
|
if (item2 != NULL && item2->pid == PROTO_ID_MIRRORED_SHADES) {
|
|
|
|
hasMirrorShades = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
Object* item1 = critterGetItem1(critter);
|
|
|
|
if (item1 != NULL && item1->pid == PROTO_ID_MIRRORED_SHADES) {
|
|
|
|
hasMirrorShades = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hasMirrorShades) {
|
|
|
|
value++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_INTELLIGENCE:
|
|
|
|
if (perkGetRank(critter, PERK_GAIN_INTELLIGENCE)) {
|
|
|
|
value++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_AGILITY:
|
|
|
|
if (perkGetRank(critter, PERK_GAIN_AGILITY)) {
|
|
|
|
value++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_LUCK:
|
|
|
|
if (perkGetRank(critter, PERK_GAIN_LUCK)) {
|
|
|
|
value++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_MAXIMUM_HIT_POINTS:
|
|
|
|
if (perkGetRank(critter, PERK_ALCOHOL_RAISED_HIT_POINTS)) {
|
|
|
|
value += 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (perkGetRank(critter, PERK_ALCOHOL_RAISED_HIT_POINTS_II)) {
|
|
|
|
value += 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (perkGetRank(critter, PERK_ALCOHOL_LOWERED_HIT_POINTS)) {
|
|
|
|
value -= 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (perkGetRank(critter, PERK_ALCOHOL_LOWERED_HIT_POINTS_II)) {
|
|
|
|
value -= 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (perkGetRank(critter, PERK_AUTODOC_RAISED_HIT_POINTS)) {
|
|
|
|
value += 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (perkGetRank(critter, PERK_AUTODOC_RAISED_HIT_POINTS_II)) {
|
|
|
|
value += 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (perkGetRank(critter, PERK_AUTODOC_LOWERED_HIT_POINTS)) {
|
|
|
|
value -= 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (perkGetRank(critter, PERK_AUTODOC_LOWERED_HIT_POINTS_II)) {
|
|
|
|
value -= 4;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_DAMAGE_RESISTANCE:
|
2022-06-04 15:01:49 -07:00
|
|
|
case STAT_DAMAGE_RESISTANCE_EXPLOSION:
|
2022-05-19 01:51:26 -07:00
|
|
|
if (perkGetRank(critter, PERK_DERMAL_IMPACT_ARMOR)) {
|
|
|
|
value += 5;
|
|
|
|
} else if (perkGetRank(critter, PERK_DERMAL_IMPACT_ASSAULT_ENHANCEMENT)) {
|
|
|
|
value += 10;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_DAMAGE_RESISTANCE_LASER:
|
|
|
|
case STAT_DAMAGE_RESISTANCE_FIRE:
|
|
|
|
case STAT_DAMAGE_RESISTANCE_PLASMA:
|
|
|
|
if (perkGetRank(critter, PERK_PHOENIX_ARMOR_IMPLANTS)) {
|
|
|
|
value += 5;
|
|
|
|
} else if (perkGetRank(critter, PERK_PHOENIX_ASSAULT_ENHANCEMENT)) {
|
|
|
|
value += 10;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case STAT_RADIATION_RESISTANCE:
|
|
|
|
case STAT_POISON_RESISTANCE:
|
|
|
|
if (perkGetRank(critter, PERK_VAULT_CITY_INOCULATIONS)) {
|
|
|
|
value += 10;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-28 04:45:48 -07:00
|
|
|
value = std::clamp(value, gStatDescriptions[stat].minimumValue, gStatDescriptions[stat].maximumValue);
|
2022-05-19 01:51:26 -07:00
|
|
|
} else {
|
|
|
|
switch (stat) {
|
|
|
|
case STAT_CURRENT_HIT_POINTS:
|
|
|
|
value = critterGetHitPoints(critter);
|
|
|
|
break;
|
|
|
|
case STAT_CURRENT_POISON_LEVEL:
|
|
|
|
value = critterGetPoison(critter);
|
|
|
|
break;
|
|
|
|
case STAT_CURRENT_RADIATION_LEVEL:
|
|
|
|
value = critterGetRadiation(critter);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
value = 0;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns base stat value (accounting for traits if critter is dude).
|
|
|
|
//
|
|
|
|
// 0x4AF3E0
|
|
|
|
int critterGetBaseStatWithTraitModifier(Object* critter, int stat)
|
|
|
|
{
|
|
|
|
int value = critterGetBaseStat(critter, stat);
|
|
|
|
|
|
|
|
if (critter == gDude) {
|
|
|
|
value += traitGetStatModifier(stat);
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF408
|
|
|
|
int critterGetBaseStat(Object* critter, int stat)
|
|
|
|
{
|
|
|
|
Proto* proto;
|
|
|
|
|
|
|
|
if (stat >= 0 && stat < SAVEABLE_STAT_COUNT) {
|
|
|
|
protoGetProto(critter->pid, &proto);
|
|
|
|
return proto->critter.data.baseStats[stat];
|
|
|
|
} else {
|
|
|
|
switch (stat) {
|
|
|
|
case STAT_CURRENT_HIT_POINTS:
|
|
|
|
return critterGetHitPoints(critter);
|
|
|
|
case STAT_CURRENT_POISON_LEVEL:
|
|
|
|
return critterGetPoison(critter);
|
|
|
|
case STAT_CURRENT_RADIATION_LEVEL:
|
|
|
|
return critterGetRadiation(critter);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF474
|
|
|
|
int critterGetBonusStat(Object* critter, int stat)
|
|
|
|
{
|
|
|
|
if (stat >= 0 && stat < SAVEABLE_STAT_COUNT) {
|
|
|
|
Proto* proto;
|
|
|
|
protoGetProto(critter->pid, &proto);
|
|
|
|
return proto->critter.data.bonusStats[stat];
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF4BC
|
|
|
|
int critterSetBaseStat(Object* critter, int stat, int value)
|
|
|
|
{
|
|
|
|
Proto* proto;
|
|
|
|
|
|
|
|
if (!statIsValid(stat)) {
|
|
|
|
return -5;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stat >= 0 && stat < SAVEABLE_STAT_COUNT) {
|
|
|
|
if (stat > STAT_LUCK && stat <= STAT_POISON_RESISTANCE) {
|
|
|
|
// Cannot change base value of derived stats.
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (critter == gDude) {
|
|
|
|
value -= traitGetStatModifier(stat);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value < gStatDescriptions[stat].minimumValue) {
|
|
|
|
return -2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value > gStatDescriptions[stat].maximumValue) {
|
|
|
|
return -3;
|
|
|
|
}
|
|
|
|
|
|
|
|
protoGetProto(critter->pid, &proto);
|
|
|
|
proto->critter.data.baseStats[stat] = value;
|
|
|
|
|
|
|
|
if (stat >= STAT_STRENGTH && stat <= STAT_LUCK) {
|
|
|
|
critterUpdateDerivedStats(critter);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (stat) {
|
|
|
|
case STAT_CURRENT_HIT_POINTS:
|
|
|
|
return critterAdjustHitPoints(critter, value - critterGetHitPoints(critter));
|
|
|
|
case STAT_CURRENT_POISON_LEVEL:
|
|
|
|
return critterAdjustPoison(critter, value - critterGetPoison(critter));
|
|
|
|
case STAT_CURRENT_RADIATION_LEVEL:
|
|
|
|
return critterAdjustRadiation(critter, value - critterGetRadiation(critter));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Should be unreachable
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF5D4
|
|
|
|
int critterIncBaseStat(Object* critter, int stat)
|
|
|
|
{
|
|
|
|
int value = critterGetBaseStat(critter, stat);
|
|
|
|
|
|
|
|
if (critter == gDude) {
|
|
|
|
value += traitGetStatModifier(stat);
|
|
|
|
}
|
|
|
|
|
|
|
|
return critterSetBaseStat(critter, stat, value + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF608
|
|
|
|
int critterDecBaseStat(Object* critter, int stat)
|
|
|
|
{
|
|
|
|
int value = critterGetBaseStat(critter, stat);
|
|
|
|
|
|
|
|
if (critter == gDude) {
|
|
|
|
value += traitGetStatModifier(stat);
|
|
|
|
}
|
|
|
|
|
|
|
|
return critterSetBaseStat(critter, stat, value - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF63C
|
|
|
|
int critterSetBonusStat(Object* critter, int stat, int value)
|
|
|
|
{
|
|
|
|
if (!statIsValid(stat)) {
|
|
|
|
return -5;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stat >= 0 && stat < SAVEABLE_STAT_COUNT) {
|
|
|
|
Proto* proto;
|
|
|
|
protoGetProto(critter->pid, &proto);
|
|
|
|
proto->critter.data.bonusStats[stat] = value;
|
|
|
|
|
|
|
|
if (stat >= STAT_STRENGTH && stat <= STAT_LUCK) {
|
|
|
|
critterUpdateDerivedStats(critter);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
} else {
|
|
|
|
switch (stat) {
|
|
|
|
case STAT_CURRENT_HIT_POINTS:
|
|
|
|
return critterAdjustHitPoints(critter, value);
|
|
|
|
case STAT_CURRENT_POISON_LEVEL:
|
|
|
|
return critterAdjustPoison(critter, value);
|
|
|
|
case STAT_CURRENT_RADIATION_LEVEL:
|
|
|
|
return critterAdjustRadiation(critter, value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Should be unreachable
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF6CC
|
|
|
|
void protoCritterDataResetStats(CritterProtoData* data)
|
|
|
|
{
|
|
|
|
for (int stat = 0; stat < SAVEABLE_STAT_COUNT; stat++) {
|
|
|
|
data->baseStats[stat] = gStatDescriptions[stat].defaultValue;
|
|
|
|
data->bonusStats[stat] = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF6FC
|
|
|
|
void critterUpdateDerivedStats(Object* critter)
|
|
|
|
{
|
|
|
|
int strength = critterGetStat(critter, STAT_STRENGTH);
|
|
|
|
int perception = critterGetStat(critter, STAT_PERCEPTION);
|
|
|
|
int endurance = critterGetStat(critter, STAT_ENDURANCE);
|
|
|
|
int intelligence = critterGetStat(critter, STAT_INTELLIGENCE);
|
|
|
|
int agility = critterGetStat(critter, STAT_AGILITY);
|
|
|
|
int luck = critterGetStat(critter, STAT_LUCK);
|
|
|
|
|
|
|
|
Proto* proto;
|
|
|
|
protoGetProto(critter->pid, &proto);
|
|
|
|
CritterProtoData* data = &(proto->critter.data);
|
|
|
|
|
|
|
|
data->baseStats[STAT_MAXIMUM_HIT_POINTS] = critterGetBaseStatWithTraitModifier(critter, STAT_STRENGTH) + critterGetBaseStatWithTraitModifier(critter, STAT_ENDURANCE) * 2 + 15;
|
|
|
|
data->baseStats[STAT_MAXIMUM_ACTION_POINTS] = agility / 2 + 5;
|
|
|
|
data->baseStats[STAT_ARMOR_CLASS] = agility;
|
2022-05-28 04:45:48 -07:00
|
|
|
data->baseStats[STAT_MELEE_DAMAGE] = std::max(strength - 5, 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
data->baseStats[STAT_CARRY_WEIGHT] = 25 * strength + 25;
|
|
|
|
data->baseStats[STAT_SEQUENCE] = 2 * perception;
|
2022-05-28 04:45:48 -07:00
|
|
|
data->baseStats[STAT_HEALING_RATE] = std::max(endurance / 3, 1);
|
2022-05-19 01:51:26 -07:00
|
|
|
data->baseStats[STAT_CRITICAL_CHANCE] = luck;
|
|
|
|
data->baseStats[STAT_BETTER_CRITICALS] = 0;
|
|
|
|
data->baseStats[STAT_RADIATION_RESISTANCE] = 2 * endurance;
|
|
|
|
data->baseStats[STAT_POISON_RESISTANCE] = 5 * endurance;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF854
|
|
|
|
char* statGetName(int stat)
|
|
|
|
{
|
|
|
|
return statIsValid(stat) ? gStatDescriptions[stat].name : NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF898
|
|
|
|
char* statGetDescription(int stat)
|
|
|
|
{
|
|
|
|
return statIsValid(stat) ? gStatDescriptions[stat].description : NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF8DC
|
|
|
|
char* statGetValueDescription(int value)
|
|
|
|
{
|
|
|
|
if (value < PRIMARY_STAT_MIN) {
|
|
|
|
value = PRIMARY_STAT_MIN;
|
|
|
|
} else if (value > PRIMARY_STAT_MAX) {
|
|
|
|
value = PRIMARY_STAT_MAX;
|
|
|
|
}
|
|
|
|
|
|
|
|
return gStatValueDescriptions[value - PRIMARY_STAT_MIN];
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF8FC
|
|
|
|
int pcGetStat(int pcStat)
|
|
|
|
{
|
|
|
|
return pcStatIsValid(pcStat) ? gPcStatValues[pcStat] : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF910
|
|
|
|
int pcSetStat(int pcStat, int value)
|
|
|
|
{
|
|
|
|
int result;
|
|
|
|
|
|
|
|
if (!pcStatIsValid(pcStat)) {
|
|
|
|
return -5;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value < gPcStatDescriptions[pcStat].minimumValue) {
|
|
|
|
return -2;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (value > gPcStatDescriptions[pcStat].maximumValue) {
|
|
|
|
return -3;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pcStat != PC_STAT_EXPERIENCE || value >= gPcStatValues[PC_STAT_EXPERIENCE]) {
|
|
|
|
gPcStatValues[pcStat] = value;
|
|
|
|
if (pcStat == PC_STAT_EXPERIENCE) {
|
|
|
|
result = pcAddExperienceWithOptions(0, true);
|
|
|
|
} else {
|
|
|
|
result = 0;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
result = pcSetExperience(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset stats.
|
|
|
|
//
|
|
|
|
// 0x4AF980
|
|
|
|
void pcStatsReset()
|
|
|
|
{
|
|
|
|
for (int pcStat = 0; pcStat < PC_STAT_COUNT; pcStat++) {
|
|
|
|
gPcStatValues[pcStat] = gPcStatDescriptions[pcStat].defaultValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns experience to reach next level.
|
|
|
|
//
|
|
|
|
// 0x4AF9A0
|
|
|
|
int pcGetExperienceForNextLevel()
|
|
|
|
{
|
|
|
|
return pcGetExperienceForLevel(gPcStatValues[PC_STAT_LEVEL] + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns exp to reach given level.
|
|
|
|
//
|
|
|
|
// 0x4AF9A8
|
|
|
|
int pcGetExperienceForLevel(int level)
|
|
|
|
{
|
|
|
|
if (level >= PC_LEVEL_MAX) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
int v1 = level / 2;
|
|
|
|
if ((level & 1) != 0) {
|
|
|
|
return 1000 * v1 * level;
|
|
|
|
} else {
|
|
|
|
return 1000 * v1 * (level - 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AF9F4
|
|
|
|
char* pcStatGetName(int pcStat)
|
|
|
|
{
|
|
|
|
return pcStat >= 0 && pcStat < PC_STAT_COUNT ? gPcStatDescriptions[pcStat].name : NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AFA14
|
|
|
|
char* pcStatGetDescription(int pcStat)
|
|
|
|
{
|
|
|
|
return pcStat >= 0 && pcStat < PC_STAT_COUNT ? gPcStatDescriptions[pcStat].description : NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AFA34
|
|
|
|
int statGetFrmId(int stat)
|
|
|
|
{
|
|
|
|
return statIsValid(stat) ? gStatDescriptions[stat].frmId : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Roll D10 against specified stat.
|
|
|
|
//
|
|
|
|
// This function is intended to be used with one of SPECIAL stats (which are
|
|
|
|
// capped at 10, hence d10), not with artitrary stat, but does not enforce it.
|
|
|
|
//
|
|
|
|
// An optional [modifier] can be supplied as a bonus (or penalty) to the stat's
|
|
|
|
// value.
|
|
|
|
//
|
|
|
|
// Upon return [howMuch] will be set to difference between stat's value
|
|
|
|
// (accounting for given [modifier]) and d10 roll, which can be positive (or
|
|
|
|
// zero) when roll succeeds, or negative when roll fails. Set [howMuch] to
|
|
|
|
// `NULL` if you're not interested in this value.
|
|
|
|
//
|
|
|
|
// 0x4AFA78
|
|
|
|
int statRoll(Object* critter, int stat, int modifier, int* howMuch)
|
|
|
|
{
|
|
|
|
int value = critterGetStat(critter, stat) + modifier;
|
|
|
|
int chance = randomBetween(PRIMARY_STAT_MIN, PRIMARY_STAT_MAX);
|
|
|
|
|
|
|
|
if (howMuch != NULL) {
|
|
|
|
*howMuch = value - chance;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (chance <= value) {
|
|
|
|
return ROLL_SUCCESS;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ROLL_FAILURE;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AFAA8
|
|
|
|
int pcAddExperience(int xp)
|
|
|
|
{
|
|
|
|
return pcAddExperienceWithOptions(xp, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AFAB8
|
|
|
|
int pcAddExperienceWithOptions(int xp, bool a2)
|
|
|
|
{
|
|
|
|
int newXp = gPcStatValues[PC_STAT_EXPERIENCE];
|
|
|
|
newXp += xp;
|
|
|
|
newXp += perkGetRank(gDude, PERK_SWIFT_LEARNER) * 5 * xp / 100;
|
|
|
|
|
|
|
|
if (newXp < gPcStatDescriptions[PC_STAT_EXPERIENCE].minimumValue) {
|
|
|
|
newXp = gPcStatDescriptions[PC_STAT_EXPERIENCE].minimumValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (newXp > gPcStatDescriptions[PC_STAT_EXPERIENCE].maximumValue) {
|
|
|
|
newXp = gPcStatDescriptions[PC_STAT_EXPERIENCE].maximumValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
gPcStatValues[PC_STAT_EXPERIENCE] = newXp;
|
|
|
|
|
|
|
|
while (gPcStatValues[PC_STAT_LEVEL] < PC_LEVEL_MAX) {
|
|
|
|
if (newXp < pcGetExperienceForNextLevel()) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pcSetStat(PC_STAT_LEVEL, gPcStatValues[PC_STAT_LEVEL] + 1) == 0) {
|
|
|
|
int maxHpBefore = critterGetStat(gDude, STAT_MAXIMUM_HIT_POINTS);
|
|
|
|
|
|
|
|
// You have gone up a level.
|
|
|
|
MessageListItem messageListItem;
|
|
|
|
messageListItem.num = 600;
|
|
|
|
if (messageListGetItem(&gStatsMessageList, &messageListItem)) {
|
|
|
|
displayMonitorAddMessage(messageListItem.text);
|
|
|
|
}
|
|
|
|
|
|
|
|
dudeEnableState(DUDE_STATE_LEVEL_UP_AVAILABLE);
|
|
|
|
|
|
|
|
soundPlayFile("levelup");
|
|
|
|
|
|
|
|
// NOTE: Uninline.
|
|
|
|
int endurance = critterGetBaseStatWithTraitModifier(gDude, STAT_ENDURANCE);
|
|
|
|
|
|
|
|
int hpPerLevel = endurance / 2 + 2;
|
|
|
|
hpPerLevel += perkGetRank(gDude, PERK_LIFEGIVER) * 4;
|
|
|
|
|
|
|
|
int bonusHp = critterGetBonusStat(gDude, STAT_MAXIMUM_HIT_POINTS);
|
|
|
|
critterSetBonusStat(gDude, STAT_MAXIMUM_HIT_POINTS, bonusHp + hpPerLevel);
|
|
|
|
|
|
|
|
int maxHpAfter = critterGetStat(gDude, STAT_MAXIMUM_HIT_POINTS);
|
|
|
|
critterAdjustHitPoints(gDude, maxHpAfter - maxHpBefore);
|
|
|
|
|
|
|
|
interfaceRenderHitPoints(false);
|
|
|
|
|
|
|
|
if (a2) {
|
|
|
|
_partyMemberIncLevels();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0x4AFC38
|
|
|
|
int pcSetExperience(int xp)
|
|
|
|
{
|
|
|
|
int oldLevel = gPcStatValues[PC_STAT_LEVEL];
|
|
|
|
gPcStatValues[PC_STAT_EXPERIENCE] = xp;
|
|
|
|
|
|
|
|
int level = 1;
|
|
|
|
do {
|
|
|
|
level += 1;
|
|
|
|
} while (xp >= pcGetExperienceForLevel(level) && level < PC_LEVEL_MAX);
|
|
|
|
|
|
|
|
int newLevel = level - 1;
|
|
|
|
|
|
|
|
pcSetStat(PC_STAT_LEVEL, newLevel);
|
|
|
|
dudeDisableState(DUDE_STATE_LEVEL_UP_AVAILABLE);
|
|
|
|
|
|
|
|
int endurance = critterGetBaseStat(gDude, STAT_ENDURANCE);
|
|
|
|
if (gDude == gDude) {
|
|
|
|
endurance += traitGetStatModifier(STAT_ENDURANCE);
|
|
|
|
}
|
|
|
|
|
|
|
|
int hpPerLevel = endurance / 2 + 2;
|
|
|
|
hpPerLevel += perkGetRank(gDude, PERK_LIFEGIVER) * 4;
|
|
|
|
|
|
|
|
int deltaHp = (oldLevel - newLevel) * hpPerLevel;
|
|
|
|
critterAdjustHitPoints(gDude, -deltaHp);
|
|
|
|
|
|
|
|
int bonusHp = critterGetBonusStat(gDude, STAT_MAXIMUM_HIT_POINTS);
|
|
|
|
|
|
|
|
critterSetBonusStat(gDude, STAT_MAXIMUM_HIT_POINTS, bonusHp - deltaHp);
|
|
|
|
|
|
|
|
interfaceRenderHitPoints(false);
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|