Soulstorm-wiki-frontend/components/pages/BuildingPageClient.tsx
2026-08-15 18:42:06 +03:00

359 lines
18 KiB
TypeScript

'use client';
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 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)' },
}));
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>
);
}
}