Sergeants frontend

This commit is contained in:
Anibus 2025-03-16 12:07:55 +03:00
parent 2b4352ff21
commit 98b5a47c0f
20 changed files with 938 additions and 479 deletions

View File

@ -1,70 +1,6 @@
# Getting Started with Create React App # Soulstorm wiki frontend
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). ## Стек технологий:
## Available Scripts - react (вперемешку классы и функции)
- typescript
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

View File

@ -24,8 +24,9 @@ function App() {
cssVariables: true, cssVariables: true,
}); });
return ( return (
<div> <div >
<AppBar position="static"> <AppBar position="static">
<Toolbar> <Toolbar>
<IconButton <IconButton
@ -37,7 +38,7 @@ function App() {
> >
</IconButton> </IconButton>
<Typography variant="h6" component="div" sx={{flexGrow: 1}}> <Typography variant="h6" component="div" sx={{flexGrow: 1}}>
Soulstorm wiki Soulstorm autogeneration wiki
</Typography> </Typography>
<Button color="inherit" onClick={steamLogin}> <Button color="inherit" onClick={steamLogin}>
Log in through Steam Log in through Steam

View File

@ -1,7 +1,8 @@
import {Routes, Route, BrowserRouter} from "react-router-dom"; import {Routes, Route, BrowserRouter} from "react-router-dom";
import RacePage from "./pages/RacePage";
import ModsPage from "./pages/ModsPage"; import ModsPage from "./pages/ModsPage";
import ModPage from "./pages/ModPage"; import ModPage from "./pages/ModPage";
import RacePageFast from "./pages/RacePageFast";
import UnitPage from "./pages/UnitPage";
export const MyRoutes = () => { export const MyRoutes = () => {
@ -9,8 +10,9 @@ export const MyRoutes = () => {
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/" element={<ModsPage/>}/> <Route path="/" element={<ModsPage/>}/>
<Route path="/mod/:id" element={<ModPage/>}/> <Route path="/mod/:modId" element={<ModPage/>}/>
<Route path="/mod/:modId/race/:raceId" element={<RacePage/>}/> <Route path="/mod/:modId/race/:raceId" element={<RacePageFast/>}/>
<Route path="/mod/:modId/race/:raceId/unit/:unitId" element={<UnitPage/>}/>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
) )

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import {Tooltip} from "@mui/material";
interface IArmorType{ interface IArmorType{
name: string, name: string,
@ -11,8 +12,8 @@ class ArmorType extends React.Component<IArmorType, any> {
withName: false withName: false
}; };
renderArmorImage(armorType: String): string { renderArmorImage(armorTypeId: string): string {
switch(armorType) { switch(armorTypeId) {
case 'Infantry Low': case 'Infantry Low':
return '/images/ARM_Inf_Lo.webp'; return '/images/ARM_Inf_Lo.webp';
case 'Infantry Medium': case 'Infantry Medium':
@ -44,13 +45,14 @@ class ArmorType extends React.Component<IArmorType, any> {
case 'Demon High': case 'Demon High':
return '/images/ARM_Dmn_Hi.webp'; return '/images/ARM_Dmn_Hi.webp';
default: default:
return 'Unknown armor'; return armorTypeId;
} }
} }
render() { render() {
return (<span> <img style={{verticalAlign: "top"}} src={this.renderArmorImage(this.props.name)}/> {this.props.withName && this.props.name} </span>); return (<Tooltip title={this.props.name}><span> <img style={{verticalAlign: "top"}}
src={this.renderArmorImage(this.props.name)}/> {this.props.withName && this.props.name}</span></Tooltip>);
} }
} }

View File

@ -1,16 +0,0 @@
import React from 'react';
export default class Item extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>Shopping List for {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
);
}
}

132
src/classes/Sergeant.tsx Normal file
View File

@ -0,0 +1,132 @@
import React, {useState} from "react";
import {IconUrl, UserUrl} from "../core/api";
import {ISergeant, IWeapon} from "../types/IUnit";
import {
Accordion, AccordionDetails,
AccordionSummary,
Grid2,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableRow
} from "@mui/material";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import ArmorType from "./ArmorType";
import {ExpandMore} from "@mui/icons-material";
import WeaponSlot from "./WeaponSlot";
export interface SergeantProps {
sergeant: ISergeant
}
const Sergeant = (props: SergeantProps) => {
const sergeant = props.sergeant
const detect = sergeant.detectRadius > 0 ? <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/> {sergeant.detectRadius}</span> :
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_NO.webp"/></span>
let mapWithUnitWeapons: Map<number, Map<number, IWeapon>> = new Map();
sergeant.weapons.forEach(weapon => {
const weaponMap = mapWithUnitWeapons.get(weapon.hardpoint)
if (weaponMap == null) {
const weaponMap = new Map()
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
mapWithUnitWeapons.set(weapon.hardpoint, weaponMap)
} else {
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
}
})
return (
<div>
<Grid2 container spacing={2}>
<Grid2 size= {{xs: 12, md: 4}}>
<TableContainer component={Paper}>
<Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table">
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">cost</TableCell>
<TableCell component="th" scope="row">
{sergeant.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{sergeant.buildCostRequisition}</span>}
{sergeant.buildCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{sergeant.buildCostPower}</span>}
{(sergeant.buildCostPopulation !== undefined && sergeant.buildCostPopulation > 0) && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{sergeant.buildCostPopulation}</span>}
{(sergeant.buildCostTime !== undefined && sergeant.buildCostTime > 0) && <span>&nbsp;<AvTimerOutlinedIcon style={{verticalAlign: "top", fontSize:"18px"}} />&nbsp;
{sergeant.buildCostTime}s</span>}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">armor type</TableCell>
<TableCell component="th" scope="row">
<ArmorType name={sergeant.armorType.name}/>
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">health</TableCell>
<TableCell component="th" scope="row">
<span><img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp;
{sergeant.health} {sergeant.healthRegeneration > 0 &&
<span>+{sergeant.healthRegeneration}/s</span>} </span>
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">morale</TableCell>
<TableCell component="th" scope="row">
<img style={{verticalAlign: "top"}}
src="/images/Kills_icon.webp"/> -{sergeant.moraleDeathPenalty}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">detect</TableCell>
<TableCell component="th" scope="row">
{detect}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid2>
<Grid2 size={8}>
<div style={{whiteSpace: "pre-wrap"}}>
{sergeant.description}
</div>
</Grid2>
<Grid2 size={12}>
{[...mapWithUnitWeapons.keys()].sort(function (a, b) {
return a - b;
}).map(h => <WeaponSlot unitWeapons={mapWithUnitWeapons.get(h)} hardpoint={h}/>)}
</Grid2>
</Grid2>
<i className="rgdFrom">{sergeant.filename}</i>
</div>)
};
export default Sergeant;

140
src/classes/UnitsTable.tsx Normal file
View File

@ -0,0 +1,140 @@
import React, {useEffect, useState} from "react";
import {AvailableUnits} from "../core/api";
import {Grid2, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material";
import {NavLink} from "react-router-dom";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
interface IUnitsTable {
racesUnits: IRaceUnits[];
}
export default function UnitsTable(prop: {modId: number}) {
const [unitsTable, setUnitsTable] = useState<IUnitsTable>({
racesUnits: []
});
function getUnitRef(modId: number, raceId: string, unit: IUnitShort) {
return <span style={{fontSize: 11}}><a href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
{unit.name}
</a><br/></span>
}
useEffect(() => {
fetch(AvailableUnits + "/mod/" + prop.modId)
.then(res => res.json())
.then((res: IRaceUnits[]) => {
setUnitsTable({
racesUnits : res
});
})
}, []);
function armourTypePriority(armorTypeId: string): number {
switch(armorTypeId) {
case 'Infantry Low':
return 1;
case 'Infantry Medium':
return 2;
case 'Infantry High':
return 3;
case 'Demon Medium':
return 4;
case 'Infantry Heavy Medium':
return 5;
case 'Infantry Heavy High':
return 6;
case 'Commander':
return 7;
case 'Vehicle Low':
return 8;
case 'Vehicle Medium':
return 9;
case 'Vehicle High':
return 10;
case 'Demon High':
return 11;
case 'Air':
return 12;
case 'Building Low':
return 13;
case 'Building Medium':
return 14;
case 'Building High':
return 15;
default:
return 0;
}
}
function getUnitsRef(units: IUnitShort[], raceId: string): JSX.Element[] {
return units
.sort((a, b) => armourTypePriority(a.armourTypeName) - armourTypePriority(b.armourTypeName))
.map(unit =>
getUnitRef(prop.modId, raceId, unit)
)
}
function generateRaceUnitTable(racesUnitsPart: IRaceUnits[]){
return (<TableContainer>
{<Table sx={{minWidth: 650}} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
{racesUnitsPart.map(raceUnits => <TableCell><h2><NavLink state={raceUnits.race.id}
to={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</NavLink>
</h2></TableCell>)}
</TableRow>
</TableHead>
<TableBody>
<TableRow style={{verticalAlign: 'top'}}>
{racesUnitsPart.map(raceUnits =>
<TableCell>
{getUnitsRef(raceUnits.infantry, raceUnits.race.id)}
</TableCell>)
}
</TableRow>
<TableRow style={{verticalAlign: 'top'}}>
{racesUnitsPart.map(raceUnits =>
<TableCell>
{getUnitsRef(raceUnits.tech, raceUnits.race.id)}
</TableCell>)
}
</TableRow>
<TableRow style={{verticalAlign: 'top'}}>
{racesUnitsPart.map(raceUnits =>
<TableCell>
{getUnitsRef(raceUnits.support, raceUnits.race.id)}
</TableCell>)
}
</TableRow>
</TableBody>
</Table>}
</TableContainer>)
}
function generateRaceUnitTables(racesUnits: IRaceUnits[], raceCount: number){
var elements: JSX.Element[] = []
for(let i = 0; i < racesUnits.length;i = i + raceCount){
elements.push(generateRaceUnitTable(racesUnits.slice(i, i + raceCount)))
}
return elements
}
return (
<Grid2>
<Grid2 display={{ lg: 'block', xs: 'none' }} >
{generateRaceUnitTables(unitsTable.racesUnits, 9)}
</Grid2>
<Grid2 display={{ lg: 'none', xs: 'block' }}>
{unitsTable.racesUnits.map(raceUnits => <h4><NavLink state={raceUnits.race.id}
to={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</NavLink>
</h4>)}
</Grid2>
</Grid2>
)
}

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import {IWeapon, WeaponPiercing} from "../types/IUnit"; import {IWeapon, IWeaponPiercing} from "../types/IUnit";
import { import {
Grid2, Grid2,
Paper, Paper,
@ -14,9 +14,11 @@ import {
} from "@mui/material"; } from "@mui/material";
import ArmorType from "./ArmorType"; import ArmorType from "./ArmorType";
import ArmorTypeNames from "../types/ArmorTypeValues"; import ArmorTypeNames from "../types/ArmorTypeValues";
import {IconUrl} from "../core/api";
interface IWeaponProps{ interface IWeaponProps{
weapon: IWeapon, weapon: IWeapon,
isDefault: Boolean,
} }
interface IWeaponState{ interface IWeaponState{
@ -25,6 +27,10 @@ interface IWeaponState{
class Weapon extends React.Component<IWeaponProps, any> { class Weapon extends React.Component<IWeaponProps, any> {
public static defaultProps = {
isDefault: false
};
humanReadableName(unit: string) { humanReadableName(unit: string) {
const firstUpper = String(unit).charAt(0).toUpperCase() + String(unit).slice(1); const firstUpper = String(unit).charAt(0).toUpperCase() + String(unit).slice(1);
return firstUpper.replaceAll('_', ' ').replace('.rgd', ''); return firstUpper.replaceAll('_', ' ').replace('.rgd', '');
@ -56,7 +62,7 @@ class Weapon extends React.Component<IWeaponProps, any> {
const weapon = this.props.weapon const weapon = this.props.weapon
const dpsK = (this.state.currentTable == "dps") ? weapon.accuracy * (1/(weapon.reloadTime - weapon.reloadTime % 0.125)) : 1 const dpsK = (this.state.currentTable == "dps") ? weapon.accuracy * (1/(weapon.reloadTime - (weapon.reloadTime % 0.125))) : 1
const infLowPiercing = this.getPiercingK(ArmorTypeNames.InfantryLow) const infLowPiercing = this.getPiercingK(ArmorTypeNames.InfantryLow)
const infMedPiercing = this.getPiercingK(ArmorTypeNames.InfantryMedium) const infMedPiercing = this.getPiercingK(ArmorTypeNames.InfantryMedium)
@ -77,6 +83,9 @@ class Weapon extends React.Component<IWeaponProps, any> {
const getTotalDamage = (damagePiercing: number, isAir: boolean = false) => { const getTotalDamage = (damagePiercing: number, isAir: boolean = false) => {
if(!isAir && !weapon.canAttackGround && !weapon.isMeleeWeapon) return ""
if(isAir && !weapon.canAttackAir) return ""
var minDamage = damagePiercing * weapon.minDamage var minDamage = damagePiercing * weapon.minDamage
var maxDamage = damagePiercing * weapon.maxDamage var maxDamage = damagePiercing * weapon.maxDamage
@ -101,18 +110,20 @@ class Weapon extends React.Component<IWeaponProps, any> {
[`&.${tableCellClasses.head}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: "rgb(234, 234, 234)", backgroundColor: "rgb(234, 234, 234)",
color: theme.palette.common.white, color: theme.palette.common.white,
paddingLeft: 10
}, },
[`&.${tableCellClasses.body}`]: { [`&.${tableCellClasses.body}`]: {
fontSize: 14, fontSize: 13,
paddingLeft: 10,
}, },
})); }));
return ( return (
<div> <div>
<h4>{this.humanReadableName(weapon.name)}</h4> <h4>{ weapon.name ? weapon.name : this.humanReadableName(weapon.filename)}</h4>
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size={3}> <Grid2 size= {{xs: 12, md: 3}}>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
<TableBody> <TableBody>
@ -163,11 +174,37 @@ class Weapon extends React.Component<IWeaponProps, any> {
scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(2) : "-"} scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(2) : "-"}
</TableCell> </TableCell>
</TableRow> </TableRow>
{(weapon.costRequisition > 0 || weapon.costPower > 0) &&
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Cost</TableCell>
<TableCell component="th"
scope="row">
{weapon.costRequisition > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{weapon.costRequisition}</span>}
{weapon.costPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{weapon.costPower}</span>}
</TableCell>
</TableRow>
}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
</Grid2> </Grid2>
<Grid2 size={9}> <Grid2 size={9}>
<div style={{whiteSpace: "pre-wrap"}}>
{!this.props.isDefault && weapon.icon ? <img className="unitIcon" src={IconUrl + weapon.icon.replaceAll('\\', '/')}/> : (
weapon.isMeleeWeapon ?
<img src="/images/MeleeStance_icon_bw.jpg"/> :
<img src="/images/RangedStance_icon_bw.jpg"/>
) } <br/>
{this.props.isDefault ? "Default weapon" : weapon.description}
</div>
</Grid2>
<Grid2 size={12}>
<ToggleButtonGroup <ToggleButtonGroup
color="primary" color="primary"
exclusive exclusive
@ -179,57 +216,62 @@ class Weapon extends React.Component<IWeaponProps, any> {
<ToggleButton size="small" value="one hit">One hit average damage</ToggleButton> <ToggleButton size="small" value="one hit">One hit average damage</ToggleButton>
</ToggleButtonGroup> </ToggleButtonGroup>
<TableContainer> <TableContainer>
<Table sx={{minWidth: 600, marginTop: "10px"}} size="small" aria-label="a dense table"> <Table sx={{marginTop: "10px"}} size="small" aria-label="a dense table">
<TableHead> <TableHead>
<TableRow> <TableRow>
<StyledTableCell ><ArmorType name={ArmorTypeNames.InfantryLow}/></StyledTableCell> <StyledTableCell><ArmorType
<StyledTableCell><ArmorType name={ArmorTypeNames.InfantryMedium}/></StyledTableCell> name={ArmorTypeNames.InfantryLow}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.InfantryHigh}/></StyledTableCell> <StyledTableCell><ArmorType
<StyledTableCell><ArmorType name={ArmorTypeNames.InfantryHeavyMedium}/></StyledTableCell> name={ArmorTypeNames.InfantryMedium}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.InfantryHeavyHigh}/></StyledTableCell> <StyledTableCell><ArmorType
<StyledTableCell><ArmorType name={ArmorTypeNames.DemonMedium}/></StyledTableCell> name={ArmorTypeNames.InfantryHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.DemonHigh}/></StyledTableCell> <StyledTableCell><ArmorType
<StyledTableCell><ArmorType name={ArmorTypeNames.Commander}/></StyledTableCell> name={ArmorTypeNames.InfantryHeavyMedium}/></StyledTableCell>
</TableRow> <StyledTableCell><ArmorType
</TableHead> name={ArmorTypeNames.InfantryHeavyHigh}/></StyledTableCell>
<TableBody> <StyledTableCell><ArmorType name={ArmorTypeNames.Commander}/></StyledTableCell>
<TableRow> <StyledTableCell><ArmorType
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell> name={ArmorTypeNames.DemonMedium}/></StyledTableCell>
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell> <StyledTableCell><ArmorType name={ArmorTypeNames.DemonHigh}/></StyledTableCell>
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell> <StyledTableCell><ArmorType name={ArmorTypeNames.Air}/></StyledTableCell>
<StyledTableCell>{getTotalDamage(infHeavyMedPiercing)}</StyledTableCell> <StyledTableCell><ArmorType name={ArmorTypeNames.VehicleLow}/></StyledTableCell>
<StyledTableCell>{getTotalDamage(infHeavyHighPiercing)}</StyledTableCell> <StyledTableCell><ArmorType
<StyledTableCell>{getTotalDamage(demonPiercing)}</StyledTableCell> name={ArmorTypeNames.VehicleMedium}/></StyledTableCell>
<StyledTableCell>{getTotalDamage(demonHighPiercing)}</StyledTableCell> <StyledTableCell><ArmorType
<StyledTableCell>{getTotalDamage(commanderPiercing)}</StyledTableCell> name={ArmorTypeNames.VehicleHigh}/></StyledTableCell>
</TableRow> <StyledTableCell><ArmorType
</TableBody> name={ArmorTypeNames.BuildingLow}/></StyledTableCell>
<TableHead> <StyledTableCell><ArmorType
<TableRow > name={ArmorTypeNames.BuildingMedium}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.Air}/></StyledTableCell> <StyledTableCell><ArmorType
<StyledTableCell><ArmorType name={ArmorTypeNames.VehicleLow}/></StyledTableCell> name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.VehicleMedium}/></StyledTableCell> <StyledTableCell><img style={{verticalAlign: "top"}}
<StyledTableCell><ArmorType name={ArmorTypeNames.VehicleHigh}/></StyledTableCell> src="/images/ARM_Morale.webp"/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.BuildingLow}/></StyledTableCell> </TableRow>
<StyledTableCell><ArmorType name={ArmorTypeNames.BuildingMedium}/></StyledTableCell> </TableHead>
<StyledTableCell><ArmorType name={ArmorTypeNames.BuildingHigh}/></StyledTableCell> <TableBody>
<StyledTableCell><img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/></StyledTableCell> <TableRow>
</TableRow> <StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
</TableHead> <StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
<TableBody> <StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
<TableRow> <StyledTableCell>{getTotalDamage(infHeavyMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(airPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(infHeavyHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(commanderPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(demonPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(demonHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(buildingLowPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(airPiercing, true)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(buildingMedPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(buildingHighPiercing)}</StyledTableCell> <StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
<StyledTableCell>{getMoraleDamage()}</StyledTableCell> <StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
</TableRow> <StyledTableCell>{getTotalDamage(buildingLowPiercing)}</StyledTableCell>
</TableBody> <StyledTableCell>{getTotalDamage(buildingMedPiercing)}</StyledTableCell>
</Table> <StyledTableCell>{getTotalDamage(buildingHighPiercing)}</StyledTableCell>
<StyledTableCell>{getMoraleDamage()}</StyledTableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer> </TableContainer>
<i className="rgdFrom">{weapon.filename}</i>
</Grid2> </Grid2>
</Grid2> </Grid2>
</div> </div>

View File

@ -0,0 +1,37 @@
import Weapon from "./Weapon";
import React from "react";
import {IWeapon} from "../types/IUnit";
export interface WeaponSlotProps {
hardpoint: number,
unitWeapons: Map<number, IWeapon> | undefined
}
const WeaponSlot= (props: WeaponSlotProps) => {
let weaponThisSlotMap = props.unitWeapons ?? []
let weaponsSorted = [...weaponThisSlotMap].sort()
let header = "Weapon slot " + props.hardpoint
let haveDummy = weaponsSorted.find(wp => !wp[1].filename.includes("dummy"))
let firstWeapon = weaponsSorted[0][1]
let onlyDummy = weaponsSorted.filter(wp => !wp[1].filename.includes("dummy")).length === 0
if(onlyDummy) return (<div></div>)
return (
<div>
<h3>{header}</h3>
{weaponsSorted.filter(wp => !wp[1].filename.includes("dummy")).map(wp =>
<div style={{marginLeft: 20}}><Weapon isDefault={!haveDummy || wp[1].filename === firstWeapon.filename} weapon={wp[1]}/></div>
)}
</div>
)
}
export default WeaponSlot;

View File

@ -1,5 +1,10 @@
.unitIcon{ .unitIcon{
width: 64px; width: 50px;
}
.sergeantIcon{
padding: 0px;
width: 40px;
} }
.unitHeader{ .unitHeader{
@ -16,6 +21,6 @@
} }
#filtered-units{ .rgdFrom{
margin-top: 10px; font-size: 11px;
} }

View File

@ -1,57 +1,45 @@
import {AvailableMods, AvailableRacesPart} from "../core/api"; import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import React, {useEffect, useRef, useState} from "react"; import React, {useEffect, useRef, useState} from "react";
import {NavLink, useLocation, useParams} from "react-router-dom"; 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 {Irace} from "../types/Irace";
import {Button} from "@mui/material"; import {Button, ListItem, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material";
import {ArrowBack} from "@mui/icons-material"; import {ArrowBack} from "@mui/icons-material";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import UnitsTable from "../classes/UnitsTable";
interface ModPageState { interface ModPageState {
mod: IMod, mod: IMod,
races: Irace[],
modId: Number,
} }
class ModPage extends React.Component<any, ModPageState> { class ModPage extends React.Component<any, ModPageState> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
console.log(this.props.match.params.id); fetch(AvailableMods + "/" + this.props.match.params.modId)
this.setState({ .then(res => res.json())
modId : this.props.match.params.id .then((res: IMod) => {
}); this.setState({
mod : res
});
});
} }
async componentDidMount() {
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.id);
const modData: IMod = await responseMod.json();
console.log(modData);
this.setState({
mod : modData
});
const response = await fetch(AvailableRacesPart + "/mod/" + this.props.match.params.id);
const racesData: Irace[] = await response.json();
this.setState({
races : racesData
});
}
render() { render() {
if(this.state != null && this.state.races != null ){ if(this.state != null && this.state.mod != null ){
return <div> return <div>
<a href="/"><Button id="back-button" variant="contained" <a href="/"><Button id="back-button" variant="contained"
startIcon={<ArrowBack/>}> Back</Button></a> startIcon={<ArrowBack/>}> Back</Button></a>
<h1>{this.state.mod.name} ({this.state.mod.version})</h1> <h1>{this.state.mod.name} ({this.state.mod.version})</h1>
{this.state.races.map(race => <ul><NavLink state={race.id} <UnitsTable modId={this.state.mod.id}/>
to={"/mod/" + this.state.mod.id + "/race/" + race.id}>{race.name}</NavLink>
</ul>)}
</div>; </div>;
} else { } else {
return ""; return "";
@ -59,4 +47,6 @@ class ModPage extends React.Component<any, ModPageState> {
} }
} }
export default withRouter(ModPage); export default withRouter(ModPage);

View File

@ -24,7 +24,13 @@ function Mods (mods: IMod[]) {
return( return(
<div> <div>
<h1>{modName}</h1> <h1>{modName}</h1>
{sameMods.map(mod => <ul><NavLink state={mod.id} to= {"/mod/" + mod.id} >{mod.version}</NavLink></ul>)}
{sameMods.filter((m) => !m.isBeta).map(mod =>
<ul><NavLink state={mod.id} to= {"/mod/" + mod.id} >{mod.version}</NavLink></ul>)}
{sameMods.find((m) => m.isBeta) != null && <div><i>Beta versions:</i></div>}
{sameMods.find((m) => m.isBeta) != null &&
sameMods.filter((m) => m.isBeta).map(mod =>
<ul><NavLink state={mod.id} to= {"/mod/" + mod.id} >{mod.version}</NavLink></ul>)}
</div> </div>
) )
} }

View File

@ -1,296 +0,0 @@
import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import React, {useState} from "react";
import {withRouter} from "../core/withrouter";
import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace";
import {IUnit, IUnitResponse, IWeapon} from "../types/IUnit";
import '../css/Unit.css'
import {
Accordion,
AccordionDetails,
AccordionSummary,
Button,
ButtonGroup,
Grid2,
List,
ListItem,
ListSubheader,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
ToggleButton,
ToggleButtonClassKey,
ToggleButtonGroup,
Typography
} from "@mui/material";
import {ArrowBack, ArrowDropDown} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType";
import Weapon from "../classes/Weapon";
interface RacePageState {
mod: IMod,
modId: Number,
race: Irace,
units: IUnitResponse,
}
function Unit (unit: IUnit) {
const morale = (unit.moraleMax !== null) ? <span>
<img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/>&nbsp;{unit.moraleMax}
+{unit.moraleRegeneration}/s
{unit.moraleDeathPenalty > 0 && <span> <img style={{verticalAlign: "top"}} src="/images/Kills_icon.webp"/> -{unit.moraleDeathPenalty}</span>}
</span> : "-"
const detect = unit.detectRadius > 0 ? <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/> {unit.detectRadius}</span> :
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_NO.webp"/></span>
let mapWithUnitWeapons: Map<number, IWeapon[]> = new Map();
unit.weapons.forEach( weapon => {
const weaponList = mapWithUnitWeapons.get(weapon.hardpoint)
if(weaponList == null){
mapWithUnitWeapons.set(weapon.hardpoint, new Array(weapon.weapon))
} else {
weaponList.push(weapon.weapon)
}
})
function WeaponSlot (hardpoint: number) {
let weaponThisSlot = mapWithUnitWeapons.get(hardpoint) ?? []
let header = "Weapon slot " + hardpoint
return(
<div>
<h3>{header}</h3>
{weaponThisSlot.map(weapon => <div><Weapon weapon={weapon}/>
</div>)}
</div>
)
}
return (
<Accordion>
<AccordionSummary
expandIcon={<ArrowDropDown/>}
aria-controls="panel1-content"
id={unit.filename}
>
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>
<h2> {unit.name}</h2>
{unit.detectRadius > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>}
</AccordionSummary>
<AccordionDetails>
<Grid2 container spacing={2}>
<Grid2 size={4}>
<TableContainer component={Paper}>
<Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table">
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">cost</TableCell>
<TableCell component="th" scope="row">
{unit.buildCostRequisition > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{unit.buildCostRequisition}</span>}
{unit.buildCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{unit.buildCostPower}</span>}
{unit.capInfantry > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_cap_infantry.gif"/>&nbsp;
{unit.capInfantry}</span>}
{unit.capSupport > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_cap_vehicle.gif"/>&nbsp;
{unit.capSupport}</span>}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">armor type</TableCell>
<TableCell component="th" scope="row">
<ArmorType name={unit.armorType.name} withName={true}/>
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">health</TableCell>
<TableCell component="th" scope="row">
<span><img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp;
{unit.health} {unit.healthRegeneration > 0 &&
<span>+{unit.healthRegeneration}/s</span>} </span>
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">move speed</TableCell>
<TableCell component="th" scope="row">
{unit.moveSpeed}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">morale</TableCell>
<TableCell component="th" scope="row">
{morale}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">detect</TableCell>
<TableCell component="th" scope="row">
{detect}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid2>
<Grid2 size={8}>
<div style={{whiteSpace: "pre-wrap"}} >
{unit.description}
</div>
</Grid2>
<Grid2 size={12}>
{[...mapWithUnitWeapons.keys()].sort(function(a, b) {
return a - b;
}).map(h => WeaponSlot(h))}
</Grid2>
</Grid2>
<i>{unit.filename}</i>
</AccordionDetails>
</Accordion>)
}
interface UnitsState {
selectedUnits: String | null,
}
class Units extends React.Component<IUnitResponse, UnitsState> {
constructor(props: any) {
super(props);
this.state = ({
selectedUnits: 'infantry'
});
}
handleChange = (e: React.MouseEvent, newUnitType: String | null) => {
this.setState({
selectedUnits: newUnitType
});
}
render () {
let unitsToRender: IUnit[] = [];
if(this.state.selectedUnits === 'infantry'){
unitsToRender = this.props.units.filter(unit => unit.capInfantry > 0)
} else if (this.state.selectedUnits === 'tech'){
unitsToRender = this.props.units.filter(unit => unit.capSupport > 0)
} else if (this.state.selectedUnits === 'support'){
unitsToRender = this.props.units.filter(unit => unit.capSupport == 0 && unit.capInfantry == 0)
} else {
unitsToRender = this.props.units
}
if (this.state) {
return (<div>
<ToggleButtonGroup
color="primary"
exclusive
onChange={this.handleChange}
value={this.state.selectedUnits}
aria-label="Platform"
>
<ToggleButton value="infantry">Infantry</ToggleButton>
<ToggleButton value="tech">Tech</ToggleButton>
<ToggleButton value="support">Support</ToggleButton>
</ToggleButtonGroup>
<div id = 'filtered-units'>
{unitsToRender.map(unit => Unit(unit))}
</div>
</div>)
} else {
return "";
}
}
}
class RacePage extends React.Component<any, RacePageState> {
constructor(props: any) {
super(props);
console.log(this.props.match.params.id);
this.setState({
modId: this.props.match.params.id
});
}
async componentDidMount() {
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
const modData: IMod = await responseMod.json();
this.setState({
mod: modData
});
const response = await fetch(AvailableRacesPart + "/" + this.props.match.params.raceId);
const racesData: Irace = await response.json();
this.setState({
race : racesData
});
const unitsResponse = await fetch(AvailableUnits + "/" + this.state.mod.id + "/" + this.props.match.params.raceId);
const unitsData: IUnitResponse = await unitsResponse.json();
console.log(unitsData);
this.setState({
units : unitsData
});
}
render() {
if(this.state != null && this.state.units != null ){
const backRef = "/mod/" + this.state.mod.id
return <div><a href={backRef}><Button id="back-button" variant="contained" startIcon={<ArrowBack/>}> Back</Button></a>
<h1>{this.state.mod.name} ({this.state.mod.version}) - {this.state.race.name}</h1>
<Units race={this.state.race.name} units={this.state.units.units}/>
</div>;
} else {
return "loading...";
}
}
}
export default withRouter(RacePage);

163
src/pages/RacePageFast.tsx Normal file
View File

@ -0,0 +1,163 @@
import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import React, {useState} from "react";
import {withRouter} from "../core/withrouter";
import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace";
import '../css/Unit.css'
import {
Accordion,
AccordionDetails,
AccordionSummary,
Button,
ButtonGroup,
Grid2,
List,
ListItem,
ListSubheader,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
ToggleButton,
ToggleButtonClassKey,
ToggleButtonGroup,
Typography
} from "@mui/material";
import {ArrowBack, ArrowDropDown} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType";
import Weapon from "../classes/Weapon";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
interface RacePageState {
mod: IMod,
race: Irace,
units: IUnitShort[],
}
function Unit (unit: IUnitShort, modId: number, raceId: String) {
return (<a href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}><ListItem>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> }
{unit.name}
{unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>}
</ListItem></a>)
}
interface UnitsProps{
raceId: string,
modId: number,
}
interface UnitsState {
selectedUnits: String | null,
units: IRaceUnits | null,
}
class Units extends React.Component<UnitsProps, UnitsState> {
constructor(props: any) {
super(props);
let url = AvailableUnits + "/" + this.props.modId + "/" + this.props.raceId;
fetch(url)
.then(res => res.json())
.then((res: IRaceUnits) => {
this.setState({
units: res,
})
})
}
handleChange = (e: React.MouseEvent, newUnitType: String | null) => {
this.setState({
selectedUnits: newUnitType
});
}
render () {
if (this.state && this.state.units) {
return (<div>
{this.state.units != null ?
<Grid2 container spacing={2}>
<Grid2 size= {{xs: 12, md: 4}}>
<h3>Infantry</h3>
{this.state.units.infantry.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</Grid2>
<Grid2 size={{xs: 12, md: 4}}>
<h3>Tech</h3>
<List sx={{width: '100%', maxWidth: 360, bgcolor: 'background.paper'}}>
{this.state.units.tech.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</List>
</Grid2>
<Grid2 size={{xs: 12, md: 4}}>
<h3>Support</h3>
<List sx={{width: '100%', maxWidth: 360, bgcolor: 'background.paper'}}>
{this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</List>
</Grid2>
</Grid2>
: "Loading"}
</div>)
} else {
return "Loading...";
}
}
}
class RacePageFast extends React.Component<any, RacePageState> {
constructor(props: any) {
super(props);
console.log(this.props.match.params.id);
}
async componentDidMount() {
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
const modData: IMod = await responseMod.json();
this.setState({
mod: modData
});
const response = await fetch(AvailableRacesPart + "/" + this.props.match.params.raceId);
const racesData: Irace = await response.json();
this.setState({
race : racesData
});
}
render() {
if(this.state != null && this.state.mod != null && this.state.race != null ){
const backRef = "/mod/" + this.state.mod.id
return <div><a href={backRef}><Button id="back-button" variant="contained" startIcon={<ArrowBack/>}> Back</Button></a>
<h1>{this.state.mod.name} ({this.state.mod.version}) - {this.state.race.name}</h1>
<Units raceId={this.state.race.id} modId={this.state.mod.id}/>
</div>;
} else {
return "loading...";
}
}
}
export default withRouter(RacePageFast);

263
src/pages/UnitPage.tsx Normal file
View File

@ -0,0 +1,263 @@
import {AvailableMods, AvailableUnits, IconUrl} from "../core/api";
import React from "react";
import {withRouter} from "../core/withrouter";
import {IUnit, IWeapon} from "../types/IUnit";
import '../css/Unit.css'
import {
Accordion,
AccordionDetails,
AccordionSummary,
Button,
Grid2,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableRow
} from "@mui/material";
import {ArrowBack, ExpandMore} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType";
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
import Sergeant from "../classes/Sergeant";
import WeaponSlot from "../classes/WeaponSlot";
import UnitsTable from "../classes/UnitsTable";
import {IMod} from "../types/Imod";
interface UintPageState {
unit: IUnit,
mod: IMod,
}
function Unit(unit: IUnit, mod: IMod) {
const morale = (unit.moraleMax !== null) ? <span>
<img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/>&nbsp;{unit.moraleMax}
+{unit.moraleRegeneration}/s
{unit.moraleDeathPenalty > 0 && <span> <img style={{verticalAlign: "top"}}
src="/images/Kills_icon.webp"/> -{unit.moraleDeathPenalty}</span>}
</span> : "-"
const detect = unit.detectRadius > 0 ? <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/> {unit.detectRadius}</span> :
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_NO.webp"/></span>
let mapWithUnitWeapons: Map<number, Map<number, IWeapon>> = new Map();
unit.weapons.forEach(weapon => {
const weaponMap = mapWithUnitWeapons.get(weapon.hardpoint)
if (weaponMap == null) {
const weaponMap = new Map()
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
mapWithUnitWeapons.set(weapon.hardpoint, weaponMap)
} else {
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
}
})
type sergeantProps = {
name: string
icon: String
canDetect: Boolean
}
const SergeantShort = (props: sergeantProps) => {
return (
<span style={{fontSize: 20}}>
{props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')}/> }
&nbsp; {props.name}
{props.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>}
</span>
)
}
console.log(unit);
return (
<div>
<h1>{mod.name} ({mod.version})</h1>
<h2>{unit.icon &&
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>} {unit.name} </h2>
<Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}>
<Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table">
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">cost</TableCell>
<TableCell component="th" scope="row">
{unit.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{unit.buildCostRequisition}</span>}
{unit.buildCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{unit.buildCostPower}</span>}
{(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{unit.buildCostPopulation}</span>}
{unit.capInfantry > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_cap_infantry.gif"/>&nbsp;
{unit.capInfantry}</span>}
{unit.capSupport > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_cap_vehicle.gif"/>&nbsp;
{unit.capSupport}</span>}
{(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{verticalAlign: "top", fontSize: "18px"}}/>&nbsp;
{unit.buildCostTime}s</span>}
</TableCell>
</TableRow>
{(unit?.reinforceTime !== 0 && unit.reinforceTime !== null) &&
<TableRow>
<TableCell>reinforce cost</TableCell>
<TableCell>
{unit.reinforceCostRequisition && unit.reinforceCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{unit.reinforceCostRequisition}</span>}
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{unit.reinforceCostPower}</span>}
{(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{unit.reinforceCostPopulation}</span>}
{(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{verticalAlign: "top", fontSize: "18px"}}/>&nbsp;
{unit.reinforceTime}s</span>}
</TableCell>
</TableRow>
}
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">armor type</TableCell>
<TableCell component="th" scope="row">
<ArmorType name={unit.armorType.name}/>
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">health</TableCell>
<TableCell component="th" scope="row">
<span><img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp;
{unit.health} {unit.healthRegeneration > 0 &&
<span>+{unit.healthRegeneration}/s</span>} </span>
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">move speed</TableCell>
<TableCell component="th" scope="row">
{unit.moveSpeed}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">morale</TableCell>
<TableCell component="th" scope="row">
{morale}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">detect</TableCell>
<TableCell component="th" scope="row">
{detect}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid2>
<Grid2 size={8}>
<div style={{whiteSpace: "pre-wrap"}}>
{unit.description}
</div>
</Grid2>
<Grid2 size={12}>
{unit.sergeants.map(s =>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMore/>}
aria-controls="panel1-content"
id="panel1-header"
>
<SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0}/>
</AccordionSummary>
<AccordionDetails>
<Sergeant sergeant={s}/>
<hr/>
</AccordionDetails>
</Accordion>)}
</Grid2>
<Grid2 size={12}>
{[...mapWithUnitWeapons.keys()].sort(function (a, b) {
return a - b;
}).map(h => <WeaponSlot unitWeapons={mapWithUnitWeapons.get(h)} hardpoint={h}/>)}
</Grid2>
</Grid2>
<i className="rgdFrom">{unit.filename}</i>
<hr/>
<UnitsTable modId={mod.id}/>
</div>)
}
class UnitPage extends React.Component<any, UintPageState> {
async componentDidMount() {
const unitsResponse = await fetch(AvailableUnits + "/" + this.props.match.params.unitId);
const unitsData: IUnit = await unitsResponse.json();
this.setState({
unit: unitsData
});
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
const modData: IMod = await responseMod.json();
this.setState({
mod: modData
});
}
render() {
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
return <div><a href={backRef}><Button id="back-button" variant="contained"
startIcon={<ArrowBack/>}> Back</Button></a>
{Unit(this.state.unit, this.state.mod)}
</div>;
} else {
return "loading...";
}
}
}
export default withRouter(UnitPage);

View File

@ -16,6 +16,10 @@ export interface IUnit {
description: string description: string
buildCostRequisition: number buildCostRequisition: number
buildCostPower: number buildCostPower: number
buildCostPopulation: number
buildCostFaith: number
buildCostSouls: number
buildCostTime: number
capInfantry: number capInfantry: number
capSupport: number capSupport: number
squadStartSize: number squadStartSize: number
@ -34,9 +38,36 @@ export interface IUnit {
detectRadius: number detectRadius: number
reinforceCostRequisition?: number reinforceCostRequisition?: number
reinforceCostPower?: number reinforceCostPower?: number
reinforceCostPopulation ?: number
reinforceCostFaith ?: number
reinforceTime?: number reinforceTime?: number
icon: string icon: string
modId: number modId: number
sergeants: ISergeant[]
weapons: WeaponHardpoint[]
}
export interface ISergeant {
id: number
armorType: IArmorType
armorType2?: IArmorType
name: string
filename: string
description: string
buildCostRequisition: number
buildCostPower: number
buildCostPopulation: number
buildCostFaith: number
buildCostSouls: number
buildCostTime: number
health: number
healthRegeneration: number
moraleDeathPenalty: number
mass: number
upTime: number
sightRadius: number
detectRadius: number
icon: string
weapons: WeaponHardpoint[] weapons: WeaponHardpoint[]
} }
@ -44,11 +75,14 @@ export interface IUnit {
export interface WeaponHardpoint { export interface WeaponHardpoint {
weapon: IWeapon weapon: IWeapon
hardpoint: number hardpoint: number
hardpointOrder: number
} }
export interface IWeapon { export interface IWeapon {
id: number id: number
name: string name: string
filename: string
description: string
costRequisition: number costRequisition: number
costPower: number costPower: number
costTimeSeconds: number costTimeSeconds: number
@ -64,11 +98,13 @@ export interface IWeapon {
isMeleeWeapon: boolean isMeleeWeapon: boolean
canAttackAir: boolean canAttackAir: boolean
canAttackGround: boolean canAttackGround: boolean
haveEquipButton: boolean
icon: string
modId: number modId: number
weaponPiercings: WeaponPiercing[] weaponPiercings: IWeaponPiercing[]
} }
export interface WeaponPiercing { export interface IWeaponPiercing {
id: number id: number
armorType: IArmorType armorType: IArmorType
piercingValue: number piercingValue: number

16
src/types/IUnitShort.tsx Normal file
View File

@ -0,0 +1,16 @@
import {Irace} from "./Irace";
export interface IRaceUnits {
race: Irace
infantry: IUnitShort[]
tech: IUnitShort[]
support: IUnitShort[]
}
export interface IUnitShort {
name: string
icon: string
id: number
canDetect: boolean
armourTypeName: string
}