Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c302e66
Add dock terminal lobby bezel art and bezel classes
Jackriip Nov 10, 2026
8481e0c
Add dock terminal lobby shell and section seams
Jackriip Nov 10, 2026
0d32785
Add the dock terminal HOUSING tab and lobby wiki updates
Jackriip Nov 10, 2026
6eaed8f
Add the CREWS tab to the dock terminal lobby
Jackriip Nov 10, 2026
7366ab6
Add the dock terminal shipyard backend with a single commission bill
Jackriip Nov 10, 2026
32d2b87
Tell a requisitioning player when a berth opened instead of a load fa…
Jackriip Nov 10, 2026
acc8c6e
Draw the lobby terminal's small buttons from lobby sprite PNGs
Jackriip Nov 10, 2026
ba5e2a2
Build the dock terminal shipyard screens
Jackriip Nov 10, 2026
ae2bc08
Merge feature/lobby-terminal-art into the lobby terminal
Jackriip Nov 10, 2026
bdf72e4
Merge feature/lobby-terminal-crews into the lobby terminal
Jackriip Nov 10, 2026
49db90d
Merge feature/lobby-terminal-yard-dm into the lobby terminal
Jackriip Nov 10, 2026
dc806a8
Merge feature/lobby-terminal-yard-ui into the lobby terminal
Jackriip Nov 10, 2026
a3ef696
Merge feature/lobby-terminal-housing into the lobby terminal
Jackriip Nov 10, 2026
232509d
Integrate the dock terminal lobby packages
Jackriip Nov 10, 2026
99b2f74
Define ship part classes before hull templates and fit the deck with …
Jackriip Nov 10, 2026
ecf835d
Hide the classic lobby under the terminal, load ship previews on firs…
Jackriip Nov 10, 2026
df3c8a3
Merge branch 'master' into feature/lobby-terminal
Jackriip Nov 10, 2026
168312b
Skip FIT for hulls with nothing to fit and clear the lobby notice on …
Jackriip Nov 10, 2026
05ecc30
Merge remote-tracking branch 'origin2/feature/lobby-terminal' into fe…
Jackriip Nov 10, 2026
0a21303
Add the CHARACTER tab seams to the dock terminal
Jackriip Nov 10, 2026
1e6ce44
Dock terminal CHARACTER backend: char actions, pushes, saving and tg …
Jackriip Nov 10, 2026
2773d87
Build the CHARACTER file view, BODY editor and colour entry
Jackriip Nov 10, 2026
4387bad
Build the ROLES, TRAITS and KIT editors for the CHARACTER tab
Jackriip Nov 10, 2026
2d66d73
Render the dock terminal's character preview on the server, one facin…
Jackriip Nov 10, 2026
80f9d41
Merge branch 'feature/lobby-char-c3-body' into feature/lobby-character
Jackriip Nov 10, 2026
190eaa8
Merge branch 'feature/lobby-char-c4-editors' into feature/lobby-chara…
Jackriip Nov 10, 2026
7fadc2a
Merge branch 'feature/lobby-terminal' into feature/lobby-character
Jackriip Nov 10, 2026
a578821
Add bark pitch and speed rows, default the traits selection, tell the…
Jackriip Nov 10, 2026
3631dbf
Retry the preferences catalog until char_open has sent it, show untou…
Jackriip Nov 10, 2026
bd02c3f
Send the lobby terminal's first update before its assets, and retry a…
Jackriip Nov 11, 2026
8d2e246
Merge branch 'feature/lobby-terminal' into feature/lobby-character
Jackriip Nov 11, 2026
75add13
Keyboard focus, key clicks, crew manifest and bounded text for the do…
Jackriip Nov 11, 2026
f3e012e
Tell the lobby terminal its UI before the assets go out
Jackriip Nov 11, 2026
7684e90
Let a modal layer keep the terminal's keyboard focus inside it
Jackriip Nov 11, 2026
99fc27f
Merge feature/lobby-terminal: keyboard focus, key clicks and bounded …
Jackriip Nov 11, 2026
cc87549
Give the dock terminal's first focus to the screen's own target once …
Jackriip Nov 11, 2026
52f05b9
Keep the dock terminal's port name and clock from overlapping on the …
Jackriip Nov 11, 2026
96c64d5
Space the crew list's names from their jobs and don't double a cut mark
Jackriip Nov 11, 2026
50a4c61
Merge branch 'feature/lobby-terminal' into feature/lobby-character
Jackriip Nov 11, 2026
c853b64
Merge pull request #526 from voidcrew/feature/lobby-character
Jackriip Nov 11, 2026
e7f2fbc
Merge master into the dock terminal lobby
Jackriip Nov 11, 2026
20b314b
Merge remote-tracking branch 'origin2/feature/lobby-terminal' into fe…
Jackriip Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions interface/skin.dmf
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ window "mapwindow"
saved-params = ""
text = ""
align = left
elem "lobby_browser"
type = BROWSER
pos = 0,0
size = 640x480
anchor1 = 0,0
anchor2 = 100,100
is-visible = false
background-color = #0b0b0e
saved-params = ""

