Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eebe50a373 | ||
|
|
fa0baa62c1 | ||
|
|
23bfad8956 | ||
|
|
2184f1642c | ||
|
|
4e4f798d85 | ||
|
|
5cbaab4b1f | ||
|
|
03f152fbad | ||
|
|
405b562f8f | ||
|
|
57779cacbd | ||
|
|
c750e19443 | ||
|
|
b603732dd6 | ||
|
|
63455578b1 | ||
|
|
5669a70156 | ||
|
|
bc36f1e56d |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#### Сборка
|
||||
|
||||
При сборке дистрибутива формируй архив dow-wiki-deploy.tar.gz
|
||||
Предварительно удали всё что было до этого в папке deploy и собирай туда
|
||||
@@ -1,6 +1,6 @@
|
||||
# Soulstorm wiki frontend
|
||||
|
||||
## Стек технологий:
|
||||
## Tech stack:
|
||||
|
||||
- react (вперемешку классы и функции)
|
||||
- react (mix of classes and functions)
|
||||
- typescript
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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`,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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> <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> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_requisition.gif" />
|
||||
{building.buildCostRequisition.toFixed(0)}</span>}
|
||||
{building.buildCostPower > 0 && <span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_power.gif" />
|
||||
{building.buildCostPower.toFixed(0)}</span>}
|
||||
{(building.buildCostPopulation !== undefined && building.buildCostPopulation > 0) &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_orksquadcap.gif" />
|
||||
{building.buildCostPopulation.toFixed(0)}</span>}
|
||||
{(building.buildCostFaith !== undefined && building.buildCostFaith > 0) &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_faith.gif" />
|
||||
{building.buildCostFaith}</span>}
|
||||
{(building.buildCostSouls !== undefined && building.buildCostSouls > 0) &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_souls.gif" />
|
||||
{building.buildCostSouls.toFixed(0)}</span>}
|
||||
{(building.buildCostTime !== undefined && building.buildCostTime > 0) &&
|
||||
<span> <AvTimerOutlinedIcon
|
||||
style={{ verticalAlign: "top", fontSize: "18px" }} />
|
||||
{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> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_requisition.gif" />
|
||||
{building.requisitionIncome}</span>}
|
||||
{building.powerIncome !== undefined && building.powerIncome > 0 &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_power.gif" />
|
||||
{building.powerIncome}</span>}
|
||||
{building.faithIncome !== undefined && building.faithIncome > 0 &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_faith.gif" />
|
||||
{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" />
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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> <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('\\', '/')} />}
|
||||
<span style={{ fontSize: 14 }}>{unitName}</span>
|
||||
{unit.canDetect && <span> <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('\\', '/')} />}
|
||||
<StyledLink>{building.name}</StyledLink>
|
||||
{building.canDetect && <span> <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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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" /> {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('\\', '/')} />}
|
||||
{props.name}
|
||||
{props.canDetect && <span> <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> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_requisition.gif" />
|
||||
{unit.buildCostRequisition.toFixed(0)}</span>}
|
||||
{unit.buildCostPower > 0 && <span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_power.gif" />
|
||||
{unit.buildCostPower.toFixed(0)}</span>}
|
||||
{(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_orksquadcap.gif" />
|
||||
{unit.buildCostPopulation.toFixed(0)}</span>}
|
||||
{(unit.buildCostFaith !== undefined && unit.buildCostFaith > 0) &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_faith.gif" />
|
||||
{unit.buildCostFaith}</span>}
|
||||
{(unit.buildCostSouls !== undefined && unit.buildCostSouls > 0) &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_souls.gif" />
|
||||
{unit.buildCostSouls.toFixed(0)}</span>}
|
||||
{unit.capInfantry > 0 && <span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_cap_infantry.gif" />
|
||||
{unit.capInfantry}</span>}
|
||||
{unit.capSupport > 0 && <span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_cap_vehicle.gif" />
|
||||
{unit.capSupport}</span>}
|
||||
{(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
|
||||
<span> <AvTimerOutlinedIcon
|
||||
style={{ verticalAlign: "top", fontSize: "18px" }} />
|
||||
{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> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_requisition.gif" />
|
||||
{unit.reinforceCostRequisition.toFixed(0)}</span> : <span />}
|
||||
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_power.gif" />
|
||||
{unit.reinforceCostPower.toFixed(0)}</span>}
|
||||
{(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_orksquadcap.gif" />
|
||||
{unit.reinforceCostPopulation.toFixed(0)}</span>}
|
||||
{(unit.reinforceCostFaith !== undefined && unit.reinforceCostFaith > 0) &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_faith.gif" />
|
||||
{unit.reinforceCostFaith}</span>}
|
||||
{(unit.reinforceCostSouls !== undefined && unit.reinforceCostSouls > 0) &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_souls.gif" />
|
||||
{unit.reinforceCostSouls.toFixed(0)}</span>}
|
||||
{(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
|
||||
<span> <AvTimerOutlinedIcon
|
||||
style={{ verticalAlign: "top", fontSize: "18px" }} />
|
||||
{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> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_requisition.gif" />
|
||||
{unit.requisitionIncome}</span>}
|
||||
{unit.powerIncome !== undefined && unit.powerIncome > 0 &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_power.gif" />
|
||||
{unit.powerIncome}</span>}
|
||||
{unit.faithIncome !== undefined && unit.faithIncome > 0 &&
|
||||
<span> <img style={{ verticalAlign: "top" }}
|
||||
src="/images/Resource_faith.gif" />
|
||||
{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" />
|
||||
{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>
|
||||
{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>
|
||||
{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>
|
||||
{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>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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.
|
||||
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -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>
|
||||
@@ -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",
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import './App.css';
|
||||
import React from "react";
|
||||
import {AppBar, Container, createTheme, IconButton, ThemeProvider, Toolbar, Typography} from "@mui/material";
|
||||
import {MyRoutes} from "./Routes";
|
||||
|
||||
function App() {
|
||||
console.log(process.env);
|
||||
|
||||
const steamLogin = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
(document.getElementById('submit-steam-login') as HTMLFormElement).click()
|
||||
};
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: "#191919",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<div >
|
||||
<ThemeProvider theme={theme}>
|
||||
<AppBar position="static">
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
size="large"
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
sx={{mr: 2}}
|
||||
>
|
||||
</IconButton>
|
||||
<Typography variant="h6" component="div" sx={{flexGrow: 1}}>
|
||||
Autogeneration wiki
|
||||
</Typography>
|
||||
{/*<Button color="inherit" onClick={steamLogin}>
|
||||
Log in through Steam
|
||||
</Button>*/}
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Container>
|
||||
<MyRoutes/>
|
||||
</Container>
|
||||
</ThemeProvider>
|
||||
|
||||
|
||||
<form action="https://steamcommunity.com/openid/login" method="post">
|
||||
<input type="hidden" name="openid.identity"
|
||||
value="http://specs.openid.net/auth/2.0/identifier_select"/>
|
||||
<input type="hidden" name="openid.claimed_id"
|
||||
value="http://specs.openid.net/auth/2.0/identifier_select"/>
|
||||
<input type="hidden" name="openid.ns" value="http://specs.openid.net/auth/2.0"/>
|
||||
<input type="hidden" name="openid.mode" value="checkid_setup"/>
|
||||
<input type="hidden" name="openid.realm" value={process.env.REACT_APP_HOST_URL}/>
|
||||
<input type="hidden" name="openid.return_to"
|
||||
value={process.env.REACT_APP_HOST_URL + "\\login"}/>
|
||||
<button id="submit-steam-login" type="submit" hidden={true}></button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +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 {StyledAccordion} from "../commons/StyledAccordion";
|
||||
|
||||
interface IAbilityProps {
|
||||
ability: IAbilityShort,
|
||||
@@ -15,25 +16,29 @@ 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}><Accordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
|
||||
return <div className='addon-research-accordion' id={"research-" + ability.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore/>}
|
||||
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
|
||||
expandIcon={<ExpandMore sx={{color: textColor}}/>}
|
||||
aria-controls="panel1-content"
|
||||
>
|
||||
<span style={{fontSize: 20}}>
|
||||
{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)}/>}
|
||||
{ability.name ? ability.name : ability.fileName.charAt(0).toUpperCase() + ability.fileName.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
|
||||
<i style={{fontSize: 12}}> ({ability.activationType})</i>
|
||||
<i style={{fontSize: 12, color: 'rgba(255,255,255,0.5)', marginLeft: 4}}>({ability.activationType})</i>
|
||||
</span>
|
||||
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<AccordionDetails sx={{color: textColor}}>
|
||||
<AbilityFull abilityId={ability.id} mod={props.mod} race={props.race} />
|
||||
</AccordionDetails>
|
||||
</Accordion></div>
|
||||
</StyledAccordion></div>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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,33 +25,27 @@ import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
||||
import ability from "./Ability";
|
||||
import AbilitySpawnedObject from "./AbilitySpawnedObject";
|
||||
import DpsTable from "./DpsTable";
|
||||
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: "rgb(244,244,244)",
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
paddingLeft: 10
|
||||
},
|
||||
[`&.${tableCellClasses.body}`]: {
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
paddingRight: 18,
|
||||
paddingLeft: 10
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if(props.abilityFull !== undefined){
|
||||
@@ -90,21 +82,21 @@ 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>
|
||||
<div style={{color: textColor}}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{xs: 12}}>
|
||||
{ability.initialDelayTime !== undefined && props.isChild === true &&
|
||||
<b><br/><center> After {ability.initialDelayTime.toFixed(1)}s
|
||||
<b><br/><center style={{color: textColor}}> After {ability.initialDelayTime.toFixed(1)}s
|
||||
<img style={{verticalAlign: "top"}}
|
||||
src="/images/Time_icon.webp"/></center></b>}
|
||||
</Grid2>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody id="unit-stats-table">
|
||||
<TableRow>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<TableCell component="th" scope="row" >Affected on</TableCell>
|
||||
<TableCell component="th" scope="row">{ability.areaFilter}</TableCell>
|
||||
<TableCell>{ability.areaFilter}</TableCell>
|
||||
</TableRow>
|
||||
{props.isChild !== true && (ability.costPower !== undefined && ability.costPower > 0 ||
|
||||
ability.costRequisition !== undefined && ability.costRequisition > 0 ||
|
||||
@@ -114,7 +106,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
{ability.costRequisition !== undefined && ability.costRequisition > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
@@ -136,18 +128,18 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
||||
{props.isChild !== true && ability.rechargeTime !== undefined && ability.rechargeTime > 0 && ability.activation !== "On/off ability" &&
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<TableCell component="th" scope="row" >Recharge time</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
<img style={{verticalAlign: "top"}}
|
||||
src="/images/Time_icon.webp"/>
|
||||
{ability.rechargeTime}s
|
||||
{ability.rechargeTimerGlobal && <i style={{fontSize: 10}}>global cooldown</i>}
|
||||
{ability.rechargeTimerGlobal && <i style={{fontSize: 10, color: 'rgba(255,255,255,0.5)'}}>global cooldown</i>}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{props.isChild !== true && ability.initialDelayTime !== undefined && ability.initialDelayTime > 0 &&
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<TableCell component="th" scope="row" >Delay after cast</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
<img style={{verticalAlign: "top"}}
|
||||
src="/images/Time_icon.webp"/>
|
||||
{ability.initialDelayTime.toFixed(1)}s
|
||||
@@ -157,7 +149,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
||||
{ability.durationTime !== undefined && ability.durationTime > 1.5 &&
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<TableCell component="th" scope="row" >Duration time</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
<img style={{verticalAlign: "top"}}
|
||||
src="/images/Time_icon.webp"/>
|
||||
{ability.durationTime.toFixed(1)}s
|
||||
@@ -167,15 +159,15 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
||||
{ability.areaEffect !== null && ability.radius !== undefined && ability.radius > 0 &&
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<TableCell component="th" scope="row" >Radius</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{ability.radius.toFixed(0)} <i style={{fontSize: 10}}>{ability.areaEffect}</i>
|
||||
<TableCell>
|
||||
{ability.radius.toFixed(0)} <i style={{fontSize: 10, color: 'rgba(255,255,255,0.5)'}}>{ability.areaEffect}</i>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{props.isChild !== true && ability.range !== undefined && ability.areaFilter !== "Own" && ability.range > 0 &&
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<TableCell component="th" scope="row" >Range</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
{ability.range.toFixed(0)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -185,32 +177,89 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Throw force</TableCell>
|
||||
<TableCell component="th"
|
||||
scope="row">{ability.throwForceMin != 0 ? ability.throwForceMin + " - " + ability.throwForceMax : "-"}
|
||||
<TableCell>{ability.throwForceMin != 0 ? ability.throwForceMin + " - " + ability.throwForceMax : "-"}
|
||||
</TableCell>
|
||||
</TableRow>}
|
||||
<TableRow>
|
||||
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer><br/>
|
||||
</Grid2>
|
||||
<Grid2 size={8}>
|
||||
<div style={{whiteSpace: "pre-wrap"}}>
|
||||
{props.isChild !== true && ability.description}
|
||||
</div>
|
||||
{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>Every <b>{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
|
||||
@@ -235,7 +284,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell>
|
||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell>
|
||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell>
|
||||
@@ -272,7 +321,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleLow)}</StyledTableCell>
|
||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleMedium)}</StyledTableCell>
|
||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleHigh)}</StyledTableCell>
|
||||
@@ -286,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
|
||||
@@ -321,7 +370,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell>
|
||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell>
|
||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell>
|
||||
@@ -342,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}/>}
|
||||
@@ -352,10 +402,10 @@ 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>}
|
||||
{ability.activation !== "Passive ability" && props.isChild !== true && <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>loading...</div>;
|
||||
} else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary, Grid2, Paper,
|
||||
AccordionSummary, Grid2,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell, TableContainer,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Tooltip
|
||||
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";
|
||||
@@ -21,6 +23,16 @@ import ArmorType from "./ArmorType";
|
||||
import Vision from "./Vision";
|
||||
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,
|
||||
mb: 2,
|
||||
fontSize: '1.25rem',
|
||||
}));
|
||||
|
||||
|
||||
interface AbilityEnvironmentProps {
|
||||
@@ -30,39 +42,43 @@ 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>
|
||||
<h3> {props.abilityEnvironment.name ?? props.abilityEnvironment.filename.charAt(0).toUpperCase() + props.abilityEnvironment.filename.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
|
||||
<i style={{fontSize: 11}}> (Spawned object)</i>
|
||||
</h3>
|
||||
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>
|
||||
</SectionTitle>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody id="unit-stats-table">
|
||||
{abilityEnvironment.armorType !== null &&
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Armor type</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<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 component="th" scope="row">
|
||||
<StyledTableCell component="th" scope="row" >Health</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<img style={{verticalAlign: "top"}}
|
||||
src="/images/Health_icon.webp"/>
|
||||
{abilityEnvironment.health} {abilityEnvironment.healthRegeneration != null && abilityEnvironment.healthRegeneration > 0 &&
|
||||
@@ -70,38 +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 component="th" scope="row">
|
||||
{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 component="th" scope="row">
|
||||
<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 component="th" scope="row">
|
||||
<StyledTableCell component="th" scope="row" >Lifetime</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<img style={{verticalAlign: "top"}}
|
||||
src="/images/Time_icon.webp"/>
|
||||
{abilityEnvironment.lifetime.toFixed(0)}s
|
||||
</TableCell>
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
}
|
||||
</TableBody>
|
||||
@@ -109,9 +123,9 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
|
||||
</TableContainer>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs: 12, md: 8}}>
|
||||
<div style={{whiteSpace: "pre-wrap"}}>
|
||||
<DescriptionBox>
|
||||
{abilityEnvironment.description}
|
||||
</div>
|
||||
</DescriptionBox>
|
||||
</Grid2>
|
||||
{abilityEnvironment.abilities.length > 0 && <Grid2 size={12}>
|
||||
{abilityEnvironment.abilities.map(a =>
|
||||
@@ -120,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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,57 +24,60 @@ import {IDeathExplosion} from "../types/IDeathExplosion";
|
||||
import Required from "./Required";
|
||||
import ArmorTypeNames from "../types/ArmorTypeValues";
|
||||
import DpsTable from "./DpsTable";
|
||||
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 ?
|
||||
<div className='addon-research-accordion' id='death-explosion'><Accordion>
|
||||
return deathExplosions.length > 0 ?
|
||||
<div className='addon-research-accordion' id='death-explosion'><StyledAccordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore/>}
|
||||
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
|
||||
expandIcon={<ExpandMore sx={{color: textColor}}/>}
|
||||
aria-controls="panel1-content"
|
||||
>
|
||||
<span style={{fontSize: 20}}>
|
||||
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
|
||||
Death Explosion
|
||||
</span>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<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={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody id="unit-stats-table">
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Chance</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{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 component="th" scope="row">
|
||||
{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 component="th" scope="row">
|
||||
{deathExplosion.throwForceMin} - {deathExplosion.throwForceMax}
|
||||
</TableCell>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<StyledTableCell component="th" scope="row" >Throw force</StyledTableCell>
|
||||
<StyledTableCell>{deathExplosion.throwForceMin} - {deathExplosion.throwForceMax}</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -87,8 +90,9 @@ function DeathExplosion(props: DeathExpProps) {
|
||||
moraleDamage={deathExplosion.moraleDamage}/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
</Accordion></div> : <span></span>
|
||||
</StyledAccordion></div> : <span></span>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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,23 +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: "rgb(244,244,244)",
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
paddingLeft: 10
|
||||
},
|
||||
[`&.${tableCellClasses.body}`]: {
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
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
|
||||
@@ -89,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") ?
|
||||
@@ -150,15 +206,13 @@ 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>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
|
||||
@@ -172,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
|
||||
@@ -204,15 +258,14 @@ 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>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
|
||||
|
||||
@@ -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}/>
|
||||
{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"/> {jumps.setupTime}s</span> },
|
||||
{ label: "Breakdown time", value: <span><img style={{verticalAlign: "top"}} src="/images/Time_icon.webp"/> {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"/> {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"/> {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;
|
||||
@@ -1,11 +1,16 @@
|
||||
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";
|
||||
import {IAffectedData} from "../types/IAffectedData";
|
||||
import {IMod} from "../types/Imod";
|
||||
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
||||
import {styled} from '@mui/material/styles';
|
||||
import {StyledLink} from "../commons/StyledLink";
|
||||
import {StyledPaper} from "../commons/StyledPaper";
|
||||
|
||||
|
||||
interface IModifiersProvidesTable {
|
||||
modifiers: IModifier[],
|
||||
@@ -138,19 +143,21 @@ 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"))
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
{noWeaponMods.map(m =>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">{getModName(m.reference)}</TableCell>
|
||||
<TableCell component="th"
|
||||
scope="row">{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>
|
||||
@@ -159,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
|
||||
|
||||
@@ -168,37 +178,37 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
|
||||
var affectedUnit = props.affectedData.affectedUnits.find(au => au.filename.replaceAll('.rgd', '').split(";")[0] === target || au.filename.replaceAll('.rgd', '').split(";")[1] === target);
|
||||
if (affectedUnit != null) {
|
||||
return <span><img style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(affectedUnit.icon)}/><a
|
||||
src={getIcon(affectedUnit.icon)}/><StyledLink
|
||||
href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedUnit.id}`}>
|
||||
{affectedUnit.name}
|
||||
</a></span>
|
||||
</StyledLink></span>
|
||||
}
|
||||
var affectedSergeant = props.affectedData.affectedSergeants.find(as => as.filename.replaceAll('.rgd', '') === target);
|
||||
if (affectedSergeant != null) {
|
||||
return <span><img style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(affectedSergeant.icon)}/>{affectedSergeant.name} at <img
|
||||
style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(affectedSergeant.unit.icon)}/><a
|
||||
src={getIcon(affectedSergeant.unit.icon)}/><StyledLink
|
||||
href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedSergeant.unit.id}`}>
|
||||
{affectedSergeant.unit.name}
|
||||
</a></span>
|
||||
</StyledLink></span>
|
||||
}
|
||||
var affectedBuilding = props.affectedData.affectedBuildings.find(ab => ab.filename.replaceAll('.rgd', '') === target);
|
||||
if (affectedBuilding != null) {
|
||||
return <span><img style={{verticalAlign: "top", height: 25}} src={getIcon(affectedBuilding.icon)}/><a
|
||||
return <span><img style={{verticalAlign: "top", height: 25}} src={getIcon(affectedBuilding.icon)}/><StyledLink
|
||||
href={`/mod/${props.modId}/race/${props.race.id}/building/${affectedBuilding.id}`}>
|
||||
{affectedBuilding.name}
|
||||
</a> </span>
|
||||
</StyledLink> </span>
|
||||
}
|
||||
|
||||
var affectedWeapon = props.affectedData.affectedWeapons.find(aw => aw.filename.replaceAll('.rgd', '') === target);
|
||||
if (affectedWeapon != null) {
|
||||
return <span>{affectedWeapon.name ? affectedWeapon.name : affectedWeapon.filename.replaceAll('_', ' ').replace('.rgd', '')} at {
|
||||
affectedWeapon.units.map(unit =>
|
||||
<span>{affectedWeapon != null && affectedWeapon.units.length + affectedWeapon.sergeants.length > 1 ? <br/> : <span></span>}<img style={{verticalAlign: "top", height: 25}} src={getIcon(unit.icon)}/><a
|
||||
<span>{affectedWeapon != null && affectedWeapon.units.length + affectedWeapon.sergeants.length > 1 ? <br/> : <span></span>}<img style={{verticalAlign: "top", height: 25}} src={getIcon(unit.icon)}/><StyledLink
|
||||
href={`/mod/${props.modId}/race/${props.race.id}/unit/${unit.id}`}>
|
||||
{unit.name}
|
||||
</a> </span>
|
||||
</StyledLink> </span>
|
||||
)
|
||||
}
|
||||
{
|
||||
@@ -206,18 +216,18 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
|
||||
<span>{affectedWeapon != null && affectedWeapon.units.length + affectedWeapon.sergeants.length > 1 ? <br/> : <span></span>}<img style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(affectedSergeant.icon)}/>{affectedSergeant.name} at <img
|
||||
style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(affectedSergeant.unit.icon)}/><a
|
||||
src={getIcon(affectedSergeant.unit.icon)}/><StyledLink
|
||||
href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedSergeant.unit.id}`}>
|
||||
{affectedSergeant.unit.name}
|
||||
</a> </span>
|
||||
</StyledLink> </span>
|
||||
)
|
||||
}
|
||||
{
|
||||
affectedWeapon.buildings.map(affectedBuilding =>
|
||||
<span><img style={{verticalAlign: "top", height: 25}} src={getIcon(affectedBuilding.icon)}/><a
|
||||
<span><img style={{verticalAlign: "top", height: 25}} src={getIcon(affectedBuilding.icon)}/><StyledLink
|
||||
href={`/mod/${props.modId}/race/${props.race.id}/building/${affectedBuilding.id}`}>
|
||||
{affectedBuilding.name}
|
||||
</a> </span>
|
||||
</StyledLink> </span>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
@@ -228,23 +238,21 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
{mods.map(m =>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">{getTarget(m.target)}</TableCell>
|
||||
<TableCell style={{minWidth: 120}} component="th"
|
||||
scope="row">{getModName(m.reference)}</TableCell>
|
||||
<TableCell component="th"
|
||||
scope="row">{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>}
|
||||
<i style={{fontSize: 11}}>{m.exclusive ? " does not stuck" : " does stuck"}</i>
|
||||
<i style={{fontSize: 11}}>{m.exclusive ? " does not stack" : " does stack"}</i>
|
||||
{m.probabilityOfApplying !== undefined && m.probabilityOfApplying !== 1.0 &&
|
||||
<span><i style={{fontSize: 11}}> ({m.probabilityOfApplying * 100}% chance)</i>
|
||||
<span><i style={{fontSize: 11, color: 'rgba(255,255,255,0.5)'}}> ({m.probabilityOfApplying * 100}% chance)</i>
|
||||
</span>}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -257,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'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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";
|
||||
import {IResearchRequirements} from "../types/IResearchRequirements";
|
||||
import {IRequirement, IRequirementLimitByBuildingDto, IRequirementSquadsDto} from "../types/IRequirement";
|
||||
import {IUnitShort} from "../types/IUnitShort";
|
||||
import {StyledLink} from "../commons/StyledLink";
|
||||
|
||||
interface IRequiredProps{
|
||||
requirement: IRequirement,
|
||||
@@ -13,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[],) {
|
||||
|
||||
@@ -20,9 +25,9 @@ function Required (props: IRequiredProps) {
|
||||
|
||||
<span> Global addon: <img style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(rga.icon)}/>
|
||||
<a href={`/mod/${props.modId}/race/${props.raceId}/building/${rga.buildingId}#addon-${rga.id}`}>
|
||||
<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${rga.buildingId}#addon-${rga.id}`}>
|
||||
{rga.name}
|
||||
</a></span>
|
||||
</StyledLink></span>
|
||||
)}</div>
|
||||
}
|
||||
|
||||
@@ -30,29 +35,29 @@ function Required (props: IRequiredProps) {
|
||||
return requirementBuildings.length > 0 && (
|
||||
<div> Buildings: <img style={{verticalAlign: "top", height: 25}}
|
||||
src={IconUrl + requirementBuildings[0].icon.replaceAll('\\', '/')}/>
|
||||
{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[0].id}`}>
|
||||
{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[0].id}`}>
|
||||
{requirementBuildings[0].name}
|
||||
</a> {requirementBuildings[1] !== undefined && <span>or <img style={{verticalAlign: "top", height: 25}}
|
||||
</StyledLink> {requirementBuildings[1] !== undefined && <span>or <img style={{verticalAlign: "top", height: 25}}
|
||||
src={IconUrl + requirementBuildings[1].icon.replaceAll('\\', '/')}/>
|
||||
{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[1].id}`}>
|
||||
{requirementBuildings[1].name}</a></span> } </div>
|
||||
{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[1].id}`}>
|
||||
{requirementBuildings[1].name}</StyledLink></span> } </div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderRequiremenStructureExclusive(buildingExclusive: IBuildingShort) {
|
||||
return <div> Can't be build with: <img style={{verticalAlign: "top", height: 25}}
|
||||
src={IconUrl + buildingExclusive.icon.replaceAll('\\', '/')}/>
|
||||
{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${buildingExclusive.id}`}>
|
||||
{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${buildingExclusive.id}`}>
|
||||
{buildingExclusive.name}
|
||||
</a> </div>
|
||||
</StyledLink> </div>
|
||||
}
|
||||
|
||||
function renderRequiremenAddonExclusive(addonShort: IBuildingAddonShort) {
|
||||
return <div> Can't be build with: <img style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(addonShort.icon)}/>
|
||||
<a href={`/mod/${props.modId}/race/${props.raceId}/building/${addonShort.buildingId}#addon-${addonShort.id}`}>
|
||||
<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${addonShort.buildingId}#addon-${addonShort.id}`}>
|
||||
{addonShort.name}
|
||||
</a></div>
|
||||
</StyledLink></div>
|
||||
}
|
||||
|
||||
function renderRequirementOwnership(requirementOwnership: string[],) {
|
||||
@@ -70,8 +75,8 @@ function Required (props: IRequiredProps) {
|
||||
function renderLimitPerBuilding(limitPerBuilding: IRequirementLimitByBuildingDto,) {
|
||||
return <span> One <img style={{verticalAlign: "top", height: 25}}
|
||||
src={IconUrl + limitPerBuilding.building.icon.replaceAll('\\', '/')}/>
|
||||
{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${limitPerBuilding.building.id}`}>
|
||||
{limitPerBuilding.building.name} </a> allow produce max {limitPerBuilding.limit}</span>
|
||||
{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${limitPerBuilding.building.id}`}>
|
||||
{limitPerBuilding.building.name} </StyledLink> allows producing a maximum of {limitPerBuilding.limit}</span>
|
||||
}
|
||||
|
||||
function renderRequirementResearches(requirementResearches: IResearchRequirements[],) {
|
||||
@@ -79,9 +84,9 @@ function Required (props: IRequiredProps) {
|
||||
|
||||
<span> Research {rs.researchMustNotBeComplete ? "not" : ""}: <img style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(rs.researchShortDto.icon)}/>
|
||||
<a href={`/mod/${props.modId}/race/${props.raceId}/building/${rs.researchShortDto.buildingId}#research-${rs.researchShortDto.id}`}>
|
||||
<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${rs.researchShortDto.buildingId}#research-${rs.researchShortDto.id}`}>
|
||||
{rs.researchShortDto.name}
|
||||
</a></span>
|
||||
</StyledLink></span>
|
||||
)}</div>
|
||||
}
|
||||
|
||||
@@ -90,9 +95,9 @@ function Required (props: IRequiredProps) {
|
||||
|
||||
<span> Squad: {rs.count} <img style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(rs.squad.icon)}/>
|
||||
<a href={`/mod/${props.modId}/race/${props.raceId}/unit/${rs.squad.id}`}>
|
||||
<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/unit/${rs.squad.id}`}>
|
||||
{rs.squad.name}
|
||||
</a></span>
|
||||
</StyledLink></span>
|
||||
)}</div>
|
||||
}
|
||||
|
||||
@@ -101,12 +106,12 @@ function Required (props: IRequiredProps) {
|
||||
return requirementResearches.length > 0 && (
|
||||
<div> Researches: <span> <img style={{verticalAlign: "top", height: 25}}
|
||||
src={getIcon(requirementResearches[0].researchShortDto.icon)}/>
|
||||
<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementResearches[0].researchShortDto.buildingId}#research-${requirementResearches[0].researchShortDto.id}`}>
|
||||
<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementResearches[0].researchShortDto.buildingId}#research-${requirementResearches[0].researchShortDto.id}`}>
|
||||
{requirementResearches[0].researchShortDto.name}
|
||||
</a></span> {requirementResearches[1] != null && <span> or <img style={{verticalAlign: "top", height: 25}} src={getIcon(requirementResearches[1].researchShortDto.icon)}/>
|
||||
<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementResearches[1].researchShortDto.buildingId}#research-${requirementResearches[1].researchShortDto.id}`}>
|
||||
</StyledLink></span> {requirementResearches[1] != null && <span> or <img style={{verticalAlign: "top", height: 25}} src={getIcon(requirementResearches[1].researchShortDto.icon)}/>
|
||||
<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementResearches[1].researchShortDto.buildingId}#research-${requirementResearches[1].researchShortDto.id}`}>
|
||||
{requirementResearches[1].researchShortDto.name}
|
||||
</a></span>} </div>
|
||||
</StyledLink></span>} </div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,48 +119,48 @@ function Required (props: IRequiredProps) {
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>Required: </h4>
|
||||
<div style={{color: textColor}}>
|
||||
<h4 style={{color: textColor, fontWeight: 700}}>Required: </h4>
|
||||
{requirement.requiredTotalPop !== undefined && requirement.requiredTotalPop !== null &&
|
||||
<div> Population: <img style={{verticalAlign: "top", height: 25}}
|
||||
<div style={{color: textColor}}> Population: <img style={{verticalAlign: "top", height: 25}}
|
||||
src="/images/Resource_orksquadcap.gif"/>
|
||||
{requirement.requiredTotalPop}</div>
|
||||
}
|
||||
{requirement.mobBonus !== null && requirement.mobBonus !== undefined &&
|
||||
<div> Mob requirement: <img style={{verticalAlign: "top", height: 25}}
|
||||
<div style={{color: textColor}}> Mob requirement: <img style={{verticalAlign: "top", height: 25}}
|
||||
src="/images/Mob_bonus.gif"/>
|
||||
{requirement.mobBonus.mobvalueRequired} in radius {requirement.mobBonus.proximityRequired}</div>
|
||||
}
|
||||
{requirement.requireAddon !== null &&
|
||||
<div> Addon: <img style={{verticalAlign: "top", height: 25}}
|
||||
<div style={{color: textColor}}> Addon: <img style={{verticalAlign: "top", height: 25}}
|
||||
src={IconUrl + requirement.requireAddon.icon.replaceAll('\\', '/')}/>
|
||||
<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirement.requireAddon.buildingId}#addon-${requirement.requireAddon.id}`}>
|
||||
<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirement.requireAddon.buildingId}#addon-${requirement.requireAddon.id}`}>
|
||||
{requirement.requireAddon.name}
|
||||
</a>
|
||||
</StyledLink>
|
||||
{requirement.replaceWhenDone && <i> (old provides replace)</i>}
|
||||
</div>
|
||||
}
|
||||
{requirement.requirementAddonExclusive != null &&
|
||||
renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)}
|
||||
<div style={{color: textColor}}>{renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)}</div>}
|
||||
{requirement.requirementBuildings.map(b =>
|
||||
<div key={b.id}> Building: <img style={{verticalAlign: "top", height: 25}}
|
||||
<div key={b.id} style={{color: textColor}}> Building: <img style={{verticalAlign: "top", height: 25}}
|
||||
src={IconUrl + b.icon.replaceAll('\\', '/')}/>
|
||||
<a href= {'/mod/'+ props.modId +'/race/'+ props.raceId +'/building/' + b.id + "/"}>{b.name}</a></div>)
|
||||
<StyledLink href= {'/mod/'+ props.modId +'/race/'+ props.raceId +'/building/' + b.id + "/"}>{b.name}</StyledLink></div>)
|
||||
}
|
||||
{requirement.requirementResearches.length !== 0 &&
|
||||
renderRequirementResearches(requirement.requirementResearches,)}
|
||||
<div style={{color: textColor}}>{renderRequirementResearches(requirement.requirementResearches,)}</div>}
|
||||
{requirement.requirementSquads.length !== 0 &&
|
||||
renderRequirementSquads(requirement.requirementSquads,)}
|
||||
<div style={{color: textColor}}>{renderRequirementSquads(requirement.requirementSquads,)}</div>}
|
||||
{requirement.requirementResearchesEither.length !== 0 &&
|
||||
renderRequirementResearchesEither(requirement.requirementResearchesEither,)}
|
||||
<div style={{color: textColor}}>{renderRequirementResearchesEither(requirement.requirementResearchesEither,)}</div>}
|
||||
{requirement.requirementBuildingsEither.length !== 0 &&
|
||||
renderRequirementBuildingEither(requirement.requirementBuildingsEither,)}
|
||||
<div style={{color: textColor}}>{renderRequirementBuildingEither(requirement.requirementBuildingsEither,)}</div>}
|
||||
{requirement.requirementStructureExclusive != null &&
|
||||
renderRequiremenStructureExclusive(requirement.requirementStructureExclusive,)}
|
||||
{requirement.limitByBuilding !== undefined && requirement.limitByBuilding !== null && renderLimitPerBuilding(requirement.limitByBuilding)}
|
||||
<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 &&
|
||||
renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)}
|
||||
{renderRequirementOwnership(requirement.requiredOwnership)}
|
||||
<div style={{color: textColor}}>{renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)}</div>}
|
||||
<div style={{color: textColor}}>{renderRequirementOwnership(requirement.requiredOwnership)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
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} 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";
|
||||
import Ability from "./Ability";
|
||||
import DeathExplosion from "./DeathExplosion";
|
||||
import {styled} from '@mui/material/styles';
|
||||
import {DescriptionBox} from "../commons/DescriptionBox";
|
||||
import {StyledPaper} from "../commons/StyledPaper";
|
||||
|
||||
const SectionTitle = styled(Typography)(({ theme }) => ({
|
||||
fontWeight: 700,
|
||||
mb: 2,
|
||||
fontSize: '1.25rem',
|
||||
}));
|
||||
|
||||
|
||||
export interface SergeantProps {
|
||||
@@ -21,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
|
||||
|
||||
@@ -38,17 +51,17 @@ const Sergeant = (props: SergeantProps) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{color: textColor}}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size= {{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody id="unit-stats-table">
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
{sergeant.buildCostRequisition > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
@@ -93,7 +106,7 @@ const Sergeant = (props: SergeantProps) => {
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Armor type</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
{sergeant.armorType2 == null ? <ArmorType name={sergeant.armorType.name} compact={false}/>
|
||||
: <Tooltip title={"upgrade/debuff can turns to " + sergeant.armorType2.name}><span><ArmorType name={sergeant.armorType.name} compact={false}/></span></Tooltip> }
|
||||
</TableCell>
|
||||
@@ -102,7 +115,7 @@ const Sergeant = (props: SergeantProps) => {
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Health</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
<span><img style={{verticalAlign: "top"}}
|
||||
src="/images/Health_icon.webp"/>
|
||||
{sergeant.health} {sergeant.healthRegeneration > 0 &&
|
||||
@@ -113,7 +126,7 @@ const Sergeant = (props: SergeantProps) => {
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Morale</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
{sergeant.moraleIncomeMax && <span><img style={{verticalAlign: "top"}}
|
||||
src="/images/ARM_Morale.webp"/> +{sergeant.moraleIncomeMax} </span>}
|
||||
{sergeant.moraleRate && <span>+ {sergeant.moraleRate}/s</span> }
|
||||
@@ -125,21 +138,19 @@ const Sergeant = (props: SergeantProps) => {
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Mass</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{sergeant.mass}
|
||||
</TableCell>
|
||||
<TableCell>{sergeant.mass}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Vision</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
<Vision sight={sergeant.sightRadius} detect={sergeant.detectRadius}/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{sergeant.mobValue != null &&
|
||||
<TableRow>
|
||||
<TableCell>Mob value</TableCell>
|
||||
<TableCell component="th" scope="row" >Mob value</TableCell>
|
||||
<TableCell>
|
||||
<span> <img style={{height: 20, verticalAlign: "top"}} src="/images/Mob_bonus.gif"/> {sergeant.mobValue} </span>
|
||||
</TableCell>
|
||||
@@ -150,20 +161,20 @@ const Sergeant = (props: SergeantProps) => {
|
||||
</TableContainer>
|
||||
</Grid2>
|
||||
<Grid2 size={8}>
|
||||
<div style={{whiteSpace: "pre-wrap"}}>
|
||||
<DescriptionBox>
|
||||
{sergeant.description}
|
||||
</div>
|
||||
</DescriptionBox>
|
||||
</Grid2>
|
||||
{sergeant.requirements !== null &&
|
||||
<Grid2 size={{xs: 12, md: 12}}>
|
||||
<Required requirement={sergeant.requirements} modId={props.mod.id} raceId={props.race.id}/>
|
||||
</Grid2>}
|
||||
{sergeant.modifiers.length > 0 && <Grid2 size={12}>
|
||||
<h3>Affected on</h3>
|
||||
<SectionTitle>Affected on</SectionTitle>
|
||||
<ModifiersProvidesTable modifiers={sergeant.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/>
|
||||
</Grid2>}
|
||||
{sergeant.abilities.length > 0 && <Grid2 size={12}>
|
||||
<h3>Abilities</h3>
|
||||
<SectionTitle>Abilities</SectionTitle>
|
||||
{sergeant.abilities.map(a =>
|
||||
<Ability mod={props.mod} ability={a} race={props.race}/>
|
||||
)}
|
||||
@@ -176,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>)
|
||||
};
|
||||
|
||||
|
||||
@@ -1,58 +1,66 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {AvailableBuildings, AvailableUnits} from "../core/api";
|
||||
import {Grid2, Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} 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";
|
||||
import {StyledPaper} from "../commons/StyledPaper";
|
||||
|
||||
const StyledTable = styled(Table)(({ theme }) => ({
|
||||
'& .MuiTableCell-head': {
|
||||
backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.05)',
|
||||
fontWeight: 700,
|
||||
fontSize: '1rem',
|
||||
borderBottom: `2px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.2)'}`,
|
||||
},
|
||||
'& .MuiTableCell-body': {
|
||||
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: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledHeaderLink = styled(StyledLink)(({ theme }) => ({
|
||||
fontWeight: 700,
|
||||
fontSize: '1.1rem',
|
||||
'&:hover': {
|
||||
color: theme.palette.mode === 'dark' ? '#dee2e6' : 'rgba(0, 0, 0, 0.85)',
|
||||
},
|
||||
}));
|
||||
|
||||
interface IUnitsTable {
|
||||
racesUnits: IRaceUnits[]
|
||||
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) {
|
||||
return <span style={{fontSize: 11}}><Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
|
||||
return <span style={{fontSize: 11}}>
|
||||
<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
|
||||
{unit.name}
|
||||
</Link><br/></span>
|
||||
</StyledLink><br/>
|
||||
</span>
|
||||
}
|
||||
|
||||
function getBuildingRef(modId: number, raceId: string, building: IBuildingShort) {
|
||||
return <span style={{fontSize: 11}}><Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}>
|
||||
return <span style={{fontSize: 11}}>
|
||||
<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}>
|
||||
{building.name}
|
||||
<br/></Link></span>
|
||||
</StyledLink><br/>
|
||||
</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':
|
||||
@@ -63,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;
|
||||
@@ -77,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;
|
||||
@@ -99,31 +107,31 @@ export default function UnitsTable(prop: {modId: number}) {
|
||||
}
|
||||
|
||||
function generateRaceUnitTable(racesUnitsPart: IRaceUnits[], racesBuildings: IRaceBuildings[]){
|
||||
return (<TableContainer>
|
||||
{<Table sx={{minWidth: 650}} size="small" aria-label="a dense table">
|
||||
return (<StyledPaper elevation={0} sx={{borderRadius: 0}}>
|
||||
{<StyledTable sx={{minWidth: 650}} size="small" aria-label="a dense table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{racesUnitsPart.map(raceUnits => <TableCell><h2><Link color="inherit"
|
||||
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</Link>
|
||||
{racesUnitsPart.map(raceUnits => <TableCell><h2><StyledHeaderLink
|
||||
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</StyledHeaderLink>
|
||||
</h2></TableCell>)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow style={{verticalAlign: 'top'}}>
|
||||
<TableRow>
|
||||
{racesUnitsPart.map(raceUnits =>
|
||||
<TableCell>
|
||||
{getUnitsRef(raceUnits.infantry.concat(raceUnits.support), raceUnits.race.id)}
|
||||
</TableCell>)
|
||||
}
|
||||
</TableRow>
|
||||
<TableRow style={{verticalAlign: 'top'}}>
|
||||
<TableRow>
|
||||
{racesUnitsPart.map(raceUnits =>
|
||||
<TableCell>
|
||||
{getUnitsRef(raceUnits.tech, raceUnits.race.id)}
|
||||
</TableCell>)
|
||||
}
|
||||
</TableRow>
|
||||
<TableRow style={{verticalAlign: 'top'}}>
|
||||
<TableRow>
|
||||
{racesBuildings.map(raceBuilding =>
|
||||
<TableCell>
|
||||
{raceBuilding.buildings.map(building => getBuildingRef(prop.modId, raceBuilding.race.id, building))}
|
||||
@@ -131,8 +139,8 @@ export default function UnitsTable(prop: {modId: number}) {
|
||||
}
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>}
|
||||
</TableContainer>)
|
||||
</StyledTable>}
|
||||
</StyledPaper>)
|
||||
}
|
||||
|
||||
function generateRaceUnitTables(racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[], raceCount: number){
|
||||
@@ -156,8 +164,8 @@ export default function UnitsTable(prop: {modId: number}) {
|
||||
function generateRaceList(unitsTable: IUnitsTable){
|
||||
return unitsTable.racesUnits.sort(function (a, b) {
|
||||
return a.race.name.localeCompare(b.race.name);
|
||||
}).map(raceUnits => <h5 key={raceUnits.race.id}><Link color="inherit"
|
||||
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</Link><br/>
|
||||
}).map(raceUnits => <h5 key={raceUnits.race.id}><StyledLink
|
||||
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</StyledLink><br/>
|
||||
</h5>)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +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 {StyledAccordion} from "../commons/StyledAccordion";
|
||||
|
||||
interface IWeaponProps {
|
||||
weapon: IShortWeapon,
|
||||
@@ -30,29 +32,35 @@ 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}}><Accordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
|
||||
<div style={{marginBottom: 10}}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore/>}
|
||||
expandIcon={<ExpandMore sx={{color: textColor}}/>}
|
||||
aria-controls="panel1-content"
|
||||
id="panel1-header"
|
||||
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
|
||||
>
|
||||
<span style={{fontSize: 18}}> {!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)} </span>
|
||||
)} {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>
|
||||
<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>
|
||||
</Accordion></div>
|
||||
</StyledAccordion></div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Weapon;
|
||||
export default withTheme(Weapon);
|
||||
|
||||
@@ -1,27 +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 {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;
|
||||
@@ -29,26 +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: "rgb(244,244,244)",
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
paddingLeft: 10
|
||||
},
|
||||
[`&.${tableCellClasses.body}`]: {
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
paddingRight: 18,
|
||||
paddingLeft: 10
|
||||
},
|
||||
const [enabledResearchIds, setEnabledResearchIds] = useState<Set<number>>(new Set());
|
||||
const [damageRoll, setDamageRoll] = useState<number | null>(null);
|
||||
|
||||
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(() => {
|
||||
|
||||
@@ -64,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
|
||||
}
|
||||
|
||||
@@ -80,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)
|
||||
@@ -107,110 +174,104 @@ 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>
|
||||
<div style={{color: textColor}}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={12}>
|
||||
<div style={{whiteSpace: "pre-wrap"}}>
|
||||
<span
|
||||
className="weaponDescription">{props.isDefault ? "Default weapon" : weapon.description}</span>
|
||||
</div>
|
||||
</Grid2>
|
||||
{!props.isDefault && <Grid2 size={12}>
|
||||
<span className="weaponDescription" style={{ whiteSpace: 'pre-wrap' }}>{weapon.description}</span>
|
||||
</Grid2>}
|
||||
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
{/*<TableHead><TableRow><TableCell component="th" scope="row"><b>Damage</b></TableCell><TableCell/></TableRow></TableHead>*/}
|
||||
<TableBody>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Base damage</TableCell>
|
||||
<TableCell component="th"
|
||||
scope="row">{weapon.minDamage} {weapon.maxDamage !== weapon.minDamage && "- " + weapon.maxDamage}
|
||||
</TableCell>
|
||||
<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 component="th"
|
||||
scope="row">{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 component="th"
|
||||
scope="row">{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>
|
||||
</TableContainer>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Reload time</TableCell>
|
||||
<TableCell component="th"
|
||||
scope="row">{weapon.reloadTime}
|
||||
</TableCell>
|
||||
<TableCell>{displayWeapon.reloadTime}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Range</TableCell>
|
||||
<TableCell component="th"
|
||||
scope="row">{weapon.maxRange ? (weapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}
|
||||
</TableCell>
|
||||
<TableCell>{displayWeapon.maxRange ? (displayWeapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Damage radius</TableCell>
|
||||
<TableCell component="th"
|
||||
scope="row">{weapon.damageRadius ? (weapon.damageRadius) : "-"}
|
||||
</TableCell>
|
||||
<TableCell>{weapon.damageRadius ? (weapon.damageRadius) : "-"}</TableCell>
|
||||
</TableRow>
|
||||
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody>
|
||||
<TableRow
|
||||
@@ -218,8 +279,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
>
|
||||
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||
{(weapon.costRequisition > 0 || weapon.costPower > 0) ?
|
||||
<TableCell component="th"
|
||||
scope="row">
|
||||
<TableCell>
|
||||
{weapon.costRequisition > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
@@ -228,24 +288,24 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_power.gif"/>
|
||||
{weapon.costPower.toFixed(0)}</span>}
|
||||
</TableCell> : <TableCell component="th" scope="row"> - </TableCell>
|
||||
{(weapon.costTimeSeconds !== undefined && weapon.costTimeSeconds > 0) &&
|
||||
<span> <AvTimerOutlinedIcon
|
||||
style={{verticalAlign: "top", fontSize: "18px"}}/>
|
||||
{weapon.costTimeSeconds}s</span>}
|
||||
</TableCell> : <TableCell> - </TableCell>
|
||||
}
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Setup time</TableCell>
|
||||
<TableCell component="th"
|
||||
scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(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}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Throw force</TableCell>
|
||||
<TableCell component="th"
|
||||
scope="row">{weapon.throwForceMin != 0 ? weapon.throwForceMin + " - " + weapon.throwForceMax : "-"}
|
||||
</TableCell>
|
||||
<TableCell>{weapon.throwForceMin != 0 ? weapon.throwForceMin + " - " + weapon.throwForceMax : "-"}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -253,6 +313,7 @@ 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
|
||||
@@ -260,11 +321,175 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
value={weaoponFull.currentTable}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<ToggleButton size="small" value="dps">Dps</ToggleButton>
|
||||
<ToggleButton size="small" value="one hit">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>
|
||||
@@ -290,7 +515,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
|
||||
@@ -325,13 +550,13 @@ 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}}>
|
||||
<div style={{width: 20, fontSize: 12, height: 50, color: textColor}}>
|
||||
<i>Morale</i></div>
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
|
||||
@@ -377,15 +602,13 @@ 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}}>
|
||||
<i>Morale</i></div>
|
||||
<StyledTableCell><ArmorType
|
||||
name='Morale damage'/>
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
|
||||
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
|
||||
@@ -406,12 +629,11 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
|
||||
</TableContainer>
|
||||
|
||||
</Grid2>
|
||||
{weapon.modifiers.length > 0 && <Grid2 size={12}>
|
||||
<h3>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 &&
|
||||
@@ -419,13 +641,14 @@ 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>loading...</div>;
|
||||
} else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@ import {IShortWeapon, IWeapon} from "../types/IUnit";
|
||||
import {IMod} from "../types/Imod";
|
||||
import {Irace} from "../types/Irace";
|
||||
import WeaponShort from "./WeaponFull";
|
||||
import {Typography} from "@mui/material";
|
||||
import {styled} from '@mui/material/styles';
|
||||
|
||||
const SectionTitle = styled(Typography)(({ theme }) => ({
|
||||
fontWeight: 700,
|
||||
mb: 2,
|
||||
fontSize: '1.25rem',
|
||||
}));
|
||||
|
||||
export interface WeaponSlotProps {
|
||||
hardpoint: number,
|
||||
@@ -33,10 +41,10 @@ const WeaponSlot= (props: WeaponSlotProps) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{header}</h3>
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from "react";
|
||||
import {useTheme} from "@mui/material/styles";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid2,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow
|
||||
TableRow,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import {IBuilding, IBuildingAddon} from "../../types/IBuilding";
|
||||
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
||||
@@ -17,6 +17,10 @@ import {ExpandMore} from "@mui/icons-material";
|
||||
import {IMod} from "../../types/Imod";
|
||||
import {getIcon, ModifiersProvidesTable,} from "../ModifiersProvideTable";
|
||||
import Required from "../Required";
|
||||
import {StyledAccordion} from "../../commons/StyledAccordion";
|
||||
import {StyledPaper} from "../../commons/StyledPaper";
|
||||
import {styled} from "@mui/material/styles";
|
||||
|
||||
|
||||
interface IBuildingAddonProps {
|
||||
addon: IBuildingAddon,
|
||||
@@ -25,33 +29,37 @@ 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
|
||||
|
||||
|
||||
|
||||
return <div className='addon-research-accordion' id={"addon-" + addon.id} ><Accordion>
|
||||
return <div className='addon-research-accordion' id={"addon-" + addon.id} ><StyledAccordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore/>}
|
||||
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
|
||||
expandIcon={<ExpandMore sx={{color: textColor}}/>}
|
||||
aria-controls="panel1-content"
|
||||
>
|
||||
<span style={{fontSize: 20}}>
|
||||
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
|
||||
<img className="sergeantIcon" src={getIcon(addon.icon)}/>
|
||||
{addon.name}
|
||||
</span>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<AccordionDetails sx={{color: textColor}}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody id="unit-stats-table">
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
{addon.addonCostRequisition > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
@@ -83,9 +91,7 @@ function BuildingAddon(props: IBuildingAddonProps){
|
||||
</TableContainer><br/>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs: 12, md: 8}}>
|
||||
<div style={{whiteSpace: "pre-wrap"}}>
|
||||
{addon.description}
|
||||
</div>
|
||||
<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 &&
|
||||
@@ -93,9 +99,9 @@ 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>
|
||||
</Accordion></div>
|
||||
</StyledAccordion></div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {IMod} from "../../types/Imod";
|
||||
import {IResearch} from "../../types/IResearch";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid2,
|
||||
@@ -14,13 +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 {StyledAccordion} from "../../commons/StyledAccordion";
|
||||
import {StyledLink} from "../../commons/StyledLink";
|
||||
|
||||
interface IResearchProps {
|
||||
research: IResearchShort,
|
||||
@@ -29,38 +28,46 @@ 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}><Accordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
|
||||
return <div className='addon-research-accordion' id={"research-" + research.id}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore/>}
|
||||
sx={{color: textColor, '& .MuiAccordionSummary-content': { margin: '12px 0' }, '& .MuiAccordionSummary-content.Mui-expanded': { margin: '12px 0' }}}
|
||||
expandIcon={<ExpandMore sx={{color: textColor}}/>}
|
||||
aria-controls="panel1-content"
|
||||
>
|
||||
<span style={{fontSize: 20}}>
|
||||
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
|
||||
<img className="sergeantIcon" src={getIcon(research.icon)}/>
|
||||
{research.name}
|
||||
</span>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<AccordionDetails sx={{color: textColor}}>
|
||||
<ResearchFull id={research.id} building={building}/>
|
||||
</AccordionDetails>
|
||||
</Accordion></div>
|
||||
</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}}
|
||||
src={getIcon(rs.icon)}/>
|
||||
<a href={`/mod/${modId}/race/${raceId}/building/${rs.buildingId}#research-${rs.id}`}>
|
||||
<StyledLink href={`/mod/${modId}/race/${raceId}/building/${rs.buildingId}#research-${rs.id}`}>
|
||||
{rs.name}
|
||||
</a></span>
|
||||
</StyledLink></span>
|
||||
}
|
||||
|
||||
return <div> {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>
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Grid2,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow
|
||||
TableRow,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import {ExpandMore} from "@mui/icons-material";
|
||||
import {getIcon, ModifiersProvidesTable} from "../ModifiersProvideTable";
|
||||
@@ -21,6 +21,8 @@ import {IBuilding} from "../../types/IBuilding";
|
||||
import {IResearchShort} from "../../types/IResearchShort";
|
||||
import {ResearchUrl, WeaponUrl} from "../../core/api";
|
||||
import {IWeapon} from "../../types/IUnit";
|
||||
import {styled} from '@mui/material/styles';
|
||||
import {StyledPaper} from "../../commons/StyledPaper";
|
||||
|
||||
|
||||
interface IResearchFull {
|
||||
@@ -54,14 +56,14 @@ function ResearchFull(props: { id: number, building: IBuilding }){
|
||||
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<TableContainer component={StyledPaper} elevation={0}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody id="unit-stats-table">
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<TableCell>
|
||||
{research.costRequisition > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
@@ -93,10 +95,7 @@ function ResearchFull(props: { id: number, building: IBuilding }){
|
||||
</TableContainer><br/>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs: 12, md: 8}}>
|
||||
<div style={{whiteSpace: "pre-wrap"}}>
|
||||
{research.description}
|
||||
</div>
|
||||
|
||||
<span style={{ whiteSpace: 'pre-wrap' }}>{research.description}</span>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs: 12, md: 12}}>
|
||||
<ModifiersProvidesTable modifiers={research.modifiers} affectedData={research.affectedData}
|
||||
@@ -107,9 +106,9 @@ 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>loading...</div>
|
||||
} else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>
|
||||
}
|
||||
|
||||
export default ResearchFull;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {Button, Link} from "@mui/material";
|
||||
import {styled} from '@mui/material/styles';
|
||||
|
||||
export const BackButton = styled(Button)(({ theme }) => {
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
return {
|
||||
marginBottom: theme.spacing(3),
|
||||
color: isDark ? '#FFD700' : '#1976d2',
|
||||
borderColor: isDark ? 'rgba(255, 215, 0, 0.5)' : 'rgba(25, 118, 210, 0.5)',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
'&:hover': {
|
||||
borderColor: isDark ? '#FFD700' : '#1976d2',
|
||||
backgroundColor: isDark ? 'rgba(255, 215, 0, 0.1)' : 'rgba(25, 118, 210, 0.1)',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import {styled} from '@mui/material/styles';
|
||||
|
||||
export const DescriptionBox = styled('div')(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
lineHeight: 1.7,
|
||||
padding: theme.spacing(3),
|
||||
background: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.03)',
|
||||
borderRadius: '12px',
|
||||
border: `1px solid ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}`,
|
||||
whiteSpace: 'pre-wrap',
|
||||
}));
|
||||
@@ -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' },
|
||||
}));
|
||||
@@ -0,0 +1,12 @@
|
||||
import {Link} from "@mui/material";
|
||||
import {styled} from '@mui/material/styles';
|
||||
|
||||
export const StyledLink = styled(Link)(({ theme }) => ({
|
||||
color: theme.palette.mode === 'dark' ? '#abc3ff' : '#1976d2',
|
||||
textDecoration: 'none',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
color: theme.palette.mode === 'dark' ? '#ffffff' : '#1565c0',
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}));
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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} />;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
'use client';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
export function withRouter(Children) {
|
||||
return (props) => {
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.jumpIcon{
|
||||
padding: 0px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.unitHeader{
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,77 @@
|
||||
@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: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
html[data-theme="light"] code {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--accent) var(--scrollbar-track);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track);
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--accent);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,269 +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,
|
||||
Grid2,
|
||||
Link,
|
||||
ListItem,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Tooltip
|
||||
} from "@mui/material";
|
||||
import {ArrowBack} from "@mui/icons-material";
|
||||
import ArmorType from "../classes/ArmorType";
|
||||
import AvTimerOutlinedIcon from '@mui/icons-material/AvTimer';
|
||||
import WeaponSlot from "../classes/WeaponSlot";
|
||||
import UnitsTable from "../classes/UnitsTable";
|
||||
import {IMod} from "../types/Imod";
|
||||
import {IBuilding} from "../types/IBuilding";
|
||||
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";
|
||||
|
||||
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}><ListItem>
|
||||
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/> }
|
||||
{unit.name}
|
||||
{unit.canDetect && <span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/DETECT_YES.webp"/></span>}
|
||||
</ListItem></Link></Grid2>)
|
||||
}
|
||||
|
||||
|
||||
function Building(building: IBuilding, mod: IMod) {
|
||||
|
||||
document.title = building.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 (
|
||||
<div>
|
||||
<h1>{mod.name} ({mod.version})</h1>
|
||||
<h2>{building.icon &&
|
||||
<img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>} {buildingName} </h2>
|
||||
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody id="unit-stats-table">
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Cost</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{building.buildCostRequisition > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
{building.buildCostRequisition.toFixed(0)}</span>}
|
||||
{building.buildCostPower > 0 && <span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_power.gif"/>
|
||||
{building.buildCostPower.toFixed(0)}</span>}
|
||||
{(building.buildCostPopulation !== undefined && building.buildCostPopulation > 0) &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_orksquadcap.gif"/>
|
||||
{building.buildCostPopulation.toFixed(0)}</span>}
|
||||
{(building.buildCostFaith !== undefined && building.buildCostFaith > 0) &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_faith.gif"/>
|
||||
{building.buildCostFaith}</span>}
|
||||
{(building.buildCostSouls !== undefined && building.buildCostSouls > 0) &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_souls.gif"/>
|
||||
{building.buildCostSouls.toFixed(0)}</span>}
|
||||
{(building.buildCostTime !== undefined && building.buildCostTime > 0) &&
|
||||
<span> <AvTimerOutlinedIcon
|
||||
style={{verticalAlign: "top", fontSize: "18px"}}/>
|
||||
{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> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
{building.requisitionIncome}</span>}
|
||||
{building.powerIncome !== undefined && building.powerIncome > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_power.gif"/>
|
||||
{building.powerIncome}</span>}
|
||||
{building.faithIncome !== undefined && building.faithIncome > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_faith.gif"/>
|
||||
{building.faithIncome}</span>}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Armor type</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{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
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Health</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<span><img style={{verticalAlign: "top"}}
|
||||
src="/images/Health_icon.webp"/>
|
||||
{building.health} {building.healthRegeneration > 0 &&
|
||||
<span>+{building.healthRegeneration}/s</span>} </span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Sight</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<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 component="th" scope="row">
|
||||
{building.repairMax}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid2>
|
||||
<Grid2 size={8}>
|
||||
<div style={{whiteSpace: "pre-wrap"}}>
|
||||
{building.description}
|
||||
</div>
|
||||
</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}} >
|
||||
<h3>Affected on</h3>
|
||||
<ModifiersProvidesTable modifiers={building.modifiers} race={building.race} modId={building.modId} affectedData={building.affectedData} />
|
||||
</Grid2>}
|
||||
{building.units.length > 0 && <Grid2 size={12}>
|
||||
<h3>Unit production</h3>
|
||||
<Grid2 container spacing={2}>
|
||||
{building.units.map(unit =>
|
||||
Unit(unit, mod.id, building.race.id)
|
||||
)}
|
||||
</Grid2>
|
||||
</Grid2>}
|
||||
{building.abilities.length > 0 && <Grid2 size={12}>
|
||||
<h3>Abilities</h3>
|
||||
{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}>
|
||||
<h3>Addons</h3>
|
||||
{building.addons.map(b =>
|
||||
<BuildingAddon mod={mod} addon={b} building={building}/>
|
||||
)}
|
||||
</Grid2> }
|
||||
{building.researches.length > 0 && <Grid2 size={12}>
|
||||
<h3>Researches</h3>
|
||||
{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>
|
||||
<b className="hotkey">Hotkey: {building.hotkey}</b>
|
||||
<hr/>
|
||||
<UnitsTable modId={mod.id}/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
class BuildingPage extends React.Component<any, UintPageState> {
|
||||
|
||||
async componentDidMount() {
|
||||
const buildingResponse = await fetch(AvailableBuildings + "/" + this.props.match.params.buildingId);
|
||||
const buildingData: IBuilding = await buildingResponse.json();
|
||||
|
||||
this.setState({
|
||||
building: buildingData
|
||||
});
|
||||
|
||||
const responseMod = await fetch(AvailableMods + "/" + this.props.match.params.modId);
|
||||
const modData: IMod = await responseMod.json();
|
||||
|
||||
this.setState({
|
||||
mod: modData
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
if (this.state != null && this.state.building != null && this.state.mod != null) {
|
||||
const backRef = "/mod/" + this.props.match.params.modId + "/race/" + this.props.match.params.raceId
|
||||
|
||||
return <div><a href={backRef}><Button id="back-button" variant="contained"
|
||||
startIcon={<ArrowBack/>}> Back</Button></a>
|
||||
{Building(this.state.building, this.state.mod)}
|
||||
</div>;
|
||||
} else {
|
||||
return "loading...";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(BuildingPage);
|
||||
@@ -1,54 +0,0 @@
|
||||
import {AvailableMods, AvailableRacesPart, AvailableUnits, IconUrl} from "../core/api";
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {NavLink, useLocation, useParams} from "react-router-dom";
|
||||
import {withRouter} from "../core/withrouter";
|
||||
import {IMod} from "../types/Imod";
|
||||
import {Irace} from "../types/Irace";
|
||||
import {Button, ListItem, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material";
|
||||
import {ArrowBack} from "@mui/icons-material";
|
||||
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
|
||||
import UnitsTable from "../classes/UnitsTable";
|
||||
|
||||
|
||||
interface ModPageState {
|
||||
mod: IMod,
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ModPage extends React.Component<any, ModPageState> {
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
fetch(AvailableMods + "/" + this.props.match.params.modId)
|
||||
.then(res => res.json())
|
||||
.then((res: IMod) => {
|
||||
this.setState({
|
||||
mod : res
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
if(this.state != null && this.state.mod != null ){
|
||||
|
||||
document.title = this.state.mod.name
|
||||
|
||||
return <div>
|
||||
<a href="/"><Button id="back-button" variant="contained"
|
||||
startIcon={<ArrowBack/>}> Back</Button></a>
|
||||
<h1>{this.state.mod.name} ({this.state.mod.version})</h1>
|
||||
<UnitsTable modId={this.state.mod.id}/>
|
||||
</div>;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default withRouter(ModPage);
|
||||
@@ -1,72 +0,0 @@
|
||||
import {AvailableMods} from "../core/api";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {NavLink} from "react-router-dom";
|
||||
import {IMod} from "../types/Imod";
|
||||
|
||||
function Mods (mods: IMod[]) {
|
||||
|
||||
let mapWithModVersions: Map<String, IMod[]> = new Map();
|
||||
|
||||
mods.forEach( mod => {
|
||||
const versionList = mapWithModVersions.get(mod.name)
|
||||
if(versionList == null){
|
||||
mapWithModVersions.set(mod.name, new Array(mod))
|
||||
} else {
|
||||
versionList.push(mod)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
function Mod (modName: String) {
|
||||
let sameMods = mapWithModVersions.get(modName) ?? []
|
||||
|
||||
let lastBeta = sameMods.filter((m) => m.isBeta).reverse()[0]
|
||||
|
||||
return(
|
||||
<div>
|
||||
<h1>{modName}</h1>
|
||||
{modName == "Unification new races" && <ul><a href="https://dawn-of-war-unification-mod.fandom.com/">Official unification wiki</a></ul>}
|
||||
{sameMods.filter((m) => !m.isBeta).map(mod =>
|
||||
<ul><NavLink state={mod.id} to= {"/mod/" + mod.id} >{mod.version}</NavLink></ul>)}
|
||||
{sameMods.find((m) => m.isBeta) != null && <div><i>Beta versions:</i></div>}
|
||||
{lastBeta != null &&
|
||||
<ul><NavLink state={lastBeta.id} to={"/mod/" + lastBeta.id}>{lastBeta.version}</NavLink></ul>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return(
|
||||
[...mapWithModVersions.keys()].map(m => Mod(m))
|
||||
)
|
||||
}
|
||||
|
||||
interface ModsPageState {
|
||||
mods: IMod[]
|
||||
}
|
||||
|
||||
class ModsPage extends React.Component<any, ModsPageState> {
|
||||
|
||||
constructor({props}: { props: any }) {
|
||||
super(props);
|
||||
this.state = {
|
||||
mods: []
|
||||
}
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const response = await fetch(AvailableMods);
|
||||
const data: IMod[] = await response.json();
|
||||
console.log(data);
|
||||
this.setState({
|
||||
mods : data
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>{Mods(this.state.mods)}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default ModsPage
|
||||
@@ -1,228 +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,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Grid2, Link,
|
||||
List,
|
||||
ListItem,
|
||||
ListSubheader,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
ToggleButton,
|
||||
ToggleButtonClassKey,
|
||||
ToggleButtonGroup,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import {ArrowBack, ArrowDropDown, ExpandMore} from "@mui/icons-material";
|
||||
import ArmorType from "../classes/ArmorType";
|
||||
import Weapon from "../classes/Weapon";
|
||||
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
|
||||
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
|
||||
|
||||
interface RacePageState {
|
||||
mod: IMod,
|
||||
race: Irace,
|
||||
units: IUnitShort[],
|
||||
}
|
||||
|
||||
|
||||
function Unit(unit: IUnitShort, modId: number, raceId: String) {
|
||||
|
||||
return (<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}><ListItem>
|
||||
{unit.icon && <img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
|
||||
{unit.name}
|
||||
{unit.canDetect && <span> <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 (<Link href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
|
||||
{unit.icon && <img className="unitIconSmall" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>}
|
||||
<span style={{fontSize: 12}}>{unitName}</span>
|
||||
{unit.canDetect && <span> <img
|
||||
src="/images/DETECT_YES.webp"/></span>}<br/></Link>)
|
||||
}
|
||||
|
||||
function Building(building: IBuildingShort, modId: number, raceId: String) {
|
||||
|
||||
return (<Grid2 size={{xs: 12, md: 3}}><Link href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}><ListItem>
|
||||
{building.icon && <img className="unitIcon" src={IconUrl + building.icon.replaceAll('\\', '/')}/>}
|
||||
{building.name}
|
||||
{building.canDetect && <span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/DETECT_YES.webp"/></span>}
|
||||
|
||||
</ListItem></Link>
|
||||
{building.units.map(unit => UnitSmall(unit, modId, raceId))}
|
||||
</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
|
||||
}
|
||||
|
||||
return (this.state.buildings != null ?
|
||||
<div>
|
||||
<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 && <div><hr/><Grid2 container spacing={2}>
|
||||
{this.state.buildings.buildingsAdvanced.map(building => Building(building, this.props.modId, this.props.raceId))}
|
||||
</Grid2><br/></div>}
|
||||
<Grid2>
|
||||
<Grid2 size={{xs: 12, md: 12}}>
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore/>}
|
||||
aria-controls="units-accordion"
|
||||
id="units-accordion"
|
||||
>
|
||||
<h3>All units</h3>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<h3>Infantry</h3>
|
||||
{this.state.units.infantry.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
|
||||
</Grid2>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<h3>Tech</h3>
|
||||
<List sx={{
|
||||
width: '100%',
|
||||
maxWidth: 360,
|
||||
bgcolor: 'background.paper'
|
||||
}}>
|
||||
{this.state.units.tech.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
|
||||
</List>
|
||||
</Grid2>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<h3>Support</h3>
|
||||
<List sx={{
|
||||
width: '100%',
|
||||
maxWidth: 360,
|
||||
bgcolor: 'background.paper'
|
||||
}}>
|
||||
{this.state.units.support.map(unit => Unit(unit, this.props.modId, this.props.raceId))}
|
||||
</List>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid2><br/>
|
||||
</Grid2>
|
||||
</div> : "Loading"
|
||||
)
|
||||
} else {
|
||||
return "Loading...";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RacePageFast extends React.Component<any, RacePageState> {
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
console.log(this.props.match.params.id);
|
||||
}
|
||||
|
||||
|
||||
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 <div><Link href={backRef}><Button id="back-button" variant="contained"
|
||||
startIcon={<ArrowBack/>}> Back</Button></Link>
|
||||
<h1>{this.state.mod.name} ({this.state.mod.version}) - {this.state.race.name}</h1>
|
||||
<Units raceId={this.state.race.id} modId={this.state.mod.id}/>
|
||||
</div>;
|
||||
} else {
|
||||
return "loading...";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(RacePageFast);
|
||||
@@ -1,384 +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,
|
||||
Button,
|
||||
Grid2,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
Tooltip
|
||||
} 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";
|
||||
|
||||
interface UintPageState {
|
||||
unit: IUnit,
|
||||
mod: IMod,
|
||||
}
|
||||
|
||||
|
||||
function Unit(unit: IUnit, mod: IMod) {
|
||||
|
||||
document.title = unit.name
|
||||
|
||||
const morale = (unit.moraleMax !== null) ? <span>
|
||||
<img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/> {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}}>
|
||||
{props.icon && <img className="sergeantIcon" src={IconUrl + props.icon.replaceAll('\\', '/')}/> }
|
||||
{props.name}
|
||||
{props.canDetect && <span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/DETECT_YES.webp"/></span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{mod.name} ({mod.version})</h1>
|
||||
<h2>{unit.icon &&
|
||||
<img className="unitIcon" src={IconUrl + unit.icon.replaceAll('\\', '/')}/>} {unit.name} </h2>
|
||||
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{xs: 12, md: 4}}>
|
||||
<TableContainer component={Paper}>
|
||||
<Table size="small" aria-label="a dense table">
|
||||
<TableBody id="unit-stats-table">
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Cost</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{unit.buildCostRequisition > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
{unit.buildCostRequisition.toFixed(0)}</span>}
|
||||
{unit.buildCostPower > 0 && <span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_power.gif"/>
|
||||
{unit.buildCostPower.toFixed(0)}</span>}
|
||||
{(unit.buildCostPopulation !== undefined && unit.buildCostPopulation > 0) &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_orksquadcap.gif"/>
|
||||
{unit.buildCostPopulation.toFixed(0)}</span>}
|
||||
{(unit.buildCostFaith !== undefined && unit.buildCostFaith > 0) &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_faith.gif"/>
|
||||
{unit.buildCostFaith}</span>}
|
||||
{(unit.buildCostSouls !== undefined && unit.buildCostSouls > 0) &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_souls.gif"/>
|
||||
{unit.buildCostSouls.toFixed(0)}</span>}
|
||||
|
||||
{unit.capInfantry > 0 && <span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_cap_infantry.gif"/>
|
||||
{unit.capInfantry}</span>}
|
||||
{unit.capSupport > 0 && <span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_cap_vehicle.gif"/>
|
||||
{unit.capSupport}</span>}
|
||||
{(unit.buildCostTime !== undefined && unit.buildCostTime > 0) &&
|
||||
<span> <AvTimerOutlinedIcon
|
||||
style={{verticalAlign: "top", fontSize: "18px"}}/>
|
||||
{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> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
{unit.reinforceCostRequisition.toFixed(0)}</span>:<span/>}
|
||||
{unit.reinforceCostPower !== undefined && unit.reinforceCostPower > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_power.gif"/>
|
||||
{unit.reinforceCostPower.toFixed(0)}</span>}
|
||||
{(unit.reinforceCostPopulation !== undefined && unit.reinforceCostPopulation > 0) &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_orksquadcap.gif"/>
|
||||
{unit.reinforceCostPopulation.toFixed(0)}</span>}
|
||||
{(unit.reinforceCostFaith !== undefined && unit.reinforceCostFaith > 0) &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_faith.gif"/>
|
||||
{unit.reinforceCostFaith}</span>}
|
||||
{(unit.reinforceCostSouls !== undefined && unit.reinforceCostSouls > 0) &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_souls.gif"/>
|
||||
{unit.reinforceCostSouls.toFixed(0)}</span>}
|
||||
{(unit.reinforceTime !== undefined && unit.reinforceTime > 0) &&
|
||||
<span> <AvTimerOutlinedIcon
|
||||
style={{verticalAlign: "top", fontSize: "18px"}}/>
|
||||
{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> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_requisition.gif"/>
|
||||
{unit.requisitionIncome}</span>}
|
||||
{unit.powerIncome !== undefined && unit.powerIncome > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_power.gif"/>
|
||||
{unit.powerIncome}</span>}
|
||||
{unit.faithIncome !== undefined && unit.faithIncome > 0 &&
|
||||
<span> <img style={{verticalAlign: "top"}}
|
||||
src="/images/Resource_faith.gif"/>
|
||||
{unit.faithIncome}</span>}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{unit.squadMaxSize > 1 &&
|
||||
<TableRow>
|
||||
<TableCell>Squad size</TableCell>
|
||||
<TableCell>
|
||||
{unit.squadStartSize} / {unit.squadMaxSize}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Armor type</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{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
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Health</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<img style={{verticalAlign: "top"}}
|
||||
src="/images/Health_icon.webp"/>
|
||||
{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
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Move speed</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{unit.moveSpeed}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Morale</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{morale}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Mass</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
{unit.mass}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
>
|
||||
<TableCell component="th" scope="row">Vision</TableCell>
|
||||
<TableCell component="th" scope="row">
|
||||
<Vision sight={unit.sightRadius} detect={unit.detectRadius}/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{unit.repairMax !== undefined && unit.repairMax !== null &&
|
||||
<TableRow>
|
||||
<TableCell>Repair max</TableCell>
|
||||
<TableCell>
|
||||
{unit.repairMax}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{unit.repairSpeed !== undefined && unit.repairSpeed !== null && unit.repairCostPercent !== null &&
|
||||
<TableRow>
|
||||
<TableCell>Repair</TableCell>
|
||||
<TableCell>
|
||||
{unit.repairSpeed} hp/s; {unit.repairCostPercent}% cost
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
{unit.mobValue != null &&
|
||||
<TableRow>
|
||||
<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>
|
||||
<TableCell>Limit</TableCell>
|
||||
<TableCell>
|
||||
{unit.squadLimit}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid2>
|
||||
<Grid2 size={8}>
|
||||
<div style={{whiteSpace: "pre-wrap"}}>
|
||||
{unit.description}
|
||||
</div>
|
||||
</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}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore/>}
|
||||
aria-controls="panel1-content"
|
||||
id="panel1-header"
|
||||
>
|
||||
<SergeantShort name={s.name} icon={s.icon} canDetect={s.detectRadius > 0}/>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Sergeant mod={mod} sergeant={s} race={unit.race}/>
|
||||
<hr/>
|
||||
</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}>
|
||||
<h3><img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/> Morale broken unusual behavior <img style={{verticalAlign: "top"}} src="/images/ARM_Morale.webp"/></h3>
|
||||
<ModifiersProvidesTable modifiers={unit.moraleBrakeModifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/>
|
||||
</Grid2>}
|
||||
|
||||
{unit.modifiers.length > 0 && <Grid2 size={12}>
|
||||
<h3>Affected on</h3>
|
||||
<ModifiersProvidesTable modifiers={unit.modifiers} modId={unit.modId} race={unit.race} affectedData={unit.affectedData}/>
|
||||
</Grid2>}
|
||||
|
||||
{unit.abilities.length > 0 && <Grid2 size={12}>
|
||||
<h3>Abilities</h3>
|
||||
{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>
|
||||
<b className="hotkey">Hotkey: {unit.hotkey}</b>
|
||||
<hr/>
|
||||
<UnitsTable modId={mod.id}/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
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 <div key={this.state.unit.id}><a href={backRef}><Button id="back-button" variant="contained"
|
||||
startIcon={<ArrowBack/>}> Back</Button></a>
|
||||
{Unit(this.state.unit, this.state.mod)}
|
||||
</div>;
|
||||
} else {
|
||||
return <div key = {'loading'}>loading...</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(UnitPage);
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -32,6 +32,7 @@ export interface IBuilding {
|
||||
sightRadius: number
|
||||
detectRadius: number
|
||||
repairMax: number
|
||||
buildingLimit?: number
|
||||
icon: string
|
||||
modId: number
|
||||
modifiers: IModifier[]
|
||||
|
||||
@@ -2,5 +2,6 @@ export interface IResearchShort {
|
||||
id: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
uiIndexHint: number;
|
||||
buildingId: number;
|
||||
}
|
||||
@@ -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,7 +1,9 @@
|
||||
export interface IMod {
|
||||
id: number;
|
||||
name: string;
|
||||
order: number;
|
||||
version: string;
|
||||
technicalName: string;
|
||||
isBeta: boolean;
|
||||
isHide: boolean;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||