init
@@ -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.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();
|
||||
|
||||
|
||||
@@ -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 Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@@ -8,3 +8,6 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using CiselnaVyprava
|
||||
@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>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CiselnaVyprava</title>
|
||||
<title>Číselná Výprava</title>
|
||||
<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="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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
<div class="wasm-loader-container">
|
||||
<div class="wasm-loader-box">
|
||||
<div class="wasm-crown">👑</div>
|
||||
<h1 class="wasm-title">ČÍSELNÁ VÝPRAVA</h1>
|
||||
<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 id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="." class="reload">Reload</a>
|
||||
Nastala neočakávaná chyba.
|
||||
<a href="." class="reload">Obnoviť hru</a>
|
||||
<span class="dismiss">🗙</span>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
</html>
|
||||
|
||||