Compare commits

..
12 Commits
Author SHA1 Message Date
anibus eebe50a373 Add some params
- start jump charge
- building limit
- movement params

Rework weapon dps one hit and add dps move
2026-10-04 02:58:50 +03:00
anibus fa0baa62c1 Add on/off weapon research dps and params tables feature 2026-09-03 20:22:01 +03:00
anibus 23bfad8956 Moved to next js ssr (with fixes) 2026-08-15 18:42:06 +03:00
anibus 2184f1642c Moved to next js ssr 2026-08-15 14:46:19 +03:00
anibus 4e4f798d85 Fix reload time 2026-08-15 09:17:26 +03:00
anibus 5cbaab4b1f Улучшение дизайна после телепорт фичи 2026-08-02 14:24:49 +03:00
anibus 03f152fbad Add filename to entites 2026-08-01 22:11:20 +03:00
anibus 405b562f8f Add teleport feature 2026-08-01 21:43:14 +03:00
anibus 57779cacbd Vibe-code дизайн fix 2026-07-23 11:15:57 +03:00
anibus c750e19443 Vibe-code дизайн light theme 2026-07-21 03:55:59 +03:00
anibus b603732dd6 Vibe-code дизайн dark theme re-vibecode 2026-07-21 02:27:22 +03:00
anibus 63455578b1 Vibe-code дизайн fix (2) 2026-07-17 14:09:30 +03:00
78 changed files with 3493 additions and 19831 deletions
+5 -2
View File
@@ -1,2 +1,5 @@
#REACT_APP_HOST_URL=https://wiki-backend.dawn-of-war.pro
REACT_APP_HOST_URL=http://localhost:8082
#NEXT_PUBLIC_HOST_URL=http://localhost:8082
NEXT_PUBLIC_HOST_URL=https://wiki-backend.dawn-of-war.pro
HOST_URL=http://localhost:8082
NEXT_PUBLIC_SITE_URL=https://wiki.dawn-of-war.pro
+4 -1
View File
@@ -2,6 +2,7 @@
# dependencies
/node_modules
/.next
/.pnp
.pnp.js
@@ -10,7 +11,9 @@
# production
/build
/deploy
dow-wiki-deploy.tar.gz
tsconfig.tsbuildinfo
# misc
.DS_Store
.env.local
+4
View File
@@ -0,0 +1,4 @@
#### Сборка
При сборке дистрибутива формируй архив dow-wiki-deploy.tar.gz
Предварительно удали всё что было до этого в папке deploy и собирай туда
+83
View File
@@ -0,0 +1,83 @@
import type { Metadata, Viewport } from 'next';
import { cookies } from 'next/headers';
import '@/src/index.css';
import '@/src/App.css';
import '@/src/css/Unit.css';
import '@/src/css/Building.css';
import { ThemeProvider } from '@/src/context/ThemeContext';
import AppShell from '@/components/AppShell';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://dow-wiki.example.com';
export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
title: {
default: 'Dawn of War Wiki — юниты, расы и здания всех модов',
template: '%s — Dawn of War Wiki',
},
description:
'Dawn of War wiki. Unification mod wiki. Unification mod unit stats. Supported all popular mods.',
manifest: '/manifest.json',
applicationName: 'Dawn of War Wiki',
verification: {
google: 'Q4XMn2UDpL2xmK7iBfaWQpVN8EPy8oGxN-7HqSKapb0',
},
openGraph: {
type: 'website',
siteName: 'Dawn of War Wiki',
title: 'Dawn of War Wiki — юниты, расы и здания всех модов',
description:
'Dawn of War wiki. Unification mod wiki. Unification mod unit stats. Supported all popular mods.',
images: ['/logo512.png'],
},
twitter: {
card: 'summary_large_image',
},
robots: {
index: true,
follow: true,
},
};
export const viewport: Viewport = {
themeColor: '#000000',
width: 'device-width',
initialScale: 1,
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
// Читаем тему из cookie на сервере, чтобы сразу отрендерить нужную тему без мерцания.
const themeCookie = cookies().get('theme')?.value;
const initialMode = themeCookie === 'light' ? 'light' : 'dark';
return (
<html lang="en" data-theme={initialMode} style={{ colorScheme: initialMode }}>
<body>
{/* Yandex.Metrika */}
<script
type="text/javascript"
dangerouslySetInnerHTML={{
__html:
"(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});",
}}
/>
<noscript>
<div>
<img
src="https://mc.yandex.ru/watch/99502794"
style={{ position: 'absolute', left: -9999 }}
alt=""
/>
</div>
</noscript>
<ThemeProvider initialMode={initialMode}>
<AppShell>{children}</AppShell>
</ThemeProvider>
</body>
</html>
);
}
+43
View File
@@ -0,0 +1,43 @@
import type { Metadata } from 'next';
import ModPageClient from '@/components/pages/ModPageClient';
import { getMod, getUnitsForMod, getBuildingsForMod } from '@/lib/api-server';
import { IRaceBuildings } from '@/src/types/IBuildingShort';
export const dynamic = 'force-dynamic';
interface Props {
params: { modId: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const mod = await getMod(params.modId);
if (!mod) {
return { title: 'Mod' };
}
const title = `${mod.name} (${mod.version})`;
const description = `Dawn of War wiki. ${mod.name} mod. Units, races, buildings and stats.`;
return {
title,
description,
alternates: {
canonical: `/mod/${params.modId}`,
},
openGraph: {
title,
description,
},
};
}
export default async function ModPage({ params }: Props) {
const [mod, racesUnits] = await Promise.all([
getMod(params.modId),
getUnitsForMod(params.modId),
]);
const units = racesUnits ?? [];
let racesBuildings: IRaceBuildings[] = [];
if (units.length <= 10) {
racesBuildings = (await getBuildingsForMod(params.modId)) ?? [];
}
return <ModPageClient initialMod={mod} racesUnits={units} racesBuildings={racesBuildings} />;
}
@@ -0,0 +1,49 @@
import type { Metadata } from 'next';
import BuildingPageClient from '@/components/pages/BuildingPageClient';
import { getBuilding, getMod } from '@/lib/api-server';
import { IBuilding } from '@/src/types/IBuilding';
import { IMod } from '@/src/types/Imod';
export const dynamic = 'force-dynamic';
interface Props {
params: { modId: string; raceId: string; buildingId: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const [mod, building] = await Promise.all([getMod(params.modId), getBuilding(params.buildingId)]);
if (!mod || !building) {
return { title: 'Building' };
}
let buildingName = building.name;
if (!buildingName) {
buildingName = building.filename.replaceAll('_', ' ').replace('.rgd', '');
}
const title = `${buildingName} — ${mod.name}`;
const description = `${buildingName} building in ${mod.name} mod. Stats, production, researches.`;
return {
title,
description,
alternates: {
canonical: `/mod/${params.modId}/race/${params.raceId}/building/${params.buildingId}`,
},
openGraph: {
title,
description,
},
};
}
export default async function BuildingPage({ params }: Props) {
const [mod, building] = await Promise.all([
getMod(params.modId),
getBuilding(params.buildingId),
]);
return (
<BuildingPageClient
key={params.buildingId}
initialBuilding={building as IBuilding | null}
initialMod={mod as IMod | null}
/>
);
}
+46
View File
@@ -0,0 +1,46 @@
import type { Metadata } from 'next';
import RacePageClient from '@/components/pages/RacePageClient';
import { getMod, getRace, getRaceUnits, getRaceBuildings } from '@/lib/api-server';
export const dynamic = 'force-dynamic';
interface Props {
params: { modId: string; raceId: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const [mod, race] = await Promise.all([getMod(params.modId), getRace(params.raceId)]);
if (!mod || !race) {
return { title: 'Race' };
}
const title = `${race.name} — ${mod.name}`;
const description = `${race.name} race in ${mod.name} mod. Units, buildings and stats.`;
return {
title,
description,
alternates: {
canonical: `/mod/${params.modId}/race/${params.raceId}`,
},
openGraph: {
title,
description,
},
};
}
export default async function RacePage({ params }: Props) {
const [mod, race, raceUnits, raceBuildings] = await Promise.all([
getMod(params.modId),
getRace(params.raceId),
getRaceUnits(params.modId, params.raceId),
getRaceBuildings(params.modId, params.raceId),
]);
return (
<RacePageClient
initialMod={mod}
initialRace={race}
raceUnits={raceUnits}
raceBuildings={raceBuildings}
/>
);
}
@@ -0,0 +1,47 @@
import type { Metadata } from 'next';
import UnitPageClient from '@/components/pages/UnitPageClient';
import { getMod, getUnit } from '@/lib/api-server';
import { IUnit } from '@/src/types/IUnit';
import { IMod } from '@/src/types/Imod';
export const dynamic = 'force-dynamic';
interface Props {
params: { modId: string; raceId: string; unitId: string };
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const [mod, unit] = await Promise.all([getMod(params.modId), getUnit(params.unitId)]);
if (!mod || !unit) {
return { title: 'Unit' };
}
const title = `${unit.name} — ${mod.name}`;
const description = unit.description
? `${unit.name} unit in ${mod.name} mod. ${unit.description}`
: `${unit.name} unit in ${mod.name} mod. Stats, weapons, abilities.`;
return {
title,
description,
alternates: {
canonical: `/mod/${params.modId}/race/${params.raceId}/unit/${params.unitId}`,
},
openGraph: {
title,
description,
},
};
}
export default async function UnitPage({ params }: Props) {
const [mod, unit] = await Promise.all([
getMod(params.modId),
getUnit(params.unitId),
]);
return (
<UnitPageClient
key={params.unitId}
initialUnit={unit as IUnit | null}
initialMod={mod as IMod | null}
/>
);
}
+21
View File
@@ -0,0 +1,21 @@
import type { Metadata } from 'next';
import ModsPageClient from '@/components/pages/ModsPageClient';
import { getMods } from '@/lib/api-server';
import { IMod } from '@/src/types/Imod';
export const metadata: Metadata = {
title: 'Dawn of War Wiki — юниты, расы и здания всех модов',
description:
'Dawn of War wiki. Unification mod wiki. Unification mod unit stats. Supported all popular mods.',
alternates: {
canonical: '/',
},
};
export const dynamic = 'force-dynamic';
export default async function HomePage() {
const mods = (await getMods()) ?? [];
const visibleMods = mods.filter(mod => !mod.isHide);
return <ModsPageClient initialMods={visibleMods} />;
}
+12
View File
@@ -0,0 +1,12 @@
import type { MetadataRoute } from 'next';
import { SITE_URL } from '@/lib/api-server';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
},
sitemap: `${SITE_URL}/sitemap.xml`,
};
}
+78
View File
@@ -0,0 +1,78 @@
import type { MetadataRoute } from 'next';
import {
getBuildingsForMod,
getMods,
getUnitsForMod,
SITE_URL,
} from '@/lib/api-server';
export const dynamic = 'force-dynamic';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const mods = (await getMods()) ?? [];
const modEntries: MetadataRoute.Sitemap = mods.map((mod) => ({
url: `${SITE_URL}/mod/${mod.id}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.8,
}));
const entries: MetadataRoute.Sitemap = [...modEntries];
// Gather races/units/buildings per mod (limit parallelism)
const details = await Promise.all(
mods.slice(0, 20).map(async (mod) => {
const [units, buildings] = await Promise.all([
getUnitsForMod(mod.id),
getBuildingsForMod(mod.id),
]);
return { mod, units, buildings };
})
);
for (const { mod, units, buildings } of details) {
const raceIds = new Set<string>();
for (const unitGroup of units ?? []) raceIds.add(unitGroup.race.id);
for (const buildingGroup of buildings ?? []) raceIds.add(buildingGroup.race.id);
for (const raceId of raceIds) {
entries.push({
url: `${SITE_URL}/mod/${mod.id}/race/${raceId}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.7,
});
}
for (const unitGroup of units ?? []) {
for (const unit of [...unitGroup.infantry, ...unitGroup.support, ...unitGroup.tech]) {
entries.push({
url: `${SITE_URL}/mod/${mod.id}/race/${unitGroup.race.id}/unit/${unit.id}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
});
}
}
for (const buildingGroup of buildings ?? []) {
for (const building of [...buildingGroup.buildings, ...buildingGroup.buildingsAdvanced]) {
entries.push({
url: `${SITE_URL}/mod/${mod.id}/race/${buildingGroup.race.id}/building/${building.id}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
});
}
}
}
return [
{
url: SITE_URL,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...entries,
];
}
+67
View 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
}
+112
View File
@@ -0,0 +1,112 @@
'use client';
import React from 'react';
import {
AppBar,
Box,
Container,
IconButton,
Toolbar,
Typography,
} from '@mui/material';
import Link from 'next/link';
import { styled } from '@mui/material/styles';
import { useThemeContext } from '@/src/context/ThemeContext';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
// Styled components
const StyledAppBar = styled(AppBar)(({ theme }) => ({
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
}));
const StyledAppBarLight = styled(AppBar)(({ theme }) => ({
background: 'linear-gradient(135deg, #ffffff 0%, #f0f0f0 50%, #e0e0e0 100%)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.1)',
borderBottom: '1px solid rgba(0, 0, 0, 0.1)',
}));
const LogoText = styled(Typography)(({ theme }) => ({
fontSize: '1.5rem',
fontWeight: 800,
background: theme.palette.mode === 'dark'
? 'linear-gradient(135deg, #dee2e6 0%, #cccccc 100%)'
: 'linear-gradient(135deg, #1a1a2e 0%, #333333 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
letterSpacing: '-0.5px',
cursor: 'pointer',
'&:hover': {
transform: 'scale(1.05)',
transition: 'transform 0.2s ease',
},
}));
const ThemeToggle = styled(IconButton)(({ theme }) => ({
color: 'inherit',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
}));
const ThemeToggleLight = styled(IconButton)(({ theme }) => ({
color: 'inherit',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.05)',
},
}));
const LogoLink = styled(Link)({
display: 'flex',
alignItems: 'center',
gap: 8,
textDecoration: 'none',
color: 'inherit',
});
export default function AppShell({ children }: { children: React.ReactNode }) {
const { mode, toggleTheme } = useThemeContext();
const isDark = mode === 'dark';
return (
<Box sx={{ minHeight: '100vh' }}>
{isDark ? (
<StyledAppBar position="sticky">
<Container maxWidth="lg" sx={{ px: { xs: 1, sm: 3, md: 3 } }}>
<Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
<LogoLink href="/">
<img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} />
<LogoText variant="h6">
Wiki
</LogoText>
</LogoLink>
<ThemeToggle onClick={toggleTheme} size="large">
<LightModeIcon />
</ThemeToggle>
</Toolbar>
</Container>
</StyledAppBar>
) : (
<StyledAppBarLight position="sticky">
<Container maxWidth="lg" sx={{ px: { xs: 1, sm: 3, md: 3 } }}>
<Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
<LogoLink href="/">
<img src="/images/dowdelogo.png" alt="logo" style={{ height: 32, cursor: 'pointer' }} />
<LogoText variant="h6">
Wiki
</LogoText>
</LogoLink>
<ThemeToggleLight onClick={toggleTheme} size="large">
<DarkModeIcon sx={{ color: '#000000' }} />
</ThemeToggleLight>
</Toolbar>
</Container>
</StyledAppBarLight>
)}
<Container maxWidth="lg" sx={{ py: 4, px: { xs: 1, sm: 3, md: 3 } }}>
{children}
</Container>
</Box>
);
}
+362
View File
@@ -0,0 +1,362 @@
'use client';
import React, { useEffect, useState } from 'react';
import {
Box,
Container,
Divider,
Grid2,
Link,
ListItem,
Paper,
Table,
TableBody,
TableCell,
TableRow,
Tooltip,
Typography,
LinearProgress,
Theme,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';
import { useParams } from 'next/navigation';
import { ArrowBack } from '@mui/icons-material';
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
import { AvailableBuildings, AvailableMods, AvailableUnits, IconUrl } from '@/src/core/api';
import { IShortWeapon } from '@/src/types/IUnit';
import { IRaceUnits } from '@/src/types/IUnitShort';
import { IRaceBuildings } from '@/src/types/IBuildingShort';
import '@/src/css/Building.css';
import ArmorType from '@/src/classes/ArmorType';
import WeaponSlot from '@/src/classes/WeaponSlot';
import UnitsTable from '@/src/classes/UnitsTable';
import { IMod } from '@/src/types/Imod';
import { IBuilding } from '@/src/types/IBuilding';
import Vision from '@/src/classes/Vision';
import BuildingAddon from '@/src/classes/building/BuildingAddon';
import { IUnitShort } from '@/src/types/IUnitShort';
import Research, { AffectedResearches } from '@/src/classes/building/Research';
import Required from '@/src/classes/Required';
import { ModifiersProvidesTable } from '@/src/classes/ModifiersProvideTable';
import Ability from '@/src/classes/Ability';
import DeathExplosion from '@/src/classes/DeathExplosion';
import { DescriptionBox } from '@/src/commons/DescriptionBox';
import { BackButton } from '@/src/commons/BackButton';
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
fontSize: '1.25rem',
}));
const StatsPaper = styled(Paper)(({ theme }) => ({
background: theme.palette.mode === 'dark'
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: theme.palette.text.primary,
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
fontWeight: 600,
},
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
my: 3,
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
}));
const BuildingTitle = styled(Typography)(({ theme }) => ({
fontWeight: 800,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 2,
}));
const BuildingSubtitle = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
fontWeight: 500,
}));
const UnitLink = styled(Link)(({ theme }) => ({
color: theme.palette.text.primary,
textDecoration: 'none',
'&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' },
}));
const UnitListItem = styled(ListItem)(({ theme }) => ({
color: theme.palette.text.primary,
padding: '8px 12px',
background: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)',
borderRadius: '8px',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'}`,
transition: 'all 0.2s ease',
'&:hover': { background: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.08)', borderColor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)' },
}));
function Unit(unit: IUnitShort, modId: number, raceId: string, theme: Theme) {
return (<Grid2 key={unit.id} size={{ xs: 12, md: 3 }}>
<UnitLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
<UnitListItem>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')} />}
{unit.name}
{unit.canDetect && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/DETECT_YES.webp" /></span>}
</UnitListItem>
</UnitLink>
</Grid2>)
}
function Building(building: IBuilding, mod: IMod, theme: Theme, racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[]) {
const isDark = theme.palette.mode === 'dark';
let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = 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)
}
})
var buildingName = building.name;
if (building.name == null) {
buildingName = building.filename.replaceAll('_', ' ').replace('.rgd', '');
}
return (
<Box>
<Box sx={{ mb: 3 }}>
<BuildingTitle variant="h4">
{building.icon &&
<img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')} />}
{buildingName}
</BuildingTitle>
<BuildingSubtitle variant="subtitle1">
{mod.name} ({mod.version})
</BuildingSubtitle>
</Box>
<Grid2 container spacing={3}>
<Grid2 size={{ xs: 12, md: 4 }}>
<StatsPaper elevation={0}>
<Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table">
<TableRow>
<TableCell component="th" scope="row" >Cost</TableCell>
<TableCell>
{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>
{(building.requisitionIncome !== undefined && building.requisitionIncome > 0 || building.powerIncome !== undefined && building.powerIncome !== null || building.faithIncome !== undefined && building.faithIncome !== null) &&
<TableRow>
<TableCell >Resource income</TableCell>
<TableCell>
{building.requisitionIncome !== undefined && building.requisitionIncome != null &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_requisition.gif" />&nbsp;
{building.requisitionIncome}</span>}
{building.powerIncome !== undefined && building.powerIncome > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_power.gif" />&nbsp;
{building.powerIncome}</span>}
{building.faithIncome !== undefined && building.faithIncome > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_faith.gif" />&nbsp;
{building.faithIncome}</span>}
</TableCell>
</TableRow>
}
<TableRow>
<TableCell component="th" scope="row" >Armor type</TableCell>
<TableCell>
{building.armorType2 == null ? <ArmorType name={building.armorType.name} compact={false} />
: <Tooltip title={"upgrade/debuff can turns to " + building.armorType2.name}><span><ArmorType name={building.armorType.name} compact={false} /></span></Tooltip>}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" >Health</TableCell>
<TableCell>
<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>
<TableCell component="th" scope="row" >Sight</TableCell>
<TableCell>
<Vision sight={building.sightRadius} detect={building.detectRadius} />
</TableCell>
</TableRow>
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row" >Repair max</TableCell>
<TableCell>{building.repairMax}</TableCell>
</TableRow>
{building.buildingLimit !== undefined && building.buildingLimit !== null &&
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell component="th" scope="row" >Limit</TableCell>
<TableCell>{building.buildingLimit}</TableCell>
</TableRow>
}
</TableBody>
</Table>
</StatsPaper>
</Grid2>
<Grid2 size={{ xs: 12, md: 8 }}>
<DescriptionBox>
{building.description}
</DescriptionBox>
</Grid2>
{building.requirements !== null &&
<Grid2 size={{ xs: 12, md: 12 }}>
<Required requirement={building.requirements} modId={building.modId} raceId={building.race.id} />
</Grid2>}
{building.modifiers.length > 0 &&
<Grid2 size={{ xs: 12, md: 12 }}>
<SectionTitle>Affected on</SectionTitle>
<ModifiersProvidesTable modifiers={building.modifiers} race={building.race} modId={building.modId} affectedData={building.affectedData} />
</Grid2>}
{building.units.length > 0 && <Grid2 size={12}>
<SectionTitle>Unit production</SectionTitle>
<Grid2 container spacing={2}>
{building.units.map(unit =>
Unit(unit, mod.id, building.race.id, theme)
)}
</Grid2>
</Grid2>}
{building.abilities.length > 0 && <Grid2 size={12}>
<SectionTitle>Abilities</SectionTitle>
{building.abilities.map(a =>
<Ability mod={mod} ability={a} race={building.race} />
)}
</Grid2>}
{building.deathExplosions.length > 0 &&
<Grid2 size={12}>
<DeathExplosion deathExplosions={building.deathExplosions} mod={mod} />
</Grid2>
}
{building.addons.length > 0 && <Grid2 size={12}>
<SectionTitle>Addons</SectionTitle>
{building.addons.map(b =>
<BuildingAddon mod={mod} addon={b} building={building} />
)}
</Grid2>}
{building.researches.length > 0 && <Grid2 size={12}>
<SectionTitle>Researches</SectionTitle>
{building.researches.map(r =>
<Research key={r.id} research={r} building={building} mod={mod} />
)}
</Grid2>}
<Grid2 size={12}>
{[...mapBuildingWeapons.keys()].sort(function (a, b) {
return a - b;
}).map(h => <WeaponSlot race={building.race} mod={mod} unitWeapons={mapBuildingWeapons.get(h)} hardpoint={h} />)}
</Grid2>
<Grid2 size={12}>
<AffectedResearches researches={building.affectedResearches} modId={mod.id} raceId={building.race.id} />
</Grid2>
<b className="hotkey" >Hotkey: {building.hotkey} <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {building.filename}</span></b>
</Grid2>
<StyledDivider />
<UnitsTable modId={mod.id} racesUnits={racesUnits} racesBuildings={racesBuildings} />
</Box>)
}
export default function BuildingPageClient({
initialBuilding,
initialMod,
}: {
initialBuilding: IBuilding | null;
initialMod: IMod | null;
}) {
const params = useParams() as Record<string, string>;
const { modId, raceId, buildingId } = params;
const theme = useTheme();
const [building, setBuilding] = useState<IBuilding | null>(initialBuilding);
const [mod, setMod] = useState<IMod | null>(initialMod);
const [racesUnits, setRacesUnits] = useState<IRaceUnits[]>([]);
const [racesBuildings, setRacesBuildings] = useState<IRaceBuildings[]>([]);
useEffect(() => {
let cancelled = false;
if (building == null) {
fetch(AvailableBuildings + "/" + buildingId)
.then(res => res.json())
.then((res: IBuilding) => { if (!cancelled) setBuilding(res); });
}
if (mod == null) {
fetch(AvailableMods + "/" + modId)
.then(res => res.json())
.then((res: IMod) => { if (!cancelled) setMod(res); });
}
fetch(AvailableUnits + "/mod/" + modId)
.then(res => res.json())
.then((resUnits: IRaceUnits[]) => {
if (cancelled) return;
setRacesUnits(resUnits);
if (resUnits.length <= 10) {
fetch(AvailableBuildings + "/mod/" + modId)
.then(res => res.json())
.then((resBuildings: IRaceBuildings[]) => {
if (!cancelled) setRacesBuildings(resBuildings);
});
}
});
return () => { cancelled = true; };
}, [modId, raceId, buildingId]);
if (building != null && mod != null) {
const backRef = "/mod/" + modId + "/race/" + raceId;
return (
<Container maxWidth="lg">
<BackButton
variant="outlined"
startIcon={<ArrowBack />}
href={backRef}
>
Back to race
</BackButton>
{Building(building, mod, theme, racesUnits, racesBuildings)}
</Container>
);
} else {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
);
}
}
+71
View File
@@ -0,0 +1,71 @@
'use client';
import React from 'react';
import { ArrowBack } from '@mui/icons-material';
import {
Box,
Container,
Typography,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';
import { IMod } from '@/src/types/Imod';
import { IRaceUnits } from '@/src/types/IUnitShort';
import { IRaceBuildings } from '@/src/types/IBuildingShort';
import UnitsTable from '@/src/classes/UnitsTable';
import { BackButton } from '@/src/commons/BackButton';
const ModNotFound = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
interface ModPageClientProps {
initialMod: IMod | null;
racesUnits: IRaceUnits[];
racesBuildings: IRaceBuildings[];
}
export default function ModPageClient({ initialMod, racesUnits, racesBuildings }: ModPageClientProps) {
const theme = useTheme();
if (initialMod != null) {
const isDark = theme.palette.mode === 'dark';
return (
<Container maxWidth="lg">
<BackButton
variant="outlined"
startIcon={<ArrowBack />}
href="/"
>
Back to mods list
</BackButton>
<Typography variant="h3" component="h1" sx={{
fontWeight: 800,
mb: 1,
WebkitBackgroundClip: isDark ? 'text' : 'initial',
WebkitTextFillColor: isDark ? '#dee2e6' : 'initial',
color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
}}>
{initialMod.name} <Box component="span" sx={{
fontSize: '0.5em',
color: theme.palette.text.secondary,
fontWeight: 500,
}}>({initialMod.version})</Box>
</Typography>
<Box>
<UnitsTable modId={initialMod.id} racesUnits={racesUnits} racesBuildings={racesBuildings} />
</Box>
</Container>
);
} else {
return (
<Container maxWidth="lg" sx={{ py: 8, textAlign: 'center' }}>
<ModNotFound variant="h5">
Mod not found
</ModNotFound>
</Container>
);
}
}
+248
View File
@@ -0,0 +1,248 @@
'use client';
import React, { useEffect } from 'react';
import {
Box,
Typography,
Grid,
Card,
CardContent,
CardActions,
Chip,
Button,
useTheme,
Link,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { IMod } from '@/src/types/Imod';
const ModCard = styled(Card)(({ theme }) => ({
height: '100%',
display: 'flex',
flexDirection: 'column',
background: theme.palette.mode === 'dark'
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: '16px',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: theme.palette.mode === 'dark'
? 'linear-gradient(90deg, #dee2e6 0%, #cccccc 100%)'
: 'linear-gradient(90deg, #000000 0%, #333333 100%)',
transform: 'scaleX(0)',
transition: 'transform 0.3s ease',
},
'&:hover': {
boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
'&::before': {
transform: 'scaleX(1)',
},
},
}));
const VersionLink = styled(Link)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
padding: theme.spacing(1.5, 2),
marginBottom: theme.spacing(1),
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)',
borderRadius: '10px',
textDecoration: 'none',
color: 'inherit',
transition: 'all 0.2s ease',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
'&:hover': {
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
},
}));
const ModTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 1,
background: theme.palette.mode === 'dark'
? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
: 'none',
WebkitBackgroundClip: theme.palette.mode === 'dark' ? 'text' : 'initial',
WebkitTextFillColor: theme.palette.mode === 'dark' ? '#dee2e6' : 'initial',
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
}));
const VersionsChip = styled(Chip)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)',
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
fontWeight: 600,
}));
const VersionText = styled(Typography)(({ theme }) => ({
fontWeight: 600,
color: theme.palette.text.secondary,
}));
const VersionArrow = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
const BetaVersionsLabel = styled(Typography)(({ theme }) => ({
color: 'rgba(255, 193, 7, 0.9)',
fontWeight: 600,
mb: 1,
}));
interface ModsPageClientProps {
initialMods: IMod[];
}
export default function ModsPageClient({ initialMods }: ModsPageClientProps) {
const theme = useTheme();
// Очистка кэша сайта при заходе на главную страницу (один раз за сессию)
useEffect(() => {
if (!sessionStorage.getItem('cache_cleared')) {
sessionStorage.setItem('cache_cleared', '1');
(async () => {
try {
if ('caches' in window) {
const names = await caches.keys();
await Promise.all(names.map(name => caches.delete(name)));
}
if ('serviceWorker' in navigator) {
const regs = await navigator.serviceWorker.getRegistrations();
await Promise.all(regs.map(r => r.unregister()));
}
} catch (e) {
console.error('Cache clear failed:', e);
}
})();
}
}, []);
// Sort all mods by order, then group by name
const sortedMods = [...initialMods].sort((a, b) => a.order - b.order);
const mapWithModVersions = new Map<string, IMod[]>();
sortedMods.forEach(mod => {
const versionList = mapWithModVersions.get(mod.name);
if (versionList == null) {
mapWithModVersions.set(mod.name, [mod]);
} else {
versionList.push(mod);
}
});
const getLatestVersion = (modName: string) => {
const sameMods = mapWithModVersions.get(modName) ?? [];
// The latest version is the one with the maximum id
const latest = sameMods.reduce((max, mod) => mod.id > max.id ? mod : max, sameMods[0]);
const betaMods = sameMods.filter(m => m.isBeta);
return {
latest,
allVersions: sameMods,
hasBeta: betaMods.length > 0,
betaVersion: betaMods[0],
};
};
function ModCardComponent(modName: string) {
const sameMods = mapWithModVersions.get(modName) ?? [];
const { latest, hasBeta, betaVersion } = getLatestVersion(modName);
return (
<ModCard>
<CardContent sx={{ flexGrow: 1, p: 3 }}>
<Box sx={{ mb: 2 }}>
<ModTitle variant="h5">
{modName}
</ModTitle>
</Box>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<VersionsChip
label={`${sameMods.length} versions`}
size="small"
/>
</Box>
{sameMods.filter(m => !m.isBeta).map(mod => (
<VersionLink key={mod.id} href={"/mod/" + mod.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<VersionText variant="body2">
Version {mod.version}
</VersionText>
<VersionArrow variant="caption">
→
</VersionArrow>
</Box>
</VersionLink>
))}
{hasBeta && (
<Box sx={{ mt: 2 }}>
<BetaVersionsLabel variant="subtitle2">
Beta versions:
</BetaVersionsLabel>
{betaVersion && (
<VersionLink href={"/mod/" + betaVersion.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Version {betaVersion.version} (Beta)
</Typography>
<VersionArrow variant="caption">
→
</VersionArrow>
</Box>
</VersionLink>
)}
</Box>
)}
</CardContent>
<CardActions sx={{ px: 3, pb: 3 }}>
<Button
size="medium"
variant="outlined"
href={"/mod/" + latest?.id}
sx={{
width: '100%',
color: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 215, 0, 0.5)' : 'rgba(25, 118, 210, 0.5)',
fontWeight: 600,
borderRadius: '10px',
textTransform: 'none',
fontSize: '1rem',
py: 1.5,
'&:hover': {
borderColor: theme.palette.mode === 'dark' ? '#FFD700' : '#1976d2',
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 215, 0, 0.1)' : 'rgba(25, 118, 210, 0.1)',
boxShadow: theme.palette.mode === 'dark' ? '0 8px 20px rgba(255, 215, 0, 0.2)' : '0 8px 20px rgba(25, 118, 210, 0.2)',
},
}}
>
Open latest {latest?.version ?? ''}
</Button>
</CardActions>
</ModCard>
);
}
return (
<Box>
<Grid container spacing={3}>
{[...new Set(sortedMods.map(m => m.name))].map((modName) => (
<Grid item xs={12} sm={6} md={4}>
{ModCardComponent(modName)}
</Grid>
))}
</Grid>
</Box>
);
}
+262
View File
@@ -0,0 +1,262 @@
'use client';
import React from 'react';
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Container,
Divider,
Grid2,
Link,
List,
ListItem,
Paper,
Typography,
Theme,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';
import { ArrowBack, ExpandMore } from '@mui/icons-material';
import { IconUrl } from '@/src/core/api';
import { IMod } from '@/src/types/Imod';
import { Irace } from '@/src/types/Irace';
import '@/src/css/Unit.css';
import { IRaceUnits, IUnitShort } from '@/src/types/IUnitShort';
import { IBuildingShort, IRaceBuildings } from '@/src/types/IBuildingShort';
import { StyledLink } from '@/src/commons/StyledLink';
import { BackButton } from '@/src/commons/BackButton';
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
fontSize: '1.25rem',
}));
const UnitCard = styled(Paper)(({ theme }) => ({
background: theme.palette.mode === 'dark'
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: '16px',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: theme.palette.mode === 'dark' ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
},
}));
const BuildingCard = styled(UnitCard)(({ theme }) => ({
padding: theme.spacing(1),
}));
const UnitLink = styled(Link)(({ theme }) => ({
color: theme.palette.text.primary,
textDecoration: 'none',
display: 'block',
'&:hover': { color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }
}));
const LoadingText = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
}));
function Unit(unit: IUnitShort, modId: number, raceId: string, theme: Theme) { const isDark = theme.palette.mode === 'dark';
return (
<UnitLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
<ListItem sx={{
color: theme.palette.text.primary,
padding: '1.5rem 2rem',
marginBottom: '0.5rem',
background: isDark
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
borderRadius: '16px',
border: `1px solid ${isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
'&:hover': {
boxShadow: isDark ? '0 20px 40px rgba(255, 255, 255, 0.1)' : '0 20px 40px rgba(0, 0, 0, 0.2)',
borderColor: isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)',
}
}}>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')} />}
{unit.name}
{unit.canDetect && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/DETECT_YES.webp" /></span>}
</ListItem>
</UnitLink>
)
}
function UnitSmall(unit: IUnitShort, modId: number, raceId: string) {
var unitName = ""
if (unit.name.length > 21) {
unitName = unit.name.substring(0, 19) + "...";
} else {
unitName = unit.name;
}
return (<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} >
{unit.icon && <img className="unitIconSmall" src={IconUrl + unit.icon.replaceAll('\\', '/')} />}
&nbsp;<span style={{ fontSize: 14 }}>{unitName}</span>
{unit.canDetect && <span>&nbsp;<img
src="/images/DETECT_YES.webp" /></span>}<br /></StyledLink>)
}
function Building(building: IBuildingShort, modId: number, raceId: string, theme: Theme) {
const isDark = theme.palette.mode === 'dark';
return (<Grid2 size={{ xs: 12, md: 3 }}>
<Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id} sx={{
textDecoration: 'none',
display: 'block',
'&:hover': { color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }
}}>
<BuildingCard elevation={0}>
<ListItem sx={{}}>
{building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')} />}
&nbsp;<StyledLink>{building.name}</StyledLink>
{building.canDetect && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/DETECT_YES.webp" /></span>}
</ListItem>
{building.units.map(unit => UnitSmall(unit, modId, raceId))}
</BuildingCard>
</Link>
</Grid2>)
}
interface UnitsProps {
raceId: string;
modId: number;
units: IRaceUnits | null;
buildings: IRaceBuildings | null;
}
function Units({ raceId, modId, units, buildings }: UnitsProps) {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
if (units) {
const accordionSx = isDark ? {
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
} : {
background: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: '1px solid rgba(0, 0, 0, 0.1)',
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
};
return (buildings != null ?
<Box>
<Grid2 container spacing={2}>
{buildings.buildings.map(building => Building(building, modId, raceId, theme))}
</Grid2>
{buildings.buildingsAdvanced.length > 0 && <Box sx={{ mt: 3 }}>
<Divider sx={{ my: 2, borderColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)' }} />
<SectionTitle>Advanced buildings</SectionTitle>
<Grid2 container spacing={2}>
{buildings.buildingsAdvanced.map(building => Building(building, modId, raceId, theme))}
</Grid2><br /></Box>}
<Box sx={{ mt: 3 }}>
<Accordion sx={accordionSx}>
<AccordionSummary
expandIcon={<ExpandMore sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }} />}
aria-controls="units-accordion"
id="units-accordion"
sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' } }}
>
<SectionTitle sx={{ mb: 0, display: 'flex', alignItems: 'center' }}>All units</SectionTitle>
</AccordionSummary>
<AccordionDetails>
<Grid2 container spacing={2}>
<Grid2 size={{ xs: 12, md: 4 }}>
<Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', fontWeight: 600, mb: 2 }}>Infantry</Typography>
{units.infantry.map(unit => Unit(unit, modId, raceId, theme))}
</Grid2>
<Grid2 size={{ xs: 12, md: 4 }}>
<Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', fontWeight: 600, mb: 2 }}>Tech</Typography>
<List>
{units.tech.map(unit => Unit(unit, modId, raceId, theme))}
</List>
</Grid2>
<Grid2 size={{ xs: 12, md: 4 }}>
<Typography variant="h6" sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', fontWeight: 600, mb: 2 }}>Support</Typography>
<List>
{units.support.map(unit => Unit(unit, modId, raceId, theme))}
</List>
</Grid2>
</Grid2>
</AccordionDetails>
</Accordion>
</Box>
</Box> : <LoadingText>Loading</LoadingText>
)
} else {
return <LoadingText>Loading...</LoadingText>;
}
}
interface RacePageClientProps {
initialMod: IMod | null;
initialRace: Irace | null;
raceUnits: IRaceUnits | null;
raceBuildings: IRaceBuildings | null;
}
export default function RacePageClient({ initialMod, initialRace, raceUnits, raceBuildings }: RacePageClientProps) {
const theme = useTheme();
if (initialMod != null && initialRace != null) {
const backRef = "/mod/" + initialMod.id;
const isDark = theme.palette.mode === 'dark';
return (
<Container maxWidth="lg">
<BackButton
variant="outlined"
startIcon={<ArrowBack />}
href={backRef}
>
Back to mod
</BackButton>
<Box sx={{ mb: 4 }}>
<Typography variant="h3" component="h1" sx={{
fontWeight: 800,
mb: 1,
background: isDark
? 'linear-gradient(135deg, #dee2e6 0%, #e0e0e0 100%)'
: 'none',
WebkitBackgroundClip: isDark ? 'text' : 'initial',
WebkitTextFillColor: isDark ? '#dee2e6' : 'initial',
color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
}}>
{initialRace.name}
</Typography>
<Typography variant="h6" sx={{
color: theme.palette.text.secondary,
fontWeight: 500,
}}>
{initialMod.name} ({initialMod.version})
</Typography>
</Box>
<Units raceId={initialRace.id} modId={initialMod.id} units={raceUnits} buildings={raceBuildings} />
</Container>
);
} else {
return (
<Container maxWidth="lg">
<LoadingText variant="h5" sx={{ py: 8, textAlign: 'center' }}>
Race not found
</LoadingText>
</Container>
);
}
}
+467
View File
@@ -0,0 +1,467 @@
'use client';
import React, { useEffect, useState } from 'react';
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Container,
Divider,
Grid2,
LinearProgress,
Paper,
Table,
TableBody,
TableCell,
TableRow,
Tooltip,
Typography,
Theme,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { useTheme } from '@mui/material/styles';
import { useParams } from 'next/navigation';
import { ArrowBack, ExpandMore } from '@mui/icons-material';
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
import { AvailableBuildings, AvailableMods, AvailableUnits, IconUrl } from '@/src/core/api';
import { IShortWeapon, IUnit } from '@/src/types/IUnit';
import { IRaceUnits } from '@/src/types/IUnitShort';
import { IRaceBuildings } from '@/src/types/IBuildingShort';
import '@/src/css/Unit.css';
import ArmorType from '@/src/classes/ArmorType';
import Sergeant from '@/src/classes/Sergeant';
import WeaponSlot from '@/src/classes/WeaponSlot';
import UnitsTable from '@/src/classes/UnitsTable';
import { IMod } from '@/src/types/Imod';
import Vision from '@/src/classes/Vision';
import Required from '@/src/classes/Required';
import { AffectedResearches } from '@/src/classes/building/Research';
import { ModifiersProvidesTable } from '@/src/classes/ModifiersProvideTable';
import Ability from '@/src/classes/Ability';
import DeathExplosion from '@/src/classes/DeathExplosion';
import Jump from '@/src/classes/Jump';
import { DescriptionBox } from '@/src/commons/DescriptionBox';
import { BackButton } from '@/src/commons/BackButton';
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
fontSize: '1.25rem',
}));
const StatsPaper = styled(Paper)(({ theme }) => ({
background: theme.palette.mode === 'dark'
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root': {
'&:last-child td, &:last-child th': { border: 0 },
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
fontWeight: 600,
},
}));
const SergeantAccordion = styled(Accordion)(({ theme }) => ({
background: theme.palette.mode === 'dark'
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: '12px',
mb: 1,
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
my: 3,
borderColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)',
}));
const UnitTitle = styled(Typography)(({ theme }) => ({
fontWeight: 800,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 2,
}));
const UnitSubtitle = styled(Typography)(({ theme }) => ({
color: theme.palette.text.secondary,
fontWeight: 500,
}));
function Unit(unit: IUnit, mod: IMod, theme: Theme, racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[]) {
const isDark = theme.palette.mode === 'dark';
const morale = (unit.moraleMax !== null) ? <span>
<img style={{ verticalAlign: "top" }} src="/images/ARM_Morale.webp" />&nbsp;{unit.moraleMax}
+{unit.moraleRegeneration}/s
{unit.moraleDeathPenalty > 0 && <span> <img style={{ verticalAlign: "top" }}
src="/images/Kills_icon.webp" /> -{unit.moraleDeathPenalty}</span>}
</span> : "-"
let mapWithUnitWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
unit.weapons.forEach(weapon => {
const weaponMap = mapWithUnitWeapons.get(weapon.hardpoint)
if (weaponMap == null) {
const weaponMap = new Map()
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
mapWithUnitWeapons.set(weapon.hardpoint, weaponMap)
} else {
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
}
})
type sergeantProps = {
name: string
icon: String
canDetect: Boolean
}
const SergeantShort = (props: sergeantProps) => {
return (
<span style={{ fontSize: 20, color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', display: 'flex', alignItems: 'center' }}>
{props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')} />}
&nbsp; {props.name}
{props.canDetect && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/DETECT_YES.webp" /></span>}
</span>
)
}
return (
<Box>
<Box sx={{ mb: 3 }}>
<UnitTitle variant="h4">
{unit.icon &&
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')} />}
{unit.name}
</UnitTitle>
<UnitSubtitle variant="subtitle1">
{mod.name} ({mod.version})
</UnitSubtitle>
</Box>
<Grid2 container spacing={3}>
<Grid2 size={{ xs: 12, md: 4 }}>
<StatsPaper>
<Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table">
<TableRow>
<TableCell component="th" scope="row" >Cost</TableCell>
<TableCell>
{unit.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_requisition.gif" />&nbsp;
{unit.buildCostRequisition.toFixed(0)}</span>}
{unit.buildCostPower > 0 && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_power.gif" />&nbsp;
{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.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;
{unit.capInfantry}</span>}
{unit.capSupport > 0 && <span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_cap_vehicle.gif" />&nbsp;
{unit.capSupport}</span>}
{(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{ verticalAlign: "top", fontSize: "18px" }} />&nbsp;
{unit.buildCostTime}s</span>}
</TableCell>
</TableRow>
{(unit?.reinforceTime !== 0 && unit.reinforceTime !== null && unit.squadMaxSize > 1) &&
<TableRow>
<TableCell>Reinforce cost</TableCell>
<TableCell>
{unit.reinforceCostRequisition && unit.reinforceCostRequisition > 0 ?
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_requisition.gif" />&nbsp;
{unit.reinforceCostRequisition.toFixed(0)}</span> : <span />}
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_power.gif" />&nbsp;
{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.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;
{unit.reinforceTime}s</span>}
</TableCell>
</TableRow>
}
{(unit.requisitionIncome !== undefined && unit.requisitionIncome !== null || unit.powerIncome !== undefined && unit.powerIncome !== null || unit.faithIncome !== undefined && unit.faithIncome !== null) &&
<TableRow>
<TableCell >Resource income</TableCell>
<TableCell>
{unit.requisitionIncome !== undefined && unit.requisitionIncome > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_requisition.gif" />&nbsp;
{unit.requisitionIncome}</span>}
{unit.powerIncome !== undefined && unit.powerIncome > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_power.gif" />&nbsp;
{unit.powerIncome}</span>}
{unit.faithIncome !== undefined && unit.faithIncome > 0 &&
<span>&nbsp;<img style={{ verticalAlign: "top" }}
src="/images/Resource_faith.gif" />&nbsp;
{unit.faithIncome}</span>}
</TableCell>
</TableRow>
}
{unit.squadMaxSize > 1 &&
<TableRow>
<TableCell >Squad size</TableCell>
<TableCell>{unit.squadStartSize} / {unit.squadMaxSize}</TableCell>
</TableRow>
}
<TableRow>
<TableCell >Armor type</TableCell>
<TableCell>
{unit.armorType2 == null ? <ArmorType name={unit.armorType.name} compact={false} /> : <Tooltip title={"upgrade/debuff can turns to " + unit.armorType2.name}><span><ArmorType name={unit.armorType.name} compact={false} /></span></Tooltip>}
</TableCell>
</TableRow>
<TableRow>
<TableCell >Health</TableCell>
<TableCell>
<img style={{ verticalAlign: "top" }}
src="/images/Health_icon.webp" />&nbsp;
{unit.health} {unit.healthRegeneration > 0 &&
<span>+{unit.healthRegeneration}/s</span>}
{unit.armour !== undefined && unit.armour !== 0 && <span><img style={{ height: 20, verticalAlign: "top" }} src="/images/defence.png" />{unit.armour} </span>}
</TableCell>
</TableRow>
<TableRow>
<TableCell >Movement</TableCell>
<TableCell>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
<Tooltip title="Movement speed"><span><img style={{ height: 20, verticalAlign: "top" }} src="/images/Movement_speed.svg" /></span></Tooltip>&nbsp;
{unit.moveSpeed}
</span>
{unit.accelerationTime !== undefined && unit.accelerationTime !== null &&
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
<Tooltip title="Acceleration time"><span><img style={{ height: 20, verticalAlign: "top" }} src="/images/Movement_acceleration.svg" /></span></Tooltip>&nbsp;
{unit.accelerationTime}s
</span>
}
{unit.decelerationTime !== undefined && unit.decelerationTime !== null &&
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
<Tooltip title="Deceleration time"><span><img style={{ height: 20, verticalAlign: "top" }} src="/images/Movement_deceleration.svg" /></span></Tooltip>&nbsp;
{unit.decelerationTime}s
</span>
}
{unit.rotationRate !== undefined && unit.rotationRate !== null &&
<span style={{ display: 'inline-flex', alignItems: 'center' }}>
<Tooltip title="Rotation rate"><span><img style={{ height: 20, verticalAlign: "top" }} src="/images/Movement_rotation.svg" /></span></Tooltip>&nbsp;
{unit.rotationRate}
</span>
}
</span>
</TableCell>
</TableRow>
<TableRow>
<TableCell >Morale</TableCell>
<TableCell>{morale}</TableCell>
</TableRow>
<TableRow>
<TableCell >Mass</TableCell>
<TableCell>{unit.mass}</TableCell>
</TableRow>
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell >Vision</TableCell>
<TableCell><Vision sight={unit.sightRadius} detect={unit.detectRadius} /></TableCell>
</TableRow>
{unit.repairMax !== undefined && unit.repairMax !== null &&
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell >Repair max</TableCell>
<TableCell>{unit.repairMax}</TableCell>
</TableRow>
}
{unit.repairSpeed !== undefined && unit.repairSpeed !== null && unit.repairCostPercent !== null &&
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell >Repair</TableCell>
<TableCell>{unit.repairSpeed} hp/s; {unit.repairCostPercent}% cost</TableCell>
</TableRow>
}
{unit.mobValue != null &&
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell >Mob value</TableCell>
<TableCell><span> <img style={{ height: 20, verticalAlign: "top" }} src="/images/Mob_bonus.gif" /> {unit.mobValue} </span></TableCell>
</TableRow>
}
{unit.squadLimit !== undefined && unit.squadLimit !== null &&
<TableRow sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<TableCell >Limit</TableCell>
<TableCell>{unit.squadLimit}</TableCell>
</TableRow>
}
</TableBody>
</Table>
</StatsPaper>
</Grid2>
<Grid2 size={{ xs: 12, md: 8 }}>
<DescriptionBox>
{unit.description}
</DescriptionBox>
</Grid2>
{unit.requirements !== null &&
<Grid2 size={{ xs: 12, md: 12 }}>
<Required requirement={unit.requirements} modId={unit.modId} raceId={unit.race.id} />
</Grid2>}
<Grid2 size={12}>
{unit.sergeants.map(s =>
<SergeantAccordion key={s.id} TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary
expandIcon={<ExpandMore sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)' }} />}
sx={{ color: isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)', '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' } }}>
<SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0} />
</AccordionSummary>
<AccordionDetails sx={{ color: theme.palette.text.primary }}>
<Sergeant mod={mod} sergeant={s} race={unit.race} />
</AccordionDetails>
</SergeantAccordion>)}
</Grid2>
{unit.moraleMax !== null && !(unit.moraleBrakeModifiers.find(m => m.reference.includes("accuracy_weapon_modifier") && m.value === 0.2) !== undefined &&
unit.moraleBrakeModifiers.find(m => m.reference.includes("speed_maximum_modifier") && m.value === 1.2) !== undefined &&
unit.moraleBrakeModifiers.length === 2)
&& <Grid2 size={12}>
<SectionTitle><img style={{ verticalAlign: "top" }} src="/images/ARM_Morale.webp" /> Morale broken <img style={{ verticalAlign: "top" }} src="/images/ARM_Morale.webp" /></SectionTitle>
<ModifiersProvidesTable modifiers={unit.moraleBrakeModifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData} />
</Grid2>}
{unit.modifiers.length > 0 && <Grid2 size={12}>
<SectionTitle>Affected on</SectionTitle>
<ModifiersProvidesTable modifiers={unit.modifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData} />
</Grid2>}
{unit.abilities.length > 0 && <Grid2 size={12}>
<SectionTitle>Abilities</SectionTitle>
{unit.abilities.map(a =>
<Ability mod={mod} ability={a} race={unit.race} />
)}
</Grid2>}
{unit.jumps != null && <Grid2 size={12}>
<Jump jumps={unit.jumps} race={unit.race} mod={mod} />
</Grid2>}
<Grid2 size={12}>
{[...mapWithUnitWeapons.keys()].sort(function (a, b) {
return a - b;
}).map(h => <WeaponSlot haveReinforceMenu={unit.haveReinforceMenu} key={h} race={unit.race} mod={mod} unitWeapons={mapWithUnitWeapons.get(h)} hardpoint={h} />)}
</Grid2>
{unit.deathExplosions.length > 0 &&
<Grid2 size={12}>
<DeathExplosion deathExplosions={unit.deathExplosions} mod={mod} />
</Grid2>
}
<Grid2 size={12}>
<AffectedResearches researches={unit.affectedResearches} modId={mod.id} raceId={unit.race.id} />
</Grid2>
</Grid2>
<Box sx={{ mt: 3, mb: 2, fontWeight: 600, fontSize: '0.85rem' }}>
Hotkey: {unit.hotkey} <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {unit.filename}</span>
</Box>
<StyledDivider />
<UnitsTable modId={mod.id} racesUnits={racesUnits} racesBuildings={racesBuildings} />
</Box>
)
}
export default function UnitPageClient({
initialUnit,
initialMod,
}: {
initialUnit: IUnit | null;
initialMod: IMod | null;
}) {
const params = useParams() as Record<string, string>;
const { modId, raceId, unitId } = params;
const theme = useTheme();
const [unit, setUnit] = useState<IUnit | null>(initialUnit);
const [mod, setMod] = useState<IMod | null>(initialMod);
const [racesUnits, setRacesUnits] = useState<IRaceUnits[]>([]);
const [racesBuildings, setRacesBuildings] = useState<IRaceBuildings[]>([]);
useEffect(() => {
let cancelled = false;
if (unit == null) {
fetch(AvailableUnits + "/" + unitId)
.then(res => res.json())
.then((res: IUnit) => { if (!cancelled) setUnit(res); });
}
if (mod == null) {
fetch(AvailableMods + "/" + modId)
.then(res => res.json())
.then((res: IMod) => { if (!cancelled) setMod(res); });
}
fetch(AvailableUnits + "/mod/" + modId)
.then(res => res.json())
.then((resUnits: IRaceUnits[]) => {
if (cancelled) return;
setRacesUnits(resUnits);
if (resUnits.length <= 10) {
fetch(AvailableBuildings + "/mod/" + modId)
.then(res => res.json())
.then((resBuildings: IRaceBuildings[]) => {
if (!cancelled) setRacesBuildings(resBuildings);
});
}
});
return () => { cancelled = true; };
}, [modId, raceId, unitId]);
if (unit != null && mod != null) {
const backRef = "/mod/" + modId + "/race/" + raceId;
return (
<Container maxWidth="lg">
<BackButton
variant="outlined"
startIcon={<ArrowBack />}
href={backRef}
>
Back to race
</BackButton>
{Unit(unit, mod, theme, racesUnits, racesBuildings)}
</Container>
);
} else {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
);
}
}
+3
View File
@@ -2,6 +2,9 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>Webpack App</title>
</head>
<body>
+62
View File
@@ -0,0 +1,62 @@
// Server-side API helpers for metadata generation.
// 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';
import { IRaceUnits } from '@/src/types/IUnitShort';
import { IRaceBuildings } from '@/src/types/IBuildingShort';
import { IUnit } from '@/src/types/IUnit';
import { IBuilding } from '@/src/types/IBuilding';
const API = process.env.HOST_URL || '';
export const SITE_URL = 'https://wiki.dawn-of-war.pro';
async function fetchJson<T>(url: string): Promise<T | null> {
try {
const res = await fetch(url, { cache: 'no-store' });
if (!res.ok) return null;
return (await res.json()) as T;
} catch {
return null;
}
}
export async function getMod(modId: string) {
return fetchJson<IMod>(`${API}/api/v1/mods/${modId}`);
}
export async function getRace(raceId: string) {
return fetchJson<Irace>(`${API}/api/v1/races/${raceId}`);
}
export async function getUnit(unitId: string) {
return fetchJson<IUnit>(`${API}/api/v1/units/${unitId}`);
}
export async function getBuilding(buildingId: string) {
return fetchJson<IBuilding>(`${API}/api/v1/buildings/${buildingId}`);
}
export async function getMods() {
return fetchJson<IMod[]>(`${API}/api/v1/mods`);
}
export async function getRacesForMod(modId: string | number) {
return fetchJson<Array<{ race: { id: string; name: string } }>>(`${API}/api/v1/units/mod/${modId}`);
}
export async function getUnitsForMod(modId: string | number) {
return fetchJson<IRaceUnits[]>(`${API}/api/v1/units/mod/${modId}`);
}
export async function getBuildingsForMod(modId: string | number) {
return fetchJson<IRaceBuildings[]>(`${API}/api/v1/buildings/mod/${modId}`);
}
export async function getRaceUnits(modId: string | number, raceId: string) {
return fetchJson<IRaceUnits>(`${API}/api/v1/units/${modId}/${raceId}`);
}
export async function getRaceBuildings(modId: string | number, raceId: string) {
return fetchJson<IRaceBuildings>(`${API}/api/v1/buildings/${modId}/${raceId}`);
}
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default nextConfig;
+271 -17601
View File
File diff suppressed because it is too large Load Diff
+14 -31
View File
@@ -1,43 +1,26 @@
{
"name": "dow-wiki-frontend",
"version": "0.1.0",
"version": "0.2.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.3.1",
"@mui/material": "^6.3.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"next": "^14.2.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.28.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
"react-dom": "^18.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"devDependencies": {
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.3.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#64b5f6" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M7 21 V9"/>
<path d="M2.5 13.5 L7 9 L11.5 13.5"/>
<path d="M17 21 V3"/>
<path d="M12.5 7.5 L17 3 L21.5 7.5"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#64b5f6" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M7 3 V15"/>
<path d="M2.5 10.5 L7 15 L11.5 10.5"/>
<path d="M17 3 V21"/>
<path d="M12.5 16.5 L17 21 L21.5 16.5"/>
</svg>

After

Width:  |  Height:  |  Size: 317 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#64b5f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12 a9 9 0 1 1 -2.64 -6.36"/>
<path d="M21 3 v6 h-6"/>
</svg>

After

Width:  |  Height:  |  Size: 255 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#64b5f6" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 8 H16"/>
<path d="M12 4 L16 8 L12 12"/>
<path d="M3 16 H16"/>
<path d="M12 12 L16 16 L12 20"/>
</svg>

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

-46
View File
@@ -1,46 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Dawn of War wiki. Unification mod wiki. Unification mod unit stats. Supported all popular mods."
/>
<meta name="google-site-verification" content="Q4XMn2UDpL2xmK7iBfaWQpVN8EPy8oGxN-7HqSKapb0" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
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`.
-->
<title>Dawn of War Wiki</title>
</head>
<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>
<script type="text/babel" src="./index_bundle.js"></script>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+2 -2
View File
@@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "DoW Wiki",
"name": "Dawn of War Wiki",
"icons": [
{
"src": "favicon.ico",
-106
View File
@@ -1,106 +0,0 @@
import './App.css';
import React from "react";
import {
AppBar,
Box,
Container,
createTheme,
ThemeProvider,
Toolbar,
Typography,
useTheme
} from "@mui/material";
import { MyRoutes } from "./Routes";
import { styled } from '@mui/material/styles';
// Styled components
const StyledAppBar = styled(AppBar)(({ theme }) => ({
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
}));
const LogoText = styled(Typography)(({ theme }) => ({
fontSize: '1.5rem',
fontWeight: 800,
background: 'linear-gradient(135deg, #e94560 0%, #ff6b6b 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
letterSpacing: '-0.5px',
cursor: 'pointer',
'&:hover': {
transform: 'scale(1.05)',
transition: 'transform 0.2s ease',
},
}));
function App() {
const theme = useTheme();
const themeConfig = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 900,
lg: 1320,
xl: 1536,
},
},
palette: {
primary: {
main: "#1a1a2e",
},
background: {
default: '#0a0a0a',
paper: '#1a1a2e',
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", sans-serif',
},
shape: {
borderRadius: 12,
},
components: {
MuiTableCell: {
styleOverrides: {
root: {
color: 'rgba(255, 255, 255, 0.85)',
},
},
},
},
});
return (
<Box sx={{ minHeight: '100vh', backgroundColor: '#0a0a0a' }}>
<ThemeProvider theme={themeConfig}>
<a href="/">
<StyledAppBar position="sticky">
<Container maxWidth="lg">
<Toolbar disableGutters sx={{ justifyContent: 'space-between' }}>
{/* Left side - Logo */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<LogoText variant="h6" sx={{ textDecoration: 'none' }}>
🎮 Autogenerated wiki
</LogoText>
</Box>
{/* Right side - Navigation */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
</Box>
</Toolbar>
</Container>
</StyledAppBar>
</a>
<Container maxWidth="lg" sx={{ py: 4 }}>
<MyRoutes />
</Container>
</ThemeProvider>
</Box>
);
}
export default App;
-21
View File
@@ -1,21 +0,0 @@
import {Routes, Route, BrowserRouter} from "react-router-dom";
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 = () => {
return(
<BrowserRouter>
<Routes>
<Route path="/" element={<ModsPage/>}/>
<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>
)
}
+12 -16
View File
@@ -1,20 +1,13 @@
import {Accordion, AccordionDetails, AccordionSummary} from "@mui/material";
import {AccordionDetails, AccordionSummary} from "@mui/material";
import {ExpandMore} from "@mui/icons-material";
import React from "react";
import {useTheme} from "@mui/material/styles";
import {IAbilityShort} from "../types/IAbility";
import {getIcon} from "./ModifiersProvideTable";
import {IMod} from "../types/Imod";
import AbilityFull from "./AbilityFull";
import {Irace} from "../types/Irace";
import {styled} from '@mui/material/styles';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}));
import {StyledAccordion} from "../commons/StyledAccordion";
interface IAbilityProps {
ability: IAbilityShort,
@@ -23,23 +16,26 @@ interface IAbilityProps {
}
function Ability(props: IAbilityProps){
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const ability = props.ability
return <div className='addon-research-accordion' id={"research-" + ability.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
expandIcon={<ExpandMore sx={{color: textColor}}/>}
aria-controls="panel1-content"
sx={{color: '#ffffff'}}
>
<span style={{fontSize: 20, color: '#ffffff'}}>
{ability.activationType === "Passive ability" ? <img className="abilityIcon" src="/images/PassiveIcon.gif"/> : <img className="abilityIcon" src={getIcon(ability.icon)}/>}
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
{ability.activationType === "Passive ability" ? <img className="abilityIcon" src="/images/PassiveIcon.png"/> : <img className="abilityIcon" src={getIcon(ability.icon)}/>}
&nbsp; {ability.name ? ability.name : ability.fileName.charAt(0).toUpperCase() + ability.fileName.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
<i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)'}}> ({ability.activationType})</i>
<i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)', marginLeft: 4}}>({ability.activationType})</i>
</span>
</AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<AccordionDetails sx={{color: textColor}}>
<AbilityFull abilityId={ability.id} mod={props.mod} race={props.race} />
</AccordionDetails>
</StyledAccordion></div>
+84 -46
View File
@@ -1,22 +1,20 @@
import React, {useEffect, useState} from "react";
import {useTheme} from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import {AbilityUrl, AvailableBuildings, AvailableUnits, UserUrl, WeaponUrl} from "../core/api";
import {IWeapon} from "../types/IUnit";
import ArmorTypeNames from "../types/ArmorTypeValues";
import {
AccordionDetails,
Grid2,
Paper,
styled,
Table,
TableBody,
TableCell,
tableCellClasses,
TableContainer, TableHead,
TableRow, ToggleButton, ToggleButtonGroup
} from "@mui/material";
import ArmorType from "./ArmorType";
import Required from "./Required";
import {renderAffectedResearches} from "./building/Research";
import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace";
import {IRaceUnits} from "../types/IUnitShort";
@@ -27,51 +25,27 @@ import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import ability from "./Ability";
import AbilitySpawnedObject from "./AbilitySpawnedObject";
import DpsTable from "./DpsTable";
import {DescriptionBox} from "../commons/DescriptionBox";
import {StyledPaper} from "../commons/StyledPaper";
import {StyledTableCell} from "../commons/StyledTableCell";
interface IAbilityFullState {
ability?: IAbilityFill;
}
export default function AbilityFull(props: { abilityId?: number, mod: IMod, race: Irace, isChild?: boolean, abilityFull?: IAbilityFill }) {
const theme = useTheme();
const isSmallMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isMediumMobile = useMediaQuery(theme.breakpoints.between('sm', 'md'));
const isMobile = isSmallMobile || isMediumMobile;
const columnsPerRow = isSmallMobile ? 4 : 6;
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const [abilityFull, setAbilityFull] = useState<IAbilityFullState>({
ability: undefined,
});
const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: 'rgba(233, 69, 96, 0.15)',
fontWeight: 700,
marginRight: 'auto',
marginLeft: 'auto',
paddingLeft: 10,
borderBottom: '2px solid rgba(233, 69, 96, 0.3)',
},
[`&.${tableCellClasses.body}`]: {
fontSize: 12,
textAlign: 'center',
color: 'rgba(255, 255, 255, 0.85)',
paddingRight: 18,
paddingLeft: 10,
},
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
useEffect(() => {
if(props.abilityFull !== undefined){
@@ -108,11 +82,11 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
const showTargetTable = !showDpsTable && ability.abilityEnvironment == null && (ability.targetFilter.length > 0 || ability.radius !== 0)
return (
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
<div style={{color: textColor}}>
<Grid2 container spacing={2}>
<Grid2 size={{xs: 12}}>
{ability.initialDelayTime !== undefined && props.isChild === true &&
<b><br/><center style={{color: 'rgba(255,255,255,0.7)'}}>&nbsp;After {ability.initialDelayTime.toFixed(1)}s&nbsp;
<b><br/><center style={{color: textColor}}>&nbsp;After {ability.initialDelayTime.toFixed(1)}s&nbsp;
<img style={{verticalAlign: "top"}}
src="/images/Time_icon.webp"/></center></b>}
</Grid2>
@@ -211,18 +185,81 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
</TableContainer><br/>
</Grid2>
<Grid2 size={8}>
{props.isChild !== true && <DescriptionBox>{ability.description}</DescriptionBox>}
{props.isChild !== true && <span style={{ whiteSpace: 'pre-wrap' }}>{ability.description}</span>}
</Grid2>
{showDpsTable ?
<Grid2 size={12}>
{(ability.refreshTime !== undefined && ability.durationTime !== undefined && ability.refreshTime * 2 < ability.durationTime) && <i style={{color: 'rgba(255,255,255,0.7)'}}>Every <b style={{color: '#ffffff'}}>{ability.refreshTime.toFixed(1)}s</b> deal damege:</i>}
{(ability.refreshTime !== undefined && ability.durationTime !== undefined && ability.refreshTime * 2 < ability.durationTime) && <i style={{color: textColor}}>Every <b style={{color: textColor}}>{ability.refreshTime.toFixed(1)}s</b> deal damege:</i>}
<DpsTable mod={props.mod} minDamageValue={ability.minDamageValue} minDamage={ability.minDamage} maxDamage={ability.maxDamage} piercings={ability.piercings} targetFilter={ability.targetFilter} moraleDamage={ability.moraleDamage}
/>
</Grid2> : showTargetTable ?
<Grid2 size={12}>
<TableContainer>
{ props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ?
<Table sx={{ textAlign: "center"}} size="small" aria-label="a dense table">
{isMobile ? (() => {
const items = props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ? [
{ label: <ArmorType name={ArmorTypeNames.InfantryLow}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryLow) },
{ label: <ArmorType name={ArmorTypeNames.InfantryMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryMedium) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryHigh) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryHeavyMedium) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryHeavyHigh) },
{ label: <ArmorType name={ArmorTypeNames.Commander}/>, value: () => isAbilityApplyTo(ArmorTypeNames.Commander) },
{ label: <ArmorType name={ArmorTypeNames.DemonLow}/>, value: () => isAbilityApplyTo(ArmorTypeNames.DemonLow) },
{ label: <ArmorType name={ArmorTypeNames.DemonMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.DemonMedium) },
{ label: <ArmorType name={ArmorTypeNames.DemonHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.DemonHigh) },
{ label: <ArmorType name={ArmorTypeNames.Air}/>, value: () => isAbilityApplyTo(ArmorTypeNames.Air) },
{ label: <ArmorType name={ArmorTypeNames.VehicleLow}/>, value: () => isAbilityApplyTo(ArmorTypeNames.VehicleLow) },
{ label: <ArmorType name={ArmorTypeNames.VehicleMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.VehicleMedium) },
{ label: <ArmorType name={ArmorTypeNames.VehicleHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.VehicleHigh) },
{ label: <ArmorType name={ArmorTypeNames.LivingMetal}/>, value: () => isAbilityApplyTo(ArmorTypeNames.LivingMetal) },
{ label: <ArmorType name={ArmorTypeNames.Titan}/>, value: () => isAbilityApplyTo(ArmorTypeNames.Titan) },
{ label: <ArmorType name={ArmorTypeNames.BuildingLow}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingLow) },
{ label: <ArmorType name={ArmorTypeNames.BuildingMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingMedium) },
{ label: <ArmorType name={ArmorTypeNames.BuildingHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingHigh) },
{ label: <ArmorType name={ArmorTypeNames.BuildingSuper}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingSuper) },
{ label: null, value: () => null },
] : [
{ label: <ArmorType name={ArmorTypeNames.InfantryLow}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryLow) },
{ label: <ArmorType name={ArmorTypeNames.InfantryMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryMedium) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryHigh) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryHeavyMedium) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.InfantryHeavyHigh) },
{ label: <ArmorType name={ArmorTypeNames.Commander}/>, value: () => isAbilityApplyTo(ArmorTypeNames.Commander) },
{ label: <ArmorType name={ArmorTypeNames.DemonMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.DemonMedium) },
{ label: <ArmorType name={ArmorTypeNames.DemonHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.DemonHigh) },
{ label: <ArmorType name={ArmorTypeNames.Air}/>, value: () => isAbilityApplyTo(ArmorTypeNames.Air) },
{ label: <ArmorType name={ArmorTypeNames.VehicleLow}/>, value: () => isAbilityApplyTo(ArmorTypeNames.VehicleLow) },
{ label: <ArmorType name={ArmorTypeNames.VehicleMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.VehicleMedium) },
{ label: <ArmorType name={ArmorTypeNames.VehicleHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.VehicleHigh) },
{ label: <ArmorType name={ArmorTypeNames.BuildingLow}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingLow) },
{ label: <ArmorType name={ArmorTypeNames.BuildingMedium}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingMedium) },
{ label: <ArmorType name={ArmorTypeNames.BuildingHigh}/>, value: () => isAbilityApplyTo(ArmorTypeNames.BuildingHigh) },
];
const chunks: any[] = [];
for (let i = 0; i < items.length; i += columnsPerRow) {
chunks.push(items.slice(i, i + columnsPerRow));
}
return (<>{chunks.map((chunk, chunkIndex) => (
<Table key={chunkIndex} sx={{marginTop: "10px", textAlign: "center"}} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
{chunk.map((item, i) => (
<StyledTableCell key={i}>{item.label}</StyledTableCell>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
{chunk.map((item, i) => (
<StyledTableCell key={i}>{item.value()}</StyledTableCell>
))}
</TableRow>
</TableBody>
</Table>
))}</>)
})() : props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ?
<Table sx={{ textAlign: "center", minWidth: 600}} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<StyledTableCell><ArmorType
@@ -298,7 +335,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
</TableRow>
</TableBody>
</Table> :
<Table size="small" aria-label="a dense table">
<Table sx={{ minWidth: 800 }} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<StyledTableCell><ArmorType
@@ -354,7 +391,8 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
</Table>
}
</TableContainer>
</Grid2> : <span></span>}
</Grid2>
: <span></span>}
<ModifiersProvidesTable modifiers={ability.modifiers} affectedData={ability.affectedData} modId={props.mod.id} race={props.race}/>
</Grid2>
{(ability.abilityEnvironment != null) && <AbilitySpawnedObject abilityEnvironment={ability.abilityEnvironment} race={props.race} mod={props.mod}/>}
@@ -364,7 +402,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
<Grid2 size={{xs: 12, md: 12}}>
<Required requirement={ability.requirements} modId={props.mod.id} raceId={props.race.id}/>
</Grid2>}
{props.isChild !== true && ability.activation !== "Passive ability" && <b className="hotkey" >Hotkey: {ability.hotkey}</b>}
{props.isChild !== true && ability.activation !== "Passive ability" && <b className="hotkey" >Hotkey: {ability.hotkey} {ability.filename && <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {ability.filename}</span>}</b>}
</div>
)
} else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>;
+24 -34
View File
@@ -1,16 +1,17 @@
import {
Accordion,
AccordionDetails,
AccordionSummary, Grid2, Paper,
AccordionSummary, Grid2,
Table,
TableBody,
TableCell, TableContainer,
TableContainer,
TableRow,
Tooltip,
Typography
} from "@mui/material";
import {ExpandMore} from "@mui/icons-material";
import React from "react";
import {useTheme} from "@mui/material/styles";
import {IAbilityShort} from "../types/IAbility";
import {getIcon} from "./ModifiersProvideTable";
import {IMod} from "../types/Imod";
@@ -24,6 +25,8 @@ import Ability from "./Ability";
import DeathExplosion from "./DeathExplosion";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox";
import {StyledPaper} from "../commons/StyledPaper";
import {StyledTableCell} from "../commons/StyledTableCell";
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
@@ -31,20 +34,6 @@ const SectionTitle = styled(Typography)(({ theme }) => ({
fontSize: '1.25rem',
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface AbilityEnvironmentProps {
abilityEnvironment: IAbilityEnvironment,
@@ -53,10 +42,13 @@ interface AbilityEnvironmentProps {
}
function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const abilityEnvironment = props.abilityEnvironment
return <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
return <div style={{color: textColor}}>
<SectionTitle>
{props.abilityEnvironment.name ?? props.abilityEnvironment.filename.charAt(0).toUpperCase() + props.abilityEnvironment.filename.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
<i style={{fontSize: 11, color: 'rgba(255,255,255,0.5)'}}> (Spawned object)</i>
@@ -70,23 +62,23 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Armor type</TableCell>
<TableCell>
<StyledTableCell component="th" scope="row" >Armor type</StyledTableCell>
<StyledTableCell>
{abilityEnvironment.armorType2 == null ?
<ArmorType name={abilityEnvironment.armorType.name} compact={false}/> :
<Tooltip
title={"upgrade/debuff can turns to " + abilityEnvironment.armorType2.name}><span><ArmorType
name={abilityEnvironment.armorType.name}
compact={false}/></span></Tooltip>}
</TableCell>
</StyledTableCell>
</TableRow>
}
{abilityEnvironment.health !== null &&
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Health</TableCell>
<TableCell>
<StyledTableCell component="th" scope="row" >Health</StyledTableCell>
<StyledTableCell>
<img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp;
{abilityEnvironment.health} {abilityEnvironment.healthRegeneration != null && abilityEnvironment.healthRegeneration > 0 &&
@@ -94,36 +86,36 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
{abilityEnvironment.armour !== undefined && abilityEnvironment.armour !== 0 &&
<span><img style={{height: 20, verticalAlign: "top"}}
src="/images/defence.png"/>{abilityEnvironment.armour} </span>}
</TableCell>
</StyledTableCell>
</TableRow>
}
{abilityEnvironment.moveSpeed != null &&
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Move speed</TableCell>
<TableCell>{abilityEnvironment.moveSpeed}</TableCell>
<StyledTableCell component="th" scope="row" >Move speed</StyledTableCell>
<StyledTableCell>{abilityEnvironment.moveSpeed}</StyledTableCell>
</TableRow>
}
{abilityEnvironment.sightRadius != null &&
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Vision</TableCell>
<TableCell>
<StyledTableCell component="th" scope="row" >Vision</StyledTableCell>
<StyledTableCell>
<Vision sight={abilityEnvironment.sightRadius ?? 0}
detect={abilityEnvironment.detectRadius ?? 0}/>
</TableCell>
</StyledTableCell>
</TableRow>
}
{abilityEnvironment.lifetime != null &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Lifetime</TableCell>
<TableCell>
<StyledTableCell component="th" scope="row" >Lifetime</StyledTableCell>
<StyledTableCell>
<img style={{verticalAlign: "top"}}
src="/images/Time_icon.webp"/>&nbsp;
{abilityEnvironment.lifetime.toFixed(0)}s&nbsp;
</TableCell>
</StyledTableCell>
</TableRow>
}
</TableBody>
@@ -142,9 +134,7 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
</Grid2>}
{abilityEnvironment.deathExplosions.length > 0 &&
<Grid2 size={12}>
{abilityEnvironment.deathExplosions.map(da =>
<DeathExplosion deathExplosion={da} mod={props.mod}/>
)}
<DeathExplosion deathExplosions={abilityEnvironment.deathExplosions} mod={props.mod}/>
</Grid2>
}
</Grid2>
+2
View File
@@ -53,6 +53,8 @@ class ArmorType extends React.Component<IArmorType, any> {
return '/images/ARM_Dmn_Hi.webp';
case 'Titan':
return '/images/ARM_titan.webp';
case 'Morale damage':
return '/images/ARM_Morale.webp';
default:
return armorTypeId;
}
+29 -41
View File
@@ -1,15 +1,15 @@
import {
Accordion,
AccordionDetails,
AccordionSummary, Grid2, Paper,
AccordionSummary, Grid2,
Table,
TableBody,
TableCell, TableContainer, TableHead,
TableContainer, TableHead,
TableRow,
Tooltip
} from "@mui/material";
import {ExpandMore} from "@mui/icons-material";
import React from "react";
import {useTheme} from "@mui/material/styles";
import {IAbilityShort} from "../types/IAbility";
import {getIcon, ModifiersProvidesTable} from "./ModifiersProvideTable";
import {IMod} from "../types/Imod";
@@ -24,54 +24,41 @@ import {IDeathExplosion} from "../types/IDeathExplosion";
import Required from "./Required";
import ArmorTypeNames from "../types/ArmorTypeValues";
import DpsTable from "./DpsTable";
import {styled} from '@mui/material/styles';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.05)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.85)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
import {StyledAccordion} from "../commons/StyledAccordion";
import {StyledPaper} from "../commons/StyledPaper";
import {styled} from "@mui/material/styles";
import {StyledTableCell} from "../commons/StyledTableCell";
interface DeathExpProps {
deathExplosion: IDeathExplosion,
deathExplosions: IDeathExplosion[],
mod: IMod,
}
function DeathExplosion(props: DeathExpProps) {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const deathExplosion = props.deathExplosion
const chanceTotalNotZero = deathExplosion.globalChance * deathExplosion.chance != 0
const deathExplosions = props.deathExplosions.filter(deathExplosion =>
deathExplosion.globalChance * deathExplosion.chance != 0
)
return chanceTotalNotZero ?
return deathExplosions.length > 0 ?
<div className='addon-research-accordion' id='death-explosion'><StyledAccordion>
<AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
expandIcon={<ExpandMore sx={{color: textColor}}/>}
aria-controls="panel1-content"
sx={{color: '#ffffff'}}
>
<span style={{fontSize: 20, color: '#ffffff'}}>
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
Death Explosion
</span>
</AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<Grid2 container spacing={2}>
<AccordionDetails sx={{color: textColor}}>
{deathExplosions.map((deathExplosion, index) =>
<Grid2 container spacing={2} key={deathExplosion.id ?? index}
sx={{mt: index > 0 ? 2 : 0}}>
<Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="a dense table">
@@ -79,18 +66,18 @@ function DeathExplosion(props: DeathExpProps) {
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Chance</TableCell>
<TableCell>{deathExplosion.globalChance * deathExplosion.chance}%</TableCell>
<StyledTableCell component="th" scope="row" >Chance</StyledTableCell>
<StyledTableCell>{deathExplosion.globalChance * deathExplosion.chance}%</StyledTableCell>
</TableRow>
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" >Damage radius</TableCell>
<TableCell>{deathExplosion.damageRadius}</TableCell>
<StyledTableCell component="th" scope="row" >Damage radius</StyledTableCell>
<StyledTableCell>{deathExplosion.damageRadius}</StyledTableCell>
</TableRow>
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Throw force</TableCell>
<TableCell>{deathExplosion.throwForceMin} - {deathExplosion.throwForceMax}</TableCell>
<StyledTableCell component="th" scope="row" >Throw force</StyledTableCell>
<StyledTableCell>{deathExplosion.throwForceMin} - {deathExplosion.throwForceMax}</StyledTableCell>
</TableRow>
</TableBody>
</Table>
@@ -103,6 +90,7 @@ function DeathExplosion(props: DeathExpProps) {
moraleDamage={deathExplosion.moraleDamage}/>
</Grid2>
</Grid2>
)}
</AccordionDetails>
</StyledAccordion></div> : <span></span>
+80 -30
View File
@@ -1,11 +1,10 @@
import React from "react";
import {useTheme} from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import ArmorTypeNames from "../types/ArmorTypeValues";
import {
styled,
Table,
TableBody,
TableCell,
tableCellClasses,
TableContainer,
TableHead,
TableRow
@@ -14,26 +13,14 @@ import ArmorType from "./ArmorType";
import {IMod} from "../types/Imod";
import {IPiercing} from "../types/IPiercing";
import {IArmorType} from "../types/IArmorType";
import {StyledTableCell} from "../commons/StyledTableCell";
export default function DpsTable(props: { mod: IMod, minDamageValue?: number, minDamage?: number, maxDamage?: number, piercings: IPiercing[], targetFilter: IArmorType[], moraleDamage?: number }) {
const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: 'rgba(233, 69, 96, 0.15)',
fontWeight: 700,
marginRight: 'auto',
marginLeft: 'auto',
paddingLeft: 10,
borderBottom: '2px solid rgba(233, 69, 96, 0.3)',
},
[`&.${tableCellClasses.body}`]: {
fontSize: 12,
textAlign: 'center',
color: 'rgba(255, 255, 255, 0.85)',
paddingRight: 18,
paddingLeft: 10,
},
}));
const theme = useTheme();
const isSmallMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isMediumMobile = useMediaQuery(theme.breakpoints.between('sm', 'md'));
const isMobile = isSmallMobile || isMediumMobile;
const columnsPerRow = isSmallMobile ? 4 : 6;
function getPiercingK(armorType: string): number|null {
if(props.targetFilter.length > 0 && props.targetFilter.find(tf => tf.name == armorType) === undefined) return null
@@ -92,6 +79,72 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
if(props.moraleDamage !== undefined) return (props.moraleDamage / 2).toFixed(2)
}
if (isMobile) {
const items = props.mod !== undefined && props.mod.name.includes("Ultimate Apocalypse") ? [
{ label: <ArmorType name={ArmorTypeNames.InfantryLow}/>, value: () => getTotalDamage(infLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryMedium}/>, value: () => getTotalDamage(infMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHigh}/>, value: () => getTotalDamage(infHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyMedium}/>, value: () => getTotalDamage(infHeavyMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyHigh}/>, value: () => getTotalDamage(infHeavyHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Commander}/>, value: () => getTotalDamage(commanderPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonLow}/>, value: () => getTotalDamage(demonLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonMedium}/>, value: () => getTotalDamage(demonPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonHigh}/>, value: () => getTotalDamage(demonHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Air}/>, value: () => getTotalDamage(airPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleLow}/>, value: () => getTotalDamage(vehLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleMedium}/>, value: () => getTotalDamage(vehMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleHigh}/>, value: () => getTotalDamage(vehHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.LivingMetal}/>, value: () => getTotalDamage(livingMetalPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Titan}/>, value: () => getTotalDamage(titanPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingLow}/>, value: () => getTotalDamage(buildingLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingMedium}/>, value: () => getTotalDamage(buildingMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingHigh}/>, value: () => getTotalDamage(buildingHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingSuper}/>, value: () => getTotalDamage(buildingSuperPiercing) },
{ label: <ArmorType name="Morale damage"/>, value: () => getMoraleDamage() },
] : [
{ label: <ArmorType name={ArmorTypeNames.InfantryLow}/>, value: () => getTotalDamage(infLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryMedium}/>, value: () => getTotalDamage(infMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHigh}/>, value: () => getTotalDamage(infHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyMedium}/>, value: () => getTotalDamage(infHeavyMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyHigh}/>, value: () => getTotalDamage(infHeavyHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Commander}/>, value: () => getTotalDamage(commanderPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonMedium}/>, value: () => getTotalDamage(demonPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonHigh}/>, value: () => getTotalDamage(demonHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Air}/>, value: () => getTotalDamage(airPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleLow}/>, value: () => getTotalDamage(vehLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleMedium}/>, value: () => getTotalDamage(vehMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleHigh}/>, value: () => getTotalDamage(vehHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingLow}/>, value: () => getTotalDamage(buildingLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingMedium}/>, value: () => getTotalDamage(buildingMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingHigh}/>, value: () => getTotalDamage(buildingHighPiercing) },
{ label: <ArmorType name="Morale damage"/>, value: () => getMoraleDamage() },
];
const chunks: any[] = [];
for (let i = 0; i < items.length; i += columnsPerRow) {
chunks.push(items.slice(i, i + columnsPerRow));
}
return (<>{chunks.map((chunk, chunkIndex) => (
<Table key={chunkIndex} sx={{marginTop: "10px", textAlign: "center"}} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
{chunk.map((item, i) => (
<StyledTableCell key={i}>{item.label}</StyledTableCell>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
{chunk.map((item, i) => (
<StyledTableCell key={i}>{item.value()}</StyledTableCell>
))}
</TableRow>
</TableBody>
</Table>
))}</>)
}
return (
<TableContainer>
{ props.mod !== undefined && props.mod.name.includes("Ultimate Apocalypse") ?
@@ -153,10 +206,8 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.BuildingSuper}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/>
<div style={{width: 20, fontSize: 12, height: 50}}>
<i>Morale</i></div>
<StyledTableCell><ArmorType
name='Morale damage'/>
</StyledTableCell>
</TableRow>
</TableHead>
@@ -175,7 +226,7 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
</TableRow>
</TableBody>
</Table> :
<Table size="small" aria-label="a dense table">
<Table sx={{ minWidth: 800 }} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<StyledTableCell><ArmorType
@@ -207,10 +258,9 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
name={ArmorTypeNames.BuildingMedium}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/>
<div style={{width: 20, fontSize: 12, height: 50}}>
<i>Morale</i></div>
<StyledTableCell>
<ArmorType
name='Morale damage'/>
</StyledTableCell>
</TableRow>
</TableHead>
+102
View File
@@ -0,0 +1,102 @@
import React from "react";
import {AccordionDetails, AccordionSummary, Grid2, Table, TableBody, TableCell, TableRow, TableContainer} from "@mui/material";
import {ExpandMore} from "@mui/icons-material";
import {useTheme} from "@mui/material/styles";
import {IJumps} from "../types/IUnit";
import {Irace} from "../types/Irace";
import {IMod} from "../types/Imod";
import {StyledAccordion} from "../commons/StyledAccordion";
import {StyledPaper} from "../commons/StyledPaper";
import Required from "./Required";
interface IJumpProps {
jumps: IJumps,
race: Irace,
mod: IMod,
}
function Jump(props: IJumpProps) {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const jumps = props.jumps;
const isTeleport = jumps.isTeleport;
const icon = isTeleport ? "/images/Teleport.png" : "/images/Jump.png";
const title = isTeleport ? "Teleport" : "Jump";
return (
<div style={{marginBottom: 10}}>
<StyledAccordion TransitionProps={{unmountOnExit: true, timeout: 100}}>
<AccordionSummary
expandIcon={<ExpandMore sx={{color: textColor}}/>}
aria-controls="jump-content"
id="jump-header"
sx={{
color: textColor,
'& .MuiAccordionSummary-content': {margin: '12px 0'},
'& .MuiAccordionSummary-content.Mui-expanded': {margin: '12px 0'}
}}
>
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
<img className="jumpIcon" src={icon}/>
&nbsp; {title}
</span>
</AccordionSummary>
<AccordionDetails sx={{color: textColor}}>
<Grid2 container spacing={2}>
{(() => {
const rows: { label: string; value: React.ReactNode }[] = [
{ label: "Charge cost", value: jumps.chargeJumpCostMin === jumps.chargeJumpCostMax
? jumps.chargeJumpCostMin.toFixed(0)
: `${jumps.chargeJumpCostMin.toFixed(0)} - ${jumps.chargeJumpCostMax.toFixed(0)}` },
{ label: "Charge max", value: jumps.chargeMax.toFixed(0) },
{ label: "Charge regeneration", value: `${jumps.chargeRegeneration}/s` },
{ label: "Distance", value: jumps.jumpDistanceMax.toFixed(0) },
{ label: "Setup time", value: <span><img style={{verticalAlign: "top"}} src="/images/Time_icon.webp"/>&nbsp;{jumps.setupTime}s</span> },
{ label: "Breakdown time", value: <span><img style={{verticalAlign: "top"}} src="/images/Time_icon.webp"/>&nbsp;{jumps.breakdownTime}s</span> },
{ label: "Starting charge", value: `${(jumps.chargeStartingFraction * 100).toFixed(0)}%` }
];
if (!isTeleport) {
if (jumps.goUpTime !== undefined && jumps.goUpTime !== null) {
rows.push({ label: "Go up time", value: <span><img style={{verticalAlign: "top"}} src="/images/Time_icon.webp"/>&nbsp;{jumps.goUpTime}s</span> });
}
if (jumps.goDownTime !== undefined && jumps.goDownTime !== null) {
rows.push({ label: "Go down time", value: <span><img style={{verticalAlign: "top"}} src="/images/Time_icon.webp"/>&nbsp;{jumps.goDownTime}s</span> });
}
}
const chunkSize = isTeleport ? 2 : 3;
const chunks: { label: string; value: React.ReactNode }[][] = [];
for (let i = 0; i < rows.length; i += chunkSize) {
chunks.push(rows.slice(i, i + chunkSize));
}
return chunks.map((chunk, chunkIndex) => (
<Grid2 key={chunkIndex} size={{xs: 12, md: 4}}>
<TableContainer component={StyledPaper} elevation={0}>
<Table size="small" aria-label="jump stats table">
<TableBody>
{chunk.map((row, i) => (
<TableRow key={i} sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row">{row.label}</TableCell>
<TableCell>{row.value}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid2>
));
})()}
</Grid2>
{jumps.requirements !== null &&
<Grid2 size={{xs: 12, md: 12}}>
<Required requirement={jumps.requirements} modId={props.mod.id}
raceId={props.race.id}/>
</Grid2>}
</AccordionDetails>
</StyledAccordion>
</div>
);
}
export default Jump;
+14 -17
View File
@@ -1,5 +1,6 @@
import {Paper, Table, TableCell, TableContainer, TableRow} from "@mui/material";
import React from "react";
import {useTheme} from "@mui/material/styles";
import {IModifier} from "../types/IModifier";
import {IconUrl} from "../core/api";
import {Irace} from "../types/Irace";
@@ -8,17 +9,7 @@ import {IMod} from "../types/Imod";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import {styled} from '@mui/material/styles';
import {StyledLink} from "../commons/StyledLink";
// Styled components for dark theme
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.85)',
},
}));
import {StyledPaper} from "../commons/StyledPaper";
interface IModifiersProvidesTable {
@@ -152,6 +143,9 @@ function getChangeDescription(ref: String, v: number) {
}
export function ModifiersProvidesAddonTable(props: IModifiersProvidesTable) {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
let noWeaponMods = props.modifiers.filter(m => !m.reference.includes("default_weapon_modifier_hardpoint"))
@@ -162,8 +156,8 @@ export function ModifiersProvidesAddonTable(props: IModifiersProvidesTable) {
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getModName(m.reference)}</TableCell>
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}</TableCell>
<TableCell component="th" scope="row" sx={{color: textColor}}>{getModName(m.reference)}</TableCell>
<TableCell component="th" scope="row" sx={{color: textColor}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}</TableCell>
</TableRow>
)}
</Table>
@@ -172,6 +166,9 @@ export function ModifiersProvidesAddonTable(props: IModifiersProvidesTable) {
}
export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
let mods = props.modifiers
@@ -247,9 +244,9 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
<TableRow
sx={{'&:last-child td, &:last-child th': {border: 0}}}
>
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getTarget(m.target)}</TableCell>
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)', minWidth: 120}}>{getModName(m.reference)}</TableCell>
<TableCell component="th" scope="row" sx={{color: 'rgba(255,255,255,0.85)'}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}
<TableCell component="th" scope="row" sx={{color: textColor}}>{getTarget(m.target)}</TableCell>
<TableCell component="th" scope="row" sx={{color: textColor, minWidth: 120}}>{getModName(m.reference)}</TableCell>
<TableCell component="th" scope="row" sx={{color: textColor}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}
{m.maxLifeTime !== undefined && m.maxLifeTime > 0 &&
<span><img style={{verticalAlign: "top"}} src='/images/Time_icon.webp'/>{m.maxLifeTime}s
</span>}
@@ -268,7 +265,7 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
export function getIcon(icon?: string): string {
if (icon !== null && icon !== undefined) {
return IconUrl + icon.replaceAll('\\', '/')
} else return '/images/NoImg.gif'
} else return '/images/NoImg.png'
}
+20 -16
View File
@@ -1,4 +1,5 @@
import React from "react";
import {useTheme} from "@mui/material/styles";
import {IBuildingShort} from "../types/IBuildingShort";
import {IBuildingAddonShort} from "../types/IBuilding";
import {IconUrl} from "../core/api";
@@ -14,6 +15,9 @@ interface IRequiredProps{
}
function Required (props: IRequiredProps) {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
function renderRequirementGlobalAddons(rgas: IBuildingAddonShort[],) {
@@ -72,7 +76,7 @@ function Required (props: IRequiredProps) {
return <span>&nbsp; One <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + limitPerBuilding.building.icon.replaceAll('\\', '/')}/>
&nbsp;{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${limitPerBuilding.building.id}`}>
{limitPerBuilding.building.name} </StyledLink> allow produce max {limitPerBuilding.limit}</span>
{limitPerBuilding.building.name} </StyledLink> allows producing a maximum of {limitPerBuilding.limit}</span>
}
function renderRequirementResearches(requirementResearches: IResearchRequirements[],) {
@@ -115,20 +119,20 @@ function Required (props: IRequiredProps) {
return (
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
<h4 style={{color: '#ffffff', fontWeight: 700}}>Required: </h4>
<div style={{color: textColor}}>
<h4 style={{color: textColor, fontWeight: 700}}>Required: </h4>
{requirement.requiredTotalPop !== undefined && requirement.requiredTotalPop !== null &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Population: <img style={{verticalAlign: "top", height: 25}}
<div style={{color: textColor}}>&nbsp; Population: <img style={{verticalAlign: "top", height: 25}}
src="/images/Resource_orksquadcap.gif"/>&nbsp;
{requirement.requiredTotalPop}</div>
}
{requirement.mobBonus !== null && requirement.mobBonus !== undefined &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Mob requirement: <img style={{verticalAlign: "top", height: 25}}
<div style={{color: textColor}}>&nbsp; Mob requirement: <img style={{verticalAlign: "top", height: 25}}
src="/images/Mob_bonus.gif"/>&nbsp;
{requirement.mobBonus.mobvalueRequired} in radius {requirement.mobBonus.proximityRequired}</div>
}
{requirement.requireAddon !== null &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Addon: <img style={{verticalAlign: "top", height: 25}}
<div style={{color: textColor}}>&nbsp; Addon: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + requirement.requireAddon.icon.replaceAll('\\', '/')}/>&nbsp;
<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirement.requireAddon.buildingId}#addon-${requirement.requireAddon.id}`}>
{requirement.requireAddon.name}
@@ -137,26 +141,26 @@ function Required (props: IRequiredProps) {
</div>
}
{requirement.requirementAddonExclusive != null &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)}</div>}
<div style={{color: textColor}}>{renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)}</div>}
{requirement.requirementBuildings.map(b =>
<div key={b.id} style={{color: 'rgba(255, 255, 255, 0.85)'}}>&nbsp; Building: <img style={{verticalAlign: "top", height: 25}}
<div key={b.id} style={{color: textColor}}>&nbsp; Building: <img style={{verticalAlign: "top", height: 25}}
src={IconUrl + b.icon.replaceAll('\\', '/')}/>&nbsp;
<StyledLink href= {'/mod/'+ props.modId +'/race/'+ props.raceId +'/building/' + b.id + "/"}>{b.name}</StyledLink></div>)
}
{requirement.requirementResearches.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementResearches(requirement.requirementResearches,)}</div>}
<div style={{color: textColor}}>{renderRequirementResearches(requirement.requirementResearches,)}</div>}
{requirement.requirementSquads.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementSquads(requirement.requirementSquads,)}</div>}
<div style={{color: textColor}}>{renderRequirementSquads(requirement.requirementSquads,)}</div>}
{requirement.requirementResearchesEither.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementResearchesEither(requirement.requirementResearchesEither,)}</div>}
<div style={{color: textColor}}>{renderRequirementResearchesEither(requirement.requirementResearchesEither,)}</div>}
{requirement.requirementBuildingsEither.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementBuildingEither(requirement.requirementBuildingsEither,)}</div>}
<div style={{color: textColor}}>{renderRequirementBuildingEither(requirement.requirementBuildingsEither,)}</div>}
{requirement.requirementStructureExclusive != null &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequiremenStructureExclusive(requirement.requirementStructureExclusive,)}</div>}
{requirement.limitByBuilding !== undefined && requirement.limitByBuilding !== null && <div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderLimitPerBuilding(requirement.limitByBuilding)}</div>}
<div style={{color: textColor}}>{renderRequiremenStructureExclusive(requirement.requirementStructureExclusive,)}</div>}
{requirement.limitByBuilding !== undefined && requirement.limitByBuilding !== null && <div style={{color: textColor}}>{renderLimitPerBuilding(requirement.limitByBuilding)}</div>}
{requirement.requirementsGlobalAddons.length !== 0 &&
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)}</div>}
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>{renderRequirementOwnership(requirement.requiredOwnership)}</div>
<div style={{color: textColor}}>{renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)}</div>}
<div style={{color: textColor}}>{renderRequirementOwnership(requirement.requiredOwnership)}</div>
</div>
)
}
+11 -22
View File
@@ -1,12 +1,13 @@
import React from "react";
import {useTheme} from "@mui/material/styles";
import {ISergeant, IShortWeapon} from "../types/IUnit";
import {Grid2, Paper, Table, TableBody, TableCell, TableContainer, TableRow, Tooltip, Typography} from "@mui/material";
import {Grid2, Table, TableBody, TableCell, TableContainer, TableRow, Tooltip, Typography} from "@mui/material";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import ArmorType from "./ArmorType";
import WeaponSlot from "./WeaponSlot";
import Vision from "./Vision";
import {IMod} from "../types/Imod";
import {renderAffectedResearches} from "./building/Research";
import {AffectedResearches} from "./building/Research";
import {Irace} from "../types/Irace";
import Required from "./Required";
import {ModifiersProvidesTable} from "./ModifiersProvideTable";
@@ -14,20 +15,7 @@ import Ability from "./Ability";
import DeathExplosion from "./DeathExplosion";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox";
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
import {StyledPaper} from "../commons/StyledPaper";
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
@@ -43,6 +31,9 @@ export interface SergeantProps {
}
const Sergeant = (props: SergeantProps) => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const sergeant = props.sergeant
@@ -60,7 +51,7 @@ const Sergeant = (props: SergeantProps) => {
})
return (
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
<div style={{color: textColor}}>
<Grid2 container spacing={2}>
<Grid2 size= {{xs: 12, md: 4}}>
<TableContainer component={StyledPaper} elevation={0}>
@@ -196,16 +187,14 @@ const Sergeant = (props: SergeantProps) => {
</Grid2>
{sergeant.deathExplosions.length > 0 &&
<Grid2 size={12}>
{sergeant.deathExplosions.map(da =>
<DeathExplosion deathExplosion={da} mod={props.mod}/>
)}
<DeathExplosion deathExplosions={sergeant.deathExplosions} mod={props.mod}/>
</Grid2>
}
<Grid2 size={12}>
{renderAffectedResearches(sergeant.affectedResearches, props.mod.id, props.race.id)}
<AffectedResearches researches={sergeant.affectedResearches} modId={props.mod.id} raceId={props.race.id} />
</Grid2>
</Grid2>
<b className="hotkey" >Hotkey: {sergeant.hotkey}</b>
<b className="hotkey" >Hotkey: {sergeant.hotkey} <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {sergeant.filename}</span></b>
</div>)
};
+19 -51
View File
@@ -1,34 +1,25 @@
import React, {useEffect, useState} from "react";
import {AvailableBuildings, AvailableUnits} from "../core/api";
import {Grid2, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper} from "@mui/material";
import React from "react";
import {Grid2, Table, TableBody, TableCell, TableHead, TableRow} from "@mui/material";
import {styled} from '@mui/material/styles';
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
import {StyledLink} from "../commons/StyledLink";
// Styled components for dark theme
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
}));
import {StyledPaper} from "../commons/StyledPaper";
const StyledTable = styled(Table)(({ theme }) => ({
'& .MuiTableCell-head': {
backgroundColor: 'rgba(233, 69, 96, 0.15)',
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
fontWeight: 700,
fontSize: '1rem',
borderBottom: '2px solid rgba(233, 69, 96, 0.3)',
borderBottom: `2px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.2)'}`,
},
'& .MuiTableCell-body': {
color: 'rgba(255, 255, 255, 0.85)',
borderBottom: '1px solid rgba(255, 255, 255, 0.05)',
color: theme.palette.text.primary,
borderBottom: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}`,
fontSize: '0.9rem',
},
'& .MuiTableRow-hover:hover': {
backgroundColor: 'rgba(233, 69, 96, 0.08)',
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.04)',
},
}));
@@ -36,7 +27,7 @@ const StyledHeaderLink = styled(StyledLink)(({ theme }) => ({
fontWeight: 700,
fontSize: '1.1rem',
'&:hover': {
color: '#e94560',
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
},
}));
@@ -45,13 +36,13 @@ interface IUnitsTable {
racesBuildings: IRaceBuildings[]
}
export default function UnitsTable(prop: {modId: number}) {
export default function UnitsTable(prop: {modId: number, racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[]}) {
const [unitsTable, setUnitsTable] = useState<IUnitsTable>({
racesUnits: [],
racesBuildings: []
});
const unitsTable: IUnitsTable = {
racesUnits: prop.racesUnits,
racesBuildings: prop.racesBuildings
};
function getUnitRef(modId: number, raceId: string, unit: IUnitShort) {
@@ -70,29 +61,6 @@ export default function UnitsTable(prop: {modId: number}) {
</span>
}
useEffect(() => {
fetch(AvailableUnits + "/mod/" + prop.modId)
.then(resUnits => resUnits.json())
.then((resUnits: IRaceUnits[]) => {
if(resUnits.length <= 10){
fetch(AvailableBuildings + "/mod/" + prop.modId)
.then(resBuildings => resBuildings.json())
.then((resBuildings: IRaceBuildings[]) => {
setUnitsTable({
racesUnits : resUnits,
racesBuildings: resBuildings
});
});
}else {
setUnitsTable({
racesUnits : resUnits,
racesBuildings: []
});
}
})
}, []);
function armourTypePriority(armorTypeId: string): number {
switch(armorTypeId) {
case 'Infantry Low':
@@ -103,11 +71,11 @@ export default function UnitsTable(prop: {modId: number}) {
return 3;
case 'Demon Medium':
return 4;
case 'Infantry Heavy Medium':
case 'Infantry H.Med.':
return 5;
case 'Infantry Heavy High':
case 'Infantry H.High':
return 6;
case 'Commander':
case 'Infantry Com.':
return 7;
case 'Vehicle Low':
return 8;
@@ -117,7 +85,7 @@ export default function UnitsTable(prop: {modId: number}) {
return 10;
case 'Demon High':
return 11;
case 'Air':
case 'Vehicle Air':
return 12;
case 'Building Low':
return 13;
@@ -139,7 +107,7 @@ export default function UnitsTable(prop: {modId: number}) {
}
function generateRaceUnitTable(racesUnitsPart: IRaceUnits[], racesBuildings: IRaceBuildings[]){
return (<StyledPaper elevation={0}>
return (<StyledPaper elevation={0} sx={{borderRadius: 0}}>
{<StyledTable sx={{minWidth: 650}} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
+13 -17
View File
@@ -1,20 +1,13 @@
import React from "react";
import {IShortWeapon, IWeapon} from "../types/IUnit";
import {Accordion, AccordionDetails, AccordionSummary} from "@mui/material";
import {AccordionDetails, AccordionSummary} from "@mui/material";
import {withTheme} from "../core/api";
import {IconUrl} from "../core/api";
import {ExpandMore} from "@mui/icons-material";
import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace";
import WeaponFull from "./WeaponFull";
import {styled} from '@mui/material/styles';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}));
import {StyledAccordion} from "../commons/StyledAccordion";
interface IWeaponProps {
weapon: IShortWeapon,
@@ -39,27 +32,30 @@ class Weapon extends React.Component<IWeaponProps, any> {
render() {
const theme = (this.props as any).theme;
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const weapon = this.props.weapon
return (
<div style={{marginBottom: 10}}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
expandIcon={<ExpandMore sx={{color: textColor}}/>}
aria-controls="panel1-content"
id="panel1-header"
sx={{color: '#ffffff'}}
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
>
<span style={{fontSize: 18, color: '#ffffff'}}> {!this.props.isDefault && weapon.icon && !weapon.icon.endsWith("upgrade.png") ?
<span style={{fontSize: 18, color: textColor, display: 'flex', alignItems: 'center'}}> {!this.props.isDefault && weapon.icon && !weapon.icon.endsWith("upgrade.png") ?
<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)}
{this.props.isDefault && <i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)'}}> (default)</i>}
)} &nbsp; {weapon.name ? weapon.name : this.humanReadableName(weapon.filename)}
{this.props.isDefault && <i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)', marginLeft: 4}}>(default)</i>}
</span>
</AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<AccordionDetails sx={{color: textColor}}>
<WeaponFull weaponId={weapon.id} isDefault={this.props.isDefault} mod={this.props.mod} race={this.props.race} haveReinforceMenu={this.props.haveReinforceMenu}/>
</AccordionDetails>
</StyledAccordion></div>
@@ -67,4 +63,4 @@ class Weapon extends React.Component<IWeaponProps, any> {
}
}
export default Weapon;
export default withTheme(Weapon);
+297 -76
View File
@@ -1,29 +1,27 @@
import React, {useEffect, useState} from "react";
import {AvailableBuildings, AvailableUnits, UserUrl, WeaponUrl} from "../core/api";
import {IWeapon} from "../types/IUnit";
import {useTheme} from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import {WeaponUrl} from "../core/api";
import ArmorTypeNames from "../types/ArmorTypeValues";
import {
AccordionDetails,
Grid2,
Paper,
styled,
Table,
TableBody,
TableCell,
tableCellClasses,
TableContainer, TableHead,
TableRow, ToggleButton, ToggleButtonGroup
TableRow, ToggleButton, ToggleButtonGroup, Slider
} from "@mui/material";
import ArmorType from "./ArmorType";
import Required from "./Required";
import {renderAffectedResearches} from "./building/Research";
import {AffectedResearches} from "./building/Research";
import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace";
import {IRaceUnits} from "../types/IUnitShort";
import {IRaceBuildings} from "../types/IBuildingShort";
import {ModifiersProvidesTable} from "./ModifiersProvideTable";
import {DescriptionBox} from "../commons/DescriptionBox";
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;
@@ -31,42 +29,84 @@ interface IWeaponFull {
}
export default function WeaponFull(props: {weaponId: number, isDefault: Boolean, mod: IMod, race: Irace, haveReinforceMenu: Boolean}) {
const theme = useTheme();
const isSmallMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isMediumMobile = useMediaQuery(theme.breakpoints.between('sm', 'md'));
const isMobile = isSmallMobile || isMediumMobile;
const columnsPerRow = isSmallMobile ? 4 : 6;
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const [weaoponFull, setWeaponFull] = useState<IWeaponFull>({
currentTable: "dps",
weapon: undefined,
});
const StyledTableCell = styled(TableCell)(({theme}) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: 'rgba(233, 69, 96, 0.15)',
marginRight: 'auto',
marginLeft: 'auto',
paddingLeft: 10,
borderBottom: '2px solid rgba(233, 69, 96, 0.3)',
},
[`&.${tableCellClasses.body}`]: {
fontSize: 12,
textAlign: 'center',
color: 'rgba(255, 255, 255, 0.85)',
paddingRight: 18,
paddingLeft: 10,
},
}));
const [enabledResearchIds, setEnabledResearchIds] = useState<Set<number>>(new Set());
const [damageRoll, setDamageRoll] = useState<number | null>(null);
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
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(() => {
@@ -82,14 +122,14 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
}, []);
if(weaoponFull.weapon !== undefined){
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
}
@@ -98,9 +138,18 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
currentTable: selectedDamageTable,
weapon: weaoponFull.weapon,
});
if (selectedDamageTable === "one hit") {
setDamageRoll((displayWeapon.minDamage + displayWeapon.maxDamage) / 2);
}
}
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 movingAccuracy = displayWeapon.accuracy - displayWeapon.accuracyReductionMoving
const hasMovingAccuracy = displayWeapon.setupTime === 0 && movingAccuracy > 0
const reloadK = 1 / (displayWeapon.reloadTime - (displayWeapon.reloadTime % 0.125))
const dpsK = (weaoponFull.currentTable == "dps") ? displayWeapon.accuracy * reloadK
: (weaoponFull.currentTable == "dps move") ? movingAccuracy * reloadK
: 1
const infLowPiercing = getPiercingK(ArmorTypeNames.InfantryLow)
const infMedPiercing = getPiercingK(ArmorTypeNames.InfantryMedium)
@@ -125,38 +174,48 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
const livingMetalPiercing = getPiercingK(ArmorTypeNames.LivingMetal)
const titanPiercing = getPiercingK(ArmorTypeNames.Titan)
const damageRollMin = displayWeapon.minDamage
const damageRollMax = displayWeapon.maxDamage
const hasDamageRollRange = damageRollMax > damageRollMin
const currentDamageRoll = damageRoll !== null
? Math.min(Math.max(damageRoll, damageRollMin), damageRollMax)
: (damageRollMin + damageRollMax) / 2
const damageRollFraction = hasDamageRollRange
? (currentDamageRoll - damageRollMin) / (damageRollMax - damageRollMin)
: 0.5
const getTotalDamage = (damagePiercing: number, isAir: boolean = false) => {
if (!isAir && !weapon.canAttackGround && !weapon.isMeleeWeapon) return ""
if (isAir && !weapon.canAttackAir) return ""
var minDamage = damagePiercing * weapon.minDamage
var maxDamage = damagePiercing * weapon.maxDamage
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
const hitDamage = weaoponFull.currentTable === "one hit"
? minDamage + (maxDamage - minDamage) * damageRollFraction
: (minDamage + maxDamage) / 2
return (averageDmg * dpsK).toFixed(2)
return (hitDamage * dpsK).toFixed(2)
}
const getMoraleDamage = () => {
return (weapon.moraleDamage * dpsK / 2).toFixed(2)
return (displayWeapon.moraleDamage * dpsK / 2).toFixed(2)
}
return (
<div style={{color: 'rgba(255, 255, 255, 0.85)'}}>
<div style={{color: textColor}}>
<Grid2 container spacing={2}>
{!props.isDefault && <Grid2 size={12}>
<DescriptionBox>
<span className="weaponDescription">{weapon.description}</span>
</DescriptionBox>
<span className="weaponDescription" style={{ whiteSpace: 'pre-wrap' }}>{weapon.description}</span>
</Grid2>}
<Grid2 size={{xs: 12, md: 4}}>
@@ -167,19 +226,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>{hasMovingAccuracy ? movingAccuracy.toFixed(3).replace(/[,.]?0+$/, '') : "-"}</TableCell>
</TableRow>
</TableBody>
</Table>
@@ -193,13 +252,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}}}
@@ -240,7 +299,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}}}
@@ -254,19 +313,183 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
</Grid2>
<Grid2 size={12}>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 12, marginBottom: 8 }}>
<ToggleButtonGroup
color="primary"
exclusive
aria-label="Platform"
value={weaoponFull.currentTable}
onChange={handleChange}
sx={{ mb: 2 }}
>
<ToggleButton size="small" value="dps" sx={{ color: '#ffffff', '&.Mui-selected': { borderColor: '#e94560', color: '#e94560', backgroundColor: 'rgba(233, 69, 96, 0.15)' } }}>Dps</ToggleButton>
<ToggleButton size="small" value="one hit" sx={{ color: '#ffffff', '&.Mui-selected': {borderColor: '#e94560', color: '#e94560', backgroundColor: 'rgba(233, 69, 96, 0.15)' } }}>One hit average damage</ToggleButton>
<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="dps move" disabled={!hasMovingAccuracy} sx={{ color: textColor, '&.Mui-selected': { borderColor: textColor, color: textColor, backgroundColor: isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)' } }}>Dps move</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 damage</ToggleButton>
</ToggleButtonGroup>
{weaoponFull.currentTable === 'one hit' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 200, flexGrow: 1, maxWidth: 220 }}>
<span style={{ whiteSpace: 'nowrap', fontSize: 14 }}>{damageRollMin.toFixed(2)}</span>
<Slider
size="small"
min={damageRollMin}
max={damageRollMax}
step={hasDamageRollRange ? (damageRollMax - damageRollMin) / 100 : 1}
value={currentDamageRoll}
onChange={(e, value) => setDamageRoll(value as number)}
disabled={!hasDamageRollRange}
sx={{ color: textColor, flexGrow: 1 }}
/>
<span style={{ whiteSpace: 'nowrap', fontSize: 14 }}>{damageRollMax.toFixed(2)}</span>
<span style={{ whiteSpace: 'nowrap', fontSize: 14, minWidth: 44, textAlign: 'right', fontWeight: 700 }}>{Math.round((1 - damageRollFraction) * 100)}%</span>
</div>
)}
{weapon.researches && weapon.researches.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, flexBasis: '100%', width: '100%' }}>
{(() => {
// 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);
setDamageRoll(null);
}}
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>
{ props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ?
{isMobile ? (() => {
const items = props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ? [
{ label: <ArmorType name={ArmorTypeNames.InfantryLow}/>, value: () => getTotalDamage(infLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryMedium}/>, value: () => getTotalDamage(infMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHigh}/>, value: () => getTotalDamage(infHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyMedium}/>, value: () => getTotalDamage(infHeavyMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyHigh}/>, value: () => getTotalDamage(infHeavyHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Commander}/>, value: () => getTotalDamage(commanderPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonLow}/>, value: () => getTotalDamage(demonLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonMedium}/>, value: () => getTotalDamage(demonPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonHigh}/>, value: () => getTotalDamage(demonHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Air}/>, value: () => getTotalDamage(airPiercing, true) },
{ label: <ArmorType name={ArmorTypeNames.VehicleLow}/>, value: () => getTotalDamage(vehLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleMedium}/>, value: () => getTotalDamage(vehMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleHigh}/>, value: () => getTotalDamage(vehHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.LivingMetal}/>, value: () => getTotalDamage(livingMetalPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Titan}/>, value: () => getTotalDamage(titanPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingLow}/>, value: () => getTotalDamage(buildingLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingMedium}/>, value: () => getTotalDamage(buildingMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingHigh}/>, value: () => getTotalDamage(buildingHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingSuper}/>, value: () => getTotalDamage(buildingSuperPiercing) },
{ label: (<><img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/><div style={{width: 20, fontSize: 12, height: 50, color: textColor}}><i>Morale</i></div></>), value: () => getMoraleDamage() },
] : [
{ label: <ArmorType name={ArmorTypeNames.InfantryLow}/>, value: () => getTotalDamage(infLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryMedium}/>, value: () => getTotalDamage(infMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHigh}/>, value: () => getTotalDamage(infHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyMedium}/>, value: () => getTotalDamage(infHeavyMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.InfantryHeavyHigh}/>, value: () => getTotalDamage(infHeavyHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Commander}/>, value: () => getTotalDamage(commanderPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonMedium}/>, value: () => getTotalDamage(demonPiercing) },
{ label: <ArmorType name={ArmorTypeNames.DemonHigh}/>, value: () => getTotalDamage(demonHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.Air}/>, value: () => getTotalDamage(airPiercing, true) },
{ label: <ArmorType name={ArmorTypeNames.VehicleLow}/>, value: () => getTotalDamage(vehLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleMedium}/>, value: () => getTotalDamage(vehMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.VehicleHigh}/>, value: () => getTotalDamage(vehHighPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingLow}/>, value: () => getTotalDamage(buildingLowPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingMedium}/>, value: () => getTotalDamage(buildingMedPiercing) },
{ label: <ArmorType name={ArmorTypeNames.BuildingHigh}/>, value: () => getTotalDamage(buildingHighPiercing) },
{ label: <ArmorType name="Morale damage"/>, value: () => getMoraleDamage() },
];
const chunks: any[] = [];
for (let i = 0; i < items.length; i += columnsPerRow) {
chunks.push(items.slice(i, i + columnsPerRow));
}
return chunks.map((chunk, chunkIndex) => (
<Table key={chunkIndex} sx={{marginTop: "10px", textAlign: "center"}} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
{chunk.map((item, i) => (
<StyledTableCell key={i}>{item.label}</StyledTableCell>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
{chunk.map((item, i) => (
<StyledTableCell key={i}>{item.value()}</StyledTableCell>
))}
</TableRow>
</TableBody>
</Table>
));
})() : props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ?
<Table sx={{marginTop: "10px", textAlign: "center"}} size="small" aria-label="a dense table">
<TableHead>
<TableRow>
@@ -327,7 +550,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
name={ArmorTypeNames.BuildingSuper}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/>
<div style={{width: 20, fontSize: 12, height: 50, color: 'rgba(255,255,255,0.7)'}}>
<div style={{width: 20, fontSize: 12, height: 50, color: textColor}}>
<i>Morale</i></div>
</StyledTableCell>
</TableRow>
@@ -379,10 +602,8 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
name={ArmorTypeNames.BuildingMedium}/></StyledTableCell>
<StyledTableCell><ArmorType
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
<StyledTableCell><img style={{verticalAlign: "top"}}
src="/images/ARM_Morale.webp"/>
<div style={{width: 20, fontSize: 12, height: 50, color: 'rgba(255,255,255,0.7)'}}>
<i>Morale</i></div>
<StyledTableCell><ArmorType
name='Morale damage'/>
</StyledTableCell>
</TableRow>
</TableHead>
@@ -408,12 +629,11 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
</TableBody>
</Table>
}
</TableContainer>
</Grid2>
{weapon.modifiers.length > 0 && <Grid2 size={12}>
<h3 style={{color: '#ffffff', fontWeight: 700}}>Modifiers</h3>
<h3 style={{color: textColor, fontWeight: 700}}>Modifiers</h3>
<ModifiersProvidesTable modifiers={weapon.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/>
</Grid2>}
{weapon.requirements !== null && !props.isDefault && props.haveReinforceMenu &&
@@ -421,10 +641,11 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
<Required requirement={weapon.requirements} modId={props.mod.id} raceId={props.race.id}/>
</Grid2>}
<Grid2 size={12}>
{renderAffectedResearches(weapon.affectedResearches, props.mod.id, props.race.id)}
<AffectedResearches researches={weapon.affectedResearches} modId={props.mod.id} raceId={props.race.id} />
</Grid2>
</Grid2>
{!props.isDefault && props.haveReinforceMenu && <b className="hotkey" >Hotkey: {weapon.hotkey}</b>}
<b className="hotkey" >{!props.isDefault && props.haveReinforceMenu && <span>Hotkey: {weapon.hotkey} </span>}
<span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {weapon.filename}</span></b>
</div>
)
} else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>;
+1 -1
View File
@@ -44,7 +44,7 @@ const WeaponSlot= (props: WeaponSlotProps) => {
<SectionTitle>{header}</SectionTitle>
{weaponsSorted.filter(wp => !wp[1].filename.includes("dummy")).map(wp =>
<div style={{marginLeft: 20}}><Weapon race={props.race} mod={props.mod} isDefault={!haveDummy || wp[1].filename === firstWeapon.filename} weapon={wp[1]} haveReinforceMenu={haveReinforceMenu}/></div>
<div style={{marginTop: 10}}><Weapon race={props.race} mod={props.mod} isDefault={!haveDummy || wp[1].filename === firstWeapon.filename} weapon={wp[1]} haveReinforceMenu={haveReinforceMenu}/></div>
)}
</div>
)
+13 -33
View File
@@ -1,10 +1,9 @@
import React from "react";
import {useTheme} from "@mui/material/styles";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Grid2,
Paper,
Table,
TableBody,
TableCell,
@@ -18,30 +17,10 @@ import {ExpandMore} from "@mui/icons-material";
import {IMod} from "../../types/Imod";
import {getIcon, ModifiersProvidesTable,} from "../ModifiersProvideTable";
import Required from "../Required";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../../commons/DescriptionBox";
import {StyledAccordion} from "../../commons/StyledAccordion";
import {StyledPaper} from "../../commons/StyledPaper";
import {styled} from "@mui/material/styles";
const StyledAccordion = styled(Accordion)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}));
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.05)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.85)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface IBuildingAddonProps {
addon: IBuildingAddon,
@@ -50,6 +29,9 @@ interface IBuildingAddonProps {
}
function BuildingAddon(props: IBuildingAddonProps){
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const addon = props.addon
const building = props.building
@@ -58,16 +40,16 @@ function BuildingAddon(props: IBuildingAddonProps){
return <div className='addon-research-accordion' id={"addon-" + addon.id} ><StyledAccordion>
<AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
expandIcon={<ExpandMore sx={{color: textColor}}/>}
aria-controls="panel1-content"
sx={{color: '#ffffff'}}
>
<span style={{fontSize: 20, color: '#ffffff'}}>
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
<img className="sergeantIcon" src={getIcon(addon.icon)}/>
&nbsp; {addon.name}
</span>
</AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<AccordionDetails sx={{color: textColor}}>
<Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}>
<TableContainer component={StyledPaper} elevation={0}>
@@ -109,9 +91,7 @@ function BuildingAddon(props: IBuildingAddonProps){
</TableContainer><br/>
</Grid2>
<Grid2 size={{xs: 12, md: 8}}>
<DescriptionBox>
{addon.description}
</DescriptionBox>
<span style={{ whiteSpace: 'pre-wrap' }}>{addon.description}</span>
</Grid2>
<ModifiersProvidesTable modifiers={addon.addonModifiers} affectedData={addon.affectedData} modId={props.mod.id} race={building.race}/>
{addon.addonRequirement !== null &&
@@ -119,7 +99,7 @@ function BuildingAddon(props: IBuildingAddonProps){
<Required requirement={addon.addonRequirement} modId={building.modId} raceId={building.race.id}/>
</Grid2>}
</Grid2>
<b className="hotkey" >Hotkey: {addon.hotkey}</b>
<b className="hotkey" >Hotkey: {addon.hotkey} <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {addon.filename}</span></b>
</AccordionDetails>
</StyledAccordion></div>
}
+16 -28
View File
@@ -1,7 +1,6 @@
import {IMod} from "../../types/Imod";
import {IResearch} from "../../types/IResearch";
import {
Accordion,
AccordionDetails,
AccordionSummary,
Grid2,
@@ -14,31 +13,13 @@ import {
} from "@mui/material";
import {ExpandMore} from "@mui/icons-material";
import {getIcon, ModifiersProvidesTable} from "../ModifiersProvideTable";
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
import Required from "../Required";
import React from "react";
import {useTheme} from "@mui/material/styles";
import {IBuilding} from "../../types/IBuilding";
import {IResearchShort} from "../../types/IResearchShort";
import ResearchFull from "./ResearchFull";
import {styled} from '@mui/material/styles';
const StyledAccordion = styled(Accordion)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}));
const StyledLink = styled('a')(({ theme }) => ({
color: '#e94560',
textDecoration: 'none',
'&:hover': {
color: '#ff6b6b',
textDecoration: 'underline',
},
}));
import {StyledAccordion} from "../../commons/StyledAccordion";
import {StyledLink} from "../../commons/StyledLink";
interface IResearchProps {
research: IResearchShort,
@@ -47,29 +28,36 @@ interface IResearchProps {
}
function Research(props: IResearchProps){
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
const research = props.research
const building = props.building
return <div className='addon-research-accordion' id={"research-" + research.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
<AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
expandIcon={<ExpandMore sx={{color: textColor}}/>}
aria-controls="panel1-content"
sx={{color: '#ffffff'}}
>
<span style={{fontSize: 20, color: '#ffffff'}}>
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
<img className="sergeantIcon" src={getIcon(research.icon)}/>
&nbsp; {research.name}
</span>
</AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255,255,255,0.85)'}}>
<AccordionDetails sx={{color: textColor}}>
<ResearchFull id={research.id} building={building}/>
</AccordionDetails>
</StyledAccordion></div>
}
export function renderAffectedResearches(researches: IResearchShort[], modId: number, raceId: string) {
export function AffectedResearches(props: { researches: IResearchShort[], modId: number, raceId: string }) {
const { researches, modId, raceId } = props;
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
function researchLink(rs: IResearchShort) {
return <span><img style={{verticalAlign: "top", height: 25}}
@@ -79,7 +67,7 @@ export function renderAffectedResearches(researches: IResearchShort[], modId: nu
</StyledLink></span>
}
return <div style={{color: 'rgba(255, 255, 255, 0.85)'}}> {researches.map(rs =>
return <div style={{color: textColor}}> {researches.map(rs =>
rs.buildingId == null ? <span key={rs.id}></span> :
<span key={rs.id}>Research affect: {researchLink(rs)}<br/></span>
)}</div>
+3 -21
View File
@@ -5,7 +5,6 @@ import {
AccordionDetails,
AccordionSummary,
Grid2,
Paper,
Table,
TableBody,
TableCell,
@@ -23,21 +22,7 @@ import {IResearchShort} from "../../types/IResearchShort";
import {ResearchUrl, WeaponUrl} from "../../core/api";
import {IWeapon} from "../../types/IUnit";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../../commons/DescriptionBox";
const StyledPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
import {StyledPaper} from "../../commons/StyledPaper";
interface IResearchFull {
@@ -110,10 +95,7 @@ function ResearchFull(props: { id: number, building: IBuilding }){
</TableContainer><br/>
</Grid2>
<Grid2 size={{xs: 12, md: 8}}>
<DescriptionBox>
{research.description}
</DescriptionBox>
<span style={{ whiteSpace: 'pre-wrap' }}>{research.description}</span>
</Grid2>
<Grid2 size={{xs: 12, md: 12}}>
<ModifiersProvidesTable modifiers={research.modifiers} affectedData={research.affectedData}
@@ -124,7 +106,7 @@ function ResearchFull(props: { id: number, building: IBuilding }){
<Required requirement={research.requirements} modId={building.modId} raceId={building.race.id}/>
</Grid2>}
</Grid2>
<b className="hotkey" >Hotkey: {research.hotkey}</b>
<b className="hotkey" >Hotkey: {research.hotkey} <span style={{ fontSize: '12px', fontWeight: 400, color: 'rgba(128,128,128,0.7)' }}>Filename: {research.filename}</span></b>
</div>
} else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>
}
+9 -6
View File
@@ -1,14 +1,17 @@
import {Button, Link} from "@mui/material";
import {styled} from '@mui/material/styles';
export const BackButton = styled(Button)(({ theme }) => ({
export const BackButton = styled(Button)(({ theme }) => {
const isDark = theme.palette.mode === 'dark';
return {
marginBottom: theme.spacing(3),
color: '#e94560BB',
borderColor: 'rgba(233, 69, 96, 0.5)',
color: isDark ? '#FFD700' : '#1976d2',
borderColor: isDark ? 'rgba(255, 215, 0, 0.5)' : 'rgba(25, 118, 210, 0.5)',
textTransform: 'none',
fontWeight: 600,
'&:hover': {
borderColor: '#e94560',
backgroundColor: 'rgba(233, 69, 96, 0.1)',
borderColor: isDark ? '#FFD700' : '#1976d2',
backgroundColor: isDark ? 'rgba(255, 215, 0, 0.1)' : 'rgba(25, 118, 210, 0.1)',
},
}));
};
});
+3 -4
View File
@@ -1,12 +1,11 @@
import {styled} from '@mui/material/styles';
export const DescriptionBox = styled('div')(({ theme }) => ({
color: 'rgba(255, 255, 255, 0.85)',
color: theme.palette.text.primary,
lineHeight: 1.7,
fontSize: '0.95rem',
padding: theme.spacing(3),
background: 'rgba(255, 255, 255, 0.03)',
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.03)',
borderRadius: '12px',
border: '1px solid rgba(255, 255, 255, 0.05)',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}`,
whiteSpace: 'pre-wrap',
}));
+12
View File
@@ -0,0 +1,12 @@
import {Accordion} from '@mui/material';
import {styled} from '@mui/material/styles';
export const StyledAccordion = styled(Accordion)(({ theme }) => ({
background: theme.palette.mode === 'dark'
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}));
+2 -2
View File
@@ -2,11 +2,11 @@ import {Link} from "@mui/material";
import {styled} from '@mui/material/styles';
export const StyledLink = styled(Link)(({ theme }) => ({
color: '#e94560BB',
color: theme.palette.mode === 'dark' ? '#abc3ff' : '#1976d2',
textDecoration: 'none',
transition: 'all 0.2s ease',
'&:hover': {
color: '#ff6b6bBB',
color: theme.palette.mode === 'dark' ? '#ffffff' : '#1565c0',
textDecoration: 'underline',
},
}));
+19
View File
@@ -0,0 +1,19 @@
import {Paper} from '@mui/material';
import {styled} from '@mui/material/styles';
export const StyledPaper = styled(Paper)(({ theme }) => ({
background: theme.palette.mode === 'dark'
? 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)'
: 'linear-gradient(145deg, #ffffff 0%, #f5f5f5 100%)',
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)'}`,
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: theme.palette.text.primary,
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
fontWeight: 600,
},
}));
+26
View File
@@ -0,0 +1,26 @@
import {styled, TableCell, tableCellClasses} from "@mui/material";
export const StyledTableCell = styled(TableCell)(({theme}) => {
const isDark = theme.palette.mode === 'dark';
const textColor = isDark ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)';
return {
[`&.${tableCellClasses.head}`]: {
backgroundColor: isDark ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
marginRight: 'auto',
marginLeft: 'auto',
paddingLeft: 10,
borderBottom: `2px solid ${isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.2)'}`,
},
[`&.${tableCellClasses.body}`]: {
fontSize: 12,
textAlign: 'center',
color: textColor,
paddingRight: 18,
paddingLeft: 10,
[theme.breakpoints.down('md')]: {
paddingRight: 4,
paddingLeft: 2,
},
},
};
});
+62
View File
@@ -0,0 +1,62 @@
'use client';
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { darkTheme, lightTheme } from '../themes/theme';
type ThemeMode = 'dark' | 'light';
interface ThemeContextType {
mode: ThemeMode;
toggleTheme: () => void;
}
interface ThemeProviderProps {
initialMode?: ThemeMode;
children: ReactNode;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 год
/**
* Синхронизирует выбранную тему с <html>, cookie и localStorage.
* Cookie нужен, чтобы сервер (SSR) сразу отрендерил правильную тему без мерцания.
*/
function applyModeToDocument(mode: ThemeMode) {
document.cookie = `theme=${mode}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; SameSite=Lax`;
}
export const ThemeProvider = ({ initialMode = 'dark', children }: ThemeProviderProps) => {
const [mode, setMode] = useState<ThemeMode>(initialMode);
// Применяем тему к <html> и синхронизируем с cookie/localStorage.
useEffect(() => {
applyModeToDocument(mode);
}, [mode]);
const toggleTheme = () => {
setMode(prev => prev === 'dark' ? 'light' : 'dark');
};
const theme = mode === 'dark' ? darkTheme : lightTheme;
return (
<ThemeContext.Provider value={{ mode, toggleTheme }}>
<MuiThemeProvider theme={theme}>
<CssBaseline />
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
);
};
export const useThemeContext = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useThemeContext must be used within a ThemeProvider');
}
return context;
};
+20 -9
View File
@@ -1,10 +1,21 @@
export const UserUrl = process.env.REACT_APP_HOST_URL + '/api/v1/user';
export const WeaponUrl = process.env.REACT_APP_HOST_URL + '/api/v1/weapon';
export const AbilityUrl = process.env.REACT_APP_HOST_URL + '/api/v1/ability';
export const ResearchUrl = process.env.REACT_APP_HOST_URL + '/api/v1/research';
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/';
import React from 'react';
import { useTheme } from '@mui/material/styles';
export const UserUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/user';
export const WeaponUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/weapon';
export const AbilityUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/ability';
export const ResearchUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/research';
export const AvailableMods = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/mods';
export const AvailableRacesPart = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/races';
export const AvailableUnits = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/units';
export const AvailableBuildings = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/buildings';
export const IconUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/grapics/icon/';
export function withTheme(Component) {
return function WithTheme(props) {
const theme = useTheme();
return <Component theme={theme} {...props} />;
};
}
+2 -1
View File
@@ -1,4 +1,5 @@
import { useParams } from 'react-router-dom';
'use client';
import { useParams } from 'next/navigation';
export function withRouter(Children) {
return (props) => {
+5
View File
@@ -21,6 +21,11 @@
width: 40px;
}
.jumpIcon{
padding: 0px;
width: 40px;
}
.unitHeader{
margin-left: 15px;
}
+39 -5
View File
@@ -1,5 +1,35 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
:root {
--bg-primary: #0a0a0a;
--bg-secondary: #1a1a2e;
--bg-tertiary: #16213e;
--text-primary: #dee2e6;
--text-secondary: #dee2e6;
--text-muted: rgba(255, 255, 255, 0.5);
--border-color: rgba(255, 255, 255, 0.1);
--border-light: rgba(255, 255, 255, 0.05);
--card-bg: rgba(255, 255, 255, 0.03);
--accent: #dee2e6;
--accent-light: #cccccc;
--scrollbar-track: #1a1a2e;
}
html[data-theme="light"] {
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
--bg-tertiary: #e8e8e8;
--text-primary: rgba(0, 0, 0, 0.85);
--text-secondary: rgba(0, 0, 0, 0.6);
--text-muted: rgba(0, 0, 0, 0.5);
--border-color: rgba(0, 0, 0, 0.1);
--border-light: rgba(0, 0, 0, 0.05);
--card-bg: rgba(0, 0, 0, 0.03);
--accent: #e94560;
--accent-light: #ff6b6b;
--scrollbar-track: #e8e8e8;
}
body {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@@ -7,8 +37,12 @@ body {
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #0a0a0a;
color: rgba(255, 255, 255, 0.85);
background-color: var(--bg-primary);
color: var(--text-primary);
}
html[data-theme="light"] code {
background: rgba(0, 0, 0, 0.1);
}
code {
@@ -21,7 +55,7 @@ code {
* {
scrollbar-width: thin;
scrollbar-color: #e94560 #1a1a2e;
scrollbar-color: var(--accent) var(--scrollbar-track);
}
*::-webkit-scrollbar {
@@ -29,11 +63,11 @@ code {
}
*::-webkit-scrollbar-track {
background: #1a1a2e;
background: var(--scrollbar-track);
}
*::-webkit-scrollbar-thumb {
background-color: #e94560;
background-color: var(--accent);
border-radius: 4px;
}
-13
View File
@@ -1,13 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
-326
View File
@@ -1,326 +0,0 @@
import {AvailableBuildings, AvailableMods, IconUrl} from "../core/api";
import React from "react";
import {withRouter} from "../core/withrouter";
import {IShortWeapon} from "../types/IUnit";
import '../css/Building.css'
import {
Button,
Box,
Container,
Divider,
Grid2,
Link,
ListItem,
Paper,
Table,
TableBody,
TableCell,
TableRow,
Tooltip,
Typography, LinearProgress
} from "@mui/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";
import Vision from "../classes/Vision";
import BuildingAddon from "../classes/building/BuildingAddon";
import {IUnitShort} from "../types/IUnitShort";
import Research, {renderAffectedResearches} from "../classes/building/Research";
import Required from "../classes/Required";
import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
import Ability from "../classes/Ability";
import DeathExplosion from "../classes/DeathExplosion";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox";
import {ArrowBack} from "@mui/icons-material";
import {BackButton} from "../commons/BackButton";
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
fontSize: '1.25rem',
}));
const StatsPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-root': {
color: 'rgba(255, 255, 255, 0.9)',
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface UintPageState {
building: IBuilding,
mod: IMod,
}
function Unit (unit: IUnitShort, modId: number, raceId: String) {
return (<Grid2 key={unit.id} size={{xs: 12, md: 3}}>
<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} sx={{color: 'rgba(255,255,255,0.85)', textDecoration: 'none', '&:hover': {color: '#e94560'}}}>
<ListItem sx={{
color: 'rgba(255,255,255,0.85)',
padding: '8px 12px',
background: 'rgba(255,255,255,0.03)',
borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.05)',
transition: 'all 0.2s ease',
'&:hover': { background: 'rgba(233, 69, 96, 0.1)', borderColor: 'rgba(233, 69, 96, 0.3)' }
}}>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> }
{unit.name}
{unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>}
</ListItem>
</Link>
</Grid2>)
}
function Building(building: IBuilding, mod: IMod) {
document.title = building.name + " — " + mod.name
let mapBuildingWeapons: Map<number, Map<number, IShortWeapon>> = 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)
}
})
var buildingName = building.name;
if(building.name == null){
buildingName = building.filename.replaceAll('_', ' ').replace('.rgd', '');
}
return (
<Box>
<Box sx={{ mb: 3 }}>
<Typography variant="h4" component="h2" sx={{
fontWeight: 800,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 2,
}}>
{building.icon &&
<img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
{buildingName}
</Typography>
<Typography variant="subtitle1" sx={{
color: 'rgba(255, 255, 255, 0.6)',
fontWeight: 500,
}}>
{mod.name} ({mod.version})
</Typography>
</Box>
<Grid2 container spacing={3}>
<Grid2 size={{xs: 12, md: 4}}>
<StatsPaper elevation={0}>
<Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table">
<TableRow>
<TableCell component="th" scope="row" >Cost</TableCell>
<TableCell>
{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>
{(building.requisitionIncome !== undefined && building.requisitionIncome > 0 || building.powerIncome !== undefined && building.powerIncome !== null || building.faithIncome !== undefined && building.faithIncome !== null) &&
<TableRow>
<TableCell >Resource income</TableCell>
<TableCell>
{building.requisitionIncome !== undefined && building.requisitionIncome != null &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{building.requisitionIncome}</span>}
{building.powerIncome !== undefined && building.powerIncome > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{building.powerIncome}</span>}
{building.faithIncome !== undefined && building.faithIncome > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_faith.gif"/>&nbsp;
{building.faithIncome}</span>}
</TableCell>
</TableRow>
}
<TableRow>
<TableCell component="th" scope="row" >Armor type</TableCell>
<TableCell>
{building.armorType2 == null ? <ArmorType name={building.armorType.name} compact={false}/>
: <Tooltip title={"upgrade/debuff can turns to " + building.armorType2.name}><span><ArmorType name={building.armorType.name} compact={false}/></span></Tooltip> }
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row" >Health</TableCell>
<TableCell>
<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>
<TableCell component="th" scope="row" >Sight</TableCell>
<TableCell>
<Vision sight={building.sightRadius} detect={building.detectRadius}/>
</TableCell>
</TableRow>
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell component="th" scope="row" >Repair max</TableCell>
<TableCell>{building.repairMax}</TableCell>
</TableRow>
</TableBody>
</Table>
</StatsPaper>
</Grid2>
<Grid2 size={{xs: 12, md: 8}}>
<DescriptionBox>
{building.description}
</DescriptionBox>
</Grid2>
{building.requirements !== null &&
<Grid2 size={{xs: 12, md: 12}}>
<Required requirement={building.requirements} modId={building.modId} raceId={building.race.id}/>
</Grid2>}
{building.modifiers.length > 0 &&
<Grid2 size={{xs: 12, md: 12}}>
<SectionTitle>Affected on</SectionTitle>
<ModifiersProvidesTable modifiers={building.modifiers} race={building.race} modId={building.modId} affectedData={building.affectedData} />
</Grid2>}
{building.units.length > 0 && <Grid2 size={12}>
<SectionTitle>Unit production</SectionTitle>
<Grid2 container spacing={2}>
{building.units.map(unit =>
Unit(unit, mod.id, building.race.id)
)}
</Grid2>
</Grid2>}
{building.abilities.length > 0 && <Grid2 size={12}>
<SectionTitle>Abilities</SectionTitle>
{building.abilities.map(a =>
<Ability mod={mod} ability={a} race={building.race}/>
)}
</Grid2>}
{building.deathExplosions.length > 0 &&
<Grid2 size={12}>
{building.deathExplosions.map(da =>
<DeathExplosion deathExplosion={da} mod={mod}/>
)}
</Grid2>
}
{building.addons.length > 0 && <Grid2 size={12}>
<SectionTitle>Addons</SectionTitle>
{building.addons.map(b =>
<BuildingAddon mod={mod} addon={b} building={building}/>
)}
</Grid2> }
{building.researches.length > 0 && <Grid2 size={12}>
<SectionTitle>Researches</SectionTitle>
{building.researches.map(r =>
<Research key={r.id} research={r} building={building} mod={mod}/>
)}
</Grid2> }
<Grid2 size={12}>
{[...mapBuildingWeapons.keys()].sort(function (a, b) {
return a - b;
}).map(h => <WeaponSlot race={building.race} mod={mod} unitWeapons={mapBuildingWeapons.get(h)} hardpoint={h}/>)}
</Grid2>
<Grid2 size={12}>
{renderAffectedResearches(building.affectedResearches, mod.id, building.race.id)}
</Grid2>
</Grid2>
<Box sx={{ mt: 3, mb: 2, color: '#e94560', fontWeight: 600, fontSize: '0.85rem' }}>
Hotkey: {building.hotkey}
</Box>
<Divider sx={{ my: 3, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<UnitsTable modId={mod.id}/>
</Box>)
}
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 (
<Container maxWidth="lg" sx={{ py: 4 }}>
<BackButton
variant="outlined"
startIcon={<ArrowBack/>}
href={backRef}
>
Back to race
</BackButton>
{Building(this.state.building, this.state.mod)}
</Container>
);
} else {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
);
}
}
}
export default withRouter(BuildingPage);
-134
View File
@@ -1,134 +0,0 @@
import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import React, { useState } from "react";
import {withRouter} from "../core/withrouter";
import {IMod} from "../types/Imod";
import {
ArrowBack,
} from "@mui/icons-material";
import {
Box,
Button,
Container,
Paper,
Typography,
Divider,
LinearProgress
} from "@mui/material";
import {styled} from '@mui/material/styles';
import UnitsTable from "../classes/UnitsTable";
import {BackButton} from "../commons/BackButton";
// Styled components
const PageHeader = styled(Paper)(({ theme }) => ({
padding: theme.spacing(4, 4),
marginBottom: theme.spacing(4),
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
borderRadius: '16px',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 30% 50%, rgba(233, 69, 96, 0.1) 0%, transparent 50%)',
pointerEvents: 'none',
},
}));
interface ModPageState {
mod: IMod | null,
loading: boolean,
}
class ModPage extends React.Component<any, ModPageState> {
constructor(props: any) {
super(props);
this.state = {
mod: null,
loading: true
};
fetch(AvailableMods + "/" + this.props.match.params.modId)
.then(res => res.json())
.then((res: IMod) => {
this.setState({
mod: res,
loading: false
});
})
.catch(() => {
this.setState({ loading: false });
});
}
render() {
if (this.state.loading) {
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<LinearProgress sx={{ width: '200px', mx: 'auto', display: 'block' }} />
</Container>
);
}
if (this.state != null && this.state.mod != null) {
document.title = this.state.mod.name + " — ModWiki";
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<BackButton
variant="outlined"
startIcon={<ArrowBack/>}
href="/"
>
Back to mods list
</BackButton>
<PageHeader elevation={0}>
<Typography variant="h3" component="h1" sx={{
fontWeight: 800,
mb: 1,
background: 'linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
{this.state.mod.name}
</Typography>
<Typography variant="h6" sx={{
color: 'rgba(255, 255, 255, 0.7)',
fontWeight: 500,
}}>
Version {this.state.mod.version}
</Typography>
</PageHeader>
<Divider sx={{ my: 4, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<Box>
<UnitsTable modId={this.state.mod.id}/>
</Box>
</Container>
);
} else {
return (
<Container maxWidth="lg" sx={{ py: 8, textAlign: 'center' }}>
<Typography variant="h5" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
Mod not found
</Typography>
</Container>
);
}
}
}
export default withRouter(ModPage);
-309
View File
@@ -1,309 +0,0 @@
import {AvailableMods} from "../core/api";
import React from "react";
import { NavLink } from "react-router-dom";
import { IMod } from "../types/Imod";
import {
Box,
Container,
Typography,
Grid,
Card,
CardContent,
CardActions,
Chip,
Button,
Divider,
useTheme,
Paper,
LinearProgress
} from "@mui/material";
import { styled } from '@mui/material/styles';
// Styled components
const PageHeader = styled(Paper)(({ theme }) => ({
padding: theme.spacing(6, 4),
marginBottom: theme.spacing(6),
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)',
borderRadius: '16px',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'radial-gradient(circle at 30% 50%, rgba(233, 69, 96, 0.1) 0%, transparent 50%)',
pointerEvents: 'none',
},
}));
const ModCard = styled(Card)(({ theme }) => ({
height: '100%',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '16px',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative',
overflow: 'hidden',
'&::before': {
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: '3px',
background: 'linear-gradient(90deg, #e94560 0%, #ff6b6b 100%)',
transform: 'scaleX(0)',
transition: 'transform 0.3s ease',
},
'&:hover': {
transform: 'translateY(-8px)',
boxShadow: '0 20px 40px rgba(233, 69, 96, 0.2)',
borderColor: 'rgba(233, 69, 96, 0.3)',
'&::before': {
transform: 'scaleX(1)',
},
},
}));
const VersionLink = styled(NavLink)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
padding: theme.spacing(1.5, 2),
marginBottom: theme.spacing(1),
background: 'rgba(255, 255, 255, 0.05)',
borderRadius: '10px',
textDecoration: 'none',
color: 'inherit',
transition: 'all 0.2s ease',
border: '1px solid rgba(255, 255, 255, 0.1)',
'&:hover': {
background: 'rgba(233, 69, 96, 0.15)',
borderColor: 'rgba(233, 69, 96, 0.3)',
transform: 'translateX(4px)',
},
}));
interface ModsProps {
mods: IMod[];
}
function Mods({ mods }: ModsProps) {
const theme = useTheme();
// Sort all mods by order, then group by name
const sortedMods = [...mods].sort((a, b) => a.order - b.order);
const mapWithModVersions = new Map<String, IMod[]>();
sortedMods.forEach(mod => {
const versionList = mapWithModVersions.get(mod.name);
if (versionList == null) {
mapWithModVersions.set(mod.name, [mod]);
} else {
versionList.push(mod);
}
});
const getLatestVersion = (modName: String) => {
const sameMods = mapWithModVersions.get(modName) ?? [];
// The latest version is the one with the maximum id
const latest = sameMods.reduce((max, mod) => mod.id > max.id ? mod : max, sameMods[0]);
const nonBetaMods = sameMods.filter(m => !m.isBeta);
const betaMods = sameMods.filter(m => m.isBeta);
return {
latest: latest,
allVersions: sameMods,
hasBeta: betaMods.length > 0,
betaVersion: betaMods[0]
};
};
function ModCardComponent(modName: String, index: number) {
const sameMods = mapWithModVersions.get(modName) ?? [];
const { latest, hasBeta, betaVersion } = getLatestVersion(modName);
return (
<ModCard sx={{ animationDelay: `${index * 100}ms` }}>
<CardContent sx={{ flexGrow: 1, p: 3 }}>
<Box sx={{ mb: 2 }}>
<Typography variant="h5" component="h2" sx={{
fontWeight: 700,
mb: 1,
background: 'linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
{modName}
</Typography>
<Typography variant="body2" sx={{
color: 'rgba(255, 255, 255, 0.7)',
lineHeight: 1.6,
}}>
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<Chip
label={`${sameMods.length} versions`}
size="small"
sx={{
backgroundColor: 'rgba(233, 69, 96, 0.2)',
color: '#ff6b6b',
fontWeight: 600,
}}
/>
{hasBeta && (
<Chip
label="Beta"
size="small"
color="warning"
sx={{ fontWeight: 600 }}
/>
)}
</Box>
<Divider sx={{ my: 2, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<Typography variant="subtitle2" sx={{
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: 600,
mb: 1.5,
}}>
Main versions:
</Typography>
{sameMods.filter(m => !m.isBeta).map(mod => (
<VersionLink key={mod.id} to={"/mod/" + mod.id} state={mod.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'rgba(255, 255, 255, 0.7)' }}>
Version {mod.version}
</Typography>
<Typography variant="caption" sx={{
color: 'rgba(255, 255, 255, 0.5)',
}}>
→
</Typography>
</Box>
</VersionLink>
))}
{hasBeta && (
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" sx={{
color: 'rgba(255, 193, 7, 0.9)',
fontWeight: 600,
mb: 1,
}}>
Beta versions:
</Typography>
{betaVersion && (
<VersionLink to={"/mod/" + betaVersion.id} state={betaVersion.id}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
Version {betaVersion.version} (Beta)
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
→
</Typography>
</Box>
</VersionLink>
)}
</Box>
)}
</CardContent>
<CardActions sx={{ px: 3, pb: 3 }}>
<Button
size="medium"
variant="outlined"
component={NavLink}
to={"/mod/" + latest?.id}
state={latest?.id}
sx={{
width: '100%',
color: '#e94560',
borderColor: 'rgba(233, 69, 96, 0.5)',
fontWeight: 600,
borderRadius: '10px',
textTransform: 'none',
fontSize: '1rem',
py: 1.5,
'&:hover': {
borderColor: '#e94560',
backgroundColor: 'rgba(233, 69, 96, 0.1)',
transform: 'translateY(-2px)',
boxShadow: '0 8px 20px rgba(233, 69, 96, 0.2)',
},
}}
>
Open latest {latest?.version ?? ''}
</Button>
</CardActions>
</ModCard>
);
}
return (
<Box>
<Grid container spacing={3}>
{[...new Set(sortedMods.map(m => m.name))].map((modName, index) => (
<Grid item xs={12} sm={6} md={4} >
{ModCardComponent(modName, index)}
</Grid>
))}
</Grid>
</Box>
);
}
interface ModsPageState {
mods: IMod[];
loading: boolean;
}
class ModsPage extends React.Component<any, ModsPageState> {
constructor({props}: { props: any }) {
super(props);
this.state = {
mods: [],
loading: true
};
}
async componentDidMount() {
try {
const response = await fetch(AvailableMods);
const data: IMod[] = await response.json();
this.setState({
mods: data,
loading: false
});
} catch (error) {
console.error('Error loading mods:', error);
this.setState({ loading: false });
}
}
render() {
if (this.state.loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
);
}
return <Mods mods={this.state.mods} />;
}
}
export default ModsPage;
-297
View File
@@ -1,297 +0,0 @@
import {AvailableBuildings, AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
import React, {useState} from "react";
import {withRouter} from "../core/withrouter";
import {IMod} from "../types/Imod";
import {Irace} from "../types/Irace";
import '../css/Unit.css'
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Container,
Divider,
Grid2, LinearProgress,
Link,
List,
ListItem,
Paper,
Typography,
} from "@mui/material";
import {ArrowBack, ExpandMore} from "@mui/icons-material";
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
import {styled} from '@mui/material/styles';
import {StyledLink} from "../commons/StyledLink";
import {BackButton} from "../commons/BackButton";
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
fontSize: '1.25rem',
}));
const UnitCard = styled(Paper)(({ theme }) => ({
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid rgba(255, 255, 255, 0.05)',
borderRadius: '10px',
padding: theme.spacing(1.5, 2),
transition: 'all 0.2s ease',
'&:hover': {
background: 'rgba(233, 69, 96, 0.1)',
borderColor: 'rgba(233, 69, 96, 0.3)',
},
}));
const BuildingCard = styled(UnitCard)(({ theme }) => ({}));
interface RacePageState {
mod: IMod,
race: Irace,
units: IUnitShort[],
}
function Unit(unit: IUnitShort, modId: number, raceId: String) {
return (<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} sx={{
color: 'rgba(255,255,255,0.85)',
textDecoration: 'none',
display: 'block',
'&:hover': { color: '#e94560' }
}}>
<ListItem sx={{
color: 'rgba(255,255,255,0.85)',
padding: '6px 12px',
background: 'rgba(255,255,255,0.02)',
borderRadius: '8px',
border: '1px solid rgba(255,255,255,0.05)',
transition: 'all 0.2s ease',
'&:hover': { background: 'rgba(233, 69, 96, 0.1)', borderColor: 'rgba(233, 69, 96, 0.3)' }
}}>
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
{unit.name}
{unit.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>}
</ListItem>
</Link>)
}
function UnitSmall(unit: IUnitShort, modId: number, raceId: String) {
var unitName = ""
if (unit.name.length > 21) {
unitName = unit.name.substring(0, 20) + "...";
} else {
unitName = unit.name;
}
return (<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id} >
{unit.icon && <img className="unitIconSmall" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
<span style={{fontSize: 14}}>{unitName}</span>
{unit.canDetect && <span>&nbsp;<img
src="/images/DETECT_YES.webp"/></span>}<br/></StyledLink>)
}
function Building(building: IBuildingShort, modId: number, raceId: String) {
return (<Grid2 size={{xs: 12, md: 3}}>
<Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id} sx={{
textDecoration: 'none',
display: 'block',
'&:hover': { color: '#e94560' }
}}>
<BuildingCard elevation={0}>
<ListItem sx={{
padding: '8px 12px',
}}>
{building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
<StyledLink>{building.name}</StyledLink>
{building.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>}
</ListItem>
{building.units.map(unit => UnitSmall(unit, modId, raceId))}
</BuildingCard>
</Link>
</Grid2>)
}
interface UnitsProps {
raceId: string,
modId: number,
}
interface UnitsState {
selectedUnits: String | null,
units: IRaceUnits | null,
buildings: IRaceBuildings | null,
}
class Units extends React.Component<UnitsProps, UnitsState> {
constructor(props: any) {
super(props);
let urlUnits = AvailableUnits + "/" + this.props.modId + "/" + this.props.raceId;
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,
})
})
}
render() {
if (this.state && this.state.units) {
if(this.state.buildings?.race.name !== undefined){
document.title = this.state.buildings?.race.name
}
const accordionSx = {
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
};
return (this.state.buildings != null ?
<Box>
<Grid2 container spacing={2}>
{this.state.buildings.buildings.map(building => Building(building, this.props.modId, this.props.raceId))}
</Grid2>
{this.state.buildings.buildingsAdvanced.length > 0 && <Box sx={{ mt: 3 }}>
<Divider sx={{ my: 2, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<SectionTitle>Advanced buildings</SectionTitle>
<Grid2 container spacing={2}>
{this.state.buildings.buildingsAdvanced.map(building => Building(building, this.props.modId, this.props.raceId))}
</Grid2><br/></Box>}
<Box sx={{ mt: 3 }}>
<Accordion sx={accordionSx}>
<AccordionSummary
expandIcon={<ExpandMore sx={{color: '#ffffff'}}/>}
aria-controls="units-accordion"
id="units-accordion"
sx={{color: '#ffffff'}}
>
<SectionTitle sx={{ mb: 0 }}>All units</SectionTitle>
</AccordionSummary>
<AccordionDetails>
<Grid2 container spacing={2}>
<Grid2 size={{xs: 12, md: 4}}>
<Typography variant="h6" sx={{ color: '#ffffff', fontWeight: 600, mb: 2 }}>Infantry</Typography>
{this.state.units.infantry.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</Grid2>
<Grid2 size={{xs: 12, md: 4}}>
<Typography variant="h6" sx={{ color: '#ffffff', fontWeight: 600, mb: 2 }}>Tech</Typography>
<List>
{this.state.units.tech.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</List>
</Grid2>
<Grid2 size={{xs: 12, md: 4}}>
<Typography variant="h6" sx={{ color: '#ffffff', fontWeight: 600, mb: 2 }}>Support</Typography>
<List>
{this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
</List>
</Grid2>
</Grid2>
</AccordionDetails>
</Accordion>
</Box>
</Box> : <Box sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>Loading</Box>
)
} else {
return <Box sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>Loading...</Box>;
}
}
}
class RacePageFast extends React.Component<any, RacePageState> {
constructor(props: any) {
super(props);
}
async componentDidMount() {
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
const modData: IMod = await responseMod.json();
this.setState({
mod: modData
});
const response = await fetch(AvailableRacesPart + "/" + this.props.match.params.raceId);
const racesData: Irace = await response.json();
this.setState({
race: racesData
});
}
render() {
if (this.state != null && this.state.mod != null && this.state.race != null) {
const backRef = "/mod/" + this.state.mod.id
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<BackButton
variant="outlined"
startIcon={<ArrowBack/>}
href={backRef}
>
Back to mod
</BackButton>
<Box sx={{ mb: 4 }}>
<Typography variant="h3" component="h1" sx={{
fontWeight: 800,
mb: 1,
background: 'linear-gradient(135deg, #ffffff 0%, #e0e0e0 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
}}>
{this.state.race.name}
</Typography>
<Typography variant="h6" sx={{
color: 'rgba(255, 255, 255, 0.6)',
fontWeight: 500,
}}>
{this.state.mod.name} ({this.state.mod.version})
</Typography>
</Box>
<Units raceId={this.state.race.id} modId={this.state.mod.id}/>
</Container>
);
} else {
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<LinearProgress sx={{ width: '200px', mx: 'auto', display: 'block' }} />
</Container>
);
}
}
}
export default withRouter(RacePageFast);
-413
View File
@@ -1,413 +0,0 @@
import {AvailableMods, AvailableUnits, IconUrl} from "../core/api";
import React from "react";
import {withRouter} from "../core/withrouter";
import {IShortWeapon, IUnit} from "../types/IUnit";
import '../css/Unit.css'
import {
Accordion,
AccordionDetails,
AccordionSummary,
Box,
Button,
Container,
Divider,
Grid2, LinearProgress,
Paper,
Table,
TableBody,
TableCell,
TableRow,
Tooltip,
Typography,
} from "@mui/material";
import {ArrowBack, ExpandMore} from "@mui/icons-material";
import ArmorType from "../classes/ArmorType";
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
import Sergeant from "../classes/Sergeant";
import WeaponSlot from "../classes/WeaponSlot";
import UnitsTable from "../classes/UnitsTable";
import {IMod} from "../types/Imod";
import Vision from "../classes/Vision";
import Required from "../classes/Required";
import {renderAffectedResearches} from "../classes/building/Research";
import {ModifiersProvidesTable} from "../classes/ModifiersProvideTable";
import Ability from "../classes/Ability";
import DeathExplosion from "../classes/DeathExplosion";
import {styled} from '@mui/material/styles';
import {DescriptionBox} from "../commons/DescriptionBox";
import {BackButton} from "../commons/BackButton";
const SectionTitle = styled(Typography)(({ theme }) => ({
fontWeight: 700,
mb: 2,
fontSize: '1.25rem',
}));
const StatsPaper = styled(Paper)(({ theme }) => ({
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
overflow: 'hidden',
'& .MuiTableBody .MuiTableRow-root': {
'&:last-child td, &:last-child th': { border: 0 },
},
'& .MuiTableBody .MuiTableRow-root .MuiTableCell-head': {
color: '#e94560',
fontWeight: 600,
},
}));
interface UintPageState {
unit: IUnit,
mod: IMod,
}
function Unit(unit: IUnit, mod: IMod) {
document.title = unit.name + " — " + mod.name
const morale = (unit.moraleMax !== null) ? <span>
<img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/>&nbsp;{unit.moraleMax}
+{unit.moraleRegeneration}/s
{unit.moraleDeathPenalty > 0 && <span> <img style={{verticalAlign: "top"}}
src="/images/Kills_icon.webp"/> -{unit.moraleDeathPenalty}</span>}
</span> : "-"
let mapWithUnitWeapons: Map<number, Map<number, IShortWeapon>> = new Map();
unit.weapons.forEach(weapon => {
const weaponMap = mapWithUnitWeapons.get(weapon.hardpoint)
if (weaponMap == null) {
const weaponMap = new Map()
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
mapWithUnitWeapons.set(weapon.hardpoint, weaponMap)
} else {
weaponMap.set(weapon.hardpointOrder, weapon.weapon)
}
})
type sergeantProps = {
name: string
icon: String
canDetect: Boolean
}
const SergeantShort = (props: sergeantProps) => {
return (
<span style={{fontSize: 20, color: '#ffffff'}}>
{props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')}/> }
&nbsp; {props.name}
{props.canDetect && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/DETECT_YES.webp"/></span>}
</span>
)
}
return (
<Box>
<Box sx={{ mb: 3 }}>
<Typography variant="h4" component="h2" sx={{
fontWeight: 800,
mb: 1,
display: 'flex',
alignItems: 'center',
gap: 2,
}}>
{unit.icon &&
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
{unit.name}
</Typography>
<Typography variant="subtitle1" sx={{
color: 'rgba(255, 255, 255, 0.6)',
fontWeight: 500,
}}>
{mod.name} ({mod.version})
</Typography>
</Box>
<Grid2 container spacing={3}>
<Grid2 size={{xs: 12, md: 4}}>
<StatsPaper >
<Table size="small" aria-label="a dense table">
<TableBody id="unit-stats-table" >
<TableRow>
<TableCell component="th" scope="row" >Cost</TableCell>
<TableCell>
{unit.buildCostRequisition > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{unit.buildCostRequisition.toFixed(0)}</span>}
{unit.buildCostPower > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{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.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;
{unit.capInfantry}</span>}
{unit.capSupport > 0 && <span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_cap_vehicle.gif"/>&nbsp;
{unit.capSupport}</span>}
{(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
<span>&nbsp;<AvTimerOutlinedIcon
style={{verticalAlign: "top", fontSize: "18px"}}/>&nbsp;
{unit.buildCostTime}s</span>}
</TableCell>
</TableRow>
{(unit?.reinforceTime !== 0 && unit.reinforceTime !== null && unit.squadMaxSize > 1) &&
<TableRow>
<TableCell >Reinforce cost</TableCell>
<TableCell>
{unit.reinforceCostRequisition && unit.reinforceCostRequisition > 0 ?
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{unit.reinforceCostRequisition.toFixed(0)}</span>:<span/>}
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{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.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;
{unit.reinforceTime}s</span>}
</TableCell>
</TableRow>
}
{(unit.requisitionIncome !== undefined && unit.requisitionIncome !== null || unit.powerIncome !== undefined && unit.powerIncome !== null || unit.faithIncome !== undefined && unit.faithIncome !== null) &&
<TableRow>
<TableCell >Resource income</TableCell>
<TableCell>
{unit.requisitionIncome !== undefined && unit.requisitionIncome > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_requisition.gif"/>&nbsp;
{unit.requisitionIncome}</span>}
{unit.powerIncome !== undefined && unit.powerIncome > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_power.gif"/>&nbsp;
{unit.powerIncome}</span>}
{unit.faithIncome !== undefined && unit.faithIncome > 0 &&
<span>&nbsp;<img style={{verticalAlign: "top"}}
src="/images/Resource_faith.gif"/>&nbsp;
{unit.faithIncome}</span>}
</TableCell>
</TableRow>
}
{unit.squadMaxSize > 1 &&
<TableRow>
<TableCell >Squad size</TableCell>
<TableCell>{unit.squadStartSize} / {unit.squadMaxSize}</TableCell>
</TableRow>
}
<TableRow>
<TableCell >Armor type</TableCell>
<TableCell>
{unit.armorType2 == null ? <ArmorType name={unit.armorType.name} compact={false}/> : <Tooltip title={"upgrade/debuff can turns to " + unit.armorType2.name}><span><ArmorType name={unit.armorType.name} compact={false}/></span></Tooltip> }
</TableCell>
</TableRow>
<TableRow>
<TableCell >Health</TableCell>
<TableCell>
<img style={{verticalAlign: "top"}}
src="/images/Health_icon.webp"/>&nbsp;
{unit.health} {unit.healthRegeneration > 0 &&
<span>+{unit.healthRegeneration}/s</span>}
{unit.armour !== undefined && unit.armour !== 0 && <span><img style={{height: 20, verticalAlign: "top"}} src="/images/defence.png"/>{unit.armour} </span> }
</TableCell>
</TableRow>
<TableRow>
<TableCell >Move speed</TableCell>
<TableCell>{unit.moveSpeed}</TableCell>
</TableRow>
<TableRow>
<TableCell >Morale</TableCell>
<TableCell>{morale}</TableCell>
</TableRow>
<TableRow>
<TableCell >Mass</TableCell>
<TableCell>{unit.mass}</TableCell>
</TableRow>
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell >Vision</TableCell>
<TableCell><Vision sight={unit.sightRadius} detect={unit.detectRadius}/></TableCell>
</TableRow>
{unit.repairMax !== undefined && unit.repairMax !== null &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell >Repair max</TableCell>
<TableCell>{unit.repairMax}</TableCell>
</TableRow>
}
{unit.repairSpeed !== undefined && unit.repairSpeed !== null && unit.repairCostPercent !== null &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell >Repair</TableCell>
<TableCell>{unit.repairSpeed} hp/s; {unit.repairCostPercent}% cost</TableCell>
</TableRow>
}
{unit.mobValue != null &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell >Mob value</TableCell>
<TableCell><span> <img style={{height: 20, verticalAlign: "top"}} src="/images/Mob_bonus.gif"/> {unit.mobValue} </span></TableCell>
</TableRow>
}
{unit.squadLimit !== undefined && unit.squadLimit !== null &&
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
<TableCell >Limit</TableCell>
<TableCell>{unit.squadLimit}</TableCell>
</TableRow>
}
</TableBody>
</Table>
</StatsPaper>
</Grid2>
<Grid2 size={{xs: 12, md: 8}}>
<DescriptionBox>
{unit.description}
</DescriptionBox>
</Grid2>
{unit.requirements !== null &&
<Grid2 size={{xs: 12, md: 12}}>
<Required requirement={unit.requirements} modId={unit.modId} raceId={unit.race.id}/>
</Grid2>}
<Grid2 size={12}>
{unit.sergeants.map(s =>
<Accordion key={s.id} sx={{
background: 'linear-gradient(145deg, #1a1a2e 0%, #16213e 100%)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: '12px',
mb: 1,
'&:before': { display: 'none' },
'&.Mui-expanded': { margin: '0 0 8px 0' },
}}>
<AccordionSummary
sx={{color: '#ffffff'}}>
<SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0}/>
</AccordionSummary>
<AccordionDetails sx={{color: 'rgba(255, 255, 255, 0.85)'}}>
<Sergeant mod={mod} sergeant={s} race={unit.race}/>
</AccordionDetails>
</Accordion>)}
</Grid2>
{unit.moraleMax !== null && !(unit.moraleBrakeModifiers.find(m => m.reference.includes("accuracy_weapon_modifier") && m.value === 0.2) !== undefined &&
unit.moraleBrakeModifiers.find(m => m.reference.includes("speed_maximum_modifier") && m.value === 1.2) !== undefined &&
unit.moraleBrakeModifiers.length === 2)
&& <Grid2 size={12}>
<SectionTitle><img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/> Morale broken unusual behavior <img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/></SectionTitle>
<ModifiersProvidesTable modifiers={unit.moraleBrakeModifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/>
</Grid2>}
{unit.modifiers.length > 0 && <Grid2 size={12}>
<SectionTitle>Affected on</SectionTitle>
<ModifiersProvidesTable modifiers={unit.modifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/>
</Grid2>}
{unit.abilities.length > 0 && <Grid2 size={12}>
<SectionTitle>Abilities</SectionTitle>
{unit.abilities.map(a =>
<Ability mod={mod} ability={a} race={unit.race}/>
)}
</Grid2>}
<Grid2 size={12}>
{[...mapWithUnitWeapons.keys()].sort(function (a, b) {
return a - b;
}).map(h => <WeaponSlot haveReinforceMenu={unit.haveReinforceMenu} key ={h} race={unit.race} mod={mod} unitWeapons={mapWithUnitWeapons.get(h)} hardpoint={h}/>)}
</Grid2>
{unit.deathExplosions.length > 0 &&
<Grid2 size={12}>
{unit.deathExplosions.map(da =>
<DeathExplosion deathExplosion={da} mod={mod}/>
)}
</Grid2>
}
<Grid2 size={12}>
{renderAffectedResearches(unit.affectedResearches, mod.id, unit.race.id)}
</Grid2>
</Grid2>
<Box sx={{ mt: 3, mb: 2, fontWeight: 600, fontSize: '0.85rem' }}>
Hotkey: {unit.hotkey}
</Box>
<Divider sx={{ my: 3, borderColor: 'rgba(255, 255, 255, 0.1)' }} />
<UnitsTable modId={mod.id}/>
</Box>
)
}
class UnitPage extends React.Component<any, UintPageState> {
async componentDidMount() {
const unitsResponse = await fetch(AvailableUnits + "/" + this.props.match.params.unitId);
const unitsData: IUnit = await unitsResponse.json();
this.setState({
unit: unitsData
});
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
const modData: IMod = await responseMod.json();
this.setState({
mod: modData
});
}
render() {
if (this.state != null && this.state.unit != null && this.state.mod != null) {
const backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<BackButton
variant="outlined"
startIcon={<ArrowBack/>}
href={backRef}
>
Back to race
</BackButton>
{Unit(this.state.unit, this.state.mod)}
</Container>
);
} else {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<LinearProgress sx={{ width: '200px' }} />
</Box>
);
}
}
}
export default withRouter(UnitPage);
+83
View File
@@ -0,0 +1,83 @@
import { createTheme } from '@mui/material/styles';
export const darkTheme = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 900,
lg: 1320,
xl: 1536,
},
},
palette: {
mode: 'dark',
primary: {
main: "#1a1a2e",
},
background: {
default: '#0a0a0a',
paper: '#1a1a2e',
},
text: {
primary: '#dee2e6',
secondary: '#dee2e6',
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", sans-serif',
},
shape: {
borderRadius: 12,
},
components: {
MuiTableCell: {
styleOverrides: {
root: {
color: '#dee2e6',
},
},
},
},
});
export const lightTheme = createTheme({
breakpoints: {
values: {
xs: 0,
sm: 600,
md: 900,
lg: 1320,
xl: 1536,
},
},
palette: {
mode: 'light',
primary: {
main: "#1a1a2e",
},
background: {
default: '#f5f5f5',
paper: '#ffffff',
},
text: {
primary: '#111111',
secondary: '#333333',
},
},
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", sans-serif',
},
shape: {
borderRadius: 12,
},
components: {
MuiTableCell: {
styleOverrides: {
root: {
color: '#111111',
},
},
},
},
});
+1
View File
@@ -32,6 +32,7 @@ export interface IBuilding {
sightRadius: number
detectRadius: number
repairMax: number
buildingLimit?: number
icon: string
modId: number
modifiers: IModifier[]
+1
View File
@@ -2,5 +2,6 @@ export interface IResearchShort {
id: number;
name: string;
icon: string;
uiIndexHint: number;
buildingId: number;
}
+33
View File
@@ -13,6 +13,22 @@ export interface IUnitResponse {
units: IUnit[]
}
export interface IJumps {
chargeRegeneration: number;
goUpTime?: number;
jumpDistanceMax: number;
breakdownTime: number;
chargeJumpCostMin: number;
chargeJumpCostMax: number;
jumpHeight: number;
chargeMax: number;
chargeStartingFraction: number;
setupTime: number;
goDownTime?: number;
isTeleport: boolean;
requirements: IRequirement | null;
}
export interface IUnit {
id: number
race: Irace
@@ -43,6 +59,9 @@ export interface IUnit {
mass: number
upTime: number
moveSpeed: number
accelerationTime?: number
decelerationTime?: number
rotationRate?: number
sightRadius: number
detectRadius: number
reinforceCostRequisition?: number
@@ -69,6 +88,7 @@ export interface IUnit {
affectedData: IAffectedData,
affectedResearches: IResearchShort[],
deathExplosions: IDeathExplosion[],
jumps?: IJumps | null,
hotkey?: string,
}
@@ -125,6 +145,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
@@ -156,6 +188,7 @@ export interface IWeapon {
modifiers: IModifier[]
weaponArmorPiercing: IPiercing[]
affectedResearches: IResearchShort[],
researches: IWeaponResearch[],
hotkey?: string,
}
+1
View File
@@ -5,4 +5,5 @@ export interface IMod {
version: string;
technicalName: string;
isBeta: boolean;
isHide: boolean;
}
+25 -9
View File
@@ -1,26 +1,42 @@
{
"compilerOptions": {
"target": "ES6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "ES2017",
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"strictNullChecks": false,
"noEmit": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
},
"strictNullChecks": true
},
"include": [
"src"
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}