diff --git a/cast.lic b/cast.lic index e07bc2e5fd..0e6457d3b1 100644 --- a/cast.lic +++ b/cast.lic @@ -499,13 +499,14 @@ class Cast rest = @total_mana - prep_mana charge_values = [] charges_left = num_charges - while rest > 0 + while rest > 0 && charges_left > 0 next_charge = (rest * 1.0 / charges_left).ceil charge_values << next_charge rest -= next_charge charges_left -= 1 end - [prep_mana, charge_values] + # Anything the charges could not take goes back into the prep. + [prep_mana + rest, charge_values] end def optimize_charge_time @@ -514,9 +515,9 @@ class Cast max_charges = 10 max_charges.downto(min_charges).each do |num_charges| - prep_mana, charge_values = distribute_num_charges(num_charges) - charge_time = calculate_charge_time(charge_values) - return [prep_mana, charge_values] if charge_time <= charge_time_budget_secs + prep_mana, cambrinth = charge_plan(num_charges) + charge_time = calculate_charge_time(cambrinth.flatten) + return [prep_mana, cambrinth] if charge_time <= charge_time_budget_secs end end @@ -539,21 +540,61 @@ class Cast end if @charge.end_with?("s") - prep_mana, charge_values = optimize_charge_time + prep_mana, cambrinth = optimize_charge_time else - prep_mana, charge_values = distribute_num_charges + prep_mana, cambrinth = charge_plan end logd "Optimized prep and charges:", { "Prep mana" => prep_mana, - "Cambrinth charges" => charge_values, - "Expected charge time" => calculate_charge_time(charge_values), + "Cambrinth charges" => cambrinth, + "Expected charge time" => calculate_charge_time(cambrinth.flatten), } - # Create an array of arrays representing amounts to charge in each cambrinth item. + @spell_data["mana"] = prep_mana + @spell_data["cambrinth"] = cambrinth + end + + # Works out the base prep and the charges for each cambrinth item. + # + # With cambrinth_distribute_charges set, this hands the work to DRCA so that + # ,cast and the waggle scripts charge the same way. Each item takes as much as + # its cap allows, in the order the profile lists them, so a small worn item + # fills before a large stored one. + # + # Without the setting, the charges are all the same size and each item is + # filled up to its cap before the next one is used. A charge larger than an + # item's cap skips that item for good, so small items often go unused. + def charge_plan(num_charges = @charge.to_i) + prep_mana, charge_values = distribute_num_charges(num_charges) + if distribute_charges? + cambrinth, leftover = DRCA.allocate_cambrinth_charges(charge_values.sum, @camb_items, num_charges) + else + cambrinth, leftover = pack_charge_values(charge_values) + end + if leftover > 0 + logd "Ran out of room in all cambrinth items, will add the remaining #{leftover} to base prep (now #{prep_mana + leftover})." + end + [prep_mana + leftover, cambrinth] + end + + def distribute_charges? + return false unless @settings.cambrinth_distribute_charges + + unless DRCA.respond_to?(:allocate_cambrinth_charges) + logd "cambrinth_distribute_charges is set but this Lich has no DRCA.allocate_cambrinth_charges. Using the built-in split." + return false + end + true + end + + # The original scheme: fill each item up to its cap, in order, and never come + # back to an item once a charge has passed it by. + def pack_charge_values(charge_values) cambrinth = Array.new(@camb_items.length) { [] } camb_index = 0 already_charged = 0 + leftover = 0 until charge_values.empty? do charge_value = charge_values.shift camb_cap = @camb_items[camb_index]["cap"] @@ -568,18 +609,13 @@ class Cast already_charged = 0 camb_index += 1 else - remainder = charge_value + charge_values.sum - prep_mana += remainder - logd "Ran out of room in all cambrinth items, will add the remaining #{remainder} to base prep (now #{prep_mana})." + leftover = charge_value + charge_values.sum break end end # Clean up any empty arrays at the end (which may be all of them, if we're doing no charging) - cambrinth = cambrinth.reverse.drop_while { |c| c.empty? }.reverse - - @spell_data["mana"] = prep_mana - @spell_data["cambrinth"] = cambrinth + [cambrinth.reverse.drop_while { |c| c.empty? }.reverse, leftover] end def set_target! diff --git a/profiles/base.yaml b/profiles/base.yaml index eb241e45e0..7fb888fca9 100644 --- a/profiles/base.yaml +++ b/profiles/base.yaml @@ -741,6 +741,13 @@ cambrinth_items: cambrinth_num_charges: 4 # invoke a specific cambrinth amount -- useful for traders with Avtalia Array cambrinth_invoke_exact_amount: false +# Spread charges over your cambrinth_items instead of charging each item in full. +# Only matters if cambrinth_items lists more than one item. Without this, a spell +# with "cambrinth: [4, 10]" charges 4 and 10 into EVERY item. With it, the charges +# are packed into each item up to its cap, then the next item is used. It also +# makes use_auto_mana respect each item's cap. A single cambrinth item is +# unaffected either way. An older Lich ignores this setting. +cambrinth_distribute_charges: false # Trader only - enables starlight management - how many seconds in between checking starlight auras in combat # see https://github.com/elanthia-online/dr-scripts/wiki/Trader-Tutorials aura_frequency: diff --git a/spec/cast_spec.rb b/spec/cast_spec.rb new file mode 100644 index 0000000000..1a3f8ad9ec --- /dev/null +++ b/spec/cast_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'ostruct' + +require_relative 'spec_helper' + +load_lic_class('cast.lic', 'Cast') + +RSpec.describe Cast do + # Athlya's profile: two small worn items and one large stored one. This shape + # shows the difference between the two charge schemes. + def three_cambrinth_items + [ + { 'name' => 'cambrinth earcuff', 'cap' => 4, 'stored' => false }, + { 'name' => 'cambrinth anklet', 'cap' => 4, 'stored' => false }, + { 'name' => 'sea urchin', 'cap' => 48, 'stored' => true } + ] + end + + def build_instance(distribute: false, total_mana: 25, charge: '3', camb_items: nil) + instance = described_class.allocate + { + settings: OpenStruct.new(cambrinth_distribute_charges: distribute), + camb_items: camb_items || three_cambrinth_items, + total_mana: total_mana, + min_prep_mana: 1, + charge: charge, + runestone: nil, + arcana_ranks: 500, + spell_data: {}, + debug: false + }.each { |k, v| instance.instance_variable_set(:"@#{k}", v) } + instance + end + + describe '#charge_plan without cambrinth_distribute_charges' do + it 'splits the charges evenly and skips items too small for one' do + # 25 mana, 3 charges: prep 7, then charges of 6, 6, 6. + prep, cambrinth = build_instance(distribute: false, total_mana: 25).send(:charge_plan) + expect(prep).to eq(7) + expect(cambrinth).to eq([[], [], [6, 6, 6]]) + end + + it 'uses the small items only when a charge happens to fit' do + # 16 mana, 3 charges: prep 4, then charges of 4, 4, 4. + prep, cambrinth = build_instance(distribute: false, total_mana: 16).send(:charge_plan) + expect(prep).to eq(4) + expect(cambrinth).to eq([[4], [4], [4]]) + end + + it 'moves mana that fits in no item into the prep' do + items = [{ 'name' => 'ring', 'cap' => 4, 'stored' => false }] + prep, cambrinth = build_instance(distribute: false, total_mana: 40, camb_items: items).send(:charge_plan) + expect(cambrinth.flatten.sum + prep).to eq(40) + end + end + + describe '#charge_plan with cambrinth_distribute_charges' do + it 'asks DRCA to allocate the charges and uses the answer' do + instance = build_instance(distribute: true, total_mana: 25) + expect(DRCA).to receive(:allocate_cambrinth_charges) + .with(18, three_cambrinth_items, 3) + .and_return([[[4], [4], [10]], 0]) + prep, cambrinth = instance.send(:charge_plan) + expect(prep).to eq(7) + expect(cambrinth).to eq([[4], [4], [10]]) + end + + it 'moves the mana DRCA could not place into the prep' do + instance = build_instance(distribute: true, total_mana: 25) + allow(DRCA).to receive(:allocate_cambrinth_charges).and_return([[[4], [4], [8]], 2]) + prep, cambrinth = instance.send(:charge_plan) + expect(prep).to eq(9) + expect(cambrinth.flatten.sum + prep).to eq(25) + end + + # The harness DRCA has no allocate_cambrinth_charges, exactly like an older + # Lich, so this drives the real guard rather than a stub of it. + it 'falls back to the built-in split when Lich has no allocator' do + instance = build_instance(distribute: true, total_mana: 25) + expect(DRCA).not_to respond_to(:allocate_cambrinth_charges) + prep, cambrinth = instance.send(:charge_plan) + expect(prep).to eq(7) + expect(cambrinth).to eq([[], [], [6, 6, 6]]) + end + end + + describe '#distribute_num_charges' do + it 'puts the remainder in the first charge' do + prep, charges = build_instance(total_mana: 60, charge: '4').send(:distribute_num_charges) + expect(prep).to eq(12) + expect(charges).to eq([12, 12, 12, 12]) + end + + it 'returns undistributed mana to the prep instead of dividing by zero' do + instance = build_instance(total_mana: 60, charge: '0') + instance.instance_variable_set(:@runestone, 'runestone') + prep, charges = instance.send(:distribute_num_charges) + expect(charges).to eq([]) + expect(prep).to eq(60) + end + end +end