diff --git a/sounds/bosses/bandit/base_destroyed.ogg b/sounds/bosses/bandit/base_destroyed.ogg new file mode 100644 index 0000000..9bb3aeb --- /dev/null +++ b/sounds/bosses/bandit/base_destroyed.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:97071eb858ef9be74f2d68b5697c39c61bb499575bf7b269bd907ab8d534661a +size 43408 diff --git a/sounds/items/item_breaks.ogg b/sounds/items/item_breaks.ogg new file mode 100644 index 0000000..108af34 --- /dev/null +++ b/sounds/items/item_breaks.ogg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d9d65a36e0f5edd19f395dace69df938c4c1ba8c744a2ee6f232ab8af1add41 +size 10315 diff --git a/src/base_system.nvgt b/src/base_system.nvgt index 66cd885..26db11b 100644 --- a/src/base_system.nvgt +++ b/src/base_system.nvgt @@ -202,17 +202,70 @@ const int RESIDENT_WEAPON_SPEAR = 0; const int RESIDENT_WEAPON_SLING = 1; const int RESIDENT_WEAPON_BOW = 2; +int get_stored_runed_weapon_count(int equipType) { + int total = 0; + int[] runeTypes; + get_all_rune_types(runeTypes); + for (uint i = 0; i < runeTypes.length(); i++) { + total += get_stored_runed_item_count(equipType, runeTypes[i]); + } + return total; +} + +int get_total_stored_weapon_count(int equipType, int itemType) { + return get_storage_count(itemType) + get_stored_runed_weapon_count(equipType); +} + +bool remove_random_stored_runed_weapon(int equipType) { + int[] runeTypes; + get_all_rune_types(runeTypes); + int total = 0; + for (uint i = 0; i < runeTypes.length(); i++) { + total += get_stored_runed_item_count(equipType, runeTypes[i]); + } + if (total <= 0) return false; + + int roll = random(1, total); + int running = 0; + for (uint i = 0; i < runeTypes.length(); i++) { + int count = get_stored_runed_item_count(equipType, runeTypes[i]); + if (count <= 0) continue; + running += count; + if (roll <= running) { + remove_stored_runed_item(equipType, runeTypes[i]); + return true; + } + } + return false; +} + +bool remove_random_stored_weapon(int equipType, int itemType) { + int unrunedCount = get_storage_count(itemType); + int runedCount = get_stored_runed_weapon_count(equipType); + int total = unrunedCount + runedCount; + if (total <= 0) return false; + + int roll = random(1, total); + if (roll <= unrunedCount) { + if (unrunedCount > 0) add_storage_count(itemType, -1); + return true; + } + return remove_random_stored_runed_weapon(equipType); +} + int get_available_defense_weapons() { - int count = get_storage_count(ITEM_SPEARS); + int spearCount = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); + int slingCount = 0; + int bowCount = 0; // Slings only count if stones are available - if (get_storage_count(ITEM_SLINGS) > 0 && get_storage_count(ITEM_STONES) > 0) { - count += get_storage_count(ITEM_SLINGS); + if (get_storage_count(ITEM_STONES) > 0) { + slingCount = get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS); } // Bows only count if arrows are available - if (get_storage_count(ITEM_BOWS) > 0 && get_storage_count(ITEM_ARROWS) > 0) { - count += get_storage_count(ITEM_BOWS); + if (get_storage_count(ITEM_ARROWS) > 0) { + bowCount = get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS); } - return count; + return spearCount + slingCount + bowCount; } bool can_residents_defend() { @@ -222,13 +275,15 @@ bool can_residents_defend() { int choose_defense_weapon_type() { // Prefer bows if available - int bowCount = (get_storage_count(ITEM_BOWS) > 0 && get_storage_count(ITEM_ARROWS) > 0) - ? get_storage_count(ITEM_BOWS) + int bowCount = (get_storage_count(ITEM_ARROWS) > 0) + ? get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS) : 0; if (bowCount > 0) return RESIDENT_WEAPON_BOW; - int spearCount = get_storage_count(ITEM_SPEARS); - int slingCount = (get_storage_count(ITEM_SLINGS) > 0 && get_storage_count(ITEM_STONES) > 0) ? get_storage_count(ITEM_SLINGS) : 0; + int spearCount = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); + int slingCount = (get_storage_count(ITEM_STONES) > 0) + ? get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS) + : 0; int total = spearCount + slingCount; if (total == 0) return RESIDENT_WEAPON_SPEAR; @@ -244,18 +299,25 @@ int perform_resident_defense() { // Choose weapon type (bows preferred, otherwise weighted by availability) int weapon_type = choose_defense_weapon_type(); + int bowCount = (get_storage_count(ITEM_ARROWS) > 0) + ? get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS) + : 0; + int spearCount = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); + int slingCount = (get_storage_count(ITEM_STONES) > 0) + ? get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS) + : 0; int damage = 0; - if (weapon_type == RESIDENT_WEAPON_BOW && get_storage_count(ITEM_BOWS) > 0 && get_storage_count(ITEM_ARROWS) > 0) { + if (weapon_type == RESIDENT_WEAPON_BOW && bowCount > 0) { damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX)); add_storage_count(ITEM_ARROWS, -1); play_1d_with_volume_step("sounds/weapons/bow_fire.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP); - } else if (weapon_type == RESIDENT_WEAPON_SPEAR && get_storage_count(ITEM_SPEARS) > 0) { + } else if (weapon_type == RESIDENT_WEAPON_SPEAR && spearCount > 0) { damage = apply_resident_damage_bonus(RESIDENT_SPEAR_DAMAGE); // Weapons don't get consumed on use - they break via daily breakage check // Just play the sound play_1d_with_volume_step("sounds/weapons/spear_swing.ogg", x, BASE_END + 1, false, RESIDENT_DEFENSE_VOLUME_STEP); - } else if (weapon_type == RESIDENT_WEAPON_SLING && get_storage_count(ITEM_SLINGS) > 0 && get_storage_count(ITEM_STONES) > 0) { + } else if (weapon_type == RESIDENT_WEAPON_SLING && slingCount > 0) { damage = apply_resident_damage_bonus(random(RESIDENT_SLING_DAMAGE_MIN, RESIDENT_SLING_DAMAGE_MAX)); // Slings use stones as ammo, so consume a stone add_storage_count(ITEM_STONES, -1); @@ -271,8 +333,10 @@ timer resident_ranged_timer; void attempt_resident_ranged_defense() { // Only if residents exist and have ranged weapons if (residents_count <= 0) return; - bool has_bow = (get_storage_count(ITEM_BOWS) > 0 && get_storage_count(ITEM_ARROWS) > 0); - bool has_sling = (get_storage_count(ITEM_SLINGS) > 0 && get_storage_count(ITEM_STONES) > 0); + int bowCount = get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS); + int slingCount = get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS); + bool has_bow = (bowCount > 0 && get_storage_count(ITEM_ARROWS) > 0); + bool has_sling = (slingCount > 0 && get_storage_count(ITEM_STONES) > 0); if (!has_bow && !has_sling) return; // Cooldown between shots @@ -338,7 +402,10 @@ void attempt_resident_ranged_defense() { void process_daily_weapon_breakage() { if (residents_count <= 0) return; - int totalWeapons = get_storage_count(ITEM_SPEARS) + get_storage_count(ITEM_SLINGS) + get_storage_count(ITEM_BOWS); + int spearTotal = get_total_stored_weapon_count(EQUIP_SPEAR, ITEM_SPEARS); + int slingTotal = get_total_stored_weapon_count(EQUIP_SLING, ITEM_SLINGS); + int bowTotal = get_total_stored_weapon_count(EQUIP_BOW, ITEM_BOWS); + int totalWeapons = spearTotal + slingTotal + bowTotal; if (totalWeapons == 0) return; // Number of breakage checks = min(residents, weapons) @@ -350,9 +417,9 @@ void process_daily_weapon_breakage() { int bowChecks = 0; for (int i = 0; i < checksToPerform; i++) { - int remainingSpears = get_storage_count(ITEM_SPEARS) - spearChecks; - int remainingSlings = get_storage_count(ITEM_SLINGS) - slingChecks; - int remainingBows = get_storage_count(ITEM_BOWS) - bowChecks; + int remainingSpears = spearTotal - spearChecks; + int remainingSlings = slingTotal - slingChecks; + int remainingBows = bowTotal - bowChecks; int remaining = remainingSpears + remainingSlings + remainingBows; if (remaining <= 0) break; @@ -393,8 +460,9 @@ void process_daily_weapon_breakage() { // Apply breakage if (spearsBroken > 0) { - add_storage_count(ITEM_SPEARS, -spearsBroken); - if (get_storage_count(ITEM_SPEARS) < 0) set_storage_count(ITEM_SPEARS, 0); + for (int i = 0; i < spearsBroken; i++) { + remove_random_stored_weapon(EQUIP_SPEAR, ITEM_SPEARS); + } string msg = (spearsBroken == 1) ? "A resident's spear broke from wear." : spearsBroken + " spears broke from wear."; @@ -402,8 +470,9 @@ void process_daily_weapon_breakage() { } if (slingsBroken > 0) { - add_storage_count(ITEM_SLINGS, -slingsBroken); - if (get_storage_count(ITEM_SLINGS) < 0) set_storage_count(ITEM_SLINGS, 0); + for (int i = 0; i < slingsBroken; i++) { + remove_random_stored_weapon(EQUIP_SLING, ITEM_SLINGS); + } string msg = (slingsBroken == 1) ? "A resident's sling broke from wear." : slingsBroken + " slings broke from wear."; @@ -411,8 +480,9 @@ void process_daily_weapon_breakage() { } if (bowsBroken > 0) { - add_storage_count(ITEM_BOWS, -bowsBroken); - if (get_storage_count(ITEM_BOWS) < 0) set_storage_count(ITEM_BOWS, 0); + for (int i = 0; i < bowsBroken; i++) { + remove_random_stored_weapon(EQUIP_BOW, ITEM_BOWS); + } string msg = (bowsBroken == 1) ? "A resident's bow broke from wear." : bowsBroken + " bows broke from wear."; diff --git a/src/bosses/adventure_combat.nvgt b/src/bosses/adventure_combat.nvgt index d7ba40f..219792d 100644 --- a/src/bosses/adventure_combat.nvgt +++ b/src/bosses/adventure_combat.nvgt @@ -89,6 +89,7 @@ void adventure_release_bow_attack(int player_x, int player_facing, AdventureRang } int damage = get_bow_draw_damage(bow_draw_timer.elapsed); + damage = apply_weapon_rune_damage(damage); add_personal_count(ITEM_ARROWS, -1); p.play_stationary("sounds/weapons/bow_fire.ogg", false); @@ -130,6 +131,7 @@ void adventure_release_sling_attack(int player_x, int player_facing, AdventureRa int search_direction = (player_facing == 1) ? 1 : -1; int target_x = -1; int damage = random(SLING_DAMAGE_MIN, SLING_DAMAGE_MAX); + damage = apply_weapon_rune_damage(damage); if (@ranged_callback !is null) { target_x = ranged_callback(player_x, search_direction, SLING_RANGE, ADVENTURE_WEAPON_SLING, damage); diff --git a/src/bosses/bandit_hideout.nvgt b/src/bosses/bandit_hideout.nvgt index 462adca..24224c0 100644 --- a/src/bosses/bandit_hideout.nvgt +++ b/src/bosses/bandit_hideout.nvgt @@ -272,7 +272,7 @@ void run_bandit_hideout_adventure() { if (hideoutBarricadeHealth <= 0) { cleanup_bandit_hideout_adventure(); - p.play_stationary("sounds/actions/break_snare.ogg", false); + p.play_stationary("sounds/bosses/bandit/base_destroyed.ogg", false); give_bandit_hideout_rewards(); return; } @@ -446,7 +446,8 @@ void play_hideout_melee_hit(int weaponType) { bool hideout_melee_hit(int weaponType) { int range = (weaponType == ADVENTURE_WEAPON_SPEAR) ? 1 : 0; - int damage = (weaponType == ADVENTURE_WEAPON_SPEAR) ? SPEAR_DAMAGE : AXE_DAMAGE; + int baseDamage = (weaponType == ADVENTURE_WEAPON_SPEAR) ? SPEAR_DAMAGE : AXE_DAMAGE; + int damage = apply_weapon_rune_damage(baseDamage); for (int offset = -range; offset <= range; offset++) { int targetX = hideoutPlayerX + offset; @@ -653,7 +654,7 @@ void give_bandit_hideout_rewards() { rewards.insert_last("Storage rewards:"); bool anyItems = false; for (int itemType = 0; itemType < ITEM_COUNT; itemType++) { - int roll = random(0, 9); + int roll = random(0, 5); if (roll <= 0) continue; int addedAmount = add_hideout_storage_item(itemType, roll); if (addedAmount <= 0) continue; @@ -665,5 +666,28 @@ void give_bandit_hideout_rewards() { } } + bool newRune = !rune_destruction_unlocked; + rune_destruction_unlocked = true; + rewards.insert_last(""); + if (newRune) { + rewards.insert_last("Learned Rune of Destruction!"); + rewards.insert_last("You can now engrave weapons with this rune at the crafting menu."); + } else { + rewards.insert_last("You have already mastered the Rune of Destruction."); + } + + if (residents_count < MAX_RESIDENTS) { + int survivorRoll = random(1, 100); + if (survivorRoll <= 50) { + residents_count += 1; + if (residents_count > MAX_RESIDENTS) residents_count = MAX_RESIDENTS; + rewards.insert_last(""); + rewards.insert_last("A survivor joins your base."); + } else { + rewards.insert_last(""); + rewards.insert_last("No survivors were found."); + } + } + text_reader_lines(rewards, "Bandit's Hideout", true); } diff --git a/src/bosses/unicorn/unicorn_boss.nvgt b/src/bosses/unicorn/unicorn_boss.nvgt index 0833eb6..a11013d 100644 --- a/src/bosses/unicorn/unicorn_boss.nvgt +++ b/src/bosses/unicorn/unicorn_boss.nvgt @@ -372,7 +372,8 @@ bool unicorn_melee_hit(int weapon_type) { if (target_support != -1 && bridge_supports_health[target_support] > 0) { if (weapon_type == ADVENTURE_WEAPON_AXE) { - bridge_supports_health[target_support] -= AXE_DAMAGE; + int damage = apply_weapon_rune_damage(AXE_DAMAGE); + bridge_supports_health[target_support] -= damage; if (bridge_supports_health[target_support] <= 0) { check_bridge_collapse(); } @@ -381,7 +382,8 @@ bool unicorn_melee_hit(int weapon_type) { } if (abs(player_arena_x - unicorn.x) <= 1) { - int damage = (weapon_type == ADVENTURE_WEAPON_SPEAR) ? SPEAR_DAMAGE : AXE_DAMAGE; + int baseDamage = (weapon_type == ADVENTURE_WEAPON_SPEAR) ? SPEAR_DAMAGE : AXE_DAMAGE; + int damage = apply_weapon_rune_damage(baseDamage); apply_unicorn_damage(damage); return true; } diff --git a/src/combat.nvgt b/src/combat.nvgt index 5586c8c..208fd2b 100644 --- a/src/combat.nvgt +++ b/src/combat.nvgt @@ -102,7 +102,8 @@ bool attack_enemy(int target_x, int damage) { void perform_spear_attack(int current_x) { p.play_stationary("sounds/weapons/spear_swing.ogg", false); - int hit_pos = attack_enemy_ranged(current_x - 1, current_x + 1, SPEAR_DAMAGE); + int damage = apply_weapon_rune_damage(SPEAR_DAMAGE); + int hit_pos = attack_enemy_ranged(current_x - 1, current_x + 1, damage); if (hit_pos != -1) { p.play_stationary("sounds/weapons/spear_hit.ogg", false); // Play hit sound based on enemy type (both use same hit sound for now) @@ -123,7 +124,8 @@ void perform_spear_attack(int current_x) { void perform_axe_attack(int current_x) { p.play_stationary("sounds/weapons/axe_swing.ogg", false); - if (attack_enemy(current_x, AXE_DAMAGE)) { + int damage = apply_weapon_rune_damage(AXE_DAMAGE); + if (attack_enemy(current_x, damage)) { p.play_stationary("sounds/weapons/axe_hit.ogg", false); // Play hit sound based on enemy type if (get_bandit_at(current_x) != null) { @@ -140,7 +142,7 @@ void perform_axe_attack(int current_x) { // Range: current_x (0 distance) // Damage: 4 // Target: Trees - damage_tree(current_x, 4); + damage_tree(current_x, damage); } void perform_sling_attack(int current_x) { @@ -273,6 +275,7 @@ void release_bow_attack(int player_x) { } int damage = get_bow_draw_damage(bow_draw_timer.elapsed); + damage = apply_weapon_rune_damage(damage); add_personal_count(ITEM_ARROWS, -1); p.play_stationary("sounds/weapons/bow_fire.ogg", false); @@ -379,6 +382,7 @@ void release_sling_attack(int player_x) { } int damage = random(SLING_DAMAGE_MIN, SLING_DAMAGE_MAX); + damage = apply_weapon_rune_damage(damage); // Damage the correct enemy type if (hit_bandit) { diff --git a/src/constants.nvgt b/src/constants.nvgt index d82a50c..83ea370 100644 --- a/src/constants.nvgt +++ b/src/constants.nvgt @@ -204,6 +204,10 @@ const int RESIDENT_SNARE_CHECK_CHANCE = 15; // 15% chance per hour to check sna const int RESIDENT_FISHING_CHANCE = 6; // 6% chance per resident per hour to catch a fish const int RESIDENT_SMOKE_FISH_CHANCE = 10; // 10% chance per hour to smoke a stored fish const int RESIDENT_TOOL_BREAK_CHANCE = 2; // 2% chance tools break during resident use (fishing poles, knives, baskets) +const int PLAYER_ITEM_BREAK_CHANCE_MIN = 1; +const int PLAYER_ITEM_BREAK_CHANCE_MAX = 100; +const int PLAYER_ITEM_BREAK_CHANCE_INCREMENT = 1; +const int PLAYER_ITEM_BREAKS_PER_DAY = 2; // Goose settings const int GOOSE_HEALTH = 1; diff --git a/src/crafting/craft_clothing.nvgt b/src/crafting/craft_clothing.nvgt index 3f0abdf..80dcd09 100644 --- a/src/crafting/craft_clothing.nvgt +++ b/src/crafting/craft_clothing.nvgt @@ -3,7 +3,11 @@ // Get total pouches available (unruned + runed) int get_total_pouch_count() { int total = get_personal_count(ITEM_SKIN_POUCHES); - total += get_runed_item_count(EQUIP_POUCH, RUNE_SWIFTNESS); + int[] runeTypes; + get_all_rune_types(runeTypes); + for (uint i = 0; i < runeTypes.length(); i++) { + total += get_runed_item_count(EQUIP_POUCH, runeTypes[i]); + } return total; } @@ -21,10 +25,18 @@ void consume_pouches(int amount) { // Then consume runed pouches if needed while (remaining > 0) { - if (get_runed_item_count(EQUIP_POUCH, RUNE_SWIFTNESS) > 0) { - remove_runed_item(EQUIP_POUCH, RUNE_SWIFTNESS); - remaining--; - } else { + bool removed = false; + int[] runeTypes; + get_all_rune_types(runeTypes); + for (uint i = 0; i < runeTypes.length(); i++) { + if (get_runed_item_count(EQUIP_POUCH, runeTypes[i]) > 0) { + remove_runed_item(EQUIP_POUCH, runeTypes[i]); + remaining--; + removed = true; + break; + } + } + if (!removed) { break; } } diff --git a/src/crafting/craft_runes.nvgt b/src/crafting/craft_runes.nvgt index 6ebe1a2..969bdb2 100644 --- a/src/crafting/craft_runes.nvgt +++ b/src/crafting/craft_runes.nvgt @@ -64,10 +64,13 @@ void run_runes_menu() { // 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); + int[] unlocked_runes; + get_unlocked_rune_types(unlocked_runes); + for (uint i = 0; i < unlocked_runes.length(); i++) { + int rune_type = unlocked_runes[i]; + string label = get_rune_name(rune_type) + " (1 Clay, 1 Favor) [Requires Knife]"; + rune_options.insert_last(label); + rune_types.insert_last(rune_type); } if (rune_options.length() == 0) { @@ -112,7 +115,7 @@ void run_rune_equipment_menu(int rune_type) { string[] equipment_options; int[] equipment_types; - int[] runeable = get_runeable_equipment_types(); + int[] runeable = get_runeable_equipment_types_for_rune(rune_type); for (uint i = 0; i < runeable.length(); i++) { int equip_type = runeable[i]; int unruned_count = get_unruned_equipment_count(equip_type); diff --git a/src/inventory_items.nvgt b/src/inventory_items.nvgt index deceacf..2f746be 100644 --- a/src/inventory_items.nvgt +++ b/src/inventory_items.nvgt @@ -261,6 +261,95 @@ string get_item_count_binding_name(int item_type) { return get_item_display_name(item_type); } +string get_runed_item_display_name(int equip_type, int rune_type) { + return "Runed " + get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); +} + +string get_equipment_display_name_with_rune(int equip_type, int rune_type) { + if (rune_type != RUNE_NONE) { + return get_base_equipment_name(equip_type) + " of " + get_rune_effect_name(rune_type); + } + return get_equipment_name(equip_type); +} + +bool is_breakable_item_type(int itemType) { + if (is_runed_item_type(itemType)) return true; + if (itemType == ITEM_SPEARS) return true; + if (itemType == ITEM_SLINGS) return true; + if (itemType == ITEM_AXES) return true; + if (itemType == ITEM_SNARES) return true; + if (itemType == ITEM_KNIVES) return true; + if (itemType == ITEM_FISHING_POLES) return true; + if (itemType == ITEM_SKIN_HATS) return true; + if (itemType == ITEM_SKIN_GLOVES) return true; + if (itemType == ITEM_SKIN_PANTS) return true; + if (itemType == ITEM_SKIN_TUNICS) return true; + if (itemType == ITEM_MOCCASINS) return true; + if (itemType == ITEM_SKIN_POUCHES) return true; + if (itemType == ITEM_ROPES) return true; + if (itemType == ITEM_REED_BASKETS) return true; + if (itemType == ITEM_CLAY_POTS) return true; + if (itemType == ITEM_BOWS) return true; + if (itemType == ITEM_QUIVERS) return true; + if (itemType == ITEM_BACKPACKS) return true; + if (itemType == ITEM_CANOES) return true; + return false; +} + +string get_breakable_item_name(int itemType) { + if (is_runed_item_type(itemType)) { + int equipType = 0; + int runeType = 0; + decode_runed_item_type(itemType, equipType, runeType); + return "Runed " + get_base_equipment_name(equipType) + " of " + get_rune_effect_name(runeType); + } + return get_item_label_singular(itemType); +} + +void get_breakable_personal_item_types(int[]@ items) { + if (@items == null) return; + items.resize(0); + for (int itemType = 0; itemType < ITEM_COUNT; itemType++) { + if (!is_breakable_item_type(itemType)) continue; + if (get_personal_count(itemType) <= 0) continue; + items.insert_last(itemType); + } + + int[] runeable = get_runeable_equipment_types(); + for (uint i = 0; i < runeable.length(); i++) { + int equipType = runeable[i]; + int[] runeTypes; + get_all_rune_types(runeTypes); + for (uint j = 0; j < runeTypes.length(); j++) { + int runeType = runeTypes[j]; + int runedCount = get_runed_item_count(equipType, runeType); + if (runedCount <= 0) continue; + int encoded = encode_runed_item_type(equipType, runeType); + items.insert_last(encoded); + } + } +} + +bool remove_breakable_personal_item(int itemType) { + if (is_runed_item_type(itemType)) { + int equipType = 0; + int runeType = 0; + decode_runed_item_type(itemType, equipType, runeType); + int current = get_runed_item_count(equipType, runeType); + if (current <= 0) return false; + remove_runed_item(equipType, runeType); + if (get_runed_item_count(equipType, runeType) <= 0 && get_equipped_rune_for_slot(equipType) == runeType) { + clear_equipped_rune_for_slot(equipType); + } + return true; + } + + if (!is_breakable_item_type(itemType)) return false; + if (get_personal_count(itemType) <= 0) return false; + add_personal_count(itemType, -1); + return true; +} + bool try_consume_heal_scroll() { if (player_health > 0) return false; if (get_personal_count(ITEM_HEAL_SCROLL) <= 0) return false; @@ -304,13 +393,13 @@ void activate_quick_slot(int slot_index) { } set_equipped_rune_for_slot(equip_type, rune_type); update_max_health_from_equipment(); - speak_with_history(get_equipment_name(equip_type) + " equipped.", true); + speak_with_history(get_equipment_display_name_with_rune(equip_type, rune_type) + " equipped.", true); return; } unequip_equipment_type(equip_type); clear_equipped_rune_for_slot(equip_type); update_max_health_from_equipment(); - speak_with_history(get_equipment_name(equip_type) + " unequipped.", true); + speak_with_history(get_equipment_display_name_with_rune(equip_type, rune_type) + " unequipped.", true); return; } @@ -327,7 +416,7 @@ void activate_quick_slot(int slot_index) { equip_equipment_type(equip_type); set_equipped_rune_for_slot(equip_type, rune_type); update_max_health_from_equipment(); - speak_with_history(get_equipment_name(equip_type) + " equipped.", true); + speak_with_history(get_equipment_display_name_with_rune(equip_type, rune_type) + " equipped.", true); } void check_quick_slot_keys() { diff --git a/src/menus/equipment_menu.nvgt b/src/menus/equipment_menu.nvgt index b97f5e3..f4de707 100644 --- a/src/menus/equipment_menu.nvgt +++ b/src/menus/equipment_menu.nvgt @@ -16,9 +16,13 @@ bool has_any_equipment() { // Check runed items int[] runeable = get_runeable_equipment_types(); + int[] rune_types; + get_all_rune_types(rune_types); for (uint i = 0; i < runeable.length(); i++) { - if (get_runed_item_count(runeable[i], RUNE_SWIFTNESS) > 0) { - return true; + for (uint j = 0; j < rune_types.length(); j++) { + if (get_runed_item_count(runeable[i], rune_types[j]) > 0) { + return true; + } } } @@ -39,7 +43,7 @@ void check_equipment_menu() { 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 get_runed_item_display_name(equip_type, rune_type); } return base_name; } @@ -132,17 +136,21 @@ void run_equipment_menu() { rune_types.insert_last(RUNE_NONE); } - // Add runed items (currently only swiftness rune) + // Add runed items int[] runeable = get_runeable_equipment_types(); + int[] all_rune_types; + get_all_rune_types(all_rune_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)" : ""; + for (uint j = 0; j < all_rune_types.length(); j++) { + int rune_type = all_rune_types[j]; + int count = get_runed_item_count(equip_type, rune_type); + if (count <= 0) continue; + string name = get_full_equipment_name(equip_type, rune_type); + string status = is_runed_item_equipped(equip_type, rune_type) ? " (equipped)" : ""; options.insert_last(name + status); equipment_types.insert_last(equip_type); - rune_types.insert_last(RUNE_SWIFTNESS); + rune_types.insert_last(rune_type); } } string filter_text = ""; diff --git a/src/menus/inventory_core.nvgt b/src/menus/inventory_core.nvgt index 780e841..7d72bc3 100644 --- a/src/menus/inventory_core.nvgt +++ b/src/menus/inventory_core.nvgt @@ -16,13 +16,17 @@ void build_personal_inventory_options(string[]@ options, int[]@ item_types) { // Add runed items int[] runeable = get_runeable_equipment_types(); + int[] rune_types; + get_all_rune_types(rune_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 = "Runed " + get_base_equipment_name(equip_type) + " of Quickness"; + for (uint j = 0; j < rune_types.length(); j++) { + int rune_type = rune_types[j]; + int count = get_runed_item_count(equip_type, rune_type); + if (count <= 0) continue; + string name = get_runed_item_display_name(equip_type, rune_type); options.insert_last(name + ": " + count); - item_types.insert_last(encode_runed_item_type(equip_type, RUNE_SWIFTNESS)); + item_types.insert_last(encode_runed_item_type(equip_type, rune_type)); } } } @@ -49,11 +53,16 @@ void show_inventory() { // Add runed items summary string runed_info = ""; int[] runeable = get_runeable_equipment_types(); + int[] rune_types; + get_all_rune_types(rune_types); for (uint i = 0; i < runeable.length(); i++) { - int count = get_runed_item_count(runeable[i], RUNE_SWIFTNESS); - if (count > 0) { + int equip_type = runeable[i]; + for (uint j = 0; j < rune_types.length(); j++) { + int rune_type = rune_types[j]; + int count = get_runed_item_count(equip_type, rune_type); + if (count <= 0) continue; if (runed_info.length() > 0) runed_info += ", "; - runed_info += count + " Runed " + get_base_equipment_name(runeable[i]) + " of Quickness"; + runed_info += count + " " + get_runed_item_display_name(equip_type, rune_type); } } if (runed_info.length() > 0) { diff --git a/src/menus/storage_menu.nvgt b/src/menus/storage_menu.nvgt index f6b5c37..c6cdae3 100644 --- a/src/menus/storage_menu.nvgt +++ b/src/menus/storage_menu.nvgt @@ -271,13 +271,17 @@ void build_storage_inventory_options(string[]@ options, int[]@ item_types) { // Add stored runed items int[] runeable = get_runeable_equipment_types(); + int[] rune_types; + get_all_rune_types(rune_types); for (uint i = 0; i < runeable.length(); i++) { int equip_type = runeable[i]; - int count = get_stored_runed_item_count(equip_type, RUNE_SWIFTNESS); - if (count > 0) { - string name = "Runed " + get_base_equipment_name(equip_type) + " of Quickness"; + for (uint j = 0; j < rune_types.length(); j++) { + int rune_type = rune_types[j]; + int count = get_stored_runed_item_count(equip_type, rune_type); + if (count <= 0) continue; + string name = get_runed_item_display_name(equip_type, rune_type); options.insert_last(name + ": " + count); - item_types.insert_last(encode_runed_item_type(equip_type, RUNE_SWIFTNESS)); + item_types.insert_last(encode_runed_item_type(equip_type, rune_type)); } } } diff --git a/src/runes/rune_data.nvgt b/src/runes/rune_data.nvgt index 24547c1..e00d2ab 100644 --- a/src/runes/rune_data.nvgt +++ b/src/runes/rune_data.nvgt @@ -4,9 +4,11 @@ // Rune type constants const int RUNE_NONE = -1; const int RUNE_SWIFTNESS = 0; +const int RUNE_DESTRUCTION = 1; // Rune unlock tracking (persisted in save) bool rune_swiftness_unlocked = false; +bool rune_destruction_unlocked = false; // Boss defeat tracking bool unicorn_boss_defeated = false; @@ -37,26 +39,43 @@ 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"; + if (rune_type == RUNE_DESTRUCTION) return "Rune of Destruction"; 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"; + if (rune_type == RUNE_DESTRUCTION) return "Destruction"; return "Unknown"; } // Check if any rune has been unlocked bool any_rune_unlocked() { - return rune_swiftness_unlocked; + return rune_swiftness_unlocked || rune_destruction_unlocked; } // Check if a specific rune is unlocked bool is_rune_unlocked(int rune_type) { if (rune_type == RUNE_SWIFTNESS) return rune_swiftness_unlocked; + if (rune_type == RUNE_DESTRUCTION) return rune_destruction_unlocked; return false; } +void get_all_rune_types(int[]@ runeTypes) { + if (@runeTypes == null) return; + runeTypes.resize(0); + runeTypes.insert_last(RUNE_SWIFTNESS); + runeTypes.insert_last(RUNE_DESTRUCTION); +} + +void get_unlocked_rune_types(int[]@ runeTypes) { + if (@runeTypes == null) return; + runeTypes.resize(0); + if (rune_swiftness_unlocked) runeTypes.insert_last(RUNE_SWIFTNESS); + if (rune_destruction_unlocked) runeTypes.insert_last(RUNE_DESTRUCTION); +} + // Create dictionary key for runed item storage string make_runed_key(int equip_type, int rune_type) { return "" + equip_type + ":" + rune_type; @@ -89,8 +108,11 @@ void remove_runed_item(int equip_type, int rune_type) { // 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; + int[] runeTypes; + get_all_rune_types(runeTypes); + for (uint i = 0; i < runeTypes.length(); i++) { + if (get_runed_item_count(equip_type, runeTypes[i]) > 0) return true; + } return false; } @@ -191,9 +213,22 @@ int[] get_runeable_equipment_types() { return types; } +int[] get_runeable_equipment_types_for_rune(int rune_type) { + if (rune_type == RUNE_DESTRUCTION) { + int[] types; + types.insert_last(EQUIP_SPEAR); + types.insert_last(EQUIP_AXE); + types.insert_last(EQUIP_SLING); + types.insert_last(EQUIP_BOW); + return types; + } + return get_runeable_equipment_types(); +} + // Reset all rune data for new game void reset_rune_data() { rune_swiftness_unlocked = false; + rune_destruction_unlocked = false; unicorn_boss_defeated = false; runed_items.delete_all(); stored_runed_items.delete_all(); diff --git a/src/runes/rune_effects.nvgt b/src/runes/rune_effects.nvgt index 4005d7f..4033d53 100644 --- a/src/runes/rune_effects.nvgt +++ b/src/runes/rune_effects.nvgt @@ -1,6 +1,8 @@ // Rune Effects - Calculate bonuses from equipped runed items // This file handles the actual gameplay effects of runes +const int RUNE_DESTRUCTION_DAMAGE_MULTIPLIER = 2; + // Calculate total walking speed bonus from all equipped runed items int get_total_rune_walk_speed_bonus() { int bonus = 0; @@ -79,3 +81,11 @@ string get_rune_speed_description() { } return desc; } + +int apply_weapon_rune_damage(int baseDamage) { + if (baseDamage <= 0) return baseDamage; + if (equipped_weapon_rune == RUNE_DESTRUCTION) { + return baseDamage * RUNE_DESTRUCTION_DAMAGE_MULTIPLIER; + } + return baseDamage; +} diff --git a/src/save_system.nvgt b/src/save_system.nvgt index f5d780d..402172e 100644 --- a/src/save_system.nvgt +++ b/src/save_system.nvgt @@ -625,6 +625,13 @@ void reset_game_state() { invasion_scheduled_hour = -1; quest_roll_done_today = false; quest_queue.resize(0); + playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN; + playerItemBreaksToday = 0; + playerItemBreakPending = false; + playerItemBreakPendingType = -1; + playerItemBreakMessage = ""; + playerItemBreakSoundHandle = -1; + playerItemBreakSoundPending = false; walktimer.restart(); jumptimer.restart(); @@ -823,6 +830,7 @@ bool save_game_state() { // Rune system data saveData.set("rune_swiftness_unlocked", rune_swiftness_unlocked); + saveData.set("rune_destruction_unlocked", rune_destruction_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); @@ -869,6 +877,10 @@ bool save_game_state() { saveData.set("time_invasion_roll_done_today", invasion_roll_done_today); saveData.set("time_invasion_scheduled_hour", invasion_scheduled_hour); saveData.set("time_invasion_enemy_type", invasion_enemy_type); + saveData.set("player_item_break_chance", playerItemBreakChance); + saveData.set("player_item_breaks_today", playerItemBreaksToday); + saveData.set("player_item_break_pending", playerItemBreakPending); + saveData.set("player_item_break_pending_type", playerItemBreakPendingType); saveData.set("quest_roll_done_today", quest_roll_done_today); saveData.set("wight_spawn_chance", wight_spawn_chance); saveData.set("wight_spawned_this_night", wight_spawned_this_night); @@ -1186,6 +1198,7 @@ bool load_game_state_from_file(const string&in filename) { // Load rune system data rune_swiftness_unlocked = get_bool(saveData, "rune_swiftness_unlocked", false); + rune_destruction_unlocked = get_bool(saveData, "rune_destruction_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)); @@ -1287,6 +1300,31 @@ bool load_game_state_from_file(const string&in filename) { if (invasion_chance > 100) invasion_chance = 100; if (invasion_scheduled_hour < -1) invasion_scheduled_hour = -1; if (invasion_scheduled_hour > 23) invasion_scheduled_hour = -1; + playerItemBreakChance = int(get_number(saveData, "player_item_break_chance", PLAYER_ITEM_BREAK_CHANCE_MIN)); + playerItemBreaksToday = int(get_number(saveData, "player_item_breaks_today", 0)); + playerItemBreakPending = get_bool(saveData, "player_item_break_pending", false); + playerItemBreakPendingType = int(get_number(saveData, "player_item_break_pending_type", -1)); + if (playerItemBreakChance < PLAYER_ITEM_BREAK_CHANCE_MIN) { + playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN; + } + if (playerItemBreakChance > PLAYER_ITEM_BREAK_CHANCE_MAX) { + playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MAX; + } + if (playerItemBreaksToday < 0) playerItemBreaksToday = 0; + if (playerItemBreaksToday > PLAYER_ITEM_BREAKS_PER_DAY) { + playerItemBreaksToday = PLAYER_ITEM_BREAKS_PER_DAY; + } + if (playerItemBreakPending) { + bool validPending = (playerItemBreakPendingType >= 0 && playerItemBreakPendingType < ITEM_COUNT) + || is_runed_item_type(playerItemBreakPendingType); + if (!validPending) { + playerItemBreakPending = false; + playerItemBreakPendingType = -1; + } + } else { + playerItemBreakPendingType = -1; + } + reset_player_item_break_audio_state(); quest_roll_done_today = get_bool(saveData, "quest_roll_done_today", false); wight_spawn_chance = int(get_number(saveData, "wight_spawn_chance", WIGHT_SPAWN_CHANCE_START)); wight_spawned_this_night = get_bool(saveData, "wight_spawned_this_night", false); diff --git a/src/time_system.nvgt b/src/time_system.nvgt index c7b05a2..5e6e78e 100644 --- a/src/time_system.nvgt +++ b/src/time_system.nvgt @@ -12,6 +12,14 @@ bool is_daytime = true; bool sun_setting_warned = false; bool sunrise_warned = false; +int playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN; +int playerItemBreaksToday = 0; +bool playerItemBreakPending = false; +int playerItemBreakPendingType = -1; +string playerItemBreakMessage = ""; +int playerItemBreakSoundHandle = -1; +bool playerItemBreakSoundPending = false; + // Crossfade state bool crossfade_active = false; bool crossfade_to_night = false; // true = fading to night, false = fading to day @@ -53,9 +61,95 @@ void init_time() { invasion_roll_done_today = false; invasion_scheduled_hour = -1; invasion_enemy_type = "bandit"; + reset_player_item_break_state(); update_ambience(true); // Force start } +void reset_player_item_break_audio_state() { + playerItemBreakMessage = ""; + playerItemBreakSoundHandle = -1; + playerItemBreakSoundPending = false; +} + +void reset_player_item_break_state() { + playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN; + playerItemBreaksToday = 0; + playerItemBreakPending = false; + playerItemBreakPendingType = -1; + reset_player_item_break_audio_state(); +} + +void queue_player_item_break_message(const string&in message) { + if (message.length() == 0) return; + playerItemBreakMessage = message; + playerItemBreakSoundHandle = p.play_stationary("sounds/items/item_breaks.ogg", false); + playerItemBreakSoundPending = true; +} + +void update_player_item_break_message() { + if (!playerItemBreakSoundPending) return; + if (playerItemBreakSoundHandle != -1 && p.sound_is_active(playerItemBreakSoundHandle)) { + return; + } + playerItemBreakSoundHandle = -1; + playerItemBreakSoundPending = false; + if (playerItemBreakMessage.length() > 0) { + speak_with_history(playerItemBreakMessage, true); + playerItemBreakMessage = ""; + } +} + +void resolve_pending_player_item_break() { + if (!playerItemBreakPending) return; + if (x > BASE_END) return; + if (playerItemBreakPendingType == -1) { + playerItemBreakPending = false; + return; + } + if (remove_breakable_personal_item(playerItemBreakPendingType)) { + cleanup_equipment_after_inventory_change(); + string itemName = get_breakable_item_name(playerItemBreakPendingType); + queue_player_item_break_message(itemName + " is worn out so you discard it."); + } + playerItemBreakPending = false; + playerItemBreakPendingType = -1; +} + +void attempt_player_item_break_check() { + if (playerItemBreakPending) return; + if (playerItemBreaksToday >= PLAYER_ITEM_BREAKS_PER_DAY) return; + + int[] breakableItems; + get_breakable_personal_item_types(breakableItems); + if (breakableItems.length() == 0) return; + + int roll = random(1, 100); + if (roll <= playerItemBreakChance) { + int pickIndex = random(0, int(breakableItems.length()) - 1); + int itemType = breakableItems[pickIndex]; + playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MIN; + playerItemBreaksToday++; + + if (x <= BASE_END) { + if (remove_breakable_personal_item(itemType)) { + cleanup_equipment_after_inventory_change(); + string itemName = get_breakable_item_name(itemType); + queue_player_item_break_message(itemName + " is worn out so you discard it."); + } + } else { + playerItemBreakPending = true; + playerItemBreakPendingType = itemType; + } + } else { + if (playerItemBreakChance < PLAYER_ITEM_BREAK_CHANCE_MAX) { + playerItemBreakChance += PLAYER_ITEM_BREAK_CHANCE_INCREMENT; + } + if (playerItemBreakChance > PLAYER_ITEM_BREAK_CHANCE_MAX) { + playerItemBreakChance = PLAYER_ITEM_BREAK_CHANCE_MAX; + } + } +} + string get_invasion_enemy_type_for_terrain(const string&in terrain_type) { for (uint i = 0; i < invasion_terrain_enemy_map.length(); i++) { string entry = invasion_terrain_enemy_map[i]; @@ -452,6 +546,9 @@ bool should_attempt_resident_foraging(int hour) { } void update_time() { + update_player_item_break_message(); + resolve_pending_player_item_break(); + if (hour_timer.elapsed >= MS_PER_HOUR) { hour_timer.restart(); current_hour++; @@ -464,6 +561,7 @@ void update_time() { invasion_roll_done_today = false; invasion_scheduled_hour = -1; quest_roll_done_today = false; + playerItemBreaksToday = 0; } // Residents consume food every 8 hours (at midnight, 8am, 4pm) @@ -530,6 +628,7 @@ void update_time() { } attempt_daily_invasion(); keep_base_fires_fed(); + attempt_player_item_break_check(); update_incense_burning(); attempt_hourly_flying_creature_spawn(); attempt_hourly_boar_spawn();