This commit is contained in:
Michal Vrba
2026-09-21 17:45:24 +02:00
parent 33733ccd41
commit 0a03721a86
137 changed files with 2992 additions and 86 deletions
+175
View File
@@ -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);
}
}
+63
View File
@@ -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;
}
}
+23
View File
@@ -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;
}
+26
View File
@@ -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;
}
+70
View File
@@ -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; }
}
+121 -4
View File
@@ -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;
}
}
+5
View File
@@ -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();
+163
View File
@@ -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();
}
}
+4 -1
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 981 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 966 KiB

File diff suppressed because it is too large Load Diff
@@ -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
}
]
}
]
+20 -11
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Some files were not shown because too many files have changed in this diff Show More