Multiple characters allowed. Starting base health lowered. Fylgjr system implemented.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,7 +5,7 @@ lib_mac/
|
|||||||
lib_windows/
|
lib_windows/
|
||||||
stub/
|
stub/
|
||||||
include/
|
include/
|
||||||
save.dat
|
*.dat
|
||||||
*.bak
|
*.bak
|
||||||
*.wav
|
*.wav
|
||||||
*.opus
|
*.opus
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Draugnorak Project Guidelines
|
# Draugnorak Project Guidelines
|
||||||
|
|
||||||
|
## Engine Reference
|
||||||
|
|
||||||
|
- If unsure about NVGT behavior or API details, consult the engine source in `nvgt-git/`.
|
||||||
|
|
||||||
## Asset Usage
|
## Asset Usage
|
||||||
|
|
||||||
**CRITICAL**: The `bloodshed/` directory is for reference only. DO NOT use any assets from the bloodshed directory in this game. It exists solely to help with coding examples and understanding patterns, but none of its files should be included or referenced in the actual game code.
|
**CRITICAL**: The `bloodshed/` directory is for reference only. DO NOT use any assets from the bloodshed directory in this game. It exists solely to help with coding examples and understanding patterns, but none of its files should be included or referenced in the actual game code.
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ sound_pool p(300);
|
|||||||
#include "src/quest_system.nvgt"
|
#include "src/quest_system.nvgt"
|
||||||
#include "src/environment.nvgt"
|
#include "src/environment.nvgt"
|
||||||
#include "src/combat.nvgt"
|
#include "src/combat.nvgt"
|
||||||
|
#include "src/fylgja_system.nvgt"
|
||||||
#include "src/save_system.nvgt"
|
#include "src/save_system.nvgt"
|
||||||
#include "src/base_system.nvgt"
|
#include "src/base_system.nvgt"
|
||||||
#include "src/time_system.nvgt"
|
#include "src/time_system.nvgt"
|
||||||
@@ -43,7 +44,7 @@ int run_main_menu() {
|
|||||||
speak_with_history("Draugnorak. Main menu.", true);
|
speak_with_history("Draugnorak. Main menu.", true);
|
||||||
|
|
||||||
int selection = 0;
|
int selection = 0;
|
||||||
string load_label = has_save_game() ? "Load Game" : "Load Game (no save found)";
|
string load_label = has_save_game() ? "Load Game" : "Load Game (no saves found)";
|
||||||
string[] options = {"New Game", load_label, "Exit"};
|
string[] options = {"New Game", load_label, "Exit"};
|
||||||
|
|
||||||
speak_with_history(options[selection], true);
|
speak_with_history(options[selection], true);
|
||||||
@@ -75,7 +76,7 @@ int run_main_menu() {
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
void main()
|
void run_game()
|
||||||
{
|
{
|
||||||
// Configure sound pool for better spatial audio
|
// Configure sound pool for better spatial audio
|
||||||
p.volume_step = AUDIO_VOLUME_STEP / float(AUDIO_TILE_SCALE); // Default falloff in audio units
|
p.volume_step = AUDIO_VOLUME_STEP / float(AUDIO_TILE_SCALE); // Default falloff in audio units
|
||||||
@@ -91,28 +92,28 @@ void main()
|
|||||||
int selection = run_main_menu();
|
int selection = run_main_menu();
|
||||||
|
|
||||||
if (selection == 0) {
|
if (selection == 0) {
|
||||||
// Check if save file exists and confirm before overwriting
|
if (!setup_new_character()) {
|
||||||
if (has_save_game()) {
|
continue;
|
||||||
int confirm = ui_question("", "Save found. Are you sure you want to start a new game?");
|
|
||||||
if (confirm != 1) {
|
|
||||||
continue; // Return to main menu
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
start_new_game();
|
start_new_game();
|
||||||
speak_with_history("New game started.", true);
|
speak_with_history("New game started.", true);
|
||||||
game_started = true;
|
game_started = true;
|
||||||
} else if (selection == 1) {
|
} else if (selection == 1) {
|
||||||
if (load_game_state()) {
|
if (!has_save_game()) {
|
||||||
|
ui_info_box("Draugnorak", "Load Game", "No saves found.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
string selectedFile;
|
||||||
|
if (!select_save_file(selectedFile)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (load_game_state_from_file(selectedFile)) {
|
||||||
speak_with_history("Game loaded.", true);
|
speak_with_history("Game loaded.", true);
|
||||||
game_started = true;
|
game_started = true;
|
||||||
} else {
|
} else {
|
||||||
if (has_save_game()) {
|
string message = last_save_error;
|
||||||
string message = last_save_error;
|
if (message == "") message = "Unable to load save.";
|
||||||
if (message == "") message = "Unable to load save.";
|
ui_info_box("Draugnorak", "Load Game", message);
|
||||||
ui_info_box("Draugnorak", "Load Game", message);
|
|
||||||
} else {
|
|
||||||
ui_info_box("Draugnorak", "Load Game", "No save found.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
exit();
|
exit();
|
||||||
@@ -198,6 +199,11 @@ void main()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fylgjaCharging) {
|
||||||
|
update_fylgja_charge();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Inventory & Actions
|
// Inventory & Actions
|
||||||
check_inventory_keys(x);
|
check_inventory_keys(x);
|
||||||
check_action_menu(x);
|
check_action_menu(x);
|
||||||
@@ -206,6 +212,7 @@ void main()
|
|||||||
check_altar_menu(x);
|
check_altar_menu(x);
|
||||||
check_equipment_menu();
|
check_equipment_menu();
|
||||||
check_quest_menu();
|
check_quest_menu();
|
||||||
|
check_fylgja_menu();
|
||||||
check_quick_slot_keys();
|
check_quick_slot_keys();
|
||||||
check_time_input();
|
check_time_input();
|
||||||
check_notification_keys();
|
check_notification_keys();
|
||||||
@@ -304,6 +311,9 @@ void main()
|
|||||||
|
|
||||||
movetime = jumping ? jump_speed : walk_speed;
|
movetime = jumping ? jump_speed : walk_speed;
|
||||||
|
|
||||||
|
bool left_active = key_down(KEY_LEFT);
|
||||||
|
bool right_active = key_down(KEY_RIGHT);
|
||||||
|
|
||||||
// Movement Logic
|
// Movement Logic
|
||||||
if (key_pressed(KEY_LEFT) && facing != 0 && !climbing && !falling && !rope_climbing) {
|
if (key_pressed(KEY_LEFT) && facing != 0 && !climbing && !falling && !rope_climbing) {
|
||||||
facing = 0;
|
facing = 0;
|
||||||
@@ -328,13 +338,13 @@ void main()
|
|||||||
MountainRange@ current_mountain = get_mountain_at(x);
|
MountainRange@ current_mountain = get_mountain_at(x);
|
||||||
Tree@ current_tree = get_tree_at(x);
|
Tree@ current_tree = get_tree_at(x);
|
||||||
int ground_elevation = get_mountain_elevation_at(x);
|
int ground_elevation = get_mountain_elevation_at(x);
|
||||||
if((key_down(KEY_LEFT) || key_down(KEY_RIGHT)) && y > ground_elevation && !jumping && !falling && !rope_climbing && current_mountain is null && current_tree !is null) {
|
if((left_active || right_active) && y > ground_elevation && !jumping && !falling && !rope_climbing && current_mountain is null && current_tree !is null) {
|
||||||
// Fall out of tree
|
// Fall out of tree
|
||||||
climbing = false;
|
climbing = false;
|
||||||
start_falling();
|
start_falling();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(key_down(KEY_LEFT) && x > 0 && !climbing && !falling && !rope_climbing)
|
if(left_active && x > 0 && !climbing && !falling && !rope_climbing)
|
||||||
{
|
{
|
||||||
facing = 0;
|
facing = 0;
|
||||||
int target_x = x - 1;
|
int target_x = x - 1;
|
||||||
@@ -353,7 +363,7 @@ void main()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(key_down(KEY_RIGHT) && x < MAP_SIZE - 1 && !climbing && !falling && !rope_climbing)
|
else if(right_active && x < MAP_SIZE - 1 && !climbing && !falling && !rope_climbing)
|
||||||
{
|
{
|
||||||
facing = 1;
|
facing = 1;
|
||||||
int target_x = x + 1;
|
int target_x = x + 1;
|
||||||
@@ -384,10 +394,7 @@ void main()
|
|||||||
|
|
||||||
// Searching Logic
|
// Searching Logic
|
||||||
bool shift_down = (key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT));
|
bool shift_down = (key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT));
|
||||||
if (!shift_down) {
|
if (!shift_down && !searching) {
|
||||||
if (searching) {
|
|
||||||
searching = false;
|
|
||||||
}
|
|
||||||
search_timer.restart();
|
search_timer.restart();
|
||||||
}
|
}
|
||||||
// Apply rune gathering bonus to search time
|
// Apply rune gathering bonus to search time
|
||||||
@@ -475,3 +482,13 @@ void main()
|
|||||||
p.update_listener_1d(x);
|
p.update_listener_1d(x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
run_game();
|
||||||
|
} catch {
|
||||||
|
log_unhandled_exception("main");
|
||||||
|
ui_info_box("Draugnorak", "Unhandled exception", "A crash log was written to crash.log.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
BIN
sounds/environment/tree.ogg
LFS
BIN
sounds/environment/tree.ogg
LFS
Binary file not shown.
BIN
sounds/player_female_damage.ogg
LFS
Normal file
BIN
sounds/player_female_damage.ogg
LFS
Normal file
Binary file not shown.
BIN
sounds/player_male_damage.ogg
LFS
Normal file
BIN
sounds/player_male_damage.ogg
LFS
Normal file
Binary file not shown.
@@ -181,6 +181,20 @@ void play_item_collect_sound(string itemName)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string get_player_damage_sound() {
|
||||||
|
if (player_sex == SEX_FEMALE) {
|
||||||
|
return "sounds/player_female_damage.ogg";
|
||||||
|
}
|
||||||
|
return "sounds/player_male_damage.ogg";
|
||||||
|
}
|
||||||
|
|
||||||
|
void play_player_damage_sound() {
|
||||||
|
string soundFile = get_player_damage_sound();
|
||||||
|
if (file_exists(soundFile)) {
|
||||||
|
p.play_stationary(soundFile, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Safe sound handle cleanup - checks if handle is valid and sound is active before destroying
|
// Safe sound handle cleanup - checks if handle is valid and sound is active before destroying
|
||||||
void safe_destroy_sound(int &inout handle)
|
void safe_destroy_sound(int &inout handle)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -473,7 +473,7 @@ void attempt_resident_snare_retrieval() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resident butchering - processes up to two games per day when blessed
|
// Resident butchering - processes up to residents_count games per day (doubled when blessed)
|
||||||
void attempt_resident_butchering() {
|
void attempt_resident_butchering() {
|
||||||
// Need residents
|
// Need residents
|
||||||
if (residents_count <= 0) return;
|
if (residents_count <= 0) return;
|
||||||
@@ -490,7 +490,7 @@ void attempt_resident_butchering() {
|
|||||||
// Need a fire in base
|
// Need a fire in base
|
||||||
if (!has_burning_fire_in_base()) return;
|
if (!has_burning_fire_in_base()) return;
|
||||||
|
|
||||||
int attempts = get_resident_effect_multiplier();
|
int attempts = residents_count * get_resident_effect_multiplier();
|
||||||
int break_chance = get_resident_break_chance(RESIDENT_TOOL_BREAK_CHANCE);
|
int break_chance = get_resident_break_chance(RESIDENT_TOOL_BREAK_CHANCE);
|
||||||
for (int attempt = 0; attempt < attempts; attempt++) {
|
for (int attempt = 0; attempt < attempts; attempt++) {
|
||||||
// Need game in storage
|
// Need game in storage
|
||||||
|
|||||||
144
src/bosses/adventure_combat.nvgt
Normal file
144
src/bosses/adventure_combat.nvgt
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// Shared combat helpers for adventures
|
||||||
|
|
||||||
|
const int ADVENTURE_WEAPON_BOW = 1;
|
||||||
|
const int ADVENTURE_WEAPON_SLING = 2;
|
||||||
|
const int ADVENTURE_WEAPON_SPEAR = 3;
|
||||||
|
const int ADVENTURE_WEAPON_AXE = 4;
|
||||||
|
|
||||||
|
funcdef int AdventureRangedReleaseCallback(int player_x, int direction, int range, int weapon_type, int damage);
|
||||||
|
|
||||||
|
bool adventure_arrow_recover_pending = false;
|
||||||
|
|
||||||
|
void reset_adventure_combat_state() {
|
||||||
|
bow_drawing = false;
|
||||||
|
sling_charging = false;
|
||||||
|
adventure_arrow_recover_pending = false;
|
||||||
|
stop_bow_shot_audio();
|
||||||
|
safe_destroy_sound(sling_sound_handle);
|
||||||
|
last_sling_stage = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_weapon_range_audio_with_listener(int listener_x, int creature_pos, bool &inout was_in_range) {
|
||||||
|
int range = get_current_weapon_range();
|
||||||
|
bool in_range = (range >= 0) && (abs(creature_pos - listener_x) <= range);
|
||||||
|
if (in_range && !was_in_range) {
|
||||||
|
play_weapon_range_sound("sounds/enemies/enter_range.ogg", creature_pos);
|
||||||
|
} else if (!in_range && was_in_range) {
|
||||||
|
play_weapon_range_sound("sounds/enemies/exit_range.ogg", creature_pos);
|
||||||
|
}
|
||||||
|
was_in_range = in_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
void adventure_start_bow_shot_audio(int listener_x, int start_x, int end_x, int hit_x, int duration_ms) {
|
||||||
|
stop_bow_shot_audio();
|
||||||
|
bow_shot_active = true;
|
||||||
|
bow_shot_timer.restart();
|
||||||
|
bow_shot_start_x = start_x;
|
||||||
|
bow_shot_end_x = end_x;
|
||||||
|
bow_shot_hit_x = hit_x;
|
||||||
|
bow_shot_duration_ms = duration_ms;
|
||||||
|
if (bow_shot_duration_ms < 1) bow_shot_duration_ms = 1;
|
||||||
|
|
||||||
|
bow_shot_sound_handle = play_1d_with_volume_step(
|
||||||
|
"sounds/weapons/arrow_flies.ogg",
|
||||||
|
listener_x,
|
||||||
|
bow_shot_start_x,
|
||||||
|
false,
|
||||||
|
PLAYER_WEAPON_SOUND_VOLUME_STEP
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void adventure_update_bow_shot(int listener_x) {
|
||||||
|
if (!bow_shot_active) return;
|
||||||
|
if (bow_shot_duration_ms < 1) bow_shot_duration_ms = 1;
|
||||||
|
|
||||||
|
int elapsed = bow_shot_timer.elapsed;
|
||||||
|
float progress = float(elapsed) / float(bow_shot_duration_ms);
|
||||||
|
if (progress > 1.0f) progress = 1.0f;
|
||||||
|
|
||||||
|
int travel = int(float(bow_shot_end_x - bow_shot_start_x) * progress);
|
||||||
|
int current_pos = bow_shot_start_x + travel;
|
||||||
|
if (bow_shot_sound_handle != -1) {
|
||||||
|
p.update_sound_1d(bow_shot_sound_handle, current_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elapsed >= bow_shot_duration_ms) {
|
||||||
|
int hit_x = bow_shot_hit_x;
|
||||||
|
bool recover_pending = adventure_arrow_recover_pending;
|
||||||
|
stop_bow_shot_audio();
|
||||||
|
adventure_arrow_recover_pending = false;
|
||||||
|
if (hit_x >= 0) {
|
||||||
|
play_1d_with_volume_step(
|
||||||
|
"sounds/weapons/arrow_hit.ogg",
|
||||||
|
listener_x,
|
||||||
|
hit_x,
|
||||||
|
false,
|
||||||
|
PLAYER_WEAPON_SOUND_VOLUME_STEP
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (recover_pending) {
|
||||||
|
add_personal_count(ITEM_ARROWS, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void adventure_release_bow_attack(int player_x, int player_facing, AdventureRangedReleaseCallback@ ranged_callback) {
|
||||||
|
if (get_personal_count(ITEM_ARROWS) <= 0) {
|
||||||
|
speak_ammo_blocked("No arrows.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int damage = get_bow_draw_damage(bow_draw_timer.elapsed);
|
||||||
|
add_personal_count(ITEM_ARROWS, -1);
|
||||||
|
p.play_stationary("sounds/weapons/bow_fire.ogg", false);
|
||||||
|
|
||||||
|
int search_direction = (player_facing == 1) ? 1 : -1;
|
||||||
|
int target_x = -1;
|
||||||
|
if (@ranged_callback !is null) {
|
||||||
|
target_x = ranged_callback(player_x, search_direction, BOW_RANGE, ADVENTURE_WEAPON_BOW, damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
int end_x = (target_x != -1)
|
||||||
|
? target_x
|
||||||
|
: (player_x + (search_direction * (BOW_RANGE + BOW_MISS_EXTRA_TILES)));
|
||||||
|
|
||||||
|
int duration_ms = ARROW_FLIES_DURATION_MS;
|
||||||
|
if (target_x != -1) {
|
||||||
|
int distance = abs(target_x - player_x);
|
||||||
|
if (distance < 1) distance = 1;
|
||||||
|
duration_ms = int(float(ARROW_FLIES_DURATION_MS) * (float(distance) / float(BOW_RANGE)));
|
||||||
|
if (duration_ms < 1) duration_ms = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
adventure_arrow_recover_pending = (random(1, 100) <= 25);
|
||||||
|
adventure_start_bow_shot_audio(player_x, player_x, end_x, target_x, duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
void adventure_release_sling_attack(int player_x, int player_facing, AdventureRangedReleaseCallback@ ranged_callback) {
|
||||||
|
add_personal_count(ITEM_STONES, -1);
|
||||||
|
|
||||||
|
int elapsed = sling_charge_timer.elapsed;
|
||||||
|
int cycle_time = 1500;
|
||||||
|
int time_in_cycle = elapsed % cycle_time;
|
||||||
|
int stage = time_in_cycle / 500; // 0=low, 1=in-range, 2=high
|
||||||
|
|
||||||
|
if (stage != 1) {
|
||||||
|
speak_with_history("Stone missed.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int search_direction = (player_facing == 1) ? 1 : -1;
|
||||||
|
int target_x = -1;
|
||||||
|
int damage = random(SLING_DAMAGE_MIN, SLING_DAMAGE_MAX);
|
||||||
|
|
||||||
|
if (@ranged_callback !is null) {
|
||||||
|
target_x = ranged_callback(player_x, search_direction, SLING_RANGE, ADVENTURE_WEAPON_SLING, damage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target_x == -1) {
|
||||||
|
speak_with_history("Stone missed.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, target_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// Adventure System
|
// Adventure System
|
||||||
// Handles triggering and managing terrain-specific adventures
|
// Handles triggering and managing terrain-specific adventures
|
||||||
|
|
||||||
|
#include "src/bosses/adventure_combat.nvgt"
|
||||||
#include "src/bosses/unicorn/unicorn_boss.nvgt"
|
#include "src/bosses/unicorn/unicorn_boss.nvgt"
|
||||||
|
|
||||||
void check_adventure_menu(int player_x) {
|
void check_adventure_menu(int player_x) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class UnicornBoss {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
health = 10000;
|
health = 450;
|
||||||
speed = UNICORN_SPEED;
|
speed = UNICORN_SPEED;
|
||||||
facing = 0; // Start facing west (toward player)
|
facing = 0; // Start facing west (toward player)
|
||||||
x = 0;
|
x = 0;
|
||||||
@@ -33,6 +33,8 @@ const int BRIDGE_END = 54;
|
|||||||
const int BRIDGE_SUPPORT_MAX_HEALTH = 100;
|
const int BRIDGE_SUPPORT_MAX_HEALTH = 100;
|
||||||
const float UNICORN_SOUND_VOLUME_STEP = 2.5; // Lower = audible from further away
|
const float UNICORN_SOUND_VOLUME_STEP = 2.5; // Lower = audible from further away
|
||||||
const int UNICORN_SPEED = 80; // ms per tile, 100 tiles * 80ms = 8 seconds per charge
|
const int UNICORN_SPEED = 80; // ms per tile, 100 tiles * 80ms = 8 seconds per charge
|
||||||
|
const bool UNICORN_BOW_CAN_DAMAGE_SUPPORTS = false;
|
||||||
|
const bool UNICORN_SLING_CAN_DAMAGE_SUPPORTS = false;
|
||||||
|
|
||||||
// State
|
// State
|
||||||
UnicornBoss unicorn;
|
UnicornBoss unicorn;
|
||||||
@@ -46,16 +48,23 @@ int player_arena_facing = 1; // 0 = west, 1 = east
|
|||||||
int[] bridge_supports_health; // 2 supports: Left (start) and Right (end)
|
int[] bridge_supports_health; // 2 supports: Left (start) and Right (end)
|
||||||
bool bridge_collapsed = false;
|
bool bridge_collapsed = false;
|
||||||
string current_unicorn_sound = "";
|
string current_unicorn_sound = "";
|
||||||
|
bool unicorn_defeated = false;
|
||||||
|
bool unicorn_defeated_by_fall = false;
|
||||||
|
bool unicorn_in_weapon_range = false;
|
||||||
|
|
||||||
void init_unicorn_adventure() {
|
void init_unicorn_adventure() {
|
||||||
unicorn.reset();
|
unicorn.reset();
|
||||||
unicorn.x = UNICORN_ARENA_SIZE - 1; // Start at east end
|
unicorn.x = UNICORN_ARENA_SIZE - 1; // Start at east end
|
||||||
|
|
||||||
|
reset_adventure_combat_state();
|
||||||
player_arena_x = 0; // Start player at west end
|
player_arena_x = 0; // Start player at west end
|
||||||
player_arena_y = 0;
|
player_arena_y = 0;
|
||||||
player_arena_jumping = false;
|
player_arena_jumping = false;
|
||||||
bridge_collapsed = false;
|
bridge_collapsed = false;
|
||||||
current_unicorn_sound = "";
|
current_unicorn_sound = "";
|
||||||
|
unicorn_defeated = false;
|
||||||
|
unicorn_defeated_by_fall = false;
|
||||||
|
unicorn_in_weapon_range = false;
|
||||||
|
|
||||||
// Initialize supports
|
// Initialize supports
|
||||||
bridge_supports_health.resize(2);
|
bridge_supports_health.resize(2);
|
||||||
@@ -65,6 +74,7 @@ void init_unicorn_adventure() {
|
|||||||
|
|
||||||
void cleanup_unicorn_adventure() {
|
void cleanup_unicorn_adventure() {
|
||||||
p.destroy_all();
|
p.destroy_all();
|
||||||
|
reset_adventure_combat_state();
|
||||||
if (unicorn.sound_handle != -1) {
|
if (unicorn.sound_handle != -1) {
|
||||||
p.destroy_sound(unicorn.sound_handle);
|
p.destroy_sound(unicorn.sound_handle);
|
||||||
unicorn.sound_handle = -1;
|
unicorn.sound_handle = -1;
|
||||||
@@ -90,6 +100,7 @@ void run_unicorn_adventure() {
|
|||||||
intro.insert_last("Strategy:");
|
intro.insert_last("Strategy:");
|
||||||
intro.insert_last(" - Use your axe to destroy a bridge support");
|
intro.insert_last(" - Use your axe to destroy a bridge support");
|
||||||
intro.insert_last(" - Lure the Unicorn onto the bridge");
|
intro.insert_last(" - Lure the Unicorn onto the bridge");
|
||||||
|
intro.insert_last(" - Or fight the Unicorn directly (it has massive health)");
|
||||||
intro.insert_last(" - Jump (UP arrow) to avoid being trampled");
|
intro.insert_last(" - Jump (UP arrow) to avoid being trampled");
|
||||||
intro.insert_last(" - When the bridge collapses with the Unicorn on it, you win!");
|
intro.insert_last(" - When the bridge collapses with the Unicorn on it, you win!");
|
||||||
intro.insert_last("");
|
intro.insert_last("");
|
||||||
@@ -130,10 +141,24 @@ void run_unicorn_adventure() {
|
|||||||
// Updates
|
// Updates
|
||||||
update_player_jump();
|
update_player_jump();
|
||||||
update_unicorn();
|
update_unicorn();
|
||||||
|
adventure_update_bow_shot(player_arena_x);
|
||||||
|
update_unicorn_weapon_range_audio();
|
||||||
|
|
||||||
// Check Conditions - unicorn falls when on collapsed bridge
|
// Check Conditions - unicorn falls when on collapsed bridge
|
||||||
if (bridge_collapsed && unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END) {
|
if (!unicorn_defeated && bridge_collapsed && unicorn.x >= BRIDGE_START && unicorn.x <= BRIDGE_END) {
|
||||||
play_unicorn_death_sequence();
|
mark_unicorn_defeated(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unicorn_defeated && unicorn.health <= 0) {
|
||||||
|
mark_unicorn_defeated(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unicorn_defeated) {
|
||||||
|
if (unicorn_defeated_by_fall) {
|
||||||
|
play_unicorn_death_sequence();
|
||||||
|
} else {
|
||||||
|
play_unicorn_ground_death_sequence();
|
||||||
|
}
|
||||||
cleanup_unicorn_adventure();
|
cleanup_unicorn_adventure();
|
||||||
give_unicorn_rewards();
|
give_unicorn_rewards();
|
||||||
return;
|
return;
|
||||||
@@ -247,55 +272,207 @@ void handle_player_actions() {
|
|||||||
// Can't attack while jumping
|
// Can't attack while jumping
|
||||||
if (player_arena_jumping) return;
|
if (player_arena_jumping) return;
|
||||||
|
|
||||||
// Attack cooldown like main game
|
bool ctrl_down = (key_down(KEY_LCTRL) || key_down(KEY_RCTRL));
|
||||||
int attack_cooldown = 1000;
|
|
||||||
if (spear_equipped) attack_cooldown = 800;
|
|
||||||
if (axe_equipped) attack_cooldown = 1600;
|
|
||||||
|
|
||||||
if ((key_down(KEY_LCTRL) || key_down(KEY_RCTRL)) && arena_attack_timer.elapsed > attack_cooldown) {
|
// Bow draw detection
|
||||||
arena_attack_timer.restart();
|
if (bow_equipped) {
|
||||||
|
if (ctrl_down && !bow_drawing) {
|
||||||
// Check for bridge supports
|
if (get_personal_count(ITEM_ARROWS) > 0) {
|
||||||
int target_support = -1;
|
bow_drawing = true;
|
||||||
|
bow_draw_timer.restart();
|
||||||
// Check Left Support (at BRIDGE_START)
|
p.play_stationary("sounds/weapons/bow_draw.ogg", false);
|
||||||
if (abs(player_arena_x - BRIDGE_START) <= 1) {
|
|
||||||
target_support = 0;
|
|
||||||
}
|
|
||||||
// Check Right Support (at BRIDGE_END)
|
|
||||||
else if (abs(player_arena_x - BRIDGE_END) <= 1) {
|
|
||||||
target_support = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target_support != -1) {
|
|
||||||
if (bridge_supports_health[target_support] > 0) {
|
|
||||||
// Only axe can damage supports (like chopping trees)
|
|
||||||
if (axe_equipped) {
|
|
||||||
bridge_supports_health[target_support] -= AXE_DAMAGE;
|
|
||||||
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
|
|
||||||
|
|
||||||
if (bridge_supports_health[target_support] <= 0) {
|
|
||||||
check_bridge_collapse();
|
|
||||||
}
|
|
||||||
} else if (spear_equipped) {
|
|
||||||
// Spear just makes sound, no damage (like hitting trees)
|
|
||||||
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
|
|
||||||
} else {
|
|
||||||
// No weapon or sling - swing sound, no effect
|
|
||||||
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal attack (useless vs unicorn but gives feedback)
|
|
||||||
if (abs(player_arena_x - unicorn.x) <= 1) {
|
|
||||||
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
|
|
||||||
} else {
|
} else {
|
||||||
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
|
speak_ammo_blocked("No arrows.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bow_drawing && !ctrl_down) {
|
||||||
|
adventure_release_bow_attack(player_arena_x, player_arena_facing, @unicorn_ranged_attack);
|
||||||
|
bow_drawing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bow_equipped && bow_drawing) {
|
||||||
|
bow_drawing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sling charge detection
|
||||||
|
if (!bow_equipped && sling_equipped && ctrl_down && !sling_charging) {
|
||||||
|
if (get_personal_count(ITEM_STONES) > 0) {
|
||||||
|
sling_charging = true;
|
||||||
|
sling_charge_timer.restart();
|
||||||
|
sling_sound_handle = p.play_stationary("sounds/weapons/sling_swing.ogg", true);
|
||||||
|
last_sling_stage = -1;
|
||||||
|
} else {
|
||||||
|
speak_ammo_blocked("No stones.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update sling charge state while holding
|
||||||
|
if (sling_charging && ctrl_down) {
|
||||||
|
update_sling_charge();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sling release detection
|
||||||
|
if (sling_charging && !ctrl_down) {
|
||||||
|
adventure_release_sling_attack(player_arena_x, player_arena_facing, @unicorn_ranged_attack);
|
||||||
|
sling_charging = false;
|
||||||
|
safe_destroy_sound(sling_sound_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-sling weapon attacks (existing pattern)
|
||||||
|
if (!bow_equipped && !bow_drawing && !sling_equipped && !sling_charging) {
|
||||||
|
if (fishing_pole_equipped) return;
|
||||||
|
|
||||||
|
int weapon_type = get_unicorn_melee_weapon_type();
|
||||||
|
if (weapon_type == -1) return;
|
||||||
|
|
||||||
|
int attack_cooldown = 1000;
|
||||||
|
if (weapon_type == ADVENTURE_WEAPON_SPEAR) attack_cooldown = 800;
|
||||||
|
if (weapon_type == ADVENTURE_WEAPON_AXE) attack_cooldown = 1600;
|
||||||
|
|
||||||
|
if (ctrl_down && arena_attack_timer.elapsed > attack_cooldown) {
|
||||||
|
arena_attack_timer.restart();
|
||||||
|
play_unicorn_melee_swing(weapon_type);
|
||||||
|
if (unicorn_melee_hit(weapon_type)) {
|
||||||
|
play_unicorn_melee_hit(weapon_type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int get_unicorn_melee_weapon_type() {
|
||||||
|
if (spear_equipped) return ADVENTURE_WEAPON_SPEAR;
|
||||||
|
if (axe_equipped) return ADVENTURE_WEAPON_AXE;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void play_unicorn_melee_swing(int weapon_type) {
|
||||||
|
if (weapon_type == ADVENTURE_WEAPON_SPEAR) {
|
||||||
|
p.play_stationary("sounds/weapons/spear_swing.ogg", false);
|
||||||
|
} else if (weapon_type == ADVENTURE_WEAPON_AXE) {
|
||||||
|
p.play_stationary("sounds/weapons/axe_swing.ogg", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void play_unicorn_melee_hit(int weapon_type) {
|
||||||
|
if (weapon_type == ADVENTURE_WEAPON_SPEAR) {
|
||||||
|
p.play_stationary("sounds/weapons/spear_hit.ogg", false);
|
||||||
|
} else if (weapon_type == ADVENTURE_WEAPON_AXE) {
|
||||||
|
p.play_stationary("sounds/weapons/axe_hit.ogg", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool unicorn_melee_hit(int weapon_type) {
|
||||||
|
int target_support = -1;
|
||||||
|
if (abs(player_arena_x - BRIDGE_START) <= 1) {
|
||||||
|
target_support = 0;
|
||||||
|
} else if (abs(player_arena_x - BRIDGE_END) <= 1) {
|
||||||
|
target_support = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target_support != -1 && bridge_supports_health[target_support] > 0) {
|
||||||
|
if (weapon_type == ADVENTURE_WEAPON_AXE) {
|
||||||
|
bridge_supports_health[target_support] -= AXE_DAMAGE;
|
||||||
|
if (bridge_supports_health[target_support] <= 0) {
|
||||||
|
check_bridge_collapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs(player_arena_x - unicorn.x) <= 1) {
|
||||||
|
int damage = (weapon_type == ADVENTURE_WEAPON_SPEAR) ? SPEAR_DAMAGE : AXE_DAMAGE;
|
||||||
|
apply_unicorn_damage(damage);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int find_unicorn_ranged_target(int player_x, int direction, int range) {
|
||||||
|
int unicorn_distance = (unicorn.x - player_x) * direction;
|
||||||
|
if (unicorn_distance > 0 && unicorn_distance <= range) {
|
||||||
|
return unicorn.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int dist = 1; dist <= range; dist++) {
|
||||||
|
int check_x = player_x + (dist * direction);
|
||||||
|
if (check_x < 0 || check_x >= UNICORN_ARENA_SIZE) break;
|
||||||
|
|
||||||
|
if (bridge_supports_health[0] > 0 && check_x == BRIDGE_START) {
|
||||||
|
return check_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bridge_supports_health[1] > 0 && check_x == BRIDGE_END) {
|
||||||
|
return check_x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int unicorn_ranged_attack(int player_x, int direction, int range, int weapon_type, int damage) {
|
||||||
|
int target_x = find_unicorn_ranged_target(player_x, direction, range);
|
||||||
|
if (target_x == -1) return -1;
|
||||||
|
|
||||||
|
if (target_x == unicorn.x) {
|
||||||
|
apply_unicorn_damage(damage);
|
||||||
|
return target_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target_x == BRIDGE_START || target_x == BRIDGE_END) {
|
||||||
|
int support_index = (target_x == BRIDGE_START) ? 0 : 1;
|
||||||
|
bool can_damage = false;
|
||||||
|
if (weapon_type == ADVENTURE_WEAPON_BOW) {
|
||||||
|
can_damage = UNICORN_BOW_CAN_DAMAGE_SUPPORTS;
|
||||||
|
} else if (weapon_type == ADVENTURE_WEAPON_SLING) {
|
||||||
|
can_damage = UNICORN_SLING_CAN_DAMAGE_SUPPORTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (can_damage && bridge_supports_health[support_index] > 0) {
|
||||||
|
bridge_supports_health[support_index] -= damage;
|
||||||
|
if (bridge_supports_health[support_index] <= 0) {
|
||||||
|
check_bridge_collapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mark_unicorn_defeated(bool by_fall) {
|
||||||
|
if (unicorn_defeated) return;
|
||||||
|
unicorn_defeated = true;
|
||||||
|
unicorn_defeated_by_fall = by_fall;
|
||||||
|
if (by_fall) {
|
||||||
|
unicorn.health = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_unicorn_damage(int damage) {
|
||||||
|
if (damage <= 0) return;
|
||||||
|
if (unicorn.health <= 0) return;
|
||||||
|
|
||||||
|
unicorn.health -= damage;
|
||||||
|
if (unicorn.health <= 0) {
|
||||||
|
unicorn.health = 0;
|
||||||
|
mark_unicorn_defeated(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void play_unicorn_ground_death_sequence() {
|
||||||
|
if (unicorn.sound_handle != -1) {
|
||||||
|
p.destroy_sound(unicorn.sound_handle);
|
||||||
|
unicorn.sound_handle = -1;
|
||||||
|
}
|
||||||
|
p.play_stationary("sounds/actions/hit_ground.ogg", false);
|
||||||
|
wait(800);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_unicorn_weapon_range_audio() {
|
||||||
|
update_weapon_range_audio_with_listener(player_arena_x, unicorn.x, unicorn_in_weapon_range);
|
||||||
|
}
|
||||||
|
|
||||||
void check_bridge_collapse() {
|
void check_bridge_collapse() {
|
||||||
// Bridge collapses when any support is destroyed
|
// Bridge collapses when any support is destroyed
|
||||||
if (bridge_supports_health[0] <= 0 || bridge_supports_health[1] <= 0) {
|
if (bridge_supports_health[0] <= 0 || bridge_supports_health[1] <= 0) {
|
||||||
@@ -339,6 +516,7 @@ void update_unicorn() {
|
|||||||
if (unicorn.x == player_arena_x && player_arena_y == 0) {
|
if (unicorn.x == player_arena_x && player_arena_y == 0) {
|
||||||
player_health -= 10;
|
player_health -= 10;
|
||||||
p.play_stationary("sounds/actions/hit_ground.ogg", false);
|
p.play_stationary("sounds/actions/hit_ground.ogg", false);
|
||||||
|
play_player_damage_sound();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,6 +618,8 @@ void give_unicorn_rewards() {
|
|||||||
rewards.insert_last("You have already mastered the Rune of Swiftness.");
|
rewards.insert_last("You have already mastered the Rune of Swiftness.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
append_adventure_completion_rewards(ADVENTURE_UNICORN, rewards);
|
||||||
|
|
||||||
// Display rewards in text reader
|
// Display rewards in text reader
|
||||||
text_reader_lines(rewards, "Unicorn Victory", true);
|
text_reader_lines(rewards, "Unicorn Victory", true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,9 +281,23 @@ void release_bow_attack(int player_x) {
|
|||||||
bool hit_flying_creature = false;
|
bool hit_flying_creature = false;
|
||||||
bool hit_boar = false;
|
bool hit_boar = false;
|
||||||
int target_x = find_ranged_enemy(player_x, BOW_RANGE, search_direction, true, hit_bandit, hit_boar, hit_flying_creature);
|
int target_x = find_ranged_enemy(player_x, BOW_RANGE, search_direction, true, hit_bandit, hit_boar, hit_flying_creature);
|
||||||
|
bool hit_tree = false;
|
||||||
|
|
||||||
|
if (target_x == -1) {
|
||||||
|
for (int dist = 1; dist <= BOW_RANGE; dist++) {
|
||||||
|
int check_x = player_x + (dist * search_direction);
|
||||||
|
if (check_x < 0 || check_x >= MAP_SIZE) break;
|
||||||
|
Tree@ tree = get_tree_at(check_x);
|
||||||
|
if (tree != null && !tree.is_chopped) {
|
||||||
|
target_x = check_x;
|
||||||
|
hit_tree = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int hit_type = BOW_HIT_NONE;
|
int hit_type = BOW_HIT_NONE;
|
||||||
if (target_x != -1) {
|
if (target_x != -1 && !hit_tree) {
|
||||||
if (hit_bandit) {
|
if (hit_bandit) {
|
||||||
damage_bandit_at(target_x, damage);
|
damage_bandit_at(target_x, damage);
|
||||||
hit_type = BOW_HIT_BANDIT;
|
hit_type = BOW_HIT_BANDIT;
|
||||||
@@ -297,6 +311,8 @@ void release_bow_attack(int player_x) {
|
|||||||
damage_zombie_at(target_x, damage);
|
damage_zombie_at(target_x, damage);
|
||||||
hit_type = BOW_HIT_ZOMBIE;
|
hit_type = BOW_HIT_ZOMBIE;
|
||||||
}
|
}
|
||||||
|
} else if (hit_tree) {
|
||||||
|
hit_type = BOW_HIT_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
int end_x = (target_x != -1)
|
int end_x = (target_x != -1)
|
||||||
@@ -351,7 +367,6 @@ void release_sling_attack(int player_x) {
|
|||||||
if (tree != null && !tree.is_chopped) {
|
if (tree != null && !tree.is_chopped) {
|
||||||
// Stone hits tree but doesn't damage it
|
// Stone hits tree but doesn't damage it
|
||||||
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, check_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
|
play_1d_with_volume_step("sounds/weapons/sling_hit.ogg", player_x, check_x, false, PLAYER_WEAPON_SOUND_VOLUME_STEP);
|
||||||
speak_with_history("Stone hit tree at " + check_x + ".", true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ const int BLESSING_TRIGGER_CHANCE = 10;
|
|||||||
const int BLESSING_WALK_SPEED = 320;
|
const int BLESSING_WALK_SPEED = 320;
|
||||||
const int FISH_WEIGHT_MIN = 1;
|
const int FISH_WEIGHT_MIN = 1;
|
||||||
const int FISH_WEIGHT_MAX = 30;
|
const int FISH_WEIGHT_MAX = 30;
|
||||||
|
// Player sex constants
|
||||||
|
const int SEX_MALE = 0;
|
||||||
|
const int SEX_FEMALE = 1;
|
||||||
|
|
||||||
// Weapon damage
|
// Weapon damage
|
||||||
const int SPEAR_DAMAGE = 3;
|
const int SPEAR_DAMAGE = 3;
|
||||||
@@ -77,7 +80,7 @@ const int BOAR_CHARGE_SPEED = 500; // ms per tile when charging
|
|||||||
const int BOAR_SPAWN_CHANCE_PER_HOUR = 30;
|
const int BOAR_SPAWN_CHANCE_PER_HOUR = 30;
|
||||||
|
|
||||||
// Barricade configuration
|
// Barricade configuration
|
||||||
const int BARRICADE_BASE_HEALTH = 100;
|
const int BARRICADE_BASE_HEALTH = 40;
|
||||||
const int BARRICADE_MAX_HEALTH = 500;
|
const int BARRICADE_MAX_HEALTH = 500;
|
||||||
const int BARRICADE_STICK_COST = 3;
|
const int BARRICADE_STICK_COST = 3;
|
||||||
const int BARRICADE_STICK_HEALTH = 10;
|
const int BARRICADE_STICK_HEALTH = 10;
|
||||||
@@ -247,7 +250,7 @@ const int FALL_DAMAGE_MAX = 4;
|
|||||||
// Base Automation
|
// Base Automation
|
||||||
const int RESIDENT_SLING_COOLDOWN = 4000; // 4 seconds between shots
|
const int RESIDENT_SLING_COOLDOWN = 4000; // 4 seconds between shots
|
||||||
const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour
|
const int RESIDENT_COLLECTION_CHANCE = 10; // 10% chance per basket per hour
|
||||||
const int RESIDENT_FORAGING_CHANCE = 50; // 50% chance per resident per attempt (twice daily)
|
const int RESIDENT_FORAGING_CHANCE = 50; // 50% chance per resident per attempt (daily)
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
int abs(int value) {
|
int abs(int value) {
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ bool try_attack_player_bandit(Bandit@ bandit) {
|
|||||||
} else if (bandit.weapon_type == "axe") {
|
} else if (bandit.weapon_type == "axe") {
|
||||||
play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
}
|
}
|
||||||
|
play_player_damage_sound();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -199,14 +200,13 @@ void try_attack_barricade_bandit(Bandit@ bandit) {
|
|||||||
barricade_health -= damage;
|
barricade_health -= damage;
|
||||||
if (barricade_health < 0) barricade_health = 0;
|
if (barricade_health < 0) barricade_health = 0;
|
||||||
|
|
||||||
// Play weapon swing sound
|
// Play weapon swing sound (barricade hits share a common impact sound)
|
||||||
if (bandit.weapon_type == "spear") {
|
if (bandit.weapon_type == "spear") {
|
||||||
play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
play_creature_attack_sound("sounds/weapons/spear_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
play_creature_attack_sound("sounds/weapons/spear_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
|
||||||
} else if (bandit.weapon_type == "axe") {
|
} else if (bandit.weapon_type == "axe") {
|
||||||
play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
play_creature_attack_sound("sounds/weapons/axe_swing.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
|
||||||
}
|
}
|
||||||
|
play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
|
||||||
|
|
||||||
// Resident defense counter-attack
|
// Resident defense counter-attack
|
||||||
if (can_residents_defend()) {
|
if (can_residents_defend()) {
|
||||||
|
|||||||
@@ -119,12 +119,10 @@ bool try_attack_player_ground_game(GroundGame@ game) {
|
|||||||
game.attack_timer.restart();
|
game.attack_timer.restart();
|
||||||
|
|
||||||
// Attack!
|
// Attack!
|
||||||
// TODO: Add specific boar attack sound? For now re-use zombie hit as generic impact
|
|
||||||
play_creature_attack_sound("sounds/enemies/zombie_hits_player.ogg", x, game.position, BOAR_SOUND_VOLUME_STEP);
|
|
||||||
|
|
||||||
int damage = random(BOAR_DAMAGE_MIN, BOAR_DAMAGE_MAX);
|
int damage = random(BOAR_DAMAGE_MIN, BOAR_DAMAGE_MAX);
|
||||||
player_health -= damage;
|
player_health -= damage;
|
||||||
if (player_health < 0) player_health = 0;
|
if (player_health < 0) player_health = 0;
|
||||||
|
play_player_damage_sound();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ void try_attack_barricade_undead(Undead@ undead) {
|
|||||||
barricade_health -= damage;
|
barricade_health -= damage;
|
||||||
if (barricade_health < 0) barricade_health = 0;
|
if (barricade_health < 0) barricade_health = 0;
|
||||||
|
|
||||||
play_creature_attack_sound("sounds/enemies/zombie_hits_player.ogg", x, undead.position, ZOMBIE_SOUND_VOLUME_STEP);
|
play_creature_attack_sound("sounds/weapons/axe_hit.ogg", x, undead.position, ZOMBIE_SOUND_VOLUME_STEP);
|
||||||
|
|
||||||
// Resident defense counter-attack
|
// Resident defense counter-attack
|
||||||
if (can_residents_defend()) {
|
if (can_residents_defend()) {
|
||||||
@@ -144,7 +144,7 @@ bool try_attack_player_undead(Undead@ undead) {
|
|||||||
player_health = 0;
|
player_health = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
play_creature_attack_sound("sounds/enemies/zombie_hits_player.ogg", x, undead.position, ZOMBIE_SOUND_VOLUME_STEP);
|
play_player_damage_sound();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ void apply_falling_damage(int fall_height) {
|
|||||||
// Apply damage
|
// Apply damage
|
||||||
player_health -= damage;
|
player_health -= damage;
|
||||||
if (player_health < 0) player_health = 0;
|
if (player_health < 0) player_health = 0;
|
||||||
|
if (damage > 0) {
|
||||||
|
play_player_damage_sound();
|
||||||
|
}
|
||||||
|
|
||||||
// Feedback
|
// Feedback
|
||||||
speak_with_history("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true);
|
speak_with_history("Fell " + fall_height + " feet! Took " + damage + " damage. " + player_health + " health remaining.", true);
|
||||||
|
|||||||
308
src/fylgja_system.nvgt
Normal file
308
src/fylgja_system.nvgt
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// Fylgja system
|
||||||
|
// Tracks adventure completion stages, unlocks, and activation
|
||||||
|
|
||||||
|
const int ADVENTURE_UNICORN = 1;
|
||||||
|
const int FYLGJA_STAGE_COUNT = 9;
|
||||||
|
const int FYLGJA_UNICORN = 0;
|
||||||
|
const int UNICORN_TRAMPLE_DAMAGE = 20;
|
||||||
|
|
||||||
|
string[] fylgjaStageNames = {
|
||||||
|
"tenuous",
|
||||||
|
"faint",
|
||||||
|
"stirring",
|
||||||
|
"budding",
|
||||||
|
"kindled",
|
||||||
|
"bound",
|
||||||
|
"sworn",
|
||||||
|
"ascendant",
|
||||||
|
"ultimate"
|
||||||
|
};
|
||||||
|
|
||||||
|
int[] adventureIds = {ADVENTURE_UNICORN};
|
||||||
|
string[] adventureStageTargets = {"unicorn"};
|
||||||
|
int[] adventureCompletionCounts = {0};
|
||||||
|
|
||||||
|
int[] fylgjaAdventureIds = {ADVENTURE_UNICORN};
|
||||||
|
string[] fylgjaNames = {"Unicorn"};
|
||||||
|
|
||||||
|
int lastFylgjaDay = -1;
|
||||||
|
bool fylgjaCharging = false;
|
||||||
|
int fylgjaChargeDirection = 1;
|
||||||
|
timer fylgjaChargeTimer;
|
||||||
|
int fylgjaSoundHandle = -1;
|
||||||
|
string currentFylgjaSound = "";
|
||||||
|
|
||||||
|
void reset_fylgja_state() {
|
||||||
|
lastFylgjaDay = -1;
|
||||||
|
fylgjaCharging = false;
|
||||||
|
fylgjaChargeDirection = 1;
|
||||||
|
currentFylgjaSound = "";
|
||||||
|
if (fylgjaSoundHandle != -1) {
|
||||||
|
p.destroy_sound(fylgjaSoundHandle);
|
||||||
|
fylgjaSoundHandle = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint i = 0; i < adventureCompletionCounts.length(); i++) {
|
||||||
|
adventureCompletionCounts[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_adventure_index(int adventureId) {
|
||||||
|
for (uint i = 0; i < adventureIds.length(); i++) {
|
||||||
|
if (adventureIds[i] == adventureId) return int(i);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_fylgja_index_for_adventure(int adventureId) {
|
||||||
|
for (uint i = 0; i < fylgjaAdventureIds.length(); i++) {
|
||||||
|
if (fylgjaAdventureIds[i] == adventureId) return int(i);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_fylgja_unlocked(int fylgjaIndex) {
|
||||||
|
if (fylgjaIndex < 0 || fylgjaIndex >= int(fylgjaAdventureIds.length())) return false;
|
||||||
|
int adventureIndex = get_adventure_index(fylgjaAdventureIds[fylgjaIndex]);
|
||||||
|
if (adventureIndex < 0) return false;
|
||||||
|
return adventureCompletionCounts[adventureIndex] >= FYLGJA_STAGE_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_unlocked_fylgja_count() {
|
||||||
|
int unlockedCount = 0;
|
||||||
|
for (uint i = 0; i < fylgjaNames.length(); i++) {
|
||||||
|
if (is_fylgja_unlocked(int(i))) unlockedCount++;
|
||||||
|
}
|
||||||
|
return unlockedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_adventure_completion_rewards(int adventureId, string[]@ rewards) {
|
||||||
|
int adventureIndex = get_adventure_index(adventureId);
|
||||||
|
if (adventureIndex < 0 || @rewards == null) return;
|
||||||
|
|
||||||
|
adventureCompletionCounts[adventureIndex]++;
|
||||||
|
int completionCount = adventureCompletionCounts[adventureIndex];
|
||||||
|
|
||||||
|
int stageIndex = completionCount - 1;
|
||||||
|
if (stageIndex < 0) stageIndex = 0;
|
||||||
|
if (stageIndex >= int(fylgjaStageNames.length())) stageIndex = int(fylgjaStageNames.length()) - 1;
|
||||||
|
|
||||||
|
string stageName = fylgjaStageNames[stageIndex];
|
||||||
|
string targetName = adventureStageTargets[adventureIndex];
|
||||||
|
rewards.insert_last("You have a " + stageName + " connection with the " + targetName + ".");
|
||||||
|
|
||||||
|
int fylgjaIndex = get_fylgja_index_for_adventure(adventureId);
|
||||||
|
if (fylgjaIndex == -1) return;
|
||||||
|
|
||||||
|
if (completionCount >= FYLGJA_STAGE_COUNT) {
|
||||||
|
if (completionCount == FYLGJA_STAGE_COUNT) {
|
||||||
|
rewards.insert_last("You have unlocked the " + fylgjaNames[fylgjaIndex] + " Fylgja!");
|
||||||
|
} else {
|
||||||
|
rewards.insert_last("You have already unlocked the " + fylgjaNames[fylgjaIndex] + " Fylgja.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void check_fylgja_menu() {
|
||||||
|
if (!key_pressed(KEY_F)) return;
|
||||||
|
if (fylgjaCharging) return;
|
||||||
|
if (get_unlocked_fylgja_count() == 0) return;
|
||||||
|
|
||||||
|
if (lastFylgjaDay == current_day) {
|
||||||
|
speak_with_history("You have already used your Fylgja today.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
run_fylgja_menu();
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_fylgja_menu() {
|
||||||
|
string[] options;
|
||||||
|
int[] fylgjaIndices;
|
||||||
|
|
||||||
|
for (uint i = 0; i < fylgjaNames.length(); i++) {
|
||||||
|
if (is_fylgja_unlocked(int(i))) {
|
||||||
|
options.insert_last(fylgjaNames[i] + " Fylgja");
|
||||||
|
fylgjaIndices.insert_last(int(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.length() == 0) return;
|
||||||
|
|
||||||
|
speak_with_history("Fylgja menu.", true);
|
||||||
|
|
||||||
|
int selection = 0;
|
||||||
|
speak_with_history(options[selection], true);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
wait(5);
|
||||||
|
menu_background_tick();
|
||||||
|
|
||||||
|
if (key_pressed(KEY_ESCAPE)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_pressed(KEY_DOWN)) {
|
||||||
|
selection++;
|
||||||
|
if (selection >= int(options.length())) selection = 0;
|
||||||
|
speak_with_history(options[selection], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_pressed(KEY_UP)) {
|
||||||
|
selection--;
|
||||||
|
if (selection < 0) selection = int(options.length()) - 1;
|
||||||
|
speak_with_history(options[selection], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key_pressed(KEY_RETURN)) {
|
||||||
|
int chosenIndex = fylgjaIndices[selection];
|
||||||
|
if (activate_fylgja(chosenIndex)) {
|
||||||
|
lastFylgjaDay = current_day;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool activate_fylgja(int fylgjaIndex) {
|
||||||
|
if (fylgjaIndex == FYLGJA_UNICORN) {
|
||||||
|
start_unicorn_fylgja_charge();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_unicorn_fylgja_charge() {
|
||||||
|
reset_fishing_session();
|
||||||
|
|
||||||
|
if (sling_charging) {
|
||||||
|
sling_charging = false;
|
||||||
|
if (sling_sound_handle != -1) {
|
||||||
|
p.destroy_sound(sling_sound_handle);
|
||||||
|
sling_sound_handle = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bow_drawing) {
|
||||||
|
bow_drawing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
searching = false;
|
||||||
|
search_timer.restart();
|
||||||
|
search_delay_timer.restart();
|
||||||
|
|
||||||
|
jumping = false;
|
||||||
|
climbing = false;
|
||||||
|
falling = false;
|
||||||
|
rope_climbing = false;
|
||||||
|
pending_rope_climb_x = -1;
|
||||||
|
pending_rope_climb_elevation = 0;
|
||||||
|
y = get_mountain_elevation_at(x);
|
||||||
|
|
||||||
|
fylgjaCharging = true;
|
||||||
|
fylgjaChargeDirection = facing;
|
||||||
|
fylgjaChargeTimer.restart();
|
||||||
|
currentFylgjaSound = "";
|
||||||
|
update_fylgja_charge_audio();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool should_stop_charge_for_climb_up(int fromX, int toX) {
|
||||||
|
MountainRange@ mountain = get_mountain_at(toX);
|
||||||
|
if (mountain is null) return false;
|
||||||
|
|
||||||
|
int elevationChange = mountain.get_elevation_change(fromX, toX);
|
||||||
|
return elevationChange >= MOUNTAIN_STEEP_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply_fylgja_trample_damage(int posX) {
|
||||||
|
damage_undead_at(posX, UNICORN_TRAMPLE_DAMAGE);
|
||||||
|
damage_bandit_at(posX, UNICORN_TRAMPLE_DAMAGE);
|
||||||
|
damage_boar_at(posX, UNICORN_TRAMPLE_DAMAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
string get_unicorn_charge_sound(int posX) {
|
||||||
|
string terrain = get_terrain_at_position(posX);
|
||||||
|
if (terrain == "stone" || terrain == "gravel") {
|
||||||
|
return "sounds/bosses/unicorn/unicorn_on_bridge.ogg";
|
||||||
|
}
|
||||||
|
return "sounds/bosses/unicorn/unicorn_galloping.ogg";
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_fylgja_charge_audio() {
|
||||||
|
string soundFile = get_unicorn_charge_sound(x);
|
||||||
|
bool needNewSound = (fylgjaSoundHandle == -1 || !p.sound_is_active(fylgjaSoundHandle) || currentFylgjaSound != soundFile);
|
||||||
|
|
||||||
|
if (needNewSound) {
|
||||||
|
if (fylgjaSoundHandle != -1) {
|
||||||
|
p.destroy_sound(fylgjaSoundHandle);
|
||||||
|
fylgjaSoundHandle = -1;
|
||||||
|
}
|
||||||
|
fylgjaSoundHandle = play_1d_with_volume_step(soundFile, x, x, true, UNICORN_SOUND_VOLUME_STEP);
|
||||||
|
currentFylgjaSound = soundFile;
|
||||||
|
} else {
|
||||||
|
p.update_sound_1d(fylgjaSoundHandle, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void stop_fylgja_charge() {
|
||||||
|
fylgjaCharging = false;
|
||||||
|
currentFylgjaSound = "";
|
||||||
|
if (fylgjaSoundHandle != -1) {
|
||||||
|
p.destroy_sound(fylgjaSoundHandle);
|
||||||
|
fylgjaSoundHandle = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_fylgja_charge() {
|
||||||
|
if (!fylgjaCharging) return;
|
||||||
|
|
||||||
|
if (key_down(KEY_LSHIFT) || key_down(KEY_RSHIFT)) {
|
||||||
|
stop_fylgja_charge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x <= BASE_END || x <= 0 || x >= MAP_SIZE - 1) {
|
||||||
|
stop_fylgja_charge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fylgjaChargeTimer.elapsed >= UNICORN_SPEED) {
|
||||||
|
fylgjaChargeTimer.restart();
|
||||||
|
int step = (fylgjaChargeDirection == 1) ? 1 : -1;
|
||||||
|
int targetX = x + step;
|
||||||
|
|
||||||
|
if (targetX < 0 || targetX >= MAP_SIZE) {
|
||||||
|
stop_fylgja_charge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (should_stop_charge_for_climb_up(x, targetX)) {
|
||||||
|
stop_fylgja_charge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int previousY = y;
|
||||||
|
x = targetX;
|
||||||
|
int targetElevation = get_mountain_elevation_at(x);
|
||||||
|
if (targetElevation != y) {
|
||||||
|
y = targetElevation;
|
||||||
|
if (targetElevation < previousY) {
|
||||||
|
int fallHeight = previousY - targetElevation;
|
||||||
|
apply_falling_damage(fallHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check_snare_collision(x);
|
||||||
|
apply_fylgja_trample_damage(x);
|
||||||
|
|
||||||
|
if (x <= BASE_END || x <= 0 || x >= MAP_SIZE - 1) {
|
||||||
|
stop_fylgja_charge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_fylgja_charge_audio();
|
||||||
|
update_fishing();
|
||||||
|
update_bow_shot();
|
||||||
|
p.update_listener_1d(x);
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ int equipped_feet = EQUIP_NONE;
|
|||||||
|
|
||||||
// Quick slots
|
// Quick slots
|
||||||
int[] quick_slots;
|
int[] quick_slots;
|
||||||
|
int[] quick_slot_runes;
|
||||||
int[] item_count_slots;
|
int[] item_count_slots;
|
||||||
|
|
||||||
void reset_quick_slots() {
|
void reset_quick_slots() {
|
||||||
@@ -47,6 +48,11 @@ void reset_quick_slots() {
|
|||||||
quick_slots[i] = -1;
|
quick_slots[i] = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quick_slot_runes.resize(10);
|
||||||
|
for (uint i = 0; i < quick_slot_runes.length(); i++) {
|
||||||
|
quick_slot_runes[i] = RUNE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
item_count_slots.resize(10);
|
item_count_slots.resize(10);
|
||||||
for (uint i = 0; i < item_count_slots.length(); i++) {
|
for (uint i = 0; i < item_count_slots.length(); i++) {
|
||||||
item_count_slots[i] = -1;
|
item_count_slots[i] = -1;
|
||||||
@@ -272,6 +278,10 @@ void activate_quick_slot(int slot_index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int equip_type = quick_slots[slot_index];
|
int equip_type = quick_slots[slot_index];
|
||||||
|
int rune_type = RUNE_NONE;
|
||||||
|
if (slot_index >= 0 && slot_index < int(quick_slot_runes.length())) {
|
||||||
|
rune_type = quick_slot_runes[slot_index];
|
||||||
|
}
|
||||||
if (equip_type < 0) {
|
if (equip_type < 0) {
|
||||||
int item_type = -1;
|
int item_type = -1;
|
||||||
if (slot_index >= 0 && slot_index < int(item_count_slots.length())) {
|
if (slot_index >= 0 && slot_index < int(item_count_slots.length())) {
|
||||||
@@ -287,6 +297,16 @@ void activate_quick_slot(int slot_index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (equipment_is_equipped(equip_type)) {
|
if (equipment_is_equipped(equip_type)) {
|
||||||
|
if (get_equipped_rune_for_slot(equip_type) != rune_type) {
|
||||||
|
if (rune_type != RUNE_NONE && get_runed_item_count(equip_type, rune_type) <= 0) {
|
||||||
|
speak_with_history("Item not available.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set_equipped_rune_for_slot(equip_type, rune_type);
|
||||||
|
update_max_health_from_equipment();
|
||||||
|
speak_with_history(get_equipment_name(equip_type) + " equipped.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
unequip_equipment_type(equip_type);
|
unequip_equipment_type(equip_type);
|
||||||
clear_equipped_rune_for_slot(equip_type);
|
clear_equipped_rune_for_slot(equip_type);
|
||||||
update_max_health_from_equipment();
|
update_max_health_from_equipment();
|
||||||
@@ -294,12 +314,18 @@ void activate_quick_slot(int slot_index) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!equipment_available(equip_type)) {
|
if (rune_type != RUNE_NONE) {
|
||||||
|
if (get_runed_item_count(equip_type, rune_type) <= 0) {
|
||||||
|
speak_with_history("Item not available.", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (!equipment_available(equip_type)) {
|
||||||
speak_with_history("Item not available.", true);
|
speak_with_history("Item not available.", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
equip_equipment_type(equip_type);
|
equip_equipment_type(equip_type);
|
||||||
|
set_equipped_rune_for_slot(equip_type, rune_type);
|
||||||
update_max_health_from_equipment();
|
update_max_health_from_equipment();
|
||||||
speak_with_history(get_equipment_name(equip_type) + " equipped.", true);
|
speak_with_history(get_equipment_name(equip_type) + " equipped.", true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ void show_character_info() {
|
|||||||
else missing_slots.insert_last("feet");
|
else missing_slots.insert_last("feet");
|
||||||
|
|
||||||
string info = "Character info. ";
|
string info = "Character info. ";
|
||||||
|
if (player_name != "") {
|
||||||
|
string sex_label = (player_sex == SEX_FEMALE) ? "Female" : "Male";
|
||||||
|
info += "Name " + player_name + ". Sex " + sex_label + ". ";
|
||||||
|
}
|
||||||
info += "Health " + player_health + " of " + max_health + ". ";
|
info += "Health " + player_health + " of " + max_health + ". ";
|
||||||
info += "Weapon " + get_equipped_weapon_name() + ". ";
|
info += "Weapon " + get_equipped_weapon_name() + ". ";
|
||||||
if (equipped_clothing.length() > 0) {
|
if (equipped_clothing.length() > 0) {
|
||||||
|
|||||||
@@ -192,6 +192,9 @@ void run_equipment_menu() {
|
|||||||
int equip_type = equipment_types[filtered_indices[selection]];
|
int equip_type = equipment_types[filtered_indices[selection]];
|
||||||
int rune_type = rune_types[filtered_indices[selection]];
|
int rune_type = rune_types[filtered_indices[selection]];
|
||||||
quick_slots[slot_index] = equip_type;
|
quick_slots[slot_index] = equip_type;
|
||||||
|
if (slot_index >= 0 && slot_index < int(quick_slot_runes.length())) {
|
||||||
|
quick_slot_runes[slot_index] = rune_type;
|
||||||
|
}
|
||||||
string name = get_full_equipment_name(equip_type, rune_type);
|
string name = get_full_equipment_name(equip_type, rune_type);
|
||||||
speak_with_history(name + " set to slot " + slot_index + ".", true);
|
speak_with_history(name + " set to slot " + slot_index + ".", true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
int x = 0;
|
int x = 0;
|
||||||
int y = 0;
|
int y = 0;
|
||||||
int facing = 1; // 0: left, 1: right
|
int facing = 1; // 0: left, 1: right
|
||||||
|
string player_name = "";
|
||||||
|
int player_sex = SEX_MALE;
|
||||||
bool jumping = false;
|
bool jumping = false;
|
||||||
bool climbing = false;
|
bool climbing = false;
|
||||||
bool falling = false;
|
bool falling = false;
|
||||||
@@ -85,7 +87,6 @@ timer jumptimer;
|
|||||||
timer search_timer;
|
timer search_timer;
|
||||||
timer search_delay_timer;
|
timer search_delay_timer;
|
||||||
timer attack_timer;
|
timer attack_timer;
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
bool searching = false;
|
bool searching = false;
|
||||||
|
|
||||||
@@ -104,6 +105,7 @@ void restart_all_timers() {
|
|||||||
sling_charge_timer.restart();
|
sling_charge_timer.restart();
|
||||||
bow_draw_timer.restart();
|
bow_draw_timer.restart();
|
||||||
bow_shot_timer.restart();
|
bow_shot_timer.restart();
|
||||||
|
fylgjaChargeTimer.restart();
|
||||||
|
|
||||||
// Fire fuel timers
|
// Fire fuel timers
|
||||||
for (uint i = 0; i < world_fires.length(); i++) {
|
for (uint i = 0; i < world_fires.length(); i++) {
|
||||||
|
|||||||
@@ -1,12 +1,35 @@
|
|||||||
// Save system
|
// Save system
|
||||||
|
|
||||||
const string SAVE_FILE_PATH = "save.dat";
|
const string SAVE_EXTENSION = ".dat";
|
||||||
const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1";
|
const string SAVE_ENCRYPTION_KEY = "draugnorak_save_v1";
|
||||||
const int SAVE_VERSION = 2;
|
const int SAVE_VERSION = 3;
|
||||||
string last_save_error = "";
|
string last_save_error = "";
|
||||||
|
string current_save_file = "";
|
||||||
|
|
||||||
|
string[] get_save_files() {
|
||||||
|
string[] result;
|
||||||
|
string[]@ items = glob("*" + SAVE_EXTENSION);
|
||||||
|
if (@items == null) return result;
|
||||||
|
|
||||||
|
for (uint i = 0; i < items.length(); i++) {
|
||||||
|
string item = items[i];
|
||||||
|
if (item.length() >= SAVE_EXTENSION.length() &&
|
||||||
|
item.substr(item.length() - SAVE_EXTENSION.length()) == SAVE_EXTENSION) {
|
||||||
|
result.insert_last(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.length() > 1) {
|
||||||
|
result.sort(sort_string_case_insensitive);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sort_string_case_insensitive(const string &in a, const string &in b) {
|
||||||
|
return a.lower() < b.lower();
|
||||||
|
}
|
||||||
|
|
||||||
bool has_save_game() {
|
bool has_save_game() {
|
||||||
return file_exists(SAVE_FILE_PATH);
|
return get_save_files().length() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
string encrypt_save_data(const string&in rawData) {
|
string encrypt_save_data(const string&in rawData) {
|
||||||
@@ -93,6 +116,345 @@ string[] get_string_list(dictionary@ data, const string&in key) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string flatten_exception_text(const string&in text) {
|
||||||
|
string result = "";
|
||||||
|
bool lastWasSpace = false;
|
||||||
|
for (uint i = 0; i < text.length(); i++) {
|
||||||
|
string ch = text.substr(i, 1);
|
||||||
|
if (ch == "\r" || ch == "\n") {
|
||||||
|
if (!lastWasSpace) {
|
||||||
|
result += " | ";
|
||||||
|
lastWasSpace = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result += ch;
|
||||||
|
lastWasSpace = false;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
string format_log_timestamp() {
|
||||||
|
datetime dt;
|
||||||
|
string stamp = dt.format(DATE_TIME_FORMAT_RFC1123);
|
||||||
|
return "[" + stamp + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
void log_unhandled_exception(const string&in context) {
|
||||||
|
string info = get_exception_info();
|
||||||
|
string filePath = get_exception_file();
|
||||||
|
int line = get_exception_line();
|
||||||
|
string func = get_exception_function();
|
||||||
|
string stack = flatten_exception_text(last_exception_call_stack);
|
||||||
|
|
||||||
|
string message = "Unhandled exception";
|
||||||
|
if (context != "") message += " (" + context + ")";
|
||||||
|
if (info != "") message += ": " + info;
|
||||||
|
if (filePath != "") message += " at " + filePath;
|
||||||
|
if (line > 0) message += ":" + line;
|
||||||
|
if (func != "") message += " in " + func;
|
||||||
|
if (stack != "") message += " | stack: " + stack;
|
||||||
|
message += " " + format_log_timestamp();
|
||||||
|
|
||||||
|
file logFile;
|
||||||
|
if (logFile.open("crash.log", "ab")) {
|
||||||
|
logFile.write(message + "\r\n");
|
||||||
|
logFile.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string normalize_player_name(string name) {
|
||||||
|
string result = "";
|
||||||
|
bool lastWasSpace = true;
|
||||||
|
for (uint i = 0; i < name.length(); i++) {
|
||||||
|
string ch = name.substr(i, 1);
|
||||||
|
bool isSpace = (ch == " " || ch == "\t" || ch == "\r" || ch == "\n");
|
||||||
|
if (isSpace) {
|
||||||
|
if (!lastWasSpace) {
|
||||||
|
result += " ";
|
||||||
|
lastWasSpace = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result += ch;
|
||||||
|
lastWasSpace = false;
|
||||||
|
}
|
||||||
|
if (result.length() > 0 && result.substr(result.length() - 1) == " ") {
|
||||||
|
result = result.substr(0, result.length() - 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_windows_reserved_name(const string&in upperName) {
|
||||||
|
if (upperName == "CON" || upperName == "PRN" || upperName == "AUX" || upperName == "NUL") return true;
|
||||||
|
if (upperName.length() == 4 && upperName.substr(0, 3) == "COM") {
|
||||||
|
int num = parse_int(upperName.substr(3));
|
||||||
|
if (num >= 1 && num <= 9) return true;
|
||||||
|
}
|
||||||
|
if (upperName.length() == 4 && upperName.substr(0, 3) == "LPT") {
|
||||||
|
int num = parse_int(upperName.substr(3));
|
||||||
|
if (num >= 1 && num <= 9) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string sanitize_save_filename_base(string name) {
|
||||||
|
string normalized = normalize_player_name(name);
|
||||||
|
string result = "";
|
||||||
|
bool lastSeparator = false;
|
||||||
|
|
||||||
|
for (uint i = 0; i < normalized.length(); i++) {
|
||||||
|
string ch = normalized.substr(i, 1);
|
||||||
|
bool isUpper = (ch >= "A" && ch <= "Z");
|
||||||
|
bool isLower = (ch >= "a" && ch <= "z");
|
||||||
|
bool isDigit = (ch >= "0" && ch <= "9");
|
||||||
|
if (isUpper || isLower || isDigit) {
|
||||||
|
result += ch;
|
||||||
|
lastSeparator = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch == " " || ch == "_" || ch == "-") {
|
||||||
|
if (!lastSeparator && result.length() > 0) {
|
||||||
|
result += "_";
|
||||||
|
lastSeparator = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (result.length() > 0 && result.substr(result.length() - 1) == "_") {
|
||||||
|
result = result.substr(0, result.length() - 1);
|
||||||
|
}
|
||||||
|
if (result.length() == 0) {
|
||||||
|
result = "character";
|
||||||
|
}
|
||||||
|
if (result.length() > 40) {
|
||||||
|
result = result.substr(0, 40);
|
||||||
|
}
|
||||||
|
while (result.length() > 0 && result.substr(result.length() - 1) == "_") {
|
||||||
|
result = result.substr(0, result.length() - 1);
|
||||||
|
}
|
||||||
|
string upperName = result.upper();
|
||||||
|
if (upperName == "." || upperName == "..") {
|
||||||
|
result = "character";
|
||||||
|
upperName = result.upper();
|
||||||
|
}
|
||||||
|
if (is_windows_reserved_name(upperName)) {
|
||||||
|
result = "save_" + result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
string get_save_filename_for_name(const string&in name) {
|
||||||
|
return sanitize_save_filename_base(name) + SAVE_EXTENSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
string strip_save_extension(const string&in filename) {
|
||||||
|
if (filename.length() >= SAVE_EXTENSION.length() &&
|
||||||
|
filename.substr(filename.length() - SAVE_EXTENSION.length()) == SAVE_EXTENSION) {
|
||||||
|
return filename.substr(0, filename.length() - SAVE_EXTENSION.length());
|
||||||
|
}
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool read_save_metadata(const string&in filename, string &out displayName, int &out sex, int &out day) {
|
||||||
|
string encryptedData;
|
||||||
|
if (!read_file_string(filename, encryptedData)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string rawData = decrypt_save_data(encryptedData);
|
||||||
|
dictionary@ saveData = deserialize(rawData);
|
||||||
|
if (@saveData == null || !dictionary_has_keys(saveData)) {
|
||||||
|
saveData = deserialize(encryptedData);
|
||||||
|
}
|
||||||
|
if (@saveData == null || !dictionary_has_keys(saveData)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayName = "";
|
||||||
|
if (!saveData.get("player_name", displayName)) {
|
||||||
|
displayName = "";
|
||||||
|
}
|
||||||
|
sex = int(get_number(saveData, "player_sex", SEX_MALE));
|
||||||
|
if (sex != SEX_FEMALE) sex = SEX_MALE;
|
||||||
|
day = int(get_number(saveData, "time_current_day", 1));
|
||||||
|
if (day < 1) day = 1;
|
||||||
|
if (displayName == "") {
|
||||||
|
displayName = strip_save_extension(filename);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_name_used(const string&in name, const string[]@ usedNames) {
|
||||||
|
string target = name.lower();
|
||||||
|
if (@usedNames != null) {
|
||||||
|
for (uint i = 0; i < usedNames.length(); i++) {
|
||||||
|
if (usedNames[i].lower() == target) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (file_exists(get_save_filename_for_name(name))) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string pick_random_name(const string[]@ pool, const string[]@ usedNames) {
|
||||||
|
string[] available;
|
||||||
|
if (@pool != null) {
|
||||||
|
for (uint i = 0; i < pool.length(); i++) {
|
||||||
|
if (!is_name_used(pool[i], usedNames)) {
|
||||||
|
available.insert_last(pool[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const string[]@ pickFrom = @available;
|
||||||
|
if (available.length() == 0) {
|
||||||
|
@pickFrom = pool;
|
||||||
|
}
|
||||||
|
if (@pickFrom == null || pickFrom.length() == 0) return "character";
|
||||||
|
int index = random(0, int(pickFrom.length()) - 1);
|
||||||
|
return pickFrom[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] get_existing_character_names() {
|
||||||
|
string[] names;
|
||||||
|
string[] files = get_save_files();
|
||||||
|
for (uint i = 0; i < files.length(); i++) {
|
||||||
|
string displayName;
|
||||||
|
int sex = SEX_MALE;
|
||||||
|
int day = 1;
|
||||||
|
if (read_save_metadata(files[i], displayName, sex, day)) {
|
||||||
|
if (displayName.length() > 0) {
|
||||||
|
names.insert_last(displayName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] male_name_pool = {
|
||||||
|
"Arne", "Asbjorn", "Aegir", "Bjorn", "Brand", "Egil", "Einar", "Eirik", "Erik", "Gunnar",
|
||||||
|
"Gudmund", "Hakon", "Halfdan", "Hallvard", "Harald", "Hjalmar", "Hrafn", "Hrolf", "Ivar", "Ketil",
|
||||||
|
"Knut", "Leif", "Magnus", "Njord", "Odd", "Olaf", "Orm", "Ragnar", "Roald", "Rolf",
|
||||||
|
"Sigurd", "Sten", "Stig", "Sven", "Svend", "Thor", "Toke", "Torbjorn", "Torstein", "Trygve",
|
||||||
|
"Ulf", "Ulrik", "Valdemar", "Vidar", "Yngvar", "Haldor", "Skjold", "Eystein", "Gorm", "Havard"
|
||||||
|
};
|
||||||
|
|
||||||
|
string[] female_name_pool = {
|
||||||
|
"Astrid", "Asta", "Birgit", "Brynhild", "Dagny", "Eira", "Freya", "Frida", "Gerda", "Gudrun",
|
||||||
|
"Gunhild", "Halla", "Helga", "Hild", "Hilda", "Inga", "Ingrid", "Kari", "Lagertha", "Liv",
|
||||||
|
"Ragna", "Ragnhild", "Randi", "Runa", "Sif", "Signy", "Sigrid", "Solveig", "Sunniva", "Thora",
|
||||||
|
"Thyra", "Tora", "Tove", "Tyrna", "Ulla", "Yrsa", "Ylva", "Aud", "Eydis", "Herdis",
|
||||||
|
"Ingunn", "Jorunn", "Ragnheid", "Sigrun", "Torhild", "Ase", "Alfhild", "Gudlaug", "Katra", "Rikissa"
|
||||||
|
};
|
||||||
|
|
||||||
|
string pick_random_name_for_sex(int sex, const string[]@ usedNames) {
|
||||||
|
if (sex == SEX_FEMALE) {
|
||||||
|
return pick_random_name(female_name_pool, usedNames);
|
||||||
|
}
|
||||||
|
return pick_random_name(male_name_pool, usedNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool select_player_sex(int &out sex) {
|
||||||
|
string[] options = {"Male", "Female"};
|
||||||
|
int selection = 0;
|
||||||
|
string prompt = "Choose your character's sex.";
|
||||||
|
speak_with_history(prompt + " " + options[selection], true);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
wait(5);
|
||||||
|
if (key_pressed(KEY_DOWN)) {
|
||||||
|
selection++;
|
||||||
|
if (selection >= options.length()) selection = 0;
|
||||||
|
speak_with_history(prompt + " " + options[selection], true);
|
||||||
|
}
|
||||||
|
if (key_pressed(KEY_UP)) {
|
||||||
|
selection--;
|
||||||
|
if (selection < 0) selection = options.length() - 1;
|
||||||
|
speak_with_history(prompt + " " + options[selection], true);
|
||||||
|
}
|
||||||
|
if (key_pressed(KEY_RETURN)) {
|
||||||
|
sex = (selection == 1) ? SEX_FEMALE : SEX_MALE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key_pressed(KEY_ESCAPE)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setup_new_character() {
|
||||||
|
int selectedSex = SEX_MALE;
|
||||||
|
if (!select_player_sex(selectedSex)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] existingNames = get_existing_character_names();
|
||||||
|
while (true) {
|
||||||
|
string entered = ui_input_box("Draugnorak", "Enter your name or press Enter for random.", "");
|
||||||
|
string normalized = normalize_player_name(entered);
|
||||||
|
if (normalized.length() == 0) {
|
||||||
|
normalized = pick_random_name_for_sex(selectedSex, existingNames);
|
||||||
|
}
|
||||||
|
string saveFile = get_save_filename_for_name(normalized);
|
||||||
|
if (file_exists(saveFile)) {
|
||||||
|
int confirm = ui_question("", "Save found for " + normalized + ". Overwrite?");
|
||||||
|
if (confirm != 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
player_name = normalized;
|
||||||
|
player_sex = selectedSex;
|
||||||
|
current_save_file = saveFile;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool select_save_file(string &out filename) {
|
||||||
|
string[] files = get_save_files();
|
||||||
|
if (files.length() == 0) return false;
|
||||||
|
|
||||||
|
string[] options;
|
||||||
|
for (uint i = 0; i < files.length(); i++) {
|
||||||
|
string displayName;
|
||||||
|
int sex = SEX_MALE;
|
||||||
|
int day = 1;
|
||||||
|
if (!read_save_metadata(files[i], displayName, sex, day)) {
|
||||||
|
displayName = strip_save_extension(files[i]);
|
||||||
|
options.insert_last(displayName);
|
||||||
|
} else {
|
||||||
|
string sex_label = (sex == SEX_FEMALE) ? "female" : "male";
|
||||||
|
options.insert_last(displayName + ", " + sex_label + ", day " + day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int selection = 0;
|
||||||
|
speak_with_history("Load game. Select character.", true);
|
||||||
|
speak_with_history(options[selection], true);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
wait(5);
|
||||||
|
if (key_pressed(KEY_DOWN)) {
|
||||||
|
selection++;
|
||||||
|
if (selection >= options.length()) selection = 0;
|
||||||
|
speak_with_history(options[selection], true);
|
||||||
|
}
|
||||||
|
if (key_pressed(KEY_UP)) {
|
||||||
|
selection--;
|
||||||
|
if (selection < 0) selection = int(options.length()) - 1;
|
||||||
|
speak_with_history(options[selection], true);
|
||||||
|
}
|
||||||
|
if (key_pressed(KEY_RETURN)) {
|
||||||
|
filename = files[selection];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key_pressed(KEY_ESCAPE)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void stop_active_sounds() {
|
void stop_active_sounds() {
|
||||||
if (day_sound_handle != -1) {
|
if (day_sound_handle != -1) {
|
||||||
p.destroy_sound(day_sound_handle);
|
p.destroy_sound(day_sound_handle);
|
||||||
@@ -217,6 +579,7 @@ void reset_game_state() {
|
|||||||
incense_burning = false;
|
incense_burning = false;
|
||||||
blessing_speed_active = false;
|
blessing_speed_active = false;
|
||||||
blessing_resident_active = false;
|
blessing_resident_active = false;
|
||||||
|
reset_fylgja_state();
|
||||||
|
|
||||||
// Reset inventory using the registry system
|
// Reset inventory using the registry system
|
||||||
reset_inventory();
|
reset_inventory();
|
||||||
@@ -283,6 +646,12 @@ void start_new_game() {
|
|||||||
init_barricade();
|
init_barricade();
|
||||||
init_time();
|
init_time();
|
||||||
init_weather();
|
init_weather();
|
||||||
|
if (player_name.length() == 0) {
|
||||||
|
player_name = "Character";
|
||||||
|
}
|
||||||
|
if (current_save_file == "") {
|
||||||
|
current_save_file = get_save_filename_for_name(player_name);
|
||||||
|
}
|
||||||
save_game_state();
|
save_game_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,8 +767,11 @@ bool save_game_state() {
|
|||||||
saveData.set("player_health", player_health);
|
saveData.set("player_health", player_health);
|
||||||
saveData.set("player_base_health", base_max_health);
|
saveData.set("player_base_health", base_max_health);
|
||||||
saveData.set("player_max_health", max_health);
|
saveData.set("player_max_health", max_health);
|
||||||
|
saveData.set("player_name", player_name);
|
||||||
|
saveData.set("player_sex", player_sex);
|
||||||
saveData.set("player_favor", favor);
|
saveData.set("player_favor", favor);
|
||||||
saveData.set("player_last_adventure_day", last_adventure_day);
|
saveData.set("player_last_adventure_day", last_adventure_day);
|
||||||
|
saveData.set("adventure_completion_counts", serialize_inventory_array(adventureCompletionCounts));
|
||||||
saveData.set("incense_hours_remaining", incense_hours_remaining);
|
saveData.set("incense_hours_remaining", incense_hours_remaining);
|
||||||
saveData.set("incense_burning", incense_burning);
|
saveData.set("incense_burning", incense_burning);
|
||||||
|
|
||||||
@@ -435,6 +807,11 @@ bool save_game_state() {
|
|||||||
quickSlotData.insert_last("" + quick_slots[i]);
|
quickSlotData.insert_last("" + quick_slots[i]);
|
||||||
}
|
}
|
||||||
saveData.set("equipment_quick_slots", join_string_array(quickSlotData));
|
saveData.set("equipment_quick_slots", join_string_array(quickSlotData));
|
||||||
|
string[] quickSlotRuneData;
|
||||||
|
for (uint i = 0; i < quick_slot_runes.length(); i++) {
|
||||||
|
quickSlotRuneData.insert_last("" + quick_slot_runes[i]);
|
||||||
|
}
|
||||||
|
saveData.set("equipment_quick_slot_runes", join_string_array(quickSlotRuneData));
|
||||||
|
|
||||||
string[] itemCountSlotData;
|
string[] itemCountSlotData;
|
||||||
for (uint i = 0; i < item_count_slots.length(); i++) {
|
for (uint i = 0; i < item_count_slots.length(); i++) {
|
||||||
@@ -585,20 +962,23 @@ bool save_game_state() {
|
|||||||
}
|
}
|
||||||
saveData.set("drops_data", join_string_array(dropData));
|
saveData.set("drops_data", join_string_array(dropData));
|
||||||
|
|
||||||
|
if (current_save_file == "") {
|
||||||
|
current_save_file = get_save_filename_for_name(player_name);
|
||||||
|
}
|
||||||
string rawData = saveData.serialize();
|
string rawData = saveData.serialize();
|
||||||
string encryptedData = encrypt_save_data(rawData);
|
string encryptedData = encrypt_save_data(rawData);
|
||||||
return save_data(SAVE_FILE_PATH, encryptedData);
|
return save_data(current_save_file, encryptedData);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool load_game_state() {
|
bool load_game_state_from_file(const string&in filename) {
|
||||||
last_save_error = "";
|
last_save_error = "";
|
||||||
if (!file_exists(SAVE_FILE_PATH)) {
|
if (!file_exists(filename)) {
|
||||||
last_save_error = "No save file found.";
|
last_save_error = "No save file found.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
string encryptedData;
|
string encryptedData;
|
||||||
if (!read_file_string(SAVE_FILE_PATH, encryptedData)) {
|
if (!read_file_string(filename, encryptedData)) {
|
||||||
last_save_error = "Unable to read save file.";
|
last_save_error = "Unable to read save file.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -623,6 +1003,7 @@ bool load_game_state() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset_game_state();
|
reset_game_state();
|
||||||
|
current_save_file = filename;
|
||||||
|
|
||||||
MAP_SIZE = int(get_number(saveData, "world_map_size", 35));
|
MAP_SIZE = int(get_number(saveData, "world_map_size", 35));
|
||||||
expanded_area_start = int(get_number(saveData, "world_expanded_area_start", -1));
|
expanded_area_start = int(get_number(saveData, "world_expanded_area_start", -1));
|
||||||
@@ -651,8 +1032,20 @@ bool load_game_state() {
|
|||||||
player_health = int(get_number(saveData, "player_health", 10));
|
player_health = int(get_number(saveData, "player_health", 10));
|
||||||
max_health = int(get_number(saveData, "player_max_health", 10));
|
max_health = int(get_number(saveData, "player_max_health", 10));
|
||||||
base_max_health = int(get_number(saveData, "player_base_health", max_health));
|
base_max_health = int(get_number(saveData, "player_base_health", max_health));
|
||||||
|
string loadedName;
|
||||||
|
if (saveData.get("player_name", loadedName)) {
|
||||||
|
player_name = loadedName;
|
||||||
|
} else {
|
||||||
|
player_name = strip_save_extension(filename);
|
||||||
|
}
|
||||||
|
player_sex = int(get_number(saveData, "player_sex", SEX_MALE));
|
||||||
|
if (player_sex != SEX_FEMALE) player_sex = SEX_MALE;
|
||||||
favor = get_number(saveData, "player_favor", 0.0);
|
favor = get_number(saveData, "player_favor", 0.0);
|
||||||
last_adventure_day = int(get_number(saveData, "player_last_adventure_day", -1));
|
last_adventure_day = int(get_number(saveData, "player_last_adventure_day", -1));
|
||||||
|
string adventureCountsStr;
|
||||||
|
if (saveData.get("adventure_completion_counts", adventureCountsStr)) {
|
||||||
|
deserialize_inventory_array(adventureCountsStr, adventureCompletionCounts, int(adventureIds.length()));
|
||||||
|
}
|
||||||
incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0));
|
incense_hours_remaining = int(get_number(saveData, "incense_hours_remaining", 0));
|
||||||
incense_burning = get_bool(saveData, "incense_burning", false);
|
incense_burning = get_bool(saveData, "incense_burning", false);
|
||||||
if (incense_hours_remaining > 0) incense_burning = true;
|
if (incense_hours_remaining > 0) incense_burning = true;
|
||||||
@@ -757,6 +1150,15 @@ bool load_game_state() {
|
|||||||
quick_slots[i] = slot_value;
|
quick_slots[i] = slot_value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
string[] loadedQuickSlotRunes = get_string_list_or_split(saveData, "equipment_quick_slot_runes");
|
||||||
|
uint rune_slot_count = loadedQuickSlotRunes.length();
|
||||||
|
if (rune_slot_count > quick_slot_runes.length()) rune_slot_count = quick_slot_runes.length();
|
||||||
|
for (uint i = 0; i < rune_slot_count; i++) {
|
||||||
|
int slot_value = parse_int(loadedQuickSlotRunes[i]);
|
||||||
|
if (slot_value >= RUNE_NONE) {
|
||||||
|
quick_slot_runes[i] = slot_value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
string[] loadedItemCountSlots = get_string_list_or_split(saveData, "item_count_slots");
|
string[] loadedItemCountSlots = get_string_list_or_split(saveData, "item_count_slots");
|
||||||
uint count_slot_count = loadedItemCountSlots.length();
|
uint count_slot_count = loadedItemCountSlots.length();
|
||||||
@@ -1085,3 +1487,11 @@ bool load_game_state() {
|
|||||||
update_ambience(true);
|
update_ambience(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool load_game_state() {
|
||||||
|
if (current_save_file == "") {
|
||||||
|
last_save_error = "No save selected.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return load_game_state_from_file(current_save_file);
|
||||||
|
}
|
||||||
|
|||||||
@@ -454,11 +454,21 @@ void update_time() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_daytime && residents_count > 0 && barricade_health < BARRICADE_MAX_HEALTH && current_hour % 4 == 0) {
|
if (is_daytime && residents_count > 0 && barricade_health < BARRICADE_MAX_HEALTH) {
|
||||||
if (has_any_storage_food()) {
|
const int day_start_hour = 6;
|
||||||
int gained = add_barricade_health(residents_count * get_resident_effect_multiplier());
|
const int day_end_hour = 18; // Exclusive for repair scheduling (12-hour window)
|
||||||
if (gained > 0 && x <= BASE_END) {
|
if (current_hour >= day_start_hour && current_hour < day_end_hour) {
|
||||||
speak_with_history("Residents repaired the barricade. +" + gained + " health.", true);
|
int repair_window_hours = day_end_hour - day_start_hour;
|
||||||
|
int interval = repair_window_hours / residents_count;
|
||||||
|
if (interval < 1) interval = 1;
|
||||||
|
int day_hour = current_hour - day_start_hour;
|
||||||
|
if (day_hour % interval == 0) {
|
||||||
|
if (has_any_storage_food()) {
|
||||||
|
int gained = add_barricade_health(residents_count * get_resident_effect_multiplier());
|
||||||
|
if (gained > 0 && x <= BASE_END) {
|
||||||
|
speak_with_history("Residents repaired the barricade. +" + gained + " health.", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,9 +479,6 @@ void update_time() {
|
|||||||
attempt_resident_butchering();
|
attempt_resident_butchering();
|
||||||
attempt_resident_foraging();
|
attempt_resident_foraging();
|
||||||
}
|
}
|
||||||
if (current_hour == 12) {
|
|
||||||
attempt_resident_foraging();
|
|
||||||
}
|
|
||||||
attempt_daily_invasion();
|
attempt_daily_invasion();
|
||||||
keep_base_fires_fed();
|
keep_base_fires_fed();
|
||||||
update_incense_burning();
|
update_incense_burning();
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ string get_terrain_at_position(int pos_x) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
string ui_input_box(const string title, const string prompt, const string default_value) {
|
string ui_input_box(const string title, const string prompt, const string default_value) {
|
||||||
string result = virtual_input_box(title, prompt, default_value);
|
string result = virtual_input_box(prompt, prompt, default_value);
|
||||||
show_window("Draugnorak");
|
show_window("Draugnorak");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user