Update TEG NanoUI to TGUI, rename TEG to Stirling engine (#21933)

As title. TEGs are basically just thermopiles IRL; a Stirling engine
more accurately reflects its functionality.
<img width="551" height="303" alt="image"
src="https://github.com/user-attachments/assets/eb0f0c73-c1cc-4a04-85bc-13a413d8ac48"
/>

---------

Signed-off-by: Batrachophreno <Batrochophreno@gmail.com>
Co-authored-by: Cody Brittain <1779662+Generalcamo@users.noreply.github.com>
This commit is contained in:
Batrachophreno
2026-03-02 09:41:29 +00:00
committed by GitHub
co-authored by Cody Brittain
parent 04d2cdd020
commit eb6c7ff844
7 changed files with 243 additions and 120 deletions
@@ -21,6 +21,8 @@
var/volume_capacity_used = 0
var/stored_energy = 0
var/temperature_overlay
/// When passing its status to the main Stirling generator, this informs labeling on the TGUI.
var/is_hot_loop = FALSE
density = TRUE
+40 -41
View File
@@ -1,6 +1,6 @@
/obj/machinery/power/generator
name = "thermoelectric generator"
desc = "It's a high efficiency thermoelectric generator."
name = "\improper Stirling engine"
desc = "It's a high efficiency Stirling engine. This model produces electricity from the temperature and differential between two gas loops."
icon_state = "teg-unassembled"
density = TRUE
anchored = FALSE
@@ -44,11 +44,13 @@
circ2 = null
return ..()
//generators connect in dir and reverse_dir(dir) directions
//mnemonic to determine circulator/generator directions: the cirulators orbit clockwise around the generator
//so a circulator to the NORTH of the generator connects first to the EAST, then to the WEST
//and a circulator to the WEST of the generator connects first to the NORTH, then to the SOUTH
//note that the circulator's outlet dir is it's always facing dir, and it's inlet is always the reverse
/**
* Circulators connect in dir and reverse_dir(dir) directions
* A mnemonic to determine circulator/generator directions: the cirulators orbit clockwise around the generator.
* So a circulator to the NORTH of the generator connects first to the EAST, then to the WEST,
* and a circulator to the WEST of the generator connects first to the NORTH, then to the SOUTH.
* Note that the circulator's outlet dir is it's always facing dir, and it's inlet is always the reverse
*/
/obj/machinery/power/generator/proc/reconnect()
if(circ1)
circ1.temperature_overlay = null
@@ -78,11 +80,11 @@
/obj/machinery/power/generator/update_icon()
icon_state = anchored ? "teg-assembled" : "teg-unassembled"
ClearOverlays()
if (circ1)
if(circ1)
circ1.temperature_overlay = null
if (circ2)
if(circ2)
circ2.temperature_overlay = null
if (stat & (NOPOWER|BROKEN))
if(stat & (NOPOWER|BROKEN))
return TRUE
else
if (lastgenlev != 0)
@@ -91,10 +93,14 @@
var/extreme = (lastgenlev > 9) ? "ex" : ""
if (circ1.last_temperature < circ2.last_temperature)
circ1.temperature_overlay = "circ-[extreme]cold"
circ1.is_hot_loop = FALSE
circ2.temperature_overlay = "circ-[extreme]hot"
circ2.is_hot_loop = TRUE
else
circ1.temperature_overlay = "circ-[extreme]hot"
circ1.is_hot_loop = TRUE
circ2.temperature_overlay = "circ-[extreme]cold"
circ2.is_hot_loop = FALSE
return TRUE
/obj/machinery/power/generator/process()
@@ -102,8 +108,6 @@
stored_energy = 0
return
updateDialog()
var/datum/gas_mixture/air1 = circ1.return_transfer_air()
var/datum/gas_mixture/air2 = circ2.return_transfer_air()
@@ -174,6 +178,7 @@
if(attacking_item.tool_behaviour == TOOL_WRENCH)
attacking_item.play_tool_sound(get_turf(src), 75)
anchored = !anchored
balloon_alert(user, "[anchored ? "secure" : "unsecure"]")
user.visible_message("[user.name] [anchored ? "secures" : "unsecures"] the bolts holding [src.name] to the floor.", \
"You [anchored ? "secure" : "unsecure"] the bolts holding [src] to the floor.", \
"You hear a ratchet")
@@ -193,20 +198,27 @@
reconnect()
ui_interact(user)
/obj/machinery/power/generator/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1)
// this is the data which will be sent to the ui
var/vertical = 0
if (dir == NORTH || dir == SOUTH)
vertical = 1
/obj/machinery/power/generator/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
// Interface ID should match your tgui route registration/name.
ui = new(user, src, "StirlingEngine", "Stirling Engine")
ui.open()
var/data[0]
data["totalOutput"] = effective_gen/1000
data["maxTotalOutput"] = max_power/1000
data["thermalOutput"] = last_thermal_gen/1000
data["circConnected"] = 0
/obj/machinery/power/generator/ui_data(mob/user)
var/list/data = list()
var/vertical = FALSE
if(dir == NORTH || dir == SOUTH)
vertical = TRUE
data["totalOutput"] = effective_gen / 1000
data["maxTotalOutput"] = max_power / 1000
data["thermalOutput"] = last_thermal_gen / 1000
data["circConnected"] = FALSE
if(circ1)
//The one on the left (or top)
// The one on the left (or top)
data["primaryDir"] = vertical ? "top" : "left"
data["primaryOutput"] = last_circ1_gen/1000
data["primaryFlowCapacity"] = circ1.volume_capacity_used*100
@@ -214,9 +226,10 @@
data["primaryInletTemperature"] = circ1.air1.temperature
data["primaryOutletPressure"] = XGM_PRESSURE(circ1.air2)
data["primaryOutletTemperature"] = circ1.air2.temperature
data["primaryIsHot"] = circ1.is_hot_loop
if(circ2)
//Now for the one on the right (or bottom)
// The one on the right (or bottom)
data["secondaryDir"] = vertical ? "bottom" : "right"
data["secondaryOutput"] = last_circ2_gen/1000
data["secondaryFlowCapacity"] = circ2.volume_capacity_used*100
@@ -224,25 +237,11 @@
data["secondaryInletTemperature"] = circ2.air1.temperature
data["secondaryOutletPressure"] = XGM_PRESSURE(circ2.air2)
data["secondaryOutletTemperature"] = circ2.air2.temperature
data["secondaryIsHot"] = circ2.is_hot_loop
if(circ1 && circ2)
data["circConnected"] = 1
else
data["circConnected"] = 0
data["circConnected"] = (circ1 && circ2) ? TRUE : FALSE
// update the ui if it exists, returns null if no ui is passed/found
ui = SSnanoui.try_update_ui(user, src, ui_key, ui, data, force_open)
if(!ui)
// the ui does not exist, so we'll create a new() one
// for a list of parameters and their descriptions see the code docs in \code\modules\nano\nanoui.dm
ui = new(user, src, ui_key, "generator.tmpl", "Thermoelectric Generator", 450, 500)
// when the ui is first opened this is the data it will use
ui.set_initial_data(data)
// open the new ui window
ui.open()
// auto update every Master Controller tick
ui.set_auto_update(1)
return data
/obj/machinery/power/generator/power_change()
..()
+14
View File
@@ -0,0 +1,14 @@
# Your name.
author: Batrachophrenoboocosmomachia
# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
delete-after: True
# Any changes you've made. See valid prefix list above.
# INDENT WITH TWO SPACES. NOT TABS. SPACES.
# SCREW THIS UP AND IT WON'T WORK.
# Also, this gets changed to [] after reading. Just remove the brackets when you add new shit.
# Please surround your changes in double quotes ("). It works without them, but if you use certain characters it screws up compiling. The quotes will not show up in the changelog.
changes:
- spellcheck: "Renames TEGs to Stirling engines to more accurately reflect their functionality."
- refactor: "Refactor of interface for TEG/Stirling Engines to use TGUI instead of NanoUI."
-19
View File
@@ -1,19 +0,0 @@
<!--
Title: Jukebox UI
Used In File(s): \code\game\machinery\jukebox.dm
-->
<H3><span class="white">Current track:</span> <span class="average">{{:data.current_track}}</span></H3>
<div>
{{:helper.link('Play' , 'play', {'play' : 1}, data.playing == 1 ? 'disabled' : null, null)}}
{{:helper.link('Stop' , 'stop', {'stop' : 1}, data.playing == 0 ? 'disabled' : null, null)}}
</div>
<H3><span class="white">Available tracks:</span></H3>
<div class="itemContent">
{{for data.tracks}}
<div class="item">
{{:helper.link( value.track, 'gear', {'change_track' : 1, 'title' : value.track}, value.track == data.current_track ? 'disabled' : null, null)}}
</div>
{{/for}}
</div>
-24
View File
@@ -1,24 +0,0 @@
The current alert level is:
{{if data.alertLevel == 'green'}}
<span class='good'>Green</span>
{{/if}}
{{if data.alertLevel == 'blue'}}
<span style='color: rgb(13, 186, 212);font-weight: bold;'>Blue</span>
{{/if}}
{{if data.alertLevel == 'red'}}
<span class='bad'>Red</span>
{{/if}}
{{if data.alertLevel == 'delta'}}
<span class='bad'>DELTA</span>
{{/if}}
<hr/>
<h3>State</h3>
{{:helper.link('No Party :(', '', {'state': 'inactive'}, data.active ? null : 'selected')}}
{{:helper.link('PARTY!!!', '', {'state': 'active'}, data.active ? 'selected' : null)}}
<h3>Timed Lockdown</h3>
{{:helper.link('Set', '', {'tmr' : 'set'})}}
{{if data.timing}}
{{:helper.link('Stop', '', {'tmr' : 'stop'})}}
{{/if}}
{{if !(data.timing)}}{{:helper.link('Start', '', {'tmr' : 'start'})}}{{/if}}<br/>
{{if data.time}}Time Left: {{:data.time}} seconds.{{/if}}
@@ -1,36 +0,0 @@
<div class="item" style="padding-top: 10px">
<div class="item">
<div class="itemLabel">
Chamber Pressure:
</div>
<div class="itemContent">
{{:helper.displayBar(data.chamber_pressure, 0, 200, (data.chamber_pressure < 80) || (data.chamber_pressure > 120) ? 'bad' : (data.chamber_pressure < 95) || (data.chamber_pressure > 110) ? 'average' : 'good')}}
<div class="statusValue">
{{:data.chamber_pressure}} kPa
</div>
</div>
</div>
</div>
<div class="item" style="padding-top: 10px">
<div class="item">
<div class="itemContent" style="width: 100%">
{{:helper.link('Cycle to Exterior', 'arrowthickstop-1-w', {'command' : 'cycle_ext'}, data.processing ? 'disabled' : null)}}
{{:helper.link('Cycle to Interior', 'arrowthickstop-1-e', {'command' : 'cycle_int'}, data.processing ? 'disabled' : null)}}
</div>
<div class="itemContent" style="padding-top: 2px; width: 100%">
{{if data.interior_status.state == "open"}}
{{:helper.link('Force exterior door', 'alert', {'command' : 'force_ext'}, null, 'redButton')}}
{{else}}
{{:helper.link('Force exterior door', 'alert', {'command' : 'force_ext'}, null, data.processing ? 'yellowButton' : null)}}
{{/if}}
{{if data.exterior_status.state == "open"}}
{{:helper.link('Force interior door', 'alert', {'command' : 'force_int'}, null, 'redButton')}}
{{else}}
{{:helper.link('Force interior door', 'alert', {'command' : 'force_int'}, null, data.processing ? 'yellowButton' : null)}}
{{/if}}
</div>
</div>
<div class="item" style="padding-top: 10px; width: 100%">
{{:helper.link('Abort', 'cancel', {'command' : 'abort'}, data.processing ? null : 'disabled', data.processing ? 'redButton' : null)}}
</div>
</div>
@@ -0,0 +1,187 @@
import {
Box,
LabeledList,
NoticeBox,
ProgressBar,
Section,
Stack,
} from '../components';
import { useBackend } from '../backend';
import { round } from 'common/math';
import { Window } from '../layouts';
import { BooleanLike } from '../../common/react';
type Data = {
totalOutput: number;
maxTotalOutput: number;
thermalOutput: number;
circConnected: BooleanLike;
isHot: BooleanLike;
primaryDir?: string;
primaryOutput?: number;
primaryFlowCapacity?: number;
primaryInletPressure?: number;
primaryInletTemperature?: number;
primaryOutletPressure?: number;
primaryOutletTemperature?: number;
primaryIsHot?: BooleanLike;
secondaryDir?: string;
secondaryOutput?: number;
secondaryFlowCapacity?: number;
secondaryInletPressure?: number;
secondaryInletTemperature?: number;
secondaryOutletPressure?: number;
secondaryOutletTemperature?: number;
secondaryIsHot?: BooleanLike;
};
// Round numbers to one decimal place
const f1 = (n?: number) => round(n ?? 0, 0.1);
export const StirlingEngine = (props, context) => {
const { data } = useBackend<Data>(context);
const {
totalOutput,
maxTotalOutput,
thermalOutput,
circConnected,
primaryDir,
primaryOutput,
primaryFlowCapacity,
primaryInletPressure,
primaryInletTemperature,
primaryOutletPressure,
primaryOutletTemperature,
primaryIsHot,
secondaryDir,
secondaryOutput,
secondaryFlowCapacity,
secondaryInletPressure,
secondaryInletTemperature,
secondaryOutletPressure,
secondaryOutletTemperature,
secondaryIsHot,
} = data;
const bothConnected = !!circConnected;
const maxValue = Math.max(maxTotalOutput || 0, 0.001);
return (
<Window width={550} height={300}>
<Window.Content scrollable>
<Section>
<LabeledList>
<LabeledList.Item label="Total Output">
<ProgressBar
value={totalOutput || 0}
minValue={0}
maxValue={maxValue}
/>
<Box mt={0.5}>{f1(totalOutput)} kW</Box>
</LabeledList.Item>
<LabeledList.Item label="Thermal Output">
{f1(thermalOutput)} kWth
</LabeledList.Item>
</LabeledList>
</Section>
{bothConnected ? (
<Stack>
<Stack.Item grow basis={0}>
<CirculatorBlock
title="Primary Circulator"
dir={primaryDir}
output={primaryOutput}
flowCapacity={primaryFlowCapacity}
inletPressure={primaryInletPressure}
inletTemperature={primaryInletTemperature}
outletPressure={primaryOutletPressure}
outletTemperature={primaryOutletTemperature}
isHot={primaryIsHot}
/>
</Stack.Item>
<Stack.Item grow basis={0}>
<CirculatorBlock
title="Secondary Circulator"
dir={secondaryDir}
output={secondaryOutput}
flowCapacity={secondaryFlowCapacity}
inletPressure={secondaryInletPressure}
inletTemperature={secondaryInletTemperature}
outletPressure={secondaryOutletPressure}
outletTemperature={secondaryOutletTemperature}
isHot={secondaryIsHot}
/>
</Stack.Item>
</Stack>
) : (
<NoticeBox danger>
ERROR: Both circulators must be connected!
</NoticeBox>
)}
</Window.Content>
</Window>
);
};
const CirculatorBlock = (props: {
title: string;
dir?: string;
output?: number;
flowCapacity?: number;
inletPressure?: number;
inletTemperature?: number;
outletPressure?: number;
outletTemperature?: number;
isHot?: BooleanLike;
}) => {
const {
title,
dir,
output,
flowCapacity,
inletPressure,
inletTemperature,
outletPressure,
outletTemperature,
isHot,
} = props;
return (
<Section
title={`${title} (${dir ?? '-'})`}
fill
backgroundColor={
isHot ? 'rgba(255, 25, 25, 0.1)' : 'rgba(25, 25, 255, 0.1)'
}
textColor={isHot ? 'rgb(255, 225, 200)' : 'rgb(200, 225, 255)'}
>
<Box>
<LabeledList>
<LabeledList.Item label="Turbine Output">
{f1(output)} kWth
</LabeledList.Item>
<LabeledList.Item label="Flow Capacity">
{f1(flowCapacity)} %
</LabeledList.Item>
<LabeledList.Item label="Inlet Pressure">
{f1(inletPressure)} kPa
</LabeledList.Item>
<LabeledList.Item label="Inlet Temperature">
{f1(inletTemperature)} K
</LabeledList.Item>
<LabeledList.Item label="Outlet Pressure">
{f1(outletPressure)} kPa
</LabeledList.Item>
<LabeledList.Item label="Outlet Temperature">
{f1(outletTemperature)} K
</LabeledList.Item>
</LabeledList>
</Box>
</Section>
);
};