Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ship map backend #220

Merged
merged 6 commits into from
Feb 16, 2022
90 changes: 90 additions & 0 deletions client/src/docs/Plugins/ship-maps.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: Ship Maps
order: 3
---

# Ship Maps

_TODO: Explanation here about what ship maps are and how they impact the
simulation._

## Building Ship Maps

Ship maps consist of three things: Decks, which include a background image that
shows what that deck looks like, Nodes, which are points of interest on the
deck, and Edges, which are straight lines that connect the nodes. Nodes most
typically are rooms, but they are also used to direct the edges. For example,
several nodes can be connected in a curved shape to allow for circular walkways.

Cargo and crew members can move between nodes using edges. _TODO: Add more
explanation here about how nodes and edges are constructed._

Each ship class includes the full definition for the ship map. As you are
creating ship maps for new ships, keep these guidelines in mind.

### Plan Out your Rooms

There are a lot of factors that go into how many and what rooms your ship should
have. You'll need to consider the size and length of the ship, the number of
crew you want to have, rooms for any extra passengers, and the required rooms
necessary for the ship to function. Every ship system should have at least one
room associated with it (although multiple systems can share a room). Crew also
need a room to sleep in. Additional rooms, like science labs, gymnasiums, and
lounges, are optional but can add additional depth to your ship.

The deck count also plays a role in this. Once you figure out the length of your
ship, you can use the calculated height (based on the 3D model of the ship) to
determine the number of decks. If your crew is human with an average height of
1.8 meters, it's safe to assume the decks are 3 meters high. Divide the total
height of the ship by 3 and round down to get the number of decks.

The rooms on each deck will conform to the shape of the ship at that level. It
might be helpful to use software to create slices in the 3D model where each
deck would be to get an idea for the shape of the deck. With that shape, you can
begin to sketch out how the hallways are laid out and the size and shape of each
room. Don't forget to include the bridge, which is the room the player crew will
be in.

### Design the SVG

The background image for each deck should be an SVG, although any image format
is compatible. SVGs provide the most flexibility, since they can be dynamically
altered by the controls (to change colors for the current alert condition, for
example), can scale infinitely, and have a small file size. Creating SVGs is
possible with tools like Adobe Illustrator, Infinity Designer, Inkscape, and
many online tools.

Whether using SVGs or other images, Thorium Nova treats 10 pixels/points as 1
meter, and expects the deck images to be looking down from the top with the ship
in a horizontal orientation. That means if your ship is 200 meters long and 100
meters wide, the background image should be 2000x1000 pixels.

While the actual construction of the SVG is irrelevant, it might be helpful to
create separate paths for each room to better organize them. Don't forget to
include a hallway and places where the crew can move between decks.

The design is also up to you, but the standard for Thorium Nova is to have the
ship background have a black fill with a white outline, and for all of the rooms
to have a stroke but no fill. The hull of the ship has a 3 point stroke, the
hallway has a 2 point stroke, and each of the rooms has a 1 point stroke.

If you want your SVG to change colors with the alert condition, you'll have to
manually edit it in a text editor after designing it. Change the stroke color
for all of the paths to `currentColor`, as in
`<path stroke="currentColor" ... />`. This lets Thorium Nova change the color of
the SVG.

### Create the Decks

In Thorium Nova, go to the ship plugin editor, pick your ship, and start adding
decks. Upload the background image for each deck and change the name, if
necessary. If you need to reorder decks, you can do that by dragging the decks
above or below each other.

### Add the Nodes and Edges

_TODO: Will add this later, once nodes and edges are implemented._

### Assign Extra Properties to Nodes and Edges

