Compare commits

...
Author SHA1 Message Date
Mike Klaas f9a5aeff9a PR feedback 2026-04-10 23:48:45 -07:00
Mike Klaas bf55a6fbb1 cleanup 2026-04-10 23:48:45 -07:00
github-actions[bot] 9764e2911d chore: auto-format with clang-format 2026-04-11 06:30:12 +00:00
Mike Klaas 5d5bb78087 Implement perk carryover, plus get/set_perk_owed
As in Sfall, the 0/1 "perk owned" boolean becomes an unsigned char, allowing perks to accumulate if they are not spent by the time the next perk is granted.

Unlike Sfall, we add a mode where perks must be spent "in order".  For instance, if the player has not spent their level 3 perk when they hit level 6, they still have 2 perks to spend, but the first perk must be their level 3 perk (with only level 3 perks available), and then they can spend their level 6 perk.  This behaviour is configurable to be: vanilla, CE, or Sfall.

This value is the same char saved in vanilla and Sfall, so is backward compatible with old saves.

To implement `get/set_perk_owed`, perk incrementing has to happen at level-up time instead of the first time the character sheet is opened after level up.  This matches Sfall's implementation.

It would be pretty easy to implement set_perk_freq / seq_perk_freq / set_perk_level_mod / get_perk_available.  For now leaving this out since the PR is big and I'm not sure how often these are used in mods.
2026-04-10 23:21:41 -07:00
11 changed files with 221 additions and 41 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/)
| Skills | get/set_critter_skill_points<br>get/set_available_skill_points<br>set_skill_max<br>set_critter_skill_mod<br>set_base_skill_mod<br>mod_skill_points_per_level | not implemented | - |
| Graphics | graphics_funcs_available<br>force_graphics_refresh<br>get_screen_width<br>get_screen_height<br>set_palette | implemented: only get_screen_width, get_screen_height | - |
| Shaders | load_shader<br>free_shader<br>activate_shader<br>deactivate_shader<br>set/get_shader_* | đźš« | likely will not implement direct compatibility
| Perks and traits | set_perk_image<br>set_perk_*<br>set_pyromaniac_mod<br>apply_heaveho_fix<br>set_swiftlearner_mod<br>has/set_fake_perk<br>has/set_fake_trait<br>set_selectable_perk<br>set_perkbox_title<br>show/hide_real_perks<br>perk_add_mode<br>clear_selectable_perks<br>add/remove_trait<br>seq_perk_freq | not implemented | - |
| Perks and traits | get_perk_owed<br>set_perk_owed<br>set_perk_image<br>set_perk_*<br>set_pyromaniac_mod<br>apply_heaveho_fix<br>set_swiftlearner_mod<br>has/set_fake_perk<br>has/set_fake_trait<br>set_selectable_perk<br>set_perkbox_title<br>show/hide_real_perks<br>perk_add_mode<br>clear_selectable_perks<br>add/remove_trait<br>seq_perk_freq | implemented: only get_perk_owed, set_perk_owed | - |
| Virtual file system | fs_create<br>fs_copy<br>fs_find<br>fs_read/write_*<br>fs_delete<br>fs_size<br>fs_pos<br>fs_seek<br>fs_resize | đźš« | Open an issue if you have a use case for these |
| Combat / Knockback | set_weapon_knockback<br>set_target_knockback<br>set_attacker_knockback<br>remove_weapon_knockback<br>remove_target_knockback<br>remove_attacker_knockback | not implemented | - |
| Maps and encounters | in_world_map<br>force_encounter<br>force_encounter_with_flags<br>set_map_time_multi<br>get/set_map_enter_position<br>exec_map_update_scripts<br>get/set_terrain_name<br>set_town_title<br>get/set_can_rest_on_map<br>set_rest_heal_time<br>set_rest_mode<br>set_worldmap_heal_time | implemented: in_world_map, force_encounter, force_encounter_with_flags, set_map_time_multi | - |
+152 -32
View File
@@ -37,6 +37,7 @@
#include "platform_compat.h"
#include "proto.h"
#include "scripts.h"
#include "settings.h"
#include "sfall_config.h"
#include "skill.h"
#include "stat.h"
@@ -144,6 +145,11 @@ enum {
EDITOR_BUTTONS_COUNT = EDITOR_FIRST_TRAIT + TRAIT_COUNT,
};
static constexpr int kMaxPerkOwed = 250;
// nb. this is number of perks, not total perk ranks. This only prevents level ups from granting more perks.
static constexpr int kMaxSelectablePerks = 37;
static constexpr int kPrimaryStatDescriptionWidth = 100;
enum {
EDITOR_GRAPHIC_BIG_NUMBERS,
EDITOR_GRAPHIC_AGE_MASK,
@@ -282,6 +288,8 @@ static int perkDialogDrawTraits(int a1);
static int perkDialogOptionCompare(const void* a1, const void* a2);
static int perkDialogDrawCard(int frmId, const char* name, const char* rank, char* description);
static void _pop_perks();
static int characterEditorGetLevelsPerPerk();
static void characterEditorGrantPerkAtLevel(int level);
static int _is_supper_bonus();
static int characterEditorFolderViewInit();
static void characterEditorFolderViewScroll(int direction);
@@ -769,7 +777,7 @@ static int gCharacterEditorTaggedSkillCount;
static int gCharacterEditorTempTaggedSkills[NUM_TAGGED_SKILLS];
// 0x570A28
static char gCharacterEditorHasFreePerkBackup;
static unsigned char gCharacterEditorHasFreePerkBackup;
// 0x570A29
static unsigned char gCharacterEditorHasFreePerk;
@@ -2474,7 +2482,7 @@ static void characterEditorDrawPrimaryStat(int stat, bool animate, int previousV
characterEditorDrawBigNumber(58, gCharacterEditorPrimaryStatY[stat], flags, value, previousValue, gCharacterEditorWindow);
blitBufferToBuffer(_editorBackgroundFrmImage.getData() + off, 40, fontGetLineHeight(), 640, gCharacterEditorWindowBuffer + off, 640);
blitBufferToBuffer(_editorBackgroundFrmImage.getData() + off, kPrimaryStatDescriptionWidth, fontGetLineHeight(), 640, gCharacterEditorWindowBuffer + off, 640);
messageListItemId = critterGetStat(gDude, stat) + 199;
if (messageListItemId > 210) {
@@ -2486,7 +2494,7 @@ static void characterEditorDrawPrimaryStat(int stat, bool animate, int previousV
} else {
value = critterGetStat(gDude, stat);
characterEditorDrawBigNumber(58, gCharacterEditorPrimaryStatY[stat], 0, value, 0, gCharacterEditorWindow);
blitBufferToBuffer(_editorBackgroundFrmImage.getData() + off, 40, fontGetLineHeight(), 640, gCharacterEditorWindowBuffer + off, 640);
blitBufferToBuffer(_editorBackgroundFrmImage.getData() + off, kPrimaryStatDescriptionWidth, fontGetLineHeight(), 640, gCharacterEditorWindowBuffer + off, 640);
value = critterGetStat(gDude, stat);
if (value > 10) {
@@ -5668,11 +5676,117 @@ int characterEditorLoad(File* stream)
return 0;
}
int characterEditorGetPerkOwed()
{
return gCharacterEditorHasFreePerk;
}
void characterEditorSetPerkOwed(int value)
{
unsigned int maskedValue = static_cast<unsigned int>(value) & 0xFF;
if (maskedValue <= kMaxPerkOwed) {
gCharacterEditorHasFreePerk = maskedValue;
}
}
// External interface: called when level up occurs
void characterEditorHandleLevelUp(int level)
{
characterEditorGrantPerkAtLevel(level);
}
// compute the character level that the next perk selection should use
int characterEditorGetPerkSelectionLevel()
{
int currentLevel = pcGetStat(PC_STAT_LEVEL);
if (settings.gameplay.perk_carryover != PERK_CARRY_OVER_MODE_ON) {
return currentLevel;
}
int owed = characterEditorGetPerkOwed();
if (owed <= 1) {
// If a single perk is owed, use the current level. This prevents odd effects in
// saves where perks have been skipped and is a general safe fallback.
return currentLevel;
}
int levelsPerPerk = characterEditorGetLevelsPerPerk();
// This therefore uses the number of perk thresholds strictly before the
// current level, then compares that against the total perk ranks already
// spent. In the common non-scripted case:
// - level 6 with 0 spent perks yields one deferred threshold (level 3)
// - after spending one perk, there is no older backlog left, so the next
// owed perk uses level 6 requirements
// - level 9 with 2 spent perks has no older backlog, so owed perks are
// immediately spendable at level 9
int previousThresholdsReached = (currentLevel - 1) / levelsPerPerk;
int spentPerks = 0;
for (int perk = 0; perk < PERK_COUNT; perk++) {
spentPerks += perkGetRank(gDude, perk);
}
// This is a heuristic for the 99.9% case where perk ranks come from normal
// perk picks. If enough perk ranks have already been spent to cover all
// earlier thresholds, there is no deferred backlog to force in order.
if (spentPerks >= previousThresholdsReached) {
return currentLevel;
}
// The next deferred pick corresponds to the earliest previously missed
// threshold after the already-spent perk ranks.
int thresholdIndex = spentPerks + 1;
int selectionLevel = std::max(3, thresholdIndex * levelsPerPerk);
if (selectionLevel <= 0 || selectionLevel > currentLevel) {
return currentLevel;
}
return selectionLevel;
}
// 0x43C20C
void characterEditorReset()
{
gCharacterEditorRemainingCharacterPoints = 5;
gCharacterEditorLastLevel = 1;
characterEditorSetPerkOwed(0);
}
static int characterEditorGetLevelsPerPerk()
{
int progression = 3;
if (traitIsSelected(TRAIT_SKILLED)) {
progression += 1;
}
return progression;
}
static void characterEditorGrantPerkAtLevel(int level)
{
if (level <= 0 || level % characterEditorGetLevelsPerPerk() != 0) {
return;
}
int selectedPerksCount = 0;
for (int perk = 0; perk < PERK_COUNT; perk++) {
if (perkGetRank(gDude, perk) != 0) {
selectedPerksCount += 1;
if (selectedPerksCount >= kMaxSelectablePerks) {
return;
}
}
}
switch (settings.gameplay.perk_carryover) {
case PERK_CARRY_OVER_MODE_OFF:
characterEditorSetPerkOwed(1);
break;
case PERK_CARRY_OVER_MODE_ON:
case PERK_CARRY_OVER_MODE_SFALL:
characterEditorSetPerkOwed(characterEditorGetPerkOwed() + 1);
break;
}
}
// level up if needed
@@ -5682,6 +5796,13 @@ static int characterEditorUpdateLevel()
{
int level = pcGetStat(PC_STAT_LEVEL);
if (level != gCharacterEditorLastLevel && level <= PC_LEVEL_MAX) {
// Runtime level-up now grants perks immediately. However, older saves
// may still have last_level behind the real level with no owed perks
// recorded, because perk grants used to be deferred until the next
// character screen visit. Replay perk grants only for that legacy
// catch-up case.
bool shouldReplayPerkGrants = characterEditorGetPerkOwed() == 0;
for (int nextLevel = gCharacterEditorLastLevel + 1; nextLevel <= level; nextLevel++) {
int sp = pcGetStat(PC_STAT_UNSPENT_SKILL_POINTS);
sp += 5;
@@ -5700,30 +5821,13 @@ static int characterEditorUpdateLevel()
pcSetStat(PC_STAT_UNSPENT_SKILL_POINTS, sp);
int selectedPerksCount = 0;
for (int perk = 0; perk < PERK_COUNT; perk++) {
if (perkGetRank(gDude, perk) != 0) {
selectedPerksCount += 1;
if (selectedPerksCount >= 37) {
break;
}
}
}
if (selectedPerksCount < 37) {
int progression = 3;
if (traitIsSelected(TRAIT_SKILLED)) {
progression += 1;
}
if (nextLevel % progression == 0) {
gCharacterEditorHasFreePerk = 1;
}
if (shouldReplayPerkGrants) {
characterEditorGrantPerkAtLevel(nextLevel);
}
}
}
if (gCharacterEditorHasFreePerk != 0) {
while (characterEditorGetPerkOwed() != 0) {
characterEditorWindowSelectedFolder = 0;
characterEditorDrawFolders();
windowRefresh(gCharacterEditorWindow);
@@ -5732,11 +5836,15 @@ static int characterEditorUpdateLevel()
if (rc == -1) {
debugPrint("\n *** Error running perks dialog! ***\n");
return -1;
} else if (rc == 0) {
}
if (rc == 0) {
characterEditorDrawFolders();
} else if (rc == 1) {
break;
}
if (rc == 1) {
characterEditorDrawFolders();
gCharacterEditorHasFreePerk = 0;
}
}
@@ -5785,6 +5893,10 @@ static int perkDialogShow()
gPerkDialogCardFrmId = -1;
gPerkDialogCardTitle[0] = '\0';
gPerkDialogCardDrawn = false;
int previousPerkRanks[PERK_COUNT];
for (int perk = 0; perk < PERK_COUNT; perk++) {
previousPerkRanks[perk] = perkGetRank(gDude, perk);
}
int backgroundFid = buildFid(OBJ_TYPE_INTERFACE, 86, 0, 0, 0);
if (!_perkDialogBackgroundFrmImage.lock(backgroundFid)) {
@@ -5910,6 +6022,12 @@ static int perkDialogShow()
fontDrawText(gPerkDialogWindowBuffer + PERK_WINDOW_WIDTH * 186 + 171, msg, PERK_WINDOW_WIDTH, PERK_WINDOW_WIDTH, _colorTable[18979]);
int count = perkDialogDrawPerks();
if (count == 0) {
debugPrint("*** No perks available for current owed slot! ***");
_perkDialogBackgroundFrmImage.unlock();
windowDestroy(gPerkDialogWindow);
return -1;
}
// NOTE: Original code is slightly different, but does the same thing.
int perk = gPerkDialogOptionList[gPerkDialogTopLine + gPerkDialogCurrentLine].value;
@@ -5931,7 +6049,8 @@ static int perkDialogShow()
int rc = perkDialogHandleInput(count, perkDialogRefreshPerks);
if (rc == 1) {
if (perkAdd(gDude, gPerkDialogOptionList[gPerkDialogTopLine + gPerkDialogCurrentLine].value) == -1) {
int selectionLevel = characterEditorGetPerkSelectionLevel();
if (perkAddAtLevel(gDude, gPerkDialogOptionList[gPerkDialogTopLine + gPerkDialogCurrentLine].value, selectionLevel) == -1) {
debugPrint("\n*** Unable to add perk! ***\n");
rc = 2;
}
@@ -5940,19 +6059,20 @@ static int perkDialogShow()
rc &= 1;
if (rc != 0) {
if (perkGetRank(gDude, PERK_TAG) != 0 && gCharacterEditorPerksBackup[PERK_TAG] == 0) {
characterEditorSetPerkOwed(characterEditorGetPerkOwed() - 1);
if (perkGetRank(gDude, PERK_TAG) != 0 && previousPerkRanks[PERK_TAG] == 0) {
if (!perkDialogHandleTagPerk()) {
perkRemove(gDude, PERK_TAG);
}
} else if (perkGetRank(gDude, PERK_MUTATE) != 0 && gCharacterEditorPerksBackup[PERK_MUTATE] == 0) {
} else if (perkGetRank(gDude, PERK_MUTATE) != 0 && previousPerkRanks[PERK_MUTATE] == 0) {
if (!perkDialogHandleMutatePerk()) {
perkRemove(gDude, PERK_MUTATE);
}
} else if (perkGetRank(gDude, PERK_LIFEGIVER) != gCharacterEditorPerksBackup[PERK_LIFEGIVER]) {
} else if (perkGetRank(gDude, PERK_LIFEGIVER) != previousPerkRanks[PERK_LIFEGIVER]) {
int maxHp = critterGetBonusStat(gDude, STAT_MAXIMUM_HIT_POINTS);
critterSetBonusStat(gDude, STAT_MAXIMUM_HIT_POINTS, maxHp + 4);
critterAdjustHitPoints(gDude, 4);
} else if (perkGetRank(gDude, PERK_EDUCATED) != gCharacterEditorPerksBackup[PERK_EDUCATED]) {
} else if (perkGetRank(gDude, PERK_EDUCATED) != previousPerkRanks[PERK_EDUCATED]) {
int sp = pcGetStat(PC_STAT_UNSPENT_SKILL_POINTS);
pcSetStat(PC_STAT_UNSPENT_SKILL_POINTS, sp + 2);
}
@@ -6266,7 +6386,7 @@ static int perkDialogDrawPerks()
fontSetCurrent(101);
int perks[PERK_COUNT];
int count = perkGetAvailablePerks(gDude, perks);
int count = perkGetAvailablePerks(gDude, characterEditorGetPerkSelectionLevel(), perks);
if (count == 0) {
return 0;
}
+14
View File
@@ -7,6 +7,16 @@ namespace fallout {
extern int gCharacterEditorRemainingCharacterPoints;
enum PerkCarryOverMode {
// Unspent perks are lost on level up (vanilla)
PERK_CARRY_OVER_MODE_OFF = 0,
// Unspent perks are preserved on level up, but you still spend unspent perks at the appropriate levels.
// E.g. if you reach level 9 and have never spent any perks, you first choose from the level 3 perks, then 6, then 9. (CE)
PERK_CARRY_OVER_MODE_ON = 1,
// Unspent perks are preserved on level up, and you can choose to spend them on perks for the current level.
PERK_CARRY_OVER_MODE_SFALL = 2,
};
int characterEditorShow(bool isCreationMode);
void characterEditorInit();
bool _isdoschar(int ch);
@@ -14,6 +24,10 @@ char* _strmfe(char* dest, const char* name, const char* ext);
int characterEditorSave(File* stream);
int characterEditorLoad(File* stream);
void characterEditorReset();
int characterEditorGetPerkOwed();
void characterEditorSetPerkOwed(int value);
void characterEditorHandleLevelUp(int level);
int characterEditorGetPerkSelectionLevel();
} // namespace fallout
+2
View File
@@ -11,6 +11,7 @@ namespace fallout {
#define GAME_CONFIG_SYSTEM_KEY "system"
#define GAME_CONFIG_SCREEN_KEY "screen"
#define GAME_CONFIG_UI_KEY "ui"
#define GAME_CONFIG_GAMEPLAY_KEY "gameplay"
#define GAME_CONFIG_PREFERENCES_KEY "preferences"
#define GAME_CONFIG_SOUND_KEY "sound"
#define GAME_CONFIG_MAPPER_KEY "mapper"
@@ -86,6 +87,7 @@ namespace fallout {
#define GAME_CONFIG_SORT_SCRIPT_LIST_KEY "sort_script_list"
#define GAME_CONFIG_PLAYER_SPEEDUP_KEY "player_speedup"
#define GAME_CONFIG_UI_ANIM_SPEED_KEY "ui_anim_speed"
#define GAME_CONFIG_PERK_CARRYOVER_KEY "perk_carryover"
#define GAME_CONFIG_RESOLUTION_X_KEY "resolution_x"
#define GAME_CONFIG_RESOLUTION_Y_KEY "resolution_y"
+11 -6
View File
@@ -49,7 +49,7 @@ typedef struct PerkRankData {
} PerkRankData;
static PerkRankData* perkGetRankData(Object* critter);
static bool perkCanAdd(Object* critter, int perk);
static bool perkCanAddAtLevel(Object* critter, int perk, int level);
static void perkResetRanks();
// 0x519DCC
@@ -299,7 +299,7 @@ static PerkRankData* perkGetRankData(Object* critter)
}
// 0x49680C
static bool perkCanAdd(Object* critter, int perk)
static bool perkCanAddAtLevel(Object* critter, int perk, int level)
{
if (!perkIsValid(perk)) {
return false;
@@ -317,7 +317,7 @@ static bool perkCanAdd(Object* critter, int perk)
}
if (critter == gDude) {
if (pcGetStat(PC_STAT_LEVEL) < perkDescription->minLevel) {
if (level < perkDescription->minLevel) {
return false;
}
}
@@ -432,12 +432,17 @@ static void perkResetRanks()
// 0x496A5C
int perkAdd(Object* critter, int perk)
{
return perkAddAtLevel(critter, perk, pcGetStat(PC_STAT_LEVEL));
}
int perkAddAtLevel(Object* critter, int perk, int level)
{
if (!perkIsValid(perk)) {
return -1;
}
if (!perkCanAdd(critter, perk)) {
if (!perkCanAddAtLevel(critter, perk, level)) {
return -1;
}
@@ -498,11 +503,11 @@ int perkRemove(Object* critter, int perk)
// Returns perks available to pick.
//
// 0x496B44
int perkGetAvailablePerks(Object* critter, int* perks)
int perkGetAvailablePerks(Object* critter, int level, int* perks)
{
int count = 0;
for (int perk = 0; perk < PERK_COUNT; perk++) {
if (perkCanAdd(critter, perk)) {
if (perkCanAddAtLevel(critter, perk, level)) {
perks[count] = perk;
count++;
}
+2 -1
View File
@@ -13,9 +13,10 @@ void perksExit();
int perksLoad(File* stream);
int perksSave(File* stream);
int perkAdd(Object* critter, int perk);
int perkAddAtLevel(Object* critter, int perk, int level);
int perkAddForce(Object* critter, int perk);
int perkRemove(Object* critter, int perk);
int perkGetAvailablePerks(Object* critter, int* perks);
int perkGetAvailablePerks(Object* critter, int level, int* perks);
int perkGetRank(Object* critter, int perk);
char* perkGetName(int perk);
char* perkGetDescription(int perk);
+3 -1
View File
@@ -135,6 +135,8 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = {
{ 0x189, "set_perk_name" },
{ 0x18a, "set_perk_desc" },
{ 0x18b, "set_pipboy_available" },
{ 0x18e, "get_perk_owed" },
{ 0x18f, "set_perk_owed" },
{ 0x190, "get_perk_available" },
{ 0x192, "set_critter_current_ap" },
{ 0x195, "set_weapon_knockback" },
@@ -288,4 +290,4 @@ static struct SfallOpcodeInfo opcodeInfoArray[] = {
{ 0x281, "sfall_func8" }
};
#endif
#endif
+6
View File
@@ -74,6 +74,10 @@ static void settingsFromConfig()
settingsRead(GAME_CONFIG_UI_KEY, GAME_CONFIG_SPLASH_SCREEN_SIZE_KEY, settings.ui.splash_screen_size, 0, 2);
settingsRead(GAME_CONFIG_UI_KEY, GAME_CONFIG_IGNORE_MAP_EDGES_KEY, settings.ui.ignore_map_edges);
int perkCarryOver = settings.gameplay.perk_carryover;
settingsRead(GAME_CONFIG_GAMEPLAY_KEY, GAME_CONFIG_PERK_CARRYOVER_KEY, perkCarryOver, 0, 2);
settings.gameplay.perk_carryover = static_cast<PerkCarryOverMode>(perkCarryOver);
settingsRead(GAME_CONFIG_PREFERENCES_KEY, GAME_CONFIG_GAME_DIFFICULTY_KEY, settings.preferences.game_difficulty);
settingsRead(GAME_CONFIG_PREFERENCES_KEY, GAME_CONFIG_COMBAT_DIFFICULTY_KEY, settings.preferences.combat_difficulty);
settingsRead(GAME_CONFIG_PREFERENCES_KEY, GAME_CONFIG_VIOLENCE_LEVEL_KEY, settings.preferences.violence_level);
@@ -163,6 +167,8 @@ static void settingsToConfig()
settingsWrite(GAME_CONFIG_UI_KEY, GAME_CONFIG_SPLASH_SCREEN_SIZE_KEY, settings.ui.splash_screen_size);
settingsWrite(GAME_CONFIG_UI_KEY, GAME_CONFIG_IGNORE_MAP_EDGES_KEY, settings.ui.ignore_map_edges);
settingsWrite(GAME_CONFIG_GAMEPLAY_KEY, GAME_CONFIG_PERK_CARRYOVER_KEY, static_cast<int>(settings.gameplay.perk_carryover));
settingsWrite(GAME_CONFIG_PREFERENCES_KEY, GAME_CONFIG_GAME_DIFFICULTY_KEY, settings.preferences.game_difficulty);
settingsWrite(GAME_CONFIG_PREFERENCES_KEY, GAME_CONFIG_COMBAT_DIFFICULTY_KEY, settings.preferences.combat_difficulty);
settingsWrite(GAME_CONFIG_PREFERENCES_KEY, GAME_CONFIG_VIOLENCE_LEVEL_KEY, settings.preferences.violence_level);
+6
View File
@@ -3,6 +3,7 @@
#include <string>
#include "character_editor.h"
#include "game_config.h"
namespace fallout {
@@ -52,6 +53,10 @@ struct UISettings {
bool ignore_map_edges = false;
};
struct GameplaySettings {
PerkCarryOverMode perk_carryover = PERK_CARRY_OVER_MODE_ON;
};
struct PreferencesSettings {
int game_difficulty = GAME_DIFFICULTY_NORMAL;
int combat_difficulty = COMBAT_DIFFICULTY_NORMAL;
@@ -126,6 +131,7 @@ struct Settings {
SystemSettings system;
ScreenSettings screen;
UISettings ui;
GameplaySettings gameplay;
PreferencesSettings preferences;
SoundSettings sound;
DebugSettings debug;
+14
View File
@@ -5,6 +5,7 @@
#include "animation.h"
#include "art.h"
#include "character_editor.h"
#include "color.h"
#include "combat.h"
#include "critter.h"
@@ -219,6 +220,17 @@ static void op_set_global_script_repeat(Program* program)
sfall_gl_scr_set_repeat(program, frames);
}
static void op_get_perk_owed(Program* program)
{
programStackPushInteger(program, characterEditorGetPerkOwed());
}
static void op_set_perk_owed(Program* program)
{
int value = programStackPopInteger(program);
characterEditorSetPerkOwed(value);
}
// key_pressed
static void op_key_pressed(Program* program)
{
@@ -1663,7 +1675,9 @@ void sfallOpcodesInit()
// 0x818d - void mod_kill_counter(int critterType, int amount)
// 0x818e - int get_perk_owed()
interpreterRegisterOpcode(0x818E, op_get_perk_owed);
// 0x818f - void set_perk_owed(int value)
interpreterRegisterOpcode(0x818F, op_set_perk_owed);
// 0x8190 - int get_perk_available(int perk)
// 0x8191 - int get_critter_current_ap(object critter)
+10
View File
@@ -5,6 +5,7 @@
#include <algorithm>
#include "art.h"
#include "character_editor.h"
#include "combat.h"
#include "critter.h"
#include "display_monitor.h"
@@ -755,6 +756,8 @@ int pcAddExperienceWithOptions(int xp, bool doParty, int* xpGained)
}
if (pcSetStat(PC_STAT_LEVEL, gPcStatValues[PC_STAT_LEVEL] + 1) == 0) {
characterEditorHandleLevelUp(gPcStatValues[PC_STAT_LEVEL]);
int maxHpBefore = critterGetStat(gDude, STAT_MAXIMUM_HIT_POINTS);
// You have gone up a level.
@@ -815,6 +818,13 @@ int pcSetExperience(int xp)
int newLevel = level - 1;
pcSetStat(PC_STAT_LEVEL, newLevel);
if (newLevel > oldLevel) {
for (int levelToGrant = oldLevel + 1; levelToGrant <= newLevel; levelToGrant++) {
characterEditorHandleLevelUp(levelToGrant);
}
}
dudeDisableState(DUDE_STATE_LEVEL_UP_AVAILABLE);
// NOTE: Uninline.