359 lines
18 KiB
TypeScript
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> <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 }}>
|
|
<LinearProgress sx={{ width: '200px' }} />
|
|
</Box>
|
|
);
|
|
}
|
|
}
|