diff --git a/features/gui/rpg_military.lua b/features/gui/rpg_military.lua new file mode 100644 index 000000000..aa0e0417f --- /dev/null +++ b/features/gui/rpg_military.lua @@ -0,0 +1,591 @@ +local Command = require 'utils.command' +local Event = require 'utils.event' +local Global = require 'utils.global' +local Gui = require 'utils.gui' +local Table = require 'utils.table' +local Task = require 'utils.task' +local Token = require 'utils.token' +local Toast = require 'features.gui.toast' + +local Manager = {} +local Interface = {} + +-- LOCAL VARIABLES ============================================================ + +local auras = {} +local records = {} +local regens = {} +local regens_map = {} +local update_map = {} + +Global.register({ + auras = auras, + records = records, + regens = regens, + regens_map = regens_map, + update_map = update_map, +}, function(tbl) + auras = tbl.auras + records = tbl.records + regens = tbl.regens + regens_map = tbl.regens_map + update_map = tbl.update_map +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 +} + +-- 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 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) + player.character_crafting_speed_modifier = player.character_crafting_speed_modifier + value + end, + ['inventory'] = function(player, value) + player.character_inventory_slots_bonus = player.character_inventory_slots_bonus + value + end, + ['max_hp'] = function(player, value) + player.character_health_bonus = player.character_health_bonus + value + end, + ['mining'] = function(player, value) + player.character_mining_speed_modifier = player.character_mining_speed_modifier + value + end, + ['reach'] = function(player, value) + player.character_build_distance_bonus = player.character_build_distance_bonus + value + player.character_reach_distance_bonus = player.character_reach_distance_bonus + value + player.character_resource_reach_distance_bonus = player.character_resource_reach_distance_bonus + value + end, + ['regen'] = function(player, value) + regens[player.index] = (regens[player.index] or 0) + value + end, + ['speed'] = function(player, value) + player.character_running_speed_modifier = player.character_running_speed_modifier + value + end, +} + +-- 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 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 = {}, + } + records[player_index] = record + end + return record +end + +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 + 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 + 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 + 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 + + FX.xp(player, xp) +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 + + Interface.update(player) +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 + +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 * (1.0 - 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) + +-- == 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() + if game.tick % 60 == 0 then + for player_index in pairs(regens_map) do + Manager.restore_health(player_index, regens[player_index]) + end + end + + 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 + if not Manager.check_player_level(player) then + Interface.update(player) + end + end + update_map[player_index] = nil + end + end +end) + +Gui.on_click(main_button_name, Interface.toggle_main_button) + +-- ============================================================================ + +if _DEBUG then + Command.add( + 'level-up', + { + description = { '+1 RPG Level' }, + allowed_by_server = false, + log_command = false + }, + function() + Manager.on_player_level_up(game.player) + end + ) + Command.add( + 'level-up-all', + { + description = { 'Sets RPG level to Max' }, + allowed_by_server = false, + log_command = false + }, + function() + for _ = 1, MAX_LEVEL do + Manager.on_player_level_up(game.player) + end + end + ) +end + +return { + manager = Manager, + interface = Interface +} diff --git a/map_gen/maps/crash_site/outpost_builder.lua b/map_gen/maps/crash_site/outpost_builder.lua index cb0a16ef5..f27c23cfc 100644 --- a/map_gen/maps/crash_site/outpost_builder.lua +++ b/map_gen/maps/crash_site/outpost_builder.lua @@ -10,6 +10,7 @@ local Donator = require 'features.donator' local RS = require 'map_gen.shared.redmew_surface' local Server = require 'features.server' local CrashSiteToast = require 'map_gen.maps.crash_site.crash_site_toast' +local RPG = require 'features.gui.rpg_military' local table = require 'utils.table' --local next = next @@ -1014,6 +1015,7 @@ local function do_outpost_upgrade(event) CrashSiteToast.do_outpost_toast(outpost_data.market, message) Server.to_discord_bold(concat {'*** ', message, ' ***'}) + RPG.manager.award_xp(event.player, 5 * level) for i = 1, #outpost_magic_crafters do local crafter = outpost_magic_crafters[i] @@ -1063,6 +1065,20 @@ function Public.activate_market_upgrade(outpost_id) activate_market_upgrade(outpost_data) end +local function find_rearest_player(position) + if #game.connected_players == 0 then + return + end + local online = {} + for _, player in pairs(game.connected_players) do + local pos = player.physical_position + local distance = (position.x - pos.x) ^ 2 + (position.y - pos.y) ^ 2 + table.insert(online, { player = player, distance = distance }) + end + table.sort(online, function(first, second) return first.distance < second.distance end) + return online[1].player +end + local function do_capture_outpost(outpost_data) local area = {top_left = outpost_data.top_left, bottom_right = outpost_data.bottom_right} local walls = RS.get_surface().find_entities_filtered {area = area, force = 'enemy', name = 'stone-wall'} @@ -1087,7 +1103,13 @@ local function do_capture_outpost(outpost_data) end end - local message = 'Outpost captured: ' .. name + local message = ('Outpost "%s" captured'):format(name) + local nearest = find_rearest_player({ x = (area.bottom_right.x - area.top_left.x) / 2, y = (area.bottom_right.y - area.top_left.y) / 2 }) + if nearest and nearest.valid then + message = message .. ' by ' .. nearest.name + RPG.manager.award_xp(nearest, 'outpost-capture') + end + CrashSiteToast.do_outpost_toast(outpost_data.market, message) Server.to_discord_bold(concat {'*** ', message, ' ***'})