window "info_and_buttons"
elem "info_and_buttons"
Expand Down
10 changes: 10 additions & 0 deletions tgstation.dme
Original file line number Diff line number Diff line change
Expand Up @@ -7245,6 +7245,16 @@
#include "voidcrew\modules\lich\lich_spells.dm"
#include "voidcrew\modules\lich\lich_thrall.dm"
#include "voidcrew\modules\lich\lich_wards.dm"
#include "voidcrew\modules\lobby_terminal\_lobby_terminal.dm"
#include "voidcrew\modules\lobby_terminal\lobby_assets.dm"
#include "voidcrew\modules\lobby_terminal\lobby_bezel.dm"
#include "voidcrew\modules\lobby_terminal\lobby_character.dm"
#include "voidcrew\modules\lobby_terminal\lobby_character_prefs.dm"
#include "voidcrew\modules\lobby_terminal\lobby_character_preview.dm"
#include "voidcrew\modules\lobby_terminal\lobby_crews.dm"
#include "voidcrew\modules\lobby_terminal\lobby_housing.dm"
#include "voidcrew\modules\lobby_terminal\lobby_tgui.dm"
#include "voidcrew\modules\lobby_terminal\lobby_yard.dm"
#include "voidcrew\modules\loot\abandoned_crate.dm"
#include "voidcrew\modules\loot\gene_injectors.dm"
#include "voidcrew\modules\loot\loot_guns.dm"
Expand Down
272 changes: 272 additions & 0 deletions tgui/packages/voidcrew_tgui/interfaces/LobbyTerminal/Bezel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
/**
* The terminal's bezel: frame, top strip, deck with the big keys, grille and the tray of
* small lobby buttons. Class tree from the art's lobby_terminal.css:
*
* .lt-bezel (--s) > .lt-screen, .lt-frame > .lt-strip, .lt-deck > .lt-deck-row >
* .lt-keys > .lt-key..., .lt-grille-slot > .lt-grille, .lt-tray > .lt-sbtn...
*
* Without the art (static.bezel null) the same tree is drawn by PLACEHOLDER_CSS.
*/
import type { ReactNode } from 'react';
import { resolveAsset } from 'tgui/assets';
import { useBackend } from 'tgui/backend';
import { useFocusable } from './nav';
import type { LobbyData } from './types';

type KeyId = 'join' | 'ready' | 'notready' | 'observe' | 'character' | 'start';

const KEY_LABEL: Record<KeyId, string> = {
join: 'JOIN',
ready: 'READY',
notready: 'READY',
observe: 'OBSERVE',
character: 'CHARACTER',
start: 'START',
};

function Key(props: {
id: KeyId;
disabled?: boolean;
/** Glows while its screen is open (CHARACTER). */
lit?: boolean;
title: string;
onClick: () => void;
}) {
const { id, disabled, lit, title, onClick } = props;
const focus = useFocusable<HTMLButtonElement>(!disabled, {
activate: onClick,
});
return (
<button
ref={focus.ref}
type="button"
className={`lt-key lt-key--${id}${disabled ? ' is-disabled' : ''}${lit ? ' is-lit' : ''}${focus.focused ? ' is-focus' : ''}`}
disabled={disabled}
title={title}
onClick={disabled ? undefined : onClick}
onMouseEnter={focus.onMouseEnter}
>
<span className="lt-key-legend">{KEY_LABEL[id]}</span>
</button>
);
}

type SmallId = 'wiki' | 'poll' | 'crew_manifest' | 'settings' | 'changelog';

