Files
bbassieandClaude Opus 5.5 a6c453a88e Add a community_details feature for the community name and links
The community name, Discord, website, status, GitHub, and Patreon links
lived in exp_legacy/config/server_detail.lua, so changing them meant
editing the scenario. They are now the community_details feature and can
be edited from the Scenario Features page.

Chat auto reply, help bubbles, and the readme read the live config
instead of copying the values when the file loads, so an edit shows up
without a restart. Help bubbles store the location name in their task
and build the messages each cycle; the readme tab tooltip can be a
function which is called when the readme opens.

Every feature gets an enabled switch from the framework. There is no
sensible meaning for a disabled community name, so the values are
always used and the feature description says the switch does nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:10:22 +00:00

137 lines
4.4 KiB
Lua

--[[-- Control - Help Bubbles
Adds friendly biters that walk around and give helpful messages
]]
local Async = require("modules/exp_util/async")
local Storage = require("modules/exp_util/storage")
local Feature = require("modules/exp_scenario/features")
local community_details = require("modules/exp_scenario/config/community_details").config
local feature = Feature.register("help_bubbles")
--- Ticks between message changes
local message_cycle = 60 * 15
local locations = {
["Spawn"] = {
spawn_position = { x = 0, y = 0 },
spawn_surface = "nauvis",
entity_name = "small-biter",
--- Built each cycle so changes to community_details show without a restart
messages = function()
return {
{ "info.website", community_details.website },
{ "info.read-readme" },
{ "info.discord", community_details.discord },
{ "info.softmod" },
{ "info.redmew" },
{ "info.custom-commands" },
{ "info.status", community_details.status },
{ "info.lhd" },
{ "info.github", community_details.github },
{ "info.patreon", community_details.patreon },
}
end,
}
}
--- @type table<string, Async.AsyncReturn<any>>
local persistent_locations = {}
Storage.register(persistent_locations, function(tbl)
persistent_locations = tbl
end)
--- @class speech_bubble_task_param
--- @field entity LuaEntity
--- @field messages LocalisedString[]?
--- @field location string? Used when there are no messages, a key of locations
--- @field previous_message LuaEntity?
--- @field current_message_index number
--- Used by create_entity within speech_bubble_async
local speech_bubble_param = {
name = "compi-speech-bubble",
position = { 0, 0 },
}
--- Cycle between a set category of messages above an entity
local speech_bubble_task =
Async.register(function(task)
--- @cast task speech_bubble_task_param
local entity = task.entity
if not entity.valid then
return Async.status.complete()
end
local messages = task.messages or locations[task.location].messages()
local index = task.current_message_index
if index > #messages then
index = 1
end
local previous_message = task.previous_message
if previous_message and previous_message.valid then
previous_message.destroy()
end
speech_bubble_param.source = entity
speech_bubble_param.text = messages[index]
task.previous_message = entity.surface.create_entity(speech_bubble_param)
task.current_message_index = index + 1
return Async.status.delay(message_cycle, task)
end)
--- Register an entity to start spawning speech bubbles
--- @param entity LuaEntity the entity which will have messages spawn from it
--- @param messages LocalisedString[] the messages which should be shown
--- @param starting_index number? the message index to start at, default 1
--- @return Async.AsyncReturn<any>
local function register_entity(entity, messages, starting_index)
return speech_bubble_task{
entity = entity,
messages = messages,
current_message_index = starting_index or 1,
}
end
--- Check all persistent locations from the config are active
local function check_persistent_locations()
for name, location in pairs(locations) do
local task = persistent_locations[name]
if task and not task.completed then
goto continue
end
local surface = game.get_surface(location.spawn_surface)
if not surface then
goto continue
end
local position = surface.find_non_colliding_position(location.entity_name, location.spawn_position, 1.5, 0.5)
if not position then
goto continue
end
local entity = surface.create_entity{ name = location.entity_name, position = position, force = game.forces.neutral }
if not entity then
goto continue
end
persistent_locations[name] = speech_bubble_task{
entity = entity,
location = name,
current_message_index = 1,
}
::continue::
end
end
return feature:guard{
on_nth_tick = {
[message_cycle] = check_persistent_locations,
},
register_entity = register_entity,
}