Compare commits

...

17 Commits

Author SHA1 Message Date
morbalint b50a54f92b fix multiclass level up display 2024-02-14 11:34:11 +01:00
morbalint 9476c1f44c fix fegyverek
fix data
fix megterheltseg
2024-02-14 11:33:55 +01:00
morbalint 35b12524ab feat: multiclassing almost works in 2e 2024-02-13 23:57:50 +01:00
morbalint 2fe8fb05e7 upgrade GHA actions to node20 in ci.yml 2024-01-24 22:08:18 +01:00
morbalint 540c665a14 [be] optional open telemetry console logging
staging logs are full :(
2024-01-24 16:42:25 +01:00
morbalint 8f32daf337 [fe] [2e] add basic szintlepes 2024-01-24 16:38:16 +01:00
morbalint 978618d1b1 [be] open telemetry: only log to console when no other exporter exists 2024-01-23 13:31:22 +01:00
morbalint 65132fc7c6 add open telemetry and refactor setup 2024-01-23 13:10:16 +01:00
morbalint 8abe228d89 [fe] improve first time save UX 2024-01-22 12:51:40 +01:00
morbalint cc9a83607c use the official discord button on registration page as well
and disable the not yet approved external login providers on the registration page
2024-01-21 15:25:37 +01:00
morbalint 8092210ec8 [fe] fix unused import 2024-01-21 14:53:41 +01:00
morbalint fb8964062b use official discord button and disable other not yet approved external login providers 2024-01-21 14:51:17 +01:00
morbalint 55eeae5693 change save response
- no need for "pop up" access in browser
- more visibility (the notification is still ugly)
2024-01-21 14:09:01 +01:00
morbalint a1f694e30c frontend deps upgrade 2024-01-20 10:15:08 +01:00
morbalint fc5ce6d1ba fix save redirect url 2024-01-19 15:38:06 +01:00
morbalint dd86e8bd47 add CI status badge README.md 2024-01-19 12:01:33 +01:00
dependabot[bot] 0cd83000ab Bump follow-redirects from 1.15.3 to 1.15.4 in /Kemkas.Web/frontend (#1)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-19 11:28:03 +01:00
34 changed files with 1518 additions and 560 deletions
+4 -4
View File
@@ -11,14 +11,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Node.js 18.x - name: Setup Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
- name: Cache NPM packages - name: Cache NPM packages
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
key: npm-${{ hashFiles('./Kemkas.Web/frontend/yarn.lock') }} key: npm-${{ hashFiles('./Kemkas.Web/frontend/yarn.lock') }}
path: | path: |
@@ -40,7 +40,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
- name: Install doctl - name: Install doctl
uses: digitalocean/action-doctl@v2 uses: digitalocean/action-doctl@v2
@@ -75,7 +75,7 @@ jobs:
- docker-build - docker-build
if: ${{ github.event_name == 'push' || ( github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged ) }} if: ${{ github.event_name == 'push' || ( github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged ) }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install doctl - name: Install doctl
uses: digitalocean/action-doctl@v2 uses: digitalocean/action-doctl@v2
with: with:
@@ -65,7 +65,8 @@
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal"> <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div> <div>
<p> <p>
@foreach (var provider in Model.ExternalLogins!) <button type="submit" class="btn" name="provider" value="Discord" title="Log in using your Discord account"><img height="24px" alt="Discord" src="/img/discord-logo-blue.svg" /></button>
@foreach (var provider in Model.ExternalLogins!.Where(el => el.Name != "Discord"))
{ {
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
} }
@@ -22,11 +22,13 @@ namespace Kemkas.Web.Areas.Identity.Pages.Account
{ {
private readonly SignInManager<ApplicationUser> _signInManager; private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LoginModel> _logger; private readonly ILogger<LoginModel> _logger;
private readonly IWebHostEnvironment _environment;
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger) public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger, IWebHostEnvironment environment)
{ {
_signInManager = signInManager; _signInManager = signInManager;
_logger = logger; _logger = logger;
_environment = environment;
} }
/// <summary> /// <summary>
@@ -98,6 +100,12 @@ namespace Kemkas.Web.Areas.Identity.Pages.Account
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
// Disable not yet approved external login providers in production
if (!_environment.IsDevelopment())
{
ExternalLogins = ExternalLogins.Where(el => el.Name == "Discord").ToList();
}
ReturnUrl = returnUrl; ReturnUrl = returnUrl;
} }
@@ -107,6 +115,12 @@ namespace Kemkas.Web.Areas.Identity.Pages.Account
returnUrl ??= Url.Content("~/"); returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
// Disable not yet approved external login providers in production
if (!_environment.IsDevelopment())
{
ExternalLogins = ExternalLogins.Where(el => el.Name == "Discord").ToList();
}
if (ModelState.IsValid) if (ModelState.IsValid)
{ {
@@ -49,7 +49,8 @@
<form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal"> <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
<div> <div>
<p> <p>
@foreach (var provider in Model.ExternalLogins!) <button type="submit" class="btn" name="provider" value="Discord" title="Log in using your Discord account"><img height="24px" alt="Discord" src="/img/discord-logo-blue.svg" /></button>
@foreach (var provider in Model.ExternalLogins!.Where(el => el.Name != "Discord"))
{ {
<button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
} }
@@ -30,13 +30,15 @@ namespace Kemkas.Web.Areas.Identity.Pages.Account
private readonly IUserEmailStore<ApplicationUser> _emailStore; private readonly IUserEmailStore<ApplicationUser> _emailStore;
private readonly ILogger<RegisterModel> _logger; private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender; private readonly IEmailSender _emailSender;
private readonly IWebHostEnvironment _environment;
public RegisterModel( public RegisterModel(
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
IUserStore<ApplicationUser> userStore, IUserStore<ApplicationUser> userStore,
SignInManager<ApplicationUser> signInManager, SignInManager<ApplicationUser> signInManager,
ILogger<RegisterModel> logger, ILogger<RegisterModel> logger,
IEmailSender emailSender) IEmailSender emailSender,
IWebHostEnvironment environment)
{ {
_userManager = userManager; _userManager = userManager;
_userStore = userStore; _userStore = userStore;
@@ -44,6 +46,7 @@ namespace Kemkas.Web.Areas.Identity.Pages.Account
_signInManager = signInManager; _signInManager = signInManager;
_logger = logger; _logger = logger;
_emailSender = emailSender; _emailSender = emailSender;
_environment = environment;
} }
/// <summary> /// <summary>
@@ -105,12 +108,22 @@ namespace Kemkas.Web.Areas.Identity.Pages.Account
{ {
ReturnUrl = returnUrl; ReturnUrl = returnUrl;
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
// Disable not yet approved external login providers in production
if (!_environment.IsDevelopment())
{
ExternalLogins = ExternalLogins.Where(el => el.Name == "Discord").ToList();
}
} }
public async Task<IActionResult> OnPostAsync(string returnUrl = null) public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{ {
returnUrl ??= Url.Content("~/"); returnUrl ??= Url.Content("~/");
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
// Disable not yet approved external login providers in production
if (!_environment.IsDevelopment())
{
ExternalLogins = ExternalLogins.Where(el => el.Name == "Discord").ToList();
}
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
return Page(); return Page();
+10
View File
@@ -0,0 +1,10 @@
namespace Kemkas.Web.Config;
public class ExternalAuthOptions
{
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
public bool IsValid() => ClientId != null && ClientSecret != null;
}
@@ -0,0 +1,84 @@
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
namespace Kemkas.Web.Config;
public static class WebApplicationBuilderExtensions
{
public static void AddAuth(this WebApplicationBuilder builder)
{
var externalAuthConfigSection = builder.Configuration.GetSection("Authentication");
var authenticationBuilder = builder.Services.AddAuthentication();
var googleAuthOptions = new ExternalAuthOptions();
externalAuthConfigSection.GetSection("Google").Bind(googleAuthOptions);
if (googleAuthOptions.IsValid())
{
authenticationBuilder.AddGoogle(options =>
{
options.ClientId = googleAuthOptions.ClientId!;
options.ClientSecret = googleAuthOptions.ClientSecret!;
});
}
var facebookAuthOptions = new ExternalAuthOptions();
externalAuthConfigSection.GetSection("Facebook").Bind(facebookAuthOptions);
if (facebookAuthOptions.IsValid())
{
authenticationBuilder.AddFacebook(options =>
{
options.ClientId = facebookAuthOptions.ClientId!;
options.ClientSecret = facebookAuthOptions.ClientSecret!;
});
}
var discordAuthOptions = new ExternalAuthOptions();
externalAuthConfigSection.GetSection("Discord").Bind(discordAuthOptions);
if (discordAuthOptions.IsValid())
{
authenticationBuilder.AddDiscord(options =>
{
options.ClientId = discordAuthOptions.ClientId!;
options.ClientSecret = discordAuthOptions.ClientSecret!;
});
}
}
public static void AddOpenTelemetry(this WebApplicationBuilder builder)
{
var otlResourceBuilder = ResourceBuilder.CreateDefault().AddService("kemkas-api");
var openTelemetryEndpoint = builder.Configuration.GetSection("monitoring")["url"];
var consoleEnabled = builder.Configuration.GetSection("monitoring")["console"] == "true";
builder.Services.AddOpenTelemetry().WithTracing(tracerProviderBuilder =>
{
tracerProviderBuilder
.SetResourceBuilder(otlResourceBuilder)
.AddAspNetCoreInstrumentation();
if (openTelemetryEndpoint != null)
{
tracerProviderBuilder.AddOtlpExporter(opt => opt.Endpoint = new Uri(openTelemetryEndpoint));
}
else if (consoleEnabled)
{
tracerProviderBuilder.AddConsoleExporter();
}
})
.WithMetrics(meterProviderBuilder =>
{
meterProviderBuilder
.SetResourceBuilder(otlResourceBuilder)
.AddAspNetCoreInstrumentation();
if (openTelemetryEndpoint != null)
{
meterProviderBuilder.AddOtlpExporter(opt => opt.Endpoint = new Uri(openTelemetryEndpoint));
}
else if (consoleEnabled)
{
meterProviderBuilder.AddConsoleExporter();
}
});
}
}
+5
View File
@@ -18,6 +18,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.Discord" Version="8.0.0" /> <PackageReference Include="AspNet.Security.OAuth.Discord" Version="8.0.0" />
<PackageReference Include="Grpc.Core" Version="2.46.6" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection.EntityFrameworkCore" Version="8.0.0" />
@@ -36,6 +37,10 @@
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.7.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.7.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.7.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.7.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+10 -19
View File
@@ -11,6 +11,9 @@ using Kemkas.Web.Services.Identity;
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Identity.UI.Services;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -34,25 +37,7 @@ builder.Services.AddSingleton<IEmailSender, MailgunEmailSender>();
builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true) builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>(); .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddAuthentication() builder.AddAuth();
.AddGoogle(options =>
{
var section = builder.Configuration.GetSection("Authentication:Google");
options.ClientId = section["ClientId"];
options.ClientSecret = section["ClientSecret"];
})
.AddFacebook(options =>
{
var section = builder.Configuration.GetSection("Authentication:Facebook");
options.ClientId = section["ClientId"];
options.ClientSecret = section["ClientSecret"];
})
.AddDiscord(options =>
{
var section = builder.Configuration.GetSection("Authentication:Discord");
options.ClientId = section["ClientId"];
options.ClientSecret = section["ClientSecret"];
});
builder.Services.AddControllersWithViews().AddJsonOptions(options => builder.Services.AddControllersWithViews().AddJsonOptions(options =>
{ {
@@ -62,6 +47,10 @@ builder.Services.AddControllersWithViews().AddJsonOptions(options =>
}); });
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.AddOpenTelemetry();
builder.Services.AddHealthChecks();
builder.Services.AddCharacterServices(); builder.Services.AddCharacterServices();
var app = builder.Build(); var app = builder.Build();
@@ -93,6 +82,8 @@ else
app.UseHsts(); // TODO: check if this should be handled by proxy app.UseHsts(); // TODO: check if this should be handled by proxy
} }
app.UseHealthChecks("/health");
await using (var scope = app.Services.CreateAsyncScope()) await using (var scope = app.Services.CreateAsyncScope())
{ {
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
@@ -21,14 +21,15 @@ import {CalculateMasodlagosErtekek} from "../domain-models/masodlagos_ertekek";
import {KarakterInputToPdfView} from "../pdf/karakter_pdf_view"; import {KarakterInputToPdfView} from "../pdf/karakter_pdf_view";
import JellemSelector from "../components/JellemSelector"; import JellemSelector from "../components/JellemSelector";
import {CanLevelUp, LevelDown, LevelUp} from "../domain-models/level"; import {CanLevelUp, LevelDown, LevelUp} from "../domain-models/level";
import {Card, OverlayTrigger, Toast, ToastContainer} from "react-bootstrap"; import {Button, Card, InputGroup, Modal, OverlayTrigger, Toast, ToastContainer} from "react-bootstrap";
import Form from "react-bootstrap/Form";
import LevelUps from "../components/LevelUps"; import LevelUps from "../components/LevelUps";
import HarcosFegyverSpecializacio from "../components/HarcosFegyverSpecializacio"; import HarcosFegyverSpecializacio from "../components/HarcosFegyverSpecializacio";
import Felszereles from '../components/Felszereles'; import Felszereles from '../components/Felszereles';
import { KarakterFelszereles } from '../domain-models/felszereles'; import { KarakterFelszereles } from '../domain-models/felszereles';
import {Faro} from "@grafana/faro-web-sdk"; import {Faro} from "@grafana/faro-web-sdk";
import {StoreNewCharacter, UpdateCharacter} from "../api/character.api"; import {StoreNewCharacter, UpdateCharacter} from "../api/character.api";
import {useLoaderData, useParams} from "react-router-dom"; import {useLoaderData, useNavigate, useParams} from "react-router-dom";
import saveOverlayTooltip from "../components/SaveOverlayTooltip"; import saveOverlayTooltip from "../components/SaveOverlayTooltip";
import {UserContext} from "../../shared/contexts/UserContext"; import {UserContext} from "../../shared/contexts/UserContext";
import {KarakterTulajdonsagok} from "../domain-models/tulajdonsag"; import {KarakterTulajdonsagok} from "../domain-models/tulajdonsag";
@@ -39,7 +40,6 @@ function CreateCharacterPage(props: {
const {faro} = props const {faro} = props
const loaderData = useLoaderData() as KarakterInputs & { isPublic: boolean } | undefined; const loaderData = useLoaderData() as KarakterInputs & { isPublic: boolean } | undefined;
const initialIsPublic = loaderData?.isPublic ?? false;
let initialKarakterInputs = KarakterDefaults; let initialKarakterInputs = KarakterDefaults;
if (loaderData != null) { if (loaderData != null) {
const { isPublic: t1, ...t2 } = loaderData; const { isPublic: t1, ...t2 } = loaderData;
@@ -47,6 +47,10 @@ function CreateCharacterPage(props: {
} }
const { id } = useParams(); const { id } = useParams();
const fetchedUser = useContext(UserContext); const fetchedUser = useContext(UserContext);
const initialIsPublic = loaderData?.isPublic ?? fetchedUser?.data == null;
const navigate = useNavigate();
let [karakter, changeKarakter] = useState(initialKarakterInputs) let [karakter, changeKarakter] = useState(initialKarakterInputs)
@@ -64,9 +68,25 @@ function CreateCharacterPage(props: {
const levelDown = () => LevelDown(karakter, changeKarakter) const levelDown = () => LevelDown(karakter, changeKarakter)
const canLevelUp = CanLevelUp(karakter) const canLevelUp = CanLevelUp(karakter)
let [showSaveModal, setShowSaveModal] = useState(false);
let [newId, setNewId] = useState(null as string | null)
let [showSaved, setShowSaved] = useState(false); let [showSaved, setShowSaved] = useState(false);
let [isPublic, setIsPublic] = useState(initialIsPublic); let [isPublic, setIsPublic] = useState(initialIsPublic);
const newCharacterUrl = () => `${window.location.origin}/1e/karakter/${newId}`
const handleSaveModalCopyAndClose = async () => {
await navigator.clipboard.writeText(newCharacterUrl());
handleSaveModalClose()
}
const handleSaveModalClose = () => {
setShowSaveModal(false);
navigate(`/1e/karakter/${newId}`)
}
const hideSaved = () => setShowSaved(false); const hideSaved = () => setShowSaved(false);
const onSaveClicked = async () => { const onSaveClicked = async () => {
@@ -77,11 +97,9 @@ function CreateCharacterPage(props: {
is_public: isPublic.toString(), is_public: isPublic.toString(),
}) })
if (id == null) { if (id == null) {
let newId = await StoreNewCharacter(karakter, isPublic); let recievedId = await StoreNewCharacter(karakter, isPublic);
const win = window.open(`/${newId}`, '_blank'); setNewId(recievedId)
if (win != null) { setShowSaveModal(true);
win.focus();
}
} else { } else {
await UpdateCharacter(id, karakter, isPublic); await UpdateCharacter(id, karakter, isPublic);
setShowSaved(true); setShowSaved(true);
@@ -98,9 +116,31 @@ function CreateCharacterPage(props: {
</div> </div>
<ToastContainer className="position-fixed" position="top-end"> <ToastContainer className="position-fixed" position="top-end">
<Toast show={showSaved} onClose={hideSaved} bg="success"> <Toast show={showSaved} onClose={hideSaved} bg="success">
<Toast.Header><strong>Karakter mentve!</strong></Toast.Header> <Toast.Body className="text-light">Karakter mentve!</Toast.Body>
</Toast> </Toast>
</ToastContainer> </ToastContainer>
<Modal show={showSaveModal} onHide={handleSaveModalClose}>
<Modal.Header closeButton>
<Modal.Title>Karakter mentve!</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Karaktered az alábbi {isPublic ? 'publikus' : 'privát'} URLen érhető el.</p>
<p><a href={newCharacterUrl()}>{newCharacterUrl()}</a></p>
<InputGroup className="mb-3">
<Form.Control id="input" value={newCharacterUrl()} />
<Button variant={"outline-dark"} onClick={() => {
let copyText = document.querySelector("#input") as any;
copyText?.select()
document.execCommand("copy");
}}>Másolás</Button>
</InputGroup>
</Modal.Body>
<Modal.Footer>
<Button variant="danger" onClick={handleSaveModalCopyAndClose}>
Másolás és bezárás
</Button>
</Modal.Footer>
</Modal>
<div className='p-3'> <div className='p-3'>
<form onSubmit={async (event) => event.preventDefault()}> <form onSubmit={async (event) => event.preventDefault()}>
<div className='row'> <div className='row'>
@@ -0,0 +1,90 @@
import React from "react";
import {Karakter2E} from "../domain-models/karakter2E";
import {SignedNumberToText} from "../../shared/components/Helpers";
import {Mentok, MentoTipus, MentoTipusLabel, MentokTotal} from "../domain-models/mentok";
import {BaseHP, HP} from "../domain-models/masodlagos_ertekek";
import {MultiClassTamadasBonusz} from "../domain-models/tamadas_bonusz";
import {Osztaly2E} from "../domain-models/osztaly2E";
function MentoNovekedes(props : {tipus: MentoTipus, ujMentok: Mentok, regiMentok: Mentok}) {
const {tipus, ujMentok, regiMentok} = props
if (ujMentok[tipus] === regiMentok[tipus]) {
return null
}
return <div className='row m-2'>
<label className='col-sm-6 col-8 col-form-label'>{MentoTipusLabel(tipus)} mentő növekedés{' '}</label>
<span className='col col-form-label'>{SignedNumberToText(ujMentok[tipus] - regiMentok[tipus])}</span>
</div>;
}
function UjMento(props : {tipus: MentoTipus, ujMentok: Mentok, regiMentok: Mentok}) {
const {tipus, ujMentok, regiMentok} = props
if (ujMentok[tipus] === regiMentok[tipus]) {
return null
}
return <div className='row m-2'>
<label className='col-sm-6 col-8 col-form-label'>{MentoTipusLabel(tipus)} mentő új értéke{' '}</label>
<span className='col col-form-label'>{SignedNumberToText(ujMentok[tipus])}</span>
</div>;
}
function BasicNewLevel(props: {
karakter: Pick<Karakter2E, 'tulajdonsagok' | 'szintlepesek'>
szint: number
osztaly: Osztaly2E
changeRolledHP: (hp: number) => void,
}) {
const {karakter, szint, osztaly, changeRolledHP} = props
const regiMentok = MentokTotal({...karakter, szintlepesek: karakter.szintlepesek.slice(0, szint - 1)})
const ujMentok = MentokTotal({...karakter, szintlepesek: karakter.szintlepesek.slice(0, szint)})
const regiTB = MultiClassTamadasBonusz(karakter.szintlepesek.slice(0, szint-1))
const ujTB = MultiClassTamadasBonusz(karakter.szintlepesek.slice(0, szint))
if (regiTB.length < ujTB.length) {
regiTB.push(0)
}
const TBdiff = ujTB.map((x, i) => x - regiTB[i])
const isTBincreased = TBdiff.length > 0 && TBdiff.some(x => x > 0)
return <>
<div className='row'>
<div className='col-lg-6'>
<div className='row m-2'>
<label className='col-sm-6 col-8 col-form-label'>Plusz dobott HP (d{BaseHP(osztaly)}){' '}</label>
<div className='col-sm-2 col'>
<input
className='form-control'
maxLength={2}
type='number'
value={karakter.szintlepesek[szint-1].HProll || 1}
min={1}
max={12}
onChange={e => changeRolledHP(Number(e.target.value))} />
</div>
</div>
{ isTBincreased && <div className='row m-2'>
<label className='col-sm-6 col-8 col-form-label'>Támadás bónusz növekedés{' '}</label>
<span className='col col-form-label'>{TBdiff.map(SignedNumberToText).join(" / ")}</span>
</div>}
<MentoNovekedes tipus={'kitartas'} ujMentok={ujMentok} regiMentok={regiMentok} />
<MentoNovekedes tipus={'reflex'} ujMentok={ujMentok} regiMentok={regiMentok} />
<MentoNovekedes tipus={'akaratero'} ujMentok={ujMentok} regiMentok={regiMentok} />
</div>
<div className='col-lg-6'>
<div className='row m-2'>
<label className='col-sm-6 col-8 col-form-label'>Új HP{' '}</label>
<span className='col col-form-label'>{HP({...karakter, szintlepesek: karakter.szintlepesek.slice(0, szint)})}</span>
</div>
{isTBincreased && <div className='row m-2'>
<label className='col-sm-6 col-8 col-form-label'>Új Támadás bónusz{' '}</label>
<span className='col col-form-label'>{ujTB.map(SignedNumberToText).join(" / ")}</span>
</div>}
<UjMento tipus={'kitartas'} ujMentok={ujMentok} regiMentok={regiMentok} />
<UjMento tipus={'reflex'} ujMentok={ujMentok} regiMentok={regiMentok} />
<UjMento tipus={'akaratero'} ujMentok={ujMentok} regiMentok={regiMentok} />
</div>
</div>
</>
}
export default BasicNewLevel
@@ -0,0 +1,89 @@
import React from "react";
import {Fegyver} from "../domain-models/felszereles";
export function FegyverLabel(fegyver: Fegyver): string {
const flags = [] as string[]
if (fegyver.Megterheltseg) {
flags.push("M")
}
if (fegyver.Harcos) {
flags.push("H")
}
if (fegyver.Masfelkezes) {
flags.push("1½K")
}
if (fegyver.Ketkezes) {
if (fegyver.Type === "kozelharci") {
flags.push("2K")
} else {
if (fegyver.KetkezesBonusz) {
flags.push("2KB")
} else {
flags.push("2KN")
}
}
}
if (fegyver.HarciManover) {
flags.push("+2 HM")
}
if (fegyver.Alakzat){
flags.push("AL")
}
if (fegyver.Hosszu) {
flags.push("HO")
}
if (fegyver.Kabito) {
flags.push("KO")
}
if (fegyver.LovasRoham) {
flags.push("LR")
}
if (fegyver.PancelToro) {
flags.push("PT")
}
if (fegyver.PajzsZuzo) {
flags.push("PZ")
}
if (fegyver.Rohamtoro) {
flags.push("RT")
}
// Replaced by new group "dobhato"
// if (fegyver.EroBonusz) {
// flags.push("ER")
// }
if (fegyver.Megjegyzes != null && fegyver.Megjegyzes.length > 0) {
flags.push(fegyver.Megjegyzes)
}
const serializedFlags = flags.reduce((p,c) => `${p}${c} `, '');
const sizeLabel = fegyver.Size < 1 && fegyver.Size > 0 ? ` [1/${Math.round(1 / fegyver.Size)}]` : (fegyver.Size > 1 ? ` [${fegyver.Size}]` : '');
return `${fegyver.Name}${sizeLabel} ${serializedFlags.length > 0 ? "| " + serializedFlags : ''}| ${
fegyver.DamageMultiplier > 1 ? `${fegyver.DamageMultiplier}*` : ''}${fegyver.NumberOfDamageDice}d${fegyver.DamageDice}${fegyver.DamageBonus > 0 ? ` +${fegyver.DamageBonus}` : ''} ${
fegyver.CritRangeStart < 20 ? `${fegyver.CritRangeStart}-20` : ''}x${fegyver.CritMultiplier}${
fegyver.Price > 0 ? ` | ár: ${fegyver.Price} at ` : ''}${
fegyver.Range > 10 ? ` | táv: ${fegyver.Range}` : ''}`
}
function FegyverSelector(props: {fegyverek: Fegyver[], selectedId: string, onChange: (id: string) => void}) {
const { fegyverek, selectedId, onChange } = props
return <>
<select className='col form-select' value={selectedId}
onChange={e => onChange(e.target.value)}>
<optgroup label="Közelharci">
{fegyverek.filter(f => f.Type === 'kozelharci').map(f => <option
key={f.Id} value={f.Id}>{FegyverLabel(f)}</option>)}
</optgroup>
<optgroup label="Közelharci, dobható">
{fegyverek.filter(f => f.Type === 'dobhato').map(f => <option
key={f.Id} value={f.Id}>{FegyverLabel(f)}</option>)}
</optgroup>
<optgroup label="Távolsági">
{fegyverek.filter(f => f.Type === 'lofegyver').map(f => <option
key={f.Id} value={f.Id}>{FegyverLabel(f)}</option>)}
</optgroup>
</select>
</>
}
export default FegyverSelector
@@ -5,64 +5,7 @@ import {AllowedPajzsTypes, AllowedPancelTypes} from '../domain-models/allowed-pa
import fegyverek from '../domain-models/fegyver.json' import fegyverek from '../domain-models/fegyver.json'
import {arraySetN} from "../../util"; import {arraySetN} from "../../util";
import {AllowedFegyver} from "../domain-models/allowed-fegyver"; import {AllowedFegyver} from "../domain-models/allowed-fegyver";
import FegyverSelector from "./FegyverSelector";
export function FegyverLabel(fegyver: Fegyver): string {
const flags = [] as string[]
if (fegyver.Harcos) {
flags.push("H")
}
if (fegyver.Masfelkezes) {
flags.push("1½K")
}
if (fegyver.Ketkezes) {
if (fegyver.Type === "kozelharci") {
flags.push("2K")
} else {
if (fegyver.KetkezesBonusz) {
flags.push("2KB")
} else {
flags.push("2KN")
}
}
}
if (fegyver.HarciManover) {
flags.push("+2 HM")
}
if (fegyver.Alakzat){
flags.push("AL")
}
if (fegyver.Hosszu) {
flags.push("HO")
}
if (fegyver.Kabito) {
flags.push("KO")
}
if (fegyver.LovasRoham) {
flags.push("LR")
}
if (fegyver.PancelToro) {
flags.push("PT")
}
if (fegyver.PajzsZuzo) {
flags.push("PZ")
}
if (fegyver.Rohamtoro) {
flags.push("RT")
}
// if (fegyver.EroBonusz) {
// flags.push("ER")
// }
if (fegyver.Megjegyzes != null && fegyver.Megjegyzes.length > 0) {
flags.push(fegyver.Megjegyzes)
}
const serializedFlags = flags.reduce((p,c) => `${p}${c} `, '');
const sizeLabel = fegyver.Size < 1 && fegyver.Size > 0 ? ` [1/${Math.round(1 / fegyver.Size)}]` : (fegyver.Size > 1 ? ` [${fegyver.Size}]` : '');
return `${fegyver.Name}${sizeLabel} ${serializedFlags.length > 0 ? "| " + serializedFlags : ''}| ${
fegyver.DamageMultiplier > 1 ? `${fegyver.DamageMultiplier}*` : ''}${fegyver.NumberOfDamageDice}d${fegyver.DamageDice}${fegyver.DamageBonus > 0 ? ` +${fegyver.DamageBonus}` : ''} ${
fegyver.CritRangeStart < 20 ? `${fegyver.CritRangeStart}-20` : ''}x${fegyver.CritMultiplier}${
fegyver.Price > 0 ? ` | ár: ${fegyver.Price} at ` : ''}${
fegyver.Range > 10 ? ` | táv: ${fegyver.Range}` : ''}`
}
function Felszereles(props: {felszereles: KarakterFelszereles, changeFelszereles: (felszereles: KarakterFelszereles) => void, osztaly: Osztaly2E}) { function Felszereles(props: {felszereles: KarakterFelszereles, changeFelszereles: (felszereles: KarakterFelszereles) => void, osztaly: Osztaly2E}) {
const { felszereles, changeFelszereles, osztaly } = props; const { felszereles, changeFelszereles, osztaly } = props;
@@ -116,21 +59,7 @@ function Felszereles(props: {felszereles: KarakterFelszereles, changeFelszereles
{valasztottFegyverek.map(({idx, fegyver}) => ( {valasztottFegyverek.map(({idx, fegyver}) => (
<div key={`fegyver-${idx}`} className='row m-2'> <div key={`fegyver-${idx}`} className='row m-2'>
<label className='col-md-2 col-sm-3 col-form-label'>Fegyver</label> <label className='col-md-2 col-sm-3 col-form-label'>Fegyver</label>
<select className='col form-select' value={fegyver.Id} <FegyverSelector fegyverek={allowedFegyverek} selectedId={fegyver.Id} onChange={(fegyverId) => changeFegyver(idx, fegyverId)} />
onChange={e => changeFegyver(idx, e.target.value)}>
<optgroup label="Közelharci">
{allowedFegyverek.filter(f => f.Type === 'kozelharci').map(f => <option
key={f.Id} value={f.Id}>{FegyverLabel(f)}</option>)}
</optgroup>
<optgroup label="Közelharci, dobható">
{allowedFegyverek.filter(f => f.Type === 'dobhato').map(f => <option
key={f.Id} value={f.Id}>{FegyverLabel(f)}</option>)}
</optgroup>
<optgroup label="Távolsági">
{allowedFegyverek.filter(f => f.Type === 'lofegyver').map(f => <option
key={f.Id} value={f.Id}>{FegyverLabel(f)}</option>)}
</optgroup>
</select>
<button className='col-md-2 col-sm-3 btn btn-outline-dark btn-lg ms-2' type='button' <button className='col-md-2 col-sm-3 btn btn-outline-dark btn-lg ms-2' type='button'
onClick={() => changeFelszereles({ onClick={() => changeFelszereles({
...felszereles, ...felszereles,
@@ -0,0 +1,32 @@
import React from "react";
import {GetFegyver} from "../domain-models/felszereles";
import FegyverSelector from "./FegyverSelector";
import {AllowedFegyver} from "../domain-models/allowed-fegyver";
import {Osztaly2E} from "../domain-models/osztaly2E";
function HarcosFegyverSpecializacio(props: {
specialization: string,
existingSpecializations: (string | undefined)[],
changeSpecialization: (specialization: string) => void
szint: number
}) {
const {
specialization,
existingSpecializations,
changeSpecialization,
szint
} = props
const fegyver = GetFegyver(specialization)
const availableFegyverek = AllowedFegyver(Osztaly2E.Harcos)
const pickableFegyverek = availableFegyverek.filter(f => f.Id === specialization || existingSpecializations.filter(x => x === f.Id).length < (szint < 9 ? 1 : 2))
return <>
<div className='row m-2'>
<label className='col-md-2 col-sm-3 col-form-label' >Fegyver specializáció</label>
<FegyverSelector fegyverek={pickableFegyverek} selectedId={specialization} onChange={changeSpecialization} />
{fegyver == null && <span className='form-field-error'>Ismeretlen fegyver: {specialization}</span>}
</div>
</>
}
export default HarcosFegyverSpecializacio
@@ -0,0 +1,28 @@
import React from "react";
import {Osztaly2E} from "../domain-models/osztaly2E";
import {AllowedFegyver} from "../domain-models/allowed-fegyver";
import FegyverSelector from "./FegyverSelector";
function KalozKritikus(props: {
kritFegyverId: string,
existingKrits: (string | undefined)[],
changeKrit: (krit: string) => void,
szint: number
}) {
const {
kritFegyverId,
existingKrits,
changeKrit,
szint
} = props
const fegyverek = AllowedFegyver(Osztaly2E.Tengeresz).filter(f => f.Id === kritFegyverId || existingKrits.filter(x => x === f.Id).length < (szint < 9 ? 1 : 2))
return <>
<div className='row m-2'>
<label className='col-md-2 col-sm-3 col-form-label'>Kritikus</label>
<FegyverSelector fegyverek={fegyverek} selectedId={kritFegyverId} onChange={changeKrit} />
</div>
</>
}
export default KalozKritikus
@@ -0,0 +1,147 @@
import React from "react";
import {ChangeOsztalyAtSzint, Karakter2E} from "../domain-models/karakter2E";
import BasicNewLevel from "./BasicNewLevel";
import TulajdonsagNoveles from "./TulajdonsagNoveles";
import {Tulajdonsag2E, TulajdonsagokTotal} from "../domain-models/tulajdonsag2E";
import {arraySetN} from "../../util";
import {Osztaly2E} from "../domain-models/osztaly2E";
import KalozKritikus from "./KalozKritikus";
import HarcosFegyverSpecializacio from "./HarcosFegyverSpec";
import MultiClassOsztalySelector2E from "./MultiClassOsztalySelector2E";
import {getClassLevels} from "../domain-models/szintlepes";
function LevelUp(props: {
szint: number,
osztaly: Osztaly2E,
karakter: Karakter2E,
changeTulajdonsagNoveles: (t: Tulajdonsag2E) => void,
changeRolledHP: (rolledHP: number) => void,
changeHarcosSpec: (spec: string) => void,
changeKalozKritikus: (krit: string) => void,
changeOsztaly: (osztaly: Osztaly2E) => void
}) {
const {
szint,
osztaly,
karakter,
changeTulajdonsagNoveles,
changeRolledHP,
changeHarcosSpec,
changeKalozKritikus,
changeOsztaly
} = props
const classLevels = getClassLevels(karakter.szintlepesek)
const harcosSzint = classLevels[Osztaly2E.Harcos]
const kalozSzint = classLevels[Osztaly2E.Tengeresz]
const specialization = karakter.szintlepesek[szint-1].harcosFegyver
const krit = karakter.szintlepesek[szint-1].kalozKritikus
return <div>
<div className='row mt-3'>
<h5 className='col align-self-center'>{szint}. Szint</h5>
</div>
{osztaly !== Osztaly2E.Druida &&
<MultiClassOsztalySelector2E
faj={karakter.faj}
osztaly={osztaly}
changeOsztaly={changeOsztaly}
osztalySzint={classLevels[osztaly]}
/> }
{szint % 4 === 0 &&
<TulajdonsagNoveles
tulajdonsagok={karakter.tulajdonsagok}
tulajdonsagNoveles={karakter.szintlepesek[szint-1].tulajdonsagNoveles!}
changeTulajdonsagNoveles={changeTulajdonsagNoveles}
/> }
<BasicNewLevel
szint={szint}
karakter={karakter}
osztaly={osztaly}
changeRolledHP={changeRolledHP}
/>
{osztaly === Osztaly2E.Harcos && harcosSzint % 2 === 1 &&
<HarcosFegyverSpecializacio
specialization={specialization!}
existingSpecializations={karakter.szintlepesek.map(x => x.harcosFegyver)}
changeSpecialization={changeHarcosSpec}
szint={harcosSzint}
/>
}
{osztaly === Osztaly2E.Tengeresz && kalozSzint % 3 === 0 &&
<KalozKritikus
kritFegyverId={krit!}
existingKrits={karakter.szintlepesek.map(x => x.kalozKritikus)}
changeKrit={changeKalozKritikus}
szint={kalozSzint}
/>
}
</div>
}
export function LevelUps(props: {karakter: Karakter2E, changeKarakter: (inputs: Karakter2E) => void}){
const {karakter, changeKarakter} = props;
const tulajdonsagokAtSzint = (szint: number) => TulajdonsagokTotal({...karakter, szintlepesek: karakter.szintlepesek.slice(0, szint)})
const changeRolledHPAtSzint = (szint: number) => (newHProll: number) => {
changeKarakter({...karakter, szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {...karakter.szintlepesek[szint-1], HProll: newHProll})})
console.log(karakter)
}
const changeTulajdonsagNovelesAtSzint = (szint: number) => {
return (t: Tulajdonsag2E) => {
changeKarakter({...karakter,
szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {
...karakter.szintlepesek[szint-1],
tulajdonsagNoveles: t
})
})
};
}
const changeHarcosSpecializacioAtSzint = (szint: number) => {
return (spec: string) => {
changeKarakter({
...karakter,
szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {
...karakter.szintlepesek[szint-1],
harcosFegyver: spec
})
})
};
}
const changeKalozKritikusAtSzint = (szint: number) => {
return (krit: string) => {
changeKarakter({
...karakter,
szintlepesek: arraySetN(karakter.szintlepesek, szint-1, {
...karakter.szintlepesek[szint-1],
kalozKritikus: krit
})
})
};
}
const levels = karakter.szintlepesek.map((_, i) => i+1);
return <>
{levels.slice(1).map(szint =>
<LevelUp
key={`level-up-${szint}`}
szint={szint}
osztaly={karakter.szintlepesek[szint-1].osztaly}
karakter={{...karakter, szintlepesek: karakter.szintlepesek.slice(0, szint) ,tulajdonsagok: tulajdonsagokAtSzint(szint)}}
changeTulajdonsagNoveles={changeTulajdonsagNovelesAtSzint(szint)}
changeRolledHP={changeRolledHPAtSzint(szint)}
changeHarcosSpec={changeHarcosSpecializacioAtSzint(szint)}
changeKalozKritikus={changeKalozKritikusAtSzint(szint)}
changeOsztaly={(o) => changeKarakter(ChangeOsztalyAtSzint(karakter, o, szint))}
/>
)}
</>
}
export default LevelUps
@@ -0,0 +1,54 @@
import {Faj2E, SzintKorlatokFajokra} from "../domain-models/faj2E";
import {Osztaly2E, OsztalyLabel} from "../domain-models/osztaly2E";
import React from "react";
function MultiClassOsztalySelector2E(props: {
faj: Faj2E,
osztaly: Osztaly2E,
changeOsztaly: (o: Osztaly2E) => void
osztalySzint: number
}) {
const {faj, osztaly, changeOsztaly, osztalySzint} = props
return <>
<div className='row'>
<div className="col-lg-6">
<div className="row m-md-2">
<label className='col-8 col-sm-6 col-form-label'>Osztály</label>
<div className="col">
<select className="col form-select" value={osztaly}
onChange={(e) => changeOsztaly(e.target.value as Osztaly2E)}>
<optgroup label='Harcos'>
<option value={Osztaly2E.Harcos}>{OsztalyLabel(Osztaly2E.Harcos)}</option>
{SzintKorlatokFajokra(faj)[Osztaly2E.Amazon] > 0 && (
<option value={Osztaly2E.Amazon}>{OsztalyLabel(Osztaly2E.Amazon)}</option>)}
<option value={Osztaly2E.Barbar}>{OsztalyLabel(Osztaly2E.Barbar)}</option>
<option value={Osztaly2E.Ijasz}>{OsztalyLabel(Osztaly2E.Ijasz)}</option>
<option value={Osztaly2E.Tengeresz}>{OsztalyLabel(Osztaly2E.Tengeresz)}</option>
</optgroup>
<option value={Osztaly2E.Pap}>{OsztalyLabel(Osztaly2E.Pap)}</option>
<option value={Osztaly2E.Tolvaj}>{OsztalyLabel(Osztaly2E.Tolvaj)}</option>
<optgroup label='Varázsló'>
<option value={Osztaly2E.Varazslo}>{OsztalyLabel(Osztaly2E.Varazslo)}</option>
<option value={Osztaly2E.Illuzionista}>{OsztalyLabel(Osztaly2E.Illuzionista)}</option>
</optgroup>
<optgroup label='Opcionális'>
<option value={Osztaly2E.Dalnok}>{OsztalyLabel(Osztaly2E.Dalnok)}</option>
{/* Druida nem lehet tobb osztalyu karakter! */}
<option value={Osztaly2E.Vandor}>{OsztalyLabel(Osztaly2E.Vandor)}</option>
</optgroup>
</select>
</div>
</div>
</div>
<div className="col-lg-6">
<div className="row m-2">
<label className='col-sm-6 col-8 col-form-label'>Új {OsztalyLabel(osztaly)} szint{' '}</label>
<span className='col col-form-label'>{osztalySzint}</span>
</div>
</div>
</div>
</>
}
export default MultiClassOsztalySelector2E
@@ -9,8 +9,8 @@ import {
import {Faj2E, SzintKorlatokFajokra} from "../domain-models/faj2E"; import {Faj2E, SzintKorlatokFajokra} from "../domain-models/faj2E";
function OsztalySelector2E(props: { function OsztalySelector2E(props: {
currentFaj: Faj2E, currentFaj: Faj2E,
currentOsztaly: Osztaly2E, currentOsztaly: Osztaly2E,
changeOsztaly: (newOsztaly: Osztaly2E) => void changeOsztaly: (newOsztaly: Osztaly2E) => void
}) { }) {
const {currentFaj, currentOsztaly, changeOsztaly} = props; const {currentFaj, currentOsztaly, changeOsztaly} = props;
@@ -0,0 +1,37 @@
import React from "react";
import {
KarakterTulajdonsagok,
Modifier,
Tulajdonsag2E,
TulajdonsagIDs,
TulajdonsagLabel
} from "../domain-models/tulajdonsag2E";
import {SignedNumberToText} from "../../shared/components/Helpers";
function TulajdonsagNoveles(props: {tulajdonsagok: KarakterTulajdonsagok, tulajdonsagNoveles: Tulajdonsag2E, changeTulajdonsagNoveles: (tulajdonsag: Tulajdonsag2E) => void}) {
const {tulajdonsagok, tulajdonsagNoveles, changeTulajdonsagNoveles} = props
return <>
<div className='row'>
<div className='col-lg-6'>
<div className='row m-2'>
<label className='col-sm-6 col-8 col-form-label'>Tulajdonság növelés{' '}</label>
<div className='col'>
<select className='form-select' value={tulajdonsagNoveles} onChange={(e) => changeTulajdonsagNoveles(e.target.value as Tulajdonsag2E)}>
{TulajdonsagIDs.filter(t => t === tulajdonsagNoveles || tulajdonsagok[t] < 18).map(t => (
<option value={t} key={t}>{TulajdonsagLabel(t)}</option>
))}
</select>
</div>
</div>
</div>
<div className='col-lg-6'>
<div className='row m-2'>
<label className='col-sm-6 col-8 col-form-label'>Új {TulajdonsagLabel(tulajdonsagNoveles)}{' '}</label>
<span className='col col-form-label'>{tulajdonsagok[tulajdonsagNoveles]} ({SignedNumberToText(Modifier(tulajdonsagok[tulajdonsagNoveles]))})</span>
</div>
</div>
</div>
</>
}
export default TulajdonsagNoveles
@@ -487,7 +487,7 @@
"Harcos": true, "Harcos": true,
"CsakNemHarcos": false, "CsakNemHarcos": false,
"Masfelkezes": false, "Masfelkezes": false,
"Ketkezes": false, "Ketkezes": true,
"KetkezesBonusz": false, "KetkezesBonusz": false,
"HarciManover": false, "HarciManover": false,
"Alakzat": false, "Alakzat": false,
@@ -4,6 +4,10 @@ import {Osztaly2E} from "./osztaly2E";
import {KepzettsegId} from "./kepzettsegek2E"; import {KepzettsegId} from "./kepzettsegek2E";
import {JellemID} from "./jellem"; import {JellemID} from "./jellem";
import {DefaultFelszereles, KarakterFelszereles} from "./felszereles"; import {DefaultFelszereles, KarakterFelszereles} from "./felszereles";
import {Szintlepes} from "./szintlepes";
import {AllowedFegyver} from "./allowed-fegyver";
import {BaseHP} from "./masodlagos_ertekek";
import {dAny} from "../../shared/domain-models/kockak";
export interface Karakter2E { export interface Karakter2E {
name: string, name: string,
@@ -13,14 +17,11 @@ export interface Karakter2E {
jellem: JellemID, jellem: JellemID,
tulajdonsagok: KarakterTulajdonsagok tulajdonsagok: KarakterTulajdonsagok
faj: Faj2E faj: Faj2E
osztaly: Osztaly2E
harcosSpecializaciok: string[],
kalozKritikus: string[],
kepzettsegek: KepzettsegId[] kepzettsegek: KepzettsegId[]
tolvajKepzettsegek?: KepzettsegId[] tolvajKepzettsegek?: KepzettsegId[]
szint: number, szint: number,
hpRolls: number[]
felszereles: KarakterFelszereles, felszereles: KarakterFelszereles,
szintlepesek: Szintlepes[]
} }
export const DefaultKarakter: Karakter2E = { export const DefaultKarakter: Karakter2E = {
@@ -29,12 +30,103 @@ export const DefaultKarakter: Karakter2E = {
jellem: 'S', jellem: 'S',
tulajdonsagok: TulajdonsagDefaults, tulajdonsagok: TulajdonsagDefaults,
faj: Faj2E.Ember, faj: Faj2E.Ember,
osztaly: Osztaly2E.Tengeresz,
harcosSpecializaciok: [],
kalozKritikus: [],
kepzettsegek: [], kepzettsegek: [],
tolvajKepzettsegek: [], tolvajKepzettsegek: [],
szint: 1, szint: 1,
hpRolls: [],
felszereles: DefaultFelszereles, felszereles: DefaultFelszereles,
} szintlepesek: [{
osztaly: Osztaly2E.Tengeresz,
HProll: 10,
}]
}
function setOsztalyForSzintLepes(szintlepesek: Szintlepes[], osztaly: Osztaly2E, szint: number): Szintlepes[] {
const prev = szintlepesek.slice(0, szint-1)
const current : Szintlepes = {
...szintlepesek[szint-1],
osztaly,
harcosFegyver: undefined,
kalozKritikus: undefined
}
const tail = szintlepesek.slice(szint).map(x => {return {...x}})
let harcosSzint = prev.filter(x => x.osztaly === Osztaly2E.Harcos).length
// TODO: only pick specializations from valid levels
const harcosSpecializaciok = szintlepesek
.map(x => x.harcosFegyver)
.filter(x => x != null)
const harcosSpecAtSzint = (hSzint: number): (string | undefined) => {
if (hSzint % 2 === 1) {
const specIndex = (hSzint+1)/2 -1
if (harcosSpecializaciok.length > specIndex ) {
return harcosSpecializaciok[specIndex]
} else {
return AllowedFegyver(Osztaly2E.Harcos)[specIndex].Id
}
}
else {
return undefined
}
}
if (osztaly === Osztaly2E.Harcos) {
harcosSzint += 1
current.harcosFegyver = harcosSpecAtSzint(harcosSzint)
}
for (let i = 0; i < tail.length; i++) {
if (tail[i].osztaly === Osztaly2E.Harcos) {
harcosSzint++
tail[i].harcosFegyver = harcosSpecAtSzint(harcosSzint)
}
}
let kalozSzint = prev.filter(x => x.osztaly === Osztaly2E.Tengeresz).length
const existingKalozKrits = szintlepesek
.map(x => x.kalozKritikus)
.filter(x => x != null)
const kalozKritAtSzint = (kSzint: number): (string | undefined) => {
if (kSzint % 3 === 0) {
const specIndex = kSzint/3 -1
if (existingKalozKrits.length > specIndex ) {
return existingKalozKrits[specIndex]
} else {
return AllowedFegyver(Osztaly2E.Tengeresz)[specIndex].Id
}
}
else {
return undefined
}
}
if (osztaly === Osztaly2E.Tengeresz) {
kalozSzint++
current.kalozKritikus = kalozKritAtSzint(szint)
}
for (let i = 0; i < tail.length; i++) {
if (tail[i].osztaly === Osztaly2E.Tengeresz) {
kalozSzint++
tail[i].kalozKritikus = kalozKritAtSzint(kalozSzint)
}
}
const HD = BaseHP(osztaly)
if (szint === 1) {
current.HProll = HD
} else {
current.HProll = dAny(HD)
console.log(`Level ${szint} HP roll on d${HD} is: ${current.HProll} due to class change`)
}
return [...prev, current, ...tail]
}
export function ChangeLvl1Osztaly(karakter: Karakter2E, osztaly: Osztaly2E): Karakter2E {
return {
...karakter,
szintlepesek: [{...karakter.szintlepesek[0], osztaly}, ...karakter.szintlepesek.slice(1)]
}
}
export function ChangeOsztalyAtSzint(karakter: Karakter2E, osztaly: Osztaly2E, szint: number): Karakter2E {
return {
...karakter,
szintlepesek: setOsztalyForSzintLepes(karakter.szintlepesek, osztaly, szint)
}
}
@@ -209,12 +209,12 @@ export const Kepzettsegek: Record<KepzettsegId, Kepzettseg> =
(acc: Record<KepzettsegId, Kepzettseg>, k) => { acc[k.Id] = k; return acc; }, (acc: Record<KepzettsegId, Kepzettseg>, k) => { acc[k.Id] = k; return acc; },
{} as Record<KepzettsegId, Kepzettseg>); {} as Record<KepzettsegId, Kepzettseg>);
export function AvailableKezpettsegList(osztaly: Osztaly2E): Kepzettseg[] { export function AvailableKezpettsegList(...osztalyok: Osztaly2E[]): Kepzettseg[] {
return KepzettsegLista.filter(k => k.Osztalyok == null || k.Osztalyok.includes(osztaly)) return KepzettsegLista.filter(k => k.Osztalyok == null || k.Osztalyok.some(o => osztalyok.includes(o)))
} }
export function GetNumberOfKepzettsegek(t_int: number, faj: Faj2E, max: number = 11) { export function GetNumberOfKepzettsegek(t_int: number, faj: Faj2E, max: number = 11) {
//max = 3 + 1 + 3 + 4 // 3 base, 1 human, 3 from max ability modifier, 4 from Thief //max = 3 + 1 + 3 + 5 = 12 // 3 base, 1 human, 3 from max ability modifier, 5 from Thief at 9th level
let numberOfKepzettseg = 3 + Modifier(t_int) + (faj === Faj2E.Ember ? 1 : 0) let numberOfKepzettseg = 3 + Modifier(t_int) + (faj === Faj2E.Ember ? 1 : 0)
if (numberOfKepzettseg < 1) { if (numberOfKepzettseg < 1) {
@@ -228,18 +228,20 @@ export function GetNumberOfKepzettsegek(t_int: number, faj: Faj2E, max: number =
return numberOfKepzettseg; return numberOfKepzettseg;
} }
export function SetDefaultTolvajKepzettsegek(karakter: Pick<Karakter2E, 'osztaly' | 'tolvajKepzettsegek'>, changeTolvajKepzettsegek: (tolvajKepzettsegek?: KepzettsegId[]) => void) { export function SetDefaultTolvajKepzettsegek(karakter: Pick<Karakter2E, 'szintlepesek' | 'tolvajKepzettsegek'>, changeTolvajKepzettsegek: (tolvajKepzettsegek?: KepzettsegId[]) => void) {
if (karakter.osztaly !== Osztaly2E.Tolvaj && karakter.tolvajKepzettsegek != null) { const hasTolvajSzint = karakter.szintlepesek.some(szint => szint.osztaly === Osztaly2E.Tolvaj)
if (!hasTolvajSzint && karakter.tolvajKepzettsegek != null) {
changeTolvajKepzettsegek(undefined) changeTolvajKepzettsegek(undefined)
} }
if (karakter.osztaly === Osztaly2E.Tolvaj && (karakter.tolvajKepzettsegek == null || karakter.tolvajKepzettsegek.length === 0)) { if (hasTolvajSzint && (karakter.tolvajKepzettsegek == null || karakter.tolvajKepzettsegek.length === 0)) {
const tolvajKepzettsegek = TolvajKepzettsegList.slice(0, 4).map(x => x.Id) const tolvajKepzettsegek = TolvajKepzettsegList.slice(0, 4).map(x => x.Id)
changeTolvajKepzettsegek(tolvajKepzettsegek) changeTolvajKepzettsegek(tolvajKepzettsegek)
} }
} }
export function GetKepzettsegListaN(karakter: Pick<Karakter2E, 'osztaly' | 'faj' | 'kepzettsegek' | 'tolvajKepzettsegek'>) { export function GetKepzettsegListaN(karakter: Pick<Karakter2E, 'szintlepesek' | 'faj' | 'kepzettsegek' | 'tolvajKepzettsegek'>) {
const availableKepzettsegList = AvailableKezpettsegList(karakter.osztaly) const osztalyok = new Set(karakter.szintlepesek.map(x => x.osztaly))
const availableKepzettsegList = AvailableKezpettsegList(...osztalyok)
return (n: number): Kepzettseg[] => { return (n: number): Kepzettseg[] => {
let available = [...availableKepzettsegList] let available = [...availableKepzettsegList]
if (n === 0) { if (n === 0) {
@@ -249,7 +251,8 @@ export function GetKepzettsegListaN(karakter: Pick<Karakter2E, 'osztaly' | 'faj'
if (karakter.faj === Faj2E.Nomad) { if (karakter.faj === Faj2E.Nomad) {
return [Kepzettsegek.k_lovaglas] return [Kepzettsegek.k_lovaglas]
} }
if (karakter.faj === Faj2E.Birodalmi && karakter.osztaly !== Osztaly2E.Tolvaj) { // TODO birodalmi tolvaj, tolvajképzettségek!
if (karakter.faj === Faj2E.Birodalmi) {
return [Kepzettsegek.k_alkimia, Kepzettsegek.k_meregkeveres, Kepzettsegek.k_okkultizmus] return [Kepzettsegek.k_alkimia, Kepzettsegek.k_meregkeveres, Kepzettsegek.k_okkultizmus]
} }
if (karakter.faj === Faj2E.Torpe) { if (karakter.faj === Faj2E.Torpe) {
@@ -258,30 +261,38 @@ export function GetKepzettsegListaN(karakter: Pick<Karakter2E, 'osztaly' | 'faj'
if (karakter.faj === Faj2E.Elf || karakter.faj === Faj2E.Felelf) { if (karakter.faj === Faj2E.Elf || karakter.faj === Faj2E.Felelf) {
return [Kepzettsegek.k_vadonjaras] return [Kepzettsegek.k_vadonjaras]
} }
if (karakter.osztaly === Osztaly2E.Druida || karakter.osztaly === Osztaly2E.Vandor) { if (osztalyok.has(Osztaly2E.Druida) || osztalyok.has(Osztaly2E.Vandor)) {
return [Kepzettsegek.k_vadonjaras] return [Kepzettsegek.k_vadonjaras]
} }
if (karakter.osztaly === Osztaly2E.Dalnok) { if (osztalyok.has(Osztaly2E.Dalnok)) {
return [Kepzettsegek.k_eloadas] return [Kepzettsegek.k_eloadas]
} }
} }
if (n === 1) { if (n === 1) {
if (karakter.osztaly === Osztaly2E.Dalnok if ((osztalyok.has(Osztaly2E.Vandor) || osztalyok.has(Osztaly2E.Druida))
&& [Faj2E.Eszaki, Faj2E.Nomad, Faj2E.Birodalmi, Faj2E.Torpe, Faj2E.Elf, Faj2E.Felelf].includes(karakter.faj)) { && karakter.kepzettsegek[0] !== Kepzettsegek.k_vadonjaras.Id) {
return [Kepzettsegek.k_eloadas]
}
if ([Osztaly2E.Druida, Osztaly2E.Vandor].includes(karakter.osztaly)
&& [Faj2E.Eszaki, Faj2E.Nomad, Faj2E.Birodalmi, Faj2E.Torpe].includes(karakter.faj)) {
return [Kepzettsegek.k_vadonjaras] return [Kepzettsegek.k_vadonjaras]
} }
if (osztalyok.has(Osztaly2E.Dalnok)
&& karakter.kepzettsegek[0] !== Kepzettsegek.k_eloadas.Id) {
return [Kepzettsegek.k_eloadas]
}
}
if (n === 2) { // maybe write a better abstraction?
if (osztalyok.has(Osztaly2E.Dalnok)
&& karakter.kepzettsegek[0] !== Kepzettsegek.k_eloadas.Id
&& karakter.kepzettsegek[1] !== Kepzettsegek.k_eloadas.Id) {
return [Kepzettsegek.k_eloadas]
}
} }
const kepzettsegekWithoutN = [...karakter.kepzettsegek.slice(0, n), ...karakter.kepzettsegek.slice(n + 1)] const kepzettsegekWithoutN = [...karakter.kepzettsegek.slice(0, n), ...karakter.kepzettsegek.slice(n + 1)]
return available.filter(x => !kepzettsegekWithoutN.includes(x.Id) && !karakter.tolvajKepzettsegek?.includes(x.Id)) return available.filter(x => !kepzettsegekWithoutN.includes(x.Id) && !karakter.tolvajKepzettsegek?.includes(x.Id))
} }
} }
export function SetDefaultKepzettsegek(karakter: Pick<Karakter2E, 'osztaly' | 'faj' | 'tulajdonsagok' | 'kepzettsegek' | 'tolvajKepzettsegek'>, changeKepzettsegek: (k: KepzettsegId[]) => void) { export function SetDefaultKepzettsegek(karakter: Pick<Karakter2E, 'szintlepesek' | 'faj' | 'tulajdonsagok' | 'kepzettsegek' | 'tolvajKepzettsegek'>, changeKepzettsegek: (k: KepzettsegId[]) => void) {
const availableKepzettsegList = AvailableKezpettsegList(karakter.osztaly) const osztalyok = karakter.szintlepesek.map(x => x.osztaly)
const availableKepzettsegList = AvailableKezpettsegList(...osztalyok)
const numberOfKepzettsegek = GetNumberOfKepzettsegek(karakter.tulajdonsagok.t_int, karakter.faj, availableKepzettsegList.length) const numberOfKepzettsegek = GetNumberOfKepzettsegek(karakter.tulajdonsagok.t_int, karakter.faj, availableKepzettsegList.length)
let kepzettsegek = [...karakter.kepzettsegek] let kepzettsegek = [...karakter.kepzettsegek]
@@ -14,7 +14,7 @@ export interface MasodlagosErtekekView {
Mentok: Mentok Mentok: Mentok
} }
export type KarakterpickForMasodlagosErtekek = Pick<Karakter2E, 'osztaly' | 'hpRolls' | 'tulajdonsagok' | 'szint' | 'faj' | 'felszereles'> export type KarakterpickForMasodlagosErtekek = Pick<Karakter2E, 'tulajdonsagok' | 'szint' | 'szintlepesek' | 'faj' | 'felszereles'>
export function BaseHP(osztaly: Osztaly2E) { export function BaseHP(osztaly: Osztaly2E) {
let base = 4; let base = 4;
@@ -45,15 +45,16 @@ export function BaseHP(osztaly: Osztaly2E) {
return base return base
} }
export function HP(karakter: Pick<Karakter2E, 'osztaly' | 'tulajdonsagok' | 'hpRolls'>): number { export function HP(karakter: Pick<Karakter2E, 'tulajdonsagok' | 'szintlepesek'>): number {
const egeszsegModifier = Modifier(karakter.tulajdonsagok.t_egs) const egeszsegModifier = Modifier(karakter.tulajdonsagok.t_egs)
return BaseHP(karakter.osztaly) + egeszsegModifier + karakter.hpRolls return karakter.szintlepesek
// https://lfg.hu/forum/topic/15079-kard-es-magia/page/219/#comment-2218333 // https://lfg.hu/forum/topic/15079-kard-es-magia/page/219/#comment-2218333
.map(hp => Math.max(1, hp + egeszsegModifier)) .map(szint => Math.max(1, szint.HProll + egeszsegModifier))
.reduce((sum, val) => sum + val, 0) .reduce((sum, val) => sum + val, 0)
} }
export function VO(karakter: Pick<Karakter2E, 'tulajdonsagok' | 'faj' | 'osztaly' | 'szint' | 'felszereles'>): number { export function VO(karakter: Pick<Karakter2E, 'tulajdonsagok' | 'faj' | 'szintlepesek' | 'szint' | 'felszereles'>): number {
const osztalyok = new Set(karakter.szintlepesek.map(x => x.osztaly))
const pancel = GetPancel(karakter.felszereles.pancelID) const pancel = GetPancel(karakter.felszereles.pancelID)
const pancelVO = pancel?.VO ?? 0 const pancelVO = pancel?.VO ?? 0
const pajzsVO = GetPajzs(karakter.felszereles.pajzsID)?.VO ?? 0 const pajzsVO = GetPajzs(karakter.felszereles.pajzsID)?.VO ?? 0
@@ -62,14 +63,16 @@ export function VO(karakter: Pick<Karakter2E, 'tulajdonsagok' | 'faj' | 'osztaly
if (pancel) { if (pancel) {
abilityVO = Math.min(MaxAbilityVO(pancel.Type), abilityVO) abilityVO = Math.min(MaxAbilityVO(pancel.Type), abilityVO)
} }
if (karakter.osztaly === Osztaly2E.Tengeresz) { if (osztalyok.has(Osztaly2E.Tengeresz)) {
vo += Math.floor(karakter.szint / 3) const kalozSzintek = karakter.szintlepesek.filter(x => x.osztaly === Osztaly2E.Tengeresz).length
vo += Math.floor(kalozSzintek / 3)
} }
if (karakter.osztaly === Osztaly2E.Amazon) { if (osztalyok.has(Osztaly2E.Amazon)) {
if (karakter.tulajdonsagok.t_kar > karakter.tulajdonsagok.t_ugy) { if (karakter.tulajdonsagok.t_kar > karakter.tulajdonsagok.t_ugy) {
abilityVO = Modifier(karakter.tulajdonsagok.t_kar) abilityVO = Modifier(karakter.tulajdonsagok.t_kar)
} }
vo += Math.floor(karakter.szint / 2) const amazonSzintek = karakter.szintlepesek.filter(x => x.osztaly === Osztaly2E.Amazon).length
vo += Math.floor(amazonSzintek / 2)
} }
if (karakter.faj === Faj2E.Felszerzet || karakter.faj === Faj2E.Gnom){ if (karakter.faj === Faj2E.Felszerzet || karakter.faj === Faj2E.Gnom){
vo += 1 vo += 1
@@ -1,6 +1,7 @@
import {Osztaly2E} from "./osztaly2E"; import {Osztaly2E} from "./osztaly2E";
import {Karakter2E} from "./karakter2E"; import {Karakter2E} from "./karakter2E";
import {KarakterTulajdonsagok, TulajdonsagModosito} from "./tulajdonsag2E"; import {KarakterTulajdonsagok, TulajdonsagModosito} from "./tulajdonsag2E";
import {Szintlepes} from "./szintlepes";
export interface Mentok { export interface Mentok {
kitartas: number kitartas: number
@@ -73,8 +74,26 @@ export function MentoModositok(tulajdonsagModositok: KarakterTulajdonsagok) {
} }
} }
export function MentokTotal(karakter: Pick<Karakter2E, 'osztaly' | 'szint' | 'tulajdonsagok'>) { export function MultiClassMentokAlap(szintlepesek: Szintlepes[]) : Mentok {
const alap = MentokAlap(karakter.osztaly, karakter.szint) const osztalyok = new Set(szintlepesek.map(x => x.osztaly))
let mentok = {
kitartas: 0,
reflex: 0,
akaratero: 0
}
for (const osztaly of osztalyok) {
const szint = szintlepesek.filter(x => x.osztaly === osztaly).length
const uj_mentok = MentokAlap(osztaly, szint)
mentok.akaratero += uj_mentok.akaratero
mentok.reflex += uj_mentok.reflex
mentok.kitartas += uj_mentok.kitartas
}
return mentok
}
export function MentokTotal(karakter: Pick<Karakter2E, 'szintlepesek' | 'tulajdonsagok'>) {
const alap = MultiClassMentokAlap(karakter.szintlepesek)
const modositok = MentoModositok(TulajdonsagModosito(karakter.tulajdonsagok)) const modositok = MentoModositok(TulajdonsagModosito(karakter.tulajdonsagok))
return { return {
kitartas: alap.kitartas + modositok.kitartas, kitartas: alap.kitartas + modositok.kitartas,
@@ -0,0 +1,99 @@
import {Tulajdonsag2E, TulajdonsagIDs, TulajdonsagokTotal} from "./tulajdonsag2E";
import {KepzettsegId} from "./kepzettsegek2E";
import {Osztaly2E} from "./osztaly2E";
import {Karakter2E} from "./karakter2E";
import {BaseHP} from "./masodlagos_ertekek";
import {dAny} from "../../shared/domain-models/kockak";
import {AllowedFegyver} from "./allowed-fegyver";
export interface Szintlepes {
osztaly: Osztaly2E;
HProll: number;
tolvajExtraKepzettseg?: KepzettsegId; // only at level 5 and 9
harcosFegyver?: string; // every 3rd level
kalozKritikus?: string; // every odd level
tulajdonsagNoveles?: Tulajdonsag2E;
}
export function PickableSpecializationFilter(existingSpecilizations: (string | undefined)[], szint: number) {
return (fegyver: {Id: string}) => existingSpecilizations.filter(x => x === fegyver.Id).length < (szint < 9 ? 1 : 2)
}
export function LevelUp(karakter: Karakter2E, changeKarakter: (input: Karakter2E) => void) {
const osztaly = karakter.szintlepesek[0].osztaly
const tulajdonsagokTotal = TulajdonsagokTotal(karakter)
const szint = karakter.szint + 1
const dice = BaseHP(osztaly)
const roll = dAny(dice)
console.log(`Level ${szint} HP roll on d${dice} is: ${roll}`)
const szintlepes: Szintlepes = {
osztaly: osztaly,
HProll: roll
}
if (szint % 4 === 0) {
for (const tulajdonsag of TulajdonsagIDs) {
if (tulajdonsagokTotal[tulajdonsag] < 18) {
szintlepes.tulajdonsagNoveles = tulajdonsag;
break;
}
}
}
if(osztaly === Osztaly2E.Harcos) {
const harcosSzint = karakter.szintlepesek.filter(x => x.osztaly === Osztaly2E.Harcos).length + 1
if (harcosSzint % 2 === 1) {
const harcosSpecializaciok = karakter.szintlepesek.map(x => x.harcosFegyver)
let specializationFilter = PickableSpecializationFilter(harcosSpecializaciok, harcosSzint)
szintlepes.harcosFegyver = AllowedFegyver(Osztaly2E.Harcos).filter(specializationFilter)[0].Id
}
}
if(osztaly === Osztaly2E.Tengeresz) {
const kalozSzint = karakter.szintlepesek.filter(x => x.osztaly === Osztaly2E.Tengeresz).length + 1
if (kalozSzint % 3 === 0) {
let kalozKritikus = karakter.szintlepesek.map(x => x.kalozKritikus)
let specializationFilter = PickableSpecializationFilter(kalozKritikus, kalozSzint)
szintlepes.kalozKritikus = AllowedFegyver(Osztaly2E.Tengeresz).filter(specializationFilter)[0].Id
}
}
changeKarakter({...karakter, szint, szintlepesek: [...karakter.szintlepesek, szintlepes]})
}
export function LevelDown(karakter: Karakter2E, changeKarakter: (input: Karakter2E) => void) {
if (karakter.szintlepesek.length > 1) {
changeKarakter({...karakter, szint: karakter.szint - 1, szintlepesek: karakter.szintlepesek.slice(0, -1)})
}
else {
console.error(`Can't level down character: ${karakter}`)
}
}
export type ClassLevels = {
[key in Osztaly2E]: number;
};
const defaultClassLevels: ClassLevels = {
[Osztaly2E.Amazon]: 0,
[Osztaly2E.Barbar]: 0,
[Osztaly2E.Ijasz]: 0,
[Osztaly2E.Harcos]: 0,
[Osztaly2E.Tengeresz]: 0,
[Osztaly2E.Tolvaj]: 0,
[Osztaly2E.Pap]: 0,
[Osztaly2E.Varazslo]: 0,
[Osztaly2E.Illuzionista]: 0,
[Osztaly2E.Dalnok]: 0,
[Osztaly2E.Druida]: 0,
[Osztaly2E.Vandor]: 0,
}
export function getClassLevels(szintlepesek: Szintlepes[]) : ClassLevels {
return szintlepesek.map(x => x.osztaly).reduce((classLevels, osztaly) => {
classLevels[osztaly]++
return classLevels
}, {...defaultClassLevels})
}
@@ -1,6 +1,7 @@
import {Osztaly2E} from "./osztaly2E"; import {Osztaly2E} from "./osztaly2E";
import {Karakter2E} from "./karakter2E"; import {Karakter2E} from "./karakter2E";
import {Modifier} from "./tulajdonsag2E"; import {Modifier} from "./tulajdonsag2E";
import {Szintlepes} from "./szintlepes";
/// Finally second edition doesn't mess with the extra attack of high level clerics rouges and wizards! /// Finally second edition doesn't mess with the extra attack of high level clerics rouges and wizards!
function ExtraAttack(attack: number): number[] { function ExtraAttack(attack: number): number[] {
@@ -10,6 +11,31 @@ function ExtraAttack(attack: number): number[] {
return [Math.min(attack, 10), ...ExtraAttack(attack - 5)] return [Math.min(attack, 10), ...ExtraAttack(attack - 5)]
} }
export function TamadasBonuszBase(osztaly: Osztaly2E, szint: number = 1) : number {
let base = 0
switch (osztaly){
case Osztaly2E.Barbar:
case Osztaly2E.Amazon:
case Osztaly2E.Ijasz:
case Osztaly2E.Tengeresz:
case Osztaly2E.Harcos:
case Osztaly2E.Vandor:
base = szint;
break;
case Osztaly2E.Pap:
case Osztaly2E.Tolvaj:
case Osztaly2E.Dalnok:
case Osztaly2E.Druida:
base = Math.floor(szint * 2 / 3);
break;
case Osztaly2E.Varazslo:
case Osztaly2E.Illuzionista:
base = Math.floor(szint / 2);
break;
}
return base
}
export function TamadasBonusz(osztaly: Osztaly2E, szint: number = 1) : number[] { export function TamadasBonusz(osztaly: Osztaly2E, szint: number = 1) : number[] {
let base = 0 let base = 0
switch (osztaly){ switch (osztaly){
@@ -35,12 +61,23 @@ export function TamadasBonusz(osztaly: Osztaly2E, szint: number = 1) : number[]
return ExtraAttack(base) return ExtraAttack(base)
} }
export function KozelharciTB(karakter : Pick<Karakter2E, 'szint' | 'tulajdonsagok' | 'osztaly'>): number[] {
const base = TamadasBonusz(karakter.osztaly, karakter.szint); export function MultiClassTamadasBonusz(szintlepesek: Szintlepes[]) : number[]{
const osztalyok = new Set(szintlepesek.map(x => x.osztaly))
let TBB = 0
for (const osztaly of osztalyok) {
const szint = szintlepesek.filter(x => x.osztaly === osztaly).length
TBB += TamadasBonuszBase(osztaly, szint)
}
return ExtraAttack(TBB)
}
export function KozelharciTB(karakter : Pick<Karakter2E, 'szintlepesek' | 'tulajdonsagok'>): number[] {
const base = MultiClassTamadasBonusz(karakter.szintlepesek);
let modifier = Modifier(karakter.tulajdonsagok.t_ero) let modifier = Modifier(karakter.tulajdonsagok.t_ero)
return base.map(v => v + modifier) return base.map(v => v + modifier)
} }
export function CelzoTB(karakter : Pick<Karakter2E, 'szint' | 'tulajdonsagok' | 'osztaly'>): number[] { export function CelzoTB(karakter : Pick<Karakter2E, 'szintlepesek' | 'tulajdonsagok'>): number[] {
let base = TamadasBonusz(karakter.osztaly, karakter.szint) let base = MultiClassTamadasBonusz(karakter.szintlepesek)
return base.map(v => v + Modifier(karakter.tulajdonsagok.t_ugy)) return base.map(v => v + Modifier(karakter.tulajdonsagok.t_ugy))
} }
@@ -1,4 +1,6 @@
import {d6} from "../../shared/domain-models/kockak"; import {d6} from "../../shared/domain-models/kockak";
import {Karakter2E} from "./karakter2E";
import {TulajdonsagokFajjal} from "./faj2E";
export enum Tulajdonsag2E { export enum Tulajdonsag2E {
Ero = 't_ero', Ero = 't_ero',
@@ -73,4 +75,16 @@ export function TulajdonsagModosito(tulajdonsagok: KarakterTulajdonsagok): Karak
response[key] = Modifier(tulajdonsagok[key]) response[key] = Modifier(tulajdonsagok[key])
} }
return response return response
}
export function TulajdonsagokTotal(karakter: Pick<Karakter2E, 'tulajdonsagok' | 'faj' | 'szintlepesek'>): KarakterTulajdonsagok {
const level1 = TulajdonsagokFajjal(karakter.tulajdonsagok, karakter.faj)
return {
t_ero: level1.t_ero + karakter.szintlepesek.filter(s => s.tulajdonsagNoveles === 't_ero').length,
t_ugy: level1.t_ugy + karakter.szintlepesek.filter(s => s.tulajdonsagNoveles === 't_ugy').length,
t_egs: level1.t_egs + karakter.szintlepesek.filter(s => s.tulajdonsagNoveles === 't_egs').length,
t_int: level1.t_int + karakter.szintlepesek.filter(s => s.tulajdonsagNoveles === 't_int').length,
t_bol: level1.t_bol + karakter.szintlepesek.filter(s => s.tulajdonsagNoveles === 't_bol').length,
t_kar: level1.t_kar + karakter.szintlepesek.filter(s => s.tulajdonsagNoveles === 't_kar').length,
}
} }
@@ -1,7 +1,7 @@
import React, {useState} from 'react' import React, {useState} from 'react'
import {useParams} from "react-router-dom"; import {useParams} from "react-router-dom";
import {Toast, ToastContainer} from "react-bootstrap"; import {Toast, ToastContainer} from "react-bootstrap";
import {DefaultKarakter} from "../domain-models/karakter2E"; import {ChangeLvl1Osztaly, DefaultKarakter} from "../domain-models/karakter2E";
import {KarakterTulajdonsagok} from "../domain-models/tulajdonsag2E"; import {KarakterTulajdonsagok} from "../domain-models/tulajdonsag2E";
import FajSelector2E from "../components/FajSelector2E"; import FajSelector2E from "../components/FajSelector2E";
import Tulajdonsagok2E from "../components/Tulajdonsagok2E"; import Tulajdonsagok2E from "../components/Tulajdonsagok2E";
@@ -20,6 +20,10 @@ import MasodlagosErtekek from "../components/MasodlagosErtekek";
import {CalculateMasodlagosErtekek} from "../domain-models/masodlagos_ertekek"; import {CalculateMasodlagosErtekek} from "../domain-models/masodlagos_ertekek";
import Felszereles from "../components/Felszereles"; import Felszereles from "../components/Felszereles";
import {KarakterFelszereles} from "../domain-models/felszereles"; import {KarakterFelszereles} from "../domain-models/felszereles";
import LevelUps from "../components/LevelUps";
import {LevelDown, LevelUp} from "../domain-models/szintlepes";
import HarcosFegyverSpecializacio from "../components/HarcosFegyverSpec";
import {arraySetN} from "../../util";
function CreateCharacter2E(props: {}) { function CreateCharacter2E(props: {}) {
@@ -99,13 +103,16 @@ function CreateCharacter2E(props: {}) {
</div> </div>
<OsztalySelector2E <OsztalySelector2E
currentFaj={karakter.faj} currentFaj={karakter.faj}
currentOsztaly={karakter.osztaly} currentOsztaly={karakter.szintlepesek[0].osztaly}
changeOsztaly={osztaly => setKarakter({...karakter, osztaly})} changeOsztaly={o => setKarakter(ChangeLvl1Osztaly(karakter, o))}
/> />
{karakter.osztaly === Osztaly2E.Harcos && {karakter.szintlepesek[0].osztaly === Osztaly2E.Harcos &&
<div className='mb-6'> <HarcosFegyverSpecializacio
TODO: Harcos fegyver specializaciok!! specialization={karakter.szintlepesek[0].harcosFegyver || 'kard_hosszu'}
</div>} szint={1}
changeSpecialization={(spec) => setKarakter({...karakter, szintlepesek: arraySetN(karakter.szintlepesek, 0, {...karakter.szintlepesek[0], harcosFegyver: spec})})}
existingSpecializations={karakter.szintlepesek.map(x => x.harcosFegyver)}
/>}
<KarakterKepzettsegek <KarakterKepzettsegek
getKepzettsegListaN={GetKepzettsegListaN(karakter)} getKepzettsegListaN={GetKepzettsegListaN(karakter)}
numberOfKepzettsegek={GetNumberOfKepzettsegek(tulajdonsagokFajjal.t_int, karakter.faj)} numberOfKepzettsegek={GetNumberOfKepzettsegek(tulajdonsagokFajjal.t_int, karakter.faj)}
@@ -120,10 +127,21 @@ function CreateCharacter2E(props: {}) {
...karakter, ...karakter,
tulajdonsagok: tulajdonsagokFajjal, tulajdonsagok: tulajdonsagokFajjal,
szint: 1, szint: 1,
hpRolls: [] szintlepesek: karakter.szintlepesek.slice(0,1)
})} /> })} />
<hr /> <hr />
<Felszereles osztaly={karakter.osztaly} felszereles={karakter.felszereles} <LevelUps karakter={karakter} changeKarakter={setKarakter}/>
{karakter.szint > 1 && <hr/>}
<div className='d-grid gap-2 m-5'>
{karakter.szint < 12 &&
<button className='btn btn-dark btn-lg' type='button' onClick={() => LevelUp(karakter, setKarakter)}>Szintlépés!
</button>}
{karakter.szint > 1 &&
<button className='btn btn-dark btn-lg' type='button' onClick={() => LevelDown(karakter, setKarakter)}>Visszalépés!
</button>}
</div>
<hr/>
<Felszereles osztaly={karakter.szintlepesek[0].osztaly} felszereles={karakter.felszereles}
changeFelszereles={changeFelszereles}/> changeFelszereles={changeFelszereles}/>
<hr/> <hr/>
File diff suppressed because it is too large Load Diff
+4
View File
@@ -91,5 +91,9 @@ http {
proxy_pass https://backend; proxy_pass https://backend;
proxy_pass_request_headers on; proxy_pass_request_headers on;
} }
location /img/ {
proxy_pass https://backend;
proxy_pass_request_headers on;
}
} }
} }
+158
View File
@@ -8,6 +8,16 @@
"resolved": "8.0.0", "resolved": "8.0.0",
"contentHash": "+MtOHPctq1QT1c03h+7g4oBWtZ/buQYUJg4AR0DG+qjmD2cQRMGWXC1J6/ITeCc6xa5BcXzJmt1L/8pVXIB4kw==" "contentHash": "+MtOHPctq1QT1c03h+7g4oBWtZ/buQYUJg4AR0DG+qjmD2cQRMGWXC1J6/ITeCc6xa5BcXzJmt1L/8pVXIB4kw=="
}, },
"Grpc.Core": {
"type": "Direct",
"requested": "[2.46.6, )",
"resolved": "2.46.6",
"contentHash": "ZoRg3KmOJ2urTF4+u3H0b1Yv10xzz2Y/flFWS2tnRmj8dbKLeiJaSRqu4LOBD3ova90evqLkVZ85kUkC4JT4lw==",
"dependencies": {
"Grpc.Core.Api": "2.46.6",
"System.Memory": "4.5.3"
}
},
"Microsoft.AspNetCore.Authentication.Facebook": { "Microsoft.AspNetCore.Authentication.Facebook": {
"type": "Direct", "type": "Direct",
"requested": "[8.0.0, )", "requested": "[8.0.0, )",
@@ -124,6 +134,78 @@
"Npgsql.EntityFrameworkCore.PostgreSQL": "1.1.0" "Npgsql.EntityFrameworkCore.PostgreSQL": "1.1.0"
} }
}, },
"OpenTelemetry.Exporter.Console": {
"type": "Direct",
"requested": "[1.7.0, )",
"resolved": "1.7.0",
"contentHash": "9mcwpArzG7+5cLnG6marWdVD1TyoVR03WAjShvxADqgsEeuzLMClTlgXi41bTCAgxjYyiHV5l60fJwXTekiJdQ==",
"dependencies": {
"OpenTelemetry": "1.7.0",
"System.Text.Encodings.Web": "4.7.2",
"System.Text.Json": "4.7.2"
}
},
"OpenTelemetry.Exporter.OpenTelemetryProtocol": {
"type": "Direct",
"requested": "[1.7.0, )",
"resolved": "1.7.0",
"contentHash": "ULGTxg4HdnJD+CIuiD15Uov4xVVqwpbNGFd075J3MT3lLM9XcwU4YRz7CCNmqIlehYaiZrGhUurXrI2LgZZL1Q==",
"dependencies": {
"Google.Protobuf": "[3.22.5, 4.0.0)",
"Grpc.Net.Client": "[2.52.0, 3.0.0)",
"OpenTelemetry": "1.7.0"
}
},
"OpenTelemetry.Extensions.Hosting": {
"type": "Direct",
"requested": "[1.7.0, )",
"resolved": "1.7.0",
"contentHash": "MbB7CWWqb7xHK0jTF9Gtvw/eLWdaKqzkE1XAwLe05xyskHuwJWAbZFax4nGLA71YkMWQNO5iPIBlirvYXOLMlg==",
"dependencies": {
"Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "8.0.0",
"OpenTelemetry": "1.7.0"
}
},
"OpenTelemetry.Instrumentation.AspNetCore": {
"type": "Direct",
"requested": "[1.7.0, )",
"resolved": "1.7.0",
"contentHash": "K/mjCmuR9zgrL6fMC6sbnX/y6BOJJFI3hp42O+tz8nT/w6sxbfjrLcU+fVwfct3lsnohSKdPfrq6fSrxAF5ezw==",
"dependencies": {
"OpenTelemetry.Api.ProviderBuilderExtensions": "1.7.0"
}
},
"Google.Protobuf": {
"type": "Transitive",
"resolved": "3.22.5",
"contentHash": "tTMtDZPbLxJew8pk7NBdqhLqC4OipfkZdwPuCEUNr2AoDo1siUGcxFqJK0wDewTL8ge5Cjrb16CToMPxBUHMGA=="
},
"Grpc.Core.Api": {
"type": "Transitive",
"resolved": "2.52.0",
"contentHash": "SQiPyBczG4vKPmI6Fd+O58GcxxDSFr6nfRAJuBDUNj+PgdokhjWJvZE/La1c09AkL2FVm/jrDloG89nkzmVF7A==",
"dependencies": {
"System.Memory": "4.5.3"
}
},
"Grpc.Net.Client": {
"type": "Transitive",
"resolved": "2.52.0",
"contentHash": "hWVH9g/Nnjz40ni//2S8UIOyEmhueQREoZIkD0zKHEPqLxXcNlbp4eebXIOicZtkwDSx0TFz9NpkbecEDn6rBw==",
"dependencies": {
"Grpc.Net.Common": "2.52.0",
"Microsoft.Extensions.Logging.Abstractions": "3.0.3"
}
},
"Grpc.Net.Common": {
"type": "Transitive",
"resolved": "2.52.0",
"contentHash": "di9qzpdx525IxumZdYmu6sG2y/gXJyYeZ1ruFUzB9BJ1nj4kU1/dTAioNCMt1VLRvNVDqh8S8B1oBdKhHJ4xRg==",
"dependencies": {
"Grpc.Core.Api": "2.52.0"
}
},
"Humanizer.Core": { "Humanizer.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "2.14.1", "resolved": "2.14.1",
@@ -265,6 +347,15 @@
"Microsoft.Extensions.Primitives": "8.0.0" "Microsoft.Extensions.Primitives": "8.0.0"
} }
}, },
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Configuration.Abstractions": { "Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "8.0.0",
@@ -273,6 +364,14 @@
"Microsoft.Extensions.Primitives": "8.0.0" "Microsoft.Extensions.Primitives": "8.0.0"
} }
}, },
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
}
},
"Microsoft.Extensions.DependencyInjection": { "Microsoft.Extensions.DependencyInjection": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "8.0.0",
@@ -371,6 +470,21 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0"
} }
}, },
"Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "8.0.0",
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Logging": "8.0.0",
"Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0"
}
},
"Microsoft.Extensions.Options": { "Microsoft.Extensions.Options": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "8.0.0",
@@ -380,6 +494,18 @@
"Microsoft.Extensions.Primitives": "8.0.0" "Microsoft.Extensions.Primitives": "8.0.0"
} }
}, },
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
"Microsoft.Extensions.Configuration.Binder": "8.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Options": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
}
},
"Microsoft.Extensions.Primitives": { "Microsoft.Extensions.Primitives": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.0.0", "resolved": "8.0.0",
@@ -472,6 +598,33 @@
"Microsoft.Extensions.Logging.Abstractions": "8.0.0" "Microsoft.Extensions.Logging.Abstractions": "8.0.0"
} }
}, },
"OpenTelemetry": {
"type": "Transitive",
"resolved": "1.7.0",
"contentHash": "HNmOJg++4FtEJGCIn1dCJcDkHRLNuLKU047owrGXUOfz/C/c5oZHOPKrowKVOy2jOOh/F9+HDshzeOk5NnQEZg==",
"dependencies": {
"Microsoft.Extensions.Logging": "8.0.0",
"Microsoft.Extensions.Logging.Configuration": "8.0.0",
"OpenTelemetry.Api.ProviderBuilderExtensions": "1.7.0"
}
},
"OpenTelemetry.Api": {
"type": "Transitive",
"resolved": "1.7.0",
"contentHash": "w5uDHl1Nm7R5igWu1QjewpdPzJSavua8NTI3+iThOdRNHD3cabox8JeIJ8LpBSXGHARSNFMJZ/Xr5EGRinMTMg==",
"dependencies": {
"System.Diagnostics.DiagnosticSource": "8.0.0"
}
},
"OpenTelemetry.Api.ProviderBuilderExtensions": {
"type": "Transitive",
"resolved": "1.7.0",
"contentHash": "fHLmJcRJk1dAwddjBgvqi/Grmr04hPo1DoFwTa6hxtSvAFOXPU56Xe9Sh2VXqz6Gstzp6TCju8z0Sob1t7BzSg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"OpenTelemetry.Api": "1.7.0"
}
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.3.0", "resolved": "4.3.0",
@@ -887,6 +1040,11 @@
"System.Threading": "4.3.0" "System.Threading": "4.3.0"
} }
}, },
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.3",
"contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA=="
},
"System.Net.Http": { "System.Net.Http": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.3.0", "resolved": "4.3.0",
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 508.67 96.36"><g fill="#5865f2"><path d="M170.85,20.2h27.3q9.87,0,16.7,3.08a22.5,22.5,0,0,1,10.21,8.58,23.34,23.34,0,0,1,3.4,12.56A23.24,23.24,0,0,1,224.93,57a23.94,23.94,0,0,1-10.79,8.92q-7.24,3.3-17.95,3.29H170.85Zm25.06,36.54q6.65,0,10.22-3.32a11.8,11.8,0,0,0,3.57-9.07,11.5,11.5,0,0,0-3.18-8.5q-3.2-3.18-9.63-3.19h-8.54V56.74Z"/><path d="M269.34,69.13a37,37,0,0,1-10.22-4.27V53.24a27.77,27.77,0,0,0,9.2,4.38,39.31,39.31,0,0,0,11.17,1.71,8.71,8.71,0,0,0,3.82-.66c.86-.44,1.29-1,1.29-1.58a2.37,2.37,0,0,0-.7-1.75,6.15,6.15,0,0,0-2.73-1.19l-8.4-1.89q-7.22-1.68-10.25-4.65a10.39,10.39,0,0,1-3-7.81,10.37,10.37,0,0,1,2.66-7.07,17.13,17.13,0,0,1,7.56-4.65,36,36,0,0,1,11.48-1.65A43.27,43.27,0,0,1,292,27.69a30.25,30.25,0,0,1,8.12,3.22v11a30,30,0,0,0-7.6-3.11,34,34,0,0,0-8.85-1.16q-6.58,0-6.58,2.24a1.69,1.69,0,0,0,1,1.58,16.14,16.14,0,0,0,3.74,1.08l7,1.26Q295.65,45,299,48t3.36,8.78a11.61,11.61,0,0,1-5.57,10.12Q291.26,70.61,281,70.6A46.41,46.41,0,0,1,269.34,69.13Z"/><path d="M318.9,67.66a21,21,0,0,1-9.07-8,21.59,21.59,0,0,1-3-11.34,20.62,20.62,0,0,1,3.15-11.27,21.16,21.16,0,0,1,9.24-7.8,34.25,34.25,0,0,1,14.56-2.84q10.5,0,17.43,4.41V43.65a21.84,21.84,0,0,0-5.7-2.73,22.65,22.65,0,0,0-7-1.05q-6.51,0-10.19,2.38a7.15,7.15,0,0,0-.1,12.43q3.57,2.41,10.36,2.41a23.91,23.91,0,0,0,6.9-1,25.71,25.71,0,0,0,5.84-2.49V66a34,34,0,0,1-17.85,4.62A32.93,32.93,0,0,1,318.9,67.66Z"/><path d="M368.64,67.66a21.77,21.77,0,0,1-9.25-8,21.14,21.14,0,0,1-3.18-11.41A20.27,20.27,0,0,1,359.39,37a21.42,21.42,0,0,1,9.21-7.74,38.17,38.17,0,0,1,28.7,0,21.25,21.25,0,0,1,9.17,7.7,20.41,20.41,0,0,1,3.15,11.27,21.29,21.29,0,0,1-3.15,11.41,21.51,21.51,0,0,1-9.2,8,36.32,36.32,0,0,1-28.63,0Zm21.27-12.42a9.12,9.12,0,0,0,2.56-6.76,8.87,8.87,0,0,0-2.56-6.68,9.53,9.53,0,0,0-7-2.49,9.67,9.67,0,0,0-7,2.49,8.9,8.9,0,0,0-2.55,6.68,9.15,9.15,0,0,0,2.55,6.76,9.53,9.53,0,0,0,7,2.55A9.4,9.4,0,0,0,389.91,55.24Z"/><path d="M451.69,29V44.14a12.47,12.47,0,0,0-6.93-1.75c-3.73,0-6.61,1.14-8.61,3.4s-3,5.77-3,10.53V69.2H416V28.25h16.8v13q1.4-7.14,4.52-10.53a10.38,10.38,0,0,1,8-3.4A11.71,11.71,0,0,1,451.69,29Z"/><path d="M508.67,18.8V69.2H491.52V60a16.23,16.23,0,0,1-6.62,7.88A20.81,20.81,0,0,1,474,70.6a18.11,18.11,0,0,1-10.15-2.83A18.6,18.6,0,0,1,457.11,60a25.75,25.75,0,0,1-2.34-11.17,24.87,24.87,0,0,1,2.48-11.55,19.43,19.43,0,0,1,7.21-8,19.85,19.85,0,0,1,10.61-2.87q12.24,0,16.45,10.64V18.8ZM489,55a8.83,8.83,0,0,0,2.63-6.62A8.42,8.42,0,0,0,489,42a11,11,0,0,0-13.89,0,8.55,8.55,0,0,0-2.59,6.47A8.67,8.67,0,0,0,475.14,55,9.42,9.42,0,0,0,482,57.51,9.56,9.56,0,0,0,489,55Z"/><path d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/><ellipse cx="242.92" cy="24.93" rx="8.55" ry="7.68"/><path d="M234.36,37.9a22.08,22.08,0,0,0,17.11,0V69.42H234.36Z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+6 -4
View File
@@ -6,6 +6,12 @@ Kard és Mágia karakter alkotó segédlet (https://app.kemkas.hu)
\[HU] Kérlek nézd el a kevert angol kifejezeséket, ugyan a szerepáték nyelve magyar, de a programozás szakmai nyelve angol. \[HU] Kérlek nézd el a kevert angol kifejezeséket, ugyan a szerepáték nyelve magyar, de a programozás szakmai nyelve angol.
## Deployment
See deployment repo at: [https://github.com/morbalint/kemkas-deployment](https://github.com/morbalint/kemkas-deployment)
![CI workflow](https://github.com/morbalint/kemkas/actions/workflows/ci.yml/badge.svg)
## Coding readmes ## Coding readmes
For frontend only see: [Frontend README](./Kemkas.Web/frontend/README.md) For frontend only see: [Frontend README](./Kemkas.Web/frontend/README.md)
@@ -18,7 +24,3 @@ For the first time setup:
3. spin up the proxy from docker compose (needs the SSL certs) 3. spin up the proxy from docker compose (needs the SSL certs)
Later start the docker compose first and the project launch settings after the DB is up and running. Later start the docker compose first and the project launch settings after the DB is up and running.
## Deployment
See deployment repo at: [https://github.com/morbalint/kemkas-deployment](https://github.com/morbalint/kemkas-deployment)
+10 -1
View File
@@ -13,12 +13,21 @@
## Features ## Features
- multiclass characters - multiclass characters
- 2e -ben félig működik
- druidáknál tiltani kell
- PDF-ben varazshasznalok
- tolvaj 9ik szintű képessége
## Epics ## Epics
- Backend - Backend
- Varazslatok - Varazslatok
- Felszerelés - Felszerelés
- KéM 2.0-ra felkészülés - 1e-ben fel van már véve az adat. Gergő neki veselkedik.
- 2e-ben teljesen hiányzik
- KéM 2e
- frontend oldalon úgy, ahogy megvan
- PDF hiányzik
- backend hiányzik
- Klikkelésre dobás - Klikkelésre dobás