diff --git a/.github/agents.md b/.github/agents.md index 63dce052e77..5d77151f28c 100644 --- a/.github/agents.md +++ b/.github/agents.md @@ -1,4 +1,4 @@ -# Beyond All Reason - Copilot Instructions +# Beyond All Reason - Agent Instructions Mixed script types: LuaUI widgets, LuaRules gadgets, BOS animation scripts, shaders, RmlUi documents, busted specs. @@ -17,10 +17,23 @@ Before editing code: Keep patches narrowly scoped and easy to review. Large cross-subsystem refactors need an explicit request. +Before you hand work over: + +Four checks decide whether a change is mergeable. Three of them run Lua tooling pinned to a version you can install +locally and reproduce exactly. Run the ones your changes touch. + +| Check | Tool and pin | What it scopes | Local command | +| --- | --- | --- | --- | +| `busted` | lux-cli 0.28.3 | Whole spec suite, never a diff | `lx --lua-version 5.1 test` | +| `stylua` | StyLua 2.5.2 | Only hunks overlapping lines you wrote | `stylua --check --respect-ignores ` | +| `emmylua_check` | emmylua 0.24.0 | Whole tree, compared against the base | `emmylua_check -c .emmyrc.json .` | +| `docker compose up (headless)` | stock engine | Whole game, headless | `tools/headless_testing/README.md` | + ## Keeping These Instructions Current -Instruction files: this one, `.github/RmlUi-instructions.md`, and -`luarules/mission_api/mission-api-instructions.md`. The rules below apply to all of them. +Instruction files: this one, `.github/RmlUi-instructions.md`, `.github/spec-instructions.md`, and +(only on the `mission-api/dev` branch) `luarules/mission_api/mission-api-instructions.md`. +The rules below apply to all of them. - Update the affected file and section in the same pull request whenever a change alters a convention, tool, workflow, directory, or command they describe. A change to a subsystem covered by its own file updates that file, @@ -113,11 +126,13 @@ Match the validator to the file type. If unsure of a tool's scope, inspect the p - lux 0.28.x appends duplicate `dependencies` and `entrypoints` entries to `lux.lock` on every cold sync (a run with no `.lux/` tree), growing the file by ~13 lines each time without ever converging. Tests still pass. Do not commit that churn: `git checkout -- lux.lock` afterwards, and only commit a lockfile change you made deliberately. -- Lint: `luacheck` 1.2.0 with `.luacheckrc`; CI reports only lines changed in the PR (`.github/workflows/lint.yml`). -- Format: StyLua with `.stylua.toml` (tabs, indent width 4, 120 columns, CRLF, sorted requires) and `.styluaignore`; - `.editorconfig` mirrors the indent and whitespace rules. -- Types: EmmyLua analyzer via `.emmyrc.json`, stubs in `types/`, engine definitions from the `recoil-lua-library` - submodule. The codebase is at zero type errors — keep it there. +- Lint: `luacheck` 1.2.0 with `.luacheckrc`. Treat `.luacheckrc` as a style record, not as a gate. +- Format: StyLua 2.5.2 with `.stylua.toml` (tabs, indent width 4, 120 columns, CRLF, sorted requires) and + `.styluaignore`; `.editorconfig` mirrors the indent and whitespace rules. The gate fails only on changed hunks. +- Types: EmmyLua analyzer 0.24.0 via `.emmyrc.json`, stubs in `types/`, engine definitions from the + `recoil-lua-library` submodule. This is not a zero-errors gate. A new error blocks anywhere, and warnings run on + two budgets: fifteen net-new warnings per thousand changed lines, and thirty total warnings per thousand lines in + the files you changed. A change with no type errors still fails when it adds warnings faster than that per line. - Integration tests: headless engine via `docker compose -f tools/headless_testing/docker-compose.yml` (`.github/workflows/test_integration.yml`). They can also be run without docker against an engine already downloaded by an installed BAR client — see `tools/headless_testing/README.md`. @@ -128,17 +143,18 @@ Match the validator to the file type. If unsure of a tool's scope, inspect the p ### Commands ```sh -lx --lua-version 5.1 test # full busted suite (what CI runs) -busted --output=plainTerminal # same suite, when the .lux tree is already synced -busted spec/common/lib_spline_spec.lua # single spec file -lx lint # luacheck over the project; provisions luacheck itself -luacheck path/to/file.lua # lint one file, if luacheck is installed directly -stylua path/to/file.lua # format one file (`lx fmt` reformats the whole codebase) +lx --lua-version 5.1 test # full busted suite (what CI runs); needs lux-cli 0.28.3 +busted --output=plainTerminal # same suite, when the .lux tree is already synced +busted spec/common/lib_spline_spec.lua # single spec file +stylua --check --respect-ignores path/to/file.lua # what the format gate sees; needs stylua 2.5.2 +stylua path/to/file.lua # format one file (`lx fmt` reformats the whole codebase) +emmylua_check -c .emmyrc.json . # what the type gate sees; needs emmylua 0.24.0 ``` -Scope `luacheck` and `stylua` to the files you touched; repository-wide runs create large unrelated diffs. `lx lint` -reports pre-existing warnings across the tree, so compare against the baseline rather than assuming your change -caused them. +These mean nothing without the pinned versions, and the versions are not installed for you. + +Scope `stylua` to the files you touched; repository-wide runs create large unrelated diffs. The type check has no +scoped mode, so read its output against the baseline rather than assuming your change caused everything in it. ### By file type @@ -153,16 +169,17 @@ caused them. ## Tests +Read `.github/spec-instructions.md` before adding or changing anything under `spec/`. + - Add or update unit tests for the behavior you change, not only for shared logic in `common/` and `modules/`: new logic arrives with tests, changed logic has its tests updated, and a bug fix gets a test that fails without it. +- A new test must fail against the commit before yours and pass against yours. A test that passes both is asserting + something nobody changed, and it goes stale without anyone noticing. Never write pending tests. - When rendering or engine callins make code hard to test, extract the decision-making part into a testable function and cover that. Only genuinely rendering-bound behavior stays manual and in-engine. - Tests live in `spec/`, mirroring source layout (`spec/common/`, `spec/luaui/Widgets/`, `spec/gamedata/`) and named `*_spec.lua`. `.busted` sets `pattern = "_spec"` and `ROOT = spec/`, and puts `common/`, `luarules/`, `luaui/`, and `spec/` on `package.path`, so require modules by their repo-relative path. -- `spec/spec_helper.lua` mocks the engine surface (`Spring`, `LOG`, `GG`, `unpack`) — extend it instead of re-mocking - per file. Build engine state with `spec/builders/` (`spring_synced_builder`, `unit_def_builder`, and friends) - rather than hand-rolled tables. ## Compatibility and Data Ownership @@ -204,7 +221,8 @@ caused them. The data-driven mission runtime has load-order and dispatch conventions of its own. Read `luarules/mission_api/mission-api-instructions.md` before editing `luarules/mission_api/`, -`luarules/gadgets/api_missions*.lua`, `singleplayer/`, or `spec/mission_api/`. +`luarules/gadgets/api_missions*.lua`, `singleplayer/`, or `spec/mission_api/`. That file, and most of what it +describes, lives on `mission-api/dev` rather than `master`. ## RmlUi diff --git a/.github/spec-instructions.md b/.github/spec-instructions.md new file mode 100644 index 00000000000..be352e7422f --- /dev/null +++ b/.github/spec-instructions.md @@ -0,0 +1,87 @@ +# Spec Instructions — busted unit tests + +This file covers everything under `spec/`. Read it before adding or changing a spec, a builder, or +`spec/spec_helper.lua`. Subsystem conventions that sit on top of these rules live with their subsystem, currently +`luarules/mission_api/mission-api-instructions.md`. + +The rules here exist because a spec is read far more often than it is written, and usually by someone who did not +write it and is trying to work out whether a red test means their change is wrong. + +## The gates that judge a spec + +The gate table, the pinned versions and the handoff rule live in `.github/agents.md` under "Before You Hand Work +Over". Three things about those gates are specific to `spec/` and are easy to get wrong. + +Two exemption lists apply here and they do not agree with each other. `.styluaignore` exempts nothing under `spec/`, +so every spec and every builder is format-gated. `.emmyrc.json` exempts `spec/spec_helper.lua` and +`spec/builders/**` from type checking, and nothing else. So a builder is formatted but never type-checked, while the +spec beside it is both. Never assume a sibling directory carries the same exemptions as yours. + +Because the builders are unchecked, a mistake in one surfaces as a confusing type warning in each spec that uses it +rather than as an error where the mistake is. When warnings appear in a spec you did not expect to touch types, +suspect the builder first. + +Do not read an absent check as a passing one. `gh pr checks ` lists which gates actually ran, and a gate that +never fired looks exactly like one that passed. Run the suite yourself before you hand anything over, particularly +for work based on a long-lived branch, where a spec can go stale for days without anyone seeing red. + +## What a spec has to be + +**Readable without a tour.** Someone looking at one failing assertion must be able to decide whether the expected +value is right by reading the spec file and at most one helper. If understanding your test means opening three +builders and a subsystem helper, inline the fixture instead. + +**Indented with tabs.** The repo uses tabs and StyLua enforces them on every line you write. A space-indented spec +fails the format gate on almost every line of the file, which buries whatever else the check found. + +**Honest about what it proves.** A new test must fail against the commit before yours and pass against yours. If it +passes both, it is asserting something nobody changed, and it will go stale without anyone noticing. + +**About behavior the code actually promises.** Read the implementation before you write the assertion. A test that +invents a rule the module never implemented is worse than no test, because it fails later for a reason that has +nothing to do with the change that tripped it, and the person who hits it has no way to tell which side is wrong. + +**Self-consistent.** Before adding a case, read the neighbouring cases in the same file. Two tests in one spec that +imply contradictory rules mean at least one of them is wrong. + +## What a spec must not do + +Do not re-implement production logic inside the test harness. A builder that mirrors a production module is a second +copy that drifts, and every spec that trusts it inherits the drift. Call the production module instead. A comment of +the form `Mirrors ` in anything under `spec/builders/` is a defect, not documentation. + +Do not assert only that a stub was called. `assert.equal(1, #calls.doThing)` passes when `doThing` does nothing. Wire +the real module into the mock and assert the state it should have produced. + +Do not import the whole builder barrel. Require the builders you use by path rather than including +`spec/builders/index.lua`, which pulls in every builder on every spec file that touches it. + +Do not mix setup conventions inside one directory. Whatever a `spec//` directory does, hand-assigned +globals, a subsystem helper, or a builder, every spec in it does the same thing, so that fixing one teaches you how +to fix the next. + +Do not write specs for behavior that is not implemented. Open an issue instead. A green suite is supposed to mean +the code works. + +## How much to write + +New logic arrives with tests, changed logic has its tests updated, and a bug fix gets a test that fails without it. +That mandate has no upper bound in it, so apply one yourself. + +Specs that add more than twice the lines of the implementation they cover need a sentence in the pull request saying +why. Sometimes the answer is good, because a validation layer really does need a case per rule. More often it means +the same machinery is being driven from several directions, and two specs are covering one behavior. + +Test infrastructure changes, meaning `spec/builders/` and `spec/spec_helper.lua`, land in their own pull request +ahead of the feature that needs them. They are the files every other spec depends on, and they are impossible to +review inside a large feature diff. + +## Where specs live + +Specs mirror the source tree under `spec/` and are named `*_spec.lua`. `.busted` sets `pattern = "_spec"` and +`ROOT = spec/`, and puts `common/`, `luarules/`, `luaui/` and `spec/` on `package.path`, so require modules by their +repository-relative path. + +`spec/spec_helper.lua` mocks the engine surface, currently `Spring`, `LOG`, `GG` and `unpack`. Extend it rather than +re-mocking per file, and keep in mind that it is excluded from type checking, so mistakes in it surface as confusing +type warnings in the specs that use it rather than as errors in the helper. diff --git a/.github/workflows/test_unit.yml b/.github/workflows/test_unit.yml index 69be52da87d..59a87517aac 100644 --- a/.github/workflows/test_unit.yml +++ b/.github/workflows/test_unit.yml @@ -2,9 +2,9 @@ name: Run Unit Tests on: push: - branches: ['*'] + branches: ['**'] # needs to tolerate a slash but not run on tag pushes pull_request: - branches: ['*'] + branches: ['**'] # needs to tolerate a slash but not run on tag pushes jobs: busted: diff --git a/changelog.md b/changelog.md index e9fc7af365b..fd57ccd00cb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,10 @@ # September +- [Legion changes] + - Perdition doesn't gain extra range from elevation + - Martyr damages are added to its team's damage dealt + - EMP damage prevents reactive armor from regenerating + - Disable Air Units now removes and refunds drone spawners + - Range rings, graphics, blueprints, and other housekeeping - [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. diff --git a/common/configs/keybind_catalog.json b/common/configs/keybind_catalog.json index d74a2fc4236..c52662d3a11 100644 --- a/common/configs/keybind_catalog.json +++ b/common/configs/keybind_catalog.json @@ -857,6 +857,10 @@ "label": "actions.factory.showPresets", "alwaysModifier": "any" }, + { + "action": "factory_preset_toggle", + "label": "actions.factory.togglePresets" + }, { "action": "fov_inc 5", "label": "actions.camera.fovIncrease" diff --git a/gamedata/alldefs_post.lua b/gamedata/alldefs_post.lua index 5bdc63fd280..b47ef0df204 100644 --- a/gamedata/alldefs_post.lua +++ b/gamedata/alldefs_post.lua @@ -205,6 +205,19 @@ end -- MODULE FUNCTIONS ------------------------- +local function spawnedAirUnit(carriedUnits) + if not carriedUnits then + return + end + for carriedName in string.gmatch(carriedUnits, "%S+") do + -- Scav units still have the base unit here but check against both sets anyway. + local carriedDef = UnitDefs[carriedName] or UnitDefs[(string.gsub(carriedName, "_scav$", ""))] ---@as table + if carriedDef and carriedDef.canfly then + return carriedDef + end + end +end + local function unitDef_Post(name, uDef) local isScav = string.sub(name, -5, -1) == "_scav" local basename = isScav and string.sub(name, 1, -6) or name @@ -311,8 +324,30 @@ local function unitDef_Post(name, uDef) customparams.modoption_blocked = true elseif uDef.canfly then customparams.modoption_blocked = true - elseif customparams.restrictions_inclusion and string.find(customparams.restrictions_inclusion, "_noair_") then --used to remove factories and drone carriers with no other purpose (ex. leghive but not rampart) + elseif customparams.restrictions_inclusion and string.find(customparams.restrictions_inclusion, "_noair_") then --used to remove factories with no other purpose (ex. legap) customparams.modoption_blocked = true + else + local strippedDrones = false + for weaponName, weaponDef in pairs(weapondefs) do + local carriedUnit = weaponDef.customparams and weaponDef.customparams.carried_unit + local carriedDef = spawnedAirUnit(carriedUnit) + if carriedDef then + weapondefs[weaponName] = nil + strippedDrones = true + -- Make a minimal effort toward cost adjustments: + local count = weaponDef.customparams.startingdronecount + if count and tonumber(count) then + uDef.metalcost = (uDef.metalcost or 0) - count * (carriedDef.metalcost or 0) + uDef.energycost = (uDef.energycost or 0) - count * (carriedDef.energycost or 0) + end + uDef.metalcost = (uDef.metalcost or 0) * 0.95 + uDef.energycost = (uDef.energycost or 0) * 0.95 + end + end + -- Keep drone spawners that have other weapons: + if strippedDrones and not next(weapondefs) then + customparams.modoption_blocked = true + end end end diff --git a/language/en/commands.json b/language/en/commands.json index dfbd6b235cf..af53c877464 100644 --- a/language/en/commands.json +++ b/language/en/commands.json @@ -188,7 +188,8 @@ "queueMode": "Toggle factory repeat mode", "loadPreset": "Load factory preset %{n}", "savePreset": "Save factory preset %{n}", - "showPresets": "Show factory presets" + "showPresets": "Show factory presets", + "togglePresets": "Toggle factory presets" }, "gridMenu": { "buildKey": "Row %{row}, column %{col}", diff --git a/language/en/interface.json b/language/en/interface.json index 3378c76b96c..ed5887fdf5e 100644 --- a/language/en/interface.json +++ b/language/en/interface.json @@ -372,9 +372,53 @@ } }, "teamStats": { + "title": "Statistics", "player": "Player", "dead": "%{player} (dead)", "gone": "%{player} (gone)", + "team": "Team %{number}", + "members": "%{count} players", + "memberOne": "1 player", + "notYet": "Not available yet", + "perMinuteSuffix": " / min", + "perMinuteNote": "Shown per minute the team was in the game.", + "timeInGame": "Time in game: %{time}", + "diedAt": "Died at %{time}", + "ofTeam": "%{share} of the team's %{value}", + "ofTotal": "(%{value} of %{total})", + "unitCount": "(%{count} units)", + "unitOne": "(1 unit)", + "milestones": "Milestones", + "milestone": { + "factory": "First factory", + "tech2": "Tech 2", + "tech3": "Tech 3", + "nuke": "First nuke", + "antinuke": "First anti-nuke", + "lrpc": "First long-range cannon", + "commanderLost": "Commander lost", + "teamDied": "Out of the game" + }, + "group": { + "all": "All", + "live": "Live", + "economy": "Economy", + "combat": "Combat", + "units": "Units", + "composition": "Composition", + "activity": "Activity", + "graphs": "Graphs" + }, + "switch": { + "groupByTeam": "Group by team", + "groupByTeamDesc": "Players under their team, with the team's totals above them. Off, every player is listed in one sorted list.", + "shareOfTeam": "Share of team", + "shareOfTeamDesc": "Every amount as the share of the team's total it makes up, so it shows who carried. Ratios and levels stay as they are.", + "perMinute": "Per minute", + "perMinuteDesc": "Totals divided by the minutes each team was in the game, so a short game or an early exit compares fairly.", + "bars": "Bars", + "barsDesc": "A bar behind every number, scaled to the largest in its column." + }, "damage": "Damage", "damageDealt": "Dealt", "damageReceived": "Received", @@ -383,14 +427,122 @@ "unitsProduced": "Produced", "unitsKilled": "Killed", "unitsDied": "Lost", - "actionsPerMinute1": "Actions", - "actionsPerMinute2": "Per Minute", + "killEfficiency": "Efficiency", + "unitsCaptured": "Captured", + "unitsStolen": "Stolen", + "unitsReceived": "Received", + "unitsSent": "Sent", + "unitsActive": "Active", + "activity": "Activity", "aggression": "Aggression", - "aggressionLevel": "Level", + "actionsPerMinute": "Actions per minute", "metal": "Metal", "energy": "Energy", "resourceProduced": "Produced", - "resourceExcess": "Excess" + "resourceUsed": "Used", + "resourceExcess": "Excess", + "resourceSent": "Sent", + "resourceReceived": "Received", + "resourceStored": "Stored", + "shortReceived": "Recv", + "shortEfficiency": "Eff.", + "shortBuilt": "Built", + "shortProduced": "Prod.", + "shortAggression": "Aggr.", + "shortActionsPerMinute": "APM", + "resourceIncome": "Income", + "resourceExpense": "Expense", + "resourceLevel": "Level", + "industry": "Industry", + "conversion": "Conversion in use", + "shortConversion": "Conv.", + "buildPower": "Build power", + "shortBuildPower": "BP", + "buildPowerUse": "Build power in use", + "shortBuildPowerUse": "In use", + "value": "Unit value", + "unitValue": "Unit value", + "shortValue": "Value", + "valueArmy": "Army", + "shortArmy": "Army", + "valueAir": "Air", + "shortAir": "Air", + "valueSea": "Sea", + "shortSea": "Sea", + "valueDefense": "Defense", + "shortDefense": "Def.", + "valueStrategic": "Strategic", + "shortStrategic": "Strat.", + "valueFactories": "Factories", + "shortFactories": "Labs", + "valueBuilders": "Builders", + "shortBuilders": "Cons", + "valueEconomy": "Economy", + "shortEconomy": "Eco", + "valueUtility": "Utility", + "shortUtility": "Util", + "traded": "Value", + "killedValue": "Value destroyed", + "lostValue": "Value lost", + "valueEfficiency": "Value efficiency", + "teamKillValue": "Friendly fire", + "shortTeamKill": "Allied", + "commanders": "Commanders", + "comKills": "Commanders killed", + "comLost": "Commanders lost", + "desc": { + "damageDealt": "Damage dealt to enemy units.", + "damageReceived": "Damage taken from enemy units.", + "damageEfficiency": "Damage dealt as a share of damage received.", + "unitsProduced": "Units built.", + "unitsKilled": "Enemy units destroyed.", + "unitsDied": "Units lost.", + "killEfficiency": "Units destroyed as a share of units lost.", + "unitsCaptured": "Enemy units captured.", + "unitsStolen": "Units the enemy captured.", + "unitsReceived": "Units received from allies.", + "unitsSent": "Units given to allies.", + "unitsActive": "Units alive right now: built, received and captured, minus lost, given away and stolen.", + "metalProduced": "Metal produced.", + "metalUsed": "Metal spent.", + "metalExcess": "Metal wasted because storage was full.", + "metalSent": "Metal shared to allies.", + "metalReceived": "Metal received from allies.", + "metalStored": "Metal in storage right now: produced and received, minus spent, shared and wasted.", + "energyProduced": "Energy produced.", + "energyUsed": "Energy spent.", + "energyExcess": "Energy wasted because storage was full.", + "energySent": "Energy shared to allies.", + "energyReceived": "Energy received from allies.", + "energyStored": "Energy in storage right now: produced and received, minus spent, shared and wasted.", + "aggressionLevel": "How much damage was dealt for the resources gathered, on a logarithmic scale: 10 × log10 of damage dealt over metal plus a sixtieth of energy, produced and received. Higher is more aggressive.", + "actionsPerMinute": "Commands given per minute of game time.", + "metalIncome": "Metal coming in per second right now.", + "metalExpense": "Metal going out per second right now.", + "metalLevel": "How full the metal storage is right now.", + "energyIncome": "Energy coming in per second right now.", + "energyExpense": "Energy going out per second right now.", + "energyLevel": "How full the energy storage is right now.", + "conversion": "The share of the team's energy conversion capacity converting right now.", + "buildPower": "Build power of every builder, nano turret and factory the team has.", + "buildPowerUse": "The share of the team's build power at work right now: building, repairing, reclaiming or resurrecting.", + "unitValue": "What every unit the team has cost, in metal with energy at a sixtieth.", + "valueArmy": "Value of the mobile ground army: every armed ground and hover unit.", + "valueAir": "Value of every aircraft that is not a builder.", + "valueSea": "Value of every armed ship and submarine.", + "valueDefense": "Value of every static defense: turrets, anti-air, mines and shields with weapons.", + "valueStrategic": "Value of nukes, anti-nukes and long-range cannons.", + "valueFactories": "Value of every factory.", + "valueBuilders": "Value of every constructor, nano turret and commander.", + "valueEconomy": "Value of every energy and metal maker.", + "valueUtility": "Value of everything else: radars, jammers, storage, transports and scouts.", + "killedValue": "What the enemy units this team destroyed had cost, in metal with energy at a sixtieth. A unit under construction counts for what was put into it.", + "lostValue": "What the units this team lost had cost, the same way.", + "valueEfficiency": "Value destroyed as a share of value lost.", + "teamKillValue": "What the allied units this team destroyed had cost.", + "comKills": "Enemy commanders destroyed.", + "comLost": "Commanders lost." + } }, "reclaimInfo": { "metal": "Metal: %{metal}", @@ -788,6 +940,7 @@ "category": { "changed": "Changed", "local": "Your own", + "rml": "RmlUi", "all": "All", "favorite": "Favorites", "interface": "Interface", @@ -835,6 +988,7 @@ "otherdesc": "Widgets whose file begins with something this panel has no category for.", "countsdesc": "The count reads how many are running out of how many there are.", "localdesc": "The widgets in your own LuaUI folder rather than the ones the game ships. They carry a local tag on the row too.", + "rmldesc": "The widgets that build their interface with RmlUi, laid out from markup and style sheets, rather than drawing it themselves. Found by what their source uses rather than where the file sits, so your own count too. They carry an rml tag on the row as well.", "localonlydesc": "Show only the widgets in your own LuaUI folder, leaving out the ones the game ships.", "enabledonlydesc": "Show only the widgets the config says to load - running or not - so what is off stays out of the way.", "errorsonlydesc": "Show only the widgets that have raised an error this session - the ones tagged error - whether an error stopped them or they are running again.", diff --git a/language/en/units.json b/language/en/units.json index bbc80a0d243..6fe63bee9c2 100644 --- a/language/en/units.json +++ b/language/en/units.json @@ -757,6 +757,7 @@ "legmlv": "Sapper", "legmoho": "Advanced Metal Extractor", "legmohobp": "Fortifier", + "legmohobpct": "Fortifier", "legmohocon": "Advanced Metal Fortifier", "legmohoconct": "Advanced Metal Fortifier", "legmohoconin": "Advanced Metal Fortifier", @@ -1735,6 +1736,7 @@ "legmlv": "Stealthy Minelayer / Minesweeper", "legmoho": "Advanced Metal Extractor / Storage", "legmohobp": "Advanced Metal Extractor / Build Drone Pad", + "legmohobpct": "Advanced Metal Extractor / Build Drone Pad", "legmohocon": "Advanced Metal Extractor and Construction Turret", "legmohoconct": "Advanced Metal Extractor and Construction Turret", "legmohoconin": "You aren't supposed to see this one", diff --git a/luarules/gadgets/api_teamstats.lua b/luarules/gadgets/api_teamstats.lua new file mode 100644 index 00000000000..9f52116a515 --- /dev/null +++ b/luarules/gadgets/api_teamstats.lua @@ -0,0 +1,695 @@ +local gadget = gadget ---@type Gadget + +function gadget:GetInfo() + return { + name = "Team stats", + desc = "Per-team economy, build power, conversion, unit composition, value traded and milestones: live on request, sampled into a history", + author = "Floris", + date = "September 2026", + license = "GNU GPL, v2 or later", + layer = 0, + enabled = true, + } +end + +-- Unsynced on purpose. The unsynced half of LuaRules gets every unit event and reads +-- every team, so the tally, the history and the milestones are built here from the +-- same events the simulation runs on: the same on every client and in every replay, +-- without a byte through the synced state, the network or the replay file. LuaUI cannot +-- call into a gadget (its Script table reaches LuaUI alone), so the gadget serves it the +-- other way round: while a widget keeps a receiver registered it is handed the live +-- values every second, and a request for history is answered with the backlog. A widget +-- that is reloaded, or a player who resigned and may now see the other side, gets what +-- it may see within a second. Nothing is handed over while no widget asks. +-- +-- Should a synced consumer ever need the numbers (an awards gadget computing in synced, +-- say), the tally takes only callin arguments and reads the same engine calls exist +-- synced, so it can run there as it is; the awards gadget could as well move its +-- accounting to this side, since its results only ever go to LuaUI. +if gadgetHandler:IsSyncedCode() then + return +end + +---------------------------------------------------------------- +-- Configuration +---------------------------------------------------------------- + +-- Frames between two history samples: the engine's own team statistics period, so the +-- two histories line up sample for sample. Between samples the live values are read on +-- request and never stored. +local SAMPLE_PERIOD = 450 + +-- Frames between two hand-overs to LuaUI while a widget is listening: the responsive +-- number between two samples, read fresh each time and never stored. +local LIVE_PERIOD = 30 + +-- Energy counts at a sixtieth of metal in a unit's value, the game's usual exchange. +local ENERGY_PER_METAL = 60 + +-- What a unit counts as, from its def. Builders come before the rest so a constructor +-- with a gun is a builder; strategic and economy come before the medium split so a +-- floating nuke silo is strategic, not sea. +local BUCKETS = { "army", "air", "sea", "defense", "strategic", "factories", "builders", "economy", "utility" } + +-- The moments worth remembering, and what marks them. `built` is tested on every unit +-- a team finishes and `lost` on every unit it loses; a milestone is kept once per team, +-- or every time when `every` is set. The unit that reached it is stored with it. +local MILESTONES = { + { + key = "factory", + built = function(ud) + return ud.isFactory + end, + }, + { + key = "tech2", + built = function(ud) + return (tonumber(ud.customParams.techlevel) or 1) == 2 + end, + }, + { + key = "tech3", + built = function(ud) + return (tonumber(ud.customParams.techlevel) or 1) >= 3 + end, + }, + { + key = "nuke", + built = function(ud) + return ud.customParams.unitgroup == "nuke" + end, + }, + { + key = "antinuke", + built = function(ud) + return ud.customParams.unitgroup == "antinuke" + end, + }, + { + key = "lrpc", + built = function(ud) + return ud.customParams.islrpc ~= nil + end, + }, + { + key = "commanderLost", + lost = function(ud) + return ud.customParams.iscommander ~= nil + end, + every = true, + }, + { key = "teamDied" }, +} + +---------------------------------------------------------------- +-- What a sample holds +---------------------------------------------------------------- + +-- Every value a sample carries, in one fixed order. A live request returns the same +-- keys, read at that moment. +local SAMPLED = { + "metalIncome", + "metalExpense", + "metalCurrent", + "metalStorage", + "energyIncome", + "energyExpense", + "energyCurrent", + "energyStorage", + "convCapacity", + "convUse", + "buildPower", + "buildPowerActive", + "unitCount", + "unitValue", + "killedValue", + "killedArmyValue", + "killedEcoValue", + "lostValue", + "teamKillValue", + "comKills", + "comLost", +} +local countKey, valueKey = {}, {} +for i = 1, #BUCKETS do + local bucket = BUCKETS[i] + local cap = bucket:sub(1, 1):upper() .. bucket:sub(2) + countKey[bucket] = "count" .. cap + valueKey[bucket] = "value" .. cap + SAMPLED[#SAMPLED + 1] = countKey[bucket] + SAMPLED[#SAMPLED + 1] = valueKey[bucket] +end + +-- The keys the tally keeps as running counters; the rest of a sample is read live. +local TALLIED = { + "unitCount", + "unitValue", + "buildPower", + "killedValue", + "killedArmyValue", + "killedEcoValue", + "lostValue", + "teamKillValue", + "comKills", + "comLost", +} +for i = 1, #BUCKETS do + TALLIED[#TALLIED + 1] = countKey[BUCKETS[i]] + TALLIED[#TALLIED + 1] = valueKey[BUCKETS[i]] +end + +---------------------------------------------------------------- +-- Unit defs +---------------------------------------------------------------- + +local spGetTeamResources = Spring.GetTeamResources +local spGetTeamRulesParam = Spring.GetTeamRulesParam +local spGetUnitCurrentBuildPower = Spring.GetUnitCurrentBuildPower +local spGetUnitIsBeingBuilt = Spring.GetUnitIsBeingBuilt +local spGetGameFrame = Spring.GetGameFrame +local spGetMyAllyTeamID = Spring.GetMyAllyTeamID +local spGetSpectatingState = Spring.GetSpectatingState + +---@type table +local defCost = {} +---@type table +local defBucket = {} +---@type table +local defCountKey = {} +---@type table +local defValueKey = {} +---@type table +local defBuildSpeed = {} +---@type table +local defIsCommander = {} +---@type table +local defBuiltMilestones = {} +---@type table +local defLostMilestones = {} +---@type table +local killedAs = { + army = "killedArmyValue", + air = "killedArmyValue", + sea = "killedArmyValue", + defense = "killedArmyValue", + strategic = "killedArmyValue", + economy = "killedEcoValue", +} + +local ARMED_GROUPS = + { weapon = true, aa = true, sub = true, emp = true, explo = true, weaponaa = true, weaponsub = true } +---@type table +local SEA_CLASSES = { BOAT = true, UBOAT = true, EPICSHIP = true } + +local function bucketOf(ud) + local cp = ud.customParams + local group = cp.unitgroup or "" + if ud.isFactory then + return "factories" + end + if ud.isBuilder then + return "builders" + end + if group == "nuke" or group == "antinuke" or cp.islrpc then + return "strategic" + end + if group == "energy" or group == "metal" then + return "economy" + end + if group == "util" then + return "utility" + end + local armed = #ud.weapons > 0 or ARMED_GROUPS[group] + if ud.speed == 0 then + return armed and "defense" or "utility" + end + if ud.canFly then + return "air" + end + local moveClass = ud.moveDef and ud.moveDef.name or "" + if SEA_CLASSES[moveClass:match("^%u+") or ""] then + return "sea" + end + return armed and "army" or "utility" +end + +for unitDefID, ud in pairs(UnitDefs) do + defCost[unitDefID] = ud.metalCost + ud.energyCost / ENERGY_PER_METAL + local bucket = bucketOf(ud) + defBucket[unitDefID] = bucket + defCountKey[unitDefID] = countKey[bucket] + defValueKey[unitDefID] = valueKey[bucket] + if ud.buildSpeed > 0 and (ud.isBuilder or ud.isFactory) then + defBuildSpeed[unitDefID] = ud.buildSpeed + end + defIsCommander[unitDefID] = ud.customParams.iscommander ~= nil + for i = 1, #MILESTONES do + local m = MILESTONES[i] + if m.built and m.built(ud) then + local list = defBuiltMilestones[unitDefID] or {} + list[#list + 1] = m + defBuiltMilestones[unitDefID] = list + end + if m.lost and m.lost(ud) then + local list = defLostMilestones[unitDefID] or {} + list[#list + 1] = m + defLostMilestones[unitDefID] = list + end + end +end + +---------------------------------------------------------------- +-- State +---------------------------------------------------------------- + +-- [teamID] = the running counters, one per TALLIED key. +---@type table?> +local teams = {} +---@type table +local allyOf = {} +-- [unitID] = the team a finished unit is counted for; a unit under construction is not +-- in here and counts for nothing until it is done. +---@type table +local finished = {} +-- [teamID][unitID] = build speed, for every finished builder and factory. +---@type table> +local builders = {} +-- [teamID] = { frames = { ... }, values = { [key] = { ... } } }, one entry per sample. +---@type table }> +local history = {} +-- [teamID] = { { key, frame, unitDefID, unitID }, ... } in the order they were reached. +---@type table +local milestones = {} +local reached = {} +local dead = {} +local gameOver = false + +local function newTally() + local t = {} + for i = 1, #TALLIED do + t[TALLIED[i]] = 0 + end + return t +end + +local function newHistory() + local h = { frames = {}, values = {} } + for i = 1, #SAMPLED do + h.values[SAMPLED[i]] = {} + end + return h +end + +---------------------------------------------------------------- +-- The tally +---------------------------------------------------------------- + +local function addUnit(unitID, unitDefID, teamID) + local t = teams[teamID] + if not t or finished[unitID] then + return + end + finished[unitID] = teamID + local cost = defCost[unitDefID] + t.unitCount = t.unitCount + 1 + t.unitValue = t.unitValue + cost + local ck, vk = defCountKey[unitDefID], defValueKey[unitDefID] + t[ck] = t[ck] + 1 + t[vk] = t[vk] + cost + local buildSpeed = defBuildSpeed[unitDefID] + if buildSpeed then + builders[teamID][unitID] = buildSpeed + t.buildPower = t.buildPower + buildSpeed + end +end + +local function removeUnit(unitID, unitDefID) + local teamID = finished[unitID] + if not teamID then + return + end + finished[unitID] = nil + local t = teams[teamID] + ---@cast t -? + local cost = defCost[unitDefID] + t.unitCount = t.unitCount - 1 + t.unitValue = t.unitValue - cost + local ck, vk = defCountKey[unitDefID], defValueKey[unitDefID] + t[ck] = t[ck] - 1 + t[vk] = t[vk] - cost + local buildSpeed = builders[teamID][unitID] + if buildSpeed then + builders[teamID][unitID] = nil + t.buildPower = t.buildPower - buildSpeed + end +end + +local function markMilestone(teamID, m, unitDefID, unitID) + if not m.every then + if reached[teamID][m.key] then + return + end + reached[teamID][m.key] = true + end + local list = milestones[teamID] + list[#list + 1] = { key = m.key, frame = spGetGameFrame(), unitDefID = unitDefID, unitID = unitID } +end + +---------------------------------------------------------------- +-- Reading the live values +---------------------------------------------------------------- + +-- Fills `out` with every SAMPLED key for the team, as things stand right now: the +-- counters from the tally, the economy and the conversion from the engine, and the +-- build power in use from every builder's nano activity. +local function readLive(teamID, out) + local t = teams[teamID] + ---@cast t -? + local cur, storage, _, income, expense = spGetTeamResources(teamID, "metal") + out.metalIncome = income or 0 + out.metalExpense = expense or 0 + out.metalCurrent = cur or 0 + out.metalStorage = storage or 0 + cur, storage, _, income, expense = spGetTeamResources(teamID, "energy") + out.energyIncome = income or 0 + out.energyExpense = expense or 0 + out.energyCurrent = cur or 0 + out.energyStorage = storage or 0 + out.convCapacity = spGetTeamRulesParam(teamID, "mmCapacity") or 0 + out.convUse = spGetTeamRulesParam(teamID, "mmUse") or 0 + ---@type number + local active = 0 + for unitID, buildSpeed in pairs(builders[teamID]) do + active = active + buildSpeed * (spGetUnitCurrentBuildPower(unitID) or 0) + end + out.buildPowerActive = active + for i = 1, #TALLIED do + local key = TALLIED[i] + out[key] = t[key] + end + return out +end + +local scratch = {} + +local function sample(frame) + for teamID, h in pairs(history) do + if not dead[teamID] then + readLive(teamID, scratch) + local n = #h.frames + 1 + h.frames[n] = frame + local values = h.values + for i = 1, #SAMPLED do + local key = SAMPLED[i] + values[key][n] = scratch[key] + end + end + end +end + +---------------------------------------------------------------- +-- Callins +---------------------------------------------------------------- + +local function unitCreated(unitID, unitDefID, unitTeam) + if not spGetUnitIsBeingBuilt(unitID) then + addUnit(unitID, unitDefID, unitTeam) + end +end + +function gadget:UnitCreated(unitID, unitDefID, unitTeam) + unitCreated(unitID, unitDefID, unitTeam) +end + +function gadget:UnitFinished(unitID, unitDefID, unitTeam) + addUnit(unitID, unitDefID, unitTeam) + local list = defBuiltMilestones[unitDefID] + if list and teams[unitTeam] then + for i = 1, #list do + markMilestone(unitTeam, list[i], unitDefID, unitID) + end + end +end + +function gadget:UnitGiven(unitID, unitDefID, newTeam, oldTeam) + if finished[unitID] then + removeUnit(unitID, unitDefID) + addUnit(unitID, unitDefID, newTeam) + end +end + +function gadget:UnitDestroyed(unitID, unitDefID, unitTeam, attackerID, attackerDefID, attackerTeam) + local value = defCost[unitDefID] + if not finished[unitID] then + -- A unit still under construction is worth what was put into it. + local _, progress = spGetUnitIsBeingBuilt(unitID) + value = value * (progress or 0) + end + removeUnit(unitID, unitDefID) + + local victim = teams[unitTeam] + if victim then + victim.lostValue = victim.lostValue + value + if defIsCommander[unitDefID] then + victim.comLost = victim.comLost + 1 + end + local list = defLostMilestones[unitDefID] + if list then + for i = 1, #list do + markMilestone(unitTeam, list[i], unitDefID, unitID) + end + end + end + + local killer = attackerTeam and teams[attackerTeam] + if not killer or attackerTeam == unitTeam or value == 0 then + return + end + if allyOf[attackerTeam] == allyOf[unitTeam] then + killer.teamKillValue = killer.teamKillValue + value + return + end + killer.killedValue = killer.killedValue + value + local split = killedAs[defBucket[unitDefID]] + if split then + killer[split] = killer[split] + value + end + if defIsCommander[unitDefID] then + killer.comKills = killer.comKills + 1 + end +end + +function gadget:TeamDied(teamID) + if not teams[teamID] or dead[teamID] then + return + end + dead[teamID] = true + for i = 1, #MILESTONES do + if MILESTONES[i].key == "teamDied" then + markMilestone(teamID, MILESTONES[i]) + end + end +end + +-- The last sample is the state at the end: what happens in the minutes after is not +-- the game. +function gadget:GameOver() + if gameOver then + return + end + gameOver = true + sample(spGetGameFrame()) +end + +---------------------------------------------------------------- +-- What LuaUI may see +---------------------------------------------------------------- + +-- The same rule the engine applies to its own team statistics: a team's numbers are for +-- its allies, for a spectator watching everything, and for everyone once the game is +-- over. A spectator watching one side sees that side. +local function visible(teamID) + if not teams[teamID] then + return false + end + if gameOver then + return true + end + local spec, fullView = spGetSpectatingState() + if spec and fullView then + return true + end + return allyOf[teamID] == spGetMyAllyTeamID() +end + +local function copyMilestones(teamID) + local out = {} + local list = milestones[teamID] + for i = 1, #list do + local m = list[i] + out[i] = { key = m.key, frame = m.frame, unitDefID = m.unitDefID, unitID = m.unitID } + end + return out +end + +local function liveOf(teamID) + local out = readLive(teamID, {}) + out.dead = dead[teamID] or false + out.milestones = copyMilestones(teamID) + return out +end + +-- The values as they stand right now, for one team or for every team the caller may +-- see, keyed by team. Read fresh on every call and never stored: it is the responsive +-- number between two samples. +local function GetTeamStatsLive(teamID) + if teamID ~= nil then + if not visible(teamID) then + return nil + end + return liveOf(teamID) + end + local out = {} + for id in pairs(teams) do + if visible(id) then + out[id] = liveOf(id) + end + end + return out +end + +-- The samples from `from` (1 by default) on: the frames they were taken at and each +-- value's run, so a caller with the first n only asks for what came after them. +local function GetTeamStatsHistory(teamID, from) + if not visible(teamID) then + return nil + end + from = math.max(1, from or 1) + local h = history[teamID] + local out = { period = SAMPLE_PERIOD, from = from, frames = {}, values = {} } + for n = from, #h.frames do + out.frames[n - from + 1] = h.frames[n] + end + for i = 1, #SAMPLED do + local key = SAMPLED[i] + local run, src = {}, h.values[key] + for n = from, #src do + run[n - from + 1] = src[n] + end + out.values[key] = run + end + return out +end + +local function GetTeamStatsMilestones(teamID) + if not visible(teamID) then + return nil + end + return copyMilestones(teamID) +end + +-- What the numbers mean: the sample period, the keys in their order, the buckets and +-- the milestone kinds, so a caller need not hardcode them. +local function GetTeamStatsInfo() + local kinds = {} + for i = 1, #MILESTONES do + kinds[i] = MILESTONES[i].key + end + local keys = {} + for i = 1, #SAMPLED do + keys[i] = SAMPLED[i] + end + local buckets = {} + for i = 1, #BUCKETS do + buckets[i] = BUCKETS[i] + end + return { + period = SAMPLE_PERIOD, + keys = keys, + buckets = buckets, + milestones = kinds, + energyPerMetal = ENERGY_PER_METAL, + } +end + +-- For other unsynced gadgets, as globals. +local exports = { + GetTeamStatsLive = GetTeamStatsLive, + GetTeamStatsHistory = GetTeamStatsHistory, + GetTeamStatsMilestones = GetTeamStatsMilestones, + GetTeamStatsInfo = GetTeamStatsInfo, +} + +---------------------------------------------------------------- +-- Serving LuaUI +---------------------------------------------------------------- + +-- The engine's cross-state calls: Script.LuaUI.(...) runs a global of the LuaUI +-- state, and Script.LuaUI("") says whether there is one to run. +---@diagnostic disable-next-line: undefined-global +local Script = Script + +-- A widget takes part by registering globals: `TeamStatsLive(all, frame)` is handed +-- the live values of every team the viewer may see, keyed by team, every LIVE_PERIOD +-- frames for as long as it is registered; `TeamStatsHistoryRequest()` returning +-- { [teamID] = fromIndex } is answered through `TeamStatsHistory(teamID, history)` +-- with the samples from that index on (see GetTeamStatsHistory for the shape), so a +-- caller holding the first n samples asks for what came after them. +local function serveLuaUI(frame) + if Script.LuaUI("TeamStatsLive") then + Script.LuaUI.TeamStatsLive(GetTeamStatsLive(), frame) + end + if Script.LuaUI("TeamStatsHistoryRequest") and Script.LuaUI("TeamStatsHistory") then + local wanted = Script.LuaUI.TeamStatsHistoryRequest() + if type(wanted) == "table" then + for teamID, from in pairs(wanted) do + local h = GetTeamStatsHistory(teamID, from) + if h then + Script.LuaUI.TeamStatsHistory(teamID, h) + end + end + end + end +end + +function gadget:GameFrame(frame) + if frame % SAMPLE_PERIOD == 0 and not gameOver then + sample(frame) + end + if frame % LIVE_PERIOD == 0 then + serveLuaUI(frame) + end +end + +function gadget:Initialize() + local gaia = Spring.GetGaiaTeamID() + local teamList = Spring.GetTeamList() + ---@cast teamList -? + for _, teamID in ipairs(teamList) do + if teamID ~= gaia then + teams[teamID] = newTally() + allyOf[teamID] = select(6, Spring.GetTeamInfo(teamID, false)) + builders[teamID] = {} + history[teamID] = newHistory() + milestones[teamID] = {} + reached[teamID] = {} + local _, _, isDead = Spring.GetTeamInfo(teamID, false) + dead[teamID] = isDead == true or nil + end + end + -- Units already standing, after a reload or when the game is joined late. + for _, unitID in ipairs(Spring.GetAllUnits()) do + local unitDefID = Spring.GetUnitDefID(unitID) + ---@cast unitDefID -? + unitCreated(unitID, unitDefID, Spring.GetUnitTeam(unitID)) + end + for name, fn in pairs(exports) do + gadgetHandler:RegisterGlobal(name, fn) + end +end + +function gadget:Shutdown() + for name in pairs(exports) do + gadgetHandler:DeregisterGlobal(name) + end +end diff --git a/luarules/gadgets/unit_attached_con_turret_mex.lua b/luarules/gadgets/unit_attached_con_turret_mex.lua index ab7f61b3c38..babb8f87013 100644 --- a/luarules/gadgets/unit_attached_con_turret_mex.lua +++ b/luarules/gadgets/unit_attached_con_turret_mex.lua @@ -91,28 +91,34 @@ local function doSwapMex(unitID, unitTeam, unitData) return end Spring.SetUnitBlocking(mexID, true, true, false) - Spring.SetUnitNoSelect(mexID, true) SendToUnsynced("setUnitNoGroup", mexID, true) Spring.SetUnitStealth(mexID, true) - local piece = resolveAttachPiece(mexID) - if not piece then + local conID = Spring.CreateUnit(unitData.swapDefs.con, ux, uy, uz, unitFacing, unitTeam) + if not conID then Spring.DestroyUnit(mexID, false, true) Spring.AddTeamResource(unitTeam, "m", unitData.metal) Spring.AddTeamResource(unitTeam, "e", unitData.energy) return end + Spring.SetUnitHealth(conID, unitHealth) - local conID = Spring.CreateUnit(unitData.swapDefs.con, ux, uy, uz, unitFacing, unitTeam) - if not conID then + local piece = resolveAttachPiece(conID) + if not piece then + Spring.DestroyUnit(conID, false, true) Spring.DestroyUnit(mexID, false, true) Spring.AddTeamResource(unitTeam, "m", unitData.metal) Spring.AddTeamResource(unitTeam, "e", unitData.energy) return end - Spring.SetUnitHealth(conID, unitHealth) - Spring.UnitAttach(mexID, conID, piece, true) + -- transported units can't be targeted, so the turret carries the mex + Spring.UnitAttach(conID, mexID, piece, true) + -- attaching resets these + Spring.SetUnitNoSelect(mexID, true) + Spring.SetUnitNoMinimap(mexID, true) + Spring.SetUnitIconDraw(mexID, false) + Spring.SetUnitNoDraw(mexID, true) Spring.SetUnitRulesParam(conID, "pairedUnitID", mexID) Spring.SetUnitRulesParam(mexID, "pairedUnitID", conID) pairedUnits[conID] = mexID diff --git a/luarules/gadgets/unit_reactive_armor.lua b/luarules/gadgets/unit_reactive_armor.lua index 8ae364b0b00..022c311de0c 100644 --- a/luarules/gadgets/unit_reactive_armor.lua +++ b/luarules/gadgets/unit_reactive_armor.lua @@ -21,8 +21,8 @@ end local armorBreakMethod = "ReactiveArmorBreak" local armorRestoreMethod = "ReactiveArmorRestore" -local unitCombatDuration = math.round(5 * Game.gameSpeed) -- Also sets the minimum `reactive_armor_restore`. -local unitUpdateInterval = math.round((1 / 6) * Game.gameSpeed) +local unitCombatDuration = math.round(5 * Game.gameSpeed, 0) -- Also sets the minimum `reactive_armor_restore`. +local unitUpdateInterval = math.round((1 / 6) * Game.gameSpeed, 0) -- Localization @@ -43,8 +43,8 @@ local armoredUnitDefs = {} for unitDefID, unitDef in pairs(UnitDefs) do if unitDef.customParams.reactive_armor_health and unitDef.customParams.reactive_armor_restore then local params = { - health = tonumber(unitDef.customParams.reactive_armor_health), - frames = tonumber(unitDef.customParams.reactive_armor_restore) * gameSpeed, + health = (tonumber(unitDef.customParams.reactive_armor_health) or 0), + frames = (tonumber(unitDef.customParams.reactive_armor_restore) or 0) * gameSpeed, first = true, } @@ -65,7 +65,7 @@ end -- since that is the first time the game gives us info on them. local function checkReactiveArmor(unitID, unitDefID, params) local hasMethod - local lusEnv = Spring.UnitScript.GetScriptEnv(unitID) + local lusEnv = Spring.UnitScript.GetScriptEnv(unitID) ---@as table? if lusEnv then hasMethod = function(name) @@ -149,7 +149,7 @@ local function checkReactiveArmor(unitID, unitDefID, params) -- Fix for the different argument types used between COB and LUS. params.call = (not lusEnv and callFromCob) or function(unitID, funcName, ...) - callFromLus(unitID, lusEnv[funcName], ...) + callFromLus(unitID, lusEnv[funcName], ...) ---@diagnostic disable-line: need-check-nil end return true @@ -164,7 +164,7 @@ local regenerateFrame = table_new(0, 2 ^ 6) -- Next frame that the unit will beg local gameFrame = 0 local combatEndFrame = gameFrame + unitCombatDuration ----@return table<"countdown"|integer, integer> restoreFrames +---@return { countdown : integer, [integer] : true }|{ countdown : integer, [0] : true } restoreFrames local function getArmorRestoreFrames(defData, duration) local armorPieceCount = defData.pieces local restoreDuration = duration or defData.frames @@ -261,7 +261,7 @@ local function doArmorDamage(unitID, defData, damage) end local function restoreUnitArmor(unitID, piece) - local defData = armoredUnitDefs[spGetUnitDefID(unitID)] + local defData = armoredUnitDefs[spGetUnitDefID(unitID)] ---@type table if piece ~= true then defData.call(unitID, defData[armorRestoreMethod][piece]) @@ -317,7 +317,7 @@ end local function showDebugInfo(unitID) local info = getUnitDebugInfo(unitID) - if info.unitCountdown then + if tonumber(info.unitCountdown) then local display = ("hp:%s res:%s"):format(tostring(info.armorHealth), tostring(info.unitCountdown)) Spring.MarkerAddPoint(info.x, info.y, info.z, display) Spring.Echo("Reactive Armor", info) @@ -352,7 +352,13 @@ function gadget:UnitDestroyed(unitID, unitDefID, unitTeam) end function gadget:UnitDamaged(unitID, unitDefID, unitTeam, damage, paralyzer) - if not paralyzer and damage > 0 and armoredUnitDefs[unitDefID] then + if not armoredUnitDefs[unitDefID] then + return + end + ---@cast paralyzer boolean -- todo: odd + if paralyzer then + regenerateFrame[unitID] = combatEndFrame + elseif damage > 0 then doArmorDamage(unitID, armoredUnitDefs[unitDefID], damage) end end @@ -365,7 +371,7 @@ end ---@return boolean changed `false` when the unit has no reactive armor, its armor is ---already broken, or a repair was requested while already at full armor. GG.AddReactiveArmorDamage = function(unitID, damage) - local unitDefData = armoredUnitDefs[spGetUnitDefID(unitID)] + local unitDefData = armoredUnitDefs[spGetUnitDefID(unitID)] ---@as table if unitDefData and damage ~= 0 then return doArmorDamage(unitID, unitDefData, damage) else @@ -401,8 +407,8 @@ function gadget:Initialize() return end - local armorHealth = spGetUnitRulesParam(unitID, "reactiveArmorHealth") - local armorFrames = spGetUnitRulesParam(unitID, "reactiveArmorFrames") + local armorHealth = spGetUnitRulesParam(unitID, "reactiveArmorHealth") ---@as number|false + local armorFrames = spGetUnitRulesParam(unitID, "reactiveArmorFrames") ---@as number|false local combatUntil = spGetUnitRulesParam(unitID, "unitIsInCombatUntil") gadget:UnitFinished(unitID, unitDefID, unitTeam) spSetUnitRulesParam(unitID, "reactiveArmorFrames", nil) diff --git a/luaui/Include/blueprint_substitution/definitions.lua b/luaui/Include/blueprint_substitution/definitions.lua index 54f9d83ee05..924eada6196 100644 --- a/luaui/Include/blueprint_substitution/definitions.lua +++ b/luaui/Include/blueprint_substitution/definitions.lua @@ -62,10 +62,10 @@ function DefinitionsModule.defineUnitCategories() "ADVANCED_EXPLOITER", { [SIDES.ARMADA] = "armmoho", [SIDES.CORTEX] = "cormexp", [SIDES.LEGION] = "legmohocon" } ) - DefCat("UW_EXTRACTOR", { [SIDES.ARMADA] = "armuwmex", [SIDES.CORTEX] = "coruwmex", [SIDES.LEGION] = "leguwmex" }) + DefCat("UW_EXTRACTOR", { [SIDES.ARMADA] = "armuwmex", [SIDES.CORTEX] = "coruwmex" }) DefCat( "ADVANCED_UW_EXTRACTOR", - { [SIDES.ARMADA] = "armuwmme", [SIDES.CORTEX] = "coruwmme", [SIDES.LEGION] = "leguwmme" } + { [SIDES.ARMADA] = "armuwmme", [SIDES.CORTEX] = "coruwmme", [SIDES.LEGION] = "leganavalmex" } ) DefCat("METAL_STORAGE", { [SIDES.ARMADA] = "armmstor", [SIDES.CORTEX] = "cormstor", [SIDES.LEGION] = "legmstor" }) DefCat( @@ -78,7 +78,7 @@ function DefinitionsModule.defineUnitCategories() ) DefCat( "UW_ADVANCED_METAL_STORAGE", - { [SIDES.ARMADA] = "armuwadvms", [SIDES.CORTEX] = "coruwadvms", [SIDES.LEGION] = "coruwadvms" } + { [SIDES.ARMADA] = "armuwadvms", [SIDES.CORTEX] = "coruwadvms", [SIDES.LEGION] = "legamstor" } ) -- Energy buildings @@ -100,19 +100,28 @@ function DefinitionsModule.defineUnitCategories() DefCat("TIDAL", { [SIDES.ARMADA] = "armtide", [SIDES.CORTEX] = "cortide", [SIDES.LEGION] = "legtide" }) DefCat("FUSION", { [SIDES.ARMADA] = "armfus", [SIDES.CORTEX] = "corfus", [SIDES.LEGION] = "legfus" }) DefCat("ADVANCED_FUSION", { [SIDES.ARMADA] = "armafus", [SIDES.CORTEX] = "corafus", [SIDES.LEGION] = "legafus" }) - DefCat("UW_FUSION", { [SIDES.ARMADA] = "armuwfus", [SIDES.CORTEX] = "coruwfus", [SIDES.LEGION] = "leguwfus" }) + DefCat( + "UW_FUSION", + { [SIDES.ARMADA] = "armuwfus", [SIDES.CORTEX] = "coruwfus", [SIDES.LEGION] = "leganavalfusion" } + ) DefCat("GEOTHERMAL", { [SIDES.ARMADA] = "armageo", [SIDES.CORTEX] = "corbhmth", [SIDES.LEGION] = "leggeo" }) - DefCat("ADVANCED_GEO", { [SIDES.ARMADA] = "armgmm", [SIDES.CORTEX] = "corgmm", [SIDES.LEGION] = "leggmm" }) - DefCat("UW_ADV_GEO", { [SIDES.ARMADA] = "armuwageo", [SIDES.CORTEX] = "coruwageo", [SIDES.LEGION] = "leguwageo" }) + DefCat("ADVANCED_GEO", { [SIDES.ARMADA] = "armgmm", [SIDES.CORTEX] = "corgmm" }) + DefCat( + "UW_ADV_GEO", + { [SIDES.ARMADA] = "armuwageo", [SIDES.CORTEX] = "coruwageo", [SIDES.LEGION] = "leganavaladvgeo" } + ) DefCat("ENERGY_STORAGE", { [SIDES.ARMADA] = "armestor", [SIDES.CORTEX] = "corestor", [SIDES.LEGION] = "legestor" }) DefCat( "ADVANCED_ENERGY_STORAGE", { [SIDES.ARMADA] = "armuwadves", [SIDES.CORTEX] = "coradvestore", [SIDES.LEGION] = "legadvestore" } ) - DefCat("UW_ENERGY_STORAGE", { [SIDES.ARMADA] = "armuwes", [SIDES.CORTEX] = "coruwes", [SIDES.LEGION] = "leguwes" }) + DefCat( + "UW_ENERGY_STORAGE", + { [SIDES.ARMADA] = "armuwes", [SIDES.CORTEX] = "coruwes", [SIDES.LEGION] = "leguwestore" } + ) DefCat( "UW_ADVANCED_ENERGY_STORAGE", - { [SIDES.ARMADA] = "armuwadves", [SIDES.CORTEX] = "coruwadves", [SIDES.LEGION] = "coruwadves" } + { [SIDES.ARMADA] = "armuwadves", [SIDES.CORTEX] = "coruwadves", [SIDES.LEGION] = "legadvestore" } ) -- Factory buildings @@ -123,8 +132,11 @@ function DefinitionsModule.defineUnitCategories() "ADVANCED_AIRCRAFT_PLANT", { [SIDES.ARMADA] = "armaap", [SIDES.CORTEX] = "coraap", [SIDES.LEGION] = "legaap" } ) - DefCat("SHIPYARD", { [SIDES.ARMADA] = "armsy", [SIDES.CORTEX] = "corsy", [SIDES.LEGION] = "corsy" }) - DefCat("ADVANCED_SHIPYARD", { [SIDES.ARMADA] = "armasy", [SIDES.CORTEX] = "corasy", [SIDES.LEGION] = "legasy" }) + DefCat("SHIPYARD", { [SIDES.ARMADA] = "armsy", [SIDES.CORTEX] = "corsy", [SIDES.LEGION] = "legsy" }) + DefCat( + "ADVANCED_SHIPYARD", + { [SIDES.ARMADA] = "armasy", [SIDES.CORTEX] = "corasy", [SIDES.LEGION] = "legadvshipyard" } + ) DefCat("HOVER_PLATFORM", { [SIDES.ARMADA] = "armhp", [SIDES.CORTEX] = "corhp", [SIDES.LEGION] = "leghp" }) DefCat( "AMPHIBIOUS_COMPLEX", @@ -161,7 +173,7 @@ function DefinitionsModule.defineUnitCategories() ) DefCat( "FLOATING_HEAVY_LASER", - { [SIDES.ARMADA] = "armfhlt", [SIDES.CORTEX] = "corfhlt", [SIDES.LEGION] = "legfhlt" } + { [SIDES.ARMADA] = "armfhlt", [SIDES.CORTEX] = "corfhlt", [SIDES.LEGION] = "legfhive" } ) DefCat("FLOATING_MISSILE", { [SIDES.ARMADA] = "armfrt", [SIDES.CORTEX] = "corfrt", [SIDES.LEGION] = "legfrl" }) DefCat( @@ -169,8 +181,11 @@ function DefinitionsModule.defineUnitCategories() { [SIDES.ARMADA] = "armmercury", [SIDES.CORTEX] = "corscreamer", [SIDES.LEGION] = "leglraa" } ) DefCat("TORPEDO", { [SIDES.ARMADA] = "armdl", [SIDES.CORTEX] = "cordl", [SIDES.LEGION] = "legctl" }) - DefCat("ADV_TORPEDO", { [SIDES.ARMADA] = "armatl", [SIDES.CORTEX] = "coratl", [SIDES.LEGION] = "legatl" }) - DefCat("OFFSHORE_TORPEDO", { [SIDES.ARMADA] = "armptl", [SIDES.CORTEX] = "corptl", [SIDES.LEGION] = "legptl" }) + DefCat( + "ADV_TORPEDO", + { [SIDES.ARMADA] = "armatl", [SIDES.CORTEX] = "coratl", [SIDES.LEGION] = "leganavaltorpturret" } + ) + DefCat("OFFSHORE_TORPEDO", { [SIDES.ARMADA] = "armptl", [SIDES.CORTEX] = "corptl" }) DefCat("ARTILLERY", { [SIDES.ARMADA] = "armguard", [SIDES.CORTEX] = "corpun", [SIDES.LEGION] = "legcluster" }) DefCat( "LONG_RANGE_PLASMA_CANNON", @@ -207,8 +222,11 @@ function DefinitionsModule.defineUnitCategories() DefCat("ADV_RADAR", { [SIDES.ARMADA] = "armarad", [SIDES.CORTEX] = "corarad", [SIDES.LEGION] = "legarad" }) DefCat("JAMMER", { [SIDES.ARMADA] = "armjamt", [SIDES.CORTEX] = "corjamt", [SIDES.LEGION] = "legjam" }) DefCat("ADVANCED_JAMMER", { [SIDES.ARMADA] = "armveil", [SIDES.CORTEX] = "corshroud", [SIDES.LEGION] = "legajam" }) - DefCat("SONAR", { [SIDES.ARMADA] = "armsonar", [SIDES.CORTEX] = "corsonar", [SIDES.LEGION] = "legsonar" }) - DefCat("ADV_SONAR", { [SIDES.ARMADA] = "armason", [SIDES.CORTEX] = "corason", [SIDES.LEGION] = "legason" }) + DefCat("SONAR", { [SIDES.ARMADA] = "armsonar", [SIDES.CORTEX] = "corsonar" }) + DefCat( + "ADV_SONAR", + { [SIDES.ARMADA] = "armason", [SIDES.CORTEX] = "corason", [SIDES.LEGION] = "leganavalsonarstation" } + ) DefCat("CAMERA", { [SIDES.ARMADA] = "armeyes", [SIDES.CORTEX] = "coreyes", [SIDES.LEGION] = "legeyes" }) DefCat("NUKE", { [SIDES.ARMADA] = "armsilo", [SIDES.CORTEX] = "corsilo", [SIDES.LEGION] = "legsilo" }) DefCat("ANTINUKE", { [SIDES.ARMADA] = "armamd", [SIDES.CORTEX] = "corfmd", [SIDES.LEGION] = "legabm" }) @@ -235,7 +253,7 @@ function DefinitionsModule.defineUnitCategories() DefCat("FLOATING_RADAR_PG", { [SIDES.ARMADA] = "armfrad", [SIDES.CORTEX] = "corfrad", [SIDES.LEGION] = "legfrad" }) DefCat( "FLOATING_CONVERTER_PG", - { [SIDES.ARMADA] = "armfmkr", [SIDES.CORTEX] = "corfmkr", [SIDES.LEGION] = "legfmkr" } + { [SIDES.ARMADA] = "armfmkr", [SIDES.CORTEX] = "corfmkr", [SIDES.LEGION] = "legfeconv" } ) DefCat( "FLOATING_DRAGONSTEETH_PG", diff --git a/luaui/Widgets/cmd_factoryqmanager.lua b/luaui/Widgets/cmd_factoryqmanager.lua index 40d6bd7d7a5..257ea842388 100644 --- a/luaui/Widgets/cmd_factoryqmanager.lua +++ b/luaui/Widgets/cmd_factoryqmanager.lua @@ -1,5 +1,5 @@ include("keysym.h.lua") -local versionNumber = 1.7 +local versionNumber = 1.8 local widget = widget ---@type Widget @@ -9,7 +9,7 @@ function widget:GetInfo() desc = "Saves and Loads Factory Queues. Load: Meta+[0-9], Save: Alt+Meta+[0-9] (v" .. string.format("%.1f", versionNumber) .. ")", - author = "very_bad_soldier, Chronographer", + author = "very_bad_soldier, Chronographer, Baldric", date = "Jul 6, 2008", license = "GNU GPL, v2 or later", layer = -9000, @@ -38,6 +38,7 @@ local CMD_OPT_CTRL = CMD.OPT_CTRL local CMD_OPT_ALT = CMD.OPT_ALT --Changelog +--1.8: added: contextual hotkeys - load actions only fire/consume while the preset panel is shown and armed, so they fall through to other widgets otherwise; added 'factory_preset_toggle' action (tap to show/arm, tap again to hide). --1.7: fixed: save unit presets by unit name not unitDefId --1.6: added: support of quotas and 'alt' queued priority units in preset --1.5: added repeat icon and bindable keybind actions to activate @@ -100,14 +101,15 @@ local lastBoxY = nil local boxCoords = {} local curModId = nil local renderPresets = false +local loadEnabled = false -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -local boxWidth -local boxHeight -local boxHeightTitle -local boxIconBorder +local boxWidth = 0 +local boxHeight = 0 +local boxHeightTitle = 0 +local boxIconBorder = 0 local fontSizeTitle local fontSizeGroup @@ -382,9 +384,27 @@ function saveQueue(unitId, unitDef, groupNo) unitQuotaIdx = WG.Quotas.isOnQuotaMode(unitId) end + -- Quota-issued orders are saved separately but can't be distinguished from some player orders. + -- When a player alt-enqueues a build order to the front, then, we distinguish it by counting. + local quotaOrdersLeft = {} + local function isQuotaOrder(cmd) + if not (WG.Quotas and cmd.options.internal and cmd.options.alt) then + return false + end + local quotaDefID = -cmd.id + if not quotaOrdersLeft[quotaDefID] then + quotaOrdersLeft[quotaDefID] = WG.Quotas.getQuotaOrderCount(unitId, quotaDefID) + end + if quotaOrdersLeft[quotaDefID] > 0 then + quotaOrdersLeft[quotaDefID] = quotaOrdersLeft[quotaDefID] - 1 + return true + end + return false + end + for i = #unitQ, 1, -1 do - if unitQ[i].id >= 0 or unitQ[i].options.internal and not unitQ[i].options.alt then -- We don't want to save these commands - table.remove(unitQ, i) + if unitQ[i].id >= 0 or (unitQ[i].options.internal and not unitQ[i].options.alt) or isQuotaOrder(unitQ[i]) then + table.remove(unitQ, i) -- We don't want to save these commands else unitQ[i].name = orderToName(unitQ[i].id) unitQ[i].id = nil @@ -482,19 +502,61 @@ local function factoryPresetKeyHandler(_, _, args) local gr = tonumber(key) if selUnit == nil then - return + return false end if mode == "save" then saveQueue(selUnit, unitDef, gr) + return true elseif mode == "load" then + -- only act on (and consume) the load hotkey while the preset panel is shown AND the load has been armed + if not (renderPresets and loadEnabled) then + return false + end loadQueue(selUnit, unitDef, gr) + return true + end + + return false +end + +local function factoryPresetShow(_, _, _, _, _, release) + if not release then + renderPresets = true + loadEnabled = true + else + renderPresets = false + loadEnabled = false end + return false end -local function factoryPresetRender(_, _, _, data) - data = data or {} - renderPresets = data[1] +-- Contextual show/arm handler (action: factory_preset_toggle). +-- State machine (renderPresets, loadEnabled): +-- press while not shown -> show the panel (true, false) +-- release while shown & not armed -> arm the load hotkeys (true, true) +-- press while shown -> nothing but the load hotkeys are armed +-- release while shown & armed -> hide panel & disarm (false, false) +local function factoryPresetToggle(_, _, _, _, _, release) + if getSingleFactory() == nil then + return false + end + + if not release then + if not renderPresets then + renderPresets = true + end + else + if renderPresets then + if not loadEnabled then + loadEnabled = true + else + renderPresets = false + loadEnabled = false + end + end + end + return false end @@ -875,14 +937,20 @@ function widget:Initialize() migratePresets(savedQueues[curModId]) -- remove old presets that were saved by version < 1.7 which used numeric unitDefID instead of names widgetHandler:AddAction("factory_preset", factoryPresetKeyHandler, nil, "p") - widgetHandler:AddAction("factory_preset_show", factoryPresetRender, { true }, "p") - widgetHandler:AddAction("factory_preset_show", factoryPresetRender, { false }, "r") + widgetHandler:AddAction("factory_preset_show", factoryPresetShow, nil, "pr") + widgetHandler:AddAction("factory_preset_toggle", factoryPresetToggle, nil, "pr") end function widget:Update() local now = Spring.GetGameSeconds() local timediff = now - lastGameSeconds + -- reset the show/arm state whenever a single factory is no longer selected + if (renderPresets or loadEnabled) and getSingleFactory() == nil then + renderPresets = false + loadEnabled = false + end + if renderPresets then -- meta (space) if alpha < 1.0 then @@ -925,4 +993,5 @@ end function widget:Shutdown() widgetHandler:RemoveAction("factory_preset") widgetHandler:RemoveAction("factory_preset_show") + widgetHandler:RemoveAction("factory_preset_toggle") end diff --git a/luaui/Widgets/gui_attackrange_gl4.lua b/luaui/Widgets/gui_attackrange_gl4.lua index b758c620575..6f12f3fe432 100644 --- a/luaui/Widgets/gui_attackrange_gl4.lua +++ b/luaui/Widgets/gui_attackrange_gl4.lua @@ -343,6 +343,10 @@ local function initializeUnitDefRing(unitDefID) or weaponDef.customParams.norangering then range = 0 + elseif weaponDef.customParams.carried_unit then + -- The controller's own range is tuned for how close the carrier itself + -- closes in; how far the drones will engage is engagementrange. + range = tonumber(weaponDef.customParams.engagementrange) or range end --spEcho("weaponNum: ".. weaponNum ..", name: " .. tableToString(weaponDef.name)) local groupselectionfadescale = colorConfig[weaponTypeMap[weaponType]].groupselectionfadescale @@ -742,6 +746,11 @@ local function AddSelectedUnit(unitID, mouseover, newRange) if true then --range > 0 then -- trying something different if weapon.onlyTargets and weapon.onlyTargets.vtol then entry.weapons[weaponNum] = 3 -- weaponTypeMap[3] is "AA" + elseif weaponDef.customParams.carried_unit then + -- Drone controllers are dummy cannons that never fire. Their envelope is + -- a flat circle, so drawing them ballistically makes the ring bulge and + -- shrink over terrain that the drones ignore. + entry.weapons[weaponNum] = 1 -- weaponTypeMap[1] is "ground" elseif weaponDef.type == "Cannon" then -- if weaponDef.range < 700 then -- entry.weapons[weaponNum] = 1 -- weaponTypeMap[1] is "ground" diff --git a/luaui/Widgets/gui_buildmenu.lua b/luaui/Widgets/gui_buildmenu.lua index c36a62db8d0..40bb6346672 100644 --- a/luaui/Widgets/gui_buildmenu.lua +++ b/luaui/Widgets/gui_buildmenu.lua @@ -866,9 +866,27 @@ function drawBuildmenuBg() ) end +-- The player queue vs the widget-provided queue have different accounting due to quota mode. +-- cellQuotas contains the live count from the widget, which we should remove from the total. +local function getPlayerQueueCount(cellRectID, uDefID) + local queueCount = tonumber(cmds[cellRectID].params[1]) + if not queueCount then + return nil + end + local quotaInfo = cellQuotas[uDefID] + if quotaInfo and WG.Quotas then + queueCount = queueCount - WG.Quotas.getQuotaOrderCount(quotaInfo.builderID, uDefID) + end + if queueCount < 1 then + return nil + end + return queueCount +end + local function drawCell(cellRectID, usedZoom, cellColor, disabled, underConstruction) tracy.ZoneBeginN("W:BuildMenu:DrawCell") local uDefID = -cmds[cellRectID].id + local queueCount = getPlayerQueueCount(cellRectID, uDefID) local unitTexture = "#" .. uDefID local cellRect = cellRects[cellRectID] if not cellRect then @@ -925,7 +943,7 @@ local function drawCell(cellRectID, usedZoom, cellColor, disabled, underConstruc and (groups[units.unitGroup[uDefID]] and ":l" .. texprefix .. ":" .. groups[units.unitGroup[uDefID]] or nil) or nil, { units.unitMetalCost[uDefID], units.unitEnergyCost[uDefID] }, - tonumber(cmds[cellRectID].params[1]) + queueCount ) tracy.ZoneEnd() @@ -1056,9 +1074,9 @@ local function drawCell(cellRectID, usedZoom, cellColor, disabled, underConstruc end -- factory queue number - if cmds[cellRectID].params[1] then + if queueCount then local pad = math_floor(cellInnerSize * 0.03) - local textWidth = math_floor(font2:GetTextWidth(cmds[cellRectID].params[1] .. " ") * cellInnerSize * 0.285) + local textWidth = math_floor(font2:GetTextWidth(queueCount .. " ") * cellInnerSize * 0.285) local pad2 = 0 RectRound( cellRects[cellRectID][3] - cellPadding - iconPadding - textWidth - pad2, @@ -1100,7 +1118,7 @@ local function drawCell(cellRectID, usedZoom, cellColor, disabled, underConstruc { 1, 1, 1, 0.1 } ) font2:Print( - "\255\190\255\190" .. cmds[cellRectID].params[1], + "\255\190\255\190" .. queueCount, cellRects[cellRectID][1] + cellPadding + math_floor(cellInnerSize * 0.96) - pad2, cellRects[cellRectID][2] + cellPadding + math_floor(cellInnerSize * 0.735) - pad2, cellInnerSize * 0.29, @@ -2038,7 +2056,8 @@ function widget:MousePress(x, y, button) end end else - local queueCount = tonumber(cmds[cellRectID].params[1] or 0) + -- Ignores the count from the quota widget. + local queueCount = getPlayerQueueCount(cellRectID, -uDefID) or 0 local function decreaseQuota() if changeQuotas(-uDefID, modKeyMultiplier.right) and playSounds then diff --git a/luaui/Widgets/gui_defenserange_gl4.lua b/luaui/Widgets/gui_defenserange_gl4.lua index c3b7894394f..1470d29b44f 100644 --- a/luaui/Widgets/gui_defenserange_gl4.lua +++ b/luaui/Widgets/gui_defenserange_gl4.lua @@ -336,7 +336,7 @@ local function initUnitList() legrhapsis = { weapons = { "air" } }, --T1.5 AA legflak = { weapons = { "air" } }, --T2 AA FLAK leglraa = { weapons = { "air" } }, --T2 LR-AA - legperdition = { weapons = { "cannon" } }, --T2 LR-AA + legperdition = { weapons = { "ground" } }, --T2 napalm missile launcher legapopupdef = { weapons = { "ground" } }, --popup riot/minigun turret leganavaltorpturret = { weapons = { "ground" } }, --torpedo launcher leganavalaaturret = { weapons = { "air" } }, --Fulmen @@ -353,7 +353,7 @@ local function initUnitList() legavantinuke = { weapons = { "nuke" } }, armantiship = { weapons = { "nuke" } }, corantiship = { weapons = { "nuke" } }, - leganavyantinukecarrier = { weapons = { "nuke" } }, -- NOTE: drone weapon shown in attack ranges + leganavyantinukecarrier = { weapons = { [2] = "nuke" } }, -- weapon 1 is the drone controller -- SCAVENGERS scavbeacon_t1_scav = { weapons = { "ground" } }, @@ -1014,7 +1014,7 @@ function widget:Update(dt) local rings = unitDefRings[buildUnitDefID] if rings then -- find out which VBO to remove from: - for i, weaponType in ipairs(rings.weapons) do + for i, weaponType in pairs(rings.weapons) do buildDrawOverride[weaponType] = false for j, allyenemy in ipairs(allyenemypairs) do -- remove from all local vaokey = allyenemy .. weaponType diff --git a/luaui/Widgets/gui_gridmenu.lua b/luaui/Widgets/gui_gridmenu.lua index ed8bd23f0f9..ee94d65d3d9 100644 --- a/luaui/Widgets/gui_gridmenu.lua +++ b/luaui/Widgets/gui_gridmenu.lua @@ -2186,6 +2186,13 @@ local function drawCell(rect) local disabled = rect.opts.disabled local underConstructionDim = backgroundRect.opts.builderUnderConstruction and not rect.opts.hovered and not disabled local queuenr = rect.opts.queuenr + if queuenr and WG.Quotas then + -- Ignore the count from the quota widget. + queuenr = queuenr - WG.Quotas.getQuotaOrderCount(activeBuilderID, uid) + if queuenr < 1 then + queuenr = nil + end + end local quotaNumber if WG.Quotas and WG.Quotas.getQuotas()[activeBuilderID] and WG.Quotas.getQuotas()[activeBuilderID][uid] then quotaNumber = WG.Quotas.getQuotas()[activeBuilderID][uid] @@ -2938,7 +2945,9 @@ function widget:MousePress(x, y, button) end local isQuotaMode = WG.Quotas and WG.Quotas.isOnQuotaMode(activeBuilderID) and not alt + -- Ignore the count from the quota widget. local queueCount = tonumber(cellRect.opts.queuenr or 0) + - (WG.Quotas and WG.Quotas.getQuotaOrderCount(activeBuilderID, unitDefID) or 0) local quotas = WG.Quotas and WG.Quotas.getQuotas() local currentQuota = ( quotas @@ -3204,10 +3213,10 @@ function widget:UnitCmdDone(unitID, unitDefID, unitTeam, cmdID, cmdParams, optio return end - -- If factory is in repeat, queue does not change, except if it is alt-queued + -- The queue does not change under repeat because the order is recycled to the back. local factoryRepeat = select(4, Spring.GetUnitStates(unitID, false, true)) - - if factoryRepeat and not options.alt then + -- Internal orders are the exception; see `CFactoryCAI::DecreaseQueueCount`. + if factoryRepeat and not options.internal then return end diff --git a/luaui/Widgets/gui_teamstats.lua b/luaui/Widgets/gui_teamstats.lua index 06583a7b805..077e025e9b6 100644 --- a/luaui/Widgets/gui_teamstats.lua +++ b/luaui/Widgets/gui_teamstats.lua @@ -4,837 +4,2645 @@ function widget:GetInfo() return { name = "TeamStats", desc = "Shows game stats.", - author = "", - version = "", + author = "Floris", + version = "2.0", date = "", - license = "", + license = "GNU GPL, v2 or later", layer = -99990, enabled = true, } end +local text = VFS.Include("luaui/Include/keybind_text.lua") + -- Localized functions for performance local mathFloor = math.floor local mathMax = math.max local mathMin = math.min +local mathAbs = math.abs +local mathLog10 = math.log10 +local mathHuge = math.huge +local tableSort = table.sort +local stringFormat = string.format +local stringLower = string.lower +local formatSI = string.formatSI +local math_isInRect = math.isInRect -- Localized Spring API for performance -local spGetMouseState = Spring.GetMouseState local spGetViewGeometry = Spring.GetViewGeometry +local spGetMouseState = Spring.GetMouseState +local spIsGUIHidden = Spring.IsGUIHidden +local spGetGameFrame = Spring.GetGameFrame +local spGetTeamStatsHistory = Spring.GetTeamStatsHistory +local spGetTeamInfo = Spring.GetTeamInfo +local spGetPlayerInfo = Spring.GetPlayerInfo +local spGetTeamColor = Spring.GetTeamColor +local spGetGameRulesParam = Spring.GetGameRulesParam +local spGetLocalTeamID = Spring.GetLocalTeamID local spGetSpectatingState = Spring.GetSpectatingState +local spGetAllyTeamList = Spring.GetAllyTeamList +local spGetTeamList = Spring.GetTeamList +local spGetGaiaTeamID = Spring.GetGaiaTeamID +local spGetModKeyState = Spring.GetModKeyState +local spPlaySoundFile = Spring.PlaySoundFile +local spSetMouseCursor = Spring.SetMouseCursor -local vsx, vsy = spGetViewGeometry() - -local fontSize = 22 -- is calculated somewhere else anyway -local fontSizePercentage = 0.6 -- fontSize * X = actual fontsize -local update = 30 -- in frames -local replaceEndStats = false -local sortHighLightColour = { 1, 0.87, 0.87, 0.22 } -local sortHighLightColourDesc = { 0.9, 1, 0.9, 0.22 } -local activeSortColour = { 1, 0.62, 0.62, 0.22 } -local activeSortColourDesc = { 0.66, 1, 0.66, 0.22 } -local oddLineColour = { 0.28, 0.28, 0.28, 0.06 } -local evenLineColour = { 1, 1, 1, 0.06 } -local sortLineColour = { 0.82, 0.82, 0.82, 0.1 } +local glCreateList = gl.CreateList +local glCallList = gl.CallList +local glDeleteList = gl.DeleteList +local glColor = gl.Color +local glTexture = gl.Texture +local glBeginEnd = gl.BeginEnd +local glVertex = gl.Vertex +local GL_TRIANGLES = GL.TRIANGLES -local widgetScale = (vsy / 1080) -local math_isInRect = math.isInRect +-- Frames between refreshes while the panel is open. The engine's newest history entry is +-- live, so every refresh really is newer data. +local UPDATE_FRAMES = 30 +---@type boolean local playSounds = true local buttonclick = "LuaUI/Sounds/buildbar_waypoint.wav" -local lineHeight = fontSize +-- The Graphs entry in the column is where a history page will go. Off until there is one +-- to open, so players are not offered something that does nothing. +---@type boolean +local showPlannedPages = false -local isFFA = BAR.Utilities.Gametype.IsFFA() +local screenHeightOrg = 610 +local screenWidthOrg = 1100 +local screenHeight = screenHeightOrg +local screenWidth = screenWidthOrg -local header = { - "frame", - "damageDealt", - "damageReceived", - "unitsProduced", - "unitsKilled", - "unitsDied", - "damageEfficiency", +local vsx, vsy = spGetViewGeometry() +local widgetScale = (vsy / 1080) +local screenX = mathFloor((vsx * 0.5) - (screenWidth / 2)) +local screenY = mathFloor((vsy * 0.5) + (screenHeight / 2)) + +---@type function +local RectRound +---@type function +local UiElement +---@type function +local UiScroller +---@type function +local UiScrollerAt +---@type function +local Highlight +---@type function +local UiToggle +---@type number +local elementCorner +---@type LuaFont +local font + +---------------------------------------------------------------- +-- The columns +---------------------------------------------------------------- + +-- Every column the panel can show. `group` is the caption spanning the neighbouring +-- columns that share it, `stat` the column's full name for its tooltip and `short` the +-- caption under the group, all i18n keys under ui.teamStats: a column in the overview is +-- a few characters wide, so the caption is the short form and the tooltip the full one. +-- `fmt` says how a value prints: "si" takes an SI prefix, "percent" prints as one and +-- "plain" is the number as it is. `rate` marks a running total the per-minute switch +-- turns into a rate; a ratio, a level or what is in storage right now is not one. +---@type table +local COLUMNS = { + name = { group = "", stat = "player", short = "player", fmt = "name" }, + damageDealt = { group = "damage", stat = "damageDealt", short = "damageDealt", fmt = "si", rate = true }, + damageReceived = { group = "damage", stat = "damageReceived", short = "shortReceived", fmt = "si", rate = true }, + damageEfficiency = { group = "damage", stat = "damageEfficiency", short = "shortEfficiency", fmt = "percent" }, + unitsProduced = { group = "units", stat = "unitsProduced", short = "shortBuilt", fmt = "si", rate = true }, + unitsKilled = { group = "units", stat = "unitsKilled", short = "unitsKilled", fmt = "si", rate = true }, + unitsDied = { group = "units", stat = "unitsDied", short = "unitsDied", fmt = "si", rate = true }, + killEfficiency = { group = "units", stat = "killEfficiency", short = "shortEfficiency", fmt = "percent" }, + unitsCaptured = { group = "units", stat = "unitsCaptured", short = "unitsCaptured", fmt = "si", rate = true }, + unitsStolen = { group = "units", stat = "unitsStolen", short = "unitsStolen", fmt = "si", rate = true }, + unitsReceived = { group = "units", stat = "unitsReceived", short = "unitsReceived", fmt = "si", rate = true }, + unitsSent = { group = "units", stat = "unitsSent", short = "unitsSent", fmt = "si", rate = true }, + unitsActive = { group = "units", stat = "unitsActive", short = "unitsActive", fmt = "si" }, + metalProduced = { group = "metal", stat = "resourceProduced", short = "shortProduced", fmt = "si", rate = true }, + metalUsed = { group = "metal", stat = "resourceUsed", short = "resourceUsed", fmt = "si", rate = true }, + metalExcess = { group = "metal", stat = "resourceExcess", short = "resourceExcess", fmt = "si", rate = true }, + metalSent = { group = "metal", stat = "resourceSent", short = "resourceSent", fmt = "si", rate = true }, + metalReceived = { group = "metal", stat = "resourceReceived", short = "shortReceived", fmt = "si", rate = true }, + metalStored = { group = "metal", stat = "resourceStored", short = "resourceStored", fmt = "si" }, + energyProduced = { group = "energy", stat = "resourceProduced", short = "shortProduced", fmt = "si", rate = true }, + energyUsed = { group = "energy", stat = "resourceUsed", short = "resourceUsed", fmt = "si", rate = true }, + energyExcess = { group = "energy", stat = "resourceExcess", short = "resourceExcess", fmt = "si", rate = true }, + energySent = { group = "energy", stat = "resourceSent", short = "resourceSent", fmt = "si", rate = true }, + energyReceived = { group = "energy", stat = "resourceReceived", short = "shortReceived", fmt = "si", rate = true }, + energyStored = { group = "energy", stat = "resourceStored", short = "resourceStored", fmt = "si" }, + aggressionLevel = { group = "activity", stat = "aggression", short = "shortAggression", fmt = "plain" }, + actionsPerMinute = { group = "activity", stat = "actionsPerMinute", short = "shortActionsPerMinute", fmt = "plain" }, + -- What is happening right now, read fresh at every refresh: the economy from the engine + -- and the rest from the team stats gadget. None of these is a running total, so the + -- per-minute switch leaves them be. `of` names the two amounts a percentage is made of, + -- for its tooltip, and `count` the number of units behind a value. + metalIncome = { group = "metal", stat = "resourceIncome", short = "resourceIncome", fmt = "si" }, + metalExpense = { group = "metal", stat = "resourceExpense", short = "resourceExpense", fmt = "si" }, + metalLevel = { + group = "metal", + stat = "resourceLevel", + short = "resourceLevel", + fmt = "percent", + of = { "metalCurrent", "metalStorage" }, + }, + energyIncome = { group = "energy", stat = "resourceIncome", short = "resourceIncome", fmt = "si" }, + energyExpense = { group = "energy", stat = "resourceExpense", short = "resourceExpense", fmt = "si" }, + energyLevel = { + group = "energy", + stat = "resourceLevel", + short = "resourceLevel", + fmt = "percent", + of = { "energyCurrent", "energyStorage" }, + }, + conversion = { + group = "industry", + stat = "conversion", + short = "shortConversion", + fmt = "percent", + of = { "convUse", "convCapacity" }, + }, + buildPower = { group = "industry", stat = "buildPower", short = "shortBuildPower", fmt = "si" }, + buildPowerUse = { + group = "industry", + stat = "buildPowerUse", + short = "shortBuildPowerUse", + fmt = "percent", + of = { "buildPowerActive", "buildPower" }, + }, + -- The total sits with the unit counts, so a view showing it alone captions it "Units". + unitValue = { group = "units", stat = "unitValue", short = "shortValue", fmt = "si", count = "unitCount" }, + valueArmy = { group = "value", stat = "valueArmy", short = "shortArmy", fmt = "si", count = "countArmy" }, + valueAir = { group = "value", stat = "valueAir", short = "shortAir", fmt = "si", count = "countAir" }, + valueSea = { group = "value", stat = "valueSea", short = "shortSea", fmt = "si", count = "countSea" }, + valueDefense = { + group = "value", + stat = "valueDefense", + short = "shortDefense", + fmt = "si", + count = "countDefense", + }, + valueStrategic = { + group = "value", + stat = "valueStrategic", + short = "shortStrategic", + fmt = "si", + count = "countStrategic", + }, + valueFactories = { + group = "value", + stat = "valueFactories", + short = "shortFactories", + fmt = "si", + count = "countFactories", + }, + valueBuilders = { + group = "value", + stat = "valueBuilders", + short = "shortBuilders", + fmt = "si", + count = "countBuilders", + }, + valueEconomy = { + group = "value", + stat = "valueEconomy", + short = "shortEconomy", + fmt = "si", + count = "countEconomy", + }, + valueUtility = { + group = "value", + stat = "valueUtility", + short = "shortUtility", + fmt = "si", + count = "countUtility", + }, + -- Running totals the gadget keeps: what was destroyed and lost, in metal. + killedValue = { group = "traded", stat = "killedValue", short = "unitsKilled", fmt = "si", rate = true }, + lostValue = { group = "traded", stat = "lostValue", short = "unitsDied", fmt = "si", rate = true }, + valueEfficiency = { + group = "traded", + stat = "valueEfficiency", + short = "shortEfficiency", + fmt = "percent", + even = true, + }, + teamKillValue = { group = "traded", stat = "teamKillValue", short = "shortTeamKill", fmt = "si", rate = true }, + comKills = { group = "commanders", stat = "comKills", short = "unitsKilled", fmt = "si", rate = true }, + comLost = { group = "commanders", stat = "comLost", short = "unitsDied", fmt = "si", rate = true }, +} +for key, column in pairs(COLUMNS) do + column.key = key +end +-- A ratio that is good above even and bad below it: the efficiencies. A level or a +-- share of capacity is neither. +COLUMNS.damageEfficiency.even = true +COLUMNS.killEfficiency.even = true +-- The columns only the team stats gadget can fill. Without it they are left out of the +-- table rather than shown empty; the economy and the conversion the engine tells allies +-- on its own stay. +for _, key in ipairs({ + "buildPower", + "buildPowerUse", + "unitValue", + "killedValue", + "lostValue", + "valueEfficiency", + "teamKillValue", + "comKills", + "comLost", +}) do + COLUMNS[key].gadget = true +end +for _, column in pairs(COLUMNS) do + if column.group == "value" then + column.gadget = true + end +end + +-- The column's views: which columns each shows, in order. The first is the overview the +-- panel opens on; the others each take one side of the game and have room for all of it. +local GROUPS = { + { + key = "all", + columns = { + "damageDealt", + "damageReceived", + "damageEfficiency", + "unitsProduced", + "unitsKilled", + "unitsDied", + "metalProduced", + "metalExcess", + "energyProduced", + "energyExcess", + "aggressionLevel", + "actionsPerMinute", + }, + }, + { + key = "live", + columns = { + "metalIncome", + "metalExpense", + "metalLevel", + "energyIncome", + "energyExpense", + "energyLevel", + "conversion", + "buildPower", + "buildPowerUse", + "unitValue", + "unitsActive", + }, + }, + { + key = "economy", + columns = { + "metalProduced", + "metalUsed", + "metalExcess", + "metalSent", + "metalReceived", + "metalStored", + "energyProduced", + "energyUsed", + "energyExcess", + "energySent", + "energyReceived", + "energyStored", + }, + }, + { + key = "combat", + columns = { + "damageDealt", + "damageReceived", + "damageEfficiency", + "unitsKilled", + "unitsDied", + "killEfficiency", + "killedValue", + "lostValue", + "valueEfficiency", + "teamKillValue", + "comKills", + "comLost", + }, + }, + { + key = "units", + columns = { + "unitsProduced", + "unitsKilled", + "unitsDied", + "killEfficiency", + "unitsCaptured", + "unitsStolen", + "unitsReceived", + "unitsSent", + "unitsActive", + }, + }, + { + key = "composition", + columns = { + "unitValue", + "valueArmy", + "valueAir", + "valueSea", + "valueDefense", + "valueStrategic", + "valueFactories", + "valueBuilders", + "valueEconomy", + "valueUtility", + }, + }, + { + key = "activity", + columns = { + "actionsPerMinute", + "aggressionLevel", + "damageDealt", + "unitsProduced", + "metalProduced", + "energyProduced", + }, + }, +} +local groupByKey = {} +for _, group in ipairs(GROUPS) do + groupByKey[group.key] = group +end + +-- The engine's own counters, which an ally team's total is the sum of. Everything else a +-- row shows is derived from these, for a team and for its ally team alike. +local SUMMED = { + "metalUsed", "metalProduced", "metalExcess", + "metalReceived", + "metalSent", + "energyUsed", "energyProduced", "energyExcess", - "aggressionLevel", + "energyReceived", + "energySent", + "damageDealt", + "damageReceived", + "unitsProduced", + "unitsDied", + "unitsReceived", + "unitsSent", + "unitsCaptured", + "unitsOutCaptured", + "unitsKilled", "actionsPerMinute", + -- The live amounts, summed the same way; the ratios among them are derived from the + -- sums. A team the viewer may not see leaves its ally team's total unknown. + "metalIncome", + "metalExpense", + "metalCurrent", + "metalStorage", + "energyIncome", + "energyExpense", + "energyCurrent", + "energyStorage", + "convCapacity", + "convUse", + "buildPower", + "buildPowerActive", + "unitCount", + "unitValue", + "killedValue", + "killedArmyValue", + "killedEcoValue", + "lostValue", + "teamKillValue", + "comKills", + "comLost", } +-- The composition buckets the gadget counts, each with a count and a value. +for _, bucket in ipairs({ "Army", "Air", "Sea", "Defense", "Strategic", "Factories", "Builders", "Economy", "Utility" }) do + SUMMED[#SUMMED + 1] = "count" .. bucket + SUMMED[#SUMMED + 1] = "value" .. bucket +end -local headerRemap = {} -- filled in initialize - -local aspectMult = vsx / vsy -local guiData = { - mainPanel = { - relSizes = { - x = { - min = 0.1 + (0.08 * aspectMult), - max = 0.9 - (0.08 * aspectMult), - length = 0.49, - }, - y = { - min = 0.22, - max = 0.76, - length = 0.6, - }, - }, - draggingBorderSize = 7, - visible = false, - }, +-- The header switches, right to left as they are laid out. `groupByTeam` only means +-- something with ally teams to group by, so it is left out of a free-for-all. +---@type table[] +local switches = { + { key = "groupByTeam" }, + -- Only offered while there are bands to take a share of. + { key = "shareOfTeam" }, + { key = "perMinute" }, + { key = "bars" }, +} +---@type table +local filters = { groupByTeam = true, shareOfTeam = false, perMinute = false, bars = false } + +-- Excess is waste once it passes a share of what was produced; these say what to compare +-- each excess column with. +local WASTE_OF = { metalExcess = "metalProduced", energyExcess = "energyProduced" } + +-- The lines a player's name shows on hover, view by view: every column the panel has, +-- so the ones the open view hides are a hover away. A line with no caption continues the +-- one above it. +local CARD_LINES = { + { "damage", { "damageDealt", "damageReceived", "damageEfficiency" } }, + { "units", { "unitsProduced", "unitsKilled", "unitsDied", "killEfficiency" } }, + { "", { "unitsCaptured", "unitsStolen", "unitsReceived", "unitsSent", "unitsActive", "unitValue" } }, + { "traded", { "killedValue", "lostValue", "valueEfficiency", "teamKillValue" } }, + { "commanders", { "comKills", "comLost" } }, + { "metal", { "metalProduced", "metalUsed", "metalExcess", "metalSent", "metalReceived", "metalStored" } }, + { "", { "metalIncome", "metalExpense", "metalLevel" } }, + { "energy", { "energyProduced", "energyUsed", "energyExcess", "energySent", "energyReceived", "energyStored" } }, + { "", { "energyIncome", "energyExpense", "energyLevel" } }, + { "industry", { "conversion", "buildPower", "buildPowerUse" } }, + { "value", { "valueArmy", "valueAir", "valueSea", "valueDefense", "valueStrategic" } }, + { "", { "valueFactories", "valueBuilders", "valueEconomy", "valueUtility" } }, + { "activity", { "aggressionLevel", "actionsPerMinute" } }, } -guiData.mainPanel.relSizes.x.length = (guiData.mainPanel.relSizes.x.max - guiData.mainPanel.relSizes.x.min) * 0.92 - -local ui_opacity = Spring.GetConfigFloat("ui_opacity", 0.7) -local glColor = gl.Color -local glCreateList = gl.CreateList -local glCallList = gl.CallList -local glDeleteList = gl.DeleteList +---------------------------------------------------------------- +-- Layout and look +---------------------------------------------------------------- + +local area = { x1 = 0, y1 = 0, x2 = 0, y2 = 0 } +-- Sizes derived from the scale, in one table rather than a local each, the way the other +-- panels hold their own. +local metrics = { + rowHeight = 24, + rowFs = 13, + -- An ally team's band stands taller than the rows under it and is set larger, so it + -- reads as a divider rather than another row. + bandRowHeight = 32, + bandFs = 14, + -- The two rows of the table header: the group captions spanning their columns, and + -- the stat captions under them that sort when clicked. + groupRowHeight = 22, + groupFs = 13, + statRowHeight = 24, + statFs = 12, + catRowHeight = 29, + catFs = 13, + -- The rule in the column between the views and the pages. + dividerH = 14, + rowPad = 6, + -- Room a number keeps from the right edge of its cell; the sort marker sits in it. + cellPad = 7, + sidePad = 12, + catInset = 4, + -- The bar of team colour down a row's left edge, and how far it stops short of the + -- rows above and below so each reads as its own. + accentW = 3, + accentPad = 3, + -- The bar behind a number, inside its row by this much top and bottom. + barInset = 4, + -- The line along the bottom of a group caption and of an ally team's band. + underlineH = 2, + -- Everything that sits against the panel's right edge - the switches and the + -- scrollbar - is held off it by this much. + edgeInset = 4, + -- The band the title and the switches share, and the gap below. + headerH = 34, + headerGap = 4, + -- Clearance between the table header and the first row. + tableGap = 4, + -- Clearance between the right edge of the rows and the scrollbar beside them. + listGap = 12, + -- How far the column's 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 column starts below the table beside it, to leave the title room. + sidebarDrop = 8, + sidebarW = 190, + barW = 14, + -- Rows the wheel moves per notch. + wheelRows = 3, + -- The name column: never narrower than this, and otherwise this share of the table. + nameMinW = 150, + nameShare = 0.2, + -- The sort marker beside the caption of the sorted column. + triW = 5, + triH = 4, + toggleFs = 13, + captionBleed = 3, + switchGap = 16, + -- Corner radii, taken from FlowUI's so the panel rounds like the rest of the UI. + csSmall = 2, + csPanel = 4, + -- Filled in by the layout. + nameIndent = 0, + bandTop = 0, + groupTop = 0, + groupBottom = 0, + statTop = 0, + statBottom = 0, +} +local look = { + -- The column sits on its own darker card, so it reads apart from the table. + sidebarFill = { 0, 0, 0, 0.24 }, + sidebarFillTop = { 0, 0, 0, 0.16 }, + selectedFill = { 1, 1, 1, 0.13 }, + white = { 1, 1, 1 }, + -- Rows, entries and captions hover with the same FlowUI highlight the settings list + -- uses, at the strength it gives a plain row. + rowHoverOpacity = 0.14, + -- Underline under a group caption and an ally team's band: a thin bar fading up out + -- of the bottom edge, in the hue of the caption above it. + headerLine = { 1, 0.78, 0.51, 0.4 }, + headerLineFade = { 1, 0.78, 0.51, 0 }, + sheenTop = { 1, 1, 1, 0.05 }, + -- The rule closing the table header off from the rows, and the one in the column. + rule = { 1, 1, 1, 0.08 }, + -- The bar behind a number, scaled to the column's largest. + barFill = { 1, 1, 1, 0.07 }, + -- Every second team row under a band takes this, so the eye keeps its line across a + -- dozen columns. Faint: it is a guide, not a highlight. + stripeFill = { 1, 1, 1, 0.035 }, + -- The sort marker. + sortMark = { 0.92, 0.73, 0.27, 0.9 }, + -- The font is shared with every other widget, and whichever of them set its outline + -- last is what a bake would freeze in; pinned after every Begin, the way the keybind + -- editor and the widget selector pin theirs. + outline = { 0, 0, 0, 0.4 }, + -- The sorted column, tinted down its length in the hue of its caption, so the eye + -- follows the values the table is ordered by. + sortedFill = { 1, 0.78, 0.51, 0.04 }, + -- Excess above this share of what was produced reads as waste. + wasteShare = 0.1, +} +local colorTitle = "\255\235\235\235" +local colorHeader = "\255\255\200\130" +local colorKey = "\255\235\185\070" +local colorName = "\255\235\235\235" +local colorValue = "\255\215\212\208" +-- An ally team's totals in its band: lighter than a player's numbers, since they sit on +-- the band's own sheen, and not the warm of the caption beside them. +local colorTotal = "\255\225\222\218" +local colorSelected = "\255\210\210\205" +local colorDim = "\255\160\160\160" +-- An entry that cannot be opened: dimmer than the dim used for ordinary secondary text, +-- since it has to read as unavailable rather than merely quiet. +local colorFaded = "\255\115\115\115" +-- A ratio that came out ahead and one that did not: damage or kills traded above or below +-- even, and excess that wasted a real share of what was produced. +local colorGood = "\255\150\220\150" +local colorBad = "\255\235\135\120" + +-- The captions, in the language of the last load. +---@type table +local L = {} + +---@type boolean?, boolean? +local show, showOnceMore +---@type integer?, integer?, integer?, string? +local panelList, windowList, backgroundGuishader, panelSig +---@type number, number, number, number, number +local listTop, listBottom, listX1, listRight, barX1 = 0, 0, 0, 0, 0 + +-- The column's entries: the views, then a rule, then the pages. +---@type table[] +local entries = {} +local selectedGroup = "all" +-- The columns the table shows right now, each with the x range it takes, and the group +-- captions spanning them. +---@type table[] +local columns = {} +---@type table[] +local spans = {} + +-- What the panel shows: every ally team with its teams and their totals, and the rows the +-- table makes of them under the current sort and grouping. +---@type table[] +local allies = {} +---@type table[] +local rows = {} +-- Bumped by rebuildRows, so the baked panel knows the list behind it changed. +local rowsGen = 0 +-- Bumped by the layout. A cached row layout carries the value it was built against and +-- is measured again when it moves, without every row being walked at resize. +local layoutGen = 0 +---@type table +local rowMetrics = { gen = -1, rows = -1, totalH = 0 } +-- The largest value each column shows, for the bars. +---@type table +local colMax = {} +local scroll = 0 +---@type boolean +local dragging = false +-- Where the thumb was taken hold of, as the distance from the cursor to its top edge. +---@type number +local dragGrab = 0 +local sortKey = "damageDealt" +---@type boolean +local sortAscending = false +-- What the cursor is over, in the terms the baked panel is painted with. Refilled in +-- place each frame rather than allocated. +---@type table +local hover = { sb = 0, row = 0, col = 0, hcol = 0, tog = 0, bar = 0 } -local GetGaiaTeamID = Spring.GetGaiaTeamID -local GetAllyTeamList = Spring.GetAllyTeamList -local GetTeamList = Spring.GetTeamList -local GetTeamStatsHistory = Spring.GetTeamStatsHistory -local GetTeamInfo = Spring.GetTeamInfo -local GetPlayerInfo = Spring.GetPlayerInfo -local GetLocalTeamID = Spring.GetLocalTeamID -local GetMouseState = spGetMouseState -local GetGameFrame = Spring.GetGameFrame -local max = mathMax -local floor = mathFloor -local huge = math.huge -local sort = table.sort -local log10 = math.log10 -local round = math.round -local borderRemap = - { left = { "x", "min", -1 }, right = { "x", "max", 1 }, top = { "y", "max", 1 }, bottom = { "y", "min", -1 } } - -local RectRound, UiElement, elementCorner - -local font, font2, backgroundGuishader, gameStarted, bgpadding, gameover +local teamAPM = {} +-- What the team stats gadget last handed over: the live values of every team the +-- viewer may see, keyed by team, and the frame it did so. A gadget cannot be called +-- from a widget, so the panel registers a receiver while it is open and the gadget +-- calls it every second; after `stale` frames without, the values count as gone rather +-- than old, and the table falls back to what the engine tells allies on its own. +-- `postGame` asks for one more read after game over, so the other side's live values +-- show once they may. `on` is whether the gadget is taken to be there: yes until the +-- panel has been open (`opened`) for `stale` frames without a hand-over, or the +-- hand-overs stop; while it is not, the gadget's columns, views and the history page +-- are left out of the panel rather than shown empty. +---@type table +local handover = { all = nil, frame = nil, opened = nil, stale = UPDATE_FRAMES * 3, postGame = false, on = true } +-- The last name seen for each team, for a player who has since left. +local teamControllers = {} +-- The frame each team died on, so a rate is over the time the team was in the game. +local deathFrame = {} +---@type boolean +local gameover = false +local isFFA = BAR.Utilities.Gametype.IsFFA() local anonymousMode = Spring.GetModOptions().teamcolors_anonymous_mode local anonymousTeamColor = { Spring.GetConfigInt("anonymousColorR", 255) / 255, Spring.GetConfigInt("anonymousColorG", 0) / 255, Spring.GetConfigInt("anonymousColorB", 0) / 255, } - local isSpec = spGetSpectatingState() -local localTeamID = GetLocalTeamID() - -local playerScale = math.clamp(25 / #Spring.GetTeamList(), 0.3, 1) - -function aboveRectangle(mousePos, boxData) - local included = true - for coordName, coordData in pairs(boxData.absSizes) do - included = included and mousePos[coordName] >= coordData.min and mousePos[coordName] <= coordData.max - end - return included -end - -function isAbove(mousePos, guiData) - for boxType, boxData in pairs(guiData) do - if boxData.visible then - local mask = {} - local border = false - if aboveRectangle(mousePos, boxData) then - local draggingBorderSize = boxData.draggingBorderSize - for borderName, borderData in pairs(borderRemap) do - local coordName = borderRemap[borderName][1] - local coordDir = borderRemap[borderName][2] - local dir = borderRemap[borderName][3] - if coordDir == "min" then - mask[borderName] = mousePos[coordName] - < (boxData.absSizes[coordName][coordDir] + draggingBorderSize * -1 * dir) - else - mask[borderName] = mousePos[coordName] - > (boxData.absSizes[coordName][coordDir] + draggingBorderSize * -1 * dir) - end - border = border or mask[borderName] - end - return boxType, border, mask - end - end - end +local localTeamID = spGetLocalTeamID() + +-- Text is queued while the panel is baked and printed in one Begin/End at the end. +local pending = {} +local pendingCount = 0 + +---@type function +local rebuildRows + +---------------------------------------------------------------- +-- Numbers +---------------------------------------------------------------- + +-- Not a number: what a cell shows as "-" and sorts last. A value the viewer may not see, +-- or a ratio with nothing under it. +local NAN = 0 / 0 + +local function known(v) + return v ~= nil and v == v end -local teamData = {} -local teamAPM = {} -local maxColumnTextSize = 0 -local columnSize = 0 -local prevNumLines = 0 -local selectedLine -local selectedColumn -local textDisplayList -local backgroundDisplayList -local teamControllers = {} -local mousex, mousey = 0, 0 -local sortVar = "damageDealt" -local sortAscending = false -local numColums = #header -local playerColumnWidthWeight = 2 +-- The percentages a row derives from two amounts, in one table: the file is near Lua's +-- limit on locals. +local ratio = {} -local function getColumnBounds(column) - local left = guiData.mainPanel.absSizes.x.min + (columnSize / 2) - if column > 1 then - left = left + ((playerColumnWidthWeight + column - 2) * columnSize) +-- `a` as a percentage of `b`; unknown when either is, or when there is no `b` to be a +-- share of. +ratio.share = function(a, b) + if not known(a) or not known(b) or b == 0 then + return NAN end - local width = (column == 1 and playerColumnWidthWeight or 1) * columnSize - return left, left + width + return a / b * 100 end -local function getColumnCenter(column) - local left, right = getColumnBounds(column) - return (left + right) / 2 +-- Like share, but nothing in nothing is empty rather than unknown. +ratio.level = function(a, b) + if not known(a) or not known(b) then + return NAN + end + if b == 0 then + return 0 + end + return a / b * 100 end -function widget:SetConfigData(data) - --guiData = data.guiData or guiData -- buggy positioning, so disabled this - sortVar = data.sortVar or sortVar - sortAscending = data.sortAscending or sortAscending +-- Traded above or below even; something destroyed for nothing lost is infinitely good. +ratio.efficiency = function(killed, lost) + if not known(killed) or not known(lost) then + return NAN + end + if lost == 0 then + return killed > 0 and mathHuge or NAN + end + return killed / lost * 100 end -function widget:GetConfigData(data) - return { - guiData = guiData, - sortVar = sortVar, - sortAscending = sortAscending, - } +-- Ratios and levels, from the counters they are made of. Run for a team and for an ally +-- team's total alike, so a band's efficiency is the efficiency of its sums. +local function derive(s) + s.metalStored = s.metalProduced + s.metalReceived - (s.metalUsed + s.metalSent + s.metalExcess) + s.energyStored = s.energyProduced + s.energyReceived - (s.energyUsed + s.energySent + s.energyExcess) + s.unitsStolen = s.unitsOutCaptured + s.unitsActive = s.unitsProduced + + s.unitsReceived + + s.unitsCaptured + - (s.unitsDied + s.unitsSent + s.unitsOutCaptured) + if s.damageReceived ~= 0 then + s.damageEfficiency = (s.damageDealt / s.damageReceived) * 100 + else + s.damageEfficiency = mathHuge + end + if s.unitsDied ~= 0 then + s.killEfficiency = (s.unitsKilled / s.unitsDied) * 100 + else + s.killEfficiency = mathHuge + end + -- Energy counts at a sixtieth of metal, the game's usual exchange. + local resources = s.metalProduced + s.metalReceived + (s.energyProduced + s.energyReceived) / 60 + if resources ~= 0 and s.damageDealt ~= 0 then + s.aggressionLevel = mathFloor(10 * mathLog10(s.damageDealt / resources) + 0.5) + else + s.aggressionLevel = -mathHuge + end + -- The live ratios, from amounts that can be unknown: a team the viewer may not see + -- has none, and its ally team's sums are unknown with it. + s.metalLevel = ratio.level(s.metalCurrent, s.metalStorage) + s.energyLevel = ratio.level(s.energyCurrent, s.energyStorage) + s.conversion = ratio.share(s.convUse, s.convCapacity) + s.buildPowerUse = ratio.share(s.buildPowerActive, s.buildPower) + s.valueEfficiency = ratio.efficiency(s.killedValue, s.lostValue) end -function calcAbsSizes() - guiData.mainPanel.absSizes = { - x = { - min = (guiData.mainPanel.relSizes.x.min * vsx), - max = (guiData.mainPanel.relSizes.x.max * vsx), - length = (guiData.mainPanel.relSizes.x.length * vsx), - }, - y = { - min = (guiData.mainPanel.relSizes.y.min * vsy), - max = (guiData.mainPanel.relSizes.y.max * vsy), - length = (guiData.mainPanel.relSizes.y.length * vsy), - }, - } +local function isFinite(v) + return v == v and v ~= mathHuge and v ~= -mathHuge end -function widget:ViewResize() - vsx, vsy = spGetViewGeometry() - widgetScale = (vsy / 1080) - - font = WG.fonts.getFont() - font2 = WG.fonts.getFont(2) - for _, data in pairs(headerRemap) do - maxColumnTextSize = max(font:GetTextWidth(data[2]), max(font:GetTextWidth(data[1]), maxColumnTextSize)) +-- A number as a cell shows it: whole below a thousand, with a prefix above it, and one +-- decimal when it is small enough for that to be all there is - a rate can be. +local function formatNumber(v) + if not isFinite(v) then + return "-" end - - bgpadding = WG.FlowUI.elementPadding - elementCorner = WG.FlowUI.elementCorner - - RectRound = WG.FlowUI.Draw.RectRound - UiElement = WG.FlowUI.Draw.Element - - calcAbsSizes() - updateFontSize() - widget:GameFrame(GetGameFrame(), true) -end - -local function refreshHeaders() - headerRemap = { - frame = { " ", BAR.I18N("ui.teamStats.player") }, - metalProduced = { BAR.I18N("ui.teamStats.metal"), BAR.I18N("ui.teamStats.resourceProduced") }, - metalExcess = { BAR.I18N("ui.teamStats.metal"), BAR.I18N("ui.teamStats.resourceExcess") }, - energyProduced = { BAR.I18N("ui.teamStats.energy"), BAR.I18N("ui.teamStats.resourceProduced") }, - energyExcess = { BAR.I18N("ui.teamStats.energy"), BAR.I18N("ui.teamStats.resourceExcess") }, - damageDealt = { BAR.I18N("ui.teamStats.damage"), BAR.I18N("ui.teamStats.damageDealt") }, - damageReceived = { BAR.I18N("ui.teamStats.damage"), BAR.I18N("ui.teamStats.damageReceived") }, - damageEfficiency = { BAR.I18N("ui.teamStats.damage"), BAR.I18N("ui.teamStats.damageEfficiency") }, - unitsProduced = { BAR.I18N("ui.teamStats.units"), BAR.I18N("ui.teamStats.unitsProduced") }, - unitsDied = { BAR.I18N("ui.teamStats.units"), BAR.I18N("ui.teamStats.unitsDied") }, - unitsKilled = { BAR.I18N("ui.teamStats.units"), BAR.I18N("ui.teamStats.unitsKilled") }, - aggressionLevel = { BAR.I18N("ui.teamStats.aggression"), BAR.I18N("ui.teamStats.aggressionLevel") }, - actionsPerMinute = { - BAR.I18N("ui.teamStats.actionsPerMinute1"), - BAR.I18N("ui.teamStats.actionsPerMinute2"), - }, - } + local a = mathAbs(v) + if a < 0.05 then + return "0" + end + if a < 10 then + local s = stringFormat("%.1f", v) + return (s:gsub("%.0$", "")) + end + if a < 1000 then + return stringFormat("%d", mathFloor(v + 0.5)) + end + return formatSI(v) or stringFormat("%.0f", v) end -local function closeHandler() - if guiData.mainPanel.visible then - guiData.mainPanel.visible = false - - return true +-- `share` prints an amount as the percentage the share switch turned it into. +local function formatCell(column, v, share) + if column.fmt == "percent" or share then + if not isFinite(v) then + return "-" + end + return stringFormat("%d%%", mathFloor(v + 0.5)) + elseif column.fmt == "plain" then + if not isFinite(v) then + return "-" + end + return stringFormat("%d", mathFloor(v + 0.5)) end + return formatNumber(v) end -function widget:Initialize() - widgetHandler:AddAction("teamstatus_close", closeHandler, nil, "p") - - refreshHeaders() - guiData.mainPanel.visible = false - widget:ViewResize() - local _, _, paused = Spring.GetGameSpeed() - if paused then - widget:GameFrame(GetGameFrame(), true) +-- The whole number with its thousands marked off, for the tooltip: a cell rounds to +-- three figures. +local function formatWhole(v) + local a = mathAbs(v) + if a < 100 and mathAbs(a - mathFloor(a + 0.5)) > 0.05 then + return stringFormat("%.1f", v) end + local s = stringFormat("%d", mathFloor(a + 0.5)) + s = s:reverse():gsub("(%d%d%d)", "%1,"):reverse() + s = s:gsub("^,", "") + return (v < 0 and "-" or "") .. s +end - -- lets the handler hide the rest of the interface while the panel is open - widgetHandler:RegisterModalWindow(function() - return guiData.mainPanel.visible == true - end) +local function formatExact(column, v) + if not isFinite(v) then + return "-" + end + if column.fmt == "percent" then + return stringFormat("%.1f%%", v) + end + return formatWhole(v) +end - WG.teamstats = {} - WG.teamstats.toggle = function(state) - if state ~= nil then - guiData.mainPanel.visible = state - else - guiData.mainPanel.visible = not guiData.mainPanel.visible +-- What a cell's number is made of, for its tooltip: the units behind a value, or the two +-- amounts behind a percentage. Nothing when they are not known. +local function cellDetail(column, stats) + if column.count then + local n = stats[column.count] + if known(n) then + local key = n == 1 and "ui.teamStats.unitOne" or "ui.teamStats.unitCount" + return BAR.I18N(key, { count = formatWhole(n) }) end - if guiData.mainPanel.visible then - widget:GameFrame(GetGameFrame(), true) + elseif column.of then + local a, b = stats[column.of[1]], stats[column.of[2]] + if known(a) and known(b) then + return BAR.I18N("ui.teamStats.ofTotal", { value = formatWhole(a), total = formatWhole(b) }) end end - WG.teamstats.isvisible = function() - return guiData.mainPanel.visible - end + return nil end -function widget:Shutdown() - glDeleteList(textDisplayList) - glDeleteList(backgroundDisplayList) - if WG.guishader then - WG.guishader.RemoveDlist("teamstats_window") +local function gameTime(frames) + return stringFormat("%d:%02d", mathFloor(frames / 1800), mathFloor(frames / 30) % 60) +end + +-- Whether the table has ally teams to show: the grouping switch, in a game with sides. +local function grouped() + return filters.groupByTeam and not isFFA +end + +-- Whether amounts are shown as each player's share of their ally team's. +local function shareMode() + return filters.shareOfTeam and grouped() +end + +-- A team's counter, or the rate it makes over the team's time in the game when the +-- switch asks for one. +local function baseValue(column, team) + local v = team.stats[column.key] + if v == nil then + return NAN end - if backgroundGuishader ~= nil then - glDeleteList(backgroundGuishader) + if filters.perMinute and column.rate then + return v / team.minutes end + return v end -function compareAllyTeams(a, b) - if sortAscending then - return a[#a][sortVar] < b[#b][sortVar] - else - return a[#a][sortVar] > b[#b][sortVar] +-- What an ally team's band shows: the total, or the sum of its teams' rates, since each +-- was in the game for its own time. +local function bandValue(column, ally) + if filters.perMinute and column.rate then + local sum = 0 + for i = 1, #ally.teams do + sum = sum + baseValue(column, ally.teams[i]) + end + return sum end + return ally.total[column.key] end -function compareTeams(a, b) - if sortAscending then - return a[sortVar] < b[sortVar] - else - return a[sortVar] > b[sortVar] +-- What a team's cell shows: the counter or rate, or under the share switch the share of +-- what its band shows, so the two agree whatever the other switches say. Ratios and +-- levels have no share. +local function cellValue(column, team) + local v = baseValue(column, team) + if filters.shareOfTeam and column.fmt == "si" and team.ally and grouped() then + local total = bandValue(column, team.ally) + if total ~= 0 then + return v / total * 100 + end + return 0 end + return v end -function widget:PlayerChanged() - local newIsSpec = spGetSpectatingState() - local newLocalTeamID = GetLocalTeamID() - local needsUpdate = false - if anonymousMode ~= "disabled" then - needsUpdate = newIsSpec ~= isSpec or (not newIsSpec and newLocalTeamID ~= localTeamID) +-- The colour a value earns, or nil for the plain one: a ratio above even is good and +-- below it bad, and excess past a share of what was produced is waste. +local function tone(column, stats, v) + if column.fmt == "percent" then + if not column.even or not isFinite(v) or mathAbs(v - 100) < 0.5 then + return nil + end + return v > 100 and colorGood or colorBad end - isSpec = newIsSpec - localTeamID = newLocalTeamID - if needsUpdate then - widget:GameFrame(GetGameFrame(), true) + local produced = WASTE_OF[column.key] + if produced and stats[produced] > 0 and stats[column.key] / stats[produced] > look.wasteShare then + return colorBad end + return nil end -function widget:ApmEvent(teamID, apm) - teamAPM[teamID] = apm -end +---------------------------------------------------------------- +-- The data +---------------------------------------------------------------- + +-- `live` is what the team stats gadget says about the team right now, when it is running +-- and the viewer may see the team. +local function readTeam(teamID, allyID, frame, live) + local count = spGetTeamStatsHistory(teamID) + local history = count and spGetTeamStatsHistory(teamID, count) + local s = history and history[#history] + if not s then + return nil + end + -- The engine's entry, with everything else the row shows added to it. + ---@cast s table + s.actionsPerMinute = teamAPM[teamID] or 0 + local milestones + if live then + for key, v in pairs(live) do + if key ~= "milestones" and key ~= "dead" then + s[key] = v + end + end + milestones = live.milestones + else + -- Without the gadget the engine still tells allies their economy, and the + -- conversion gadget its use of the converters. + local current, storage, _, income, expense = Spring.GetTeamResources(teamID, "metal") + if current then + s.metalCurrent, s.metalStorage, s.metalIncome, s.metalExpense = current, storage, income, expense + current, storage, _, income, expense = Spring.GetTeamResources(teamID, "energy") + s.energyCurrent, s.energyStorage, s.energyIncome, s.energyExpense = current, storage, income, expense + s.convCapacity = Spring.GetTeamRulesParam(teamID, "mmCapacity") + s.convUse = Spring.GetTeamRulesParam(teamID, "mmUse") + end + end + derive(s) -function widget:GameFrame(n, forceupdate, allowGameoverUpdate) - if n > 0 and not gameStarted then - gameStarted = true - forceupdate = true + local _, leader, isDead = spGetTeamInfo(teamID, false) + local name, isActive = spGetPlayerInfo(leader, false) + if WG.playernames and WG.playernames.getPlayername then + name = WG.playernames.getPlayername(leader) or name + end + local aiName = spGetGameRulesParam("ainame_" .. teamID) + if aiName then + name = tostring(aiName) + end + if name then + teamControllers[teamID] = name + else + name = teamControllers[teamID] or "" + end + local gone = not isActive + local label = name + if isDead == true then + label = BAR.I18N("ui.teamStats.dead", { player = name }) + elseif gone then + label = BAR.I18N("ui.teamStats.gone", { player = name }) end - if gameover and not allowGameoverUpdate then - return + local r, g, b + if not isSpec and anonymousMode ~= "disabled" and teamID ~= localTeamID then + r, g, b = anonymousTeamColor[1], anonymousTeamColor[2], anonymousTeamColor[3] + else + r, g, b = spGetTeamColor(teamID) end - if not forceupdate and (not guiData.mainPanel.visible or n % update ~= 0) then - return + -- A rate is over the time the team was in the game, and never over less than a + -- minute, or the first seconds would show rates of thousands. + local alive = deathFrame[teamID] and mathMin(deathFrame[teamID], frame) or frame + return { + id = teamID, + allyID = allyID, + stats = s, + name = name, + sortName = stringLower(name), + label = label, + accent = { r, g, b, isDead and 0.35 or 0.9 }, + dead = isDead, + gone = gone, + isLocal = not isSpec and teamID == localTeamID, + aliveFrames = alive, + minutes = mathMax(alive, 1800) / 1800, + milestones = milestones, + } +end + +-- Reads every team's newest stats and the names, colours and states beside them. +local function refreshStats() + local frame = spGetGameFrame() + local gaia = spGetGaiaTeamID() + localTeamID = spGetLocalTeamID() + -- The gadget's last hand-over, unless it has gone quiet. + local live = handover.all + if live and frame - handover.frame > handover.stale then + live = nil end - localTeamID = GetLocalTeamID() - teamData = {} - local totalNumLines = 2 - local allyInsertCount = 1 - for _, allyTeamID in ipairs(GetAllyTeamList()) do - local allyVec = {} - local allyTotal = {} - local teamInsertCount = 1 - for _, teamID in ipairs(GetTeamList(allyTeamID)) do - if teamID ~= GetGaiaTeamID() then - local range = GetTeamStatsHistory(teamID) - local history = GetTeamStatsHistory(teamID, range) - if history then - history = history[#history] - history.resourcesProduced = history.metalProduced + history.energyProduced / 60 - history.resourcesUsed = history.metalUsed + history.energyUsed / 60 - history.resourcesExcess = history.metalExcess + history.energyExcess / 60 - history.resourcesSent = history.metalSent + history.energySent / 60 - history.resourcesReceived = history.metalReceived + history.energyReceived / 60 - history.actionsPerMinute = teamAPM[teamID] or 0 - for varName, value in pairs(history) do - allyTotal[varName] = (allyTotal[varName] or 0) + value - end - history.time = nil - local teamColor - if not isSpec and anonymousMode ~= "disabled" and teamID ~= localTeamID then - teamColor = { anonymousTeamColor[1], anonymousTeamColor[2], anonymousTeamColor[3] } - else - teamColor = { Spring.GetTeamColor(teamID) } - end - local _, leader, isDead = GetTeamInfo(teamID, false) - local playerName, isActive = GetPlayerInfo(leader, false) - playerName = (WG.playernames and WG.playernames.getPlayername) - and WG.playernames.getPlayername(leader) - or playerName - if Spring.GetGameRulesParam("ainame_" .. teamID) then - playerName = Spring.GetGameRulesParam("ainame_" .. teamID) - end - if gameStarted ~= nil then - if not playerName then - playerName = teamControllers[teamID] or BAR.I18N("ui.teamStats.gone", { player = "" }) + allies = {} + for _, allyID in ipairs(spGetAllyTeamList()) do + local ally = { id = allyID, teams = {}, total = {} } + local teamList = spGetTeamList(allyID) + ---@cast teamList -? + for _, teamID in ipairs(teamList) do + if teamID ~= gaia then + local team = readTeam(teamID, allyID, frame, live and live[teamID]) + if team then + team.ally = ally + ally.teams[#ally.teams + 1] = team + for i = 1, #SUMMED do + local key = SUMMED[i] + local v = team.stats[key] + local total = ally.total[key] + -- A member's unknown makes the total unknown, and keeps it so. + if v == nil then + ally.total[key] = NAN + elseif total == nil then + ally.total[key] = v else - teamControllers[teamID] = playerName - end - if isDead then - playerName = BAR.I18N("ui.teamStats.dead", { player = playerName }) - elseif not isActive then - playerName = BAR.I18N("ui.teamStats.gone", { player = playerName }) + ally.total[key] = total + v end end - if history.damageReceived ~= 0 then - history.damageEfficiency = (history.damageDealt / history.damageReceived) * 100 - else - history.damageEfficiency = huge - end - local totalRes = history.resourcesProduced + history.resourcesReceived - if totalRes ~= 0 and history.damageDealt ~= 0 then - history.aggressionLevel = round(10 * log10(history.damageDealt / totalRes)) - else - history.aggressionLevel = -huge - end - if history.unitsDied ~= 0 then - history.killEfficiency = (history.unitsKilled / history.unitsDied) * 100 - else - history.killEfficiency = huge - end - - playerName = playerName or "" - - history.frame = BAR.Utilities.ConvertColor(teamColor[1], teamColor[2], teamColor[3]) - .. playerName - .. " " - - allyVec[teamInsertCount] = history - totalNumLines = totalNumLines + 1 - teamInsertCount = teamInsertCount + 1 end end end - if teamInsertCount ~= 1 then - sort(allyVec, compareTeams) - if teamInsertCount > 2 then - allyTotal.frame = " " - allyTotal.time = nil - if allyTotal.damageReceived ~= 0 then - allyTotal.damageEfficiency = (allyTotal.damageDealt / allyTotal.damageReceived) * 100 - else - allyTotal.damageEfficiency = huge - end - local totalRes = allyTotal.resourcesProduced + allyTotal.resourcesReceived - if totalRes ~= 0 and allyTotal.damageDealt ~= 0 then - allyTotal.aggressionLevel = round(10 * log10(allyTotal.damageDealt / totalRes)) - else - allyTotal.aggressionLevel = -huge - end - if allyTotal.unitsDied ~= 0 then - allyTotal.killEfficiency = (allyTotal.unitsKilled / allyTotal.unitsDied) * 100 - else - allyTotal.killEfficiency = huge - end - totalNumLines = totalNumLines + 1 - allyVec[teamInsertCount] = allyTotal - end - teamData[allyInsertCount] = allyVec - if not isFFA then - totalNumLines = totalNumLines + 1 - end - allyInsertCount = allyInsertCount + 1 + if #ally.teams > 0 then + derive(ally.total) + allies[#allies + 1] = ally end end - totalNumLines = totalNumLines + 1 - sort(teamData, compareAllyTeams) - guiData.mainPanel.absSizes.y.min = guiData.mainPanel.absSizes.y.max - totalNumLines * lineHeight - prevNumLines = totalNumLines - glDeleteList(textDisplayList) - textDisplayList = glCreateList(ReGenerateTextDisplayList) - glDeleteList(backgroundDisplayList) - backgroundDisplayList = glCreateList(ReGenerateBackgroundDisplayList) + rebuildRows() end -function widget:GameOver() - gameover = true - widget:GameFrame(GetGameFrame(), true, true) - if replaceEndStats then - guiData.mainPanel.visible = true - Spring.SendCommands("endgraph 0") - end -end +---------------------------------------------------------------- +-- The rows +---------------------------------------------------------------- -function widget:MousePress(mx, my, button) - if not guiData.mainPanel.visible then - return +local function compareTeams(a, b) + if sortKey == "name" then + if a.sortName ~= b.sortName then + if sortAscending then + return a.sortName < b.sortName + end + return a.sortName > b.sortName + end + return a.id < b.id + end + if a.sortVal ~= b.sortVal then + if sortAscending then + return a.sortVal < b.sortVal + end + return a.sortVal > b.sortVal end - return mouseEvent(mx, my, button) + return a.id < b.id end -function widget:MouseRelease(mx, my, button) - if not guiData.mainPanel.visible then - return +local function compareAllies(a, b) + if sortKey ~= "name" and a.sortVal ~= b.sortVal then + if sortAscending then + return a.sortVal < b.sortVal + end + return a.sortVal > b.sortVal end - return mouseEvent(mx, my, button, true) + return a.id < b.id end -function mouseEvent(mx, my, button, release) - -- A press on a top bar button is the top bar's to handle: it closes the open windows - -- and opens the one that was clicked. Closing (and consuming) here would swallow it. - if WG.topbar and WG.topbar.buttonAt and WG.topbar.buttonAt(mx, my) then - return false +-- Sorts the teams inside their ally teams and the ally teams by their totals, then lays +-- them out as rows: a band per ally team with its teams under it, or one flat list. +rebuildRows = function() + rows = {} + rowsGen = rowsGen + 1 + local column = COLUMNS[sortKey] or COLUMNS.damageDealt + + -- An unknown sorts below everything; as itself it would compare as nothing and break + -- the order. + local function sortable(v) + if v ~= v then + return -mathHuge + end + return v end - - local boxType = isAbove({ x = mx, y = my }, guiData) - if not boxType and guiData.mainPanel.visible then - if release then - guiData.mainPanel.visible = false - return false + for i = 1, #allies do + local ally = allies[i] + for j = 1, #ally.teams do + local team = ally.teams[j] + team.sortVal = column.fmt ~= "name" and sortable(cellValue(column, team)) or 0 end - return true + tableSort(ally.teams, compareTeams) + ally.sortVal = column.fmt ~= "name" and sortable(bandValue(column, ally)) or 0 end - if boxType == "mainPanel" then - if release then - local line, column = getLineAndColumn(mx, my) - if line <= 3 then -- header - local newSort = header[column] - if newSort then - if playSounds then - Spring.PlaySoundFile(buttonclick, 0.6, "ui") - end - if sortVar == newSort then - sortAscending = not sortAscending - end - sortVar = newSort - widget:GameFrame(GetGameFrame(), true) - end + tableSort(allies, compareAllies) + + if grouped() then + for i = 1, #allies do + local ally = allies[i] + rows[#rows + 1] = { type = "band", ally = ally } + for j = 1, #ally.teams do + -- Striped by place under the band, so the pattern starts afresh with each + -- ally team rather than running on across its band. + rows[#rows + 1] = { type = "team", team = ally.teams[j], stripe = j % 2 == 0 } end end - return true + else + local teams = {} + for i = 1, #allies do + local ally = allies[i] + for j = 1, #ally.teams do + teams[#teams + 1] = ally.teams[j] + end + end + tableSort(teams, compareTeams) + for i = 1, #teams do + rows[#rows + 1] = { type = "team", team = teams[i], stripe = i % 2 == 0 } + end end -end -function getLineAndColumn(x, y) - local relativex = x - guiData.mainPanel.absSizes.x.min - columnSize / 2 - local relativey = guiData.mainPanel.absSizes.y.max - y - local line = floor(relativey / lineHeight) + 1 - local column - if relativex >= 0 then - if relativex < (playerColumnWidthWeight * columnSize) then - column = 1 - else - column = floor((relativex - (playerColumnWidthWeight * columnSize)) / columnSize) + 2 + -- The bars are scaled to the largest a column shows among the teams, whatever view + -- is open, so switching views does not walk the rows again. + colMax = {} + for key, col in pairs(COLUMNS) do + if col.fmt ~= "name" and col.fmt ~= "plain" then + local top = 0 + for i = 1, #allies do + local ally = allies[i] + for j = 1, #ally.teams do + local v = cellValue(col, ally.teams[j]) + if isFinite(v) and v > top then + top = v + end + end + end + colMax[key] = top end end - return line, column end -function updateFontSize() - columnSize = guiData.mainPanel.absSizes.x.length / (numColums - 1 + playerColumnWidthWeight) - fontSize = 11 * widgetScale + floor(columnSize / maxColumnTextSize) - fontSize = fontSize * playerScale - lineHeight = fontSize - fontSize = fontSize + mathMin(fontSize * 0.5, (fontSize * ((1 - playerScale) * 0.7))) +local function rowHeightOf(row) + if row.type == "band" then + return metrics.bandRowHeight + end + return metrics.rowHeight end -function widget:MouseMove(mx, my, dx, dy) - if not guiData.mainPanel.visible then +-- A row's position is a sum of what is above it rather than its index times one height: +-- a band is not the height of an ordinary row. The running total is stamped onto the +-- rows, and redone when the list or the layout changes. +local function ensureRowMetrics() + if rowMetrics.gen == layoutGen and rowMetrics.rows == rowsGen then return end - local boxType = isAbove({ x = mx, y = my }, guiData) - local newLine, newColumn - if boxType == "mainPanel" then - newLine, newColumn = getLineAndColumn(mx, my) - end - if selectedLine ~= newLine or selectedColumn ~= newColumn then - selectedLine, selectedColumn = newLine, newColumn - glDeleteList(backgroundDisplayList) - backgroundDisplayList = glCreateList(ReGenerateBackgroundDisplayList) - end -end -function widget:Update(dt) - local x, y = GetMouseState() - if x ~= mousex or y ~= mousey then - widget:MouseMove(x, y, x - mousex, y - mousey) + local off = 0 + for i = 1, #rows do + rows[i].off = off + off = off + rowHeightOf(rows[i]) end - mousex, mousey = x, y + rowMetrics.totalH = off + rowMetrics.gen, rowMetrics.rows = layoutGen, rowsGen end -local function DrawBackground() - if not guiData.mainPanel.visible then - return - end +-- Pixels of content above the first painted row. +local function scrollOffset() + ensureRowMetrics() + local first = rows[scroll + 1] - gl.Color(0, 0, 0, WG.guishader and 0.8 or 0.85) - local x1, y1, x2, y2 = - mathFloor(guiData.mainPanel.absSizes.x.min), - mathFloor(guiData.mainPanel.absSizes.y.min), - mathFloor(guiData.mainPanel.absSizes.x.max), - mathFloor(guiData.mainPanel.absSizes.y.max) - UiElement( - x1 - bgpadding, - y1 - bgpadding, - x2 + bgpadding, - y2 + bgpadding, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - WG.FlowUI.clampedOpacity - ) - if WG.guishader then - if backgroundGuishader ~= nil then - glDeleteList(backgroundGuishader) + return first and first.off or 0 +end + +-- Furthest offset that still fills the band. Walked from the end, so it does not depend +-- on where the list is scrolled to now. +local function maxScroll() + ensureRowMetrics() + local band = listTop - listBottom + local used = 0 + local i = #rows + while i > 0 do + local h = rowHeightOf(rows[i]) + if used + h > band then + break end - backgroundGuishader = glCreateList(function() - RectRound(x1 - bgpadding, y1 - bgpadding, x2 + bgpadding, y2 + bgpadding, elementCorner) - end) - WG.guishader.InsertDlist(backgroundGuishader, "teamstats_window", nil, widget) + used = used + h + i = i - 1 end - if backgroundDisplayList then - glCallList(backgroundDisplayList) - end + return i end -local function DrawAllStats() - if not guiData.mainPanel.visible then - return +local function clampScroll() + local top = maxScroll() + if scroll > top then + scroll = top end - if textDisplayList then - glCallList(textDisplayList) + if scroll < 0 then + scroll = 0 end end -function widget:DrawScreen() - if not guiData.mainPanel.visible then - if WG.guishader then - WG.guishader.RemoveDlist("teamstats_window") - end - return - end - - DrawBackground() - DrawAllStats() +local function setScroll(n) + scroll = n + clampScroll() +end - local mx, my = spGetMouseState() - local x1, y1, x2, y2 = - mathFloor(guiData.mainPanel.absSizes.x.min), - mathFloor(guiData.mainPanel.absSizes.y.min), - mathFloor(guiData.mainPanel.absSizes.x.max), - mathFloor(guiData.mainPanel.absSizes.y.max) - if math_isInRect(mx, my, x1, y1, x2, y2) then - Spring.SetMouseCursor("cursornormal") +-- The painted row under y, as its offset from the first painted one. Every hover test and +-- the panel signature go through this, so neither can disagree with what was drawn. nil +-- when y is outside the band or past the last whole row the band can hold. +local function rowAt(y) + ensureRowMetrics() + if y > listTop or y <= listBottom then + return nil end -end -function ReGenerateBackgroundDisplayList() - gl.Texture(false) -- some other widget left it on - local boxSizes = guiData.mainPanel.absSizes - for lineCount = 1, prevNumLines do - local colour = evenLineColour - if lineCount == 1 or lineCount == 2 then - colour = sortLineColour + local base = scrollOffset() + for i = scroll + 1, #rows do + local top = listTop - (rows[i].off - base) + local bottom = top - rowHeightOf(rows[i]) + if bottom < listBottom then + break end - if lineCount > 2 and (lineCount + 1) % 2 == 0 then - colour = oddLineColour + -- Half-open on the shared edge: rows stack, so one row's top is the next one's + -- bottom and a closed test would put the cursor in both. + if y <= top and y > bottom then + return i - scroll end - glColor(colour) - if evenLineColour and lineCount > 2 then - local bottomCorner = 0 - if mathFloor(boxSizes.x.min) >= guiData.mainPanel.absSizes.y.min then - bottomCorner = 1 + end + + return nil +end + +-- The column under x, or nil outside the table. Half-open on the shared edge. +local function columnAt(x) + for i = 1, #columns do + local c = columns[i] + if x >= c.x1 and x < c.x2 then + return i + end + end + + return nil +end + +---------------------------------------------------------------- +-- The scrollbar +---------------------------------------------------------------- + +local function scrollerThumb() + return UiScrollerAt(barX1, listBottom, area.x2 - metrics.edgeInset, listTop, rowMetrics.totalH, scrollOffset()) +end + +-- Scrolls so the thumb's top sits where the cursor has dragged it. The offset taken at the +-- grab is what keeps this relative: the thumb moves with the cursor rather than centring +-- itself on it, so taking hold of it does not shift the view before the drag begins. +local function scrollFromY(y) + local _, _, trackTop, travel = scrollerThumb() + if not travel or travel <= 0 then + return + end + + local f = (trackTop - (y - dragGrab)) / travel + if f < 0 then + f = 0 + elseif f > 1 then + f = 1 + end + setScroll(mathFloor(f * maxScroll() + 0.5)) +end + +-- Takes hold of the bar. On the thumb that is a grab, and the view stays where it is; on +-- the track either side of it the thumb jumps to the cursor first and is then dragged from +-- its middle, which is what a press on empty track is asking for. +local function grabScroller(y) + local top, height = scrollerThumb() + if not top then + return + end + + dragging = true + if y <= top and y >= top - height then + dragGrab = y - top + else + dragGrab = -mathFloor(height * 0.5) + scrollFromY(y) + end +end + +---------------------------------------------------------------- +-- Layout +---------------------------------------------------------------- + +-- The column starts below where the table does, so the title above it is not crowded by +-- the first entry. Everything in the column measures from here. +local function sidebarTop() + return metrics.bandTop - metrics.sidebarDrop +end + +local function entryRect(i) + local top = sidebarTop() + for j = 1, i - 1 do + local e = entries[j] + ---@cast e -? + top = top - (e.divider and metrics.dividerH or metrics.catRowHeight) + end + local h = entries[i].divider and metrics.dividerH or metrics.catRowHeight + + return area.x1, top - h, area.x1 + metrics.sidebarW, top +end + +-- The entry under x,y, or nil. The rule between the views and the pages is not one. +local function sidebarIndexAt(x, y) + if x < area.x1 or x > area.x1 + metrics.sidebarW then + return nil + end + for i = 1, #entries do + local _, y1, _, y2 = entryRect(i) + if y <= y2 and y > y1 then + if entries[i].divider then + return nil end - RectRound( - mathFloor(boxSizes.x.min), - mathFloor(boxSizes.y.max - lineCount * lineHeight), - mathFloor(boxSizes.x.max), - mathFloor(boxSizes.y.max - (lineCount - 1) * lineHeight), - bgpadding, - 0, - 0, - bottomCorner, - bottomCorner, - { colour[1], colour[2], colour[3], colour[4] * ui_opacity }, - { colour[1], colour[2], colour[3], colour[4] * 3 * ui_opacity } - ) - elseif lineCount == 1 then - --RectRound(boxSizes.x.min, boxSizes.y.max -(lineCount+1)*lineHeight, boxSizes.x.max, boxSizes.y.max -(lineCount-1)*lineHeight, 3*widgetScale) + return i end end - if selectedLine and selectedLine < 3 and selectedColumn and selectedColumn > 0 and selectedColumn <= numColums then - if sortAscending then - glColor( - sortHighLightColour[1], - sortHighLightColour[2], - sortHighLightColour[3], - sortHighLightColour[4] * ui_opacity - ) + + return nil +end + +local function switchAt(x, y) + for i = 1, #switches do + local hit = switches[i].hit + if hit and math_isInRect(x, y, hit[1], hit[2], hit[3], hit[4]) then + return i + end + end + + return nil +end + +-- The stat caption under x,y, which sorts the table when clicked. +local function headerColumnAt(x, y) + if y > metrics.statTop or y <= metrics.statBottom then + return nil + end + + return columnAt(x) +end + +-- The columns the open view shows, each given its x range: the name column takes its +-- share and the numeric ones split the rest evenly, with the leftover pixels going to the +-- name so the last column ends exactly at the table's right edge. +local function layoutColumns() + local group = groupByKey[selectedGroup] or GROUPS[1] + columns = { COLUMNS.name } + for i = 1, #group.columns do + local c = COLUMNS[group.columns[i]] + if handover.on or not c.gadget then + columns[#columns + 1] = c + end + end + + local tableW = listRight - listX1 + local n = #columns - 1 + ---@type number + local nameW = mathMax(metrics.nameMinW, mathFloor(tableW * metrics.nameShare)) + local colW = mathFloor((tableW - nameW) / n) + nameW = tableW - colW * n + + local x = listX1 + for i = 1, #columns do + local c = columns[i] + c.x1 = x + c.x2 = x + (i == 1 and nameW or colW) + x = c.x2 + -- The caption takes the whole cell: the sort marker sits in the cell's padding, so + -- it never takes room from the caption when the column is the sorted one. + local room = c.x2 - c.x1 - metrics.cellPad * 2 + if i == 1 then + room = c.x2 - c.x1 - metrics.nameIndent - metrics.cellPad - metrics.triW - metrics.rowPad + end + c.fitStat = text.fit(font, L.stat[c.key], room, metrics.statFs) + c.statW = mathFloor(font:GetTextWidth(c.fitStat) * metrics.statFs) + end + + -- Neighbouring columns with the same group share one caption over them. + spans = {} + ---@type table? + local span + for i = 2, #columns do + local c = columns[i] + if span and span.group == c.group then + span.x2 = c.x2 + span.rate = span.rate or c.rate else - glColor( - sortHighLightColourDesc[1], - sortHighLightColourDesc[2], - sortHighLightColourDesc[3], - sortHighLightColourDesc[4] * ui_opacity - ) + span = { group = c.group, x1 = c.x1, x2 = c.x2, rate = c.rate } + spans[#spans + 1] = span + end + end + for i = 1, #spans do + local s = spans[i] + local label = L.caption[s.group] or s.group + if filters.perMinute and s.rate then + label = label .. L.perMinuteSuffix + end + s.label = text.fit(font, label, s.x2 - s.x1 - metrics.cellPad * 2, metrics.groupFs) + end + + layoutGen = layoutGen + 1 + clampScroll() +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.rowHeight = mathFloor(24 * s) + metrics.rowFs = mathFloor(metrics.rowHeight * 0.55) + metrics.bandRowHeight = mathFloor(metrics.rowHeight * 1.35) + metrics.bandFs = mathFloor(metrics.rowFs * 0.95 * 1.13) + metrics.groupRowHeight = mathFloor(22 * s) + metrics.groupFs = metrics.rowFs + metrics.statRowHeight = metrics.rowHeight + metrics.statFs = mathFloor(metrics.rowFs * 0.92) + metrics.catRowHeight = mathFloor(29 * s) + metrics.catFs = mathFloor(metrics.catRowHeight * 0.55 * 0.85) + metrics.dividerH = mathFloor(14 * s) + metrics.rowPad = mathFloor(6 * s) + metrics.cellPad = mathFloor(7 * s) + metrics.sidePad = mathFloor(12 * s) + metrics.catInset = mathFloor(4 * s) + metrics.accentW = mathMax(2, mathFloor(3 * s)) + metrics.accentPad = mathMax(1, mathFloor(3 * s)) + metrics.barInset = mathMax(2, mathFloor(4 * s)) + metrics.underlineH = mathMax(1, mathFloor(2 * s)) + metrics.edgeInset = mathFloor(4 * s) + metrics.headerH = mathFloor(34 * s) + metrics.headerGap = mathFloor(4 * s) + metrics.tableGap = mathFloor(4 * s) + metrics.listGap = mathFloor(12 * s) + metrics.cardLip = mathFloor(5 * s) + metrics.titleY = mathFloor(17 * s) + metrics.titleFs = mathFloor(metrics.rowHeight * 0.85) + metrics.sidebarDrop = mathFloor(8 * s) + metrics.sidebarW = mathFloor(190 * s) + metrics.barW = mathFloor(14 * s) + metrics.nameMinW = mathFloor(150 * s) + -- Narrower than the cell padding it sits in. + metrics.triW = mathMax(4, mathFloor(5 * s)) + metrics.triH = mathMax(3, mathFloor(4 * s)) + metrics.switchGap = mathFloor(16 * s) + metrics.csPanel = mathFloor(elementCorner) + metrics.csSmall = mathFloor(elementCorner * 0.66) + metrics.nameIndent = metrics.accentW + metrics.rowPad * 2 + + listX1 = area.x1 + metrics.sidebarW + metrics.listGap + metrics.bandTop = area.y2 - metrics.headerH - metrics.headerGap + -- The table header: group captions, then the stat captions, then a gap to the rows. + metrics.groupTop = metrics.bandTop + metrics.groupBottom = metrics.groupTop - metrics.groupRowHeight + metrics.statTop = metrics.groupBottom + metrics.statBottom = metrics.statTop - metrics.statRowHeight + listTop = metrics.statBottom - metrics.tableGap + listBottom = area.y1 + metrics.edgeInset + -- The scrollbar owns a column of its own: its right edge lines up with the switches + -- above it, and the rows stop a clear gap short of it rather than running up against + -- it, so the bar sits in a channel rather than hugging them. + barX1 = area.x2 - metrics.edgeInset - metrics.barW + listRight = barX1 - metrics.listGap + + -- The header band: the switches, right to left from the panel's edge. A switch this + -- small is a poor click target on its own, so its caption is part of it and the hover + -- covers both. + local rowTop = area.y2 - mathFloor(4 * s) + local rowBottom = area.y2 - metrics.headerH + mathFloor(4 * s) + local togW = mathFloor(38 * s) + local togH = mathFloor((rowTop - rowBottom) * 0.62) + local togY = mathFloor((rowTop + rowBottom) * 0.5) + metrics.toggleFs = mathFloor(metrics.rowFs * 1.05) + -- Outlined text spreads past the box it is measured in, so the caption side gets back + -- the room its outline took. + metrics.captionBleed = mathFloor(metrics.toggleFs * 0.2 + 0.5) + ---@type number + local x2 = area.x2 - metrics.edgeInset + for i = #switches, 1, -1 do + local sw = switches[i] + if (sw.key == "groupByTeam" and isFFA) or (sw.key == "shareOfTeam" and not grouped()) then + sw.draw, sw.hit = nil, nil + else + local labelW = mathFloor(font:GetTextWidth(sw.label) * metrics.toggleFs) + sw.draw = { x2 - togW, togY - mathFloor(togH * 0.5), x2, togY - mathFloor(togH * 0.5) + togH } + sw.hit = { + sw.draw[1] - metrics.rowPad * 2 - labelW - metrics.captionBleed, + rowBottom, + x2 + metrics.rowPad, + rowTop, + } + x2 = sw.hit[1] - metrics.switchGap + end + end + + layoutColumns() +end + +---------------------------------------------------------------- +-- Drawing +---------------------------------------------------------------- + +local function queueText(str, x, y, size, opts) + local at = pendingCount * 5 + pending[at + 1] = str + pending[at + 2] = x + pending[at + 3] = y + pending[at + 4] = size + pending[at + 5] = opts + pendingCount = pendingCount + 1 +end + +local function flushText() + if pendingCount == 0 then + return + end + + font:Begin() + font:SetOutlineColor(look.outline) + for i = 0, pendingCount - 1 do + local at = i * 5 + font:Print(pending[at + 1], pending[at + 2], pending[at + 3], pending[at + 4], pending[at + 5]) + end + font:End() + + pendingCount = 0 +end + +-- The sort marker: a small triangle pointing the way the column is sorted. +---@type number, number, number, number, boolean +local triX, triY, triW, triH, triUp = 0, 0, 0, 0, false +local function triangleVertices() + if triUp then + glVertex(triX, triY) + glVertex(triX + triW, triY) + glVertex(triX + triW * 0.5, triY + triH) + else + glVertex(triX, triY + triH) + glVertex(triX + triW, triY + triH) + glVertex(triX + triW * 0.5, triY) + end +end + +local function drawSortMark(x, cy) + triX, triY, triW, triH, triUp = x, cy - mathFloor(metrics.triH * 0.5), metrics.triW, metrics.triH, sortAscending + glColor(look.sortMark) + glBeginEnd(GL_TRIANGLES, triangleVertices) + glColor(1, 1, 1, 1) +end + +-- What a row shows, fitted to the columns. Cached on the row and redone when the +-- columns move: the rows themselves are rebuilt whenever the values behind them change. +local function fitRow(row) + if row.fitGen == layoutGen then + return + end + row.fitGen = layoutGen + row.cells = {} + row.vals = {} + row.tones = {} + + local nameColumn = columns[1] + ---@cast nameColumn -? + if row.type == "team" then + local team = row.team + local share = shareMode() + local room = nameColumn.x2 - nameColumn.x1 - metrics.nameIndent - metrics.cellPad + row.fitName = text.fit(font, team.label, room, metrics.rowFs) + for i = 2, #columns do + local c = columns[i] + local v = cellValue(c, team) + row.vals[i] = v + row.cells[i] = formatCell(c, v, share and c.fmt == "si") + row.tones[i] = tone(c, team.stats, v) + end + else + local ally = row.ally + local caption = BAR.I18N("ui.teamStats.team", { number = ally.id + 1 }) + local members = #ally.teams == 1 and L.memberOne or BAR.I18N("ui.teamStats.members", { count = #ally.teams }) + local room = nameColumn.x2 - nameColumn.x1 - metrics.rowPad * 2 + row.caption = text.fit(font, caption, room, metrics.bandFs) + row.captionW = mathFloor(font:GetTextWidth(row.caption) * metrics.bandFs) + -- The count goes after the caption when it fits beside it, and is dropped when + -- not: cut short it would say nothing. + local left = room - row.captionW - metrics.rowPad * 2 + if font:GetTextWidth(members) * metrics.rowFs <= left then + row.members = members + else + row.members = nil + end + for i = 2, #columns do + local c = columns[i] + local v = bandValue(c, ally) + row.vals[i] = v + row.cells[i] = formatCell(c, v) + row.tones[i] = tone(c, ally.total, v) end - local x1, x2 = getColumnBounds(selectedColumn) - RectRound( - mathFloor(x1), - mathFloor(boxSizes.y.max - 2 * lineHeight), - mathFloor(x2), - mathFloor(boxSizes.y.max), - bgpadding, - 0, - 0, - 1, - 1 + end +end + +local function drawBand(row, top, bottom) + RectRound( + listX1, + bottom, + listRight, + top - metrics.csSmall, + metrics.csSmall, + 1, + 1, + 0, + 0, + look.sheenTop, + look.sheenTop + ) + RectRound( + listX1, + bottom, + listRight, + bottom + metrics.underlineH, + 0, + 0, + 0, + 0, + 0, + look.headerLine, + look.headerLineFade + ) + local by = text.baseline(font, bottom, top, metrics.bandFs) + queueText(colorHeader .. row.caption, listX1 + metrics.rowPad, by, metrics.bandFs, "o") + if row.members then + queueText( + colorDim .. row.members, + listX1 + metrics.rowPad * 3 + row.captionW, + text.baseline(font, bottom, top, metrics.rowFs), + metrics.rowFs, + "o" ) end - for selectedIndex, headerName in ipairs(header) do - if sortVar == headerName then - if sortAscending then - glColor(activeSortColour[1], activeSortColour[2], activeSortColour[3], activeSortColour[4] * ui_opacity) - else - glColor( - activeSortColourDesc[1], - activeSortColourDesc[2], - activeSortColourDesc[3], - activeSortColourDesc[4] * ui_opacity - ) + local vy = text.baseline(font, bottom, top, metrics.rowFs) + for i = 2, #columns do + queueText( + (row.tones[i] or colorTotal) .. row.cells[i], + columns[i].x2 - metrics.cellPad, + vy, + metrics.rowFs, + "or" + ) + end +end + +local function drawTeamRow(row, top, bottom, hovered) + local team = row.team + if row.stripe then + RectRound(listX1, bottom, listRight, top, metrics.csSmall, 1, 1, 1, 1, look.stripeFill) + end + if team.isLocal then + RectRound(listX1, bottom, listRight, top, metrics.csSmall, 1, 1, 1, 1, look.selectedFill) + end + if hovered then + Highlight(listX1, bottom, listRight, top, metrics.csSmall, look.rowHoverOpacity, look.white) + end + -- The team's colour down the left edge, the way an adjusted option is marked in the + -- game info panel, so a row is found by colour before its name is read. + RectRound( + listX1, + bottom + metrics.accentPad, + listX1 + metrics.accentW, + top - metrics.accentPad, + 0, + 0, + 0, + 0, + 0, + team.accent + ) + + if filters.bars then + for i = 2, #columns do + local c = columns[i] + local top_ = colMax[c.key] + local v = row.vals[i] + if top_ and top_ > 0 and v and isFinite(v) and v > 0 then + local room = c.x2 - c.x1 - metrics.cellPad * 2 + local w = mathFloor(room * mathMin(1, v / top_)) + if w >= 2 then + RectRound( + c.x1 + metrics.cellPad, + bottom + metrics.barInset, + c.x1 + metrics.cellPad + w, + top - metrics.barInset, + mathMin(metrics.csSmall, mathFloor(w * 0.5)), + 1, + 1, + 1, + 1, + look.barFill + ) + end end - local x1, x2 = getColumnBounds(selectedIndex) + end + end + + local by = text.baseline(font, bottom, top, metrics.rowFs) + local quiet = team.dead or team.gone + queueText((quiet and colorDim or colorName) .. row.fitName, listX1 + metrics.nameIndent, by, metrics.rowFs, "o") + for i = 2, #columns do + local valueColor = quiet and colorDim or (row.tones[i] or colorValue) + queueText(valueColor .. row.cells[i], columns[i].x2 - metrics.cellPad, by, metrics.rowFs, "or") + end +end + +-- Whole rows only: the band can end mid-row, and a row painted below it would be clipped +-- by nothing. +local function drawRows() + local base = scrollOffset() + + -- The sorted column's tint, from the first row to the last one drawn, under them all. + ---@type table? + local sortedColumn + for i = 2, #columns do + if columns[i].key == sortKey then + sortedColumn = columns[i] + end + end + if sortedColumn then + local bottom = listTop + for i = scroll + 1, #rows do + local rowBottom = listTop - (rows[i].off - base) - rowHeightOf(rows[i]) + if rowBottom < listBottom then + break + end + bottom = rowBottom + end + if bottom < listTop then + RectRound(sortedColumn.x1, bottom, sortedColumn.x2, listTop, metrics.csSmall, 0, 0, 1, 1, look.sortedFill) + end + end + + for i = 1, #rows - scroll do + local row = rows[scroll + i] + if not row then + break + end + local top = listTop - (row.off - base) + local bottom = top - rowHeightOf(row) + if bottom < listBottom then + break + end + fitRow(row) + if row.type == "band" then + drawBand(row, top, bottom) + else + drawTeamRow(row, top, bottom, hover.row == i) + end + end +end + +-- The table header: a caption over each group of columns with a line under it, and the +-- stat captions under those, the sorted one marked. +local function drawTableHeader() + local gy = text.baseline(font, metrics.groupBottom, metrics.groupTop, metrics.groupFs) + for i = 1, #spans do + local s = spans[i] + if s.label ~= "" then RectRound( - mathFloor(x1), - mathFloor(boxSizes.y.max - 2 * lineHeight), - mathFloor(x2), - mathFloor(boxSizes.y.max), - bgpadding, + s.x1 + metrics.rowPad, + metrics.groupBottom, + s.x2 - metrics.rowPad, + metrics.groupBottom + metrics.underlineH, + 0, + 0, + 0, 0, 0, - 1, - 1 + look.headerLine, + look.headerLineFade ) + queueText(colorHeader .. s.label, mathFloor((s.x1 + s.x2) * 0.5), gy, metrics.groupFs, "oc") + end + end + + local sy = text.baseline(font, metrics.statBottom, metrics.statTop, metrics.statFs) + local cy = mathFloor((metrics.statBottom + metrics.statTop) * 0.5) + for i = 1, #columns do + local c = columns[i] + local hovered = hover.hcol == i + local sorted = c.key == sortKey + if hovered then + Highlight( + c.x1, + metrics.statBottom, + c.x2, + metrics.statTop, + metrics.csSmall, + look.rowHoverOpacity, + look.white + ) + end + local color = sorted and colorKey or (hovered and colorTitle or colorDim) + if i == 1 then + queueText(color .. c.fitStat, c.x1 + metrics.nameIndent, sy, metrics.statFs, "o") + if sorted then + drawSortMark(c.x1 + metrics.nameIndent + c.statW + metrics.rowPad, cy) + end + else + queueText(color .. c.fitStat, c.x2 - metrics.cellPad, sy, metrics.statFs, "or") + -- In the padding after the caption, over the right edge the numbers line up on. + if sorted then + drawSortMark(c.x2 - metrics.cellPad + 1, cy) + end + end + end + + -- A rule closing the header off from the rows. + RectRound(listX1, metrics.statBottom, listRight, metrics.statBottom + 1, 0, 0, 0, 0, 0, look.rule) +end + +-- The column: its own card under the title, then the views, a rule, and the pages. +local function drawSidebar() + RectRound( + area.x1, + area.y1, + area.x1 + metrics.sidebarW, + sidebarTop() + metrics.cardLip, + metrics.csPanel, + 1, + 1, + 1, + 1, + look.sidebarFill, + look.sidebarFillTop + ) + queueText(L.titleText, area.x1 + metrics.sidePad, area.y2 - metrics.titleY, metrics.titleFs, "ov") + + for i = 1, #entries do + local e = entries[i] + local x1, y1, x2, y2 = entryRect(i) + if y1 < listBottom then break end + if e.divider then + local y = mathFloor((y1 + y2) * 0.5) + RectRound(x1 + metrics.sidePad, y, x2 - metrics.sidePad, y + 1, 0, 0, 0, 0, 0, look.rule) + else + local selected = e.key == selectedGroup + if selected then + RectRound( + x1 + metrics.catInset, + y1, + x2 - metrics.catInset, + y2, + metrics.csSmall, + 1, + 1, + 1, + 1, + look.selectedFill + ) + elseif i == hover.sb and not e.disabled then + Highlight( + x1 + metrics.catInset, + y1, + x2 - metrics.catInset, + y2, + metrics.csSmall, + look.rowHoverOpacity, + look.white + ) + end + local color = e.disabled and colorFaded or (selected and colorSelected or colorDim) + queueText(color .. e.label, x1 + metrics.sidePad, mathFloor((y1 + y2) * 0.5), metrics.catFs, "ov") + end end end -function ReGenerateTextDisplayList() - local lineCount = 1 - local boxSizes = guiData.mainPanel.absSizes - local baseYSize = boxSizes.y.max - (0.002 * vsy) -- small align adjustment so text is in the middle of a row +-- The switches and their captions. The plate goes behind the switch and the switch +-- lights itself: painting over it would only dull it. +local function drawHeader() + for i = 1, #switches do + local sw = switches[i] + if sw.draw then + local hovered = hover.tog == i + if hovered then + Highlight(sw.hit[1], sw.hit[2], sw.hit[3], sw.hit[4], metrics.csSmall, look.rowHoverOpacity, look.white) + end + UiToggle(sw.draw[1], sw.draw[2], sw.draw[3], sw.draw[4], filters[sw.key], hovered) + queueText( + (filters[sw.key] and colorSelected or colorDim) .. sw.label, + sw.draw[1] - metrics.rowPad, + mathFloor((sw.hit[2] + sw.hit[4]) * 0.5), + metrics.toggleFs, + "rov" + ) + end + end +end - font:Begin() - font:SetTextColor(1, 1, 1, 1) - font:SetOutlineColor(0, 0, 0, 1) - --print the header - local heightCorrection = lineHeight * ((1 - fontSizePercentage) / 2) - - for column, headerName in ipairs(header) do - local columnX = getColumnCenter(column) - font:Print( - headerRemap[headerName][1], - columnX, - baseYSize + heightCorrection - lineCount * lineHeight, - (fontSize * fontSizePercentage), - "dco" - ) - font:Print( - headerRemap[headerName][2], - columnX, - baseYSize + heightCorrection - (lineCount + 1) * lineHeight, - (fontSize * fontSizePercentage), - "dco" +-- Everything inside the panel: the column, the switches, the table and the scroller. +-- Baked and replayed until the cursor, the list or the screen moves. +local function drawPanel() + drawSidebar() + drawHeader() + drawTableHeader() + drawRows() + + if rowMetrics.totalH > 0 then + UiScroller( + barX1, + listBottom, + area.x2 - metrics.edgeInset, + listTop, + rowMetrics.totalH, + scrollOffset(), + hover.bar == 1, + dragging ) end - lineCount = lineCount + 3 - - for _, allyTeamData in ipairs(teamData) do - for _, teamData in ipairs(allyTeamData) do - for i, varName in ipairs(header) do - local columnX = getColumnCenter(i) - local value = teamData[varName] - if value == huge or value == -huge then - value = "-" - elseif tonumber(value) then - local v = tonumber(value) - if varName:sub(1, 5) ~= "units" and varName ~= "aggressionLevel" then - if v and math.abs(v) < 1 then - v = 0 - end - end - value = string.formatSI(v) + + flushText() + + -- The toggle's glow leaves its own tint as the current colour, and this list is + -- replayed every frame, so the leak would reach whoever draws next. + glColor(1, 1, 1, 1) +end + +local function drawWindow() + UiElement( + screenX, + screenY - screenHeight, + screenX + screenWidth, + screenY, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + WG.FlowUI.clampedOpacity + ) +end + +local function dropLists() + if panelList then + glDeleteList(panelList) + panelList = nil + panelSig = nil + end + if windowList then + glDeleteList(windowList) + windowList = nil + end +end + +local function deleteGuishader() + if backgroundGuishader ~= nil then + if WG.guishader then + WG.guishader.DeleteDlist("teamstats") + else + glDeleteList(backgroundGuishader) + end + backgroundGuishader = nil + end +end + +-- Reads the hover state and answers a signature of everything the baked panel is painted +-- from. Same signature, same picture, so the display list is replayed as it is. The body +-- column under the cursor is read too, for the tooltip, but nothing is painted from it. +local function panelSignature(mx, my) + hover.sb = sidebarIndexAt(mx, my) or 0 + hover.row = 0 + hover.col = 0 + hover.hcol = 0 + hover.tog = switchAt(mx, my) or 0 + hover.bar = 0 + + if hover.tog == 0 and mx >= listX1 and mx < listRight then + hover.hcol = headerColumnAt(mx, my) or 0 + if hover.hcol == 0 then + hover.row = rowAt(my) or 0 + if hover.row > 0 then + hover.col = columnAt(mx) or 0 + end + end + elseif mx >= barX1 and mx <= area.x2 then + -- The thumb itself, not the track: it is the part that can be taken hold of, so it + -- is the part that lights up. + local top, height = scrollerThumb() + if top and my <= top and my >= top - height then + hover.bar = 1 + end + end + + return hover.sb + .. "|" + .. hover.row + .. "|" + .. hover.hcol + .. "|" + .. hover.tog + .. "|" + .. hover.bar + .. "|" + .. scroll + .. "|" + .. rowsGen + .. "|" + .. layoutGen + .. "|" + .. (dragging and 1 or 0) +end + +---------------------------------------------------------------- +-- Callins +---------------------------------------------------------------- + +-- The column's full name, for a tooltip: its group and the stat, "Damage · Received". +local function columnTitle(column) + local caption = L.caption[column.group] + if caption and caption ~= "" then + return caption .. " \194\183 " .. L.full[column.key] + end + return L.full[column.key] +end + +-- The lines a player's name shows on hover: every column, view by view, at the values the +-- table would show them, and the time the team has been in the game. Built once per read +-- of the numbers: the rows are rebuilt whenever the values or the switches change. +local function nameCard(team) + if team.cardGen == rowsGen then + return team.card + end + + local share = shareMode() + local lines = {} + for i = 1, #CARD_LINES do + local spec = CARD_LINES[i] + local parts = {} + for j = 1, #spec[2] do + local column = COLUMNS[spec[2][j]] + if handover.on or not column.gadget then + local v = cellValue(column, team) + parts[#parts + 1] = colorDim + .. L.full[column.key] + .. " " + .. (tone(column, team.stats, v) or colorTitle) + .. formatCell(column, v, share and column.fmt == "si") + end + end + if #parts > 0 then + local caption = spec[1] ~= "" and (colorHeader .. L.caption[spec[1]] .. ": ") or " " + lines[#lines + 1] = caption .. table.concat(parts, colorDim .. " \194\183 ") + end + end + + -- The milestones, when the gadget has any: the game time, what it was and the unit + -- that made it so, a few to a line. + local marks = team.milestones + if marks and #marks > 0 then + local parts = {} + for i = 1, #marks do + local m = marks[i] + local label = L.milestone[m.key] or m.key + local ud = m.unitDefID and UnitDefs[m.unitDefID] or nil + ---@cast ud table? + if ud then + label = label .. " (" .. (ud.translatedHumanName or ud.name) .. ")" + end + parts[#parts + 1] = colorDim .. gameTime(m.frame) .. " " .. colorTitle .. label + if #parts == 3 or i == #marks then + local caption = i <= 3 and (colorHeader .. L.milestones .. ": ") or " " + lines[#lines + 1] = caption .. table.concat(parts, colorDim .. " \194\183 ") + parts = {} + end + end + end + + local time = gameTime(team.aliveFrames or 0) + local key = (team.dead and deathFrame[team.id]) and "ui.teamStats.diedAt" or "ui.teamStats.timeInGame" + lines[#lines + 1] = colorDim .. BAR.I18N(key, { time = time }) + + team.card = table.concat(lines, "\n") + team.cardGen = rowsGen + + return team.card +end + +-- The column's entries: every view with something to show, then a rule and the history +-- page. A view of the gadget's columns alone, and the page the gadget's history would +-- fill, are left out while the gadget is not there. A view that went away hands over to +-- the overview. +local function rebuildEntries() + entries = {} + local selectedStays = false + for _, group in ipairs(GROUPS) do + local shown = handover.on + if not shown then + for i = 1, #group.columns do + if not COLUMNS[group.columns[i]].gadget then + shown = true end - if varName == "damageEfficiency" or varName == "killEfficiency" then - value = value .. "%" + end + end + if shown then + entries[#entries + 1] = { key = group.key, label = L.group[group.key] } + selectedStays = selectedStays or group.key == selectedGroup + end + end + if not selectedStays then + selectedGroup = GROUPS[1].key + end + if showPlannedPages and handover.on then + entries[#entries + 1] = { divider = true } + entries[#entries + 1] = { key = "graphs", label = L.group.graphs, disabled = true } + end +end + +local function loadLabels() + L.title = BAR.I18N("ui.teamStats.title") + L.titleText = colorTitle .. L.title + L.notYet = BAR.I18N("ui.teamStats.notYet") + L.perMinuteSuffix = BAR.I18N("ui.teamStats.perMinuteSuffix") + L.perMinuteNote = BAR.I18N("ui.teamStats.perMinuteNote") + L.memberOne = BAR.I18N("ui.teamStats.memberOne") + -- The milestone kinds the gadget records. + L.milestones = BAR.I18N("ui.teamStats.milestones") + L.milestone = {} + for _, key in ipairs({ "factory", "tech2", "tech3", "nuke", "antinuke", "lrpc", "commanderLost", "teamDied" }) do + L.milestone[key] = BAR.I18N("ui.teamStats.milestone." .. key) + end + + L.group = {} + for _, group in ipairs(GROUPS) do + L.group[group.key] = BAR.I18N("ui.teamStats.group." .. group.key) + end + L.group.graphs = BAR.I18N("ui.teamStats.group.graphs") + + L.switch, L.switchDesc = {}, {} + for _, sw in ipairs(switches) do + L.switch[sw.key] = BAR.I18N("ui.teamStats.switch." .. sw.key) + L.switchDesc[sw.key] = BAR.I18N("ui.teamStats.switch." .. sw.key .. "Desc") + sw.label = L.switch[sw.key] + end + + L.caption, L.stat, L.full, L.desc = {}, {}, {}, {} + for key, column in pairs(COLUMNS) do + if column.group ~= "" and not L.caption[column.group] then + L.caption[column.group] = BAR.I18N("ui.teamStats." .. column.group) + end + L.stat[key] = BAR.I18N("ui.teamStats." .. column.short) + L.full[key] = BAR.I18N("ui.teamStats." .. column.stat) + if key ~= "name" then + L.desc[key] = BAR.I18N("ui.teamStats.desc." .. key) + end + end + + rebuildEntries() +end + +-- The panel's state, read at once: the gadget's hand-over is the only thing that can +-- change what the panel is made of between two frames. +local function syncGadgetState(frame) + local on + if handover.frame then + on = frame - handover.frame <= handover.stale + else + on = handover.opened == nil or frame - handover.opened <= handover.stale + end + if on == handover.on then + return + end + handover.on = on + rebuildEntries() + setLayout() + dropLists() +end + +-- Reads the numbers, after settling whether the gadget is still there to read from. +local function refresh() + syncGadgetState(spGetGameFrame()) + refreshStats() +end + +function widget:ViewResize() + vsx, vsy = spGetViewGeometry() + widgetScale = (vsy / 1080) + + screenHeight = mathFloor(screenHeightOrg * widgetScale) + screenWidth = mathFloor(screenWidthOrg * widgetScale) + screenX = mathFloor((vsx * 0.5) - (screenWidth / 2)) + screenY = mathFloor((vsy * 0.5) + (screenHeight / 2)) + + font = WG.fonts.getFont() + elementCorner = WG.FlowUI.elementCorner + + RectRound = WG.FlowUI.Draw.RectRound + UiElement = WG.FlowUI.Draw.Element + UiScroller = WG.FlowUI.Draw.Scroller + UiScrollerAt = WG.FlowUI.Draw.ScrollerGeometry + Highlight = WG.FlowUI.Draw.SelectHighlight + UiToggle = WG.FlowUI.Draw.Toggle + + setLayout() + dropLists() + deleteGuishader() +end + +function widget:DrawScreen() + if not (show or showOnceMore) then + deleteGuishader() + return + end + + -- Pinned rather than assumed: widgets on lower layers draw first and leave blending, + -- colour and the texture wherever they finished. + glTexture(false) + glColor(1, 1, 1, 1) + + local mx, my, lmb = spGetMouseState() + if dragging then + if lmb then + scrollFromY(my) + else + dragging = false + end + end + + local sig = panelSignature(show and mx or -1, show and my or -1) + if sig ~= panelSig then + if panelList then + glDeleteList(panelList) + end + panelList = glCreateList(drawPanel) + panelSig = sig + end + + if not windowList then + windowList = glCreateList(drawWindow) + end + ---@cast panelList -? + glCallList(windowList) + glCallList(panelList) + + 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, "teamstats", nil, widget) + end + showOnceMore = false + + if show and math_isInRect(mx, my, screenX, screenY - screenHeight, screenX + screenWidth, screenY) then + spSetMouseCursor("cursornormal") + + if WG.tooltip then + local title, tip + local entry = hover.sb > 0 and entries[hover.sb] or nil + if hover.hcol > 1 then + local column = columns[hover.hcol] + ---@cast column -? + title = columnTitle(column) + tip = L.desc[column.key] + if filters.perMinute and column.rate then + tip = tip .. "\n" .. colorDim .. L.perMinuteNote end - local color = "" - if teamData.frame == " " then - color = "\255\255\220\130" - elseif lineCount % 2 == 1 then - color = "\255\200\200\200" + elseif hover.tog > 0 then + local sw = switches[hover.tog] + ---@cast sw -? + title = sw.label + tip = L.switchDesc[sw.key] + elseif entry and entry.disabled then + title = entry.label + tip = L.notYet + elseif hover.row > 0 and hover.col == 1 then + -- Everything about the player, on the name: the columns the open view hides too. + local row = rows[scroll + hover.row] + if row and row.type == "team" then + title = row.team.label + tip = nameCard(row.team) end - if i == 1 then - font2:Begin() - font2:Print( - color .. value, - columnX, - baseYSize + (heightCorrection * 1.66) - lineCount * lineHeight, - (fontSize * fontSizePercentage), - "dco" - ) - font2:End() - else - font:Print( - color .. value, - columnX, - baseYSize + heightCorrection - lineCount * lineHeight, - (fontSize * fontSizePercentage), - "dco" - ) + elseif hover.row > 0 and hover.col > 1 then + -- The cell's whole number: a cell rounds to three figures. A share says what it + -- is a share of. + local row = rows[scroll + hover.row] + local column = columns[hover.col] + ---@cast column -? + if row then + fitRow(row) + title = row.type == "team" and row.team.name or row.caption + local v = row.vals[hover.col] + local exact = formatExact(column, v) + if row.type == "team" and shareMode() and column.fmt == "si" and isFinite(v) then + exact = BAR.I18N("ui.teamStats.ofTeam", { + share = stringFormat("%.1f%%", v), + value = formatExact(column, baseValue(column, row.team)), + }) + end + local detail = cellDetail(column, row.type == "team" and row.team.stats or row.ally.total) + if detail then + exact = exact .. " " .. colorDim .. detail + end + tip = colorDim .. columnTitle(column) .. ": " .. colorTitle .. exact end end - lineCount = lineCount + 1 + if tip then + WG.tooltip.ShowTooltip("teamstats", tip, nil, nil, title) + end + end + end +end + +-- The gadget's hand-over, every second while the panel is open: read at once, so the +-- table follows the game a second at a time. The values are kept when the panel closes, +-- so a reopen shows the last ones until the next hand-over rather than nothing. +local function receiveLive(all, frame) + handover.all, handover.frame = all, frame + if show and (not gameover or handover.postGame) then + handover.postGame = false + refresh() + end +end + +local function closePanel() + show = false + dragging = false + widgetHandler:DeregisterGlobal("TeamStatsLive") + if WG.tooltip then + WG.tooltip.RemoveTooltip("teamstats") + end +end + +local function setShown(state) + if not state then + closePanel() + return + end + + if not show and WG.topbar then + WG.topbar.hideWindows() + end + show = true + -- Registered while open only, so the gadget hands nothing over to a closed panel. + widgetHandler:RegisterGlobal("TeamStatsLive", receiveLive) + handover.opened = spGetGameFrame() + -- The numbers freeze at game over; a panel first opened after it still needs one read. + if not gameover or #allies == 0 then + refresh() + end +end + +local function selectEntry(i) + local e = entries[i] or {} + if e.disabled or e.key == selectedGroup then + return + end + selectedGroup = e.key + layoutColumns() + if playSounds then + spPlaySoundFile(buttonclick, 0.6, "ui") + end +end + +local function sortBy(column) + if column.key == sortKey then + sortAscending = not sortAscending + else + sortKey = column.key + -- Names read best from A, numbers from the largest. + sortAscending = column.key == "name" + end + rebuildRows() + if playSounds then + spPlaySoundFile(buttonclick, 0.6, "ui") + end +end + +local function toggleSwitch(i) + local sw = switches[i] or {} + local key = tostring(sw.key) + filters[key] = not filters[key] + -- Grouping decides whether the share switch is offered, so the header is laid out + -- again. The others only change what the columns say: a rate changes every value and + -- so the order, and the captions say when they are rates. + if key == "groupByTeam" then + setLayout() + else + layoutColumns() + end + rebuildRows() + if playSounds then + spPlaySoundFile(buttonclick, 0.6, "ui") + end +end + +function widget:KeyPress(key) + if show and key == 27 then + -- ESC + showOnceMore = true + closePanel() + return true + end + + return false +end + +-- Swallowed across the whole panel, not just the table: a wheel that gets through zooms +-- the camera behind it. +function widget:MouseWheel(up, _value) + if not show then + return false + end + + local x, y = spGetMouseState() + if not math_isInRect(x, y, screenX, screenY - screenHeight, screenX + screenWidth, screenY) then + return false + end + + -- The chat history's modifiers: Ctrl moves three notches' worth at once, Shift a whole + -- page - the rows the band holds from where the list is now, since a band is taller. + local _, ctrl, _, shift = spGetModKeyState() + local step = ctrl and metrics.wheelRows * 3 or metrics.wheelRows + if shift then + ensureRowMetrics() + local band, used = listTop - listBottom, 0 + step = 0 + for i = scroll + 1, #rows do + used = used + rowHeightOf(rows[i]) + if used > band then + break + end + step = step + 1 end - if not isFFA then - lineCount = lineCount + 1 -- add line break after end of allyteam + step = mathMax(1, step) + end + setScroll(scroll + (up and -step or step)) + + return true +end + +-- Clicks inside the panel pick a view, flip a switch, sort a column 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 + + -- A press on a top bar button is the top bar's to handle: it closes the open windows + -- and opens the one that was clicked. Closing (and consuming) here would swallow it. + if WG.topbar and WG.topbar.buttonAt and WG.topbar.buttonAt(x, y) then + return false + end + + if math_isInRect(x, y, screenX, screenY - screenHeight, screenX + screenWidth, screenY) then + if not release and button == 1 then + local sw = switchAt(x, y) + local i = sidebarIndexAt(x, y) + local col = x >= listX1 and x < listRight and headerColumnAt(x, y) + if sw then + toggleSwitch(sw) + elseif i then + selectEntry(i) + elseif col then + sortBy(columns[col]) + elseif math_isInRect(x, y, barX1, listBottom, area.x2, listTop) then + -- The strip between the bar and the panel edge stays grabbable too. + grabScroller(y) + end + end + + return true + 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 + closePanel() + + return true + end +end + +function widget:MousePress(x, y, button) + return mouseEvent(x, y, button, false) +end + +function widget:MouseRelease(x, y, button) + return mouseEvent(x, y, button, true) +end + +function widget:GameFrame(n) + if gameover or not show or n % UPDATE_FRAMES ~= 0 then + return + end + -- The gadget's hand-over read the numbers this second already. + if handover.frame and n - handover.frame < UPDATE_FRAMES then + return + end + refresh() +end + +-- The numbers stop at game over: what happens in the minutes after is not the game. +function widget:GameOver() + refresh() + gameover = true + handover.postGame = true +end + +function widget:TeamDied(teamID) + deathFrame[teamID] = spGetGameFrame() +end + +function widget:ApmEvent(teamID, apm) + teamAPM[teamID] = apm +end + +-- Who the viewer is decides which row is theirs and which colours they may see. +function widget:PlayerChanged() + isSpec = spGetSpectatingState() + localTeamID = spGetLocalTeamID() + if show and not gameover then + refresh() + end +end + +function widget:Initialize() + loadLabels() + widget:ViewResize() + + widgetHandler:AddAction("teamstats", function() + setShown(not show) + + return true + end, nil, "p") + widgetHandler:AddAction("teamstatus_close", function() + if show then + setShown(false) + + return true + end + end, nil, "p") + + -- lets the handler hide the rest of the interface while the panel is open + widgetHandler:RegisterModalWindow(function() + return show == true + end) + + WG.teamstats = {} + WG.teamstats.toggle = function(state) + if state == nil then + state = not show + end + setShown(state) + end + WG.teamstats.isvisible = function() + return show + end +end + +function widget:Shutdown() + dropLists() + deleteGuishader() + widgetHandler:DeregisterGlobal("TeamStatsLive") + if WG.tooltip then + WG.tooltip.RemoveTooltip("teamstats") + end + WG.teamstats = nil +end + +-- The sort, the view and the switches are kept between games: someone who reads the +-- table one way wants it that way every time they open it. +function widget:GetConfigData() + return { + sortKey = sortKey, + sortAscending = sortAscending, + group = selectedGroup, + groupByTeam = filters.groupByTeam, + perMinute = filters.perMinute, + bars = filters.bars, + } +end + +-- Runs before Initialize, so the first layout already honours it. +function widget:SetConfigData(data) + if type(data) ~= "table" then + return + end + -- The old panel saved the sort under another name, with the name column as "frame". + local key = data.sortKey or data.sortVar + if key == "frame" then + key = "name" + end + if key and COLUMNS[key] then + sortKey = key + end + if data.sortAscending ~= nil then + sortAscending = data.sortAscending == true + end + if data.group and groupByKey[data.group] then + selectedGroup = data.group + end + for filterKey in pairs(filters) do + if data[filterKey] ~= nil then + filters[filterKey] = data[filterKey] == true end end - font:End() end function widget:LanguageChanged() - refreshHeaders() + loadLabels() widget:ViewResize() + -- Names with a dead or gone suffix are read in the language of the read. + if #allies > 0 and not gameover then + refresh() + end end diff --git a/luaui/Widgets/unit_auto_cloak.lua b/luaui/Widgets/unit_auto_cloak.lua index 979af3abd2d..22e1433dd19 100644 --- a/luaui/Widgets/unit_auto_cloak.lua +++ b/luaui/Widgets/unit_auto_cloak.lua @@ -16,6 +16,7 @@ end local unitdefConfigNames = { armdecom = false, cordecom = false, + legdecom = false, armferret = false, armamb = false, armpb = false, diff --git a/luaui/Widgets/unit_factory_quota.lua b/luaui/Widgets/unit_factory_quota.lua index 93bfca89699..7c71deb4906 100644 --- a/luaui/Widgets/unit_factory_quota.lua +++ b/luaui/Widgets/unit_factory_quota.lua @@ -15,8 +15,8 @@ end -- Localized Spring API for performance local spGetMyTeamID = Spring.GetLocalTeamID -local maxBuildProg = 0.075 -- maximum build progress that gets replaced in a repeat queue -local maxMetal = 500 -- maximum metal cost that gets replaced in a repeat queue(7.5% of a juggernaut is still over 2k metal) +local maxBuildProg = 0.075 -- maximum build progress that gets replaced +local maxMetal = 500 -- maximum metal cost that gets replaced (7.5% of a juggernaut is still over 2k metal) -- factoryID is unitID of the factory local quotas = {} -- {[factoryID] = {[unitDefID] = amount, ...}, ...} @@ -24,6 +24,17 @@ local quotas = {} -- {[factoryID] = {[unitDefID] = amount, ...}, ...} local builtUnits = {} -- {[factoryID] = {[unitDefID] = {[unitID] = true, ...}, ...}, ...} local unitToFactoryID = {} -- {[unitID] = factoryID, ...} +-- Quota orders cannot be told apart from player orders by their options alone. +-- - The engine marks an alt-queued factory build order internal, but only while repeat is on. +-- - With repeat off, the internal bit reads as "quota order". +-- - With repeat on, the internal bit reads as "quota or player priority order". + +local quotaOrderTags = {} +local quotaOrderCounts = {} +local seenOrderTags = {} +local unclaimedOrders = {} +local unclaimedPasses = {} + local possibleFactories = {} local factoryDefIDs = {} local metalcosts = {} @@ -86,6 +97,84 @@ local function getMostNeedQuota(quota, factoryID) return minimumQuota, minimumUnitDefID end +local function trackQuotaOrders(factoryID) + local commandQueue = spGetFactoryCommands(factoryID, -1) + if not commandQueue then + quotaOrderTags[factoryID] = nil + quotaOrderCounts[factoryID] = nil + seenOrderTags[factoryID] = nil + unclaimedOrders[factoryID] = nil + unclaimedPasses[factoryID] = nil + return + end + + local ownedTags = quotaOrderTags[factoryID] + local seenTags = seenOrderTags[factoryID] + local unclaimed = unclaimedOrders[factoryID] + + local liveOwnedTags = {} + local liveSeenTags = {} + local counts = {} + + for i = 1, #commandQueue do + local command = commandQueue[i] + local unitDefID = -command.id + if unitDefID > 0 and command.options.internal then + local tag = command.tag + liveSeenTags[tag] = true + + local isOurs = ownedTags and ownedTags[tag] + if not isOurs and not (seenTags and seenTags[tag]) and unclaimed and (unclaimed[unitDefID] or 0) > 0 then + unclaimed[unitDefID] = unclaimed[unitDefID] - 1 + isOurs = true + end + + if isOurs then + liveOwnedTags[tag] = unitDefID + counts[unitDefID] = (counts[unitDefID] or 0) + 1 + end + end + end + + quotaOrderTags[factoryID] = liveOwnedTags + quotaOrderCounts[factoryID] = counts + seenOrderTags[factoryID] = liveSeenTags + + -- Keep a build order unclaimed for multiple passes so it continues to be pending. + if unclaimed and next(unclaimed) then + local passes = (unclaimedPasses[factoryID] or 0) + 1 + if passes > 1 then + unclaimedOrders[factoryID] = nil + unclaimedPasses[factoryID] = nil + else + unclaimedPasses[factoryID] = passes + end + else + unclaimedOrders[factoryID] = nil + unclaimedPasses[factoryID] = nil + end +end + +-- Whether a factory has any quota-issued build orders anywhere in its factory queue, +-- including issued orders that have not arrived yet over the env/net/msg boogie wop. +local function hasQuotaOrderPending(factoryID) + local counts = quotaOrderCounts[factoryID] + if counts and next(counts) then + return true + end + local unclaimed = unclaimedOrders[factoryID] + return (unclaimed and next(unclaimed)) ~= nil +end + +-- Number of build orders in the factory's queue that this widget placed. +-- Subtract from the engine's queued count to get the count of orders by the player. +local function getQuotaOrderCount(factoryID, unitDefID) + local counts = quotaOrderCounts[factoryID] + return (counts and counts[unitDefID]) or 0 +end + +-- Check the head of the queue only to answer whether the factory can accept a quota order. +-- Quota defers to enqueued-first commands that players issue (with alt) as a panic button. local function isFactoryUsable(factoryID) local commandQueue = spGetFactoryCommands(factoryID, 2) if not commandQueue then @@ -123,11 +212,17 @@ local function appendToFactoryQueue(factoryID, unitDefID) { insertPosition, -unitDefID, CMD_OPT_ALT + CMD_OPT_INTERNAL }, CMD_OPT_ALT + CMD_OPT_CTRL ) + + -- Claimed by tag on the next pass, once the order has reached the queue. + unclaimedOrders[factoryID] = unclaimedOrders[factoryID] or {} + unclaimedOrders[factoryID][unitDefID] = (unclaimedOrders[factoryID][unitDefID] or 0) + 1 + unclaimedPasses[factoryID] = 0 end local function fillQuotas() for factoryID, quota in pairs(quotas) do - if isFactoryUsable(factoryID) then + trackQuotaOrders(factoryID) + if isFactoryUsable(factoryID) and not hasQuotaOrderPending(factoryID) then for unitDefID, num in pairs(quota) do if num == 0 then quota[unitDefID] = nil @@ -174,6 +269,11 @@ local function removeUnit(unitID, unitDefID, unitTeam) elseif builtUnits[unitID] then builtUnits[unitID] = nil quotas[unitID] = nil + quotaOrderTags[unitID] = nil + quotaOrderCounts[unitID] = nil + seenOrderTags[unitID] = nil + unclaimedOrders[unitID] = nil + unclaimedPasses[unitID] = nil end end end @@ -208,6 +308,9 @@ function widget:Initialize() WG.Quotas.isOnQuotaMode = function(unitID) return isOnQuotaBuildMode(unitID) end + WG.Quotas.getQuotaOrderCount = function(factoryID, unitDefID) + return getQuotaOrderCount(factoryID, unitDefID) + end end function widget:Shutdown() diff --git a/luaui/Widgets/unit_waypoint_dragger_2.lua b/luaui/Widgets/unit_waypoint_dragger_2.lua index 564f73b630a..d65f599b734 100644 --- a/luaui/Widgets/unit_waypoint_dragger_2.lua +++ b/luaui/Widgets/unit_waypoint_dragger_2.lua @@ -176,6 +176,7 @@ local function MoveWayPoints(wpTbl, mx, my, finalize) local cmdLink = wpData[5] local cmdID = wpData[6].id local cmdTag = wpData[6].tag + local cmdOptions = wpData[6].options.coded local cmdUnitID = wpData[7] if finalize then @@ -183,11 +184,9 @@ local function MoveWayPoints(wpTbl, mx, my, finalize) cmdLink = cmdTag end if cmdFacRad > 0 then - -- spGiveOrderToUnit(cmdUnitID, CMD.INSERT, {cmdNum, cmdID, 0, cx, cy, cz, cmdFacRad}, {"alt"}) - spGiveOrderToUnit(cmdUnitID, CMD.INSERT, { cmdLink, cmdID, 0, cx, cy, cz, cmdFacRad }, 0) + spGiveOrderToUnit(cmdUnitID, CMD.INSERT, { cmdLink, cmdID, cmdOptions, cx, cy, cz, cmdFacRad }, 0) else - -- spGiveOrderToUnit(cmdUnitID, CMD.INSERT, {cmdNum, cmdID, 0, cx, cy, cz}, {"alt"}) - spGiveOrderToUnit(cmdUnitID, CMD.INSERT, { cmdLink, cmdID, 0, cx, cy, cz }, 0) + spGiveOrderToUnit(cmdUnitID, CMD.INSERT, { cmdLink, cmdID, cmdOptions, cx, cy, cz }, 0) end if not alt then spGiveOrderToUnit(cmdUnitID, CMD.REMOVE, { cmdTag }, 0) diff --git a/luaui/Widgets/widget_selector.lua b/luaui/Widgets/widget_selector.lua index df5024cb44c..96ce31d16f0 100644 --- a/luaui/Widgets/widget_selector.lua +++ b/luaui/Widgets/widget_selector.lua @@ -958,6 +958,7 @@ local function buildCategories() local counts, active, total, on = {}, {}, 0, 0 local changed, changedOn = 0, 0 local mine, mineOn = 0, 0 + local rml, rmlOn = 0, 0 local starred, starredOn = 0, 0 for i = 1, #entries do local e = entries[i] @@ -982,6 +983,12 @@ local function buildCategories() mineOn = mineOn + 1 end end + if e.isRml then + rml = rml + 1 + if e.data.active then + rmlOn = rmlOn + 1 + end + end if fav.names[e.name] then starred = starred + 1 if e.data.active then @@ -1013,6 +1020,15 @@ local function buildCategories() if mine > 0 then categories[#categories + 1] = { key = "local", label = L.mine, count = mine, active = mineOn } end + -- The widgets that build their interface through RmlUi rather than drawing it the way + -- the rest of the list does. The same kind of view as Your own, and matched the same way + -- the tag on the row is: whether the source uses the API, not where the file sits, since + -- a player's own can live anywhere. It carries no colour for the same reason Your own + -- does not - an RmlUi widget still has a prefix, and the colour ahead of it on the row + -- is its group's. + if rml > 0 then + categories[#categories + 1] = { key = "rml", label = L.rmlui, count = rml, active = rmlOn } + end for _, g in ipairs(GROUP_ORDER) do if counts[g] then categories[#categories + 1] = { key = g, label = L[g] or g, count = counts[g], active = active[g] or 0 } @@ -1148,12 +1164,13 @@ rebuildRows = function() -- The loop covers every index, so this cannot be nil. ---@cast e -? if (not filters.enabledOnly or e.state > 0) and (not filters.errorsOnly or e.errors) then - -- `favorite`, `changed` and `local` are views of the whole list rather than filename - -- prefixes, so each is matched on what it means instead of on the group. + -- `favorite`, `changed`, `local` and `rml` are views of the whole list rather than + -- filename prefixes, so each is matched on what it means instead of on the group. local inView = not selectedCategory or (selectedCategory == fav.key and fav.names[e.name]) or (selectedCategory == "changed" and e.changed) or (selectedCategory == "local" and e.isLocal) + or (selectedCategory == "rml" and e.isRml) or e.group == selectedCategory if not scored then if inView then @@ -1177,6 +1194,7 @@ rebuildRows = function() add(fav.names[e.name] and found[fav.key], on) add(e.changed and found.changed, on) add(e.isLocal and found["local"], on) + add(e.isRml and found.rml, on) end end end @@ -2757,7 +2775,7 @@ local function drawSidebar() end local ty = mathFloor((y1 + y2) * 0.5) -- A prefix group carries its colour ahead of its label, which is the key to the squares on - -- the rows. All, Changed and Your own cut across the groups, so they have none. + -- the rows. All, Changed, Your own and RmlUi cut across the groups, so they have none. -- -- Favourites carries the same star its rows are starred with instead, in the slot the -- colour would have taken: the mark on the rows and the mark on the category it @@ -3309,6 +3327,7 @@ local function loadLabels() L.search = tr("search", "Search...") L.mine = tr("category.local", "Your own") + L.rmlui = tr("category.rml", "RmlUi") L.enabledOnly = tr("enabledonly", "Enabled only") L.errorsOnly = tr("errorsonly", "Errors only") L.byOrder = tr("byorder", "By load order") @@ -3429,6 +3448,10 @@ local function loadLabels() "localdesc", "The widgets in your own LuaUI folder rather than the ones the game ships. They carry a local tag on the row too." ), + rmlui = tr( + "rmldesc", + "The widgets that build their interface with RmlUi, laid out from markup and style sheets, rather than drawing it themselves. Found by what their source uses rather than where the file sits, so your own count too. They carry an rml tag on the row as well." + ), enabledOnly = tr( "enabledonlydesc", "Show only the widgets the config says to load - running or not - so what is off stays out of the way." @@ -3872,6 +3895,8 @@ local function showTooltip(row) body = L.desc.changed elseif c.key == "local" then body = L.desc.mine + elseif c.key == "rml" then + body = L.desc.rmlui else -- Which filename prefix this one collects. The full-word label deliberately does -- not say it, and it is the one thing about a category worth knowing. diff --git a/luaui/configs/DeferredLightsGL4config.lua b/luaui/configs/DeferredLightsGL4config.lua index b090fea3ee8..0ac974a8fc8 100644 --- a/luaui/configs/DeferredLightsGL4config.lua +++ b/luaui/configs/DeferredLightsGL4config.lua @@ -5960,7 +5960,7 @@ local unitLights = { posx = 0, posy = 0, posz = 1, - radius = 33, + radius = 16, color2r = 0, color2g = 0, color2b = 0, @@ -6091,7 +6091,7 @@ local unitLights = { posx = 0, posy = 0, posz = 1, - radius = 33, + radius = 15, color2r = 0, color2g = 0, color2b = 0, @@ -9973,7 +9973,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 36, + radius = 21, color2r = 0, color2g = 0, color2b = 0, @@ -9998,7 +9998,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 36, + radius = 21, color2r = 0, color2g = 0, color2b = 0, @@ -10023,7 +10023,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 36, + radius = 21, color2r = 0, color2g = 0, color2b = 0, @@ -10048,7 +10048,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 36, + radius = 21, color2r = 0, color2g = 0, color2b = 0, @@ -10073,7 +10073,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 42, + radius = 21, color2r = 0, color2g = 0, color2b = 0, @@ -10100,7 +10100,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 36, + radius = 24, color2r = 0, color2g = 0, color2b = 0, @@ -10125,7 +10125,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 36, + radius = 24, color2r = 0, color2g = 0, color2b = 0, @@ -10150,7 +10150,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 36, + radius = 24, color2r = 0, color2g = 0, color2b = 0, @@ -10175,7 +10175,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 36, + radius = 24, color2r = 0, color2g = 0, color2b = 0, @@ -10200,7 +10200,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 42, + radius = 24, color2r = 0, color2g = 0, color2b = 0, @@ -10227,7 +10227,7 @@ local unitLights = { posx = 0, posy = 13, posz = 0, - radius = 35, + radius = 17, color2r = 0, color2g = 0, color2b = 0, @@ -10252,7 +10252,7 @@ local unitLights = { posx = -14, posy = 6, posz = 0, - radius = 32, + radius = 17, color2r = 0, color2g = 0, color2b = 0, @@ -10277,7 +10277,7 @@ local unitLights = { posx = 14, posy = 6, posz = 0, - radius = 32, + radius = 17, color2r = 0, color2g = 0, color2b = 0, @@ -10710,7 +10710,7 @@ local unitLights = { posx = 0, posy = 13, posz = 0, - radius = 35, + radius = 17, color2r = 0, color2g = 0, color2b = 0, @@ -10735,7 +10735,7 @@ local unitLights = { posx = 0, posy = 13, posz = 0, - radius = 35, + radius = 17, color2r = 0, color2g = 0, color2b = 0, @@ -10760,7 +10760,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 38, + radius = 17, color2r = 0, color2g = 0, color2b = 0, @@ -10785,7 +10785,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 38, + radius = 17, color2r = 0, color2g = 0, color2b = 0, @@ -10810,7 +10810,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 38, + radius = 17, color2r = 0, color2g = 0, color2b = 0, @@ -11527,7 +11527,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 36, + radius = 12, color2r = 0.2, color2g = 1, color2b = 0.2, @@ -11552,7 +11552,7 @@ local unitLights = { posx = -2, posy = 0, posz = 0, - radius = 24, + radius = 12, color2r = 0.2, color2g = 1, color2b = 0.2, @@ -11577,7 +11577,7 @@ local unitLights = { posx = 2, posy = 0, posz = 0, - radius = 24, + radius = 12, color2r = 0.2, color2g = 1, color2b = 0.2, @@ -11604,7 +11604,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 48, + radius = 26, color2r = 0.2, color2g = 1, color2b = 0.2, @@ -66690,7 +66690,7 @@ local unitLights = { posx = -1, posy = 1, posz = 0, - radius = 38, + radius = 21, color2r = 0, color2g = 0, color2b = 0, @@ -75084,7 +75084,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 55, + radius = 27, color2r = 0, color2g = 0, color2b = 0, @@ -75109,7 +75109,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 55, + radius = 27, color2r = 0, color2g = 0, color2b = 0, @@ -89352,7 +89352,7 @@ local unitLights = { posx = 2, posy = 5, posz = 10, - radius = 30, + radius = 11, color2r = 1, color2g = 0.2, color2b = 0.2, @@ -89377,7 +89377,7 @@ local unitLights = { posx = 0, posy = 0, posz = -2, - radius = 40, + radius = 11, color2r = 1, color2g = 0.2, color2b = 0.2, @@ -93202,7 +93202,7 @@ local unitLights = { posx = 0, posy = 0, posz = 0, - radius = 32, + radius = 13, color2r = 1, color2g = 0.2, color2b = 0.2, @@ -93329,7 +93329,7 @@ local unitLights = { posx = 0, posy = 4.5, posz = -12, - radius = 32, + radius = 13, color2r = 1, color2g = 0.2, color2b = 0.2, @@ -94753,7 +94753,7 @@ local unitLights = { posx = 0, posy = 7, posz = 0, - radius = 32, + radius = 11, color2r = 0, color2g = 0, color2b = 1, diff --git a/luaui/configs/DistortionGL4Config.lua b/luaui/configs/DistortionGL4Config.lua index 493a09ef8d9..e82344ce67a 100644 --- a/luaui/configs/DistortionGL4Config.lua +++ b/luaui/configs/DistortionGL4Config.lua @@ -126,6 +126,46 @@ local unitDistortions = { }, }, }, + + legeconv = { + distortion = { + distortionType = "point", + pieceName = "flare", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 16, + noiseStrength = 0.6, + noiseScaleSpace = 1.5, + distanceFalloff = 0.5, + lifeTime = 0, + rampUp = 30, + decay = -1.2, + effectType = 0, + }, + }, + }, + + legfeconv = { + distortion = { + distortionType = "point", + pieceName = "light", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 16, + noiseStrength = 0.6, + noiseScaleSpace = 1.5, + distanceFalloff = 0.5, + lifeTime = 0, + rampUp = 30, + decay = -1.2, + effectType = 0, + }, + }, + }, armmmkr = { distortion = { distortionType = "point", @@ -164,6 +204,46 @@ local unitDistortions = { }, }, }, + + legadveconv = { + distortion = { + distortionType = "point", + pieceName = "largeMidCell", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 27, + noiseStrength = 0.5, + noiseScaleSpace = 1.4, + distanceFalloff = 0.5, + lifeTime = 0, + rampUp = 30, + decay = -1.2, + effectType = 0, + }, + }, + }, + + leganavaleconv = { + distortion = { + distortionType = "point", + pieceName = "topLight", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 22, + noiseStrength = 0.6, + noiseScaleSpace = 1.5, + distanceFalloff = 0.5, + lifeTime = 0, + rampUp = 30, + decay = -1.2, + effectType = 0, + }, + }, + }, armestor = { distortion = { distortionType = "beam", @@ -485,6 +565,30 @@ local unitDistortions = { }, }, + legestor = { + distortion = { + distortionType = "beam", + pieceName = "legestor", + distortionConfig = { + posx = 0, + posy = 4, + posz = 0.01, + radius = 24, + pos2x = 0, + pos2y = 24, + pos2z = 0, + radius2 = 24, + noiseStrength = 0.6, + noiseScaleSpace = -1.2, + distanceFalloff = 0.8, + rampUp = 30, + decay = -1.2, + lifeTime = 0, + effectType = 0, + }, + }, + }, + coruwadves = { distortion = { distortionType = "beam", @@ -509,6 +613,30 @@ local unitDistortions = { }, }, + legadvestore = { + distortion = { + distortionType = "beam", + pieceName = "base", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0.01, + radius = 30, + pos2x = 0, + pos2y = 32, + pos2z = 0, + radius2 = 30, + noiseStrength = 0.5, + noiseScaleSpace = -1.4, + distanceFalloff = 0.8, + rampUp = 30, + decay = -1.3, + lifeTime = 0, + effectType = 0, + }, + }, + }, + armguard = { sleeve1 = { distortionType = "beam", @@ -777,6 +905,115 @@ local unitDistortions = { -- }, }, + legavp = { + heatvent1 = { + distortionType = "beam", + pieceName = "base", + distortionConfig = { + posx = 53.6, + posy = 26.2, + posz = -27.1, + radius = 16, + pos2x = 53.6, + pos2y = 44.2, + pos2z = -27.0, + noiseStrength = 0.5, + noiseScaleSpace = -2, + distanceFalloff = 1.4, + windAffected = -1, + riseRate = 1, + lifeTime = 0, + effectType = "heatDistortion", + }, + }, + }, + + legvp = { + heatvent1 = { + distortionType = "beam", + pieceName = "exhaust", + distortionConfig = { + posx = -1.17, + posy = 17.84, + posz = 1.04, + radius = 11, + pos2x = -1.17, + pos2y = 31.84, + pos2z = 1.14, + noiseStrength = 0.5, + noiseScaleSpace = -2, + distanceFalloff = 1.4, + windAffected = -1, + riseRate = 1, + lifeTime = 0, + effectType = "heatDistortion", + }, + }, + }, + + legageo = { + heatvent1 = { + distortionType = "beam", + pieceName = "exhaust1", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 9, + pos2x = 0, + pos2y = 14, + pos2z = 0.1, + noiseStrength = 0.5, + noiseScaleSpace = -2, + distanceFalloff = 1.4, + windAffected = -1, + riseRate = 1, + lifeTime = 0, + effectType = "heatDistortion", + }, + }, + heatvent2 = { + distortionType = "beam", + pieceName = "exhaust2", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 9, + pos2x = 0, + pos2y = 14, + pos2z = 0.1, + noiseStrength = 0.5, + noiseScaleSpace = -2, + distanceFalloff = 1.4, + windAffected = -1, + riseRate = 1, + lifeTime = 0, + effectType = "heatDistortion", + }, + }, + heatvent3 = { + distortionType = "beam", + pieceName = "exhaust3", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 9, + pos2x = 0, + pos2y = 14, + pos2z = 0.1, + noiseStrength = 0.5, + noiseScaleSpace = -2, + distanceFalloff = 1.4, + windAffected = -1, + riseRate = 1, + lifeTime = 0, + effectType = "heatDistortion", + }, + }, + }, + armsd = { distortion = { distortionType = "point", @@ -1120,30 +1357,6 @@ local unitDistortions = { }, }, - armblade = { - thrustdown = { - distortionType = "cone", - pieceName = "trust", - distortionConfig = { - posx = 0, - posy = 4, - posz = 4, - radius = 40, - dirx = 0, - diry = -1, - dirz = 0.1, - theta = 0.8, - noiseStrength = 0.7, - noiseScaleSpace = 1.45, - distanceFalloff = 1.0, - effectStrength = 1.5, - riseRate = -8, - lifeTime = 0, - effectType = 0, - }, - }, - }, - armbrawl = { thrustdown = { distortionType = "point", @@ -1525,6 +1738,32 @@ local unitDistortions = { }, }, + legtide = { + waterflow = { + distortionType = "beam", + pieceName = "wheel", + distortionConfig = { + posx = 0, + posy = -2.2, + posz = -16, + radius = 12, + pos2x = 0, + pos2y = -2.2, + pos2z = 16, + radius2 = 12, + noiseStrength = 2.5, + noiseScaleSpace = 0.7, + distanceFalloff = 0.75, + rampUp = 0, + decay = 0, + windAffected = -1, + riseRate = 1, + lifeTime = 0, + effectType = 0, + }, + }, + }, + corvamp = { thrust = { distortionType = "cone", @@ -2217,6 +2456,25 @@ local unitDistortions = { }, }, + legajam = { + jamdistortion = { + distortionType = "point", + pieceName = "jamLight", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 25, + noiseStrength = 10, + noiseScaleSpace = 0.4, + distanceFalloff = 1.5, + windAffected = -1, + lifeTime = 0, + effectType = 0, + }, + }, + }, + corap = { heatvent1 = { distortionType = "beam", @@ -2332,6 +2590,29 @@ local unitDistortions = { }, }, + leglrpc = { + heatvent1 = { + distortionType = "beam", + pieceName = "exhaustCell", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 8, + pos2x = 0, + pos2y = 8, + pos2z = 0.1, + noiseStrength = 0.5, + noiseScaleSpace = -2, + distanceFalloff = 1.4, + windAffected = -1, + riseRate = 1, + lifeTime = 0, + effectType = "heatDistortion", + }, + }, + }, + coravp = { factoryheat = { distortionType = "point", @@ -2812,6 +3093,44 @@ local unitDistortions = { -- }, }, + legdeflector = { + distortion = { + distortionType = "point", + pieceName = "shieldFlare", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 14, + noiseStrength = 1, + noiseScaleSpace = 0.5, + distanceFalloff = 0.3, + windAffected = -0.5, + lifeTime = 0, + effectType = 0, + }, + }, + }, + + leggatet3 = { + distortion = { + distortionType = "point", + pieceName = "base", + distortionConfig = { + posx = 0, + posy = 50, + posz = 0, + radius = 20, + noiseStrength = 1, + noiseScaleSpace = 0.5, + distanceFalloff = 0.3, + windAffected = -0.5, + lifeTime = 0, + effectType = 0, + }, + }, + }, + corfgate = { distortion = { distortionType = "point", @@ -2850,6 +3169,25 @@ local unitDistortions = { }, }, + legjam = { + jamdistortion = { + distortionType = "point", + pieceName = "strut", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 12, + noiseStrength = 10, + noiseScaleSpace = 0.4, + distanceFalloff = 1.5, + windAffected = -1, + lifeTime = 0, + effectType = 0, + }, + }, + }, + armjamt = { jamdistortion = { distortionType = "point", @@ -3130,6 +3468,25 @@ local unitDistortions = { }, }, + legjuno = { + distortion = { + distortionType = "point", + pieceName = "junoSphere", + distortionConfig = { + posx = 0, + posy = 0, + posz = 0, + radius = 10, + noiseStrength = 1.5, + noiseScaleSpace = -0.2, + distanceFalloff = 0.5, + windAffected = -0.5, + lifeTime = 0, + effectType = 0, + }, + }, + }, + corjugg = { distortion = { distortionType = "point", diff --git a/scripts/Units/leganavyflagship.bos b/scripts/Units/leganavyflagship.bos index 22e22f0444f..e4be36a851a 100644 --- a/scripts/Units/leganavyflagship.bos +++ b/scripts/Units/leganavyflagship.bos @@ -401,7 +401,7 @@ AimWeapon1(heading, pitch) FireWeapon1() { isfiring = 1; - return (0); + return (1); } diff --git a/scripts/Units/leganavyflagship.cob b/scripts/Units/leganavyflagship.cob index 04885024e4c..48f41226d82 100644 Binary files a/scripts/Units/leganavyflagship.cob and b/scripts/Units/leganavyflagship.cob differ diff --git a/scripts/Units/legkam.bos b/scripts/Units/legkam.bos index 872a1a20408..66a566c3e89 100644 --- a/scripts/Units/legkam.bos +++ b/scripts/Units/legkam.bos @@ -5,7 +5,7 @@ piece base, thrust, firepoint; -static-var statechg_DesiredState, statechg_StateChanging; +static-var statechg_DesiredState, statechg_StateChanging, detonating; #define SIG_AIM 2 @@ -95,8 +95,8 @@ FireWeapon1() { sleep 500; emit-sfx 1024 + 1 from base; + detonating = TRUE; // Killed reads this rather than inferring the cause of death from severity emit-sfx 4096 + 1 from base; //Weapon2 detonates the crawling bomb once weapon1 fires - //sleep 1000; return (0); } @@ -147,25 +147,17 @@ SweetSpot(piecenum) Killed(severity, corpsetype) { - //get PRINT(severity); - if( severity >= 500 ) + // Drop the bomb when we detonated ourselves, or when a weak killing blow left the payload intact. + if( detonating || severity <= 25 ) { + // The bomb uses this unit's model, so hide the airframe before launching it. + hide base; + hide thrust; + hide firepoint; emit-sfx 2048 + 2 from firepoint; //Triggers kamikaze weapon after unit kills itself - } - if( severity == 200 ) - { - emit-sfx 2048 + 2 from firepoint; //Triggers kamikaze weapon after unit kills itself - corpsetype = 1 ; - //emit-sfx 1024 from base; - //explode base type BITMAPONLY | NOHEATCLOUD; - return(corpsetype); - } - if( severity <= 25 ) - { - corpsetype = 1 ; - emit-sfx 1024 from base; - explode base type BITMAPONLY | NOHEATCLOUD; - return(corpsetype); + // Stay in the unit table until the bomb has landed, so its damage is credited to our team. + sleep 3000; + return (0); } if( severity <= 50 ) { diff --git a/scripts/Units/legkam.cob b/scripts/Units/legkam.cob index fb6b000091a..e4b31b28f10 100644 Binary files a/scripts/Units/legkam.cob and b/scripts/Units/legkam.cob differ diff --git a/scripts/Units/legnavydestro.bos b/scripts/Units/legnavydestro.bos index ca6caf1410e..0b45118c73e 100644 --- a/scripts/Units/legnavydestro.bos +++ b/scripts/Units/legnavydestro.bos @@ -260,6 +260,7 @@ AimWeapon2(heading, pitch) FireWeapon2() { sleep 150; + return (1); } QueryWeapon2(piecenum) diff --git a/scripts/Units/legnavydestro.cob b/scripts/Units/legnavydestro.cob index 92fec2022bf..3d930004e6c 100644 Binary files a/scripts/Units/legnavydestro.cob and b/scripts/Units/legnavydestro.cob differ diff --git a/units/Legion/Air/T2 Air/legfort.lua b/units/Legion/Air/T2 Air/legfort.lua index 7b650fa7c6b..f763fba8aa1 100644 --- a/units/Legion/Air/T2 Air/legfort.lua +++ b/units/Legion/Air/T2 Air/legfort.lua @@ -38,7 +38,7 @@ return { unitgroup = "weapon", model_author = "tHARSIS", normaltex = "unittextures/LEG_normal.dds", - subfolder = "CorAircraft/T2", + subfolder = "Legion/Air/T2 Air", techlevel = 2, }, sfxtypes = { diff --git a/units/Legion/Air/T2 Air/legheavydrone.lua b/units/Legion/Air/T2 Air/legheavydrone.lua index b8f895df587..e1ac688982a 100644 --- a/units/Legion/Air/T2 Air/legheavydrone.lua +++ b/units/Legion/Air/T2 Air/legheavydrone.lua @@ -34,7 +34,8 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "CorAircraft", + subfolder = "Legion/Air/T2 Air", + unitgroup = "weapon", drone = 1, nohealthbars = 1, }, diff --git a/units/Legion/Air/T2 Air/legheavydronesmall.lua b/units/Legion/Air/T2 Air/legheavydronesmall.lua index 9e31249a4d2..20c497a2da1 100644 --- a/units/Legion/Air/T2 Air/legheavydronesmall.lua +++ b/units/Legion/Air/T2 Air/legheavydronesmall.lua @@ -33,7 +33,8 @@ return { customparams = { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorAircraft", + subfolder = "Legion/Air/T2 Air", + unitgroup = "weapon", drone = 1, nohealthbars = 1, }, diff --git a/units/Legion/Air/T2 Air/legionnaire.lua b/units/Legion/Air/T2 Air/legionnaire.lua index 7a89bfea233..8cb58de8590 100644 --- a/units/Legion/Air/T2 Air/legionnaire.lua +++ b/units/Legion/Air/T2 Air/legionnaire.lua @@ -43,7 +43,7 @@ return { model_author = "Hornet", normaltex = "unittextures/cor_normal.dds", reaimtime = 5, - subfolder = "CorAircraft/T2", + subfolder = "Legion/Air/T2 Air", techlevel = 2, attacksafetydistance = 300, fighter = 1, diff --git a/units/Legion/Air/T2 Air/legnap.lua b/units/Legion/Air/T2 Air/legnap.lua index fa6de6c51b5..bf2f302d662 100644 --- a/units/Legion/Air/T2 Air/legnap.lua +++ b/units/Legion/Air/T2 Air/legnap.lua @@ -40,7 +40,7 @@ return { unitgroup = "weapon", model_author = "Mr Bob", normaltex = "unittextures/Arm_normal.dds", - subfolder = "CorAircraft/T2", + subfolder = "Legion/Air/T2 Air", techlevel = 2, }, sounds = { diff --git a/units/Legion/Air/T2 Air/legphoenix.lua b/units/Legion/Air/T2 Air/legphoenix.lua index 52582453286..53444e150cd 100644 --- a/units/Legion/Air/T2 Air/legphoenix.lua +++ b/units/Legion/Air/T2 Air/legphoenix.lua @@ -41,7 +41,7 @@ return { unitgroup = "weapon", model_author = "Protar/Hornet", normaltex = "unittextures/leg_normal.dds", - subfolder = "Legionaircraft/T2", + subfolder = "Legion/Air/T2 Air", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Air/T2 Air/legstronghold.lua b/units/Legion/Air/T2 Air/legstronghold.lua index 4f861b9c9de..79f58d525ae 100644 --- a/units/Legion/Air/T2 Air/legstronghold.lua +++ b/units/Legion/Air/T2 Air/legstronghold.lua @@ -40,7 +40,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, - subfolder = "CorAircraft/T2", + subfolder = "Legion/Air/T2 Air", techlevel = 2, crashable = 0, }, diff --git a/units/Legion/Air/legcib.lua b/units/Legion/Air/legcib.lua index 7f655debca8..019a1725a38 100644 --- a/units/Legion/Air/legcib.lua +++ b/units/Legion/Air/legcib.lua @@ -41,7 +41,7 @@ return { unitgroup = "weapon", model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorAircraft", + subfolder = "Legion/Air", }, sounds = { canceldestruct = "cancel2", diff --git a/units/Legion/Air/legdrone.lua b/units/Legion/Air/legdrone.lua index fd0e1546d58..c2e0591eb6e 100644 --- a/units/Legion/Air/legdrone.lua +++ b/units/Legion/Air/legdrone.lua @@ -34,7 +34,8 @@ return { customparams = { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorAircraft", + subfolder = "Legion/Air", + unitgroup = "weapon", drone = 1, nohealthbars = 1, }, diff --git a/units/Legion/Air/legmos.lua b/units/Legion/Air/legmos.lua index 05d6e019047..a6778df075a 100644 --- a/units/Legion/Air/legmos.lua +++ b/units/Legion/Air/legmos.lua @@ -32,7 +32,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "ArmAircraft", + subfolder = "Legion/Air", }, sounds = { canceldestruct = "cancel2", diff --git a/units/Legion/Bots/T2 Bots/legajamk.lua b/units/Legion/Bots/T2 Bots/legajamk.lua index e2f1796e727..9cadef180ba 100644 --- a/units/Legion/Bots/T2 Bots/legajamk.lua +++ b/units/Legion/Bots/T2 Bots/legajamk.lua @@ -42,7 +42,7 @@ return { model_author = "Tharsis, ZephyrSkies(helper)", normaltex = "unittextures/leg_normal.dds", off_on_stun = "true", - subfolder = "Legion/Bots/T2", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, unitgroup = "util", }, diff --git a/units/Legion/Bots/T2 Bots/legamph.lua b/units/Legion/Bots/T2 Bots/legamph.lua index d9ac7d3e048..62e022f3112 100644 --- a/units/Legion/Bots/T2 Bots/legamph.lua +++ b/units/Legion/Bots/T2 Bots/legamph.lua @@ -42,7 +42,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.2, reaimtime = 4, - subfolder = "Legion/T2", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, unitgroup = "weaponsub", speedfactorinwater = 1.3, diff --git a/units/Legion/Bots/T2 Bots/legaradk.lua b/units/Legion/Bots/T2 Bots/legaradk.lua index 1db73d91d66..e30e04214ad 100644 --- a/units/Legion/Bots/T2 Bots/legaradk.lua +++ b/units/Legion/Bots/T2 Bots/legaradk.lua @@ -38,7 +38,7 @@ return { juno_kill = true, model_author = "Tharsis, ZephyrSkies(helper)", normaltex = "unittextures/leg_normal.dds", - subfolder = "Legion/Bots/T2", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, unitgroup = "util", }, diff --git a/units/Legion/Bots/T2 Bots/legaspy.lua b/units/Legion/Bots/T2 Bots/legaspy.lua index cd22128d6aa..92c6a2d4589 100644 --- a/units/Legion/Bots/T2 Bots/legaspy.lua +++ b/units/Legion/Bots/T2 Bots/legaspy.lua @@ -50,7 +50,7 @@ return { model_author = "ZephyrSkies (model), Phill-Arts (Concept Art)", normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0, - subfolder = "Legion/Bots/T2", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, unitgroup = "buildert2", }, diff --git a/units/Legion/Bots/T2 Bots/legdecom.lua b/units/Legion/Bots/T2 Bots/legdecom.lua index a7fe2b1347e..6eff9e5d64e 100644 --- a/units/Legion/Bots/T2 Bots/legdecom.lua +++ b/units/Legion/Bots/T2 Bots/legdecom.lua @@ -80,7 +80,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "CorBots/T2", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, isdecoycommander = true, }, diff --git a/units/Legion/Bots/T2 Bots/leghrk.lua b/units/Legion/Bots/T2 Bots/leghrk.lua index 08a076bec4b..8ecf8e277dc 100644 --- a/units/Legion/Bots/T2 Bots/leghrk.lua +++ b/units/Legion/Bots/T2 Bots/leghrk.lua @@ -35,7 +35,7 @@ return { customparams = { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "legion/bots/T2 Bots", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, unitgroup = "weapon", }, diff --git a/units/Legion/Bots/T2 Bots/leginc.lua b/units/Legion/Bots/T2 Bots/leginc.lua index 8ab2e3d2029..d35a95b379c 100644 --- a/units/Legion/Bots/T2 Bots/leginc.lua +++ b/units/Legion/Bots/T2 Bots/leginc.lua @@ -36,7 +36,7 @@ return { model_author = "Protar, Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 1, - subfolder = "CorBots/T2", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Bots/T2 Bots/leginfestor.lua b/units/Legion/Bots/T2 Bots/leginfestor.lua index dccd8ebb551..0330eb8e28f 100644 --- a/units/Legion/Bots/T2 Bots/leginfestor.lua +++ b/units/Legion/Bots/T2 Bots/leginfestor.lua @@ -47,7 +47,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.2, selectable_as_combat_unit = true, - subfolder = "CorBots/T2", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, selectionscalemult = 1, }, diff --git a/units/Legion/Bots/T2 Bots/legsnapper.lua b/units/Legion/Bots/T2 Bots/legsnapper.lua index 3294a249d37..24a0cd32d2f 100644 --- a/units/Legion/Bots/T2 Bots/legsnapper.lua +++ b/units/Legion/Bots/T2 Bots/legsnapper.lua @@ -35,7 +35,7 @@ return { unitgroup = "explo", model_author = "Hornet", normaltex = "unittextures/leg_normal.dds", - subfolder = "Legion/bots/t2 bots", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, instantselfd = true, }, diff --git a/units/Legion/Bots/T2 Bots/legsrail.lua b/units/Legion/Bots/T2 Bots/legsrail.lua index 40df86c80c7..ef6d79dea3c 100644 --- a/units/Legion/Bots/T2 Bots/legsrail.lua +++ b/units/Legion/Bots/T2 Bots/legsrail.lua @@ -36,7 +36,7 @@ return { unitgroup = "weapon", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "Legion/gantry", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Bots/T2 Bots/legstr.lua b/units/Legion/Bots/T2 Bots/legstr.lua index cad069e5382..b597563894d 100644 --- a/units/Legion/Bots/T2 Bots/legstr.lua +++ b/units/Legion/Bots/T2 Bots/legstr.lua @@ -35,7 +35,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 3, - subfolder = "ArmGantry", + subfolder = "Legion/Bots/T2 Bots", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Bots/legbal.lua b/units/Legion/Bots/legbal.lua index 7ce2031c9ba..f266d1765e8 100644 --- a/units/Legion/Bots/legbal.lua +++ b/units/Legion/Bots/legbal.lua @@ -35,7 +35,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "CorBots", + subfolder = "Legion/Bots", }, featuredefs = { dead = { diff --git a/units/Legion/Bots/legcen.lua b/units/Legion/Bots/legcen.lua index c05059a76bd..bf6e381d38c 100644 --- a/units/Legion/Bots/legcen.lua +++ b/units/Legion/Bots/legcen.lua @@ -36,7 +36,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 3, - subfolder = "ArmBots", + subfolder = "Legion/Bots", }, featuredefs = { dead = { diff --git a/units/Legion/Bots/leggob.lua b/units/Legion/Bots/leggob.lua index 33c2a7dc3e0..6410db4ec31 100644 --- a/units/Legion/Bots/leggob.lua +++ b/units/Legion/Bots/leggob.lua @@ -36,7 +36,7 @@ return { normaltex = "unittextures/leg_normal.dds", reaimtime = 2, stompable = true, - subfolder = "CorBots", + subfolder = "Legion/Bots", }, featuredefs = { dead = { diff --git a/units/Legion/Bots/leglob.lua b/units/Legion/Bots/leglob.lua index 708d6e41fa0..c59934d12c1 100644 --- a/units/Legion/Bots/leglob.lua +++ b/units/Legion/Bots/leglob.lua @@ -37,7 +37,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "CorBots", + subfolder = "Legion/Bots", }, featuredefs = { dead = { diff --git a/units/Legion/Constructors/legaceb.lua b/units/Legion/Constructors/legaceb.lua index ce65db6be2d..36f35346b09 100644 --- a/units/Legion/Constructors/legaceb.lua +++ b/units/Legion/Constructors/legaceb.lua @@ -60,7 +60,7 @@ return { unitgroup = "buildert2", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "Legion/constructors", + subfolder = "Legion/Constructors", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Constructors/legack.lua b/units/Legion/Constructors/legack.lua index 9852abea755..a24b2ac9f88 100644 --- a/units/Legion/Constructors/legack.lua +++ b/units/Legion/Constructors/legack.lua @@ -71,7 +71,7 @@ return { area_mex_def = "legmoho", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorBots/T2", + subfolder = "Legion/Constructors", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Constructors/legca.lua b/units/Legion/Constructors/legca.lua index 64229f3a5ce..1e136154367 100644 --- a/units/Legion/Constructors/legca.lua +++ b/units/Legion/Constructors/legca.lua @@ -69,7 +69,7 @@ return { unitgroup = "builder", model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorAircraft", + subfolder = "Legion/Constructors", }, sounds = { build = "nanlath2", diff --git a/units/Legion/Constructors/legch.lua b/units/Legion/Constructors/legch.lua index af18f5a2259..af476721b36 100644 --- a/units/Legion/Constructors/legch.lua +++ b/units/Legion/Constructors/legch.lua @@ -86,7 +86,7 @@ return { unitgroup = "builder", model_author = "EnderRobo", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorHovercraft", + subfolder = "Legion/Constructors", }, featuredefs = { dead = { diff --git a/units/Legion/Constructors/legck.lua b/units/Legion/Constructors/legck.lua index 2728713378f..2ffcc1ea114 100644 --- a/units/Legion/Constructors/legck.lua +++ b/units/Legion/Constructors/legck.lua @@ -70,7 +70,7 @@ return { unitgroup = "builder", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorBots", + subfolder = "Legion/Constructors", }, featuredefs = { dead = { diff --git a/units/Legion/Constructors/legcv.lua b/units/Legion/Constructors/legcv.lua index 86fe918dfd1..b67f602761f 100644 --- a/units/Legion/Constructors/legcv.lua +++ b/units/Legion/Constructors/legcv.lua @@ -75,7 +75,7 @@ return { unitgroup = "builder", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorVehicles", + subfolder = "Legion/Constructors", }, featuredefs = { dead = { diff --git a/units/Legion/Constructors/leghack.lua b/units/Legion/Constructors/leghack.lua index 107e79ae8ce..0a2fc1ab88d 100644 --- a/units/Legion/Constructors/leghack.lua +++ b/units/Legion/Constructors/leghack.lua @@ -73,7 +73,7 @@ return { unitgroup = "buildert2", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "Legion/constructors", + subfolder = "Legion/Constructors", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Constructors/legotter.lua b/units/Legion/Constructors/legotter.lua index 74dc1503e14..d5f02ff5295 100644 --- a/units/Legion/Constructors/legotter.lua +++ b/units/Legion/Constructors/legotter.lua @@ -88,7 +88,7 @@ return { unitgroup = "builder", model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorVehicles", + subfolder = "Legion/Constructors", }, featuredefs = { dead = { diff --git a/units/Legion/Defenses/legabm.lua b/units/Legion/Defenses/legabm.lua index 405141e8f97..41bfa73a9cb 100644 --- a/units/Legion/Defenses/legabm.lua +++ b/units/Legion/Defenses/legabm.lua @@ -38,7 +38,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/LandDefenceOffence", + subfolder = "Legion/Defenses", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Defenses/legbastion.lua b/units/Legion/Defenses/legbastion.lua index ef5a96662d0..0afbefa55dc 100644 --- a/units/Legion/Defenses/legbastion.lua +++ b/units/Legion/Defenses/legbastion.lua @@ -45,7 +45,7 @@ return { normaltex = "unittextures/leg_normal.dds", reaimtime = 5, removewait = true, - subfolder = "CorBuildings/LandDefenceOffence", + subfolder = "Legion/Defenses", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Defenses/legbombard.lua b/units/Legion/Defenses/legbombard.lua index 7526376c889..57e5e6a1473 100644 --- a/units/Legion/Defenses/legbombard.lua +++ b/units/Legion/Defenses/legbombard.lua @@ -35,7 +35,7 @@ return { model_author = "Hornet", normaltex = "unittextures/cor_normal.dds", removewait = true, - subfolder = "ArmBuildings/LandDefenceOffence", + subfolder = "Legion/Defenses", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Defenses/legcluster.lua b/units/Legion/Defenses/legcluster.lua index 7314067cebc..10a1eeebd88 100644 --- a/units/Legion/Defenses/legcluster.lua +++ b/units/Legion/Defenses/legcluster.lua @@ -35,7 +35,7 @@ return { unitgroup = "weapon", model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorBuildings/LandDefenceOffence", + subfolder = "Legion/Defenses", }, featuredefs = { dead = { diff --git a/units/Legion/Defenses/legdrag.lua b/units/Legion/Defenses/legdrag.lua index 493271442b1..c71a044741f 100644 --- a/units/Legion/Defenses/legdrag.lua +++ b/units/Legion/Defenses/legdrag.lua @@ -42,7 +42,8 @@ return { paralyzemultiplier = 0, removestop = true, removewait = true, - subfolder = "CorBuildings/LandUtil", + subfolder = "Legion/Defenses", + unitgroup = "util", }, featuredefs = { rockteeth = { diff --git a/units/Legion/Defenses/legflak.lua b/units/Legion/Defenses/legflak.lua index 72116c8213f..b46417aa11e 100644 --- a/units/Legion/Defenses/legflak.lua +++ b/units/Legion/Defenses/legflak.lua @@ -36,7 +36,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", removewait = true, - subfolder = "Legion/defenses", + subfolder = "Legion/Defenses", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Defenses/legforti.lua b/units/Legion/Defenses/legforti.lua index 0ac0edae81b..af87bd53084 100644 --- a/units/Legion/Defenses/legforti.lua +++ b/units/Legion/Defenses/legforti.lua @@ -43,7 +43,8 @@ return { paralyzemultiplier = 0, removestop = true, removewait = true, - subfolder = "legion/Defenses", + subfolder = "Legion/Defenses", + unitgroup = "util", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Defenses/leghive.lua b/units/Legion/Defenses/leghive.lua index 772c60c4363..861e41bd857 100644 --- a/units/Legion/Defenses/leghive.lua +++ b/units/Legion/Defenses/leghive.lua @@ -39,7 +39,7 @@ return { model_author = "Zephyr", normaltex = "unittextures/leg_normal.dds", removewait = true, - subfolder = "CorBuildings/LandDefenceOffence", + subfolder = "Legion/Defenses", legacyname = "Gaat Gun", inheritxpratemultiplier = 1, childreninheritxp = "DRONE", diff --git a/units/Legion/Defenses/leglht.lua b/units/Legion/Defenses/leglht.lua index 4f68a76f490..b43128a5bf4 100644 --- a/units/Legion/Defenses/leglht.lua +++ b/units/Legion/Defenses/leglht.lua @@ -43,7 +43,7 @@ return { model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", removewait = true, - subfolder = "CorBuildings/LandDefenceOffence", + subfolder = "Legion/Defenses", }, featuredefs = { dead = { diff --git a/units/Legion/Defenses/leglupara.lua b/units/Legion/Defenses/leglupara.lua index 598d620d4aa..2ae3345b04f 100644 --- a/units/Legion/Defenses/leglupara.lua +++ b/units/Legion/Defenses/leglupara.lua @@ -36,7 +36,7 @@ return { model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", removewait = true, - subfolder = "Legion/defenses", + subfolder = "Legion/Defenses", }, featuredefs = { dead = { diff --git a/units/Legion/Defenses/legmg.lua b/units/Legion/Defenses/legmg.lua index 509bb426eb6..6f795c65487 100644 --- a/units/Legion/Defenses/legmg.lua +++ b/units/Legion/Defenses/legmg.lua @@ -37,7 +37,7 @@ return { model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", removewait = true, - subfolder = "ArmBuildings/LandDefenceOffence", + subfolder = "Legion/Defenses", }, featuredefs = { dead = { diff --git a/units/Legion/Defenses/legperdition.lua b/units/Legion/Defenses/legperdition.lua index 514f6d1d551..4c6b1a1e91a 100644 --- a/units/Legion/Defenses/legperdition.lua +++ b/units/Legion/Defenses/legperdition.lua @@ -32,7 +32,7 @@ return { normaltex = "unittextures/leg_normal.dds", removewait = true, selectionscalemult = 1, - subfolder = "CorBuildings/LandDefenceOffence", + subfolder = "Legion/Defenses", techlevel = 2, unitgroup = "weapon", restrictions_inclusion = "_notacnukes_", @@ -123,7 +123,9 @@ return { energypershot = 13000, explosiongenerator = "custom:fire-explosion-large", flamegfxtime = 1, - gravityaffected = true, + gravityaffected = "true", + heightboostfactor = 1, + heightmod = 0, hightrajectory = 1, impulsefactor = 0.123, interceptedbyshieldtype = 0, diff --git a/units/Legion/Defenses/legsilo.lua b/units/Legion/Defenses/legsilo.lua index 305fabff58e..ec4b85e21e5 100644 --- a/units/Legion/Defenses/legsilo.lua +++ b/units/Legion/Defenses/legsilo.lua @@ -34,7 +34,7 @@ return { model_author = "Tharsy", normaltex = "unittextures/leg_normal.dds", removewait = true, - subfolder = "CorBuildings/LandDefenceOffence", + subfolder = "Legion/Defenses", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Economy/legadvsol.lua b/units/Legion/Economy/legadvsol.lua index d39c21ed1f5..590d37800f4 100644 --- a/units/Legion/Economy/legadvsol.lua +++ b/units/Legion/Economy/legadvsol.lua @@ -39,7 +39,7 @@ return { removestop = true, removewait = true, solar = true, - subfolder = "Legion/economy", + subfolder = "Legion/Economy", }, featuredefs = { dead = { diff --git a/units/Legion/Economy/legageo.lua b/units/Legion/Economy/legageo.lua index 665d5eeb6b4..26b70d812ed 100644 --- a/units/Legion/Economy/legageo.lua +++ b/units/Legion/Economy/legageo.lua @@ -40,7 +40,7 @@ return { normaltex = "unittextures/LEG_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/LandEconomy", + subfolder = "Legion/Economy", techlevel = 2, }, sounds = { diff --git a/units/Legion/Economy/legamstor.lua b/units/Legion/Economy/legamstor.lua index d43025663ca..0156054f0d5 100644 --- a/units/Legion/Economy/legamstor.lua +++ b/units/Legion/Economy/legamstor.lua @@ -35,7 +35,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/SeaEconomy", + subfolder = "Legion/Economy", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Economy/legeconv.lua b/units/Legion/Economy/legeconv.lua index 11e4f72ce4b..e73d9d5415a 100644 --- a/units/Legion/Economy/legeconv.lua +++ b/units/Legion/Economy/legeconv.lua @@ -37,7 +37,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "ArmBuildings/LandEconomy", + subfolder = "Legion/Economy", }, sounds = { activate = "arm-bld-mm-activate", diff --git a/units/Legion/Economy/legestor.lua b/units/Legion/Economy/legestor.lua index 531634d950d..30e0241d72b 100644 --- a/units/Legion/Economy/legestor.lua +++ b/units/Legion/Economy/legestor.lua @@ -37,7 +37,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "Legion/economy", + subfolder = "Legion/Economy", }, featuredefs = { dead = { diff --git a/units/Legion/Economy/legmex.lua b/units/Legion/Economy/legmex.lua index 5cd562a990e..1346d4699a9 100644 --- a/units/Legion/Economy/legmex.lua +++ b/units/Legion/Economy/legmex.lua @@ -29,7 +29,7 @@ return { selfdestructas = "smallMex", selfdestructcountdown = 1, sightdistance = 273, - yardmap = "h cbbbbbbc bsbssbsb bbobbobb bsbbbbsb bsbbbbsb bbobbobb bsbssbsb cbbbbbbc", + yardmap = "h cbbbbbbc bsossbsb bbsbbsob bsbbbbsb bsbbbbsb bosbbsbb bsbssosb cbbbbbbc", customparams = { usebuildinggrounddecal = true, buildinggrounddecaltype = "decals/legmex_aoplane.dds", @@ -44,7 +44,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/LandEconomy", + subfolder = "Legion/Economy", }, featuredefs = { dead = { diff --git a/units/Legion/Economy/legmext15.lua b/units/Legion/Economy/legmext15.lua index fa0b03888e5..98d32bcabbe 100644 --- a/units/Legion/Economy/legmext15.lua +++ b/units/Legion/Economy/legmext15.lua @@ -43,7 +43,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/LandEconomy", + subfolder = "Legion/Economy", }, featuredefs = { dead = { diff --git a/units/Legion/Economy/legmoho.lua b/units/Legion/Economy/legmoho.lua index ed2acfa5dfa..a0f301d1ee9 100644 --- a/units/Legion/Economy/legmoho.lua +++ b/units/Legion/Economy/legmoho.lua @@ -43,7 +43,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/LandEconomy", + subfolder = "Legion/Economy", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Economy/legmohobp.lua b/units/Legion/Economy/legmohobp.lua index d183b185489..21d222cd0c7 100644 --- a/units/Legion/Economy/legmohobp.lua +++ b/units/Legion/Economy/legmohobp.lua @@ -6,7 +6,7 @@ return { buildangle = 2048, energycost = 8100, metalcost = 640, - buildpic = "LEGMOHOBP.DDS", + buildpic = "LEGMOHOCON.DDS", buildtime = 14100, builder = true, canrepeat = false, @@ -46,7 +46,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/LandEconomy", + subfolder = "Legion/Economy", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Economy/legmohobpct.lua b/units/Legion/Economy/legmohobpct.lua index 2c7f46e2937..a964ca6f928 100644 --- a/units/Legion/Economy/legmohobpct.lua +++ b/units/Legion/Economy/legmohobpct.lua @@ -4,7 +4,7 @@ return { maxdec = 4.5, energycost = 1, metalcost = 1, - buildpic = "LEGMOHOBP.DDS", + buildpic = "LEGMOHOCON.DDS", buildtime = 10, builddistance = 800, builder = true, @@ -36,7 +36,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.1, - subfolder = "CorBuildings/LandUtil", + subfolder = "Legion/Economy", }, sounds = { canceldestruct = "cancel2", diff --git a/units/Legion/Economy/legmohocon.lua b/units/Legion/Economy/legmohocon.lua index ae181eab19f..4cbe5b593a9 100644 --- a/units/Legion/Economy/legmohocon.lua +++ b/units/Legion/Economy/legmohocon.lua @@ -45,6 +45,7 @@ return { --costs should be same as legmohoconct and legmohoconin metal_extractor = 4, model_author = "Tharsis and Protar", normaltex = "unittextures/leg_normal.dds", + subfolder = "Legion/Economy", removestop = true, removewait = true, scav_swap_override_created = "null", -- (delete = removes the unit, null = cancels swap, unitdefname = overrides what unit are we swapping into) diff --git a/units/Legion/Economy/legmohoconct.lua b/units/Legion/Economy/legmohoconct.lua index 229e5f10dfe..2fdf99a2b11 100644 --- a/units/Legion/Economy/legmohoconct.lua +++ b/units/Legion/Economy/legmohoconct.lua @@ -50,6 +50,7 @@ return { --costs should be same as legmohocon and legmohoconin unitgroup = "builder", model_author = "Tharsis and Protar", normaltex = "unittextures/leg_normal.dds", + subfolder = "Legion/Economy", scav_swap_override_created = "delete", -- (delete = removes the unit, null = cancels swap, unitdefname = overrides what unit are we swapping into) scav_swap_override_captured = "legmohocon", -- (delete = removes the unit, null = cancels swap, unitdefname = overrides what unit are we swapping into) techlevel = 2, diff --git a/units/Legion/Economy/legmohoconin.lua b/units/Legion/Economy/legmohoconin.lua index de24db2bcfc..ddcb7dd8c29 100644 --- a/units/Legion/Economy/legmohoconin.lua +++ b/units/Legion/Economy/legmohoconin.lua @@ -33,13 +33,14 @@ return { --costs should be same as legmohocon and legmohoconct unitgroup = "metal", cvbuildable = true, metal_extractor = 4, + nohealthbars = true, model_author = "Tharsis and Protar", normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, scav_swap_override_created = "delete", -- (delete = removes the unit, null = cancels swap, unitdefname = overrides what unit are we swapping into) scav_swap_override_captured = "delete", -- (delete = removes the unit, null = cancels swap, unitdefname = overrides what unit are we swapping into) - subfolder = "CorBuildings/LandEconomy", + subfolder = "Legion/Economy", techlevel = 2, }, sounds = { diff --git a/units/Legion/Economy/legmstor.lua b/units/Legion/Economy/legmstor.lua index 643f04cd334..d58c548e24e 100644 --- a/units/Legion/Economy/legmstor.lua +++ b/units/Legion/Economy/legmstor.lua @@ -36,7 +36,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/LandEconomy", + subfolder = "Legion/Economy", }, featuredefs = { dead = { diff --git a/units/Legion/Economy/legrampart.lua b/units/Legion/Economy/legrampart.lua index 9067f3337a9..8ab1f80522e 100644 --- a/units/Legion/Economy/legrampart.lua +++ b/units/Legion/Economy/legrampart.lua @@ -48,7 +48,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/LEG_normal.dds", removewait = true, - subfolder = "CorBuildings/LandEconomy", + subfolder = "Legion/Economy", techlevel = 2, inheritxpratemultiplier = 1, childreninheritxp = "DRONE", diff --git a/units/Legion/Economy/legsolar.lua b/units/Legion/Economy/legsolar.lua index 0ebd32fdbfc..d03662f93b6 100644 --- a/units/Legion/Economy/legsolar.lua +++ b/units/Legion/Economy/legsolar.lua @@ -42,7 +42,7 @@ return { removestop = true, removewait = true, solar = true, - subfolder = "Legion/economy", + subfolder = "Legion/Economy", }, featuredefs = { dead = { diff --git a/units/Legion/Economy/legwin.lua b/units/Legion/Economy/legwin.lua index 8afd5e6f31c..2af712a85cf 100644 --- a/units/Legion/Economy/legwin.lua +++ b/units/Legion/Economy/legwin.lua @@ -37,7 +37,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/LandEconomy", + subfolder = "Legion/Economy", }, featuredefs = { dead = { diff --git a/units/Legion/Hovercraft/legah.lua b/units/Legion/Hovercraft/legah.lua index 48fea8957f6..6eeaae6f92d 100644 --- a/units/Legion/Hovercraft/legah.lua +++ b/units/Legion/Hovercraft/legah.lua @@ -38,7 +38,7 @@ return { model_author = "EnderRobo", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "CorHovercraft", + subfolder = "Legion/Hovercraft", }, featuredefs = { dead = { diff --git a/units/Legion/Hovercraft/legcar.lua b/units/Legion/Hovercraft/legcar.lua index ae5cc35c1af..95f9d8145b5 100644 --- a/units/Legion/Hovercraft/legcar.lua +++ b/units/Legion/Hovercraft/legcar.lua @@ -34,7 +34,7 @@ return { unitgroup = "weapon", model_author = "EnderRobo", normaltex = "unittextures/leg_normal.dds", - subfolder = "hovercraft", + subfolder = "Legion/Hovercraft", }, featuredefs = { dead = { diff --git a/units/Legion/Hovercraft/legmh.lua b/units/Legion/Hovercraft/legmh.lua index 7f009f80303..b7cd3a49aab 100644 --- a/units/Legion/Hovercraft/legmh.lua +++ b/units/Legion/Hovercraft/legmh.lua @@ -33,7 +33,7 @@ return { unitgroup = "weapon", model_author = "EnderRobo", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorHovercraft", + subfolder = "Legion/Hovercraft", }, featuredefs = { dead = { diff --git a/units/Legion/Hovercraft/legner.lua b/units/Legion/Hovercraft/legner.lua index 70a9bd4357b..52392cdf96a 100644 --- a/units/Legion/Hovercraft/legner.lua +++ b/units/Legion/Hovercraft/legner.lua @@ -33,7 +33,7 @@ return { unitgroup = "weapon", model_author = "EnderRobo", normaltex = "unittextures/leg_normal.dds", - subfolder = "hovercraft", + subfolder = "Legion/Hovercraft", }, featuredefs = { dead = { diff --git a/units/Legion/Hovercraft/legsh.lua b/units/Legion/Hovercraft/legsh.lua index cdca0d2f877..bc6a08a7ef0 100644 --- a/units/Legion/Hovercraft/legsh.lua +++ b/units/Legion/Hovercraft/legsh.lua @@ -33,7 +33,7 @@ return { unitgroup = "weapon", model_author = "EnderRobo", normaltex = "unittextures/leg_normal.dds", - subfolder = "ArmHovercraft", + subfolder = "Legion/Hovercraft", }, featuredefs = { dead = { diff --git a/units/Legion/Labs/leglab.lua b/units/Legion/Labs/leglab.lua index 747dbab83e4..87e20cd64b4 100644 --- a/units/Legion/Labs/leglab.lua +++ b/units/Legion/Labs/leglab.lua @@ -51,7 +51,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", quickstart_discountable = true, - subfolder = "CorBuildings/LandFactories", + subfolder = "Legion/Labs", }, featuredefs = { dead = { diff --git a/units/Legion/Labs/legsplab.lua b/units/Legion/Labs/legsplab.lua index 796507d9753..7a61110994a 100644 --- a/units/Legion/Labs/legsplab.lua +++ b/units/Legion/Labs/legsplab.lua @@ -58,7 +58,7 @@ return { footprintz = 7, height = 20, metal = 930, - object = "Units/legsplab_dead.s3o", + object = "Units/legsy_dead.s3o", -- placeholder, legsplab has no wreck model reclaimable = true, }, }, diff --git a/units/Legion/Legion EvoCom/legcomlvl10.lua b/units/Legion/Legion EvoCom/legcomlvl10.lua index f9d605b94be..a2c59b3c045 100644 --- a/units/Legion/Legion EvoCom/legcomlvl10.lua +++ b/units/Legion/Legion EvoCom/legcomlvl10.lua @@ -124,7 +124,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Legion EvoCom", tombstone = "legstone", workertimeboost = 6, wtboostunittype = "MOBILE", diff --git a/units/Legion/Legion EvoCom/legcomlvl2.lua b/units/Legion/Legion EvoCom/legcomlvl2.lua index bd6071768e8..002848436a3 100644 --- a/units/Legion/Legion EvoCom/legcomlvl2.lua +++ b/units/Legion/Legion EvoCom/legcomlvl2.lua @@ -100,7 +100,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Legion EvoCom", tombstone = "legstone", evolution_health_transfer = "percentage", evolution_target = "legcomlvl3", diff --git a/units/Legion/Legion EvoCom/legcomlvl3.lua b/units/Legion/Legion EvoCom/legcomlvl3.lua index 6bd8f98c0dd..c316f498773 100644 --- a/units/Legion/Legion EvoCom/legcomlvl3.lua +++ b/units/Legion/Legion EvoCom/legcomlvl3.lua @@ -113,7 +113,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Legion EvoCom", tombstone = "legstone", workertimeboost = 3, wtboostunittype = "MOBILE", diff --git a/units/Legion/Legion EvoCom/legcomlvl4.lua b/units/Legion/Legion EvoCom/legcomlvl4.lua index 7ee7e380510..6320732cf8b 100644 --- a/units/Legion/Legion EvoCom/legcomlvl4.lua +++ b/units/Legion/Legion EvoCom/legcomlvl4.lua @@ -116,7 +116,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Legion EvoCom", tombstone = "legstone", workertimeboost = 3, wtboostunittype = "MOBILE", diff --git a/units/Legion/Legion EvoCom/legcomlvl5.lua b/units/Legion/Legion EvoCom/legcomlvl5.lua index d10040fa904..4da2a60188f 100644 --- a/units/Legion/Legion EvoCom/legcomlvl5.lua +++ b/units/Legion/Legion EvoCom/legcomlvl5.lua @@ -121,7 +121,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Legion EvoCom", tombstone = "legstone", workertimeboost = 4, wtboostunittype = "MOBILE", diff --git a/units/Legion/Legion EvoCom/legcomlvl6.lua b/units/Legion/Legion EvoCom/legcomlvl6.lua index 1d3aa5e1ca3..602bbab92b2 100644 --- a/units/Legion/Legion EvoCom/legcomlvl6.lua +++ b/units/Legion/Legion EvoCom/legcomlvl6.lua @@ -122,7 +122,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Legion EvoCom", tombstone = "legstone", workertimeboost = 4, wtboostunittype = "MOBILE", diff --git a/units/Legion/Legion EvoCom/legcomlvl7.lua b/units/Legion/Legion EvoCom/legcomlvl7.lua index c98a456b69a..9002e8dec37 100644 --- a/units/Legion/Legion EvoCom/legcomlvl7.lua +++ b/units/Legion/Legion EvoCom/legcomlvl7.lua @@ -122,7 +122,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Legion EvoCom", tombstone = "legstone", workertimeboost = 5, wtboostunittype = "MOBILE", diff --git a/units/Legion/Legion EvoCom/legcomlvl8.lua b/units/Legion/Legion EvoCom/legcomlvl8.lua index 4d4f8d9cc51..508638eb053 100644 --- a/units/Legion/Legion EvoCom/legcomlvl8.lua +++ b/units/Legion/Legion EvoCom/legcomlvl8.lua @@ -123,7 +123,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Legion EvoCom", tombstone = "legstone", workertimeboost = 5, wtboostunittype = "MOBILE", diff --git a/units/Legion/Legion EvoCom/legcomlvl9.lua b/units/Legion/Legion EvoCom/legcomlvl9.lua index e8d021f6fff..6ebf941f868 100644 --- a/units/Legion/Legion EvoCom/legcomlvl9.lua +++ b/units/Legion/Legion EvoCom/legcomlvl9.lua @@ -124,7 +124,7 @@ return { normaltex = "unittextures/leg_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Legion EvoCom", tombstone = "legstone", workertimeboost = 6, wtboostunittype = "MOBILE", diff --git a/units/Legion/Other/Commanders/legcomecon.lua b/units/Legion/Other/Commanders/legcomecon.lua index 9542f06d513..90327ade42e 100644 --- a/units/Legion/Other/Commanders/legcomecon.lua +++ b/units/Legion/Other/Commanders/legcomecon.lua @@ -94,7 +94,7 @@ return { model_author = "FireStorm", normaltex = "unittextures/Arm_normal.dds", paralyzemultiplier = 0.025, - subfolder = "", + subfolder = "Legion/Other/Commanders", tombstone = "legstone", }, featuredefs = { diff --git a/units/Legion/Other/Commanders/legcomoff.lua b/units/Legion/Other/Commanders/legcomoff.lua index 5e4387fc4a4..84ad692635b 100644 --- a/units/Legion/Other/Commanders/legcomoff.lua +++ b/units/Legion/Other/Commanders/legcomoff.lua @@ -93,7 +93,7 @@ return { normaltex = "unittextures/Arm_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Other/Commanders", tombstone = "legstone", }, featuredefs = { diff --git a/units/Legion/Other/Commanders/legcomt2com.lua b/units/Legion/Other/Commanders/legcomt2com.lua index 10945227b46..4ff3af3e109 100644 --- a/units/Legion/Other/Commanders/legcomt2com.lua +++ b/units/Legion/Other/Commanders/legcomt2com.lua @@ -96,7 +96,7 @@ return { normaltex = "unittextures/Arm_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Other/Commanders", tombstone = "legstone", }, featuredefs = { diff --git a/units/Legion/Other/Commanders/legcomt2def.lua b/units/Legion/Other/Commanders/legcomt2def.lua index c72556f2f37..5da1b6af9a4 100644 --- a/units/Legion/Other/Commanders/legcomt2def.lua +++ b/units/Legion/Other/Commanders/legcomt2def.lua @@ -97,7 +97,7 @@ return { model_author = "FireStorm", normaltex = "unittextures/Arm_normal.dds", paralyzemultiplier = 0.025, - subfolder = "", + subfolder = "Legion/Other/Commanders", tombstone = "legstone", shield_color_mult = 0.8, shield_power = 1900, diff --git a/units/Legion/Other/Commanders/legcomt2off.lua b/units/Legion/Other/Commanders/legcomt2off.lua index e78515c4483..e07bbfc79be 100644 --- a/units/Legion/Other/Commanders/legcomt2off.lua +++ b/units/Legion/Other/Commanders/legcomt2off.lua @@ -104,7 +104,7 @@ return { normaltex = "unittextures/Arm_normal.dds", paralyzemultiplier = 0.025, reaimtime = 5, - subfolder = "", + subfolder = "Legion/Other/Commanders", tombstone = "legstone", paratrooper = true, }, diff --git a/units/Legion/Other/legvision.lua b/units/Legion/Other/legvision.lua index 92f022cb82a..a800dc2929f 100644 --- a/units/Legion/Other/legvision.lua +++ b/units/Legion/Other/legvision.lua @@ -36,7 +36,7 @@ return { normaltex = "unittextures/cor_normal.dds", removestop = true, removewait = true, - subfolder = "CorBuildings/LandUtil", + subfolder = "Legion/Other", }, sounds = { canceldestruct = "cancel2", diff --git a/units/Legion/SeaDefenses/legfhive.lua b/units/Legion/SeaDefenses/legfhive.lua index fde94e914b0..af78825bb8a 100644 --- a/units/Legion/SeaDefenses/legfhive.lua +++ b/units/Legion/SeaDefenses/legfhive.lua @@ -40,7 +40,7 @@ return { model_author = "Zephyr", --naval edition by / c/o Hornet normaltex = "unittextures/leg_normal.dds", removewait = true, - subfolder = "CorBuildings/LandDefenceOffence", + subfolder = "Legion/SeaDefenses", legacyname = "Gaat Gun", inheritxpratemultiplier = 1, childreninheritxp = "DRONE", diff --git a/units/Legion/SeaPlanes/legsptorpgunship.lua b/units/Legion/SeaPlanes/legsptorpgunship.lua index 7f1a974a114..a7b72e43efa 100644 --- a/units/Legion/SeaPlanes/legsptorpgunship.lua +++ b/units/Legion/SeaPlanes/legsptorpgunship.lua @@ -31,7 +31,7 @@ return { customparams = { model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", - subfolder = "Legion/Seaplanes", + subfolder = "Legion/SeaPlanes", unitgroup = "sub", }, sfxtypes = { diff --git a/units/Legion/SeaUtility/legfrad.lua b/units/Legion/SeaUtility/legfrad.lua index 093153c3e2a..a8f9b686f0f 100644 --- a/units/Legion/SeaUtility/legfrad.lua +++ b/units/Legion/SeaUtility/legfrad.lua @@ -38,7 +38,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "ArmBuildings/SeaUtil", + subfolder = "Legion/SeaUtility", unitgroup = "util", }, featuredefs = { diff --git a/units/Legion/Ships/T2/leganavybattlesub.lua b/units/Legion/Ships/T2/leganavybattlesub.lua index 9e776f4284a..898dc31ab55 100644 --- a/units/Legion/Ships/T2/leganavybattlesub.lua +++ b/units/Legion/Ships/T2/leganavybattlesub.lua @@ -34,7 +34,7 @@ return { customparams = { model_author = "Model by Tharsis, Concept by Chris/Airnac", normaltex = "unittextures/leg_normal.dds", - subfolder = "legion/Navy/T2", + subfolder = "Legion/Ships/T2", techlevel = 2, unitgroup = "sub", }, @@ -105,7 +105,7 @@ return { burnblow = true, burst = 3, burstrate = 0.33, - cegtag = "torpedotrail-small", + cegtag = "torpedotrail-tiny", collidefriendly = false, craterareaofeffect = 0, craterboost = 0, diff --git a/units/Legion/Ships/T2/leganavyheavysub.lua b/units/Legion/Ships/T2/leganavyheavysub.lua index d04f83d5418..3adc3f086d2 100644 --- a/units/Legion/Ships/T2/leganavyheavysub.lua +++ b/units/Legion/Ships/T2/leganavyheavysub.lua @@ -36,7 +36,7 @@ return { customparams = { model_author = "Model by Tharsis, Concept by Chris/Airnac", normaltex = "unittextures/leg_normal.dds", - subfolder = "legion/Navy/T2", + subfolder = "Legion/Ships/T2", techlevel = 2, unitgroup = "sub", }, diff --git a/units/Legion/Ships/T2/leganavymissileship.lua b/units/Legion/Ships/T2/leganavymissileship.lua index c0648d12cbc..c1fe2028778 100644 --- a/units/Legion/Ships/T2/leganavymissileship.lua +++ b/units/Legion/Ships/T2/leganavymissileship.lua @@ -36,7 +36,7 @@ return { maxrange = "1650", model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorShips/T2", + subfolder = "Legion/Ships/T2", techlevel = 2, unitgroup = "weapon", }, diff --git a/units/Legion/T3/leegmech.lua b/units/Legion/T3/leegmech.lua index d58e47624b4..94497b7b3da 100644 --- a/units/Legion/T3/leegmech.lua +++ b/units/Legion/T3/leegmech.lua @@ -39,7 +39,7 @@ return { model_author = "Kremenchuk", normaltex = "unittextures/leegmech_normal.dds", reaimtime = 5, - subfolder = "leggantry", + subfolder = "Legion/T3", techlevel = 3, }, featuredefs = { diff --git a/units/Legion/T3/legbunk.lua b/units/Legion/T3/legbunk.lua index a86046f9a0f..6f45ed57d24 100644 --- a/units/Legion/T3/legbunk.lua +++ b/units/Legion/T3/legbunk.lua @@ -39,7 +39,7 @@ return { model_author = "Johanthan Crimson, Tuerk", normaltex = "unittextures/leg_normal.dds", reaimtime = 3, - subfolder = "leggantry", + subfolder = "Legion/T3", techlevel = 3, }, featuredefs = { diff --git a/units/Legion/T3/legeallterrainmech.lua b/units/Legion/T3/legeallterrainmech.lua index 0b3c5f16ee6..8bfd43a5e03 100644 --- a/units/Legion/T3/legeallterrainmech.lua +++ b/units/Legion/T3/legeallterrainmech.lua @@ -43,7 +43,6 @@ return { inheritxpratemultiplier = 1, childreninheritxp = "DRONE", parentsinheritxp = "DRONE", - restrictions_inclusion = "_noair_", }, featuredefs = { dead = { diff --git a/units/Legion/T3/legerailtank.lua b/units/Legion/T3/legerailtank.lua index f3e0f776e4e..3920aa827b7 100644 --- a/units/Legion/T3/legerailtank.lua +++ b/units/Legion/T3/legerailtank.lua @@ -40,6 +40,7 @@ return { customparams = { unitgroup = "weapon", normaltex = "unittextures/leg_normal.dds", + subfolder = "Legion/T3", paralyzemultiplier = 0.5, model_author = "ZephyrSkies", reaimtime = 9, diff --git a/units/Legion/T3/legeshotgunmech.lua b/units/Legion/T3/legeshotgunmech.lua index 0df23345ce8..8587eb844d2 100644 --- a/units/Legion/T3/legeshotgunmech.lua +++ b/units/Legion/T3/legeshotgunmech.lua @@ -38,7 +38,7 @@ return { model_author = "Ghoulish & ZephyrSkies", normaltex = "unittextures/leg_normal.dds", reaimtime = 3, - subfolder = "leggantry", + subfolder = "Legion/T3", techlevel = 3, }, featuredefs = { diff --git a/units/Legion/T3/legkeres.lua b/units/Legion/T3/legkeres.lua index 79921042515..6ac4f7175ca 100644 --- a/units/Legion/T3/legkeres.lua +++ b/units/Legion/T3/legkeres.lua @@ -40,6 +40,7 @@ return { customparams = { unitgroup = "weapon", normaltex = "unittextures/leg_normal.dds", + subfolder = "Legion/T3", paralyzemultiplier = 0.5, model_author = "EnderRobo", reaimtime = 5, diff --git a/units/Legion/Utilities/legdeflector.lua b/units/Legion/Utilities/legdeflector.lua index 19d74845f1f..c1e8f0e4d68 100644 --- a/units/Legion/Utilities/legdeflector.lua +++ b/units/Legion/Utilities/legdeflector.lua @@ -43,7 +43,7 @@ return { shield_color_mult = 0.8, shield_power = 6175, shield_radius = 550, - subfolder = "CorBuildings/LandUtil", + subfolder = "Legion/Utilities", techlevel = 2, unitgroup = "util", usebuildinggrounddecal = true, diff --git a/units/Legion/Utilities/legeyes.lua b/units/Legion/Utilities/legeyes.lua index be956f73412..76fe735eb5b 100644 --- a/units/Legion/Utilities/legeyes.lua +++ b/units/Legion/Utilities/legeyes.lua @@ -37,7 +37,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "Legion/utilities", + subfolder = "Legion/Utilities", }, featuredefs = { heap = { diff --git a/units/Legion/Utilities/legjam.lua b/units/Legion/Utilities/legjam.lua index 15606746ecb..68ff85351ea 100644 --- a/units/Legion/Utilities/legjam.lua +++ b/units/Legion/Utilities/legjam.lua @@ -41,7 +41,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "Legion/utilities", + subfolder = "Legion/Utilities", }, featuredefs = { dead = { diff --git a/units/Legion/Utilities/legnanotc.lua b/units/Legion/Utilities/legnanotc.lua index 92a3d40f042..ddf405b98de 100644 --- a/units/Legion/Utilities/legnanotc.lua +++ b/units/Legion/Utilities/legnanotc.lua @@ -47,7 +47,7 @@ return { unitgroup = "builder", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorBuildings/LandUtil", + subfolder = "Legion/Utilities", }, sounds = { build = "nanlath2", diff --git a/units/Legion/Utilities/legnanotcplat.lua b/units/Legion/Utilities/legnanotcplat.lua index d26b7402e72..f20f506d7c4 100644 --- a/units/Legion/Utilities/legnanotcplat.lua +++ b/units/Legion/Utilities/legnanotcplat.lua @@ -50,7 +50,7 @@ return { unitgroup = "builder", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorBuildings/SeaUtil", + subfolder = "Legion/Utilities", }, sounds = { build = "nanlath2", diff --git a/units/Legion/Utilities/legnanotct2.lua b/units/Legion/Utilities/legnanotct2.lua index 176df15911c..360ab4faf12 100644 --- a/units/Legion/Utilities/legnanotct2.lua +++ b/units/Legion/Utilities/legnanotct2.lua @@ -47,7 +47,7 @@ return { unitgroup = "builder", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorBuildings/LandUtil", + subfolder = "Legion/Utilities", techlevel = 2, }, sounds = { diff --git a/units/Legion/Utilities/legnanotct2plat.lua b/units/Legion/Utilities/legnanotct2plat.lua index 14f1820cebf..198828ff1ca 100644 --- a/units/Legion/Utilities/legnanotct2plat.lua +++ b/units/Legion/Utilities/legnanotct2plat.lua @@ -50,7 +50,7 @@ return { unitgroup = "builder", model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", - subfolder = "CorBuildings/LandUtil", + subfolder = "Legion/Utilities", techlevel = 2, }, sounds = { diff --git a/units/Legion/Utilities/legrad.lua b/units/Legion/Utilities/legrad.lua index 72e0b75f7a4..ca92468a23d 100644 --- a/units/Legion/Utilities/legrad.lua +++ b/units/Legion/Utilities/legrad.lua @@ -44,7 +44,7 @@ return { normaltex = "unittextures/leg_normal.dds", removestop = true, removewait = true, - subfolder = "Legion/utilities", + subfolder = "Legion/Utilities", }, featuredefs = { dead = { diff --git a/units/Legion/Vehicles/T2 Vehicles/legaheattank.lua b/units/Legion/Vehicles/T2 Vehicles/legaheattank.lua index bbc99a108bd..0b6a5dfd255 100644 --- a/units/Legion/Vehicles/T2 Vehicles/legaheattank.lua +++ b/units/Legion/Vehicles/T2 Vehicles/legaheattank.lua @@ -129,9 +129,9 @@ return { reloadtime = 0.033, rgbcolor = "1 0.5 0", rgbcolor2 = "0.8 1.0 0.3", - soundhitdry = "", + soundhitdry = "flamhit1", soundhitwet = "sizzle", - soundstart = "heatray3", + soundstart = "heatray3burn", soundtrigger = 1, tolerance = 5000, thickness = 4.0, diff --git a/units/Legion/Vehicles/T2 Vehicles/legamcluster.lua b/units/Legion/Vehicles/T2 Vehicles/legamcluster.lua index 86d1155a9bf..6dc9c4b4662 100644 --- a/units/Legion/Vehicles/T2 Vehicles/legamcluster.lua +++ b/units/Legion/Vehicles/T2 Vehicles/legamcluster.lua @@ -44,7 +44,7 @@ return { kickback = "-6", model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", - subfolder = "Legion/Vehicles", + subfolder = "Legion/Vehicles/T2 Vehicles", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Vehicles/T2 Vehicles/legavroc.lua b/units/Legion/Vehicles/T2 Vehicles/legavroc.lua index f9d02e9b117..b00c93ffa29 100644 --- a/units/Legion/Vehicles/T2 Vehicles/legavroc.lua +++ b/units/Legion/Vehicles/T2 Vehicles/legavroc.lua @@ -40,7 +40,7 @@ return { unitgroup = "weapon", model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", - subfolder = "ArmVehicles/T2", + subfolder = "Legion/Vehicles/T2 Vehicles", techlevel = 2, }, featuredefs = { diff --git a/units/Legion/Vehicles/T2 Vehicles/legfloat.lua b/units/Legion/Vehicles/T2 Vehicles/legfloat.lua index bb54dfe285f..555bf37c3ce 100644 --- a/units/Legion/Vehicles/T2 Vehicles/legfloat.lua +++ b/units/Legion/Vehicles/T2 Vehicles/legfloat.lua @@ -42,7 +42,8 @@ return { model_author = "EnderRobo", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "legvehicles/T2", + subfolder = "Legion/Vehicles/T2 Vehicles", + unitgroup = "weapon", techlevel = 2, restrictions_exclusion = "_nosea_", speedfactorinwater = 1.3, diff --git a/units/Legion/Vehicles/T2 Vehicles/legmed.lua b/units/Legion/Vehicles/T2 Vehicles/legmed.lua index 8b37af6835b..7827914aaa9 100644 --- a/units/Legion/Vehicles/T2 Vehicles/legmed.lua +++ b/units/Legion/Vehicles/T2 Vehicles/legmed.lua @@ -38,6 +38,7 @@ return { unitgroup = "weapon", model_author = "ZephyrSkies, EnderRobo", normaltex = "unittextures/leg_normal.dds", + subfolder = "Legion/Vehicles/T2 Vehicles", reaimtime = 5, techlevel = 2, }, diff --git a/units/Legion/Vehicles/T2 Vehicles/legmrv.lua b/units/Legion/Vehicles/T2 Vehicles/legmrv.lua index 777cc63473f..6271e24ad1a 100644 --- a/units/Legion/Vehicles/T2 Vehicles/legmrv.lua +++ b/units/Legion/Vehicles/T2 Vehicles/legmrv.lua @@ -42,7 +42,7 @@ return { model_author = "ZephyrSkies", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "CorVehicles/T2", + subfolder = "Legion/Vehicles/T2 Vehicles", techlevel = 2, weapon1turretx = 200, weapon1turrety = 400, diff --git a/units/Legion/Vehicles/legbar.lua b/units/Legion/Vehicles/legbar.lua index 71e5e4896f9..2e302d813c1 100644 --- a/units/Legion/Vehicles/legbar.lua +++ b/units/Legion/Vehicles/legbar.lua @@ -48,7 +48,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "CorVehicles/T2", + subfolder = "Legion/Vehicles", }, featuredefs = { dead = { diff --git a/units/Legion/Vehicles/leghades.lua b/units/Legion/Vehicles/leghades.lua index 204b66cbad2..8f50cf18039 100644 --- a/units/Legion/Vehicles/leghades.lua +++ b/units/Legion/Vehicles/leghades.lua @@ -43,7 +43,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "ArmVehicles", + subfolder = "Legion/Vehicles", }, featuredefs = { dead = { @@ -107,6 +107,8 @@ return { areaofeffect = 16, avoidfeature = false, projectiles = 5, + numbounce = 1, + groundbounce = true, burnblow = false, craterareaofeffect = 0, craterboost = 0, diff --git a/units/Legion/Vehicles/leghelios.lua b/units/Legion/Vehicles/leghelios.lua index e05d4873fa0..805373f10c6 100644 --- a/units/Legion/Vehicles/leghelios.lua +++ b/units/Legion/Vehicles/leghelios.lua @@ -49,7 +49,7 @@ return { restoretime = "3000", rockstrength = "0", sleevename = "turret", - subfolder = "CorVehicles", + subfolder = "Legion/Vehicles", turretname = "turret", wpn1turretx = 192.5, wpn1turrety = 192.5, diff --git a/units/Legion/Vehicles/legrail.lua b/units/Legion/Vehicles/legrail.lua index b9db8ad050d..79e0da2029c 100644 --- a/units/Legion/Vehicles/legrail.lua +++ b/units/Legion/Vehicles/legrail.lua @@ -42,7 +42,7 @@ return { model_author = "Tharsis", normaltex = "unittextures/leg_normal.dds", reaimtime = 5, - subfolder = "ArmVehicles", + subfolder = "Legion/Vehicles", }, featuredefs = { dead = { diff --git a/units/Legion/Vehicles/legscout.lua b/units/Legion/Vehicles/legscout.lua index 541ed1e2f3e..e2c07891317 100644 --- a/units/Legion/Vehicles/legscout.lua +++ b/units/Legion/Vehicles/legscout.lua @@ -46,7 +46,7 @@ return { lumamult = "1.3", model_author = "Flaka", normaltex = "unittextures/leg_normal.dds", - subfolder = "Legion", + subfolder = "Legion/Vehicles", }, featuredefs = { dead = { diff --git a/units/Legion/legcom.lua b/units/Legion/legcom.lua index e942473d50a..32904b8013e 100644 --- a/units/Legion/legcom.lua +++ b/units/Legion/legcom.lua @@ -98,7 +98,7 @@ return { paralyzemultiplier = 0, reaimtime = 5, spawnpad_unit = "legnanotcbase", - subfolder = "", + subfolder = "Legion", tombstone = "legstone", }, featuredefs = {