First adventure added. A bit of sound management improvement.

This commit is contained in:
Storm Dragon
2026-01-22 19:51:10 -05:00
parent ab7f3dc899
commit 35e192cb21
26 changed files with 851 additions and 54 deletions
+25 -1
View File
@@ -396,5 +396,29 @@ void play_unicorn_death_sequence() {
void give_unicorn_rewards() {
speak_with_history("Victory!", true);
favor += 50;
// Calculate rewards
int favor_reward = 4 + random(1, 4); // 4 + 1d4 = 5-8 favor
favor += favor_reward;
// Track boss defeat and unlock rune
unicorn_boss_defeated = true;
bool new_rune = !rune_swiftness_unlocked;
rune_swiftness_unlocked = true;
// Build rewards display
string[] rewards;
rewards.insert_last("=== Victory Rewards ===");
rewards.insert_last("");
rewards.insert_last("The gods are pleased with your victory! " + favor_reward + " favor awarded.");
rewards.insert_last("");
if (new_rune) {
rewards.insert_last("Learned Rune of Swiftness!");
rewards.insert_last("You can now engrave equipment with this rune at the crafting menu.");
} else {
rewards.insert_last("You have already mastered the Rune of Swiftness.");
}
// Display rewards in text reader
text_reader_lines(rewards, "Unicorn Victory", true);
}
+1
View File
@@ -19,3 +19,4 @@
#include "src/crafting/craft_clothing.nvgt"
#include "src/crafting/craft_buildings.nvgt"
#include "src/crafting/craft_barricade.nvgt"
#include "src/crafting/craft_runes.nvgt"
+1 -1
View File
@@ -4,7 +4,7 @@ void run_materials_menu() {
int selection = 0;
string[] options = {
"Butcher Small Game (1 Small Game) [Requires Knife and Fire nearby]",
"Butcher Game [Requires Game, Knife, Fire nearby]",
"Incense (6 Sticks, 2 Vines, 1 Reed) [Requires Altar]"
};
+190
View File
@@ -0,0 +1,190 @@
// Rune engraving crafting menu
// Requires: Knife (tool), 1 Clay, 1 Favor
// Must be in base area
// Get the base equipment name without any rune prefix
string get_base_equipment_name(int equip_type) {
if (equip_type == EQUIP_SPEAR) return "Spear";
if (equip_type == EQUIP_AXE) return "Stone Axe";
if (equip_type == EQUIP_SLING) return "Sling";
if (equip_type == EQUIP_BOW) return "Bow";
if (equip_type == EQUIP_HAT) return "Skin Hat";
if (equip_type == EQUIP_GLOVES) return "Skin Gloves";
if (equip_type == EQUIP_PANTS) return "Skin Pants";
if (equip_type == EQUIP_TUNIC) return "Skin Tunic";
if (equip_type == EQUIP_MOCCASINS) return "Moccasins";
if (equip_type == EQUIP_POUCH) return "Skin Pouch";
if (equip_type == EQUIP_BACKPACK) return "Backpack";
return "Unknown";
}
// Get inventory count for an equipment type
int get_unruned_equipment_count(int equip_type) {
if (equip_type == EQUIP_SPEAR) return inv_spears;
if (equip_type == EQUIP_AXE) return inv_axes;
if (equip_type == EQUIP_SLING) return inv_slings;
if (equip_type == EQUIP_BOW) return inv_bows;
if (equip_type == EQUIP_HAT) return inv_skin_hats;
if (equip_type == EQUIP_GLOVES) return inv_skin_gloves;
if (equip_type == EQUIP_PANTS) return inv_skin_pants;
if (equip_type == EQUIP_TUNIC) return inv_skin_tunics;
if (equip_type == EQUIP_MOCCASINS) return inv_moccasins;
if (equip_type == EQUIP_POUCH) return inv_skin_pouches;
if (equip_type == EQUIP_BACKPACK) return inv_backpacks;
return 0;
}
// Decrement inventory for an equipment type
void decrement_unruned_equipment(int equip_type) {
if (equip_type == EQUIP_SPEAR) { inv_spears--; return; }
if (equip_type == EQUIP_AXE) { inv_axes--; return; }
if (equip_type == EQUIP_SLING) { inv_slings--; return; }
if (equip_type == EQUIP_BOW) { inv_bows--; return; }
if (equip_type == EQUIP_HAT) { inv_skin_hats--; return; }
if (equip_type == EQUIP_GLOVES) { inv_skin_gloves--; return; }
if (equip_type == EQUIP_PANTS) { inv_skin_pants--; return; }
if (equip_type == EQUIP_TUNIC) { inv_skin_tunics--; return; }
if (equip_type == EQUIP_MOCCASINS) { inv_moccasins--; return; }
if (equip_type == EQUIP_POUCH) { inv_skin_pouches--; return; }
if (equip_type == EQUIP_BACKPACK) { inv_backpacks--; return; }
}
void run_runes_menu() {
// Check if in base area
if (x > BASE_END) {
speak_with_history("Rune engraving can only be done in the base area.", true);
return;
}
speak_with_history("Runes.", true);
// Build list of unlocked runes
string[] rune_options;
int[] rune_types;
if (rune_swiftness_unlocked) {
rune_options.insert_last("Rune of Swiftness (1 Clay, 1 Favor) [Requires Knife]");
rune_types.insert_last(RUNE_SWIFTNESS);
}
if (rune_options.length() == 0) {
speak_with_history("No runes unlocked yet.", true);
return;
}
int selection = 0;
while(true) {
wait(5);
menu_background_tick();
if (key_pressed(KEY_ESCAPE)) {
speak_with_history("Closed.", true);
break;
}
if (key_pressed(KEY_DOWN)) {
selection++;
if (selection >= int(rune_options.length())) selection = 0;
speak_with_history(rune_options[selection], true);
}
if (key_pressed(KEY_UP)) {
selection--;
if (selection < 0) selection = int(rune_options.length()) - 1;
speak_with_history(rune_options[selection], true);
}
if (key_pressed(KEY_RETURN)) {
int rune_type = rune_types[selection];
run_rune_equipment_menu(rune_type);
break;
}
}
}
void run_rune_equipment_menu(int rune_type) {
speak_with_history("Select equipment to engrave with " + get_rune_name(rune_type) + ".", true);
// Build list of equipment that can be runed
string[] equipment_options;
int[] equipment_types;
int[] runeable = get_runeable_equipment_types();
for (uint i = 0; i < runeable.length(); i++) {
int equip_type = runeable[i];
int unruned_count = get_unruned_equipment_count(equip_type);
if (unruned_count > 0) {
string name = get_base_equipment_name(equip_type);
equipment_options.insert_last(name + " (" + unruned_count + " available)");
equipment_types.insert_last(equip_type);
}
}
if (equipment_options.length() == 0) {
speak_with_history("No equipment available to engrave.", true);
return;
}
int selection = 0;
while(true) {
wait(5);
menu_background_tick();
if (key_pressed(KEY_ESCAPE)) {
speak_with_history("Closed.", true);
break;
}
if (key_pressed(KEY_DOWN)) {
selection++;
if (selection >= int(equipment_options.length())) selection = 0;
speak_with_history(equipment_options[selection], true);
}
if (key_pressed(KEY_UP)) {
selection--;
if (selection < 0) selection = int(equipment_options.length()) - 1;
speak_with_history(equipment_options[selection], true);
}
if (key_pressed(KEY_RETURN)) {
int equip_type = equipment_types[selection];
engrave_rune(equip_type, rune_type);
break;
}
}
}
void engrave_rune(int equip_type, int rune_type) {
// Validate requirements
string missing = "";
if (inv_knives < 1) missing += "Stone Knife ";
if (inv_clay < 1) missing += "1 clay ";
if (favor < 1.0) missing += "1 favor ";
// Check equipment is still available
if (get_unruned_equipment_count(equip_type) < 1) {
speak_with_history("No " + get_base_equipment_name(equip_type) + " available.", true);
return;
}
if (missing == "") {
// Consume materials (knife is not consumed, it's a tool)
inv_clay--;
favor -= 1.0;
// Remove one unruned item from inventory
decrement_unruned_equipment(equip_type);
// Add one runed item to dictionary
add_runed_item(equip_type, rune_type);
// Play crafting animation
simulate_crafting(6);
string runed_name = "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type);
speak_with_history("Engraved " + runed_name + ".", true);
} else {
speak_with_history("Missing: " + missing, true);
}
}
+25 -4
View File
@@ -11,8 +11,28 @@ void run_crafting_menu() {
speak_with_history("Crafting menu.", true);
int selection = 0;
string[] categories = {"Weapons", "Tools", "Materials", "Clothing", "Buildings", "Barricade"};
int[] category_types = {0, 1, 2, 3, 4, 5};
string[] categories;
int[] category_types;
// Build categories dynamically
categories.insert_last("Weapons");
category_types.insert_last(0);
categories.insert_last("Tools");
category_types.insert_last(1);
categories.insert_last("Materials");
category_types.insert_last(2);
categories.insert_last("Clothing");
category_types.insert_last(3);
categories.insert_last("Buildings");
category_types.insert_last(4);
categories.insert_last("Barricade");
category_types.insert_last(5);
// Add Runes category if any rune is unlocked
if (any_rune_unlocked()) {
categories.insert_last("Runes");
category_types.insert_last(6);
}
while(true) {
wait(5);
@@ -24,13 +44,13 @@ void run_crafting_menu() {
if (key_pressed(KEY_DOWN)) {
selection++;
if (selection >= categories.length()) selection = 0;
if (selection >= int(categories.length())) selection = 0;
speak_with_history(categories[selection], true);
}
if (key_pressed(KEY_UP)) {
selection--;
if (selection < 0) selection = categories.length() - 1;
if (selection < 0) selection = int(categories.length()) - 1;
speak_with_history(categories[selection], true);
}
@@ -42,6 +62,7 @@ void run_crafting_menu() {
else if (category == 3) run_clothing_menu();
else if (category == 4) run_buildings_menu();
else if (category == 5) run_barricade_menu();
else if (category == 6) run_runes_menu();
break;
}
}
+7
View File
@@ -181,6 +181,13 @@ void update_bandit(Bandit@ bandit) {
if (bandit.alert_timer.elapsed > bandit.next_alert_delay) {
bandit.alert_timer.restart();
bandit.next_alert_delay = random(BANDIT_ALERT_MIN_DELAY, BANDIT_ALERT_MAX_DELAY);
// Destroy old sound handle before playing new one to prevent overlapping
if (bandit.sound_handle != -1) {
p.destroy_sound(bandit.sound_handle);
bandit.sound_handle = -1;
}
bandit.sound_handle = play_creature_voice(bandit.alert_sound, x, bandit.position, BANDIT_SOUND_VOLUME_STEP);
}
-1
View File
@@ -330,7 +330,6 @@ void update_flying_creature(FlyingCreature@ creature) {
}
play_creature_death_sound(cfg.impact_sound, x, creature.position, cfg.sound_volume_step);
notify("A " + creature.creature_type + " fell from the sky at " + creature.position + "!");
add_world_drop(creature.position, cfg.drop_type);
creature.health = 0;
}
+7
View File
@@ -134,6 +134,13 @@ void update_undead(Undead@ undead) {
if (undead.groan_timer.elapsed > undead.next_groan_delay) {
undead.groan_timer.restart();
undead.next_groan_delay = random(ZOMBIE_GROAN_MIN_DELAY, ZOMBIE_GROAN_MAX_DELAY);
// Destroy old sound handle before playing new one to prevent overlapping
if (undead.sound_handle != -1) {
p.destroy_sound(undead.sound_handle);
undead.sound_handle = -1;
}
undead.sound_handle = play_creature_voice(undead.voice_sound, x, undead.position, ZOMBIE_SOUND_VOLUME_STEP);
}
+29 -6
View File
@@ -86,6 +86,11 @@ class Tree {
if (tree_distance <= TREE_SOUND_RANGE) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
// Clean up invalid handle if it exists
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
sound_handle = p.play_1d("sounds/environment/tree.ogg", x, position, true);
if (sound_handle != -1) {
p.update_sound_positioning_values(sound_handle, -1.0, TREE_SOUND_VOLUME_STEP, true);
@@ -923,6 +928,9 @@ void start_rope_climb(bool climbing_up, int target_x, int target_elevation) {
string direction = rope_climb_up ? "up" : "down";
speak_with_history("Climbing " + direction + ". " + distance + " feet.", true);
// Start looping rope climbing sound
rope_climb_sound_handle = p.play_stationary("sounds/actions/climb_rope.ogg", true);
}
void update_rope_climbing() {
@@ -943,11 +951,9 @@ void update_rope_climbing() {
if (y < rope_climb_target_y) {
y++;
// Check if we've reached the target BEFORE playing the sound
// Check if we've reached the target
if (y >= rope_climb_target_y) {
complete_rope_climb();
} else {
p.play_stationary("sounds/actions/climb_rope.ogg", false);
}
}
} else {
@@ -955,11 +961,9 @@ void update_rope_climbing() {
if (y > rope_climb_target_y) {
y--;
// Check if we've reached the target BEFORE playing the sound
// Check if we've reached the target
if (y <= rope_climb_target_y) {
complete_rope_climb();
} else {
p.play_stationary("sounds/actions/climb_rope.ogg", false);
}
}
}
@@ -971,6 +975,12 @@ void complete_rope_climb() {
x = rope_climb_target_x;
y = rope_climb_target_y;
// Stop looping rope climbing sound
if (rope_climb_sound_handle != -1) {
p.destroy_sound(rope_climb_sound_handle);
rope_climb_sound_handle = -1;
}
// Play footstep for new terrain
play_footstep(x, BASE_END, GRASS_END);
@@ -981,6 +991,19 @@ void check_rope_climb_fall() {
if (!rope_climbing) return;
if (key_down(KEY_LEFT) || key_down(KEY_RIGHT)) {
// Stop rope climbing sound
if (rope_climb_sound_handle != -1) {
p.destroy_sound(rope_climb_sound_handle);
rope_climb_sound_handle = -1;
}
// Move one tile in the direction being pressed before falling
if (key_down(KEY_LEFT) && x > 0) {
x--;
} else if (key_down(KEY_RIGHT) && x < MAP_SIZE - 1) {
x++;
}
// Fall from rope!
rope_climbing = false;
start_falling();
+2
View File
@@ -1,4 +1,6 @@
// Inventory module includes
#include "src/inventory_items.nvgt"
#include "src/runes/rune_data.nvgt"
#include "src/runes/rune_effects.nvgt"
#include "src/inventory_menus.nvgt"
#include "src/crafting.nvgt"
+26 -4
View File
@@ -261,11 +261,21 @@ void update_max_health_from_equipment() {
if (player_health > max_health) {
player_health = max_health;
}
// Calculate walk speed with all bonuses
int base_speed = (equipped_feet == EQUIP_MOCCASINS) ? MOCCASINS_WALK_SPEED : BASE_WALK_SPEED;
walk_speed = base_speed;
// Apply rune speed bonuses (stacks with moccasins)
int rune_bonus = get_total_rune_walk_speed_bonus();
walk_speed = base_speed - rune_bonus;
// Apply blessing (if active and faster than current)
if (blessing_speed_active && BLESSING_WALK_SPEED < walk_speed) {
walk_speed = BLESSING_WALK_SPEED;
}
// Ensure minimum walk speed
if (walk_speed < 200) walk_speed = 200;
}
int get_quick_slot_key() {
@@ -522,9 +532,21 @@ string get_equipped_weapon_name() {
}
string get_speed_status() {
if (blessing_speed_active) return "blessed";
if (equipped_feet == EQUIP_MOCCASINS) return "boosted by moccasins";
return "normal";
string status = "";
int rune_bonus = get_total_rune_walk_speed_bonus();
if (blessing_speed_active) {
status = "blessed";
} else if (equipped_feet == EQUIP_MOCCASINS && rune_bonus > 0) {
status = "boosted by moccasins and runes";
} else if (equipped_feet == EQUIP_MOCCASINS) {
status = "boosted by moccasins";
} else if (rune_bonus > 0) {
status = "boosted by runes";
} else {
status = "normal";
}
return status;
}
void cleanup_equipment_after_inventory_change() {
+6
View File
@@ -9,6 +9,12 @@ void check_action_menu(int x) {
void try_place_snare(int x) {
if (inv_snares > 0) {
// Prevent placing in base area
if (x <= BASE_END) {
speak_with_history("Cannot place snares in the base area.", true);
return;
}
// Prevent placing if one already exists here
if (get_snare_at(x) != null) {
speak_with_history("There is already a snare here.", true);
+94 -22
View File
@@ -1,13 +1,30 @@
// Equipment menu system
// Functions for managing equipment (weapons and clothing)
// Check if player has any equipment (including runed items)
bool has_any_equipment() {
// Check unruned items
if (inv_spears > 0 || inv_axes > 0 || inv_slings > 0 || inv_bows > 0 ||
inv_skin_hats > 0 || inv_skin_gloves > 0 || inv_skin_pants > 0 ||
inv_skin_tunics > 0 || inv_moccasins > 0 || inv_skin_pouches > 0 ||
inv_backpacks > 0) {
return true;
}
// Check runed items
int[] runeable = get_runeable_equipment_types();
for (uint i = 0; i < runeable.length(); i++) {
if (get_runed_item_count(runeable[i], RUNE_SWIFTNESS) > 0) {
return true;
}
}
return false;
}
void check_equipment_menu() {
if (key_pressed(KEY_E)) {
// Check if player has any equipment
if (inv_spears == 0 && inv_axes == 0 && inv_slings == 0 &&
inv_skin_hats == 0 && inv_skin_gloves == 0 && inv_skin_pants == 0 &&
inv_skin_tunics == 0 && inv_moccasins == 0 && inv_skin_pouches == 0 &&
inv_backpacks == 0) {
if (!has_any_equipment()) {
speak_with_history("Nothing to equip.", true);
} else {
run_equipment_menu();
@@ -15,63 +32,109 @@ void check_equipment_menu() {
}
}
// Helper to get full equipment name including rune
string get_full_equipment_name(int equip_type, int rune_type) {
string base_name = get_base_equipment_name(equip_type);
if (rune_type != RUNE_NONE) {
return "Runed " + base_name + " of " + get_rune_effect_name(rune_type);
}
return base_name;
}
// Check if an item with specific rune is equipped
bool is_runed_item_equipped(int equip_type, int rune_type) {
if (!equipment_is_equipped(equip_type)) return false;
return get_equipped_rune_for_slot(equip_type) == rune_type;
}
void run_equipment_menu() {
speak_with_history("Equipment menu.", true);
int selection = 0;
string[] options;
int[] equipment_types;
int[] rune_types; // Track which rune is on each option (RUNE_NONE for unruned)
// Build menu dynamically based on what player has
// Add unruned items
if (inv_spears > 0) {
string status = equipment_is_equipped(EQUIP_SPEAR) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_SPEAR, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Spear" + status);
equipment_types.insert_last(EQUIP_SPEAR);
rune_types.insert_last(RUNE_NONE);
}
if (inv_slings > 0) {
string status = equipment_is_equipped(EQUIP_SLING) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_SLING, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Sling" + status);
equipment_types.insert_last(EQUIP_SLING);
rune_types.insert_last(RUNE_NONE);
}
if (inv_axes > 0) {
string status = equipment_is_equipped(EQUIP_AXE) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_AXE, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Stone Axe" + status);
equipment_types.insert_last(EQUIP_AXE);
rune_types.insert_last(RUNE_NONE);
}
if (inv_bows > 0) {
string status = is_runed_item_equipped(EQUIP_BOW, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Bow" + status);
equipment_types.insert_last(EQUIP_BOW);
rune_types.insert_last(RUNE_NONE);
}
if (inv_skin_hats > 0) {
string status = equipment_is_equipped(EQUIP_HAT) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_HAT, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Skin Hat" + status);
equipment_types.insert_last(EQUIP_HAT);
rune_types.insert_last(RUNE_NONE);
}
if (inv_skin_gloves > 0) {
string status = equipment_is_equipped(EQUIP_GLOVES) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_GLOVES, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Skin Gloves" + status);
equipment_types.insert_last(EQUIP_GLOVES);
rune_types.insert_last(RUNE_NONE);
}
if (inv_skin_pants > 0) {
string status = equipment_is_equipped(EQUIP_PANTS) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_PANTS, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Skin Pants" + status);
equipment_types.insert_last(EQUIP_PANTS);
rune_types.insert_last(RUNE_NONE);
}
if (inv_skin_tunics > 0) {
string status = equipment_is_equipped(EQUIP_TUNIC) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_TUNIC, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Skin Tunic" + status);
equipment_types.insert_last(EQUIP_TUNIC);
rune_types.insert_last(RUNE_NONE);
}
if (inv_moccasins > 0) {
string status = equipment_is_equipped(EQUIP_MOCCASINS) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_MOCCASINS, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Moccasins" + status);
equipment_types.insert_last(EQUIP_MOCCASINS);
rune_types.insert_last(RUNE_NONE);
}
if (inv_skin_pouches > 0) {
string status = equipment_is_equipped(EQUIP_POUCH) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_POUCH, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Skin Pouch" + status);
equipment_types.insert_last(EQUIP_POUCH);
rune_types.insert_last(RUNE_NONE);
}
if (inv_backpacks > 0) {
string status = equipment_is_equipped(EQUIP_BACKPACK) ? " (equipped)" : "";
string status = is_runed_item_equipped(EQUIP_BACKPACK, RUNE_NONE) ? " (equipped)" : "";
options.insert_last("Backpack" + status);
equipment_types.insert_last(EQUIP_BACKPACK);
rune_types.insert_last(RUNE_NONE);
}
// Add runed items (currently only swiftness rune)
int[] runeable = get_runeable_equipment_types();
for (uint i = 0; i < runeable.length(); i++) {
int equip_type = runeable[i];
int count = get_runed_item_count(equip_type, RUNE_SWIFTNESS);
if (count > 0) {
string name = get_full_equipment_name(equip_type, RUNE_SWIFTNESS);
string status = is_runed_item_equipped(equip_type, RUNE_SWIFTNESS) ? " (equipped)" : "";
options.insert_last(name + status);
equipment_types.insert_last(equip_type);
rune_types.insert_last(RUNE_SWIFTNESS);
}
}
while(true) {
@@ -84,31 +147,40 @@ void run_equipment_menu() {
if (key_pressed(KEY_DOWN)) {
selection++;
if (selection >= options.length()) selection = 0;
if (selection >= int(options.length())) selection = 0;
speak_with_history(options[selection], true);
}
if (key_pressed(KEY_UP)) {
selection--;
if (selection < 0) selection = options.length() - 1;
if (selection < 0) selection = int(options.length()) - 1;
speak_with_history(options[selection], true);
}
int slot_index = get_quick_slot_key();
if (slot_index != -1) {
int equip_type = equipment_types[selection];
int rune_type = rune_types[selection];
quick_slots[slot_index] = equip_type;
speak_with_history(get_equipment_name(equip_type) + " set to slot " + slot_index + ".", true);
string name = get_full_equipment_name(equip_type, rune_type);
speak_with_history(name + " set to slot " + slot_index + ".", true);
}
if (key_pressed(KEY_RETURN)) {
int equip_type = equipment_types[selection];
if (equipment_is_equipped(equip_type)) {
int rune_type = rune_types[selection];
string name = get_full_equipment_name(equip_type, rune_type);
if (is_runed_item_equipped(equip_type, rune_type)) {
// Unequip
unequip_equipment_type(equip_type);
speak_with_history(get_equipment_name(equip_type) + " unequipped.", true);
clear_equipped_rune_for_slot(equip_type);
speak_with_history(name + " unequipped.", true);
} else {
// Equip
equip_equipment_type(equip_type);
speak_with_history(get_equipment_name(equip_type) + " equipped.", true);
set_equipped_rune_for_slot(equip_type, rune_type);
speak_with_history(name + " equipped.", true);
}
update_max_health_from_equipment();
break;
+1
View File
@@ -17,6 +17,7 @@ bool rope_climb_up = true;
int rope_climb_target_x = 0;
int rope_climb_target_y = 0;
int rope_climb_start_y = 0;
int rope_climb_sound_handle = -1;
timer rope_climb_timer;
int pending_rope_climb_x = -1;
int pending_rope_climb_elevation = 0;
+158
View File
@@ -0,0 +1,158 @@
// Rune System - Core Data and Helper Functions
// Runes can be engraved onto equipment to grant permanent bonuses
// Rune type constants
const int RUNE_NONE = -1;
const int RUNE_SWIFTNESS = 0;
// Rune unlock tracking (persisted in save)
bool rune_swiftness_unlocked = false;
// Boss defeat tracking
bool unicorn_boss_defeated = false;
// Speed bonus per rune of swiftness (milliseconds) - half of moccasins (40ms)
const int RUNE_SWIFTNESS_SPEED_BONUS = 20;
// Gathering speed reduction per rune (percentage off base time)
const int RUNE_SWIFTNESS_GATHER_BONUS = 5;
// Dictionary for runed item counts
// Key format: "equip_type:rune_type" (e.g., "5:0" for pants with swiftness)
// Value: count of that runed item type
dictionary runed_items;
// Track which rune is on equipped items
int equipped_head_rune = RUNE_NONE;
int equipped_torso_rune = RUNE_NONE;
int equipped_arms_rune = RUNE_NONE;
int equipped_hands_rune = RUNE_NONE;
int equipped_legs_rune = RUNE_NONE;
int equipped_feet_rune = RUNE_NONE;
int equipped_weapon_rune = RUNE_NONE;
// Get display name for a rune type
string get_rune_name(int rune_type) {
if (rune_type == RUNE_SWIFTNESS) return "Rune of Swiftness";
return "Unknown Rune";
}
// Get the effect suffix for runed item names (e.g., "of Quickness")
string get_rune_effect_name(int rune_type) {
if (rune_type == RUNE_SWIFTNESS) return "Quickness";
return "Unknown";
}
// Check if any rune has been unlocked
bool any_rune_unlocked() {
return rune_swiftness_unlocked;
}
// Check if a specific rune is unlocked
bool is_rune_unlocked(int rune_type) {
if (rune_type == RUNE_SWIFTNESS) return rune_swiftness_unlocked;
return false;
}
// Create dictionary key for runed item storage
string make_runed_key(int equip_type, int rune_type) {
return "" + equip_type + ":" + rune_type;
}
// Get count of a specific runed item type
int get_runed_item_count(int equip_type, int rune_type) {
string key = make_runed_key(equip_type, rune_type);
if (runed_items.exists(key)) {
return int(runed_items[key]);
}
return 0;
}
// Add a runed item to inventory
void add_runed_item(int equip_type, int rune_type) {
string key = make_runed_key(equip_type, rune_type);
int current = get_runed_item_count(equip_type, rune_type);
runed_items.set(key, current + 1);
}
// Remove a runed item from inventory
void remove_runed_item(int equip_type, int rune_type) {
string key = make_runed_key(equip_type, rune_type);
int current = get_runed_item_count(equip_type, rune_type);
if (current > 0) {
runed_items.set(key, current - 1);
}
}
// Check if player has any runed version of an equipment type
bool has_any_runed_version(int equip_type) {
// Check all rune types
if (get_runed_item_count(equip_type, RUNE_SWIFTNESS) > 0) return true;
return false;
}
// Get the rune type on equipped item for a given slot
int get_equipped_rune_for_slot(int equip_type) {
if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE ||
equip_type == EQUIP_SLING || equip_type == EQUIP_BOW) {
return equipped_weapon_rune;
}
if (equip_type == EQUIP_HAT) return equipped_head_rune;
if (equip_type == EQUIP_TUNIC) return equipped_torso_rune;
if (equip_type == EQUIP_POUCH || equip_type == EQUIP_BACKPACK) return equipped_arms_rune;
if (equip_type == EQUIP_GLOVES) return equipped_hands_rune;
if (equip_type == EQUIP_PANTS) return equipped_legs_rune;
if (equip_type == EQUIP_MOCCASINS) return equipped_feet_rune;
return RUNE_NONE;
}
// Set the rune on an equipped item slot
void set_equipped_rune_for_slot(int equip_type, int rune_type) {
if (equip_type == EQUIP_SPEAR || equip_type == EQUIP_AXE ||
equip_type == EQUIP_SLING || equip_type == EQUIP_BOW) {
equipped_weapon_rune = rune_type;
return;
}
if (equip_type == EQUIP_HAT) { equipped_head_rune = rune_type; return; }
if (equip_type == EQUIP_TUNIC) { equipped_torso_rune = rune_type; return; }
if (equip_type == EQUIP_POUCH || equip_type == EQUIP_BACKPACK) { equipped_arms_rune = rune_type; return; }
if (equip_type == EQUIP_GLOVES) { equipped_hands_rune = rune_type; return; }
if (equip_type == EQUIP_PANTS) { equipped_legs_rune = rune_type; return; }
if (equip_type == EQUIP_MOCCASINS) { equipped_feet_rune = rune_type; return; }
}
// Clear rune from an equipped slot
void clear_equipped_rune_for_slot(int equip_type) {
set_equipped_rune_for_slot(equip_type, RUNE_NONE);
}
// Get all equipment types that can be runed
int[] get_runeable_equipment_types() {
int[] types;
types.insert_last(EQUIP_SPEAR);
types.insert_last(EQUIP_AXE);
types.insert_last(EQUIP_SLING);
types.insert_last(EQUIP_BOW);
types.insert_last(EQUIP_HAT);
types.insert_last(EQUIP_GLOVES);
types.insert_last(EQUIP_PANTS);
types.insert_last(EQUIP_TUNIC);
types.insert_last(EQUIP_MOCCASINS);
types.insert_last(EQUIP_POUCH);
types.insert_last(EQUIP_BACKPACK);
return types;
}
// Reset all rune data for new game
void reset_rune_data() {
rune_swiftness_unlocked = false;
unicorn_boss_defeated = false;
runed_items.delete_all();
equipped_head_rune = RUNE_NONE;
equipped_torso_rune = RUNE_NONE;
equipped_arms_rune = RUNE_NONE;
equipped_hands_rune = RUNE_NONE;
equipped_legs_rune = RUNE_NONE;
equipped_feet_rune = RUNE_NONE;
equipped_weapon_rune = RUNE_NONE;
}
+81
View File
@@ -0,0 +1,81 @@
// Rune Effects - Calculate bonuses from equipped runed items
// This file handles the actual gameplay effects of runes
// Calculate total walking speed bonus from all equipped runed items
int get_total_rune_walk_speed_bonus() {
int bonus = 0;
// Check each equipment slot for swiftness runes
if (equipped_head_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS;
if (equipped_torso_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS;
if (equipped_arms_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS;
if (equipped_hands_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS;
if (equipped_legs_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS;
if (equipped_feet_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS;
if (equipped_weapon_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_SPEED_BONUS;
return bonus;
}
// Calculate gathering speed reduction percentage from equipped runed items
// Returns total percentage reduction (e.g., 15 means 15% faster gathering)
int get_total_rune_gather_bonus() {
int bonus = 0;
// Check each equipment slot for swiftness runes
if (equipped_head_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS;
if (equipped_torso_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS;
if (equipped_arms_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS;
if (equipped_hands_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS;
if (equipped_legs_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS;
if (equipped_feet_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS;
if (equipped_weapon_rune == RUNE_SWIFTNESS) bonus += RUNE_SWIFTNESS_GATHER_BONUS;
return bonus;
}
// Apply gathering time reduction based on rune bonuses
// Takes base time in ms, returns reduced time
int apply_rune_gather_bonus(int base_time) {
int bonus_percent = get_total_rune_gather_bonus();
if (bonus_percent <= 0) return base_time;
// Cap at 50% reduction to prevent instant gathering
if (bonus_percent > 50) bonus_percent = 50;
int reduction = (base_time * bonus_percent) / 100;
return base_time - reduction;
}
// Count total equipped swiftness runes
int count_equipped_swiftness_runes() {
int count = 0;
if (equipped_head_rune == RUNE_SWIFTNESS) count++;
if (equipped_torso_rune == RUNE_SWIFTNESS) count++;
if (equipped_arms_rune == RUNE_SWIFTNESS) count++;
if (equipped_hands_rune == RUNE_SWIFTNESS) count++;
if (equipped_legs_rune == RUNE_SWIFTNESS) count++;
if (equipped_feet_rune == RUNE_SWIFTNESS) count++;
if (equipped_weapon_rune == RUNE_SWIFTNESS) count++;
return count;
}
// Get a human-readable description of current rune speed bonuses
string get_rune_speed_description() {
int walk_bonus = get_total_rune_walk_speed_bonus();
int gather_bonus = get_total_rune_gather_bonus();
if (walk_bonus == 0 && gather_bonus == 0) {
return "none";
}
string desc = "";
if (walk_bonus > 0) {
desc += walk_bonus + "ms faster walking";
}
if (gather_bonus > 0) {
if (desc.length() > 0) desc += ", ";
desc += gather_bonus + "% faster gathering";
}
return desc;
}
+50
View File
@@ -169,6 +169,7 @@ void reset_game_state() {
rope_climb_target_x = 0;
rope_climb_target_y = 0;
rope_climb_start_y = 0;
rope_climb_sound_handle = -1;
pending_rope_climb_x = -1;
pending_rope_climb_elevation = 0;
@@ -618,6 +619,29 @@ bool save_game_state() {
}
saveData.set("equipment_quick_slots", join_string_array(quickSlotData));
// Rune system data
saveData.set("rune_swiftness_unlocked", rune_swiftness_unlocked);
saveData.set("unicorn_boss_defeated", unicorn_boss_defeated);
saveData.set("equipped_head_rune", equipped_head_rune);
saveData.set("equipped_torso_rune", equipped_torso_rune);
saveData.set("equipped_arms_rune", equipped_arms_rune);
saveData.set("equipped_hands_rune", equipped_hands_rune);
saveData.set("equipped_legs_rune", equipped_legs_rune);
saveData.set("equipped_feet_rune", equipped_feet_rune);
saveData.set("equipped_weapon_rune", equipped_weapon_rune);
// Save runed items dictionary as key:value pairs
string[] runed_items_data;
string[]@ keys = runed_items.get_keys();
for (uint i = 0; i < keys.length(); i++) {
string key = keys[i];
int count = int(runed_items[key]);
if (count > 0) {
runed_items_data.insert_last(key + "=" + count);
}
}
saveData.set("runed_items", join_string_array(runed_items_data));
saveData.set("time_current_hour", current_hour);
saveData.set("time_current_day", current_day);
saveData.set("time_is_daytime", is_daytime);
@@ -919,6 +943,32 @@ bool load_game_state() {
}
update_max_health_from_equipment();
// Load rune system data
rune_swiftness_unlocked = get_bool(saveData, "rune_swiftness_unlocked", false);
unicorn_boss_defeated = get_bool(saveData, "unicorn_boss_defeated", false);
equipped_head_rune = int(get_number(saveData, "equipped_head_rune", RUNE_NONE));
equipped_torso_rune = int(get_number(saveData, "equipped_torso_rune", RUNE_NONE));
equipped_arms_rune = int(get_number(saveData, "equipped_arms_rune", RUNE_NONE));
equipped_hands_rune = int(get_number(saveData, "equipped_hands_rune", RUNE_NONE));
equipped_legs_rune = int(get_number(saveData, "equipped_legs_rune", RUNE_NONE));
equipped_feet_rune = int(get_number(saveData, "equipped_feet_rune", RUNE_NONE));
equipped_weapon_rune = int(get_number(saveData, "equipped_weapon_rune", RUNE_NONE));
// Load runed items dictionary
runed_items.delete_all();
string[] loaded_runed_items = get_string_list_or_split(saveData, "runed_items");
for (uint i = 0; i < loaded_runed_items.length(); i++) {
string entry = loaded_runed_items[i];
int eq_pos = entry.find("=");
if (eq_pos > 0) {
string key = entry.substr(0, eq_pos);
int count = parse_int(entry.substr(eq_pos + 1));
if (count > 0) {
runed_items.set(key, count);
}
}
}
current_hour = int(get_number(saveData, "time_current_hour", 8));
current_day = int(get_number(saveData, "time_current_day", 1));
sun_setting_warned = get_bool(saveData, "time_sun_setting_warned", false);
+20 -8
View File
@@ -99,14 +99,6 @@ void expand_regular_area() {
int stream_start = random(0, EXPANSION_SIZE - stream_width);
int actual_start = new_start + stream_start;
add_world_stream(actual_start, stream_width);
string width_desc = "very small";
if (stream_width == 2) width_desc = "small";
else if (stream_width == 3) width_desc = "medium";
else if (stream_width == 4) width_desc = "wide";
else if (stream_width == 5) width_desc = "very wide";
notify("A " + width_desc + " stream flows through the new area at x " + actual_start + ".");
} else {
// Try to place a tree with proper spacing and per-area limits
spawn_tree_in_area(new_start, new_end);
@@ -480,12 +472,22 @@ void start_crossfade(bool to_night) {
if (to_night) {
// Starting night sound
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
// Clean up invalid handle if it exists
if (night_sound_handle != -1) {
p.destroy_sound(night_sound_handle);
night_sound_handle = -1;
}
night_sound_handle = p.play_stationary("sounds/nature/night.ogg", true);
}
p.update_sound_start_values(night_sound_handle, 0.0, CROSSFADE_MIN_VOLUME, 1.0);
} else {
// Starting day sound
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
// Clean up invalid handle if it exists
if (day_sound_handle != -1) {
p.destroy_sound(day_sound_handle);
day_sound_handle = -1;
}
day_sound_handle = p.play_stationary("sounds/nature/day.ogg", true);
}
p.update_sound_start_values(day_sound_handle, 0.0, CROSSFADE_MIN_VOLUME, 1.0);
@@ -550,6 +552,11 @@ void update_ambience(bool force_restart) {
night_sound_handle = -1;
}
if (day_sound_handle == -1 || !p.sound_is_active(day_sound_handle)) {
// Clean up invalid handle if it exists
if (day_sound_handle != -1) {
p.destroy_sound(day_sound_handle);
day_sound_handle = -1;
}
day_sound_handle = p.play_stationary("sounds/nature/day.ogg", true);
}
} else {
@@ -559,6 +566,11 @@ void update_ambience(bool force_restart) {
day_sound_handle = -1;
}
if (night_sound_handle == -1 || !p.sound_is_active(night_sound_handle)) {
// Clean up invalid handle if it exists
if (night_sound_handle != -1) {
p.destroy_sound(night_sound_handle);
night_sound_handle = -1;
}
night_sound_handle = p.play_stationary("sounds/nature/night.ogg", true);
}
}
+17 -4
View File
@@ -280,6 +280,12 @@ void update_wind_gusts() {
void play_wind_gust() {
if (wind_intensity == INTENSITY_NONE || wind_intensity > INTENSITY_HIGH) return;
// Clean up previous wind gust if it's stale
if (wind_sound_handle != -1 && !p.sound_is_active(wind_sound_handle)) {
p.destroy_sound(wind_sound_handle);
wind_sound_handle = -1;
}
// Play the appropriate wind sound once (non-looping)
wind_sound_handle = p.play_stationary(wind_sounds[wind_intensity], false);
}
@@ -308,10 +314,17 @@ void fade_rain_to_intensity(int new_intensity) {
rain_fade_duration = random(10000, 20000);
// Start rain sound if not playing
if (rain_sound_handle == -1 && new_intensity != INTENSITY_NONE) {
rain_sound_handle = p.play_stationary(RAIN_SOUND, true);
p.update_sound_start_values(rain_sound_handle, 0.0, WEATHER_MIN_VOLUME, 1.0);
rain_fade_from_volume = WEATHER_MIN_VOLUME;
if (new_intensity != INTENSITY_NONE) {
if (rain_sound_handle == -1 || !p.sound_is_active(rain_sound_handle)) {
// Clean up invalid handle if it exists
if (rain_sound_handle != -1) {
p.destroy_sound(rain_sound_handle);
rain_sound_handle = -1;
}
rain_sound_handle = p.play_stationary(RAIN_SOUND, true);
p.update_sound_start_values(rain_sound_handle, 0.0, WEATHER_MIN_VOLUME, 1.0);
rain_fade_from_volume = WEATHER_MIN_VOLUME;
}
}
rain_fading = true;
+79
View File
@@ -42,6 +42,9 @@ class MountainRange {
// Ensure at least one steep climb (≥7 feet) reaches into 10-20 elevation range
ensure_steep_climb();
// Enforce plateau spacing: at least 3 walkable tiles between steep sections
enforce_plateau_spacing();
// Assign terrain types based on elevation
for (int i = 0; i < size; i++) {
if (elevations[i] > 20) {
@@ -94,6 +97,77 @@ class MountainRange {
}
}
void enforce_plateau_spacing() {
int size = int(elevations.length());
const int MIN_PLATEAU_TILES = 3;
const int MAX_GENTLE_SLOPE = 6; // Max slope per tile that doesn't require rope
// Find all steep sections (positions where rope would be needed)
int[] steep_positions;
for (int i = 1; i < size; i++) {
int diff = elevations[i] - elevations[i-1];
if (diff < 0) diff = -diff;
if (diff >= MOUNTAIN_STEEP_THRESHOLD) {
steep_positions.insert_last(i);
}
}
// For each steep section, ensure MIN_PLATEAU_TILES of gentle slope follow it
for (uint s = 0; s < steep_positions.length(); s++) {
int steep_pos = steep_positions[s];
// Check if we have enough tiles after this steep section for a plateau
if (steep_pos + MIN_PLATEAU_TILES >= size) continue;
// Get the elevation after the steep climb
int plateau_start_elevation = elevations[steep_pos];
// Ensure next MIN_PLATEAU_TILES have gentle slopes
for (int p = 1; p <= MIN_PLATEAU_TILES; p++) {
int tile_idx = steep_pos + p;
if (tile_idx >= size) break;
// Calculate max allowed elevation change for gentle slope
int prev_elevation = elevations[tile_idx - 1];
int max_up = prev_elevation + MAX_GENTLE_SLOPE;
int max_down = prev_elevation - MAX_GENTLE_SLOPE;
// Clamp to gentle slope range
if (elevations[tile_idx] > max_up) {
elevations[tile_idx] = max_up;
} else if (elevations[tile_idx] < max_down) {
elevations[tile_idx] = max_down;
}
// Never go below 0
if (elevations[tile_idx] < 0) {
elevations[tile_idx] = 0;
}
}
}
// Final pass: ensure no negative elevations anywhere
for (int i = 0; i < size; i++) {
if (elevations[i] < 0) elevations[i] = 0;
}
// Prefer climbing up before descending by adjusting early terrain
// If first half tends downward, flip it to go up first
if (size >= 10) {
int mid = size / 2;
int early_trend = elevations[mid] - elevations[0];
if (early_trend < 0) {
// Terrain goes down in first half, flip to go up
for (int i = 0; i <= mid; i++) {
elevations[i] = elevations[0] + (elevations[mid] - elevations[0]) * i / mid;
}
// Re-smooth after adjustment
smooth_slopes();
}
}
}
void place_streams() {
// Find valley bottoms (local minima)
int[] valleys;
@@ -247,6 +321,11 @@ class MountainRange {
// Keep nearest stream sound active so distance-based fade can work.
if (nearest_stream != -1) {
if (stream_sound_handle == -1 || !p.sound_is_active(stream_sound_handle)) {
// Clean up invalid handle if it exists
if (stream_sound_handle != -1) {
p.destroy_sound(stream_sound_handle);
stream_sound_handle = -1;
}
stream_sound_handle = p.play_1d("sounds/terrain/stream.ogg", x, nearest_stream, true);
stream_sound_position = nearest_stream;
if (stream_sound_handle != -1) {
+5
View File
@@ -19,6 +19,11 @@ class WorldDrop {
void update() {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
// Clean up invalid handle if it exists
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
sound_handle = p.play_1d("sounds/items/item.ogg", x, position, true);
if (sound_handle != -1) {
p.update_sound_positioning_values(sound_handle, -1.0, 3.0, true);
+5
View File
@@ -56,6 +56,11 @@ class WorldFire {
if (fire_distance < 0) fire_distance = -fire_distance;
if (fire_distance <= FIRE_SOUND_RANGE) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
// Clean up invalid handle if it exists
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
sound_handle = p.play_1d("sounds/items/fire.ogg", x, position, true);
if (sound_handle != -1) {
p.update_sound_positioning_values(sound_handle, -1.0, FIRE_SOUND_VOLUME_STEP, true);
+5
View File
@@ -43,6 +43,11 @@ class WorldSnare {
if (snare_distance <= SNARE_SOUND_RANGE) {
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
// Clean up invalid handle if it exists
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
sound_handle = p.play_1d("sounds/actions/set_snare.ogg", x, position, true);
if (sound_handle != -1) {
p.update_sound_positioning_values(sound_handle, SNARE_SOUND_PAN_STEP, SNARE_SOUND_VOLUME_STEP, true);
+8 -1
View File
@@ -37,7 +37,14 @@ class WorldStream {
}
// Keep stream sound active so distance-based fade can work.
if (sound_handle == -1 || !p.sound_is_active(sound_handle)) {
// Check if sound needs to be created or recreated
bool need_sound = (sound_handle == -1 || !p.sound_is_active(sound_handle));
if (need_sound) {
// Clean up invalid handle if it exists
if (sound_handle != -1) {
p.destroy_sound(sound_handle);
sound_handle = -1;
}
sound_handle = p.play_1d("sounds/terrain/stream.ogg", x, sound_pos, true);
sound_position = sound_pos;
if (sound_handle != -1) {