Add on/off weapon research dps and params tables feature
This commit is contained in:
parent
23bfad8956
commit
fa0baa62c1
4
.env
4
.env
@ -1,5 +1,5 @@
|
||||
NEXT_PUBLIC_HOST_URL=https://wiki-backend.dawn-of-war.pro
|
||||
#NEXT_PUBLIC_HOST_URL=http://localhost:8082
|
||||
NEXT_PUBLIC_HOST_URL=https://wiki-backend.dawn-of-war.pro
|
||||
HOST_URL=http://localhost:8082
|
||||
|
||||
# Домен сайта для sitemap/robots/canonical (замените на реальный)
|
||||
NEXT_PUBLIC_SITE_URL=https://wiki.dawn-of-war.pro
|
||||
67
build.ps1
Normal file
67
build.ps1
Normal file
@ -0,0 +1,67 @@
|
||||
# Скрипт сборки дистрибутива dow-wiki-frontend
|
||||
# Запуск: .\build.ps1
|
||||
# С шагами: .\build.ps1 -VerboseOutput
|
||||
|
||||
param(
|
||||
[switch]$VerboseOutput
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$root = $PSScriptRoot
|
||||
$deployDir = Join-Path $root "deploy"
|
||||
$archive = Join-Path $root "dow-wiki-deploy.tar.gz"
|
||||
|
||||
# Файлы и папки, попадающие в дистрибутив
|
||||
$artifacts = @(
|
||||
".next",
|
||||
"public",
|
||||
"node_modules",
|
||||
".env",
|
||||
"next.config.mjs",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"tsconfig.json",
|
||||
"next-env.d.ts"
|
||||
)
|
||||
|
||||
function Step($message) {
|
||||
Write-Host "==> $message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
Set-Location $root
|
||||
|
||||
try {
|
||||
# 1. Сборка Next.js
|
||||
Step "Сборка Next.js (npm run build)"
|
||||
npm run build
|
||||
if ($LASTEXITCODE -ne 0) { throw "Сборка Next.js завершилась с ошибкой" }
|
||||
|
||||
# 2. Очистка папки deploy
|
||||
Step "Очистка папки deploy"
|
||||
if (Test-Path $deployDir) { Remove-Item $deployDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $deployDir | Out-Null
|
||||
|
||||
# 3. Копирование артефактов
|
||||
Step "Копирование артефактов в deploy"
|
||||
Copy-Item -Path $artifacts -Destination $deployDir -Recurse -Force
|
||||
|
||||
# 4. Удаление старого архива
|
||||
Step "Удаление старого архива"
|
||||
if (Test-Path $archive) { Remove-Item $archive -Force }
|
||||
|
||||
# 5. Создание архива
|
||||
Step "Создание архива dow-wiki-deploy.tar.gz"
|
||||
tar -czf $archive -C $deployDir .
|
||||
if ($LASTEXITCODE -ne 0) { throw "Ошибка при создании архива" }
|
||||
|
||||
# Итог
|
||||
$size = "{0:N1} МБ" -f ((Get-Item $archive).Length / 1MB)
|
||||
Write-Host ""
|
||||
Write-Host "Сборка завершена успешно." -ForegroundColor Green
|
||||
Write-Host "Дистрибутив: $archive ($size)"
|
||||
}
|
||||
catch {
|
||||
Write-Host ""
|
||||
Write-Host "ОШИБКА: $_" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
// Server-side API helpers for metadata generation.
|
||||
// Runs only on the server (Node.js) — uses NEXT_PUBLIC_HOST_URL which is inlined at build time.
|
||||
// Runs only on the server (Node.js) — uses HOST_URL (not exposed to the browser).
|
||||
|
||||
import { IMod } from '@/src/types/Imod';
|
||||
import { Irace } from '@/src/types/Irace';
|
||||
@ -8,8 +8,8 @@ import { IRaceBuildings } from '@/src/types/IBuildingShort';
|
||||
import { IUnit } from '@/src/types/IUnit';
|
||||
import { IBuilding } from '@/src/types/IBuilding';
|
||||
|
||||
const API = process.env.NEXT_PUBLIC_HOST_URL || '';
|
||||
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://dow-wiki.example.com';
|
||||
const API = process.env.HOST_URL || '';
|
||||
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://wiki.dawn-of-war.pro';
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T | null> {
|
||||
try {
|
||||
|
||||
@ -2,7 +2,6 @@ import React, {useEffect, useState} from "react";
|
||||
import {useTheme} from "@mui/material/styles";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import {WeaponUrl} from "../core/api";
|
||||
import {IWeapon} from "../types/IUnit";
|
||||
import ArmorTypeNames from "../types/ArmorTypeValues";
|
||||
import {
|
||||
Grid2,
|
||||
@ -17,10 +16,12 @@ import Required from "./Required";
|
||||
import {AffectedResearches} from "./building/Research";
|
||||
import {IMod} from "../types/Imod";
|
||||
import {Irace} from "../types/Irace";
|
||||
import {ModifiersProvidesTable} from "./ModifiersProvideTable";
|
||||
import {ModifiersProvidesTable, getIcon} from "./ModifiersProvideTable";
|
||||
import {StyledPaper} from "../commons/StyledPaper";
|
||||
import {StyledTableCell} from "../commons/StyledTableCell";
|
||||
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
||||
import {IWeapon, IWeaponResearch} from "../types/IUnit";
|
||||
|
||||
|
||||
interface IWeaponFull {
|
||||
currentTable: string;
|
||||
@ -41,6 +42,71 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
weapon: undefined,
|
||||
});
|
||||
|
||||
const [enabledResearchIds, setEnabledResearchIds] = useState<Set<number>>(new Set());
|
||||
|
||||
function applyResearchEffects(weapon: IWeapon, enabledIds: Set<number>): IWeapon {
|
||||
if (enabledIds.size === 0 || !weapon.researches || weapon.researches.length === 0) {
|
||||
return weapon;
|
||||
}
|
||||
|
||||
const modified: any = { ...weapon };
|
||||
const enabledResearches = weapon.researches.filter((r: IWeaponResearch) => enabledIds.has(r.research.id));
|
||||
|
||||
for (const research of enabledResearches) {
|
||||
if (!research.effects || research.effects.length === 0) continue;
|
||||
|
||||
for (const effect of research.effects) {
|
||||
const usageType: string = effect.usageType;
|
||||
|
||||
const apply = (value: number): number => {
|
||||
if (usageType.includes('multiplication')) {
|
||||
return value * effect.modifierValue;
|
||||
} else if (usageType.includes('addition')) {
|
||||
return value + effect.modifierValue;
|
||||
} else if (usageType.includes('replace')) {
|
||||
return effect.modifierValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const field = effect.field;
|
||||
|
||||
switch (field) {
|
||||
case 'armourPiercing':
|
||||
if (!modified.weaponArmorPiercing || modified.weaponArmorPiercing.length === 0) continue;
|
||||
modified.weaponArmorPiercing = modified.weaponArmorPiercing.map((p: any) => ({
|
||||
...p,
|
||||
piercingValue: apply(p.piercingValue),
|
||||
}));
|
||||
break;
|
||||
case 'accuracyReductionMoving':
|
||||
modified.accuracyReductionMoving = apply(modified.accuracyReductionMoving);
|
||||
break;
|
||||
case 'accuracy':
|
||||
modified.accuracy = apply(modified.accuracy);
|
||||
break;
|
||||
case 'maxDamage':
|
||||
modified.maxDamage = apply(modified.maxDamage);
|
||||
break;
|
||||
case 'maxRange':
|
||||
modified.maxRange = apply(modified.maxRange);
|
||||
break;
|
||||
case 'minDamage':
|
||||
modified.minDamage = apply(modified.minDamage);
|
||||
break;
|
||||
case 'reloadTime':
|
||||
modified.reloadTime = apply(modified.reloadTime);
|
||||
break;
|
||||
case 'setupTime':
|
||||
modified.setupTime = apply(modified.setupTime);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modified as IWeapon;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
fetch(WeaponUrl + "/" + props.mod.id + "/" + props.weaponId)
|
||||
@ -59,8 +125,10 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
|
||||
const weapon = weaoponFull.weapon
|
||||
|
||||
const displayWeapon = applyResearchEffects(weapon, enabledResearchIds);
|
||||
|
||||
function getPiercingK(armorType: string): number {
|
||||
const weaponPiercing = weapon.weaponArmorPiercing.find((p) => p.armorType.name === armorType)
|
||||
const weaponPiercing = displayWeapon.weaponArmorPiercing.find((p) => p.armorType.name === armorType)
|
||||
return (typeof weaponPiercing !== "undefined" ? weaponPiercing.piercingValue : 10) / 100
|
||||
}
|
||||
|
||||
@ -71,8 +139,8 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
});
|
||||
}
|
||||
|
||||
if(weapon.reloadTime < 0.125) weapon.reloadTime = 0.125
|
||||
const dpsK = (weaoponFull.currentTable == "dps") ? weapon.accuracy * (1 / (weapon.reloadTime - (weapon.reloadTime % 0.125))) : 1
|
||||
if(displayWeapon.reloadTime < 0.125) displayWeapon.reloadTime = 0.125
|
||||
const dpsK = (weaoponFull.currentTable == "dps") ? displayWeapon.accuracy * (1 / (displayWeapon.reloadTime - (displayWeapon.reloadTime % 0.125))) : 1
|
||||
|
||||
const infLowPiercing = getPiercingK(ArmorTypeNames.InfantryLow)
|
||||
const infMedPiercing = getPiercingK(ArmorTypeNames.InfantryMedium)
|
||||
@ -102,14 +170,14 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
if (!isAir && !weapon.canAttackGround && !weapon.isMeleeWeapon) return ""
|
||||
if (isAir && !weapon.canAttackAir) return ""
|
||||
|
||||
var minDamage = damagePiercing * weapon.minDamage
|
||||
var maxDamage = damagePiercing * weapon.maxDamage
|
||||
var minDamage = damagePiercing * displayWeapon.minDamage
|
||||
var maxDamage = damagePiercing * displayWeapon.maxDamage
|
||||
|
||||
if (minDamage < weapon.minDamageValue) {
|
||||
minDamage = weapon.minDamageValue
|
||||
if (minDamage < displayWeapon.minDamageValue) {
|
||||
minDamage = displayWeapon.minDamageValue
|
||||
}
|
||||
if (maxDamage < weapon.minDamageValue) {
|
||||
maxDamage = weapon.minDamageValue
|
||||
if (maxDamage < displayWeapon.minDamageValue) {
|
||||
maxDamage = displayWeapon.minDamageValue
|
||||
}
|
||||
|
||||
const averageDmg = (minDamage + maxDamage) / 2
|
||||
@ -118,7 +186,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
}
|
||||
|
||||
const getMoraleDamage = () => {
|
||||
return (weapon.moraleDamage * dpsK / 2).toFixed(2)
|
||||
return (displayWeapon.moraleDamage * dpsK / 2).toFixed(2)
|
||||
}
|
||||
|
||||
|
||||
@ -137,19 +205,19 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Base damage</TableCell>
|
||||
<TableCell>{weapon.minDamage} {weapon.maxDamage !== weapon.minDamage && "- " + weapon.maxDamage}</TableCell>
|
||||
<TableCell>{displayWeapon.minDamage.toFixed(2)} {displayWeapon.maxDamage !== displayWeapon.minDamage && "- " + displayWeapon.maxDamage.toFixed(2)}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Accuracy</TableCell>
|
||||
<TableCell>{weapon.accuracy.toFixed(3).replace(/[,.]?0+$/, '')}</TableCell>
|
||||
<TableCell>{displayWeapon.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>{weapon.setupTime === 0 && weapon.accuracy - weapon.accuracyReductionMoving > 0 ? (weapon.accuracy - weapon.accuracyReductionMoving).toFixed(3).replace(/[,.]?0+$/, '') : "-"}</TableCell>
|
||||
<TableCell>{displayWeapon.setupTime === 0 && displayWeapon.accuracy - displayWeapon.accuracyReductionMoving > 0 ? (displayWeapon.accuracy - displayWeapon.accuracyReductionMoving).toFixed(3).replace(/[,.]?0+$/, '') : "-"}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@ -163,13 +231,13 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Reload time</TableCell>
|
||||
<TableCell>{weapon.reloadTime}</TableCell>
|
||||
<TableCell>{displayWeapon.reloadTime}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Range</TableCell>
|
||||
<TableCell>{weapon.maxRange ? (weapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}</TableCell>
|
||||
<TableCell>{displayWeapon.maxRange ? (displayWeapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
@ -210,7 +278,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Setup time</TableCell>
|
||||
<TableCell>{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(3).replace(/[,.]?0+$/, '') : "-"}</TableCell>
|
||||
<TableCell>{displayWeapon.setupTime != 0 ? (displayWeapon.setupTime).toFixed(3).replace(/[,.]?0+$/, '') : "-"}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
@ -224,17 +292,98 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
</Grid2>
|
||||
|
||||
<Grid2 size={12}>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
exclusive
|
||||
aria-label="Platform"
|
||||
value={weaoponFull.currentTable}
|
||||
onChange={handleChange}
|
||||
sx={{ mb: 2 }}
|
||||
>
|
||||
<ToggleButton size="small" value="dps" sx={{ color: textColor, '&.Mui-selected': { borderColor: textColor, color: textColor, backgroundColor: isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)' } }}>Dps</ToggleButton>
|
||||
<ToggleButton size="small" value="one hit" sx={{ color: textColor, '&.Mui-selected': {borderColor: textColor, color: textColor, backgroundColor: isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)' } }}>One hit average damage</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 12, marginBottom: 8 }}>
|
||||
<ToggleButtonGroup
|
||||
color="primary"
|
||||
exclusive
|
||||
aria-label="Platform"
|
||||
value={weaoponFull.currentTable}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<ToggleButton size="small" value="dps" sx={{ color: textColor, '&.Mui-selected': { borderColor: textColor, color: textColor, backgroundColor: isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)' } }}>Dps</ToggleButton>
|
||||
<ToggleButton size="small" value="one hit" sx={{ color: textColor, '&.Mui-selected': {borderColor: textColor, color: textColor, backgroundColor: isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)' } }}>One hit average damage</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
{weapon.researches && weapon.researches.length > 0 && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{(() => {
|
||||
// topological order: a research is always rendered to the right of its requirements
|
||||
const ordered: IWeaponResearch[] = [];
|
||||
const visited = new Set<number>();
|
||||
const visit = (wr: IWeaponResearch) => {
|
||||
if (visited.has(wr.research.id)) return;
|
||||
visited.add(wr.research.id);
|
||||
wr.researchRequired?.forEach(req => {
|
||||
const dep = weapon.researches.find((w: IWeaponResearch) => w.research.id === req.id);
|
||||
if (dep) visit(dep);
|
||||
});
|
||||
ordered.push(wr);
|
||||
};
|
||||
weapon.researches.forEach(visit);
|
||||
return ordered.map((r: IWeaponResearch) => {
|
||||
const rs = r.research;
|
||||
const selected = enabledResearchIds.has(rs.id);
|
||||
return (
|
||||
<ToggleButton
|
||||
key={rs.id}
|
||||
value={rs.id}
|
||||
size="small"
|
||||
selected={selected}
|
||||
onChange={() => {
|
||||
const next = new Set(enabledResearchIds);
|
||||
if (next.has(rs.id)) {
|
||||
next.delete(rs.id);
|
||||
// researches that require this one (directly or transitively) can no longer be applied
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
weapon.researches.forEach((wr: IWeaponResearch) => {
|
||||
if (next.has(wr.research.id) && wr.researchRequired?.some(req => !next.has(req.id))) {
|
||||
next.delete(wr.research.id);
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
next.add(rs.id);
|
||||
// research can only be applied if its required researches are done
|
||||
// (requirements can have their own requirements, so enable transitively)
|
||||
const queue = [...(r.researchRequired ?? [])];
|
||||
while (queue.length > 0) {
|
||||
const req = queue.pop()!;
|
||||
if (!next.has(req.id)) {
|
||||
next.add(req.id);
|
||||
const wr = weapon.researches.find((w: IWeaponResearch) => w.research.id === req.id);
|
||||
wr?.researchRequired?.forEach(rr => queue.push(rr));
|
||||
}
|
||||
}
|
||||
}
|
||||
setEnabledResearchIds(next);
|
||||
}}
|
||||
sx={{
|
||||
color: textColor,
|
||||
borderRadius: '8px',
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'rgba(255,255,255,0.23)' : 'rgba(0,0,0,0.23)',
|
||||
textTransform: 'none',
|
||||
'&.Mui-selected': {
|
||||
borderColor: isDark ? 'rgba(25, 118, 210, 0.7)' : 'rgba(25, 118, 210, 0.5)',
|
||||
color: textColor,
|
||||
backgroundColor: isDark ? 'rgba(25, 118, 210, 0.3)' : 'rgba(25, 118, 210, 0.15)',
|
||||
'&:hover': {
|
||||
backgroundColor: isDark ? 'rgba(25, 118, 210, 0.4)' : 'rgba(25, 118, 210, 0.22)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{rs.icon && <img style={{height: 20, marginRight: 6, verticalAlign: 'middle'}} src={getIcon(rs.icon)} alt={rs.name}/>}
|
||||
{rs.name}
|
||||
</ToggleButton>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<TableContainer>
|
||||
{isMobile ? (() => {
|
||||
const items = props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ? [
|
||||
|
||||
@ -2,5 +2,6 @@ export interface IResearchShort {
|
||||
id: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
uiIndexHint: number;
|
||||
buildingId: number;
|
||||
}
|
||||
@ -141,6 +141,18 @@ export interface IShortWeapon {
|
||||
isMeleeWeapon: boolean
|
||||
}
|
||||
|
||||
export interface IResearchEffect {
|
||||
field: string;
|
||||
usageType: string;
|
||||
modifierValue: number;
|
||||
}
|
||||
|
||||
export interface IWeaponResearch {
|
||||
research: IResearchShort;
|
||||
researchRequired: IResearchShort[];
|
||||
effects: IResearchEffect[];
|
||||
}
|
||||
|
||||
export interface IWeapon {
|
||||
id: number
|
||||
name: string
|
||||
@ -172,6 +184,7 @@ export interface IWeapon {
|
||||
modifiers: IModifier[]
|
||||
weaponArmorPiercing: IPiercing[]
|
||||
affectedResearches: IResearchShort[],
|
||||
researches: IWeaponResearch[],
|
||||
hotkey?: string,
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user