fallout2-ce/src/skill.cc

1219 lines
34 KiB
C++
Raw Normal View History

2022-05-19 01:51:26 -07:00
#include "skill.h"
2022-09-15 02:38:23 -07:00
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
2022-05-19 01:51:26 -07:00
#include "actions.h"
#include "color.h"
#include "combat.h"
#include "critter.h"
#include "debug.h"
#include "display_monitor.h"
#include "game.h"
#include "game_config.h"
#include "interface.h"
#include "item.h"
2022-06-18 22:28:29 -07:00
#include "message.h"
2022-05-19 01:51:26 -07:00
#include "object.h"
#include "palette.h"
#include "party_member.h"
#include "perk.h"
#include "pipboy.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 "stat.h"
#include "trait.h"
2022-06-18 22:28:29 -07:00
#define SKILLS_MAX_USES_PER_DAY (3)
#define REPAIRABLE_DAMAGE_FLAGS_LENGTH (5)
#define HEALABLE_DAMAGE_FLAGS_LENGTH (5)
typedef struct SkillDescription {
char* name;
char* description;
char* attributes;
int frmId;
int defaultValue;
int statModifier;
int stat1;
int stat2;
int field_20;
int experience;
int field_28;
} SkillDescription;
static void _show_skill_use_messages(Object* obj, int skill, Object* a3, int a4, int a5);
static int skillGetFreeUsageSlot(int skill);
2022-09-01 08:41:37 -07:00
static int skill_use_slot_clear();
2022-06-18 22:28:29 -07:00
2022-05-19 01:51:26 -07:00
// Damage flags which can be repaired using "Repair" skill.
//
// 0x4AA2F0
2022-06-18 22:28:29 -07:00
static const int gRepairableDamageFlags[REPAIRABLE_DAMAGE_FLAGS_LENGTH] = {
2022-05-19 01:51:26 -07:00
DAM_BLIND,
DAM_CRIP_ARM_LEFT,
DAM_CRIP_ARM_RIGHT,
DAM_CRIP_LEG_RIGHT,
DAM_CRIP_LEG_LEFT,
};
// Damage flags which can be healed using "Doctor" skill.
//
// 0x4AA304
2022-06-18 22:28:29 -07:00
static const int gHealableDamageFlags[HEALABLE_DAMAGE_FLAGS_LENGTH] = {
2022-05-19 01:51:26 -07:00
DAM_BLIND,
DAM_CRIP_ARM_LEFT,
DAM_CRIP_ARM_RIGHT,
DAM_CRIP_LEG_RIGHT,
DAM_CRIP_LEG_LEFT,
};
// 0x51D118
2022-06-18 22:28:29 -07:00
static SkillDescription gSkillDescriptions[SKILL_COUNT] = {
2022-05-19 01:51:26 -07:00
{ NULL, NULL, NULL, 28, 5, 4, STAT_AGILITY, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 29, 0, 2, STAT_AGILITY, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 30, 0, 2, STAT_AGILITY, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 31, 30, 2, STAT_AGILITY, STAT_STRENGTH, 1, 0, 0 },
{ NULL, NULL, NULL, 32, 20, 2, STAT_AGILITY, STAT_STRENGTH, 1, 0, 0 },
{ NULL, NULL, NULL, 33, 0, 4, STAT_AGILITY, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 34, 0, 2, STAT_PERCEPTION, STAT_INTELLIGENCE, 1, 25, 0 },
{ NULL, NULL, NULL, 35, 5, 1, STAT_PERCEPTION, STAT_INTELLIGENCE, 1, 50, 0 },
{ NULL, NULL, NULL, 36, 5, 3, STAT_AGILITY, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 37, 10, 1, STAT_PERCEPTION, STAT_AGILITY, 1, 25, 1 },
{ NULL, NULL, NULL, 38, 0, 3, STAT_AGILITY, STAT_INVALID, 1, 25, 1 },
{ NULL, NULL, NULL, 39, 10, 1, STAT_PERCEPTION, STAT_AGILITY, 1, 25, 1 },
{ NULL, NULL, NULL, 40, 0, 4, STAT_INTELLIGENCE, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 41, 0, 3, STAT_INTELLIGENCE, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 42, 0, 5, STAT_CHARISMA, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 43, 0, 4, STAT_CHARISMA, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 44, 0, 5, STAT_LUCK, STAT_INVALID, 1, 0, 0 },
{ NULL, NULL, NULL, 45, 0, 2, STAT_ENDURANCE, STAT_INTELLIGENCE, 1, 100, 0 },
};
// 0x51D430
int _gIsSteal = 0;
// Something about stealing, base value?
//
// 0x51D434
int _gStealCount = 0;
// 0x51D438
int _gStealSize = 0;
// 0x667F98
2022-06-18 22:28:29 -07:00
static int _timesSkillUsed[SKILL_COUNT][SKILLS_MAX_USES_PER_DAY];
2022-05-19 01:51:26 -07:00
// 0x668070
2022-06-18 22:28:29 -07:00
static int gTaggedSkills[NUM_TAGGED_SKILLS];
2022-05-19 01:51:26 -07:00
// skill.msg
//
// 0x668080
2022-06-18 22:28:29 -07:00
static MessageList gSkillsMessageList;
2022-05-19 01:51:26 -07:00
// 0x4AA318
int skillsInit()
{
if (!messageListInit(&gSkillsMessageList)) {
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, "skill.msg");
if (!messageListLoad(&gSkillsMessageList, path)) {
return -1;
}
for (int skill = 0; skill < SKILL_COUNT; skill++) {
MessageListItem messageListItem;
messageListItem.num = 100 + skill;
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
gSkillDescriptions[skill].name = messageListItem.text;
}
messageListItem.num = 200 + skill;
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
gSkillDescriptions[skill].description = messageListItem.text;
}
messageListItem.num = 300 + skill;
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
gSkillDescriptions[skill].attributes = messageListItem.text;
}
}
for (int index = 0; index < NUM_TAGGED_SKILLS; index++) {
gTaggedSkills[index] = -1;
}
2022-09-01 08:41:37 -07:00
// NOTE: Uninline.
skill_use_slot_clear();
2022-05-19 01:51:26 -07:00
return 0;
}
// 0x4AA448
void skillsReset()
{
for (int index = 0; index < NUM_TAGGED_SKILLS; index++) {
gTaggedSkills[index] = -1;
}
2022-09-01 08:41:37 -07:00
// NOTE: Uninline.
skill_use_slot_clear();
2022-05-19 01:51:26 -07:00
}
// 0x4AA478
void skillsExit()
{
messageListFree(&gSkillsMessageList);
}
// 0x4AA488
int skillsLoad(File* stream)
{
return fileReadInt32List(stream, gTaggedSkills, NUM_TAGGED_SKILLS);
}
// 0x4AA4A8
int skillsSave(File* stream)
{
return fileWriteInt32List(stream, gTaggedSkills, NUM_TAGGED_SKILLS);
}
// 0x4AA4C8
void protoCritterDataResetSkills(CritterProtoData* data)
{
for (int skill = 0; skill < SKILL_COUNT; skill++) {
data->skills[skill] = 0;
}
}
// 0x4AA4E4
void skillsSetTagged(int* skills, int count)
{
for (int index = 0; index < count; index++) {
gTaggedSkills[index] = skills[index];
}
}
// 0x4AA508
void skillsGetTagged(int* skills, int count)
{
for (int index = 0; index < count; index++) {
skills[index] = gTaggedSkills[index];
}
}
// 0x4AA52C
bool skillIsTagged(int skill)
{
return skill == gTaggedSkills[0]
|| skill == gTaggedSkills[1]
|| skill == gTaggedSkills[2]
|| skill == gTaggedSkills[3];
}
// 0x4AA558
int skillGetValue(Object* critter, int skill)
{
if (!skillIsValid(skill)) {
return -5;
}
int baseValue = skillGetBaseValue(critter, skill);
if (baseValue < 0) {
return baseValue;
}
SkillDescription* skillDescription = &(gSkillDescriptions[skill]);
int v7 = critterGetStat(critter, skillDescription->stat1);
if (skillDescription->stat2 != -1) {
v7 += critterGetStat(critter, skillDescription->stat2);
}
int value = skillDescription->defaultValue + skillDescription->statModifier * v7 + baseValue * skillDescription->field_20;
if (critter == gDude) {
if (skillIsTagged(skill)) {
value += baseValue * skillDescription->field_20;
if (!perkGetRank(critter, PERK_TAG) || skill != gTaggedSkills[3]) {
value += 20;
}
}
value += traitGetSkillModifier(skill);
value += perkGetSkillModifier(critter, skill);
value += skillGetGameDifficultyModifier(skill);
}
if (value > 300) {
value = 300;
}
return value;
}
// 0x4AA654
int skillGetDefaultValue(int skill)
{
return skillIsValid(skill) ? gSkillDescriptions[skill].defaultValue : -5;
}
// 0x4AA680
int skillGetBaseValue(Object* obj, int skill)
{
if (!skillIsValid(skill)) {
return 0;
}
Proto* proto;
protoGetProto(obj->pid, &proto);
return proto->critter.data.skills[skill];
}
// 0x4AA6BC
int skillAdd(Object* obj, int skill)
{
if (obj != gDude) {
return -5;
}
if (!skillIsValid(skill)) {
return -5;
}
Proto* proto;
protoGetProto(obj->pid, &proto);
int unspentSp = pcGetStat(PC_STAT_UNSPENT_SKILL_POINTS);
if (unspentSp <= 0) {
return -4;
}
int skillValue = skillGetValue(obj, skill);
if (skillValue >= 300) {
return -3;
}
// NOTE: Uninline.
int requiredSp = skillsGetCost(skillValue);
if (unspentSp < requiredSp) {
return -4;
}
int rc = pcSetStat(PC_STAT_UNSPENT_SKILL_POINTS, unspentSp - requiredSp);
if (rc == 0) {
proto->critter.data.skills[skill] += 1;
}
return rc;
}
// 0x4AA7F8
int skillAddForce(Object* obj, int skill)
{
if (obj != gDude) {
return -5;
}
if (!skillIsValid(skill)) {
return -5;
}
Proto* proto;
protoGetProto(obj->pid, &proto);
if (skillGetValue(obj, skill) >= 300) {
return -3;
}
proto->critter.data.skills[skill] += 1;
return 0;
}
// Returns the cost of raising skill value in skill points.
//
// 0x4AA87C
int skillsGetCost(int skillValue)
{
if (skillValue >= 201) {
return 6;
} else if (skillValue >= 176) {
return 5;
} else if (skillValue >= 151) {
return 4;
} else if (skillValue >= 126) {
return 3;
} else if (skillValue >= 101) {
return 2;
} else {
return 1;
}
}
// Decrements specified skill value by one, returning appropriate amount as
// unspent skill points.
//
// 0x4AA8C4
int skillSub(Object* critter, int skill)
{
if (critter != gDude) {
return -5;
}
if (!skillIsValid(skill)) {
return -5;
}
Proto* proto;
protoGetProto(critter->pid, &proto);
if (proto->critter.data.skills[skill] <= 0) {
return -2;
}
int unspentSp = pcGetStat(PC_STAT_UNSPENT_SKILL_POINTS);
int skillValue = skillGetValue(critter, skill) - 1;
// NOTE: Uninline.
int requiredSp = skillsGetCost(skillValue);
int newUnspentSp = unspentSp + requiredSp;
int rc = pcSetStat(PC_STAT_UNSPENT_SKILL_POINTS, newUnspentSp);
if (rc != 0) {
return rc;
}
proto->critter.data.skills[skill] -= 1;
if (skillIsTagged(skill)) {
int oldSkillCost = skillsGetCost(skillValue);
int newSkillCost = skillsGetCost(skillGetValue(critter, skill));
if (oldSkillCost != newSkillCost) {
rc = pcSetStat(PC_STAT_UNSPENT_SKILL_POINTS, newUnspentSp - 1);
if (rc != 0) {
return rc;
}
}
}
if (proto->critter.data.skills[skill] < 0) {
proto->critter.data.skills[skill] = 0;
}
return 0;
}
// Decrements specified skill value by one.
//
// 0x4AAA34
int skillSubForce(Object* obj, int skill)
{
Proto* proto;
if (obj != gDude) {
return -5;
}
if (!skillIsValid(skill)) {
return -5;
}
protoGetProto(obj->pid, &proto);
if (proto->critter.data.skills[skill] <= 0) {
return -2;
}
proto->critter.data.skills[skill] -= 1;
return 0;
}
// 0x4AAAA4
int skillRoll(Object* critter, int skill, int modifier, int* howMuch)
{
if (!skillIsValid(skill)) {
return ROLL_FAILURE;
}
if (critter == gDude && skill != SKILL_STEAL) {
Object* partyMember = partyMemberGetBestInSkill(skill);
if (partyMember != NULL) {
if (partyMemberGetBestSkill(partyMember) == skill) {
critter = partyMember;
}
}
}
int skillValue = skillGetValue(critter, skill);
if (critter == gDude && skill == SKILL_STEAL) {
if (dudeHasState(DUDE_STATE_SNEAKING)) {
if (dudeIsSneaking()) {
skillValue += 30;
}
}
}
int criticalChance = critterGetStat(critter, STAT_CRITICAL_CHANCE);
return randomRoll(skillValue + modifier, criticalChance, howMuch);
}
// 0x4AAB9C
char* skillGetName(int skill)
{
return skillIsValid(skill) ? gSkillDescriptions[skill].name : NULL;
}
// 0x4AABC0
char* skillGetDescription(int skill)
{
return skillIsValid(skill) ? gSkillDescriptions[skill].description : NULL;
}
// 0x4AABE4
char* skillGetAttributes(int skill)
{
return skillIsValid(skill) ? gSkillDescriptions[skill].attributes : NULL;
}
// 0x4AAC08
int skillGetFrmId(int skill)
{
return skillIsValid(skill) ? gSkillDescriptions[skill].frmId : 0;
}
// 0x4AAC2C
2022-06-18 22:28:29 -07:00
static void _show_skill_use_messages(Object* obj, int skill, Object* a3, int a4, int criticalChanceModifier)
2022-05-19 01:51:26 -07:00
{
if (obj != gDude) {
return;
}
if (a4 <= 0) {
return;
}
SkillDescription* skillDescription = &(gSkillDescriptions[skill]);
int baseExperience = skillDescription->experience;
if (baseExperience == 0) {
return;
}
if (skillDescription->field_28 && criticalChanceModifier < 0) {
baseExperience += abs(criticalChanceModifier);
}
int xpToAdd = a4 * baseExperience;
int before = pcGetStat(PC_STAT_EXPERIENCE);
if (pcAddExperience(xpToAdd) == 0 && a4 > 0) {
MessageListItem messageListItem;
messageListItem.num = 505; // You earn %d XP for honing your skills
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
int after = pcGetStat(PC_STAT_EXPERIENCE);
char text[60];
sprintf(text, messageListItem.text, after - before);
displayMonitorAddMessage(text);
}
}
}
// skill_use
// 0x4AAD08
int skillUse(Object* obj, Object* a2, int skill, int criticalChanceModifier)
{
MessageListItem messageListItem;
char text[60];
bool giveExp = true;
int currentHp = critterGetStat(a2, STAT_CURRENT_HIT_POINTS);
int maximumHp = critterGetStat(a2, STAT_MAXIMUM_HIT_POINTS);
int hpToHeal = 0;
int maximumHpToHeal = 0;
int minimumHpToHeal = 0;
if (obj == gDude) {
if (skill == SKILL_FIRST_AID || skill == SKILL_DOCTOR) {
int healerRank = perkGetRank(obj, PERK_HEALER);
minimumHpToHeal = 4 * healerRank;
maximumHpToHeal = 10 * healerRank;
}
}
int criticalChance = critterGetStat(obj, STAT_CRITICAL_CHANCE) + criticalChanceModifier;
int damageHealingAttempts = 1;
int v1 = 0;
int v2 = 0;
switch (skill) {
case SKILL_FIRST_AID:
if (skillGetFreeUsageSlot(SKILL_FIRST_AID) == -1) {
// 590: You've taxed your ability with that skill. Wait a while.
// 591: You're too tired.
// 592: The strain might kill you.
messageListItem.num = 590 + randomBetween(0, 2);
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
displayMonitorAddMessage(messageListItem.text);
}
return -1;
}
if (critterIsDead(a2)) {
// 512: You can't heal the dead.
// 513: Let the dead rest in peace.
// 514: It's dead, get over it.
messageListItem.num = 512 + randomBetween(0, 2);
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
debugPrint(messageListItem.text);
}
break;
}
if (currentHp < maximumHp) {
paletteFadeTo(gPaletteBlack);
int roll;
if (critterGetBodyType(a2) == BODY_TYPE_ROBOTIC) {
roll = ROLL_FAILURE;
} else {
roll = skillRoll(obj, skill, criticalChance, &hpToHeal);
}
if (roll == ROLL_SUCCESS || roll == ROLL_CRITICAL_SUCCESS) {
hpToHeal = randomBetween(minimumHpToHeal + 1, maximumHpToHeal + 5);
critterAdjustHitPoints(a2, hpToHeal);
if (obj == gDude) {
// You heal %d hit points.
messageListItem.num = 500;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
if (maximumHp - currentHp < hpToHeal) {
hpToHeal = maximumHp - currentHp;
}
sprintf(text, messageListItem.text, hpToHeal);
displayMonitorAddMessage(text);
}
a2->data.critter.combat.maneuver &= ~CRITTER_MANUEVER_FLEEING;
skillUpdateLastUse(SKILL_FIRST_AID);
v1 = 1;
if (a2 == gDude) {
interfaceRenderHitPoints(true);
}
} else {
// You fail to do any healing.
messageListItem.num = 503;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
sprintf(text, messageListItem.text, hpToHeal);
displayMonitorAddMessage(text);
}
scriptsExecMapUpdateProc();
paletteFadeTo(_cmap);
} else {
if (obj == gDude) {
// 501: You look healty already
// 502: %s looks healthy already
messageListItem.num = (a2 == gDude ? 501 : 502);
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
if (a2 == gDude) {
strcpy(text, messageListItem.text);
} else {
sprintf(text, messageListItem.text, objectGetName(a2));
}
displayMonitorAddMessage(text);
giveExp = false;
}
}
if (obj == gDude) {
gameTimeAddSeconds(1800);
}
break;
case SKILL_DOCTOR:
if (skillGetFreeUsageSlot(SKILL_DOCTOR) == -1) {
// 590: You've taxed your ability with that skill. Wait a while.
// 591: You're too tired.
// 592: The strain might kill you.
messageListItem.num = 590 + randomBetween(0, 2);
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
displayMonitorAddMessage(messageListItem.text);
}
return -1;
}
if (critterIsDead(a2)) {
// 512: You can't heal the dead.
// 513: Let the dead rest in peace.
// 514: It's dead, get over it.
messageListItem.num = 512 + randomBetween(0, 2);
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
displayMonitorAddMessage(messageListItem.text);
}
break;
}
if (currentHp < maximumHp || critterIsCrippled(a2)) {
paletteFadeTo(gPaletteBlack);
if (critterGetBodyType(a2) != BODY_TYPE_ROBOTIC && critterIsCrippled(a2)) {
int flags[HEALABLE_DAMAGE_FLAGS_LENGTH];
memcpy(flags, gHealableDamageFlags, sizeof(gHealableDamageFlags));
for (int index = 0; index < HEALABLE_DAMAGE_FLAGS_LENGTH; index++) {
if ((a2->data.critter.combat.results & flags[index]) != 0) {
damageHealingAttempts++;
int roll = skillRoll(obj, skill, criticalChance, &hpToHeal);
// 530: damaged eye
// 531: crippled left arm
// 532: crippled right arm
// 533: crippled right leg
// 534: crippled left leg
messageListItem.num = 530 + index;
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
MessageListItem prefix;
if (roll == ROLL_SUCCESS || roll == ROLL_CRITICAL_SUCCESS) {
a2->data.critter.combat.results &= ~flags[index];
a2->data.critter.combat.maneuver &= ~CRITTER_MANUEVER_FLEEING;
// 520: You heal your %s.
// 521: You heal the %s.
prefix.num = (a2 == gDude ? 520 : 521);
skillUpdateLastUse(SKILL_DOCTOR);
v1 = 1;
v2 = 1;
} else {
// 525: You fail to heal your %s.
// 526: You fail to heal the %s.
prefix.num = (a2 == gDude ? 525 : 526);
}
if (!messageListGetItem(&gSkillsMessageList, &prefix)) {
return -1;
}
sprintf(text, prefix.text, messageListItem.text);
displayMonitorAddMessage(text);
_show_skill_use_messages(obj, skill, a2, v1, criticalChanceModifier);
giveExp = false;
}
}
}
int roll;
if (critterGetBodyType(a2) == BODY_TYPE_ROBOTIC) {
roll = ROLL_FAILURE;
} else {
int skillValue = skillGetValue(obj, skill);
roll = randomRoll(skillValue, criticalChance, &hpToHeal);
}
if (roll == ROLL_SUCCESS || roll == ROLL_CRITICAL_SUCCESS) {
hpToHeal = randomBetween(minimumHpToHeal + 4, maximumHpToHeal + 10);
critterAdjustHitPoints(a2, hpToHeal);
if (obj == gDude) {
// You heal %d hit points.
messageListItem.num = 500;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
if (maximumHp - currentHp < hpToHeal) {
hpToHeal = maximumHp - currentHp;
}
sprintf(text, messageListItem.text, hpToHeal);
displayMonitorAddMessage(text);
}
if (!v2) {
skillUpdateLastUse(SKILL_DOCTOR);
}
a2->data.critter.combat.maneuver &= ~CRITTER_MANUEVER_FLEEING;
if (a2 == gDude) {
interfaceRenderHitPoints(true);
}
v1 = 1;
_show_skill_use_messages(obj, skill, a2, v1, criticalChanceModifier);
scriptsExecMapUpdateProc();
paletteFadeTo(_cmap);
giveExp = false;
} else {
// You fail to do any healing.
messageListItem.num = 503;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
sprintf(text, messageListItem.text, hpToHeal);
displayMonitorAddMessage(text);
scriptsExecMapUpdateProc();
paletteFadeTo(_cmap);
}
} else {
if (obj == gDude) {
// 501: You look healty already
// 502: %s looks healthy already
messageListItem.num = (a2 == gDude ? 501 : 502);
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
if (a2 == gDude) {
strcpy(text, messageListItem.text);
} else {
sprintf(text, messageListItem.text, objectGetName(a2));
}
displayMonitorAddMessage(text);
giveExp = false;
}
}
if (obj == gDude) {
gameTimeAddSeconds(3600 * damageHealingAttempts);
}
break;
case SKILL_SNEAK:
case SKILL_LOCKPICK:
break;
case SKILL_STEAL:
scriptsRequestStealing(obj, a2);
break;
case SKILL_TRAPS:
messageListItem.num = 551; // You fail to find any traps.
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
displayMonitorAddMessage(messageListItem.text);
}
return -1;
case SKILL_SCIENCE:
messageListItem.num = 552; // You fail to learn anything.
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
displayMonitorAddMessage(messageListItem.text);
}
return -1;
case SKILL_REPAIR:
if (critterGetBodyType(a2) != BODY_TYPE_ROBOTIC) {
// You cannot repair that.
messageListItem.num = 553;
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
displayMonitorAddMessage(messageListItem.text);
}
return -1;
}
if (skillGetFreeUsageSlot(SKILL_REPAIR) == -1) {
// 590: You've taxed your ability with that skill. Wait a while.
// 591: You're too tired.
// 592: The strain might kill you.
messageListItem.num = 590 + randomBetween(0, 2);
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
displayMonitorAddMessage(messageListItem.text);
}
return -1;
}
if (critterIsDead(a2)) {
// You got it?
messageListItem.num = 1101;
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
displayMonitorAddMessage(messageListItem.text);
}
break;
}
if (currentHp < maximumHp || critterIsCrippled(a2)) {
int flags[REPAIRABLE_DAMAGE_FLAGS_LENGTH];
memcpy(flags, gRepairableDamageFlags, sizeof(gRepairableDamageFlags));
paletteFadeTo(gPaletteBlack);
for (int index = 0; index < REPAIRABLE_DAMAGE_FLAGS_LENGTH; index++) {
if ((a2->data.critter.combat.results & flags[index]) != 0) {
damageHealingAttempts++;
int roll = skillRoll(obj, skill, criticalChance, &hpToHeal);
// 530: damaged eye
// 531: crippled left arm
// 532: crippled right arm
// 533: crippled right leg
// 534: crippled left leg
messageListItem.num = 530 + index;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
MessageListItem prefix;
if (roll == ROLL_SUCCESS || roll == ROLL_CRITICAL_SUCCESS) {
a2->data.critter.combat.results &= ~flags[index];
a2->data.critter.combat.maneuver &= ~CRITTER_MANUEVER_FLEEING;
// 520: You heal your %s.
// 521: You heal the %s.
prefix.num = (a2 == gDude ? 520 : 521);
skillUpdateLastUse(SKILL_REPAIR);
v1 = 1;
v2 = 1;
} else {
// 525: You fail to heal your %s.
// 526: You fail to heal the %s.
prefix.num = (a2 == gDude ? 525 : 526);
}
if (!messageListGetItem(&gSkillsMessageList, &prefix)) {
return -1;
}
sprintf(text, prefix.text, messageListItem.text);
displayMonitorAddMessage(text);
_show_skill_use_messages(obj, skill, a2, v1, criticalChanceModifier);
giveExp = false;
}
}
int skillValue = skillGetValue(obj, skill);
int roll = randomRoll(skillValue, criticalChance, &hpToHeal);
if (roll == ROLL_SUCCESS || roll == ROLL_CRITICAL_SUCCESS) {
hpToHeal = randomBetween(minimumHpToHeal + 4, maximumHpToHeal + 10);
critterAdjustHitPoints(a2, hpToHeal);
if (obj == gDude) {
// You heal %d hit points.
messageListItem.num = 500;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
if (maximumHp - currentHp < hpToHeal) {
hpToHeal = maximumHp - currentHp;
}
sprintf(text, messageListItem.text, hpToHeal);
displayMonitorAddMessage(text);
}
if (!v2) {
skillUpdateLastUse(SKILL_REPAIR);
}
a2->data.critter.combat.maneuver &= ~CRITTER_MANUEVER_FLEEING;
if (a2 == gDude) {
interfaceRenderHitPoints(true);
}
v1 = 1;
_show_skill_use_messages(obj, skill, a2, v1, criticalChanceModifier);
scriptsExecMapUpdateProc();
paletteFadeTo(_cmap);
giveExp = false;
} else {
// You fail to do any healing.
messageListItem.num = 503;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
sprintf(text, messageListItem.text, hpToHeal);
displayMonitorAddMessage(text);
scriptsExecMapUpdateProc();
paletteFadeTo(_cmap);
}
} else {
if (obj == gDude) {
// 501: You look healty already
// 502: %s looks healthy already
messageListItem.num = (a2 == gDude ? 501 : 502);
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
sprintf(text, messageListItem.text, objectGetName(a2));
displayMonitorAddMessage(text);
giveExp = false;
}
}
if (obj == gDude) {
gameTimeAddSeconds(1800 * damageHealingAttempts);
}
break;
default:
messageListItem.num = 510; // skill_use: invalid skill used.
if (messageListGetItem(&gSkillsMessageList, &messageListItem)) {
debugPrint(messageListItem.text);
}
return -1;
}
if (giveExp) {
_show_skill_use_messages(obj, skill, a2, v1, criticalChanceModifier);
}
if (skill == SKILL_FIRST_AID || skill == SKILL_DOCTOR) {
scriptsExecMapUpdateProc();
}
return 0;
}
// 0x4ABBE4
int skillsPerformStealing(Object* a1, Object* a2, Object* item, bool isPlanting)
{
int howMuch;
int stealModifier = _gStealCount;
stealModifier--;
stealModifier = -stealModifier;
if (a1 != gDude || !perkHasRank(a1, PERK_PICKPOCKET)) {
// -4% per item size
stealModifier -= 4 * itemGetSize(item);
if (FID_TYPE(a2->fid) == OBJ_TYPE_CRITTER) {
2022-05-19 01:51:26 -07:00
// check facing: -25% if face to face
if (_is_hit_from_front(a1, a2)) {
stealModifier -= 25;
}
}
}
if ((a2->data.critter.combat.results & (DAM_KNOCKED_OUT | DAM_KNOCKED_DOWN)) != 0) {
stealModifier += 20;
}
int stealChance = stealModifier + skillGetValue(a1, SKILL_STEAL);
if (stealChance > 95) {
stealChance = 95;
}
int stealRoll;
if (a1 == gDude && objectIsPartyMember(a2)) {
stealRoll = ROLL_CRITICAL_SUCCESS;
} else {
int criticalChance = critterGetStat(a1, STAT_CRITICAL_CHANCE);
stealRoll = randomRoll(stealChance, criticalChance, &howMuch);
}
int catchRoll;
if (stealRoll == ROLL_CRITICAL_SUCCESS) {
catchRoll = ROLL_CRITICAL_FAILURE;
} else if (stealRoll == ROLL_CRITICAL_FAILURE) {
catchRoll = ROLL_SUCCESS;
} else {
int catchChance;
if (PID_TYPE(a2->pid) == OBJ_TYPE_CRITTER) {
2022-05-19 01:51:26 -07:00
catchChance = skillGetValue(a2, SKILL_STEAL) - stealModifier;
} else {
catchChance = 30 - stealModifier;
}
catchRoll = randomRoll(catchChance, 0, &howMuch);
}
MessageListItem messageListItem;
char text[60];
if (catchRoll != ROLL_SUCCESS && catchRoll != ROLL_CRITICAL_SUCCESS) {
// 571: You steal the %s.
// 573: You plant the %s.
messageListItem.num = isPlanting ? 573 : 571;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
sprintf(text, messageListItem.text, objectGetName(item));
displayMonitorAddMessage(text);
return 1;
} else {
// 570: You're caught stealing the %s.
// 572: You're caught planting the %s.
messageListItem.num = isPlanting ? 572 : 570;
if (!messageListGetItem(&gSkillsMessageList, &messageListItem)) {
return -1;
}
sprintf(text, messageListItem.text, objectGetName(item));
displayMonitorAddMessage(text);
return 0;
}
}
// 0x4ABDEC
int skillGetGameDifficultyModifier(int skill)
{
switch (skill) {
case SKILL_FIRST_AID:
case SKILL_DOCTOR:
case SKILL_SNEAK:
case SKILL_LOCKPICK:
case SKILL_STEAL:
case SKILL_TRAPS:
case SKILL_SCIENCE:
case SKILL_REPAIR:
case SKILL_SPEECH:
case SKILL_BARTER:
case SKILL_GAMBLING:
case SKILL_OUTDOORSMAN:
if (1) {
int gameDifficulty = GAME_DIFFICULTY_NORMAL;
configGetInt(&gGameConfig, GAME_CONFIG_PREFERENCES_KEY, GAME_CONFIG_GAME_DIFFICULTY_KEY, &gameDifficulty);
if (gameDifficulty == GAME_DIFFICULTY_HARD) {
return -10;
} else if (gameDifficulty == GAME_DIFFICULTY_EASY) {
return 20;
}
}
break;
}
return 0;
}
// 0x4ABE44
2022-06-18 22:28:29 -07:00
static int skillGetFreeUsageSlot(int skill)
2022-05-19 01:51:26 -07:00
{
for (int slot = 0; slot < SKILLS_MAX_USES_PER_DAY; slot++) {
if (_timesSkillUsed[skill][slot] == 0) {
return slot;
}
}
int time = gameTimeGetTime();
2022-08-02 02:09:33 -07:00
int hoursSinceLastUsage = (time - _timesSkillUsed[skill][0]) / GAME_TIME_TICKS_PER_HOUR;
2022-05-19 01:51:26 -07:00
if (hoursSinceLastUsage <= 24) {
return -1;
}
return SKILLS_MAX_USES_PER_DAY - 1;
}
// 0x4ABEB8
int skillUpdateLastUse(int skill)
{
int slot = skillGetFreeUsageSlot(skill);
if (slot == -1) {
return -1;
}
if (_timesSkillUsed[skill][slot] != 0) {
2022-08-02 02:09:33 -07:00
for (int i = 0; i < slot; i++) {
2022-05-19 01:51:26 -07:00
_timesSkillUsed[skill][i] = _timesSkillUsed[skill][i + 1];
}
}
_timesSkillUsed[skill][slot] = gameTimeGetTime();
return 0;
}
2022-09-01 08:41:37 -07:00
// NOTE: Inlined.
//
// 0x4ABF24
int skill_use_slot_clear()
{
memset(_timesSkillUsed, 0, sizeof(_timesSkillUsed));
return 0;
}
2022-05-19 01:51:26 -07:00
// 0x4ABF3C
int skillsUsageSave(File* stream)
{
return fileWriteInt32List(stream, (int*)_timesSkillUsed, SKILL_COUNT * SKILLS_MAX_USES_PER_DAY);
}
// 0x4ABF5C
int skillsUsageLoad(File* stream)
{
return fileReadInt32List(stream, (int*)_timesSkillUsed, SKILL_COUNT * SKILLS_MAX_USES_PER_DAY);
}
// 0x4ABF7C
char* skillsGetGenericResponse(Object* critter, bool isDude)
{
int baseMessageId;
int count;
if (isDude) {
baseMessageId = 1100;
count = 4;
} else {
baseMessageId = 1000;
count = 5;
}
int messageId = randomBetween(0, count);
MessageListItem messageListItem;
char* msg = getmsg(&gSkillsMessageList, &messageListItem, baseMessageId + messageId);
return msg;
}