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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
import React from "react"; import React from "react";
import {IWeapon, IWeaponPiercing} from "../types/IUnit"; import {IWeapon, IWeaponPiercing} from "../types/IUnit";
import { import {
Accordion,
AccordionDetails,
AccordionSummary,
Grid2, Grid2,
Paper, Paper,
styled, styled,
@ -15,13 +18,15 @@ import {
import ArmorType from "./ArmorType"; import ArmorType from "./ArmorType";
import ArmorTypeNames from "../types/ArmorTypeValues"; import ArmorTypeNames from "../types/ArmorTypeValues";
import {IconUrl} from "../core/api"; import {IconUrl} from "../core/api";
import {ExpandMore} from "@mui/icons-material";
import Sergeant from "./Sergeant";
interface IWeaponProps{ interface IWeaponProps {
weapon: IWeapon, weapon: IWeapon,
isDefault: Boolean, isDefault: Boolean,
} }
interface IWeaponState{ interface IWeaponState {
currentTable: "dps" | "one hit", currentTable: "dps" | "one hit",
} }
@ -45,8 +50,8 @@ class Weapon extends React.Component<IWeaponProps, any> {
getPiercingK(armorType: string): number { getPiercingK(armorType: string): number {
const weaponPiercing = this.props.weapon.weaponPiercings.find((p) => p.armorType.name === armorType) const weaponPiercing = this.props.weapon.weaponArmorPiercing.find((p) => p.armorType.name === armorType)
return (typeof weaponPiercing !== "undefined" ? weaponPiercing.piercingValue : 10 ) / 100 return (typeof weaponPiercing !== "undefined" ? weaponPiercing.piercingValue : 10) / 100
} }
handleChange = (e: React.MouseEvent, selectedDamageTable: String | null) => { handleChange = (e: React.MouseEvent, selectedDamageTable: String | null) => {
@ -56,13 +61,12 @@ class Weapon extends React.Component<IWeaponProps, any> {
} }
render() { render() {
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)
@ -83,16 +87,16 @@ 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.canAttackGround && !weapon.isMeleeWeapon) return ""
if(isAir && !weapon.canAttackAir) 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
if(minDamage < weapon.minDamageValue){ if (minDamage < weapon.minDamageValue) {
minDamage = weapon.minDamageValue minDamage = weapon.minDamageValue
} }
if(maxDamage < weapon.minDamageValue){ if (maxDamage < weapon.minDamageValue) {
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}`]: { [`&.${tableCellClasses.head}`]: {
backgroundColor: "rgb(234, 234, 234)", backgroundColor: "rgb(234, 234, 234)",
color: theme.palette.common.white, color: theme.palette.common.white,
@ -119,13 +123,32 @@ class Weapon extends React.Component<IWeaponProps, any> {
})); }));
return ( return (
<div> <div style={{marginBottom: 10}}><Accordion>
<h4>{ weapon.name ? weapon.name : this.humanReadableName(weapon.filename)}</h4> <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 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}}> <Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table">
{/*<TableHead><TableRow><TableCell component="th" scope="row"><b>Damage</b></TableCell><TableCell/></TableRow></TableHead>*/}
<TableBody> <TableBody>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
@ -139,7 +162,8 @@ class Weapon extends React.Component<IWeaponProps, any> {
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">Accuracy</TableCell> <TableCell component="th" scope="row">Accuracy</TableCell>
<TableCell component="th" scope="row">{weapon.accuracy.toFixed(2)} <TableCell component="th"
scope="row">{weapon.accuracy.toFixed(3).replace(/[,.]?0+$/, '')}
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow <TableRow
@ -147,9 +171,18 @@ class Weapon extends React.Component<IWeaponProps, any> {
> >
<TableCell component="th" scope="row">Accuracy moving</TableCell> <TableCell component="th" scope="row">Accuracy moving</TableCell>
<TableCell component="th" <TableCell component="th"
scope="row">{weapon.setupTime === 0 ? (weapon.accuracy - weapon.accuracyReductionMoving).toFixed(2) : "-"} scope="row">{weapon.setupTime === 0 && weapon.accuracy - weapon.accuracyReductionMoving > 0 ? (weapon.accuracy - weapon.accuracyReductionMoving).toFixed(3).replace(/[,.]?0+$/, '') : "-"}
</TableCell> </TableCell>
</TableRow> </TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid2>
<Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={Paper}>
<Table size="small" aria-label="a dense table">
<TableBody>
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
@ -161,49 +194,67 @@ class Weapon extends React.Component<IWeaponProps, any> {
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">Max range</TableCell> <TableCell component="th" scope="row">Range</TableCell>
<TableCell component="th" <TableCell component="th"
scope="row">{weapon.maxRange ? (weapon.maxRange) : "-"} scope="row">{weapon.maxRange ? (weapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}
</TableCell> </TableCell>
</TableRow> </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 <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">Setup time</TableCell> <TableCell component="th" scope="row">Setup time</TableCell>
<TableCell component="th" <TableCell component="th"
scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(2) : "-"} scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(3).replace(/[,.]?0+$/, '') : "-"}
</TableCell> </TableCell>
</TableRow> </TableRow>
{(weapon.costRequisition > 0 || weapon.costPower > 0) &&
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">Cost</TableCell> <TableCell component="th" scope="row">Throw force</TableCell>
<TableCell component="th" <TableCell component="th"
scope="row"> scope="row">{weapon.throwForceMin != 0 ? weapon.throwForceMin + " - " + weapon.throwForceMax : "-"}
{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> </TableCell>
</TableRow> </TableRow>
}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
</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}> <Grid2 size={12}>
<ToggleButtonGroup <ToggleButtonGroup
color="primary" color="primary"
@ -229,12 +280,15 @@ class Weapon extends React.Component<IWeaponProps, any> {
name={ArmorTypeNames.InfantryHeavyMedium}/></StyledTableCell> name={ArmorTypeNames.InfantryHeavyMedium}/></StyledTableCell>
<StyledTableCell><ArmorType <StyledTableCell><ArmorType
name={ArmorTypeNames.InfantryHeavyHigh}/></StyledTableCell> name={ArmorTypeNames.InfantryHeavyHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.Commander}/></StyledTableCell> <StyledTableCell><ArmorType
name={ArmorTypeNames.Commander}/></StyledTableCell>
<StyledTableCell><ArmorType <StyledTableCell><ArmorType
name={ArmorTypeNames.DemonMedium}/></StyledTableCell> name={ArmorTypeNames.DemonMedium}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.DemonHigh}/></StyledTableCell> <StyledTableCell><ArmorType
name={ArmorTypeNames.DemonHigh}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.Air}/></StyledTableCell> <StyledTableCell><ArmorType name={ArmorTypeNames.Air}/></StyledTableCell>
<StyledTableCell><ArmorType name={ArmorTypeNames.VehicleLow}/></StyledTableCell> <StyledTableCell><ArmorType
name={ArmorTypeNames.VehicleLow}/></StyledTableCell>
<StyledTableCell><ArmorType <StyledTableCell><ArmorType
name={ArmorTypeNames.VehicleMedium}/></StyledTableCell> name={ArmorTypeNames.VehicleMedium}/></StyledTableCell>
<StyledTableCell><ArmorType <StyledTableCell><ArmorType
@ -274,7 +328,8 @@ class Weapon extends React.Component<IWeaponProps, any> {
<i className="rgdFrom">{weapon.filename}</i> <i className="rgdFrom">{weapon.filename}</i>
</Grid2> </Grid2>
</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 AvailableMods = process.env.REACT_APP_HOST_URL + '/api/v1/mods';
export const AvailableRacesPart = process.env.REACT_APP_HOST_URL + '/api/v1/races'; 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 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/'; export const IconUrl = process.env.REACT_APP_HOST_URL + '/api/v1/grapics/icon/';

View File

@ -2,6 +2,10 @@
width: 50px; width: 50px;
} }
.weaponIcon{
width: 40px;
}
.sergeantIcon{ .sergeantIcon{
padding: 0px; padding: 0px;
width: 40px; 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 React, {useState} from "react";
import {withRouter} from "../core/withrouter"; import {withRouter} from "../core/withrouter";
import {IMod} from "../types/Imod"; import {IMod} from "../types/Imod";
@ -30,6 +30,7 @@ import {ArrowBack, ArrowDropDown} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType"; import ArmorType from "../classes/ArmorType";
import Weapon from "../classes/Weapon"; import Weapon from "../classes/Weapon";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort"; import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
interface RacePageState { interface RacePageState {
mod: IMod, mod: IMod,
@ -49,14 +50,26 @@ function Unit (unit: IUnitShort, modId: number, raceId: String) {
</ListItem></a>) </ListItem></a>)
} }
interface UnitsProps{ 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, raceId: string,
modId: number, modId: number,
} }
interface UnitsState { interface UnitsState {
selectedUnits: String | null, selectedUnits: String | null,
units: IRaceUnits | null, units: IRaceUnits | null,
buildings: IRaceBuildings | null,
} }
class Units extends React.Component<UnitsProps, UnitsState> { class Units extends React.Component<UnitsProps, UnitsState> {
@ -64,26 +77,28 @@ class Units extends React.Component<UnitsProps, UnitsState> {
constructor(props: any) { constructor(props: any) {
super(props); 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 => res.json())
.then((res: IRaceUnits) => { .then((res: IRaceUnits) => {
this.setState({ this.setState({
units: res, units: res,
}) })
}) })
}
handleChange = (e: React.MouseEvent, newUnitType: String | null) => { let urlBuildings = AvailableBuildings + "/" + this.props.modId + "/" + this.props.raceId;
fetch(urlBuildings)
.then(res => res.json())
.then((res: IRaceBuildings) => {
this.setState({ this.setState({
selectedUnits: newUnitType buildings: res,
}); })
})
} }
render () { render () {
@ -91,8 +106,7 @@ class Units extends React.Component<UnitsProps, UnitsState> {
return (<div> return (<div>
{this.state.units != null && this.state.buildings != null ?
{this.state.units != null ?
<Grid2 container spacing={2}> <Grid2 container spacing={2}>
<Grid2 size= {{xs: 12, md: 4}}> <Grid2 size= {{xs: 12, md: 4}}>
<h3>Infantry</h3> <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))} {this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</List> </List>
</Grid2> </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> </Grid2>
: "Loading"} : "Loading"}

View File

@ -95,19 +95,27 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">cost</TableCell> <TableCell component="th" scope="row">Cost</TableCell>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{unit.buildCostRequisition > 0 && {unit.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp; src="/images/Resource_requisition.gif"/>&nbsp;
{unit.buildCostRequisition}</span>} {unit.buildCostRequisition.toFixed(0)}</span>}
{unit.buildCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}} {unit.buildCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp; src="/images/Resource_power.gif"/>&nbsp;
{unit.buildCostPower}</span>} {unit.buildCostPower.toFixed(0)}</span>}
{(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) && {(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp; 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"}} {unit.capInfantry > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_cap_infantry.gif"/>&nbsp; src="/images/Resource_cap_infantry.gif"/>&nbsp;
@ -121,22 +129,30 @@ function Unit(unit: IUnit, mod: IMod) {
{unit.buildCostTime}s</span>} {unit.buildCostTime}s</span>}
</TableCell> </TableCell>
</TableRow> </TableRow>
{(unit?.reinforceTime !== 0 && unit.reinforceTime !== null) && {(unit?.reinforceTime !== 0 && unit.reinforceTime !== null && unit.squadMaxSize > 1) &&
<TableRow> <TableRow>
<TableCell>reinforce cost</TableCell> <TableCell>Reinforce cost</TableCell>
<TableCell> <TableCell>
{unit.reinforceCostRequisition && unit.reinforceCostRequisition > 0 && {unit.reinforceCostRequisition && unit.reinforceCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp; src="/images/Resource_requisition.gif"/>&nbsp;
{unit.reinforceCostRequisition}</span>} {unit.reinforceCostRequisition.toFixed(0)}</span>}
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 && {unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp; src="/images/Resource_power.gif"/>&nbsp;
{unit.reinforceCostPower}</span>} {unit.reinforceCostPower.toFixed(0)}</span>}
{(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) && {(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}} <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp; 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) && {(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon <span>&nbsp;<AvTimerOutlinedIcon
style={{verticalAlign: "top", fontSize: "18px"}}/>&nbsp; style={{verticalAlign: "top", fontSize: "18px"}}/>&nbsp;
@ -144,10 +160,18 @@ function Unit(unit: IUnit, mod: IMod) {
</TableCell> </TableCell>
</TableRow> </TableRow>
} }
{unit.squadMaxSize > 1 &&
<TableRow>
<TableCell>Squad size</TableCell>
<TableCell>
{unit.squadStartSize} / {unit.squadMaxSize}
</TableCell>
</TableRow>
}
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">armor type</TableCell> <TableCell component="th" scope="row">Armor type</TableCell>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
<ArmorType name={unit.armorType.name}/> <ArmorType name={unit.armorType.name}/>
</TableCell> </TableCell>
@ -155,7 +179,7 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">health</TableCell> <TableCell component="th" scope="row">Health</TableCell>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
<span><img style={{verticalAlign: "top"}} <span><img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp; src="/images/Health_icon.webp"/>&nbsp;
@ -166,7 +190,7 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">move speed</TableCell> <TableCell component="th" scope="row">Move speed</TableCell>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{unit.moveSpeed} {unit.moveSpeed}
</TableCell> </TableCell>
@ -174,7 +198,7 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">morale</TableCell> <TableCell component="th" scope="row">Morale</TableCell>
<TableCell component="th" scope="row"> <TableCell component="th" scope="row">
{morale} {morale}
</TableCell> </TableCell>
@ -182,7 +206,15 @@ function Unit(unit: IUnit, mod: IMod) {
<TableRow <TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}} sx={{'&:last-child td, &:last-child th': {border: 0}}}
> >
<TableCell component="th" scope="row">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"> <TableCell component="th" scope="row">
{detect} {detect}
</TableCell> </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 reinforceCostPower?: number
reinforceCostPopulation ?: number reinforceCostPopulation ?: number
reinforceCostFaith ?: number reinforceCostFaith ?: number
reinforceCostSouls ?: number
reinforceTime?: number reinforceTime?: number
icon: string icon: string
modId: number modId: number
@ -90,22 +91,25 @@ export interface IWeapon {
reloadTime: number reloadTime: number
setupTime: number setupTime: number
accuracyReductionMoving: number accuracyReductionMoving: number
minRange: number
maxRange: number maxRange: number
minDamage: number minDamage: number
maxDamage: number maxDamage: number
minDamageValue: number minDamageValue: number
moraleDamage: number moraleDamage: number
damageRadius: number
throwForceMin: number
throwForceMax: number
isMeleeWeapon: boolean isMeleeWeapon: boolean
canAttackAir: boolean canAttackAir: boolean
canAttackGround: boolean canAttackGround: boolean
haveEquipButton: boolean haveEquipButton: boolean
icon: string icon: string
modId: number modId: number
weaponPiercings: IWeaponPiercing[] weaponArmorPiercing: IWeaponPiercing[]
} }
export interface IWeaponPiercing { export interface IWeaponPiercing {
id: number
armorType: IArmorType armorType: IArmorType
piercingValue: number piercingValue: number
} }