_TODO: Will add this later, once nodes and edges are implemented._
6 changes: 6 additions & 0 deletions server/src/classes/Plugins/Ship/Deck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default class DeckPlugin {
name: string;
constructor(params: Partial<DeckPlugin>) {
this.name = params.name || "Deck";
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type BasePlugin from "./index";
import {Aspect} from "./Aspect";
import type BasePlugin from "../index";
import {Aspect} from "../Aspect";
import {generateIncrementedName} from "server/src/utils/generateIncrementedName";
import DeckPlugin from "./Deck";

export type ShipCategories = "Cruiser" | "Frigate" | "Scout" | "Shuttle";

Expand Down Expand Up @@ -38,6 +39,10 @@ export default class ShipPlugin extends Aspect {
* The side view of the ship as a PNG. Usually auto-generated from the model.
*/
sideView: string;
/**
* The paths to the ship's deck images, where the index corresponds to the deck order.
*/
decks: string[];
};
/**
* The mass of the ship in kilograms
Expand All @@ -58,6 +63,10 @@ export default class ShipPlugin extends Aspect {
* The station theme used for this ship if it is a player ship.
*/
theme?: {pluginId: string; themeId: string};
/**
* The decks assigned to this ship.
*/
decks: DeckPlugin[];
constructor(params: Partial<ShipPlugin>, plugin: BasePlugin) {
const name = generateIncrementedName(
params.name || "New Ship",
Expand All @@ -75,10 +84,23 @@ export default class ShipPlugin extends Aspect {
vanity: "",
topView: "",
sideView: "",
decks: [],
};
this.mass = params.mass || 700_000_000;
this.length = params.length || 350;
this.shipSystems = params.shipSystems || [];
this.theme = params.theme || undefined;
this.decks = params.decks?.map(deck => new DeckPlugin(deck)) || [];
}
addDeck(deck: Partial<DeckPlugin>) {
let {name} = deck;
const order = this.decks.length;
if (!name) name = `Deck ${order + 1}`;
const deckNum = this.decks.push({name}) - 1;

return deckNum;
}
removeDeck(index: number) {
this.decks.splice(index, 1);
}
}
123 changes: 123 additions & 0 deletions server/src/inputs/__test__/shipDecksPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import BasePlugin from "server/src/classes/Plugins";
import ShipPlugin from "server/src/classes/Plugins/Ship";
import {decksPluginInputs} from "../plugins/ships/decks";
import {promises as fs} from "fs";

function createMockDataContext() {
return {
flight: null,
server: {
plugins: [
{
id: "Test Plugin",
name: "Test Plugin",
active: true,
aspects: {
ships: [
new ShipPlugin({name: "Test Template"}, {
name: "Test Plugin",
aspects: {ships: []},
} as unknown as BasePlugin),
],
},
},
],
},
} as any;
}

describe("ship decks plugin input", () => {
it("should create a new deck", async () => {
const mockDataContext = createMockDataContext();

const shipDeck = decksPluginInputs.pluginShipDeckCreate(mockDataContext, {
pluginId: "Test Plugin",
shipId: "Test Template",
});

expect(shipDeck).toEqual(0);
const shipDeck2 = decksPluginInputs.pluginShipDeckCreate(mockDataContext, {
pluginId: "Test Plugin",
shipId: "Test Template",
});

expect(shipDeck2).toEqual(1);
});
it("should delete a deck", async () => {
const mockDataContext = createMockDataContext();

const shipDeck = decksPluginInputs.pluginShipDeckCreate(mockDataContext, {
pluginId: "Test Plugin",
shipId: "Test Template",
});

const shipDeck2 = decksPluginInputs.pluginShipDeckCreate(mockDataContext, {
pluginId: "Test Plugin",
shipId: "Test Template",
});

expect(
mockDataContext.server.plugins[0].aspects.ships[0].decks.length
).toEqual(2);

decksPluginInputs.pluginShipDeckDelete(mockDataContext, {
pluginId: "Test Plugin",
shipId: "Test Template",
index: 0,
});

expect(
mockDataContext.server.plugins[0].aspects.ships[0].decks.length
).toEqual(1);
expect(
mockDataContext.server.plugins[0].aspects.ships[0].decks[0].name
).toEqual("Deck 2");
});
it("should update a deck", async () => {
const mockDataContext = createMockDataContext();

const shipDeck = decksPluginInputs.pluginShipDeckCreate(mockDataContext, {
pluginId: "Test Plugin",
shipId: "Test Template",
});

const shipDeck2 = decksPluginInputs.pluginShipDeckCreate(mockDataContext, {
pluginId: "Test Plugin",
shipId: "Test Template",
});

expect(
mockDataContext.server.plugins[0].aspects.ships[0].decks[0].name
).toEqual("Deck 1");

decksPluginInputs.pluginShipDeckUpdate(mockDataContext, {
pluginId: "Test Plugin",
shipId: "Test Template",
index: 0,
name: "A Deck",
});

expect(
mockDataContext.server.plugins[0].aspects.ships[0].decks[0].name
).toEqual("A Deck");

decksPluginInputs.pluginShipDeckUpdate(mockDataContext, {
pluginId: "Test Plugin",
shipId: "Test Template",
index: 1,
newIndex: 0,
});

expect(
mockDataContext.server.plugins[0].aspects.ships[0].decks[1].name
).toEqual("A Deck");
expect(
mockDataContext.server.plugins[0].aspects.ships[0].decks[0].name
).toEqual("Deck 2");
});
afterAll(async () => {
try {
await fs.rm("plugins", {recursive: true});
} catch {}
});
});
1 change: 1 addition & 0 deletions server/src/inputs/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {serverInputs} from "./server";
export {effectsInputs} from "./effects";
export {pluginInputs} from "./plugins";
export {shipsPluginInputs} from "./plugins/ships";
export {decksPluginInputs} from "./plugins/ships/decks";
export {themesPluginInput} from "./plugins/themes";
export {officerLogInputs} from "./officersLog";
export {solarSystemsPluginInputs} from "./plugins/universe/solarSystems";
90 changes: 90 additions & 0 deletions server/src/inputs/plugins/ships/decks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {DataContext} from "server/src/utils/DataContext";
import {moveArrayItem} from "server/src/utils/moveArrayItem";
import {pubsub} from "server/src/utils/pubsub";
import {getPlugin} from "../utils";
import path from "path";
import {promises as fs} from "fs";
import {thoriumPath} from "server/src/utils/appPaths";
import uniqid from "@thorium/uniqid";

export const decksPluginInputs = {
pluginShipDeckCreate(
context: DataContext,
params: {pluginId: string; shipId: string}
) {
const plugin = getPlugin(context, params.pluginId);
const ship = plugin.aspects.ships.find(ship => ship.name === params.shipId);
if (!ship) return;

const deckIndex = ship.addDeck({});

pubsub.publish("pluginShip", {
pluginId: params.pluginId,
shipId: ship.name,
});
return deckIndex;
},
pluginShipDeckDelete(
context: DataContext,
params: {pluginId: string; shipId: string; index: number}
) {
const plugin = getPlugin(context, params.pluginId);
const ship = plugin.aspects.ships.find(ship => ship.name === params.shipId);
if (!ship) return;

ship.removeDeck(params.index);

pubsub.publish("pluginShip", {
pluginId: params.pluginId,
shipId: ship.name,
});
},
async pluginShipDeckUpdate(
context: DataContext,
params: {
pluginId: string;
shipId: string;
index: number;
name?: string;
newIndex?: number;
backgroundImage?: File | string;
}
) {
const plugin = getPlugin(context, params.pluginId);
const ship = plugin.aspects.ships.find(ship => ship.name === params.shipId);
if (!ship) return;

const deck = ship.decks[params.index];

if (params.name) {
deck.name = params.name;
}
if (typeof params.newIndex === "number") {
moveArrayItem(ship.decks, params.index, params.newIndex);
moveArrayItem(ship.assets.decks, params.index, params.newIndex);
}
if (params.backgroundImage && typeof params.backgroundImage === "string") {
const ext = path.extname(params.backgroundImage);
let file = params.backgroundImage;
let filePath = `${uniqid(`deck-${params.index}`)}.${ext}`;
if (!ship) return;
if (typeof file === "string") {
await fs.mkdir(path.join(thoriumPath, ship.assetPath), {
recursive: true,
});
await fs.rename(file, path.join(thoriumPath, ship.assetPath, filePath));
ship.assets.decks =
ship.assets.decks ||
Array.from({length: ship.decks.length}).fill(null);
ship.assets.decks[params.index] = filePath;
ship.writeFile(true);
}
}
pubsub.publish("pluginShip", {
pluginId: params.pluginId,
shipId: ship.name,
});

return params.newIndex || params.index;
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import ShipPlugin, {ShipCategories} from "server/src/classes/Plugins/Ship";
import {promises as fs} from "fs";
import {DataContext} from "server/src/utils/DataContext";
import {pubsub} from "server/src/utils/pubsub";
import {getPlugin} from "./utils";
import {getPlugin} from "../utils";
import {thoriumPath} from "server/src/utils/appPaths";

export const shipsPluginInputs = {
Expand Down Expand Up @@ -104,7 +104,7 @@ export const shipsPluginInputs = {
async function moveFile(
file: Blob | File | string,
filePath: string,
propertyName: keyof NonNullable<typeof ship>["assets"]
propertyName: "logo" | "model" | "topView" | "sideView" | "vanity"
) {
if (!ship) return;
if (typeof file === "string") {
Expand Down
Loading