1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2025-12-26 00:42:20 +02:00
Files
RedMew/features/gui/rpg_military.lua

771 lines
25 KiB
Lua

local Command = require 'utils.command'
local Color = require 'resources.color_presets'
local Event = require 'utils.event'
local Game = require 'utils.game'
local Global = require 'utils.global'
local Gui = require 'utils.gui'
local Ranks = require 'resources.ranks'
local Table = require 'utils.table'
local Task = require 'utils.task'
local Token = require 'utils.token'
local Toast = require 'features.gui.toast'
local math_min = math.min
local math_max = math.max
local math_ceil = math.ceil
local math_floor = math.floor
local Manager = {}
local Interface = {}
-- LOCAL VARIABLES ============================================================
local auras = {}
local records = {}
local regens = {}
local regens_map = {}
local update_map = {}
local xp_data = {
pool_count = 0,
}
Global.register({
auras = auras,
records = records,
regens = regens,
regens_map = regens_map,
update_map = update_map,
xp_data = xp_data,
}, function(tbl)
auras = tbl.auras
records = tbl.records
regens = tbl.regens
regens_map = tbl.regens_map
update_map = tbl.update_map
xp_data = tbl.xp_data
end)
local XP_BY_ACTION = {
-- entity name -> xp
['small-biter'] = { value = 1, count = 15000 },
['small-spitter'] = { value = 1, count = 3000 },
['medium-biter'] = { value = 3, count = 5000 },
['medium-spitter'] = { value = 3, count = 3500 },
['big-biter'] = { value = 6, count = 8500 },
['big-spitter'] = { value = 6, count = 8500 },
['behemoth-biter'] = { value = 12, count = 22000 },
['behemoth-spitter'] = { value = 12, count = 22000 },
['small-worm-turret'] = { value = 5, count = 3500 },
['big-worm-turret'] = { value = 8, count = 7000 },
['medium-worm-turret'] = { value = 12, count = 12000 },
['behemoth-worm-turret'] = { value = 15, count = 7000 },
['biter-spawner'] = { value = 25, count = 1200 },
['spitter-spawner'] = { value = 25, count = 1200 },
['destroyer'] = { value = 1, count = 100000 },
['defender'] = { value = 1, count = 100000 },
['distractor'] = { value = 1, count = 0 },
['gun-turret'] = { value = 15, count = 7000 },
['flamethrower-turret'] = { value = 18, count = 2000 },
['laser-turret'] = { value = 12, count = 10000 },
['artillery-turret'] = { value = 25, count = 100 },
['outpost-upgrade'] = { value = 100, count = 25 }, -- custom
['outpost-capture'] = { value = 100, count = 100 }, -- custom
}
-- Share 20% of XP gained with other players
local XP_PUBLIC_SHARE = 0.2
local XP_PRIVATE_SHARE = 1 - XP_PUBLIC_SHARE
-- Level XP formula: fast early, slower later.
local MAX_LEVEL = 50
-- XP required to go from level L to L+1
local LEVEL_XP = {}
for lvl = 1, MAX_LEVEL - 1 do
if lvl < 5 then
LEVEL_XP[lvl] = 25 * lvl
elseif lvl < 15 then
LEVEL_XP[lvl] = 50 * lvl
elseif lvl < 30 then
LEVEL_XP[lvl] = 100 * (lvl - 10)
elseif lvl < 40 then
LEVEL_XP[lvl] = 200 * (lvl - 20)
else
LEVEL_XP[lvl] = 400 * (lvl - 30)
end
end
-- Tier thresholds and names
local TIERS = {
[1] = 'Scavenger',
[3] = 'Survivor',
[5] = 'Militiaman',
[7] = 'Corporal',
[9] = 'Sergeant',
[11] = 'Veteran',
[13] = 'Lieutenant',
[15] = 'Captain',
[18] = 'Major',
[20] = 'Commander',
[23] = 'Colonel',
[25] = 'Warlord',
[28] = 'Liberator',
[30] = 'General',
[33] = 'Field Marshal',
[35] = 'Champion',
[38] = 'High Marshal',
[40] = 'Planetary Commander',
[45] = 'Paragon',
[50] = 'Eternal Marshal',
}
local get_or_create_record = function(player_index)
local record = records[player_index]
if not record then
record = {
xp = 0,
level = 1,
rank = TIERS[1],
buffs = {},
bonuses = {
character_crafting_speed_modifier = 0,
character_inventory_slots_bonus = 0,
character_health_bonus = 0,
character_mining_speed_modifier = 0,
character_build_distance_bonus = 0,
character_reach_distance_bonus = 0,
character_resource_reach_distance_bonus = 0,
character_running_speed_modifier = 0,
},
}
records[player_index] = record
end
return record
end
local SMALL_BUFFS = {
aura = { desc = 'Toughness [color=173,255,47]+%.1f%%[/color]', value = 0.01, multiplier = 100 },
crafting = { desc = 'Crafting speed [color=173,255,47]+%.0f%%[/color]', value = 0.20, multiplier = 100 },
inventory = { desc = 'Inventory [color=173,255,47]+%d[/color] slot', value = 5 , multiplier = 1 },
max_hp = { desc = 'Max HP [color=173,255,47]+%d[/color]', value = 50 , multiplier = 1 },
mining = { desc = 'Mining speed [color=173,255,47]+%.0f%%[/color]', value = 0.50, multiplier = 100 },
reach = { desc = 'Reach [color=173,255,47]+%d[/color] tile', value = 1 , multiplier = 1 },
regen = { desc = 'HP regen [color=173,255,47]+%.2f/s[/color]', value = 0.20, multiplier = 1 },
speed = { desc = 'Running speed [color=173,255,47]+%.0f%%[/color]', value = 0.10, multiplier = 100 },
}
local SMALL_BUFFS_LIST = Table.keys(SMALL_BUFFS)
local SMALL_BUFFS_ACTION = {
['aura'] = function(player, value)
auras[player.index] = (auras[player.index] or 0) + value
end,
['crafting'] = function(player, value)
local bonuses = get_or_create_record(player.index).bonuses
bonuses.character_crafting_speed_modifier = bonuses.character_crafting_speed_modifier + value
player.character_crafting_speed_modifier = bonuses.character_crafting_speed_modifier
end,
['inventory'] = function(player, value)
local bonuses = get_or_create_record(player.index).bonuses
bonuses.character_inventory_slots_bonus = bonuses.character_inventory_slots_bonus + value
player.character_inventory_slots_bonus = bonuses.character_inventory_slots_bonus
end,
['max_hp'] = function(player, value)
local bonuses = get_or_create_record(player.index).bonuses
bonuses.character_health_bonus = bonuses.character_health_bonus + value
player.character_health_bonus = bonuses.character_health_bonus
end,
['mining'] = function(player, value)
local bonuses = get_or_create_record(player.index).bonuses
bonuses.character_mining_speed_modifier = bonuses.character_mining_speed_modifier + value
player.character_mining_speed_modifier = bonuses.character_mining_speed_modifier
end,
['reach'] = function(player, value)
local bonuses = get_or_create_record(player.index).bonuses
bonuses.character_build_distance_bonus = bonuses.character_build_distance_bonus + value
bonuses.character_reach_distance_bonus = bonuses.character_reach_distance_bonus + value
bonuses.character_resource_reach_distance_bonus = bonuses.character_resource_reach_distance_bonus + value
player.character_build_distance_bonus = bonuses.character_build_distance_bonus
player.character_reach_distance_bonus = bonuses.character_reach_distance_bonus
player.character_resource_reach_distance_bonus = bonuses.character_resource_reach_distance_bonus
end,
['regen'] = function(player, value)
regens[player.index] = (regens[player.index] or 0) + value
end,
['speed'] = function(player, value)
local bonuses = get_or_create_record(player.index).bonuses
bonuses.character_running_speed_modifier = bonuses.character_running_speed_modifier + value
player.character_running_speed_modifier = bonuses.character_running_speed_modifier
end,
}
local STATS_MAP = {
character_crafting_speed_modifier = 'crafting',
character_inventory_slots_bonus = 'inventory',
character_health_bonus = 'max_hp',
character_mining_speed_modifier = 'mining',
character_reach_distance_bonus = 'reach',
character_running_speed_modifier = 'speed',
}
-- Big buffs awarded at tier unlocks
local BIG_BUFFS = {
[5] = { type = 'speed', value = 0.15 },
[10] = { type = 'inventory', value = 20 },
[15] = { type = 'max_hp', value = 50 },
[20] = { type = 'reach', value = 1 },
[25] = { type = 'regen', value = 0.5 },
[30] = { type = 'aura', value = 0.05 },
[35] = { type = 'speed', value = 0.25 },
[40] = { type = 'max_hp', value = 100 },
[45] = { type = 'reach', value = 1 },
[50] = { type = 'aura', value = 0.25 },
}
local FX = {
aura = function(player, amount)
local character = player.character
if not (character and character.valid) then
return
end
if character.get_health_ratio() == 1 then
return
end
character.health = character.health + amount
if amount < 0.5 then
return
end
player.create_local_flying_text{
text = ('+[color=pink]%d[/color] [img=virtual-signal.signal-sun]'):format(math_ceil(amount)),
position = { x = character.position.x, y = character.position.y - 1.4 },
surface = character.surface,
time_to_live = 60,
speed = 1,
}
end,
regen = function(player, amount)
local character = player.character
if not (character and character.valid) then
return
end
if character.get_health_ratio() == 1 then
regens_map[player.index] = nil
return
end
character.health = character.health + amount
if amount < 0.5 then
return
end
player.create_local_flying_text{
text = ('+[color=blue]%d[/color] [img=virtual-signal.signal-heart]'):format(math_ceil(amount)),
position = { x = character.position.x, y = character.position.y - 1.8 },
surface = character.surface,
time_to_live = 120,
speed = 2,
}
end,
xp = function(player, amount)
local record = get_or_create_record(player.index)
record.xp = record.xp + amount
if amount < 1 then
return
end
update_map[player.index] = true
local character = player.character
if not (character and character.valid) then
return
end
player.create_local_flying_text{
text = ('+[color=green]%d[/color] [img=virtual-signal.signal-star]'):format(amount),
position = { x = character.position.x, y = character.position.y - 1.0 },
surface = character.surface,
time_to_live = 80,
speed = 1.5,
}
end,
}
local cause_by_type = {
['character'] = function(cause)
return cause.player
end,
['car'] = function(cause)
local d = cause.get_driver()
if d then
return (d.object_name == 'LuaEntity') and d.player or d
else
return cause.last_user
end
end,
['spider-vehicle'] = function(cause)
local d = cause.get_driver()
if d then
return (d.object_name == 'LuaEntity') and d.player or d
else
return cause.last_user
end
end,
['combat-robot'] = function(cause)
return cause.last_user
end,
['land-mine'] = function(cause)
return cause.last_user
end,
}
-- MANAGER ====================================================================
Manager.compute_stats = function()
local enemy_xp = 0
for _, v in pairs(XP_BY_ACTION) do
enemy_xp = enemy_xp + (v.count * v.value)
end
local player_xp = 0
for _, v in pairs(LEVEL_XP) do
player_xp = player_xp + v
end
return ('Available XP: %d | Player XP: %d | Players feeded: %.2f'):format(enemy_xp, player_xp, (enemy_xp / player_xp))
end
Manager.award_xp = function(player, award)
local xp = 0
if type(award) == 'string' then
xp = SMALL_BUFFS[award] and SMALL_BUFFS[award].value or nil
elseif type(award) == 'number' then
xp = math_floor(award)
end
if not (xp and type(xp) == 'number' and xp > 0) then
return
end
xp_data.pool_count = xp_data.pool_count + XP_PUBLIC_SHARE * xp
FX.xp(player, xp * XP_PRIVATE_SHARE)
end
Manager.pick_random_buff = function()
return SMALL_BUFFS_LIST[math.random(#SMALL_BUFFS_LIST)]
end
Manager.apply_buff = function(player, buff_id, value)
local action = SMALL_BUFFS_ACTION[buff_id]
if not action then
return
end
action(player, value)
local msg = string.format(SMALL_BUFFS[buff_id].desc, value * SMALL_BUFFS[buff_id].multiplier)
Toast.toast_player(player, 8, msg)
local buffs = get_or_create_record(player.index).buffs
buffs[#buffs + 1] = ('[font=default-small-semibold]L[color=128,204,240]%d[/color] -[/font] %s'):format(get_or_create_record(player.index).level, msg)
end
Manager.check_player_level = function(player)
local record = get_or_create_record(player.index)
if record.level < MAX_LEVEL and record.xp >= LEVEL_XP[record.level] then
Manager.on_player_level_up(player)
return true
end
return false
end
Manager.on_player_level_up = function(player)
local record = get_or_create_record(player.index)
if record.level == MAX_LEVEL then
return
end
record.xp = math_max(0, record.xp - LEVEL_XP[record.level])
record.level = record.level + 1
local rank = TIERS[record.level]
if rank then
record.rank = rank
Toast.toast_player(player, 12, 'Your new rank is: '..rank)
else
Toast.toast_player(player, 12, 'Level up!')
end
local id = Manager.pick_random_buff()
Manager.apply_buff(player, id, SMALL_BUFFS[id].value)
local perk = BIG_BUFFS[record.level]
if perk then
Manager.apply_buff(player, perk.type, perk.value)
end
end
Manager.shield_with_aura = function(player_index, amount)
local player = game.get_player(player_index)
if not (player and player.valid) then
return
end
if not amount or amount <= 0 then
return
end
FX.aura(player, amount)
end
Manager.shield_with_aura_token = Token.register(function(params)
Manager.shield_with_aura(params.player_index, params.amount)
end)
Manager.restore_health = function(player_index, amount)
local player = game.get_player(player_index)
if not (player and player.valid) then
return
end
if not amount or amount <= 0 then
return
end
FX.regen(player, amount)
end
Manager.pretty_player_stats = function(player)
local player_index = player.index
local record = get_or_create_record(player_index)
local bonuses = record.bonuses
local stats = {
'[font=default-bold]' .. player.name .. '\' stats:[/font]',
string.format('Rank: [color=orange]%s[/color]', record.rank),
string.format('Level: [color=purple]%d / %d[/color]', record.level, MAX_LEVEL),
string.format('XPs: [color=purple]%d / %s[/color]', record.xp, tostring(LEVEL_XP[record.level] or 'inf')),
string.format(SMALL_BUFFS.aura.desc, (auras[player_index] or 0) * SMALL_BUFFS.aura.multiplier),
string.format(SMALL_BUFFS.regen.desc, (regens[player_index] or 0) * SMALL_BUFFS.regen.multiplier),
}
for k, v in pairs(bonuses) do
local id = STATS_MAP[k]
if id then
stats[#stats + 1] = string.format(SMALL_BUFFS[id].desc, v * SMALL_BUFFS[id].multiplier)
end
end
return table.concat(stats, '\n')
end
Manager.reset_character_bonuses = function(player)
if not player and player.valid then
return
end
for k, v in pairs(get_or_create_record(player.index).bonuses) do
player[k] = v
end
end
Event.add(defines.events.on_entity_damaged, function(event)
local entity = event.entity
if not (entity.valid and entity.type == 'character') then
return
end
local player_index = entity.player and entity.player.index
if not player_index or event.final_damage_amount == 0 then
return
end
if auras[player_index] then
Task.set_timeout_in_ticks(1, Manager.shield_with_aura_token, {
player_index = player_index,
amount = event.final_damage_amount * math_min(1.0, auras[player_index])
})
end
if regens[player_index] then
regens_map[player_index] = true
end
end)
Event.add(defines.events.on_entity_died, function(event)
local entity = event.entity
if not (entity.valid and entity.force and entity.force.name == 'enemy') then
return
end
local xp = XP_BY_ACTION[entity.name]
if not xp then
return
end
local cause = event.cause
if not (cause and cause.valid and cause.force and cause.force.name == 'player') then
return
end
local handler = cause_by_type[cause.type]
local actor = handler and handler(cause)
if not (actor and actor.valid) then
return
end
Manager.award_xp(actor, xp.value)
end)
Event.add(defines.events.on_player_respawned, function(event)
Manager.reset_character_bonuses(game.get_player(event.player_index))
end)
-- == USER INTERFACE ==========================================================
local main_frame_name = Gui.uid_name()
local main_button_name = Gui.uid_name()
Event.add(defines.events.on_player_created, function(event)
local player = game.get_player(event.player_index)
if not (player and player.valid) then
return
end
local data = {}
local frame = Gui.add_left_element(player, {
type = 'frame',
name = main_frame_name,
direction = 'horizontal'
})
data.level = frame.add {
type = 'sprite-button',
name = main_button_name,
sprite = 'utility/empty_armor_slot',
number = 1,
style = 'frame_button'
}
Gui.set_style(data.level, { size = 48 })
Gui.set_data(data.level, data)
data.content = frame.add { type = 'flow', direction = 'vertical' }
local content = data.content
Gui.set_style(content, { natural_width = 180, maximal_width = 200 })
data.rank = content.add { type = 'label', caption = 'Rank: ', style = 'tooltip_heading_label_category' }
data.progress = content.add { type = 'progressbar', style = 'production_progressbar', value = 0 }
Gui.set_style(data.progress, { natural_width = 198 })
local regen = content.add { type = 'flow', direction = 'horizontal' }
regen.add { type = 'label', style = 'semibold_caption_label', caption = 'Regen: ', tooltip = 'Health regeneration /s'}
regen.add { type = 'label', caption = '---'}
data.regen = regen
local aura = content.add { type = 'flow', direction = 'horizontal' }
aura.add { type = 'label', style = 'semibold_caption_label', caption = 'Aura: ', tooltip = 'Increased armor toughness %' }
aura.add { type = 'label', caption = '---'}
data.aura = aura
local buffs = content.add { type = 'flow', direction = 'vertical' }
buffs.add { type = 'label', style = 'semibold_caption_label', caption = 'Perks:' }
local listbox = buffs.add { type = 'list-box', items = {} }
Gui.set_style(listbox, { maximal_height = 90, maximal_width = 200 })
data.buffs = buffs
Gui.set_data(frame, data)
Interface.update(player)
end)
Interface.toggle_main_button = function(event)
local content = Gui.get_data(event.element).content
content.visible = not content.visible
Interface.update(event.player)
end
Interface.update = function(player)
local record = get_or_create_record(player.index)
local data = Gui.get_data(Gui.get_left_element(player, main_frame_name))
data.level.number = record.level
if not data.content.visible then
if record.level < MAX_LEVEL then
data.level.tooltip = ('[color=yellow]Rank[/color]: %s\n[color=green]XP[/color]: %d / %d for next level'):format(record.rank, record.xp, LEVEL_XP[record.level])
else
data.level.tooltip = ('[color=yellow]Rank[/color]: %s'):format(record.rank)
end
return
else
data.level.tooltip = nil
end
data.rank.caption = '[color=255,230,191]Rank:[/color] '..record.rank
if record.level < MAX_LEVEL then
data.progress.value = math_min(1, record.xp / LEVEL_XP[record.level])
data.progress.tooltip = ('[color=green]XP[/color]: %d / %d for next level'):format(record.xp, LEVEL_XP[record.level])
else
data.progress.visible = false
end
local regen = regens[player.index]
if not regen then
data.regen.visible = false
else
data.regen.visible = true
data.regen.children[2].caption = ('%.2f /s [img=virtual-signal.signal-heart]'):format(regen)
end
local aura = auras[player.index]
if not aura then
data.aura.visible = false
else
data.aura.visible = true
data.aura.children[2].caption = ('+%.1f %% [img=virtual-signal.signal-sun]'):format(aura * 100)
end
local buffs = record.buffs
if #buffs > 0 then
data.buffs.visible = true
local listbox = data.buffs.children[2]
if #listbox.items ~= #buffs then
listbox.items = buffs
listbox.scroll_to_item(#buffs)
end
else
data.buffs.visible = false
end
end
Event.add(defines.events.on_tick, function()
-- Handle Health restoration every 1sec
if game.tick % 60 == 0 then
for player_index in pairs(regens_map) do
Manager.restore_health(player_index, regens[player_index])
end
end
-- Distribute XP pool every 3min
if game.tick % 10800 == 0 then
local online_players = {}
for _, player in pairs(game.connected_players) do
-- Only consider players active at least 60% of the time
if player.afk_time < 4320 then
online_players[#online_players + 1] = player
end
end
if #online_players > 0 then
local player_share = xp_data.pool_count / #online_players
xp_data.pool_count = 0
for _, player in pairs(online_players) do
FX.xp(player, player_share)
end
end
end
-- Check for levelups every 5sec
if game.tick % 300 == 0 then
for player_index in pairs(update_map) do
local player = game.get_player(player_index)
if player and player.valid then
Manager.check_player_level(player)
Interface.update(player)
end
update_map[player_index] = nil
end
end
end)
Gui.on_click(main_button_name, Interface.toggle_main_button)
-- ============================================================================
Command.add(
'rpg-level-up',
{
description = '+1 RPG Level',
allowed_by_server = false,
log_command = false,
debug_only = true,
cheat_only = true,
},
function()
Manager.on_player_level_up(game.player)
Interface.update(game.player)
end
)
Command.add(
'rpg-level-up-all',
{
description = 'Sets RPG level to Max',
allowed_by_server = false,
log_command = false,
debug_only = true,
cheat_only = true,
},
function()
for _ = 1, MAX_LEVEL do
Manager.on_player_level_up(game.player)
end
Interface.update(game.player)
end
)
Command.add(
'rpg-stats',
{
description = 'Print player stats',
allowed_by_server = false,
log_command = false,
arguments = { 'player' },
default_values = { player = '' },
},
function(args, player)
local target = (args.player and game.get_player(args.player)) or player
player.print(Manager.pretty_player_stats(target))
end
)
Command.add(
'rpg-update',
{
description = 'Updates the RPG stats and GUI for target player',
allowed_by_server = true,
log_command = true,
required_rank = Ranks.admin,
arguments = { 'player' },
default_values = { player = '' },
},
function(args, player)
local target = (args.player and game.get_player(args.player)) or player
if target and target.valid then
Game.player_print('Updating RPG module for ' .. target.name, Color.success, player)
Manager.check_player_level(player)
Manager.reset_character_bonuses(player)
Interface.update(player)
else
Game.player_print('Invalid player name ' .. target.name, Color.fail, player)
end
end
)
Command.add(
'rpg-leaderboard',
{
description = 'Print RPG leaderboard',
allowed_by_server = true,
log_command = false,
},
function(_, player)
local scores = {}
for player_index, record in pairs(records) do
local p = game.get_player(player_index)
table.insert(scores, { level = record.level, name = p.name })
end
table.sort(scores, function(a, b) return a.level > b.level end)
local lines = { '[font=default-bold]RPG Leaderboard:[/font]' }
for i, entry in ipairs(scores) do
table.insert(lines, string.format('%d. (L%d) [color=orange]%s[/color]', i, entry.level, entry.name))
end
Game.player_print(table.concat(lines, '\n'), nil, player)
end
)
return {
manager = Manager,
interface = Interface
}