Vibe-code дизайн

This commit is contained in:
anibus 2026-07-14 23:40:25 +03:00
parent 9d4bb49713
commit bc36f1e56d
26 changed files with 1505 additions and 661 deletions

View File

@ -1,6 +1,6 @@
# Soulstorm wiki frontend # Soulstorm wiki frontend
## Стек технологий: ## Tech stack:
- react (вперемешку классы и функции) - react (mix of classes and functions)
- typescript - typescript

View File

@ -1,65 +1,105 @@
import './App.css'; import './App.css';
import React from "react"; import React from "react";
import {AppBar, Container, createTheme, IconButton, ThemeProvider, Toolbar, Typography} from "@mui/material"; import {
AppBar,
Box,
Container,
createTheme,
ThemeProvider,
Toolbar,
Typography,
useTheme
} from "@mui/material";
import { MyRoutes } from "./Routes"; import { MyRoutes } from "./Routes";
import { styled } from '@mui/material/styles';
// Styled components
const StyledAppBar = styled(AppBar)(({ theme }) => ({
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
}));
const LogoText = styled(Typography)(({ theme }) => ({
fontSize: '1.5rem',
fontWeight: 800,
background: 'linear-gradient(135deg, #e94560 0%, #ff6b6b 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
letterSpacing: '-0.5px',
cursor: 'pointer',
'&:hover': {
transform: 'scale(1.05)',
transition: 'transform 0.2s ease',
},
}));
function App() { function App() {
console.log(process.env); const theme = useTheme();
const steamLogin = (event: React.MouseEvent<HTMLButtonElement>) => { const themeConfig = createTheme({
(document.getElementById('submit-steam-login') as HTMLFormElement).click() breakpoints: {
}; values: {
xs: 0,
const theme = createTheme({ sm: 600,
md: 900,
lg: 1320,
xl: 1536,
},
},
palette: { palette: {
primary: { primary: {
main: "#191919", main: "#1a1a2e",
},
background: {
default: '#0a0a0a',
paper: '#1a1a2e',
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", sans-serif',
},
shape: {
borderRadius: 12,
},
components: {
MuiTableCell: {
styleOverrides: {
root: {
color: 'rgba(255, 255, 255, 0.85)',
},
},
}, },
}, },
}); });
return ( return (
<div > <Box sx={{ minHeight: '100vh', backgroundColor: '#0a0a0a' }}>
<ThemeProvider theme={theme}> <ThemeProvider theme={themeConfig}>
<AppBar position="static"> <a href="/">
<Toolbar> <StyledAppBar position="sticky">
<IconButton <Container maxWidth="lg">
size="large" <Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
edge="start" {/* Left side - Logo */}
color="inherit" <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
aria-label="menu" <LogoText variant="h6" sx={{ textDecoration: 'none' }}>
sx={{mr: 2}} 🎮 Autogeneration wiki
> </LogoText>
</IconButton> </Box>
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>
Autogeneration wiki {/* Right side - Navigation */}
</Typography> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/*<Button color="inherit" onClick={steamLogin}> </Box>
Log in through Steam
</Button>*/}
</Toolbar> </Toolbar>
</AppBar> </Container>
<Container> </StyledAppBar>
</a>
<Container maxWidth="lg" sx={{ py: 4 }}>
<MyRoutes /> <MyRoutes />
</Container> </Container>
</ThemeProvider> </ThemeProvider>
</Box>
<form action="https://steamcommunity.com/openid/login" method="post">
<input type="hidden" name="openid.identity"
value="http://specs.openid.net/auth/2.0/identifier_select"/>
<input type="hidden" name="openid.claimed_id"
value="http://specs.openid.net/auth/2.0/identifier_select"/>
<input type="hidden" name="openid.ns" value="http://specs.openid.net/auth/2.0"/>
<input type="hidden" name="openid.mode" value="checkid_setup"/>
<input type="hidden" name="openid.realm" value={process.env.REACT_APP_HOST_URL}/>
<input type="hidden" name="openid.return_to"
value={process.env.REACT_APP_HOST_URL + "\\login"}/>
<button id="submit-steam-login" type="submit" hidden={true}></button>
</form>
</div>
); );
} }

View File

@ -6,7 +6,15 @@ import {getIcon} from "./ModifiersProvideTable";
import {IMod} from "../types/Imod"; import {IMod} from "../types/Imod";
import AbilityFull from "./AbilityFull"; import AbilityFull from "./AbilityFull";
import {Irace} from "../types/Irace"; import {Irace} from "../types/Irace";
import {styled} from '@mui/material/styles';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
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' },
}));
interface IAbilityProps { interface IAbilityProps {
ability: IAbilityShort, ability: IAbilityShort,
@ -18,22 +26,23 @@ function Ability(props: IAbilityProps){
const ability = props.ability const ability = props.ability
return <div className='addon-research-accordion' id={"research-" + ability.id}><Accordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}> return <div className='addon-research-accordion' id={"research-" + ability.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore/>} expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
sx={{color: '#ffffff'}}
> >
<span style={{fontSize: 20}}> <span style={{fontSize: 20, color: '#ffffff'}}>
{ability.activationType === "Passive ability" ? <img className="abilityIcon" src="/images/PassiveIcon.gif"/> : <img className="abilityIcon" src={getIcon(ability.icon)}/>} {ability.activationType === "Passive ability" ? <img className="abilityIcon" src="/images/PassiveIcon.gif"/> : <img className="abilityIcon" src={getIcon(ability.icon)}/>}
&nbsp; {ability.name ? ability.name : ability.fileName.charAt(0).toUpperCase() + ability.fileName.slice(1).replaceAll('_', ' ').replace('.rgd', '')} &nbsp; {ability.name ? ability.name : ability.fileName.charAt(0).toUpperCase() + ability.fileName.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
<i style={{fontSize: 12}}> ({ability.activationType})</i> <i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)'}}> ({ability.activationType})</i>
</span> </span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<AbilityFull abilityId={ability.id} mod={props.mod} race={props.race} /> <AbilityFull abilityId={ability.id} mod={props.mod} race={props.race} />
</AccordionDetails> </AccordionDetails>
</Accordion></div> </StyledAccordion></div>
} }

View File

@ -27,6 +27,7 @@ import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import ability from "./Ability"; import ability from "./Ability";
import AbilitySpawnedObject from "./AbilitySpawnedObject"; import AbilitySpawnedObject from "./AbilitySpawnedObject";
import DpsTable from "./DpsTable"; import DpsTable from "./DpsTable";
import {DescriptionBox} from "../commons/DescriptionBox";
interface IAbilityFullState { interface IAbilityFullState {
ability?: IAbilityFill; ability?: IAbilityFill;
@ -41,16 +42,34 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
const StyledTableCell = styled(TableCell)(({theme}) => ({ const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: "rgb(244,244,244)", backgroundColor: 'rgba(233, 69, 96, 0.15)',
color: '#ffffff',
fontWeight: 700,
marginRight: 'auto', marginRight: 'auto',
marginLeft: 'auto', marginLeft: 'auto',
paddingLeft: 10 paddingLeft: 10,
borderBottom: '2px solid rgba(233, 69, 96, 0.3)',
}, },
[`&.${tableCellClasses.body}`]: { [`&.${tableCellClasses.body}`]: {
fontSize: 12, fontSize: 12,
textAlign: 'center', textAlign: 'center',
color: 'rgba(255, 255, 255, 0.85)',
paddingRight: 18, paddingRight: 18,
paddingLeft: 10 paddingLeft: 10,
},
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
}, },
})); }));
@ -90,21 +109,21 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
const showTargetTable = !showDpsTable && ability.abilityEnvironment == null && (ability.targetFilter.length > 0 || ability.radius !== 0) const showTargetTable = !showDpsTable && ability.abilityEnvironment == null && (ability.targetFilter.length > 0 || ability.radius !== 0)
return ( return (
<div> <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12}}> <Grid2 size={{xs: 12}}>
{ability.initialDelayTime !== undefined && props.isChild === true && {ability.initialDelayTime !== undefined && props.isChild === true &&
<b><br/><center>&nbsp;After {ability.initialDelayTime.toFixed(1)}s&nbsp; <b><br/><center style={{color: 'rgba(255,255,255,0.7)'}}>&nbsp;After {ability.initialDelayTime.toFixed(1)}s&nbsp;
<img style={{verticalAlign: "top"}} <img style={{verticalAlign: "top"}}
src="/images/Time_icon.webp"/></center></b>} src="/images/Time_icon.webp"/></center></b>}
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table"> <TableBody id="unit-stats-table">
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Affected on</TableCell> <TableCell component="th" scope="row" >Affected on</TableCell>
<TableCell component="th" scope="row">{ability.areaFilter}</TableCell> <TableCell>{ability.areaFilter}</TableCell>
</TableRow> </TableRow>
{props.isChild !== true && (ability.costPower !== undefined && ability.costPower > 0 || {props.isChild !== true && (ability.costPower !== undefined && ability.costPower > 0 ||
ability.costRequisition !== undefined && ability.costRequisition > 0 || ability.costRequisition !== undefined && ability.costRequisition > 0 ||
@ -114,7 +133,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Cost</TableCell> <TableCell component="th" scope="row" >Cost</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{ability.costRequisition !== undefined && ability.costRequisition > 0 && {ability.costRequisition !== undefined && ability.costRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp; src="/images/Resource_requisition.gif"/>&nbsp;
@ -136,18 +155,18 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
{props.isChild !== true && ability.rechargeTime !== undefined && ability.rechargeTime > 0 && ability.activation !== "On/off ability" && {props.isChild !== true && ability.rechargeTime !== undefined && ability.rechargeTime > 0 && ability.activation !== "On/off ability" &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Recharge time</TableCell> <TableCell component="th" scope="row" >Recharge time</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<img style={{verticalAlign: "top"}} <img style={{verticalAlign: "top"}}
src="/images/Time_icon.webp"/>&nbsp; src="/images/Time_icon.webp"/>&nbsp;
{ability.rechargeTime}s&nbsp; {ability.rechargeTime}s&nbsp;
{ability.rechargeTimerGlobal && <i style={{fontSize: 10}}>global cooldown</i>} {ability.rechargeTimerGlobal && <i style={{fontSize: 10, color: 'rgba(255,255,255,0.5)'}}>global cooldown</i>}
</TableCell> </TableCell>
</TableRow> </TableRow>
} }
{props.isChild !== true && ability.initialDelayTime !== undefined && ability.initialDelayTime > 0 && {props.isChild !== true && ability.initialDelayTime !== undefined && ability.initialDelayTime > 0 &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Delay after cast</TableCell> <TableCell component="th" scope="row" >Delay after cast</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<img style={{verticalAlign: "top"}} <img style={{verticalAlign: "top"}}
src="/images/Time_icon.webp"/>&nbsp; src="/images/Time_icon.webp"/>&nbsp;
{ability.initialDelayTime.toFixed(1)}s {ability.initialDelayTime.toFixed(1)}s
@ -157,7 +176,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
{ability.durationTime !== undefined && ability.durationTime > 1.5 && {ability.durationTime !== undefined && ability.durationTime > 1.5 &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Duration time</TableCell> <TableCell component="th" scope="row" >Duration time</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<img style={{verticalAlign: "top"}} <img style={{verticalAlign: "top"}}
src="/images/Time_icon.webp"/>&nbsp; src="/images/Time_icon.webp"/>&nbsp;
{ability.durationTime.toFixed(1)}s {ability.durationTime.toFixed(1)}s
@ -167,15 +186,15 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
{ability.areaEffect !== null && ability.radius !== undefined && ability.radius > 0 && {ability.areaEffect !== null && ability.radius !== undefined && ability.radius > 0 &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Radius</TableCell> <TableCell component="th" scope="row" >Radius</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{ability.radius.toFixed(0)}&nbsp;<i style={{fontSize: 10}}>{ability.areaEffect}</i> {ability.radius.toFixed(0)}&nbsp;<i style={{fontSize: 10, color: 'rgba(255,255,255,0.5)'}}>{ability.areaEffect}</i>
</TableCell> </TableCell>
</TableRow> </TableRow>
} }
{props.isChild !== true && ability.range !== undefined && ability.areaFilter !== "Own" && ability.range > 0 && {props.isChild !== true && ability.range !== undefined && ability.areaFilter !== "Own" && ability.range > 0 &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Range</TableCell> <TableCell component="th" scope="row" >Range</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{ability.range.toFixed(0)} {ability.range.toFixed(0)}
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -185,25 +204,19 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Throw force</TableCell> <TableCell component="th" scope="row" >Throw force</TableCell>
<TableCell component="th" <TableCell>{ability.throwForceMin != 0 ? ability.throwForceMin + " - " + ability.throwForceMax : "-"}
scope="row">{ability.throwForceMin != 0 ? ability.throwForceMin + " - " + ability.throwForceMax : "-"}
</TableCell> </TableCell>
</TableRow>} </TableRow>}
<TableRow>
</TableRow>
</TableBody> </TableBody>
</Table> </Table>
</TableContainer><br/> </TableContainer><br/>
</Grid2> </Grid2>
<Grid2 size={8}> <Grid2 size={8}>
<div style={{whiteSpace: "pre-wrap"}}> {props.isChild !== true && <DescriptionBox>{ability.description}</DescriptionBox>}
{props.isChild !== true && ability.description}
</div>
</Grid2> </Grid2>
{showDpsTable ? {showDpsTable ?
<Grid2 size={12}> <Grid2 size={12}>
{(ability.refreshTime !== undefined && ability.durationTime !== undefined && ability.refreshTime * 2 < ability.durationTime) && <i>Every <b>{ability.refreshTime.toFixed(1)}s</b> deal damege:</i>} {(ability.refreshTime !== undefined && ability.durationTime !== undefined && ability.refreshTime * 2 < ability.durationTime) && <i style={{color: 'rgba(255,255,255,0.7)'}}>Every <b style={{color: '#ffffff'}}>{ability.refreshTime.toFixed(1)}s</b> deal damege:</i>}
<DpsTable mod={props.mod} minDamageValue={ability.minDamageValue} minDamage={ability.minDamage} maxDamage={ability.maxDamage} piercings={ability.piercings} targetFilter={ability.targetFilter} moraleDamage={ability.moraleDamage} <DpsTable mod={props.mod} minDamageValue={ability.minDamageValue} minDamage={ability.minDamage} maxDamage={ability.maxDamage} piercings={ability.piercings} targetFilter={ability.targetFilter} moraleDamage={ability.moraleDamage}
/> />
</Grid2> : showTargetTable ? </Grid2> : showTargetTable ?
@ -235,7 +248,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell> <StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell>
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell> <StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell>
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell> <StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell>
@ -272,7 +285,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleLow)}</StyledTableCell> <StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleLow)}</StyledTableCell>
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleMedium)}</StyledTableCell> <StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleMedium)}</StyledTableCell>
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleHigh)}</StyledTableCell> <StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleHigh)}</StyledTableCell>
@ -321,7 +334,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell> <StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell>
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell> <StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell>
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell> <StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell>
@ -352,10 +365,10 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
<Grid2 size={{xs: 12, md: 12}}> <Grid2 size={{xs: 12, md: 12}}>
<Required requirement={ability.requirements} modId={props.mod.id} raceId={props.race.id}/> <Required requirement={ability.requirements} modId={props.mod.id} raceId={props.race.id}/>
</Grid2>} </Grid2>}
{ability.activation !== "Passive ability" && props.isChild !== true && <b className="hotkey">Hotkey: {ability.hotkey}</b>} {props.isChild !== true && ability.activation !== "Passive ability" && <b className="hotkey" >Hotkey: {ability.hotkey}</b>}
</div> </div>
) )
} else return <div>loading...</div>; } else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>;

