Add addon requirements

This commit is contained in:
Anibus 2025-06-01 20:09:48 +03:00
parent 5751cec4de
commit ba699a0199
13 changed files with 1040 additions and 620 deletions

1364
package-lock.json generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -24,9 +24,10 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Wiki Soulstorm</title>
</head> </head>
<body> <body>
<!-- Yandex.Metrika counter --> <script type="text/javascript" > (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; m[i].l=1*new Date(); for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }} k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); ym(99502794, "init", { clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true }); </script> <noscript><div><img src="https://mc.yandex.ru/watch/99502794" style="position:absolute; left:-9999px;" alt="" /></div></noscript> <!-- /Yandex.Metrika counter -->
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<script type="text/babel" src="./index_bundle.js"></script> <script type="text/babel" src="./index_bundle.js"></script>
<div id="root"></div> <div id="root"></div>

View File

@ -1,3 +1,4 @@
#back-button{ #back-button{
margin-top: 10px; margin-top: 10px;
} }

View File

@ -21,12 +21,17 @@ function App() {
}; };
const theme = createTheme({ const theme = createTheme({
cssVariables: true, palette: {
primary: {
main: "#191919",
},
},
}); });
return ( return (
<div > <div >
<ThemeProvider theme={theme}>
<AppBar position="static"> <AppBar position="static">
<Toolbar> <Toolbar>
<IconButton <IconButton
@ -38,7 +43,7 @@ function App() {
> >
</IconButton> </IconButton>
<Typography variant="h6" component="div" sx={{flexGrow: 1}}> <Typography variant="h6" component="div" sx={{flexGrow: 1}}>
Soulstorm autogeneration wiki Autogeneration wiki (by Anibus)
</Typography> </Typography>
<Button color="inherit" onClick={steamLogin}> <Button color="inherit" onClick={steamLogin}>
Log in through Steam Log in through Steam
@ -48,6 +53,7 @@ function App() {
<Container> <Container>
<MyRoutes/> <MyRoutes/>
</Container> </Container>
</ThemeProvider>
<form action="https://steamcommunity.com/openid/login" method="post"> <form action="https://steamcommunity.com/openid/login" method="post">

View File

@ -1,7 +1,6 @@
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import {AvailableBuildings, AvailableUnits} from "../core/api"; import {AvailableBuildings, AvailableUnits} from "../core/api";
import {Grid2, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material"; import {Grid2, Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material";
import {NavLink} from "react-router-dom";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort"; import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort"; import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
@ -20,15 +19,15 @@ export default function UnitsTable(prop: {modId: number}) {
function getUnitRef(modId: number, raceId: string, unit: IUnitShort) { function getUnitRef(modId: number, raceId: string, unit: IUnitShort) {
return <span style={{fontSize: 11}}><a href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}> return <span style={{fontSize: 11}}><Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
{unit.name} {unit.name}
</a><br/></span> </Link><br/></span>
} }
function getBuildingRef(modId: number, raceId: string, building: IBuildingShort) { function getBuildingRef(modId: number, raceId: string, building: IBuildingShort) {
return <span style={{fontSize: 11}}><a href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}> return <span style={{fontSize: 11}}><Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}>
{building.name} {building.name}
<br/></a></span> <br/></Link></span>
} }
useEffect(() => { useEffect(() => {
@ -97,8 +96,8 @@ export default function UnitsTable(prop: {modId: number}) {
{<Table sx={{minWidth: 650}} size="small" aria-label="a dense table"> {<Table sx={{minWidth: 650}} size="small" aria-label="a dense table">
<TableHead> <TableHead>
<TableRow> <TableRow>
{racesUnitsPart.map(raceUnits => <TableCell><h2><NavLink state={raceUnits.race.id} {racesUnitsPart.map(raceUnits => <TableCell><h2><Link color="inherit"
to={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</NavLink> href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</Link>
</h2></TableCell>)} </h2></TableCell>)}
</TableRow> </TableRow>
</TableHead> </TableHead>
@ -147,19 +146,30 @@ export default function UnitsTable(prop: {modId: number}) {
return elements return elements
} }
function generateRaceList(unitsTable: IUnitsTable){
return unitsTable.racesUnits.sort(function (a, b) {
return a.race.name.localeCompare(b.race.name);
}).map(raceUnits => <h5><Link color="inherit"
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</Link><br/>
</h5>)
}
return ( if(unitsTable.racesUnits.length > 10){
<Grid2> return <Grid2>
<Grid2 display={{ lg: 'block', xs: 'none' }} > {generateRaceList(unitsTable)}
{generateRaceUnitTables(unitsTable.racesUnits, unitsTable.racesBuildings, 5)}
</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> </Grid2>
) } else {
return (
<Grid2>
<Grid2 display={{ lg: 'block', xs: 'none' }} >
{generateRaceUnitTables(unitsTable.racesUnits, unitsTable.racesBuildings, 5)}
</Grid2>
<Grid2 display={{ lg: 'none', xs: 'block' }}>
{generateRaceList(unitsTable)}
</Grid2>
</Grid2>
)
}
} }

View File

@ -129,7 +129,7 @@ class Weapon extends React.Component<IWeaponProps, any> {
aria-controls="panel1-content" aria-controls="panel1-content"
id="panel1-header" id="panel1-header"
> >
<span style={{fontSize: 18}}> {!this.props.isDefault && weapon.icon && weapon.haveEquipButton ? <span style={{fontSize: 18}}> {!this.props.isDefault && weapon.icon && !weapon.icon.endsWith("upgrade.png") ?
<img className="weaponIcon" src={IconUrl + weapon.icon.replaceAll('\\', '/')}/> : ( <img className="weaponIcon" src={IconUrl + weapon.icon.replaceAll('\\', '/')}/> : (
weapon.isMeleeWeapon ? weapon.isMeleeWeapon ?
<img className="weaponIcon" src="/images/MeleeStance_icon_bw.jpg"/> : <img className="weaponIcon" src="/images/MeleeStance_icon_bw.jpg"/> :

16
src/css/Building.css Normal file
View File

@ -0,0 +1,16 @@
.addon-accordion{
margin-top: 10px;
scroll-margin-top: 15px;
}
@-webkit-keyframes blink2 {
100% { color: rgba(34, 34, 34, 0); }
}
@keyframes blink2 {
100% { color: rgba(34, 34, 34, 0); }
}
.selected-addon div > h3 {
-webkit-animation: blink2 1s linear infinite;
animation: blink2 1s linear infinite;
}

View File

@ -1,16 +1,29 @@
import {AvailableBuildings, AvailableMods, AvailableUnits, IconUrl} from "../core/api"; import {AvailableBuildings, AvailableMods, AvailableUnits, IconUrl} from "../core/api";
import React from "react"; import React, {MutableRefObject, useEffect, useRef} from "react";
import {withRouter} from "../core/withrouter"; import {withRouter} from "../core/withrouter";
import {IWeapon} from "../types/IUnit"; import {IWeapon} from "../types/IUnit";
import '../css/Unit.css' import '../css/Building.css'
import {Button, Grid2, Paper, Table, TableBody, TableCell, TableContainer, TableRow} from "@mui/material"; import {
import {ArrowBack} from "@mui/icons-material"; 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 ArmorType from "../classes/ArmorType";
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer'; import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
import WeaponSlot from "../classes/WeaponSlot"; import WeaponSlot from "../classes/WeaponSlot";
import UnitsTable from "../classes/UnitsTable"; import UnitsTable from "../classes/UnitsTable";
import {IMod} from "../types/Imod"; import {IMod} from "../types/Imod";
import {IBuilding} from "../types/IBuilding"; import {IBuilding, IBuildingAddon, IBuildingAddonShort} from "../types/IBuilding";
import {IBuildingShort} from "../types/IBuildingShort";
interface UintPageState { interface UintPageState {
building: IBuilding, building: IBuilding,
@ -39,6 +52,118 @@ function Building(building: IBuilding, mod: IMod) {
}) })
function BuildingAddon(addon: IBuildingAddon){
return <div className='addon-accordion' id={"addon-" + addon.id} ><Accordion>
<AccordionSummary
expandIcon={<ExpandMore/>}
aria-controls="panel1-content"
>
<span style={{fontSize: 20}}>
{addon.icon && <img className="sergeantIcon" src={IconUrl + addon.icon.replaceAll('\\', '/')}/>}
&nbsp; {addon.name}
</span>
</AccordionSummary>
<AccordionDetails>
<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">
{addon.addonCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{addon.addonCostRequisition.toFixed(0)}</span>}
{addon.addonCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{addon.addonCostPower.toFixed(0)}</span>}
{(addon.addonCostPopulation !== undefined && addon.addonCostPopulation > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{addon.addonCostPopulation.toFixed(0)}</span>}
{(addon.addonCostFaith !== undefined && addon.addonCostFaith > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_faith.gif"/>&nbsp;
{addon.addonCostFaith}</span>}
{(addon.addonCostSouls !== undefined && addon.addonCostSouls > 0) &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_souls.gif"/>&nbsp;
{addon.addonCostSouls.toFixed(0)}</span>}
{(addon.addonCostTime !== undefined && addon.addonCostTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{verticalAlign: "top", fontSize: "18px"}}/>&nbsp;
{addon.addonCostTime}s</span>}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid2>
<Grid2 size={{xs: 12, md: 8}}>
<div style={{whiteSpace: "pre-wrap"}}>
{addon.description}
</div>
</Grid2>
{addon.addonRequirement !== null &&
<Grid2 size={{xs: 12, md: 12}}>
<h4>Required: </h4>
{addon.addonRequirement.requiredTotalPop !== undefined && addon.addonRequirement.requiredTotalPop !== null &&
<div>&nbsp; Population: <img style={{verticalAlign: "top", height: 25}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{addon.addonRequirement.requiredTotalPop}</div>
}
{addon.addonRequirement.requireAddon !== null &&
<div>&nbsp; Addon: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + addon.addonRequirement.requireAddon.icon.replaceAll('\\', '/')}/>&nbsp;
<a href={"#addon-" + addon.addonRequirement.requireAddon.id} onClick={() => goToAddon(`#addon-` + addon.addonRequirement.requireAddon.id)}>{addon.addonRequirement.requireAddon.name}</a></div>
}
{addon.addonRequirement.requirementBuildings.map(b =>
<div>&nbsp; Building: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + b.icon.replaceAll('\\', '/')}/>&nbsp;
<a href= {'/mod/'+ building.modId +'/race/'+ building.race.id +'/building/' + b.id + "/"}>{b.name}</a></div>)
}
{addon.addonRequirement.requirementBuildingsEither.length !== 0 &&
renderRequirementBuilding(addon.addonRequirement.requirementBuildingsEither, building)}
{addon.addonRequirement.requirementsGlobalAddons.length !== 0 &&
renderRequirementGlobalAddons(addon.addonRequirement.requirementsGlobalAddons, building)}
</Grid2>}
</Grid2>
<i className="rgdFrom">{addon.filename}</i>
</AccordionDetails>
</Accordion></div>
}
function renderRequirementBuilding(requirementBuildings: IBuildingShort[], building: IBuilding) {
return requirementBuildings.length > 0 && (
<div>&nbsp; Buildings: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + requirementBuildings[0].icon.replaceAll('\\', '/')}/>
&nbsp;{" "}<a href={`/mod/${building.modId}/race/${building.race.id}/building/${requirementBuildings[0].id}`}>
{requirementBuildings[0].name}
</a> or &nbsp;<img style={{verticalAlign: "top", height: 25}}
src={IconUrl + requirementBuildings[1].icon.replaceAll('\\', '/')}/>
&nbsp;{" "}<a href={`/mod/${building.modId}/race/${building.race.id}/building/${requirementBuildings[1].id}`}>
{requirementBuildings[1].name}</a></div>
);
}
function renderRequirementGlobalAddons(rgas: IBuildingAddonShort[], building: IBuilding) {
return <div> {rgas.map(rga =>
<span>&nbsp; Global addon: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + rga.icon.replaceAll('\\', '/')}/>
&nbsp;<a href={`/mod/${building.modId}/race/${building.race.id}/building/${rga.buildingId}#addon-${rga.id}`}>
{rga.name}
</a></span>
)}</div>
}
return ( return (
<div> <div>
<h1>{mod.name} ({mod.version})</h1> <h1>{mod.name} ({mod.version})</h1>
@ -124,6 +249,12 @@ function Building(building: IBuilding, mod: IMod) {
{building.description} {building.description}
</div> </div>
</Grid2> </Grid2>
{building.addons.length > 0 && <Grid2 size={12}>
<h3>Addons</h3>
{building.addons.map(b =>
BuildingAddon(b)
)}
</Grid2> }
<Grid2 size={12}> <Grid2 size={12}>
{[...mapBuildingWeapons.keys()].sort(function (a, b) { {[...mapBuildingWeapons.keys()].sort(function (a, b) {
return a - b; return a - b;
@ -136,6 +267,16 @@ function Building(building: IBuilding, mod: IMod) {
</div>) </div>)
} }
function goToAddon(addonHash: string) {
let addonId = addonHash.replace("#", "");
console.log(addonId);
[].forEach.call(document.querySelectorAll('div'), function (el: HTMLElement) {
el?.classList?.remove('selected-addon');
});
document?.getElementById(addonId)?.classList?.add("selected-addon");
document?.getElementById(addonId)?.scrollIntoView()
}
class BuildingPage extends React.Component<any, UintPageState> { class BuildingPage extends React.Component<any, UintPageState> {
@ -153,6 +294,7 @@ class BuildingPage extends React.Component<any, UintPageState> {
this.setState({ this.setState({
mod: modData mod: modData
}); });
goToAddon(window.location.hash);
} }
render() { render() {

View File

@ -10,7 +10,7 @@ import {
AccordionSummary, AccordionSummary,
Button, Button,
ButtonGroup, ButtonGroup,
Grid2, Grid2, Link,
List, List,
ListItem, ListItem,
ListSubheader, ListSubheader,
@ -41,24 +41,24 @@ interface RacePageState {
function Unit (unit: IUnitShort, modId: number, raceId: String) { function Unit (unit: IUnitShort, modId: number, raceId: String) {
return (<a href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}><ListItem> return (<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}><ListItem>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> } {unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> }
{unit.name} {unit.name}
{unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}} {unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>} src="/images/DETECT_YES.webp"/></span>}
</ListItem></a>) </ListItem></Link>)
} }
function Building (building: IBuildingShort, modId: number, raceId: String) { function Building (building: IBuildingShort, modId: number, raceId: String) {
return (<a href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}><ListItem> return (<Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}><ListItem>
{building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>} {building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
{building.name} {building.name}
{building.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}} {building.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>} src="/images/DETECT_YES.webp"/></span>}
</ListItem></a>) </ListItem></Link>)
} }
interface UnitsProps{ interface UnitsProps{
@ -170,7 +170,7 @@ class RacePageFast extends React.Component<any, RacePageState> {
if(this.state != null && this.state.mod != null && this.state.race != null ){ if(this.state != null && this.state.mod != null && this.state.race != null ){
const backRef = "/mod/" + this.state.mod.id const backRef = "/mod/" + this.state.mod.id
return <div><a href={backRef}><Button id="back-button" variant="contained" startIcon={<ArrowBack/>}> Back</Button></a> return <div><Link href={backRef}><Button id="back-button" variant="contained" startIcon={<ArrowBack/>}> Back</Button></Link>
<h1>{this.state.mod.name} ({this.state.mod.version}) - {this.state.race.name}</h1> <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}/> <Units raceId={this.state.race.id} modId={this.state.mod.id}/>
</div>; </div>;

View File

@ -133,10 +133,10 @@ function Unit(unit: IUnit, mod: IMod) {
<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.toFixed(0)}</span>} {unit.reinforceCostRequisition.toFixed(0)}</span>:<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;
@ -219,6 +219,14 @@ function Unit(unit: IUnit, mod: IMod) {
{detect} {detect}
</TableCell> </TableCell>
</TableRow> </TableRow>
{unit.repairMax !== undefined &&
<TableRow>
<TableCell>Repair max</TableCell>
<TableCell>
{unit.repairMax}
</TableCell>
</TableRow>
}
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>

View File

@ -1,6 +1,7 @@
import {Irace} from "./Irace"; import {Irace} from "./Irace";
import {IArmorType} from "./IArmorType"; import {IArmorType} from "./IArmorType";
import {WeaponHardpoint} from "./IUnit"; import {WeaponHardpoint} from "./IUnit";
import {IBuildingShort} from "./IBuildingShort";
export interface IBuilding { export interface IBuilding {
id: number id: number
@ -24,5 +25,43 @@ export interface IBuilding {
icon: string icon: string
modId: number modId: number
weapons: WeaponHardpoint[] weapons: WeaponHardpoint[]
addons: IBuildingAddon[]
} }
export interface IBuildingAddon {
id: number;
name: string;
description: string;
filename: string;
addonCostRequisition: number;
addonCostPower: number;
addonCostPopulation: number;
addonCostFaith: number;
addonCostSouls: number;
addonCostTime: number;
addonModifiers: IAddonModifier[];
addonRequirement: IAddonRequirement;
icon?: string;
}
export interface IBuildingAddonShort {
id: number;
buildingId: number;
name: string;
icon: string;
}
export interface IAddonRequirement {
requirementBuildings: IBuildingShort[];
requirementBuildingsEither: IBuildingShort[];
requirementsGlobalAddons: IBuildingAddonShort[];
requireAddon: IBuildingAddonShort;
requiredTotalPop?: number;
}
export interface IAddonModifier {
id: number;
reference: string;
usageType: string;
value: number;
}

View File

@ -28,6 +28,7 @@ export interface IUnit {
health: number health: number
healthRegeneration: number healthRegeneration: number
moraleDeathPenalty: number moraleDeathPenalty: number
repairMax: number
moraleMax?: number moraleMax?: number
moraleBroken?: number moraleBroken?: number
moraleRegeneration?: number moraleRegeneration?: number