- Add building page

- Rework weapon ui (hide to spoiler and adapt to mobile)
- Add additional weapon params (throw force, min radius, damage radius etc)
- Add squad start/max sise, unit mass, souls and faith to cost
This commit is contained in:
Anibus 2025-04-18 14:06:26 +03:00
parent 3e78fa3807
commit 5751cec4de
15 changed files with 576 additions and 211 deletions

3
.env
View File

@ -1 +1,2 @@
REACT_APP_HOST_URL=http://localhost:8082
REACT_APP_HOST_URL=http://wiki-backend.dawn-of-war.pro
#REACT_APP_HOST_URL=http://localhost:8082

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

View File

@ -3,6 +3,7 @@ import ModsPage from "./pages/ModsPage";
import ModPage from "./pages/ModPage";
import RacePageFast from "./pages/RacePageFast";
import UnitPage from "./pages/UnitPage";
import BuildingPage from "./pages/BuildingPage";
export const MyRoutes = () => {
@ -13,6 +14,7 @@ export const MyRoutes = () => {
<Route path="/mod/:modId" element={<ModPage/>}/>
<Route path="/mod/:modId/race/:raceId" element={<RacePageFast/>}/>
<Route path="/mod/:modId/race/:raceId/unit/:unitId" element={<UnitPage/>}/>
<Route path="/mod/:modId/race/:raceId/building/:buildingId" element={<BuildingPage/>}/>
</Routes>
</BrowserRouter>
)

View File

@ -60,13 +60,19 @@ const Sergeant = (props: SergeantProps) => {
{sergeant.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{sergeant.buildCostRequisition}</span>}
{sergeant.buildCostRequisition.toFixed(0)}</span>}
{sergeant.buildCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{sergeant.buildCostPower}</span>}
{sergeant.buildCostPower.toFixed(0)}</span>}
{(sergeant.buildCostPopulation !== undefined && sergeant.buildCostPopulation > 0) && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{sergeant.buildCostPopulation}</span>}
{sergeant.buildCostPopulation.toFixed(0)}</span>}
{(sergeant.buildCostFaith !== undefined && sergeant.buildCostFaith > 0) && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{sergeant.buildCostFaith}</span>}
{(sergeant.buildCostSouls !== undefined && sergeant.buildCostSouls > 0) && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{sergeant.buildCostSouls.toFixed(0)}</span>}
{(sergeant.buildCostTime !== undefined && sergeant.buildCostTime > 0) && <span>&nbsp;<AvTimerOutlinedIcon style={{verticalAlign: "top", fontSize:"18px"}} />&nbsp;
{sergeant.buildCostTime}s</span>}

View File

