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
|
#NEXT_PUBLIC_HOST_URL=http://localhost:8082
|
||||||
REACT_APP_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
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/.next
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
|
||||||
@@ -10,7 +11,9 @@
|
|||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
/deploy
|
||||||
|
dow-wiki-deploy.tar.gz
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#### Сборка
|
||||||
|
|
||||||
|
При сборке дистрибутива формируй архив dow-wiki-deploy.tar.gz
|
||||||
|
Предварительно удали всё что было до этого в папке deploy и собирай туда
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Soulstorm wiki frontend
|
# Soulstorm wiki frontend
|
||||||
|
|
||||||
## Стек технологий:
|
## Tech stack:
|
||||||
|
|
||||||
- react (вперемешку классы и функции)
|
- react (mix of classes and functions)
|
||||||
- typescript
|
- 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>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<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>
|
<title>Webpack App</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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",
|
"name": "dow-wiki-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@mui/icons-material": "^6.3.1",
|
"@mui/icons-material": "^6.3.1",
|
||||||
"@mui/material": "^6.3.1",
|
"@mui/material": "^6.3.1",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"next": "^14.2.23",
|
||||||
"@testing-library/react": "^13.4.0",
|
|
||||||
"@testing-library/user-event": "^13.5.0",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0"
|
||||||
"react-router-dom": "^6.28.1",
|
|
||||||
"react-scripts": "5.0.1",
|
|
||||||
"web-vitals": "^2.1.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"devDependencies": {
|
||||||
"start": "react-scripts start",
|
"@types/node": "^20.11.0",
|
||||||
"build": "react-scripts build",
|
"@types/react": "^18.2.0",
|
||||||
"test": "react-scripts test",
|
"@types/react-dom": "^18.2.0",
|
||||||
"eject": "react-scripts eject"
|
"typescript": "^5.3.0"
|
||||||
},
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
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",
|
"short_name": "DoW Wiki",
|
||||||
"name": "Create React App Sample",
|
"name": "Dawn of War Wiki",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"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 {ExpandMore} from "@mui/icons-material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
import {IAbilityShort} from "../types/IAbility";
|
import {IAbilityShort} from "../types/IAbility";
|
||||||
import {getIcon} from "./ModifiersProvideTable";
|
import {getIcon} from "./ModifiersProvideTable";
|
||||||
import {IMod} from "../types/Imod";
|
import {IMod} from "../types/Imod";
|
||||||
import AbilityFull from "./AbilityFull";
|
import AbilityFull from "./AbilityFull";
|
||||||
import {Irace} from "../types/Irace";
|
import {Irace} from "../types/Irace";
|
||||||
|
import {StyledAccordion} from "../commons/StyledAccordion";
|
||||||
|
|
||||||
interface IAbilityProps {
|
interface IAbilityProps {
|
||||||
ability: IAbilityShort,
|
ability: IAbilityShort,
|
||||||
@@ -15,25 +16,29 @@ interface IAbilityProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Ability(props: 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
|
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
|
<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"
|
aria-controls="panel1-content"
|
||||||
>
|
>
|
||||||
<span style={{fontSize: 20}}>
|
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
|
||||||
{ability.activationType === "Passive ability" ? <img className="abilityIcon" src="/images/PassiveIcon.gif"/> : <img className="abilityIcon" src={getIcon(ability.icon)}/>}
|
{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', '')}
|
{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>
|
</span>
|
||||||
|
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails sx={{color: textColor}}>
|
||||||
<AbilityFull abilityId={ability.id} mod={props.mod} race={props.race} />
|
<AbilityFull abilityId={ability.id} mod={props.mod} race={props.race} />
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion></div>
|
</StyledAccordion></div>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
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 {AbilityUrl, AvailableBuildings, AvailableUnits, UserUrl, WeaponUrl} from "../core/api";
|
||||||
import {IWeapon} from "../types/IUnit";
|
import {IWeapon} from "../types/IUnit";
|
||||||
import ArmorTypeNames from "../types/ArmorTypeValues";
|
import ArmorTypeNames from "../types/ArmorTypeValues";
|
||||||
import {
|
import {
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
Grid2,
|
Grid2,
|
||||||
Paper,
|
|
||||||
styled,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
tableCellClasses,
|
|
||||||
TableContainer, TableHead,
|
TableContainer, TableHead,
|
||||||
TableRow, ToggleButton, ToggleButtonGroup
|
TableRow, ToggleButton, ToggleButtonGroup
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ArmorType from "./ArmorType";
|
import ArmorType from "./ArmorType";
|
||||||
import Required from "./Required";
|
import Required from "./Required";
|
||||||
import {renderAffectedResearches} from "./building/Research";
|
|
||||||
import {IMod} from "../types/Imod";
|
import {IMod} from "../types/Imod";
|
||||||
import {Irace} from "../types/Irace";
|
import {Irace} from "../types/Irace";
|
||||||
import {IRaceUnits} from "../types/IUnitShort";
|
import {IRaceUnits} from "../types/IUnitShort";
|
||||||
@@ -27,33 +25,27 @@ import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
|||||||
import ability from "./Ability";
|
import ability from "./Ability";
|
||||||
import AbilitySpawnedObject from "./AbilitySpawnedObject";
|
import AbilitySpawnedObject from "./AbilitySpawnedObject";
|
||||||
import DpsTable from "./DpsTable";
|
import DpsTable from "./DpsTable";
|
||||||
|
import {StyledPaper} from "../commons/StyledPaper";
|
||||||
|
import {StyledTableCell} from "../commons/StyledTableCell";
|
||||||
|
|
||||||
interface IAbilityFullState {
|
interface IAbilityFullState {
|
||||||
ability?: IAbilityFill;
|
ability?: IAbilityFill;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AbilityFull(props: { abilityId?: number, mod: IMod, race: Irace, isChild?: boolean, abilityFull?: 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>({
|
const [abilityFull, setAbilityFull] = useState<IAbilityFullState>({
|
||||||
ability: undefined,
|
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(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
if(props.abilityFull !== undefined){
|
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)
|
const showTargetTable = !showDpsTable && ability.abilityEnvironment == null && (ability.targetFilter.length > 0 || ability.radius !== 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{color: textColor}}>
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
<Grid2 size={{xs: 12}}>
|
<Grid2 size={{xs: 12}}>
|
||||||
{ability.initialDelayTime !== undefined && props.isChild === true &&
|
{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"}}
|
<img style={{verticalAlign: "top"}}
|
||||||
src="/images/Time_icon.webp"/></center></b>}
|
src="/images/Time_icon.webp"/></center></b>}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
<Grid2 size={{xs: 12, md: 4}}>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
<TableBody id="unit-stats-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" >Affected on</TableCell>
|
||||||
<TableCell component="th" scope="row">{ability.areaFilter}</TableCell>
|
<TableCell>{ability.areaFilter}</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{props.isChild !== true && (ability.costPower !== undefined && ability.costPower > 0 ||
|
{props.isChild !== true && (ability.costPower !== undefined && ability.costPower > 0 ||
|
||||||
ability.costRequisition !== undefined && ability.costRequisition > 0 ||
|
ability.costRequisition !== undefined && ability.costRequisition > 0 ||
|
||||||
@@ -113,8 +105,8 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Cost</TableCell>
|
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
{ability.costRequisition !== undefined && ability.costRequisition > 0 &&
|
{ability.costRequisition !== undefined && ability.costRequisition > 0 &&
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
<span> <img style={{verticalAlign: "top"}}
|
||||||
src="/images/Resource_requisition.gif"/>
|
src="/images/Resource_requisition.gif"/>
|
||||||
@@ -135,19 +127,19 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
|
|
||||||
{props.isChild !== true && ability.rechargeTime !== undefined && ability.rechargeTime > 0 && ability.activation !== "On/off ability" &&
|
{props.isChild !== true && ability.rechargeTime !== undefined && ability.rechargeTime > 0 && ability.activation !== "On/off ability" &&
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<TableCell component="th" scope="row">Recharge time</TableCell>
|
<TableCell component="th" scope="row" >Recharge time</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
<img style={{verticalAlign: "top"}}
|
<img style={{verticalAlign: "top"}}
|
||||||
src="/images/Time_icon.webp"/>
|
src="/images/Time_icon.webp"/>
|
||||||
{ability.rechargeTime}s
|
{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>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
}
|
}
|
||||||
{props.isChild !== true && ability.initialDelayTime !== undefined && ability.initialDelayTime > 0 &&
|
{props.isChild !== true && ability.initialDelayTime !== undefined && ability.initialDelayTime > 0 &&
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 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" >Delay after cast</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
<img style={{verticalAlign: "top"}}
|
<img style={{verticalAlign: "top"}}
|
||||||
src="/images/Time_icon.webp"/>
|
src="/images/Time_icon.webp"/>
|
||||||
{ability.initialDelayTime.toFixed(1)}s
|
{ability.initialDelayTime.toFixed(1)}s
|
||||||
@@ -156,8 +148,8 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
}
|
}
|
||||||
{ability.durationTime !== undefined && ability.durationTime > 1.5 &&
|
{ability.durationTime !== undefined && ability.durationTime > 1.5 &&
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<TableCell component="th" scope="row">Duration time</TableCell>
|
<TableCell component="th" scope="row" >Duration time</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
<img style={{verticalAlign: "top"}}
|
<img style={{verticalAlign: "top"}}
|
||||||
src="/images/Time_icon.webp"/>
|
src="/images/Time_icon.webp"/>
|
||||||
{ability.durationTime.toFixed(1)}s
|
{ability.durationTime.toFixed(1)}s
|
||||||
@@ -166,16 +158,16 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
}
|
}
|
||||||
{ability.areaEffect !== null && ability.radius !== undefined && ability.radius > 0 &&
|
{ability.areaEffect !== null && ability.radius !== undefined && ability.radius > 0 &&
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<TableCell component="th" scope="row">Radius</TableCell>
|
<TableCell component="th" scope="row" >Radius</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
{ability.radius.toFixed(0)} <i style={{fontSize: 10}}>{ability.areaEffect}</i>
|
{ability.radius.toFixed(0)} <i style={{fontSize: 10, color: 'rgba(255,255,255,0.5)'}}>{ability.areaEffect}</i>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
}
|
}
|
||||||
{props.isChild !== true && ability.range !== undefined && ability.areaFilter !== "Own" && ability.range > 0 &&
|
{props.isChild !== true && ability.range !== undefined && ability.areaFilter !== "Own" && ability.range > 0 &&
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<TableCell component="th" scope="row">Range</TableCell>
|
<TableCell component="th" scope="row" >Range</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
{ability.range.toFixed(0)}
|
{ability.range.toFixed(0)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -184,33 +176,90 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Throw force</TableCell>
|
<TableCell component="th" scope="row" >Throw force</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell>{ability.throwForceMin != 0 ? ability.throwForceMin + " - " + ability.throwForceMax : "-"}
|
||||||
scope="row">{ability.throwForceMin != 0 ? ability.throwForceMin + " - " + ability.throwForceMax : "-"}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>}
|
</TableRow>}
|
||||||
<TableRow>
|
|
||||||
|
|
||||||
</TableRow>
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer><br/>
|
</TableContainer><br/>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={8}>
|
<Grid2 size={8}>
|
||||||
<div style={{whiteSpace: "pre-wrap"}}>
|
{props.isChild !== true && <span style={{ whiteSpace: 'pre-wrap' }}>{ability.description}</span>}
|
||||||
{props.isChild !== true && ability.description}
|
|
||||||
</div>
|
|
||||||
</Grid2>
|
</Grid2>
|
||||||
{showDpsTable ?
|
{showDpsTable ?
|
||||||
<Grid2 size={12}>
|
<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}
|
<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> : showTargetTable ?
|
||||||
<Grid2 size={12}>
|
<Grid2 size={12}>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
{ props.mod !== undefined && props.mod.technicalName === 'UltimateApocalypse' ?
|
{isMobile ? (() => {
|
||||||
<Table sx={{ textAlign: "center"}} size="small" aria-label="a dense table">
|
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>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<StyledTableCell><ArmorType
|
<StyledTableCell><ArmorType
|
||||||
@@ -235,7 +284,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell>
|
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell>
|
||||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell>
|
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell>
|
||||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell>
|
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell>
|
||||||
@@ -272,7 +321,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleLow)}</StyledTableCell>
|
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleLow)}</StyledTableCell>
|
||||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleMedium)}</StyledTableCell>
|
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleMedium)}</StyledTableCell>
|
||||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleHigh)}</StyledTableCell>
|
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.VehicleHigh)}</StyledTableCell>
|
||||||
@@ -286,7 +335,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table> :
|
</Table> :
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table sx={{ minWidth: 800 }} size="small" aria-label="a dense table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<StyledTableCell><ArmorType
|
<StyledTableCell><ArmorType
|
||||||
@@ -321,7 +370,7 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell>
|
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryLow)}</StyledTableCell>
|
||||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell>
|
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryMedium)}</StyledTableCell>
|
||||||
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell>
|
<StyledTableCell>{isAbilityApplyTo(ArmorTypeNames.InfantryHigh)}</StyledTableCell>
|
||||||
@@ -342,7 +391,8 @@ export default function AbilityFull(props: { abilityId?: number, mod: IMod, race
|
|||||||
</Table>
|
</Table>
|
||||||
}
|
}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Grid2> : <span></span>}
|
</Grid2>
|
||||||
|
: <span></span>}
|
||||||
<ModifiersProvidesTable modifiers={ability.modifiers} affectedData={ability.affectedData} modId={props.mod.id} race={props.race}/>
|
<ModifiersProvidesTable modifiers={ability.modifiers} affectedData={ability.affectedData} modId={props.mod.id} race={props.race}/>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
{(ability.abilityEnvironment != null) && <AbilitySpawnedObject abilityEnvironment={ability.abilityEnvironment} race={props.race} mod={props.mod}/>}
|
{(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}}>
|
<Grid2 size={{xs: 12, md: 12}}>
|
||||||
<Required requirement={ability.requirements} modId={props.mod.id} raceId={props.race.id}/>
|
<Required requirement={ability.requirements} modId={props.mod.id} raceId={props.race.id}/>
|
||||||
</Grid2>}
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
} else return <div>loading...</div>;
|
} else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionSummary, Grid2, Paper,
|
AccordionSummary, Grid2,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell, TableContainer,
|
TableContainer,
|
||||||
TableRow,
|
TableRow,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {ExpandMore} from "@mui/icons-material";
|
import {ExpandMore} from "@mui/icons-material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
import {IAbilityShort} from "../types/IAbility";
|
import {IAbilityShort} from "../types/IAbility";
|
||||||
import {getIcon} from "./ModifiersProvideTable";
|
import {getIcon} from "./ModifiersProvideTable";
|
||||||
import {IMod} from "../types/Imod";
|
import {IMod} from "../types/Imod";
|
||||||
@@ -21,6 +23,16 @@ import ArmorType from "./ArmorType";
|
|||||||
import Vision from "./Vision";
|
import Vision from "./Vision";
|
||||||
import Ability from "./Ability";
|
import Ability from "./Ability";
|
||||||
import DeathExplosion from "./DeathExplosion";
|
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 {
|
interface AbilityEnvironmentProps {
|
||||||
@@ -30,39 +42,43 @@ interface AbilityEnvironmentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AbilitySpawnedObject(props: 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
|
const abilityEnvironment = props.abilityEnvironment
|
||||||
|
|
||||||
return <div>
|
return <div style={{color: textColor}}>
|
||||||
<h3> {props.abilityEnvironment.name ?? props.abilityEnvironment.filename.charAt(0).toUpperCase() + props.abilityEnvironment.filename.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
|
<SectionTitle>
|
||||||
<i style={{fontSize: 11}}> (Spawned object)</i>
|
{props.abilityEnvironment.name ?? props.abilityEnvironment.filename.charAt(0).toUpperCase() + props.abilityEnvironment.filename.slice(1).replaceAll('_', ' ').replace('.rgd', '')}
|
||||||
</h3>
|
<i style={{fontSize: 11, color: 'rgba(255,255,255,0.5)'}}> (Spawned object)</i>
|
||||||
|
</SectionTitle>
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
<Grid2 size={{xs: 12, md: 4}}>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
<TableBody id="unit-stats-table">
|
<TableBody id="unit-stats-table">
|
||||||
{abilityEnvironment.armorType !== null &&
|
{abilityEnvironment.armorType !== null &&
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Armor type</TableCell>
|
<StyledTableCell component="th" scope="row" >Armor type</StyledTableCell>
|
||||||
<TableCell component="th" scope="row">
|
<StyledTableCell>
|
||||||
{abilityEnvironment.armorType2 == null ?
|
{abilityEnvironment.armorType2 == null ?
|
||||||
<ArmorType name={abilityEnvironment.armorType.name} compact={false}/> :
|
<ArmorType name={abilityEnvironment.armorType.name} compact={false}/> :
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={"upgrade/debuff can turns to " + abilityEnvironment.armorType2.name}><span><ArmorType
|
title={"upgrade/debuff can turns to " + abilityEnvironment.armorType2.name}><span><ArmorType
|
||||||
name={abilityEnvironment.armorType.name}
|
name={abilityEnvironment.armorType.name}
|
||||||
compact={false}/></span></Tooltip>}
|
compact={false}/></span></Tooltip>}
|
||||||
</TableCell>
|
</StyledTableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
}
|
}
|
||||||
{abilityEnvironment.health !== null &&
|
{abilityEnvironment.health !== null &&
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Health</TableCell>
|
<StyledTableCell component="th" scope="row" >Health</StyledTableCell>
|
||||||
<TableCell component="th" scope="row">
|
<StyledTableCell>
|
||||||
<img style={{verticalAlign: "top"}}
|
<img style={{verticalAlign: "top"}}
|
||||||
src="/images/Health_icon.webp"/>
|
src="/images/Health_icon.webp"/>
|
||||||
{abilityEnvironment.health} {abilityEnvironment.healthRegeneration != null && abilityEnvironment.healthRegeneration > 0 &&
|
{abilityEnvironment.health} {abilityEnvironment.healthRegeneration != null && abilityEnvironment.healthRegeneration > 0 &&
|
||||||
@@ -70,38 +86,36 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
|
|||||||
{abilityEnvironment.armour !== undefined && abilityEnvironment.armour !== 0 &&
|
{abilityEnvironment.armour !== undefined && abilityEnvironment.armour !== 0 &&
|
||||||
<span><img style={{height: 20, verticalAlign: "top"}}
|
<span><img style={{height: 20, verticalAlign: "top"}}
|
||||||
src="/images/defence.png"/>{abilityEnvironment.armour} </span>}
|
src="/images/defence.png"/>{abilityEnvironment.armour} </span>}
|
||||||
</TableCell>
|
</StyledTableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
}
|
}
|
||||||
{abilityEnvironment.moveSpeed != null &&
|
{abilityEnvironment.moveSpeed != null &&
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Move speed</TableCell>
|
<StyledTableCell component="th" scope="row" >Move speed</StyledTableCell>
|
||||||
<TableCell component="th" scope="row">
|
<StyledTableCell>{abilityEnvironment.moveSpeed}</StyledTableCell>
|
||||||
{abilityEnvironment.moveSpeed}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
}
|
}
|
||||||
{abilityEnvironment.sightRadius != null &&
|
{abilityEnvironment.sightRadius != null &&
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Vision</TableCell>
|
<StyledTableCell component="th" scope="row" >Vision</StyledTableCell>
|
||||||
<TableCell component="th" scope="row">
|
<StyledTableCell>
|
||||||
<Vision sight={abilityEnvironment.sightRadius ?? 0}
|
<Vision sight={abilityEnvironment.sightRadius ?? 0}
|
||||||
detect={abilityEnvironment.detectRadius ?? 0}/>
|
detect={abilityEnvironment.detectRadius ?? 0}/>
|
||||||
</TableCell>
|
</StyledTableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
}
|
}
|
||||||
{abilityEnvironment.lifetime != null &&
|
{abilityEnvironment.lifetime != null &&
|
||||||
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<TableCell component="th" scope="row">Lifetime</TableCell>
|
<StyledTableCell component="th" scope="row" >Lifetime</StyledTableCell>
|
||||||
<TableCell component="th" scope="row">
|
<StyledTableCell>
|
||||||
<img style={{verticalAlign: "top"}}
|
<img style={{verticalAlign: "top"}}
|
||||||
src="/images/Time_icon.webp"/>
|
src="/images/Time_icon.webp"/>
|
||||||
{abilityEnvironment.lifetime.toFixed(0)}s
|
{abilityEnvironment.lifetime.toFixed(0)}s
|
||||||
</TableCell>
|
</StyledTableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
}
|
}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
@@ -109,9 +123,9 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={{xs: 12, md: 8}}>
|
<Grid2 size={{xs: 12, md: 8}}>
|
||||||
<div style={{whiteSpace: "pre-wrap"}}>
|
<DescriptionBox>
|
||||||
{abilityEnvironment.description}
|
{abilityEnvironment.description}
|
||||||
</div>
|
</DescriptionBox>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
{abilityEnvironment.abilities.length > 0 && <Grid2 size={12}>
|
{abilityEnvironment.abilities.length > 0 && <Grid2 size={12}>
|
||||||
{abilityEnvironment.abilities.map(a =>
|
{abilityEnvironment.abilities.map(a =>
|
||||||
@@ -120,9 +134,7 @@ function AbilitySpawnedObject(props: AbilityEnvironmentProps) {
|
|||||||
</Grid2>}
|
</Grid2>}
|
||||||
{abilityEnvironment.deathExplosions.length > 0 &&
|
{abilityEnvironment.deathExplosions.length > 0 &&
|
||||||
<Grid2 size={12}>
|
<Grid2 size={12}>
|
||||||
{abilityEnvironment.deathExplosions.map(da =>
|
<DeathExplosion deathExplosions={abilityEnvironment.deathExplosions} mod={props.mod}/>
|
||||||
<DeathExplosion deathExplosion={da} mod={props.mod}/>
|
|
||||||
)}
|
|
||||||
</Grid2>
|
</Grid2>
|
||||||
}
|
}
|
||||||
</Grid2>
|
</Grid2>
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class ArmorType extends React.Component<IArmorType, any> {
|
|||||||
return '/images/ARM_Dmn_Hi.webp';
|
return '/images/ARM_Dmn_Hi.webp';
|
||||||
case 'Titan':
|
case 'Titan':
|
||||||
return '/images/ARM_titan.webp';
|
return '/images/ARM_titan.webp';
|
||||||
|
case 'Morale damage':
|
||||||
|
return '/images/ARM_Morale.webp';
|
||||||
default:
|
default:
|
||||||
return armorTypeId;
|
return armorTypeId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionSummary, Grid2, Paper,
|
AccordionSummary, Grid2,
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell, TableContainer, TableHead,
|
TableContainer, TableHead,
|
||||||
TableRow,
|
TableRow,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {ExpandMore} from "@mui/icons-material";
|
import {ExpandMore} from "@mui/icons-material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
import {IAbilityShort} from "../types/IAbility";
|
import {IAbilityShort} from "../types/IAbility";
|
||||||
import {getIcon, ModifiersProvidesTable} from "./ModifiersProvideTable";
|
import {getIcon, ModifiersProvidesTable} from "./ModifiersProvideTable";
|
||||||
import {IMod} from "../types/Imod";
|
import {IMod} from "../types/Imod";
|
||||||
@@ -24,71 +24,75 @@ import {IDeathExplosion} from "../types/IDeathExplosion";
|
|||||||
import Required from "./Required";
|
import Required from "./Required";
|
||||||
import ArmorTypeNames from "../types/ArmorTypeValues";
|
import ArmorTypeNames from "../types/ArmorTypeValues";
|
||||||
import DpsTable from "./DpsTable";
|
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 {
|
interface DeathExpProps {
|
||||||
deathExplosion: IDeathExplosion,
|
deathExplosions: IDeathExplosion[],
|
||||||
mod: IMod,
|
mod: IMod,
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeathExplosion(props: DeathExpProps) {
|
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 deathExplosions = props.deathExplosions.filter(deathExplosion =>
|
||||||
const chanceTotalNotZero = deathExplosion.globalChance * deathExplosion.chance != 0
|
deathExplosion.globalChance * deathExplosion.chance != 0
|
||||||
|
)
|
||||||
|
|
||||||
return chanceTotalNotZero ?
|
return deathExplosions.length > 0 ?
|
||||||
<div className='addon-research-accordion' id='death-explosion'><Accordion>
|
<div className='addon-research-accordion' id='death-explosion'><StyledAccordion>
|
||||||
<AccordionSummary
|
<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"
|
aria-controls="panel1-content"
|
||||||
>
|
>
|
||||||
<span style={{fontSize: 20}}>
|
<span style={{fontSize: 20, color: textColor, display: 'flex', alignItems: 'center'}}>
|
||||||
Death Explosion
|
Death Explosion
|
||||||
</span>
|
</span>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails sx={{color: textColor}}>
|
||||||
<Grid2 container spacing={2}>
|
{deathExplosions.map((deathExplosion, index) =>
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
<Grid2 container spacing={2} key={deathExplosion.id ?? index}
|
||||||
<TableContainer component={Paper}>
|
sx={{mt: index > 0 ? 2 : 0}}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Grid2 size={{xs: 12, md: 4}}>
|
||||||
<TableBody id="unit-stats-table">
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<TableRow
|
<Table size="small" aria-label="a dense table">
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
<TableBody id="unit-stats-table">
|
||||||
>
|
<TableRow
|
||||||
<TableCell component="th" scope="row">Chance</TableCell>
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
<TableCell component="th" scope="row">
|
>
|
||||||
{deathExplosion.globalChance * deathExplosion.chance}%
|
<StyledTableCell component="th" scope="row" >Chance</StyledTableCell>
|
||||||
</TableCell>
|
<StyledTableCell>{deathExplosion.globalChance * deathExplosion.chance}%</StyledTableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Damage radius</TableCell>
|
<StyledTableCell component="th" scope="row" >Damage radius</StyledTableCell>
|
||||||
<TableCell component="th" scope="row">
|
<StyledTableCell>{deathExplosion.damageRadius}</StyledTableCell>
|
||||||
{deathExplosion.damageRadius}
|
</TableRow>
|
||||||
</TableCell>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
</TableRow>
|
<StyledTableCell component="th" scope="row" >Throw force</StyledTableCell>
|
||||||
<TableRow
|
<StyledTableCell>{deathExplosion.throwForceMin} - {deathExplosion.throwForceMax}</StyledTableCell>
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
</TableRow>
|
||||||
>
|
</TableBody>
|
||||||
<TableCell component="th" scope="row">Throw force</TableCell>
|
</Table>
|
||||||
<TableCell component="th" scope="row">
|
</TableContainer>
|
||||||
{deathExplosion.throwForceMin} - {deathExplosion.throwForceMax}
|
</Grid2>
|
||||||
</TableCell>
|
<Grid2>
|
||||||
</TableRow>
|
<DpsTable mod={props.mod} piercings={deathExplosion.armorPiercing} targetFilter={[]}
|
||||||
</TableBody>
|
minDamageValue={deathExplosion.minDamageValue}
|
||||||
</Table>
|
minDamage={deathExplosion.minDamage} maxDamage={deathExplosion.maxDamage}
|
||||||
</TableContainer>
|
moraleDamage={deathExplosion.moraleDamage}/>
|
||||||
|
</Grid2>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2>
|
)}
|
||||||
<DpsTable mod={props.mod} piercings={deathExplosion.armorPiercing} targetFilter={[]}
|
|
||||||
minDamageValue={deathExplosion.minDamageValue}
|
|
||||||
minDamage={deathExplosion.minDamage} maxDamage={deathExplosion.maxDamage}
|
|
||||||
moraleDamage={deathExplosion.moraleDamage}/>
|
|
||||||
</Grid2>
|
|
||||||
</Grid2>
|
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion></div> : <span></span>
|
</StyledAccordion></div> : <span></span>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
import ArmorTypeNames from "../types/ArmorTypeValues";
|
import ArmorTypeNames from "../types/ArmorTypeValues";
|
||||||
import {
|
import {
|
||||||
styled,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
|
||||||
tableCellClasses,
|
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableRow
|
TableRow
|
||||||
@@ -14,23 +13,14 @@ import ArmorType from "./ArmorType";
|
|||||||
import {IMod} from "../types/Imod";
|
import {IMod} from "../types/Imod";
|
||||||
import {IPiercing} from "../types/IPiercing";
|
import {IPiercing} from "../types/IPiercing";
|
||||||
import {IArmorType} from "../types/IArmorType";
|
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 }) {
|
export default function DpsTable(props: { mod: IMod, minDamageValue?: number, minDamage?: number, maxDamage?: number, piercings: IPiercing[], targetFilter: IArmorType[], moraleDamage?: number }) {
|
||||||
|
const theme = useTheme();
|
||||||
const StyledTableCell = styled(TableCell)(({theme}) => ({
|
const isSmallMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
[`&.${tableCellClasses.head}`]: {
|
const isMediumMobile = useMediaQuery(theme.breakpoints.between('sm', 'md'));
|
||||||
backgroundColor: "rgb(244,244,244)",
|
const isMobile = isSmallMobile || isMediumMobile;
|
||||||
marginRight: 'auto',
|
const columnsPerRow = isSmallMobile ? 4 : 6;
|
||||||
marginLeft: 'auto',
|
|
||||||
paddingLeft: 10
|
|
||||||
},
|
|
||||||
[`&.${tableCellClasses.body}`]: {
|
|
||||||
fontSize: 12,
|
|
||||||
textAlign: 'center',
|
|
||||||
paddingRight: 18,
|
|
||||||
paddingLeft: 10
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
function getPiercingK(armorType: string): number|null {
|
function getPiercingK(armorType: string): number|null {
|
||||||
if(props.targetFilter.length > 0 && props.targetFilter.find(tf => tf.name == armorType) === undefined) return 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(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 (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
{ props.mod !== undefined && props.mod.name.includes("Ultimate Apocalypse") ?
|
{ 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>
|
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
|
||||||
<StyledTableCell><ArmorType
|
<StyledTableCell><ArmorType
|
||||||
name={ArmorTypeNames.BuildingSuper}/></StyledTableCell>
|
name={ArmorTypeNames.BuildingSuper}/></StyledTableCell>
|
||||||
<StyledTableCell><img style={{verticalAlign: "top"}}
|
<StyledTableCell><ArmorType
|
||||||
src="/images/ARM_Morale.webp"/>
|
name='Morale damage'/>
|
||||||
<div style={{width: 20, fontSize: 12, height: 50}}>
|
|
||||||
<i>Morale</i></div>
|
|
||||||
</StyledTableCell>
|
</StyledTableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
|
||||||
@@ -172,7 +226,7 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table> :
|
</Table> :
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table sx={{ minWidth: 800 }} size="small" aria-label="a dense table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<StyledTableCell><ArmorType
|
<StyledTableCell><ArmorType
|
||||||
@@ -204,15 +258,14 @@ export default function DpsTable(props: { mod: IMod, minDamageValue?: number, mi
|
|||||||
name={ArmorTypeNames.BuildingMedium}/></StyledTableCell>
|
name={ArmorTypeNames.BuildingMedium}/></StyledTableCell>
|
||||||
<StyledTableCell><ArmorType
|
<StyledTableCell><ArmorType
|
||||||
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
|
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
|
||||||
<StyledTableCell><img style={{verticalAlign: "top"}}
|
<StyledTableCell>
|
||||||
src="/images/ARM_Morale.webp"/>
|
<ArmorType
|
||||||
<div style={{width: 20, fontSize: 12, height: 50}}>
|
name='Morale damage'/>
|
||||||
<i>Morale</i></div>
|
|
||||||
</StyledTableCell>
|
</StyledTableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(infHighPiercing)}</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 {Paper, Table, TableCell, TableContainer, TableRow} from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
import {IModifier} from "../types/IModifier";
|
import {IModifier} from "../types/IModifier";
|
||||||
import {IconUrl} from "../core/api";
|
import {IconUrl} from "../core/api";
|
||||||
import {Irace} from "../types/Irace";
|
import {Irace} from "../types/Irace";
|
||||||
import {IAffectedData} from "../types/IAffectedData";
|
import {IAffectedData} from "../types/IAffectedData";
|
||||||
import {IMod} from "../types/Imod";
|
import {IMod} from "../types/Imod";
|
||||||
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
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 {
|
interface IModifiersProvidesTable {
|
||||||
modifiers: IModifier[],
|
modifiers: IModifier[],
|
||||||
@@ -138,19 +143,21 @@ function getChangeDescription(ref: String, v: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ModifiersProvidesAddonTable(props: IModifiersProvidesTable) {
|
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"))
|
let noWeaponMods = props.modifiers.filter(m => !m.reference.includes("default_weapon_modifier_hardpoint"))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
{noWeaponMods.map(m =>
|
{noWeaponMods.map(m =>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">{getModName(m.reference)}</TableCell>
|
<TableCell component="th" scope="row" sx={{color: textColor}}>{getModName(m.reference)}</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell component="th" scope="row" sx={{color: textColor}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}</TableCell>
|
||||||
scope="row">{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
@@ -159,6 +166,9 @@ export function ModifiersProvidesAddonTable(props: IModifiersProvidesTable) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
|
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
|
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);
|
var affectedUnit = props.affectedData.affectedUnits.find(au => au.filename.replaceAll('.rgd', '').split(";")[0] === target || au.filename.replaceAll('.rgd', '').split(";")[1] === target);
|
||||||
if (affectedUnit != null) {
|
if (affectedUnit != null) {
|
||||||
return <span><img style={{verticalAlign: "top", height: 25}}
|
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}`}>
|
href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedUnit.id}`}>
|
||||||
{affectedUnit.name}
|
{affectedUnit.name}
|
||||||
</a></span>
|
</StyledLink></span>
|
||||||
}
|
}
|
||||||
var affectedSergeant = props.affectedData.affectedSergeants.find(as => as.filename.replaceAll('.rgd', '') === target);
|
var affectedSergeant = props.affectedData.affectedSergeants.find(as => as.filename.replaceAll('.rgd', '') === target);
|
||||||
if (affectedSergeant != null) {
|
if (affectedSergeant != null) {
|
||||||
return <span><img style={{verticalAlign: "top", height: 25}}
|
return <span><img style={{verticalAlign: "top", height: 25}}
|
||||||
src={getIcon(affectedSergeant.icon)}/>{affectedSergeant.name} at <img
|
src={getIcon(affectedSergeant.icon)}/>{affectedSergeant.name} at <img
|
||||||
style={{verticalAlign: "top", height: 25}}
|
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}`}>
|
href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedSergeant.unit.id}`}>
|
||||||
{affectedSergeant.unit.name}
|
{affectedSergeant.unit.name}
|
||||||
</a></span>
|
</StyledLink></span>
|
||||||
}
|
}
|
||||||
var affectedBuilding = props.affectedData.affectedBuildings.find(ab => ab.filename.replaceAll('.rgd', '') === target);
|
var affectedBuilding = props.affectedData.affectedBuildings.find(ab => ab.filename.replaceAll('.rgd', '') === target);
|
||||||
if (affectedBuilding != null) {
|
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}`}>
|
href={`/mod/${props.modId}/race/${props.race.id}/building/${affectedBuilding.id}`}>
|
||||||
{affectedBuilding.name}
|
{affectedBuilding.name}
|
||||||
</a> </span>
|
</StyledLink> </span>
|
||||||
}
|
}
|
||||||
|
|
||||||
var affectedWeapon = props.affectedData.affectedWeapons.find(aw => aw.filename.replaceAll('.rgd', '') === target);
|
var affectedWeapon = props.affectedData.affectedWeapons.find(aw => aw.filename.replaceAll('.rgd', '') === target);
|
||||||
if (affectedWeapon != null) {
|
if (affectedWeapon != null) {
|
||||||
return <span>{affectedWeapon.name ? affectedWeapon.name : affectedWeapon.filename.replaceAll('_', ' ').replace('.rgd', '')} at {
|
return <span>{affectedWeapon.name ? affectedWeapon.name : affectedWeapon.filename.replaceAll('_', ' ').replace('.rgd', '')} at {
|
||||||
affectedWeapon.units.map(unit =>
|
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}`}>
|
href={`/mod/${props.modId}/race/${props.race.id}/unit/${unit.id}`}>
|
||||||
{unit.name}
|
{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}}
|
<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
|
src={getIcon(affectedSergeant.icon)}/>{affectedSergeant.name} at <img
|
||||||
style={{verticalAlign: "top", height: 25}}
|
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}`}>
|
href={`/mod/${props.modId}/race/${props.race.id}/unit/${affectedSergeant.unit.id}`}>
|
||||||
{affectedSergeant.unit.name}
|
{affectedSergeant.unit.name}
|
||||||
</a> </span>
|
</StyledLink> </span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
affectedWeapon.buildings.map(affectedBuilding =>
|
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}`}>
|
href={`/mod/${props.modId}/race/${props.race.id}/building/${affectedBuilding.id}`}>
|
||||||
{affectedBuilding.name}
|
{affectedBuilding.name}
|
||||||
</a> </span>
|
</StyledLink> </span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
@@ -228,23 +238,21 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
{mods.map(m =>
|
{mods.map(m =>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">{getTarget(m.target)}</TableCell>
|
<TableCell component="th" scope="row" sx={{color: textColor}}>{getTarget(m.target)}</TableCell>
|
||||||
<TableCell style={{minWidth: 120}} component="th"
|
<TableCell component="th" scope="row" sx={{color: textColor, minWidth: 120}}>{getModName(m.reference)}</TableCell>
|
||||||
scope="row">{getModName(m.reference)}</TableCell>
|
<TableCell component="th" scope="row" sx={{color: textColor}}>{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}
|
||||||
<TableCell component="th"
|
|
||||||
scope="row">{getModIcon(m.reference)} {getChangeDescription(m.usageType, m.value)}
|
|
||||||
{m.maxLifeTime !== undefined && m.maxLifeTime > 0 &&
|
{m.maxLifeTime !== undefined && m.maxLifeTime > 0 &&
|
||||||
<span><img style={{verticalAlign: "top"}} src='/images/Time_icon.webp'/>{m.maxLifeTime}s
|
<span><img style={{verticalAlign: "top"}} src='/images/Time_icon.webp'/>{m.maxLifeTime}s
|
||||||
</span>}
|
</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 &&
|
{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>}
|
</span>}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -257,7 +265,7 @@ export function ModifiersProvidesTable(props: IModifiersProvidesResearchTable) {
|
|||||||
export function getIcon(icon?: string): string {
|
export function getIcon(icon?: string): string {
|
||||||
if (icon !== null && icon !== undefined) {
|
if (icon !== null && icon !== undefined) {
|
||||||
return IconUrl + icon.replaceAll('\\', '/')
|
return IconUrl + icon.replaceAll('\\', '/')
|
||||||
} else return '/images/NoImg.gif'
|
} else return '/images/NoImg.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
import {IBuildingShort} from "../types/IBuildingShort";
|
import {IBuildingShort} from "../types/IBuildingShort";
|
||||||
import {IBuildingAddonShort} from "../types/IBuilding";
|
import {IBuildingAddonShort} from "../types/IBuilding";
|
||||||
import {IconUrl} from "../core/api";
|
import {IconUrl} from "../core/api";
|
||||||
import {IResearchRequirements} from "../types/IResearchRequirements";
|
import {IResearchRequirements} from "../types/IResearchRequirements";
|
||||||
import {IRequirement, IRequirementLimitByBuildingDto, IRequirementSquadsDto} from "../types/IRequirement";
|
import {IRequirement, IRequirementLimitByBuildingDto, IRequirementSquadsDto} from "../types/IRequirement";
|
||||||
import {IUnitShort} from "../types/IUnitShort";
|
import {IUnitShort} from "../types/IUnitShort";
|
||||||
|
import {StyledLink} from "../commons/StyledLink";
|
||||||
|
|
||||||
interface IRequiredProps{
|
interface IRequiredProps{
|
||||||
requirement: IRequirement,
|
requirement: IRequirement,
|
||||||
@@ -13,6 +15,9 @@ interface IRequiredProps{
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Required (props: 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[],) {
|
function renderRequirementGlobalAddons(rgas: IBuildingAddonShort[],) {
|
||||||
|
|
||||||
@@ -20,9 +25,9 @@ function Required (props: IRequiredProps) {
|
|||||||
|
|
||||||
<span> Global addon: <img style={{verticalAlign: "top", height: 25}}
|
<span> Global addon: <img style={{verticalAlign: "top", height: 25}}
|
||||||
src={getIcon(rga.icon)}/>
|
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}
|
{rga.name}
|
||||||
</a></span>
|
</StyledLink></span>
|
||||||
)}</div>
|
)}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,29 +35,29 @@ function Required (props: IRequiredProps) {
|
|||||||
return requirementBuildings.length > 0 && (
|
return requirementBuildings.length > 0 && (
|
||||||
<div> Buildings: <img style={{verticalAlign: "top", height: 25}}
|
<div> Buildings: <img style={{verticalAlign: "top", height: 25}}
|
||||||
src={IconUrl + requirementBuildings[0].icon.replaceAll('\\', '/')}/>
|
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}
|
{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('\\', '/')}/>
|
src={IconUrl + requirementBuildings[1].icon.replaceAll('\\', '/')}/>
|
||||||
{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[1].id}`}>
|
{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementBuildings[1].id}`}>
|
||||||
{requirementBuildings[1].name}</a></span> } </div>
|
{requirementBuildings[1].name}</StyledLink></span> } </div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRequiremenStructureExclusive(buildingExclusive: IBuildingShort) {
|
function renderRequiremenStructureExclusive(buildingExclusive: IBuildingShort) {
|
||||||
return <div> Can't be build with: <img style={{verticalAlign: "top", height: 25}}
|
return <div> Can't be build with: <img style={{verticalAlign: "top", height: 25}}
|
||||||
src={IconUrl + buildingExclusive.icon.replaceAll('\\', '/')}/>
|
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}
|
{buildingExclusive.name}
|
||||||
</a> </div>
|
</StyledLink> </div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRequiremenAddonExclusive(addonShort: IBuildingAddonShort) {
|
function renderRequiremenAddonExclusive(addonShort: IBuildingAddonShort) {
|
||||||
return <div> Can't be build with: <img style={{verticalAlign: "top", height: 25}}
|
return <div> Can't be build with: <img style={{verticalAlign: "top", height: 25}}
|
||||||
src={getIcon(addonShort.icon)}/>
|
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}
|
{addonShort.name}
|
||||||
</a></div>
|
</StyledLink></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRequirementOwnership(requirementOwnership: string[],) {
|
function renderRequirementOwnership(requirementOwnership: string[],) {
|
||||||
@@ -70,8 +75,8 @@ function Required (props: IRequiredProps) {
|
|||||||
function renderLimitPerBuilding(limitPerBuilding: IRequirementLimitByBuildingDto,) {
|
function renderLimitPerBuilding(limitPerBuilding: IRequirementLimitByBuildingDto,) {
|
||||||
return <span> One <img style={{verticalAlign: "top", height: 25}}
|
return <span> One <img style={{verticalAlign: "top", height: 25}}
|
||||||
src={IconUrl + limitPerBuilding.building.icon.replaceAll('\\', '/')}/>
|
src={IconUrl + limitPerBuilding.building.icon.replaceAll('\\', '/')}/>
|
||||||
{" "}<a href={`/mod/${props.modId}/race/${props.raceId}/building/${limitPerBuilding.building.id}`}>
|
{" "}<StyledLink href={`/mod/${props.modId}/race/${props.raceId}/building/${limitPerBuilding.building.id}`}>
|
||||||
{limitPerBuilding.building.name} </a> allow produce max {limitPerBuilding.limit}</span>
|
{limitPerBuilding.building.name} </StyledLink> allows producing a maximum of {limitPerBuilding.limit}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRequirementResearches(requirementResearches: IResearchRequirements[],) {
|
function renderRequirementResearches(requirementResearches: IResearchRequirements[],) {
|
||||||
@@ -79,9 +84,9 @@ function Required (props: IRequiredProps) {
|
|||||||
|
|
||||||
<span> Research {rs.researchMustNotBeComplete ? "not" : ""}: <img style={{verticalAlign: "top", height: 25}}
|
<span> Research {rs.researchMustNotBeComplete ? "not" : ""}: <img style={{verticalAlign: "top", height: 25}}
|
||||||
src={getIcon(rs.researchShortDto.icon)}/>
|
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}
|
{rs.researchShortDto.name}
|
||||||
</a></span>
|
</StyledLink></span>
|
||||||
)}</div>
|
)}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +95,9 @@ function Required (props: IRequiredProps) {
|
|||||||
|
|
||||||
<span> Squad: {rs.count} <img style={{verticalAlign: "top", height: 25}}
|
<span> Squad: {rs.count} <img style={{verticalAlign: "top", height: 25}}
|
||||||
src={getIcon(rs.squad.icon)}/>
|
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}
|
{rs.squad.name}
|
||||||
</a></span>
|
</StyledLink></span>
|
||||||
)}</div>
|
)}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,12 +106,12 @@ function Required (props: IRequiredProps) {
|
|||||||
return requirementResearches.length > 0 && (
|
return requirementResearches.length > 0 && (
|
||||||
<div> Researches: <span> <img style={{verticalAlign: "top", height: 25}}
|
<div> Researches: <span> <img style={{verticalAlign: "top", height: 25}}
|
||||||
src={getIcon(requirementResearches[0].researchShortDto.icon)}/>
|
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}
|
{requirementResearches[0].researchShortDto.name}
|
||||||
</a></span> {requirementResearches[1] != null && <span> or <img style={{verticalAlign: "top", height: 25}} src={getIcon(requirementResearches[1].researchShortDto.icon)}/>
|
</StyledLink></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 href={`/mod/${props.modId}/race/${props.raceId}/building/${requirementResearches[1].researchShortDto.buildingId}#research-${requirementResearches[1].researchShortDto.id}`}>
|
||||||
{requirementResearches[1].researchShortDto.name}
|
{requirementResearches[1].researchShortDto.name}
|
||||||
</a></span>} </div>
|
</StyledLink></span>} </div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,48 +119,48 @@ function Required (props: IRequiredProps) {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{color: textColor}}>
|
||||||
<h4>Required: </h4>
|
<h4 style={{color: textColor, fontWeight: 700}}>Required: </h4>
|
||||||
{requirement.requiredTotalPop !== undefined && requirement.requiredTotalPop !== null &&
|
{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"/>
|
src="/images/Resource_orksquadcap.gif"/>
|
||||||
{requirement.requiredTotalPop}</div>
|
{requirement.requiredTotalPop}</div>
|
||||||
}
|
}
|
||||||
{requirement.mobBonus !== null && requirement.mobBonus !== undefined &&
|
{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"/>
|
src="/images/Mob_bonus.gif"/>
|
||||||
{requirement.mobBonus.mobvalueRequired} in radius {requirement.mobBonus.proximityRequired}</div>
|
{requirement.mobBonus.mobvalueRequired} in radius {requirement.mobBonus.proximityRequired}</div>
|
||||||
}
|
}
|
||||||
{requirement.requireAddon !== null &&
|
{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('\\', '/')}/>
|
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}
|
{requirement.requireAddon.name}
|
||||||
</a>
|
</StyledLink>
|
||||||
{requirement.replaceWhenDone && <i> (old provides replace)</i>}
|
{requirement.replaceWhenDone && <i> (old provides replace)</i>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{requirement.requirementAddonExclusive != null &&
|
{requirement.requirementAddonExclusive != null &&
|
||||||
renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)}
|
<div style={{color: textColor}}>{renderRequiremenAddonExclusive(requirement.requirementAddonExclusive,)}</div>}
|
||||||
{requirement.requirementBuildings.map(b =>
|
{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('\\', '/')}/>
|
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 &&
|
{requirement.requirementResearches.length !== 0 &&
|
||||||
renderRequirementResearches(requirement.requirementResearches,)}
|
<div style={{color: textColor}}>{renderRequirementResearches(requirement.requirementResearches,)}</div>}
|
||||||
{requirement.requirementSquads.length !== 0 &&
|
{requirement.requirementSquads.length !== 0 &&
|
||||||
renderRequirementSquads(requirement.requirementSquads,)}
|
<div style={{color: textColor}}>{renderRequirementSquads(requirement.requirementSquads,)}</div>}
|
||||||
{requirement.requirementResearchesEither.length !== 0 &&
|
{requirement.requirementResearchesEither.length !== 0 &&
|
||||||
renderRequirementResearchesEither(requirement.requirementResearchesEither,)}
|
<div style={{color: textColor}}>{renderRequirementResearchesEither(requirement.requirementResearchesEither,)}</div>}
|
||||||
{requirement.requirementBuildingsEither.length !== 0 &&
|
{requirement.requirementBuildingsEither.length !== 0 &&
|
||||||
renderRequirementBuildingEither(requirement.requirementBuildingsEither,)}
|
<div style={{color: textColor}}>{renderRequirementBuildingEither(requirement.requirementBuildingsEither,)}</div>}
|
||||||
{requirement.requirementStructureExclusive != null &&
|
{requirement.requirementStructureExclusive != null &&
|
||||||
renderRequiremenStructureExclusive(requirement.requirementStructureExclusive,)}
|
<div style={{color: textColor}}>{renderRequiremenStructureExclusive(requirement.requirementStructureExclusive,)}</div>}
|
||||||
{requirement.limitByBuilding !== undefined && requirement.limitByBuilding !== null && renderLimitPerBuilding(requirement.limitByBuilding)}
|
{requirement.limitByBuilding !== undefined && requirement.limitByBuilding !== null && <div style={{color: textColor}}>{renderLimitPerBuilding(requirement.limitByBuilding)}</div>}
|
||||||
{requirement.requirementsGlobalAddons.length !== 0 &&
|
{requirement.requirementsGlobalAddons.length !== 0 &&
|
||||||
renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)}
|
<div style={{color: textColor}}>{renderRequirementGlobalAddons(requirement.requirementsGlobalAddons,)}</div>}
|
||||||
{renderRequirementOwnership(requirement.requiredOwnership)}
|
<div style={{color: textColor}}>{renderRequirementOwnership(requirement.requiredOwnership)}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,27 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
import {ISergeant, IShortWeapon} from "../types/IUnit";
|
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 AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
||||||
import ArmorType from "./ArmorType";
|
import ArmorType from "./ArmorType";
|
||||||
import WeaponSlot from "./WeaponSlot";
|
import WeaponSlot from "./WeaponSlot";
|
||||||
import Vision from "./Vision";
|
import Vision from "./Vision";
|
||||||
import {IMod} from "../types/Imod";
|
import {IMod} from "../types/Imod";
|
||||||
import {renderAffectedResearches} from "./building/Research";
|
import {AffectedResearches} from "./building/Research";
|
||||||
import {Irace} from "../types/Irace";
|
import {Irace} from "../types/Irace";
|
||||||
import Required from "./Required";
|
import Required from "./Required";
|
||||||
import {ModifiersProvidesTable} from "./ModifiersProvideTable";
|
import {ModifiersProvidesTable} from "./ModifiersProvideTable";
|
||||||
import Ability from "./Ability";
|
import Ability from "./Ability";
|
||||||
import DeathExplosion from "./DeathExplosion";
|
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 {
|
export interface SergeantProps {
|
||||||
@@ -21,6 +31,9 @@ export interface SergeantProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Sergeant = (props: 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
|
const sergeant = props.sergeant
|
||||||
|
|
||||||
@@ -38,17 +51,17 @@ const Sergeant = (props: SergeantProps) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{color: textColor}}>
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
<Grid2 size= {{xs: 12, md: 4}}>
|
<Grid2 size= {{xs: 12, md: 4}}>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
<TableBody id="unit-stats-table">
|
<TableBody id="unit-stats-table">
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Cost</TableCell>
|
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
{sergeant.buildCostRequisition > 0 &&
|
{sergeant.buildCostRequisition > 0 &&
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
<span> <img style={{verticalAlign: "top"}}
|
||||||
src="/images/Resource_requisition.gif"/>
|
src="/images/Resource_requisition.gif"/>
|
||||||
@@ -72,7 +85,7 @@ const Sergeant = (props: SergeantProps) => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
{(sergeant.requisitionIncome !== undefined && sergeant.requisitionIncome !== null || sergeant.powerIncome !== undefined && sergeant.powerIncome !== null || sergeant.faithIncome !== undefined && sergeant.faithIncome !== null) &&
|
{(sergeant.requisitionIncome !== undefined && sergeant.requisitionIncome !== null || sergeant.powerIncome !== undefined && sergeant.powerIncome !== null || sergeant.faithIncome !== undefined && sergeant.faithIncome !== null) &&
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Resource income</TableCell>
|
<TableCell >Resource income</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{sergeant.requisitionIncome !== undefined && sergeant.requisitionIncome > 0 &&
|
{sergeant.requisitionIncome !== undefined && sergeant.requisitionIncome > 0 &&
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
<span> <img style={{verticalAlign: "top"}}
|
||||||
@@ -92,8 +105,8 @@ const Sergeant = (props: SergeantProps) => {
|
|||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Armor type</TableCell>
|
<TableCell component="th" scope="row" >Armor type</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
{sergeant.armorType2 == null ? <ArmorType name={sergeant.armorType.name} compact={false}/>
|
{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> }
|
: <Tooltip title={"upgrade/debuff can turns to " + sergeant.armorType2.name}><span><ArmorType name={sergeant.armorType.name} compact={false}/></span></Tooltip> }
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -101,8 +114,8 @@ const Sergeant = (props: SergeantProps) => {
|
|||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Health</TableCell>
|
<TableCell component="th" scope="row" >Health</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
<span><img style={{verticalAlign: "top"}}
|
<span><img style={{verticalAlign: "top"}}
|
||||||
src="/images/Health_icon.webp"/>
|
src="/images/Health_icon.webp"/>
|
||||||
{sergeant.health} {sergeant.healthRegeneration > 0 &&
|
{sergeant.health} {sergeant.healthRegeneration > 0 &&
|
||||||
@@ -112,8 +125,8 @@ const Sergeant = (props: SergeantProps) => {
|
|||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Morale</TableCell>
|
<TableCell component="th" scope="row" >Morale</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
{sergeant.moraleIncomeMax && <span><img style={{verticalAlign: "top"}}
|
{sergeant.moraleIncomeMax && <span><img style={{verticalAlign: "top"}}
|
||||||
src="/images/ARM_Morale.webp"/> +{sergeant.moraleIncomeMax} </span>}
|
src="/images/ARM_Morale.webp"/> +{sergeant.moraleIncomeMax} </span>}
|
||||||
{sergeant.moraleRate && <span>+ {sergeant.moraleRate}/s</span> }
|
{sergeant.moraleRate && <span>+ {sergeant.moraleRate}/s</span> }
|
||||||
@@ -124,22 +137,20 @@ const Sergeant = (props: SergeantProps) => {
|
|||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Mass</TableCell>
|
<TableCell component="th" scope="row" >Mass</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>{sergeant.mass}</TableCell>
|
||||||
{sergeant.mass}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Vision</TableCell>
|
<TableCell component="th" scope="row" >Vision</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
<Vision sight={sergeant.sightRadius} detect={sergeant.detectRadius}/>
|
<Vision sight={sergeant.sightRadius} detect={sergeant.detectRadius}/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{sergeant.mobValue != null &&
|
{sergeant.mobValue != null &&
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>Mob value</TableCell>
|
<TableCell component="th" scope="row" >Mob value</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span> <img style={{height: 20, verticalAlign: "top"}} src="/images/Mob_bonus.gif"/> {sergeant.mobValue} </span>
|
<span> <img style={{height: 20, verticalAlign: "top"}} src="/images/Mob_bonus.gif"/> {sergeant.mobValue} </span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -150,20 +161,20 @@ const Sergeant = (props: SergeantProps) => {
|
|||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={8}>
|
<Grid2 size={8}>
|
||||||
<div style={{whiteSpace: "pre-wrap"}}>
|
<DescriptionBox>
|
||||||
{sergeant.description}
|
{sergeant.description}
|
||||||
</div>
|
</DescriptionBox>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
{sergeant.requirements !== null &&
|
{sergeant.requirements !== null &&
|
||||||
<Grid2 size={{xs: 12, md: 12}}>
|
<Grid2 size={{xs: 12, md: 12}}>
|
||||||
<Required requirement={sergeant.requirements} modId={props.mod.id} raceId={props.race.id}/>
|
<Required requirement={sergeant.requirements} modId={props.mod.id} raceId={props.race.id}/>
|
||||||
</Grid2>}
|
</Grid2>}
|
||||||
{sergeant.modifiers.length > 0 && <Grid2 size={12}>
|
{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}/>
|
<ModifiersProvidesTable modifiers={sergeant.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/>
|
||||||
</Grid2>}
|
</Grid2>}
|
||||||
{sergeant.abilities.length > 0 && <Grid2 size={12}>
|
{sergeant.abilities.length > 0 && <Grid2 size={12}>
|
||||||
<h3>Abilities</h3>
|
<SectionTitle>Abilities</SectionTitle>
|
||||||
{sergeant.abilities.map(a =>
|
{sergeant.abilities.map(a =>
|
||||||
<Ability mod={props.mod} ability={a} race={props.race}/>
|
<Ability mod={props.mod} ability={a} race={props.race}/>
|
||||||
)}
|
)}
|
||||||
@@ -176,16 +187,14 @@ const Sergeant = (props: SergeantProps) => {
|
|||||||
</Grid2>
|
</Grid2>
|
||||||
{sergeant.deathExplosions.length > 0 &&
|
{sergeant.deathExplosions.length > 0 &&
|
||||||
<Grid2 size={12}>
|
<Grid2 size={12}>
|
||||||
{sergeant.deathExplosions.map(da =>
|
<DeathExplosion deathExplosions={sergeant.deathExplosions} mod={props.mod}/>
|
||||||
<DeathExplosion deathExplosion={da} mod={props.mod}/>
|
|
||||||
)}
|
|
||||||
</Grid2>
|
</Grid2>
|
||||||
}
|
}
|
||||||
<Grid2 size={12}>
|
<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>
|
||||||
</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>)
|
</div>)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,58 +1,66 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React from "react";
|
||||||
import {AvailableBuildings, AvailableUnits} from "../core/api";
|
import {Grid2, Table, TableBody, TableCell, TableHead, TableRow} from "@mui/material";
|
||||||
import {Grid2, Link, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@mui/material";
|
import {styled} from '@mui/material/styles';
|
||||||
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
|
import {IRaceUnits, IUnitShort} from "../types/IUnitShort";
|
||||||
import {IBuildingShort, IRaceBuildings} from "../types/IBuildingShort";
|
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 {
|
interface IUnitsTable {
|
||||||
racesUnits: IRaceUnits[]
|
racesUnits: IRaceUnits[]
|
||||||
racesBuildings: IRaceBuildings[]
|
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: [],
|
const unitsTable: IUnitsTable = {
|
||||||
racesBuildings: []
|
racesUnits: prop.racesUnits,
|
||||||
});
|
racesBuildings: prop.racesBuildings
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
function getUnitRef(modId: number, raceId: string, unit: IUnitShort) {
|
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}}>
|
||||||
{unit.name}
|
<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/unit/" + unit.id}>
|
||||||
</Link><br/></span>
|
{unit.name}
|
||||||
|
</StyledLink><br/>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBuildingRef(modId: number, raceId: string, building: IBuildingShort) {
|
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}}>
|
||||||
{building.name}
|
<StyledLink href={"/mod/" + modId + "/race/" + raceId + "/building/" + building.id}>
|
||||||
<br/></Link></span>
|
{building.name}
|
||||||
|
</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 {
|
function armourTypePriority(armorTypeId: string): number {
|
||||||
switch(armorTypeId) {
|
switch(armorTypeId) {
|
||||||
case 'Infantry Low':
|
case 'Infantry Low':
|
||||||
@@ -63,11 +71,11 @@ export default function UnitsTable(prop: {modId: number}) {
|
|||||||
return 3;
|
return 3;
|
||||||
case 'Demon Medium':
|
case 'Demon Medium':
|
||||||
return 4;
|
return 4;
|
||||||
case 'Infantry Heavy Medium':
|
case 'Infantry H.Med.':
|
||||||
return 5;
|
return 5;
|
||||||
case 'Infantry Heavy High':
|
case 'Infantry H.High':
|
||||||
return 6;
|
return 6;
|
||||||
case 'Commander':
|
case 'Infantry Com.':
|
||||||
return 7;
|
return 7;
|
||||||
case 'Vehicle Low':
|
case 'Vehicle Low':
|
||||||
return 8;
|
return 8;
|
||||||
@@ -77,7 +85,7 @@ export default function UnitsTable(prop: {modId: number}) {
|
|||||||
return 10;
|
return 10;
|
||||||
case 'Demon High':
|
case 'Demon High':
|
||||||
return 11;
|
return 11;
|
||||||
case 'Air':
|
case 'Vehicle Air':
|
||||||
return 12;
|
return 12;
|
||||||
case 'Building Low':
|
case 'Building Low':
|
||||||
return 13;
|
return 13;
|
||||||
@@ -99,31 +107,31 @@ export default function UnitsTable(prop: {modId: number}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateRaceUnitTable(racesUnitsPart: IRaceUnits[], racesBuildings: IRaceBuildings[]){
|
function generateRaceUnitTable(racesUnitsPart: IRaceUnits[], racesBuildings: IRaceBuildings[]){
|
||||||
return (<TableContainer>
|
return (<StyledPaper elevation={0} sx={{borderRadius: 0}}>
|
||||||
{<Table sx={{minWidth: 650}} size="small" aria-label="a dense table">
|
{<StyledTable sx={{minWidth: 650}} size="small" aria-label="a dense table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{racesUnitsPart.map(raceUnits => <TableCell><h2><Link color="inherit"
|
{racesUnitsPart.map(raceUnits => <TableCell><h2><StyledHeaderLink
|
||||||
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</Link>
|
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</StyledHeaderLink>
|
||||||
</h2></TableCell>)}
|
</h2></TableCell>)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow style={{verticalAlign: 'top'}}>
|
<TableRow>
|
||||||
{racesUnitsPart.map(raceUnits =>
|
{racesUnitsPart.map(raceUnits =>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{getUnitsRef(raceUnits.infantry.concat(raceUnits.support), raceUnits.race.id)}
|
{getUnitsRef(raceUnits.infantry.concat(raceUnits.support), raceUnits.race.id)}
|
||||||
</TableCell>)
|
</TableCell>)
|
||||||
}
|
}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow style={{verticalAlign: 'top'}}>
|
<TableRow>
|
||||||
{racesUnitsPart.map(raceUnits =>
|
{racesUnitsPart.map(raceUnits =>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{getUnitsRef(raceUnits.tech, raceUnits.race.id)}
|
{getUnitsRef(raceUnits.tech, raceUnits.race.id)}
|
||||||
</TableCell>)
|
</TableCell>)
|
||||||
}
|
}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow style={{verticalAlign: 'top'}}>
|
<TableRow>
|
||||||
{racesBuildings.map(raceBuilding =>
|
{racesBuildings.map(raceBuilding =>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{raceBuilding.buildings.map(building => getBuildingRef(prop.modId, raceBuilding.race.id, building))}
|
{raceBuilding.buildings.map(building => getBuildingRef(prop.modId, raceBuilding.race.id, building))}
|
||||||
@@ -131,8 +139,8 @@ export default function UnitsTable(prop: {modId: number}) {
|
|||||||
}
|
}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>}
|
</StyledTable>}
|
||||||
</TableContainer>)
|
</StyledPaper>)
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateRaceUnitTables(racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[], raceCount: number){
|
function generateRaceUnitTables(racesUnits: IRaceUnits[], racesBuildings: IRaceBuildings[], raceCount: number){
|
||||||
@@ -156,8 +164,8 @@ export default function UnitsTable(prop: {modId: number}) {
|
|||||||
function generateRaceList(unitsTable: IUnitsTable){
|
function generateRaceList(unitsTable: IUnitsTable){
|
||||||
return unitsTable.racesUnits.sort(function (a, b) {
|
return unitsTable.racesUnits.sort(function (a, b) {
|
||||||
return a.race.name.localeCompare(b.race.name);
|
return a.race.name.localeCompare(b.race.name);
|
||||||
}).map(raceUnits => <h5 key={raceUnits.race.id}><Link color="inherit"
|
}).map(raceUnits => <h5 key={raceUnits.race.id}><StyledLink
|
||||||
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</Link><br/>
|
href={"/mod/" + prop.modId + "/race/" + raceUnits.race.id}>{raceUnits.race.name}</StyledLink><br/>
|
||||||
</h5>)
|
</h5>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {IShortWeapon, IWeapon} from "../types/IUnit";
|
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 {IconUrl} from "../core/api";
|
||||||
import {ExpandMore} from "@mui/icons-material";
|
import {ExpandMore} from "@mui/icons-material";
|
||||||
import {IMod} from "../types/Imod";
|
import {IMod} from "../types/Imod";
|
||||||
import {Irace} from "../types/Irace";
|
import {Irace} from "../types/Irace";
|
||||||
import WeaponFull from "./WeaponFull";
|
import WeaponFull from "./WeaponFull";
|
||||||
|
import {StyledAccordion} from "../commons/StyledAccordion";
|
||||||
|
|
||||||
interface IWeaponProps {
|
interface IWeaponProps {
|
||||||
weapon: IShortWeapon,
|
weapon: IShortWeapon,
|
||||||
@@ -30,29 +32,35 @@ class Weapon extends React.Component<IWeaponProps, any> {
|
|||||||
|
|
||||||
|
|
||||||
render() {
|
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
|
const weapon = this.props.weapon
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{marginBottom: 10}}><Accordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
|
<div style={{marginBottom: 10}}><StyledAccordion TransitionProps={{ unmountOnExit: true, timeout: 100 }}>
|
||||||
<AccordionSummary
|
<AccordionSummary
|
||||||
expandIcon={<ExpandMore/>}
|
expandIcon={<ExpandMore sx={{color: textColor}}/>}
|
||||||
aria-controls="panel1-content"
|
aria-controls="panel1-content"
|
||||||
id="panel1-header"
|
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('\\', '/')}/> : (
|
<img className="weaponIcon" src={IconUrl + weapon.icon.replaceAll('\\', '/')}/> : (
|
||||||
weapon.isMeleeWeapon ?
|
weapon.isMeleeWeapon ?
|
||||||
<img className="weaponIcon" src="/images/MeleeStance_icon_bw.jpg"/> :
|
<img className="weaponIcon" src="/images/MeleeStance_icon_bw.jpg"/> :
|
||||||
<img className="weaponIcon" src="/images/RangedStance_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>
|
</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}/>
|
<WeaponFull weaponId={weapon.id} isDefault={this.props.isDefault} mod={this.props.mod} race={this.props.race} haveReinforceMenu={this.props.haveReinforceMenu}/>
|
||||||
</AccordionDetails>
|
</AccordionDetails>
|
||||||
</Accordion></div>
|
</StyledAccordion></div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Weapon;
|
export default withTheme(Weapon);
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {AvailableBuildings, AvailableUnits, UserUrl, WeaponUrl} from "../core/api";
|
import {useTheme} from "@mui/material/styles";
|
||||||
import {IWeapon} from "../types/IUnit";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import {WeaponUrl} from "../core/api";
|
||||||
import ArmorTypeNames from "../types/ArmorTypeValues";
|
import ArmorTypeNames from "../types/ArmorTypeValues";
|
||||||
import {
|
import {
|
||||||
AccordionDetails,
|
|
||||||
Grid2,
|
Grid2,
|
||||||
Paper,
|
|
||||||
styled,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
tableCellClasses,
|
|
||||||
TableContainer, TableHead,
|
TableContainer, TableHead,
|
||||||
TableRow, ToggleButton, ToggleButtonGroup
|
TableRow, ToggleButton, ToggleButtonGroup, Slider
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ArmorType from "./ArmorType";
|
import ArmorType from "./ArmorType";
|
||||||
import Required from "./Required";
|
import Required from "./Required";
|
||||||
import {renderAffectedResearches} from "./building/Research";
|
import {AffectedResearches} from "./building/Research";
|
||||||
import {IMod} from "../types/Imod";
|
import {IMod} from "../types/Imod";
|
||||||
import {Irace} from "../types/Irace";
|
import {Irace} from "../types/Irace";
|
||||||
import {IRaceUnits} from "../types/IUnitShort";
|
import {ModifiersProvidesTable, getIcon} from "./ModifiersProvideTable";
|
||||||
import {IRaceBuildings} from "../types/IBuildingShort";
|
import {StyledPaper} from "../commons/StyledPaper";
|
||||||
import {ModifiersProvidesTable} from "./ModifiersProvideTable";
|
import {StyledTableCell} from "../commons/StyledTableCell";
|
||||||
|
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
||||||
|
import {IWeapon, IWeaponResearch} from "../types/IUnit";
|
||||||
|
|
||||||
|
|
||||||
interface IWeaponFull {
|
interface IWeaponFull {
|
||||||
currentTable: string;
|
currentTable: string;
|
||||||
@@ -29,26 +29,84 @@ interface IWeaponFull {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function WeaponFull(props: {weaponId: number, isDefault: Boolean, mod: IMod, race: Irace, haveReinforceMenu: Boolean}) {
|
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>({
|
const [weaoponFull, setWeaponFull] = useState<IWeaponFull>({
|
||||||
currentTable: "dps",
|
currentTable: "dps",
|
||||||
weapon: undefined,
|
weapon: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const StyledTableCell = styled(TableCell)(({theme}) => ({
|
const [enabledResearchIds, setEnabledResearchIds] = useState<Set<number>>(new Set());
|
||||||
[`&.${tableCellClasses.head}`]: {
|
const [damageRoll, setDamageRoll] = useState<number | null>(null);
|
||||||
backgroundColor: "rgb(244,244,244)",
|
|
||||||
marginRight: 'auto',
|
function applyResearchEffects(weapon: IWeapon, enabledIds: Set<number>): IWeapon {
|
||||||
marginLeft: 'auto',
|
if (enabledIds.size === 0 || !weapon.researches || weapon.researches.length === 0) {
|
||||||
paddingLeft: 10
|
return weapon;
|
||||||
},
|
}
|
||||||
[`&.${tableCellClasses.body}`]: {
|
|
||||||
fontSize: 12,
|
const modified: any = { ...weapon };
|
||||||
textAlign: 'center',
|
const enabledResearches = weapon.researches.filter((r: IWeaponResearch) => enabledIds.has(r.research.id));
|
||||||
paddingRight: 18,
|
|
||||||
paddingLeft: 10
|
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(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
@@ -64,14 +122,14 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if(weaoponFull.weapon !== undefined){
|
if(weaoponFull.weapon !== undefined){
|
||||||
|
|
||||||
const weapon = weaoponFull.weapon
|
const weapon = weaoponFull.weapon
|
||||||
|
|
||||||
|
const displayWeapon = applyResearchEffects(weapon, enabledResearchIds);
|
||||||
|
|
||||||
function getPiercingK(armorType: string): number {
|
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
|
return (typeof weaponPiercing !== "undefined" ? weaponPiercing.piercingValue : 10) / 100
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +138,18 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
currentTable: selectedDamageTable,
|
currentTable: selectedDamageTable,
|
||||||
weapon: weaoponFull.weapon,
|
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 infLowPiercing = getPiercingK(ArmorTypeNames.InfantryLow)
|
||||||
const infMedPiercing = getPiercingK(ArmorTypeNames.InfantryMedium)
|
const infMedPiercing = getPiercingK(ArmorTypeNames.InfantryMedium)
|
||||||
@@ -107,119 +174,112 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
const livingMetalPiercing = getPiercingK(ArmorTypeNames.LivingMetal)
|
const livingMetalPiercing = getPiercingK(ArmorTypeNames.LivingMetal)
|
||||||
const titanPiercing = getPiercingK(ArmorTypeNames.Titan)
|
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) => {
|
const getTotalDamage = (damagePiercing: number, isAir: boolean = false) => {
|
||||||
|
|
||||||
if (!isAir && !weapon.canAttackGround && !weapon.isMeleeWeapon) return ""
|
if (!isAir && !weapon.canAttackGround && !weapon.isMeleeWeapon) return ""
|
||||||
if (isAir && !weapon.canAttackAir) return ""
|
if (isAir && !weapon.canAttackAir) return ""
|
||||||
|
|
||||||
var minDamage = damagePiercing * weapon.minDamage
|
var minDamage = damagePiercing * displayWeapon.minDamage
|
||||||
var maxDamage = damagePiercing * weapon.maxDamage
|
var maxDamage = damagePiercing * displayWeapon.maxDamage
|
||||||
|
|
||||||
if (minDamage < weapon.minDamageValue) {
|
if (minDamage < displayWeapon.minDamageValue) {
|
||||||
minDamage = weapon.minDamageValue
|
minDamage = displayWeapon.minDamageValue
|
||||||
}
|
}
|
||||||
if (maxDamage < weapon.minDamageValue) {
|
if (maxDamage < displayWeapon.minDamageValue) {
|
||||||
maxDamage = weapon.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 = () => {
|
const getMoraleDamage = () => {
|
||||||
return (weapon.moraleDamage * dpsK / 2).toFixed(2)
|
return (displayWeapon.moraleDamage * dpsK / 2).toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{color: textColor}}>
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
<Grid2 size={12}>
|
{!props.isDefault && <Grid2 size={12}>
|
||||||
<div style={{whiteSpace: "pre-wrap"}}>
|
<span className="weaponDescription" style={{ whiteSpace: 'pre-wrap' }}>{weapon.description}</span>
|
||||||
<span
|
</Grid2>}
|
||||||
className="weaponDescription">{props.isDefault ? "Default weapon" : weapon.description}</span>
|
|
||||||
</div>
|
|
||||||
</Grid2>
|
|
||||||
|
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
<Grid2 size={{xs: 12, md: 4}}>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
{/*<TableHead><TableRow><TableCell component="th" scope="row"><b>Damage</b></TableCell><TableCell/></TableRow></TableHead>*/}
|
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Base damage</TableCell>
|
<TableCell component="th" scope="row" >Base damage</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell>{displayWeapon.minDamage.toFixed(2)} {displayWeapon.maxDamage !== displayWeapon.minDamage && "- " + displayWeapon.maxDamage.toFixed(2)}</TableCell>
|
||||||
scope="row">{weapon.minDamage} {weapon.maxDamage !== weapon.minDamage && "- " + weapon.maxDamage}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Accuracy</TableCell>
|
<TableCell component="th" scope="row" >Accuracy</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell>{displayWeapon.accuracy.toFixed(3).replace(/[,.]?0+$/, '')}</TableCell>
|
||||||
scope="row">{weapon.accuracy.toFixed(3).replace(/[,.]?0+$/, '')}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Accuracy moving</TableCell>
|
<TableCell component="th" scope="row" >Accuracy moving</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell>{hasMovingAccuracy ? movingAccuracy.toFixed(3).replace(/[,.]?0+$/, '') : "-"}</TableCell>
|
||||||
scope="row">{weapon.setupTime === 0 && weapon.accuracy - weapon.accuracyReductionMoving > 0 ? (weapon.accuracy - weapon.accuracyReductionMoving).toFixed(3).replace(/[,.]?0+$/, '') : "-"}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
<Grid2 size={{xs: 12, md: 4}}>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Reload time</TableCell>
|
<TableCell component="th" scope="row" >Reload time</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell>{displayWeapon.reloadTime}</TableCell>
|
||||||
scope="row">{weapon.reloadTime}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Range</TableCell>
|
<TableCell component="th" scope="row" >Range</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell>{displayWeapon.maxRange ? (displayWeapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}</TableCell>
|
||||||
scope="row">{weapon.maxRange ? (weapon.maxRange) : "-"} {weapon.minRange ? "(min " + (weapon.minRange) + ")" : ""}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Damage radius</TableCell>
|
<TableCell component="th" scope="row" >Damage radius</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell>{weapon.damageRadius ? (weapon.damageRadius) : "-"}</TableCell>
|
||||||
scope="row">{weapon.damageRadius ? (weapon.damageRadius) : "-"}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
<Grid2 size={{xs: 12, md: 4}}>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Cost</TableCell>
|
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||||
{(weapon.costRequisition > 0 || weapon.costPower > 0) ?
|
{(weapon.costRequisition > 0 || weapon.costPower > 0) ?
|
||||||
<TableCell component="th"
|
<TableCell>
|
||||||
scope="row">
|
|
||||||
{weapon.costRequisition > 0 &&
|
{weapon.costRequisition > 0 &&
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
<span> <img style={{verticalAlign: "top"}}
|
||||||
src="/images/Resource_requisition.gif"/>
|
src="/images/Resource_requisition.gif"/>
|
||||||
@@ -228,24 +288,24 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
<span> <img style={{verticalAlign: "top"}}
|
<span> <img style={{verticalAlign: "top"}}
|
||||||
src="/images/Resource_power.gif"/>
|
src="/images/Resource_power.gif"/>
|
||||||
{weapon.costPower.toFixed(0)}</span>}
|
{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>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Setup time</TableCell>
|
<TableCell component="th" scope="row" >Setup time</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell>{displayWeapon.setupTime != 0 ? (displayWeapon.setupTime).toFixed(3).replace(/[,.]?0+$/, '') : "-"}</TableCell>
|
||||||
scope="row">{weapon.setupTime != 0 ? (weapon.setupTime).toFixed(3).replace(/[,.]?0+$/, '') : "-"}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Throw force</TableCell>
|
<TableCell component="th" scope="row" >Throw force</TableCell>
|
||||||
<TableCell component="th"
|
<TableCell>{weapon.throwForceMin != 0 ? weapon.throwForceMin + " - " + weapon.throwForceMax : "-"}</TableCell>
|
||||||
scope="row">{weapon.throwForceMin != 0 ? weapon.throwForceMin + " - " + weapon.throwForceMax : "-"}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
@@ -253,18 +313,183 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
</Grid2>
|
</Grid2>
|
||||||
|
|
||||||
<Grid2 size={12}>
|
<Grid2 size={12}>
|
||||||
<ToggleButtonGroup
|
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 12, marginBottom: 8 }}>
|
||||||
color="primary"
|
<ToggleButtonGroup
|
||||||
exclusive
|
color="primary"
|
||||||
aria-label="Platform"
|
exclusive
|
||||||
value={weaoponFull.currentTable}
|
aria-label="Platform"
|
||||||
onChange={handleChange}
|
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>
|
||||||
</ToggleButtonGroup>
|
<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>
|
<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">
|
<Table sx={{marginTop: "10px", textAlign: "center"}} size="small" aria-label="a dense table">
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -290,7 +515,7 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
|
||||||
@@ -325,13 +550,13 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
name={ArmorTypeNames.BuildingSuper}/></StyledTableCell>
|
name={ArmorTypeNames.BuildingSuper}/></StyledTableCell>
|
||||||
<StyledTableCell><img style={{verticalAlign: "top"}}
|
<StyledTableCell><img style={{verticalAlign: "top"}}
|
||||||
src="/images/ARM_Morale.webp"/>
|
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>
|
<i>Morale</i></div>
|
||||||
</StyledTableCell>
|
</StyledTableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(vehLowPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(vehMedPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(vehHighPiercing)}</StyledTableCell>
|
||||||
@@ -377,15 +602,13 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
name={ArmorTypeNames.BuildingMedium}/></StyledTableCell>
|
name={ArmorTypeNames.BuildingMedium}/></StyledTableCell>
|
||||||
<StyledTableCell><ArmorType
|
<StyledTableCell><ArmorType
|
||||||
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
|
name={ArmorTypeNames.BuildingHigh}/></StyledTableCell>
|
||||||
<StyledTableCell><img style={{verticalAlign: "top"}}
|
<StyledTableCell><ArmorType
|
||||||
src="/images/ARM_Morale.webp"/>
|
name='Morale damage'/>
|
||||||
<div style={{width: 20, fontSize: 12, height: 50}}>
|
|
||||||
<i>Morale</i></div>
|
|
||||||
</StyledTableCell>
|
</StyledTableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
<TableRow sx={{'&:last-child td, &:last-child th': {border: 0}}}>
|
||||||
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(infLowPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(infMedPiercing)}</StyledTableCell>
|
||||||
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
|
<StyledTableCell>{getTotalDamage(infHighPiercing)}</StyledTableCell>
|
||||||
@@ -406,12 +629,11 @@ export default function WeaponFull(props: {weaponId: number, isDefault: Boolean,
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
}
|
}
|
||||||
|
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|
||||||
</Grid2>
|
</Grid2>
|
||||||
{weapon.modifiers.length > 0 && <Grid2 size={12}>
|
{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}/>
|
<ModifiersProvidesTable modifiers={weapon.modifiers} modId={props.mod.id} race={props.race} affectedData={null}/>
|
||||||
</Grid2>}
|
</Grid2>}
|
||||||
{weapon.requirements !== null && !props.isDefault && props.haveReinforceMenu &&
|
{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}/>
|
<Required requirement={weapon.requirements} modId={props.mod.id} raceId={props.race.id}/>
|
||||||
</Grid2>}
|
</Grid2>}
|
||||||
<Grid2 size={12}>
|
<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>
|
||||||
</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>
|
</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 {IMod} from "../types/Imod";
|
||||||
import {Irace} from "../types/Irace";
|
import {Irace} from "../types/Irace";
|
||||||
import WeaponShort from "./WeaponFull";
|
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 {
|
export interface WeaponSlotProps {
|
||||||
hardpoint: number,
|
hardpoint: number,
|
||||||
@@ -33,10 +41,10 @@ const WeaponSlot= (props: WeaponSlotProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3>{header}</h3>
|
<SectionTitle>{header}</SectionTitle>
|
||||||
{weaponsSorted.filter(wp => !wp[1].filename.includes("dummy")).map(wp =>
|
{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>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
Grid2,
|
Grid2,
|
||||||
Paper,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableRow
|
TableRow,
|
||||||
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {IBuilding, IBuildingAddon} from "../../types/IBuilding";
|
import {IBuilding, IBuildingAddon} from "../../types/IBuilding";
|
||||||
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
||||||
@@ -17,6 +17,10 @@ import {ExpandMore} from "@mui/icons-material";
|
|||||||
import {IMod} from "../../types/Imod";
|
import {IMod} from "../../types/Imod";
|
||||||
import {getIcon, ModifiersProvidesTable,} from "../ModifiersProvideTable";
|
import {getIcon, ModifiersProvidesTable,} from "../ModifiersProvideTable";
|
||||||
import Required from "../Required";
|
import Required from "../Required";
|
||||||
|
import {StyledAccordion} from "../../commons/StyledAccordion";
|
||||||
|
import {StyledPaper} from "../../commons/StyledPaper";
|
||||||
|
import {styled} from "@mui/material/styles";
|
||||||
|
|
||||||
|
|
||||||
interface IBuildingAddonProps {
|
interface IBuildingAddonProps {
|
||||||
addon: IBuildingAddon,
|
addon: IBuildingAddon,
|
||||||
@@ -25,33 +29,37 @@ interface IBuildingAddonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function BuildingAddon(props: 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 addon = props.addon
|
||||||
const building = props.building
|
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
|
<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"
|
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)}/>
|
<img className="sergeantIcon" src={getIcon(addon.icon)}/>
|
||||||
{addon.name}
|
{addon.name}
|
||||||
</span>
|
</span>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails sx={{color: textColor}}>
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
<Grid2 size={{xs: 12, md: 4}}>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
<TableBody id="unit-stats-table">
|
<TableBody id="unit-stats-table">
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Cost</TableCell>
|
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
{addon.addonCostRequisition > 0 &&
|
{addon.addonCostRequisition > 0 &&
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
<span> <img style={{verticalAlign: "top"}}
|
||||||
src="/images/Resource_requisition.gif"/>
|
src="/images/Resource_requisition.gif"/>
|
||||||
@@ -83,9 +91,7 @@ function BuildingAddon(props: IBuildingAddonProps){
|
|||||||
</TableContainer><br/>
|
</TableContainer><br/>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={{xs: 12, md: 8}}>
|
<Grid2 size={{xs: 12, md: 8}}>
|
||||||
<div style={{whiteSpace: "pre-wrap"}}>
|
<span style={{ whiteSpace: 'pre-wrap' }}>{addon.description}</span>
|
||||||
{addon.description}
|
|
||||||
</div>
|
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<ModifiersProvidesTable modifiers={addon.addonModifiers} affectedData={addon.affectedData} modId={props.mod.id} race={building.race}/>
|
<ModifiersProvidesTable modifiers={addon.addonModifiers} affectedData={addon.affectedData} modId={props.mod.id} race={building.race}/>
|
||||||
{addon.addonRequirement !== null &&
|
{addon.addonRequirement !== null &&
|
||||||
@@ -93,9 +99,9 @@ function BuildingAddon(props: IBuildingAddonProps){
|
|||||||
<Required requirement={addon.addonRequirement} modId={building.modId} raceId={building.race.id}/>
|
<Required requirement={addon.addonRequirement} modId={building.modId} raceId={building.race.id}/>
|
||||||
</Grid2>}
|
</Grid2>}
|
||||||
</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>
|
</AccordionDetails>
|
||||||
</Accordion></div>
|
</StyledAccordion></div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {IMod} from "../../types/Imod";
|
import {IMod} from "../../types/Imod";
|
||||||
import {IResearch} from "../../types/IResearch";
|
import {IResearch} from "../../types/IResearch";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
Grid2,
|
Grid2,
|
||||||
@@ -14,13 +13,13 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {ExpandMore} from "@mui/icons-material";
|
import {ExpandMore} from "@mui/icons-material";
|
||||||
import {getIcon, ModifiersProvidesTable} from "../ModifiersProvideTable";
|
import {getIcon, ModifiersProvidesTable} from "../ModifiersProvideTable";
|
||||||
import AvTimerOutlinedIcon from "@mui/icons-material/AvTimer";
|
|
||||||
import Required from "../Required";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import {useTheme} from "@mui/material/styles";
|
||||||
import {IBuilding} from "../../types/IBuilding";
|
import {IBuilding} from "../../types/IBuilding";
|
||||||
import {IResearchShort} from "../../types/IResearchShort";
|
import {IResearchShort} from "../../types/IResearchShort";
|
||||||
import ResearchFull from "./ResearchFull";
|
import ResearchFull from "./ResearchFull";
|
||||||
|
import {StyledAccordion} from "../../commons/StyledAccordion";
|
||||||
|
import {StyledLink} from "../../commons/StyledLink";
|
||||||
|
|
||||||
interface IResearchProps {
|
interface IResearchProps {
|
||||||
research: IResearchShort,
|
research: IResearchShort,
|
||||||
@@ -29,38 +28,46 @@ interface IResearchProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Research(props: 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 research = props.research
|
||||||
const building = props.building
|
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
|
<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"
|
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)}/>
|
<img className="sergeantIcon" src={getIcon(research.icon)}/>
|
||||||
{research.name}
|
{research.name}
|
||||||
</span>
|
</span>
|
||||||
</AccordionSummary>
|
</AccordionSummary>
|
||||||
<AccordionDetails>
|
<AccordionDetails sx={{color: textColor}}>
|
||||||
<ResearchFull id={research.id} building={building}/>
|
<ResearchFull id={research.id} building={building}/>
|
||||||
</AccordionDetails>
|
</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) {
|
function researchLink(rs: IResearchShort) {
|
||||||
return <span><img style={{verticalAlign: "top", height: 25}}
|
return <span><img style={{verticalAlign: "top", height: 25}}
|
||||||
src={getIcon(rs.icon)}/>
|
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}
|
{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> :
|
rs.buildingId == null ? <span key={rs.id}></span> :
|
||||||
<span key={rs.id}>Research affect: {researchLink(rs)}<br/></span>
|
<span key={rs.id}>Research affect: {researchLink(rs)}<br/></span>
|
||||||
)}</div>
|
)}</div>
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import {
|
|||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
Grid2,
|
Grid2,
|
||||||
Paper,
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableRow
|
TableRow,
|
||||||
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {ExpandMore} from "@mui/icons-material";
|
import {ExpandMore} from "@mui/icons-material";
|
||||||
import {getIcon, ModifiersProvidesTable} from "../ModifiersProvideTable";
|
import {getIcon, ModifiersProvidesTable} from "../ModifiersProvideTable";
|
||||||
@@ -21,6 +21,8 @@ import {IBuilding} from "../../types/IBuilding";
|
|||||||
import {IResearchShort} from "../../types/IResearchShort";
|
import {IResearchShort} from "../../types/IResearchShort";
|
||||||
import {ResearchUrl, WeaponUrl} from "../../core/api";
|
import {ResearchUrl, WeaponUrl} from "../../core/api";
|
||||||
import {IWeapon} from "../../types/IUnit";
|
import {IWeapon} from "../../types/IUnit";
|
||||||
|
import {styled} from '@mui/material/styles';
|
||||||
|
import {StyledPaper} from "../../commons/StyledPaper";
|
||||||
|
|
||||||
|
|
||||||
interface IResearchFull {
|
interface IResearchFull {
|
||||||
@@ -54,14 +56,14 @@ function ResearchFull(props: { id: number, building: IBuilding }){
|
|||||||
|
|
||||||
<Grid2 container spacing={2}>
|
<Grid2 container spacing={2}>
|
||||||
<Grid2 size={{xs: 12, md: 4}}>
|
<Grid2 size={{xs: 12, md: 4}}>
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={StyledPaper} elevation={0}>
|
||||||
<Table size="small" aria-label="a dense table">
|
<Table size="small" aria-label="a dense table">
|
||||||
<TableBody id="unit-stats-table">
|
<TableBody id="unit-stats-table">
|
||||||
<TableRow
|
<TableRow
|
||||||
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||||
>
|
>
|
||||||
<TableCell component="th" scope="row">Cost</TableCell>
|
<TableCell component="th" scope="row" >Cost</TableCell>
|
||||||
<TableCell component="th" scope="row">
|
<TableCell>
|
||||||
{research.costRequisition > 0 &&
|
{research.costRequisition > 0 &&
|
||||||
<span> <img style={{verticalAlign: "top"}}
|
<span> <img style={{verticalAlign: "top"}}
|
||||||
src="/images/Resource_requisition.gif"/>
|
src="/images/Resource_requisition.gif"/>
|
||||||
@@ -93,10 +95,7 @@ function ResearchFull(props: { id: number, building: IBuilding }){
|
|||||||
</TableContainer><br/>
|
</TableContainer><br/>
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={{xs: 12, md: 8}}>
|
<Grid2 size={{xs: 12, md: 8}}>
|
||||||
<div style={{whiteSpace: "pre-wrap"}}>
|
<span style={{ whiteSpace: 'pre-wrap' }}>{research.description}</span>
|
||||||
{research.description}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</Grid2>
|
</Grid2>
|
||||||
<Grid2 size={{xs: 12, md: 12}}>
|
<Grid2 size={{xs: 12, md: 12}}>
|
||||||
<ModifiersProvidesTable modifiers={research.modifiers} affectedData={research.affectedData}
|
<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}/>
|
<Required requirement={research.requirements} modId={building.modId} raceId={building.race.id}/>
|
||||||
</Grid2>}
|
</Grid2>}
|
||||||
</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>
|
</div>
|
||||||
} else return <div>loading...</div>
|
} else return <div style={{color: 'rgba(255, 255, 255, 0.5)'}}>loading...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResearchFull;
|
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';
|
import React from 'react';
|
||||||
export const WeaponUrl = process.env.REACT_APP_HOST_URL + '/api/v1/weapon';
|
import { useTheme } from '@mui/material/styles';
|
||||||
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 UserUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/user';
|
||||||
export const AvailableMods = process.env.REACT_APP_HOST_URL + '/api/v1/mods';
|
export const WeaponUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/weapon';
|
||||||
export const AvailableRacesPart = process.env.REACT_APP_HOST_URL + '/api/v1/races';
|
export const AbilityUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/ability';
|
||||||
export const AvailableUnits = process.env.REACT_APP_HOST_URL + '/api/v1/units';
|
export const ResearchUrl = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/research';
|
||||||
export const AvailableBuildings = process.env.REACT_APP_HOST_URL + '/api/v1/buildings';
|
export const AvailableMods = process.env.NEXT_PUBLIC_HOST_URL + '/api/v1/mods';
|
||||||
export const IconUrl = process.env.REACT_APP_HOST_URL + '/api/v1/grapics/icon/';
|
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) {
|
export function withRouter(Children) {
|
||||||
return (props) => {
|
return (props) => {
|
||||||
|
|||||||
@@ -21,6 +21,11 @@
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jumpIcon{
|
||||||
|
padding: 0px;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.unitHeader{
|
.unitHeader{
|
||||||
margin-left: 15px;
|
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 {
|
body {
|
||||||
margin: 0;
|
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',
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-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 {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
monospace;
|
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
|
sightRadius: number
|
||||||
detectRadius: number
|
detectRadius: number
|
||||||
repairMax: number
|
repairMax: number
|
||||||
|
buildingLimit?: number
|
||||||
icon: string
|
icon: string
|
||||||
modId: number
|
modId: number
|
||||||
modifiers: IModifier[]
|
modifiers: IModifier[]
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ export interface IResearchShort {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
uiIndexHint: number;
|
||||||
buildingId: number;
|
buildingId: number;
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,22 @@ export interface IUnitResponse {
|
|||||||
units: IUnit[]
|
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 {
|
export interface IUnit {
|
||||||
id: number
|
id: number
|
||||||
race: Irace
|
race: Irace
|
||||||
@@ -43,6 +59,9 @@ export interface IUnit {
|
|||||||
mass: number
|
mass: number
|
||||||
upTime: number
|
upTime: number
|
||||||
moveSpeed: number
|
moveSpeed: number
|
||||||
|
accelerationTime?: number
|
||||||
|
decelerationTime?: number
|
||||||
|
rotationRate?: number
|
||||||
sightRadius: number
|
sightRadius: number
|
||||||
detectRadius: number
|
detectRadius: number
|
||||||
reinforceCostRequisition?: number
|
reinforceCostRequisition?: number
|
||||||
@@ -69,6 +88,7 @@ export interface IUnit {
|
|||||||
affectedData: IAffectedData,
|
affectedData: IAffectedData,
|
||||||
affectedResearches: IResearchShort[],
|
affectedResearches: IResearchShort[],
|
||||||
deathExplosions: IDeathExplosion[],
|
deathExplosions: IDeathExplosion[],
|
||||||
|
jumps?: IJumps | null,
|
||||||
hotkey?: string,
|
hotkey?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +145,18 @@ export interface IShortWeapon {
|
|||||||
isMeleeWeapon: boolean
|
isMeleeWeapon: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IResearchEffect {
|
||||||
|
field: string;
|
||||||
|
usageType: string;
|
||||||
|
modifierValue: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWeaponResearch {
|
||||||
|
research: IResearchShort;
|
||||||
|
researchRequired: IResearchShort[];
|
||||||
|
effects: IResearchEffect[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface IWeapon {
|
export interface IWeapon {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@@ -156,6 +188,7 @@ export interface IWeapon {
|
|||||||
modifiers: IModifier[]
|
modifiers: IModifier[]
|
||||||
weaponArmorPiercing: IPiercing[]
|
weaponArmorPiercing: IPiercing[]
|
||||||
affectedResearches: IResearchShort[],
|
affectedResearches: IResearchShort[],
|
||||||
|
researches: IWeaponResearch[],
|
||||||
hotkey?: string,
|
hotkey?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export interface IMod {
|
export interface IMod {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
order: number;
|
||||||
version: string;
|
version: string;
|
||||||
technicalName: string;
|
technicalName: string;
|
||||||
isBeta: boolean;
|
isBeta: boolean;
|
||||||
|
isHide: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,42 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
|
"target": "ES2017",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noEmit": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"jsx": "preserve",
|
||||||
"jsx": "react-jsx"
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||