From c71941d8e4eab143a670fa13ca088a7615566562 Mon Sep 17 00:00:00 2001 From: Floris Date: Thu, 10 Sep 2026 18:08:53 +0200 Subject: [PATCH 1/2] changelog: changed style to be more similar to keybind editor (#9191) --- luaui/Widgets/gui_changelog_info.lua | 893 +++++++++++++++------------ luaui/Widgets/gui_options.lua | 4 +- 2 files changed, 517 insertions(+), 380 deletions(-) diff --git a/luaui/Widgets/gui_changelog_info.lua b/luaui/Widgets/gui_changelog_info.lua index c1fd5a1a4b2..624c26bc002 100644 --- a/luaui/Widgets/gui_changelog_info.lua +++ b/luaui/Widgets/gui_changelog_info.lua @@ -14,10 +14,17 @@ end -- Localized functions for performance local mathFloor = math.floor +local mathMax = math.max +local mathMin = math.min -- Localized Spring API for performance local spGetMouseState = Spring.GetMouseState local spGetViewGeometry = Spring.GetViewGeometry +local spIsGUIHidden = Spring.IsGUIHidden +local glCreateList = gl.CreateList +local glCallList = gl.CallList +local glDeleteList = gl.DeleteList +local math_isInRect = math.isInRect local vsx, vsy = spGetViewGeometry() @@ -29,236 +36,371 @@ local changelogFileLength = string.len(changelogFile) local lastviewedHash = "" local lastviewedChangelogLength = 0 -local screenHeightOrg = 520 -local screenWidthOrg = 1050 +local screenHeightOrg = 610 +local screenWidthOrg = 1100 local screenHeight = screenHeightOrg local screenWidth = screenWidthOrg local playSounds = true local buttonclick = "LuaUI/Sounds/buildbar_waypoint.wav" -local startLine = 1 - local centerPosX = 0.5 local centerPosY = 0.5 local screenX = mathFloor((vsx * centerPosX) - (screenWidth / 2)) local screenY = mathFloor((vsy * centerPosY) + (screenHeight / 2)) - -local spIsGUIHidden = Spring.IsGUIHidden -local glCreateList = gl.CreateList -local glCallList = gl.CallList -local glDeleteList = gl.DeleteList - -local vsx, vsy = spGetViewGeometry() local widgetScale = (vsy / 1080) -local versions = {} +local versions = {} -- version k -> index of the changelog line its heading is on +local versionLabels = {} -- version k -> the heading text shown in the month column local changelogLines = {} -local totalChangelogLines = 0 local showOnceMore = false -- used because of GUI shader delay -local RectRound, UiElement, UiScroller, elementCorner - -local versionOffsetX = 6 -local versionOffsetY = 14 -local versionFontSize = 16 -local versionWidth = 140 - -local versionQuickLinks = {} -local maxLines = 20 - -local math_isInRect = math.isInRect - -local font, loadedFontSize, font2, changelogList, titleRect, backgroundGuishader, changelogList, dlistcreated, show, bgpadding +---@type function +local RectRound +---@type function +local UiElement +---@type function +local UiScroller +---@type function +local Highlight +local elementCorner +local font, loadedFontSize +local panelList, sidebarList, backgroundGuishader, show +local titleText = "" + +-- The panel is laid out the way the keybind editor lays out its own: the same inset +-- area, the title in the top-left corner over a darker card holding the month column, +-- and a 14 px scrollbar standing in its own channel against the right edge. +local area = { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } +local metrics = { + sidePad = 12, + catInset = 4, + catRowHeight = 29, + catFs = 13, + -- The band above the text where the title sits, and the gap under it. + headerH = 34, + headerGap = 4, + -- Everything against the panel's right edge - the scrollbar - is held off it by this. + edgeInset = 4, + -- Clearance between the right edge of the text and the scrollbar beside it. + listGap = 12, + -- How far the month card rises above the first entry in it. + cardLip = 5, + -- The panel title: its baseline below the top edge, and its size. + titleY = 17, + titleFs = 20, + -- How far the month column starts below the text beside it, so the title has room. + sidebarDrop = 8, + sidebarW = 200, + barW = 14, + -- Text rows: the heading size plus a separator, shared by every kind of line so the + -- scrollbar can count rows. + lineH = 19, + fsTitle = 17, + fsDate = 13, + fsLine = 15, + -- Body lines and dates sit this far in from the headings; bullet text a further step. + bodyX = 9, + bulletX = 26, + -- Corner radii, taken from FlowUI's so the panel rounds like the rest of the UI. + csSmall = 2, + csPanel = 4, +} +local look = { + -- The month column sits on its own darker card, so it reads apart from the text. + sidebarFill = { 0, 0, 0, 0.24 }, + sidebarFillTop = { 0, 0, 0, 0.16 }, + selectedFill = { 1, 1, 1, 0.13 }, + white = { 1, 1, 1 }, + -- Entries hover with the same FlowUI highlight the settings list uses, at the + -- strength it gives a plain row. + rowHoverOpacity = 0.14, + colorTitle = { 1, 1, 1, 1 }, + colorDate = { 0.66, 0.88, 0.66, 1 }, + colorLine = { 0.8, 0.77, 0.74, 1 }, +} +local colorText = "\255\235\235\235" +local colorSelected = "\255\210\210\205" +local colorDim = "\255\160\160\160" + +local listTop = 0 +local listBottom = 0 +local listX1 = 0 +local listRight = 0 +local barX1 = 0 + +-- One entry per changelog line: the rows it wraps into, where they print and how, so +-- the draw loop and the scroll math count the same rows. +local layout = {} +-- Rows above each line, so the scrollbar knows where a line sits in the whole text. +local lineOff = {} +local totalRows = 0 +local startLine = 1 +-- The highest startLine that still fills the band with whole entries. +local maxStart = 1 +local dragging = false +-- Month column state: the entry under the cursor and the one lit as current. The +-- sidebar list is rebuilt whenever either changes. +local hoverIdx, selectedIdx +local sidebarHover, sidebarSelected + +local function dropLists() + if panelList then + glDeleteList(panelList) + panelList = nil + end + if sidebarList then + glDeleteList(sidebarList) + sidebarList = nil + end +end -function widget:ViewResize() - vsx, vsy = spGetViewGeometry() - widgetScale = (vsy / 1080) +local function deleteGuishader() + if backgroundGuishader ~= nil then + if WG.guishader then + WG.guishader.DeleteDlist("changelog") + else + glDeleteList(backgroundGuishader) + end + backgroundGuishader = nil + end +end - screenHeight = mathFloor(screenHeightOrg * widgetScale) - screenWidth = mathFloor(screenWidthOrg * widgetScale) - screenX = mathFloor((vsx * centerPosX) - (screenWidth / 2)) - screenY = mathFloor((vsy * centerPosY) + (screenHeight / 2)) +-- The version whose section holds the given line: the last heading at or above it. +local function versionAt(line) + if not versions[1] then + return nil + end + local k = 1 + for i = 2, #versions do + if versions[i] <= line then + k = i + else + break + end + end + return k +end - font, loadedFontSize = WG.fonts.getFont() - font2 = WG.fonts.getFont(2) - bgpadding = WG.FlowUI.elementPadding - elementCorner = WG.FlowUI.elementCorner +-- Moves the text so the given line is the first one shown. The current month follows +-- the top line unless a click chose one explicitly, in which case that one stays lit +-- until the text is scrolled again. +local function setStartLine(n, chosen) + n = mathMax(1, mathMin(maxStart, n)) + if n ~= startLine then + startLine = n + if panelList then + glDeleteList(panelList) + panelList = nil + end + end + selectedIdx = chosen or versionAt(startLine) +end - RectRound = WG.FlowUI.Draw.RectRound - UiElement = WG.FlowUI.Draw.Element - UiScroller = WG.FlowUI.Draw.Scroller +-- Cursor height in the band mapped straight onto the scroll range, as the keybind +-- editor's bar does: the top of the bar is the start, the bottom the end. +local function scrollFromY(y) + local f = (listTop - y) / mathMax(1, listTop - listBottom) + if f < 0 then + f = 0 + elseif f > 1 then + f = 1 + end + setStartLine(1 + mathFloor(f * (maxStart - 1) + 0.5)) +end - if changelogList then - gl.DeleteList(changelogList) +local function splitRows(s) + local rows = {} + for row in string.gmatch(s .. "\n", "([^\n]*)\n") do + rows[#rows + 1] = row + end + if #rows == 0 then + rows[1] = "" end - changelogList = gl.CreateList(DrawWindow) + return rows end -function DrawSidebar(x, y, width, height) - local fontSize = versionFontSize * widgetScale - local fontOffsetY = versionOffsetY * widgetScale - local fontOffsetX = versionOffsetX * widgetScale - - -- background - gl.Color(0.7, 0.5, 0.15, 0.14) - RectRound(x, y - height, x + width, y, bgpadding, 0, 0, 0, 1, { 0.55, 0.4, 0.12, 0.14 }, { 0.8, 0.57, 0.18, 0.14 }) - - -- version links - versionQuickLinks = {} - if changelogFile then - font:Begin() - font:SetOutlineColor(0.2, 0.17, 0, 0.33) - font:SetTextColor(0.8, 0.65, 0.15, 1) - local lineKey = 1 - local yOffset = 24 * widgetScale - local j = 0 - while j < 22 do - if ((fontSize + fontOffsetY) * j) + 4 > height - yOffset then - break - end - if versions[lineKey] == nil then - break +-- Wraps every line once against the text width, so drawing prints ready rows and the +-- scrollbar can measure the whole text instead of guessing one row per line. +local function layoutLines() + layout = {} + lineOff = {} + local off = 0 + local fsLine = metrics.fsLine + local scaleToFont = loadedFontSize / fsLine + local textW = listRight - listX1 - metrics.sidePad + local wrapBullet = (textW - metrics.bodyX - metrics.bulletX) * scaleToFont + local wrapPlain = (textW - metrics.bodyX) * scaleToFont + + for i, line in ipairs(changelogLines) do + local entry + if + string.find(line, "^([0-9][0-9][/][0-9][0-9][/][0-9][0-9])") + or string.find(line, "^([0-9][/][0-9][0-9][/][0-9][0-9])") + then + -- date line + entry = { rows = { line }, x = metrics.bodyX, fs = metrics.fsDate, color = look.colorDate } + elseif string.find(line, "^# ") then + -- version line + entry = { rows = { string.sub(line, 3) }, x = 0, fs = metrics.fsTitle, color = look.colorTitle } + elseif string.find(line, "^(-)") then + -- bulletpointed line + local firstLetterPos = 2 + if string.find(line, "^(- )") then + firstLetterPos = 3 end - local line = changelogLines[versions[lineKey]] - - -- version button title - --line = " " .. string.match(line, '( %d*%d.?%d+)') - line = " " .. string.sub(line, 3) - local textY = y - ((fontSize + fontOffsetY) * j) - (20 * widgetScale) - font:Print(line, x + fontOffsetX, textY, fontSize, "on") - - versionQuickLinks[j] = { - mathFloor(x), - mathFloor(textY - (versionFontSize * widgetScale * 0.66)), - mathFloor(x + (versionWidth * widgetScale)), - math.ceil(textY + (versionFontSize * widgetScale * 1.21)), + local text = string.upper(string.sub(line, firstLetterPos, firstLetterPos)) + .. string.sub(line, firstLetterPos + 1) + entry = { + rows = splitRows((font:WrapText(text, wrapBullet))), + x = metrics.bodyX + metrics.bulletX, + fs = fsLine, + color = look.colorLine, + bullet = true, + } + else + entry = { + rows = splitRows((font:WrapText(line, wrapPlain))), + x = metrics.bodyX, + fs = fsLine, + color = look.colorLine, } - - j = j + 1 - lineKey = lineKey + 1 end - font:End() + layout[i] = entry + lineOff[i] = off + off = off + #entry.rows end + totalRows = off + + -- Walked back from the end so the last page is full of whole entries, the way the + -- keybind editor finds its own last page. + local rowsFit = mathFloor((listTop - listBottom) / metrics.lineH) + local used = 0 + local i = #layout + while i > 0 do + local n = #layout[i].rows + if used + n > rowsFit then + break + end + used = used + n + i = i - 1 + end + maxStart = mathMax(1, i + 1) + setStartLine(startLine) end -function DrawTextarea(x, y, width, height, scrollbar) - local scrollbarOffsetTop = 0 -- note: won't add the offset to the bottom, only to top - local scrollbarOffsetBottom = 0 -- note: won't add the offset to the top, only to bottom - local scrollbarMargin = 10 * widgetScale - local scrollbarWidth = 8 * widgetScale - local scrollbarPosWidth = 4 * widgetScale - - local fontSizeTitle = 17 * widgetScale - local fontSizeDate = 13 * widgetScale - local fontSizeLine = 15 * widgetScale - local lineSeparator = 2 * widgetScale - - local fontColorTitle = { 1, 1, 1, 1 } - local fontColorDate = { 0.66, 0.88, 0.66, 1 } - local fontColorLine = { 0.8, 0.77, 0.74, 1 } - - local textRightOffset = scrollbar and scrollbarMargin + scrollbarWidth + scrollbarWidth or 0 - maxLines = mathFloor(height / (lineSeparator + fontSizeTitle)) - - -- textarea scrollbar - if scrollbar then - if totalChangelogLines > maxLines or startLine > 1 then - -- only show scroll above X lines - local scrollbarTop = y - scrollbarOffsetTop - scrollbarMargin - (scrollbarWidth - scrollbarPosWidth) - local scrollbarBottom = y - - scrollbarOffsetBottom - - height - + scrollbarMargin - + (scrollbarWidth - scrollbarPosWidth) - - UiScroller( - mathFloor(x + width - scrollbarMargin - scrollbarWidth), - mathFloor(scrollbarBottom - (scrollbarWidth - scrollbarPosWidth)), - mathFloor(x + width - scrollbarMargin), - mathFloor(scrollbarTop + (scrollbarWidth - scrollbarPosWidth)), - #changelogLines * (lineSeparator + fontSizeTitle), - (startLine - 1) * (lineSeparator + fontSizeTitle) - ) - end +-- Rebuilds every rect against the panel size. Whole pixels throughout, so glyph and +-- rectangle edges do not land between pixels. +local function setLayout() + local s = widgetScale + local pad = mathFloor(8 * s) + area.x1 = screenX + pad + area.y1 = screenY - screenHeight + pad + area.x2 = screenX + screenWidth - pad + area.y2 = screenY - pad + + metrics.sidePad = mathFloor(12 * s) + metrics.catInset = mathFloor(4 * s) + metrics.catRowHeight = mathFloor(29 * s) + metrics.catFs = mathFloor(metrics.catRowHeight * 0.55 * 0.85) + metrics.headerH = mathFloor(34 * s) + metrics.headerGap = mathFloor(4 * s) + metrics.edgeInset = mathFloor(4 * s) + metrics.listGap = mathFloor(12 * s) + metrics.cardLip = mathFloor(5 * s) + metrics.titleY = mathFloor(17 * s) + metrics.titleFs = mathFloor(mathFloor(24 * s) * 0.85) + metrics.sidebarDrop = mathFloor(8 * s) + metrics.sidebarW = mathFloor(200 * s) + metrics.barW = mathFloor(14 * s) + metrics.fsTitle = mathFloor(17 * s) + metrics.fsDate = mathFloor(13 * s) + metrics.fsLine = mathFloor(15 * s) + metrics.lineH = metrics.fsTitle + mathFloor(2 * s) + metrics.bodyX = mathFloor(9 * s) + metrics.bulletX = mathFloor(26 * s) + metrics.csPanel = mathFloor(elementCorner) + metrics.csSmall = mathFloor(elementCorner * 0.66) + + listX1 = area.x1 + metrics.sidebarW + metrics.listGap + listTop = area.y2 - metrics.headerH - metrics.headerGap + listBottom = area.y1 + metrics.edgeInset + -- The scrollbar owns a column of its own against the panel edge, and the text stops + -- a clear gap short of it, so the bar sits in a channel rather than hugging the rows. + barX1 = area.x2 - metrics.edgeInset - metrics.barW + listRight = barX1 - metrics.listGap +end + +-- The month column starts below where the text does, so the title above it is not +-- crowded by the first entry. Everything in the column measures from here. +local function sidebarTop() + return listTop - metrics.sidebarDrop +end + +local function categoryRect(i) + local top = sidebarTop() - (i - 1) * metrics.catRowHeight + + return area.x1, top - metrics.catRowHeight, area.x1 + metrics.sidebarW, top +end + +-- The month entry under x,y, or nil. Half-open on the shared edge, so one point never +-- lands in two entries. +local function sidebarIndexAt(x, y) + local top = sidebarTop() + if x < area.x1 or x > area.x1 + metrics.sidebarW or y > top or y <= listBottom then + return nil end - -- draw textarea - if changelogFile then - font:Begin() - local lineKey = startLine - local j = 1 - while j < maxLines + 1 do - -- maxlines is not exact, just a failsafe - if (lineSeparator + fontSizeTitle) * j > height then - break - end - if changelogLines[lineKey] == nil then - break - end + local i = mathFloor((top - y) / metrics.catRowHeight) + 1 + if not versions[i] then + return nil + end - local line = changelogLines[lineKey] - if - string.find(line, "^([0-9][0-9][/][0-9][0-9][/][0-9][0-9])") - or string.find(line, "^([0-9][/][0-9][0-9][/][0-9][0-9])") - then - -- date line - line = " " .. line - font:SetTextColor(fontColorDate) - font:Print(line, x, y - (lineSeparator + fontSizeTitle) * j, fontSizeDate, "n") - elseif string.find(line, "^# ") then - -- version line - line = string.sub(line, 3) - font:SetTextColor(fontColorTitle) - font:Print(line, x - (9 * widgetScale), y - (lineSeparator + fontSizeTitle) * j, fontSizeTitle, "n") - else - font:SetTextColor(fontColorLine) - local numLines - if string.find(line, "^(-)") then - -- bulletpointed line - local firstLetterPos = 2 - if string.find(line, "^(- )") then - firstLetterPos = 3 - end - line = string.upper(string.sub(line, firstLetterPos, firstLetterPos)) - .. string.sub(line, firstLetterPos + 1) - line, numLines = font:WrapText( - line, - (width - (90 * widgetScale) - textRightOffset) * (loadedFontSize / fontSizeLine) - ) - if (lineSeparator + fontSizeTitle) * (j + numLines - 1) > height then - break - end - font:Print(" - ", x, y - (lineSeparator + fontSizeTitle) * j, fontSizeLine, "n") - font:Print(line, x + (26 * widgetScale), y - (lineSeparator + fontSizeTitle) * j, fontSizeLine, "n") - else - -- line - line = " " .. line - line, numLines = font:WrapText(line, (width - (50 * widgetScale)) * (loadedFontSize / fontSizeLine)) - if (lineSeparator + fontSizeTitle) * (j + numLines - 1) > height then - break - end - font:Print(line, x, y - (lineSeparator + fontSizeTitle) * j, fontSizeLine, "n") - end - j = j + (numLines - 1) - end + local _, y1 = categoryRect(i) + if y1 < listBottom then + return nil + end - j = j + 1 - lineKey = lineKey + 1 + return i +end + +-- The month column's entries: the lit current one, the hover, then the labels. The card +-- itself is part of the panel list, since it never changes with the cursor. +local function drawSidebar() + local n = #versions + local shown = 0 + for i = 1, n do + local x1, y1, x2, y2 = categoryRect(i) + if y1 < listBottom then + break + end + shown = i + if i == selectedIdx then + RectRound(x1 + metrics.catInset, y1, x2 - metrics.catInset, y2, metrics.csSmall, 1, 1, 1, 1, look.selectedFill) + elseif i == hoverIdx then + Highlight(x1 + metrics.catInset, y1, x2 - metrics.catInset, y2, metrics.csSmall, look.rowHoverOpacity, look.white) end - font:End() end + + font:Begin() + for i = 1, shown do + local x1, y1, _, y2 = categoryRect(i) + local label = (i == selectedIdx and colorSelected or colorDim) .. versionLabels[i] + font:Print(label, x1 + metrics.sidePad, mathFloor((y1 + y2) * 0.5), metrics.catFs, "ov") + end + font:End() end -function DrawWindow() - -- background +-- The panel: its backdrop, the title, the month card, the text and the scrollbar. Baked +-- and replayed until the text scrolls or the screen resizes. +local function drawPanel() UiElement( screenX, screenY - screenHeight, screenX + screenWidth, screenY, - 0, + 1, 1, 1, 1, @@ -269,130 +411,181 @@ function DrawWindow() WG.FlowUI.clampedOpacity ) - -- title background - local title = BAR.I18N("ui.changelog.title") - local titleFontSize = 18 * widgetScale - titleRect = { - screenX, - screenY, - mathFloor(screenX + (font2:GetTextWidth(title) * titleFontSize) + (titleFontSize * 1.5)), - mathFloor(screenY + (titleFontSize * 1.7)), - } - - gl.Color(0, 0, 0, WG.FlowUI.clampedOpacity) - RectRound(titleRect[1], titleRect[2], titleRect[3], titleRect[4], elementCorner, 1, 1, 0, 0) - - -- title - font2:Begin() - font2:SetTextColor(1, 1, 1, 1) - font2:SetOutlineColor(0, 0, 0, 0.4) - font2:Print(title, screenX + (titleFontSize * 0.75), screenY + (8 * widgetScale), titleFontSize, "on") - font2:End() - - -- version links - DrawSidebar( - screenX + bgpadding, - screenY - bgpadding, - versionWidth * widgetScale, - screenHeight - bgpadding - bgpadding + -- Derived from the first entry rather than measured from the panel top, so the card + -- keeps its lip above the entries wherever the column starts. + RectRound( + area.x1, + area.y1, + area.x1 + metrics.sidebarW, + sidebarTop() + metrics.cardLip, + metrics.csPanel, + 1, + 1, + 1, + 1, + look.sidebarFill, + look.sidebarFillTop ) - -- textarea - DrawTextarea( - screenX + ((versionWidth + 30) * widgetScale), - screenY - (10 * widgetScale), - screenWidth - ((versionWidth + 30) * widgetScale), - screenHeight - (24 * widgetScale), - 1 - ) + font:Begin() + font:SetTextColor(1, 1, 1, 1) + font:Print(titleText, area.x1 + metrics.sidePad, area.y2 - metrics.titleY, metrics.titleFs, "ov") + + -- Whole entries only: the band can end mid-entry, and a row painted below it would + -- be clipped by nothing. + local lineH = metrics.lineH + local rowsFit = mathFloor((listTop - listBottom) / lineH) + local r = 0 + local i = startLine + while layout[i] do + local e = layout[i] + local rows = e.rows + if r + #rows > rowsFit then + break + end + local x = listX1 + e.x + font:SetTextColor(e.color) + if e.bullet then + font:Print(" - ", listX1 + metrics.bodyX, listTop - (r + 1) * lineH, e.fs, "n") + end + for k = 1, #rows do + font:Print(rows[k], x, listTop - (r + k) * lineH, e.fs, "n") + end + r = r + #rows + i = i + 1 + end + font:End() + + UiScroller(barX1, listBottom, area.x2 - metrics.edgeInset, listTop, totalRows * lineH, lineOff[startLine] * lineH) +end + +function widget:ViewResize() + vsx, vsy = spGetViewGeometry() + widgetScale = (vsy / 1080) + + screenHeight = mathFloor(screenHeightOrg * widgetScale) + screenWidth = mathFloor(screenWidthOrg * widgetScale) + screenX = mathFloor((vsx * centerPosX) - (screenWidth / 2)) + screenY = mathFloor((vsy * centerPosY) + (screenHeight / 2)) + + font, loadedFontSize = WG.fonts.getFont() + elementCorner = WG.FlowUI.elementCorner + + RectRound = WG.FlowUI.Draw.RectRound + UiElement = WG.FlowUI.Draw.Element + UiScroller = WG.FlowUI.Draw.Scroller + Highlight = WG.FlowUI.Draw.SelectHighlight + + titleText = colorText .. BAR.I18N("ui.changelog.title") + + setLayout() + layoutLines() + dropLists() + deleteGuishader() end function widget:DrawScreen() - -- draw the help - if not changelogList then - changelogList = gl.CreateList(DrawWindow) + if not (show or showOnceMore) then + deleteGuishader() + return end - if show or showOnceMore then - gl.Texture(false) -- some other widget left it on + gl.Texture(false) -- some other widget left it on - -- draw the changelog panel - glCallList(changelogList) - if WG.guishader then - if backgroundGuishader ~= nil then - glDeleteList(backgroundGuishader) - end - backgroundGuishader = glCreateList(function() - -- background - RectRound(screenX, screenY - screenHeight, screenX + screenWidth, screenY, elementCorner, 0, 1, 1, 1) - -- title - RectRound(titleRect[1], titleRect[2], titleRect[3], titleRect[4], elementCorner, 1, 1, 0, 0) - end) - WG.guishader.InsertDlist(backgroundGuishader, "changelog") - dlistcreated = true + local mx, my, lmb = spGetMouseState() + if dragging then + if lmb then + scrollFromY(my) + else + dragging = false end - showOnceMore = false + end - -- draw button hover - local usedScreenX = mathFloor((vsx * centerPosX) - ((screenWidth / 2) * widgetScale)) - local usedScreenY = mathFloor((vsy * centerPosY) + ((screenHeight / 2) * widgetScale)) + hoverIdx = show and sidebarIndexAt(mx, my) or nil - local x, y, pressed = spGetMouseState() - if - math_isInRect(x, y, screenX, screenY - screenHeight, screenX + screenWidth, screenY) - or math_isInRect(x, y, titleRect[1], titleRect[2], titleRect[3], titleRect[4]) - then - Spring.SetMouseCursor("cursornormal") - end - if changelogFile then - local yOffset = 24 * widgetScale - local yOffsetUp = ((versionFontSize * 0.66) + yOffset) * widgetScale - local yOffsetDown = ((versionFontSize * 1.21) - yOffset) * widgetScale - for k, v in pairs(versionQuickLinks) do - if math_isInRect(x, y, v[1], v[2], v[3], v[4]) then - if pressed then - gl.Color(1, 0.93, 0.75, 0.23) - else - gl.Color(1, 0.93, 0.75, 0.15) - end - RectRound(v[1], v[2], v[3], v[4], bgpadding, 0, 0, 0, 0) - break - end - end + if not panelList then + panelList = glCreateList(drawPanel) + end + if not sidebarList or hoverIdx ~= sidebarHover or selectedIdx ~= sidebarSelected then + if sidebarList then + glDeleteList(sidebarList) end - elseif dlistcreated and WG.guishader then - WG.guishader.RemoveDlist("changelog") - dlistcreated = nil + sidebarList = glCreateList(drawSidebar) + sidebarHover = hoverIdx + sidebarSelected = selectedIdx + end + + glCallList(panelList) + glCallList(sidebarList) + + if WG.guishader and backgroundGuishader == nil then + backgroundGuishader = glCreateList(function() + RectRound(screenX, screenY - screenHeight, screenX + screenWidth, screenY, elementCorner, 1, 1, 1, 1) + end) + WG.guishader.InsertDlist(backgroundGuishader, "changelog") + end + showOnceMore = false + + if math_isInRect(mx, my, screenX, screenY - screenHeight, screenX + screenWidth, screenY) then + Spring.SetMouseCursor("cursornormal") end end function widget:KeyPress(key) - if key == 27 then + if show and key == 27 then -- ESC show = false end end -function widget:MouseWheel(up, value) - if show then - local addLines = value * -3 -- direction is retarded +-- Swallowed across the whole panel, not just the text: a wheel that gets through zooms +-- the camera behind it. +function widget:MouseWheel(up, _value) + if not show then + return false + end - startLine = startLine + addLines - if startLine >= totalChangelogLines - maxLines then - startLine = totalChangelogLines - maxLines + 1 - end - if startLine < 1 then - startLine = 1 - end + local x, y = spGetMouseState() + if not math_isInRect(x, y, screenX, screenY - screenHeight, screenX + screenWidth, screenY) then + return false + end - if changelogList then - glDeleteList(changelogList) + setStartLine(startLine + (up and -3 or 3)) + return true +end + +-- Clicks inside the panel pick a month or grab the scrollbar; a press outside closes it. +local function mouseEvent(x, y, button, release) + if spIsGUIHidden() then + return false + end + + if not show then + return false + end + + if math_isInRect(x, y, screenX, screenY - screenHeight, screenX + screenWidth, screenY) then + if not release and button == 1 then + local i = sidebarIndexAt(x, y) + if i then + setStartLine(versions[i], i) + if playSounds then + Spring.PlaySoundFile(buttonclick, 0.6, "ui") + end + elseif math_isInRect(x, y, barX1, listBottom, area.x2, listTop) then + -- The strip between the bar and the panel edge stays grabbable too. + dragging = true + scrollFromY(y) + end end - changelogList = gl.CreateList(DrawWindow) return true - else - return false + elseif not release then + -- Only a press outside closes. A release out here belongs to a drag that started + -- on the scrollbar. + showOnceMore = true -- show once more because the guishader lags behind + show = false + return true end end @@ -404,111 +597,55 @@ function widget:MouseRelease(x, y, button) return mouseEvent(x, y, button, true) end -function mouseEvent(x, y, button, release) - if spIsGUIHidden() then +function widget:Initialize() + if not changelogFile then + Spring.Echo("Changelog: couldn't load the changelog file") + widgetHandler:RemoveWidget() return end - if show then - -- on window - if math_isInRect(x, y, screenX, screenY - screenHeight, screenX + screenWidth, screenY) then - -- version buttons - if button == 1 and release then - local usedScreenX = mathFloor((vsx * centerPosX) - ((screenWidth / 2) * widgetScale)) - local usedScreenY = mathFloor((vsy * centerPosY) + ((screenHeight / 2) * widgetScale)) - - local x, y = spGetMouseState() - if changelogFile then - for k, v in pairs(versionQuickLinks) do - if math_isInRect(x, y, v[1], v[2], v[3], v[4]) then - startLine = versions[k + 1] - if changelogList then - glDeleteList(changelogList) - end - changelogList = gl.CreateList(DrawWindow) - if playSounds then - Spring.PlaySoundFile(buttonclick, 0.6, "ui") - end - break - end - end - end - return true - end - - if button == 1 or button == 3 then - return true - end - elseif titleRect == nil or not math_isInRect(x, y, titleRect[1], titleRect[2], titleRect[3], titleRect[4]) then - if release then - showOnceMore = true -- show once more because the guishader lags behind, though this will not fully fix it - show = false - end - return true + WG.changelog = {} + WG.changelog.toggle = function(state) + if state ~= nil then + show = state + else + show = not show end - end -end - -function widget:Initialize() - widget:ViewResize() - if changelogFile then - WG.changelog = {} - WG.changelog.toggle = function(state) - if state ~= nil then - show = state - else - show = not show + if show then + lastviewedHash = changelogFileHash + if changelogFileLength > lastviewedChangelogLength then + lastviewedChangelogLength = changelogFileLength end - if show then - lastviewedHash = changelogFileHash - if changelogFileLength > lastviewedChangelogLength then - lastviewedChangelogLength = changelogFileLength - end - end - end - WG.changelog.isvisible = function() - return show - end - WG.changelog.haschanges = function() - return lastviewedHash ~= changelogFileHash and lastviewedChangelogLength < changelogFileLength end + end + WG.changelog.isvisible = function() + return show + end + WG.changelog.haschanges = function() + return lastviewedHash ~= changelogFileHash and lastviewedChangelogLength < changelogFileLength + end - -- somehow there are a few characters added at the start that we need to remove - --changelogFile = string.sub(changelogFile, 4) - - -- store changelog into array - changelogLines = string.lines(changelogFile) - - local versionKey = 0 - local insertedLatest = false - for i, line in ipairs(changelogLines) do - --if insertedLatest == false or string.find(line, '^(%d*%d.?%d+ [/-]> %d*%d.[0-9]0)$') or string.find(line, '^(%d*%d.?%d+ [/-]> %d*%d.[0-9])$') then - if insertedLatest == false or string.match(line, "^# ") then - --if string.find(line, '^(%d*%d.?%d+ [/-]> )') then - versionKey = versionKey + 1 - versions[versionKey] = i - insertedLatest = true - end - totalChangelogLines = i + -- store changelog into array + changelogLines = string.lines(changelogFile) + + -- The first line always heads the column, so the latest entry is reachable even + -- when the file does not open with a heading. + local versionKey = 0 + for i, line in ipairs(changelogLines) do + if versionKey == 0 or string.match(line, "^# ") then + versionKey = versionKey + 1 + versions[versionKey] = i + versionLabels[versionKey] = string.match(line, "^# (.*)") or line end - widget:ViewResize() - else - Spring.Echo("Changelog: couldn't load the changelog file") - widgetHandler:RemoveWidget() end + selectedIdx = versionAt(startLine) + + widget:ViewResize() end function widget:Shutdown() - if changelogList then - glDeleteList(changelogList) - changelogList = nil - end - if WG.guishader then - WG.guishader.RemoveDlist("changelog") - end - if backgroundGuishader ~= nil then - glDeleteList(backgroundGuishader) - end + dropLists() + deleteGuishader() end function widget:LanguageChanged() diff --git a/luaui/Widgets/gui_options.lua b/luaui/Widgets/gui_options.lua index f4be39891c6..c2cc1ddb403 100644 --- a/luaui/Widgets/gui_options.lua +++ b/luaui/Widgets/gui_options.lua @@ -132,8 +132,8 @@ local pauseGameWhenSingleplayerExecuted = false local backwardTex = ":l:LuaUI/Images/backward.dds" local forwardTex = ":l:LuaUI/Images/forward.dds" -local screenHeightOrg = 520 -local screenWidthOrg = 1050 +local screenHeightOrg = 550 +local screenWidthOrg = 1100 local screenHeight = screenHeightOrg local screenWidth = screenWidthOrg From a58a54967b41496277e865f45f4ae5dde14ef726 Mon Sep 17 00:00:00 2001 From: Floris Date: Thu, 10 Sep 2026 19:24:16 +0200 Subject: [PATCH 2/2] changelog: use .md file instead of .txt (#9192) --- .github/agents.md | 5 +- changelog.md | 713 ++++++++++++ changelog.txt | 748 ------------ luaui/Include/markdown.lua | 1575 ++++++++++++++++++++++++++ luaui/Widgets/gui_changelog_info.lua | 277 +++-- luaui/Widgets/logo_adjuster.lua | 39 +- 6 files changed, 2440 insertions(+), 917 deletions(-) create mode 100644 changelog.md delete mode 100644 changelog.txt create mode 100644 luaui/Include/markdown.lua diff --git a/.github/agents.md b/.github/agents.md index 74ec7664101..63dce052e77 100644 --- a/.github/agents.md +++ b/.github/agents.md @@ -184,8 +184,9 @@ caused them. (`.github/PULL_REQUEST_GUIDELINES.md`). - Fill in the "Test steps" checklist in `.github/PULL_REQUEST_TEMPLATE.md`, and attach before/after media for visible changes. -- Player-visible balance and gameplay changes get a `changelog.txt` entry under the current `# Month` heading, in the - existing style: `• [Unit] 1500 -> 1400 health`. Internal refactors and tooling changes do not. +- Player-visible balance and gameplay changes get a `changelog.md` entry under the current `# Month` heading, in the + existing style: `- [Unit] 1500 -> 1400 health`, with sub-points as nested list items. The file is Markdown and is + rendered in-game by `gui_changelog_info.lua`. Internal refactors and tooling changes do not. - Style expectations beyond this file live in `CONTRIBUTING.md` (engine-call overhead, caching Defs lookups, correct iterators, comments explain "why" not "what", no dead code). diff --git a/changelog.md b/changelog.md new file mode 100644 index 00000000000..e9fc7af365b --- /dev/null +++ b/changelog.md @@ -0,0 +1,713 @@ +# September +- [Scavenger Zombies] + - After 15 minutes into the game, zombies will swarm all teams evenly once they reach 10% of the value of all players combined. + - Zombies spawn with XP skewed to the minimum veterancy so they aren't so tanky so often. + - When zombie revive timer has been reset, a purple poof now appears above it. + - Units that don't leave corpses like the Fiend will no longer respawn as zombies. + - Zombie constructors get a boosted capture range of a minimum of 300. This makes them capable of capturing aircraft. + - Zombies now can control aircraft when they're captured or produced. + +# August +- [Spectre] 12500 -> 9000 energycost, 165 -> 150 metalcost, 380 -> 450 health +- [T1, Seaplane Air Constructors] -35% energycost, -35% buildtime, -35% speed +- [LRPCs] 1100 -> 900 weaponvelocity +- [Nukes] 1.3x base damage, 0.45 -> 0 edgeeffectiveness -> More damage dealt at the center of explosion, less damage on the edges of the explosion +- [Sumo] 6500 -> 6000 health +- [Mammoth] 22.5 -> 23 speed +- [Sheldon] 50.4 -> 50 speed +- [Floating AA turrets] Stats made to match their land counterparts +- [Legion changes] + - Basic mex now similar stats as other T1 mexes, T1.5 mex removed + - Wind generator 45m -> 43m cost + - Solar 155m -> 150m cost + - Goblin 25m, 500e -> 30m, 420e cost + - Satyr 400e -> 500e cost, 1100 -> 1220 buildtime, can no longer fire vertically + - Phalanx 50 -> 44 speed + - Alaris weapon switched from gauss to a shotgun with the same range and a 10% higher dps. Unit should feel more responsive overall with a slightly higher acceleration, turnrate, turret turnrate, and weapon projectile speed. Unit speed reduced 102->99. + - Helios 69 -> 75 speed, 330 -> 320 range, 400 -> 370 turnrate + - Lance 3800e -> 3600e cost + - Prometheus friendly fire significantly reduced + - Inferno firing pattern changed to sector fire so its shots will have much less vertical spread, 1100 -> 1200 range + - Perdition stockpile time 50s -> 40s + - Rhapsis (T1 Medium AA Tower) 156 -> 180 DPS, 840 -> 950 range + - Pluto (T2 Microflak Tower) and Fulmen (Naval Microflak Tower) 800 -> 875 range + +# July +- [Stout, Brute] +10% buildtime, 1.1667s -> 1.2s reloadtime, 330 -> 350 sightdistance +- [Rover] 1000 -> 1100 buildtime +- [Pounder] 1500 -> 1400 health +- [Grunt] 500 -> 520 sightdistance, 42 -> 43 metalcost +- [Gunslinger] 1560 -> 1800 health, 49.5 -> 50 speed, 24 AoE added, 500 -> 600 weaponvelocity +- [Welder] 9500 -> 8000 buildtime, 2950 -> 3500 health, 47.4 -> 48 speed +- [Sumo] 15000 -> 12000 buildtime, 5940 -> 6500 health, 37.5 -> 38 speed, 0.16 -> 0.3 beamtime, 25 -> 55 dmg vs air +- [Turtle] 450 -> 600 weaponvelocity, Impulse added +- [Torpedo gunships] + - Puffin: 18000 -> 14000 buildtime, speed 271 -> 210, removed special damage vs commander + - Monsoon: 13000 -> 11000 buildtime, speed 266 -> 200, 5s -> 4s reloadtime +- [AA turrets] + - Flak turrets: 775 -> 850 range, 0.5333 -> 0.5 reloadtime + - Chainsaw / Eradicator: 1125 -> 1200 range + - Ferret: 840 -> 950 range, 176 -> 208 DPS, Stealth added, 360 -> 330 metalcost, 5800 -> 5000 buildtime, 1330 -> 1600 health + - SAM: 840 -> 950 range, 190 -> 225 DPS, 315 -> 350 metalcost, 6100 -> 5500 energycost, 5240 -> 4500 buildtime, 2800 -> 2500 health +- [T2 fighters] + - Highwind: +7% costs + - Nighthawk: +7% costs, 740 -> 690 range +- [Legion changes] + - Phobos health increased 800 -> 840, turnrate increased 720 -> 800 + - Karkinos cost reduced 330m2600E -> 310m2400E, heatray reloadtime reduced 2 -> 1.8s, shotgun range increased 240 -> 250 + - Dragon's Jaw no longer gains range with elevation + - Arquebus reloadtime increased 6 -> 7s, damage increased 750 -> 850 + - Thanatos reloadtime reduced 9 -> 8s + - Quickshot turret turn speed and weapon velocity increased + - Blindfold health reduced 890 -> 600, juno bomb aoe reduced 700 -> 500 + - Phoenix bugfix, it should no longer fire up into space + +# June +- [General] Hold ALT when upgrading metal extractors to ignore allied extractors. +- [All Builders] Idle mobile builders now auto-repair units within a range that depends on the movement state. +- [Commando] 1560 -> 1800 health, EMP resistance, weapon inaccuracy removed, weapon can target air, but does reduced dmg vs air (30%) +- [Legion changes] + - Phalanx cost increased 450m4750E -> 470m5000E + - Praetorian speed increased 63 -> 72, health reduced 25000 -> 22000, flak dps reduced 514 -> 385, missile battery changed from a burst of 3 -> 12 with smaller missiles (dps 100 -> 300), cluster cannon removed + - Aquilon (T2 aa bot) airlos increased 850 -> 1100, railgun range increased 1050->1150, reloadtime reduced 4s -> 3s (dps 56 -> 75), microflak range increased 650 -> 700, dps reduced 506 -> 300 + - Glaucus (scout hover) speed increased 96 -> 102 + - Thalassa (T2 cruiser) range increased 450 -> 500 + - Belcher sprayangle reduced 2500 -> 1500 + - Advanced solar buildtime 13580 -> 12500 + +# May +- [T1 bot constructors] +70 health +- [Pawn] Weapon 3 -> 2 dmg vs Air +- [Blitz] Weapon 3 -> 2 dmg vs Air, 750 -> 730 health +- [Rover] 950 -> 1000 buildtime +- [Stout] 75.9 -> 75 speed, 2900 -> 3100 buildtime, 1780 -> 1800 health +- [Brute] 72.9 -> 72 speed, 3310 -> 3500 buildtime, 1970 -> 2000 health +- [Pounder] 40.5 -> 40 speed, 3000 -> 3100 buildtime, 1490 -> 1500 health. Weapon projectile no longer overshoots its range, and doesn't gain extra range from elevation +- [Hound] 1280 -> 1200 health, 3.167 -> 3.3s reloadtime +- [Tiger] 70.5 -> 69 speed, 665 -> 690 metalcost +- [Archangel] 1.5s -> 1.3s reloadtime for longrange missile +- [Manticore] 1.6s -> 1.4s reloadtime for longrange missile +- [Razorback] -5% dps +- [Shiva] 48.3 -> 48 speed, 1550 -> 1600 metalcost +- [Stormbringer] 105 -> 110 damage, +5% m/e/bt +- [Skyhook] 200 -> 185 speed, 6400 -> 9000 energycost +- [Abductor] 225 -> 210 speed, 6600 -> 10000 energycost +- [Stronghold] 175 -> 160 speed, 11000 -> 13000 energycost +- [Battleships] Reduced damage vs subs +- [Cloaked Fusion] 3650 -> 3550 metalcost, 75e/s -> 50e/s cloakcost + +# April +- [Brakerate] + - Set to 90 elmos/s^2 for vehicles, if lower previously. + - Mostly helps slow artillery units to not skid forward, after they've reached their firing range. + - Set to 180 elmos/s^2 for bots, if lower previously. Only affects radar and jammer bots. +- [Sightdistance] + - Set to 330 for vehicles, if lower previously. + - Set to 380 for bots, if lower previously. Except crawling bombs, are kept at 260. + - Affects mostly cons and some artillery, plus notably Centurions, Fiends and rocket bots. +- [Legion changes] + - New modoption added (legionsimplifiedmexes) which rebalances T1 legion to use the same T1 mexes as arm/cor. Light T1 units are given a higher m cost and lower E cost and heavy T1 units are given a lower m cost and higher E cost + - Impulse removed from Goblin, Wheelie, and Hippocampus + - Wheelie cost reduced 25m370E->23m350E, range increased 160->168, bullet velocity increased to improve accuracy + - Phobos health increased 750->800 + - Barrage's napalm aoe reduced 75->60 + - Cacophony cost reduced 420m5500E->380m5500E, health reduced 2350->2200, dps reduced 270->255 + - Adv solar health increased 800->1100 + - Octeres (T1 Artillery ship) reloadtime reduced 11->10s, range increased 930->960 + - Decurion energycost increased 3000->3600 + - Lance reloadtime reduced 8->7.5s + - Arquebus cost reduced 800m16000E->750m15000E + - Javelin speed increased 65->68 + - Praetorian speed increased 60->63, acceleration and turnrate improved + +# March +- [Legion changes] + - Telchine (T2 amphib bot) script and targeting improvements, range increased 400->450 + - Incinerator firing E cost 500->300E/s + - Dolus (T2 radar/jammer ship) speed increased 36->42 + - Phoenix (T2 heatray bomber) movement behavior adjusted to reduce heatray range/damage exploits + +# February +- [Centurion] 330 -> 325 range +- [Hound] buildtime 6230 -> 6500 +- [Fatboy] energycost 15000 -> 20000, buildtime 28000 -> 32000 +- [Sprinter] range 220 -> 230 +- [Abductor] sightdistance 430 -> 520 +- [Minelayers] Transportable by basic transport +- [Mines] mincloakdistance 8 -> 30 +- [T2 radars] 820 -> 1000 sight, 355 -> 500 health +- [T1 radars] 90 -> 180 health +- [Castro] -13% m/e/bt cost +- [Lightning weapons,except Thor] Firing takes -10e for all, used to vary between -5e to -35e +- [Rez subs] + - Removed reclaimspeed reduction -> +20% faster reclaim. Matches its usual buildpower now + - Autoheal 2hp/s -> 5hp/s (lost its idleautoheal of 3hp/s after 10s) + - +16% energycost, metalcost +- [T1 AA ships, subs, and frigates] Autoheal removed +- Idleautoheal standardised to 5hp/s after 60 seconds without getting hit, for everything. + - Rezbots got a 5hp/s normal autoheal to replace their near-instant idleautoheal. +- [Legion changes] + - Carriers all start with half of their drones pre-built, with the cost of those drones added to the carrier + - T1 Drone health reduced 415->325, acceleration increased, drones retreat after taking 1 aa shot + - T2 Drone health reduced 2250->1650 + - Removed health scaling for drones when gaining xp + - Drones no longer have health decay while in the air when the carrier is alive but decay quickly once the carrier is dead + - T1 drones now have to return to carrier to reload after firing 12 shots + - Reduced overall range of drones and made them more tied to their actual ranges to prevent range extension abuse + +# January 2026 +- [Incisor] 0.767s -> 0.8s reloadtime, 85.5 -> 85 speed, 2200 -> 2300 buildtime, 1040 -> 1100 energycost +- [Blitz, Pawn] 500 -> 600 weaponvelocity +- [Vehicle scouts] +10% reloadtime +- [Banshee, Roughneck] 800 -> 1000 weaponvelocity, 16 -> 40 AoE +- [Hornet] Missile tracks properly +- [T1 bombers] Sprayangle removed, Stormbringer -5 speed +- [Sprinter] 171 -> 160 metalcost, 4140 -> 3800 energycost, 500 -> 600 weaponvelocity +- [Sheldon] 2200 -> 2800 energycost, 410 -> 400 metalcost +- [Fatboy] 6.7333s -> 7s reloadtime, 0.85 -> 0.15 edge effectiveness, 240 -> 300 AoE, 11000 -> 15000 energycost +- [Tzar] 3 -> 3.5 reloadtime, 40.5 -> 39 speed +- [Bull] 60 -> 62 speed +- [Sumo, Battleships] +10% health +- [Spybots] 17600 / 22200 -> 12000 buildtime, change overrides the buildtime formula for them +- [Hound, Gunslinger, Crawling Bombs] buildtime not changed by the formula below +- [Hover platforms] -80m, -750e, -800bt cheaper +- [T1 airplants] -60m, -300bt cheaper +- [T2 constructors] +15% buildpower +- [Seaplane constructors] T2 airplant added to buildlist +- [Construction turrets] + - Metal cost: 210 -> 230 + - Energy cost: 2600 -> 3200 (floating version) +- [Factory buildpower] + - All T2 factories: -300 metalcost, 1.5x buildtime. Except Cortex Vehicleplant only -200 metalcost. + - T2 factories (bots, vehicles, navy) buildpower: 300 -> 600 + - T2 airplants buildpower: 200 -> 600 + - Seaplanes buildpower: 200 -> 300 + - T3 gantry buildpower: 600 -> 1800 +- [Units from t2, t3 and seaplane factories] + - New buildtime = old buildtime * 1.1 + (metalcost * 60 + energycost) / 20 + - Roughly 30% for most units. Less for units with already high bp costs like air, more than that for fast-building units like most ships +- [Advanced geothermals] +50% buildtime +- [Cortex fusion] + - Metal cost: 4500 -> 3600 + - Energy cost: 26000 -> 22000 + - Buildtime: 75400 -> 59000 + - Energy generation: 1100 -> 850 + - Health: 5000 -> 4300 +- [Armada fusion] + - Metal cost: 4300 -> 3350 + - Energy cost: 21000 -> 18000 + - Buildtime: 70000 -> 54000 + - Energy generation: 1000 -> 750 + - Health: 4450 -> 3800 +- [Cloaked fusion] + - Metal cost: 4700 -> 3650 + - Energy cost: 26000 -> 22000 + - Buildtime: 84400 -> 65000 + - Energy generation: 1050 -> 750 + - Cloak cost: 100 -> 75 + - Health: 4450 -> 3800 +- [Decoy Fusion] + - Metalcost: 370 -> 270 + - Health: 5200 -> 3800 +- [Advanced solars] + - Energy generation: 75 -> 80 +- [Shield Rework] + - Shields block projectiles, preventing them from bouncing unpredictably and sometimes into the backline. + - Things inside the shield are protected from blocked projectiles AoE. + - When a shield is near 0 capacity, the last hit over-damages the shield, requiring it to recharge that amount of excess capacity usage before coming back online. + - In addition, there's a minimum down time. + - Projectile types blocked by shields unchanged. +- [Resurrection] Resurrected units regain their old XP. + +- [Legion changes] Updates relevant Legion unit stats to reflect Season 3 changes. Changelog is as follows: + - Cluster weapon damages and reloadtimes increased by ~30%, cluster secondary munition damage increased ~50% with lowered projectile counts + - Napalm weapon leadlimits set to 0, meaning they will always fire at the current location of its target instead of its predicted location + - Commander aa weapon reduced to 300 range + - Medusa tracking reduced to make retargeting weaker + - Martyr speed nerfed to 220 from 230, turnrate nerfed to 750 from 800 + - Mosquito weapon AOE increased to 72 from 70, stockpile time reduced to 1.8 seconds from 2 seconds + - Spy bot became slightly cheaper and slower, buildtime reduced accordingly first to match other spybots + - Strider energy cost reduced to 5250 from 5400 + - Scylla 15% health buff in accordance with other battleships + - Prometheus speed increased to 52 from 51 + - Inferno reloadtime reduced to 7 seconds from 8 seconds + - Alaris energy cost increased to 850 from 800, buildtime increased to 1650 from 1600, reloadtime slightly increased to 2.3 seconds from 2.25 seconds + - Wheelie reloadtime increased by 10% in accordance with other scout vehicles + - Decreased Legion Advanced Solar Collector costs by 3% across the board + - Factory changes are identical to the other factions. Buildtime updates for units use the same formula as for other T2, T3, and seaplane units in the other two factions. + - Legion fusion: + - Metal cost: 4900 -> 4000 + - Energy cost: 27000 -> 25000 + - Buildtime: 80000 -> 66000 + - Energy generation: 1200 -> 950 + - Health: 5400 -> 4600 + +# December 2025 +- Unified maximum water depth for non-amphib land units to 22 (previously varied between 22-30) +- Unified minimum water depth for non-heavy ships to 8 (previously varied between 8-10) +- [Legion changes] + - T2 shipyard, ships, seaplanes, and naval structures added + - Praetorian shotgun spread reduced 1900->1400 + - Decurion range reduced 380->360, now deals 25% instead of 100% damage vs air + - Hippocampus (scout ship) now deals 25% instead of 50% damage vs air + - Small napalm blobs now deal 60dps and last 7s, previous was 45dps for 10s + - Inferno reloadtime 9s->8s + - Perdition stockpile time 70s->50s, stockpile cost reduced 500m17000E->350m14000E, impact damage 2000->1200 damage, napalm deals 120dps for 15s (3000 combined damage) + - Napalm damage cap increased 100->120dps + - Martyr damage vs commanders -25% -> -50% + - Syracusia (Destroyer) health reduced 4000->3800 + - Thalassa (Cruiser) health increased 5400->5600 + - Scylla (Battleship) health increased 8000->9000 + - Corinth (T2 artillery ship) cost increased 12000m115kE->13000m125kE, speed reduced 10% + - Ionia (T2 floating turret) mg range increased 650->700 + +# November 2025 +- [Legion changes] + - Karkinos cost increased, health increased, shotgun slightly higher dps with 2-round burst + - Telchine cost reduction 660m19000E->600m13200E, firing angle increased, speed in water increased 30% + - Triton speed reduced 60->55, range reduced 600->550, speed in water increased 30% + - T1 shipyard, ships, and naval structures added + - Iapetus (aa ship) cost 330m4800E->250m3600E, model scaled down 10%, fire rate reduced 15%, health reduced 20% + - Argonaut (frigate) tracking reduced + - Hippocampus (scout ship) speed increased 93->97, acceleration increased 4% + - Ketea (sub) cost reduced 340m2600E->320m2400E, speed increased 54->57, health reduced 640->600 + +# August 2025 +- [Aircraft] Vision raised to 430, if it was lower previously +- [T1 Bombers] Random inaccuracy removed from their bombs +- [T2 Transports] Skyhook 235 -> 200 speed, Abductor 241 -> 225 speed +- [Hound] 292 -> 340 weaponvelocity +- [Razorback] 58 -> 22 damage vs air +- [T2 AA bots] Sightdistance 925 -> 1200 +- [Flagships] Reduced damage vs submarines with main cannon +- [Grunt] 270 -> 280 health + +# July, 30 +- Gunslinger movement class changed to 3x3 and hitbox adjusted +- Sprinter movement class changed to 3x3 and colvol adjusted to cover the funit fully at all angles +- Welder movement class changed to 3x3 and footprint adjusted +- Bulls movement class changed to 4x4 and slight increase in crush damage +- Arm minelayer movement class changed to 3x3 + +# July 2025 +- [T1 Mex] +41% hp +- [Conbots] +15% hp +- [T1 turrets (not aa, not popups)] -10% buildtime +- [Commander] 25 -> 30 energymake +- [T1 factories] -150 metalcost, -250 energycost, -1500 buildtime, 100 -> 150 buildpower +- [Missile trucks] taller hitbox +- [Grunt] -2,5% costs +- [Blitz] 99 -> 101 speed +- [Starlight] 13500 -> 18500 energycost +- [Bull] 65.1 -> 60 speed +- [Jaguar] 320 -> 300 range, -10% dps (lightning) +- [Tzar] 22000 -> 28000 energycost +- [Sprinter] -10% costs +- [Termite] -10% costs, 48.3 -> 50 speed +- [Juggernaut] 33.6 -> 37 speed +- [T2 AA bots] + - New weapons. + - Longrange (1300) missile + flak for Cortex + - Longrange (1200) missile + shortrange missile for Armada + - ~Double health + - +30% energycost +- [Flagships] + - Reduced firerate on big gun, slightly increased damage per shot. Together, ~25% dps nerf + - Bigger AoE and Impulse on big gun, reduced edgeeffectiveness to 0.15 + - +17% costs + +# June 2025 +- [T2 flak turrets] increased footprint from 2x2 to 3x3 +- Warrior movement class from 2x2 to 3x3 (pawn to fido spacing) + +# June 2025 +- [Legion changes] + - 2 new T3 units added: Myrmidon T3 all-terrain mech, Charybdis T3 Hovertank + - New models added to replace the placeholder models left in T2: infestor, spybot, radar bot, jammer bot + - Phobos cost reduced 150m2400E -> 140m2200E + - Decurion buildtime increased 4000-4800 to slow down repair rate, energy cost increased 2700 -> 3000 + - Arquebus cost reduced 900m18000E -> 800m16000E + - Thanatos speed reduced 50 -> 45, turnrate reduced 750 -> 300 + - Triton minigun dps reduced 135 -> 105 + - Medusa range increased 950 -> 1000 + - Keres health reduced 23000 -> 21000 + - Praetorian shotgun fires 10% faster + - Daedalus range increased 900 -> 950 + - Starfall cost increased 58000m660000E -> 63000m720000E, reloadtime increased 15s->18s, lowered damage vs shields + +# May 2025 +- [Legion changes] + - 2 new units added: Aquilon T2 aa bot, Chimera T2 pop-up turret + - Cluster plasma was rebalanced and is overall stronger than before + - Wildfire removed from T2 air lab, Skuttle removed from t2 bot lab, Behemoth removed from T3 gantry + - Script improvements to allow several units to reliably fire while turning: Alaris, Decurion, Quickshot, Phobos + - Mosquito stockpile count reduced 8->4, stockpile rate increased slightly + - Gladiator projectile speed increased 320->360 to improve accuracy + - Lance cost reduced 260->240m, speed increased slightly +- [Tiger, Turtle, Sumo] Mass 750, to make them transportable by light transports. +- [Spybot] Emp immune again. Max stuntime vs units 8 -> 10 +- [Banisher] Energycost 17000 -> 23000, Turnrate 300 -> 250, Acceleration 0.2269 -> 0.2 +- [Tzar] Buildcost 26100 -> 30000 +- [Lava]: Units in lava slow down up to 5x depending on their submersion level + +# April 2025 +- [Pawn] 1420 -> 1650 buildtime +- [Pawn] 52m 870e -> 54m 900e +- [Grunt] 210 -> 215 range +- [Spybots] Can paralyze buildings up to 20s +- [Antinuke buildings] Health 3650 -> 3300 (facilitating a full-length stun from spybot) +- [Shuriken + Abductor] Regain targetmoveerror, revert Shuriken reloadtime 1.3s -> 1.2s + +# March 2025 +- [Armada solar] Buildtime 2800 -> 2600 +- [Armada wind] Metalcost 37 -> 40 +- [Cortex wind] Metalcost 45 -> 43, Health 199 -> 220 +- [Armada tidal] Energycost 250 -> 200 +- [Armada asolar] Metalcost 370 -> 350 +- [Cortex cons] +5% BP +- [Exploiter] Buildtime 2720 -> 2900 +- [T2 radars] Metalcost 560 -> 400, Energycost 19000 -> 14000, Buildtime 11800 -> 8000 +- [Sneaky Pete] Cloaking removed +- [Jammer ships] Costs +130%, Health +130%, Speed 61 -> 40 +- [Spybots] Cloaking cost 100/50 -> 40/15, EMP immunity removed, Attack command for self-destruct added, Damage 56000 -> 5000, Paralyzetime 35/20 -> 8s +- [Banisher] Can now fire in all directions, Sightdistance 550 -> 650, Energycost 23000 -> 17000, Improved aim/tracking +- [Tremor] Damage 150 -> 200, AoE 200 -> 210, Weapon hits a wider area +- [Poison Arrow] Energycost 29000 -> 21000, Buildtime 22200 -> 19000, Sightdistance 385 -> 500 +- [Turtle] Sightdistance 372 -> 500 +- [Garpike/Pincer] Sightdistance 305 -> 500, Faster turning turrets +- [Hound] Lost gauss weapon switch, Metalcost 300 -> 285 +- [Recluse] Range 600 -> 575, Speed 52 -> 47 +- [Grunt] Metalcost 36 --> 43, Energycost 880 -> 840, Range 230 -> 210, Turnrate 1391 -> 1200 +- [Tick] Metalcost 17 -> 21, Energycost 340 -> 300, Health 61 -> 60 +- [Pawn] Metalcost 48 -> 52, Energycost 960 -> 870, Health 335 -> 370 +- [Rover] Health 89 -> 105 +- [Rascal] Health 75 -> 90 +- [Blitz] Health 690 -> 750, Ellipsoid hitbox, Sightrange 299 -> 350 +- [Incisor] Buildtime 1761 -> 2200, Sightrange 273 -> 330 +- [Shuriken] Reloadtime 1.2s -> 1.3s +- [Stormbringer] Drops bombs 17% closer together +- [Banshee] Health 485 -> 560, Turnrate increased, New weapon graphics with functionally similar stats +- Inaccuracy removed from all T1 laser weapons + +# January 2025 +- [Artillery] The High/Low Trajectory toggle has been removed and been automated. Both modes now share the same damage and AOE. Aiming low is preferred, but if there's no low trajectory targets in range or the manually selected target cannot be shot with low trajectory, high trajectory will be used instead for a short time. + - The following units are affected: + - [Gauntlet] (T1 Plasma Artillery Turret) + - [Agitator] (T1 Plasma Artillery Turret) + - [Rattlesnake] (T2 Plasma Popup Artillery) + - [Persecutor] (T2 Plasma Popup Artillery) + - [Vanguard] (T3 Mobile All-Terrain Artillery) +- [Angler] The T2 Cortex torpedo bomber now drops 1 large torpedo instead of 3 small ones. The large torpedo is slightly slower but has some aoe +- [Serpent] The Arm T2 battlesub now fires 2 medium torpedoes at once instead of 1 large one, overall dps unchanged but some aoe is added +- [Kraken] The Cor T2 battlesub's torpedo aoe increased + +# December 2024 +- [Impulse] Nukes + Plasma Cannons + Shiva + Catapult + Vanguard + Tzar + Fatboy + Banisher + Poison Arrow + Gunslinger + Ballistic Missile Launchers got impulse added to their weapon. High-HP units with low mass got a mass increase. +- [Tremor] Impulse 140% -> 80%, EdgeEffectiveness 90% -> 15% +- [Calamity + Raghnarok] +10% E/M cost, energycost and damage of individual shot increased, reloadtime increased (same dps as before), AoE lowered +- [Light Mine] Cloakcost 0.5 -> 1e/s, metalcost 5 -> 7, buildtime 50 -> 100 +- [Medium Mine] Cloakcost 1 -> 2e/s, metalcost 16 -> 25, buildtime 100 -> 300, EdgeEffectiveness 0.7 -> 0.5, Impulse 1 -> 0.8 +- [Heavy Mine] Cloakcost 1.5 -> 6e/s, metalcost 21 -> 50, buildtime 125 -> 700, EdgeEffectiveness 0.7 -> 0.5, Impulse 1 -> 0.8, damage 1390 -> 3000, AoE 300 -> 330 +- [Crawling Bombs] Use bigger (self-d) explosion on attack command. Dying to enemy fire still results in small blast (including while being transported). Transportable. +- [Crawling Bombs death explosions] AoE 432 -> 400, damage 3350 -> 2700, damage vs Crawlling Bombs 220 -> 400 (chain easier) +- [Roach] Speed 81 -> 76 +- [Skuttle] Metalcost 540 -> 755, energycost 26 000 -> 27 000, cloakcost 150 -> 15, cloakcost moving 400 -> 40 +- [Destroyers] Depthcharge turnrate increased -> hits crawling bombs reliably + +# October 2024 +- [All units] EMP resist for units is standardized, and units that had low emp resists now take full emp damage. Units that had between 50-95% emp resist now all have 80% emp resist. Units that had 95+% emp resist are now fully emp immune. +- [New units] Heavy T1 air transport for arm (Osprey) and for cor (Hephastus) moved to the basegame. These transports can carry the same weight as T2 transports but at a lower movespeed, and are able to transport the commander. Osprey costs 190m 4000E, has 110 speed, and 630 health. Hephaestus costs 190m 4000E, has 100 speed, and 800 health. +- [Stork/Hercules] T1 light air transports no longer able to transport the commander, and can now only carry units below 750 metal. Energy/metal/buildcost reverted to earlier values (68m 1300E 3850bp for Stork and 74m 1450E 4120bp for Hercules) +- [Shuriken] emp damage per shot reduced from 800-600, weapon can no longer fire at air units +- [Abductor] emp damage per shot reduced 22500->10500, beam duration 0.5->0.2s (improves accuracy), stuntime reduced from 15->6s +- [Liche] weapon no longer has an extra impulse multiplier +- [Skyhook] T2 cor air transport speed increased 210->235, health increased 1830->2200. Line of sight increased 260->500 +- [Webber] No longer able to target air units + +# September 2024 +- [All units] Gravity standardized for all projectiles to 130, so weapons will behave the same way across all maps. +- [Rocketeer + Aggravator] Reverted the last 2 balance changes: damage per shot 173->157, reloadtime 4->3.8s, speed/acceleration 5% higher +- [Lazarus + Graverobber] Metal cost increased 110->130, E cost unchanged, buildtime increased 2400->2800 +- [Mace] Health increased 900->1000 +- [Thug] Health increased 1000->1100, buildtime increased from 1970->2100 so it doesn't get repaired too quickly +- [Tremor] Reloadtime reverted to 0.5s, impulse reduced 30% +- [Vanguard] Range increased 1325->1450, health reduced 10000->8500. Tremor and Vanguard's higher ranges now let them outrange long range defences (Rattlesnake, Persecutor, Pulsar) + +# August 2024 +- [Tremor] firemodes unified to remove the toggle, and effective DPS in target area increased. Wasp bugfixed, and now has mild tracking on rockets, so will miss shots less often. +- [Dragon Claw/Maw] Collision volume and aim position will now change when the units open up, so it can be targeted when behind a wall + +# July 2024 +- [Minelayers] Minelayers now live up to the 'minesweeper' role properly, being able to detect enemy mines within 450 radius, and slowly clear them, with a moderate ranged weapon. +- [Naval Economy Buildings] Stats made equal to land versions. Specifically, T1 and T2 Energy Converters, Energy and Metal Storages, and the T2 mex have been harmonised for both Core and Arm. +- [Salamander] Energy cost increased 4775->7000 to make the unit more difficult to rush early on and put its cost more in line with other amphibs (energy being 20x metal cost). Range reduced from 360->340 to make it slightly more difficult to kite T1 units and commanders. +- [Whistler + Lasher] Ground range increased by 25 (525/550->550/575 range) +- [Centurion] Unit script improved so the unit should keep firing while turning +- [T2 Cruisers] Reloadtime of depthcharges reduced by 10% +- [T2 Lightning/Flamethrower Ships] Cost reduced 25%, health reduced 20%, speed increased 6%, dps reduced 20%. Flamethrower ship model size scaled down by 10% + +# June 2024 +- [Rocketeer + Aggravator] Speed and acceleration reduced by 5% +- [Whistler + Lasher] Changes from proposed units rework modoption moved to base game. {Whistler and Lasher weapon switches from ground and anti-air modes. Anti-air has 700 range and tracking missiles, while ground has 525/550 range and no tracking. Ground speeds increased by 10 (45->55, 42->52), ground dps increased by 40% (17->25)} +- [T1 Air Transports] E costs increased by 70%, buildtime increased by 20%. This change is to increase the time it takes to rush transports and share them to the team. +- [Herring] unit now switches between ground missiles and longer range tracking aa missiles (750 range). No change to ground missile stats. Unit now takes 50% damage from emp instead of 30%. +- [Dragon Claw/Maw] Damage taken while closed increased from 25%->33%. Dragon Claw range reduced 440->430 + +# March 2024 +- [Proposed Units Rework Modoption] Rework to Whistler and Lasher added to modoption, which has the units switch between longer range tracking aa missiles and non-tracking ground missiles. Mauser, Quaker, Stiletto changes removed from modoption. +- [Mauser + Quaker] Rework from proposed_unit_reworks modoption moved to main game with a few additional changes. This rework aims to give these units a more mobile and aggressive role, which reduces the role overlap with the heavier T2 veh artillery options. Their ranges are reduced by 120, speed increased by 20%, acceleration increased 50%, health increased 20%, and accuracy is improved. +- [Stiletto] Rework from proposed_unit_reworks modoption moved to main game. This rework aims to give the stiletto a more specialized role for disabling specific targets while being less efficient as a defensive option against groups. The unit's metal and energy costs are doubled, buildpower cost increased 50%, health increased by 30%, paralyze time increased from 10s->20s, bomb count reduced from 5->3, aoe reduced from 240->200, emp damage per bomb increased 4000->6000 +- [Banshee] Reloadtime reduced by 10% (DPS 34->38) +- [Dragon Claw] DPS reduced by 14% (185->159) +- [Razorback] Laser damage vs air reduced from 75% to 50% +- [Demon] Health reduced from 20000->18000 +- [Crocodile and Cayman (T1 hovertanks)] Crocodile cost reduced 290m2600E->270m2400E, Cayman cost reduced 320m3300E->300m3100E +- [Salamander] EMP resist reduced from 95%->90% +- [Epoch and Black Hydra] Projectiles for main cannons increased in size, damage, and aoe, with a reduced fire rate + +# February 2024 +- [Grunt] Speed reduced from 84->81 +- [Pawn] Speed increased from 84->87 +- [Demon] Buildtime cost increased from 90000->120000 +- [Thug/Mace] movement footprint increased to prevent them blocking each other's turrets when attacking +- [New Units] Demon (T3 cortex flamethrower mech), Salamander (T2 cortex amphibious tank, replacing Alligator) moved from release candidate modoption into the base game. Dragon rework (new model, flamethrower, light aa) moved to the base game. Flamethrower ship and Lightning ship moved from expandedT2sea modoption into the base game +- [Dragon] Fire rate of anti-air weapon reduced by half +- [Demon] DPS reduced from 2000->1600, unit no longer has amphibious +- [Lightning ship and Flamethrower ship] Light aa added so they can protect themselves from light air while raiding + +# January 2024 +- [Turtle] Added a weak anti-air turret so that an amphibious turtle attack is more difficult to counter +- [T1 subs] Eel (arm sub) speed increased 63->66, Orca (cor sub) speed reduced 60->57 +- [T1 frigates] Ellysaw (arm frigate) cost reduced 390m2600E->380m2550E, Riptide (cor frigate) cost increased 410m2700E->420m2800E +- [Mauser] Cost increased from 270m4100E -> 320m4900E +- [Quaker] Cost increased from 360m4000E -> 400m4400E +- [Supporter] Line of sight increased 500 -> 600 +- [Skater and Herring] 1000 range radar added to both units. The goal is to give players better intel surrounding their fleets when playing t1 sea, where the short range radar can help to create a middle ground between fighting blind and having full vision. + +# November 2023 +- [Release Candidate Modoption] Dragon rework is added to the list of units in the release candidates modoption. This updates the dragon model and changes its weapons. An anti-air turret is added so that the unit can deal with small amounts of T1 fighters, the front weapon is changed from a laser to a flamethrower, and the side turrets have been split from 2 medium to 4 smaller laser turrets. +- [Consul] Recluse removed from build list, Webber added to build list. +- [Air units] Armorclasses merged for all air units, meaning that fighters and bombers will take the same amount of damage as gunships. + - T1 fighters will kill other T1 fighters in 2 shots instead of 1 + - T1 fighters will kill T2 fighters in 3 shots instead of 2 + - Flak will kill T2 fighters in 2 shots instead of 1 + - To make up for the extra durability, the cost of T2 fighters is increased + - Nighthawk (Cor T2 fighter) cost increased from 105m3700E -> 135m4750E, and weapon damage set to 500/shot + - Highwind (Arm T2 fighter) cost increased from 120m4900E -> 140m5700E, and weapon damage set to 750/shot + - The commander no longer deals extra damage against bombers + - Chainsaw and Eradicator damage per shot against fighters and gunships increased to match with previous damage against bombers +- [Quaker] Reworked to be larger with a heavier weapon, so that the T2 veh artillery has a similar asymmetry to the T1 veh artillery. Cost changed from 280m 3300E -> 360m 4000E, health increased 830 -> 1000, max speed reduced 54 -> 48, Damage per shot increased 300 -> 420, aoe increased 129 -> 144, reloadtime increased 4.25s -> 5s +- [Omen] Cor T2 radar vehicle's movement speed increased 36 -> 48 to be closer to arm T2 radar vehicle (57 speed) +- [Crawling Bombs] Movement speed while underwater reduced to 2/3 speed on land +- [Consul] Hound removed from build list, Sprinter and Platypus added to build list +- [Twitcher] Termite added to build list +- [Termite] Cost reduced from 700m 12000E -> 600m 9000E +- [Turtle] Cost reduced from 750m 18000E -> 750m 15000E +- [T2 Amphib Tanks] Paralyze damage taken reduced to 25% +- [T2 Cruisers] Fires 1 depthcharge at a time instead of 2 round bursts, projectile sped up and tracking improved + +# October 2023 +- [Webber] Reclaim speed increased from 100->150. + +# August 2023 +- [Shellshocker] Reworked to be the lighter version of t1 veh artillery while cor remains the heavy version. Reloadtime decreased 6.1 -> 4.3, damage per shot decreased 260 -> 182, inaccuracy reduced to 50% current area, area of effect reduced to 50% current area (-30% radius). Model size and collision volume decreased by 10% +- [Wolverine] Cost increased from 155m 2300E -> 170m 2500E. Model size and collision volume increased by 15%. Max health 640->750, max velocity 51->48. +- [Rocketeer] Damage per shot increased 10% (157->173), reloadtime increased 5% (3.8->4) +- [Aggravator] Damage per shot increased 10% (157->173), reloadtime increased 5% (3.8->4) +- [Laser/Lightning Weapons] targetmovererror set to zero for T2+ units. + - Hitscan weapons no longer randomly miss small fast moving targets. + - Affected Armada Units: Platypus, Welder, Titan blue laser, Thor side EMP lasers, Starlight. + - Affected Cortex Units: Duck, Sumo, Mammoth, Termite, Behemoth red lasers, Juggernaut heat ray. + - This matches existing behavior of Jaguar, Razorback, Thor main cannon, and Cataphract. + +# July 2023 +- [Whistler] Range decreased from 600 -> 575 +- [Lasher] Damage per shot decreased 47 -> 43 +- [Shellshocker] Reloadtime increased 5.7s -> 6.1s +- [Wolverine] Reloadtime increased 6.6s -> 7.2s +- [Dolphin] Cost increased from 165m 1400E -> 175m 1500E +- [Herring] Cost decreased from 230m 1600E -> 210m 1400E +- [Corsair] Depthcharge reloadtime decreased 2.24->2s +- [Oppressor] Depthcharge reloadtime decreased 2->1.9s +- [Tremor] The weapon's spread is now proportional to the distance between the tremor and its target. Fire rate reduced from 3->2 shots per second, damage per shot increased 100->150. +- [Veh Cons] Brake rates significantly increased, to prevent drifting into blueprints. + +# June 2023 +- [Shiva] Weapon reloadtime reduced by 33% (3s->2s), damage per shot reduced 33% (900->600), turret turnrate increased. Script and hitbox adjusted to reduce friendly fire. +- [Karganeth] Cost increased by 50% (1650->2500m), damage per shot increased by 50% (120->180 per shot, dps 400->600), health increased by 25% (10000->12500), model size increased slightly. Missiles automatically retarget midflight after current target is destroyed. Goal is to differentiate Karganeth and Shiva by increasing Karganeth's size and making it better at fighting single targets units while Shiva is better at dealing with large amounts of small units. +- [Tremor] Weapon reworked, now has a higher aoe, firerate, and impulse but lower damage per shot. The weapon is now stronger against groups of small units while significantly changing its strength against large units. +- [Incisor] Hitbox adjusted so incisors will fire more effectively in close formations, but will still obstruct each other's fire in large groups. + +### Bugfixes +- [Mines and Fiends] Fiends can now attack and damage mines. +- [Longbow and Messenger] Fixed rare case of door animation getting stuck and being unable to attack. +- [Eel] Shift aimpoint of Eel so it no longer barely outranges torpedo launchers. +- [Pitbull] Pitbull no longer blocked by T1 walls, it will correctly deploy and fire over T1 walls. +- [Resurrection] Fixed bug where units could sometimes be resurrected at full health instead of at 5% health. + +# May 2023 +[Commander] default enabled modoption "comupdate": +- Now immune to the D-Gun. +- Health 3350->4000. +Removed passive health regeneration. +- Wreckage metal 2000->1250. +- Death explosion inflict less damage. +- No longer susceptible to special damage modifiers from certain units, such as Light Laser Towers. + +### Bugfixes +- [Viper] No longer closed and armored when under construction. +- [Pitbull] Pitbull now closes into armored position after construction. +- [Wrecks and Heaps] Pincer got a heap. Duck got a wreck. Karganeth got a heap. Garpike got a heap. Wolverine got a heap. +- [Recluse] Recluse got new cruise missile behavior to drastically reduce incidences of friendly fire on cliff corners. +- [Gunslinger] Minor unit script edit to reduce friendly fire incidents. +- [Mobile Jammers] Now correctly turn back on after being damaged or stunned, and correctly turn off when stunned. +- [Air Factories] Fixed bug where it thought it had an aircraft on the buildpad and would stop producing units. +- [Nukes] Animation is now interruptible if a nuke was not fired, so an accidentally dropped target does not lock the silo into a long animation. Animation time standardized to 8.5 second door open time, and 30 second door close time. +- [RFLRPC] Ragnarok now correctly has a 0.4 reload time at 0 XP. Both Ragnarok and Calamity have animations tied to XP gain, so they properly animate faster and gain firerate increases with XP gains. + +### Other +- [Supporter] Reloadtime reduced from 1s->0.93s +- [Roughneck] Projectile speed increased 450->800 +- [Viper] Animation tweaks. Retains heading when closing up. 6 frames (0.2 sec) added to deploy animation (total of 37 frames (1.23 seconds), to match pitbull time-to-first-shot. Time to close into armored state after going idle now set to 3 seconds, to match pitbull. + +# April 2023 +- [Grunt] Range reduced from 240->230 and health reduced from 290->270 +- [Pawn] Damage per shot reduced from 10->9 +- [Tick] Metal cost increased from 15->17 +- [T1 Vehicle Scouts] Damage per shot increased from 30->35 +- Targeting improvements to tiger tank and turtle tank + +# March 2023 +- [T3 Hovertanks] Depthcharges added and main turrets can no longer target underwater units +- [Thor] Speed nerf 60->54, spark forkdamage nerf 0.5->0.25 (now deals 750 damage as aoe instead of 1500) +- [Commanders] can no longer capture allied units +- [Seaplane Platforms] added sonar distance for armada: 600, cortex: 800 +- [factories] tiny radar ranges removed + added large ranges for t1 air: 500, t2 air: 1000, armada seaplane: 750 + +# February 2023 +- [missiles] antinukes/nukes/tacnukes/empmissiles no longer collide into enemy (air) units +- [Titan/Juggernaut] added foot stomp "weapon" +- [Advanced Exploiter] General fixing of cormexp behavior. Consistent rocket salvo of 5. No longer less armored when closed. Proper decoy for moho. Continues extracting metal when deployed and attacking. + +# January 2023 +- [Twin Guard and Beamer] Added 5 range to make sure that they can't be outranged by T1 rocket bots when firing at a different elevation. +- [Thor] EMP rocket now launches immediately. To compensate, overall missile flight time increased by 2.5 seconds. Model animation now reveals to opponent how many EMP missiles are loaded. +- [vehicle scouts] accuracies and turnrate increased, damage reduced slightly. Cortex more maneuverable but slightly slower than armada, with reduced health +- [Lightning Weapons] Increased consistency of fork damage. Lightning chaining now always occurs at the lighting bolt end point, instead of only when a unit is damaged. Removed double application of flanking damage multiplier. +- [Flanking Damage] Flanking damage changed from min=0.9, max=1.9 to min=1.0, max=2.0. Approximately 5% nerf to overall bonus damage from flanking. All damage deals full 100% frontal damage instead of 90% frontal damage. All unit health increased by 11% to compensate. + - Consequences include: + - Wrecks are 11% more durable. + - Autoheal is 11% nerfed. + - Impulse of weapons increased by 11%. + +# December 2022 +- [Dragon Maw/Claw] Units now remember the location of revealed Dragon Maws/Claws even if fog-of-war covers them up. +- [Marauder] Change from tank movement to bot movement. Script fixes and torso turn rate buff to allow attacking while moving. Add 0.25 sec deploy animation to marauder AA cannons. +- [catalyst] Range -18% Nerf (2750 -> 2250), Area of Effect -26% Nerf (512 -> 380), Damage +60% Buff (2500 -> 4000) + +### Bugfixes +- [artillery] Script changes to prevent units from aiming at "illegal" out-of-firing-arc headings. + +# October 2022 +- [Warden] AimFrom point adjusted to center of unit, so the HLT cannot shoot outside of its range circle. +- [Thor] Range reduced 640->540, damage per tick reduced 320->300. Lightning chaining buffed, which better matches original intended behavior (forkdamage 0.33->0.5, maxunits 2->5, range 60->125). Overall script changes to make the commandfire EMP rocket, main tesla cannon, and side EMP turrets no longer interfere with each other. EMP rocket now takes starburst trajectory with a 3 second command delay. + +# August 2022 +- [Beamer] Script fix for continuous laser buffed effective DPS by 25%. + +# August 2022 +- [Torpedo gunships] weapon changed to match up with other torpedoes (damage and speed), accuracy and targeting improved. + +# May 2022 +- [Sniper] speed: 30->33 + +# March 2022 +- [Sabre] reloadtime 12% slower, range -11% +- [Stiletto] slower turnrate, 15 -> 10 sec paralyze duration, lower flight altitude + +# January 2022 +- [EMP-spider] health 850 -> 1000, increased turret turn speed +- [T1 walls] Removed energy cost + land walls: reduced metal cost 11 -> 8 +- [T2 walls] Halved energy cost (~1000 -> 500) + rounded up metal cost 38/39 -> 40 +- [Thor] EMP missile weapon is by manual fire only + +# December 2021 +- [Fido] cost: 270m->300m, 5600->6300E +- [Sniper] speed: 35.4->30 +- [Zeus] unnerfed: dps 200->220 +- [Fatboy] cost: 1500->1400m, 12000->11000E +- [Maverick] cost: 700->650m, 12000->11000E + range increases with experience 3x faster +- [Recluse] health increased (1050->1250) and weapon range increased (550->600) Burst 4->3 (no dps loss) +- [EMP-spider] now able to reclaim (100 buildpower), cost: 175->250m, 3400->5000E + +# 2019 - 2021 + +### General +- Enabled ground deformation +- Wreckage/heaps have the same HP as regular unit HP (~33% increase) +- Increased maxslope for all units 50% (the slope it allows to be build on) +- Units gain xp (ranks) faster, also relatively more health/firerate compared the the same old xp +- Removed the energy/metal make/use/storage from most non eco units + +### Units +- Commander wreckage is now 2000 metal instead of 2500 +- Commander has 500 metal/energy storage (lose commander and default storage is also 500) +- Commander produces 2 metal (was 1.5) +- T1 metal extractors use 3 Energy to operate, also slightly reduced buildprice +- T2 metal extractors have 40% less metal storage (600) +- Ground scouts (vehicles/bot): 10% more LoS +- T1 vehicle artillery has faster projectile and does 2x more damage per shot, but also has 2x reloadtime. +- T2 vehicle artillery has 33% more health + same damage as t1 but with adjusted reload time to compensate +- Decreased cormaw damage 27% +- Arm Peewee damage reduced by 9% +- Core AK range reduced 240->225 and damage increased 6% +- Arm Warrior health and buildtime increased by 10% +- Arm Flash health increased by 5% and firing script improved +- Cor Instigator health increased by 2% +- Arm Stumpy turn rate reduced by 10% +- Cor Raider turn rate reduced by 10% +- Claw/Zeus: lightning chains again (up to 2 neighbouring units \*0.33) +- Zeus: does 10% less damage +- Juggernaut: main weapon does 4x old damage/reloadtime +- EMP launcher: reduced range 10% +- Spy paralyzetime 45 -> 35 sec +- Spy is much more resistant to emp damage +- T2 Bot/Air Constructors can build Advanced Fusion + cost: +60 metal +- T2 Vehicle/Air Constructors can make the T3 Experimental Gantry +- Arm Guardian: Removed special damage to ships and commander, reduced low trajectory aoe from 128 to 100, increased low trajectory default damage by 20%, increased high trajectory default damage by 9% +- Cor Punisher: Removed special damage to ships and commander, reduced low trajectory aoe from 140 to 120, increased low trajectory default damage by 20%, increased high trajectory default damage by 18%, increased reloadtime by 8% +- Arm Ambusher: Removed special damage to ships and commander +- Core Toaster: Removed special damage to ships and commander, low and high trajectory damage increased by 20%, high trajectory cooldown increased by 14% +- Arm Big Bertha: Increased E cost to fire from 3000 to 5000 +- Cor Intimidator: Increased E cost to fire from 3000 to 6000 +- Arm Vulcan: Reduced E cost to fire from 14500 to 10000 +- Cor Buzzsaw: Reduced E cost to fire from 15725 to 12000 + +### Air +- Banshee: changed weapon to machine gun with more accuracy, 5 burst (instead of 3) but little less average dps (-10%) +- Air Fighters can no longer attack ground +- T1 construction plane E cost -30% +- LLT, HLLT, BEAMER, RL, RAD now transportable with T2/Heavy Transporters +- Torpedo bombers: Added 800 range radar+sonar, +10% los, -13% max velocity, reduced flight altitude 25%, split main weapon into 3-round burst (500 damage each instead of 1500), Increased cost by 21% +- Torpedo gunships: Targeting/tracking improvements, +17% los, added 535 range sonar +- EMP bomber: 20% slower (still slightly faster than t1 fighters) + +### Sea +- Ship/Hover/Amphibious transports removed +- Arm and Cor corvette health +25, Cor corvette M cost reduced by 5 (150->145) +- Arm and Cor frigate damage, speed, and health increased +- Arm and Cor destroyer E cost increased 50% (7.5E/M instead of 5E/M), turret turnrates adjusted, and arm destroyer's reloadtime increased from 1.2 to 1.6 with its damage increased to keep the same dps +- Cor Battleship and Black Hydra laser dps increased by 15%, and minor improvements to their speed/health/cost +- T1 sub damage -30% +- T2 sub killer reworked: speed increased, range decreased, reloadtime decreased, damage per shot decreased +- T2 battle sub introduced: has a high cost, long range, high damage, long cooldown, and is slow +- Arm and Cor depthcharge turrets: +31% health, +13% range, increased build time +- Arm and Cor hovertank speeds increased (+5% arm, +10% core), cor hovertank health increased 6% + +### Renamed units +- Jeffy -> Ranger +- Pack0 -> Ferret +- Krogoth -> Korgoth +- Gaat Gun -> Warden + +### 10.24 (24/02/2019) +Balanced Annihilation 10.24, the game this has been based on. diff --git a/changelog.txt b/changelog.txt deleted file mode 100644 index dd61dabc5e3..00000000000 --- a/changelog.txt +++ /dev/null @@ -1,748 +0,0 @@ -# September -• [Scavenger Zombies] - - After 15 minutes into the game, zombies will swarm all teams evenly once they reach 10% of the value of all players combined. - - Zombies spawn with XP skewed to the minimum veterancy so they aren't so tanky so often. - - When zombie revive timer has been reset, a purple poof now appears above it. - - Units that don't leave corpses like the Fiend will no longer respawn as zombies. - - Zombie constructors get a boosted capture range of a minimum of 300. This makes them capable of capturing aircraft. - - Zombies now can control aircraft when they're captured or produced. - -# August -• [Spectre] 12500 -> 9000 energycost, 165 -> 150 metalcost, 380 -> 450 health -• [T1, Seaplane Air Constructors] -35% energycost, -35% buildtime, -35% speed -• [LRPCs] 1100 -> 900 weaponvelocity -• [Nukes] 1.3x base damage, 0.45 -> 0 edgeeffectiveness -> More damage dealt at the center of explosion, less damage on the edges of the explosion -• [Sumo] 6500 -> 6000 health -• [Mammoth] 22.5 -> 23 speed -• [Sheldon] 50.4 -> 50 speed -• [Floating AA turrets] Stats made to match their land counterparts -• [Legion changes] - - Basic mex now similar stats as other T1 mexes, T1.5 mex removed - - Wind generator 45m -> 43m cost - - Solar 155m -> 150m cost - - Goblin 25m, 500e -> 30m, 420e cost - - Satyr 400e -> 500e cost, 1100 -> 1220 buildtime, can no longer fire vertically - - Phalanx 50 -> 44 speed - - Alaris weapon switched from gauss to a shotgun with the same range and a 10% higher dps. Unit should feel more responsive overall with a slightly higher acceleration, turnrate, turret turnrate, and weapon projectile speed. Unit speed reduced 102->99. - - Helios 69 -> 75 speed, 330 -> 320 range, 400 -> 370 turnrate - - Lance 3800e -> 3600e cost - - Prometheus friendly fire significantly reduced - - Inferno firing pattern changed to sector fire so its shots will have much less vertical spread, 1100 -> 1200 range - - Perdition stockpile time 50s -> 40s - - Rhapsis (T1 Medium AA Tower) 156 -> 180 DPS, 840 -> 950 range - - Pluto (T2 Microflak Tower) and Fulmen (Naval Microflak Tower) 800 -> 875 range - -# July -• [Stout, Brute] +10% buildtime, 1.1667s -> 1.2s reloadtime, 330 -> 350 sightdistance -• [Rover] 1000 -> 1100 buildtime -• [Pounder] 1500 -> 1400 health -• [Grunt] 500 -> 520 sightdistance, 42 -> 43 metalcost -• [Gunslinger] 1560 -> 1800 health, 49.5 -> 50 speed, 24 AoE added, 500 -> 600 weaponvelocity -• [Welder] 9500 -> 8000 buildtime, 2950 -> 3500 health, 47.4 -> 48 speed -• [Sumo] 15000 -> 12000 buildtime, 5940 -> 6500 health, 37.5 -> 38 speed, 0.16 -> 0.3 beamtime, 25 -> 55 dmg vs air -• [Turtle] 450 -> 600 weaponvelocity, Impulse added -• [Torpedo gunships] - - Puffin: 18000 -> 14000 buildtime, speed 271 -> 210, removed special damage vs commander - - Monsoon: 13000 -> 11000 buildtime, speed 266 -> 200, 5s -> 4s reloadtime -• [AA turrets] - - Flak turrets: 775 -> 850 range, 0.5333 -> 0.5 reloadtime - - Chainsaw / Eradicator: 1125 -> 1200 range - - Ferret: 840 -> 950 range, 176 -> 208 DPS, Stealth added, 360 -> 330 metalcost, 5800 -> 5000 buildtime, 1330 -> 1600 health - - SAM: 840 -> 950 range, 190 -> 225 DPS, 315 -> 350 metalcost, 6100 -> 5500 energycost, 5240 -> 4500 buildtime, 2800 -> 2500 health -• [T2 fighters] - - Highwind: +7% costs - - Nighthawk: +7% costs, 740 -> 690 range -• [Legion changes] - - Phobos health increased 800 -> 840, turnrate increased 720 -> 800 - - Karkinos cost reduced 330m2600E -> 310m2400E, heatray reloadtime reduced 2 -> 1.8s, shotgun range increased 240 -> 250 - - Dragon's Jaw no longer gains range with elevation - - Arquebus reloadtime increased 6 -> 7s, damage increased 750 -> 850 - - Thanatos reloadtime reduced 9 -> 8s - - Quickshot turret turn speed and weapon velocity increased - - Blindfold health reduced 890 -> 600, juno bomb aoe reduced 700 -> 500 - - Phoenix bugfix, it should no longer fire up into space - -# June -• [General] Hold ALT when upgrading metal extractors to ignore allied extractors. -• [All Builders] Idle mobile builders now auto-repair units within a range that depends on the movement state. -• [Commando] 1560 -> 1800 health, EMP resistance, weapon inaccuracy removed, weapon can target air, but does reduced dmg vs air (30%) -• [Legion changes] - - Phalanx cost increased 450m4750E -> 470m5000E - - Praetorian speed increased 63 -> 72, health reduced 25000 -> 22000, flak dps reduced 514 -> 385, missile battery changed from a burst of 3 -> 12 with smaller missiles (dps 100 -> 300), cluster cannon removed - - Aquilon (T2 aa bot) airlos increased 850 -> 1100, railgun range increased 1050->1150, reloadtime reduced 4s -> 3s (dps 56 -> 75), microflak range increased 650 -> 700, dps reduced 506 -> 300 - - Glaucus (scout hover) speed increased 96 -> 102 - - Thalassa (T2 cruiser) range increased 450 -> 500 - - Belcher sprayangle reduced 2500 -> 1500 - - Advanced solar buildtime 13580 -> 12500 - -# May -• [T1 bot constructors] +70 health -• [Pawn] Weapon 3 -> 2 dmg vs Air -• [Blitz] Weapon 3 -> 2 dmg vs Air, 750 -> 730 health -• [Rover] 950 -> 1000 buildtime -• [Stout] 75.9 -> 75 speed, 2900 -> 3100 buildtime, 1780 -> 1800 health -• [Brute] 72.9 -> 72 speed, 3310 -> 3500 buildtime, 1970 -> 2000 health -• [Pounder] 40.5 -> 40 speed, 3000 -> 3100 buildtime, 1490 -> 1500 health. Weapon projectile no longer overshoots its range, and doesn't gain extra range from elevation -• [Hound] 1280 -> 1200 health, 3.167 -> 3.3s reloadtime -• [Tiger] 70.5 -> 69 speed, 665 -> 690 metalcost -• [Archangel] 1.5s -> 1.3s reloadtime for longrange missile -• [Manticore] 1.6s -> 1.4s reloadtime for longrange missile -• [Razorback] -5% dps -• [Shiva] 48.3 -> 48 speed, 1550 -> 1600 metalcost -• [Stormbringer] 105 -> 110 damage, +5% m/e/bt -• [Skyhook] 200 -> 185 speed, 6400 -> 9000 energycost -• [Abductor] 225 -> 210 speed, 6600 -> 10000 energycost -• [Stronghold] 175 -> 160 speed, 11000 -> 13000 energycost -• [Battleships] Reduced damage vs subs -• [Cloaked Fusion] 3650 -> 3550 metalcost, 75e/s -> 50e/s cloakcost - -# April -• [Brakerate] - - Set to 90 elmos/s^2 for vehicles, if lower previously. - - Mostly helps slow artillery units to not skid forward, after they've reached their firing range. - - Set to 180 elmos/s^2 for bots, if lower previously. Only affects radar and jammer bots. -• [Sightdistance] - - Set to 330 for vehicles, if lower previously. - - Set to 380 for bots, if lower previously. Except crawling bombs, are kept at 260. - - Affects mostly cons and some artillery, plus notably Centurions, Fiends and rocket bots. -• [Legion changes] - - New modoption added (legionsimplifiedmexes) which rebalances T1 legion to use the same T1 mexes as arm/cor. Light T1 units are given a higher m cost and lower E cost and heavy T1 units are given a lower m cost and higher E cost - - Impulse removed from Goblin, Wheelie, and Hippocampus - - Wheelie cost reduced 25m370E->23m350E, range increased 160->168, bullet velocity increased to improve accuracy - - Phobos health increased 750->800 - - Barrage's napalm aoe reduced 75->60 - - Cacophony cost reduced 420m5500E->380m5500E, health reduced 2350->2200, dps reduced 270->255 - - Adv solar health increased 800->1100 - - Octeres (T1 Artillery ship) reloadtime reduced 11->10s, range increased 930->960 - - Decurion energycost increased 3000->3600 - - Lance reloadtime reduced 8->7.5s - - Arquebus cost reduced 800m16000E->750m15000E - - Javelin speed increased 65->68 - - Praetorian speed increased 60->63, acceleration and turnrate improved - -# March -• [Legion changes] - - Telchine (T2 amphib bot) script and targeting improvements, range increased 400->450 - - Incinerator firing E cost 500->300E/s - - Dolus (T2 radar/jammer ship) speed increased 36->42 - - Phoenix (T2 heatray bomber) movement behavior adjusted to reduce heatray range/damage exploits - -# February -• [Centurion] 330 -> 325 range -• [Hound] buildtime 6230 -> 6500 -• [Fatboy] energycost 15000 -> 20000, buildtime 28000 -> 32000 -• [Sprinter] range 220 -> 230 -• [Abductor] sightdistance 430 -> 520 -• [Minelayers] Transportable by basic transport -• [Mines] mincloakdistance 8 -> 30 -• [T2 radars] 820 -> 1000 sight, 355 -> 500 health -• [T1 radars] 90 -> 180 health -• [Castro] -13% m/e/bt cost -• [Lightning weapons,except Thor] Firing takes -10e for all, used to vary between -5e to -35e -• [Rez subs] - - Removed reclaimspeed reduction -> +20% faster reclaim. Matches its usual buildpower now - - Autoheal 2hp/s -> 5hp/s (lost its idleautoheal of 3hp/s after 10s) - - +16% energycost, metalcost -• [T1 AA ships, subs, and frigates] Autoheal removed -• Idleautoheal standardised to 5hp/s after 60 seconds without getting hit, for everything. - - Rezbots got a 5hp/s normal autoheal to replace their near-instant idleautoheal. -• [Legion changes] - - Carriers all start with half of their drones pre-built, with the cost of those drones added to the carrier - - T1 Drone health reduced 415->325, acceleration increased, drones retreat after taking 1 aa shot - - T2 Drone health reduced 2250->1650 - - Removed health scaling for drones when gaining xp - - Drones no longer have health decay while in the air when the carrier is alive but decay quickly once the carrier is dead - - T1 drones now have to return to carrier to reload after firing 12 shots - - Reduced overall range of drones and made them more tied to their actual ranges to prevent range extension abuse - - -# January 2026 -• [Incisor] 0.767s -> 0.8s reloadtime, 85.5 -> 85 speed, 2200 -> 2300 buildtime, 1040 -> 1100 energycost -• [Blitz, Pawn] 500 -> 600 weaponvelocity -• [Vehicle scouts] +10% reloadtime -• [Banshee, Roughneck] 800 -> 1000 weaponvelocity, 16 -> 40 AoE -• [Hornet] Missile tracks properly -• [T1 bombers] Sprayangle removed, Stormbringer -5 speed -• [Sprinter] 171 -> 160 metalcost, 4140 -> 3800 energycost, 500 -> 600 weaponvelocity -• [Sheldon] 2200 -> 2800 energycost, 410 -> 400 metalcost -• [Fatboy] 6.7333s -> 7s reloadtime, 0.85 -> 0.15 edge effectiveness, 240 -> 300 AoE, 11000 -> 15000 energycost -• [Tzar] 3 -> 3.5 reloadtime, 40.5 -> 39 speed -• [Bull] 60 -> 62 speed -• [Sumo, Battleships] +10% health -• [Spybots] 17600 / 22200 -> 12000 buildtime, change overrides the buildtime formula for them -• [Hound, Gunslinger, Crawling Bombs] buildtime not changed by the formula below -• [Hover platforms] -80m, -750e, -800bt cheaper -• [T1 airplants] -60m, -300bt cheaper -• [T2 constructors] +15% buildpower -• [Seaplane constructors] T2 airplant added to buildlist -• [Construction turrets] - - Metal cost: 210 -> 230 - - Energy cost: 2600 -> 3200 (floating version) -• [Factory buildpower] - - All T2 factories: -300 metalcost, 1.5x buildtime. Except Cortex Vehicleplant only -200 metalcost. - - T2 factories (bots, vehicles, navy) buildpower: 300 -> 600 - - T2 airplants buildpower: 200 -> 600 - - Seaplanes buildpower: 200 -> 300 - - T3 gantry buildpower: 600 -> 1800 -• [Units from t2, t3 and seaplane factories] - - New buildtime = old buildtime * 1.1 + (metalcost * 60 + energycost) / 20 - - Roughly 30% for most units. Less for units with already high bp costs like air, more than that for fast-building units like most ships -• [Advanced geothermals] +50% buildtime -• [Cortex fusion] - - Metal cost: 4500 -> 3600 - - Energy cost: 26000 -> 22000 - - Buildtime: 75400 -> 59000 - - Energy generation: 1100 -> 850 - - Health: 5000 -> 4300 -• [Armada fusion] - - Metal cost: 4300 -> 3350 - - Energy cost: 21000 -> 18000 - - Buildtime: 70000 -> 54000 - - Energy generation: 1000 -> 750 - - Health: 4450 -> 3800 -• [Cloaked fusion] - - Metal cost: 4700 -> 3650 - - Energy cost: 26000 -> 22000 - - Buildtime: 84400 -> 65000 - - Energy generation: 1050 -> 750 - - Cloak cost: 100 -> 75 - - Health: 4450 -> 3800 -• [Decoy Fusion] - - Metalcost: 370 -> 270 - - Health: 5200 -> 3800 -• [Advanced solars] - - Energy generation: 75 -> 80 -• [Shield Rework] - - Shields block projectiles, preventing them from bouncing unpredictably and sometimes into the backline. - - Things inside the shield are protected from blocked projectiles AoE. - - When a shield is near 0 capacity, the last hit over-damages the shield, requiring it to recharge that amount of excess capacity usage before coming back online. - - In addition, there's a minimum down time. - - Projectile types blocked by shields unchanged. -• [Resurrection] Resurrected units regain their old XP. - -• [Legion changes] Updates relevant Legion unit stats to reflect Season 3 changes. Changelog is as follows: - - Cluster weapon damages and reloadtimes increased by ~30%, cluster secondary munition damage increased ~50% with lowered projectile counts - - Napalm weapon leadlimits set to 0, meaning they will always fire at the current location of its target instead of its predicted location - - Commander aa weapon reduced to 300 range - - Medusa tracking reduced to make retargeting weaker - - Martyr speed nerfed to 220 from 230, turnrate nerfed to 750 from 800 - - Mosquito weapon AOE increased to 72 from 70, stockpile time reduced to 1.8 seconds from 2 seconds - - Spy bot became slightly cheaper and slower, buildtime reduced accordingly first to match other spybots - - Strider energy cost reduced to 5250 from 5400 - - Scylla 15% health buff in accordance with other battleships - - Prometheus speed increased to 52 from 51 - - Inferno reloadtime reduced to 7 seconds from 8 seconds - - Alaris energy cost increased to 850 from 800, buildtime increased to 1650 from 1600, reloadtime slightly increased to 2.3 seconds from 2.25 seconds - - Wheelie reloadtime increased by 10% in accordance with other scout vehicles - - Decreased Legion Advanced Solar Collector costs by 3% across the board - - Factory changes are identical to the other factions. Buildtime updates for units use the same formula as for other T2, T3, and seaplane units in the other two factions. - - Legion fusion: - Metal cost: 4900 -> 4000 - Energy cost: 27000 -> 25000 - Buildtime: 80000 -> 66000 - Energy generation: 1200 -> 950 - Health: 5400 -> 4600 - -# December 2025 -• Unified maximum water depth for non-amphib land units to 22 (previously varied between 22-30) -• Unified minimum water depth for non-heavy ships to 8 (previously varied between 8-10) -• [Legion changes] - - T2 shipyard, ships, seaplanes, and naval structures added - - Praetorian shotgun spread reduced 1900->1400 - - Decurion range reduced 380->360, now deals 25% instead of 100% damage vs air - - Hippocampus (scout ship) now deals 25% instead of 50% damage vs air - - Small napalm blobs now deal 60dps and last 7s, previous was 45dps for 10s - - Inferno reloadtime 9s->8s - - Perdition stockpile time 70s->50s, stockpile cost reduced 500m17000E->350m14000E, impact damage 2000->1200 damage, napalm deals 120dps for 15s (3000 combined damage) - - Napalm damage cap increased 100->120dps - - Martyr damage vs commanders -25% -> -50% - - Syracusia (Destroyer) health reduced 4000->3800 - - Thalassa (Cruiser) health increased 5400->5600 - - Scylla (Battleship) health increased 8000->9000 - - Corinth (T2 artillery ship) cost increased 12000m115kE->13000m125kE, speed reduced 10% - - Ionia (T2 floating turret) mg range increased 650->700 - -# November 2025 -• [Legion changes] - - Karkinos cost increased, health increased, shotgun slightly higher dps with 2-round burst - - Telchine cost reduction 660m19000E->600m13200E, firing angle increased, speed in water increased 30% - - Triton speed reduced 60->55, range reduced 600->550, speed in water increased 30% - - T1 shipyard, ships, and naval structures added - - Iapetus (aa ship) cost 330m4800E->250m3600E, model scaled down 10%, fire rate reduced 15%, health reduced 20% - - Argonaut (frigate) tracking reduced - - Hippocampus (scout ship) speed increased 93->97, acceleration increased 4% - - Ketea (sub) cost reduced 340m2600E->320m2400E, speed increased 54->57, health reduced 640->600 - -# August 2025 -• [Aircraft] Vision raised to 430, if it was lower previously -• [T1 Bombers] Random inaccuracy removed from their bombs -• [T2 Transports] Skyhook 235 -> 200 speed, Abductor 241 -> 225 speed -• [Hound] 292 -> 340 weaponvelocity -• [Razorback] 58 -> 22 damage vs air -• [T2 AA bots] Sightdistance 925 -> 1200 -• [Flagships] Reduced damage vs submarines with main cannon -• [Grunt] 270 -> 280 health - -# July, 30 -• Gunslinger movement class changed to 3x3 and hitbox adjusted -• Sprinter movement class changed to 3x3 and colvol adjusted to cover the funit fully at all angles -• Welder movement class changed to 3x3 and footprint adjusted -• Bulls movement class changed to 4x4 and slight increase in crush damage -• Arm minelayer movement class changed to 3x3 - -# July 2025 -• [T1 Mex] +41% hp -• [Conbots] +15% hp -• [T1 turrets (not aa, not popups)] -10% buildtime -• [Commander] 25 -> 30 energymake -• [T1 factories] -150 metalcost, -250 energycost, -1500 buildtime, 100 -> 150 buildpower -• [Missile trucks] taller hitbox -• [Grunt] -2,5% costs -• [Blitz] 99 -> 101 speed -• [Starlight] 13500 -> 18500 energycost -• [Bull] 65.1 -> 60 speed -• [Jaguar] 320 -> 300 range, -10% dps (lightning) -• [Tzar] 22000 -> 28000 energycost -• [Sprinter] -10% costs -• [Termite] -10% costs, 48.3 -> 50 speed -• [Juggernaut] 33.6 -> 37 speed -• [T2 AA bots] - - New weapons. - - Longrange (1300) missile + flak for Cortex - - Longrange (1200) missile + shortrange missile for Armada - - ~Double health - - +30% energycost -• [Flagships] - - Reduced firerate on big gun, slightly increased damage per shot. Together, ~25% dps nerf - - Bigger AoE and Impulse on big gun, reduced edgeeffectiveness to 0.15 - - +17% costs - -# June 2025 -• [T2 flak turrets] increased footprint from 2x2 to 3x3 -• Warrior movement class from 2x2 to 3x3 (pawn to fido spacing) - -# June 2025 -• [Legion changes] - - 2 new T3 units added: Myrmidon T3 all-terrain mech, Charybdis T3 Hovertank - - New models added to replace the placeholder models left in T2: infestor, spybot, radar bot, jammer bot - - Phobos cost reduced 150m2400E -> 140m2200E - - Decurion buildtime increased 4000-4800 to slow down repair rate, energy cost increased 2700 -> 3000 - - Arquebus cost reduced 900m18000E -> 800m16000E - - Thanatos speed reduced 50 -> 45, turnrate reduced 750 -> 300 - - Triton minigun dps reduced 135 -> 105 - - Medusa range increased 950 -> 1000 - - Keres health reduced 23000 -> 21000 - - Praetorian shotgun fires 10% faster - - Daedalus range increased 900 -> 950 - - Starfall cost increased 58000m660000E -> 63000m720000E, reloadtime increased 15s->18s, lowered damage vs shields - -# May 2025 -• [Legion changes] - - 2 new units added: Aquilon T2 aa bot, Chimera T2 pop-up turret - - Cluster plasma was rebalanced and is overall stronger than before - - Wildfire removed from T2 air lab, Skuttle removed from t2 bot lab, Behemoth removed from T3 gantry - - Script improvements to allow several units to reliably fire while turning: Alaris, Decurion, Quickshot, Phobos - - Mosquito stockpile count reduced 8->4, stockpile rate increased slightly - - Gladiator projectile speed increased 320->360 to improve accuracy - - Lance cost reduced 260->240m, speed increased slightly -• [Tiger, Turtle, Sumo] Mass 750, to make them transportable by light transports. -• [Spybot] Emp immune again. Max stuntime vs units 8 -> 10 -• [Banisher] Energycost 17000 -> 23000, Turnrate 300 -> 250, Acceleration 0.2269 -> 0.2 -• [Tzar] Buildcost 26100 -> 30000 -• [Lava]: Units in lava slow down up to 5x depending on their submersion level - -# April 2025 -• [Pawn] 1420 -> 1650 buildtime -• [Pawn] 52m 870e -> 54m 900e -• [Grunt] 210 -> 215 range -• [Spybots] Can paralyze buildings up to 20s -• [Antinuke buildings] Health 3650 -> 3300 (facilitating a full-length stun from spybot) -• [Shuriken + Abductor] Regain targetmoveerror, revert Shuriken reloadtime 1.3s -> 1.2s - -# March 2025 -• [Armada solar] Buildtime 2800 -> 2600 -• [Armada wind] Metalcost 37 -> 40 -• [Cortex wind] Metalcost 45 -> 43, Health 199 -> 220 -• [Armada tidal] Energycost 250 -> 200 -• [Armada asolar] Metalcost 370 -> 350 -• [Cortex cons] +5% BP -• [Exploiter] Buildtime 2720 -> 2900 -• [T2 radars] Metalcost 560 -> 400, Energycost 19000 -> 14000, Buildtime 11800 -> 8000 -• [Sneaky Pete] Cloaking removed -• [Jammer ships] Costs +130%, Health +130%, Speed 61 -> 40 -• [Spybots] Cloaking cost 100/50 -> 40/15, EMP immunity removed, Attack command for self-destruct added, Damage 56000 -> 5000, Paralyzetime 35/20 -> 8s -• [Banisher] Can now fire in all directions, Sightdistance 550 -> 650, Energycost 23000 -> 17000, Improved aim/tracking -• [Tremor] Damage 150 -> 200, AoE 200 -> 210, Weapon hits a wider area -• [Poison Arrow] Energycost 29000 -> 21000, Buildtime 22200 -> 19000, Sightdistance 385 -> 500 -• [Turtle] Sightdistance 372 -> 500 -• [Garpike/Pincer] Sightdistance 305 -> 500, Faster turning turrets -• [Hound] Lost gauss weapon switch, Metalcost 300 -> 285 -• [Recluse] Range 600 -> 575, Speed 52 -> 47 -• [Grunt] Metalcost 36 --> 43, Energycost 880 -> 840, Range 230 -> 210, Turnrate 1391 -> 1200 -• [Tick] Metalcost 17 -> 21, Energycost 340 -> 300, Health 61 -> 60 -• [Pawn] Metalcost 48 -> 52, Energycost 960 -> 870, Health 335 -> 370 -• [Rover] Health 89 -> 105 -• [Rascal] Health 75 -> 90 -• [Blitz] Health 690 -> 750, Ellipsoid hitbox, Sightrange 299 -> 350 -• [Incisor] Buildtime 1761 -> 2200, Sightrange 273 -> 330 -• [Shuriken] Reloadtime 1.2s -> 1.3s -• [Stormbringer] Drops bombs 17% closer together -• [Banshee] Health 485 -> 560, Turnrate increased, New weapon graphics with functionally similar stats -• Inaccuracy removed from all T1 laser weapons - -# January 2025 -• [Artillery] The High/Low Trajectory toggle has been removed and been automated. Both modes now share the same damage and AOE. Aiming low is preferred, but if there's no low trajectory targets in range or the manually selected target cannot be shot with low trajectory, high trajectory will be used instead for a short time. - The following units are affected: - - [Gauntlet] (T1 Plasma Artillery Turret) - - [Agitator] (T1 Plasma Artillery Turret) - - [Rattlesnake] (T2 Plasma Popup Artillery) - - [Persecutor] (T2 Plasma Popup Artillery) - - [Vanguard] (T3 Mobile All-Terrain Artillery) -• [Angler] The T2 Cortex torpedo bomber now drops 1 large torpedo instead of 3 small ones. The large torpedo is slightly slower but has some aoe -• [Serpent] The Arm T2 battlesub now fires 2 medium torpedoes at once instead of 1 large one, overall dps unchanged but some aoe is added -• [Kraken] The Cor T2 battlesub's torpedo aoe increased - -# December 2024 -• [Impulse] Nukes + Plasma Cannons + Shiva + Catapult + Vanguard + Tzar + Fatboy + Banisher + Poison Arrow + Gunslinger + Ballistic Missile Launchers got impulse added to their weapon. High-HP units with low mass got a mass increase. -• [Tremor] Impulse 140% -> 80%, EdgeEffectiveness 90% -> 15% -• [Calamity + Raghnarok] +10% E/M cost, energycost and damage of individual shot increased, reloadtime increased (same dps as before), AoE lowered -• [Light Mine] Cloakcost 0.5 -> 1e/s, metalcost 5 -> 7, buildtime 50 -> 100 -• [Medium Mine] Cloakcost 1 -> 2e/s, metalcost 16 -> 25, buildtime 100 -> 300, EdgeEffectiveness 0.7 -> 0.5, Impulse 1 -> 0.8 -• [Heavy Mine] Cloakcost 1.5 -> 6e/s, metalcost 21 -> 50, buildtime 125 -> 700, EdgeEffectiveness 0.7 -> 0.5, Impulse 1 -> 0.8, damage 1390 -> 3000, AoE 300 -> 330 -• [Crawling Bombs] Use bigger (self-d) explosion on attack command. Dying to enemy fire still results in small blast (including while being transported). Transportable. -• [Crawling Bombs death explosions] AoE 432 -> 400, damage 3350 -> 2700, damage vs Crawlling Bombs 220 -> 400 (chain easier) -• [Roach] Speed 81 -> 76 -• [Skuttle] Metalcost 540 -> 755, energycost 26 000 -> 27 000, cloakcost 150 -> 15, cloakcost moving 400 -> 40 -• [Destroyers] Depthcharge turnrate increased -> hits crawling bombs reliably - -# October 2024 -• [All units] EMP resist for units is standardized, and units that had low emp resists now take full emp damage. Units that had between 50-95% emp resist now all have 80% emp resist. Units that had 95+% emp resist are now fully emp immune. -• [New units] Heavy T1 air transport for arm (Osprey) and for cor (Hephastus) moved to the basegame. These transports can carry the same weight as T2 transports but at a lower movespeed, and are able to transport the commander. Osprey costs 190m 4000E, has 110 speed, and 630 health. Hephaestus costs 190m 4000E, has 100 speed, and 800 health. -• [Stork/Hercules] T1 light air transports no longer able to transport the commander, and can now only carry units below 750 metal. Energy/metal/buildcost reverted to earlier values (68m 1300E 3850bp for Stork and 74m 1450E 4120bp for Hercules) -• [Shuriken] emp damage per shot reduced from 800-600, weapon can no longer fire at air units -• [Abductor] emp damage per shot reduced 22500->10500, beam duration 0.5->0.2s (improves accuracy), stuntime reduced from 15->6s -• [Liche] weapon no longer has an extra impulse multiplier -• [Skyhook] T2 cor air transport speed increased 210->235, health increased 1830->2200. Line of sight increased 260->500 -• [Webber] No longer able to target air units - -# September 2024 -• [All units] Gravity standardized for all projectiles to 130, so weapons will behave the same way across all maps. -• [Rocketeer + Aggravator] Reverted the last 2 balance changes: damage per shot 173->157, reloadtime 4->3.8s, speed/acceleration 5% higher -• [Lazarus + Graverobber] Metal cost increased 110->130, E cost unchanged, buildtime increased 2400->2800 -• [Mace] Health increased 900->1000 -• [Thug] Health increased 1000->1100, buildtime increased from 1970->2100 so it doesn't get repaired too quickly -• [Tremor] Reloadtime reverted to 0.5s, impulse reduced 30% -• [Vanguard] Range increased 1325->1450, health reduced 10000->8500. Tremor and Vanguard's higher ranges now let them outrange long range defences (Rattlesnake, Persecutor, Pulsar) - -# August 2024 -• [Tremor] firemodes unified to remove the toggle, and effective DPS in target area increased. Wasp bugfixed, and now has mild tracking on rockets, so will miss shots less often. -• [Dragon Claw/Maw] Collision volume and aim position will now change when the units open up, so it can be targeted when behind a wall - -# July 2024 -• [Minelayers] Minelayers now live up to the 'minesweeper' role properly, being able to detect enemy mines within 450 radius, and slowly clear them, with a moderate ranged weapon. -• [Naval Economy Buildings] Stats made equal to land versions. Specifically, T1 and T2 Energy Converters, Energy and Metal Storages, and the T2 mex have been harmonised for both Core and Arm. -• [Salamander] Energy cost increased 4775->7000 to make the unit more difficult to rush early on and put its cost more in line with other amphibs (energy being 20x metal cost). Range reduced from 360->340 to make it slightly more difficult to kite T1 units and commanders. -• [Whistler + Lasher] Ground range increased by 25 (525/550->550/575 range) -• [Centurion] Unit script improved so the unit should keep firing while turning -• [T2 Cruisers] Reloadtime of depthcharges reduced by 10% -• [T2 Lightning/Flamethrower Ships] Cost reduced 25%, health reduced 20%, speed increased 6%, dps reduced 20%. Flamethrower ship model size scaled down by 10% - -# June 2024 -• [Rocketeer + Aggravator] Speed and acceleration reduced by 5% -• [Whistler + Lasher] Changes from proposed units rework modoption moved to base game. {Whistler and Lasher weapon switches from ground and anti-air modes. Anti-air has 700 range and tracking missiles, while ground has 525/550 range and no tracking. Ground speeds increased by 10 (45->55, 42->52), ground dps increased by 40% (17->25)} -• [T1 Air Transports] E costs increased by 70%, buildtime increased by 20%. This change is to increase the time it takes to rush transports and share them to the team. -• [Herring] unit now switches between ground missiles and longer range tracking aa missiles (750 range). No change to ground missile stats. Unit now takes 50% damage from emp instead of 30%. -• [Dragon Claw/Maw] Damage taken while closed increased from 25%->33%. Dragon Claw range reduced 440->430 - -# March 2024 -• [Proposed Units Rework Modoption] Rework to Whistler and Lasher added to modoption, which has the units switch between longer range tracking aa missiles and non-tracking ground missiles. Mauser, Quaker, Stiletto changes removed from modoption. -• [Mauser + Quaker] Rework from proposed_unit_reworks modoption moved to main game with a few additional changes. This rework aims to give these units a more mobile and aggressive role, which reduces the role overlap with the heavier T2 veh artillery options. Their ranges are reduced by 120, speed increased by 20%, acceleration increased 50%, health increased 20%, and accuracy is improved. -• [Stiletto] Rework from proposed_unit_reworks modoption moved to main game. This rework aims to give the stiletto a more specialized role for disabling specific targets while being less efficient as a defensive option against groups. The unit's metal and energy costs are doubled, buildpower cost increased 50%, health increased by 30%, paralyze time increased from 10s->20s, bomb count reduced from 5->3, aoe reduced from 240->200, emp damage per bomb increased 4000->6000 -• [Banshee] Reloadtime reduced by 10% (DPS 34->38) -• [Dragon Claw] DPS reduced by 14% (185->159) -• [Razorback] Laser damage vs air reduced from 75% to 50% -• [Demon] Health reduced from 20000->18000 -• [Crocodile and Cayman (T1 hovertanks)] Crocodile cost reduced 290m2600E->270m2400E, Cayman cost reduced 320m3300E->300m3100E -• [Salamander] EMP resist reduced from 95%->90% -• [Epoch and Black Hydra] Projectiles for main cannons increased in size, damage, and aoe, with a reduced fire rate - -# February 2024 -• [Grunt] Speed reduced from 84->81 -• [Pawn] Speed increased from 84->87 -• [Demon] Buildtime cost increased from 90000->120000 -• [Thug/Mace] movement footprint increased to prevent them blocking each other's turrets when attacking -• [New Units] Demon (T3 cortex flamethrower mech), Salamander (T2 cortex amphibious tank, replacing Alligator) moved from release candidate modoption into the base game. Dragon rework (new model, flamethrower, light aa) moved to the base game. Flamethrower ship and Lightning ship moved from expandedT2sea modoption into the base game -• [Dragon] Fire rate of anti-air weapon reduced by half -• [Demon] DPS reduced from 2000->1600, unit no longer has amphibious -• [Lightning ship and Flamethrower ship] Light aa added so they can protect themselves from light air while raiding - -# January 2024 -• [Turtle] Added a weak anti-air turret so that an amphibious turtle attack is more difficult to counter -• [T1 subs] Eel (arm sub) speed increased 63->66, Orca (cor sub) speed reduced 60->57 -• [T1 frigates] Ellysaw (arm frigate) cost reduced 390m2600E->380m2550E, Riptide (cor frigate) cost increased 410m2700E->420m2800E -• [Mauser] Cost increased from 270m4100E -> 320m4900E -• [Quaker] Cost increased from 360m4000E -> 400m4400E -• [Supporter] Line of sight increased 500 -> 600 -• [Skater and Herring] 1000 range radar added to both units. The goal is to give players better intel surrounding their fleets when playing t1 sea, where the short range radar can help to create a middle ground between fighting blind and having full vision. - -# November 2023 -• [Release Candidate Modoption] Dragon rework is added to the list of units in the release candidates modoption. This updates the dragon model and changes its weapons. An anti-air turret is added so that the unit can deal with small amounts of T1 fighters, the front weapon is changed from a laser to a flamethrower, and the side turrets have been split from 2 medium to 4 smaller laser turrets. -• [Consul] Recluse removed from build list, Webber added to build list. -• [Air units] Armorclasses merged for all air units, meaning that fighters and bombers will take the same amount of damage as gunships. - - T1 fighters will kill other T1 fighters in 2 shots instead of 1 - - T1 fighters will kill T2 fighters in 3 shots instead of 2 - - Flak will kill T2 fighters in 2 shots instead of 1 - - To make up for the extra durability, the cost of T2 fighters is increased - - Nighthawk (Cor T2 fighter) cost increased from 105m3700E -> 135m4750E, and weapon damage set to 500/shot - - Highwind (Arm T2 fighter) cost increased from 120m4900E -> 140m5700E, and weapon damage set to 750/shot - - The commander no longer deals extra damage against bombers - - Chainsaw and Eradicator damage per shot against fighters and gunships increased to match with previous damage against bombers -• [Quaker] Reworked to be larger with a heavier weapon, so that the T2 veh artillery has a similar asymmetry to the T1 veh artillery. Cost changed from 280m 3300E -> 360m 4000E, health increased 830 -> 1000, max speed reduced 54 -> 48, Damage per shot increased 300 -> 420, aoe increased 129 -> 144, reloadtime increased 4.25s -> 5s -• [Omen] Cor T2 radar vehicle's movement speed increased 36 -> 48 to be closer to arm T2 radar vehicle (57 speed) -• [Crawling Bombs] Movement speed while underwater reduced to 2/3 speed on land -• [Consul] Hound removed from build list, Sprinter and Platypus added to build list -• [Twitcher] Termite added to build list -• [Termite] Cost reduced from 700m 12000E -> 600m 9000E -• [Turtle] Cost reduced from 750m 18000E -> 750m 15000E -• [T2 Amphib Tanks] Paralyze damage taken reduced to 25% -• [T2 Cruisers] Fires 1 depthcharge at a time instead of 2 round bursts, projectile sped up and tracking improved - -# October 2023 -• [Webber] Reclaim speed increased from 100->150. - -# August 2023 -• [Shellshocker] Reworked to be the lighter version of t1 veh artillery while cor remains the heavy version. Reloadtime decreased 6.1 -> 4.3, damage per shot decreased 260 -> 182, inaccuracy reduced to 50% current area, area of effect reduced to 50% current area (-30% radius). Model size and collision volume decreased by 10% -• [Wolverine] Cost increased from 155m 2300E -> 170m 2500E. Model size and collision volume increased by 15%. Max health 640->750, max velocity 51->48. -• [Rocketeer] Damage per shot increased 10% (157->173), reloadtime increased 5% (3.8->4) -• [Aggravator] Damage per shot increased 10% (157->173), reloadtime increased 5% (3.8->4) -• [Laser/Lightning Weapons] targetmovererror set to zero for T2+ units. - - Hitscan weapons no longer randomly miss small fast moving targets. - - Affected Armada Units: Platypus, Welder, Titan blue laser, Thor side EMP lasers, Starlight. - - Affected Cortex Units: Duck, Sumo, Mammoth, Termite, Behemoth red lasers, Juggernaut heat ray. - - This matches existing behavior of Jaguar, Razorback, Thor main cannon, and Cataphract. - -# July 2023 -• [Whistler] Range decreased from 600 -> 575 -• [Lasher] Damage per shot decreased 47 -> 43 -• [Shellshocker] Reloadtime increased 5.7s -> 6.1s -• [Wolverine] Reloadtime increased 6.6s -> 7.2s -• [Dolphin] Cost increased from 165m 1400E -> 175m 1500E -• [Herring] Cost decreased from 230m 1600E -> 210m 1400E -• [Corsair] Depthcharge reloadtime decreased 2.24->2s -• [Oppressor] Depthcharge reloadtime decreased 2->1.9s -• [Tremor] The weapon's spread is now proportional to the distance between the tremor and its target. Fire rate reduced from 3->2 shots per second, damage per shot increased 100->150. -• [Veh Cons] Brake rates significantly increased, to prevent drifting into blueprints. - -# June 2023 - • [Shiva] Weapon reloadtime reduced by 33% (3s->2s), damage per shot reduced 33% (900->600), turret turnrate increased. Script and hitbox adjusted to reduce friendly fire. - • [Karganeth] Cost increased by 50% (1650->2500m), damage per shot increased by 50% (120->180 per shot, dps 400->600), health increased by 25% (10000->12500), model size increased slightly. Missiles automatically retarget midflight after current target is destroyed. Goal is to differentiate Karganeth and Shiva by increasing Karganeth's size and making it better at fighting single targets units while Shiva is better at dealing with large amounts of small units. - • [Tremor] Weapon reworked, now has a higher aoe, firerate, and impulse but lower damage per shot. The weapon is now stronger against groups of small units while significantly changing its strength against large units. - • [Incisor] Hitbox adjusted so incisors will fire more effectively in close formations, but will still obstruct each other's fire in large groups. - - -Bugfixes - • [Mines and Fiends] Fiends can now attack and damage mines. - • [Longbow and Messenger] Fixed rare case of door animation getting stuck and being unable to attack. - • [Eel] Shift aimpoint of Eel so it no longer barely outranges torpedo launchers. - • [Pitbull] Pitbull no longer blocked by T1 walls, it will correctly deploy and fire over T1 walls. - • [Resurrection] Fixed bug where units could sometimes be resurrected at full health instead of at 5% health. - - -# May 2023 -[Commander] default enabled modoption "comupdate": - • Now immune to the D-Gun. - • Health 3350->4000. +Removed passive health regeneration. - • Wreckage metal 2000->1250. - • Death explosion inflict less damage. - • No longer susceptible to special damage modifiers from certain units, such as Light Laser Towers. - - -Bugfixes - • [Viper] No longer closed and armored when under construction. - • [Pitbull] Pitbull now closes into armored position after construction. - • [Wrecks and Heaps] Pincer got a heap. Duck got a wreck. Karganeth got a heap. Garpike got a heap. Wolverine got a heap. - • [Recluse] Recluse got new cruise missile behavior to drastically reduce incidences of friendly fire on cliff corners. - • [Gunslinger] Minor unit script edit to reduce friendly fire incidents. - • [Mobile Jammers] Now correctly turn back on after being damaged or stunned, and correctly turn off when stunned. - • [Air Factories] Fixed bug where it thought it had an aircraft on the buildpad and would stop producing units. - • [Nukes] Animation is now interruptible if a nuke was not fired, so an accidentally dropped target does not lock the silo into a long animation. Animation time standardized to 8.5 second door open time, and 30 second door close time. - • [RFLRPC] Ragnarok now correctly has a 0.4 reload time at 0 XP. Both Ragnarok and Calamity have animations tied to XP gain, so they properly animate faster and gain firerate increases with XP gains. - - -Other - • [Supporter] Reloadtime reduced from 1s->0.93s - • [Roughneck] Projectile speed increased 450->800 - • [Viper] Animation tweaks. Retains heading when closing up. 6 frames (0.2 sec) added to deploy animation (total of 37 frames (1.23 seconds), to match pitbull time-to-first-shot. Time to close into armored state after going idle now set to 3 seconds, to match pitbull. - - -# April 2023 - - • [Grunt] Range reduced from 240->230 and health reduced from 290->270 - • [Pawn] Damage per shot reduced from 10->9 - • [Tick] Metal cost increased from 15->17 - • [T1 Vehicle Scouts] Damage per shot increased from 30->35 - • Targeting improvements to tiger tank and turtle tank - - -# March 2023 - - • [T3 Hovertanks] Depthcharges added and main turrets can no longer target underwater units - • [Thor] Speed nerf 60->54, spark forkdamage nerf 0.5->0.25 (now deals 750 damage as aoe instead of 1500) - • [Commanders] can no longer capture allied units - • [Seaplane Platforms] added sonar distance for armada: 600, cortex: 800 - • [factories] tiny radar ranges removed + added large ranges for t1 air: 500, t2 air: 1000, armada seaplane: 750 - - -# February 2023 - - • [missiles] antinukes/nukes/tacnukes/empmissiles no longer collide into enemy (air) units - • [Titan/Juggernaut] added foot stomp "weapon" - • [Advanced Exploiter] General fixing of cormexp behavior. Consistent rocket salvo of 5. No longer less armored when closed. Proper decoy for moho. Continues extracting metal when deployed and attacking. - - -# January 2023 - - • [Twin Guard and Beamer] Added 5 range to make sure that they can't be outranged by T1 rocket bots when firing at a different elevation. - • [Thor] EMP rocket now launches immediately. To compensate, overall missile flight time increased by 2.5 seconds. Model animation now reveals to opponent how many EMP missiles are loaded. - • [vehicle scouts] accuracies and turnrate increased, damage reduced slightly. Cortex more maneuverable but slightly slower than armada, with reduced health - • [Lightning Weapons] Increased consistency of fork damage. Lightning chaining now always occurs at the lighting bolt end point, instead of only when a unit is damaged. Removed double application of flanking damage multiplier. - • [Flanking Damage] Flanking damage changed from min=0.9, max=1.9 to min=1.0, max=2.0. Approximately 5% nerf to overall bonus damage from flanking. All damage deals full 100% frontal damage instead of 90% frontal damage. All unit health increased by 11% to compensate. -Consequences include: - • Wrecks are 11% more durable. - • Autoheal is 11% nerfed. - • Impulse of weapons increased by 11%. - - -# December 2022 - - • [Dragon Maw/Claw] Units now remember the location of revealed Dragon Maws/Claws even if fog-of-war covers them up. - • [Marauder] Change from tank movement to bot movement. Script fixes and torso turn rate buff to allow attacking while moving. Add 0.25 sec deploy animation to marauder AA cannons. - • [catalyst] Range -18% Nerf (2750 -> 2250), Area of Effect -26% Nerf (512 -> 380), Damage +60% Buff (2500 -> 4000) - - -Bugfixes - • [artillery] Script changes to prevent units from aiming at "illegal" out-of-firing-arc headings. - - -# October 2022 - - • [Warden] AimFrom point adjusted to center of unit, so the HLT cannot shoot outside of its range circle. - • [Thor] Range reduced 640->540, damage per tick reduced 320->300. Lightning chaining buffed, which better matches original intended behavior (forkdamage 0.33->0.5, maxunits 2->5, range 60->125). Overall script changes to make the commandfire EMP rocket, main tesla cannon, and side EMP turrets no longer interfere with each other. EMP rocket now takes starburst trajectory with a 3 second command delay. - - -# August 2022 - - • [Beamer] Script fix for continuous laser buffed effective DPS by 25%. - - -# August 2022 - - • [Torpedo gunships] weapon changed to match up with other torpedoes (damage and speed), accuracy and targeting improved. - - -# May 2022 - - • [Sniper] speed: 30->33 - - -# March 2022 - - • [Sabre] reloadtime 12% slower, range -11% - • [Stiletto] slower turnrate, 15 -> 10 sec paralyze duration, lower flight altitude - - -# January 2022 - - • [EMP-spider] health 850 -> 1000, increased turret turn speed - • [T1 walls] Removed energy cost + land walls: reduced metal cost 11 -> 8 - • [T2 walls] Halved energy cost (~1000 -> 500) + rounded up metal cost 38/39 -> 40 - • [Thor] EMP missile weapon is by manual fire only - - -# December 2021 - - • [Fido] cost: 270m->300m, 5600->6300E - • [Sniper] speed: 35.4->30 - • [Zeus] unnerfed: dps 200->220 - • [Fatboy] cost: 1500->1400m, 12000->11000E - • [Maverick] cost: 700->650m, 12000->11000E + range increases with experience 3x faster - • [Recluse] health increased (1050->1250) and weapon range increased (550->600) Burst 4->3 (no dps loss) - • [EMP-spider] now able to reclaim (100 buildpower), cost: 175->250m, 3400->5000E - - -# 2019 - 2021 - -General - • Enabled ground deformation - • Wreckage/heaps have the same HP as regular unit HP (~33% increase) - • Increased maxslope for all units 50% (the slope it allows to be build on) - • Units gain xp (ranks) faster, also relatively more health/firerate compared the the same old xp - • Removed the energy/metal make/use/storage from most non eco units - -Units - • Commander wreckage is now 2000 metal instead of 2500 - • Commander has 500 metal/energy storage (lose commander and default storage is also 500) - • Commander produces 2 metal (was 1.5) - • T1 metal extractors use 3 Energy to operate, also slightly reduced buildprice - • T2 metal extractors have 40% less metal storage (600) - • Ground scouts (vehicles/bot): 10% more LoS - • T1 vehicle artillery has faster projectile and does 2x more damage per shot, but also has 2x reloadtime. - • T2 vehicle artillery has 33% more health + same damage as t1 but with adjusted reload time to compensate - • Decreased cormaw damage 27% - • Arm Peewee damage reduced by 9% - • Core AK range reduced 240->225 and damage increased 6% - • Arm Warrior health and buildtime increased by 10% - • Arm Flash health increased by 5% and firing script improved - • Cor Instigator health increased by 2% - • Arm Stumpy turn rate reduced by 10% - • Cor Raider turn rate reduced by 10% - • Claw/Zeus: lightning chains again (up to 2 neighbouring units *0.33) - • Zeus: does 10% less damage - • Juggernaut: main weapon does 4x old damage/reloadtime - • EMP launcher: reduced range 10% - • Spy paralyzetime 45 -> 35 sec - • Spy is much more resistant to emp damage - • T2 Bot/Air Constructors can build Advanced Fusion + cost: +60 metal - • T2 Vehicle/Air Constructors can make the T3 Experimental Gantry - • Arm Guardian: Removed special damage to ships and commander, reduced low trajectory aoe from 128 to 100, increased low trajectory default damage by 20%, increased high trajectory default damage by 9% - • Cor Punisher: Removed special damage to ships and commander, reduced low trajectory aoe from 140 to 120, increased low trajectory default damage by 20%, increased high trajectory default damage by 18%, increased reloadtime by 8% - • Arm Ambusher: Removed special damage to ships and commander - • Core Toaster: Removed special damage to ships and commander, low and high trajectory damage increased by 20%, high trajectory cooldown increased by 14% - • Arm Big Bertha: Increased E cost to fire from 3000 to 5000 - • Cor Intimidator: Increased E cost to fire from 3000 to 6000 - • Arm Vulcan: Reduced E cost to fire from 14500 to 10000 - • Cor Buzzsaw: Reduced E cost to fire from 15725 to 12000 - -Air - • Banshee: changed weapon to machine gun with more accuracy, 5 burst (instead of 3) but little less average dps (-10%) - • Air Fighters can no longer attack ground - • T1 construction plane E cost -30% - • LLT, HLLT, BEAMER, RL, RAD now transportable with T2/Heavy Transporters - • Torpedo bombers: Added 800 range radar+sonar, +10% los, -13% max velocity, reduced flight altitude 25%, split main weapon into 3-round burst (500 damage each instead of 1500), Increased cost by 21% - • Torpedo gunships: Targeting/tracking improvements, +17% los, added 535 range sonar - • EMP bomber: 20% slower (still slightly faster than t1 fighters) - -Sea - • Ship/Hover/Amphibious transports removed - • Arm and Cor corvette health +25, Cor corvette M cost reduced by 5 (150->145) - • Arm and Cor frigate damage, speed, and health increased - • Arm and Cor destroyer E cost increased 50% (7.5E/M instead of 5E/M), turret turnrates adjusted, and arm destroyer's reloadtime increased from 1.2 to 1.6 with its damage increased to keep the same dps - • Cor Battleship and Black Hydra laser dps increased by 15%, and minor improvements to their speed/health/cost - • T1 sub damage -30% - • T2 sub killer reworked: speed increased, range decreased, reloadtime decreased, damage per shot decreased - • T2 battle sub introduced: has a high cost, long range, high damage, long cooldown, and is slow - • Arm and Cor depthcharge turrets: +31% health, +13% range, increased build time - • Arm and Cor hovertank speeds increased (+5% arm, +10% core), cor hovertank health increased 6% - - -Renamed units: - • Jeffy -> Ranger - • Pack0 -> Ferret - • Krogoth -> Korgoth - • Gaat Gun -> Warden - - -10.24 -24/02/2019 - -Balanced Annihilation 10.24, the game this has been based on. diff --git a/luaui/Include/markdown.lua b/luaui/Include/markdown.lua new file mode 100644 index 00000000000..ca42486b693 --- /dev/null +++ b/luaui/Include/markdown.lua @@ -0,0 +1,1575 @@ +-- Markdown for widget text panels. +-- +-- Three stages, each usable on its own: +-- Markdown.parse(text) -> doc blocks and headings, inline markup resolved +-- Markdown.layout(doc, ctx) -> rows word-wrapped rows of styled segments +-- Markdown.draw(rows, i, j, x, y, ctx) paints rows i..j with their top edge at y +-- +-- Block level: ATX and setext headings, paragraphs (soft breaks joined, hard breaks on +-- a trailing backslash, two trailing spaces or
), nested bullet and numbered lists +-- with continuation paragraphs, blockquotes, fenced code, horizontal rules, pipe tables, +-- link reference definitions and HTML comments. Inline: **bold**, *italic*, `code`, +-- ~~strike~~, [text](url), and bare http(s) links, images as their alt text, and +-- backslash escapes. Indented code blocks are deliberately not recognised; an indented +-- line outside a list is ordinary paragraph text, which is what changelog authors mean. +-- +-- Bold uses a heavier face and code a monospaced one, so the wrapping measures every +-- word with the face it will be drawn in. Italic is the regular face sheared with a +-- matrix, which the engine's batched font path cannot do, so italic segments print +-- unbatched; ctx.italicShear = 0 turns that into plain text. + +local Markdown = {} + +local mathFloor = math.floor +local mathMax = math.max +local mathMin = math.min +local strSub = string.sub +local strFind = string.find +local strMatch = string.match +local strGsub = string.gsub +local strLower = string.lower +local strRep = string.rep +local tableConcat = table.concat + +---------------------------------------------------------------------------------------- +-- Inline markup +---------------------------------------------------------------------------------------- + +-- A run is { text = s, b = bold, i = italic, c = code, s = strike, link = url } or +-- { br = true } for a hard break. Runs that share a style are merged as they are added. +local function sameStyle(a, b) + return a.b == b.b and a.i == b.i and a.c == b.c and a.s == b.s and a.link == b.link +end + +local function withStyle(st, key, value) + local t = { b = st.b, i = st.i, c = st.c, s = st.s, link = st.link } + t[key] = value + return t +end + +local function isSpaceAt(s, i) + local c = strSub(s, i, i) + return c == "" or c == " " or c == "\n" or c == "\t" +end + +local function isPunctAt(s, i) + local c = strSub(s, i, i) + return c ~= "" and strFind(c, "%p") ~= nil +end + +local function isWordAt(s, i) + local c = strSub(s, i, i) + return c ~= "" and strFind(c, "[%w]") ~= nil +end + +-- CommonMark's flanking test for a delimiter run at i..i+len-1: whether it may open +-- or close emphasis, judged by the characters on either side of it. +local function delimiterFlags(s, i, len, c) + local prevSpace = isSpaceAt(s, i - 1) + local nextSpace = isSpaceAt(s, i + len) + local prevPunct = isPunctAt(s, i - 1) + local nextPunct = isPunctAt(s, i + len) + local left = not nextSpace and (not nextPunct or prevSpace or prevPunct) + local right = not prevSpace and (not prevPunct or nextSpace or nextPunct) + if c == "_" then + return left and (not right or prevPunct), right and (not left or nextPunct) + end + return left, right +end + +-- The matching close bracket for the one at `i`, honouring nesting and escapes. +local function matchBracket(s, i, open, close) + local depth = 0 + local p = i + local n = #s + while p <= n do + local ch = strSub(s, p, p) + if ch == "\\" then + p = p + 2 + else + if ch == open then + depth = depth + 1 + elseif ch == close then + depth = depth - 1 + if depth == 0 then + return p + end + end + p = p + 1 + end + end + return nil +end + +local function findTickClose(s, ticks, from) + local p = from + while true do + local q = strFind(s, ticks, p, true) + if not q then + return nil + end + local after = q + #ticks + if strSub(s, q - 1, q - 1) ~= "`" and strSub(s, after, after) ~= "`" then + return q + end + p = after + end +end + +local parseInline + +-- The text of a run list with the markup dropped, for labels and searches. +function Markdown.plainText(runs) + local out = {} + for k = 1, #runs do + local r = runs[k] + out[#out + 1] = r.br and " " or r.text + end + return tableConcat(out) +end + +-- Walks `s` in style `st`, appending tokens: text { text, st }, breaks { br } and +-- emphasis delimiter runs { delim, n, open, close }, which are matched up afterwards. +local function scan(s, st, tokens, refs) + local n = #s + local i = 1 + local buf = {} + + local function text(t, style) + if t ~= "" then + tokens[#tokens + 1] = { text = t, st = style } + end + end + + local function flush() + if #buf > 0 then + text(tableConcat(buf), st) + buf = {} + end + end + + local function inlineRuns(label, style) + local runs = parseInline(label, refs, style) + for k = 1, #runs do + local r = runs[k] + if r.br then + tokens[#tokens + 1] = { br = true } + else + tokens[#tokens + 1] = { text = r.text, st = r } + end + end + end + + while i <= n do + local c = strSub(s, i, i) + local handled = false + + if c == "\\" then + local nx = strSub(s, i + 1, i + 1) + if nx ~= "" and strFind(nx, "%p") then + buf[#buf + 1] = nx + i = i + 2 + else + buf[#buf + 1] = c + i = i + 1 + end + handled = true + elseif c == "\n" then + flush() + tokens[#tokens + 1] = { br = true } + i = i + 1 + handled = true + elseif c == "`" then + local ticks = strMatch(s, "^`+", i) + local close = findTickClose(s, ticks, i + #ticks) + if close then + local code = strSub(s, i + #ticks, close - 1) + code = strGsub(code, "\n", " ") + if #code >= 2 and strSub(code, 1, 1) == " " and strSub(code, -1) == " " and strFind(code, "%S") then + code = strSub(code, 2, -2) + end + flush() + text(code, withStyle(st, "c", true)) + i = close + #ticks + else + buf[#buf + 1] = ticks + i = i + #ticks + end + handled = true + elseif c == "*" or c == "_" or c == "~" then + local run = strMatch(s, "^" .. (c == "*" and "%*+" or (c == "_" and "_+" or "~+")), i) + local len = #run + -- Only a double tilde strikes, so the lone "~" of "~25%" stays literal. + if c ~= "~" or len == 2 then + local open, close = delimiterFlags(s, i, len, c) + flush() + tokens[#tokens + 1] = { delim = c, n = len, orig = len, open = open, close = close, st = st, opens = {}, closes = {} } + else + buf[#buf + 1] = run + end + i = i + len + handled = true + elseif c == "[" or (c == "!" and strSub(s, i + 1, i + 1) == "[") then + local isImage = c == "!" + local start = isImage and i + 1 or i + local close = matchBracket(s, start, "[", "]") + if close then + local label = strSub(s, start + 1, close - 1) + local after = close + 1 + local url + local resume + local nextChar = strSub(s, after, after) + if nextChar == "(" then + local pclose = matchBracket(s, after, "(", ")") + if pclose then + local inner = strSub(s, after + 1, pclose - 1) + url = strMatch(inner, "^%s*<([^>]*)>") or strMatch(inner, "^%s*(%S+)") or "" + resume = pclose + 1 + end + elseif nextChar == "[" then + local rclose = strFind(s, "]", after + 1, true) + if rclose then + local id = strSub(s, after + 1, rclose - 1) + if id == "" then + id = label + end + url = refs[strLower(id)] + if url then + resume = rclose + 1 + end + end + else + url = refs[strLower(label)] + if url then + resume = after + end + end + if resume then + flush() + if isImage then + -- No pictures here: the alt text stands in for the image. + text(Markdown.plainText(parseInline(label, refs)), withStyle(st, "i", true)) + else + inlineRuns(label, withStyle(st, "link", url)) + end + i = resume + handled = true + end + end + if not handled then + buf[#buf + 1] = c + i = i + 1 + handled = true + end + elseif c == "<" then + local url = strMatch(s, "^<(%a[%w+.-]*://[^%s<>]*)>", i) + if url then + flush() + text(url, withStyle(st, "link", url)) + i = i + #url + 2 + else + local br = strMatch(s, "^<[bB][rR]%s*/?>", i) + if br then + flush() + tokens[#tokens + 1] = { br = true } + i = i + #br + else + buf[#buf + 1] = c + i = i + 1 + end + end + handled = true + elseif c == "h" and (strFind(s, "^https?://", i)) and not isWordAt(s, i - 1) then + local url = strMatch(s, "^https?://[^%s<]+", i) + -- Trailing punctuation belongs to the sentence, not the address. + local trimmed = strMatch(url, "^(.-)[%.,:;!%?%)]*$") + if trimmed ~= "" then + url = trimmed + end + flush() + text(url, withStyle(st, "link", url)) + i = i + #url + handled = true + end + + if not handled then + buf[#buf + 1] = c + i = i + 1 + end + end + flush() +end + +-- CommonMark's "process emphasis": every closing delimiter run looks back for the +-- nearest opener of its kind, they hand each other as many delimiters as both can +-- spare (two for bold, one for italic), and any delimiters between them are spent. +local function processEmphasis(tokens) + local i = 1 + while i <= #tokens do + local c = tokens[i] + if c.delim and c.close and c.n > 0 then + local j = i - 1 + local o + while j >= 1 do + local t = tokens[j] + if t.delim and t.delim == c.delim and t.open and t.n > 0 and not t.spent then + -- One run that could both open and close may not pair with another + -- when their lengths add up to a multiple of three, unless both do. + local odd = (t.close or c.open) and (t.orig + c.orig) % 3 == 0 and not (t.orig % 3 == 0 and c.orig % 3 == 0) + if c.delim == "~" then + odd = t.n ~= c.n + end + if not odd then + o = t + break + end + end + j = j - 1 + end + if o then + local use + if c.delim == "~" then + use = 2 + else + use = (o.n >= 2 and c.n >= 2) and 2 or 1 + end + local kind = c.delim == "~" and "s" or (use == 2 and "b" or "i") + o.opens[#o.opens + 1] = kind + c.closes[#c.closes + 1] = kind + o.n = o.n - use + c.n = c.n - use + for k = j + 1, i - 1 do + if tokens[k].delim then + tokens[k].spent = true + end + end + if c.n == 0 then + i = i + 1 + end + else + i = i + 1 + end + else + i = i + 1 + end + end +end + +-- Turns matched tokens into runs: leftover delimiters print literally, and the +-- emphasis they opened or closed nests as bold/italic/strike depth. +local function flattenTokens(tokens, runs) + local depth = { b = 0, i = 0, s = 0 } + + local function emit(t, st) + if t == "" then + return + end + local style = { + b = st.b or depth.b > 0, + i = st.i or depth.i > 0, + s = st.s or depth.s > 0, + c = st.c, + link = st.link, + } + local last = runs[#runs] + if last and not last.br and sameStyle(last, style) then + last.text = last.text .. t + else + style.text = t + runs[#runs + 1] = style + end + end + + for k = 1, #tokens do + local t = tokens[k] + if t.br then + runs[#runs + 1] = { br = true } + elseif t.text then + emit(t.text, t.st) + else + -- A closer's matches close innermost first; an opener's open outermost + -- first, which is the reverse of the order they were found in. + for m = 1, #t.closes do + local kind = t.closes[m] + depth[kind] = depth[kind] - 1 + end + if t.n > 0 then + emit(strRep(t.delim, t.n), t.st) + end + for m = #t.opens, 1, -1 do + local kind = t.opens[m] + depth[kind] = depth[kind] + 1 + end + end + end +end + +parseInline = function(text, refs, style) + local tokens = {} + scan(text, style or {}, tokens, refs or {}) + processEmphasis(tokens) + local runs = {} + flattenTokens(tokens, runs) + return runs +end + +Markdown.parseInline = parseInline + +---------------------------------------------------------------------------------------- +-- Block structure +---------------------------------------------------------------------------------------- + +local function expandTabs(line) + return (strGsub(line, "\t", " ")) +end + +local function indentOf(line) + local _, e = strFind(line, "^ *") + return e +end + +local function isBlank(line) + return strFind(line, "^%s*$") ~= nil +end + +-- Level and text of an ATX heading, or nil. +local function atxHeading(s) + local hashes, rest = strMatch(s, "^(#+)[ \t]+(.-)[ \t]*$") + if not hashes then + hashes = strMatch(s, "^(#+)[ \t]*$") + rest = "" + end + if not hashes or #hashes > 6 then + return nil + end + rest = strGsub(rest, "[ \t]+#+$", "") + if strFind(rest, "^#+$") then + rest = "" + end + return #hashes, rest +end + +local function isRule(s) + local t = strGsub(s, "[ \t]", "") + return #t >= 3 and (strFind(t, "^%-+$") or strFind(t, "^%*+$") or strFind(t, "^_+$")) ~= nil +end + +local function fenceOpen(s) + local sp, fence, info = strMatch(s, "^( *)(```+)(.*)$") + if not fence then + sp, fence, info = strMatch(s, "^( *)(~~~+)(.*)$") + end + if not fence then + return nil + end + if strSub(fence, 1, 1) == "`" and strFind(info, "`") then + return nil + end + return #sp, fence, strMatch(info, "^%s*(%S*)") +end + +local function fenceClose(s, fence) + local t = strMatch(s, "^ *(%S+)%s*$") + return t ~= nil and #t >= #fence and strFind(t, "^" .. strSub(fence, 1, 1) .. "+$") ~= nil +end + +-- Indent, content column, ordered flag, number and text of a list item line, or nil. +local function listMarker(s) + local sp, mark = strMatch(s, "^( *)([-*+])[ \t]*$") + if mark then + return #sp, #sp + 2, false, nil, "" + end + local gap, text + sp, mark, gap, text = strMatch(s, "^( *)([-*+])( +)(.*)$") + if mark then + local g = #gap + if g > 4 then + g = 1 + end + return #sp, #sp + 1 + g, false, nil, text + end + local num + sp, num, mark = strMatch(s, "^( *)(%d+)([.)])[ \t]*$") + if num and #num <= 9 then + return #sp, #sp + #num + 2, true, tonumber(num), "" + end + sp, num, mark, gap, text = strMatch(s, "^( *)(%d+)([.)])( +)(.*)$") + if num and #num <= 9 then + local g = #gap + if g > 4 then + g = 1 + end + return #sp, #sp + #num + 1 + g, true, tonumber(num), text + end + return nil +end + +local function splitCells(s) + s = strGsub(s, "^%s*|", "") + s = strGsub(s, "|%s*$", "") + local cells = {} + local cur = {} + local i = 1 + local n = #s + while i <= n do + local c = strSub(s, i, i) + if c == "\\" and strSub(s, i + 1, i + 1) == "|" then + cur[#cur + 1] = "|" + i = i + 2 + elseif c == "|" then + cells[#cells + 1] = tableConcat(cur) + cur = {} + i = i + 1 + else + cur[#cur + 1] = c + i = i + 1 + end + end + cells[#cells + 1] = tableConcat(cur) + for k = 1, #cells do + cells[k] = strMatch(cells[k], "^%s*(.-)%s*$") + end + return cells +end + +-- Column alignments of a table delimiter row such as | --- | :-: | --: |, or nil. +local function delimiterRow(s) + if not strFind(s, "|", 1, true) or not strFind(s, "%-") then + return nil + end + local cells = splitCells(s) + local align = {} + for k, c in ipairs(cells) do + if not strFind(c, "^:?%-+:?$") then + return nil + end + local l = strSub(c, 1, 1) == ":" + local r = strSub(c, -1) == ":" + align[k] = (l and r and "center") or (r and "right") or "left" + end + return align +end + +local function isQuoteLine(s) + return strFind(s, "^ ? ? ?>") ~= nil +end + +local function stripQuote(s) + return (strGsub(s, "^ ? ? ?> ?", "")) +end + +local function startsBlock(s, nextLine) + if atxHeading(s) or isRule(s) or fenceOpen(s) or isQuoteLine(s) or listMarker(s) then + return true + end + if strFind(s, "|", 1, true) and nextLine and delimiterRow(nextLine) then + return true + end + return false +end + +-- Paragraph lines become one string: a soft break is a space, a hard break (trailing +-- backslash or two spaces) a newline for the inline scanner. +local function joinLines(lines) + local out = {} + local n = #lines + for k = 1, n do + local l = lines[k] + local body = strGsub(l, "%s+$", "") + if k < n then + local hard = strFind(l, " $") ~= nil + if not hard and strFind(body, "\\$") then + hard = true + body = strSub(body, 1, -2) + end + out[#out + 1] = body + out[#out + 1] = hard and "\n" or " " + else + out[#out + 1] = body + end + end + return tableConcat(out) +end + +local parseBlocks + +-- Parses `lines` into `blocks`. `quote` is the blockquote depth of everything found. +parseBlocks = function(lines, blocks, quote, refs) + local n = #lines + local i = 1 + -- Open list items, innermost last: { col = content column, ordered, count }. + local stack = {} + -- Lists that a new item at each depth would continue: { ordered, count }. + local listAt = {} + -- The paragraph or item still collecting text lines. + local open = nil + local blankSeen = false + + local function closeOpen() + open = nil + end + + local function add(block) + block.quote = quote + blocks[#blocks + 1] = block + return block + end + + local function closeListsBelow(level) + for k = #listAt, level + 1, -1 do + listAt[k] = nil + end + end + + while i <= n do + local line = expandTabs(lines[i]) + if isBlank(line) then + closeOpen() + blankSeen = true + i = i + 1 + else + local ind = indentOf(line) + local nextLine = lines[i + 1] and expandTabs(lines[i + 1]) or nil + local t0 = strSub(line, ind + 1) + local lazy = open ~= nil and not blankSeen and not startsBlock(t0, nextLine) and not strFind(t0, "^=+%s*$") + if lazy then + open.lines[#open.lines + 1] = strSub(line, ind + 1) + i = i + 1 + else + while #stack > 0 and ind < stack[#stack].col do + stack[#stack] = nil + end + local level = #stack + local base = level > 0 and stack[level].col or 0 + local s = strSub(line, base + 1) + local t = strSub(s, indentOf(s) + 1) + + local fenceIndent, fence, lang = fenceOpen(s) + local hLevel, hText = atxHeading(t) + local _, mCol, ordered, number, mText = listMarker(s) + + if fence then + closeOpen() + closeListsBelow(level) + local code = {} + i = i + 1 + while i <= n do + local l = expandTabs(lines[i]) + if fenceClose(strSub(l, base + 1), fence) then + i = i + 1 + break + end + -- Lines keep their own indentation past the fence's. + local strip = mathMin(base + fenceIndent, indentOf(l)) + code[#code + 1] = strSub(l, strip + 1) + i = i + 1 + end + add({ kind = "code", lines = code, lang = lang, indent = level }) + blankSeen = false + elseif hLevel then + closeOpen() + closeListsBelow(level) + add({ kind = "heading", level = hLevel, lines = { hText }, indent = level }) + blankSeen = false + i = i + 1 + elseif open and open.kind == "para" and not blankSeen and (strFind(t, "^=+%s*$") or strFind(t, "^%-+%s*$")) then + -- A setext underline turns the paragraph above into a heading. + open.kind = "heading" + open.level = strFind(t, "^=") and 1 or 2 + closeOpen() + i = i + 1 + elseif isRule(t) then + closeOpen() + closeListsBelow(level) + add({ kind = "rule", indent = level }) + blankSeen = false + i = i + 1 + elseif isQuoteLine(s) then + closeOpen() + closeListsBelow(level) + local inner = {} + while i <= n do + local l = expandTabs(lines[i]) + local rel = strSub(l, base + 1) + if isQuoteLine(rel) then + inner[#inner + 1] = stripQuote(rel) + elseif not isBlank(l) and #inner > 0 and not startsBlock(strSub(l, indentOf(l) + 1), nil) and not isBlank(inner[#inner]) then + -- Lazy continuation of the quoted paragraph. + inner[#inner + 1] = strSub(l, indentOf(l) + 1) + else + break + end + i = i + 1 + end + local sub = {} + parseBlocks(inner, sub, quote + 1, refs) + for k = 1, #sub do + sub[k].indent = (sub[k].indent or 0) + level + blocks[#blocks + 1] = sub[k] + end + blankSeen = false + elseif strFind(t, "|", 1, true) and nextLine and delimiterRow(strSub(nextLine, base + 1)) then + closeOpen() + closeListsBelow(level) + local align = delimiterRow(strSub(nextLine, base + 1)) + local header = splitCells(t) + local rows = {} + i = i + 2 + while i <= n do + local l = expandTabs(lines[i]) + if isBlank(l) or not strFind(l, "|", 1, true) then + break + end + rows[#rows + 1] = splitCells(strSub(l, indentOf(l) + 1)) + i = i + 1 + end + add({ kind = "table", header = header, align = align, rows = rows, indent = level }) + blankSeen = false + elseif mCol then + closeOpen() + local newLevel = level + 1 + closeListsBelow(newLevel) + local list = listAt[newLevel] + local loose = false + if list and list.ordered == ordered then + list.count = list.count + 1 + loose = blankSeen + else + list = { ordered = ordered, count = number or 1 } + listAt[newLevel] = list + end + stack[newLevel] = { col = base + mCol } + local item = add({ + kind = "item", + level = newLevel, + ordered = ordered, + number = list.count, + lines = { mText }, + loose = loose, + indent = newLevel, + }) + open = item + blankSeen = false + i = i + 1 + else + local id, url = strMatch(t, "^%[([^%]]+)%]:%s*(%S+)") + if id and not open then + refs[strLower(id)] = url + i = i + 1 + else + closeListsBelow(level) + local para = add({ kind = "para", lines = { t }, indent = level, loose = blankSeen and level > 0 }) + open = para + blankSeen = false + i = i + 1 + end + end + end + end + end +end + +-- Parses a whole document. Blocks carry `inline` (runs) where they hold text, `indent` +-- (enclosing list depth) and `quote` (blockquote depth). `doc.headings` lists every +-- heading with its block index; `doc.chapterLevel` is the shallowest level used, the +-- one a table of contents should be built from. +function Markdown.parse(text) + text = text or "" + if strSub(text, 1, 3) == "\239\187\191" then + text = strSub(text, 4) + end + text = strGsub(text, "", "") + local lines = {} + for line in string.gmatch(text .. "\n", "(.-)\r?\n") do + lines[#lines + 1] = line + end + + local refs = {} + local blocks = {} + parseBlocks(lines, blocks, 0, refs) + + local headings = {} + local chapterLevel = 7 + for k = 1, #blocks do + local b = blocks[k] + if b.lines and b.kind ~= "code" then + b.text = joinLines(b.lines) + b.inline = parseInline(b.text, refs) + b.lines = nil + end + if b.kind == "heading" then + b.text = Markdown.plainText(b.inline) + headings[#headings + 1] = { level = b.level, text = b.text, block = k } + if b.level < chapterLevel then + chapterLevel = b.level + end + elseif b.kind == "table" then + b.headerInline = {} + for c = 1, #b.header do + b.headerInline[c] = parseInline(b.header[c], refs) + end + b.rowsInline = {} + for r = 1, #b.rows do + local row = {} + for c = 1, #b.header do + row[c] = parseInline(b.rows[r][c] or "", refs) + end + b.rowsInline[r] = row + end + end + end + if chapterLevel == 7 then + chapterLevel = 1 + end + + return { blocks = blocks, headings = headings, chapterLevel = chapterLevel } +end + +---------------------------------------------------------------------------------------- +-- Layout +---------------------------------------------------------------------------------------- + +-- Sizes and colours at a given UI scale. Callers override what they need to and must +-- supply ctx.fonts = { regular, bold, mono } and ctx.width. +function Markdown.defaultContext(scale) + scale = scale or 1 + local function px(v) + return mathFloor(v * scale) + end + return { + scale = scale, + width = 600, + fonts = nil, + fs = { + body = px(15), + code = px(14), + h = { px(18), px(16), px(15), px(15), px(14), px(14) }, + }, + -- Row height as a multiple of the font size; 15 px text sits on a 19 px grid. + lineMul = 1.27, + listIndent = px(24), + markerGap = px(14), + numberGap = px(6), + quoteIndent = px(14), + quoteBar = px(3), + cellPad = px(10), + tableRowPad = px(3), + gapPara = px(6), + gapHeading = px(8), + gapCode = px(4), + codePad = px(5), + gapRule = px(6), + corner = px(3), + italicShear = 0.18, + bullets = { "\226\128\162", "\226\151\166", "\226\150\170" }, -- • ◦ ▪ + colors = { + text = { 0.8, 0.77, 0.74, 1 }, + heading = { 1, 1, 1, 1 }, + bold = { 0.95, 0.94, 0.92, 1 }, + code = { 0.85, 0.9, 0.95, 1 }, + codeBg = { 1, 1, 1, 0.07 }, + codeBlockBg = { 0, 0, 0, 0.22 }, + link = { 0.55, 0.75, 1, 1 }, + strike = { 0.58, 0.56, 0.54, 1 }, + marker = { 0.6, 0.58, 0.55, 1 }, + quoteBar = { 1, 1, 1, 0.22 }, + quoteText = { 0.68, 0.66, 0.64, 1 }, + rule = { 1, 1, 1, 0.15 }, + tableBg = { 1, 1, 1, 0.04 }, + tableHeadBg = { 1, 1, 1, 0.07 }, + tableStripe = { 1, 1, 1, 0.03 }, + tableLine = { 1, 1, 1, 0.22 }, + }, + } +end + +-- Word widths per face at size 1, so a page of repeated words measures once. +local widthCache = setmetatable({}, { __mode = "k" }) + +local function textWidth(font, s) + local cache = widthCache[font] + if not cache then + cache = {} + widthCache[font] = cache + end + local w = cache[s] + if not w then + w = font:GetTextWidth(s) + cache[s] = w + end + return w +end + +local function fontFor(run, ctx, forceBold) + local fonts = ctx.fonts + if run.c and fonts.mono then + return fonts.mono, "mono" + elseif (run.b or forceBold) and fonts.bold then + return fonts.bold, "bold" + end + return fonts.regular, "regular" +end + +local function colorFor(run, ctx, base) + local colors = ctx.colors + if run.link then + return colors.link + elseif run.c then + return colors.code + elseif run.s then + return colors.strike + elseif run.b and base == colors.text then + return colors.bold + end + return base +end + +-- Splits text into UTF-8 characters, for words wider than the whole column. +local function utf8Chars(s) + local out = {} + for ch in string.gmatch(s, "[%z\1-\127\194-\244][\128-\191]*") do + out[#out + 1] = ch + end + return out +end + +-- Wraps `runs` into rows of segments no wider than `width`. Segment x is relative to +-- the text column. Returns the rows as arrays of segments with `w` (used width). +local function wrapRuns(runs, width, fs, ctx, baseColor, forceBold) + local rows = {} + local row = { w = 0 } + rows[1] = row + local pendingSpace = false + + local function newRow() + row = { w = 0 } + rows[#rows + 1] = row + pendingSpace = false + end + + local function place(text, w, run, font, face, color, joinSpace, spaceW) + local last = row[#row] + if joinSpace and last and last.run == run then + last.text = last.text .. " " .. text + last.w = last.w + spaceW + w + row.w = row.w + spaceW + w + else + -- Whole pixels, so glyphs do not land between them after a style change. + local x = mathFloor(row.w + (joinSpace and spaceW or 0)) + row[#row + 1] = { + x = x, + w = w, + text = text, + font = font, + face = face, + fs = fs, + color = color, + run = run, + code = run.c, + strike = run.s, + link = run.link, + italic = run.i and ctx.italicShear ~= 0, + } + row.w = x + w + end + end + + for k = 1, #runs do + local run = runs[k] + if run.br then + newRow() + else + local font, face = fontFor(run, ctx, forceBold) + local color = colorFor(run, ctx, baseColor) + local spaceW = textWidth(font, " ") * fs + local text = run.text + local p = 1 + local n = #text + while p <= n do + local a, b = strFind(text, "^%s+", p) + if a then + if row.w > 0 then + pendingSpace = true + end + p = b + 1 + else + a, b = strFind(text, "^%S+", p) + local tok = strSub(text, a, b) + p = b + 1 + local w = textWidth(font, tok) * fs + if row.w == 0 then + if w > width then + -- Too wide for a whole row: break it by character. + local chars = utf8Chars(tok) + local piece = "" + local pieceW = 0 + for c = 1, #chars do + local cw = textWidth(font, chars[c]) * fs + if pieceW + cw > width and piece ~= "" then + place(piece, pieceW, run, font, face, color, false, 0) + newRow() + piece = "" + pieceW = 0 + end + piece = piece .. chars[c] + pieceW = pieceW + cw + end + place(piece, pieceW, run, font, face, color, false, 0) + else + place(tok, w, run, font, face, color, false, 0) + end + elseif pendingSpace then + if row.w + spaceW + w <= width then + place(tok, w, run, font, face, color, true, spaceW) + else + newRow() + if w > width then + local chars = utf8Chars(tok) + local piece = "" + local pieceW = 0 + for c = 1, #chars do + local cw = textWidth(font, chars[c]) * fs + if pieceW + cw > width and piece ~= "" then + place(piece, pieceW, run, font, face, color, false, 0) + newRow() + piece = "" + pieceW = 0 + end + piece = piece .. chars[c] + pieceW = pieceW + cw + end + place(piece, pieceW, run, font, face, color, false, 0) + else + place(tok, w, run, font, face, color, false, 0) + end + end + else + -- Glued to the previous token (a style change mid-word). + if row.w + w <= width or row.w == 0 then + place(tok, w, run, font, face, color, false, 0) + else + newRow() + place(tok, w, run, font, face, color, false, 0) + end + end + pendingSpace = false + end + end + end + end + return rows +end + +-- The measured width of runs on one line, for table columns. +local function runsWidth(runs, fs, ctx) + local w = 0 + for k = 1, #runs do + local run = runs[k] + if not run.br then + local font = fontFor(run, ctx) + w = w + textWidth(font, run.text) * fs + end + end + return w +end + +-- Lays a document out against ctx.width. Each row: h (advance), pad (space above the +-- text box), box (text box height), base (baseline below the row top), x (left indent), +-- segs, plus block, kind, quote, marker and decoration flags. Blocks get `firstRow`. +function Markdown.layout(doc, ctx) + local rows = {} + local blocks = doc.blocks + local colors = ctx.colors + local lineMul = ctx.lineMul + + local function boxFor(fs) + return mathFloor(fs * lineMul + 0.5) + end + + local function addRow(block, kind, fs, xIndent, segs, marker) + local box = boxFor(fs) + local row = { + h = box, + pad = 0, + box = box, + base = box, + x = xIndent, + w = segs and segs.w or 0, + segs = segs or {}, + block = block, + kind = kind, + quote = block.quote or 0, + marker = marker, + } + rows[#rows + 1] = row + return row + end + + local function padTop(row, gap) + row.pad = row.pad + gap + row.base = row.base + gap + row.h = row.h + gap + end + + local function gapBottom(row, gap) + row.h = row.h + gap + end + + for bi = 1, #blocks do + local b = blocks[bi] + local prev = blocks[bi - 1] + local nxt = blocks[bi + 1] + local xIndent = (b.indent or 0) * ctx.listIndent + (b.quote or 0) * ctx.quoteIndent + local width = mathMax(ctx.width - xIndent, ctx.listIndent) + local first = #rows + 1 + local baseColor = (b.quote or 0) > 0 and colors.quoteText or colors.text + + if b.kind == "heading" then + local fs = ctx.fs.h[mathMin(b.level, 6)] + local wrapped = wrapRuns(b.inline, width, fs, ctx, colors.heading, true) + for r = 1, #wrapped do + addRow(b, "heading", fs, xIndent, wrapped[r]) + end + if prev then + padTop(rows[first], ctx.gapHeading) + end + gapBottom(rows[#rows], mathFloor(ctx.gapPara * 0.34)) + elseif b.kind == "para" then + local fs = ctx.fs.body + local wrapped = wrapRuns(b.inline, width, fs, ctx, baseColor) + for r = 1, #wrapped do + addRow(b, "text", fs, xIndent, wrapped[r]) + end + if b.loose then + padTop(rows[first], ctx.gapPara) + end + if not (nxt and nxt.kind == "item" and nxt.level == (b.indent or 0)) then + gapBottom(rows[#rows], ctx.gapPara) + end + elseif b.kind == "item" then + local fs = ctx.fs.body + local wrapped = wrapRuns(b.inline, width, fs, ctx, baseColor) + local marker + if b.ordered then + local label = tostring(b.number) .. "." + marker = { + text = label, + font = ctx.fonts.regular, + face = "regular", + fs = fs, + color = colors.marker, + x = -ctx.numberGap - mathFloor(textWidth(ctx.fonts.regular, label) * fs + 0.5), + } + else + local glyph = ctx.bullets[((b.level - 1) % #ctx.bullets) + 1] + marker = { + text = glyph, + font = ctx.fonts.mono or ctx.fonts.regular, + face = ctx.fonts.mono and "mono" or "regular", + fs = fs, + color = colors.marker, + x = -ctx.markerGap, + } + end + for r = 1, #wrapped do + addRow(b, "text", fs, xIndent, wrapped[r], r == 1 and marker or nil) + end + if b.loose then + padTop(rows[first], ctx.gapPara) + end + -- No gap before the next item of the same list, a nested one, or the parent + -- list resuming; a different list or anything else gets one. + local continues = nxt + and ((nxt.kind == "item" and (nxt.level ~= b.level or nxt.ordered == b.ordered)) or (nxt.kind == "para" and (nxt.indent or 0) > 0)) + if not continues then + gapBottom(rows[#rows], ctx.gapPara) + end + elseif b.kind == "code" then + local fs = ctx.fs.code + local font = ctx.fonts.mono or ctx.fonts.regular + local run = { c = true, text = "" } + local lines = b.lines + if #lines == 0 then + lines = { "" } + end + for l = 1, #lines do + local text = strGsub(lines[l], "%s+$", "") + local segs = { w = 0 } + if text ~= "" then + -- Code keeps its spacing, so it wraps by character, not by word. + local chars = utf8Chars(text) + local piece = {} + local pieceW = 0 + local innerW = width - ctx.codePad * 2 + for c = 1, #chars do + local cw = textWidth(font, chars[c]) * fs + if pieceW + cw > innerW and #piece > 0 then + segs[#segs + 1] = { x = ctx.codePad, w = pieceW, text = tableConcat(piece), font = font, face = "mono", fs = fs, color = colors.code, run = run } + segs.w = pieceW + addRow(b, "code", fs, xIndent, segs).codeBlock = bi + segs = { w = 0 } + piece = {} + pieceW = 0 + end + piece[#piece + 1] = chars[c] + pieceW = pieceW + cw + end + segs[#segs + 1] = { x = ctx.codePad, w = pieceW, text = tableConcat(piece), font = font, face = "mono", fs = fs, color = colors.code, run = run } + segs.w = pieceW + end + addRow(b, "code", fs, xIndent, segs).codeBlock = bi + end + padTop(rows[first], ctx.gapCode + ctx.codePad) + gapBottom(rows[#rows], ctx.gapCode + ctx.codePad) + elseif b.kind == "rule" then + local row = addRow(b, "rule", 0, xIndent, nil) + row.pad = ctx.gapRule + row.box = 1 + row.base = ctx.gapRule + 1 + row.h = ctx.gapRule * 2 + 1 + row.rule = true + elseif b.kind == "table" then + local fs = ctx.fs.body + local ncol = #b.header + local colW = {} + local headerRuns = {} + for c = 1, ncol do + local runs = {} + for k = 1, #b.headerInline[c] do + local r = b.headerInline[c][k] + runs[k] = r.br and r or { text = r.text, b = true, i = r.i, c = r.c, s = r.s, link = r.link } + end + headerRuns[c] = runs + colW[c] = runsWidth(runs, fs, ctx) + end + for r = 1, #b.rowsInline do + for c = 1, ncol do + colW[c] = mathMax(colW[c], runsWidth(b.rowsInline[r][c], fs, ctx)) + end + end + for c = 1, ncol do + colW[c] = mathFloor(colW[c] + 0.999) + end + local total = 0 + for c = 1, ncol do + total = total + colW[c] + ctx.cellPad * 2 + end + -- A table wider than the column shrinks to fit rather than running under the + -- scrollbar. + if total > width and total > 0 then + local f = width / total + fs = mathMax(mathFloor(fs * f), mathFloor(ctx.fs.body * 0.6)) + for c = 1, ncol do + colW[c] = mathFloor(colW[c] * fs / ctx.fs.body) + end + total = 0 + for c = 1, ncol do + total = total + colW[c] + ctx.cellPad * 2 + end + end + local colX = {} + local x = 0 + for c = 1, ncol do + colX[c] = x + x = x + colW[c] + ctx.cellPad * 2 + end + + local function cellSegs(cellRuns, c, segs) + local runs = cellRuns[c] or {} + local cw = runsWidth(runs, fs, ctx) + local align = b.align[c] or "left" + local sx = colX[c] + ctx.cellPad + if align == "center" then + sx = sx + mathFloor((colW[c] - cw) * 0.5) + elseif align == "right" then + sx = sx + mathFloor(colW[c] - cw) + end + for k = 1, #runs do + local run = runs[k] + if not run.br then + local font, face = fontFor(run, ctx) + local w = textWidth(font, run.text) * fs + segs[#segs + 1] = { + x = sx, + w = w, + text = run.text, + font = font, + face = face, + fs = fs, + color = colorFor(run, ctx, baseColor), + run = run, + code = run.c, + strike = run.s, + link = run.link, + italic = run.i and ctx.italicShear ~= 0, + } + sx = mathFloor(sx + w + 0.5) + end + end + end + + local segs = { w = total } + for c = 1, ncol do + cellSegs(headerRuns, c, segs) + end + local head = addRow(b, "table", fs, xIndent, segs) + head.tableHead = true + head.tableW = total + head.tableBlock = bi + head.tableRow = 0 + padTop(head, ctx.tableRowPad) + gapBottom(head, ctx.tableRowPad) + for r = 1, #b.rowsInline do + segs = { w = total } + for c = 1, ncol do + cellSegs(b.rowsInline[r], c, segs) + end + local row = addRow(b, "table", fs, xIndent, segs) + row.tableW = total + row.tableBlock = bi + row.tableRow = r + padTop(row, ctx.tableRowPad) + gapBottom(row, ctx.tableRowPad) + end + gapBottom(rows[#rows], ctx.gapPara) + end + + b.firstRow = first + b.lastRow = #rows + end + + return rows +end + +-- Total height of rows i..j when i is the first row shown (its top padding is not +-- drawn, so the text starts flush with the top edge). +function Markdown.rowsHeight(rows, i, j) + local h = 0 + for k = i, j do + h = h + rows[k].h + end + if rows[i] then + h = h - rows[i].pad + end + return h +end + +---------------------------------------------------------------------------------------- +-- Drawing +---------------------------------------------------------------------------------------- + +local glColor = gl.Color +local glRect = gl.Rect +local glPushMatrix = gl.PushMatrix +local glPopMatrix = gl.PopMatrix +local glTranslate = gl.Translate +local glMultMatrix = gl.MultMatrix + +-- Paints rows first..last of `rows` with the text column's left edge at `x` and the +-- first row's text box starting at `top`. ctx.rectRound(x1, y1, x2, y2, cs, tl, tr, br, +-- bl, color) draws the rounded fills (FlowUI's RectRound); without it plain rects are +-- used. +function Markdown.draw(rows, first, last, x, top, ctx) + local colors = ctx.colors + local rectRound = ctx.rectRound + local width = ctx.width + + local function fill(x1, y1, x2, y2, color, cs, tl, tr, br, bl) + if rectRound and cs and cs > 0 then + rectRound(x1, y1, x2, y2, cs, tl or 1, tr or 1, br or 1, bl or 1, color) + else + glColor(color) + glRect(x1, y1, x2, y2) + end + end + + -- Pass 1: row tops and baselines. The first row's padding is eaten so the text + -- starts at `top` whatever block it belongs to. + local tops = {} + local bases = {} + local y = top + (rows[first] and rows[first].pad or 0) + for i = first, last do + local row = rows[i] + tops[i] = y + bases[i] = y - row.base + y = y - row.h + end + + -- Pass 2: block backgrounds. Code boxes span every contiguous run of their rows on + -- this page; quote bars and rules are per row and join up seamlessly. + local groupStart, groupBlock + local function flushCode(i) + if groupStart then + local r1 = rows[groupStart] + local yTop = tops[groupStart] - (groupStart == r1.block.firstRow and ctx.gapCode or 0) + if yTop > top then + yTop = top + end + local rN = rows[i] + local yBot = tops[i] - rN.h + (i == rN.block.lastRow and ctx.gapCode or 0) + fill(x + r1.x, yBot, x + width, yTop, colors.codeBlockBg, ctx.corner) + groupStart = nil + groupBlock = nil + end + end + for i = first, last do + local row = rows[i] + if row.codeBlock then + if groupBlock ~= row.codeBlock then + if groupStart then + flushCode(i - 1) + end + groupStart = i + groupBlock = row.codeBlock + end + elseif groupStart then + flushCode(i - 1) + end + end + if groupStart then + flushCode(last) + end + + -- Tables: a faint card under every contiguous run of a table's rows on this page, a + -- band behind the header with a line beneath it, and every other body row shaded. + -- Bands run from the row's top to just under its text, so the trailing gap after + -- the table stays clear. + local function bandTop(i) + local t = tops[i] + if t > top then + t = top + end + return t + end + local function bandBottom(i) + local r = rows[i] + return tops[i] - r.pad - r.box - ctx.tableRowPad + end + local tStart, tBlock + local function flushTable(i) + if tStart then + local r1 = rows[tStart] + local x1 = x + r1.x + local x2 = x1 + r1.tableW + fill(x1, bandBottom(i), x2, bandTop(tStart), colors.tableBg, ctx.corner) + for k = tStart, i do + local r = rows[k] + local topCorner = k == tStart and 1 or 0 + local bottomCorner = k == i and 1 or 0 + if r.tableRow == 0 then + fill(x1, bandBottom(k), x2, bandTop(k), colors.tableHeadBg, ctx.corner, topCorner, topCorner, bottomCorner, bottomCorner) + fill(x1, bandBottom(k), x2, bandBottom(k) + 1, colors.tableLine) + elseif r.tableRow % 2 == 0 then + fill(x1, bandBottom(k), x2, bandTop(k), colors.tableStripe, ctx.corner, topCorner, topCorner, bottomCorner, bottomCorner) + end + end + tStart = nil + tBlock = nil + end + end + for i = first, last do + local row = rows[i] + if row.tableBlock then + if tBlock ~= row.tableBlock then + if tStart then + flushTable(i - 1) + end + tStart = i + tBlock = row.tableBlock + end + elseif tStart then + flushTable(i - 1) + end + end + if tStart then + flushTable(last) + end + + for i = first, last do + local row = rows[i] + local rowTop = tops[i] + local rowBottom = rowTop - row.h + if row.quote > 0 then + for d = 1, row.quote do + local bx = x + row.x - (row.quote - d + 1) * ctx.quoteIndent + fill(bx, rowBottom, bx + ctx.quoteBar, rowTop, colors.quoteBar) + end + end + if row.rule then + local ly = rowTop - ctx.gapRule + fill(x + row.x, ly - 1, x + width, ly, colors.rule) + end + end + + -- Pass 3: inline decorations under the glyphs. + for i = first, last do + local row = rows[i] + local segs = row.segs + local base = bases[i] + for k = 1, #segs do + local seg = segs[k] + local sx = x + row.x + seg.x + local ex = mathFloor(sx + seg.w + 0.5) + if seg.code and row.kind ~= "code" then + local pad = mathFloor(seg.fs * 0.15) + fill(sx - pad, base - mathFloor(seg.fs * 0.22), ex + pad, base + mathFloor(seg.fs * 0.82), colors.codeBg, mathFloor(ctx.corner * 0.66)) + end + if seg.link then + fill(sx, base - 2, ex, base - 1, seg.color) + end + if seg.strike then + local ly = base + mathFloor(seg.fs * 0.3) + fill(sx, ly, ex, ly + 1, seg.color) + end + end + end + + -- Pass 4: glyphs, one batch per face. Italic segments are sheared, so they print + -- on their own outside the batches. + local italics = {} + local faces = { "regular", "bold", "mono" } + for f = 1, #faces do + local face = faces[f] + local font = ctx.fonts[face] + if font then + font:Begin() + for i = first, last do + local row = rows[i] + local base = bases[i] + local marker = row.marker + if marker and marker.face == face then + font:SetTextColor(marker.color) + font:Print(marker.text, x + row.x + marker.x, base, marker.fs, "n") + end + local segs = row.segs + for k = 1, #segs do + local seg = segs[k] + if seg.face == face then + if seg.italic then + italics[#italics + 1] = { seg = seg, x = x + row.x + seg.x, y = base } + else + font:SetTextColor(seg.color) + font:Print(seg.text, x + row.x + seg.x, base, seg.fs, "n") + end + end + end + end + font:End() + end + end + + local shear = ctx.italicShear + for k = 1, #italics do + local it = italics[k] + local seg = it.seg + glPushMatrix() + glTranslate(it.x, it.y, 0) + glMultMatrix(1, 0, 0, 0, shear, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) + seg.font:SetTextColor(seg.color) + seg.font:Print(seg.text, 0, 0, seg.fs, "n") + glPopMatrix() + end + glColor(1, 1, 1, 1) +end + +return Markdown diff --git a/luaui/Widgets/gui_changelog_info.lua b/luaui/Widgets/gui_changelog_info.lua index 624c26bc002..976af9bc1ce 100644 --- a/luaui/Widgets/gui_changelog_info.lua +++ b/luaui/Widgets/gui_changelog_info.lua @@ -12,6 +12,8 @@ function widget:GetInfo() } end +local Markdown = VFS.Include("luaui/Include/markdown.lua") + -- Localized functions for performance local mathFloor = math.floor local mathMax = math.max @@ -28,11 +30,9 @@ local math_isInRect = math.isInRect local vsx, vsy = spGetViewGeometry() -local changelogFile = VFS.LoadFile("changelog.txt") --- Convert tabs to 4 spaces -changelogFile = string.gsub(changelogFile, "\t", " ") -local changelogFileHash = VFS.CalculateHash(changelogFile, 0) -local changelogFileLength = string.len(changelogFile) +local changelogFile = VFS.LoadFile("changelog.md") +local changelogFileHash = changelogFile and VFS.CalculateHash(changelogFile, 0) or "" +local changelogFileLength = changelogFile and string.len(changelogFile) or 0 local lastviewedHash = "" local lastviewedChangelogLength = 0 @@ -50,9 +50,12 @@ local screenX = mathFloor((vsx * centerPosX) - (screenWidth / 2)) local screenY = mathFloor((vsy * centerPosY) + (screenHeight / 2)) local widgetScale = (vsy / 1080) -local versions = {} -- version k -> index of the changelog line its heading is on -local versionLabels = {} -- version k -> the heading text shown in the month column -local changelogLines = {} +-- The changelog is markdown: the month column lists its chapter headings (the +-- shallowest heading level the file uses), and the text is rendered by the shared +-- markdown include, so bold, lists, code and links look the way they do anywhere else. +local doc +local versions = {} -- chapter k -> index of the row its heading starts on +local versionLabels = {} -- chapter k -> the heading text shown in the month column local showOnceMore = false -- used because of GUI shader delay @@ -65,7 +68,7 @@ local UiScroller ---@type function local Highlight local elementCorner -local font, loadedFontSize +local font, fontBold, fontMono local panelList, sidebarList, backgroundGuishader, show local titleText = "" @@ -94,15 +97,8 @@ local metrics = { sidebarDrop = 8, sidebarW = 200, barW = 14, - -- Text rows: the heading size plus a separator, shared by every kind of line so the - -- scrollbar can count rows. - lineH = 19, - fsTitle = 17, - fsDate = 13, - fsLine = 15, - -- Body lines and dates sit this far in from the headings; bullet text a further step. - bodyX = 9, - bulletX = 26, + -- Rows the wheel moves per notch. + wheelRows = 3, -- Corner radii, taken from FlowUI's so the panel rounds like the rest of the UI. csSmall = 2, csPanel = 4, @@ -117,7 +113,6 @@ local look = { -- strength it gives a plain row. rowHoverOpacity = 0.14, colorTitle = { 1, 1, 1, 1 }, - colorDate = { 0.66, 0.88, 0.66, 1 }, colorLine = { 0.8, 0.77, 0.74, 1 }, } local colorText = "\255\235\235\235" @@ -130,14 +125,16 @@ local listX1 = 0 local listRight = 0 local barX1 = 0 --- One entry per changelog line: the rows it wraps into, where they print and how, so --- the draw loop and the scroll math count the same rows. -local layout = {} --- Rows above each line, so the scrollbar knows where a line sits in the whole text. -local lineOff = {} -local totalRows = 0 -local startLine = 1 --- The highest startLine that still fills the band with whole entries. +-- The markdown layout: sizes, faces and colours the include renders with. Rebuilt on +-- resize, since every size in it is in pixels. +local ctx +-- The laid-out text, one entry per wrapped row, and where each row's top sits in the +-- whole text so the scrollbar can place its thumb exactly. +local rows = {} +local rowTop = {} +local totalH = 0 +local startRow = 1 +-- The highest startRow that still fills the band. local maxStart = 1 local dragging = false -- Month column state: the entry under the cursor and the one lit as current. The @@ -167,14 +164,14 @@ local function deleteGuishader() end end --- The version whose section holds the given line: the last heading at or above it. -local function versionAt(line) +-- The chapter whose section holds the given row: the last heading at or above it. +local function versionAt(row) if not versions[1] then return nil end local k = 1 for i = 2, #versions do - if versions[i] <= line then + if versions[i] <= row then k = i else break @@ -183,19 +180,19 @@ local function versionAt(line) return k end --- Moves the text so the given line is the first one shown. The current month follows --- the top line unless a click chose one explicitly, in which case that one stays lit +-- Moves the text so the given row is the first one shown. The current month follows +-- the top row unless a click chose one explicitly, in which case that one stays lit -- until the text is scrolled again. -local function setStartLine(n, chosen) +local function setStartRow(n, chosen) n = mathMax(1, mathMin(maxStart, n)) - if n ~= startLine then - startLine = n + if n ~= startRow then + startRow = n if panelList then glDeleteList(panelList) panelList = nil end end - selectedIdx = chosen or versionAt(startLine) + selectedIdx = chosen or versionAt(startRow) end -- Cursor height in the band mapped straight onto the scroll range, as the keybind @@ -207,87 +204,96 @@ local function scrollFromY(y) elseif f > 1 then f = 1 end - setStartLine(1 + mathFloor(f * (maxStart - 1) + 0.5)) + setStartRow(1 + mathFloor(f * (maxStart - 1) + 0.5)) +end + +-- The space a row takes below its text box: the gap to the next block. The last row +-- on a page may let that gap spill past the band, since nothing is drawn in it. +local function rowTail(row) + return row.h - row.pad - row.box end -local function splitRows(s) - local rows = {} - for row in string.gmatch(s .. "\n", "([^\n]*)\n") do - rows[#rows + 1] = row +-- The last row that fits on a page starting at `first`. The first row's top padding +-- is not drawn, so it does not count. +local function lastRowFrom(first) + local band = listTop - listBottom + local used = -rows[first].pad + local i = first + while rows[i] and used + rows[i].h - rowTail(rows[i]) <= band do + used = used + rows[i].h + i = i + 1 end - if #rows == 0 then - rows[1] = "" + return mathMax(first, i - 1) +end + +-- Shortens a heading to the month column's width, since the column clips nothing. +local function fitLabel(text) + local maxW = metrics.sidebarW - metrics.sidePad * 2 + if font:GetTextWidth(text) * metrics.catFs <= maxW then + return text + end + local chars = {} + for ch in string.gmatch(text, "[%z\1-\127\194-\244][\128-\191]*") do + chars[#chars + 1] = ch + end + local n = #chars + while n > 1 do + n = n - 1 + local short = table.concat(chars, "", 1, n) .. "..." + if font:GetTextWidth(short) * metrics.catFs <= maxW then + return short + end end - return rows + return "..." end --- Wraps every line once against the text width, so drawing prints ready rows and the --- scrollbar can measure the whole text instead of guessing one row per line. -local function layoutLines() - layout = {} - lineOff = {} +-- Lays the whole text out against the text width, so drawing prints ready rows and the +-- scrollbar can measure the text instead of guessing one row per line. +local function layoutRows() + ctx.width = listRight - listX1 - metrics.sidePad + rows = Markdown.layout(doc, ctx) + rowTop = {} local off = 0 - local fsLine = metrics.fsLine - local scaleToFont = loadedFontSize / fsLine - local textW = listRight - listX1 - metrics.sidePad - local wrapBullet = (textW - metrics.bodyX - metrics.bulletX) * scaleToFont - local wrapPlain = (textW - metrics.bodyX) * scaleToFont - - for i, line in ipairs(changelogLines) do - local entry - if - string.find(line, "^([0-9][0-9][/][0-9][0-9][/][0-9][0-9])") - or string.find(line, "^([0-9][/][0-9][0-9][/][0-9][0-9])") - then - -- date line - entry = { rows = { line }, x = metrics.bodyX, fs = metrics.fsDate, color = look.colorDate } - elseif string.find(line, "^# ") then - -- version line - entry = { rows = { string.sub(line, 3) }, x = 0, fs = metrics.fsTitle, color = look.colorTitle } - elseif string.find(line, "^(-)") then - -- bulletpointed line - local firstLetterPos = 2 - if string.find(line, "^(- )") then - firstLetterPos = 3 - end - local text = string.upper(string.sub(line, firstLetterPos, firstLetterPos)) - .. string.sub(line, firstLetterPos + 1) - entry = { - rows = splitRows((font:WrapText(text, wrapBullet))), - x = metrics.bodyX + metrics.bulletX, - fs = fsLine, - color = look.colorLine, - bullet = true, - } - else - entry = { - rows = splitRows((font:WrapText(line, wrapPlain))), - x = metrics.bodyX, - fs = fsLine, - color = look.colorLine, - } + for i = 1, #rows do + rowTop[i] = off + off = off + rows[i].h + end + totalH = off + + versions = {} + versionLabels = {} + for _, h in ipairs(doc.headings) do + if h.level == doc.chapterLevel then + versions[#versions + 1] = doc.blocks[h.block].firstRow + versionLabels[#versionLabels + 1] = fitLabel(h.text) + end + end + -- The first row always heads the column, so the latest entry is reachable even + -- when the file does not open with a heading. + if rows[1] and versions[1] ~= 1 then + local first = doc.blocks[1] + local label = first.inline and Markdown.plainText(first.inline) or "" + if label == "" then + label = "..." end - layout[i] = entry - lineOff[i] = off - off = off + #entry.rows + table.insert(versions, 1, 1) + table.insert(versionLabels, 1, fitLabel(label)) end - totalRows = off - -- Walked back from the end so the last page is full of whole entries, the way the - -- keybind editor finds its own last page. - local rowsFit = mathFloor((listTop - listBottom) / metrics.lineH) - local used = 0 - local i = #layout + -- Walked back from the end so the last page is full, the way the keybind editor + -- finds its own last page. The final row's trailing gap is not part of the text. + local band = listTop - listBottom + local i = #rows + local acc = rows[i] and -rowTail(rows[i]) or 0 while i > 0 do - local n = #layout[i].rows - if used + n > rowsFit then + if acc + rows[i].h - rows[i].pad > band then break end - used = used + n + acc = acc + rows[i].h i = i - 1 end maxStart = mathMax(1, i + 1) - setStartLine(startLine) + setStartRow(startRow) end -- Rebuilds every rect against the panel size. Whole pixels throughout, so glyph and @@ -314,12 +320,6 @@ local function setLayout() metrics.sidebarDrop = mathFloor(8 * s) metrics.sidebarW = mathFloor(200 * s) metrics.barW = mathFloor(14 * s) - metrics.fsTitle = mathFloor(17 * s) - metrics.fsDate = mathFloor(13 * s) - metrics.fsLine = mathFloor(15 * s) - metrics.lineH = metrics.fsTitle + mathFloor(2 * s) - metrics.bodyX = mathFloor(9 * s) - metrics.bulletX = mathFloor(26 * s) metrics.csPanel = mathFloor(elementCorner) metrics.csSmall = mathFloor(elementCorner * 0.66) @@ -330,6 +330,14 @@ local function setLayout() -- a clear gap short of it, so the bar sits in a channel rather than hugging the rows. barX1 = area.x2 - metrics.edgeInset - metrics.barW listRight = barX1 - metrics.listGap + + -- The markdown sizes scale with the panel; its palette follows the panel's look. + ctx = Markdown.defaultContext(s) + ctx.fonts = { regular = font, bold = fontBold, mono = fontMono } + ctx.rectRound = RectRound + ctx.corner = metrics.csSmall + ctx.colors.text = look.colorLine + ctx.colors.heading = look.colorTitle end -- The month column starts below where the text does, so the title above it is not @@ -430,33 +438,16 @@ local function drawPanel() font:Begin() font:SetTextColor(1, 1, 1, 1) font:Print(titleText, area.x1 + metrics.sidePad, area.y2 - metrics.titleY, metrics.titleFs, "ov") + font:End() - -- Whole entries only: the band can end mid-entry, and a row painted below it would + -- Whole rows only: the band can end mid-paragraph, and a row painted below it would -- be clipped by nothing. - local lineH = metrics.lineH - local rowsFit = mathFloor((listTop - listBottom) / lineH) - local r = 0 - local i = startLine - while layout[i] do - local e = layout[i] - local rows = e.rows - if r + #rows > rowsFit then - break - end - local x = listX1 + e.x - font:SetTextColor(e.color) - if e.bullet then - font:Print(" - ", listX1 + metrics.bodyX, listTop - (r + 1) * lineH, e.fs, "n") - end - for k = 1, #rows do - font:Print(rows[k], x, listTop - (r + k) * lineH, e.fs, "n") - end - r = r + #rows - i = i + 1 + if rows[startRow] then + Markdown.draw(rows, startRow, lastRowFrom(startRow), listX1, listTop, ctx) end - font:End() - UiScroller(barX1, listBottom, area.x2 - metrics.edgeInset, listTop, totalRows * lineH, lineOff[startLine] * lineH) + local pos = rows[startRow] and (rowTop[startRow] + rows[startRow].pad) or 0 + UiScroller(barX1, listBottom, area.x2 - metrics.edgeInset, listTop, totalH, pos) end function widget:ViewResize() @@ -468,7 +459,10 @@ function widget:ViewResize() screenX = mathFloor((vsx * centerPosX) - (screenWidth / 2)) screenY = mathFloor((vsy * centerPosY) + (screenHeight / 2)) - font, loadedFontSize = WG.fonts.getFont() + -- Bold text takes the heavier weight of the UI face; code takes the monospaced one. + font = WG.fonts.getFont() + fontBold = WG.fonts.getFont("fonts/Poppins-Medium.otf") + fontMono = WG.fonts.getFont(3) elementCorner = WG.FlowUI.elementCorner RectRound = WG.FlowUI.Draw.RectRound @@ -479,7 +473,7 @@ function widget:ViewResize() titleText = colorText .. BAR.I18N("ui.changelog.title") setLayout() - layoutLines() + layoutRows() dropLists() deleteGuishader() end @@ -550,7 +544,7 @@ function widget:MouseWheel(up, _value) return false end - setStartLine(startLine + (up and -3 or 3)) + setStartRow(startRow + (up and -metrics.wheelRows or metrics.wheelRows)) return true end @@ -568,7 +562,7 @@ local function mouseEvent(x, y, button, release) if not release and button == 1 then local i = sidebarIndexAt(x, y) if i then - setStartLine(versions[i], i) + setStartRow(versions[i], i) if playSounds then Spring.PlaySoundFile(buttonclick, 0.6, "ui") end @@ -625,20 +619,7 @@ function widget:Initialize() return lastviewedHash ~= changelogFileHash and lastviewedChangelogLength < changelogFileLength end - -- store changelog into array - changelogLines = string.lines(changelogFile) - - -- The first line always heads the column, so the latest entry is reachable even - -- when the file does not open with a heading. - local versionKey = 0 - for i, line in ipairs(changelogLines) do - if versionKey == 0 or string.match(line, "^# ") then - versionKey = versionKey + 1 - versions[versionKey] = i - versionLabels[versionKey] = string.match(line, "^# (.*)") or line - end - end - selectedIdx = versionAt(startLine) + doc = Markdown.parse(changelogFile) widget:ViewResize() end diff --git a/luaui/Widgets/logo_adjuster.lua b/luaui/Widgets/logo_adjuster.lua index 8f3a58714bb..6c9ce1956c9 100644 --- a/luaui/Widgets/logo_adjuster.lua +++ b/luaui/Widgets/logo_adjuster.lua @@ -40,10 +40,25 @@ local gameover = false local currentIcon = nil -- Track currently desired icon for periodic reapplication local faction = "_a" -if UnitDefs[Spring.GetTeamRulesParam(spGetMyTeamID(), "startUnit")].name == "corcom" then - faction = "_c" + +-- The "startUnit" rules param isn't set yet during pregame (and never for spectators), +-- so it can be nil or point at a nonexistent unitdef. Keep the previous faction then. +local function UpdateFaction() + local startUnit = Spring.GetTeamRulesParam(spGetMyTeamID(), "startUnit") + local unitDef = startUnit and UnitDefs[startUnit] + if not unitDef then + return false + end + local newFaction = unitDef.name == "corcom" and "_c" or "_a" + if newFaction == faction then + return false + end + faction = newFaction + return true end +UpdateFaction() + local mouseOffscreen = select(6, spGetMouseState()) local function SetIcon(path) @@ -64,13 +79,7 @@ function widget:Initialize() end function widget:GameStart() - local prevFaction = faction - if UnitDefs[Spring.GetTeamRulesParam(spGetMyTeamID(), "startUnit")].name == "corcom" then - faction = "_c" - else - faction = "_a" - end - if prevFaction ~= faction then + if UpdateFaction() then SetIcon(imgPrefix .. faction .. imageBattle) end end @@ -115,16 +124,8 @@ function widget:Update(dt) paused = false end end - else - local prevFaction = faction - if UnitDefs[Spring.GetTeamRulesParam(spGetMyTeamID(), "startUnit")].name == "corcom" then - faction = "_c" - else - faction = "_a" - end - if prevFaction ~= faction then - SetIcon(imgPrefix .. faction .. imageBattle) - end + elseif UpdateFaction() then + SetIcon(imgPrefix .. faction .. imageBattle) end previousGameFrame = gameFrame