444 lines
24 KiB
TypeScript
444 lines
24 KiB
TypeScript
'use client';
|
|
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 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" /> {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 }}>
|
|
<LinearProgress sx={{ width: '200px' }} />
|
|
</Box>
|
|
);
|
|
}
|
|
}
|