init
@@ -0,0 +1,175 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
public class ImageProcessor
|
||||||
|
{
|
||||||
|
public static void MakeCleanTransparent(string inputPath, string outputPath)
|
||||||
|
{
|
||||||
|
using var original = (Bitmap)Image.FromFile(inputPath);
|
||||||
|
int w = original.Width;
|
||||||
|
int h = original.Height;
|
||||||
|
|
||||||
|
using var bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb);
|
||||||
|
using (var g = Graphics.FromImage(bmp))
|
||||||
|
{
|
||||||
|
g.DrawImage(original, 0, 0, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rect = new Rectangle(0, 0, w, h);
|
||||||
|
var data = bmp.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
|
||||||
|
|
||||||
|
int bytesPerPixel = 4;
|
||||||
|
int byteCount = data.Stride * h;
|
||||||
|
byte[] pixels = new byte[byteCount];
|
||||||
|
System.Runtime.InteropServices.Marshal.Copy(data.Scan0, pixels, 0, byteCount);
|
||||||
|
|
||||||
|
bool[,] isBg = new bool[w, h];
|
||||||
|
var queue = new Queue<Point>();
|
||||||
|
|
||||||
|
bool CheckWhite(int x, int y)
|
||||||
|
{
|
||||||
|
int idx = y * data.Stride + x * bytesPerPixel;
|
||||||
|
byte b = pixels[idx];
|
||||||
|
byte g = pixels[idx + 1];
|
||||||
|
byte r = pixels[idx + 2];
|
||||||
|
// Pure/near white or light background
|
||||||
|
return r > 215 && g > 215 && b > 215 && Math.Abs(r - g) < 25 && Math.Abs(g - b) < 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed borders
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
{
|
||||||
|
if (CheckWhite(x, 0)) { isBg[x, 0] = true; queue.Enqueue(new Point(x, 0)); }
|
||||||
|
if (CheckWhite(x, h - 1)) { isBg[x, h - 1] = true; queue.Enqueue(new Point(x, h - 1)); }
|
||||||
|
}
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
{
|
||||||
|
if (CheckWhite(0, y)) { isBg[0, y] = true; queue.Enqueue(new Point(0, y)); }
|
||||||
|
if (CheckWhite(w - 1, y)) { isBg[w - 1, y] = true; queue.Enqueue(new Point(w - 1, y)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] dx = { 0, 0, 1, -1 };
|
||||||
|
int[] dy = { 1, -1, 0, 0 };
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
var p = queue.Dequeue();
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
int nx = p.X + dx[i];
|
||||||
|
int ny = p.Y + dy[i];
|
||||||
|
if (nx >= 0 && nx < w && ny >= 0 && ny < h && !isBg[nx, ny])
|
||||||
|
{
|
||||||
|
if (CheckWhite(nx, ny))
|
||||||
|
{
|
||||||
|
isBg[nx, ny] = true;
|
||||||
|
queue.Enqueue(new Point(nx, ny));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply transparency
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
{
|
||||||
|
int idx = y * data.Stride + x * bytesPerPixel;
|
||||||
|
if (isBg[x, y])
|
||||||
|
{
|
||||||
|
pixels[idx + 3] = 0; // Alpha = 0
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Check if adjacent to background for soft anti-aliased edge
|
||||||
|
bool nearBg = false;
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
int nx = x + dx[i];
|
||||||
|
int ny = y + dy[i];
|
||||||
|
if (nx >= 0 && nx < w && ny >= 0 && ny < h && isBg[nx, ny])
|
||||||
|
{
|
||||||
|
nearBg = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte b = pixels[idx];
|
||||||
|
byte g = pixels[idx + 1];
|
||||||
|
byte r = pixels[idx + 2];
|
||||||
|
if (nearBg && r > 210 && g > 210 && b > 210)
|
||||||
|
{
|
||||||
|
double avg = (r + g + b) / 3.0;
|
||||||
|
int alpha = (int)(255.0 * (1.0 - (avg - 210.0) / 45.0));
|
||||||
|
pixels[idx + 3] = (byte)Math.Clamp(alpha, 0, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Runtime.InteropServices.Marshal.Copy(pixels, 0, data.Scan0, byteCount);
|
||||||
|
bmp.UnlockBits(data);
|
||||||
|
|
||||||
|
// Ensure output dir
|
||||||
|
var dir = Path.GetDirectoryName(outputPath);
|
||||||
|
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||||
|
|
||||||
|
bmp.Save(outputPath, ImageFormat.Png);
|
||||||
|
Console.WriteLine($"Exported: {outputPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
string brain = @"C:\Users\mvrba\.gemini\antigravity\brain\f097144f-e914-4120-8461-4bdfe0d3cdef";
|
||||||
|
string root = @"src\CiselnaVyprava\wwwroot\assets\images";
|
||||||
|
|
||||||
|
// 1. Knight (Exact same boy character)
|
||||||
|
string knightIdleSrc = Path.Combine(brain, "disney_hero_knight_1789996129818.jpg");
|
||||||
|
string knightAtkSrc = Path.Combine(brain, "knight_action_strike_1789998031553.jpg");
|
||||||
|
string knightDefSrc = Path.Combine(brain, "knight_action_shield_1789998042563.jpg");
|
||||||
|
string knightDizzySrc = Path.Combine(brain, "hero_knight_dizzy_1789997064946.jpg");
|
||||||
|
|
||||||
|
// 2. Goblin
|
||||||
|
string goblinIdleSrc = Path.Combine(brain, "disney_monster_goblin_1789996145106.jpg");
|
||||||
|
string goblinAtkSrc = Path.Combine(brain, "goblin_monster_attack_1789997081347.jpg");
|
||||||
|
string goblinHurtSrc = Path.Combine(brain, "goblin_monster_hurt_1789997090819.jpg");
|
||||||
|
|
||||||
|
// 3. Golem Boss
|
||||||
|
string golemSrc = Path.Combine(brain, "stone_golem_boss_1789997102336.jpg");
|
||||||
|
|
||||||
|
// Process Knight
|
||||||
|
for (int p = 1; p <= 4; p++)
|
||||||
|
{
|
||||||
|
string idleSrc = (p <= 2) ? knightIdleSrc : knightDizzySrc;
|
||||||
|
MakeCleanTransparent(idleSrc, Path.Combine(root, $"heroes/knight_idle_p{p}.png"));
|
||||||
|
MakeCleanTransparent(knightAtkSrc, Path.Combine(root, $"heroes/knight_attack_p{p}.png"));
|
||||||
|
MakeCleanTransparent(knightDefSrc, Path.Combine(root, $"heroes/knight_defend_p{p}.png"));
|
||||||
|
MakeCleanTransparent(knightDizzySrc, Path.Combine(root, $"heroes/knight_hurt_p{p}.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process Goblin & Slime
|
||||||
|
for (int p = 1; p <= 4; p++)
|
||||||
|
{
|
||||||
|
MakeCleanTransparent(goblinIdleSrc, Path.Combine(root, $"enemies/goblin_idle_p{p}.png"));
|
||||||
|
MakeCleanTransparent(goblinAtkSrc, Path.Combine(root, $"enemies/goblin_attack_p{p}.png"));
|
||||||
|
MakeCleanTransparent(goblinHurtSrc, Path.Combine(root, $"enemies/goblin_hurt_p{p}.png"));
|
||||||
|
|
||||||
|
MakeCleanTransparent(goblinIdleSrc, Path.Combine(root, $"enemies/slime_idle_p{p}.png"));
|
||||||
|
MakeCleanTransparent(goblinAtkSrc, Path.Combine(root, $"enemies/slime_attack_p{p}.png"));
|
||||||
|
MakeCleanTransparent(goblinHurtSrc, Path.Combine(root, $"enemies/slime_hurt_p{p}.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process Golem
|
||||||
|
for (int p = 1; p <= 4; p++)
|
||||||
|
{
|
||||||
|
MakeCleanTransparent(golemSrc, Path.Combine(root, $"enemies/golem_idle_p{p}.png"));
|
||||||
|
MakeCleanTransparent(golemSrc, Path.Combine(root, $"enemies/golem_attack_p{p}.png"));
|
||||||
|
MakeCleanTransparent(golemSrc, Path.Combine(root, $"enemies/golem_hurt_p{p}.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("All assets converted to clean 32-bit transparent PNGs!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
@inject IBattleEngine BattleEngine
|
||||||
|
@inject ILocalStorageService Storage
|
||||||
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
|
<div class="battle-arena-wrapper" tabindex="0" @ref="arenaElement" @onkeydown="HandleKeyDown" autofocus>
|
||||||
|
<!-- Top Header / Bar -->
|
||||||
|
<div class="battle-header">
|
||||||
|
|
||||||
|
<button class="btn-back" @onclick="GoBackToMap">⬅ Mapa</button>
|
||||||
|
<div class="level-info">
|
||||||
|
<span class="world-title">@CurrentLevel.Name</span>
|
||||||
|
@if (CurrentLevel.IsBoss)
|
||||||
|
{
|
||||||
|
<span class="boss-badge">👑 BOSS</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="combo-counter @(State.ComboStreak > 1 ? "active-streak" : "")">
|
||||||
|
🔥 Kombo: <strong>@State.ComboStreak</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feedback / Toast Banner -->
|
||||||
|
@if (!string.IsNullOrEmpty(State.LastFeedbackMessage))
|
||||||
|
{
|
||||||
|
<div class="feedback-banner @GetFeedbackClass()">
|
||||||
|
@State.LastFeedbackMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Main Combat Arena -->
|
||||||
|
<div class="arena-stage" style="background: linear-gradient(rgba(0,0,0,0.15), rgba(0,0,0,0.35)), url('assets/images/backgrounds/world1_meadow.jpg') center/cover no-repeat;">
|
||||||
|
<!-- Player Side -->
|
||||||
|
|
||||||
|
<div class="combatant player-side @GetPlayerAnimationClass()">
|
||||||
|
<div class="combatant-info">
|
||||||
|
<span class="combatant-name">@Hero.Name</span>
|
||||||
|
<div class="hp-hearts">
|
||||||
|
@for (int i = 1; i <= State.PlayerMaxHp; i++)
|
||||||
|
{
|
||||||
|
<span class="heart @(i <= State.PlayerCurrentHp ? "full" : "empty")">
|
||||||
|
@(i <= State.PlayerCurrentHp ? "❤️" : "🤍")
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Player Visual (4 Phases & Combat Poses) -->
|
||||||
|
<div class="character-sprite-box">
|
||||||
|
<div class="sprite-phase-indicator">Fáza @State.PlayerHpPhase / 4</div>
|
||||||
|
<img src="@GetCharacterImage(Hero.AssetFolder, State.PlayerHpPhase, State.PlayerPose)"
|
||||||
|
alt="@Hero.Name"
|
||||||
|
class="character-image"
|
||||||
|
onerror="this.src='data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'180\' height=\'240\'><rect width=\'100%\' height=\'100%\' fill=\'%234a90e2\'/><text x=\'50%\' y=\'50%\' fill=\'white\' font-size=\'20\' text-anchor=\'middle\'>🧙♂️ @Hero.Name (Fáza @State.PlayerHpPhase)</text></svg>'"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- VS Divider / Dynamic Timer -->
|
||||||
|
<div class="arena-center">
|
||||||
|
<div class="timer-orb @GetTimerColorClass()">
|
||||||
|
<div class="timer-seconds">@RemainingTimeSeconds.ToString("F1")s</div>
|
||||||
|
<div class="timer-label">@(State.IsDefensePhase ? "OBRANA!" : "ÚTOK")</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enemy Side -->
|
||||||
|
<div class="combatant enemy-side @GetEnemyAnimationClass()">
|
||||||
|
<div class="combatant-info">
|
||||||
|
<span class="combatant-name">@State.Enemy.Name</span>
|
||||||
|
<div class="hp-bar-wrapper">
|
||||||
|
<div class="hp-bar-fill" style="width: @(EnemyHpPercent)%"></div>
|
||||||
|
<span class="hp-text">@State.EnemyCurrentHp / @State.EnemyMaxHp HP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enemy Visual (4 Phases & Combat Poses) -->
|
||||||
|
<div class="character-sprite-box">
|
||||||
|
<div class="sprite-phase-indicator">Fáza @State.EnemyHpPhase / 4</div>
|
||||||
|
<img src="@GetCharacterImage(State.Enemy.AssetFolder, State.EnemyHpPhase, State.EnemyPose)"
|
||||||
|
alt="@State.Enemy.Name"
|
||||||
|
class="character-image enemy-img"
|
||||||
|
onerror="this.src='data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'180\' height=\'240\'><rect width=\'100%\' height=\'100%\' fill=\'%23e74c3c\'/><text x=\'50%\' y=\'50%\' fill=\'white\' font-size=\'20\' text-anchor=\'middle\'>👾 @State.Enemy.Name (Fáza @State.EnemyHpPhase)</text></svg>'"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Parchment / Math Action Area -->
|
||||||
|
<div class="parchment-action-board">
|
||||||
|
@if (!State.IsBattleOver)
|
||||||
|
{
|
||||||
|
<div class="equation-box @(State.IsDefensePhase ? "defense-glow" : "attack-glow")">
|
||||||
|
<span class="equation-title">
|
||||||
|
@(State.IsDefensePhase ? "🛡️ OBRANNÝ TEST: Vyber správnu odpoveď!" : "⚔️ VYPOČÍTAJ A ZAÚTOČ:")
|
||||||
|
</span>
|
||||||
|
<h1 class="equation-text">@State.CurrentProblem.QuestionText</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-action-zone">
|
||||||
|
@if (State.CurrentProblem.InputType == "MultipleChoice" || State.IsDefensePhase)
|
||||||
|
{
|
||||||
|
<ChoiceSelector Choices="State.CurrentProblem.Choices" OnChoiceSelected="HandleChoiceSelected" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MathKeypad @bind-CurrentValue="CurrentInput" OnSubmit="HandleInputSubmit" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<!-- Victory / Defeat Modal -->
|
||||||
|
<div class="battle-end-card @(State.IsVictory ? "victory-card" : "defeat-card")">
|
||||||
|
<h2>@(State.IsVictory ? "🏆 VÍŤAZSTVO!" : "💀 PORÁŽKA!")</h2>
|
||||||
|
<p>@State.LastFeedbackMessage</p>
|
||||||
|
<div class="end-actions">
|
||||||
|
@if (State.IsVictory)
|
||||||
|
{
|
||||||
|
<button class="action-btn next-level-btn" @onclick="OnVictoryContinue">Pokračovať v ceste ➡️</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button class="action-btn retry-btn" @onclick="RetryBattle">Skúsiť znova 🔄</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public Hero Hero { get; set; } = new();
|
||||||
|
[Parameter] public Level CurrentLevel { get; set; } = new();
|
||||||
|
[Parameter] public GameConfig Config { get; set; } = new();
|
||||||
|
[Parameter] public EventCallback OnReturnToMap { get; set; }
|
||||||
|
[Parameter] public EventCallback<Level> OnLevelCompleted { get; set; }
|
||||||
|
|
||||||
|
private BattleState State => BattleEngine.State;
|
||||||
|
private string CurrentInput { get; set; } = string.Empty;
|
||||||
|
private System.Threading.Timer? _timer;
|
||||||
|
private double RemainingTimeSeconds { get; set; } = 6.0;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
StartBattle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartBattle()
|
||||||
|
{
|
||||||
|
BattleEngine.StartBattle(Hero, CurrentLevel, Config);
|
||||||
|
CurrentInput = string.Empty;
|
||||||
|
StartTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartTimer()
|
||||||
|
{
|
||||||
|
_timer?.Dispose();
|
||||||
|
RemainingTimeSeconds = State.IsDefensePhase ? Config.Battle.DefenseTestDurationSeconds : Config.Battle.StandardTimeSeconds;
|
||||||
|
_timer = new System.Threading.Timer(async _ =>
|
||||||
|
{
|
||||||
|
var elapsed = (DateTime.UtcNow - State.ProblemStartTime).TotalSeconds;
|
||||||
|
double limit = State.IsDefensePhase ? Config.Battle.DefenseTestDurationSeconds : Config.Battle.StandardTimeSeconds;
|
||||||
|
RemainingTimeSeconds = Math.Max(0, limit - elapsed);
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}, null, 100, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleInputSubmit(string input)
|
||||||
|
{
|
||||||
|
if (State.IsBattleOver) return;
|
||||||
|
|
||||||
|
if (State.IsDefensePhase)
|
||||||
|
{
|
||||||
|
await BattleEngine.SubmitDefenseAnswerAsync(input);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await BattleEngine.SubmitAnswerAsync(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrentInput = string.Empty;
|
||||||
|
await ProcessPostTurn();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleChoiceSelected(string choice)
|
||||||
|
{
|
||||||
|
if (State.IsBattleOver) return;
|
||||||
|
|
||||||
|
if (State.IsDefensePhase)
|
||||||
|
{
|
||||||
|
await BattleEngine.SubmitDefenseAnswerAsync(choice);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await BattleEngine.SubmitAnswerAsync(choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ProcessPostTurn();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessPostTurn()
|
||||||
|
{
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
if (!State.IsBattleOver)
|
||||||
|
{
|
||||||
|
// Short delay to display attack/hurt animation before next question
|
||||||
|
await Task.Delay(1000);
|
||||||
|
if (!State.IsDefensePhase)
|
||||||
|
{
|
||||||
|
BattleEngine.NextProblem();
|
||||||
|
}
|
||||||
|
StartTimer();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_timer?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RetryBattle()
|
||||||
|
{
|
||||||
|
StartBattle();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnVictoryContinue()
|
||||||
|
{
|
||||||
|
await OnLevelCompleted.InvokeAsync(CurrentLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GoBackToMap()
|
||||||
|
{
|
||||||
|
_timer?.Dispose();
|
||||||
|
await OnReturnToMap.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int EnemyHpPercent => State.EnemyMaxHp > 0
|
||||||
|
? (int)((double)State.EnemyCurrentHp / State.EnemyMaxHp * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
private string GetCharacterImage(string folder, int phase, CombatPose pose)
|
||||||
|
{
|
||||||
|
string poseName = pose.ToString().ToLowerInvariant();
|
||||||
|
return $"{folder}_{poseName}_p{phase}.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetPlayerAnimationClass() => State.PlayerPose switch
|
||||||
|
{
|
||||||
|
CombatPose.Attack => "anim-hero-attack",
|
||||||
|
CombatPose.Defend => "anim-hero-defend",
|
||||||
|
CombatPose.Hurt => "anim-hero-hurt",
|
||||||
|
_ => "anim-hero-idle"
|
||||||
|
};
|
||||||
|
|
||||||
|
private string GetEnemyAnimationClass() => State.EnemyPose switch
|
||||||
|
{
|
||||||
|
CombatPose.Attack => "anim-enemy-attack",
|
||||||
|
CombatPose.Hurt => "anim-enemy-hurt",
|
||||||
|
_ => "anim-enemy-idle"
|
||||||
|
};
|
||||||
|
|
||||||
|
private string GetTimerColorClass()
|
||||||
|
{
|
||||||
|
if (RemainingTimeSeconds > 3.0) return "timer-green";
|
||||||
|
if (RemainingTimeSeconds > 1.5) return "timer-yellow";
|
||||||
|
return "timer-red";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFeedbackClass() => State.LastResult switch
|
||||||
|
{
|
||||||
|
CombatResultType.CriticalHit => "feedback-critical",
|
||||||
|
CombatResultType.StandardHit => "feedback-standard",
|
||||||
|
CombatResultType.DefenseSuccess => "feedback-defense",
|
||||||
|
CombatResultType.PlayerMissed or CombatResultType.DefenseFailed => "feedback-error",
|
||||||
|
_ => ""
|
||||||
|
};
|
||||||
|
|
||||||
|
private ElementReference arenaElement;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await arenaElement.FocusAsync();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||||
|
{
|
||||||
|
if (State.IsBattleOver)
|
||||||
|
{
|
||||||
|
if (e.Key == "Enter")
|
||||||
|
{
|
||||||
|
if (State.IsVictory)
|
||||||
|
await OnVictoryContinue();
|
||||||
|
else
|
||||||
|
await RetryBattle();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (State.CurrentProblem.InputType == "MultipleChoice" || State.IsDefensePhase)
|
||||||
|
{
|
||||||
|
// Handle number keys 1, 2, 3 to pick choices
|
||||||
|
if (int.TryParse(e.Key, out int choiceIndex) && choiceIndex >= 1 && choiceIndex <= State.CurrentProblem.Choices.Count)
|
||||||
|
{
|
||||||
|
await HandleChoiceSelected(State.CurrentProblem.Choices[choiceIndex - 1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle A/N shortcuts for ÁNO / NIE
|
||||||
|
if (e.Key.Equals("a", StringComparison.OrdinalIgnoreCase) || e.Key.Equals("y", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var match = State.CurrentProblem.Choices.FirstOrDefault(c => c.StartsWith("Á", StringComparison.OrdinalIgnoreCase) || c.StartsWith("A", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (match != null) await HandleChoiceSelected(match);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.Key.Equals("n", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var match = State.CurrentProblem.Choices.FirstOrDefault(c => c.StartsWith("N", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (match != null) await HandleChoiceSelected(match);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Direct numeric input (0-9, Backspace, Enter)
|
||||||
|
if (e.Key.Length == 1 && char.IsDigit(e.Key[0]))
|
||||||
|
{
|
||||||
|
if (CurrentInput.Length < 6)
|
||||||
|
{
|
||||||
|
CurrentInput += e.Key;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.Key == "Backspace")
|
||||||
|
{
|
||||||
|
if (CurrentInput.Length > 0)
|
||||||
|
{
|
||||||
|
CurrentInput = CurrentInput.Substring(0, CurrentInput.Length - 1);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.Key == "Enter")
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(CurrentInput))
|
||||||
|
{
|
||||||
|
await HandleInputSubmit(CurrentInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
@inject IGameDataLoader DataLoader
|
||||||
|
@inject ILocalStorageService Storage
|
||||||
|
|
||||||
|
<div class="character-select-screen">
|
||||||
|
<div class="parchment-container">
|
||||||
|
<h1 class="game-main-title">🏰 ČÍSELNÁ VÝPRAVA</h1>
|
||||||
|
<p class="subtitle">Záchrana Číselného Kráľovstva</p>
|
||||||
|
|
||||||
|
<!-- KROK 1: VÝBER ROČNÍKA -->
|
||||||
|
<div class="selection-section">
|
||||||
|
<h2 class="step-title">1. Krok: Vyber si svoj ročník</h2>
|
||||||
|
<div class="grade-toggle-group grade-grid-3">
|
||||||
|
<button class="grade-btn @(SelectedGrade == 1 ? "active" : "")" @onclick="() => SetGrade(1)">
|
||||||
|
<span class="grade-badge">🎒 1. ROČNÍK</span>
|
||||||
|
<span class="grade-desc">Počítanie a sčítanie do 10/20</span>
|
||||||
|
</button>
|
||||||
|
<button class="grade-btn @(SelectedGrade == 2 ? "active" : "")" @onclick="() => SetGrade(2)">
|
||||||
|
<span class="grade-badge">🎒 2. ROČNÍK</span>
|
||||||
|
<span class="grade-desc">Sčítanie a odčítanie do 20/100</span>
|
||||||
|
</button>
|
||||||
|
<button class="grade-btn @(SelectedGrade == 3 ? "active" : "")" @onclick="() => SetGrade(3)">
|
||||||
|
<span class="grade-badge">🎒 3. ROČNÍK</span>
|
||||||
|
<span class="grade-desc">Malá násobilka a delenie</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KROK 2: VÝBER HRDINU -->
|
||||||
|
<div class="selection-section">
|
||||||
|
<h2 class="step-title">2. Krok: Vyber si svojho hrdinu</h2>
|
||||||
|
<div class="heroes-grid">
|
||||||
|
@foreach (var hero in DataLoader.Heroes)
|
||||||
|
{
|
||||||
|
<div class="hero-card @(SelectedHeroId == hero.Id ? "selected-card" : "")"
|
||||||
|
@onclick="() => SelectHero(hero.Id)">
|
||||||
|
<div class="hero-avatar">
|
||||||
|
@if (hero.Id == "knight")
|
||||||
|
{
|
||||||
|
<span>⚔️</span>
|
||||||
|
}
|
||||||
|
else if (hero.Id == "mage")
|
||||||
|
{
|
||||||
|
<span>🧙♂️</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>🏹</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<h3 class="hero-name">@hero.Name</h3>
|
||||||
|
<span class="hero-title">@hero.Title</span>
|
||||||
|
<p class="hero-description">@hero.Description</p>
|
||||||
|
<div class="hero-skill">
|
||||||
|
✨ Špeciál: <strong>@hero.SpecialAttackName</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KROK 3: MENO HRDINU & ŠTART -->
|
||||||
|
<div class="selection-section name-input-section">
|
||||||
|
<h2 class="step-title">3. Krok: Tvoje hrdinské meno</h2>
|
||||||
|
<div class="name-field-row">
|
||||||
|
<input type="text" class="hero-name-input" @bind="HeroName" placeholder="Zadaj svoje meno..." maxlength="20" />
|
||||||
|
<button class="btn-start-quest" @onclick="StartQuest">
|
||||||
|
Vstúpiť do kráľovstva 🚀
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public EventCallback<PlayerProfile> OnProfileReady { get; set; }
|
||||||
|
|
||||||
|
private int SelectedGrade { get; set; } = 2;
|
||||||
|
private string SelectedHeroId { get; set; } = "knight";
|
||||||
|
private string HeroName { get; set; } = "Mladý Hrdina";
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
if (DataLoader.Heroes.Any())
|
||||||
|
{
|
||||||
|
SelectedHeroId = DataLoader.Heroes.First().Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetGrade(int grade)
|
||||||
|
{
|
||||||
|
SelectedGrade = grade;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectHero(string heroId)
|
||||||
|
{
|
||||||
|
SelectedHeroId = heroId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartQuest()
|
||||||
|
{
|
||||||
|
var profile = new PlayerProfile
|
||||||
|
{
|
||||||
|
Name = string.IsNullOrWhiteSpace(HeroName) ? "Mladý Hrdina" : HeroName,
|
||||||
|
SelectedGrade = SelectedGrade,
|
||||||
|
SelectedHeroId = SelectedHeroId,
|
||||||
|
CurrentWorldId = 1,
|
||||||
|
UnlockedLevels = new HashSet<string> { $"{SelectedGrade}-1-1" }
|
||||||
|
};
|
||||||
|
|
||||||
|
await Storage.SaveProfileAsync(profile);
|
||||||
|
await OnProfileReady.InvokeAsync(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<div class="choice-selector-container">
|
||||||
|
<div class="choices-grid">
|
||||||
|
@foreach (var choice in Choices)
|
||||||
|
{
|
||||||
|
<button class="choice-btn" @onclick="() => SelectChoice(choice)">
|
||||||
|
@choice
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public List<string> Choices { get; set; } = new();
|
||||||
|
[Parameter] public EventCallback<string> OnChoiceSelected { get; set; }
|
||||||
|
|
||||||
|
private async Task SelectChoice(string choice)
|
||||||
|
{
|
||||||
|
await OnChoiceSelected.InvokeAsync(choice);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
@inject ILocalStorageService Storage
|
||||||
|
|
||||||
|
<div class="main-menu-fullscreen" style="background: linear-gradient(rgba(0,0,0,0.1), rgba(0,0,0,0.4)), url('assets/images/backgrounds/title_screen_bg.jpg') center/cover no-repeat;">
|
||||||
|
<div class="main-menu-overlay">
|
||||||
|
<!-- Game Title Logo -->
|
||||||
|
<div class="title-logo-box">
|
||||||
|
<div class="title-crown">👑</div>
|
||||||
|
<h1 class="main-game-title">ČÍSELNÁ VÝPRAVA</h1>
|
||||||
|
<div class="title-banner">Záchrana Číselného Kráľovstva</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PC Menu Action Box -->
|
||||||
|
<div class="menu-actions-parchment">
|
||||||
|
@if (HasSavedGame)
|
||||||
|
{
|
||||||
|
<button class="pc-menu-btn continue-btn" @onclick="ContinueGame">
|
||||||
|
<span class="btn-icon">📜</span>
|
||||||
|
<div class="btn-text-block">
|
||||||
|
<span class="btn-title">POKRAČOVAŤ</span>
|
||||||
|
<span class="btn-subtitle">@Profile.Name (@Profile.SelectedGrade. Ročník) • ⭐ @Profile.TotalStars</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button class="pc-menu-btn new-game-btn" @onclick="NewGame">
|
||||||
|
<span class="btn-icon">⚔️</span>
|
||||||
|
<div class="btn-text-block">
|
||||||
|
<span class="btn-title">NOVÁ HRA</span>
|
||||||
|
<span class="btn-subtitle">Začať nové rozprávkové dobrodružstvo</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (HasSavedGame)
|
||||||
|
{
|
||||||
|
<button class="pc-menu-btn reset-btn" @onclick="ConfirmReset">
|
||||||
|
<span class="btn-icon">🗑️</span>
|
||||||
|
<span class="btn-title-small">Vymazať uloženú hru</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Footer -->
|
||||||
|
<div class="main-menu-footer">
|
||||||
|
<span>Matematické RPG pre 1., 2. a 3. ročník ZŠ</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public PlayerProfile Profile { get; set; } = new();
|
||||||
|
[Parameter] public EventCallback OnNewGame { get; set; }
|
||||||
|
[Parameter] public EventCallback OnContinueGame { get; set; }
|
||||||
|
[Parameter] public EventCallback OnResetGame { get; set; }
|
||||||
|
|
||||||
|
private bool HasSavedGame => Profile.CompletedLevels.Any() || Profile.UnlockedLevels.Count > 1;
|
||||||
|
|
||||||
|
private async Task ContinueGame()
|
||||||
|
{
|
||||||
|
await OnContinueGame.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NewGame()
|
||||||
|
{
|
||||||
|
await OnNewGame.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConfirmReset()
|
||||||
|
{
|
||||||
|
await OnResetGame.InvokeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<div class="keypad-container">
|
||||||
|
<div class="keypad-display">
|
||||||
|
<span class="entered-value">@(string.IsNullOrEmpty(CurrentValue) ? "?" : CurrentValue)</span>
|
||||||
|
</div>
|
||||||
|
<div class="keypad-grid">
|
||||||
|
@for (int i = 1; i <= 9; i++)
|
||||||
|
{
|
||||||
|
var digit = i.ToString();
|
||||||
|
<button class="key-btn num-key" @onclick="() => AppendDigit(digit)">@digit</button>
|
||||||
|
}
|
||||||
|
<button class="key-btn action-key backspace" @onclick="Backspace">⌫</button>
|
||||||
|
<button class="key-btn num-key" @onclick='() => AppendDigit("0")'>0</button>
|
||||||
|
<button class="key-btn action-key submit" @onclick="Submit">⚔️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string CurrentValue { get; set; } = string.Empty;
|
||||||
|
[Parameter] public EventCallback<string> CurrentValueChanged { get; set; }
|
||||||
|
[Parameter] public EventCallback<string> OnSubmit { get; set; }
|
||||||
|
|
||||||
|
private async Task AppendDigit(string digit)
|
||||||
|
{
|
||||||
|
if (CurrentValue.Length < 6)
|
||||||
|
{
|
||||||
|
CurrentValue += digit;
|
||||||
|
await CurrentValueChanged.InvokeAsync(CurrentValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Backspace()
|
||||||
|
{
|
||||||
|
if (CurrentValue.Length > 0)
|
||||||
|
{
|
||||||
|
CurrentValue = CurrentValue.Substring(0, CurrentValue.Length - 1);
|
||||||
|
await CurrentValueChanged.InvokeAsync(CurrentValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Submit()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(CurrentValue))
|
||||||
|
{
|
||||||
|
await OnSubmit.InvokeAsync(CurrentValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
@inject IGameDataLoader DataLoader
|
||||||
|
|
||||||
|
<div class="world-map-screen">
|
||||||
|
<!-- Top Player Status Bar -->
|
||||||
|
<div class="map-top-bar">
|
||||||
|
<div class="player-pill">
|
||||||
|
<span class="avatar-icon">@(Profile.SelectedHeroId == "knight" ? "⚔️" : Profile.SelectedHeroId == "mage" ? "🧙♂️" : "🏹")</span>
|
||||||
|
<span class="player-name">@Profile.Name</span>
|
||||||
|
<span class="grade-tag">@(Profile.SelectedGrade). Ročník</span>
|
||||||
|
</div>
|
||||||
|
<div class="stats-pill">
|
||||||
|
<span>⭐ Hviezdy: <strong>@Profile.TotalStars</strong></span>
|
||||||
|
</div>
|
||||||
|
<button class="btn-change-hero" @onclick="OnChangeHero">Zmeniť hrdinu 🔄</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Worlds Container -->
|
||||||
|
<div class="worlds-carousel">
|
||||||
|
@foreach (var world in Worlds)
|
||||||
|
{
|
||||||
|
<div class="world-card">
|
||||||
|
<div class="world-header">
|
||||||
|
<h2>@world.Name</h2>
|
||||||
|
<p>@world.Description</p>
|
||||||
|
</div>
|
||||||
|
<div class="levels-path">
|
||||||
|
@foreach (var level in world.Levels)
|
||||||
|
{
|
||||||
|
bool isUnlocked = Profile.UnlockedLevels.Contains(level.Id);
|
||||||
|
bool isCompleted = Profile.CompletedLevels.Contains(level.Id);
|
||||||
|
|
||||||
|
<button class="level-node @(level.IsBoss ? "boss-node" : "") @(isUnlocked ? "unlocked" : "locked") @(isCompleted ? "completed" : "")"
|
||||||
|
disabled="@(!isUnlocked)"
|
||||||
|
@onclick="() => SelectLevel(level)">
|
||||||
|
<div class="node-icon">
|
||||||
|
@if (isCompleted)
|
||||||
|
{
|
||||||
|
<span>⭐</span>
|
||||||
|
}
|
||||||
|
else if (level.IsBoss)
|
||||||
|
{
|
||||||
|
<span>👑</span>
|
||||||
|
}
|
||||||
|
else if (isUnlocked)
|
||||||
|
{
|
||||||
|
<span>⚔️</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>🔒</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span class="level-title">@level.Name</span>
|
||||||
|
@if (level.IsBoss)
|
||||||
|
{
|
||||||
|
<span class="boss-tag">BOSS</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public PlayerProfile Profile { get; set; } = new();
|
||||||
|
[Parameter] public EventCallback<Level> OnLevelSelected { get; set; }
|
||||||
|
[Parameter] public EventCallback OnChangeHero { get; set; }
|
||||||
|
|
||||||
|
private List<World> Worlds => DataLoader.GetWorldsForGrade(Profile.SelectedGrade);
|
||||||
|
|
||||||
|
private async Task SelectLevel(Level level)
|
||||||
|
{
|
||||||
|
await OnLevelSelected.InvokeAsync(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
namespace CiselnaVyprava.Models;
|
||||||
|
|
||||||
|
public enum CombatPose
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Attack,
|
||||||
|
Defend,
|
||||||
|
Hurt
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CombatResultType
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
CriticalHit,
|
||||||
|
StandardHit,
|
||||||
|
WeakHit,
|
||||||
|
PlayerMissed,
|
||||||
|
DefenseSuccess,
|
||||||
|
DefenseFailed,
|
||||||
|
Victory,
|
||||||
|
Defeat
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BattleState
|
||||||
|
{
|
||||||
|
public Hero Hero { get; set; } = new();
|
||||||
|
public Enemy Enemy { get; set; } = new();
|
||||||
|
public Level CurrentLevel { get; set; } = new();
|
||||||
|
|
||||||
|
public int PlayerCurrentHp { get; set; }
|
||||||
|
public int PlayerMaxHp { get; set; }
|
||||||
|
public int EnemyCurrentHp { get; set; }
|
||||||
|
public int EnemyMaxHp { get; set; }
|
||||||
|
|
||||||
|
public int PlayerHpPhase => CalculateHpPhase(PlayerCurrentHp, PlayerMaxHp);
|
||||||
|
public int EnemyHpPhase => CalculateHpPhase(EnemyCurrentHp, EnemyMaxHp);
|
||||||
|
|
||||||
|
public CombatPose PlayerPose { get; set; } = CombatPose.Idle;
|
||||||
|
public CombatPose EnemyPose { get; set; } = CombatPose.Idle;
|
||||||
|
|
||||||
|
public MathProblem CurrentProblem { get; set; } = new();
|
||||||
|
public DateTime ProblemStartTime { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public int ComboStreak { get; set; } = 0;
|
||||||
|
public int SolvedInLevel { get; set; } = 0;
|
||||||
|
|
||||||
|
public bool IsDefensePhase { get; set; } = false;
|
||||||
|
public bool IsBattleOver { get; set; } = false;
|
||||||
|
public bool IsVictory { get; set; } = false;
|
||||||
|
|
||||||
|
public CombatResultType LastResult { get; set; } = CombatResultType.None;
|
||||||
|
public string LastFeedbackMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public static int CalculateHpPhase(int current, int max)
|
||||||
|
{
|
||||||
|
if (max <= 0) return 1;
|
||||||
|
double pct = (double)current / max * 100.0;
|
||||||
|
if (pct >= 75.0) return 1;
|
||||||
|
if (pct >= 50.0) return 2;
|
||||||
|
if (pct >= 25.0) return 3;
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace CiselnaVyprava.Models;
|
||||||
|
|
||||||
|
public class Hero
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public int BaseHp { get; set; } = 5;
|
||||||
|
public int AttackPower { get; set; } = 10;
|
||||||
|
public string AttackName { get; set; } = string.Empty;
|
||||||
|
public string SpecialAttackName { get; set; } = string.Empty;
|
||||||
|
public string AssetFolder { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Enemy
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int MaxHp { get; set; } = 30;
|
||||||
|
public int AttackPower { get; set; } = 1;
|
||||||
|
public string AssetFolder { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace CiselnaVyprava.Models;
|
||||||
|
|
||||||
|
public class GameConfig
|
||||||
|
{
|
||||||
|
public BattleConfig Battle { get; set; } = new();
|
||||||
|
public List<VisualPhaseConfig> VisualPhases { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BattleConfig
|
||||||
|
{
|
||||||
|
public double CriticalTimeSeconds { get; set; } = 3.0;
|
||||||
|
public double StandardTimeSeconds { get; set; } = 6.0;
|
||||||
|
public double CriticalDamageMultiplier { get; set; } = 1.5;
|
||||||
|
public double WeakDamageMultiplier { get; set; } = 0.5;
|
||||||
|
public double DefenseTestDurationSeconds { get; set; } = 5.0;
|
||||||
|
public int DefaultPlayerHp { get; set; } = 5;
|
||||||
|
public int ComboStreakBonusDmg { get; set; } = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VisualPhaseConfig
|
||||||
|
{
|
||||||
|
public int Phase { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int MinHpPercent { get; set; }
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
namespace CiselnaVyprava.Models;
|
||||||
|
|
||||||
|
public class World
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int Grade { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string Background { get; set; } = string.Empty;
|
||||||
|
public List<Level> Levels { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Level
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public int LevelNumber { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public bool IsBoss { get; set; } = false;
|
||||||
|
public Enemy Enemy { get; set; } = new();
|
||||||
|
public MathConfig MathConfig { get; set; } = new();
|
||||||
|
public int RequiredCorrectAnswers { get; set; } = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MathConfig
|
||||||
|
{
|
||||||
|
public List<string> Operations { get; set; } = new();
|
||||||
|
public int MinA { get; set; } = 1;
|
||||||
|
public int MaxA { get; set; } = 10;
|
||||||
|
public int MinB { get; set; } = 1;
|
||||||
|
public int MaxB { get; set; } = 10;
|
||||||
|
public int MaxSum { get; set; } = 20;
|
||||||
|
public bool AllowCarryOver { get; set; } = true;
|
||||||
|
public List<int>? Factors { get; set; }
|
||||||
|
public int MinMultiplier { get; set; } = 1;
|
||||||
|
public int MaxMultiplier { get; set; } = 10;
|
||||||
|
public string InputType { get; set; } = "DirectInput"; // DirectInput | MultipleChoice
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MathProblem
|
||||||
|
{
|
||||||
|
public string QuestionText { get; set; } = string.Empty;
|
||||||
|
public string DisplayA { get; set; } = string.Empty;
|
||||||
|
public string Operation { get; set; } = "+";
|
||||||
|
public string DisplayB { get; set; } = string.Empty;
|
||||||
|
public string CorrectAnswer { get; set; } = string.Empty;
|
||||||
|
public List<string> Choices { get; set; } = new();
|
||||||
|
public string InputType { get; set; } = "DirectInput";
|
||||||
|
public bool IsDefenseTest { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerProfile
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = "Mladý Hrdina";
|
||||||
|
public int SelectedGrade { get; set; } = 2;
|
||||||
|
public string SelectedHeroId { get; set; } = "knight";
|
||||||
|
public int TotalStars { get; set; } = 0;
|
||||||
|
public int Gold { get; set; } = 0;
|
||||||
|
public int CurrentWorldId { get; set; } = 1;
|
||||||
|
public HashSet<string> UnlockedLevels { get; set; } = new() { "2-1-1", "3-1-1" };
|
||||||
|
public HashSet<string> CompletedLevels { get; set; } = new();
|
||||||
|
public PlayerStats Stats { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlayerStats
|
||||||
|
{
|
||||||
|
public int TotalSolved { get; set; }
|
||||||
|
public int CorrectAnswers { get; set; }
|
||||||
|
public int CriticalHits { get; set; }
|
||||||
|
public int MaxStreak { get; set; }
|
||||||
|
}
|
||||||
@@ -1,7 +1,124 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
@inject IGameDataLoader DataLoader
|
||||||
|
@inject ILocalStorageService Storage
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<PageTitle>Číselná Výprava - Rozprávková Matematika</PageTitle>
|
||||||
|
|
||||||
<h1>Hello, world!</h1>
|
<div class="game-viewport @(CurrentScreen == ScreenType.MainMenu ? "viewport-fullscreen" : "")">
|
||||||
|
@if (IsLoading)
|
||||||
|
{
|
||||||
|
<div class="loading-screen">
|
||||||
|
<div class="parchment-box">
|
||||||
|
<h2>📜 Načítavam Číselné Kráľovstvo...</h2>
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (CurrentScreen == ScreenType.MainMenu)
|
||||||
|
{
|
||||||
|
<MainMenu Profile="Profile"
|
||||||
|
OnNewGame="() => CurrentScreen = ScreenType.CharacterSelect"
|
||||||
|
OnContinueGame="() => CurrentScreen = ScreenType.WorldMap"
|
||||||
|
OnResetGame="HandleResetGame" />
|
||||||
|
}
|
||||||
|
else if (CurrentScreen == ScreenType.CharacterSelect)
|
||||||
|
{
|
||||||
|
<CharacterSelect OnProfileReady="HandleProfileReady" />
|
||||||
|
}
|
||||||
|
else if (CurrentScreen == ScreenType.WorldMap)
|
||||||
|
{
|
||||||
|
<WorldMap Profile="Profile"
|
||||||
|
OnLevelSelected="HandleLevelSelected"
|
||||||
|
OnChangeHero="() => CurrentScreen = ScreenType.MainMenu" />
|
||||||
|
}
|
||||||
|
else if (CurrentScreen == ScreenType.Battle && SelectedLevel != null)
|
||||||
|
{
|
||||||
|
var hero = DataLoader.GetHero(Profile.SelectedHeroId) ?? DataLoader.Heroes.First();
|
||||||
|
<BattleArena Hero="hero"
|
||||||
|
CurrentLevel="SelectedLevel"
|
||||||
|
Config="DataLoader.Config"
|
||||||
|
OnReturnToMap="() => CurrentScreen = ScreenType.WorldMap"
|
||||||
|
OnLevelCompleted="HandleLevelCompleted" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
Welcome to your new app.
|
@code {
|
||||||
|
private enum ScreenType
|
||||||
|
{
|
||||||
|
Loading,
|
||||||
|
MainMenu,
|
||||||
|
CharacterSelect,
|
||||||
|
WorldMap,
|
||||||
|
Battle
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsLoading { get; set; } = true;
|
||||||
|
private ScreenType CurrentScreen { get; set; } = ScreenType.Loading;
|
||||||
|
private PlayerProfile Profile { get; set; } = new();
|
||||||
|
private Level? SelectedLevel { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DataLoader.InitializeAsync();
|
||||||
|
Profile = await Storage.LoadProfileAsync();
|
||||||
|
CurrentScreen = ScreenType.MainMenu;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleResetGame()
|
||||||
|
{
|
||||||
|
Profile = new PlayerProfile();
|
||||||
|
await Storage.SaveProfileAsync(Profile);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleProfileReady(PlayerProfile newProfile)
|
||||||
|
{
|
||||||
|
Profile = newProfile;
|
||||||
|
CurrentScreen = ScreenType.WorldMap;
|
||||||
|
await Storage.SaveProfileAsync(Profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLevelSelected(Level level)
|
||||||
|
{
|
||||||
|
SelectedLevel = level;
|
||||||
|
CurrentScreen = ScreenType.Battle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleLevelCompleted(Level completedLevel)
|
||||||
|
{
|
||||||
|
Profile.CompletedLevels.Add(completedLevel.Id);
|
||||||
|
Profile.TotalStars += completedLevel.IsBoss ? 3 : 1;
|
||||||
|
Profile.Gold += completedLevel.IsBoss ? 50 : 20;
|
||||||
|
|
||||||
|
// Unlock next level in current world
|
||||||
|
var worlds = DataLoader.GetWorldsForGrade(Profile.SelectedGrade);
|
||||||
|
var currentWorld = worlds.FirstOrDefault(w => w.Levels.Any(l => l.Id == completedLevel.Id));
|
||||||
|
if (currentWorld != null)
|
||||||
|
{
|
||||||
|
var currentIndex = currentWorld.Levels.FindIndex(l => l.Id == completedLevel.Id);
|
||||||
|
if (currentIndex + 1 < currentWorld.Levels.Count)
|
||||||
|
{
|
||||||
|
Profile.UnlockedLevels.Add(currentWorld.Levels[currentIndex + 1].Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unlock first level of next world
|
||||||
|
var nextWorldIndex = worlds.IndexOf(currentWorld) + 1;
|
||||||
|
if (nextWorldIndex < worlds.Count && worlds[nextWorldIndex].Levels.Any())
|
||||||
|
{
|
||||||
|
Profile.UnlockedLevels.Add(worlds[nextWorldIndex].Levels.First().Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Storage.SaveProfileAsync(Profile);
|
||||||
|
CurrentScreen = ScreenType.WorldMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,5 +7,10 @@ builder.RootComponents.Add<App>("#app");
|
|||||||
builder.RootComponents.Add<HeadOutlet>("head::after");
|
builder.RootComponents.Add<HeadOutlet>("head::after");
|
||||||
|
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
|
builder.Services.AddScoped<CiselnaVyprava.Services.IGameDataLoader, CiselnaVyprava.Services.GameDataLoader>();
|
||||||
|
builder.Services.AddScoped<CiselnaVyprava.Services.ILocalStorageService, CiselnaVyprava.Services.LocalStorageService>();
|
||||||
|
builder.Services.AddScoped<CiselnaVyprava.Services.IMathProblemGenerator, CiselnaVyprava.Services.MathProblemGenerator>();
|
||||||
|
builder.Services.AddScoped<CiselnaVyprava.Services.IBattleEngine, CiselnaVyprava.Services.BattleEngine>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
using CiselnaVyprava.Models;
|
||||||
|
|
||||||
|
namespace CiselnaVyprava.Services;
|
||||||
|
|
||||||
|
public interface IBattleEngine
|
||||||
|
{
|
||||||
|
BattleState State { get; }
|
||||||
|
void StartBattle(Hero hero, Level level, GameConfig config);
|
||||||
|
Task<CombatResultType> SubmitAnswerAsync(string answer);
|
||||||
|
Task<CombatResultType> SubmitDefenseAnswerAsync(string answer);
|
||||||
|
void NextProblem();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BattleEngine : IBattleEngine
|
||||||
|
{
|
||||||
|
private readonly IMathProblemGenerator _mathGenerator;
|
||||||
|
private GameConfig _config = new();
|
||||||
|
|
||||||
|
public BattleState State { get; private set; } = new();
|
||||||
|
|
||||||
|
public BattleEngine(IMathProblemGenerator mathGenerator)
|
||||||
|
{
|
||||||
|
_mathGenerator = mathGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartBattle(Hero hero, Level level, GameConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
State = new BattleState
|
||||||
|
{
|
||||||
|
Hero = hero,
|
||||||
|
Enemy = level.Enemy,
|
||||||
|
CurrentLevel = level,
|
||||||
|
PlayerMaxHp = hero.BaseHp,
|
||||||
|
PlayerCurrentHp = hero.BaseHp,
|
||||||
|
EnemyMaxHp = level.Enemy.MaxHp,
|
||||||
|
EnemyCurrentHp = level.Enemy.MaxHp,
|
||||||
|
PlayerPose = CombatPose.Idle,
|
||||||
|
EnemyPose = CombatPose.Idle,
|
||||||
|
ComboStreak = 0,
|
||||||
|
SolvedInLevel = 0,
|
||||||
|
IsBattleOver = false,
|
||||||
|
IsVictory = false,
|
||||||
|
IsDefensePhase = false,
|
||||||
|
LastResult = CombatResultType.None,
|
||||||
|
LastFeedbackMessage = string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
NextProblem();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NextProblem()
|
||||||
|
{
|
||||||
|
State.CurrentProblem = _mathGenerator.GenerateProblem(State.CurrentLevel.MathConfig);
|
||||||
|
State.ProblemStartTime = DateTime.UtcNow;
|
||||||
|
State.IsDefensePhase = false;
|
||||||
|
State.PlayerPose = CombatPose.Idle;
|
||||||
|
State.EnemyPose = CombatPose.Idle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CombatResultType> SubmitAnswerAsync(string answer)
|
||||||
|
{
|
||||||
|
var elapsed = (DateTime.UtcNow - State.ProblemStartTime).TotalSeconds;
|
||||||
|
bool isCorrect = string.Equals(answer.Trim(), State.CurrentProblem.CorrectAnswer.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isCorrect)
|
||||||
|
{
|
||||||
|
State.ComboStreak++;
|
||||||
|
State.SolvedInLevel++;
|
||||||
|
|
||||||
|
CombatResultType result;
|
||||||
|
double multiplier;
|
||||||
|
if (elapsed <= _config.Battle.CriticalTimeSeconds)
|
||||||
|
{
|
||||||
|
result = CombatResultType.CriticalHit;
|
||||||
|
multiplier = _config.Battle.CriticalDamageMultiplier;
|
||||||
|
State.LastFeedbackMessage = $"⚡ BLESKOVO! ({elapsed:F1}s) - KRITICKÝ ÚDER!";
|
||||||
|
}
|
||||||
|
else if (elapsed <= _config.Battle.StandardTimeSeconds)
|
||||||
|
{
|
||||||
|
result = CombatResultType.StandardHit;
|
||||||
|
multiplier = 1.0;
|
||||||
|
State.LastFeedbackMessage = $"⚔️ SKVELÝ ZÁSAH! ({elapsed:F1}s)";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = CombatResultType.WeakHit;
|
||||||
|
multiplier = _config.Battle.WeakDamageMultiplier;
|
||||||
|
State.LastFeedbackMessage = $"✨ ZÁSAH! ({elapsed:F1}s)";
|
||||||
|
}
|
||||||
|
|
||||||
|
int baseDmg = State.Hero.AttackPower;
|
||||||
|
int comboBonus = (State.ComboStreak - 1) * _config.Battle.ComboStreakBonusDmg;
|
||||||
|
int totalDamage = (int)(baseDmg * multiplier) + comboBonus;
|
||||||
|
|
||||||
|
State.EnemyCurrentHp = Math.Max(0, State.EnemyCurrentHp - totalDamage);
|
||||||
|
State.PlayerPose = CombatPose.Attack;
|
||||||
|
State.EnemyPose = CombatPose.Hurt;
|
||||||
|
State.LastResult = result;
|
||||||
|
|
||||||
|
if (State.EnemyCurrentHp <= 0)
|
||||||
|
{
|
||||||
|
State.IsBattleOver = true;
|
||||||
|
State.IsVictory = true;
|
||||||
|
State.LastResult = CombatResultType.Victory;
|
||||||
|
State.LastFeedbackMessage = $"🎉 VÍŤAZSTVO! {State.Enemy.Name} bol porazený!";
|
||||||
|
return CombatResultType.Victory;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Player made a mistake -> Trigger defense test
|
||||||
|
State.ComboStreak = 0;
|
||||||
|
State.PlayerPose = CombatPose.Hurt;
|
||||||
|
State.EnemyPose = CombatPose.Attack;
|
||||||
|
State.IsDefensePhase = true;
|
||||||
|
State.LastResult = CombatResultType.PlayerMissed;
|
||||||
|
State.LastFeedbackMessage = $"❌ Vedľa! Pozor na protiútok! Aktivuj štít!";
|
||||||
|
|
||||||
|
State.CurrentProblem = _mathGenerator.GenerateDefenseTest();
|
||||||
|
State.ProblemStartTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
return CombatResultType.PlayerMissed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CombatResultType> SubmitDefenseAnswerAsync(string answer)
|
||||||
|
{
|
||||||
|
bool isCorrect = string.Equals(answer.Trim(), State.CurrentProblem.CorrectAnswer.Trim(), StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (isCorrect)
|
||||||
|
{
|
||||||
|
State.PlayerPose = CombatPose.Defend;
|
||||||
|
State.EnemyPose = CombatPose.Idle;
|
||||||
|
State.LastResult = CombatResultType.DefenseSuccess;
|
||||||
|
State.LastFeedbackMessage = "🛡️ ÚTOK VYKRYTÝ ŠTÍTOM! Bez straty života.";
|
||||||
|
State.IsDefensePhase = false;
|
||||||
|
return CombatResultType.DefenseSuccess;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
State.PlayerCurrentHp = Math.Max(0, State.PlayerCurrentHp - State.Enemy.AttackPower);
|
||||||
|
State.PlayerPose = CombatPose.Hurt;
|
||||||
|
State.EnemyPose = CombatPose.Attack;
|
||||||
|
State.LastResult = CombatResultType.DefenseFailed;
|
||||||
|
State.LastFeedbackMessage = $"💥 Zásah! Stratil si 1 život! (Správne bolo: {State.CurrentProblem.CorrectAnswer})";
|
||||||
|
State.IsDefensePhase = false;
|
||||||
|
|
||||||
|
if (State.PlayerCurrentHp <= 0)
|
||||||
|
{
|
||||||
|
State.IsBattleOver = true;
|
||||||
|
State.IsVictory = false;
|
||||||
|
State.LastResult = CombatResultType.Defeat;
|
||||||
|
State.LastFeedbackMessage = "💔 Nevzdávaj sa! Skús to znova a poraz nepriateľa!";
|
||||||
|
return CombatResultType.Defeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CombatResultType.DefenseFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
using CiselnaVyprava.Models;
|
||||||
|
|
||||||
|
namespace CiselnaVyprava.Services;
|
||||||
|
|
||||||
|
public interface IGameDataLoader
|
||||||
|
{
|
||||||
|
Task InitializeAsync();
|
||||||
|
GameConfig Config { get; }
|
||||||
|
List<Hero> Heroes { get; }
|
||||||
|
List<World> GetWorldsForGrade(int grade);
|
||||||
|
Hero? GetHero(string id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GameDataLoader : IGameDataLoader
|
||||||
|
{
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
private GameConfig _config = new();
|
||||||
|
private List<Hero> _heroes = new();
|
||||||
|
private List<World> _grade1Worlds = new();
|
||||||
|
private List<World> _grade2Worlds = new();
|
||||||
|
private List<World> _grade3Worlds = new();
|
||||||
|
|
||||||
|
public GameDataLoader(HttpClient http)
|
||||||
|
{
|
||||||
|
_http = http;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameConfig Config => _config;
|
||||||
|
public List<Hero> Heroes => _heroes;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
try { _config = await _http.GetFromJsonAsync<GameConfig>("data/game-config.json") ?? new(); } catch { _config = new(); }
|
||||||
|
try { _heroes = await _http.GetFromJsonAsync<List<Hero>>("data/heroes.json") ?? new(); } catch { _heroes = new(); }
|
||||||
|
try { _grade1Worlds = await _http.GetFromJsonAsync<List<World>>("data/worlds-grade1.json") ?? new(); } catch { _grade1Worlds = new(); }
|
||||||
|
try { _grade2Worlds = await _http.GetFromJsonAsync<List<World>>("data/worlds-grade2.json") ?? new(); } catch { _grade2Worlds = new(); }
|
||||||
|
try { _grade3Worlds = await _http.GetFromJsonAsync<List<World>>("data/worlds-grade3.json") ?? new(); } catch { _grade3Worlds = new(); }
|
||||||
|
|
||||||
|
if (!_heroes.Any())
|
||||||
|
{
|
||||||
|
_heroes.Add(new Hero { Id = "knight", Name = "Rytier Pravítko", Title = "Ochranca čísiel", Description = "Odvážny rytier s mečom pravdy.", BaseHp = 5, AttackPower = 10, AssetFolder = "assets/images/heroes/knight" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<World> GetWorldsForGrade(int grade)
|
||||||
|
{
|
||||||
|
return grade switch
|
||||||
|
{
|
||||||
|
1 => _grade1Worlds,
|
||||||
|
3 => _grade3Worlds,
|
||||||
|
_ => _grade2Worlds
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Hero? GetHero(string id)
|
||||||
|
{
|
||||||
|
return _heroes.FirstOrDefault(h => h.Id.Equals(id, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? _heroes.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using CiselnaVyprava.Models;
|
||||||
|
|
||||||
|
namespace CiselnaVyprava.Services;
|
||||||
|
|
||||||
|
public interface ILocalStorageService
|
||||||
|
{
|
||||||
|
Task<PlayerProfile> LoadProfileAsync();
|
||||||
|
Task SaveProfileAsync(PlayerProfile profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LocalStorageService : ILocalStorageService
|
||||||
|
{
|
||||||
|
private const string ProfileStorageKey = "CiselnaVyprava_PlayerProfile";
|
||||||
|
private readonly IJSRuntime _js;
|
||||||
|
|
||||||
|
public LocalStorageService(IJSRuntime js)
|
||||||
|
{
|
||||||
|
_js = js;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PlayerProfile> LoadProfileAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await _js.InvokeAsync<string?>("localStorage.getItem", ProfileStorageKey);
|
||||||
|
if (!string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
var profile = JsonSerializer.Deserialize<PlayerProfile>(json, options);
|
||||||
|
if (profile != null) return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback upon any browser error
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PlayerProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveProfileAsync(PlayerProfile profile)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(profile);
|
||||||
|
await _js.InvokeVoidAsync("localStorage.setItem", ProfileStorageKey, json);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignored if storage is blocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
using CiselnaVyprava.Models;
|
||||||
|
|
||||||
|
namespace CiselnaVyprava.Services;
|
||||||
|
|
||||||
|
public interface IMathProblemGenerator
|
||||||
|
{
|
||||||
|
MathProblem GenerateProblem(MathConfig config);
|
||||||
|
MathProblem GenerateDefenseTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MathProblemGenerator : IMathProblemGenerator
|
||||||
|
{
|
||||||
|
private readonly Random _rand = new();
|
||||||
|
|
||||||
|
public MathProblem GenerateProblem(MathConfig config)
|
||||||
|
{
|
||||||
|
var op = config.Operations[_rand.Next(config.Operations.Count)];
|
||||||
|
|
||||||
|
return op switch
|
||||||
|
{
|
||||||
|
"+" => GenerateAddition(config),
|
||||||
|
"-" => GenerateSubtraction(config),
|
||||||
|
"*" => GenerateMultiplication(config),
|
||||||
|
"/" => GenerateDivision(config),
|
||||||
|
"<" or ">" or "=" => GenerateComparison(config),
|
||||||
|
_ => GenerateAddition(config)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private MathProblem GenerateAddition(MathConfig config)
|
||||||
|
{
|
||||||
|
int a, b;
|
||||||
|
if (config.AllowCarryOver)
|
||||||
|
{
|
||||||
|
a = _rand.Next(config.MinA, config.MaxA + 1);
|
||||||
|
b = _rand.Next(config.MinB, Math.Min(config.MaxB, config.MaxSum - a) + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
a = _rand.Next(config.MinA, config.MaxA + 1);
|
||||||
|
b = _rand.Next(config.MinB, config.MaxB + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sum = a + b;
|
||||||
|
var problem = new MathProblem
|
||||||
|
{
|
||||||
|
DisplayA = a.ToString(),
|
||||||
|
Operation = "+",
|
||||||
|
DisplayB = b.ToString(),
|
||||||
|
CorrectAnswer = sum.ToString(),
|
||||||
|
QuestionText = $"{a} + {b} = ?",
|
||||||
|
InputType = config.InputType
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.InputType == "MultipleChoice")
|
||||||
|
{
|
||||||
|
problem.Choices = GenerateNumericChoices(sum, 1, config.MaxSum);
|
||||||
|
}
|
||||||
|
|
||||||
|
return problem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MathProblem GenerateSubtraction(MathConfig config)
|
||||||
|
{
|
||||||
|
int b = _rand.Next(config.MinB, config.MaxB + 1);
|
||||||
|
int a = _rand.Next(Math.Max(b, config.MinA), config.MaxA + 1);
|
||||||
|
int diff = a - b;
|
||||||
|
|
||||||
|
var problem = new MathProblem
|
||||||
|
{
|
||||||
|
DisplayA = a.ToString(),
|
||||||
|
Operation = "-",
|
||||||
|
DisplayB = b.ToString(),
|
||||||
|
CorrectAnswer = diff.ToString(),
|
||||||
|
QuestionText = $"{a} - {b} = ?",
|
||||||
|
InputType = config.InputType
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.InputType == "MultipleChoice")
|
||||||
|
{
|
||||||
|
problem.Choices = GenerateNumericChoices(diff, 0, config.MaxA);
|
||||||
|
}
|
||||||
|
|
||||||
|
return problem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MathProblem GenerateMultiplication(MathConfig config)
|
||||||
|
{
|
||||||
|
int factorA;
|
||||||
|
if (config.Factors != null && config.Factors.Count > 0)
|
||||||
|
{
|
||||||
|
factorA = config.Factors[_rand.Next(config.Factors.Count)];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
factorA = _rand.Next(2, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
int factorB = _rand.Next(config.MinMultiplier, config.MaxMultiplier + 1);
|
||||||
|
int product = factorA * factorB;
|
||||||
|
|
||||||
|
var problem = new MathProblem
|
||||||
|
{
|
||||||
|
DisplayA = factorA.ToString(),
|
||||||
|
Operation = "×",
|
||||||
|
DisplayB = factorB.ToString(),
|
||||||
|
CorrectAnswer = product.ToString(),
|
||||||
|
QuestionText = $"{factorA} × {factorB} = ?",
|
||||||
|
InputType = config.InputType
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.InputType == "MultipleChoice")
|
||||||
|
{
|
||||||
|
problem.Choices = GenerateNumericChoices(product, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return problem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MathProblem GenerateDivision(MathConfig config)
|
||||||
|
{
|
||||||
|
int divisor;
|
||||||
|
if (config.Factors != null && config.Factors.Count > 0)
|
||||||
|
{
|
||||||
|
divisor = config.Factors[_rand.Next(config.Factors.Count)];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
divisor = _rand.Next(2, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
int result = _rand.Next(config.MinMultiplier, config.MaxMultiplier + 1);
|
||||||
|
int dividend = divisor * result;
|
||||||
|
|
||||||
|
var problem = new MathProblem
|
||||||
|
{
|
||||||
|
DisplayA = dividend.ToString(),
|
||||||
|
Operation = "÷",
|
||||||
|
DisplayB = divisor.ToString(),
|
||||||
|
CorrectAnswer = result.ToString(),
|
||||||
|
QuestionText = $"{dividend} ÷ {divisor} = ?",
|
||||||
|
InputType = config.InputType
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.InputType == "MultipleChoice")
|
||||||
|
{
|
||||||
|
problem.Choices = GenerateNumericChoices(result, 1, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return problem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MathProblem GenerateComparison(MathConfig config)
|
||||||
|
{
|
||||||
|
int a = _rand.Next(config.MinA, config.MaxA + 1);
|
||||||
|
int b = _rand.Next(config.MinB, config.MaxB + 1);
|
||||||
|
|
||||||
|
string answer = a < b ? "<" : (a > b ? ">" : "=");
|
||||||
|
|
||||||
|
return new MathProblem
|
||||||
|
{
|
||||||
|
DisplayA = a.ToString(),
|
||||||
|
Operation = "?",
|
||||||
|
DisplayB = b.ToString(),
|
||||||
|
CorrectAnswer = answer,
|
||||||
|
QuestionText = $"{a} ? {b}",
|
||||||
|
InputType = "MultipleChoice",
|
||||||
|
Choices = new List<string> { "<", "=", ">" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public MathProblem GenerateDefenseTest()
|
||||||
|
{
|
||||||
|
// Simple True/False defense shield challenge
|
||||||
|
int a = _rand.Next(1, 10);
|
||||||
|
int b = _rand.Next(1, 10);
|
||||||
|
bool isCorrect = _rand.Next(2) == 0;
|
||||||
|
int displayedResult = isCorrect ? (a + b) : (a + b + (_rand.Next(2) == 0 ? 1 : -1));
|
||||||
|
if (displayedResult <= 0) displayedResult = a + b + 2;
|
||||||
|
|
||||||
|
return new MathProblem
|
||||||
|
{
|
||||||
|
DisplayA = $"{a} + {b} = {displayedResult}",
|
||||||
|
Operation = "",
|
||||||
|
DisplayB = "",
|
||||||
|
CorrectAnswer = isCorrect ? "ÁNO" : "NIE",
|
||||||
|
QuestionText = $"Je to správne?\n{a} + {b} = {displayedResult}",
|
||||||
|
InputType = "MultipleChoice",
|
||||||
|
Choices = new List<string> { "ÁNO", "NIE" },
|
||||||
|
IsDefenseTest = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GenerateNumericChoices(int correct, int min, int max)
|
||||||
|
{
|
||||||
|
var set = new HashSet<int> { correct };
|
||||||
|
while (set.Count < 3)
|
||||||
|
{
|
||||||
|
int offset = _rand.Next(-5, 6);
|
||||||
|
if (offset == 0) offset = 1;
|
||||||
|
int candidate = Math.Clamp(correct + offset, min, max);
|
||||||
|
set.Add(candidate);
|
||||||
|
}
|
||||||
|
return set.OrderBy(_ => _rand.Next()).Select(x => x.ToString()).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@using System.Net.Http
|
@using System.Net.Http
|
||||||
@using System.Net.Http.Json
|
@using System.Net.Http.Json
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@@ -8,3 +8,6 @@
|
|||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using CiselnaVyprava
|
@using CiselnaVyprava
|
||||||
@using CiselnaVyprava.Layout
|
@using CiselnaVyprava.Layout
|
||||||
|
@using CiselnaVyprava.Models
|
||||||
|
@using CiselnaVyprava.Services
|
||||||
|
@using CiselnaVyprava.Components
|
||||||
|
|||||||
|
After Width: | Height: | Size: 981 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 966 KiB |
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"battle": {
|
||||||
|
"criticalTimeSeconds": 3.0,
|
||||||
|
"standardTimeSeconds": 6.0,
|
||||||
|
"criticalDamageMultiplier": 1.5,
|
||||||
|
"weakDamageMultiplier": 0.5,
|
||||||
|
"defenseTestDurationSeconds": 5.0,
|
||||||
|
"defaultPlayerHp": 5,
|
||||||
|
"comboStreakBonusDmg": 1
|
||||||
|
},
|
||||||
|
"visualPhases": [
|
||||||
|
{ "phase": 1, "name": "FullHealth", "minHpPercent": 75, "description": "Plná sila a úsmev" },
|
||||||
|
{ "phase": 2, "name": "Focused", "minHpPercent": 50, "description": "Bojové nasadenie a sústredenie" },
|
||||||
|
{ "phase": 3, "name": "Fatigued", "minHpPercent": 25, "description": "Zadýchaný, leukoplast" },
|
||||||
|
{ "phase": 4, "name": "Critical", "minHpPercent": 1, "description": "Kritický stav, motajúce sa hviezdičky" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "knight",
|
||||||
|
"name": "Rytier Pravítko",
|
||||||
|
"title": "Ochranca čísiel",
|
||||||
|
"description": "Odvážny rytier s mečom pravdy a štítom obrany.",
|
||||||
|
"baseHp": 5,
|
||||||
|
"attackPower": 10,
|
||||||
|
"attackName": "Úder pravítkom",
|
||||||
|
"specialAttackName": "Kritické seknutie",
|
||||||
|
"assetFolder": "assets/images/heroes/knight"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mage",
|
||||||
|
"name": "Mág Číselník",
|
||||||
|
"title": "Pán matematických kúziel",
|
||||||
|
"description": "Ovláda bleskovú algebru a mrazivé rovnice.",
|
||||||
|
"baseHp": 5,
|
||||||
|
"attackPower": 10,
|
||||||
|
"attackName": "Ohnivá rovnica",
|
||||||
|
"specialAttackName": "Bleskový výpočet",
|
||||||
|
"assetFolder": "assets/images/heroes/mage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "archer",
|
||||||
|
"name": "Lučostrelec Stovka",
|
||||||
|
"title": "Majster presnosti",
|
||||||
|
"description": "Jeho šípy zasahujú presné výsledky na meter presne.",
|
||||||
|
"baseHp": 5,
|
||||||
|
"attackPower": 10,
|
||||||
|
"attackName": "Presný šíp",
|
||||||
|
"specialAttackName": "Bleskový trojitý výstrel",
|
||||||
|
"assetFolder": "assets/images/heroes/archer"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"grade": 1,
|
||||||
|
"name": "Rozprávková lúka (Počítanie do 10)",
|
||||||
|
"description": "Jednoduché sčítanie a odčítanie do 10 pre malých prváčikov.",
|
||||||
|
"background": "assets/images/backgrounds/world1_meadow.jpg",
|
||||||
|
"levels": [
|
||||||
|
{
|
||||||
|
"id": "1-1-1",
|
||||||
|
"levelNumber": 1,
|
||||||
|
"name": "Lúčny Motýlik",
|
||||||
|
"enemy": {
|
||||||
|
"id": "meadow_butterfly",
|
||||||
|
"name": "Lúčny Motýlik",
|
||||||
|
"maxHp": 20,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/goblin"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["+"],
|
||||||
|
"minA": 1,
|
||||||
|
"maxA": 5,
|
||||||
|
"minB": 1,
|
||||||
|
"maxB": 5,
|
||||||
|
"maxSum": 10,
|
||||||
|
"allowCarryOver": false,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-1-2",
|
||||||
|
"levelNumber": 2,
|
||||||
|
"name": "Zvedavý Zajačik",
|
||||||
|
"enemy": {
|
||||||
|
"id": "curious_bunny",
|
||||||
|
"name": "Zvedavý Zajačik",
|
||||||
|
"maxHp": 25,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/slime"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["+", "-"],
|
||||||
|
"minA": 2,
|
||||||
|
"maxA": 10,
|
||||||
|
"minB": 1,
|
||||||
|
"maxB": 5,
|
||||||
|
"maxSum": 10,
|
||||||
|
"allowCarryOver": false,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "1-1-3",
|
||||||
|
"levelNumber": 3,
|
||||||
|
"name": "Lesný Medveď (Boss)",
|
||||||
|
"isBoss": true,
|
||||||
|
"enemy": {
|
||||||
|
"id": "gentle_bear_boss",
|
||||||
|
"name": "Strážca Lúky",
|
||||||
|
"maxHp": 40,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/golem"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["+", "-"],
|
||||||
|
"minA": 3,
|
||||||
|
"maxA": 10,
|
||||||
|
"minB": 1,
|
||||||
|
"maxB": 5,
|
||||||
|
"maxSum": 10,
|
||||||
|
"allowCarryOver": false,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"grade": 1,
|
||||||
|
"name": "Slnečná čistinka (Počítanie do 20 bez prechodu)",
|
||||||
|
"description": "Sčítanie a odčítanie v obore do 20 (napr. 10 + 4, 15 - 3).",
|
||||||
|
"background": "assets/images/backgrounds/world2_forest.jpg",
|
||||||
|
"levels": [
|
||||||
|
{
|
||||||
|
"id": "1-2-1",
|
||||||
|
"levelNumber": 1,
|
||||||
|
"name": "Lesný Škriatok",
|
||||||
|
"enemy": {
|
||||||
|
"id": "forest_gnome",
|
||||||
|
"name": "Lesný Škriatok",
|
||||||
|
"maxHp": 30,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/goblin"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["+", "-"],
|
||||||
|
"minA": 10,
|
||||||
|
"maxA": 19,
|
||||||
|
"minB": 1,
|
||||||
|
"maxB": 5,
|
||||||
|
"maxSum": 20,
|
||||||
|
"allowCarryOver": false,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"grade": 2,
|
||||||
|
"name": "Lúka sčítania a odčítania",
|
||||||
|
"description": "Základné sčítanie a odčítanie do 20 s prechodom cez desiatku.",
|
||||||
|
"background": "assets/images/backgrounds/world1_meadow.jpg",
|
||||||
|
"levels": [
|
||||||
|
{
|
||||||
|
"id": "2-1-1",
|
||||||
|
"levelNumber": 1,
|
||||||
|
"name": "Škriatok Počtárik",
|
||||||
|
"enemy": {
|
||||||
|
"id": "goblin_apprentice",
|
||||||
|
"name": "Škriatok Počtárik",
|
||||||
|
"maxHp": 30,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/goblin"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["+"],
|
||||||
|
"minA": 1,
|
||||||
|
"maxA": 10,
|
||||||
|
"minB": 1,
|
||||||
|
"maxB": 10,
|
||||||
|
"maxSum": 20,
|
||||||
|
"allowCarryOver": true,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2-1-2",
|
||||||
|
"levelNumber": 2,
|
||||||
|
"name": "Lesný Slizík",
|
||||||
|
"enemy": {
|
||||||
|
"id": "forest_slime",
|
||||||
|
"name": "Lesný Slizík",
|
||||||
|
"maxHp": 40,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/slime"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["+", "-"],
|
||||||
|
"minA": 5,
|
||||||
|
"maxA": 20,
|
||||||
|
"minB": 1,
|
||||||
|
"maxB": 10,
|
||||||
|
"maxSum": 20,
|
||||||
|
"allowCarryOver": true,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2-1-3",
|
||||||
|
"levelNumber": 3,
|
||||||
|
"name": "Kamenný Golem (Boss)",
|
||||||
|
"isBoss": true,
|
||||||
|
"enemy": {
|
||||||
|
"id": "stone_golem_boss",
|
||||||
|
"name": "Golem Čísiel",
|
||||||
|
"maxHp": 60,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/golem"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["+", "-"],
|
||||||
|
"minA": 8,
|
||||||
|
"maxA": 20,
|
||||||
|
"minB": 4,
|
||||||
|
"maxB": 10,
|
||||||
|
"maxSum": 20,
|
||||||
|
"allowCarryOver": true,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"grade": 2,
|
||||||
|
"name": "Les porovnávania a rovností",
|
||||||
|
"description": "Porovnávanie čísel pomocou znakov <, >, = a dopĺňanie do desiatky.",
|
||||||
|
"background": "assets/images/backgrounds/world2_forest.jpg",
|
||||||
|
"levels": [
|
||||||
|
{
|
||||||
|
"id": "2-2-1",
|
||||||
|
"levelNumber": 1,
|
||||||
|
"name": "Múdra Sova",
|
||||||
|
"enemy": {
|
||||||
|
"id": "wise_owl",
|
||||||
|
"name": "Múdra Sova",
|
||||||
|
"maxHp": 30,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/owl"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["<", ">", "="],
|
||||||
|
"minA": 1,
|
||||||
|
"maxA": 100,
|
||||||
|
"minB": 1,
|
||||||
|
"maxB": 100,
|
||||||
|
"inputType": "MultipleChoice"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"grade": 3,
|
||||||
|
"name": "Rieka malej násobilky (2, 3, 4, 5)",
|
||||||
|
"description": "Základy násobenia a delenia číslami 2, 3, 4, 5.",
|
||||||
|
"background": "assets/images/backgrounds/world3_river.jpg",
|
||||||
|
"levels": [
|
||||||
|
{
|
||||||
|
"id": "3-1-1",
|
||||||
|
"levelNumber": 1,
|
||||||
|
"name": "Riečny Škriatok",
|
||||||
|
"enemy": {
|
||||||
|
"id": "river_sprite",
|
||||||
|
"name": "Riečny Škriatok",
|
||||||
|
"maxHp": 30,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/sprite"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["*"],
|
||||||
|
"factors": [2, 3, 4, 5],
|
||||||
|
"minMultiplier": 1,
|
||||||
|
"maxMultiplier": 10,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3-1-2",
|
||||||
|
"levelNumber": 2,
|
||||||
|
"name": "Vodník Násobka",
|
||||||
|
"enemy": {
|
||||||
|
"id": "water_demon",
|
||||||
|
"name": "Vodník Násobka",
|
||||||
|
"maxHp": 40,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/water_demon"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["*", "/"],
|
||||||
|
"factors": [2, 3, 4, 5],
|
||||||
|
"minMultiplier": 1,
|
||||||
|
"maxMultiplier": 10,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3-1-3",
|
||||||
|
"levelNumber": 3,
|
||||||
|
"name": "Jazerný Drak (Boss)",
|
||||||
|
"isBoss": true,
|
||||||
|
"enemy": {
|
||||||
|
"id": "lake_dragon_boss",
|
||||||
|
"name": "Jazerný Drak",
|
||||||
|
"maxHp": 60,
|
||||||
|
"attackPower": 1,
|
||||||
|
"assetFolder": "assets/images/enemies/dragon"
|
||||||
|
},
|
||||||
|
"mathConfig": {
|
||||||
|
"operations": ["*", "/"],
|
||||||
|
"factors": [2, 3, 4, 5],
|
||||||
|
"minMultiplier": 1,
|
||||||
|
"maxMultiplier": 10,
|
||||||
|
"inputType": "DirectInput"
|
||||||
|
},
|
||||||
|
"requiredCorrectAnswers": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -4,30 +4,39 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>CiselnaVyprava</title>
|
<title>Číselná Výprava</title>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Fredoka:wght@400;600;700&family=Nunito:wght@400;700;900&display=swap" rel="stylesheet">
|
||||||
<link rel="preload" id="webassembly" />
|
<link rel="preload" id="webassembly" />
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
<link rel="stylesheet" href="css/app.css" />
|
||||||
<!-- If you add any scoped CSS files, uncomment the following to load them
|
<link href="CiselnaVyprava.styles.css" rel="stylesheet" />
|
||||||
<link href="CiselnaVyprava.styles.css" rel="stylesheet" /> -->
|
|
||||||
<script type="importmap"></script>
|
<script type="importmap"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<svg class="loading-progress">
|
<div class="wasm-loader-container">
|
||||||
<circle r="40%" cx="50%" cy="50%" />
|
<div class="wasm-loader-box">
|
||||||
<circle r="40%" cx="50%" cy="50%" />
|
<div class="wasm-crown">👑</div>
|
||||||
</svg>
|
<h1 class="wasm-title">ČÍSELNÁ VÝPRAVA</h1>
|
||||||
<div class="loading-progress-text"></div>
|
<p class="wasm-subtitle">Načítavam rozprávkový svet...</p>
|
||||||
|
<div class="wasm-progress-bar">
|
||||||
|
<div class="wasm-progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="blazor-error-ui">
|
<div id="blazor-error-ui">
|
||||||
An unhandled error has occurred.
|
Nastala neočakávaná chyba.
|
||||||
<a href="." class="reload">Reload</a>
|
<a href="." class="reload">Obnoviť hru</a>
|
||||||
<span class="dismiss">🗙</span>
|
<span class="dismiss">🗙</span>
|
||||||
</div>
|
</div>
|
||||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
<script src="_framework/blazor.webassembly.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |