1
0
mirror of https://github.com/ComfyFactory/ComfyFactorio.git synced 2025-02-05 13:15:03 +02:00

Class related changes

Changes:
- Captain cabin's market now sells random class.
- Disabled 3 classes: Gourmet, Deckhand, Boatswain.
- Added 3 new classes: Medic, Doctor and Shaman.
- More than one class of same type can be acquired now. Same classes now can be offered again every 5 class purchases.
This commit is contained in:
Piratux 2023-01-31 00:25:43 +02:00
parent 0f2bf5a5f6
commit a1a3e7d1b9
12 changed files with 249 additions and 42 deletions

View File

@ -183,6 +183,7 @@ market_description_upgrade_rockets=Unlock the sale of rockets at island markets.
market_description_upgrade_turrets=Upgrade your ship's artilerry turrets max health and also fully heal them.
market_description_purchase_class=Purchase the class __1__.
market_description_extra_time_at_sea=Relax at sea: Increase the next destination's loading time by 60 seconds.
market_description_random_class=Purchase a random class to boost your powers.
market_description_reroll_prices=Reroll prices.
market_event_sell=__1__ sold __2__ for __3__.
@ -275,7 +276,7 @@ class_iron_leg=Iron Leg
class_iron_leg_explanation_advanced=They receive __1__% less damage when carrying at least __2__ iron ore.
class_quartermaster=Quartermaster
# class_quartermaster_explanation=Nearby crewmates get +10% physical attack and generate ore for the cabin.
class_quartermaster_explanation_advanced=Nearby crewmates (at __1__ tile radius) get +__2__% physical attack bonus and generate ore for the cabin (ore amount depends on nearby crewmate count).
class_quartermaster_explanation_advanced=Nearby crewmates (at __1__ tile radius) get +__2__% physical attack bonus and generate ore for the cabin (ore amount depends on nearby crewmate count). No ore is generated while at sea.
class_dredger=Dredger
# class_dredger_explanation=They find surprising items when they fish.
class_dredger_explanation_advanced=They can grab fish from an insane distance (__1__ extra tile range), and catch more (+__2__ fish). In addition, they find surprising items when fishing.
@ -293,7 +294,12 @@ class_soldier=Soldier
class_soldier_explanation_advanced=When eating fish, they have __1__% chance to summon defender to protect them.
class_veteran=Veteran
class_veteran_explanation_advanced=When eating fish, they have __1__% chance to summon destroyer to protect them.\n\nIn addition, they have __2__% chance to slow the enemy that hits them.
class_medic=Medic
class_medic_explanation_advanced=When eating fish they additionally heal themselves and nearby players (__1__ tile radius) for __2__% of max hp.
class_doctor=Doctor
class_doctor_explanation_advanced=When eating fish they additionally heal themselves and nearby players (__1__ tile radius) for __2__% of max hp.
class_shaman=Shaman
class_shaman_explanation_advanced=Shamans energize themselves when standing next to accumulators. They can use this accumulated energy to summon biters when eating fish.
class_explanation=__1__: __2__
class_explanation_upgraded_class=__1__: An upgrade of __2__. __3__

View File

@ -1042,6 +1042,11 @@ local function base_kill_rewards(event)
if not (event.force and event.force.valid) then return end
local entity_name = entity.name
-- Don't give coins for friendly biter death
if Utils.contains(CoreData.enemy_units, entity_name) and entity.force and entity.force.name == memory.force_name then
return
end
local revenge_target
if event.cause and event.cause.valid and event.cause.name == 'character' then
revenge_target = event.cause

View File

@ -75,6 +75,14 @@ Public.rock_eater_required_stone_furnace_to_heal_count = 1
Public.soldier_defender_summon_chance = 0.2
Public.veteran_destroyer_summon_chance = 0.2
Public.veteran_on_hit_slow_chance = 0.1
Public.medic_heal_radius = 15
Public.medic_heal_percentage_amount = 0.1
Public.doctor_heal_radius = 20
Public.doctor_heal_percentage_amount = 0.15
Public.shaman_energy_required_per_summon = 1000000
Public.shaman_max_charge = 30000000
Public.class_cycle_count = 5 -- How many classes should be purchased to have a chance to buy the same class again
Public.maximum_fish_allowed_to_catch_at_sea = 30
@ -107,7 +115,7 @@ function Public.cost_to_leave_multiplier()
-- extra factor now that the cost scales with time:
-- return Math.sloped(Common.difficulty_scale(), 8/10)
return Math.sloped(Common.difficulty_scale(), 0.5)
return 0.6 * Math.sloped(Common.difficulty_scale(), 0.4)
end
-- Avoid using e >= 1/4 in calculations "crew_scale()^(e)" to strictly avoid situations where people want to have less people in the crew
@ -561,7 +569,7 @@ function Public.krakens_per_free_slot(overworldx)
end
function Public.biter_boat_health()
return Math.ceil(800 * Math.max(1, 1 + 0.075 * (Common.overworldx()/40)^(13/10)) * (Public.crew_scale()^(1/5)) * Math.sloped(Common.difficulty_scale(), 3/4))
return Math.ceil(700 * Math.max(1, 1 + 0.075 * (Common.overworldx()/40)^(13/10)) * (Public.crew_scale()^(1/5)) * Math.sloped(Common.difficulty_scale(), 3/4))
end
function Public.main_shop_cost_multiplier()

View File

@ -45,8 +45,10 @@ Public.colors = {
notify_gameover = {r=249, g=84, b=84},
renderingtext_green = {r=88, g=219, b=88},
renderingtext_yellow = {r=79, g=136, b=209},
quartermaster_rendering = {r=237, g=157, b=45, a=0.15},
quartermaster_rendering = {r=237, g=157, b=45, a=0.2},
healing_radius_rendering = {r=255, g=91, b=138, a=0.2},
toughness_rendering = {r=40, g=40, b=40, a=0.5},
shaman_charge_rendering = {r=0, g=100, b=255, a=0.6},
}
Public.static_boat_floor = 'brown-refined-concrete'

View File

