Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions app/.server/classes/Plugins/ShipSystems/MainCamera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type BasePlugin from "..";
import BaseShipSystemPlugin, { registerSystem } from "./BaseSystem";
import type { ShipSystemFlags } from "./shipSystemTypes";

export default class MainCameraPlugin extends BaseShipSystemPlugin {
static flags: ShipSystemFlags[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want to include damage on here. The view screen being damaged will happen in missions I'm sure, and we should give the crew the ability to repair it themselves.

Not sure about power — is it worth an extra bar of power for such a minor system? I don't know.

type = "mainCamera" as const;
fov: number;

constructor(params: Partial<MainCameraPlugin>, plugin: BasePlugin) {
super(params, plugin);
this.fov = params.fov ?? 45;
}
}
registerSystem("mainCamera", MainCameraPlugin);
2 changes: 2 additions & 0 deletions app/.server/classes/Plugins/ShipSystems/shipSystemTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import MainComputerPlugin from "@thorium/.server/classes/Plugins/ShipSystems/Mai
import CoolantTankSystemPlugin from "@thorium/.server/classes/Plugins/ShipSystems/CoolantTank";
import NavigationPlugin from "@thorium/.server/classes/Plugins/ShipSystems/Navigation";
import LongRangeCommPlugin from "@thorium/.server/classes/Plugins/ShipSystems/LongRangeComm";
import MainCameraPlugin from "@thorium/.server/classes/Plugins/ShipSystems/MainCamera";

// Make sure you update the isShipSystem component when adding a new ship system type
// We can't derive the isShipSystem list from this list because ECS components
Expand All @@ -35,6 +36,7 @@ export const ShipSystemTypes = {
coolantTank: CoolantTankSystemPlugin,
navigation: NavigationPlugin,
longRangeComm: LongRangeCommPlugin,
mainCamera: MainCameraPlugin,
};

export type ShipSystemFlags = "power" | "heat" | "damage" | "sounds";
Expand Down
2 changes: 2 additions & 0 deletions app/.server/data/plugins/systems/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { sensors } from "./sensors";
import { mainComputer } from "./mainComputer";
import { navigation } from "@thorium/.server/data/plugins/systems/navigation";
import { longRangeComm } from "@thorium/.server/data/plugins/systems/longRangeComm";
import { mainCamera } from "@thorium/.server/data/plugins/systems/mainCamera";

const systemTypes = createUnionSchema(
Object.keys(ShipSystemTypes) as (keyof typeof ShipSystemTypes)[],
Expand All @@ -44,6 +45,7 @@ export const systems = t.router({
mainComputer,
navigation,
longRangeComm,
mainCamera,
all: t.procedure
.input(z.object({ pluginId: z.string() }).optional())
.filter((publish: { pluginId: string } | null, { input }) => {
Expand Down
59 changes: 59 additions & 0 deletions app/.server/data/plugins/systems/mainCamera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { pubsub } from "@thorium/.server/init/pubsub";
import { t } from "@thorium/.server/init/t";
import inputAuth from "@thorium/utils/.server/inputAuth";
import { z } from "zod";
import {
getShipSystem,
getShipSystemForInput,
pluginFilter,
systemInput,
} from "../utils";
import type MainCameraPlugin from "@thorium/.server/classes/Plugins/ShipSystems/MainCamera";

export const mainCamera = t.router({
get: t.procedure
.input(systemInput)
.filter(pluginFilter)
.request(({ ctx, input }) => {
const system = getShipSystem({ input, ctx });

if (system.type !== "mainCamera")
throw new Error("System is not Main Camera");

return system as MainCameraPlugin;
}),
update: t.procedure
.input(
z.object({
pluginId: z.string(),
systemId: z.string(),
shipPluginId: z.string().optional(),
shipId: z.string().optional(),
fov: z.number().min(10).max(120).optional(),
}),
)
.send(({ ctx, input }) => {
inputAuth(ctx);
const [system, override] = getShipSystemForInput<"mainCamera">(
ctx,
input,
);
const shipSystem = override || system;

if (typeof input.fov !== "undefined") {
shipSystem.fov = input.fov;
}

pubsub.publish.plugin.systems.get({
pluginId: input.pluginId,
});
if (input.shipPluginId && input.shipId) {
pubsub.publish.plugin.ship.get({
pluginId: input.shipPluginId,
shipId: input.shipId,
});
}

return shipSystem;
}),
});
1 change: 1 addition & 0 deletions app/cards/DamageReports/systemCategories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const systemCategories: Record<
thrusters: "Propulsion",
torpedoLauncher: "Defense",
warpEngines: "Propulsion",
mainCamera: "Misc.",
};

export const systemSortValues = ["Name", "Type", "Offline", "Damage"];
33 changes: 33 additions & 0 deletions app/cards/Viewscreen/NoSignal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const colorBars = [
"#ffffff",
"#ffff00",
"#00ffff",
"#00ff00",
"#ff00ff",
"#ff0000",
"#0000ff",
];

export function NoSignal() {
return (
<div className="w-full h-full bg-black flex items-center justify-center">
<div className="w-full">
<div className="flex h-16">
{colorBars.map((color) => (
<div
key={color}
className="flex-1"
style={{ backgroundColor: color }}
/>
))}
</div>
<span className="block text-white text-3xl tracking-[0.3em] font-bold text-center mt-4 uppercase">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use the uppercase class here, and instead put a descriptive class like viewscreen-no-signal-header so themes can selectively uppercase it if they want.

Suggested change
<span className="block text-white text-3xl tracking-[0.3em] font-bold text-center mt-4 uppercase">
<span className="block text-white text-3xl tracking-[0.3em] font-bold text-center mt-4">

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes... I did think about this... and thought who in their right mind would want to customize the "No Signal" screen in a theme so absently snuck in my own design sense. I'll try not to do it again.

image

No Signal Found
</span>
<p className="text-white/60 text-sm tracking-widest text-center mt-2 uppercase">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case you couldn't tell, I really don't like uppercase.

Suggested change
<p className="text-white/60 text-sm tracking-widest text-center mt-2 uppercase">
<p className="text-white/60 text-sm tracking-widest text-center mt-2">

Copy link
Contributor Author

@mechatronics-studio mechatronics-studio Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I can change this. I was emulating real NO SIGNAL screens and had falsely assumed it would be wanted that way.

image

Main Camera System Missing
</p>
</div>
</div>
);
}
16 changes: 16 additions & 0 deletions app/cards/Viewscreen/data.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import { t } from "@thorium/.server/init/t";
import { z } from "zod";

export const viewscreen = t.router({
camera: t.procedure
.input(z.object({ shipId: z.number() }))
.autoPublish([], () => null)
.request(({ ctx, input }) => {
if (!ctx.flight?.ecs) return null;
for (const [, entity] of ctx.flight.ecs.entities) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be better. We cache entities by component using ctx.ecs.componentCache.get(componentName)

Suggested change
for (const [, entity] of ctx.flight.ecs.entities) {
for (const entity of ctx.ecs.componentCache.get('isMainCamera') || []) {

if (
entity.components.isShipSystem?.type === "mainCamera" &&
entity.components.isShipSystem?.shipId === input.shipId &&
entity.components.isMainCamera
) {
return { fov: entity.components.isMainCamera.fov };
}
}
return null;
}),
system: t.procedure
.input(z.object({ clientId: z.string() }))
.autoPublish([], () => null)
Expand Down
8 changes: 7 additions & 1 deletion app/cards/Viewscreen/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { WarpStars } from "./WarpStars";
import { CircleGridStoreProvider } from "@thorium/cards/Pilot/useCircleGridStore";
import { useStation } from "@thorium/routes/station/useStation";
import { Gizmos } from "./gizmos";
import { NoSignal } from "./NoSignal";

const forwardQuaternion = new Quaternion(0, 1, 0, 0);

Expand Down Expand Up @@ -58,12 +59,17 @@ export function Viewscreen() {
const currentSystem = useStarmapStore((store) => store.currentSystem);
const [initialized, setInitialized] = useState(false);
const { shipId } = useStation();
const [camera] = q.viewscreen.camera.useNetRequest({ shipId });
q.viewscreen.stream.useDataStream({ shipId });

if (!camera) {
return <NoSignal />;
}

return (
<div className="w-full h-full flex items-center justify-center text-white text-6xl">
<CircleGridStoreProvider>
<StarmapCanvas>
<StarmapCanvas fov={camera.fov}>
<ViewscreenEffects onDone={() => setInitialized(true)} />
{initialized ? (
<>
Expand Down
4 changes: 3 additions & 1 deletion app/components/Starmap/StarmapCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ export default function StarmapCanvas({
shouldRender = true,
alpha = true,
className = "",
fov = 45,
...props
}: {
children: ReactNode;
shouldRender?: boolean;
alpha?: boolean;
className?: string;
fov?: number;
} & CanvasProps) {
const client = useQueryClient();

Expand All @@ -61,7 +63,7 @@ export default function StarmapCanvas({
e.preventDefault();
}}
gl={{ antialias: true, logarithmicDepthBuffer: true, alpha }}
camera={{ fov: 45, near: 0.01, far: FAR }}
camera={{ fov, near: 0.01, far: FAR }}
frameloop={shouldRender ? "always" : "demand"}
{...props}
>
Expand Down
1 change: 1 addition & 0 deletions app/ecs-components/shipSystems/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from "./isLegacySensors";
export * from "./isLegacySensorScanning";
export * from "./isNavigation";
export * from "./isLongRangeComm";
export * from "./isMainCamera";
7 changes: 7 additions & 0 deletions app/ecs-components/shipSystems/isMainCamera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";

export const isMainCamera = z
.object({
fov: z.number().default(45),
})
.default({});
1 change: 1 addition & 0 deletions app/ecs-components/shipSystems/isShipSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const shipSystemTypes = z.enum([
"coolantTank",
"navigation",
"longRangeComm",
"mainCamera",
]);

export type ShipSystemTypes = z.infer<typeof shipSystemTypes>;
Expand Down
64 changes: 64 additions & 0 deletions app/routes/config/systems/SystemConfigs/mainCamera.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { q } from "@thorium/context/AppContext";
import Input from "@thorium/ui/Input";
import { useContext, useReducer } from "react";
import { useParams } from "react-router";
import { ShipPluginIdContext } from "@thorium/context/ShipSystemOverrideContext";
import { OverrideResetButton } from "../OverrideResetButton";
import { Navigate } from "@thorium/components/Navigate";

export default function MainCameraConfig() {
const { pluginId, systemId, shipId } = useParams() as {
pluginId: string;
systemId: string;
shipId: string;
};
const shipPluginId = useContext(ShipPluginIdContext);

const [system] = q.plugin.systems.mainCamera.get.useNetRequest({
pluginId,
systemId,
shipId,
shipPluginId,
});
const [rekey, setRekey] = useReducer(() => Math.random(), Math.random());
const key = `${systemId}${rekey}`;
if (!system) return <Navigate to={`/config/${pluginId}/systems`} />;

return (
<fieldset key={key} className="flex-1 overflow-y-auto">
<div className="flex flex-wrap">
<div className="flex-1 pr-4">
<div className="pb-2 flex">
<Input
label="Viewscreen FOV"
labelHidden={false}
type="number"
inputMode="numeric"
min={10}
max={120}
defaultValue={system.fov}
helperText="Vertical field of view for the viewscreen display (degrees)"
onBlur={(e: any) => {
const val = Number(e.target.value);
if (!Number.isNaN(val) && val >= 10 && val <= 120) {
q.plugin.systems.mainCamera.update.netSend({
pluginId,
systemId,
shipId,
shipPluginId,
fov: val,
});
}
}}
/>
<OverrideResetButton
property="fov"
setRekey={setRekey}
className="mt-6"
/>
</div>
</div>
</div>
</fieldset>
);
}
Loading