Moved to next js ssr (with fixes)

This commit is contained in:
anibus
2026-08-15 18:42:06 +03:00
parent 2184f1642c
commit 23bfad8956
21 changed files with 1473 additions and 1659 deletions
+354 -12
View File
@@ -1,16 +1,358 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
import React, { useEffect, useState } from 'react';
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'), {
ssr: false,
loading: () => (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
),
});
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
fontSize: '1.25rem',
}));
export default function BuildingPageClient() {
return <BuildingPageLegacy />;
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>&nbsp;<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>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_requisition.gif" />&nbsp;
{building.buildCostRequisition.toFixed(0)}</span>}
{building.buildCostPower > 0 && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_power.gif" />&nbsp;
{building.buildCostPower.toFixed(0)}</span>}
{(building.buildCostPopulation !== undefined && building.buildCostPopulation > 0) &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_orksquadcap.gif" />&nbsp;
{building.buildCostPopulation.toFixed(0)}</span>}
{(building.buildCostFaith !== undefined && building.buildCostFaith > 0) &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_faith.gif" />&nbsp;
{building.buildCostFaith}</span>}
{(building.buildCostSouls !== undefined && building.buildCostSouls > 0) &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_souls.gif" />&nbsp;
{building.buildCostSouls.toFixed(0)}</span>}
{(building.buildCostTime !== undefined && building.buildCostTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{ verticalAlign: "top", fontSize: "18px" }} />&nbsp;
{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>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_requisition.gif" />&nbsp;
{building.requisitionIncome}</span>}
{building.powerIncome !== undefined && building.powerIncome > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_power.gif" />&nbsp;
{building.powerIncome}</span>}
{building.faithIncome !== undefined && building.faithIncome > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_faith.gif" />&nbsp;
{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" />&nbsp;
{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 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
);
}
}
+67 -12
View File
@@ -1,16 +1,71 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
import React from 'react';
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'), {
ssr: false,
loading: () => (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
),
});
const ModNotFound = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
export default function ModPageClient() {
return <ModPageLegacy />;
interface ModPageClientProps {
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>
);
}
}
+238 -4
View File
@@ -1,14 +1,248 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
import React, { useEffect } from 'react';
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';
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 {
initialMods: IMod[];
}
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>
);
}
+258 -12
View File
@@ -1,16 +1,262 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
import React from 'react';
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'), {
ssr: false,
loading: () => (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
),
});
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
fontSize: '1.25rem',
}));
export default function RacePageClient() {
return <RacePageLegacy />;
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,
}));
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>&nbsp;<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('\\', '/')} />}
&nbsp;<span style={{ fontSize: 14 }}>{unitName}</span>
{unit.canDetect && <span>&nbsp;<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('\\', '/')} />}
&nbsp;<StyledLink>{building.name}</StyledLink>
{building.canDetect && <span>&nbsp;<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>
);
}
}
+438 -11
View File
@@ -1,16 +1,443 @@
'use client';
import dynamic from 'next/dynamic';
import { Box, LinearProgress } from '@mui/material';
import React, { useEffect, useState } from 'react';
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'), {
ssr: false,
loading: () => (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
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,
}));
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" />&nbsp;{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('\\', '/')} />}
&nbsp; {props.name}
{props.canDetect && <span>&nbsp;<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>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_requisition.gif" />&nbsp;
{unit.buildCostRequisition.toFixed(0)}</span>}
{unit.buildCostPower > 0 && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_power.gif" />&nbsp;
{unit.buildCostPower.toFixed(0)}</span>}
{(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_orksquadcap.gif" />&nbsp;
{unit.buildCostPopulation.toFixed(0)}</span>}
{(unit.buildCostFaith !== undefined && unit.buildCostFaith > 0) &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_faith.gif" />&nbsp;
{unit.buildCostFaith}</span>}
{(unit.buildCostSouls !== undefined && unit.buildCostSouls > 0) &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_souls.gif" />&nbsp;
{unit.buildCostSouls.toFixed(0)}</span>}
{unit.capInfantry > 0 && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_cap_infantry.gif" />&nbsp;
{unit.capInfantry}</span>}
{unit.capSupport > 0 && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_cap_vehicle.gif" />&nbsp;
{unit.capSupport}</span>}
{(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{ verticalAlign: "top", fontSize: "18px" }} />&nbsp;
{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>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_requisition.gif" />&nbsp;
{unit.reinforceCostRequisition.toFixed(0)}</span> : <span />}
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_power.gif" />&nbsp;
{unit.reinforceCostPower.toFixed(0)}</span>}
{(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_orksquadcap.gif" />&nbsp;
{unit.reinforceCostPopulation.toFixed(0)}</span>}
{(unit.reinforceCostFaith !== undefined && unit.reinforceCostFaith > 0) &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_faith.gif" />&nbsp;
{unit.reinforceCostFaith}</span>}
{(unit.reinforceCostSouls !== undefined && unit.reinforceCostSouls > 0) &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_souls.gif" />&nbsp;
{unit.reinforceCostSouls.toFixed(0)}</span>}
{(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{ verticalAlign: "top", fontSize: "18px" }} />&nbsp;
{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>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_requisition.gif" />&nbsp;
{unit.requisitionIncome}</span>}
{unit.powerIncome !== undefined && unit.powerIncome > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_power.gif" />&nbsp;
{unit.powerIncome}</span>}
{unit.faithIncome !== undefined && unit.faithIncome > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_faith.gif" />&nbsp;
{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" />&nbsp;
{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() {
return <UnitPageLegacy />;
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 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
);
}
}