366 lines
14 KiB
Lua
366 lines
14 KiB
Lua
local mod_gui = require("__core__.lualib.mod-gui")
|
|
local radar = require("radar")
|
|
|
|
local radar_ui = {}
|
|
|
|
local GRID = 32
|
|
|
|
local CELL_STYLE = {
|
|
unscanned = "pvp_radar_cell_gray",
|
|
charted = "pvp_radar_cell_green",
|
|
fresh = "pvp_radar_cell_blue",
|
|
jammed = "pvp_radar_cell_red",
|
|
scanning = "pvp_radar_cell_purple"
|
|
}
|
|
|
|
local STATE_LABEL = {
|
|
unscanned = "unscanned",
|
|
charted = "scanned",
|
|
fresh = "just scanned",
|
|
jammed = "JAMMED"
|
|
}
|
|
|
|
-- Viewport origin and per-player state; scan and jam data live in shared storage.
|
|
local function get_state(player)
|
|
storage.radar_ui = storage.radar_ui or {}
|
|
storage.radar_ui[player.index] = storage.radar_ui[player.index] or {ox = 0, oy = 0}
|
|
return storage.radar_ui[player.index]
|
|
end
|
|
|
|
-- Planet the player can see right now: below their platform, or the surface
|
|
-- they stand on. Traveling platforms show nothing.
|
|
local function viewed_planet(player)
|
|
local surface = player.surface
|
|
if not surface then return nil end
|
|
local platform = surface.platform
|
|
if platform then
|
|
local location = platform.space_location
|
|
if location and game.planets[location.name] then return location.name end
|
|
return nil
|
|
end
|
|
return game.planets[surface.name] and surface.name or nil
|
|
end
|
|
|
|
function radar_ui.create_button(player)
|
|
if not (player and player.valid) then return end
|
|
-- Created inside synced events, so every peer has the same entries; refresh
|
|
-- paths must never insert here or local GUI state would desync the save.
|
|
storage.radar_ui = storage.radar_ui or {}
|
|
storage.radar_ui[player.index] = storage.radar_ui[player.index] or {ox = 0, oy = 0}
|
|
local flow = mod_gui.get_button_flow(player)
|
|
if flow.pvp_radar_button then return end
|
|
flow.add{
|
|
type = "sprite-button",
|
|
name = "pvp_radar_button",
|
|
sprite = "entity/radar",
|
|
tooltip = "Orbital radar"
|
|
}
|
|
end
|
|
|
|
-- Per-player paint cache. Client-local on purpose: it only mirrors GUI state,
|
|
-- so keeping it out of storage is safe for multiplayer.
|
|
local ui_cache = {}
|
|
local function get_cache(player)
|
|
local c = ui_cache[player.index]
|
|
if not c then
|
|
c = {disp = {}, fresh = {}, cursor = 1, planet = false,
|
|
had_jammed = false, jam_version = -1, last_dirty_seen = 0}
|
|
ui_cache[player.index] = c
|
|
end
|
|
return c
|
|
end
|
|
|
|
local function set_cell(cell, sx, sy, display)
|
|
local style_name = CELL_STYLE[display]
|
|
local tags = cell.tags
|
|
if cell.style.name == style_name and tags.sx == sx and tags.sy == sy then return end
|
|
cell.style = style_name
|
|
cell.tags = {sx = sx, sy = sy}
|
|
cell.tooltip = "Sector (" .. sx .. ", " .. sy .. ") - " .. STATE_LABEL[display]
|
|
end
|
|
|
|
-- Full repaints happen in slices (rows top-to-bottom) so a 256-cell repaint
|
|
-- spreads over a few seconds of ticks instead of one spike, and reads as a
|
|
-- radar sweep. Read-only on storage: a player's open window is client-local,
|
|
-- so writing synced state here would desync multiplayer.
|
|
local PAINT_ROWS_PER_TICK = 1
|
|
local PAINT_PER_TICK = PAINT_ROWS_PER_TICK * GRID
|
|
|
|
local function update_labels(player, frame, planet_name)
|
|
frame.pvp_radar_planet.caption = "Planet: " .. (planet_name or "none (platform traveling)")
|
|
if not planet_name then
|
|
frame.pvp_radar_stats.caption = ""
|
|
return
|
|
end
|
|
local stats = radar.scan_stats(player.force.name, planet_name)
|
|
frame.pvp_radar_stats.caption = string.format("Scans/min: %.1f Power: %d%% Radars: %d",
|
|
stats.rate, math.floor(stats.power * 100 + 0.5), stats.radars)
|
|
end
|
|
|
|
local function cell_display(player, surface, sx, sy, cell, jammed_set)
|
|
local tags = cell.tags
|
|
local was_charted = tags.sx == sx and tags.sy == sy
|
|
and (cell.style.name == CELL_STYLE.charted or cell.style.name == CELL_STYLE.fresh)
|
|
return radar.sector_display_state(player.force, surface, sx, sy, game.tick, was_charted, jammed_set)
|
|
end
|
|
|
|
local function paint_next_slice(player)
|
|
local frame = player.gui.screen.pvp_radar_frame
|
|
if not (frame and frame.valid) then return end
|
|
local state = storage.radar_ui and storage.radar_ui[player.index]
|
|
if not state then return end
|
|
local cache = get_cache(player)
|
|
local i0 = cache.repaint_from
|
|
if not i0 then return end
|
|
local planet_name = viewed_planet(player)
|
|
cache.planet = planet_name or false
|
|
cache.ox, cache.oy = state.ox, state.oy
|
|
cache.jam_version = storage.jam_version
|
|
|
|
local children = frame.pvp_radar_grid.children
|
|
local surface = planet_name and game.planets[planet_name].surface
|
|
local jammed_set = surface and radar.jammed_sector_set(surface, player.force)
|
|
if i0 == 1 then
|
|
cache.had_jammed = jammed_set and next(jammed_set) ~= nil or false
|
|
-- Scans during the sweep stay dirty and get repainted after it ends.
|
|
cache.last_dirty_seen = game.tick
|
|
end
|
|
|
|
-- Resolve the slice painted purple last pass to its true colors.
|
|
for i = math.max(1, i0 - PAINT_PER_TICK), math.min(i0 - 1, GRID * GRID) do
|
|
local sx = state.ox + (i - 1) % GRID
|
|
local sy = state.oy + math.floor((i - 1) / GRID)
|
|
local display = "unscanned"
|
|
if surface then display = cell_display(player, surface, sx, sy, children[i], jammed_set) end
|
|
cache.disp[i] = display
|
|
cache.fresh[i] = display == "fresh" and game.tick or nil
|
|
set_cell(children[i], sx, sy, display)
|
|
end
|
|
|
|
-- Paint this slice purple; the next pass resolves it. The purple edge is
|
|
-- the visible scan line.
|
|
for i = i0, math.min(i0 + PAINT_PER_TICK - 1, GRID * GRID) do
|
|
local sx = state.ox + (i - 1) % GRID
|
|
local sy = state.oy + math.floor((i - 1) / GRID)
|
|
local display = "unscanned"
|
|
if surface then display = cell_display(player, surface, sx, sy, children[i], jammed_set) end
|
|
cache.disp[i] = display
|
|
cache.fresh[i] = display == "fresh" and game.tick or nil
|
|
local cell = children[i]
|
|
cell.style = CELL_STYLE.scanning
|
|
cell.tags = {sx = sx, sy = sy}
|
|
cell.tooltip = "Sector (" .. sx .. ", " .. sy .. ") - scanning"
|
|
end
|
|
|
|
-- One extra pass after the last slice resolves the final purple row.
|
|
cache.repaint_from = i0 <= GRID * GRID and i0 + PAINT_PER_TICK or nil
|
|
end
|
|
|
|
local function request_repaint(player)
|
|
local frame = player.gui.screen.pvp_radar_frame
|
|
if not (frame and frame.valid) then return end
|
|
get_cache(player).repaint_from = 1
|
|
update_labels(player, frame, viewed_planet(player))
|
|
paint_next_slice(player) -- first slice now so the window never looks dead
|
|
end
|
|
|
|
local function build_window(player)
|
|
get_state(player)
|
|
if player.gui.screen.pvp_radar_frame then player.gui.screen.pvp_radar_frame.destroy() end
|
|
local frame = player.gui.screen.add{
|
|
type = "frame",
|
|
name = "pvp_radar_frame",
|
|
caption = "ORBITAL RADAR",
|
|
direction = "vertical"
|
|
}
|
|
frame.auto_center = true
|
|
|
|
frame.add{type = "label", name = "pvp_radar_planet", caption = ""}
|
|
frame.add{type = "label", name = "pvp_radar_stats", caption = ""}
|
|
|
|
local grid = frame.add{type = "table", name = "pvp_radar_grid", column_count = GRID}
|
|
grid.style.horizontal_spacing = 1
|
|
grid.style.vertical_spacing = 1
|
|
for i = 1, GRID * GRID do
|
|
grid.add{type = "button", name = "pvp_radar_cell_" .. i, style = CELL_STYLE.unscanned, tags = {sx = 0, sy = 0}}
|
|
end
|
|
|
|
-- dx/dy are in sectors; one GRID is a page. Panning only moves the viewport.
|
|
local pan = frame.add{type = "flow", direction = "horizontal"}
|
|
for i, spec in ipairs({
|
|
{"<<", -GRID, 0}, {"<", -1, 0}, {">", 1, 0}, {">>", GRID, 0},
|
|
{"^^", 0, -GRID}, {"^", 0, -1}, {"v", 0, 1}, {"vv", 0, GRID},
|
|
}) do
|
|
pan.add{type = "button", name = "pvp_radar_pan_" .. i, caption = spec[1],
|
|
style = "tool_button", tags = {dx = spec[2], dy = spec[3]}}
|
|
end
|
|
pan.add{type = "button", name = "pvp_radar_close", caption = "Close"}
|
|
|
|
request_repaint(player)
|
|
end
|
|
|
|
-- Spam-clicking pans or cells must not trigger a full 1024-cell repaint per
|
|
-- click; repaint at most every 6 ticks and let the periodic refresh catch up.
|
|
local last_paint = {}
|
|
local function repaint_throttled(player)
|
|
local last = last_paint[player.index] or -60
|
|
if game.tick - last >= 6 then
|
|
last_paint[player.index] = game.tick
|
|
request_repaint(player)
|
|
end
|
|
end
|
|
|
|
function radar_ui.on_gui_click(event)
|
|
local element = event.element
|
|
if not (element and element.valid) then return false end
|
|
local player = game.get_player(event.player_index)
|
|
if not (player and player.valid) then return false end
|
|
local name = element.name
|
|
|
|
if name == "pvp_radar_button" then
|
|
if player.gui.screen.pvp_radar_frame then
|
|
player.gui.screen.pvp_radar_frame.destroy()
|
|
else
|
|
build_window(player)
|
|
end
|
|
return true
|
|
end
|
|
if name == "pvp_radar_close" then
|
|
if player.gui.screen.pvp_radar_frame then player.gui.screen.pvp_radar_frame.destroy() end
|
|
return true
|
|
end
|
|
if name:find("^pvp_radar_pan_") then
|
|
local tags = element.tags
|
|
local state = get_state(player)
|
|
state.ox = state.ox + (tags.dx or 0)
|
|
state.oy = state.oy + (tags.dy or 0)
|
|
repaint_throttled(player)
|
|
return true
|
|
end
|
|
if name:find("^pvp_radar_cell_") then
|
|
-- Click centres the viewport on that sector.
|
|
local tags = element.tags
|
|
local state = get_state(player)
|
|
state.ox = (tags.sx or 0) - math.floor(GRID / 2)
|
|
state.oy = (tags.sy or 0) - math.floor(GRID / 2)
|
|
repaint_throttled(player)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Cheap per-open-window update: only repaint cells whose sector was scanned
|
|
-- since last pass, expire blue flashes, and probe a few gray cells for ground
|
|
-- charting. Full repaints only on pan/planet/jam changes.
|
|
local PROBES_PER_TICK = 64
|
|
local function update_open_frame(player, frame, state)
|
|
local cache = get_cache(player)
|
|
local planet_name = viewed_planet(player)
|
|
|
|
-- Viewport moved since last paint (pan click may have been throttled).
|
|
if state.ox ~= cache.ox or state.oy ~= cache.oy
|
|
or (planet_name or false) ~= cache.planet then
|
|
request_repaint(player)
|
|
return
|
|
end
|
|
if not planet_name then return end
|
|
local surface = game.planets[planet_name].surface
|
|
if not surface then return end
|
|
local children = frame.pvp_radar_grid.children
|
|
|
|
-- A sweep in progress owns the grid until it finishes.
|
|
if cache.repaint_from then paint_next_slice(player) return end
|
|
|
|
update_labels(player, frame, planet_name)
|
|
|
|
if storage.jam_version ~= cache.jam_version then
|
|
-- Zones rebuilt this second; repaint only if this view is affected.
|
|
if cache.had_jammed then
|
|
request_repaint(player)
|
|
return
|
|
end
|
|
local set = radar.jammed_sector_set(surface, player.force)
|
|
if set and next(set) then request_repaint(player) return end
|
|
cache.jam_version = storage.jam_version
|
|
end
|
|
|
|
-- Sectors charted by orbital scans since our last pass.
|
|
local dirty = storage.scan_dirty
|
|
and storage.scan_dirty[player.force.name]
|
|
and storage.scan_dirty[player.force.name][planet_name]
|
|
if dirty then
|
|
for key, tick in pairs(dirty) do
|
|
if tick > cache.last_dirty_seen then
|
|
local sep = key:find(",")
|
|
local sx, sy = tonumber(key:sub(1, sep - 1)), tonumber(key:sub(sep + 1))
|
|
local gx, gy = sx - state.ox, sy - state.oy
|
|
if gx >= 0 and gx < GRID and gy >= 0 and gy < GRID then
|
|
local i = gy * GRID + gx + 1
|
|
local display = radar.sector_display_state(player.force, surface, sx, sy, game.tick, true)
|
|
set_cell(children[i], sx, sy, display)
|
|
cache.disp[i] = display
|
|
cache.fresh[i] = display == "fresh" and tick or nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
cache.last_dirty_seen = game.tick
|
|
|
|
-- Blue flash expired: we charted it, so it is charted now, no API call.
|
|
for i, tick in pairs(cache.fresh) do
|
|
if game.tick - tick >= radar.FRESH_TICKS then
|
|
set_cell(children[i], children[i].tags.sx, children[i].tags.sy, "charted")
|
|
cache.disp[i] = "charted"
|
|
cache.fresh[i] = nil
|
|
end
|
|
end
|
|
|
|
-- Ground sources can chart sectors we never scanned; probe a bounded slice
|
|
-- of still-gray cells each pass instead of polling the whole grid.
|
|
local grid_n = GRID * GRID
|
|
local probed, tries = 0, 0
|
|
while probed < PROBES_PER_TICK and tries < grid_n do
|
|
local i = cache.cursor
|
|
cache.cursor = cache.cursor % grid_n + 1
|
|
tries = tries + 1
|
|
if cache.disp[i] == "unscanned" then
|
|
probed = probed + 1
|
|
local sx = state.ox + (i - 1) % GRID
|
|
local sy = state.oy + math.floor((i - 1) / GRID)
|
|
if player.force.is_chunk_charted(surface, {x = sx, y = sy}) then
|
|
set_cell(children[i], sx, sy, "charted")
|
|
cache.disp[i] = "charted"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function radar_ui.on_ui_tick()
|
|
if not storage.radar_ui then return end
|
|
for index, state in pairs(storage.radar_ui) do
|
|
local player = game.get_player(index)
|
|
if player and player.connected then
|
|
local frame = player.gui.screen.pvp_radar_frame
|
|
if frame and frame.valid then update_open_frame(player, frame, state) end
|
|
end
|
|
end
|
|
end
|
|
|
|
function radar_ui.on_player_changed_surface(event)
|
|
local player = game.get_player(event.player_index)
|
|
if player and player.gui.screen.pvp_radar_frame then request_repaint(player) end
|
|
end
|
|
|
|
function radar_ui.on_platform_state_changed(event)
|
|
local platform = event.platform
|
|
local surface = platform and platform.valid and platform.surface
|
|
if not surface then return end
|
|
for _, player in pairs(game.connected_players) do
|
|
if player.surface == surface and player.gui.screen.pvp_radar_frame then
|
|
request_repaint(player)
|
|
end
|
|
end
|
|
end
|
|
|
|
return radar_ui
|