@ -1,18 +1,21 @@
import React, {useEffect, useState} from "react";
import {AvailableUnits} from "../core/api";
import {AvailableBuildings, 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";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
interface IUnitsTable {
racesUnits: IRaceUnits[];
racesUnits: IRaceUnits[]
racesBuildings: IRaceBuildings[]
}
export default function UnitsTable(prop: {modId: number}) {
const [unitsTable, setUnitsTable] = useState<IUnitsTable>({
racesUnits: []
racesUnits: [],
racesBuildings: []
});
@ -22,13 +25,25 @@ export default function UnitsTable(prop: {modId: number}) {
</a><br/></span>
}
function getBuildingRef(modId: number, raceId: string, building: IBuildingShort) {
return <span style={{fontSize: 11}}><a href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}>
{building.name}
<br/></a></span>
}
useEffect(() => {
fetch(AvailableUnits + "/mod/" + prop.modId)
fetch(AvailableBuildings + "/mod/" + prop.modId)
.then(res => res.json())
.then((res: IRaceUnits[]) => {
setUnitsTable({
racesUnits : res
});
.then((resBuildings: IRaceBuildings[]) => {
fetch(AvailableUnits + "/mod/" + prop.modId)
.then(res => res.json())
.then((res: IRaceUnits[]) => {
setUnitsTable({
racesUnits : res,
racesBuildings: resBuildings
});
})
})
}, []);
@ -77,7 +92,7 @@ export default function UnitsTable(prop: {modId: number}) {
)
}
function generateRaceUnitTable(racesUnitsPart: IRaceUnits[]){
function generateRaceUnitTable(racesUnitsPart: IRaceUnits[], racesBuildings: IRaceBuildings[]){
return (<TableContainer>
{<Table sx={{minWidth: 650}} size="small" aria-label="a dense table">
<TableHead>
@ -91,7 +106,7 @@ export default function UnitsTable(prop: {modId: number}) {
<TableRow style={{verticalAlign: 'top'}}>
{racesUnitsPart.map(raceUnits =>
<TableCell>
{getUnitsRef(raceUnits.infantry, raceUnits.race.id)}
{getUnitsRef(raceUnits.infantry.concat(raceUnits.support), raceUnits.race.id)}
</TableCell>)
}
</TableRow>
@ -103,9 +118,9 @@ export default function UnitsTable(prop: {modId: number}) {
}
</TableRow>
<TableRow style={{verticalAlign: 'top'}}>
{racesUnitsPart.map(raceUnits =>
{racesBuildings.map(raceBuilding =>
<TableCell>
{getUnitsRef(raceUnits.support, raceUnits.race.id)}
{raceBuilding.buildings.map(building => getBuildingRef(prop.modId, raceBuilding.race.id, building))}
</TableCell>)
}
</TableRow>
@ -114,10 +129,20 @@ export default function UnitsTable(prop: {modId: number}) {
</TableContainer>)
}
function generateRaceUnitTables(racesUnits: IRaceUnits[], raceCount: number){
function generateRaceUnitTables(racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[], raceCount: number){
var elements: JSX.Element[] = []
racesBuildings = racesBuildings.reverse()
for(let i = 0; i < racesUnits.length;i = i + raceCount){
elements.push(generateRaceUnitTable(racesUnits.slice(i, i + raceCount)))
var unitsThisTable = racesUnits.slice(i, i + raceCount)
var buildingsThisTable: IRaceBuildings[] = []
unitsThisTable.forEach(ut => {
var buildings = racesBuildings.find(rb => rb.race.id === ut.race.id)
if (buildings != null) {
buildingsThisTable.push(buildings)
}
})
elements.push(generateRaceUnitTable(unitsThisTable, buildingsThisTable))
}
return elements
}
@ -126,7 +151,7 @@ export default function UnitsTable(prop: {modId: number}) {
return (
<Grid2>
<Grid2 display={{ lg: 'block', xs: 'none' }} >
{generateRaceUnitTables(unitsTable.racesUnits, 9)}
{generateRaceUnitTables(unitsTable.racesUnits, unitsTable.racesBuildings, 5)}
</Grid2>
<Grid2 display={{ lg: 'none', xs: 'block' }}>
{unitsTable.racesUnits.map(raceUnits => <h4><NavLink state={raceUnits.race.id}

View File

@ -1,6 +1,9 @@
import React from "react";
import {IWeapon, IWeaponPiercing} from "../types/IUnit";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Grid2,
Paper,
styled,
@ -15,13 +18,15 @@ import {
import ArmorType from "./ArmorType";
import ArmorTypeNames from "../types/ArmorTypeValues";
import {IconUrl} from "../core/api";
import {ExpandMore} from "@mui/icons-material";
import Sergeant from "./Sergeant";
interface IWeaponProps{
interface IWeaponProps {
weapon: IWeapon,
isDefault: Boolean,
}
interface IWeaponState{
interface IWeaponState {
currentTable: "dps" | "one hit",
}
@ -45,24 +50,23 @@ class Weapon extends React.Component<IWeaponProps, any> {
getPiercingK(armorType: string): number {
const weaponPiercing = this.props.weapon.weaponPiercings.find((p) => p.armorType.name === armorType)
return (typeof weaponPiercing !== "undefined" ? weaponPiercing.piercingValue : 10 ) / 100
const weaponPiercing = this.props.weapon.weaponArmorPiercing.find((p) => p.armorType.name === armorType)
return (typeof weaponPiercing !== "undefined" ? weaponPiercing.piercingValue : 10) / 100
}
handleChange = (e: React.MouseEvent, selectedDamageTable: String | null) => {
handleChange = (e: React.MouseEvent, selectedDamageTable: String | null) => {
this.setState({
currentTable: selectedDamageTable
});
}
render() {
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)
@ -83,16 +87,16 @@ 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 ""
if (!isAir && !weapon.canAttackGround && !weapon.isMeleeWeapon) return ""
if (isAir && !weapon.canAttackAir) return ""
var minDamage = damagePiercing * weapon.minDamage
var maxDamage = damagePiercing * weapon.maxDamage
if(minDamage < weapon.minDamageValue){
if (minDamage < weapon.minDamageValue) {
minDamage = weapon.minDamageValue
}
if(maxDamage < weapon.minDamageValue){
if (maxDamage < weapon.minDamageValue) {
maxDamage = weapon.minDamageValue
}
@ -106,7 +110,7 @@ class Weapon extends React.Component<IWeaponProps, any> {
}
const StyledTableCell = styled(TableCell)(({ theme }) => ({
const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: "rgb(234, 234, 234)",
color: theme.palette.common.white,
@ -119,162 +123,213 @@ class Weapon extends React.Component<IWeaponProps, any> {
}));
return (
<div>
<h4>{ weapon.name ? weapon.name : this.humanReadableName(weapon.filename)}</h4>
<Grid2 container spacing={2}>
<div style={{marginBottom: 10}}><Accordion>
<AccordionSummary
expandIcon={<ExpandMore/>}
aria-controls="panel1-content"
id="panel1-header"
>
<span style={{fontSize: 18}}> {!this.props.isDefault && weapon.icon && weapon.haveEquipButton ?
<img className="weaponIcon" src={IconUrl + weapon.icon.replaceAll('\\', '/')}/> : (
weapon.isMeleeWeapon ?
<img className="weaponIcon" src="/images/MeleeStance_icon_bw.jpg"/> :
<img className="weaponIcon" src="/images/RangedStance_icon_bw.jpg"/>
)} {weapon.name ? weapon.name : this.humanReadableName(weapon.filename)} </span>
</AccordionSummary>
<AccordionDetails>
<Grid2 container spacing={2}>
<Grid2 size={12}>
<div style={{whiteSpace: "pre-wrap"}}>
<span
className="weaponDescription">{this.props.isDefault ? "Default weapon" : weapon.description}</span>
</div>
</Grid2>
<Grid2 size= {{xs: 12, md: 3}}>
<TableContainer component={Paper}>
<Table size="small" aria-label="a dense table">
<TableBody>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Base damage</TableCell>
<TableCell component="th"
scope="row">{weapon.minDamage} {weapon.maxDamage !== weapon.minDamage && -weapon.maxDamage}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Accuracy</TableCell>
<TableCell component="th" scope="row">{weapon.accuracy.toFixed(2)}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Accuracy moving</TableCell>
<TableCell component="th"
scope="row">{weapon.setupTime === 0 ? (weapon.accuracy - weapon.accuracyReductionMoving).toFixed(2) : "-"}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Reload time</TableCell>
<TableCell component="th"
scope="row">{weapon.reloadTime}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Max range</TableCell>
<TableCell component="th"
scope="row">{weapon.maxRange ? (weapon.maxRange) : "-"}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Setup time</TableCell>
<TableCell component="th"
scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(2) : "-"}
</TableCell>
</TableRow>
{(weapon.costRequisition > 0 || weapon.costPower > 0) &&
<Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}>
<Table size="small" aria-label="a dense table">
{/*<TableHead><TableRow><TableCell component="th" scope="row"><b>Damage</b></TableCell><TableCell/></TableRow></TableHead>*/}
<TableBody>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Base damage</TableCell>
<TableCell component="th"
scope="row">{weapon.minDamage} {weapon.maxDamage !== weapon.minDamage && -weapon.maxDamage}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Accuracy</TableCell>
<TableCell component="th"
scope="row">{weapon.accuracy.toFixed(3).replace(/[,.]?0+$/, '')}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Accuracy moving</TableCell>
<TableCell component="th"
scope="row">{weapon.setupTime === 0 && weapon.accuracy - weapon.accuracyReductionMoving > 0 ? (weapon.accuracy - weapon.accuracyReductionMoving).toFixed(3).replace(/[,.]?0+$/, '') : "-"}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid2>
<Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}>
<Table size="small" aria-label="a dense table">
<TableBody>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Reload time</TableCell>
<TableCell component="th"
scope="row">{weapon.reloadTime}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Range</TableCell>
<TableCell component="th"
scope="row">{weapon.maxRange ? (weapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}
</TableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Damage radius</TableCell>
<TableCell component="th"
scope="row">{weapon.damageRadius ? (weapon.damageRadius) : "-"}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid2>
<Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}>
<Table size="small" aria-label="a dense table">
<TableBody>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Cost</TableCell>
{(weapon.costRequisition > 0 || weapon.costPower > 0) ?
<TableCell component="th"
scope="row">
{weapon.costRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{weapon.costRequisition.toFixed(0)}</span>}
{weapon.costPower > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{weapon.costPower.toFixed(0)}</span>}
</TableCell> : <TableCell component="th" scope="row"> - </TableCell>
}
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Setup time</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>}
scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(3).replace(/[,.]?0+$/, '') : "-"}
</TableCell>
</TableRow>
}
</TableBody>
</Table>
</TableContainer>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Throw force</TableCell>
<TableCell component="th"
scope="row">{weapon.throwForceMin != 0 ? weapon.throwForceMin + " - " + weapon.throwForceMax : "-"}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid2>
<Grid2 size={12}>
<ToggleButtonGroup
color="primary"
exclusive
value={this.state.currentTable}
onChange={this.handleChange}
aria-label="Platform"
>
<ToggleButton size="small" value="dps">Dps</ToggleButton>
<ToggleButton size="small" value="one hit">One hit average damage</ToggleButton>
</ToggleButtonGroup>
<TableContainer>
<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>
<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
value={this.state.currentTable}
onChange={this.handleChange}
aria-label="Platform"
>
<ToggleButton size="small" value="dps">Dps</ToggleButton>
<ToggleButton size="small" value="one hit">One hit average damage</ToggleButton>
</ToggleButtonGroup>
<TableContainer>
<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>
</AccordionDetails>
</Accordion></div>
);
}
}

View File

@ -3,4 +3,5 @@ export const UserUrl = process.env.REACT_APP_HOST_URL + '/api/v1/user';
export const AvailableMods = process.env.REACT_APP_HOST_URL + '/api/v1/mods';
export const AvailableRacesPart = process.env.REACT_APP_HOST_URL + '/api/v1/races';
export const AvailableUnits = process.env.REACT_APP_HOST_URL + '/api/v1/units';
export const AvailableBuildings = process.env.REACT_APP_HOST_URL + '/api/v1/buildings';
export const IconUrl = process.env.REACT_APP_HOST_URL + '/api/v1/grapics/icon/';

View File

@ -2,6 +2,10 @@
width: 50px;
}
.weaponIcon{
width: 40px;
}
.sergeantIcon{
padding: 0px;
width: 40px;

173
src/pages/BuildingPage.tsx Normal file
View File

@ -0,0 +1,173 @@
import {AvailableBuildings, AvailableMods, AvailableUnits, IconUrl} from "../core/api";
import React from "react";
import {withRouter} from "../core/withrouter";
import {IWeapon} from "../types/IUnit";
import '../css/Unit.css'
import {Button, Grid2, Paper, Table, TableBody, TableCell, TableContainer, TableRow} from "@mui/material";
import {ArrowBack} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType";
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
import WeaponSlot from "../classes/WeaponSlot";
import UnitsTable from "../classes/UnitsTable";
import {IMod} from "../types/Imod";
import {IBuilding} from "../types/IBuilding";
interface UintPageState {
building: IBuilding,
mod: IMod,
}
function Building(building: IBuilding, mod: IMod) {
const detect = building.detectRadius > 0 ? <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/> {building.detectRadius}</span> :
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_NO.webp"/></span>
let mapBuildingWeapons: Map<number, Map<number, IWeapon>> = new Map();
building.weapons.forEach(weapon => {
const weaponMap = mapBuildingWeapons.get(weapon.hardpoint)
if (weaponMap == null) {
const weaponMap = new Map()
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
mapBuildingWeapons.set(weapon.hardpoint, weaponMap)
} else {
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
}
})
return (
<div>
<h1>{mod.name} ({mod.version})</h1>
<h2>{building.icon &&
<img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>} {building.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">
{building.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{building.buildCostRequisition.toFixed(0)}</span>}
{building.buildCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{building.buildCostPower.toFixed(0)}</span>}
{(building.buildCostPopulation !== undefined && building.buildCostPopulation > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{building.buildCostPopulation.toFixed(0)}</span>}
{(building.buildCostFaith !== undefined && building.buildCostFaith > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_faith.gif"/>&nbsp;
{building.buildCostFaith}</span>}
{(building.buildCostSouls !== undefined && building.buildCostSouls > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_souls.gif"/>&nbsp;
{building.buildCostSouls.toFixed(0)}</span>}
{(building.buildCostTime !== undefined && building.buildCostTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{verticalAlign: "top", fontSize: "18px"}}/>&nbsp;
{building.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={building.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;
{building.health} {building.healthRegeneration > 0 &&
<span>+{building.healthRegeneration}/s</span>} </span>
</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>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">Repair max</TableCell>
<TableCell component="th" scope="row">
{building.repairMax}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid2>
<Grid2 size={8}>
<div style={{whiteSpace: "pre-wrap"}}>
{building.description}
</div>
</Grid2>
<Grid2 size={12}>
{[...mapBuildingWeapons.keys()].sort(function (a, b) {
return a - b;
}).map(h => <WeaponSlot unitWeapons={mapBuildingWeapons.get(h)} hardpoint={h}/>)}
</Grid2>
</Grid2>
<i className="rgdFrom">{building.filename}</i>
<hr/>
<UnitsTable modId={mod.id}/>
</div>)
}
class BuildingPage extends React.Component<any, UintPageState> {
async componentDidMount() {
const buildingResponse = await fetch(AvailableBuildings + "/" + this.props.match.params.buildingId);
const buildingData: IBuilding = await buildingResponse.json();
this.setState({
building: buildingData
});
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.building != 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>
{Building(this.state.building, this.state.mod)}
</div>;
} else {
return "loading...";
}
}
}
export default withRouter(BuildingPage);

View File

@ -1,4 +1,4 @@
import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import {AvailableBuildings, AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import React, {useState} from "react";
import {withRouter} from "../core/withrouter";
import {IMod} from "../types/Imod";
@ -30,6 +30,7 @@ import {ArrowBack, ArrowDropDown} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType";
import Weapon from "../classes/Weapon";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
interface RacePageState {
mod: IMod,
@ -49,14 +50,26 @@ function Unit (unit: IUnitShort, modId: number, raceId: String) {
</ListItem></a>)
}
interface UnitsProps{
raceId: string,
modId: number,
}
function Building (building: IBuildingShort, modId: number, raceId: String) {
return (<a href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}><ListItem>
{building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
{building.name}
{building.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,
buildings: IRaceBuildings | null,
}
class Units extends React.Component<UnitsProps, UnitsState> {
@ -64,26 +77,28 @@ class Units extends React.Component<UnitsProps, UnitsState> {
constructor(props: any) {
super(props);
let url = AvailableUnits + "/" + this.props.modId + "/" + this.props.raceId;
let urlUnits = AvailableUnits + "/" + this.props.modId + "/" + this.props.raceId;
fetch(url)
fetch(urlUnits)
.then(res => res.json())
.then((res: IRaceUnits) => {
this.setState({
units: res,
})
})
let urlBuildings = AvailableBuildings + "/" + this.props.modId + "/" + this.props.raceId;
fetch(urlBuildings)
.then(res => res.json())
.then((res: IRaceBuildings) => {
this.setState({
buildings: res,
})
})
}
handleChange = (e: React.MouseEvent, newUnitType: String | null) => {
this.setState({
selectedUnits: newUnitType
});
}
render () {
@ -91,8 +106,7 @@ class Units extends React.Component<UnitsProps, UnitsState> {
return (<div>
{this.state.units != null ?
{this.state.units != null && this.state.buildings != null ?
<Grid2 container spacing={2}>
<Grid2 size= {{xs: 12, md: 4}}>
<h3>Infantry</h3>
@ -110,6 +124,12 @@ class Units extends React.Component<UnitsProps, UnitsState> {
{this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</List>
</Grid2>
<Grid2 size={{xs: 12, md: 4}}>
<h3>Buildings</h3>
<List sx={{width: '100%', maxWidth: 360, bgcolor: 'background.paper'}}>
{this.state.buildings.buildings.map(building => Building(building, this.props.modId, this.props.raceId))}
</List>
</Grid2>
</Grid2>
: "Loading"}

View File

@ -95,19 +95,27 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">cost</TableCell>
<TableCell component="th" scope="row">Cost</TableCell>
<TableCell component="th" scope="row">
{unit.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{unit.buildCostRequisition}</span>}
{unit.buildCostRequisition.toFixed(0)}</span>}
{unit.buildCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{unit.buildCostPower}</span>}
{unit.buildCostPower.toFixed(0)}</span>}
{(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{unit.buildCostPopulation}</span>}
{unit.buildCostPopulation.toFixed(0)}</span>}
{(unit.buildCostFaith !== undefined && unit.buildCostFaith > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_faith.gif"/>&nbsp;
{unit.buildCostFaith}</span>}
{(unit.buildCostSouls !== undefined && unit.buildCostSouls > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_souls.gif"/>&nbsp;
{unit.buildCostSouls.toFixed(0)}</span>}
{unit.capInfantry > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_cap_infantry.gif"/>&nbsp;
@ -121,22 +129,30 @@ function Unit(unit: IUnit, mod: IMod) {
{unit.buildCostTime}s</span>}
</TableCell>
</TableRow>
{(unit?.reinforceTime !== 0 && unit.reinforceTime !== null) &&
{(unit?.reinforceTime !== 0 && unit.reinforceTime !== null && unit.squadMaxSize > 1) &&
<TableRow>
<TableCell>reinforce cost</TableCell>
<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.reinforceCostRequisition.toFixed(0)}</span>}
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{unit.reinforceCostPower}</span>}
{unit.reinforceCostPower.toFixed(0)}</span>}
{(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{unit.reinforceCostPopulation}</span>}
{unit.reinforceCostPopulation.toFixed(0)}</span>}
{(unit.reinforceCostFaith !== undefined && unit.reinforceCostFaith > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_faith.gif"/>&nbsp;
{unit.reinforceCostFaith}</span>}
{(unit.reinforceCostSouls !== undefined && unit.reinforceCostSouls > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_souls.gif"/>&nbsp;
{unit.reinforceCostSouls.toFixed(0)}</span>}
{(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{verticalAlign: "top", fontSize: "18px"}}/>&nbsp;
@ -144,10 +160,18 @@ function Unit(unit: IUnit, mod: IMod) {
</TableCell>
</TableRow>
}
{unit.squadMaxSize > 1 &&
<TableRow>
<TableCell>Squad size</TableCell>
<TableCell>
{unit.squadStartSize} / {unit.squadMaxSize}
</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">Armor type</TableCell>
<TableCell component="th" scope="row">
<ArmorType name={unit.armorType.name}/>
</TableCell>
@ -155,7 +179,7 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">health</TableCell>
<TableCell component="th" scope="row">Health</TableCell>
<TableCell component="th" scope="row">
<span><img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp;
@ -166,7 +190,7 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">move speed</TableCell>
<TableCell component="th" scope="row">Move speed</TableCell>
<TableCell component="th" scope="row">
{unit.moveSpeed}
</TableCell>
@ -174,7 +198,7 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">morale</TableCell>
<TableCell component="th" scope="row">Morale</TableCell>
<TableCell component="th" scope="row">
{morale}
</TableCell>
@ -182,7 +206,15 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row">detect</TableCell>
<TableCell component="th" scope="row">Mass</TableCell>
<TableCell component="th" scope="row">
{unit.mass}
</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>

28
src/types/IBuilding.tsx Normal file
View File

@ -0,0 +1,28 @@
import {Irace} from "./Irace";
import {IArmorType} from "./IArmorType";
import {WeaponHardpoint} from "./IUnit";
export interface IBuilding {
id: number
race: Irace
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
sightRadius: number
detectRadius: number
repairMax: number
icon: string
modId: number
weapons: WeaponHardpoint[]
}

View File

@ -0,0 +1,14 @@
import {Irace} from "./Irace";
export interface IRaceBuildings {
race: Irace
buildings: IBuildingShort[]
}
export interface IBuildingShort {
name: string
icon: string
id: number
canDetect: boolean
armourTypeName: string
}

View File

@ -40,6 +40,7 @@ export interface IUnit {
reinforceCostPower?: number
reinforceCostPopulation ?: number
reinforceCostFaith ?: number
reinforceCostSouls ?: number
reinforceTime?: number
icon: string
modId: number
@ -90,22 +91,25 @@ export interface IWeapon {
reloadTime: number
setupTime: number
accuracyReductionMoving: number
minRange: number
maxRange: number
minDamage: number
maxDamage: number
minDamageValue: number
moraleDamage: number
damageRadius: number
throwForceMin: number
throwForceMax: number
isMeleeWeapon: boolean
canAttackAir: boolean
canAttackGround: boolean
haveEquipButton: boolean
icon: string
modId: number
weaponPiercings: IWeaponPiercing[]
weaponArmorPiercing: IWeaponPiercing[]
}
export interface IWeaponPiercing {
id: number
armorType: IArmorType
piercingValue: number
}