Moved to next js ssr (with fixes)
This commit is contained in:
parent
2184f1642c
commit
23bfad8956
4
.gitignore
vendored
4
.gitignore
vendored
@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
/deploy
|
||||||
|
dow-wiki-deploy.tar.gz
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
4
KODA.md
Normal file
4
KODA.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#### Сборка
|
||||||
|
|
||||||
|
При сборке дистрибутива формируй архив dow-wiki-deploy.tar.gz
|
||||||
|
Предварительно удали всё что было до этого в папке deploy и собирай туда
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import type { Metadata, Viewport } from 'next';
|
import type { Metadata, Viewport } from 'next';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
import '@/src/index.css';
|
import '@/src/index.css';
|
||||||
import '@/src/App.css';
|
import '@/src/App.css';
|
||||||
import '@/src/css/Unit.css';
|
import '@/src/css/Unit.css';
|
||||||
@ -49,8 +50,12 @@ export default function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
// Читаем тему из cookie на сервере, чтобы сразу отрендерить нужную тему без мерцания.
|
||||||
|
const themeCookie = cookies().get('theme')?.value;
|
||||||
|
const initialMode = themeCookie === 'light' ? 'light' : 'dark';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" data-theme={initialMode} style={{ colorScheme: initialMode }}>
|
||||||
<body>
|
<body>
|
||||||
{/* Yandex.Metrika */}
|
{/* Yandex.Metrika */}
|
||||||
<script
|
<script
|
||||||
@ -69,7 +74,7 @@ export default function RootLayout({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</noscript>
|
</noscript>
|
||||||
<ThemeProvider>
|
<ThemeProvider initialMode={initialMode}>
|
||||||
<AppShell>{children}</AppShell>
|
<AppShell>{children}</AppShell>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import ModPageClient from '@/components/pages/ModPageClient';
|
import ModPageClient from '@/components/pages/ModPageClient';
|
||||||
import { getMod } from '@/lib/api-server';
|
import { getMod, getUnitsForMod, getBuildingsForMod } from '@/lib/api-server';
|
||||||
|
import { IRaceBuildings } from '@/src/types/IBuildingShort';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@ -28,6 +29,15 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModPage() {
|
export default async function ModPage({ params }: Props) {
|
||||||
return <ModPageClient />;
|
const [mod, racesUnits] = await Promise.all([
|
||||||
|
getMod(params.modId),
|
||||||
|
getUnitsForMod(params.modId),
|
||||||
|
]);
|
||||||
|
const units = racesUnits ?? [];
|
||||||
|
let racesBuildings: IRaceBuildings[] = [];
|
||||||
|
if (units.length <= 10) {
|
||||||
|
racesBuildings = (await getBuildingsForMod(params.modId)) ?? [];
|
||||||
|
}
|
||||||
|
return <ModPageClient initialMod={mod} racesUnits={units} racesBuildings={racesBuildings} />;
|
||||||
}
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import BuildingPageClient from '@/components/pages/BuildingPageClient';
|
import BuildingPageClient from '@/components/pages/BuildingPageClient';
|
||||||
import { getBuilding, getMod } from '@/lib/api-server';
|
import { getBuilding, getMod } from '@/lib/api-server';
|
||||||
|
import { IBuilding } from '@/src/types/IBuilding';
|
||||||
|
import { IMod } from '@/src/types/Imod';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@ -32,6 +34,16 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BuildingPage() {
|
export default async function BuildingPage({ params }: Props) {
|
||||||
return <BuildingPageClient />;
|
const [mod, building] = await Promise.all([
|
||||||
|
getMod(params.modId),
|
||||||
|
getBuilding(params.buildingId),
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<BuildingPageClient
|
||||||
|
key={params.buildingId}
|
||||||
|
initialBuilding={building as IBuilding | null}
|
||||||
|
initialMod={mod as IMod | null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import RacePageClient from '@/components/pages/RacePageClient';
|
import RacePageClient from '@/components/pages/RacePageClient';
|
||||||
import { getMod, getRace } from '@/lib/api-server';
|
import { getMod, getRace, getRaceUnits, getRaceBuildings } from '@/lib/api-server';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@ -28,6 +28,19 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RacePage() {
|
export default async function RacePage({ params }: Props) {
|
||||||
return <RacePageClient />;
|
const [mod, race, raceUnits, raceBuildings] = await Promise.all([
|
||||||
|
getMod(params.modId),
|
||||||
|
getRace(params.raceId),
|
||||||
|
getRaceUnits(params.modId, params.raceId),
|
||||||
|
getRaceBuildings(params.modId, params.raceId),
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<RacePageClient
|
||||||
|
initialMod={mod}
|
||||||
|
initialRace={race}
|
||||||
|
raceUnits={raceUnits}
|
||||||
|
raceBuildings={raceBuildings}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import UnitPageClient from '@/components/pages/UnitPageClient';
|
import UnitPageClient from '@/components/pages/UnitPageClient';
|
||||||
import { getMod, getUnit } from '@/lib/api-server';
|
import { getMod, getUnit } from '@/lib/api-server';
|
||||||
|
import { IUnit } from '@/src/types/IUnit';
|
||||||
|
import { IMod } from '@/src/types/Imod';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
@ -30,6 +32,16 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UnitPage() {
|
export default async function UnitPage({ params }: Props) {
|
||||||
return <UnitPageClient />;
|
const [mod, unit] = await Promise.all([
|
||||||
|
getMod(params.modId),
|
||||||
|
getUnit(params.unitId),
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<UnitPageClient
|
||||||
|
key={params.unitId}
|
||||||
|
initialUnit={unit as IUnit | null}
|
||||||
|
initialMod={mod as IMod | null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@ -1,16 +1,358 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import dynamic from 'next/dynamic';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Box, LinearProgress } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Grid2,
|
||||||
|
Link,
|
||||||
|
ListItem,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
LinearProgress,
|
||||||
|
Theme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { ArrowBack } from '@mui/icons-material';
|
||||||
|
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
|
||||||
|
import { AvailableBuildings, AvailableMods, AvailableUnits, IconUrl } from '@/src/core/api';
|
||||||
|
import { IShortWeapon } from '@/src/types/IUnit';
|
||||||
|
import { IRaceUnits } from '@/src/types/IUnitShort';
|
||||||
|
import { IRaceBuildings } from '@/src/types/IBuildingShort';
|
||||||
|
import '@/src/css/Building.css';
|
||||||
|
import ArmorType from '@/src/classes/ArmorType';
|
||||||
|
import WeaponSlot from '@/src/classes/WeaponSlot';
|
||||||
|
import UnitsTable from '@/src/classes/UnitsTable';
|
||||||
|
import { IMod } from '@/src/types/Imod';
|
||||||
|
import { IBuilding } from '@/src/types/IBuilding';
|
||||||
|
import Vision from '@/src/classes/Vision';
|
||||||
|
import BuildingAddon from '@/src/classes/building/BuildingAddon';
|
||||||
|
import { IUnitShort } from '@/src/types/IUnitShort';
|
||||||
|
import Research, { AffectedResearches } from '@/src/classes/building/Research';
|
||||||
|
import Required from '@/src/classes/Required';
|
||||||
|
import { ModifiersProvidesTable } from '@/src/classes/ModifiersProvideTable';
|
||||||
|
import Ability from '@/src/classes/Ability';
|
||||||
|
import DeathExplosion from '@/src/classes/DeathExplosion';
|
||||||
|
import { DescriptionBox } from '@/src/commons/DescriptionBox';
|
||||||
|
import { BackButton } from '@/src/commons/BackButton';
|
||||||
|
|
||||||
const BuildingPageLegacy = dynamic(() => import('@/src/legacy-pages/BuildingPage'), {
|
const SectionTitle = styled(Typography)(({ theme }) => ({
|
||||||
ssr: false,
|
fontWeight: 700,
|
||||||
loading: () => (
|
mb: 2,
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StatsPaper = styled(Paper)(({ theme }) => ({
|
||||||
|
background: theme.palette.mode === 'dark'
|
||||||
|
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
||||||
|
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
||||||
|
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||||
|
borderRadius: '12px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
|
||||||
|
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
|
my: 3,
|
||||||
|
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const BuildingTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 800,
|
||||||
|
mb: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const BuildingSubtitle = styled(Typography)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontWeight: 500,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const UnitLink = styled(Link)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
textDecoration: 'none',
|
||||||
|
'&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const UnitListItem = styled(ListItem)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'}`,
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': { background: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)', borderColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
function Unit(unit: IUnitShort, modId: number, raceId: string, theme: Theme) {
|
||||||
|
return (<Grid2 key={unit.id} size={{ xs: 12, md: 3 }}>
|
||||||
|
<UnitLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
|
||||||
|
<UnitListItem>
|
||||||
|
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')} />}
|
||||||
|
{unit.name}
|
||||||
|
{unit.canDetect && <span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/DETECT_YES.webp" /></span>}
|
||||||
|
</UnitListItem>
|
||||||
|
</UnitLink>
|
||||||
|
</Grid2>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Building(building: IBuilding, mod: IMod, theme: Theme, racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[]) {
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
|
||||||
|
|
||||||
|
building.weapons.forEach(weapon => {
|
||||||
|
const weaponMap = mapBuildingWeapons.get(weapon.hardpoint)
|
||||||
|
if (weaponMap == null) {
|
||||||
|
const weaponMap = new Map()
|
||||||
|
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
|
||||||
|
mapBuildingWeapons.set(weapon.hardpoint, weaponMap)
|
||||||
|
} else {
|
||||||
|
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var buildingName = building.name;
|
||||||
|
if (building.name == null) {
|
||||||
|
buildingName = building.filename.replaceAll('_', ' ').replace('.rgd', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<BuildingTitle variant="h4">
|
||||||
|
{building.icon &&
|
||||||
|
<img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')} />}
|
||||||
|
{buildingName}
|
||||||
|
</BuildingTitle>
|
||||||
|
<BuildingSubtitle variant="subtitle1">
|
||||||
|
{mod.name} ({mod.version})
|
||||||
|
</BuildingSubtitle>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid2 container spacing={3}>
|
||||||
|
<Grid2 size={{ xs: 12, md: 4 }}>
|
||||||
|
<StatsPaper elevation={0}>
|
||||||
|
<Table size="small" aria-label="a dense table">
|
||||||
|
<TableBody id="unit-stats-table">
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{building.buildCostRequisition > 0 &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_requisition.gif" />
|
||||||
|
{building.buildCostRequisition.toFixed(0)}</span>}
|
||||||
|
{building.buildCostPower > 0 && <span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_power.gif" />
|
||||||
|
{building.buildCostPower.toFixed(0)}</span>}
|
||||||
|
{(building.buildCostPopulation !== undefined && building.buildCostPopulation > 0) &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_orksquadcap.gif" />
|
||||||
|
{building.buildCostPopulation.toFixed(0)}</span>}
|
||||||
|
{(building.buildCostFaith !== undefined && building.buildCostFaith > 0) &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_faith.gif" />
|
||||||
|
{building.buildCostFaith}</span>}
|
||||||
|
{(building.buildCostSouls !== undefined && building.buildCostSouls > 0) &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_souls.gif" />
|
||||||
|
{building.buildCostSouls.toFixed(0)}</span>}
|
||||||
|
{(building.buildCostTime !== undefined && building.buildCostTime > 0) &&
|
||||||
|
<span> <AvTimerOutlinedIcon
|
||||||
|
style={{ verticalAlign: "top", fontSize: "18px" }} />
|
||||||
|
{building.buildCostTime}s</span>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{(building.requisitionIncome !== undefined && building.requisitionIncome > 0 || building.powerIncome !== undefined && building.powerIncome !== null || building.faithIncome !== undefined && building.faithIncome !== null) &&
|
||||||
|
<TableRow>
|
||||||
|
<TableCell >Resource income</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{building.requisitionIncome !== undefined && building.requisitionIncome != null &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_requisition.gif" />
|
||||||
|
{building.requisitionIncome}</span>}
|
||||||
|
{building.powerIncome !== undefined && building.powerIncome > 0 &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_power.gif" />
|
||||||
|
{building.powerIncome}</span>}
|
||||||
|
{building.faithIncome !== undefined && building.faithIncome > 0 &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_faith.gif" />
|
||||||
|
{building.faithIncome}</span>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="th" scope="row" >Armor type</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{building.armorType2 == null ? <ArmorType name={building.armorType.name} compact={false} />
|
||||||
|
: <Tooltip title={"upgrade/debuff can turns to " + building.armorType2.name}><span><ArmorType name={building.armorType.name} compact={false} /></span></Tooltip>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="th" scope="row" >Health</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span><img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Health_icon.webp" />
|
||||||
|
{building.health} {building.healthRegeneration > 0 &&
|
||||||
|
<span>+{building.healthRegeneration}/s</span>} </span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="th" scope="row" >Sight</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Vision sight={building.sightRadius} detect={building.detectRadius} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||||
|
<TableCell component="th" scope="row" >Repair max</TableCell>
|
||||||
|
<TableCell>{building.repairMax}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</StatsPaper>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={{ xs: 12, md: 8 }}>
|
||||||
|
<DescriptionBox>
|
||||||
|
{building.description}
|
||||||
|
</DescriptionBox>
|
||||||
|
</Grid2>
|
||||||
|
{building.requirements !== null &&
|
||||||
|
<Grid2 size={{ xs: 12, md: 12 }}>
|
||||||
|
<Required requirement={building.requirements} modId={building.modId} raceId={building.race.id} />
|
||||||
|
</Grid2>}
|
||||||
|
{building.modifiers.length > 0 &&
|
||||||
|
<Grid2 size={{ xs: 12, md: 12 }}>
|
||||||
|
<SectionTitle>Affected on</SectionTitle>
|
||||||
|
<ModifiersProvidesTable modifiers={building.modifiers} race={building.race} modId={building.modId} affectedData={building.affectedData} />
|
||||||
|
</Grid2>}
|
||||||
|
{building.units.length > 0 && <Grid2 size={12}>
|
||||||
|
<SectionTitle>Unit production</SectionTitle>
|
||||||
|
<Grid2 container spacing={2}>
|
||||||
|
{building.units.map(unit =>
|
||||||
|
Unit(unit, mod.id, building.race.id, theme)
|
||||||
|
)}
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>}
|
||||||
|
{building.abilities.length > 0 && <Grid2 size={12}>
|
||||||
|
<SectionTitle>Abilities</SectionTitle>
|
||||||
|
{building.abilities.map(a =>
|
||||||
|
<Ability mod={mod} ability={a} race={building.race} />
|
||||||
|
)}
|
||||||
|
</Grid2>}
|
||||||
|
{building.deathExplosions.length > 0 &&
|
||||||
|
<Grid2 size={12}>
|
||||||
|
{building.deathExplosions.map(da =>
|
||||||
|
<DeathExplosion deathExplosion={da} mod={mod} />
|
||||||
|
)}
|
||||||
|
</Grid2>
|
||||||
|
}
|
||||||
|
{building.addons.length > 0 && <Grid2 size={12}>
|
||||||
|
<SectionTitle>Addons</SectionTitle>
|
||||||
|
{building.addons.map(b =>
|
||||||
|
<BuildingAddon mod={mod} addon={b} building={building} />
|
||||||
|
)}
|
||||||
|
</Grid2>}
|
||||||
|
{building.researches.length > 0 && <Grid2 size={12}>
|
||||||
|
<SectionTitle>Researches</SectionTitle>
|
||||||
|
{building.researches.map(r =>
|
||||||
|
<Research key={r.id} research={r} building={building} mod={mod} />
|
||||||
|
)}
|
||||||
|
</Grid2>}
|
||||||
|
<Grid2 size={12}>
|
||||||
|
{[...mapBuildingWeapons.keys()].sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
}).map(h => <WeaponSlot race={building.race} mod={mod} unitWeapons={mapBuildingWeapons.get(h)} hardpoint={h} />)}
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={12}>
|
||||||
|
<AffectedResearches researches={building.affectedResearches} modId={mod.id} raceId={building.race.id} />
|
||||||
|
</Grid2>
|
||||||
|
<b className="hotkey" >Hotkey: {building.hotkey} <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {building.filename}</span></b>
|
||||||
|
</Grid2>
|
||||||
|
<StyledDivider />
|
||||||
|
<UnitsTable modId={mod.id} racesUnits={racesUnits} racesBuildings={racesBuildings} />
|
||||||
|
</Box>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BuildingPageClient({
|
||||||
|
initialBuilding,
|
||||||
|
initialMod,
|
||||||
|
}: {
|
||||||
|
initialBuilding: IBuilding | null;
|
||||||
|
initialMod: IMod | null;
|
||||||
|
}) {
|
||||||
|
const params = useParams() as Record<string, string>;
|
||||||
|
const { modId, raceId, buildingId } = params;
|
||||||
|
const theme = useTheme();
|
||||||
|
const [building, setBuilding] = useState<IBuilding | null>(initialBuilding);
|
||||||
|
const [mod, setMod] = useState<IMod | null>(initialMod);
|
||||||
|
const [racesUnits, setRacesUnits] = useState<IRaceUnits[]>([]);
|
||||||
|
const [racesBuildings, setRacesBuildings] = useState<IRaceBuildings[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
if (building == null) {
|
||||||
|
fetch(AvailableBuildings + "/" + buildingId)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((res: IBuilding) => { if (!cancelled) setBuilding(res); });
|
||||||
|
}
|
||||||
|
if (mod == null) {
|
||||||
|
fetch(AvailableMods + "/" + modId)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((res: IMod) => { if (!cancelled) setMod(res); });
|
||||||
|
}
|
||||||
|
fetch(AvailableUnits + "/mod/" + modId)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((resUnits: IRaceUnits[]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setRacesUnits(resUnits);
|
||||||
|
if (resUnits.length <= 10) {
|
||||||
|
fetch(AvailableBuildings + "/mod/" + modId)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((resBuildings: IRaceBuildings[]) => {
|
||||||
|
if (!cancelled) setRacesBuildings(resBuildings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [modId, raceId, buildingId]);
|
||||||
|
|
||||||
|
if (building != null && mod != null) {
|
||||||
|
const backRef = "/mod/" + modId + "/race/" + raceId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<BackButton
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<ArrowBack />}
|
||||||
|
href={backRef}
|
||||||
|
>
|
||||||
|
Back to race
|
||||||
|
</BackButton>
|
||||||
|
{Building(building, mod, theme, racesUnits, racesBuildings)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||||
<LinearProgress sx={{ width: '200px' }} />
|
<LinearProgress sx={{ width: '200px' }} />
|
||||||
</Box>
|
</Box>
|
||||||
),
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default function BuildingPageClient() {
|
|
||||||
return <BuildingPageLegacy />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,71 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import dynamic from 'next/dynamic';
|
import React from 'react';
|
||||||
import { Box, LinearProgress } from '@mui/material';
|
import { ArrowBack } from '@mui/icons-material';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { IMod } from '@/src/types/Imod';
|
||||||
|
import { IRaceUnits } from '@/src/types/IUnitShort';
|
||||||
|
import { IRaceBuildings } from '@/src/types/IBuildingShort';
|
||||||
|
import UnitsTable from '@/src/classes/UnitsTable';
|
||||||
|
import { BackButton } from '@/src/commons/BackButton';
|
||||||
|
|
||||||
const ModPageLegacy = dynamic(() => import('@/src/legacy-pages/ModPage'), {
|
const ModNotFound = styled(Typography)(({ theme }) => ({
|
||||||
ssr: false,
|
color: theme.palette.text.secondary,
|
||||||
loading: () => (
|
}));
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
|
||||||
<LinearProgress sx={{ width: '200px' }} />
|
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function ModPageClient() {
|
interface ModPageClientProps {
|
||||||
return <ModPageLegacy />;
|
initialMod: IMod | null;
|
||||||
|
racesUnits: IRaceUnits[];
|
||||||
|
racesBuildings: IRaceBuildings[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ModPageClient({ initialMod, racesUnits, racesBuildings }: ModPageClientProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (initialMod != null) {
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<BackButton
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<ArrowBack />}
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
Back to mods list
|
||||||
|
</BackButton>
|
||||||
|
|
||||||
|
<Typography variant="h3" component="h1" sx={{
|
||||||
|
fontWeight: 800,
|
||||||
|
mb: 1,
|
||||||
|
WebkitBackgroundClip: isDark ? 'text' : 'initial',
|
||||||
|
WebkitTextFillColor: isDark ? '#dee2e6' : 'initial',
|
||||||
|
color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
||||||
|
}}>
|
||||||
|
{initialMod.name} <Box component="span" sx={{
|
||||||
|
fontSize: '0.5em',
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}>({initialMod.version})</Box>
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<UnitsTable modId={initialMod.id} racesUnits={racesUnits} racesBuildings={racesBuildings} />
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg" sx={{ py: 8, textAlign: 'center' }}>
|
||||||
|
<ModNotFound variant="h5">
|
||||||
|
Mod not found
|
||||||
|
</ModNotFound>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,248 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import dynamic from 'next/dynamic';
|
import React, { useEffect } from 'react';
|
||||||
import { Box, LinearProgress } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardActions,
|
||||||
|
Chip,
|
||||||
|
Button,
|
||||||
|
useTheme,
|
||||||
|
Link,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
import { IMod } from '@/src/types/Imod';
|
import { IMod } from '@/src/types/Imod';
|
||||||
|
|
||||||
const ModsPageLegacy = dynamic(() => import('@/src/legacy-pages/ModsPage'));
|
const ModCard = styled(Card)(({ theme }) => ({
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
background: theme.palette.mode === 'dark'
|
||||||
|
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
||||||
|
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
||||||
|
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||||
|
borderRadius: '16px',
|
||||||
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'&::before': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: '3px',
|
||||||
|
background: theme.palette.mode === 'dark'
|
||||||
|
? 'linear-gradient(90deg, #dee2e6 0%, #cccccc 100%)'
|
||||||
|
: 'linear-gradient(90deg, #000000 0%, #333333 100%)',
|
||||||
|
transform: 'scaleX(0)',
|
||||||
|
transition: 'transform 0.3s ease',
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
|
||||||
|
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
|
||||||
|
'&::before': {
|
||||||
|
transform: 'scaleX(1)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const VersionLink = styled(Link)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: theme.spacing(1.5, 2),
|
||||||
|
marginBottom: theme.spacing(1),
|
||||||
|
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'inherit',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||||
|
'&:hover': {
|
||||||
|
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
||||||
|
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ModTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 700,
|
||||||
|
mb: 1,
|
||||||
|
background: theme.palette.mode === 'dark'
|
||||||
|
? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
|
||||||
|
: 'none',
|
||||||
|
WebkitBackgroundClip: theme.palette.mode === 'dark' ? 'text' : 'initial',
|
||||||
|
WebkitTextFillColor: theme.palette.mode === 'dark' ? '#dee2e6' : 'initial',
|
||||||
|
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const VersionsChip = styled(Chip)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)',
|
||||||
|
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
||||||
|
fontWeight: 600,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const VersionText = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 600,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const VersionArrow = styled(Typography)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const BetaVersionsLabel = styled(Typography)(({ theme }) => ({
|
||||||
|
color: 'rgba(255, 193, 7, 0.9)',
|
||||||
|
fontWeight: 600,
|
||||||
|
mb: 1,
|
||||||
|
}));
|
||||||
|
|
||||||
interface ModsPageClientProps {
|
interface ModsPageClientProps {
|
||||||
initialMods: IMod[];
|
initialMods: IMod[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ModsPageClient({ initialMods }: ModsPageClientProps) {
|
export default function ModsPageClient({ initialMods }: ModsPageClientProps) {
|
||||||
return <ModsPageLegacy initialMods={initialMods} />;
|
const theme = useTheme();
|
||||||
|
|
||||||
|
// Очистка кэша сайта при заходе на главную страницу (один раз за сессию)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sessionStorage.getItem('cache_cleared')) {
|
||||||
|
sessionStorage.setItem('cache_cleared', '1');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if ('caches' in window) {
|
||||||
|
const names = await caches.keys();
|
||||||
|
await Promise.all(names.map(name => caches.delete(name)));
|
||||||
|
}
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
const regs = await navigator.serviceWorker.getRegistrations();
|
||||||
|
await Promise.all(regs.map(r => r.unregister()));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Cache clear failed:', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Sort all mods by order, then group by name
|
||||||
|
const sortedMods = [...initialMods].sort((a, b) => a.order - b.order);
|
||||||
|
|
||||||
|
const mapWithModVersions = new Map<string, IMod[]>();
|
||||||
|
sortedMods.forEach(mod => {
|
||||||
|
const versionList = mapWithModVersions.get(mod.name);
|
||||||
|
if (versionList == null) {
|
||||||
|
mapWithModVersions.set(mod.name, [mod]);
|
||||||
|
} else {
|
||||||
|
versionList.push(mod);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const getLatestVersion = (modName: string) => {
|
||||||
|
const sameMods = mapWithModVersions.get(modName) ?? [];
|
||||||
|
// The latest version is the one with the maximum id
|
||||||
|
const latest = sameMods.reduce((max, mod) => mod.id > max.id ? mod : max, sameMods[0]);
|
||||||
|
const betaMods = sameMods.filter(m => m.isBeta);
|
||||||
|
return {
|
||||||
|
latest,
|
||||||
|
allVersions: sameMods,
|
||||||
|
hasBeta: betaMods.length > 0,
|
||||||
|
betaVersion: betaMods[0],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function ModCardComponent(modName: string) {
|
||||||
|
const sameMods = mapWithModVersions.get(modName) ?? [];
|
||||||
|
const { latest, hasBeta, betaVersion } = getLatestVersion(modName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModCard>
|
||||||
|
<CardContent sx={{ flexGrow: 1, p: 3 }}>
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<ModTitle variant="h5">
|
||||||
|
{modName}
|
||||||
|
</ModTitle>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||||
|
<VersionsChip
|
||||||
|
label={`${sameMods.length} versions`}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{sameMods.filter(m => !m.isBeta).map(mod => (
|
||||||
|
<VersionLink key={mod.id} href={"/mod/" + mod.id}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||||
|
<VersionText variant="body2">
|
||||||
|
Version {mod.version}
|
||||||
|
</VersionText>
|
||||||
|
<VersionArrow variant="caption">
|
||||||
|
→
|
||||||
|
</VersionArrow>
|
||||||
|
</Box>
|
||||||
|
</VersionLink>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{hasBeta && (
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<BetaVersionsLabel variant="subtitle2">
|
||||||
|
Beta versions:
|
||||||
|
</BetaVersionsLabel>
|
||||||
|
{betaVersion && (
|
||||||
|
<VersionLink href={"/mod/" + betaVersion.id}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
Version {betaVersion.version} (Beta)
|
||||||
|
</Typography>
|
||||||
|
<VersionArrow variant="caption">
|
||||||
|
→
|
||||||
|
</VersionArrow>
|
||||||
|
</Box>
|
||||||
|
</VersionLink>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardActions sx={{ px: 3, pb: 3 }}>
|
||||||
|
<Button
|
||||||
|
size="medium"
|
||||||
|
variant="outlined"
|
||||||
|
href={"/mod/" + latest?.id}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
color: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
|
||||||
|
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 215, 0, 0.5)' : 'rgba(25, 118, 210, 0.5)',
|
||||||
|
fontWeight: 600,
|
||||||
|
borderRadius: '10px',
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: '1rem',
|
||||||
|
py: 1.5,
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
|
||||||
|
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 215, 0, 0.1)' : 'rgba(25, 118, 210, 0.1)',
|
||||||
|
boxShadow: theme.palette.mode === 'dark' ? '0 8px 20px rgba(255, 215, 0, 0.2)' : '0 8px 20px rgba(25, 118, 210, 0.2)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open latest {latest?.version ?? ''}
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
</ModCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{[...new Set(sortedMods.map(m => m.name))].map((modName) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4}>
|
||||||
|
{ModCardComponent(modName)}
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,262 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import dynamic from 'next/dynamic';
|
import React from 'react';
|
||||||
import { Box, LinearProgress } from '@mui/material';
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Grid2,
|
||||||
|
Link,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
Theme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { ArrowBack, ExpandMore } from '@mui/icons-material';
|
||||||
|
import { IconUrl } from '@/src/core/api';
|
||||||
|
import { IMod } from '@/src/types/Imod';
|
||||||
|
import { Irace } from '@/src/types/Irace';
|
||||||
|
import '@/src/css/Unit.css';
|
||||||
|
import { IRaceUnits, IUnitShort } from '@/src/types/IUnitShort';
|
||||||
|
import { IBuildingShort, IRaceBuildings } from '@/src/types/IBuildingShort';
|
||||||
|
import { StyledLink } from '@/src/commons/StyledLink';
|
||||||
|
import { BackButton } from '@/src/commons/BackButton';
|
||||||
|
|
||||||
const RacePageLegacy = dynamic(() => import('@/src/legacy-pages/RacePageFast'), {
|
const SectionTitle = styled(Typography)(({ theme }) => ({
|
||||||
ssr: false,
|
fontWeight: 700,
|
||||||
loading: () => (
|
mb: 2,
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
fontSize: '1.25rem',
|
||||||
<LinearProgress sx={{ width: '200px' }} />
|
}));
|
||||||
</Box>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function RacePageClient() {
|
const UnitCard = styled(Paper)(({ theme }) => ({
|
||||||
return <RacePageLegacy />;
|
background: theme.palette.mode === 'dark'
|
||||||
|
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
||||||
|
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
||||||
|
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||||
|
borderRadius: '16px',
|
||||||
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
|
||||||
|
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const BuildingCard = styled(UnitCard)(({ theme }) => ({
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const UnitLink = styled(Link)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
textDecoration: 'none',
|
||||||
|
display: 'block',
|
||||||
|
'&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
const LoadingText = styled(Typography)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function Unit(unit: IUnitShort, modId: number, raceId: string, theme: Theme) { const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnitLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
|
||||||
|
<ListItem sx={{
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
padding: '1.5rem 2rem',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
background: isDark
|
||||||
|
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
||||||
|
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
border: `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||||
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: isDark ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
|
||||||
|
borderColor: isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')} />}
|
||||||
|
{unit.name}
|
||||||
|
{unit.canDetect && <span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/DETECT_YES.webp" /></span>}
|
||||||
|
</ListItem>
|
||||||
|
</UnitLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnitSmall(unit: IUnitShort, modId: number, raceId: string) {
|
||||||
|
var unitName = ""
|
||||||
|
if (unit.name.length > 21) {
|
||||||
|
unitName = unit.name.substring(0, 19) + "...";
|
||||||
|
} else {
|
||||||
|
unitName = unit.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} >
|
||||||
|
{unit.icon && <img className="unitIconSmall" src={IconUrl + unit.icon.replaceAll('\\', '/')} />}
|
||||||
|
<span style={{ fontSize: 14 }}>{unitName}</span>
|
||||||
|
{unit.canDetect && <span> <img
|
||||||
|
src="/images/DETECT_YES.webp" /></span>}<br /></StyledLink>)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Building(building: IBuildingShort, modId: number, raceId: string, theme: Theme) {
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
return (<Grid2 size={{ xs: 12, md: 3 }}>
|
||||||
|
<Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id} sx={{
|
||||||
|
textDecoration: 'none',
|
||||||
|
display: 'block',
|
||||||
|
'&:hover': { color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }
|
||||||
|
}}>
|
||||||
|
<BuildingCard elevation={0}>
|
||||||
|
<ListItem sx={{}}>
|
||||||
|
{building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')} />}
|
||||||
|
<StyledLink>{building.name}</StyledLink>
|
||||||
|
{building.canDetect && <span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/DETECT_YES.webp" /></span>}
|
||||||
|
</ListItem>
|
||||||
|
{building.units.map(unit => UnitSmall(unit, modId, raceId))}
|
||||||
|
</BuildingCard>
|
||||||
|
</Link>
|
||||||
|
</Grid2>)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnitsProps {
|
||||||
|
raceId: string;
|
||||||
|
modId: number;
|
||||||
|
units: IRaceUnits | null;
|
||||||
|
buildings: IRaceBuildings | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Units({ raceId, modId, units, buildings }: UnitsProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
if (units) {
|
||||||
|
const accordionSx = isDark ? {
|
||||||
|
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
'&:before': { display: 'none' },
|
||||||
|
'&.Mui-expanded': { margin: '0 0 8px 0' },
|
||||||
|
} : {
|
||||||
|
background: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
||||||
|
border: '1px solid rgba(0, 0, 0, 0.1)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
'&:before': { display: 'none' },
|
||||||
|
'&.Mui-expanded': { margin: '0 0 8px 0' },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (buildings != null ?
|
||||||
|
<Box>
|
||||||
|
<Grid2 container spacing={2}>
|
||||||
|
{buildings.buildings.map(building => Building(building, modId, raceId, theme))}
|
||||||
|
</Grid2>
|
||||||
|
{buildings.buildingsAdvanced.length > 0 && <Box sx={{ mt: 3 }}>
|
||||||
|
<Divider sx={{ my: 2, borderColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)' }} />
|
||||||
|
<SectionTitle>Advanced buildings</SectionTitle>
|
||||||
|
<Grid2 container spacing={2}>
|
||||||
|
{buildings.buildingsAdvanced.map(building => Building(building, modId, raceId, theme))}
|
||||||
|
</Grid2><br /></Box>}
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<Accordion sx={accordionSx}>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMore sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }} />}
|
||||||
|
aria-controls="units-accordion"
|
||||||
|
id="units-accordion"
|
||||||
|
sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' } }}
|
||||||
|
>
|
||||||
|
<SectionTitle sx={{ mb: 0, display: 'flex', alignItems: 'center' }}>All units</SectionTitle>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Grid2 container spacing={2}>
|
||||||
|
<Grid2 size={{ xs: 12, md: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', fontWeight: 600, mb: 2 }}>Infantry</Typography>
|
||||||
|
{units.infantry.map(unit => Unit(unit, modId, raceId, theme))}
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={{ xs: 12, md: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', fontWeight: 600, mb: 2 }}>Tech</Typography>
|
||||||
|
<List>
|
||||||
|
{units.tech.map(unit => Unit(unit, modId, raceId, theme))}
|
||||||
|
</List>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={{ xs: 12, md: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', fontWeight: 600, mb: 2 }}>Support</Typography>
|
||||||
|
<List>
|
||||||
|
{units.support.map(unit => Unit(unit, modId, raceId, theme))}
|
||||||
|
</List>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</Box>
|
||||||
|
</Box> : <LoadingText>Loading</LoadingText>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return <LoadingText>Loading...</LoadingText>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RacePageClientProps {
|
||||||
|
initialMod: IMod | null;
|
||||||
|
initialRace: Irace | null;
|
||||||
|
raceUnits: IRaceUnits | null;
|
||||||
|
raceBuildings: IRaceBuildings | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RacePageClient({ initialMod, initialRace, raceUnits, raceBuildings }: RacePageClientProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (initialMod != null && initialRace != null) {
|
||||||
|
const backRef = "/mod/" + initialMod.id;
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<BackButton
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<ArrowBack />}
|
||||||
|
href={backRef}
|
||||||
|
>
|
||||||
|
Back to mod
|
||||||
|
</BackButton>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<Typography variant="h3" component="h1" sx={{
|
||||||
|
fontWeight: 800,
|
||||||
|
mb: 1,
|
||||||
|
background: isDark
|
||||||
|
? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
|
||||||
|
: 'none',
|
||||||
|
WebkitBackgroundClip: isDark ? 'text' : 'initial',
|
||||||
|
WebkitTextFillColor: isDark ? '#dee2e6' : 'initial',
|
||||||
|
color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
||||||
|
}}>
|
||||||
|
{initialRace.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" sx={{
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}>
|
||||||
|
{initialMod.name} ({initialMod.version})
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Units raceId={initialRace.id} modId={initialMod.id} units={raceUnits} buildings={raceBuildings} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<LoadingText variant="h5" sx={{ py: 8, textAlign: 'center' }}>
|
||||||
|
Race not found
|
||||||
|
</LoadingText>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,443 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import dynamic from 'next/dynamic';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Box, LinearProgress } from '@mui/material';
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Grid2,
|
||||||
|
LinearProgress,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
Theme,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { ArrowBack, ExpandMore } from '@mui/icons-material';
|
||||||
|
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
|
||||||
|
import { AvailableBuildings, AvailableMods, AvailableUnits, IconUrl } from '@/src/core/api';
|
||||||
|
import { IShortWeapon, IUnit } from '@/src/types/IUnit';
|
||||||
|
import { IRaceUnits } from '@/src/types/IUnitShort';
|
||||||
|
import { IRaceBuildings } from '@/src/types/IBuildingShort';
|
||||||
|
import '@/src/css/Unit.css';
|
||||||
|
import ArmorType from '@/src/classes/ArmorType';
|
||||||
|
import Sergeant from '@/src/classes/Sergeant';
|
||||||
|
import WeaponSlot from '@/src/classes/WeaponSlot';
|
||||||
|
import UnitsTable from '@/src/classes/UnitsTable';
|
||||||
|
import { IMod } from '@/src/types/Imod';
|
||||||
|
import Vision from '@/src/classes/Vision';
|
||||||
|
import Required from '@/src/classes/Required';
|
||||||
|
import { AffectedResearches } from '@/src/classes/building/Research';
|
||||||
|
import { ModifiersProvidesTable } from '@/src/classes/ModifiersProvideTable';
|
||||||
|
import Ability from '@/src/classes/Ability';
|
||||||
|
import DeathExplosion from '@/src/classes/DeathExplosion';
|
||||||
|
import Jump from '@/src/classes/Jump';
|
||||||
|
import { DescriptionBox } from '@/src/commons/DescriptionBox';
|
||||||
|
import { BackButton } from '@/src/commons/BackButton';
|
||||||
|
|
||||||
const UnitPageLegacy = dynamic(() => import('@/src/legacy-pages/UnitPage'), {
|
const SectionTitle = styled(Typography)(({ theme }) => ({
|
||||||
ssr: false,
|
fontWeight: 700,
|
||||||
loading: () => (
|
mb: 2,
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StatsPaper = styled(Paper)(({ theme }) => ({
|
||||||
|
background: theme.palette.mode === 'dark'
|
||||||
|
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
||||||
|
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
||||||
|
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||||
|
borderRadius: '12px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'& .MuiTableBody .MuiTableRow-root': {
|
||||||
|
'&:last-child td, &:last-child th': { border: 0 },
|
||||||
|
},
|
||||||
|
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
|
||||||
|
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SergeantAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
|
background: theme.palette.mode === 'dark'
|
||||||
|
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
||||||
|
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
||||||
|
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
||||||
|
borderRadius: '12px',
|
||||||
|
mb: 1,
|
||||||
|
'&:before': { display: 'none' },
|
||||||
|
'&.Mui-expanded': { margin: '0 0 8px 0' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
|
my: 3,
|
||||||
|
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const UnitTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 800,
|
||||||
|
mb: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 2,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const UnitSubtitle = styled(Typography)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontWeight: 500,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function Unit(unit: IUnit, mod: IMod, theme: Theme, racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[]) {
|
||||||
|
const isDark = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
const morale = (unit.moraleMax !== null) ? <span>
|
||||||
|
<img style={{ verticalAlign: "top" }} src="/images/ARM_Morale.webp" /> {unit.moraleMax}
|
||||||
|
+{unit.moraleRegeneration}/s
|
||||||
|
{unit.moraleDeathPenalty > 0 && <span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Kills_icon.webp" /> -{unit.moraleDeathPenalty}</span>}
|
||||||
|
</span> : "-"
|
||||||
|
|
||||||
|
let mapWithUnitWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
|
||||||
|
|
||||||
|
unit.weapons.forEach(weapon => {
|
||||||
|
const weaponMap = mapWithUnitWeapons.get(weapon.hardpoint)
|
||||||
|
if (weaponMap == null) {
|
||||||
|
const weaponMap = new Map()
|
||||||
|
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
|
||||||
|
mapWithUnitWeapons.set(weapon.hardpoint, weaponMap)
|
||||||
|
} else {
|
||||||
|
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
type sergeantProps = {
|
||||||
|
name: string
|
||||||
|
icon: String
|
||||||
|
canDetect: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const SergeantShort = (props: sergeantProps) => {
|
||||||
|
return (
|
||||||
|
<span style={{ fontSize: 20, color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', display: 'flex', alignItems: 'center' }}>
|
||||||
|
{props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')} />}
|
||||||
|
{props.name}
|
||||||
|
{props.canDetect && <span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/DETECT_YES.webp" /></span>}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box sx={{ mb: 3 }}>
|
||||||
|
<UnitTitle variant="h4">
|
||||||
|
{unit.icon &&
|
||||||
|
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')} />}
|
||||||
|
{unit.name}
|
||||||
|
</UnitTitle>
|
||||||
|
<UnitSubtitle variant="subtitle1">
|
||||||
|
{mod.name} ({mod.version})
|
||||||
|
</UnitSubtitle>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid2 container spacing={3}>
|
||||||
|
<Grid2 size={{ xs: 12, md: 4 }}>
|
||||||
|
<StatsPaper>
|
||||||
|
<Table size="small" aria-label="a dense table">
|
||||||
|
<TableBody id="unit-stats-table">
|
||||||
|
<TableRow>
|
||||||
|
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{unit.buildCostRequisition > 0 &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_requisition.gif" />
|
||||||
|
{unit.buildCostRequisition.toFixed(0)}</span>}
|
||||||
|
{unit.buildCostPower > 0 && <span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_power.gif" />
|
||||||
|
{unit.buildCostPower.toFixed(0)}</span>}
|
||||||
|
{(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_orksquadcap.gif" />
|
||||||
|
{unit.buildCostPopulation.toFixed(0)}</span>}
|
||||||
|
{(unit.buildCostFaith !== undefined && unit.buildCostFaith > 0) &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_faith.gif" />
|
||||||
|
{unit.buildCostFaith}</span>}
|
||||||
|
{(unit.buildCostSouls !== undefined && unit.buildCostSouls > 0) &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_souls.gif" />
|
||||||
|
{unit.buildCostSouls.toFixed(0)}</span>}
|
||||||
|
{unit.capInfantry > 0 && <span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_cap_infantry.gif" />
|
||||||
|
{unit.capInfantry}</span>}
|
||||||
|
{unit.capSupport > 0 && <span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_cap_vehicle.gif" />
|
||||||
|
{unit.capSupport}</span>}
|
||||||
|
{(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
|
||||||
|
<span> <AvTimerOutlinedIcon
|
||||||
|
style={{ verticalAlign: "top", fontSize: "18px" }} />
|
||||||
|
{unit.buildCostTime}s</span>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{(unit?.reinforceTime !== 0 && unit.reinforceTime !== null && unit.squadMaxSize > 1) &&
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Reinforce cost</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{unit.reinforceCostRequisition && unit.reinforceCostRequisition > 0 ?
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_requisition.gif" />
|
||||||
|
{unit.reinforceCostRequisition.toFixed(0)}</span> : <span />}
|
||||||
|
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_power.gif" />
|
||||||
|
{unit.reinforceCostPower.toFixed(0)}</span>}
|
||||||
|
{(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_orksquadcap.gif" />
|
||||||
|
{unit.reinforceCostPopulation.toFixed(0)}</span>}
|
||||||
|
{(unit.reinforceCostFaith !== undefined && unit.reinforceCostFaith > 0) &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_faith.gif" />
|
||||||
|
{unit.reinforceCostFaith}</span>}
|
||||||
|
{(unit.reinforceCostSouls !== undefined && unit.reinforceCostSouls > 0) &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_souls.gif" />
|
||||||
|
{unit.reinforceCostSouls.toFixed(0)}</span>}
|
||||||
|
{(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
|
||||||
|
<span> <AvTimerOutlinedIcon
|
||||||
|
style={{ verticalAlign: "top", fontSize: "18px" }} />
|
||||||
|
{unit.reinforceTime}s</span>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
{(unit.requisitionIncome !== undefined && unit.requisitionIncome !== null || unit.powerIncome !== undefined && unit.powerIncome !== null || unit.faithIncome !== undefined && unit.faithIncome !== null) &&
|
||||||
|
<TableRow>
|
||||||
|
<TableCell >Resource income</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{unit.requisitionIncome !== undefined && unit.requisitionIncome > 0 &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_requisition.gif" />
|
||||||
|
{unit.requisitionIncome}</span>}
|
||||||
|
{unit.powerIncome !== undefined && unit.powerIncome > 0 &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_power.gif" />
|
||||||
|
{unit.powerIncome}</span>}
|
||||||
|
{unit.faithIncome !== undefined && unit.faithIncome > 0 &&
|
||||||
|
<span> <img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Resource_faith.gif" />
|
||||||
|
{unit.faithIncome}</span>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
{unit.squadMaxSize > 1 &&
|
||||||
|
<TableRow>
|
||||||
|
<TableCell >Squad size</TableCell>
|
||||||
|
<TableCell>{unit.squadStartSize} / {unit.squadMaxSize}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell >Armor type</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{unit.armorType2 == null ? <ArmorType name={unit.armorType.name} compact={false} /> : <Tooltip title={"upgrade/debuff can turns to " + unit.armorType2.name}><span><ArmorType name={unit.armorType.name} compact={false} /></span></Tooltip>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell >Health</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<img style={{ verticalAlign: "top" }}
|
||||||
|
src="/images/Health_icon.webp" />
|
||||||
|
{unit.health} {unit.healthRegeneration > 0 &&
|
||||||
|
<span>+{unit.healthRegeneration}/s</span>}
|
||||||
|
{unit.armour !== undefined && unit.armour !== 0 && <span><img style={{ height: 20, verticalAlign: "top" }} src="/images/defence.png" />{unit.armour} </span>}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell >Move speed</TableCell>
|
||||||
|
<TableCell>{unit.moveSpeed}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell >Morale</TableCell>
|
||||||
|
<TableCell>{morale}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell >Mass</TableCell>
|
||||||
|
<TableCell>{unit.mass}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||||
|
<TableCell >Vision</TableCell>
|
||||||
|
<TableCell><Vision sight={unit.sightRadius} detect={unit.detectRadius} /></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{unit.repairMax !== undefined && unit.repairMax !== null &&
|
||||||
|
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||||
|
<TableCell >Repair max</TableCell>
|
||||||
|
<TableCell>{unit.repairMax}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
{unit.repairSpeed !== undefined && unit.repairSpeed !== null && unit.repairCostPercent !== null &&
|
||||||
|
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||||
|
<TableCell >Repair</TableCell>
|
||||||
|
<TableCell>{unit.repairSpeed} hp/s; {unit.repairCostPercent}% cost</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
{unit.mobValue != null &&
|
||||||
|
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||||
|
<TableCell >Mob value</TableCell>
|
||||||
|
<TableCell><span> <img style={{ height: 20, verticalAlign: "top" }} src="/images/Mob_bonus.gif" /> {unit.mobValue} </span></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
{unit.squadLimit !== undefined && unit.squadLimit !== null &&
|
||||||
|
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||||
|
<TableCell >Limit</TableCell>
|
||||||
|
<TableCell>{unit.squadLimit}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</StatsPaper>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={{ xs: 12, md: 8 }}>
|
||||||
|
<DescriptionBox>
|
||||||
|
{unit.description}
|
||||||
|
</DescriptionBox>
|
||||||
|
</Grid2>
|
||||||
|
{unit.requirements !== null &&
|
||||||
|
<Grid2 size={{ xs: 12, md: 12 }}>
|
||||||
|
<Required requirement={unit.requirements} modId={unit.modId} raceId={unit.race.id} />
|
||||||
|
</Grid2>}
|
||||||
|
<Grid2 size={12}>
|
||||||
|
{unit.sergeants.map(s =>
|
||||||
|
<SergeantAccordion key={s.id} TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
|
||||||
|
<AccordionSummary
|
||||||
|
sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' } }}>
|
||||||
|
<SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0} />
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails sx={{ color: theme.palette.text.primary }}>
|
||||||
|
<Sergeant mod={mod} sergeant={s} race={unit.race} />
|
||||||
|
</AccordionDetails>
|
||||||
|
</SergeantAccordion>)}
|
||||||
|
</Grid2>
|
||||||
|
|
||||||
|
{unit.moraleMax !== null && !(unit.moraleBrakeModifiers.find(m => m.reference.includes("accuracy_weapon_modifier") && m.value === 0.2) !== undefined &&
|
||||||
|
unit.moraleBrakeModifiers.find(m => m.reference.includes("speed_maximum_modifier") && m.value === 1.2) !== undefined &&
|
||||||
|
unit.moraleBrakeModifiers.length === 2)
|
||||||
|
&& <Grid2 size={12}>
|
||||||
|
<SectionTitle><img style={{ verticalAlign: "top" }} src="/images/ARM_Morale.webp" /> Morale broken <img style={{ verticalAlign: "top" }} src="/images/ARM_Morale.webp" /></SectionTitle>
|
||||||
|
<ModifiersProvidesTable modifiers={unit.moraleBrakeModifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData} />
|
||||||
|
</Grid2>}
|
||||||
|
|
||||||
|
{unit.modifiers.length > 0 && <Grid2 size={12}>
|
||||||
|
<SectionTitle>Affected on</SectionTitle>
|
||||||
|
<ModifiersProvidesTable modifiers={unit.modifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData} />
|
||||||
|
</Grid2>}
|
||||||
|
|
||||||
|
{unit.abilities.length > 0 && <Grid2 size={12}>
|
||||||
|
<SectionTitle>Abilities</SectionTitle>
|
||||||
|
{unit.abilities.map(a =>
|
||||||
|
<Ability mod={mod} ability={a} race={unit.race} />
|
||||||
|
)}
|
||||||
|
</Grid2>}
|
||||||
|
|
||||||
|
{unit.jumps != null && <Grid2 size={12}>
|
||||||
|
<Jump jumps={unit.jumps} race={unit.race} mod={mod} />
|
||||||
|
</Grid2>}
|
||||||
|
|
||||||
|
<Grid2 size={12}>
|
||||||
|
{[...mapWithUnitWeapons.keys()].sort(function (a, b) {
|
||||||
|
return a - b;
|
||||||
|
}).map(h => <WeaponSlot haveReinforceMenu={unit.haveReinforceMenu} key={h} race={unit.race} mod={mod} unitWeapons={mapWithUnitWeapons.get(h)} hardpoint={h} />)}
|
||||||
|
</Grid2>
|
||||||
|
{unit.deathExplosions.length > 0 &&
|
||||||
|
<Grid2 size={12}>
|
||||||
|
{unit.deathExplosions.map(da =>
|
||||||
|
<DeathExplosion deathExplosion={da} mod={mod} />
|
||||||
|
)}
|
||||||
|
</Grid2>
|
||||||
|
}
|
||||||
|
<Grid2 size={12}>
|
||||||
|
<AffectedResearches researches={unit.affectedResearches} modId={mod.id} raceId={unit.race.id} />
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, mb: 2, fontWeight: 600, fontSize: '0.85rem' }}>
|
||||||
|
Hotkey: {unit.hotkey} <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {unit.filename}</span>
|
||||||
|
</Box>
|
||||||
|
<StyledDivider />
|
||||||
|
<UnitsTable modId={mod.id} racesUnits={racesUnits} racesBuildings={racesBuildings} />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UnitPageClient({
|
||||||
|
initialUnit,
|
||||||
|
initialMod,
|
||||||
|
}: {
|
||||||
|
initialUnit: IUnit | null;
|
||||||
|
initialMod: IMod | null;
|
||||||
|
}) {
|
||||||
|
const params = useParams() as Record<string, string>;
|
||||||
|
const { modId, raceId, unitId } = params;
|
||||||
|
const theme = useTheme();
|
||||||
|
const [unit, setUnit] = useState<IUnit | null>(initialUnit);
|
||||||
|
const [mod, setMod] = useState<IMod | null>(initialMod);
|
||||||
|
const [racesUnits, setRacesUnits] = useState<IRaceUnits[]>([]);
|
||||||
|
const [racesBuildings, setRacesBuildings] = useState<IRaceBuildings[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
if (unit == null) {
|
||||||
|
fetch(AvailableUnits + "/" + unitId)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((res: IUnit) => { if (!cancelled) setUnit(res); });
|
||||||
|
}
|
||||||
|
if (mod == null) {
|
||||||
|
fetch(AvailableMods + "/" + modId)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((res: IMod) => { if (!cancelled) setMod(res); });
|
||||||
|
}
|
||||||
|
fetch(AvailableUnits + "/mod/" + modId)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((resUnits: IRaceUnits[]) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setRacesUnits(resUnits);
|
||||||
|
if (resUnits.length <= 10) {
|
||||||
|
fetch(AvailableBuildings + "/mod/" + modId)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((resBuildings: IRaceBuildings[]) => {
|
||||||
|
if (!cancelled) setRacesBuildings(resBuildings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [modId, raceId, unitId]);
|
||||||
|
|
||||||
|
if (unit != null && mod != null) {
|
||||||
|
const backRef = "/mod/" + modId + "/race/" + raceId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="lg">
|
||||||
|
<BackButton
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<ArrowBack />}
|
||||||
|
href={backRef}
|
||||||
|
>
|
||||||
|
Back to race
|
||||||
|
</BackButton>
|
||||||
|
{Unit(unit, mod, theme, racesUnits, racesBuildings)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
||||||
<LinearProgress sx={{ width: '200px' }} />
|
<LinearProgress sx={{ width: '200px' }} />
|
||||||
</Box>
|
</Box>
|
||||||
),
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
export default function UnitPageClient() {
|
|
||||||
return <UnitPageLegacy />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,11 @@
|
|||||||
// Runs only on the server (Node.js) — uses NEXT_PUBLIC_HOST_URL which is inlined at build time.
|
// Runs only on the server (Node.js) — uses NEXT_PUBLIC_HOST_URL which is inlined at build time.
|
||||||
|
|
||||||
import { IMod } from '@/src/types/Imod';
|
import { IMod } from '@/src/types/Imod';
|
||||||
|
import { Irace } from '@/src/types/Irace';
|
||||||
|
import { IRaceUnits } from '@/src/types/IUnitShort';
|
||||||
|
import { IRaceBuildings } from '@/src/types/IBuildingShort';
|
||||||
|
import { IUnit } from '@/src/types/IUnit';
|
||||||
|
import { IBuilding } from '@/src/types/IBuilding';
|
||||||
|
|
||||||
const API = process.env.NEXT_PUBLIC_HOST_URL || '';
|
const API = process.env.NEXT_PUBLIC_HOST_URL || '';
|
||||||
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://dow-wiki.example.com';
|
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://dow-wiki.example.com';
|
||||||
@ -17,19 +22,19 @@ async function fetchJson<T>(url: string): Promise<T | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getMod(modId: string) {
|
export async function getMod(modId: string) {
|
||||||
return fetchJson<{ name: string; version: string }>(`${API}/api/v1/mods/${modId}`);
|
return fetchJson<IMod>(`${API}/api/v1/mods/${modId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRace(raceId: string) {
|
export async function getRace(raceId: string) {
|
||||||
return fetchJson<{ name: string }>(`${API}/api/v1/races/${raceId}`);
|
return fetchJson<Irace>(`${API}/api/v1/races/${raceId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUnit(unitId: string) {
|
export async function getUnit(unitId: string) {
|
||||||
return fetchJson<{ name: string; description: string }>(`${API}/api/v1/units/${unitId}`);
|
return fetchJson<IUnit>(`${API}/api/v1/units/${unitId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBuilding(buildingId: string) {
|
export async function getBuilding(buildingId: string) {
|
||||||
return fetchJson<{ name: string; filename: string }>(`${API}/api/v1/buildings/${buildingId}`);
|
return fetchJson<IBuilding>(`${API}/api/v1/buildings/${buildingId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMods() {
|
export async function getMods() {
|
||||||
@ -41,9 +46,17 @@ export async function getRacesForMod(modId: string | number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getUnitsForMod(modId: string | number) {
|
export async function getUnitsForMod(modId: string | number) {
|
||||||
return fetchJson<Array<{ race: { id: string }; infantry: Array<{ id: number }>; support: Array<{ id: number }>; tech: Array<{ id: number }> }>>(`${API}/api/v1/units/mod/${modId}`);
|
return fetchJson<IRaceUnits[]>(`${API}/api/v1/units/mod/${modId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBuildingsForMod(modId: string | number) {
|
export async function getBuildingsForMod(modId: string | number) {
|
||||||
return fetchJson<Array<{ race: { id: string }; buildings: Array<{ id: number }>; buildingsAdvanced: Array<{ id: number }> }>>(`${API}/api/v1/buildings/mod/${modId}`);
|
return fetchJson<IRaceBuildings[]>(`${API}/api/v1/buildings/mod/${modId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRaceUnits(modId: string | number, raceId: string) {
|
||||||
|
return fetchJson<IRaceUnits>(`${API}/api/v1/units/${modId}/${raceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRaceBuildings(modId: string | number, raceId: string) {
|
||||||
|
return fetchJson<IRaceBuildings>(`${API}/api/v1/buildings/${modId}/${raceId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React from "react";
|
||||||
import {AvailableBuildings, AvailableUnits} from "../core/api";
|
import {Grid2, Table, TableBody, TableCell, TableHead, TableRow} from "@mui/material";
|
||||||
import {Grid2, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper} from "@mui/material";
|
|
||||||
import {styled} from '@mui/material/styles';
|
import {styled} from '@mui/material/styles';
|
||||||
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
|
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
|
||||||
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
|
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
|
||||||
@ -37,13 +36,13 @@ interface IUnitsTable {
|
|||||||
racesBuildings: IRaceBuildings[]
|
racesBuildings: IRaceBuildings[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function UnitsTable(prop: {modId: number}) {
|
export default function UnitsTable(prop: {modId: number, racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[]}) {
|
||||||
|
|
||||||
|
|
||||||
const [unitsTable, setUnitsTable] = useState<IUnitsTable>({
|
const unitsTable: IUnitsTable = {
|
||||||
racesUnits: [],
|
racesUnits: prop.racesUnits,
|
||||||
racesBuildings: []
|
racesBuildings: prop.racesBuildings
|
||||||
});
|
};
|
||||||
|
|
||||||
|
|
||||||
function getUnitRef(modId: number, raceId: string, unit: IUnitShort) {
|
function getUnitRef(modId: number, raceId: string, unit: IUnitShort) {
|
||||||
@ -62,29 +61,6 @@ export default function UnitsTable(prop: {modId: number}) {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
fetch(AvailableUnits + "/mod/" + prop.modId)
|
|
||||||
.then(resUnits => resUnits.json())
|
|
||||||
.then((resUnits: IRaceUnits[]) => {
|
|
||||||
if(resUnits.length <= 10){
|
|
||||||
fetch(AvailableBuildings + "/mod/" + prop.modId)
|
|
||||||
.then(resBuildings => resBuildings.json())
|
|
||||||
.then((resBuildings: IRaceBuildings[]) => {
|
|
||||||
setUnitsTable({
|
|
||||||
racesUnits : resUnits,
|
|
||||||
racesBuildings: resBuildings
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}else {
|
|
||||||
setUnitsTable({
|
|
||||||
racesUnits : resUnits,
|
|
||||||
racesBuildings: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function armourTypePriority(armorTypeId: string): number {
|
function armourTypePriority(armorTypeId: string): number {
|
||||||
switch(armorTypeId) {
|
switch(armorTypeId) {
|
||||||
case 'Infantry Low':
|
case 'Infantry Low':
|
||||||
|
|||||||
@ -11,23 +11,32 @@ interface ThemeContextType {
|
|||||||
toggleTheme: () => void;
|
toggleTheme: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ThemeProviderProps {
|
||||||
|
initialMode?: ThemeMode;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 год
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
/**
|
||||||
const [mode, setMode] = useState<ThemeMode>('dark');
|
* Синхронизирует выбранную тему с <html>, cookie и localStorage.
|
||||||
|
* Cookie нужен, чтобы сервер (SSR) сразу отрендерил правильную тему без мерцания.
|
||||||
useEffect(() => {
|
*/
|
||||||
const saved = localStorage.getItem('theme');
|
function applyModeToDocument(mode: ThemeMode) {
|
||||||
if (saved === 'light') {
|
document.cookie = `theme=${mode}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; SameSite=Lax`;
|
||||||
setMode('light');
|
|
||||||
}
|
}
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
export const ThemeProvider = ({ initialMode = 'dark', children }: ThemeProviderProps) => {
|
||||||
|
const [mode, setMode] = useState<ThemeMode>(initialMode);
|
||||||
|
|
||||||
|
// Применяем тему к <html> и синхронизируем с cookie/localStorage.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('theme', mode);
|
applyModeToDocument(mode);
|
||||||
}, [mode]);
|
}, [mode]);
|
||||||
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
setMode(prev => prev === 'dark' ? 'light' : 'dark');
|
setMode(prev => prev === 'dark' ? 'light' : 'dark');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
--scrollbar-track: #1a1a2e;
|
--scrollbar-track: #1a1a2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-theme {
|
html[data-theme="light"] {
|
||||||
--bg-primary: #f5f5f5;
|
--bg-primary: #f5f5f5;
|
||||||
--bg-secondary: #ffffff;
|
--bg-secondary: #ffffff;
|
||||||
--bg-tertiary: #e8e8e8;
|
--bg-tertiary: #e8e8e8;
|
||||||
@ -41,7 +41,7 @@ body {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.light-theme code {
|
html[data-theme="light"] code {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,344 +0,0 @@
|
|||||||
import {AvailableBuildings, AvailableMods, IconUrl} from "../core/api";
|
|
||||||
import React from "react";
|
|
||||||
import {withRouter} from "../core/withrouter";
|
|
||||||
import {IShortWeapon} from "../types/IUnit";
|
|
||||||
import '../css/Building.css'
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Box,
|
|
||||||
Container,
|
|
||||||
Divider,
|
|
||||||
Grid2,
|
|
||||||
Link,
|
|
||||||
ListItem,
|
|
||||||
Paper,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableRow,
|
|
||||||
Tooltip,
|
|
||||||
Typography, LinearProgress, Theme,
|
|
||||||
} from "@mui/material";
|
|
||||||
import ArmorType from "../classes/ArmorType";
|
|
||||||
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
|
|
||||||
import WeaponSlot from "../classes/WeaponSlot";
|
|
||||||
import UnitsTable from "../classes/UnitsTable";
|
|
||||||
import {IMod} from "../types/Imod";
|
|
||||||
import {IBuilding} from "../types/IBuilding";
|
|
||||||
import Vision from "../classes/Vision";
|
|
||||||
import BuildingAddon from "../classes/building/BuildingAddon";
|
|
||||||
import {IUnitShort} from "../types/IUnitShort";
|
|
||||||
import Research, {AffectedResearches} from "../classes/building/Research";
|
|
||||||
import Required from "../classes/Required";
|
|
||||||
import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
|
|
||||||
import Ability from "../classes/Ability";
|
|
||||||
import DeathExplosion from "../classes/DeathExplosion";
|
|
||||||
import {styled} from '@mui/material/styles';
|
|
||||||
import {withTheme} from "../core/api";
|
|
||||||
import {DescriptionBox} from "../commons/DescriptionBox";
|
|
||||||
import {ArrowBack} from "@mui/icons-material";
|
|
||||||
import {BackButton} from "../commons/BackButton";
|
|
||||||
|
|
||||||
|
|
||||||
const SectionTitle = styled(Typography)(({ theme }) => ({
|
|
||||||
fontWeight: 700,
|
|
||||||
mb: 2,
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StatsPaper = styled(Paper)(({ theme }) => ({
|
|
||||||
background: theme.palette.mode === 'dark'
|
|
||||||
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
|
||||||
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
|
||||||
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
|
||||||
borderRadius: '12px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
|
|
||||||
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDivider = styled(Divider)(({ theme }) => ({
|
|
||||||
my: 3,
|
|
||||||
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const BuildingTitle = styled(Typography)(({ theme }) => ({
|
|
||||||
fontWeight: 800,
|
|
||||||
mb: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 2,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const BuildingSubtitle = styled(Typography)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
fontWeight: 500,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const UnitLink = styled(Link)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
textDecoration: 'none',
|
|
||||||
'&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
const UnitListItem = styled(ListItem)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
padding: '8px 12px',
|
|
||||||
background: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'}`,
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
'&:hover': { background: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)', borderColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface UintPageState {
|
|
||||||
building: IBuilding,
|
|
||||||
mod: IMod,
|
|
||||||
}
|
|
||||||
|
|
||||||
function Unit (unit: IUnitShort, modId: number, raceId: String, theme: Theme) {
|
|
||||||
|
|
||||||
return (<Grid2 key={unit.id} size={{xs: 12, md: 3}}>
|
|
||||||
<UnitLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
|
|
||||||
<UnitListItem>
|
|
||||||
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> }
|
|
||||||
{unit.name}
|
|
||||||
{unit.canDetect && <span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/DETECT_YES.webp"/></span>}
|
|
||||||
</UnitListItem>
|
|
||||||
</UnitLink>
|
|
||||||
</Grid2>)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function Building(building: IBuilding, mod: IMod, theme: Theme) {
|
|
||||||
|
|
||||||
const isDark = theme.palette.mode === 'dark';
|
|
||||||
|
|
||||||
let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
|
|
||||||
|
|
||||||
building.weapons.forEach(weapon => {
|
|
||||||
const weaponMap = mapBuildingWeapons.get(weapon.hardpoint)
|
|
||||||
if (weaponMap == null) {
|
|
||||||
const weaponMap = new Map()
|
|
||||||
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
|
|
||||||
mapBuildingWeapons.set(weapon.hardpoint, weaponMap)
|
|
||||||
} else {
|
|
||||||
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var buildingName = building.name;
|
|
||||||
if(building.name == null){
|
|
||||||
buildingName = building.filename.replaceAll('_', ' ').replace('.rgd', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box sx={{ mb: 3 }}>
|
|
||||||
<BuildingTitle variant="h4">
|
|
||||||
{building.icon &&
|
|
||||||
<img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
|
|
||||||
{buildingName}
|
|
||||||
</BuildingTitle>
|
|
||||||
<BuildingSubtitle variant="subtitle1">
|
|
||||||
{mod.name} ({mod.version})
|
|
||||||
</BuildingSubtitle>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Grid2 container spacing={3}>
|
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
|
||||||
<StatsPaper elevation={0}>
|
|
||||||
<Table size="small" aria-label="a dense table">
|
|
||||||
<TableBody id="unit-stats-table">
|
|
||||||
<TableRow>
|
|
||||||
<TableCell component="th" scope="row" >Cost</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{building.buildCostRequisition > 0 &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_requisition.gif"/>
|
|
||||||
{building.buildCostRequisition.toFixed(0)}</span>}
|
|
||||||
{building.buildCostPower > 0 && <span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_power.gif"/>
|
|
||||||
{building.buildCostPower.toFixed(0)}</span>}
|
|
||||||
{(building.buildCostPopulation !== undefined && building.buildCostPopulation > 0) &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_orksquadcap.gif"/>
|
|
||||||
{building.buildCostPopulation.toFixed(0)}</span>}
|
|
||||||
{(building.buildCostFaith !== undefined && building.buildCostFaith > 0) &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_faith.gif"/>
|
|
||||||
{building.buildCostFaith}</span>}
|
|
||||||
{(building.buildCostSouls !== undefined && building.buildCostSouls > 0) &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_souls.gif"/>
|
|
||||||
{building.buildCostSouls.toFixed(0)}</span>}
|
|
||||||
{(building.buildCostTime !== undefined && building.buildCostTime > 0) &&
|
|
||||||
<span> <AvTimerOutlinedIcon
|
|
||||||
style={{verticalAlign: "top", fontSize: "18px"}}/>
|
|
||||||
{building.buildCostTime}s</span>}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{(building.requisitionIncome !== undefined && building.requisitionIncome > 0 || building.powerIncome !== undefined && building.powerIncome !== null || building.faithIncome !== undefined && building.faithIncome !== null) &&
|
|
||||||
<TableRow>
|
|
||||||
<TableCell >Resource income</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{building.requisitionIncome !== undefined && building.requisitionIncome != null &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_requisition.gif"/>
|
|
||||||
{building.requisitionIncome}</span>}
|
|
||||||
{building.powerIncome !== undefined && building.powerIncome > 0 &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_power.gif"/>
|
|
||||||
{building.powerIncome}</span>}
|
|
||||||
{building.faithIncome !== undefined && building.faithIncome > 0 &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_faith.gif"/>
|
|
||||||
{building.faithIncome}</span>}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
}
|
|
||||||
<TableRow>
|
|
||||||
<TableCell component="th" scope="row" >Armor type</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{building.armorType2 == null ? <ArmorType name={building.armorType.name} compact={false}/>
|
|
||||||
: <Tooltip title={"upgrade/debuff can turns to " + building.armorType2.name}><span><ArmorType name={building.armorType.name} compact={false}/></span></Tooltip> }
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell component="th" scope="row" >Health</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<span><img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Health_icon.webp"/>
|
|
||||||
{building.health} {building.healthRegeneration > 0 &&
|
|
||||||
<span>+{building.healthRegeneration}/s</span>} </span>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell component="th" scope="row" >Sight</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Vision sight={building.sightRadius} detect={building.detectRadius}/>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
|
||||||
<TableCell component="th" scope="row" >Repair max</TableCell>
|
|
||||||
<TableCell>{building.repairMax}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</StatsPaper>
|
|
||||||
</Grid2>
|
|
||||||
<Grid2 size={{xs: 12, md: 8}}>
|
|
||||||
<DescriptionBox>
|
|
||||||
{building.description}
|
|
||||||
</DescriptionBox>
|
|
||||||
</Grid2>
|
|
||||||
{building.requirements !== null &&
|
|
||||||
<Grid2 size={{xs: 12, md: 12}}>
|
|
||||||
<Required requirement={building.requirements} modId={building.modId} raceId={building.race.id}/>
|
|
||||||
</Grid2>}
|
|
||||||
{building.modifiers.length > 0 &&
|
|
||||||
<Grid2 size={{xs: 12, md: 12}}>
|
|
||||||
<SectionTitle>Affected on</SectionTitle>
|
|
||||||
<ModifiersProvidesTable modifiers={building.modifiers} race={building.race} modId={building.modId} affectedData={building.affectedData} />
|
|
||||||
</Grid2>}
|
|
||||||
{building.units.length > 0 && <Grid2 size={12}>
|
|
||||||
<SectionTitle>Unit production</SectionTitle>
|
|
||||||
<Grid2 container spacing={2}>
|
|
||||||
{building.units.map(unit =>
|
|
||||||
Unit(unit, mod.id, building.race.id, theme)
|
|
||||||
)}
|
|
||||||
</Grid2>
|
|
||||||
</Grid2>}
|
|
||||||
{building.abilities.length > 0 && <Grid2 size={12}>
|
|
||||||
<SectionTitle>Abilities</SectionTitle>
|
|
||||||
{building.abilities.map(a =>
|
|
||||||
<Ability mod={mod} ability={a} race={building.race}/>
|
|
||||||
)}
|
|
||||||
</Grid2>}
|
|
||||||
{building.deathExplosions.length > 0 &&
|
|
||||||
<Grid2 size={12}>
|
|
||||||
{building.deathExplosions.map(da =>
|
|
||||||
<DeathExplosion deathExplosion={da} mod={mod}/>
|
|
||||||
)}
|
|
||||||
</Grid2>
|
|
||||||
}
|
|
||||||
{building.addons.length > 0 && <Grid2 size={12}>
|
|
||||||
<SectionTitle>Addons</SectionTitle>
|
|
||||||
{building.addons.map(b =>
|
|
||||||
<BuildingAddon mod={mod} addon={b} building={building}/>
|
|
||||||
)}
|
|
||||||
</Grid2> }
|
|
||||||
{building.researches.length > 0 && <Grid2 size={12}>
|
|
||||||
<SectionTitle>Researches</SectionTitle>
|
|
||||||
{building.researches.map(r =>
|
|
||||||
<Research key={r.id} research={r} building={building} mod={mod}/>
|
|
||||||
)}
|
|
||||||
</Grid2> }
|
|
||||||
<Grid2 size={12}>
|
|
||||||
{[...mapBuildingWeapons.keys()].sort(function (a, b) {
|
|
||||||
return a - b;
|
|
||||||
}).map(h => <WeaponSlot race={building.race} mod={mod} unitWeapons={mapBuildingWeapons.get(h)} hardpoint={h}/>)}
|
|
||||||
</Grid2>
|
|
||||||
<Grid2 size={12}>
|
|
||||||
<AffectedResearches researches={building.affectedResearches} modId={mod.id} raceId={building.race.id} />
|
|
||||||
</Grid2>
|
|
||||||
<b className="hotkey" >Hotkey: {building.hotkey} <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {building.filename}</span></b>
|
|
||||||
</Grid2>
|
|
||||||
<StyledDivider />
|
|
||||||
<UnitsTable modId={mod.id}/>
|
|
||||||
</Box>)
|
|
||||||
}
|
|
||||||
|
|
||||||
class BuildingPage extends React.Component<any, UintPageState> {
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const buildingResponse = await fetch(AvailableBuildings + "/" + this.props.match.params.buildingId);
|
|
||||||
const buildingData: IBuilding = await buildingResponse.json();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
building: buildingData
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
|
|
||||||
const modData: IMod = await responseMod.json();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
mod: modData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
if (this.state != null && this.state.building != null && this.state.mod != null) {
|
|
||||||
const backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId
|
|
||||||
const { theme } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxWidth="lg">
|
|
||||||
<BackButton
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<ArrowBack/>}
|
|
||||||
href={backRef}
|
|
||||||
>
|
|
||||||
Back to race
|
|
||||||
</BackButton>
|
|
||||||
{Building(this.state.building, this.state.mod, theme)}
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
|
||||||
<LinearProgress sx={{ width: '200px' }} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(withTheme(BuildingPage));
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import {withRouter} from "../core/withrouter";
|
|
||||||
import {withTheme} from "../core/api";
|
|
||||||
import {IMod} from "../types/Imod";
|
|
||||||
import {
|
|
||||||
ArrowBack,
|
|
||||||
} from "@mui/icons-material";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Paper,
|
|
||||||
Typography,
|
|
||||||
Divider,
|
|
||||||
LinearProgress
|
|
||||||
} from "@mui/material";
|
|
||||||
import {styled} from '@mui/material/styles';
|
|
||||||
import UnitsTable from "../classes/UnitsTable";
|
|
||||||
import {BackButton} from "../commons/BackButton";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const ModNotFound = styled(Typography)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
interface ModPageState {
|
|
||||||
mod: IMod | null,
|
|
||||||
loading: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ModPage extends React.Component<any, ModPageState> {
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
mod: null,
|
|
||||||
loading: true
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch(AvailableMods + "/" + this.props.match.params.modId)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then((res: IMod) => {
|
|
||||||
this.setState({
|
|
||||||
mod: res,
|
|
||||||
loading: false
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
this.setState({ loading: false });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.loading) {
|
|
||||||
return (
|
|
||||||
<Container maxWidth="lg" >
|
|
||||||
<LinearProgress sx={{ width: '200px', mx: 'auto', display: 'block' }} />
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state != null && this.state.mod != null) {
|
|
||||||
const isDark = this.props.theme.palette.mode === 'dark';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxWidth="lg">
|
|
||||||
<BackButton
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<ArrowBack/>}
|
|
||||||
href="/"
|
|
||||||
>
|
|
||||||
Back to mods list
|
|
||||||
</BackButton>
|
|
||||||
|
|
||||||
<Typography variant="h3" component="h1" sx={{
|
|
||||||
fontWeight: 800,
|
|
||||||
mb: 1,
|
|
||||||
WebkitBackgroundClip: isDark ? 'text' : 'initial',
|
|
||||||
WebkitTextFillColor: isDark ? '#dee2e6' : 'initial',
|
|
||||||
color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
|
||||||
}}>
|
|
||||||
{this.state.mod.name} <Box component="span" sx={{
|
|
||||||
fontSize: '0.5em',
|
|
||||||
color: this.props.theme.palette.text.secondary,
|
|
||||||
fontWeight: 500,
|
|
||||||
}}>({this.state.mod.version})</Box>
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<UnitsTable modId={this.state.mod.id}/>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Container maxWidth="lg" sx={{ py: 8, textAlign: 'center' }}>
|
|
||||||
<ModNotFound variant="h5">
|
|
||||||
Mod not found
|
|
||||||
</ModNotFound>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default withRouter(withTheme(ModPage));
|
|
||||||
@ -1,321 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Container,
|
|
||||||
Typography,
|
|
||||||
Grid,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardActions,
|
|
||||||
Chip,
|
|
||||||
Button,
|
|
||||||
Divider,
|
|
||||||
useTheme,
|
|
||||||
Paper,
|
|
||||||
LinearProgress,
|
|
||||||
Link,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import { IMod } from "../types/Imod";
|
|
||||||
|
|
||||||
|
|
||||||
const ModCard = styled(Card)(({ theme }) => ({
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
background: theme.palette.mode === 'dark'
|
|
||||||
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
|
||||||
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
|
||||||
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
|
||||||
borderRadius: '16px',
|
|
||||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
position: 'relative',
|
|
||||||
overflow: 'hidden',
|
|
||||||
'&::before': {
|
|
||||||
content: '""',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: '3px',
|
|
||||||
background: theme.palette.mode === 'dark'
|
|
||||||
? 'linear-gradient(90deg, #dee2e6 0%, #cccccc 100%)'
|
|
||||||
: 'linear-gradient(90deg, #000000 0%, #333333 100%)',
|
|
||||||
transform: 'scaleX(0)',
|
|
||||||
transition: 'transform 0.3s ease',
|
|
||||||
},
|
|
||||||
'&:hover': {
|
|
||||||
boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
|
|
||||||
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
|
|
||||||
'&::before': {
|
|
||||||
transform: 'scaleX(1)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const VersionLink = styled(Link)(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: theme.spacing(1.5, 2),
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
|
|
||||||
borderRadius: '10px',
|
|
||||||
textDecoration: 'none',
|
|
||||||
color: 'inherit',
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
|
||||||
'&:hover': {
|
|
||||||
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
|
||||||
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ModTitle = styled(Typography)(({ theme }) => ({
|
|
||||||
fontWeight: 700,
|
|
||||||
mb: 1,
|
|
||||||
background: theme.palette.mode === 'dark'
|
|
||||||
? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
|
|
||||||
: 'none',
|
|
||||||
WebkitBackgroundClip: theme.palette.mode === 'dark' ? 'text' : 'initial',
|
|
||||||
WebkitTextFillColor: theme.palette.mode === 'dark' ? '#dee2e6' : 'initial',
|
|
||||||
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ModDescription = styled(Typography)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
lineHeight: 1.6,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const VersionsChip = styled(Chip)(({ theme }) => ({
|
|
||||||
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)',
|
|
||||||
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
|
||||||
fontWeight: 600,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDivider = styled(Divider)(({ theme }) => ({
|
|
||||||
my: 2,
|
|
||||||
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const MainVersionsLabel = styled(Typography)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
fontWeight: 600,
|
|
||||||
mb: 1.5,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const VersionText = styled(Typography)(({ theme }) => ({
|
|
||||||
fontWeight: 600,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const VersionArrow = styled(Typography)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const BetaVersionsLabel = styled(Typography)(({ theme }) => ({
|
|
||||||
color: 'rgba(255, 193, 7, 0.9)',
|
|
||||||
fontWeight: 600,
|
|
||||||
mb: 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const OpenLatestButton = styled(Button)(({ theme }) => {
|
|
||||||
const isDark = theme.palette.mode === 'dark';
|
|
||||||
return {
|
|
||||||
width: '100%',
|
|
||||||
color: isDark ? '#FFD700' : '#1976d2',
|
|
||||||
borderColor: isDark ? 'rgba(255, 215, 0, 0.5)' : 'rgba(25, 118, 210, 0.5)',
|
|
||||||
fontWeight: 600,
|
|
||||||
borderRadius: '10px',
|
|
||||||
textTransform: 'none',
|
|
||||||
fontSize: '1rem',
|
|
||||||
py: 1.5,
|
|
||||||
'&:hover': {
|
|
||||||
borderColor: isDark ? '#FFD700' : '#1976d2',
|
|
||||||
backgroundColor: isDark ? 'rgba(255, 215, 0, 0.1)' : 'rgba(25, 118, 210, 0.1)',
|
|
||||||
boxShadow: isDark ? '0 8px 20px rgba(255, 215, 0, 0.2)' : '0 8px 20px rgba(25, 118, 210, 0.2)',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
interface ModsProps {
|
|
||||||
mods: IMod[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function Mods({ mods }: ModsProps) {
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
// Sort all mods by order, then group by name
|
|
||||||
const sortedMods = [...mods].sort((a, b) => a.order - b.order);
|
|
||||||
|
|
||||||
const mapWithModVersions = new Map<String, IMod[]>();
|
|
||||||
sortedMods.forEach(mod => {
|
|
||||||
const versionList = mapWithModVersions.get(mod.name);
|
|
||||||
if (versionList == null) {
|
|
||||||
mapWithModVersions.set(mod.name, [mod]);
|
|
||||||
} else {
|
|
||||||
versionList.push(mod);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const getLatestVersion = (modName: String) => {
|
|
||||||
const sameMods = mapWithModVersions.get(modName) ?? [];
|
|
||||||
// The latest version is the one with the maximum id
|
|
||||||
const latest = sameMods.reduce((max, mod) => mod.id > max.id ? mod : max, sameMods[0]);
|
|
||||||
const betaMods = sameMods.filter(m => m.isBeta);
|
|
||||||
return {
|
|
||||||
latest: latest,
|
|
||||||
allVersions: sameMods,
|
|
||||||
hasBeta: betaMods.length > 0,
|
|
||||||
betaVersion: betaMods[0]
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function ModCardComponent(modName: String, index: number) {
|
|
||||||
const sameMods = mapWithModVersions.get(modName) ?? [];
|
|
||||||
const { latest, hasBeta, betaVersion } = getLatestVersion(modName);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModCard>
|
|
||||||
<CardContent sx={{ flexGrow: 1, p: 3 }}>
|
|
||||||
<Box sx={{ mb: 2 }}>
|
|
||||||
<ModTitle variant="h5">
|
|
||||||
{modName}
|
|
||||||
</ModTitle>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
|
||||||
<VersionsChip
|
|
||||||
label={`${sameMods.length} versions`}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{sameMods.filter(m => !m.isBeta).map(mod => (
|
|
||||||
<VersionLink key={mod.id} href={"/mod/" + mod.id}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
|
||||||
<VersionText variant="body2">
|
|
||||||
Version {mod.version}
|
|
||||||
</VersionText>
|
|
||||||
<VersionArrow variant="caption">
|
|
||||||
→
|
|
||||||
</VersionArrow>
|
|
||||||
</Box>
|
|
||||||
</VersionLink>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{hasBeta && (
|
|
||||||
<Box sx={{ mt: 2 }}>
|
|
||||||
<BetaVersionsLabel variant="subtitle2">
|
|
||||||
Beta versions:
|
|
||||||
</BetaVersionsLabel>
|
|
||||||
{betaVersion && (
|
|
||||||
<VersionLink href={"/mod/" + betaVersion.id}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
|
||||||
Version {betaVersion.version} (Beta)
|
|
||||||
</Typography>
|
|
||||||
<VersionArrow variant="caption">
|
|
||||||
→
|
|
||||||
</VersionArrow>
|
|
||||||
</Box>
|
|
||||||
</VersionLink>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
|
|
||||||
<CardActions sx={{ px: 3, pb: 3 }}>
|
|
||||||
<Button
|
|
||||||
size="medium"
|
|
||||||
variant="outlined"
|
|
||||||
href={"/mod/" + latest?.id}
|
|
||||||
sx={{
|
|
||||||
width: '100%',
|
|
||||||
color: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
|
|
||||||
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 215, 0, 0.5)' : 'rgba(25, 118, 210, 0.5)',
|
|
||||||
fontWeight: 600,
|
|
||||||
borderRadius: '10px',
|
|
||||||
textTransform: 'none',
|
|
||||||
fontSize: '1rem',
|
|
||||||
py: 1.5,
|
|
||||||
'&:hover': {
|
|
||||||
borderColor: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
|
|
||||||
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 215, 0, 0.1)' : 'rgba(25, 118, 210, 0.1)',
|
|
||||||
boxShadow: theme.palette.mode === 'dark' ? '0 8px 20px rgba(255, 215, 0, 0.2)' : '0 8px 20px rgba(25, 118, 210, 0.2)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Open latest {latest?.version ?? ''}
|
|
||||||
</Button>
|
|
||||||
</CardActions>
|
|
||||||
</ModCard>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
{[...new Set(sortedMods.map(m => m.name))].map((modName, index) => (
|
|
||||||
<Grid item xs={12} sm={6} md={4} >
|
|
||||||
{ModCardComponent(modName, index)}
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModsPageState {
|
|
||||||
mods: IMod[];
|
|
||||||
loading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModsPageProps {
|
|
||||||
initialMods?: IMod[];
|
|
||||||
}
|
|
||||||
|
|
||||||
class ModsPage extends React.Component<ModsPageProps, ModsPageState> {
|
|
||||||
|
|
||||||
constructor(props: ModsPageProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
mods: props.initialMods ?? [],
|
|
||||||
loading: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
// Очистка кэша сайта при заходе на главную страницу (один раз за сессию)
|
|
||||||
if (!sessionStorage.getItem('cache_cleared')) {
|
|
||||||
sessionStorage.setItem('cache_cleared', '1');
|
|
||||||
try {
|
|
||||||
if ('caches' in window) {
|
|
||||||
const names = await caches.keys();
|
|
||||||
await Promise.all(names.map(name => caches.delete(name)));
|
|
||||||
}
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
const regs = await navigator.serviceWorker.getRegistrations();
|
|
||||||
await Promise.all(regs.map(r => r.unregister()));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Cache clear failed:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (this.state.loading) {
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
|
||||||
<LinearProgress sx={{ width: '200px' }} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Mods mods={this.state.mods} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModsPage;
|
|
||||||
@ -1,332 +0,0 @@
|
|||||||
import {AvailableBuildings, AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
|
|
||||||
import React from "react";
|
|
||||||
import {withRouter} from "../core/withrouter";
|
|
||||||
import {IMod} from "../types/Imod";
|
|
||||||
import {Irace} from "../types/Irace";
|
|
||||||
import '../css/Unit.css'
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Divider,
|
|
||||||
Grid2, LinearProgress,
|
|
||||||
Link,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
Paper,
|
|
||||||
Typography,
|
|
||||||
Theme,
|
|
||||||
} from "@mui/material";
|
|
||||||
import {ArrowBack, ExpandMore} from "@mui/icons-material";
|
|
||||||
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
|
|
||||||
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
|
|
||||||
import {styled} from '@mui/material/styles';
|
|
||||||
import {withTheme} from "../core/api";
|
|
||||||
import {StyledLink} from "../commons/StyledLink";
|
|
||||||
import {BackButton} from "../commons/BackButton";
|
|
||||||
|
|
||||||
const SectionTitle = styled(Typography)(({ theme }) => ({
|
|
||||||
fontWeight: 700,
|
|
||||||
mb: 2,
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const UnitCard = styled(Paper)(({ theme }) => ({
|
|
||||||
background: theme.palette.mode === 'dark'
|
|
||||||
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
|
||||||
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
|
||||||
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
|
||||||
borderRadius: '16px',
|
|
||||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
'&:hover': {
|
|
||||||
boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
|
|
||||||
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const BuildingCard = styled(UnitCard)(({ theme }) => ({
|
|
||||||
padding: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const UnitLink = styled(Link)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
textDecoration: 'none',
|
|
||||||
display: 'block',
|
|
||||||
'&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }
|
|
||||||
}));
|
|
||||||
|
|
||||||
const LoadingText = styled(Typography)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface RacePageState {
|
|
||||||
mod: IMod,
|
|
||||||
race: Irace,
|
|
||||||
units: IUnitShort[],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function Unit(unit: IUnitShort, modId: number, raceId: String, theme: Theme) {
|
|
||||||
const isDark = theme.palette.mode === 'dark';
|
|
||||||
const borderColor = isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)';
|
|
||||||
const bgColor = isDark ? 'rgba(255,255,255,0.02)' : 'rgba(0,0,0,0.02)';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UnitLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
|
|
||||||
<ListItem sx={{
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
padding: '1.5rem 2rem',
|
|
||||||
marginBottom: '0.5rem',
|
|
||||||
background: isDark
|
|
||||||
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
|
||||||
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
|
||||||
borderRadius: '16px',
|
|
||||||
border: `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
|
||||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
'&:hover': {
|
|
||||||
boxShadow: isDark ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
|
|
||||||
borderColor: isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
|
|
||||||
{unit.name}
|
|
||||||
{unit.canDetect && <span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/DETECT_YES.webp"/></span>}
|
|
||||||
</ListItem>
|
|
||||||
</UnitLink>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function UnitSmall(unit: IUnitShort, modId: number, raceId: String) {
|
|
||||||
|
|
||||||
var unitName = ""
|
|
||||||
if (unit.name.length > 21) {
|
|
||||||
unitName = unit.name.substring(0, 19) + "...";
|
|
||||||
} else {
|
|
||||||
unitName = unit.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} >
|
|
||||||
{unit.icon && <img className="unitIconSmall" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
|
|
||||||
<span style={{fontSize: 14}}>{unitName}</span>
|
|
||||||
{unit.canDetect && <span> <img
|
|
||||||
src="/images/DETECT_YES.webp"/></span>}<br/></StyledLink>)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Building(building: IBuildingShort, modId: number, raceId: String, theme: Theme) {
|
|
||||||
const isDark = theme.palette.mode === 'dark';
|
|
||||||
|
|
||||||
return (<Grid2 size={{xs: 12, md: 3}}>
|
|
||||||
<Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id} sx={{
|
|
||||||
textDecoration: 'none',
|
|
||||||
display: 'block',
|
|
||||||
'&:hover': { color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }
|
|
||||||
}}>
|
|
||||||
<BuildingCard elevation={0}>
|
|
||||||
<ListItem sx={{}}>
|
|
||||||
{building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
|
|
||||||
<StyledLink>{building.name}</StyledLink>
|
|
||||||
{building.canDetect && <span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/DETECT_YES.webp"/></span>}
|
|
||||||
</ListItem>
|
|
||||||
{building.units.map(unit => UnitSmall(unit, modId, raceId))}
|
|
||||||
</BuildingCard>
|
|
||||||
</Link>
|
|
||||||
</Grid2>)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UnitsProps {
|
|
||||||
raceId: string,
|
|
||||||
modId: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UnitsState {
|
|
||||||
selectedUnits: String | null,
|
|
||||||
units: IRaceUnits | null,
|
|
||||||
buildings: IRaceBuildings | null,
|
|
||||||
}
|
|
||||||
|
|
||||||
class Units extends React.Component<UnitsProps & { theme: Theme }, UnitsState> {
|
|
||||||
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
let urlUnits = AvailableUnits + "/" + this.props.modId + "/" + this.props.raceId;
|
|
||||||
|
|
||||||
|
|
||||||
fetch(urlUnits)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then((res: IRaceUnits) => {
|
|
||||||
this.setState({
|
|
||||||
units: res,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
let urlBuildings = AvailableBuildings + "/" + this.props.modId + "/" + this.props.raceId;
|
|
||||||
|
|
||||||
fetch(urlBuildings)
|
|
||||||
.then(res => res.json())
|
|
||||||
.then((res: IRaceBuildings) => {
|
|
||||||
this.setState({
|
|
||||||
buildings: res,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { theme } = this.props;
|
|
||||||
const isDark = theme.palette.mode === 'dark';
|
|
||||||
|
|
||||||
if (this.state && this.state.units) {
|
|
||||||
|
|
||||||
if(this.state.buildings?.race.name !== undefined){
|
|
||||||
// title managed by Helmet in parent component
|
|
||||||
}
|
|
||||||
|
|
||||||
const accordionSx = isDark ? {
|
|
||||||
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
|
|
||||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
||||||
borderRadius: '12px',
|
|
||||||
'&:before': { display: 'none' },
|
|
||||||
'&.Mui-expanded': { margin: '0 0 8px 0' },
|
|
||||||
} : {
|
|
||||||
background: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
|
||||||
border: '1px solid rgba(0, 0, 0, 0.1)',
|
|
||||||
borderRadius: '12px',
|
|
||||||
'&:before': { display: 'none' },
|
|
||||||
'&.Mui-expanded': { margin: '0 0 8px 0' },
|
|
||||||
};
|
|
||||||
|
|
||||||
return (this.state.buildings != null ?
|
|
||||||
<Box>
|
|
||||||
<Grid2 container spacing={2}>
|
|
||||||
{this.state.buildings.buildings.map(building => Building(building, this.props.modId, this.props.raceId, theme))}
|
|
||||||
</Grid2>
|
|
||||||
{this.state.buildings.buildingsAdvanced.length > 0 && <Box sx={{ mt: 3 }}>
|
|
||||||
<Divider sx={{ my: 2, borderColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)' }} />
|
|
||||||
<SectionTitle>Advanced buildings</SectionTitle>
|
|
||||||
<Grid2 container spacing={2}>
|
|
||||||
{this.state.buildings.buildingsAdvanced.map(building => Building(building, this.props.modId, this.props.raceId, theme))}
|
|
||||||
</Grid2><br/></Box>}
|
|
||||||
<Box sx={{ mt: 3 }}>
|
|
||||||
<Accordion sx={accordionSx}>
|
|
||||||
<AccordionSummary
|
|
||||||
expandIcon={<ExpandMore sx={{color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)'}}/>}
|
|
||||||
aria-controls="units-accordion"
|
|
||||||
id="units-accordion"
|
|
||||||
sx={{color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
|
|
||||||
>
|
|
||||||
<SectionTitle sx={{ mb: 0, display: 'flex', alignItems: 'center' }}>All units</SectionTitle>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails>
|
|
||||||
<Grid2 container spacing={2}>
|
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
|
||||||
<Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', fontWeight: 600, mb: 2 }}>Infantry</Typography>
|
|
||||||
{this.state.units.infantry.map(unit => Unit(unit, this.props.modId, this.props.raceId, theme))}
|
|
||||||
</Grid2>
|
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
|
||||||
<Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', fontWeight: 600, mb: 2 }}>Tech</Typography>
|
|
||||||
<List>
|
|
||||||
{this.state.units.tech.map(unit => Unit(unit, this.props.modId, this.props.raceId, theme))}
|
|
||||||
</List>
|
|
||||||
</Grid2>
|
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
|
||||||
<Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', fontWeight: 600, mb: 2 }}>Support</Typography>
|
|
||||||
<List>
|
|
||||||
{this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId, theme))}
|
|
||||||
</List>
|
|
||||||
</Grid2>
|
|
||||||
</Grid2>
|
|
||||||
</AccordionDetails>
|
|
||||||
</Accordion>
|
|
||||||
</Box>
|
|
||||||
</Box> : <LoadingText>Loading</LoadingText>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return <LoadingText>Loading...</LoadingText>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const UnitsWithTheme = withTheme(Units) as React.ComponentType<UnitsProps>;
|
|
||||||
|
|
||||||
class RacePageFast extends React.Component<any, RacePageState> {
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
|
|
||||||
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
|
|
||||||
const modData: IMod = await responseMod.json();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
mod: modData
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(AvailableRacesPart + "/" + this.props.match.params.raceId);
|
|
||||||
const racesData: Irace = await response.json();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
race: racesData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
if (this.state != null && this.state.mod != null && this.state.race != null) {
|
|
||||||
const backRef = "/mod/" + this.state.mod.id
|
|
||||||
const theme = this.props.theme;
|
|
||||||
const isDark = theme.palette.mode === 'dark';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxWidth="lg">
|
|
||||||
<BackButton
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<ArrowBack/>}
|
|
||||||
href={backRef}
|
|
||||||
>
|
|
||||||
Back to mod
|
|
||||||
</BackButton>
|
|
||||||
|
|
||||||
<Box sx={{ mb: 4 }}>
|
|
||||||
<Typography variant="h3" component="h1" sx={{
|
|
||||||
fontWeight: 800,
|
|
||||||
mb: 1,
|
|
||||||
background: isDark
|
|
||||||
? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
|
|
||||||
: 'none',
|
|
||||||
WebkitBackgroundClip: isDark ? 'text' : 'initial',
|
|
||||||
WebkitTextFillColor: isDark ? '#dee2e6' : 'initial',
|
|
||||||
color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
|
||||||
}}>
|
|
||||||
{this.state.race.name}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h6" sx={{
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
fontWeight: 500,
|
|
||||||
}}>
|
|
||||||
{this.state.mod.name} ({this.state.mod.version})
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<UnitsWithTheme raceId={this.state.race.id} modId={this.state.mod.id}/>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Container maxWidth="lg">
|
|
||||||
<LinearProgress sx={{ width: '200px', mx: 'auto', display: 'block' }} />
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(withTheme(RacePageFast));
|
|
||||||
@ -1,435 +0,0 @@
|
|||||||
import {AvailableMods, AvailableUnits, IconUrl} from "../core/api";
|
|
||||||
import React from "react";
|
|
||||||
import {withRouter} from "../core/withrouter";
|
|
||||||
import {IShortWeapon, IUnit} from "../types/IUnit";
|
|
||||||
import '../css/Unit.css'
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
|
||||||
AccordionSummary,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Divider,
|
|
||||||
Grid2, LinearProgress,
|
|
||||||
Paper,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableRow,
|
|
||||||
Tooltip,
|
|
||||||
Typography, Theme,
|
|
||||||
} from "@mui/material";
|
|
||||||
import {ArrowBack, ExpandMore} from "@mui/icons-material";
|
|
||||||
import ArmorType from "../classes/ArmorType";
|
|
||||||
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
|
|
||||||
import Sergeant from "../classes/Sergeant";
|
|
||||||
import WeaponSlot from "../classes/WeaponSlot";
|
|
||||||
import UnitsTable from "../classes/UnitsTable";
|
|
||||||
import {IMod} from "../types/Imod";
|
|
||||||
import Vision from "../classes/Vision";
|
|
||||||
import Required from "../classes/Required";
|
|
||||||
import {AffectedResearches} from "../classes/building/Research";
|
|
||||||
import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
|
|
||||||
import Ability from "../classes/Ability";
|
|
||||||
import DeathExplosion from "../classes/DeathExplosion";
|
|
||||||
import Jump from "../classes/Jump";
|
|
||||||
import {styled} from '@mui/material/styles';
|
|
||||||
import {withTheme} from "../core/api";
|
|
||||||
import {DescriptionBox} from "../commons/DescriptionBox";
|
|
||||||
import {BackButton} from "../commons/BackButton";
|
|
||||||
|
|
||||||
const SectionTitle = styled(Typography)(({ theme }) => ({
|
|
||||||
fontWeight: 700,
|
|
||||||
mb: 2,
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StatsPaper = styled(Paper)(({ theme }) => ({
|
|
||||||
background: theme.palette.mode === 'dark'
|
|
||||||
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
|
||||||
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
|
||||||
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
|
||||||
borderRadius: '12px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
'& .MuiTableBody .MuiTableRow-root': {
|
|
||||||
'&:last-child td, &:last-child th': { border: 0 },
|
|
||||||
},
|
|
||||||
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
|
|
||||||
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
|
||||||
fontWeight: 600,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const SergeantAccordion = styled(Accordion)(({ theme }) => ({
|
|
||||||
background: theme.palette.mode === 'dark'
|
|
||||||
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
|
|
||||||
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
|
|
||||||
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
|
|
||||||
borderRadius: '12px',
|
|
||||||
mb: 1,
|
|
||||||
'&:before': { display: 'none' },
|
|
||||||
'&.Mui-expanded': { margin: '0 0 8px 0' },
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDivider = styled(Divider)(({ theme }) => ({
|
|
||||||
my: 3,
|
|
||||||
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const UnitTitle = styled(Typography)(({ theme }) => ({
|
|
||||||
fontWeight: 800,
|
|
||||||
mb: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 2,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const UnitSubtitle = styled(Typography)(({ theme }) => ({
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
fontWeight: 500,
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
|
||||||
interface UintPageState {
|
|
||||||
unit: IUnit,
|
|
||||||
mod: IMod,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function Unit(unit: IUnit, mod: IMod, theme: Theme) {
|
|
||||||
|
|
||||||
const isDark = theme.palette.mode === 'dark';
|
|
||||||
|
|
||||||
const morale = (unit.moraleMax !== null) ? <span>
|
|
||||||
<img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/> {unit.moraleMax}
|
|
||||||
+{unit.moraleRegeneration}/s
|
|
||||||
{unit.moraleDeathPenalty > 0 && <span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Kills_icon.webp"/> -{unit.moraleDeathPenalty}</span>}
|
|
||||||
|
|
||||||
</span> : "-"
|
|
||||||
|
|
||||||
let mapWithUnitWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
|
|
||||||
|
|
||||||
unit.weapons.forEach(weapon => {
|
|
||||||
const weaponMap = mapWithUnitWeapons.get(weapon.hardpoint)
|
|
||||||
if (weaponMap == null) {
|
|
||||||
const weaponMap = new Map()
|
|
||||||
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
|
|
||||||
mapWithUnitWeapons.set(weapon.hardpoint, weaponMap)
|
|
||||||
} else {
|
|
||||||
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
type sergeantProps = {
|
|
||||||
name: string
|
|
||||||
icon: String
|
|
||||||
canDetect: Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const SergeantShort = (props: sergeantProps) => {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span style={{fontSize: 20, color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', display: 'flex', alignItems: 'center'}}>
|
|
||||||
{props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')}/> }
|
|
||||||
{props.name}
|
|
||||||
{props.canDetect && <span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/DETECT_YES.webp"/></span>}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Box sx={{ mb: 3 }}>
|
|
||||||
<UnitTitle variant="h4">
|
|
||||||
{unit.icon &&
|
|
||||||
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
|
|
||||||
{unit.name}
|
|
||||||
</UnitTitle>
|
|
||||||
<UnitSubtitle variant="subtitle1">
|
|
||||||
{mod.name} ({mod.version})
|
|
||||||
</UnitSubtitle>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Grid2 container spacing={3}>
|
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
|
||||||
<StatsPaper >
|
|
||||||
<Table size="small" aria-label="a dense table">
|
|
||||||
<TableBody id="unit-stats-table" >
|
|
||||||
<TableRow>
|
|
||||||
<TableCell component="th" scope="row" >Cost</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{unit.buildCostRequisition > 0 &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_requisition.gif"/>
|
|
||||||
{unit.buildCostRequisition.toFixed(0)}</span>}
|
|
||||||
{unit.buildCostPower > 0 && <span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_power.gif"/>
|
|
||||||
{unit.buildCostPower.toFixed(0)}</span>}
|
|
||||||
{(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_orksquadcap.gif"/>
|
|
||||||
{unit.buildCostPopulation.toFixed(0)}</span>}
|
|
||||||
{(unit.buildCostFaith !== undefined && unit.buildCostFaith > 0) &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_faith.gif"/>
|
|
||||||
{unit.buildCostFaith}</span>}
|
|
||||||
{(unit.buildCostSouls !== undefined && unit.buildCostSouls > 0) &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_souls.gif"/>
|
|
||||||
{unit.buildCostSouls.toFixed(0)}</span>}
|
|
||||||
|
|
||||||
{unit.capInfantry > 0 && <span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_cap_infantry.gif"/>
|
|
||||||
{unit.capInfantry}</span>}
|
|
||||||
{unit.capSupport > 0 && <span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_cap_vehicle.gif"/>
|
|
||||||
{unit.capSupport}</span>}
|
|
||||||
{(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
|
|
||||||
<span> <AvTimerOutlinedIcon
|
|
||||||
style={{verticalAlign: "top", fontSize: "18px"}}/>
|
|
||||||
{unit.buildCostTime}s</span>}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{(unit?.reinforceTime !== 0 && unit.reinforceTime !== null && unit.squadMaxSize > 1) &&
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>Reinforce cost</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{unit.reinforceCostRequisition && unit.reinforceCostRequisition > 0 ?
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_requisition.gif"/>
|
|
||||||
{unit.reinforceCostRequisition.toFixed(0)}</span>:<span/>}
|
|
||||||
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_power.gif"/>
|
|
||||||
{unit.reinforceCostPower.toFixed(0)}</span>}
|
|
||||||
{(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_orksquadcap.gif"/>
|
|
||||||
{unit.reinforceCostPopulation.toFixed(0)}</span>}
|
|
||||||
{(unit.reinforceCostFaith !== undefined && unit.reinforceCostFaith > 0) &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_faith.gif"/>
|
|
||||||
{unit.reinforceCostFaith}</span>}
|
|
||||||
{(unit.reinforceCostSouls !== undefined && unit.reinforceCostSouls > 0) &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_souls.gif"/>
|
|
||||||
{unit.reinforceCostSouls.toFixed(0)}</span>}
|
|
||||||
{(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
|
|
||||||
<span> <AvTimerOutlinedIcon
|
|
||||||
style={{verticalAlign: "top", fontSize: "18px"}}/>
|
|
||||||
{unit.reinforceTime}s</span>}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
}
|
|
||||||
{(unit.requisitionIncome !== undefined && unit.requisitionIncome !== null || unit.powerIncome !== undefined && unit.powerIncome !== null || unit.faithIncome !== undefined && unit.faithIncome !== null) &&
|
|
||||||
<TableRow>
|
|
||||||
<TableCell >Resource income</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{unit.requisitionIncome !== undefined && unit.requisitionIncome > 0 &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_requisition.gif"/>
|
|
||||||
{unit.requisitionIncome}</span>}
|
|
||||||
{unit.powerIncome !== undefined && unit.powerIncome > 0 &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_power.gif"/>
|
|
||||||
{unit.powerIncome}</span>}
|
|
||||||
{unit.faithIncome !== undefined && unit.faithIncome > 0 &&
|
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Resource_faith.gif"/>
|
|
||||||
{unit.faithIncome}</span>}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
}
|
|
||||||
{unit.squadMaxSize > 1 &&
|
|
||||||
<TableRow>
|
|
||||||
<TableCell >Squad size</TableCell>
|
|
||||||
<TableCell>{unit.squadStartSize} / {unit.squadMaxSize}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
}
|
|
||||||
<TableRow>
|
|
||||||
<TableCell >Armor type</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{unit.armorType2 == null ? <ArmorType name={unit.armorType.name} compact={false}/> : <Tooltip title={"upgrade/debuff can turns to " + unit.armorType2.name}><span><ArmorType name={unit.armorType.name} compact={false}/></span></Tooltip> }
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell >Health</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<img style={{verticalAlign: "top"}}
|
|
||||||
src="/images/Health_icon.webp"/>
|
|
||||||
{unit.health} {unit.healthRegeneration > 0 &&
|
|
||||||
<span>+{unit.healthRegeneration}/s</span>}
|
|
||||||
{unit.armour !== undefined && unit.armour !== 0 && <span><img style={{height: 20, verticalAlign: "top"}} src="/images/defence.png"/>{unit.armour} </span> }
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell >Move speed</TableCell>
|
|
||||||
<TableCell>{unit.moveSpeed}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell >Morale</TableCell>
|
|
||||||
<TableCell>{morale}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell >Mass</TableCell>
|
|
||||||
<TableCell>{unit.mass}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
|
||||||
<TableCell >Vision</TableCell>
|
|
||||||
<TableCell><Vision sight={unit.sightRadius} detect={unit.detectRadius}/></TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{unit.repairMax !== undefined && unit.repairMax !== null &&
|
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
|
||||||
<TableCell >Repair max</TableCell>
|
|
||||||
<TableCell>{unit.repairMax}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
}
|
|
||||||
{unit.repairSpeed !== undefined && unit.repairSpeed !== null && unit.repairCostPercent !== null &&
|
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
|
||||||
<TableCell >Repair</TableCell>
|
|
||||||
<TableCell>{unit.repairSpeed} hp/s; {unit.repairCostPercent}% cost</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
}
|
|
||||||
{unit.mobValue != null &&
|
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
|
||||||
<TableCell >Mob value</TableCell>
|
|
||||||
<TableCell><span> <img style={{height: 20, verticalAlign: "top"}} src="/images/Mob_bonus.gif"/> {unit.mobValue} </span></TableCell>
|
|
||||||
</TableRow>
|
|
||||||
}
|
|
||||||
{unit.squadLimit !== undefined && unit.squadLimit !== null &&
|
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
|
||||||
<TableCell >Limit</TableCell>
|
|
||||||
<TableCell>{unit.squadLimit}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</StatsPaper>
|
|
||||||
</Grid2>
|
|
||||||
<Grid2 size={{xs: 12, md: 8}}>
|
|
||||||
<DescriptionBox>
|
|
||||||
{unit.description}
|
|
||||||
</DescriptionBox>
|
|
||||||
</Grid2>
|
|
||||||
{unit.requirements !== null &&
|
|
||||||
<Grid2 size={{xs: 12, md: 12}}>
|
|
||||||
<Required requirement={unit.requirements} modId={unit.modId} raceId={unit.race.id}/>
|
|
||||||
</Grid2>}
|
|
||||||
<Grid2 size={12}>
|
|
||||||
{unit.sergeants.map(s =>
|
|
||||||
<SergeantAccordion key={s.id}>
|
|
||||||
<AccordionSummary
|
|
||||||
sx={{color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}>
|
|
||||||
<SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0}/>
|
|
||||||
</AccordionSummary>
|
|
||||||
<AccordionDetails sx={{color: theme.palette.text.primary}}>
|
|
||||||
<Sergeant mod={mod} sergeant={s} race={unit.race}/>
|
|
||||||
</AccordionDetails>
|
|
||||||
</SergeantAccordion>)}
|
|
||||||
</Grid2>
|
|
||||||
|
|
||||||
{unit.moraleMax !== null && !(unit.moraleBrakeModifiers.find(m => m.reference.includes("accuracy_weapon_modifier") && m.value === 0.2) !== undefined &&
|
|
||||||
unit.moraleBrakeModifiers.find(m => m.reference.includes("speed_maximum_modifier") && m.value === 1.2) !== undefined &&
|
|
||||||
unit.moraleBrakeModifiers.length === 2)
|
|
||||||
&& <Grid2 size={12}>
|
|
||||||
<SectionTitle><img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/> Morale broken <img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/></SectionTitle>
|
|
||||||
<ModifiersProvidesTable modifiers={unit.moraleBrakeModifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/>
|
|
||||||
</Grid2>}
|
|
||||||
|
|
||||||
{unit.modifiers.length > 0 && <Grid2 size={12}>
|
|
||||||
<SectionTitle>Affected on</SectionTitle>
|
|
||||||
<ModifiersProvidesTable modifiers={unit.modifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/>
|
|
||||||
</Grid2>}
|
|
||||||
|
|
||||||
{unit.abilities.length > 0 && <Grid2 size={12}>
|
|
||||||
<SectionTitle>Abilities</SectionTitle>
|
|
||||||
{unit.abilities.map(a =>
|
|
||||||
<Ability mod={mod} ability={a} race={unit.race}/>
|
|
||||||
)}
|
|
||||||
</Grid2>}
|
|
||||||
|
|
||||||
{unit.jumps != null && <Grid2 size={12}>
|
|
||||||
<Jump jumps={unit.jumps} race={unit.race} mod={mod}/>
|
|
||||||
</Grid2>}
|
|
||||||
|
|
||||||
<Grid2 size={12}>
|
|
||||||
{[...mapWithUnitWeapons.keys()].sort(function (a, b) {
|
|
||||||
return a - b;
|
|
||||||
}).map(h => <WeaponSlot haveReinforceMenu={unit.haveReinforceMenu} key ={h} race={unit.race} mod={mod} unitWeapons={mapWithUnitWeapons.get(h)} hardpoint={h}/>)}
|
|
||||||
</Grid2>
|
|
||||||
{unit.deathExplosions.length > 0 &&
|
|
||||||
<Grid2 size={12}>
|
|
||||||
{unit.deathExplosions.map(da =>
|
|
||||||
<DeathExplosion deathExplosion={da} mod={mod}/>
|
|
||||||
)}
|
|
||||||
</Grid2>
|
|
||||||
}
|
|
||||||
<Grid2 size={12}>
|
|
||||||
<AffectedResearches researches={unit.affectedResearches} modId={mod.id} raceId={unit.race.id} />
|
|
||||||
</Grid2>
|
|
||||||
</Grid2>
|
|
||||||
|
|
||||||
<Box sx={{ mt: 3, mb: 2, fontWeight: 600, fontSize: '0.85rem' }}>
|
|
||||||
Hotkey: {unit.hotkey} <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {unit.filename}</span>
|
|
||||||
</Box>
|
|
||||||
<StyledDivider />
|
|
||||||
<UnitsTable modId={mod.id}/>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UnitPage extends React.Component<any, UintPageState> {
|
|
||||||
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const unitsResponse = await fetch(AvailableUnits + "/" + this.props.match.params.unitId);
|
|
||||||
const unitsData: IUnit = await unitsResponse.json();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
unit: unitsData
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
|
|
||||||
const modData: IMod = await responseMod.json();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
mod: modData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
if (this.state != null && this.state.unit != null && this.state.mod != null) {
|
|
||||||
const backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId
|
|
||||||
const { theme } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxWidth="lg">
|
|
||||||
<BackButton
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<ArrowBack/>}
|
|
||||||
href={backRef}
|
|
||||||
>
|
|
||||||
Back to race
|
|
||||||
</BackButton>
|
|
||||||
{Unit(this.state.unit, this.state.mod, theme)}
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
|
|
||||||
<LinearProgress sx={{ width: '200px' }} />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(withTheme(UnitPage));
|
|
||||||
Loading…
x
Reference in New Issue
Block a user