View File

@ -6,7 +6,8 @@ import {
TableBody, TableBody,
TableCell, TableContainer, TableCell, TableContainer,
TableRow, TableRow,
Tooltip Tooltip,
Typography
} from "@mui/material"; } from "@mui/material";
import {ExpandMore} from "@mui/icons-material"; import {ExpandMore} from "@mui/icons-material";
import React from "react"; import React from "react";
@ -21,6 +22,29 @@ import ArmorType from "./ArmorType";
import Vision from "./Vision"; import Vision from "./Vision";
import Ability from "./Ability"; import Ability from "./Ability";
import DeathExplosion from "./DeathExplosion"; import DeathExplosion from "./DeathExplosion";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox";
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
color: '#ffffff',
fontSize: '1.25rem',
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface AbilityEnvironmentProps { interface AbilityEnvironmentProps {
@ -33,13 +57,14 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
const abilityEnvironment = props.abilityEnvironment const abilityEnvironment = props.abilityEnvironment
return <div> return <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
<h3> {props.abilityEnvironment.name ?? props.abilityEnvironment.filename.charAt(0).toUpperCase() + props.abilityEnvironment.filename.slice(1).replaceAll('_', ' ').replace('.rgd', '')} <SectionTitle>
<i style={{fontSize: 11}}> (Spawned object)</i> {props.abilityEnvironment.name ?? props.abilityEnvironment.filename.charAt(0).toUpperCase() + props.abilityEnvironment.filename.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
</h3> <i style={{fontSize: 11, color: 'rgba(255,255,255,0.5)'}}> (Spawned object)</i>
</SectionTitle>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table"> <TableBody id="unit-stats-table">
{abilityEnvironment.armorType !== null && {abilityEnvironment.armorType !== null &&
@ -47,7 +72,7 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Armor type</TableCell> <TableCell component="th" scope="row" >Armor type</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{abilityEnvironment.armorType2 == null ? {abilityEnvironment.armorType2 == null ?
<ArmorType name={abilityEnvironment.armorType.name} compact={false}/> : <ArmorType name={abilityEnvironment.armorType.name} compact={false}/> :
<Tooltip <Tooltip
@ -62,7 +87,7 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Health</TableCell> <TableCell component="th" scope="row" >Health</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<img style={{verticalAlign: "top"}} <img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp; src="/images/Health_icon.webp"/>&nbsp;
{abilityEnvironment.health} {abilityEnvironment.healthRegeneration != null && abilityEnvironment.healthRegeneration > 0 && {abilityEnvironment.health} {abilityEnvironment.healthRegeneration != null && abilityEnvironment.healthRegeneration > 0 &&
@ -78,9 +103,7 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Move speed</TableCell> <TableCell component="th" scope="row" >Move speed</TableCell>
<TableCell component="th" scope="row"> <TableCell>{abilityEnvironment.moveSpeed}</TableCell>
{abilityEnvironment.moveSpeed}
</TableCell>
</TableRow> </TableRow>
} }
{abilityEnvironment.sightRadius != null && {abilityEnvironment.sightRadius != null &&
@ -88,7 +111,7 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Vision</TableCell> <TableCell component="th" scope="row" >Vision</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<Vision sight={abilityEnvironment.sightRadius ?? 0} <Vision sight={abilityEnvironment.sightRadius ?? 0}
detect={abilityEnvironment.detectRadius ?? 0}/> detect={abilityEnvironment.detectRadius ?? 0}/>
</TableCell> </TableCell>
@ -97,7 +120,7 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
{abilityEnvironment.lifetime != null && {abilityEnvironment.lifetime != null &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Lifetime</TableCell> <TableCell component="th" scope="row" >Lifetime</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<img style={{verticalAlign: "top"}} <img style={{verticalAlign: "top"}}
src="/images/Time_icon.webp"/>&nbsp; src="/images/Time_icon.webp"/>&nbsp;
{abilityEnvironment.lifetime.toFixed(0)}s&nbsp; {abilityEnvironment.lifetime.toFixed(0)}s&nbsp;
@ -109,9 +132,9 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
</TableContainer> </TableContainer>
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 8}}> <Grid2 size={{xs: 12, md: 8}}>
<div style={{whiteSpace: "pre-wrap"}}> <DescriptionBox>
{abilityEnvironment.description} {abilityEnvironment.description}
</div> </DescriptionBox>
</Grid2> </Grid2>
{abilityEnvironment.abilities.length > 0 && <Grid2 size={12}> {abilityEnvironment.abilities.length > 0 && <Grid2 size={12}>
{abilityEnvironment.abilities.map(a => {abilityEnvironment.abilities.map(a =>

View File

@ -24,6 +24,29 @@ import {IDeathExplosion} from "../types/IDeathExplosion";
import Required from "./Required"; import Required from "./Required";
import ArmorTypeNames from "../types/ArmorTypeValues"; import ArmorTypeNames from "../types/ArmorTypeValues";
import DpsTable from "./DpsTable"; import DpsTable from "./DpsTable";
import {styled} from '@mui/material/styles';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
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' },
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.05)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.85)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface DeathExpProps { interface DeathExpProps {
@ -37,44 +60,37 @@ function DeathExplosion(props: DeathExpProps) {
const chanceTotalNotZero = deathExplosion.globalChance * deathExplosion.chance != 0 const chanceTotalNotZero = deathExplosion.globalChance * deathExplosion.chance != 0
return chanceTotalNotZero ? return chanceTotalNotZero ?
<div className='addon-research-accordion' id='death-explosion'><Accordion> <div className='addon-research-accordion' id='death-explosion'><StyledAccordion>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore/>} expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
sx={{color: '#ffffff'}}
> >
<span style={{fontSize: 20}}> <span style={{fontSize: 20, color: '#ffffff'}}>
Death Explosion Death Explosion
</span> </span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table"> <TableBody id="unit-stats-table">
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Chance</TableCell> <TableCell component="th" scope="row" >Chance</TableCell>
<TableCell component="th" scope="row"> <TableCell>{deathExplosion.globalChance * deathExplosion.chance}%</TableCell>
{deathExplosion.globalChance * deathExplosion.chance}%
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Damage radius</TableCell> <TableCell component="th" scope="row" >Damage radius</TableCell>
<TableCell component="th" scope="row"> <TableCell>{deathExplosion.damageRadius}</TableCell>
{deathExplosion.damageRadius}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Throw force</TableCell> <TableCell component="th" scope="row" >Throw force</TableCell>
<TableCell component="th" scope="row"> <TableCell>{deathExplosion.throwForceMin} - {deathExplosion.throwForceMax}</TableCell>
{deathExplosion.throwForceMin} - {deathExplosion.throwForceMax}
</TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
@ -88,7 +104,7 @@ function DeathExplosion(props: DeathExpProps) {
</Grid2> </Grid2>
</Grid2> </Grid2>
</AccordionDetails> </AccordionDetails>
</Accordion></div> : <span></span> </StyledAccordion></div> : <span></span>
} }

View File

@ -19,16 +19,20 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
const StyledTableCell = styled(TableCell)(({theme}) => ({ const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: "rgb(244,244,244)", backgroundColor: 'rgba(233, 69, 96, 0.15)',
color: '#ffffff',
fontWeight: 700,
marginRight: 'auto', marginRight: 'auto',
marginLeft: 'auto', marginLeft: 'auto',
paddingLeft: 10 paddingLeft: 10,
borderBottom: '2px solid rgba(233, 69, 96, 0.3)',
}, },
[`&.${tableCellClasses.body}`]: { [`&.${tableCellClasses.body}`]: {
fontSize: 12, fontSize: 12,
textAlign: 'center', textAlign: 'center',
color: 'rgba(255, 255, 255, 0.85)',
paddingRight: 18, paddingRight: 18,
paddingLeft: 10 paddingLeft: 10,
}, },
})); }));
@ -158,7 +162,7 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
@ -212,7 +216,7 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>

View File

@ -6,6 +6,20 @@ import {Irace} from "../types/Irace";
import {IAffectedData} from "../types/IAffectedData"; import {IAffectedData} from "../types/IAffectedData";
import {IMod} from "../types/Imod"; import {IMod} from "../types/Imod";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer"; import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import {styled} from '@mui/material/styles';
import {StyledLink} from "../commons/StyledLink";
// Styled components for dark theme
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.85)',
},
}));
interface IModifiersProvidesTable { interface IModifiersProvidesTable {
modifiers: IModifier[], modifiers: IModifier[],
@ -142,15 +156,14 @@ export function ModifiersProvidesAddonTable(props: IModifiersProvidesTable) {
let noWeaponMods = props.modifiers.filter(m => !m.reference.includes("default_weapon_modifier_hardpoint")) let noWeaponMods = props.modifiers.filter(m => !m.reference.includes("default_weapon_modifier_hardpoint"))
return ( return (
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
{noWeaponMods.map(m => {noWeaponMods.map(m =>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">{getModName(m.reference)}</TableCell> <TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getModName(m.reference)}</TableCell>
<TableCell component="th" <TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}</TableCell>
scope="row">{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}</TableCell>
</TableRow> </TableRow>
)} )}
</Table> </Table>
@ -168,37 +181,37 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
var affectedUnit = props.affectedData.affectedUnits.find(au => au.filename.replaceAll('.rgd', '').split(";")[0] === target || au.filename.replaceAll('.rgd', '').split(";")[1] === target); var affectedUnit = props.affectedData.affectedUnits.find(au => au.filename.replaceAll('.rgd', '').split(";")[0] === target || au.filename.replaceAll('.rgd', '').split(";")[1] === target);
if (affectedUnit != null) { if (affectedUnit != null) {
return <span><img style={{verticalAlign: "top", height: 25}} return <span><img style={{verticalAlign: "top", height: 25}}
src={getIcon(affectedUnit.icon)}/><a src={getIcon(affectedUnit.icon)}/><StyledLink
href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedUnit.id}`}> href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedUnit.id}`}>
{affectedUnit.name} {affectedUnit.name}
</a></span> </StyledLink></span>
} }
var affectedSergeant = props.affectedData.affectedSergeants.find(as => as.filename.replaceAll('.rgd', '') === target); var affectedSergeant = props.affectedData.affectedSergeants.find(as => as.filename.replaceAll('.rgd', '') === target);
if (affectedSergeant != null) { if (affectedSergeant != null) {
return <span><img style={{verticalAlign: "top", height: 25}} return <span><img style={{verticalAlign: "top", height: 25}}
src={getIcon(affectedSergeant.icon)}/>{affectedSergeant.name} at <img src={getIcon(affectedSergeant.icon)}/>{affectedSergeant.name} at <img
style={{verticalAlign: "top", height: 25}} style={{verticalAlign: "top", height: 25}}
src={getIcon(affectedSergeant.unit.icon)}/><a src={getIcon(affectedSergeant.unit.icon)}/><StyledLink
href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedSergeant.unit.id}`}> href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedSergeant.unit.id}`}>
{affectedSergeant.unit.name} {affectedSergeant.unit.name}
</a></span> </StyledLink></span>
} }
var affectedBuilding = props.affectedData.affectedBuildings.find(ab => ab.filename.replaceAll('.rgd', '') === target); var affectedBuilding = props.affectedData.affectedBuildings.find(ab => ab.filename.replaceAll('.rgd', '') === target);
if (affectedBuilding != null) { if (affectedBuilding != null) {
return <span><img style={{verticalAlign: "top", height: 25}} src={getIcon(affectedBuilding.icon)}/><a return <span><img style={{verticalAlign: "top", height: 25}} src={getIcon(affectedBuilding.icon)}/><StyledLink
href={`/mod/${props.modId}/race/${props.race.id}/building/${affectedBuilding.id}`}> href={`/mod/${props.modId}/race/${props.race.id}/building/${affectedBuilding.id}`}>
{affectedBuilding.name} {affectedBuilding.name}
</a> </span> </StyledLink> </span>
} }
var affectedWeapon = props.affectedData.affectedWeapons.find(aw => aw.filename.replaceAll('.rgd', '') === target); var affectedWeapon = props.affectedData.affectedWeapons.find(aw => aw.filename.replaceAll('.rgd', '') === target);
if (affectedWeapon != null) { if (affectedWeapon != null) {
return <span>{affectedWeapon.name ? affectedWeapon.name : affectedWeapon.filename.replaceAll('_', ' ').replace('.rgd', '')} at { return <span>{affectedWeapon.name ? affectedWeapon.name : affectedWeapon.filename.replaceAll('_', ' ').replace('.rgd', '')} at {
affectedWeapon.units.map(unit => affectedWeapon.units.map(unit =>
<span>{affectedWeapon != null && affectedWeapon.units.length + affectedWeapon.sergeants.length > 1 ? <br/> : <span></span>}<img style={{verticalAlign: "top", height: 25}} src={getIcon(unit.icon)}/><a <span>{affectedWeapon != null && affectedWeapon.units.length + affectedWeapon.sergeants.length > 1 ? <br/> : <span></span>}<img style={{verticalAlign: "top", height: 25}} src={getIcon(unit.icon)}/><StyledLink
href={`/mod/${props.modId}/race/${props.race.id}/unit/${unit.id}`}> href={`/mod/${props.modId}/race/${props.race.id}/unit/${unit.id}`}>
{unit.name} {unit.name}
</a> </span> </StyledLink> </span>
) )
} }
{ {
@ -206,18 +219,18 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
<span>{affectedWeapon != null && affectedWeapon.units.length + affectedWeapon.sergeants.length > 1 ? <br/> : <span></span>}<img style={{verticalAlign: "top", height: 25}} <span>{affectedWeapon != null && affectedWeapon.units.length + affectedWeapon.sergeants.length > 1 ? <br/> : <span></span>}<img style={{verticalAlign: "top", height: 25}}
src={getIcon(affectedSergeant.icon)}/>{affectedSergeant.name} at <img src={getIcon(affectedSergeant.icon)}/>{affectedSergeant.name} at <img
style={{verticalAlign: "top", height: 25}} style={{verticalAlign: "top", height: 25}}
src={getIcon(affectedSergeant.unit.icon)}/><a src={getIcon(affectedSergeant.unit.icon)}/><StyledLink
href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedSergeant.unit.id}`}> href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedSergeant.unit.id}`}>
{affectedSergeant.unit.name} {affectedSergeant.unit.name}
</a> </span> </StyledLink> </span>
) )
} }
{ {
affectedWeapon.buildings.map(affectedBuilding => affectedWeapon.buildings.map(affectedBuilding =>
<span><img style={{verticalAlign: "top", height: 25}} src={getIcon(affectedBuilding.icon)}/><a <span><img style={{verticalAlign: "top", height: 25}} src={getIcon(affectedBuilding.icon)}/><StyledLink
href={`/mod/${props.modId}/race/${props.race.id}/building/${affectedBuilding.id}`}> href={`/mod/${props.modId}/race/${props.race.id}/building/${affectedBuilding.id}`}>
{affectedBuilding.name} {affectedBuilding.name}
</a> </span> </StyledLink> </span>
) )
} }
</span> </span>
@ -228,23 +241,21 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
} }
return ( return (
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
{mods.map(m => {mods.map(m =>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">{getTarget(m.target)}</TableCell> <TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getTarget(m.target)}</TableCell>
<TableCell style={{minWidth: 120}} component="th" <TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)', minWidth: 120}}>{getModName(m.reference)}</TableCell>
scope="row">{getModName(m.reference)}</TableCell> <TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}
<TableCell component="th"
scope="row">{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}
{m.maxLifeTime !== undefined && m.maxLifeTime > 0 && {m.maxLifeTime !== undefined && m.maxLifeTime > 0 &&
<span><img style={{verticalAlign: "top"}} src='/images/Time_icon.webp'/>{m.maxLifeTime}s <span><img style={{verticalAlign: "top"}} src='/images/Time_icon.webp'/>{m.maxLifeTime}s
</span>} </span>}
<i style={{fontSize: 11}}>{m.exclusive ? " does not stuck" : " does stuck"}</i> <i style={{fontSize: 11}}>{m.exclusive ? " does not stack" : " does stack"}</i>
{m.probabilityOfApplying !== undefined && m.probabilityOfApplying !== 1.0 && {m.probabilityOfApplying !== undefined && m.probabilityOfApplying !== 1.0 &&
<span><i style={{fontSize: 11}}> ({m.probabilityOfApplying * 100}% chance)</i> <span><i style={{fontSize: 11, color: 'rgba(255,255,255,0.5)'}}> ({m.probabilityOfApplying * 100}% chance)</i>
</span>} </span>}
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -5,6 +5,7 @@ import {IconUrl} from "../core/api";
import {IResearchRequirements} from "../types/IResearchRequirements"; import {IResearchRequirements} from "../types/IResearchRequirements";
import {IRequirement, IRequirementLimitByBuildingDto, IRequirementSquadsDto} from "../types/IRequirement"; import {IRequirement, IRequirementLimitByBuildingDto, IRequirementSquadsDto} from "../types/IRequirement";
import {IUnitShort} from "../types/IUnitShort"; import {IUnitShort} from "../types/IUnitShort";
import {StyledLink} from "../commons/StyledLink";
interface IRequiredProps{ interface IRequiredProps{
requirement: IRequirement, requirement: IRequirement,
@ -20,9 +21,9 @@ function Required (props: IRequiredProps) {
<span>&nbsp; Global addon: <img style={{verticalAlign: "top", height: 25}} <span>&nbsp; Global addon: <img style={{verticalAlign: "top", height: 25}}
src={getIcon(rga.icon)}/> src={getIcon(rga.icon)}/>
&nbsp;<a href={`/mod/${props.modId}/race/${props.raceId}/building/${rga.buildingId}#addon-${rga.id}`}> &nbsp;<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${rga.buildingId}#addon-${rga.id}`}>
{rga.name} {rga.name}
</a></span> </StyledLink></span>
)}</div> )}</div>
} }
@ -30,29 +31,29 @@ function Required (props: IRequiredProps) {
return requirementBuildings.length > 0 && ( return requirementBuildings.length > 0 && (
<div>&nbsp; Buildings: <img style={{verticalAlign: "top", height: 25}} <div>&nbsp; Buildings: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + requirementBuildings[0].icon.replaceAll('\\', '/')}/> src={IconUrl + requirementBuildings[0].icon.replaceAll('\\', '/')}/>
&nbsp;{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[0].id}`}> &nbsp;{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[0].id}`}>
{requirementBuildings[0].name} {requirementBuildings[0].name}
</a> {requirementBuildings[1] !== undefined && <span>or &nbsp;<img style={{verticalAlign: "top", height: 25}} </StyledLink> {requirementBuildings[1] !== undefined && <span>or &nbsp;<img style={{verticalAlign: "top", height: 25}}
src={IconUrl + requirementBuildings[1].icon.replaceAll('\\', '/')}/> src={IconUrl + requirementBuildings[1].icon.replaceAll('\\', '/')}/>
&nbsp;{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[1].id}`}> &nbsp;{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[1].id}`}>
{requirementBuildings[1].name}</a></span> } </div> {requirementBuildings[1].name}</StyledLink></span> } </div>
); );
} }
function renderRequiremenStructureExclusive(buildingExclusive: IBuildingShort) { function renderRequiremenStructureExclusive(buildingExclusive: IBuildingShort) {
return <div>&nbsp; Can't be build with: <img style={{verticalAlign: "top", height: 25}} return <div>&nbsp; Can't be build with: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + buildingExclusive.icon.replaceAll('\\', '/')}/> src={IconUrl + buildingExclusive.icon.replaceAll('\\', '/')}/>
&nbsp;{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${buildingExclusive.id}`}> &nbsp;{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${buildingExclusive.id}`}>
{buildingExclusive.name} {buildingExclusive.name}
</a> </div> </StyledLink> </div>
} }
function renderRequiremenAddonExclusive(addonShort: IBuildingAddonShort) { function renderRequiremenAddonExclusive(addonShort: IBuildingAddonShort) {
return <div>&nbsp; Can't be build with: <img style={{verticalAlign: "top", height: 25}} return <div>&nbsp; Can't be build with: <img style={{verticalAlign: "top", height: 25}}
src={getIcon(addonShort.icon)}/> src={getIcon(addonShort.icon)}/>
&nbsp;<a href={`/mod/${props.modId}/race/${props.raceId}/building/${addonShort.buildingId}#addon-${addonShort.id}`}> &nbsp;<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${addonShort.buildingId}#addon-${addonShort.id}`}>
{addonShort.name} {addonShort.name}
</a></div> </StyledLink></div>
} }
function renderRequirementOwnership(requirementOwnership: string[],) { function renderRequirementOwnership(requirementOwnership: string[],) {
@ -70,8 +71,8 @@ function Required (props: IRequiredProps) {
function renderLimitPerBuilding(limitPerBuilding: IRequirementLimitByBuildingDto,) { function renderLimitPerBuilding(limitPerBuilding: IRequirementLimitByBuildingDto,) {
return <span>&nbsp; One <img style={{verticalAlign: "top", height: 25}} return <span>&nbsp; One <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + limitPerBuilding.building.icon.replaceAll('\\', '/')}/> src={IconUrl + limitPerBuilding.building.icon.replaceAll('\\', '/')}/>
&nbsp;{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${limitPerBuilding.building.id}`}> &nbsp;{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${limitPerBuilding.building.id}`}>
{limitPerBuilding.building.name} </a> allow produce max {limitPerBuilding.limit}</span> {limitPerBuilding.building.name} </StyledLink> allow produce max {limitPerBuilding.limit}</span>
} }
function renderRequirementResearches(requirementResearches: IResearchRequirements[],) { function renderRequirementResearches(requirementResearches: IResearchRequirements[],) {
@ -79,9 +80,9 @@ function Required (props: IRequiredProps) {
<span>&nbsp; Research {rs.researchMustNotBeComplete ? "not" : ""}: <img style={{verticalAlign: "top", height: 25}} <span>&nbsp; Research {rs.researchMustNotBeComplete ? "not" : ""}: <img style={{verticalAlign: "top", height: 25}}
src={getIcon(rs.researchShortDto.icon)}/> src={getIcon(rs.researchShortDto.icon)}/>
&nbsp;<a href={`/mod/${props.modId}/race/${props.raceId}/building/${rs.researchShortDto.buildingId}#research-${rs.researchShortDto.id}`}> &nbsp;<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${rs.researchShortDto.buildingId}#research-${rs.researchShortDto.id}`}>
{rs.researchShortDto.name} {rs.researchShortDto.name}
</a></span> </StyledLink></span>
)}</div> )}</div>
} }
@ -90,9 +91,9 @@ function Required (props: IRequiredProps) {
<span>&nbsp; Squad: {rs.count} <img style={{verticalAlign: "top", height: 25}} <span>&nbsp; Squad: {rs.count} <img style={{verticalAlign: "top", height: 25}}
src={getIcon(rs.squad.icon)}/> src={getIcon(rs.squad.icon)}/>
&nbsp;<a href={`/mod/${props.modId}/race/${props.raceId}/unit/${rs.squad.id}`}> &nbsp;<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/unit/${rs.squad.id}`}>
{rs.squad.name} {rs.squad.name}
</a></span> </StyledLink></span>
)}</div> )}</div>
} }
@ -101,12 +102,12 @@ function Required (props: IRequiredProps) {
return requirementResearches.length > 0 && ( return requirementResearches.length > 0 && (
<div>&nbsp; Researches: <span> <img style={{verticalAlign: "top", height: 25}} <div>&nbsp; Researches: <span> <img style={{verticalAlign: "top", height: 25}}
src={getIcon(requirementResearches[0].researchShortDto.icon)}/> src={getIcon(requirementResearches[0].researchShortDto.icon)}/>
&nbsp;<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementResearches[0].researchShortDto.buildingId}#research-${requirementResearches[0].researchShortDto.id}`}> &nbsp;<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementResearches[0].researchShortDto.buildingId}#research-${requirementResearches[0].researchShortDto.id}`}>
{requirementResearches[0].researchShortDto.name} {requirementResearches[0].researchShortDto.name}
</a></span> {requirementResearches[1] != null && <span> or <img style={{verticalAlign: "top", height: 25}} src={getIcon(requirementResearches[1].researchShortDto.icon)}/> </StyledLink></span> {requirementResearches[1] != null && <span> or <img style={{verticalAlign: "top", height: 25}} src={getIcon(requirementResearches[1].researchShortDto.icon)}/>
&nbsp; <a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementResearches[1].researchShortDto.buildingId}#research-${requirementResearches[1].researchShortDto.id}`}> &nbsp; <StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementResearches[1].researchShortDto.buildingId}#research-${requirementResearches[1].researchShortDto.id}`}>
{requirementResearches[1].researchShortDto.name} {requirementResearches[1].researchShortDto.name}
</a></span>} </div> </StyledLink></span>} </div>
); );
} }
@ -114,48 +115,48 @@ function Required (props: IRequiredProps) {
return ( return (
<div> <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
<h4>Required: </h4> <h4 style={{color: '#ffffff', fontWeight: 700}}>Required: </h4>
{requirement.requiredTotalPop !== undefined && requirement.requiredTotalPop !== null && {requirement.requiredTotalPop !== undefined && requirement.requiredTotalPop !== null &&
<div>&nbsp; Population: <img style={{verticalAlign: "top", height: 25}} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Population: <img style={{verticalAlign: "top", height: 25}}
src="/images/Resource_orksquadcap.gif"/>&nbsp; src="/images/Resource_orksquadcap.gif"/>&nbsp;
{requirement.requiredTotalPop}</div> {requirement.requiredTotalPop}</div>
} }
{requirement.mobBonus !== null && requirement.mobBonus !== undefined && {requirement.mobBonus !== null && requirement.mobBonus !== undefined &&
<div>&nbsp; Mob requirement: <img style={{verticalAlign: "top", height: 25}} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Mob requirement: <img style={{verticalAlign: "top", height: 25}}
src="/images/Mob_bonus.gif"/>&nbsp; src="/images/Mob_bonus.gif"/>&nbsp;
{requirement.mobBonus.mobvalueRequired} in radius {requirement.mobBonus.proximityRequired}</div> {requirement.mobBonus.mobvalueRequired} in radius {requirement.mobBonus.proximityRequired}</div>
} }
{requirement.requireAddon !== null && {requirement.requireAddon !== null &&
<div>&nbsp; Addon: <img style={{verticalAlign: "top", height: 25}} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Addon: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + requirement.requireAddon.icon.replaceAll('\\', '/')}/>&nbsp; src={IconUrl + requirement.requireAddon.icon.replaceAll('\\', '/')}/>&nbsp;
<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirement.requireAddon.buildingId}#addon-${requirement.requireAddon.id}`}> <StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirement.requireAddon.buildingId}#addon-${requirement.requireAddon.id}`}>
{requirement.requireAddon.name} {requirement.requireAddon.name}
</a> </StyledLink>
{requirement.replaceWhenDone && <i> (old provides replace)</i>} {requirement.replaceWhenDone && <i> (old provides replace)</i>}
</div> </div>
} }
{requirement.requirementAddonExclusive != null && {requirement.requirementAddonExclusive != null &&
renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)}</div>}
{requirement.requirementBuildings.map(b => {requirement.requirementBuildings.map(b =>
<div key={b.id}>&nbsp; Building: <img style={{verticalAlign: "top", height: 25}} <div key={b.id} style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Building: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + b.icon.replaceAll('\\', '/')}/>&nbsp; src={IconUrl + b.icon.replaceAll('\\', '/')}/>&nbsp;
<a href= {'/mod/'+ props.modId +'/race/'+ props.raceId +'/building/' + b.id + "/"}>{b.name}</a></div>) <StyledLink href= {'/mod/'+ props.modId +'/race/'+ props.raceId +'/building/' + b.id + "/"}>{b.name}</StyledLink></div>)
} }
{requirement.requirementResearches.length !== 0 && {requirement.requirementResearches.length !== 0 &&
renderRequirementResearches(requirement.requirementResearches,)} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementResearches(requirement.requirementResearches,)}</div>}
{requirement.requirementSquads.length !== 0 && {requirement.requirementSquads.length !== 0 &&
renderRequirementSquads(requirement.requirementSquads,)} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementSquads(requirement.requirementSquads,)}</div>}
{requirement.requirementResearchesEither.length !== 0 && {requirement.requirementResearchesEither.length !== 0 &&
renderRequirementResearchesEither(requirement.requirementResearchesEither,)} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementResearchesEither(requirement.requirementResearchesEither,)}</div>}
{requirement.requirementBuildingsEither.length !== 0 && {requirement.requirementBuildingsEither.length !== 0 &&
renderRequirementBuildingEither(requirement.requirementBuildingsEither,)} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementBuildingEither(requirement.requirementBuildingsEither,)}</div>}
{requirement.requirementStructureExclusive != null && {requirement.requirementStructureExclusive != null &&
renderRequiremenStructureExclusive(requirement.requirementStructureExclusive,)} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequiremenStructureExclusive(requirement.requirementStructureExclusive,)}</div>}
{requirement.limitByBuilding !== undefined && requirement.limitByBuilding !== null && renderLimitPerBuilding(requirement.limitByBuilding)} {requirement.limitByBuilding !== undefined && requirement.limitByBuilding !== null && <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderLimitPerBuilding(requirement.limitByBuilding)}</div>}
{requirement.requirementsGlobalAddons.length !== 0 && {requirement.requirementsGlobalAddons.length !== 0 &&
renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)}</div>}
{renderRequirementOwnership(requirement.requiredOwnership)} <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementOwnership(requirement.requiredOwnership)}</div>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import {ISergeant, IShortWeapon} from "../types/IUnit"; import {ISergeant, IShortWeapon} from "../types/IUnit";
import {Grid2, Paper, Table, TableBody, TableCell, TableContainer, TableRow, Tooltip} from "@mui/material"; import {Grid2, Paper, Table, TableBody, TableCell, TableContainer, TableRow, Tooltip, Typography} from "@mui/material";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer"; import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import ArmorType from "./ArmorType"; import ArmorType from "./ArmorType";
import WeaponSlot from "./WeaponSlot"; import WeaponSlot from "./WeaponSlot";
@ -12,6 +12,29 @@ import Required from "./Required";
import {ModifiersProvidesTable} from "./ModifiersProvideTable"; import {ModifiersProvidesTable} from "./ModifiersProvideTable";
import Ability from "./Ability"; import Ability from "./Ability";
import DeathExplosion from "./DeathExplosion"; import DeathExplosion from "./DeathExplosion";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox";
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
color: '#ffffff',
fontSize: '1.25rem',
}));
export interface SergeantProps { export interface SergeantProps {
@ -38,17 +61,17 @@ const Sergeant = (props: SergeantProps) => {
}) })
return ( return (
<div> <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size= {{xs: 12, md: 4}}> <Grid2 size= {{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table"> <TableBody id="unit-stats-table">
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Cost</TableCell> <TableCell component="th" scope="row" >Cost</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{sergeant.buildCostRequisition > 0 && {sergeant.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp; src="/images/Resource_requisition.gif"/>&nbsp;
@ -93,7 +116,7 @@ const Sergeant = (props: SergeantProps) => {
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Armor type</TableCell> <TableCell component="th" scope="row" >Armor type</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{sergeant.armorType2 == null ? <ArmorType name={sergeant.armorType.name} compact={false}/> {sergeant.armorType2 == null ? <ArmorType name={sergeant.armorType.name} compact={false}/>
: <Tooltip title={"upgrade/debuff can turns to " + sergeant.armorType2.name}><span><ArmorType name={sergeant.armorType.name} compact={false}/></span></Tooltip> } : <Tooltip title={"upgrade/debuff can turns to " + sergeant.armorType2.name}><span><ArmorType name={sergeant.armorType.name} compact={false}/></span></Tooltip> }
</TableCell> </TableCell>
@ -102,7 +125,7 @@ const Sergeant = (props: SergeantProps) => {
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Health</TableCell> <TableCell component="th" scope="row" >Health</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<span><img style={{verticalAlign: "top"}} <span><img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp; src="/images/Health_icon.webp"/>&nbsp;
{sergeant.health} {sergeant.healthRegeneration > 0 && {sergeant.health} {sergeant.healthRegeneration > 0 &&
@ -113,7 +136,7 @@ const Sergeant = (props: SergeantProps) => {
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Morale</TableCell> <TableCell component="th" scope="row" >Morale</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{sergeant.moraleIncomeMax && <span><img style={{verticalAlign: "top"}} {sergeant.moraleIncomeMax && <span><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/> +{sergeant.moraleIncomeMax}&nbsp;</span>} src="/images/ARM_Morale.webp"/> +{sergeant.moraleIncomeMax}&nbsp;</span>}
{sergeant.moraleRate && <span>+ {sergeant.moraleRate}/s</span> } {sergeant.moraleRate && <span>+ {sergeant.moraleRate}/s</span> }
@ -125,21 +148,19 @@ const Sergeant = (props: SergeantProps) => {
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Mass</TableCell> <TableCell component="th" scope="row" >Mass</TableCell>
<TableCell component="th" scope="row"> <TableCell>{sergeant.mass}</TableCell>
{sergeant.mass}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Vision</TableCell> <TableCell component="th" scope="row" >Vision</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<Vision sight={sergeant.sightRadius} detect={sergeant.detectRadius}/> <Vision sight={sergeant.sightRadius} detect={sergeant.detectRadius}/>
</TableCell> </TableCell>
</TableRow> </TableRow>
{sergeant.mobValue != null && {sergeant.mobValue != null &&
<TableRow> <TableRow>
<TableCell>Mob value</TableCell> <TableCell component="th" scope="row" >Mob value</TableCell>
<TableCell> <TableCell>
<span> <img style={{height: 20, verticalAlign: "top"}} src="/images/Mob_bonus.gif"/> {sergeant.mobValue} </span> <span> <img style={{height: 20, verticalAlign: "top"}} src="/images/Mob_bonus.gif"/> {sergeant.mobValue} </span>
</TableCell> </TableCell>
@ -150,20 +171,20 @@ const Sergeant = (props: SergeantProps) => {
</TableContainer> </TableContainer>
</Grid2> </Grid2>
<Grid2 size={8}> <Grid2 size={8}>
<div style={{whiteSpace: "pre-wrap"}}> <DescriptionBox>
{sergeant.description} {sergeant.description}
</div> </DescriptionBox>
</Grid2> </Grid2>
{sergeant.requirements !== null && {sergeant.requirements !== null &&
<Grid2 size={{xs: 12, md: 12}}> <Grid2 size={{xs: 12, md: 12}}>
<Required requirement={sergeant.requirements} modId={props.mod.id} raceId={props.race.id}/> <Required requirement={sergeant.requirements} modId={props.mod.id} raceId={props.race.id}/>
</Grid2>} </Grid2>}
{sergeant.modifiers.length > 0 && <Grid2 size={12}> {sergeant.modifiers.length > 0 && <Grid2 size={12}>
<h3>Affected on</h3> <SectionTitle>Affected on</SectionTitle>
<ModifiersProvidesTable modifiers={sergeant.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/> <ModifiersProvidesTable modifiers={sergeant.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/>
</Grid2>} </Grid2>}
{sergeant.abilities.length > 0 && <Grid2 size={12}> {sergeant.abilities.length > 0 && <Grid2 size={12}>
<h3>Abilities</h3> <SectionTitle>Abilities</SectionTitle>
{sergeant.abilities.map(a => {sergeant.abilities.map(a =>
<Ability mod={props.mod} ability={a} race={props.race}/> <Ability mod={props.mod} ability={a} race={props.race}/>
)} )}

View File

@ -1,10 +1,48 @@
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {AvailableBuildings, AvailableUnits} from "../core/api"; import {AvailableBuildings, AvailableUnits} from "../core/api";
import {Grid2, Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material"; import {Grid2, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper} from "@mui/material";
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";
import {StyledLink} from "../commons/StyledLink";
// Styled components for dark theme
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
}));
const StyledTable = styled(Table)(({ theme }) => ({
color: '#ffffff',
'& .MuiTableCell-head': {
backgroundColor: 'rgba(233, 69, 96, 0.15)',
color: '#ffffff',
fontWeight: 700,
fontSize: '1rem',
borderBottom: '2px solid rgba(233, 69, 96, 0.3)',
},
'& .MuiTableCell-body': {
color: 'rgba(255, 255, 255, 0.85)',
borderBottom: '1px solid rgba(255, 255, 255, 0.05)',
fontSize: '0.9rem',
},
'& .MuiTableRow-hover:hover': {
backgroundColor: 'rgba(233, 69, 96, 0.08)',
},
}));
const StyledHeaderLink = styled(StyledLink)(({ theme }) => ({
color: '#ffffff',
fontWeight: 700,
fontSize: '1.1rem',
'&:hover': {
color: '#e94560',
},
}));
interface IUnitsTable { interface IUnitsTable {
racesUnits: IRaceUnits[] racesUnits: IRaceUnits[]
racesBuildings: IRaceBuildings[] racesBuildings: IRaceBuildings[]
@ -12,6 +50,7 @@ interface IUnitsTable {
export default function UnitsTable(prop: {modId: number}) { export default function UnitsTable(prop: {modId: number}) {
const [unitsTable, setUnitsTable] = useState<IUnitsTable>({ const [unitsTable, setUnitsTable] = useState<IUnitsTable>({
racesUnits: [], racesUnits: [],
racesBuildings: [] racesBuildings: []
@ -19,15 +58,19 @@ export default function UnitsTable(prop: {modId: number}) {
function getUnitRef(modId: number, raceId: string, unit: IUnitShort) { function getUnitRef(modId: number, raceId: string, unit: IUnitShort) {
return <span style={{fontSize: 11}}><Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}> return <span style={{fontSize: 11}}>
<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
{unit.name} {unit.name}
</Link><br/></span> </StyledLink><br/>
</span>
} }
function getBuildingRef(modId: number, raceId: string, building: IBuildingShort) { function getBuildingRef(modId: number, raceId: string, building: IBuildingShort) {
return <span style={{fontSize: 11}}><Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}> return <span style={{fontSize: 11}}>
<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}>
{building.name} {building.name}
<br/></Link></span> </StyledLink><br/>
</span>
} }
useEffect(() => { useEffect(() => {
@ -99,31 +142,31 @@ export default function UnitsTable(prop: {modId: number}) {
} }
function generateRaceUnitTable(racesUnitsPart: IRaceUnits[], racesBuildings: IRaceBuildings[]){ function generateRaceUnitTable(racesUnitsPart: IRaceUnits[], racesBuildings: IRaceBuildings[]){
return (<TableContainer> return (<StyledPaper elevation={0}>
{<Table sx={{minWidth: 650}} size="small" aria-label="a dense table"> {<StyledTable sx={{minWidth: 650}} size="small" aria-label="a dense table">
<TableHead> <TableHead>
<TableRow> <TableRow>
{racesUnitsPart.map(raceUnits => <TableCell><h2><Link color="inherit" {racesUnitsPart.map(raceUnits => <TableCell><h2><StyledHeaderLink
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</Link> href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</StyledHeaderLink>
</h2></TableCell>)} </h2></TableCell>)}
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow style={{verticalAlign: 'top'}}> <TableRow>
{racesUnitsPart.map(raceUnits => {racesUnitsPart.map(raceUnits =>
<TableCell> <TableCell>
{getUnitsRef(raceUnits.infantry.concat(raceUnits.support), raceUnits.race.id)} {getUnitsRef(raceUnits.infantry.concat(raceUnits.support), raceUnits.race.id)}
</TableCell>) </TableCell>)
} }
</TableRow> </TableRow>
<TableRow style={{verticalAlign: 'top'}}> <TableRow>
{racesUnitsPart.map(raceUnits => {racesUnitsPart.map(raceUnits =>
<TableCell> <TableCell>
{getUnitsRef(raceUnits.tech, raceUnits.race.id)} {getUnitsRef(raceUnits.tech, raceUnits.race.id)}
</TableCell>) </TableCell>)
} }
</TableRow> </TableRow>
<TableRow style={{verticalAlign: 'top'}}> <TableRow>
{racesBuildings.map(raceBuilding => {racesBuildings.map(raceBuilding =>
<TableCell> <TableCell>
{raceBuilding.buildings.map(building => getBuildingRef(prop.modId, raceBuilding.race.id, building))} {raceBuilding.buildings.map(building => getBuildingRef(prop.modId, raceBuilding.race.id, building))}
@ -131,8 +174,8 @@ export default function UnitsTable(prop: {modId: number}) {
} }
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table>} </StyledTable>}
</TableContainer>) </StyledPaper>)
} }
function generateRaceUnitTables(racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[], raceCount: number){ function generateRaceUnitTables(racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[], raceCount: number){
@ -156,13 +199,13 @@ export default function UnitsTable(prop: {modId: number}) {
function generateRaceList(unitsTable: IUnitsTable){ function generateRaceList(unitsTable: IUnitsTable){
return unitsTable.racesUnits.sort(function (a, b) { return unitsTable.racesUnits.sort(function (a, b) {
return a.race.name.localeCompare(b.race.name); return a.race.name.localeCompare(b.race.name);
}).map(raceUnits => <h5 key={raceUnits.race.id}><Link color="inherit" }).map(raceUnits => <h5 key={raceUnits.race.id}><StyledLink
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</Link><br/> href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</StyledLink><br/>
</h5>) </h5>)
} }
if(unitsTable.racesUnits.length > 10){ if(unitsTable.racesUnits.length > 10){
return <Grid2> return <Grid2 sx={{ color: '#ffffff' }}>
{generateRaceList(unitsTable)} {generateRaceList(unitsTable)}
</Grid2> </Grid2>
} else { } else {

View File

@ -6,6 +6,15 @@ import {ExpandMore} from "@mui/icons-material";
import {IMod} from "../types/Imod"; import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace"; import {Irace} from "../types/Irace";
import WeaponFull from "./WeaponFull"; import WeaponFull from "./WeaponFull";
import {styled} from '@mui/material/styles';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
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' },
}));
interface IWeaponProps { interface IWeaponProps {
weapon: IShortWeapon, weapon: IShortWeapon,
@ -34,23 +43,26 @@ class Weapon extends React.Component<IWeaponProps, any> {
const weapon = this.props.weapon const weapon = this.props.weapon
return ( return (
<div style={{marginBottom: 10}}><Accordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}> <div style={{marginBottom: 10}}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore/>} expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
id="panel1-header" id="panel1-header"
sx={{color: '#ffffff'}}
> >
<span style={{fontSize: 18}}> {!this.props.isDefault && weapon.icon && !weapon.icon.endsWith("upgrade.png") ? <span style={{fontSize: 18, color: '#ffffff'}}> {!this.props.isDefault && weapon.icon && !weapon.icon.endsWith("upgrade.png") ?
<img className="weaponIcon" src={IconUrl + weapon.icon.replaceAll('\\', '/')}/> : ( <img className="weaponIcon" src={IconUrl + weapon.icon.replaceAll('\\', '/')}/> : (
weapon.isMeleeWeapon ? weapon.isMeleeWeapon ?
<img className="weaponIcon" src="/images/MeleeStance_icon_bw.jpg"/> : <img className="weaponIcon" src="/images/MeleeStance_icon_bw.jpg"/> :
<img className="weaponIcon" src="/images/RangedStance_icon_bw.jpg"/> <img className="weaponIcon" src="/images/RangedStance_icon_bw.jpg"/>
)} {weapon.name ? weapon.name : this.humanReadableName(weapon.filename)} </span> )} {weapon.name ? weapon.name : this.humanReadableName(weapon.filename)}
{this.props.isDefault && <i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)'}}> (default)</i>}
</span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<WeaponFull weaponId={weapon.id} isDefault={this.props.isDefault} mod={this.props.mod} race={this.props.race} haveReinforceMenu={this.props.haveReinforceMenu}/> <WeaponFull weaponId={weapon.id} isDefault={this.props.isDefault} mod={this.props.mod} race={this.props.race} haveReinforceMenu={this.props.haveReinforceMenu}/>
</AccordionDetails> </AccordionDetails>
</Accordion></div> </StyledAccordion></div>
); );
} }
} }

View File

@ -22,6 +22,8 @@ import {Irace} from "../types/Irace";
import {IRaceUnits} from "../types/IUnitShort"; import {IRaceUnits} from "../types/IUnitShort";
import {IRaceBuildings} from "../types/IBuildingShort"; import {IRaceBuildings} from "../types/IBuildingShort";
import {ModifiersProvidesTable} from "./ModifiersProvideTable"; import {ModifiersProvidesTable} from "./ModifiersProvideTable";
import {DescriptionBox} from "../commons/DescriptionBox";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
interface IWeaponFull { interface IWeaponFull {
currentTable: string; currentTable: string;
@ -37,16 +39,33 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
const StyledTableCell = styled(TableCell)(({theme}) => ({ const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: "rgb(244,244,244)", backgroundColor: 'rgba(233, 69, 96, 0.15)',
color: '#ffffff',
marginRight: 'auto', marginRight: 'auto',
marginLeft: 'auto', marginLeft: 'auto',
paddingLeft: 10 paddingLeft: 10,
borderBottom: '2px solid rgba(233, 69, 96, 0.3)',
}, },
[`&.${tableCellClasses.body}`]: { [`&.${tableCellClasses.body}`]: {
fontSize: 12, fontSize: 12,
textAlign: 'center', textAlign: 'center',
color: 'rgba(255, 255, 255, 0.85)',
paddingRight: 18, paddingRight: 18,
paddingLeft: 10 paddingLeft: 10,
},
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
}, },
})); }));
@ -133,84 +152,68 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
return ( return (
<div> <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={12}> {!props.isDefault && <Grid2 size={12}>
<div style={{whiteSpace: "pre-wrap"}}> <DescriptionBox>
<span <span className="weaponDescription">{weapon.description}</span>
className="weaponDescription">{props.isDefault ? "Default weapon" : weapon.description}</span> </DescriptionBox>
</div> </Grid2>}
</Grid2>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
{/*<TableHead><TableRow><TableCell component="th" scope="row"><b>Damage</b></TableCell><TableCell/></TableRow></TableHead>*/}
<TableBody> <TableBody>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Base damage</TableCell> <TableCell component="th" scope="row" >Base damage</TableCell>
<TableCell component="th" <TableCell>{weapon.minDamage} {weapon.maxDamage !== weapon.minDamage && "- " + weapon.maxDamage}</TableCell>
scope="row">{weapon.minDamage} {weapon.maxDamage !== weapon.minDamage && "- " + weapon.maxDamage}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Accuracy</TableCell> <TableCell component="th" scope="row" >Accuracy</TableCell>
<TableCell component="th" <TableCell>{weapon.accuracy.toFixed(3).replace(/[,.]?0+$/, '')}</TableCell>
scope="row">{weapon.accuracy.toFixed(3).replace(/[,.]?0+$/, '')}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Accuracy moving</TableCell> <TableCell component="th" scope="row" >Accuracy moving</TableCell>
<TableCell component="th" <TableCell>{weapon.setupTime === 0 && weapon.accuracy - weapon.accuracyReductionMoving > 0 ? (weapon.accuracy - weapon.accuracyReductionMoving).toFixed(3).replace(/[,.]?0+$/, '') : "-"}</TableCell>
scope="row">{weapon.setupTime === 0 && weapon.accuracy - weapon.accuracyReductionMoving > 0 ? (weapon.accuracy - weapon.accuracyReductionMoving).toFixed(3).replace(/[,.]?0+$/, '') : "-"}
</TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody> <TableBody>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Reload time</TableCell> <TableCell component="th" scope="row" >Reload time</TableCell>
<TableCell component="th" <TableCell>{weapon.reloadTime}</TableCell>
scope="row">{weapon.reloadTime}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Range</TableCell> <TableCell component="th" scope="row" >Range</TableCell>
<TableCell component="th" <TableCell>{weapon.maxRange ? (weapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}</TableCell>
scope="row">{weapon.maxRange ? (weapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Damage radius</TableCell> <TableCell component="th" scope="row" >Damage radius</TableCell>
<TableCell component="th" <TableCell>{weapon.damageRadius ? (weapon.damageRadius) : "-"}</TableCell>
scope="row">{weapon.damageRadius ? (weapon.damageRadius) : "-"}
</TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody> <TableBody>
<TableRow <TableRow
@ -218,8 +221,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
> >
<TableCell component="th" scope="row" >Cost</TableCell> <TableCell component="th" scope="row" >Cost</TableCell>
{(weapon.costRequisition > 0 || weapon.costPower > 0) ? {(weapon.costRequisition > 0 || weapon.costPower > 0) ?
<TableCell component="th" <TableCell>
scope="row">
{weapon.costRequisition > 0 && {weapon.costRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp; src="/images/Resource_requisition.gif"/>&nbsp;
@ -228,24 +230,24 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp; src="/images/Resource_power.gif"/>&nbsp;
{weapon.costPower.toFixed(0)}</span>} {weapon.costPower.toFixed(0)}</span>}
</TableCell> : <TableCell component="th" scope="row"> - </TableCell> {(weapon.costTimeSeconds !== undefined && weapon.costTimeSeconds > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{verticalAlign: "top", fontSize: "18px"}}/>&nbsp;
{weapon.costTimeSeconds}s</span>}
</TableCell> : <TableCell> - </TableCell>
} }
</TableRow> </TableRow>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Setup time</TableCell> <TableCell component="th" scope="row" >Setup time</TableCell>
<TableCell component="th" <TableCell>{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(3).replace(/[,.]?0+$/, '') : "-"}</TableCell>
scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(3).replace(/[,.]?0+$/, '') : "-"}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Throw force</TableCell> <TableCell component="th" scope="row" >Throw force</TableCell>
<TableCell component="th" <TableCell>{weapon.throwForceMin != 0 ? weapon.throwForceMin + " - " + weapon.throwForceMax : "-"}</TableCell>
scope="row">{weapon.throwForceMin != 0 ? weapon.throwForceMin + " - " + weapon.throwForceMax : "-"}
</TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
@ -259,9 +261,10 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
aria-label="Platform" aria-label="Platform"
value={weaoponFull.currentTable} value={weaoponFull.currentTable}
onChange={handleChange} onChange={handleChange}
sx={{ mb: 2 }}
> >
<ToggleButton size="small" value="dps">Dps</ToggleButton> <ToggleButton size="small" value="dps" sx={{ color: '#ffffff', '&.Mui-selected': { borderColor: '#e94560', color: '#e94560', backgroundColor: 'rgba(233, 69, 96, 0.15)' } }}>Dps</ToggleButton>
<ToggleButton size="small" value="one hit">One hit average damage</ToggleButton> <ToggleButton size="small" value="one hit" sx={{ color: '#ffffff', '&.Mui-selected': {borderColor: '#e94560', color: '#e94560', backgroundColor: 'rgba(233, 69, 96, 0.15)' } }}>One hit average damage</ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
<TableContainer> <TableContainer>
{ props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ? { props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ?
@ -290,7 +293,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
@ -325,13 +328,13 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
name={ArmorTypeNames.BuildingSuper}/></StyledTableCell> name={ArmorTypeNames.BuildingSuper}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}} <StyledTableCell><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/> src="/images/ARM_Morale.webp"/>
<div style={{width: 20, fontSize: 12, height: 50}}> <div style={{width: 20, fontSize: 12, height: 50, color: 'rgba(255,255,255,0.7)'}}>
<i>Morale</i></div> <i>Morale</i></div>
</StyledTableCell> </StyledTableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
@ -379,13 +382,13 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell> name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}} <StyledTableCell><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/> src="/images/ARM_Morale.webp"/>
<div style={{width: 20, fontSize: 12, height: 50}}> <div style={{width: 20, fontSize: 12, height: 50, color: 'rgba(255,255,255,0.7)'}}>
<i>Morale</i></div> <i>Morale</i></div>
</StyledTableCell> </StyledTableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
@ -411,7 +414,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
</Grid2> </Grid2>
{weapon.modifiers.length > 0 && <Grid2 size={12}> {weapon.modifiers.length > 0 && <Grid2 size={12}>
<h3>Modifiers</h3> <h3 style={{color: '#ffffff', fontWeight: 700}}>Modifiers</h3>
<ModifiersProvidesTable modifiers={weapon.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/> <ModifiersProvidesTable modifiers={weapon.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/>
</Grid2>} </Grid2>}
{weapon.requirements !== null && !props.isDefault && props.haveReinforceMenu && {weapon.requirements !== null && !props.isDefault && props.haveReinforceMenu &&
@ -425,7 +428,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
{!props.isDefault && props.haveReinforceMenu && <b className="hotkey" >Hotkey: {weapon.hotkey}</b>} {!props.isDefault && props.haveReinforceMenu && <b className="hotkey" >Hotkey: {weapon.hotkey}</b>}
</div> </div>
) )
} else return <div>loading...</div>; } else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>;

View File

@ -4,6 +4,15 @@ import {IShortWeapon, IWeapon} from "../types/IUnit";
import {IMod} from "../types/Imod"; import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace"; import {Irace} from "../types/Irace";
import WeaponShort from "./WeaponFull"; import WeaponShort from "./WeaponFull";
import {Typography} from "@mui/material";
import {styled} from '@mui/material/styles';
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
color: '#ffffff',
fontSize: '1.25rem',
}));
export interface WeaponSlotProps { export interface WeaponSlotProps {
hardpoint: number, hardpoint: number,
@ -33,7 +42,7 @@ const WeaponSlot= (props: WeaponSlotProps) => {
return ( return (
<div> <div>
<h3>{header}</h3> <SectionTitle>{header}</SectionTitle>
{weaponsSorted.filter(wp => !wp[1].filename.includes("dummy")).map(wp => {weaponsSorted.filter(wp => !wp[1].filename.includes("dummy")).map(wp =>
<div style={{marginLeft: 20}}><Weapon race={props.race} mod={props.mod} isDefault={!haveDummy || wp[1].filename === firstWeapon.filename} weapon={wp[1]} haveReinforceMenu={haveReinforceMenu}/></div> <div style={{marginLeft: 20}}><Weapon race={props.race} mod={props.mod} isDefault={!haveDummy || wp[1].filename === firstWeapon.filename} weapon={wp[1]} haveReinforceMenu={haveReinforceMenu}/></div>

View File

@ -9,7 +9,8 @@ import {
TableBody, TableBody,
TableCell, TableCell,
TableContainer, TableContainer,
TableRow TableRow,
Typography
} from "@mui/material"; } from "@mui/material";
import {IBuilding, IBuildingAddon} from "../../types/IBuilding"; import {IBuilding, IBuildingAddon} from "../../types/IBuilding";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer"; import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
@ -17,6 +18,30 @@ import {ExpandMore} from "@mui/icons-material";
import {IMod} from "../../types/Imod"; import {IMod} from "../../types/Imod";
import {getIcon, ModifiersProvidesTable,} from "../ModifiersProvideTable"; import {getIcon, ModifiersProvidesTable,} from "../ModifiersProvideTable";
import Required from "../Required"; import Required from "../Required";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../../commons/DescriptionBox";
const StyledAccordion = styled(Accordion)(({ theme }) => ({
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' },
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.05)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.85)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface IBuildingAddonProps { interface IBuildingAddonProps {
addon: IBuildingAddon, addon: IBuildingAddon,
@ -31,27 +56,28 @@ function BuildingAddon(props: IBuildingAddonProps){
return <div className='addon-research-accordion' id={"addon-" + addon.id} ><Accordion> return <div className='addon-research-accordion' id={"addon-" + addon.id} ><StyledAccordion>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore/>} expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
sx={{color: '#ffffff'}}
> >
<span style={{fontSize: 20}}> <span style={{fontSize: 20, color: '#ffffff'}}>
<img className="sergeantIcon" src={getIcon(addon.icon)}/> <img className="sergeantIcon" src={getIcon(addon.icon)}/>
&nbsp; {addon.name} &nbsp; {addon.name}
</span> </span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table"> <TableBody id="unit-stats-table">
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Cost</TableCell> <TableCell component="th" scope="row" >Cost</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{addon.addonCostRequisition > 0 && {addon.addonCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp; src="/images/Resource_requisition.gif"/>&nbsp;
@ -83,9 +109,9 @@ function BuildingAddon(props: IBuildingAddonProps){
</TableContainer><br/> </TableContainer><br/>
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 8}}> <Grid2 size={{xs: 12, md: 8}}>
<div style={{whiteSpace: "pre-wrap"}}> <DescriptionBox>
{addon.description} {addon.description}
</div> </DescriptionBox>
</Grid2> </Grid2>
<ModifiersProvidesTable modifiers={addon.addonModifiers} affectedData={addon.affectedData} modId={props.mod.id} race={building.race}/> <ModifiersProvidesTable modifiers={addon.addonModifiers} affectedData={addon.affectedData} modId={props.mod.id} race={building.race}/>
{addon.addonRequirement !== null && {addon.addonRequirement !== null &&
@ -95,7 +121,7 @@ function BuildingAddon(props: IBuildingAddonProps){
</Grid2> </Grid2>
<b className="hotkey" >Hotkey: {addon.hotkey}</b> <b className="hotkey" >Hotkey: {addon.hotkey}</b>
</AccordionDetails> </AccordionDetails>
</Accordion></div> </StyledAccordion></div>
} }

View File

@ -20,6 +20,24 @@ import React from "react";
import {IBuilding} from "../../types/IBuilding"; import {IBuilding} from "../../types/IBuilding";
import {IResearchShort} from "../../types/IResearchShort"; import {IResearchShort} from "../../types/IResearchShort";
import ResearchFull from "./ResearchFull"; import ResearchFull from "./ResearchFull";
import {styled} from '@mui/material/styles';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
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' },
}));
const StyledLink = styled('a')(({ theme }) => ({
color: '#e94560',
textDecoration: 'none',
'&:hover': {
color: '#ff6b6b',
textDecoration: 'underline',
},
}));
interface IResearchProps { interface IResearchProps {
@ -33,20 +51,21 @@ function Research(props: IResearchProps){
const research = props.research const research = props.research
const building = props.building const building = props.building
return <div className='addon-research-accordion' id={"research-" + research.id}><Accordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}> return <div className='addon-research-accordion' id={"research-" + research.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore/>} expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
aria-controls="panel1-content" aria-controls="panel1-content"
sx={{color: '#ffffff'}}
> >
<span style={{fontSize: 20}}> <span style={{fontSize: 20, color: '#ffffff'}}>
<img className="sergeantIcon" src={getIcon(research.icon)}/> <img className="sergeantIcon" src={getIcon(research.icon)}/>
&nbsp; {research.name} &nbsp; {research.name}
</span> </span>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<ResearchFull id={research.id} building={building}/> <ResearchFull id={research.id} building={building}/>
</AccordionDetails> </AccordionDetails>
</Accordion></div> </StyledAccordion></div>
} }
@ -55,12 +74,12 @@ export function renderAffectedResearches(researches: IResearchShort[], modId: nu
function researchLink(rs: IResearchShort) { function researchLink(rs: IResearchShort) {
return <span><img style={{verticalAlign: "top", height: 25}} return <span><img style={{verticalAlign: "top", height: 25}}
src={getIcon(rs.icon)}/> src={getIcon(rs.icon)}/>
&nbsp;<a href={`/mod/${modId}/race/${raceId}/building/${rs.buildingId}#research-${rs.id}`}> &nbsp;<StyledLink href={`/mod/${modId}/race/${raceId}/building/${rs.buildingId}#research-${rs.id}`}>
{rs.name} {rs.name}
</a></span> </StyledLink></span>
} }
return <div> {researches.map(rs => return <div style={{color: 'rgba(255, 255, 255, 0.85)'}}> {researches.map(rs =>
rs.buildingId == null ? <span key={rs.id}></span> : rs.buildingId == null ? <span key={rs.id}></span> :
<span key={rs.id}>Research affect: {researchLink(rs)}<br/></span> <span key={rs.id}>Research affect: {researchLink(rs)}<br/></span>
)}</div> )}</div>

View File

@ -10,7 +10,8 @@ import {
TableBody, TableBody,
TableCell, TableCell,
TableContainer, TableContainer,
TableRow TableRow,
Typography
} from "@mui/material"; } from "@mui/material";
import {ExpandMore} from "@mui/icons-material"; import {ExpandMore} from "@mui/icons-material";
import {getIcon, ModifiersProvidesTable} from "../ModifiersProvideTable"; import {getIcon, ModifiersProvidesTable} from "../ModifiersProvideTable";
@ -21,6 +22,22 @@ import {IBuilding} from "../../types/IBuilding";
import {IResearchShort} from "../../types/IResearchShort"; import {IResearchShort} from "../../types/IResearchShort";
import {ResearchUrl, WeaponUrl} from "../../core/api"; import {ResearchUrl, WeaponUrl} from "../../core/api";
import {IWeapon} from "../../types/IUnit"; import {IWeapon} from "../../types/IUnit";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../../commons/DescriptionBox";
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface IResearchFull { interface IResearchFull {
@ -54,14 +71,14 @@ function ResearchFull(props: { id: number, building: IBuilding }){
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table"> <TableBody id="unit-stats-table">
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row" >Cost</TableCell> <TableCell component="th" scope="row" >Cost</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{research.costRequisition > 0 && {research.costRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp; src="/images/Resource_requisition.gif"/>&nbsp;
@ -93,9 +110,9 @@ function ResearchFull(props: { id: number, building: IBuilding }){
</TableContainer><br/> </TableContainer><br/>
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 8}}> <Grid2 size={{xs: 12, md: 8}}>
<div style={{whiteSpace: "pre-wrap"}}> <DescriptionBox>
{research.description} {research.description}
</div> </DescriptionBox>
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 12}}> <Grid2 size={{xs: 12, md: 12}}>
@ -109,7 +126,7 @@ function ResearchFull(props: { id: number, building: IBuilding }){
</Grid2> </Grid2>
<b className="hotkey" >Hotkey: {research.hotkey}</b> <b className="hotkey" >Hotkey: {research.hotkey}</b>
</div> </div>
} else return <div>loading...</div> } else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>
} }
export default ResearchFull; export default ResearchFull;

View File

@ -0,0 +1,12 @@
import {styled} from '@mui/material/styles';
export const DescriptionBox = styled('div')(({ theme }) => ({
color: 'rgba(255, 255, 255, 0.85)',
lineHeight: 1.7,
fontSize: '0.95rem',
padding: theme.spacing(3),
background: 'rgba(255, 255, 255, 0.03)',
borderRadius: '12px',
border: '1px solid rgba(255, 255, 255, 0.05)',
whiteSpace: 'pre-wrap',
}));

View File

@ -0,0 +1,12 @@
import {Link} from "@mui/material";
import {styled} from '@mui/material/styles';
export const StyledLink = styled(Link)(({ theme }) => ({
color: '#e94560',
textDecoration: 'none',
transition: 'all 0.2s ease',
'&:hover': {
color: '#ff6b6b',
textDecoration: 'underline',
},
}));

View File

@ -1,13 +1,43 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
background-color: #0a0a0a;
color: #ffffff;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
background: rgba(255, 255, 255, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
* {
scrollbar-width: thin;
scrollbar-color: #e94560 #1a1a2e;
}
*::-webkit-scrollbar {
width: 8px;
}
*::-webkit-scrollbar-track {
background: #1a1a2e;
}
*::-webkit-scrollbar-thumb {
background-color: #e94560;
border-radius: 4px;
}
a {
color: inherit;
text-decoration: none;
} }

View File

@ -5,6 +5,9 @@ import {IShortWeapon} from "../types/IUnit";
import '../css/Building.css' import '../css/Building.css'
import { import {
Button, Button,
Box,
Container,
Divider,
Grid2, Grid2,
Link, Link,
ListItem, ListItem,
@ -12,11 +15,10 @@ import {
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableContainer,
TableRow, TableRow,
Tooltip Tooltip,
Typography, LinearProgress
} from "@mui/material"; } from "@mui/material";
import {ArrowBack} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType"; import ArmorType from "../classes/ArmorType";
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer'; import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
import WeaponSlot from "../classes/WeaponSlot"; import WeaponSlot from "../classes/WeaponSlot";
@ -31,6 +33,43 @@ import Required from "../classes/Required";
import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable"; import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
import Ability from "../classes/Ability"; import Ability from "../classes/Ability";
import DeathExplosion from "../classes/DeathExplosion"; import DeathExplosion from "../classes/DeathExplosion";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox";
import {ArrowBack} from "@mui/icons-material";
// Styled components
const BackButton = styled(Button)(({ theme }) => ({
marginBottom: theme.spacing(3),
color: '#e94560',
borderColor: 'rgba(233, 69, 96, 0.5)',
textTransform: 'none',
fontWeight: 600,
'&:hover': {
borderColor: '#e94560',
backgroundColor: 'rgba(233, 69, 96, 0.1)',
},
}));
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
color: '#ffffff',
fontSize: '1.25rem',
}));
const StatsPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface UintPageState { interface UintPageState {
building: IBuilding, building: IBuilding,
@ -39,18 +78,30 @@ interface UintPageState {
function Unit (unit: IUnitShort, modId: number, raceId: String) { function Unit (unit: IUnitShort, modId: number, raceId: String) {
return (<Grid2 key={unit.id} size={{xs: 12, md: 3}}><Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}><ListItem> return (<Grid2 key={unit.id} size={{xs: 12, md: 3}}>
<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} sx={{color: 'rgba(255,255,255,0.85)', textDecoration: 'none', '&:hover': {color: '#e94560'}}}>
<ListItem sx={{
color: 'rgba(255,255,255,0.85)',
padding: '8px 12px',
background: 'rgba(255,255,255,0.03)',
borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.05)',
transition: 'all 0.2s ease',
'&:hover': { background: 'rgba(233, 69, 96, 0.1)', borderColor: 'rgba(233, 69, 96, 0.3)' }
}}>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> } {unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> }
{unit.name} {unit.name}
{unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}} {unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>} src="/images/DETECT_YES.webp"/></span>}
</ListItem></Link></Grid2>) </ListItem>
</Link>
</Grid2>)
} }
function Building(building: IBuilding, mod: IMod) { function Building(building: IBuilding, mod: IMod) {
document.title = building.name document.title = building.name + " — " + mod.name
let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = new Map(); let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
@ -72,21 +123,36 @@ function Building(building: IBuilding, mod: IMod) {
} }
return ( return (
<div> <Box>
<h1>{mod.name} ({mod.version})</h1> <Box sx={{ mb: 3 }}>
<h2>{building.icon && <Typography variant="h4" component="h2" sx={{
<img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>} {buildingName} </h2> fontWeight: 800,
mb: 1,
color: '#ffffff',
display: 'flex',
alignItems: 'center',
gap: 2,
}}>
{building.icon &&
<img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
{buildingName}
</Typography>
<Typography variant="subtitle1" sx={{
color: 'rgba(255, 255, 255, 0.6)',
fontWeight: 500,
}}>
{mod.name} ({mod.version})
</Typography>
</Box>
<Grid2 container spacing={2}> <Grid2 container spacing={3}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <StatsPaper elevation={0}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table"> <TableBody id="unit-stats-table">
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Cost</TableCell> <TableCell component="th" scope="row" >Cost</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{building.buildCostRequisition > 0 && {building.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp; src="/images/Resource_requisition.gif"/>&nbsp;
@ -131,50 +197,40 @@ function Building(building: IBuilding, mod: IMod) {
</TableCell> </TableCell>
</TableRow> </TableRow>
} }
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Armor type</TableCell> <TableCell component="th" scope="row" >Armor type</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{building.armorType2 == null ? <ArmorType name={building.armorType.name} compact={false}/> {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> } : <Tooltip title={"upgrade/debuff can turns to " + building.armorType2.name}><span><ArmorType name={building.armorType.name} compact={false}/></span></Tooltip> }
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Health</TableCell> <TableCell component="th" scope="row" >Health</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<span><img style={{verticalAlign: "top"}} <span><img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp; src="/images/Health_icon.webp"/>&nbsp;
{building.health} {building.healthRegeneration > 0 && {building.health} {building.healthRegeneration > 0 &&
<span>+{building.healthRegeneration}/s</span>} </span> <span>+{building.healthRegeneration}/s</span>} </span>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Sight</TableCell> <TableCell component="th" scope="row" >Sight</TableCell>
<TableCell component="th" scope="row"> <TableCell>
<Vision sight={building.sightRadius} detect={building.detectRadius}/> <Vision sight={building.sightRadius} detect={building.detectRadius}/>
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Repair max</TableCell> <TableCell component="th" scope="row" >Repair max</TableCell>
<TableCell component="th" scope="row"> <TableCell>{building.repairMax}</TableCell>
{building.repairMax}
</TableCell>
</TableRow> </TableRow>
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </StatsPaper>
</Grid2> </Grid2>
<Grid2 size={8}> <Grid2 size={{xs: 12, md: 8}}>
<div style={{whiteSpace: "pre-wrap"}}> <DescriptionBox>
{building.description} {building.description}
</div> </DescriptionBox>
</Grid2> </Grid2>
{building.requirements !== null && {building.requirements !== null &&
<Grid2 size={{xs: 12, md: 12}}> <Grid2 size={{xs: 12, md: 12}}>
@ -182,11 +238,11 @@ function Building(building: IBuilding, mod: IMod) {
</Grid2>} </Grid2>}
{building.modifiers.length > 0 && {building.modifiers.length > 0 &&
<Grid2 size={{xs: 12, md: 12}}> <Grid2 size={{xs: 12, md: 12}}>
<h3>Affected on</h3> <SectionTitle>Affected on</SectionTitle>
<ModifiersProvidesTable modifiers={building.modifiers} race={building.race} modId={building.modId} affectedData={building.affectedData} /> <ModifiersProvidesTable modifiers={building.modifiers} race={building.race} modId={building.modId} affectedData={building.affectedData} />
</Grid2>} </Grid2>}
{building.units.length > 0 && <Grid2 size={12}> {building.units.length > 0 && <Grid2 size={12}>
<h3>Unit production</h3> <SectionTitle>Unit production</SectionTitle>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
{building.units.map(unit => {building.units.map(unit =>
Unit(unit, mod.id, building.race.id) Unit(unit, mod.id, building.race.id)
@ -194,7 +250,7 @@ function Building(building: IBuilding, mod: IMod) {
</Grid2> </Grid2>
</Grid2>} </Grid2>}
{building.abilities.length > 0 && <Grid2 size={12}> {building.abilities.length > 0 && <Grid2 size={12}>
<h3>Abilities</h3> <SectionTitle>Abilities</SectionTitle>
{building.abilities.map(a => {building.abilities.map(a =>
<Ability mod={mod} ability={a} race={building.race}/> <Ability mod={mod} ability={a} race={building.race}/>
)} )}
@ -207,13 +263,13 @@ function Building(building: IBuilding, mod: IMod) {
</Grid2> </Grid2>
} }
{building.addons.length > 0 && <Grid2 size={12}> {building.addons.length > 0 && <Grid2 size={12}>
<h3>Addons</h3> <SectionTitle>Addons</SectionTitle>
{building.addons.map(b => {building.addons.map(b =>
<BuildingAddon mod={mod} addon={b} building={building}/> <BuildingAddon mod={mod} addon={b} building={building}/>
)} )}
</Grid2> } </Grid2> }
{building.researches.length > 0 && <Grid2 size={12}> {building.researches.length > 0 && <Grid2 size={12}>
<h3>Researches</h3> <SectionTitle>Researches</SectionTitle>
{building.researches.map(r => {building.researches.map(r =>
<Research key={r.id} research={r} building={building} mod={mod}/> <Research key={r.id} research={r} building={building} mod={mod}/>
)} )}
@ -227,10 +283,12 @@ function Building(building: IBuilding, mod: IMod) {
{renderAffectedResearches(building.affectedResearches, mod.id, building.race.id)} {renderAffectedResearches(building.affectedResearches, mod.id, building.race.id)}
</Grid2> </Grid2>
</Grid2> </Grid2>
<b className="hotkey">Hotkey: {building.hotkey}</b> <Box sx={{ mt: 3, mb: 2, color: '#e94560', fontWeight: 600, fontSize: '0.85rem' }}>
<hr/> Hotkey: {building.hotkey}
</Box>
<Divider sx={{ my: 3, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<UnitsTable modId={mod.id}/> <UnitsTable modId={mod.id}/>
</div>) </Box>)
} }
class BuildingPage extends React.Component<any, UintPageState> { class BuildingPage extends React.Component<any, UintPageState> {
@ -256,12 +314,24 @@ class BuildingPage extends React.Component<any, UintPageState> {
if (this.state != null && this.state.building != null && this.state.mod != null) { 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 backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId
return <div><a href={backRef}><Button id="back-button" variant="contained" return (
startIcon={<ArrowBack/>}> Back</Button></a> <Container maxWidth="lg" sx={{ py: 4 }}>
<BackButton
variant="outlined"
startIcon={<ArrowBack/>}
href={backRef}
>
Back to race
</BackButton>
{Building(this.state.building, this.state.mod)} {Building(this.state.building, this.state.mod)}
</div>; </Container>
);
} else { } else {
return "loading..."; return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
);
} }
} }
} }

View File

@ -1,17 +1,58 @@
import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api"; import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import React, {useEffect, useRef, useState} from "react"; import React, { useState } from "react";
import {NavLink, useLocation, useParams} from "react-router-dom";
import {withRouter} from "../core/withrouter"; import {withRouter} from "../core/withrouter";
import {IMod} from "../types/Imod"; import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace"; import {
import {Button, ListItem, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material"; ArrowBack,
import {ArrowBack} from "@mui/icons-material"; } from "@mui/icons-material";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort"; import {
Box,
Button,
Container,
Paper,
Typography,
Divider,
LinearProgress
} from "@mui/material";
import {styled} from '@mui/material/styles';
import UnitsTable from "../classes/UnitsTable"; import UnitsTable from "../classes/UnitsTable";
// Styled components
const PageHeader = styled(Paper)(({ theme }) => ({
padding: theme.spacing(4, 4),
marginBottom: theme.spacing(4),
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
borderRadius: '16px',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 30% 50%, rgba(233, 69, 96, 0.1) 0%, transparent 50%)',
pointerEvents: 'none',
},
}));
const BackButton = styled(Button)(({ theme }) => ({
marginBottom: theme.spacing(3),
color: '#e94560',
borderColor: 'rgba(233, 69, 96, 0.5)',
textTransform: 'none',
fontWeight: 600,
'&:hover': {
borderColor: '#e94560',
backgroundColor: 'rgba(233, 69, 96, 0.1)',
},
}));
interface ModPageState { interface ModPageState {
mod: IMod, mod: IMod | null,
loading: boolean,
} }
@ -20,31 +61,80 @@ class ModPage extends React.Component<any, ModPageState> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.state = {
mod: null,
loading: true
};
fetch(AvailableMods + "/" + this.props.match.params.modId) fetch(AvailableMods + "/" + this.props.match.params.modId)
.then(res => res.json()) .then(res => res.json())
.then((res: IMod) => { .then((res: IMod) => {
this.setState({ this.setState({
mod : res mod: res,
loading: false
}); });
})
.catch(() => {
this.setState({ loading: false });
}); });
} }
render() { render() {
if (this.state.loading) {
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<LinearProgress sx={{ width: '200px', mx: 'auto', display: 'block' }} />
</Container>
);
}
if (this.state != null && this.state.mod != null) { if (this.state != null && this.state.mod != null) {
document.title = this.state.mod.name + " — ModWiki";
document.title = this.state.mod.name return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<BackButton
variant="outlined"
startIcon={<ArrowBack/>}
href="/"
>
Back to mods list
</BackButton>
return <div> <PageHeader elevation={0}>
<a href="/"><Button id="back-button" variant="contained" <Typography variant="h3" component="h1" sx={{
startIcon={<ArrowBack/>}> Back</Button></a> fontWeight: 800,
<h1>{this.state.mod.name} ({this.state.mod.version})</h1> mb: 1,
background: 'linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
{this.state.mod.name}
</Typography>
<Typography variant="h6" sx={{
color: 'rgba(255, 255, 255, 0.7)',
fontWeight: 500,
}}>
Version {this.state.mod.version}
</Typography>
</PageHeader>
<Divider sx={{ my: 4, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<Box>
<UnitsTable modId={this.state.mod.id}/> <UnitsTable modId={this.state.mod.id}/>
</div>; </Box>
</Container>
);
} else { } else {
return ""; return (
<Container maxWidth="lg" sx={{ py: 8, textAlign: 'center' }}>
<Typography variant="h5" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
Mod not found
</Typography>
</Container>
);
} }
} }
} }

View File

@ -1,49 +1,272 @@
import {AvailableMods} from "../core/api"; import {AvailableMods} from "../core/api";
import React, {useEffect, useState} from "react"; import React from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { IMod } from "../types/Imod"; import { IMod } from "../types/Imod";
import {
Box,
Container,
Typography,
Grid,
Card,
CardContent,
CardActions,
Chip,
Button,
Divider,
useTheme,
Paper,
LinearProgress
} from "@mui/material";
import { styled } from '@mui/material/styles';
function Mods (mods: IMod[]) { // Styled components
const PageHeader = styled(Paper)(({ theme }) => ({
padding: theme.spacing(6, 4),
marginBottom: theme.spacing(6),
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
borderRadius: '16px',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 30% 50%, rgba(233, 69, 96, 0.1) 0%, transparent 50%)',
pointerEvents: 'none',
},
}));
let mapWithModVersions: Map<String, IMod[]> = new Map(); const ModCard = styled(Card)(({ theme }) => ({
height: '100%',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 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: 'linear-gradient(90deg, #e94560 0%, #ff6b6b 100%)',
transform: 'scaleX(0)',
transition: 'transform 0.3s ease',
},
'&:hover': {
transform: 'translateY(-8px)',
boxShadow: '0 20px 40px rgba(233, 69, 96, 0.2)',
borderColor: 'rgba(233, 69, 96, 0.3)',
'&::before': {
transform: 'scaleX(1)',
},
},
}));
mods.forEach( mod => { const VersionLink = styled(NavLink)(({ theme }) => ({
const versionList = mapWithModVersions.get(mod.name) display: 'flex',
alignItems: 'center',
padding: theme.spacing(1.5, 2),
marginBottom: theme.spacing(1),
background: 'rgba(255, 255, 255, 0.05)',
borderRadius: '10px',
textDecoration: 'none',
color: 'inherit',
transition: 'all 0.2s ease',
border: '1px solid rgba(255, 255, 255, 0.1)',
'&:hover': {
background: 'rgba(233, 69, 96, 0.15)',
borderColor: 'rgba(233, 69, 96, 0.3)',
transform: 'translateX(4px)',
},
}));
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) { if (versionList == null) {
mapWithModVersions.set(mod.name, new Array(mod)) mapWithModVersions.set(mod.name, [mod]);
} else { } else {
versionList.push(mod) 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 nonBetaMods = sameMods.filter(m => !m.isBeta);
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) {
function Mod (modName: String) { const sameMods = mapWithModVersions.get(modName) ?? [];
let sameMods = mapWithModVersions.get(modName) ?? [] const { latest, hasBeta, betaVersion } = getLatestVersion(modName);
let lastBeta = sameMods.filter((m) => m.isBeta).reverse()[0]
return ( return (
<div> <ModCard sx={{ animationDelay: `${index * 100}ms` }}>
<h1>{modName}</h1> <CardContent sx={{ flexGrow: 1, p: 3 }}>
{modName == "Unification new races" && <ul><a href="https://dawn-of-war-unification-mod.fandom.com/">Official unification wiki</a></ul>} <Box sx={{ mb: 2 }}>
{sameMods.filter((m) => !m.isBeta).map(mod => <Typography variant="h5" component="h2" sx={{
<ul><NavLink state={mod.id} to= {"/mod/" + mod.id} >{mod.version}</NavLink></ul>)} fontWeight: 700,
{sameMods.find((m) => m.isBeta) != null && <div><i>Beta versions:</i></div>} mb: 1,
{lastBeta != null && background: 'linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)',
<ul><NavLink state={lastBeta.id} to={"/mod/" + lastBeta.id}>{lastBeta.version}</NavLink></ul> WebkitBackgroundClip: 'text',
} WebkitTextFillColor: 'transparent',
</div> }}>
) {modName}
</Typography>
<Typography variant="body2" sx={{
color: 'rgba(255, 255, 255, 0.7)',
lineHeight: 1.6,
}}>
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<Chip
label={`${sameMods.length} versions`}
size="small"
sx={{
backgroundColor: 'rgba(233, 69, 96, 0.2)',
color: '#ff6b6b',
fontWeight: 600,
}}
/>
{hasBeta && (
<Chip
label="Beta"
size="small"
color="warning"
sx={{ fontWeight: 600 }}
/>
)}
</Box>
<Divider sx={{ my: 2, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<Typography variant="subtitle2" sx={{
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 600,
mb: 1.5,
}}>
Main versions:
</Typography>
{sameMods.filter(m => !m.isBeta).map(mod => (
<VersionLink key={mod.id} to={"/mod/" + mod.id} state={mod.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Version {mod.version}
</Typography>
<Typography variant="caption" sx={{
color: 'rgba(255, 255, 255, 0.5)',
}}>
</Typography>
</Box>
</VersionLink>
))}
{hasBeta && (
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" sx={{
color: 'rgba(255, 193, 7, 0.9)',
fontWeight: 600,
mb: 1,
}}>
Beta versions:
</Typography>
{betaVersion && (
<VersionLink to={"/mod/" + betaVersion.id} state={betaVersion.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Version {betaVersion.version} (Beta)
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
</Typography>
</Box>
</VersionLink>
)}
</Box>
)}
</CardContent>
<CardActions sx={{ px: 3, pb: 3 }}>
<Button
size="medium"
variant="outlined"
component={NavLink}
to={"/mod/" + latest?.id}
state={latest?.id}
sx={{
width: '100%',
color: '#e94560',
borderColor: 'rgba(233, 69, 96, 0.5)',
fontWeight: 600,
borderRadius: '10px',
textTransform: 'none',
fontSize: '1rem',
py: 1.5,
'&:hover': {
borderColor: '#e94560',
backgroundColor: 'rgba(233, 69, 96, 0.1)',
transform: 'translateY(-2px)',
boxShadow: '0 8px 20px rgba(233, 69, 96, 0.2)',
},
}}
>
Open latest {latest?.version ?? ''}
</Button>
</CardActions>
</ModCard>
);
} }
return ( return (
[...mapWithModVersions.keys()].map(m => Mod(m)) <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 { interface ModsPageState {
mods: IMod[] mods: IMod[];
loading: boolean;
} }
class ModsPage extends React.Component<any, ModsPageState> { class ModsPage extends React.Component<any, ModsPageState> {
@ -51,22 +274,36 @@ class ModsPage extends React.Component<any, ModsPageState> {
constructor({props}: { props: any }) { constructor({props}: { props: any }) {
super(props); super(props);
this.state = { this.state = {
mods: [] mods: [],
} loading: true
};
} }
async componentDidMount() { async componentDidMount() {
try {
const response = await fetch(AvailableMods); const response = await fetch(AvailableMods);
const data: IMod[] = await response.json(); const data: IMod[] = await response.json();
console.log(data);
this.setState({ this.setState({
mods : data mods: data,
loading: false
}); });
} catch (error) {
console.error('Error loading mods:', error);
this.setState({ loading: false });
}
} }
render() { render() {
return <div>{Mods(this.state.mods)}</div>; 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 export default ModsPage;

View File

@ -8,29 +8,56 @@ import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
Box,
Button, Button,
ButtonGroup, Container,
Grid2, Link, Divider,
Grid2, LinearProgress,
Link,
List, List,
ListItem, ListItem,
ListSubheader,
Paper, Paper,
Table, Typography,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
ToggleButton,
ToggleButtonClassKey,
ToggleButtonGroup,
Typography
} from "@mui/material"; } from "@mui/material";
import {ArrowBack, ArrowDropDown, ExpandMore} from "@mui/icons-material"; import {ArrowBack, ExpandMore} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType";
import Weapon from "../classes/Weapon";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort"; import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort"; import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
import {styled} from '@mui/material/styles';
import {StyledLink} from "../commons/StyledLink";
// Styled components
const BackButton = styled(Button)(({ theme }) => ({
marginBottom: theme.spacing(3),
color: '#e94560',
borderColor: 'rgba(233, 69, 96, 0.5)',
textTransform: 'none',
fontWeight: 600,
'&:hover': {
borderColor: '#e94560',
backgroundColor: 'rgba(233, 69, 96, 0.1)',
},
}));
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
color: '#ffffff',
fontSize: '1.25rem',
}));
const UnitCard = styled(Paper)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.05)',
borderRadius: '10px',
padding: theme.spacing(1.5, 2),
transition: 'all 0.2s ease',
'&:hover': {
background: 'rgba(233, 69, 96, 0.1)',
borderColor: 'rgba(233, 69, 96, 0.3)',
},
}));
const BuildingCard = styled(UnitCard)(({ theme }) => ({}));
interface RacePageState { interface RacePageState {
mod: IMod, mod: IMod,
@ -41,13 +68,27 @@ interface RacePageState {
function Unit(unit: IUnitShort, modId: number, raceId: String) { function Unit(unit: IUnitShort, modId: number, raceId: String) {
return (<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}><ListItem> return (<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} sx={{
color: 'rgba(255,255,255,0.85)',
textDecoration: 'none',
display: 'block',
'&:hover': { color: '#e94560' }
}}>
<ListItem sx={{
color: 'rgba(255,255,255,0.85)',
padding: '6px 12px',
background: 'rgba(255,255,255,0.02)',
borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.05)',
transition: 'all 0.2s ease',
'&:hover': { background: 'rgba(233, 69, 96, 0.1)', borderColor: 'rgba(233, 69, 96, 0.3)' }
}}>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>} {unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
{unit.name} {unit.name}
{unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}} {unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>} src="/images/DETECT_YES.webp"/></span>}
</ListItem>
</ListItem></Link>) </Link>)
} }
function UnitSmall(unit: IUnitShort, modId: number, raceId: String) { function UnitSmall(unit: IUnitShort, modId: number, raceId: String) {
@ -59,23 +100,33 @@ function UnitSmall(unit: IUnitShort, modId: number, raceId: String) {
unitName = unit.name; unitName = unit.name;
} }
return (<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}> return (<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} >
{unit.icon && <img className="unitIconSmall" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>} {unit.icon && <img className="unitIconSmall" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
<span style={{fontSize: 12}}>{unitName}</span> <span style={{fontSize: 14}}>{unitName}</span>
{unit.canDetect && <span>&nbsp;<img {unit.canDetect && <span>&nbsp;<img
src="/images/DETECT_YES.webp"/></span>}<br/></Link>) src="/images/DETECT_YES.webp"/></span>}<br/></StyledLink>)
} }
function Building(building: IBuildingShort, modId: number, raceId: String) { function Building(building: IBuildingShort, modId: number, raceId: String) {
return (<Grid2 size={{xs: 12, md: 3}}><Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}><ListItem> return (<Grid2 size={{xs: 12, md: 3}}>
<Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id} sx={{
textDecoration: 'none',
display: 'block',
'&:hover': { color: '#e94560' }
}}>
<BuildingCard elevation={0}>
<ListItem sx={{
padding: '8px 12px',
}}>
{building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>} {building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
{building.name} <StyledLink>{building.name}</StyledLink>
{building.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}} {building.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>} src="/images/DETECT_YES.webp"/></span>}
</ListItem>
</ListItem></Link>
{building.units.map(unit => UnitSmall(unit, modId, raceId))} {building.units.map(unit => UnitSmall(unit, modId, raceId))}
</BuildingCard>
</Link>
</Grid2>) </Grid2>)
} }
@ -127,59 +178,61 @@ class Units extends React.Component<UnitsProps, UnitsState> {
document.title = this.state.buildings?.race.name document.title = this.state.buildings?.race.name
} }
const accordionSx = {
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' },
};
return (this.state.buildings != null ? return (this.state.buildings != null ?
<div> <Box>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
{this.state.buildings.buildings.map(building => Building(building, this.props.modId, this.props.raceId))} {this.state.buildings.buildings.map(building => Building(building, this.props.modId, this.props.raceId))}
</Grid2> </Grid2>
{this.state.buildings.buildingsAdvanced.length > 0 && <div><hr/><Grid2 container spacing={2}> {this.state.buildings.buildingsAdvanced.length > 0 && <Box sx={{ mt: 3 }}>
<Divider sx={{ my: 2, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<SectionTitle>Advanced buildings</SectionTitle>
<Grid2 container spacing={2}>
{this.state.buildings.buildingsAdvanced.map(building => Building(building, this.props.modId, this.props.raceId))} {this.state.buildings.buildingsAdvanced.map(building => Building(building, this.props.modId, this.props.raceId))}
</Grid2><br/></div>} </Grid2><br/></Box>}
<Grid2> <Box sx={{ mt: 3 }}>
<Grid2 size={{xs: 12, md: 12}}> <Accordion sx={accordionSx}>
<Accordion>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore/>} expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
aria-controls="units-accordion" aria-controls="units-accordion"
id="units-accordion" id="units-accordion"
sx={{color: '#ffffff'}}
> >
<h3>All units</h3> <SectionTitle sx={{ mb: 0 }}>All units</SectionTitle>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<h3>Infantry</h3> <Typography variant="h6" sx={{ color: '#ffffff', fontWeight: 600, mb: 2 }}>Infantry</Typography>
{this.state.units.infantry.map(unit => Unit(unit, this.props.modId, this.props.raceId))} {this.state.units.infantry.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<h3>Tech</h3> <Typography variant="h6" sx={{ color: '#ffffff', fontWeight: 600, mb: 2 }}>Tech</Typography>
<List sx={{ <List>
width: '100%',
maxWidth: 360,
bgcolor: 'background.paper'
}}>
{this.state.units.tech.map(unit => Unit(unit, this.props.modId, this.props.raceId))} {this.state.units.tech.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</List> </List>
</Grid2> </Grid2>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<h3>Support</h3> <Typography variant="h6" sx={{ color: '#ffffff', fontWeight: 600, mb: 2 }}>Support</Typography>
<List sx={{ <List>
width: '100%',
maxWidth: 360,
bgcolor: 'background.paper'
}}>
{this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId))} {this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</List> </List>
</Grid2> </Grid2>
</Grid2> </Grid2>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
</Grid2><br/> </Box>
</Grid2> </Box> : <Box sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>Loading</Box>
</div> : "Loading"
) )
} else { } else {
return "Loading..."; return <Box sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>Loading...</Box>;
} }
} }
} }
@ -188,7 +241,6 @@ class RacePageFast extends React.Component<any, RacePageState> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
console.log(this.props.match.params.id);
} }
@ -214,13 +266,43 @@ class RacePageFast extends React.Component<any, RacePageState> {
if (this.state != null && this.state.mod != null && this.state.race != null) { if (this.state != null && this.state.mod != null && this.state.race != null) {
const backRef = "/mod/" + this.state.mod.id const backRef = "/mod/" + this.state.mod.id
return <div><Link href={backRef}><Button id="back-button" variant="contained" return (
startIcon={<ArrowBack/>}> Back</Button></Link> <Container maxWidth="lg" sx={{ py: 4 }}>
<h1>{this.state.mod.name} ({this.state.mod.version}) - {this.state.race.name}</h1> <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: 'linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
{this.state.race.name}
</Typography>
<Typography variant="h6" sx={{
color: 'rgba(255, 255, 255, 0.6)',
fontWeight: 500,
}}>
{this.state.mod.name} ({this.state.mod.version})
</Typography>
</Box>
<Units raceId={this.state.race.id} modId={this.state.mod.id}/> <Units raceId={this.state.race.id} modId={this.state.mod.id}/>
</div>; </Container>
);
} else { } else {
return "loading..."; return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<LinearProgress sx={{ width: '200px', mx: 'auto', display: 'block' }} />
</Container>
);
} }
} }
} }

View File

@ -7,15 +7,18 @@ import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
Box,
Button, Button,
Grid2, Container,
Divider,
Grid2, LinearProgress,
Paper, Paper,
Table, Table,
TableBody, TableBody,
TableCell, TableCell,
TableContainer,
TableRow, TableRow,
Tooltip Tooltip,
Typography,
} from "@mui/material"; } from "@mui/material";
import {ArrowBack, ExpandMore} from "@mui/icons-material"; import {ArrowBack, ExpandMore} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType"; import ArmorType from "../classes/ArmorType";
@ -30,6 +33,43 @@ import {renderAffectedResearches} from "../classes/building/Research";
import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable"; import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
import Ability from "../classes/Ability"; import Ability from "../classes/Ability";
import DeathExplosion from "../classes/DeathExplosion"; import DeathExplosion from "../classes/DeathExplosion";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox";
// Styled components
const BackButton = styled(Button)(({ theme }) => ({
marginBottom: theme.spacing(3),
color: '#e94560',
borderColor: 'rgba(233, 69, 96, 0.5)',
textTransform: 'none',
fontWeight: 600,
'&:hover': {
borderColor: '#e94560',
backgroundColor: 'rgba(233, 69, 96, 0.1)',
},
}));
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
color: '#ffffff',
fontSize: '1.25rem',
}));
const StatsPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root': {
'&:last-child td, &:last-child th': { border: 0 },
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface UintPageState { interface UintPageState {
unit: IUnit, unit: IUnit,
@ -39,7 +79,7 @@ interface UintPageState {
function Unit(unit: IUnit, mod: IMod) { function Unit(unit: IUnit, mod: IMod) {
document.title = unit.name document.title = unit.name + " — " + mod.name
const morale = (unit.moraleMax !== null) ? <span> const morale = (unit.moraleMax !== null) ? <span>
<img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/>&nbsp;{unit.moraleMax} <img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/>&nbsp;{unit.moraleMax}
@ -71,7 +111,7 @@ function Unit(unit: IUnit, mod: IMod) {
const SergeantShort = (props: sergeantProps) => { const SergeantShort = (props: sergeantProps) => {
return ( return (
<span style={{fontSize: 20}}> <span style={{fontSize: 20, color: '#ffffff'}}>
{props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')}/> } {props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')}/> }
&nbsp; {props.name} &nbsp; {props.name}
{props.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}} {props.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
@ -82,21 +122,36 @@ function Unit(unit: IUnit, mod: IMod) {
return ( return (
<div> <Box>
<h1>{mod.name} ({mod.version})</h1> <Box sx={{ mb: 3 }}>
<h2>{unit.icon && <Typography variant="h4" component="h2" sx={{
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>} {unit.name} </h2> fontWeight: 800,
mb: 1,
color: '#ffffff',
display: 'flex',
alignItems: 'center',
gap: 2,
}}>
{unit.icon &&
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
{unit.name}
</Typography>
<Typography variant="subtitle1" sx={{
color: 'rgba(255, 255, 255, 0.6)',
fontWeight: 500,
}}>
{mod.name} ({mod.version})
</Typography>
</Box>
<Grid2 container spacing={2}> <Grid2 container spacing={3}>
<Grid2 size={{xs: 12, md: 4}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <StatsPaper >
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table" > <TableBody id="unit-stats-table" >
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Cost</TableCell> <TableCell component="th" scope="row" >Cost</TableCell>
<TableCell component="th" scope="row"> <TableCell>
{unit.buildCostRequisition > 0 && {unit.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp; src="/images/Resource_requisition.gif"/>&nbsp;
@ -182,24 +237,18 @@ function Unit(unit: IUnit, mod: IMod) {
{unit.squadMaxSize > 1 && {unit.squadMaxSize > 1 &&
<TableRow> <TableRow>
<TableCell >Squad size</TableCell> <TableCell >Squad size</TableCell>
<TableCell> <TableCell>{unit.squadStartSize} / {unit.squadMaxSize}</TableCell>
{unit.squadStartSize} / {unit.squadMaxSize}
</TableCell>
</TableRow> </TableRow>
} }
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}} <TableCell >Armor type</TableCell>
> <TableCell>
<TableCell component="th" scope="row">Armor type</TableCell>
<TableCell component="th" scope="row">
{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> } {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> </TableCell>
</TableRow> </TableRow>
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}} <TableCell >Health</TableCell>
> <TableCell>
<TableCell component="th" scope="row">Health</TableCell>
<TableCell component="th" scope="row">
<img style={{verticalAlign: "top"}} <img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp; src="/images/Health_icon.webp"/>&nbsp;
{unit.health} {unit.healthRegeneration > 0 && {unit.health} {unit.healthRegeneration > 0 &&
@ -207,78 +256,54 @@ function Unit(unit: IUnit, mod: IMod) {
{unit.armour !== undefined && unit.armour !== 0 && <span><img style={{height: 20, verticalAlign: "top"}} src="/images/defence.png"/>{unit.armour} </span> } {unit.armour !== undefined && unit.armour !== 0 && <span><img style={{height: 20, verticalAlign: "top"}} src="/images/defence.png"/>{unit.armour} </span> }
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}} <TableCell >Move speed</TableCell>
> <TableCell>{unit.moveSpeed}</TableCell>
<TableCell component="th" scope="row">Move speed</TableCell>
<TableCell component="th" scope="row">
{unit.moveSpeed}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}} <TableCell >Morale</TableCell>
> <TableCell>{morale}</TableCell>
<TableCell component="th" scope="row">Morale</TableCell>
<TableCell component="th" scope="row">
{morale}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow>
sx={{'&:last-child td, &:last-child th': {border: 0}}} <TableCell >Mass</TableCell>
> <TableCell>{unit.mass}</TableCell>
<TableCell component="th" scope="row">Mass</TableCell>
<TableCell component="th" scope="row">
{unit.mass}
</TableCell>
</TableRow> </TableRow>
<TableRow <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
sx={{'&:last-child td, &:last-child th': {border: 0}}} <TableCell >Vision</TableCell>
> <TableCell><Vision sight={unit.sightRadius} detect={unit.detectRadius}/></TableCell>
<TableCell component="th" scope="row">Vision</TableCell>
<TableCell component="th" scope="row">
<Vision sight={unit.sightRadius} detect={unit.detectRadius}/>
</TableCell>
</TableRow> </TableRow>
{unit.repairMax !== undefined && unit.repairMax !== null && {unit.repairMax !== undefined && unit.repairMax !== null &&
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell >Repair max</TableCell> <TableCell >Repair max</TableCell>
<TableCell> <TableCell>{unit.repairMax}</TableCell>
{unit.repairMax}
</TableCell>
</TableRow> </TableRow>
} }
{unit.repairSpeed !== undefined && unit.repairSpeed !== null && unit.repairCostPercent !== null && {unit.repairSpeed !== undefined && unit.repairSpeed !== null && unit.repairCostPercent !== null &&
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell >Repair</TableCell> <TableCell >Repair</TableCell>
<TableCell> <TableCell>{unit.repairSpeed} hp/s; {unit.repairCostPercent}% cost</TableCell>
{unit.repairSpeed} hp/s; {unit.repairCostPercent}% cost
</TableCell>
</TableRow> </TableRow>
} }
{unit.mobValue != null && {unit.mobValue != null &&
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell >Mob value</TableCell> <TableCell >Mob value</TableCell>
<TableCell> <TableCell><span> <img style={{height: 20, verticalAlign: "top"}} src="/images/Mob_bonus.gif"/> {unit.mobValue} </span></TableCell>
<span> <img style={{height: 20, verticalAlign: "top"}} src="/images/Mob_bonus.gif"/> {unit.mobValue} </span>
</TableCell>
</TableRow> </TableRow>
} }
{unit.squadLimit !== undefined && unit.squadLimit !== null && {unit.squadLimit !== undefined && unit.squadLimit !== null &&
<TableRow> <TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell >Limit</TableCell> <TableCell >Limit</TableCell>
<TableCell> <TableCell>{unit.squadLimit}</TableCell>
{unit.squadLimit}
</TableCell>
</TableRow> </TableRow>
} }
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </StatsPaper>
</Grid2> </Grid2>
<Grid2 size={8}> <Grid2 size={{xs: 12, md: 8}}>
<div style={{whiteSpace: "pre-wrap"}}> <DescriptionBox>
{unit.description} {unit.description}
</div> </DescriptionBox>
</Grid2> </Grid2>
{unit.requirements !== null && {unit.requirements !== null &&
<Grid2 size={{xs: 12, md: 12}}> <Grid2 size={{xs: 12, md: 12}}>
@ -286,19 +311,21 @@ function Unit(unit: IUnit, mod: IMod) {
</Grid2>} </Grid2>}
<Grid2 size={12}> <Grid2 size={12}>
{unit.sergeants.map(s => {unit.sergeants.map(s =>
<Accordion key={s.id}> <Accordion key={s.id} sx={{
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
mb: 1,
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}}>
<AccordionSummary <AccordionSummary
expandIcon={<ExpandMore/>} sx={{color: '#ffffff'}}>
aria-controls="panel1-content"
id="panel1-header"
>
<SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0}/> <SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0}/>
</AccordionSummary> </AccordionSummary>
<AccordionDetails> <AccordionDetails sx={{color: 'rgba(255, 255, 255, 0.85)'}}>
<Sergeant mod={mod} sergeant={s} race={unit.race}/> <Sergeant mod={mod} sergeant={s} race={unit.race}/>
<hr/>
</AccordionDetails> </AccordionDetails>
</Accordion>)} </Accordion>)}
</Grid2> </Grid2>
@ -306,17 +333,17 @@ function Unit(unit: IUnit, mod: IMod) {
unit.moraleBrakeModifiers.find(m => m.reference.includes("speed_maximum_modifier") && m.value === 1.2) !== undefined && unit.moraleBrakeModifiers.find(m => m.reference.includes("speed_maximum_modifier") && m.value === 1.2) !== undefined &&
unit.moraleBrakeModifiers.length === 2) unit.moraleBrakeModifiers.length === 2)
&& <Grid2 size={12}> && <Grid2 size={12}>
<h3><img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/> Morale broken unusual behavior <img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/></h3> <SectionTitle><img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/> Morale broken unusual behavior <img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/></SectionTitle>
<ModifiersProvidesTable modifiers={unit.moraleBrakeModifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/> <ModifiersProvidesTable modifiers={unit.moraleBrakeModifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/>
</Grid2>} </Grid2>}
{unit.modifiers.length > 0 && <Grid2 size={12}> {unit.modifiers.length > 0 && <Grid2 size={12}>
<h3>Affected on</h3> <SectionTitle>Affected on</SectionTitle>
<ModifiersProvidesTable modifiers={unit.modifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/> <ModifiersProvidesTable modifiers={unit.modifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/>
</Grid2>} </Grid2>}
{unit.abilities.length > 0 && <Grid2 size={12}> {unit.abilities.length > 0 && <Grid2 size={12}>
<h3>Abilities</h3> <SectionTitle>Abilities</SectionTitle>
{unit.abilities.map(a => {unit.abilities.map(a =>
<Ability mod={mod} ability={a} race={unit.race}/> <Ability mod={mod} ability={a} race={unit.race}/>
)} )}
@ -338,10 +365,14 @@ function Unit(unit: IUnit, mod: IMod) {
{renderAffectedResearches(unit.affectedResearches, mod.id, unit.race.id)} {renderAffectedResearches(unit.affectedResearches, mod.id, unit.race.id)}
</Grid2> </Grid2>
</Grid2> </Grid2>
<b className="hotkey">Hotkey: {unit.hotkey}</b>
<hr/> <Box sx={{ mt: 3, mb: 2, fontWeight: 600, fontSize: '0.85rem' }}>
Hotkey: {unit.hotkey}
</Box>
<Divider sx={{ my: 3, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<UnitsTable modId={mod.id}/> <UnitsTable modId={mod.id}/>
</div>) </Box>
)
} }
@ -371,12 +402,24 @@ class UnitPage extends React.Component<any, UintPageState> {
if (this.state != null && this.state.unit != null && this.state.mod != null) { 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 backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId
return <div key={this.state.unit.id}><a href={backRef}><Button id="back-button" variant="contained" return (
startIcon={<ArrowBack/>}> Back</Button></a> <Container maxWidth="lg" sx={{ py: 4 }}>
<BackButton
variant="outlined"
startIcon={<ArrowBack/>}
href={backRef}
>
Back to race
</BackButton>
{Unit(this.state.unit, this.state.mod)} {Unit(this.state.unit, this.state.mod)}
</div>; </Container>
);
} else { } else {
return <div key = {'loading'}>loading...</div>; return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
);
} }
} }
} }

View File

@ -1,6 +1,7 @@
export interface IMod { export interface IMod {
id: number; id: number;
name: string; name: string;
order: number;
version: string; version: string;
technicalName: string; technicalName: string;
isBeta: boolean; isBeta: boolean;