function SmallButton(props: {
id: SmallId;
title: string;
disabled?: boolean;
mark?: boolean;
onClick: () => void;
}) {
const { id, title, disabled, mark, onClick } = props;
const focus = useFocusable<HTMLButtonElement>(!disabled, {
activate: onClick,
});
const state = disabled ? '_disabled' : focus.focused ? '_highlighted' : '';
return (
<button
ref={focus.ref}
type="button"
className={`lt-sbtn${disabled ? ' is-disabled' : ''}${focus.focused ? ' is-focus' : ''}`}
title={title}
disabled={disabled}
onClick={disabled ? undefined : onClick}
onMouseEnter={focus.onMouseEnter}
>
<img
className="lt-sbtn-icon"
src={resolveAsset(`lt_${id}${state}.png`)}
width={24}
height={24}
alt=""
/>
{mark ? (
<img
className="lt-sbtn-mark"
src={resolveAsset('lt_new_poll.png')}
width={24}
height={26}
alt=""
/>
) : null}
</button>
);
}

export function Bezel(props: {
s: number;
children: ReactNode;
/** The CHARACTER tab is open: the key glows. */
characterLit?: boolean;
/** The CHARACTER key: switches to the CHARACTER tab. */
onCharacter: () => void;
}) {
const { s, children, characterLit, onCharacter } = props;
const { data, act } = useBackend<LobbyData>();
const art = !!data.bezel;
const preRound = data.phase === 'loading' || data.phase === 'pregame';
const ready = !!data.ready;
const showReady = preRound || data.phase === 'setup';

return (
<div
className={art ? 'lt-bezel' : 'lt-bezel lt-ph'}
style={{ ['--s' as string]: s }}
>
{!art && <style>{PLACEHOLDER_CSS}</style>}
<div className="lt-screen">{children}</div>
<div className="lt-frame">
<div className="lt-strip" />
</div>
<div className="lt-deck">
<div className="lt-deck-row">
<div className="lt-keys">
{showReady ? (
<>
<Key
id={ready ? 'ready' : 'notready'}
disabled={!preRound}
title={ready ? 'Ready' : 'Not ready'}
onClick={() => act('ready')}
/>
<span
className={`lt-lamp lt-lamp--${ready ? 'ready' : 'notready'}`}
/>
</>
) : (
<Key
id="join"
disabled={data.phase !== 'playing'}
title="Join"
onClick={() => act('join')}
/>
)}
<Key
id="observe"
disabled={!data.can_observe}
title="Observe"
onClick={() => act('observe')}
/>
<Key
id="character"
disabled={!data.prefs_ready}
lit={characterLit}
title="Character"
onClick={onCharacter}
/>
{data.localhost_admin && preRound ? (
<Key
id="start"
title="Start now"
onClick={() => act('start_now')}
/>
) : null}
</div>
<div className="lt-grille-slot">
<div className="lt-grille" />
</div>
<div className="lt-tray">
<SmallButton
id="wiki"
title="Wiki"
disabled={!data.wiki}
onClick={() => act('wiki')}
/>
<SmallButton
id="poll"
title="Polls"
disabled={!data.polls}
mark={data.polls === 2}
onClick={() => act('polls')}
/>
<SmallButton
id="crew_manifest"
title="Crew manifest"
onClick={() => act('manifest')}
/>
<SmallButton
id="settings"
title="Settings"
disabled={!data.prefs_ready}
onClick={() => act('settings')}
/>
<SmallButton
id="changelog"
title="Changelog"
onClick={() => act('changelog')}
/>
</div>
</div>
</div>
</div>
);
}

/**
* Keycap legends are baked into the art; only the placeholder (and the START key, which has
* no art) shows the text legend.
*/
const LEGEND_CSS = `
.lt-key-legend { display: none; }
.lt-key--start { width: calc(var(--s) * 52px); height: calc(var(--s) * 34px); position: relative;
background: #9aa39f; box-shadow: inset 0 0 0 calc(var(--s) * 2px) #20202a; }
.lt-key--start .lt-key-legend, .lt-ph .lt-key-legend { display: block; position: absolute;
left: calc(var(--s) * 6px); right: calc(var(--s) * 6px); top: calc(var(--s) * 9px); height: calc(var(--s) * 13px);
background: #36373a; color: #7effbf; font-family: 'VCR', monospace; font-size: calc(var(--s) * 9px);
line-height: calc(var(--s) * 13px); text-align: center; letter-spacing: calc(var(--s) * 1px); }
.lt-key--start.is-focus .lt-key-legend { color: #ffc41f; }
.lt-key-legend { pointer-events: none; }
.lt-key.is-lit { position: relative; }
.lt-key.is-lit::after { content: ''; position: absolute; pointer-events: none;
left: calc(var(--s) * 9px); top: calc(var(--s) * 5px); width: calc(var(--s) * 77px); height: calc(var(--s) * 15px);
background: rgba(126,255,191,.16); mix-blend-mode: screen;
box-shadow: 0 0 calc(var(--s) * 6px) rgba(126,255,191,.55), inset 0 0 calc(var(--s) * 3px) rgba(126,255,191,.5); }
`;