@ -734,7 +734,6 @@ function Public.initialise_crew(accepted_proposal)
memory.destinationsvisited_indices = {}
memory.stored_fuel = Balance.starting_fuel
memory.available_classes_pool = Classes.initial_class_pool()
memory.playtesting_stats = {
coins_gained_by_biters = 0,
coins_gained_by_nests_and_worms = 0,
@ -751,10 +750,13 @@ function Public.initialise_crew(accepted_proposal)
memory.captain_accrued_time_data = {}
memory.max_players_recorded = 0
memory.classes_table = {}
memory.officers_table = {}
memory.spare_classes = {}
memory.unlocked_classes = {}
memory.classes_table = {} -- stores all unlocked untaken classes
memory.spare_classes = {} -- stores all unlocked taken classes
memory.recently_purchased_classes = {} -- stores recently unlocked classes to add it back to available class pool list later
memory.unlocked_classes = {} -- stores all unlocked classes just for GUI (to have consistent order)
memory.available_classes_pool = Classes.initial_class_pool() -- stores classes that can be randomly picked for unlocking
memory.class_entry_count = 0 -- used to track whether new class entries should be added during "full_update"
memory.healthbars = {}

View File

@ -100,7 +100,7 @@ function Public.random_float_in_range(from, to)
end
-- Returns vector in random direction.
-- scalar: sets returned vector length. If null, 1 will be chosen
-- scalar: sets returned vector length. If nil, 1 will be chosen
function Public.random_vec(scalar)
scalar = scalar or 1
local random_angle = Public.random_float_in_range(0, 2 * Public.pi)

View File

@ -135,6 +135,7 @@ function Public.go_from_starting_dock_to_first_destination()
Hold.create_hold_surface(1)
boat.EEI_stage = 1
boat.random_class_purchase_count = 0
Cabin.create_cabin_surface()
local items = Balance.starting_items_crew_upstairs()

View File

@ -36,6 +36,9 @@ local enum = {
ROCK_EATER = 'rock_eater',
SOLDIER = 'soldier',
VETERAN = 'veteran',
MEDIC = 'medic',
DOCTOR = 'doctor',
SHAMAN = 'shaman',
}
Public.enum = enum
@ -69,6 +72,9 @@ Public.eng_form = {
[enum.ROCK_EATER] = 'Rock Eater',
[enum.SOLDIER] = 'Soldier',
[enum.VETERAN] = 'Veteran',
[enum.MEDIC] = 'Medic',
[enum.DOCTOR] = 'Doctor',
[enum.SHAMAN] = 'Shaman',
}
function Public.display_form(class)
@ -144,6 +150,14 @@ function Public.explanation(class, add_is_class_obtainable)
local chance = Balance.veteran_destroyer_summon_chance * 100
local chance2 = Balance.veteran_on_hit_slow_chance * 100
full_explanation = {'', {explanation, chance, chance2}}
elseif class == enum.MEDIC then
local radius = Balance.medic_heal_radius
local heal_percentage = Math.ceil(Balance.medic_heal_percentage_amount * 100)
full_explanation = {'', {explanation, radius, heal_percentage}}
elseif class == enum.DOCTOR then
local radius = Balance.doctor_heal_radius
local heal_percentage = Math.ceil(Balance.doctor_heal_percentage_amount * 100)
full_explanation = {'', {explanation, radius, heal_percentage}}
else
full_explanation = {'', {explanation}}
end
@ -183,6 +197,7 @@ Public.class_unlocks = {
[enum.SAMURAI] = {enum.HATAMOTO},
[enum.MASTER_ANGLER] = {enum.DREDGER},
[enum.SOLDIER] = {enum.VETERAN},
[enum.MEDIC] = {enum.DOCTOR},
}
Public.class_purchase_requirement = {
@ -192,18 +207,19 @@ Public.class_purchase_requirement = {
[enum.HATAMOTO] = enum.SAMURAI,
[enum.DREDGER] = enum.MASTER_ANGLER,
[enum.VETERAN] = enum.SOLDIER,
[enum.DOCTOR] = enum.MEDIC,
}
function Public.initial_class_pool()
return {
enum.DECKHAND, --good for afk players
-- enum.DECKHAND, --good for afk players, but it's boring and bloats class pool
enum.SHORESMAN,
enum.QUARTERMASTER,
enum.FISHERMAN,
enum.SCOUT,
enum.SAMURAI,
-- enum.MERCHANT, --not interesting, breaks coin economy
enum.BOATSWAIN,
-- enum.BOATSWAIN, -- boring and bloats class pool
-- enum.PROSPECTOR, --lumberjack is just more fun
enum.LUMBERJACK,
enum.IRON_LEG,
@ -212,6 +228,8 @@ function Public.initial_class_pool()
enum.CHEF,
enum.ROCK_EATER,
enum.SOLDIER,
enum.MEDIC,
enum.SHAMAN,
}
end
@ -295,18 +313,24 @@ end
function Public.generate_class_for_sale()
local memory = Memory.get_crew_memory()
local destination = Common.current_destination()
-- if #memory.available_classes_pool == 0 then
-- -- memory.available_classes_pool = Public.initial_class_pool() --reset to initial state
-- -- turned off as this makes too many classes
-- end
local class
if #memory.available_classes_pool > 0 then
class = memory.available_classes_pool[Math.random(#memory.available_classes_pool)]
if #memory.available_classes_pool > 1 then
-- Avoid situations where you can purchase same class twice in a row (or faster than Balance.class_cycle_count) when purchasing random class from cabin and then from quest market
while true do
local class = memory.available_classes_pool[Math.random(#memory.available_classes_pool)]
if not (destination and destination.static_params and destination.static_params.class_for_sale == class) then
return class
end
end
else
return memory.available_classes_pool[1]
end
return class
end
@ -422,6 +446,56 @@ local function class_on_player_used_capsule(event)
e.combat_robot_owner = player.character
end
end
elseif class == Public.enum.MEDIC then
for _, member in pairs(Common.crew_get_crew_members()) do
if Math.distance(player.position, member.position) <= Balance.medic_heal_radius then
if member.character then
local amount = Math.ceil(member.character.prototype.max_health * Balance.medic_heal_percentage_amount)
game.print('amount: ' .. tostring(amount))
game.print('healing')
member.character.health = member.character.health + amount
end
end
end
elseif class == Public.enum.DOCTOR then
for _, member in pairs(Common.crew_get_crew_members()) do
if Math.distance(player.position, member.position) <= Balance.doctor_heal_radius then
if member.character then
local amount = Math.ceil(member.character.prototype.max_health * Balance.doctor_heal_percentage_amount)
game.print('amount: ' .. tostring(amount))
game.print('healing')
member.character.health = member.character.health + amount
end
end
end
elseif class == Public.enum.SHAMAN then
local data = memory.class_auxiliary_data[player.index]
if data and data.shaman_charge then
for _ = 1, 3 do
if data.shaman_charge < Balance.shaman_energy_required_per_summon then break end
local pos = Math.vector_sum(player.position, Math.random_vec(2))
local name = Common.get_random_unit_type(Math.clamp(0, 1, memory.evolution_factor))
if player.surface.can_place_entity{name = name, position = pos, force = memory.force} then
local e = player.surface.create_entity{name = name, position = pos, force = memory.force}
if e and e.valid then
data.shaman_charge = data.shaman_charge - Balance.shaman_energy_required_per_summon
rendering.draw_text {
text = '~' .. player.name .. "'s minion~",
surface = player.surface,
target = e,
target_offset = {0, -2.6},
color = player.force.color,
scale = 1.05,
font = 'default-large-semibold',
alignment = 'center',
scale_with_zoom = false
}
end
end
end
end
end
end
@ -459,6 +533,24 @@ function Public.try_unlock_class(class_for_sale, player, force_unlock)
if not Public.class_is_obtainable(class_for_sale) then return false end
if player.force and player.force.valid then
local message
if required_class then
message = {'pirates.class_upgrade', player.name, Public.display_form(required_class), Public.display_form(class_for_sale), Public.explanation(class_for_sale)}
else
message = {'pirates.class_purchase', player.name, Public.display_form(class_for_sale), Public.explanation(class_for_sale)}
end
Common.notify_force_light(player.force, message)
end
memory.available_classes_pool = Utils.ordered_table_with_single_value_removed(memory.available_classes_pool, class_for_sale)
if Public.class_unlocks[class_for_sale] then
for _, upgrade in pairs(Public.class_unlocks[class_for_sale]) do
memory.available_classes_pool[#memory.available_classes_pool + 1] = upgrade
end
end
if required_class then
-- check if pre-requisite class is taken by someone
for p_index, chosen_class in pairs(memory.classes_table) do
@ -522,6 +614,14 @@ function Public.try_unlock_class(class_for_sale, player, force_unlock)
-- update GUI data
memory.unlocked_classes[#memory.unlocked_classes + 1] = {class = class_for_sale}
end
memory.recently_purchased_classes[#memory.recently_purchased_classes + 1] = class_for_sale
if #memory.recently_purchased_classes >= Balance.class_cycle_count then
local class_removed = table.remove(memory.recently_purchased_classes, 1)
memory.available_classes_pool[#memory.available_classes_pool + 1] = class_removed
end
return true
end

View File

@ -14,6 +14,31 @@ local _inspect = require 'utils.inspect'.inspect
local Public = {}
-- Copied from modules
local function discharge_accumulators(surface, position, force, power_needs)
local accumulators = surface.find_entities_filtered {name = 'accumulator', force = force, position = position, radius = 13}
local power_drained = 0
power_needs = power_needs
for _, accu in pairs(accumulators) do
if accu.valid then
if accu.energy > 3000000 and power_needs > 0 then
if power_needs >= 1000000 then
power_drained = power_drained + 1000000
accu.energy = accu.energy - 1000000
power_needs = power_needs - 1000000
else
power_drained = power_drained + power_needs
accu.energy = accu.energy - power_needs
end
elseif power_needs <= 0 then
break
end
end
end
return power_drained
end
-- NOTE: You can currently switch between classes shaman -> iron leg -> shaman, without losing your shaman charge, but I'm too lazy to fix.
function Public.class_update_auxiliary_data(tickinterval)
local memory = Memory.get_crew_memory()
if not memory.classes_table then return end
@ -27,7 +52,10 @@ function Public.class_update_auxiliary_data(tickinterval)
for _, player in pairs(crew) do
local player_index = player.index
if Classes.get_class(player_index) == Classes.enum.IRON_LEG then
if (not class_auxiliary_data[player_index]) then class_auxiliary_data[player_index] = {} end
if (not class_auxiliary_data[player_index]) then
class_auxiliary_data[player_index] = {}
end
local data = class_auxiliary_data[player_index]
processed_players[player_index] = true
local check
@ -45,6 +73,20 @@ function Public.class_update_auxiliary_data(tickinterval)
else
data.iron_leg_active = false
end
elseif Classes.get_class(player_index) == Classes.enum.SHAMAN then
if (not class_auxiliary_data[player_index]) then
class_auxiliary_data[player_index] = {}
class_auxiliary_data[player_index].shaman_charge = 0
end
local data = class_auxiliary_data[player_index]
processed_players[player_index] = true
if data.shaman_charge < Balance.shaman_max_charge then
local power_need = Balance.shaman_max_charge - data.shaman_charge
local energy = discharge_accumulators(player.surface, player.position, memory.force, power_need)
data.shaman_charge = data.shaman_charge + energy
end
end
end
@ -74,7 +116,9 @@ function Public.class_renderings(tickinterval)
local r = rendering_data.rendering
local c = rendering_data.class
processed_players[player_index] = true
if Common.validate_player_and_character(player) and (c ~= Classes.enum.IRON_LEG or (memory.class_auxiliary_data[player_index] and memory.class_auxiliary_data[player_index].iron_leg_active)) then
local data = memory.class_auxiliary_data[player_index]
-- if Common.validate_player_and_character(player) and (c ~= Classes.enum.IRON_LEG or (memory.class_auxiliary_data[player_index] and memory.class_auxiliary_data[player_index].iron_leg_active)) then
if Common.validate_player_and_character(player) then
if class == c then
if r and rendering.is_valid(r) then
rendering.set_target(r, player.character)
@ -102,7 +146,7 @@ function Public.class_renderings(tickinterval)
target = player.character,
color = CoreData.colors.toughness_rendering,
filled = false,
radius = (1 - Balance.samurai_damage_taken_multiplier)^2,
radius = 1 - Balance.samurai_damage_taken_multiplier,
only_in_alt_mode = false,
draw_on_ground = true,
}
@ -114,19 +158,57 @@ function Public.class_renderings(tickinterval)
target = player.character,
color = CoreData.colors.toughness_rendering,
filled = false,
radius = (1 - Balance.hatamoto_damage_taken_multiplier)^2,
radius = 1 - Balance.hatamoto_damage_taken_multiplier,
only_in_alt_mode = false,
draw_on_ground = true,
}
}
elseif class == Classes.enum.IRON_LEG and memory.class_auxiliary_data[player_index] and memory.class_auxiliary_data[player_index].iron_leg_active then
elseif class == Classes.enum.IRON_LEG and data and data.iron_leg_active then
class_renderings[player_index] = {
rendering = rendering.draw_circle{
surface = player.surface,
target = player.character,
color = CoreData.colors.toughness_rendering,
filled = false,
radius = (1 - Balance.iron_leg_damage_taken_multiplier)^2,
radius = 1 - Balance.iron_leg_damage_taken_multiplier,
only_in_alt_mode = false,
draw_on_ground = true,
}
}
elseif class == Classes.enum.MEDIC then
class_renderings[player_index] = {
rendering = rendering.draw_circle{
surface = player.surface,
target = player.character,
color = CoreData.colors.healing_radius_rendering,
filled = false,
radius = Balance.medic_heal_radius,
only_in_alt_mode = true,
draw_on_ground = true,
}
}
elseif class == Classes.enum.DOCTOR then
class_renderings[player_index] = {
rendering = rendering.draw_circle{
surface = player.surface,
target = player.character,
color = CoreData.colors.healing_radius_rendering,
filled = false,
radius = Balance.doctor_heal_radius,
only_in_alt_mode = true,
draw_on_ground = true,
}
}
elseif class == Classes.enum.SHAMAN and data and data.shaman_charge and data.shaman_charge > 0 then
local max_render_radius = 3
local radius = max_render_radius * (data.shaman_charge / Balance.shaman_max_charge)
class_renderings[player_index] = {
rendering = rendering.draw_circle{
surface = player.surface,
target = player.character,
color = CoreData.colors.shaman_charge_rendering,
filled = true,
radius = radius,
only_in_alt_mode = false,
draw_on_ground = true,
}

View File

@ -225,24 +225,7 @@ function Public.event_on_market_item_purchased(event)
local ok = Classes.try_unlock_class(class_for_sale, player, false)
if ok then
if force and force.valid then
local message = {'pirates.class_purchase', player.name, Classes.display_form(class_for_sale), Classes.explanation(class_for_sale)}
Common.notify_force_light(force, message)
end
memory.available_classes_pool = Utils.ordered_table_with_single_value_removed(memory.available_classes_pool, class_for_sale)
-- if destination.dynamic_data and destination.dynamic_data.market_class_offer_rendering then
-- rendering.destroy(destination.dynamic_data.market_class_offer_rendering)
-- end
market.remove_market_item(offer_index)
if Classes.class_unlocks[class_for_sale] then
for _, upgrade in pairs(Classes.class_unlocks[class_for_sale]) do
memory.available_classes_pool[#memory.available_classes_pool + 1] = upgrade
end
end
else -- if this happens, I believe there is something wrong with code
if force and force.valid then
Common.notify_force_error(force, {'pirates.class_purchase_error_prerequisite_class', Classes.display_form(required_class)})
@ -296,6 +279,16 @@ function Public.event_on_market_item_purchased(event)
Upgrades.execute_upgade(Upgrades.enum.EXTRA_HOLD, player)
elseif offer_index == Cabin.enum.SLOT_MORE_POWER then
Upgrades.execute_upgade(Upgrades.enum.MORE_POWER, player)
elseif offer_index == Cabin.enum.SLOT_RANDOM_CLASS then
local class = Classes.generate_class_for_sale()
local ok = Classes.try_unlock_class(class, player, false)
if ok then
memory.boat.random_class_purchase_count = memory.boat.random_class_purchase_count + 1
else
if player.force and player.force.valid then
Common.notify_force_error(player.force, 'Oops, something went wrong trying to purchase a class!')
end
end
end
Cabin.handle_purchase(market, offer_index)

View File

@ -16,7 +16,8 @@ local enum = {
DEFAULT = 'Default',
SLOT_EXTRA_HOLD = 5,
SLOT_MORE_POWER = 6,
SLOT_REROLL_PRICES = 7,
SLOT_RANDOM_CLASS = 7,
SLOT_REROLL_PRICES = 8,
}
Public.enum = enum
@ -126,6 +127,10 @@ Public.cabin_shop_data = {
price = {}, -- price set later
offer = {type='nothing', effect_description={'pirates.market_description_upgrade_power'}}
},
{
price = {}, -- price set later
offer = {type='nothing', effect_description={'pirates.market_description_random_class'}}
},
{
price = {{'coin', 100}, {'raw-fish', 1}},
offer = {type='nothing', effect_description={'pirates.market_description_reroll_prices'}}
@ -370,6 +375,9 @@ function Public.get_market_random_price(slot)
elseif slot == enum.SLOT_MORE_POWER then
local tier = memory.boat.EEI_stage
return Common.pick_random_price(tier, 0.5*Public.market_price_scale, math.min(1, 0.05 + tier * 0.15))
elseif slot == enum.SLOT_RANDOM_CLASS then
local tier = memory.boat.random_class_purchase_count + 1
return Common.pick_random_price(tier, Public.market_price_scale, math.min(1, 0.05 + tier * 0.15))
end
return nil

View File

@ -35,7 +35,7 @@ local function spawn_market(args, is_main)
-- This doesn't really prevent markets spawning near each other, since markets aren't spawned immediately for a given chunk, but it helps a bit
local cave_miner = destination_data.dynamic_data.cave_miner
if not cave_miner then
-- this can be null if cave island is first and run is launched using proposal
-- this can be nil if cave island is first and run is launched using proposal
-- should probably investigate this some time
local message = 'ERROR: cave_miner is nil'
if _DEBUG then