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
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)
- react (вперемешку классы и функции)
- typescript

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

View File

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

View File

@ -1,4 +1,5 @@
import React from "react";
import {Tooltip} from "@mui/material";
interface IArmorType{
name: string,
@ -11,8 +12,8 @@ class ArmorType extends React.Component<IArmorType, any> {
withName: false
};
renderArmorImage(armorType: String): string {
switch(armorType) {
renderArmorImage(armorTypeId: string): string {
switch(armorTypeId) {
case 'Infantry Low':
return '/images/ARM_Inf_Lo.webp';
case 'Infantry Medium':
@ -44,13 +45,14 @@ class ArmorType extends React.Component<IArmorType, any> {
case 'Demon High':
return '/images/ARM_Dmn_Hi.webp';
default:
return 'Unknown armor';
return armorTypeId;
}
}
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 {IWeapon, WeaponPiercing} from "../types/IUnit";
import {IWeapon, IWeaponPiercing} from "../types/IUnit";
import {
Grid2,
Paper,
@ -14,9 +14,11 @@ import {
} from "@mui/material";
import ArmorType from "./ArmorType";
import ArmorTypeNames from "../types/ArmorTypeValues";
import {IconUrl} from "../core/api";
interface IWeaponProps{
weapon: IWeapon,
isDefault: Boolean,
}
interface IWeaponState{
@ -25,6 +27,10 @@ interface IWeaponState{
class Weapon extends React.Component<IWeaponProps, any> {
public static defaultProps = {
isDefault: false
};
humanReadableName(unit: string) {
const firstUpper = String(unit).charAt(0).toUpperCase() + String(unit).slice(1);
return firstUpper.replaceAll('_', ' ').replace('.rgd', '');
@ -56,7 +62,7 @@ class Weapon extends React.Component<IWeaponProps, any> {
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 infMedPiercing = this.getPiercingK(ArmorTypeNames.InfantryMedium)
@ -77,6 +83,9 @@ class Weapon extends React.Component<IWeaponProps, any> {
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 maxDamage = damagePiercing * weapon.maxDamage
@ -101,18 +110,20 @@ class Weapon extends React.Component<IWeaponProps, any> {
[`&.${tableCellClasses.head}`]: {
backgroundColor: "rgb(234, 234, 234)",
color: theme.palette.common.white,
paddingLeft: 10
},
[`&.${tableCellClasses.body}`]: {
fontSize: 14,
fontSize: 13,
paddingLeft: 10,
},
}));
return (
<div>
<h4>{this.humanReadableName(weapon.name)}</h4>
<h4>{ weapon.name ? weapon.name : this.humanReadableName(weapon.filename)}</h4>
<Grid2 container spacing={2}>
<Grid2 size={3}>
<Grid2 size= {{xs: 12, md: 3}}>
<TableContainer component={Paper}>
<Table size="small" aria-label="a dense table">
<TableBody>
@ -163,11 +174,37 @@ class Weapon extends React.Component<IWeaponProps, any> {
scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(2) : "-"}
</TableCell>
</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>
</Table>
</TableContainer>
</Grid2>
<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
color="primary"
exclusive
@ -179,57 +216,62 @@ class Weapon extends React.Component<IWeaponProps, any> {
<ToggleButton size="small" value="one hit">One hit average damage</ToggleButton>
</ToggleButtonGroup>
<TableContainer>
<Table sx={{minWidth: 600, marginTop: "10px"}} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<StyledTableCell ><ArmorType name={ArmorTypeNames.InfantryLow}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.InfantryMedium}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.InfantryHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.InfantryHeavyMedium}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.InfantryHeavyHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.DemonMedium}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.DemonHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.Commander}/></StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infHeavyMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infHeavyHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(demonPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(demonHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(commanderPiercing)}</StyledTableCell>
</TableRow>
</TableBody>
<TableHead>
<TableRow >
<StyledTableCell><ArmorType name={ArmorTypeNames.Air}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.VehicleLow}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.VehicleMedium}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.VehicleHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.BuildingLow}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.BuildingMedium}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/></StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<StyledTableCell>{getTotalDamage(airPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(buildingLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(buildingMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(buildingHighPiercing)}</StyledTableCell>
<StyledTableCell>{getMoraleDamage()}</StyledTableCell>
</TableRow>
</TableBody>
</Table>
<Table sx={{marginTop: "10px"}} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<StyledTableCell><ArmorType
name={ArmorTypeNames.InfantryLow}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.InfantryMedium}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.InfantryHigh}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.InfantryHeavyMedium}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.InfantryHeavyHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.Commander}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.DemonMedium}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.DemonHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.Air}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.VehicleLow}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.VehicleMedium}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.VehicleHigh}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.BuildingLow}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.BuildingMedium}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/></StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infHeavyMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(infHeavyHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(commanderPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(demonPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(demonHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(airPiercing, true)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(buildingLowPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(buildingMedPiercing)}</StyledTableCell>
<StyledTableCell>{getTotalDamage(buildingHighPiercing)}</StyledTableCell>
<StyledTableCell>{getMoraleDamage()}</StyledTableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<i className="rgdFrom">{weapon.filename}</i>
</Grid2>
</Grid2>
</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{
width: 64px;
width: 50px;
}
.sergeantIcon{
padding: 0px;
width: 40px;
}
.unitHeader{
@ -16,6 +21,6 @@
}
#filtered-units{
margin-top: 10px;
.rgdFrom{
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 {NavLink, useLocation, useParams} from "react-router-dom";
import {withRouter} from "../core/withrouter";
import {IMod} from "../types/Imod";
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 {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import UnitsTable from "../classes/UnitsTable";
interface ModPageState {
mod: IMod,
races: Irace[],
modId: Number,
}
class ModPage extends React.Component<any, ModPageState> {
constructor(props: any) {
super(props);
console.log(this.props.match.params.id);
this.setState({
modId : this.props.match.params.id
});
fetch(AvailableMods + "/" + this.props.match.params.modId)
.then(res => res.json())
.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() {
if(this.state != null && this.state.races != null ){
if(this.state != null && this.state.mod != null ){
return <div>
<a href="/"><Button id="back-button" variant="contained"
startIcon={<ArrowBack/>}> Back</Button></a>
<h1>{this.state.mod.name} ({this.state.mod.version})</h1>
{this.state.races.map(race => <ul><NavLink state={race.id}
to={"/mod/" + this.state.mod.id + "/race/" + race.id}>{race.name}</NavLink>
</ul>)}
<UnitsTable modId={this.state.mod.id}/>
</div>;
} else {
return "";
@ -59,4 +47,6 @@ class ModPage extends React.Component<any, ModPageState> {
}
}
export default withRouter(ModPage);

View File

@ -24,7 +24,13 @@ function Mods (mods: IMod[]) {
return(
<div>
<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>
)
}

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
buildCostRequisition: number
buildCostPower: number
buildCostPopulation: number
buildCostFaith: number
buildCostSouls: number
buildCostTime: number
capInfantry: number
capSupport: number
squadStartSize: number
@ -34,9 +38,36 @@ export interface IUnit {
detectRadius: number
reinforceCostRequisition?: number
reinforceCostPower?: number
reinforceCostPopulation ?: number
reinforceCostFaith ?: number
reinforceTime?: number
icon: string
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[]
}
@ -44,11 +75,14 @@ export interface IUnit {
export interface WeaponHardpoint {
weapon: IWeapon
hardpoint: number
hardpointOrder: number
}
export interface IWeapon {
id: number
name: string
filename: string
description: string
costRequisition: number
costPower: number
costTimeSeconds: number
@ -64,11 +98,13 @@ export interface IWeapon {
isMeleeWeapon: boolean
canAttackAir: boolean
canAttackGround: boolean
haveEquipButton: boolean
icon: string
modId: number
weaponPiercings: WeaponPiercing[]
weaponPiercings: IWeaponPiercing[]
}
export interface WeaponPiercing {
export interface IWeaponPiercing {
id: number
armorType: IArmorType
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
}