diff --git a/scripts/Killtracker.lic b/scripts/killtracker.lic similarity index 81% rename from scripts/Killtracker.lic rename to scripts/killtracker.lic index d81b12379..ce8cf828d 100644 --- a/scripts/Killtracker.lic +++ b/scripts/killtracker.lic @@ -13,9 +13,15 @@ contributors: Nisugi game: Gemstone tags: hunting, combat, tracking, gemstones, jewels, dust, klocks, data - version: 2.11 + version: 2.12 Change Log: + v2.12 (2026-05-24) + - Added separate codex-eligible search counter (weekly_codex_searches) + - Gem and codex eligibility are gated independently + - Added codex tracking for onslaught creatures (10/week cap) + - Added ;kt codex report command and codex section in summary + - Codex finds submitted to external spreadsheet when enabled v2.11 (2026-05-24) - Fix for capitalization in dust found messaging v2.10 (2025-09-21) @@ -90,6 +96,10 @@ module Killtracker if File.exist?(@filename) begin $killtracker = YAML.load_file(@filename) + unless $killtracker.is_a?(Hash) + log_data_warning("#{@filename} did not load as a Hash; starting with empty data") + $killtracker = {} + end # Only create initial_load backup if it doesn't exist initial_backup = File.join(@backup_dir, "initial_load.yaml") @@ -122,6 +132,10 @@ module Killtracker $killtracker[:weekly_counts] ||= {} $killtracker[:jewel_found_this_week] ||= false $killtracker[:debug_eligibility] ||= false + $killtracker[:codex_found] ||= {} + $killtracker[:weekly_codex] ||= 0 + $killtracker[:weekly_codex_searches] ||= 0 + $killtracker[:searches_since_codex] ||= 0 end def self.create_backup(reason = "manual") @@ -153,6 +167,10 @@ module Killtracker latest_backup = backups.last begin $killtracker = YAML.load_file(latest_backup) + unless $killtracker.is_a?(Hash) + log_data_warning("#{latest_backup} did not load as a Hash; restore aborted") + return false + end echo "Successfully restored from backup: #{File.basename(latest_backup)}" save(true) return true @@ -162,6 +180,10 @@ module Killtracker end end + def self.log_data_warning(message) + Lich.log("Killtracker data warning: #{message}") rescue nil + end + def self.save(force = false) @last_save_hash ||= 0 @next_save_time ||= 0 @@ -206,7 +228,7 @@ module Killtracker # Check for old :searches key that needs to be renamed if $killtracker[:dust_found] $killtracker[:dust_found].each do |_, ev| - if ev.key?(:searches) + if ev.is_a?(Hash) && ev.key?(:searches) needs_migration = true break end @@ -232,7 +254,7 @@ module Killtracker end $killtracker[:dust_found].each do |_key, ev| - if ev.key?(:searches) + if ev.is_a?(Hash) && ev.key?(:searches) ev[:searches_since] = ev.delete(:searches) end end @@ -248,6 +270,7 @@ module Killtracker end events.each do |_, ev| + next unless ev.is_a?(Hash) ev[:searches_week] = ev[:searches_week].to_i if ev[:searches_week] ev[:searches_since] = ev[:searches_since].to_i if ev[:searches_since] ev[:room] = ev[:room].to_s if ev[:room] @@ -330,16 +353,22 @@ module Killtracker total_this_week = $killtracker[:weekly_ascension_searches].to_i weekly_dust = $killtracker[:weekly_dust].to_i weekly_gemstone = $killtracker[:weekly_gemstone].to_i + weekly_codex = $killtracker[:weekly_codex].to_i + weekly_codex_searches = $killtracker[:weekly_codex_searches].to_i $killtracker[:weekly_counts][:"week_#{finished_week}_ascension_searches"] = total_this_week $killtracker[:weekly_counts][:"week_#{finished_week}_dust"] = weekly_dust $killtracker[:weekly_counts][:"week_#{finished_week}_gemstone"] = weekly_gemstone + $killtracker[:weekly_counts][:"week_#{finished_week}_codex"] = weekly_codex + $killtracker[:weekly_counts][:"week_#{finished_week}_codex_searches"] = weekly_codex_searches $killtracker[:monthly_ascension_searches] += total_this_week $killtracker[:weekly_ascension_searches] = 0 $killtracker[:weekly_gemstone] = 0 $killtracker[:weekly_dust] = 0 + $killtracker[:weekly_codex] = 0 + $killtracker[:weekly_codex_searches] = 0 $killtracker[:jewel_found_this_week] = false $killtracker[:last_week_reset] = $killtracker[:cached_reset_time] @@ -406,6 +435,8 @@ module Killtracker $killtracker[:weekly_gemstone] = 0 $killtracker[:monthly_gemstones] = 0 $killtracker[:weekly_dust] = 0 + $killtracker[:weekly_codex] = 0 + $killtracker[:weekly_codex_searches] = 0 $killtracker[:monthly_ascension_searches] = 0 $killtracker[:jewel_found_this_week] = false @@ -432,6 +463,14 @@ module Killtracker end end + $killtracker[:codex_found].each do |key, _| + next unless key.is_a?(Integer) + local = tz.to_local(Time.at(key)) + if local.strftime("%U").to_i == current_week && local.year == current_year + $killtracker[:weekly_codex] += 1 + end + end + $killtracker[:weekly_counts].each do |week_key, searches| next unless week_key.to_s.include?('ascension_searches') next unless searches.is_a?(Integer) @@ -448,6 +487,7 @@ module Killtracker update_eligibility respond("Counters successfully recalculated.") respond(" Weekly gems: #{$killtracker[:weekly_gemstone]}, Monthly gems: #{$killtracker[:monthly_gemstones]}") + respond(" Weekly codex: #{$killtracker[:weekly_codex]}") respond(" Jewel found this week: #{$killtracker[:jewel_found_this_week]}") rescue => e respond("Error during backfill: #{e.message}") @@ -465,6 +505,7 @@ module Killtracker respond(";kt gemstones report - find report broken down by week") respond(";kt jewel report - find report of all jewels") respond(";kt dust report - find report of all dust") + respond(";kt codex report - find report of all codex") respond(";kt fix find count - fixes monthly/weekly gemstone count in summary") respond(";kt save - force search data to be saved to file") respond(";kt backup - create a manual backup of current data") @@ -480,15 +521,19 @@ module Killtracker begin gems_total = $killtracker[:jewel_found].size dust_total = $killtracker[:dust_found].size + codex_total = $killtracker[:codex_found].size weekly_searches = $killtracker[:weekly_ascension_searches].to_i monthly_searches = calculate_monthly_eligible_searches gems_this_week = $killtracker[:weekly_gemstone].to_i dust_this_week = $killtracker[:weekly_dust].to_i + codex_this_week = $killtracker[:weekly_codex].to_i + codex_searches_this_week = $killtracker[:weekly_codex_searches].to_i gems_this_month = $killtracker[:monthly_gemstones].to_i since_last_gem = $killtracker[:searches_since_jewel].to_i since_last_dust = $killtracker[:searches_since_dust].to_i + since_last_codex = $killtracker[:searches_since_codex].to_i total_searches_for_gems = $killtracker[:jewel_found].values .map { |ev| ev[:searches_week].to_i } @@ -498,10 +543,18 @@ module Killtracker .map { |ev| ev[:searches_week].to_i } .sum - avg_per_gem = gems_total > 0 ? (total_searches_for_gems.to_f / gems_total).round : 0 - avg_per_dust = dust_total > 0 ? (total_searches_for_dust.to_f / dust_total).round : 0 + total_searches_for_codex = $killtracker[:codex_found].values + .map { |ev| ev[:searches_week].to_i } + .sum - remaining_gems = [0, 3 - gems_this_month].max + avg_per_gem = gems_total > 0 ? (total_searches_for_gems.to_f / gems_total).round : 0 + avg_per_dust = dust_total > 0 ? (total_searches_for_dust.to_f / dust_total).round : 0 + avg_per_codex = codex_total > 0 ? (total_searches_for_codex.to_f / codex_total).round : 0 + + remaining_gems = [0, 3 - gems_this_month].max + remaining_codex = [0, CODEX_WEEKLY_CAP - codex_this_week].max + + codex_status = codex_currently_eligible? ? "Eligible (#{codex_this_week}/#{CODEX_WEEKLY_CAP})" : "Ineligible (#{codex_this_week}/#{CODEX_WEEKLY_CAP})" # Determine weekly status with gem number if currently_eligible? @@ -535,8 +588,16 @@ module Killtracker [" This Week", dust_this_week], [" Avg Searches/Dust", avg_per_dust.with_commas], [], + ["Codex Found (all)", codex_total.with_commas], + [" This Week", codex_this_week], + [" Codex Searches This Week", codex_searches_this_week.with_commas], + [" Remaining This Week", remaining_codex], + [" Status", codex_status], + [" Avg Searches/Codex", avg_per_codex.with_commas], + [], ["Since Last Gem", since_last_gem.with_commas], ["Since Last Dust", since_last_dust.with_commas], + ["Since Last Codex", since_last_codex.with_commas], ] table = Terminal::Table.new( @@ -607,6 +668,35 @@ module Killtracker end end + def self.codex_report + begin + events = $killtracker[:codex_found].sort_by { |key, _| key.to_i } + + total_searches_for_codex = events.map { |_, ev| ev[:searches_week].to_i }.sum + + rows = events.map do |key, ev| + time_str = format_time_eastern(key.to_i) + searches = (ev[:searches_week] || 0).with_commas + since_last = (ev[:searches_since] || 0).with_commas + creature = ev[:creature] || "" + room = ev[:room] || "" + name = ev[:name] || "" + [time_str, searches, since_last, creature, room, name] + end + + title = "Detailed Codex Report: #{$killtracker[:codex_found].size} Codex over #{total_searches_for_codex.with_commas} Eligible Searches" + table = Terminal::Table.new( + title: title, + headings: ["Time", "Week Searches", "Since Last Codex", "Creature", "Room", "Name"], + rows: rows + ) + + respond table.to_s + rescue => e + respond "Error generating codex report: #{e.message}" + end + end + def self.gemstones_report(weeks_back = nil) begin tz = get_eastern_tz @@ -629,6 +719,14 @@ module Killtracker since: ev[:searches_since] ) end + $killtracker[:codex_found].each do |key, ev| + next unless key.is_a?(Integer) && ev.is_a?(Hash) + combined << ev.merge( + timestamp: key, + type: "Codex", + since: ev[:searches_since] + ) + end events_by_week = combined.group_by do |ev| tz.to_local(Time.at(ev[:timestamp])).strftime("%U").to_i @@ -693,6 +791,10 @@ module Killtracker f.flock(File::LOCK_SH) content = f.read @jewel_eligibility = content.empty? ? {} : YAML.load(content) || {} + unless @jewel_eligibility.is_a?(Hash) + log_data_warning("#{eligibility_file} did not load as a Hash; using empty eligibility data") + @jewel_eligibility = {} + end end else @jewel_eligibility = {} @@ -751,6 +853,8 @@ module Killtracker ev = $killtracker[:dust_found][key] when "jewel" ev = $killtracker[:jewel_found][key] + when "codex" + ev = $killtracker[:codex_found][key] end return unless ev @@ -801,6 +905,16 @@ module Killtracker end respond(" Sent #{sent_dust} dust records") + respond("Sending found codex...") + sent_codex = 0 + $killtracker[:codex_found].each do |timestamp, _| + next unless timestamp.is_a?(Integer) + if send_to_sheet("codex", timestamp) + sent_codex += 1 + end + end + respond(" Sent #{sent_codex} codex records") + respond("Sending complete.") respond("View the data at: https://docs.google.com/spreadsheets/d/1IOLs8AGRR45Kr6Y9nz6CXlMVBKYR7cHLaz0jjAbjMv0") respond("") @@ -902,6 +1016,18 @@ module Killtracker eligible end + def self.codex_currently_eligible? + eligible = $killtracker[:weekly_codex].to_i < CODEX_WEEKLY_CAP + + if $killtracker[:debug_eligibility] + echo "Codex Eligibility Check:" + echo " Weekly codex: #{$killtracker[:weekly_codex]} / #{CODEX_WEEKLY_CAP}" + echo " Eligible: #{eligible}" + end + + eligible + end + def self.update_eligibility begin @eligibility_file = File.join(DATA_DIR, XMLData.game, "jewel_eligibility.yaml") @@ -914,6 +1040,10 @@ module Killtracker f.flock(File::LOCK_SH) content = f.read @jewel_eligibility = content.empty? ? {} : YAML.load(content) || {} + unless @jewel_eligibility.is_a?(Hash) + log_data_warning("#{@eligibility_file} did not load as a Hash; using empty eligibility data") + @jewel_eligibility = {} + end end end @@ -967,13 +1097,14 @@ module Killtracker errors = [] [:weekly_ascension_searches, :monthly_ascension_searches, :searches_since_jewel, - :searches_since_dust, :monthly_gemstones, :weekly_gemstone, :weekly_dust].each do |key| + :searches_since_dust, :monthly_gemstones, :weekly_gemstone, :weekly_dust, + :weekly_codex, :weekly_codex_searches, :searches_since_codex].each do |key| if $killtracker[key] && $killtracker[key] < 0 errors << "#{key} is negative: #{$killtracker[key]}" end end - [$killtracker[:jewel_found], $killtracker[:dust_found]].each do |events| + [$killtracker[:jewel_found], $killtracker[:dust_found], $killtracker[:codex_found]].each do |events| next unless events events.each do |timestamp, _data| unless timestamp.is_a?(Integer) @@ -1000,6 +1131,10 @@ module Killtracker errors << "Weekly gemstone exceeds maximum: #{$killtracker[:weekly_gemstone]}" end + if $killtracker[:weekly_codex].to_i > CODEX_WEEKLY_CAP + errors << "Weekly codex exceeds maximum: #{$killtracker[:weekly_codex]}" + end + errors end @@ -1016,6 +1151,8 @@ module Killtracker FOUND_DUST = %r{You notice a scintillating mote of gemstone dust on the ground and gather it quickly\.}i FOUND_GEMSTONE = %r{ \*\* A glint of light catches your eye, and you notice an? (?[^<]+) at your feet! \*\*} + FOUND_CODEX = %r{Among the remains, you find an? (?[^<]+), which falls alongside your feet\.} + CODEX_WEEKLY_CAP = 10 unless const_defined?(:CODEX_WEEKLY_CAP) ASCENSION_CREATURES = Regexp.union( %r{armored battle mastodon}, %r{black valravn}, @@ -1064,6 +1201,16 @@ module Killtracker %r{merrow oracle} ) + ONSLAUGHT_CREATURES = Regexp.union( + %r{battle-worn Empyrean captain}, + %r{branded goliath diviner}, + %r{burly goliath engineer}, + %r{haze-shrouded goliath diviner}, + %r{masked goliath plunderer}, + %r{radiant-eyed goliath auramancer}, + %r{tawny armor-clad pegasus} + ) + def self.parse_downstream(line) case line when FOUND_GEMSTONE @@ -1089,6 +1236,26 @@ module Killtracker REPORT_QUEUE.push(report) REPORT_QUEUE.push(["send jewel report", key]) if $killtracker[:submit_finds] + when FOUND_CODEX + key = Time.now.to_i + name = Regexp.last_match[:n] + room = Room.current.id.to_s + + create_backup("pre_codex_find") + + $killtracker[:weekly_codex] += 1 + $killtracker[:codex_found][key] = { + searches_since: $killtracker[:searches_since_codex], + searches_week: $killtracker[:weekly_ascension_searches], + name: name, + room: room, + creature: $killtracker[:creature] + } + report = ['found codex', $killtracker[:creature], $killtracker[:weekly_codex], $killtracker[:searches_since_codex]] + $killtracker[:searches_since_codex] = 0 + REPORT_QUEUE.push(report) + REPORT_QUEUE.push(["send codex report", key]) if $killtracker[:submit_finds] + when FOUND_DUST key = Time.now.to_i room = Room.current.id.to_s @@ -1110,7 +1277,14 @@ module Killtracker maybe_reset_monthly_counter name = Regexp.last_match[:creature] $killtracker[:creature] = name - if ASCENSION_CREATURES.match?(name) + + is_ascension = ASCENSION_CREATURES.match?(name) + is_onslaught = ONSLAUGHT_CREATURES.match?(name) + + # Ascension and onslaught creatures both drop gems and dust. + # Codex only drops from onslaught creatures. + # Gem and codex eligibility are gated independently. + if is_ascension || is_onslaught # Always count dust searches (dust has no limits) $killtracker[:searches_since_dust] += 1 @@ -1124,6 +1298,16 @@ module Killtracker elsif $killtracker[:debug_eligibility] echo "Search not counted for gems - currently ineligible (#{name})" end + + # Onslaught creatures additionally contribute to codex tracking + if is_onslaught + if codex_currently_eligible? + $killtracker[:weekly_codex_searches] += 1 + $killtracker[:searches_since_codex] += 1 + elsif $killtracker[:debug_eligibility] + echo "Search not counted for codex - currently ineligible (#{name})" + end + end end end line @@ -1173,10 +1357,16 @@ module Killtracker Lich::Messaging.stream_window("Found a gemstone in #{week} searches. (#{creature}) - Since last Jewel: (#{jewel})", "speech") save update_eligibility + when "found codex" + _, creature, weekly_codex, since = report + Lich::Messaging.stream_window("Found a codex after #{since} searches. (#{creature}) - Week Total: (#{weekly_codex}/#{CODEX_WEEKLY_CAP})", "speech") + save when /send dust report/ send_to_sheet("dust", report[1]) when /send jewel report/ send_to_sheet("jewel", report[1]) + when /send codex report/ + send_to_sheet("codex", report[1]) end end @@ -1210,6 +1400,8 @@ module Killtracker Killtracker.jewel_report when /dust report/ Killtracker.dust_report + when /codex report/ + Killtracker.codex_report when /gemstones? report(?:\s+(\d+))?$/ weeks = $1 ? $1.to_i : nil Killtracker.gemstones_report(weeks)