Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions cast.lic
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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"]
Expand All @@ -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!
Expand Down
7 changes: 7 additions & 0 deletions profiles/base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
103 changes: 103 additions & 0 deletions spec/cast_spec.rb
Original file line number Diff line number Diff line change
@@ -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