export const BEZEL_EXTRA_CSS = LEGEND_CSS;

/** Stand-in for the art: the same layout in flat lobby colours. */
const PLACEHOLDER_CSS = `
.lt-ph.lt-bezel { --s: 1; position: absolute; inset: 0; overflow: hidden; background: #9aa39f; user-select: none; }
.lt-ph .lt-screen { position: absolute; top: calc(var(--s) * 15px); left: calc(var(--s) * 13px);
right: calc(var(--s) * 13px); bottom: calc(var(--s) * 52px); overflow: hidden; z-index: 1; }
.lt-ph .lt-frame { position: absolute; top: calc(var(--s) * 12px); left: calc(var(--s) * 10px);
right: calc(var(--s) * 10px); bottom: calc(var(--s) * 49px); box-sizing: border-box;
border: calc(var(--s) * 3px) solid #51575e; border-bottom-color: #e0e3dc; border-right-color: #e0e3dc;
pointer-events: none; z-index: 2; }
.lt-ph .lt-strip { position: absolute; top: calc(var(--s) * -10px); left: calc(50% - var(--s) * 30px);
width: calc(var(--s) * 61px); height: calc(var(--s) * 6px); background: #36373a; }
.lt-ph .lt-deck { position: absolute; left: 0; right: 0; bottom: 0; height: calc(var(--s) * 49px); z-index: 2;
border-top: calc(var(--s) * 2px) solid #e0e3dc; box-sizing: border-box; }
.lt-ph .lt-deck-row { position: absolute; top: calc(var(--s) * 6px); left: calc(var(--s) * 30px);
right: calc(var(--s) * 29px); display: flex; align-items: flex-start; }
.lt-ph .lt-keys { display: flex; align-items: flex-start; flex: none; }
.lt-ph .lt-key { display: block; flex: none; margin: 0; padding: 0; border: 0; outline: 0; position: relative;
height: calc(var(--s) * 34px); background: #bec4bf; box-shadow: inset 0 0 0 calc(var(--s) * 2px) #20202a; cursor: pointer; }
.lt-ph .lt-key + .lt-key { margin-left: calc(var(--s) * 4px); }
.lt-ph .lt-key--join, .lt-ph .lt-key--ready, .lt-ph .lt-key--notready { width: calc(var(--s) * 68px); }
.lt-ph .lt-key--observe { width: calc(var(--s) * 84px); }
.lt-ph .lt-key--character { width: calc(var(--s) * 100px); }
.lt-ph .lt-key.is-focus { background: #e0e3dc; }
.lt-ph .lt-key:active { background: #9aa39f; }
.lt-ph .lt-key.is-disabled { cursor: default; background: #9aa39f; }
.lt-ph .lt-key.is-disabled .lt-key-legend { color: #33676a; }
.lt-ph .lt-key--notready .lt-key-legend { color: #ff4f6a; }
.lt-ph .lt-lamp { flex: none; width: calc(var(--s) * 10px); height: calc(var(--s) * 9px);
margin: calc(var(--s) * 11px) calc(var(--s) * 1px) 0 calc(var(--s) * 2px); background: #20202a; }
.lt-ph .lt-lamp + .lt-key { margin-left: 0; }
.lt-ph .lt-lamp--ready { background: #7effbf; }
.lt-ph .lt-lamp--notready { background: #ff4f6a; }
.lt-ph .lt-grille-slot { flex: 1 1 0; min-width: 0; }
.lt-ph .lt-tray { flex: none; position: relative; display: flex; margin-top: calc(var(--s) * 1px);
padding: calc(var(--s) * 3px); width: calc(var(--s) * 142px); height: calc(var(--s) * 31px);
box-sizing: border-box; background: #36373a; }
.lt-ph .lt-sbtn { position: relative; flex: none; margin: 0; padding: 0; border: 0; background: none;
width: calc(var(--s) * 24px); height: calc(var(--s) * 24px); cursor: pointer; }
.lt-ph .lt-sbtn + .lt-sbtn { margin-left: calc(var(--s) * 4px); }
.lt-ph .lt-sbtn-icon, .lt-ph .lt-sbtn-mark { position: absolute; left: 0; top: 0;
transform: scale(var(--s)); transform-origin: 0 0; pointer-events: none; image-rendering: pixelated; }
.lt-ph .lt-sbtn-mark { top: calc(var(--s) * -1px); }
`;
Loading
Loading