Skip to content

Commit

Permalink
feat(map): add fleets and fleet names to system maps
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelMakesGames committed Oct 25, 2024
1 parent fc38941 commit e51312c
Show file tree
Hide file tree
Showing 21 changed files with 442 additions and 77 deletions.
5 changes: 1 addition & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,5 @@
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"eslint.run": "onType",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
"eslint.run": "onType"
}
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"platform-paths": "^1.2.2",
"polylabel": "^1.1.0",
"rambda": "^9.2.0",
"romans": "^2.0.16",
"steam-path": "^1.0.2",
"tauri-plugin-fs-watch-api": "github:tauri-apps/tauri-plugin-fs-watch#v1",
"topojson-client": "^3.1.0",
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/src/intl/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export default {
},
triangle: 'Triangle',
triangle_flat: 'Triangle (flat top)',
triangle_narrow: 'Triangle (narrow)',
diamond: 'Diamond',
square: 'Square',
pentagon: 'Pentagon',
Expand Down Expand Up @@ -425,6 +426,13 @@ export default {
systemMapLabelMoonsEnabled: 'Moon Names Enabled',
systemMapLabelAsteroidsEnabled: 'Asteroid Names Enabled',
systemMapHyperlanesEnabled: 'Hyperlane Connections Enabled',
systemMapCivilianFleetIcon: 'Civilian Fleet Icon',
systemMapCivilianStationIcon: 'Civilian Station Icon',
systemMapMilitaryFleetIcon: 'Military Fleet Icon',
systemMapMilitaryStationIcon: 'Military Station Icon',
systemMapLabelFleetsEnabled: 'Fleet/Station Names',
systemMapLabelFleetsFontSize: 'Fleet/Station Name Font Size',
systemMapLabelFleetsPosition: 'Fleet/Station Name Position',
appLocale: 'StellarMaps Language',
appLocale_tooltip:
'Join the Discord server (link in the top bar) if you want to help translate!',
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/src/lib/ExportModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@
target: solarSystemMapTarget,
props: {
id: 'exportSystemMap',
colors: colors,
gameState: gameState,
colors,
mapData,
gameState,
system: openedSystem,
exportMode: true,
},
Expand Down Expand Up @@ -453,6 +454,7 @@
{#if openedSystem}
<SolarSystemMap
system={openedSystem}
{mapData}
{colors}
{gameState}
id="systemMapPreview"
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/src/lib/GameState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ export type Starbase = WithId<z.infer<typeof starbaseSchema>>;

const shipSchema = z.object({
fleet: z.number(),
coordinate: z.object({ x: z.number(), y: z.number() }),
rotation: z.number(),
});

/**
Expand All @@ -258,8 +260,15 @@ const shipSchema = z.object({
export type Ship = WithId<z.infer<typeof shipSchema>>;

const fleetSchema = z.object({
name: localizedTextSchema,
station: z.boolean().optional(),
military_power: z.number(),
mobile: z.boolean().optional(),
ships: z.array(z.number()),
movement_manager: z.object({
coordinate: z.object({ x: z.number(), y: z.number() }),
formation: z.object({ angle: z.number() }),
}),
});

/**
Expand Down
104 changes: 53 additions & 51 deletions src/renderer/src/lib/SettingControl/IconSettingControl.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -68,61 +68,63 @@
/>
</label>
</div>
<Accordion
regionControl="text-sm text-secondary-300"
hover="hover:bg-secondary-700"
padding="p-2"
spacing="space-y-1"
regionPanel="pt-0"
>
<AccordionItem>
<svelte:fragment slot="summary">
{$t('control.icon.advanced_options.header')}
</svelte:fragment>
<div slot="content" class="flex-col space-y-1">
<div class="flex items-baseline text-sm">
<label for="{config.id}-smoothing" class="ms-1 w-24 cursor-pointer">
{$t('control.icon.advanced_options.position')}
</label>
<select
id="{config.id}-position"
class="select p-1 text-sm"
value={value.position}
on:change={(e) => {
value = {
...value,
position: asIconPosition(e.currentTarget.value),
};
}}
>
{#each ICON_POSITIONS as position}
<option value={position}>{$t(`option.icon_position.${position}`)}</option>
{/each}
</select>
</div>
<div class="flex items-baseline text-sm">
<label for="{config.id}-smoothing" class="ms-1 w-24 cursor-pointer">
{$t('control.icon.advanced_options.priority')}
</label>
<input
id="{config.id}-priority"
class="input p-1 text-sm"
type="number"
value={value.priority}
on:change={(e) => {
const parsed = parseFloat(e.currentTarget.value);
if (Number.isNaN(parsed)) {
{#if !config.noAdvanced}
<Accordion
regionControl="text-sm text-secondary-300"
hover="hover:bg-secondary-700"
padding="p-2"
spacing="space-y-1"
regionPanel="pt-0"
>
<AccordionItem>
<svelte:fragment slot="summary">
{$t('control.icon.advanced_options.header')}
</svelte:fragment>
<div slot="content" class="flex-col space-y-1">
<div class="flex items-baseline text-sm">
<label for="{config.id}-smoothing" class="ms-1 w-24 cursor-pointer">
{$t('control.icon.advanced_options.position')}
</label>
<select
id="{config.id}-position"
class="select p-1 text-sm"
value={value.position}
on:change={(e) => {
value = {
...value,
priority: parseInt(e.currentTarget.value),
position: asIconPosition(e.currentTarget.value),
};
}
}}
/>
}}
>
{#each ICON_POSITIONS as position}
<option value={position}>{$t(`option.icon_position.${position}`)}</option>
{/each}
</select>
</div>
<div class="flex items-baseline text-sm">
<label for="{config.id}-smoothing" class="ms-1 w-24 cursor-pointer">
{$t('control.icon.advanced_options.priority')}
</label>
<input
id="{config.id}-priority"
class="input p-1 text-sm"
type="number"
value={value.priority}
on:change={(e) => {
const parsed = parseFloat(e.currentTarget.value);
if (Number.isNaN(parsed)) {
value = {
...value,
priority: parseInt(e.currentTarget.value),
};
}
}}
/>
</div>
</div>
</div>
</AccordionItem>
</Accordion>
</AccordionItem>
</Accordion>
{/if}
<ColorSettingControl
bind:value={color}
config={{ id: asAny(`${config.id}-color`), type: 'color' }}
Expand Down
18 changes: 16 additions & 2 deletions src/renderer/src/lib/map/Icons.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@
}`;
}
function pointsToSvgPath(points: string[]) {
return `${points.map((p, i) => (i === 0 ? `M${p}` : `L${p}`)).join(' ')} z`;
}
function makeRegularPolygonPath(n: number, { flatTop }: { flatTop: boolean }) {
const points = R.range(0, n).map((i) =>
pointOnCircle(i / n - 0.25 - (flatTop ? 0.5 / n : 0), 90),
);
return `${points.map((p, i) => (i === 0 ? `M${p}` : `L${p}`)).join(' ')} z`;
return pointsToSvgPath(points);
}
function makeStarPath(n: number) {
const points = R.range(0, n).flatMap((i) => [
pointOnCircle(i / n - 0.25, 90),
pointOnCircle((i * 2 + 1) / (n * 2) - 0.25, 30),
]);
return `${points.map((p, i) => (i === 0 ? `M${p}` : `L${p}`)).join(' ')} z`;
return pointsToSvgPath(points);
}
</script>

Expand All @@ -33,6 +37,16 @@
<path d={makeRegularPolygonPath(3, { flatTop: true })} />
</symbol>

<symbol id="icon-triangle-narrow" {viewBox}>
<path
d={pointsToSvgPath([
pointOnCircle(-0.25, 90),
pointOnCircle(-0.65, 90),
pointOnCircle(-0.85, 90),
])}
/>
</symbol>

<symbol id="icon-diamond" {viewBox}>
<path d={makeRegularPolygonPath(4, { flatTop: false })} />
</symbol>
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/lib/map/MapContainer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -489,10 +489,11 @@
</div>
</div>
{/await}
{#if openedSystem && gameStateOrNull && colorsOrNull}
{#if openedSystem && gameStateOrNull && colorsOrNull && dataOrNull}
<SolarSystemMap
id="systemMap"
gameState={gameStateOrNull}
mapData={dataOrNull}
system={openedSystem}
colors={colorsOrNull}
onSystemSelected={(system) => {
Expand Down
7 changes: 6 additions & 1 deletion src/renderer/src/lib/map/MapTooltip.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { onDestroy, onMount } from 'svelte';
import { locale, type MessageID, t } from '../../intl';
import debug from '../debug';
import type { GalacticObject, GameState, LocalizedText } from '../GameState';
import HeroiconUserMicro from '../icons/HeroiconUserMicro.svelte';
import { mapSettings } from '../settings';
Expand Down Expand Up @@ -91,9 +92,13 @@
{#await localizeText(system.name)}
{$t('generic.loading')}
{:then name}
{name} {system.id}
{name}
{/await}
</strong>
{#if $debug}
<div>System ID: {system.id}</div>
<div>Country ID: {processedSystem?.countryId}</div>
{/if}
{#if processedSystem?.mapModeCountryLabel}
<div class="flex flex-row justify-between gap-1 text-sm">
<span>
Expand Down
9 changes: 5 additions & 4 deletions src/renderer/src/lib/map/SystemIcons.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
systemProperty?: keyof PickByValue<ProcessedSystem, boolean>;
mustKnowOwner?: boolean;
}
const metadata: Record<IconMapSettings, IconSettingMetadata> = {
const metadata: Partial<Record<IconMapSettings, IconSettingMetadata>> = {
countryCapitalIcon: { systemProperty: 'isCountryCapital', mustKnowOwner: true },
sectorCapitalIcon: { systemProperty: 'isSectorCapital', mustKnowOwner: true },
populatedSystemIcon: { systemProperty: 'isColonized', mustKnowOwner: true },
Expand All @@ -41,10 +41,11 @@
)
.filter((config) => mapSettings[config.id].enabled)
.filter((config) => {
const systemProperty = metadata[config.id].systemProperty;
const meta = metadata[config.id];
if (!meta) return false;
const systemProperty = meta.systemProperty;
if (systemProperty != null && !system[systemProperty]) return false;
if (metadata[config.id].mustKnowOwner && mapSettings.terraIncognita && !system.ownerIsKnown)
return false;
if (meta.mustKnowOwner && mapSettings.terraIncognita && !system.ownerIsKnown) return false;
return true;
})
.sort((a, b) => mapSettings[b.id].priority - mapSettings[a.id].priority);
Expand Down
57 changes: 56 additions & 1 deletion src/renderer/src/lib/map/data/locUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
import { romanize } from 'romans';
import { get } from 'svelte/store';

import type { LocalizedText } from '../../GameState';
import { stellarisDataPromiseStore } from '../../loadStellarisData';
import { appStellarisLanguageCode, appStellarisLanguageOrdinals } from '../../settings';
import { isDefined } from '../../utils';

const formatOrdinals = (n: number) => {
const enOrdinalRules = new Intl.PluralRules(get(appStellarisLanguageCode), { type: 'ordinal' });
const rule = enOrdinalRules.select(n);
const suffix = get(appStellarisLanguageOrdinals)[rule] ?? '';
return `${new Intl.NumberFormat(get(appStellarisLanguageCode)).format(n)}${suffix}`;
};

const numFormatters = {
CARD: (n: number) => new Intl.NumberFormat(get(appStellarisLanguageCode)).format(n),
C: (n: number) =>
new Intl.NumberFormat(get(appStellarisLanguageCode), { minimumIntegerDigits: 1 }).format(n),
CC: (n: number) =>
new Intl.NumberFormat(get(appStellarisLanguageCode), { minimumIntegerDigits: 2 }).format(n),
CCC: (n: number) =>
new Intl.NumberFormat(get(appStellarisLanguageCode), { minimumIntegerDigits: 3 }).format(n),
CC0: (n: number) =>
new Intl.NumberFormat(get(appStellarisLanguageCode), { minimumIntegerDigits: 2 }).format(n - 1),
ORD: formatOrdinals,
ORD0: (n: number) => formatOrdinals(n - 1),
R: (n: number) => romanize(n),
HEX: (n: number) => n.toString(16),
};

export function localizeText(text: LocalizedText) {
return get(stellarisDataPromiseStore).then(({ loc }) => localizeTextSync(text, loc));
Expand All @@ -12,7 +38,36 @@ export function localizeTextSync(
loc: Record<string, string>,
): string {
if (text == null) return 'NULL';
if (text.key === '%ADJECTIVE%') {
if (text.key === '%ACRONYM%') {
const base = text.variables?.find((v) => v.key === 'base')?.value;
return base
? localizeTextSync(base, loc)
.split(' ')
.filter((s) => s.length > 0)
// first char of each word + last char of final word (cause that's how acronyms work in Stellaris *shrug*)
.flatMap((s, i, a) => (i < a.length - 1 ? [s[0]] : [s[0], s[s.length - 1]]))
.filter(isDefined)
.join('')
.toLocaleUpperCase(get(appStellarisLanguageCode))
: '';
} else if (text.key === '%SEQ%') {
const fmt = text.variables?.find((v) => v.key === 'fmt')?.value;
const num = text.variables?.find((v) => v.key === 'num')?.value;
if (fmt == null || num == null) {
console.warn('localization failed', text);
return 'LOCALIZATION FAILED';
}
return localizeTextSync(
{
key: fmt.key,
variables: Object.entries(numFormatters).map(([key, formatter]) => ({
key,
value: { key: formatter(parseInt(num.key)) },
})),
},
loc,
);
} else if (text.key === '%ADJECTIVE%') {
try {
const var0 = text.variables?.[0];
const var1 = text.variables?.[1];
Expand Down
Loading

0 comments on commit e51312c

Please sign in to comment.