Bladeren bron

update game

student 3 weken geleden
bovenliggende
commit
58ccb4bfa8

+ 95 - 26
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/Business/Game/GameBusinessImpl.cs

@@ -15,6 +15,10 @@ namespace Kitty_Fall.Apis.Business.Game
     public class GameBusinessImpl : IGameBusiness
     {
         private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(GameBusinessImpl));
+        private const int MinGameLevel = 1;
+        private const int DbMaxGameLevel = 50;
+        private const long MaxAttemptScore = int.MaxValue;
+        private const long MaxTotalScore = 999999999999;
 
         private readonly ModelContext dbContext;
         private readonly IConfiguration configuration;
@@ -85,6 +89,9 @@ namespace Kitty_Fall.Apis.Business.Game
                     .Include(x => x.GameRewardLog)
                     .Where(x => x.Msisdn == msisdn
                         && x.Status == 2
+                        && x.RewardType != null
+                        && x.RewardType != ""
+                        && (x.RewardValue ?? 0) > 0
                         && ((x.SubmitTime ?? x.StartTime) >= fromDate)
                         && ((x.SubmitTime ?? x.StartTime) < toExclusive))
                     .OrderByDescending(x => x.SubmitTime ?? x.StartTime)
@@ -582,9 +589,13 @@ namespace Kitty_Fall.Apis.Business.Game
                 }
 
                 var now = DateTime.Now;
-                var levelNo = (byte)Math.Clamp(request.level <= 0 ? 1 : request.level, 1, 255);
                 var mode = await GetGameModeAsync(request.gameType);
                 if (mode == null) return CommonLogic.BuildResponse(url, json, CommonErrorCode.Error, "Game mode is not configured", new { });
+                var levelNo = NormalizeRequestedLevel(request.level);
+                if (levelNo == null || !await IsConfiguredLevelAsync(mode.ModeId, levelNo.Value))
+                {
+                    return CommonLogic.BuildResponse(url, json, CommonErrorCode.Error, "Game level is not configured", new { });
+                }
 
                 var account = await dbContext.AccountUsers.AsNoTracking()
                     .Where(x => x.Msisdn == msisdn)
@@ -604,9 +615,9 @@ namespace Kitty_Fall.Apis.Business.Game
                         Msisdn = msisdn,
                         UserId = account?.Id,
                         ModeId = mode.ModeId,
-                        StartLevel = levelNo,
-                        CurrentLevel = levelNo,
-                        MaxLevelReached = levelNo,
+                        StartLevel = levelNo.Value,
+                        CurrentLevel = levelNo.Value,
+                        MaxLevelReached = levelNo.Value,
                         TotalScore = 0,
                         TotalRewardScore = 0,
                         TotalDataMb = 0,
@@ -633,7 +644,7 @@ namespace Kitty_Fall.Apis.Business.Game
                     }
 
                     session.TurnDeducted = true;
-                    session.TurnSource = turn.ChannelAdd;
+                    session.TurnSource = ToTurnSource(turn.ChannelAdd);
                     session.TurnRefId = turn.Id.ToString(CultureInfo.InvariantCulture);
                 }
 
@@ -685,9 +696,13 @@ namespace Kitty_Fall.Apis.Business.Game
                 }
 
                 var now = DateTime.Now;
-                var levelNo = (byte)Math.Clamp(request.level <= 0 ? 1 : request.level, 1, 255);
                 var mode = await GetGameModeAsync(request.gameType);
                 if (mode == null) return CommonLogic.BuildResponse(url, json, CommonErrorCode.Error, "Game mode is not configured", new { });
+                var levelNo = NormalizeRequestedLevel(request.level);
+                if (levelNo == null)
+                {
+                    return CommonLogic.BuildResponse(url, json, CommonErrorCode.Error, "Game level is not configured", new { });
+                }
 
                 var account = await dbContext.AccountUsers.AsNoTracking()
                     .Where(x => x.Msisdn == msisdn)
@@ -727,25 +742,35 @@ namespace Kitty_Fall.Apis.Business.Game
                 }
 
                 var levelConfig = await dbContext.GameLevelConfigs.AsNoTracking()
-                    .Where(x => x.ModeId == mode.ModeId && x.LevelNo == levelNo && x.Status != false)
+                    .Where(x => x.ModeId == mode.ModeId && x.LevelNo == levelNo.Value && x.Status != false)
                     .FirstOrDefaultAsync();
+                if (levelConfig == null)
+                {
+                    return CommonLogic.BuildResponse(url, json, CommonErrorCode.Error, "Game level is not configured", new { });
+                }
 
-                var clientScore = request.score < 0 ? 0 : request.score;
+                var clientScore = ClampScore(request.score, MaxAttemptScore);
                 var awardedScore = clientScore > session.TotalScore
                     ? clientScore - session.TotalScore
                     : clientScore;
-                if (awardedScore <= 0) awardedScore = levelConfig?.BaseScore ?? 0;
+                if (awardedScore <= 0) awardedScore = levelConfig.BaseScore;
+                awardedScore = Math.Min(awardedScore, MaxAttemptScore);
 
                 // Chon cau hinh thuong cho mode/level, uu tien default roi den weight cao.
                 var rewardOption = await dbContext.GameLevelRewardOptions.AsNoTracking()
-                    .Where(x => x.ModeId == mode.ModeId && x.LevelNo == levelNo && x.Status != false)
+                    .Where(x => x.ModeId == mode.ModeId && x.LevelNo == levelNo.Value && x.Status != false)
                     .OrderByDescending(x => x.IsDefault == true)
                     .ThenByDescending(x => x.Weight)
                     .ThenBy(x => x.RewardOptionId)
                     .FirstOrDefaultAsync();
+                if (rewardOption == null)
+                {
+                    // Level da cau hinh de choi nhung chua co giai, khong ghi history bi trong prize.
+                    return CommonLogic.BuildResponse(url, json, CommonErrorCode.Error, "Game reward is not configured", new { });
+                }
 
                 var attemptNo = ((await dbContext.GameLevelAttempts.AsNoTracking()
-                    .Where(x => x.SessionId == session.SessionId && x.LevelNo == levelNo)
+                    .Where(x => x.SessionId == session.SessionId && x.LevelNo == levelNo.Value)
                     .MaxAsync(x => (short?)x.AttemptNo)) ?? 0) + 1;
                 var childTransId = $"{request.transId}-{levelNo}-{attemptNo}";
 
@@ -757,9 +782,9 @@ namespace Kitty_Fall.Apis.Business.Game
                     ChildTransId = childTransId,
                     Msisdn = msisdn,
                     ModeId = mode.ModeId,
-                    LevelNo = levelNo,
+                    LevelNo = levelNo.Value,
                     AttemptNo = (short)attemptNo,
-                    ExpectedScore = levelConfig?.BaseScore ?? 0,
+                    ExpectedScore = levelConfig.BaseScore,
                     ClientScore = (int)Math.Min(int.MaxValue, clientScore),
                     AwardedScore = (int)Math.Min(int.MaxValue, awardedScore),
                     RewardOptionId = rewardOption?.RewardOptionId,
@@ -776,9 +801,9 @@ namespace Kitty_Fall.Apis.Business.Game
                 };
                 dbContext.GameLevelAttempts.Add(attempt);
 
-                session.CurrentLevel = levelNo;
-                session.MaxLevelReached = Math.Max(session.MaxLevelReached, levelNo);
-                session.TotalScore += awardedScore;
+                session.CurrentLevel = levelNo.Value;
+                session.MaxLevelReached = Math.Max(session.MaxLevelReached, levelNo.Value);
+                session.TotalScore = ClampScore(session.TotalScore + awardedScore, MaxTotalScore);
                 session.LastActionTime = now;
                 session.UpdatedTime = now;
 
@@ -791,7 +816,7 @@ namespace Kitty_Fall.Apis.Business.Game
                     AttemptId = attempt.AttemptId,
                     RootTransId = session.RootTransId,
                     ChildTransId = childTransId,
-                    ScoreType = "PLAY",
+                    ScoreType = "LEVEL",
                     ScoreDelta = awardedScore,
                     BalanceAfter = session.TotalScore,
                     Description = $"Passed level {levelNo}",
@@ -809,8 +834,26 @@ namespace Kitty_Fall.Apis.Business.Game
                     {
                         rewardScoreDelta = (long)rewardOption.RewardValue;
                         session.TotalRewardScore += rewardScoreDelta;
-                        session.TotalScore += rewardScoreDelta;
+                        session.TotalScore = ClampScore(session.TotalScore + rewardScoreDelta, MaxTotalScore);
                         responsePoint = FormatPrizeValue(rewardOption.RewardValue);
+
+                        dbContext.GameScoreLedgers.Add(new GameScoreLedger
+                        {
+                            LedgerId = await NextDecimalIdAsync("LEDGER"),
+                            Msisdn = msisdn,
+                            UserId = account?.Id,
+                            SessionId = session.SessionId,
+                            AttemptId = attempt.AttemptId,
+                            RootTransId = session.RootTransId,
+                            ChildTransId = childTransId,
+                            ScoreType = "REWARD",
+                            ScoreDelta = rewardScoreDelta,
+                            BalanceAfter = session.TotalScore,
+                            Description = $"Reward score for level {levelNo}",
+                            CreatedTime = now,
+                            CreatedDateKey = ToDateKey(now),
+                            CreatedMonthKey = ToMonthKey(now)
+                        });
                     }
                     else if (string.Equals(rewardOption.RewardType, "DATA", StringComparison.OrdinalIgnoreCase))
                     {
@@ -833,7 +876,7 @@ namespace Kitty_Fall.Apis.Business.Game
                         RootTransId = session.RootTransId,
                         ChildTransId = childTransId,
                         ModeId = mode.ModeId,
-                        LevelNo = levelNo,
+                        LevelNo = levelNo.Value,
                         RewardOptionId = rewardOption.RewardOptionId,
                         RewardType = rewardOption.RewardType,
                         RewardValue = rewardOption.RewardValue,
@@ -891,6 +934,21 @@ namespace Kitty_Fall.Apis.Business.Game
             return await DbLogic.GenIdAsync(dbContext, sequence) ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
         }
 
+        private static byte? NormalizeRequestedLevel(int level)
+        {
+            // Level thuc te phai ton tai trong cau hinh DB, 50 chi la gioi han constraint DB.
+            var requestedLevel = level <= 0 ? MinGameLevel : level;
+            if (requestedLevel < MinGameLevel || requestedLevel > DbMaxGameLevel) return null;
+            return (byte)requestedLevel;
+        }
+
+        private static long ClampScore(long score, long maxScore)
+        {
+            // Diem tu client khong duoc vuot precision NUMBER cua bang game.
+            if (score < 0) return 0;
+            return score > maxScore ? maxScore : score;
+        }
+
         private async Task<GameMode?> GetGameModeAsync(string? gameType)
         {
             var modeCode = (gameType ?? "EASY").Trim().ToUpperInvariant();
@@ -907,6 +965,13 @@ namespace Kitty_Fall.Apis.Business.Game
                 .FirstOrDefaultAsync();
         }
 
+        private async Task<bool> IsConfiguredLevelAsync(decimal modeId, byte levelNo)
+        {
+            // Moi loai game co so level rieng, lay theo GAME_LEVEL_CONFIG dang active.
+            return await dbContext.GameLevelConfigs.AsNoTracking()
+                .AnyAsync(x => x.ModeId == modeId && x.LevelNo == levelNo && x.Status != false);
+        }
+
         private async Task<int> GetAvailableTurnAsync(string msisdn, DateTime now)
         {
             var total = await dbContext.LuckySpins.AsNoTracking()
@@ -983,6 +1048,17 @@ namespace Kitty_Fall.Apis.Business.Game
             return value.Length <= maxLength ? value : value[..maxLength];
         }
 
+        private static string? ToTurnSource(string? channelAdd)
+        {
+            var value = (channelAdd ?? "").Trim().ToUpperInvariant();
+            if (value.Contains("SUB") || value.Contains("REG")) return "SUB";
+            if (value.Contains("BUY") || value.Contains("CHARGE")) return "BUY";
+            if (value.Contains("FREE")) return "FREE";
+            if (value.Contains("GIFT")) return "GIFT";
+            if (value.Contains("ADMIN")) return "ADMIN";
+            return null;
+        }
+
         private static DateTime? ParseHistoryDate(string? value)
         {
             if (string.IsNullOrWhiteSpace(value)) return null;
@@ -1046,13 +1122,6 @@ namespace Kitty_Fall.Apis.Business.Game
         private string ToLocalMsisdn(string? msisdn)
         {
             var value = new string((msisdn ?? "").Where(char.IsDigit).ToArray());
-            var countryCode = new string(GetParameter("CountryCode").Where(char.IsDigit).ToArray());
-
-            if (!string.IsNullOrWhiteSpace(countryCode) && value.StartsWith(countryCode, StringComparison.Ordinal))
-            {
-                value = "0" + value[countryCode.Length..];
-            }
-
             return value;
         }
 

+ 8 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Controllers/GameController.cs

@@ -59,7 +59,11 @@ namespace Kitty_Fall.Website.Controllers
                         point = apiResult.data.point,
                         data = apiResult.data.data,
                         score = apiResult.data.score,
-                        transId = apiResult.data.transId
+                        transId = apiResult.data.transId,
+                        rewardType = apiResult.data.rewardType,
+                        rewardValue = apiResult.data.rewardValue,
+                        rewardUnit = apiResult.data.rewardUnit,
+                        success = apiResult.errorCode == "0"
                     });
                 }
 
@@ -69,6 +73,9 @@ namespace Kitty_Fall.Website.Controllers
                     data = "",
                     score = request.score.ToString(),
                     transId = request.transId ?? "",
+                    rewardType = "",
+                    rewardValue = 0,
+                    rewardUnit = "",
                     success = false,
                     message = apiResult.message ?? "Save level result failed."
                 });

+ 30 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/Views/Play/Index.cshtml

@@ -106,4 +106,34 @@
             <strong>@Lang.PlayNow</strong>
         </button>
     </div>
+
+    <section class="game-over-modal" data-game-over-modal aria-hidden="true">
+        <div class="game-over-backdrop"></div>
+        <div class="game-over-card">
+            <button class="game-over-close" type="button" data-game-over-close aria-label="Close">×</button>
+            <img class="game-over-cat" src="~/kitty/assets/kitty-fall/cat.png" alt="">
+            <h2>Game Over</h2>
+            <p>@Lang.Rewards</p>
+            <div class="game-over-summary">
+                <div>
+                    <span>@Lang.Score</span>
+                    <strong data-game-over-score>0</strong>
+                </div>
+                <div>
+                    <span>DATA</span>
+                    <strong data-game-over-data>0</strong>
+                </div>
+                <div>
+                    <span>@Lang.Points</span>
+                    <strong data-game-over-point>0</strong>
+                </div>
+            </div>
+            <a class="game-over-play-button" href="@Url.Action("Index", "Play")">
+                <span class="play-icon">
+                    <img src="~/kitty/assets/kitty-fall/play-icon.svg" alt="">
+                </span>
+                <strong>@Lang.PlayNow</strong>
+            </a>
+        </div>
+    </section>
 </main>

+ 22 - 29
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/wwwroot/catpuzzle/scripts/project/main.js

@@ -107,7 +107,6 @@ function Tick(runtime)
     }
 
     // ===== GAME OVER (CHỈ 1 LẦN) =====
-    /*
 	if (
         runtime.globalVars.gameOver === 1 &&
         lastGameOver === 0
@@ -121,7 +120,6 @@ function Tick(runtime)
     {
         lastGameOver = 0;
     }
-	*/
 
 	// ===== Exit game =====
     if (
@@ -198,6 +196,20 @@ async function SaveLevelResult(runtime)
 			result.transId ?? "";
 
 		runtime.globalVars.showPrizePopup = 1;
+		window.parent.postMessage(
+			{
+				type: "LEVEL_REWARD",
+				level: runtime.globalVars.cur_level,
+				point: result.point ?? "",
+				data: result.data ?? "",
+				score: result.score ?? "",
+				transId: result.transId ?? "",
+				rewardType: result.rewardType ?? "",
+				rewardValue: result.rewardValue ?? 0,
+				rewardUnit: result.rewardUnit ?? ""
+			},
+			"*"
+		);
 		console.log("Chuẩn bị gọi vào function trong event sheet:");
 		//runtime.callFunction("Quangbh");
 		console.log("Da goi goi vao function event sheet xong nhe!!!");
@@ -259,33 +271,16 @@ async function SaveGameOver(runtime)
 	try
 	{
 		console.log("Calling GameOver...");
-
-		const response = await fetch(
-			runtime.globalVars.root+"/Game/GameOver",
+		window.parent.postMessage(
 			{
-				method: "POST",
-				headers:
-				{
-					"Content-Type": "application/json"
-				},
-				body: JSON.stringify(
-				{
-					token: runtime.globalVars.token,
-					msisdn: runtime.globalVars.msisdn,
-					transId: runtime.globalVars.transId,
-					score: runtime.globalVars.score,
-					gameType: runtime.globalVars.gameType
-				})
-			}
+				type: "GAME_OVER",
+				level: runtime.globalVars.cur_level,
+				score: runtime.globalVars.score,
+				gameType: runtime.globalVars.gameType,
+				transId: runtime.globalVars.transId
+			},
+			"*"
 		);
-
-		const result = await response.json();
-
-		console.log("GameOver Result:", result);
-
-		runtime.globalVars.finalPrize =
-			result.finalPrize ?? "";
-
 		runtime.globalVars.showGameOverPopup = 1;
 	}
 	catch (error)
@@ -361,5 +356,3 @@ function MovePlayer(offsetX, offsetY)
     player.y += offsetY;
 }
 
-
-

File diff suppressed because it is too large
+ 1207 - 1813
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/wwwroot/kitty/css/all.min.css


+ 143 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/wwwroot/kitty/css/input.css

@@ -2700,6 +2700,149 @@ button {
     display: block;
 }
 
+.game-over-modal {
+    position: absolute;
+    z-index: 30;
+    inset: 0;
+    display: grid;
+    place-items: center;
+    padding: 22px;
+    visibility: hidden;
+    opacity: 0;
+    pointer-events: none;
+    transition: opacity 0.18s ease, visibility 0.18s ease;
+}
+
+.game-over-modal.is-open {
+    visibility: visible;
+    opacity: 1;
+    pointer-events: auto;
+}
+
+.game-over-backdrop {
+    position: absolute;
+    inset: 0;
+    background: rgba(255, 220, 235, 0.42);
+    -webkit-backdrop-filter: blur(6px);
+    backdrop-filter: blur(6px);
+}
+
+.game-over-card {
+    position: relative;
+    width: min(100%, 348px);
+    min-height: 390px;
+    border: 3px solid #e63bc9;
+    border-radius: 28px;
+    padding: 84px 20px 22px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    background: rgba(255, 248, 251, 0.94);
+    box-shadow: 0 5px 0 rgba(207, 61, 144, 0.24);
+    text-align: center;
+}
+
+.game-over-close {
+    position: absolute;
+    z-index: 2;
+    top: -20px;
+    right: -18px;
+    width: 42px;
+    height: 42px;
+    border: 3px solid #e63bc9;
+    border-radius: 50%;
+    display: grid;
+    place-items: center;
+    background: linear-gradient(180deg, #ff6bd4 0%, #e83fb3 100%);
+    color: #fff;
+    font-size: 34px;
+    line-height: 0.85;
+    font-weight: 900;
+    box-shadow: 0 3px 0 rgba(107, 32, 130, 0.24);
+    text-shadow: 0 2px 0 rgba(107, 32, 130, 0.2);
+}
+
+.game-over-cat {
+    position: absolute;
+    top: -46px;
+    width: 118px;
+    height: 118px;
+    object-fit: contain;
+}
+
+.game-over-card h2 {
+    margin: 0;
+    color: #ff2b7c;
+    font-size: 38px;
+    line-height: 1;
+    font-weight: 900;
+    text-transform: uppercase;
+    text-shadow: 0 3px 0 rgba(117, 43, 170, 0.2);
+}
+
+.game-over-card p {
+    margin: 10px 0 14px;
+    color: #5621e8;
+    font-size: 15px;
+    font-weight: 900;
+}
+
+.game-over-summary {
+    width: 100%;
+    display: grid;
+    gap: 10px;
+}
+
+.game-over-summary div {
+    height: 56px;
+    border: 2px solid #ff99df;
+    border-radius: 16px;
+    display: grid;
+    grid-template-columns: 1fr auto;
+    align-items: center;
+    padding: 0 16px;
+    background: rgba(255, 255, 255, 0.86);
+}
+
+.game-over-summary span {
+    color: #5721ea;
+    font-size: 13px;
+    font-weight: 900;
+    text-align: left;
+    text-transform: uppercase;
+}
+
+.game-over-summary strong {
+    color: #e83fb3;
+    font-size: 22px;
+    line-height: 1;
+    font-weight: 900;
+}
+
+.game-over-play-button {
+    width: min(100%, 240px);
+    height: 52px;
+    margin-top: 20px;
+    border: 0;
+    border-radius: 999px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    background: linear-gradient(180deg, #ff55df 0%, #d91ac5 100%);
+    color: #fff;
+    text-decoration: none;
+    box-shadow: inset 0 4px 0 rgba(255, 255, 255, 0.35), 0 4px 0 rgba(126, 36, 148, 0.26);
+}
+
+.game-over-play-button strong {
+    color: #fff;
+    font-size: 22px;
+    font-weight: 900;
+    text-transform: uppercase;
+    text-shadow: 0 2px 2px rgba(0, 0, 0, 0.25);
+}
+
 .subscription-modal {
     position: absolute;
     z-index: 20;

+ 94 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/wwwroot/kitty/js/all.min.js

@@ -252,11 +252,22 @@ const initKittyFall = () => {
         const startGameButton = playGameRoot.querySelector("[data-start-game]");
         const gameShell = playGameRoot.querySelector("[data-game-shell]");
         const gameFrame = playGameRoot.querySelector("[data-game-frame]");
+        const gameOverModal = playGameRoot.querySelector("[data-game-over-modal]");
+        const gameOverClose = playGameRoot.querySelector("[data-game-over-close]");
+        const gameOverScore = playGameRoot.querySelector("[data-game-over-score]");
+        const gameOverData = playGameRoot.querySelector("[data-game-over-data]");
+        const gameOverPoint = playGameRoot.querySelector("[data-game-over-point]");
         let selectedLevel = playGameRoot.querySelector("[data-level].is-selected")?.dataset.level
             || levelButtons[0]?.dataset.level
             || "Easy";
         let gameStarted = false;
         let startingGame = false;
+        let finishingGame = false;
+        let runRewards = {
+            score: 0,
+            data: 0,
+            point: 0
+        };
 
         const setSelectedLevel = (button) => {
             selectedLevel = button.dataset.level || selectedLevel;
@@ -267,10 +278,86 @@ const initKittyFall = () => {
             });
         };
 
+        const resetRunRewards = () => {
+            runRewards = {
+                score: 0,
+                data: 0,
+                point: 0
+            };
+        };
+
+        const toNumber = (value) => {
+            const normalized = String(value ?? "0").replace(/[^\d.-]/g, "");
+            const parsed = Number(normalized);
+            return Number.isFinite(parsed) ? parsed : 0;
+        };
+
+        const formatNumber = (value) => Math.max(0, Math.round(value)).toLocaleString("en-US");
+
+        const addLevelReward = (data) => {
+            const rewardType = String(data?.rewardType || "").toUpperCase();
+            const rewardValue = toNumber(data?.rewardValue);
+
+            runRewards.score = Math.max(runRewards.score, toNumber(data?.score));
+            if (rewardType === "DATA") runRewards.data += rewardValue;
+            if (rewardType === "LOYALTY" || rewardType === "SCORE") runRewards.point += rewardValue;
+        };
+
+        const closeEmbeddedGame = () => {
+            gameStarted = false;
+            playGameRoot.classList.remove("is-game-running");
+            gameShell?.setAttribute("aria-hidden", "true");
+            if (gameFrame) gameFrame.removeAttribute("src");
+        };
+
+        const showGameOverSummary = () => {
+            if (gameOverScore) gameOverScore.textContent = formatNumber(runRewards.score);
+            if (gameOverData) gameOverData.textContent = `${formatNumber(runRewards.data)} MB`;
+            if (gameOverPoint) gameOverPoint.textContent = formatNumber(runRewards.point);
+            gameOverModal?.classList.add("is-open");
+            gameOverModal?.setAttribute("aria-hidden", "false");
+        };
+
+        const hideGameOverSummary = () => {
+            gameOverModal?.classList.remove("is-open");
+            gameOverModal?.setAttribute("aria-hidden", "true");
+        };
+
+        const saveGameOver = async (data) => {
+            if (finishingGame) return;
+            finishingGame = true;
+
+            try {
+                await fetch(`${playGameRoot.dataset.root || ""}/Game/SaveLevelResult`, {
+                    method: "POST",
+                    headers: {
+                        "Content-Type": "application/json"
+                    },
+                    body: JSON.stringify({
+                        msisdn: playGameRoot.dataset.msisdn || "",
+                        token: playGameRoot.dataset.token || "",
+                        transId: playGameRoot.dataset.transId || "",
+                        gameType: selectedLevel,
+                        level: data?.level || 1,
+                        score: data?.score || runRewards.score || 0,
+                        gameOver: 1
+                    })
+                });
+            } catch (error) {
+                console.error("Save game over error:", error);
+            } finally {
+                closeEmbeddedGame();
+                showGameOverSummary();
+                finishingGame = false;
+            }
+        };
+
         levelButtons.forEach((button) => {
             button.addEventListener("click", () => setSelectedLevel(button));
         });
 
+        gameOverClose?.addEventListener("click", hideGameOverSummary);
+
         const sendGameInit = () => {
             if (!gameFrame?.contentWindow) return;
 
@@ -289,6 +376,12 @@ const initKittyFall = () => {
             if (event.data?.type === "GAME_READY") {
                 sendGameInit();
             }
+            if (event.data?.type === "LEVEL_REWARD") {
+                addLevelReward(event.data);
+            }
+            if (event.data?.type === "GAME_OVER") {
+                saveGameOver(event.data);
+            }
         });
 
         const startSession = async () => {
@@ -324,6 +417,7 @@ const initKittyFall = () => {
                     return;
                 }
 
+                resetRunRewards();
                 gameStarted = true;
                 playGameRoot.classList.add("is-game-running");
                 gameShell.setAttribute("aria-hidden", "false");

+ 94 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Website/wwwroot/kitty/js/input.js

@@ -252,11 +252,22 @@ const initKittyFall = () => {
         const startGameButton = playGameRoot.querySelector("[data-start-game]");
         const gameShell = playGameRoot.querySelector("[data-game-shell]");
         const gameFrame = playGameRoot.querySelector("[data-game-frame]");
+        const gameOverModal = playGameRoot.querySelector("[data-game-over-modal]");
+        const gameOverClose = playGameRoot.querySelector("[data-game-over-close]");
+        const gameOverScore = playGameRoot.querySelector("[data-game-over-score]");
+        const gameOverData = playGameRoot.querySelector("[data-game-over-data]");
+        const gameOverPoint = playGameRoot.querySelector("[data-game-over-point]");
         let selectedLevel = playGameRoot.querySelector("[data-level].is-selected")?.dataset.level
             || levelButtons[0]?.dataset.level
             || "Easy";
         let gameStarted = false;
         let startingGame = false;
+        let finishingGame = false;
+        let runRewards = {
+            score: 0,
+            data: 0,
+            point: 0
+        };
 
         const setSelectedLevel = (button) => {
             selectedLevel = button.dataset.level || selectedLevel;
@@ -267,10 +278,86 @@ const initKittyFall = () => {
             });
         };
 
+        const resetRunRewards = () => {
+            runRewards = {
+                score: 0,
+                data: 0,
+                point: 0
+            };
+        };
+
+        const toNumber = (value) => {
+            const normalized = String(value ?? "0").replace(/[^\d.-]/g, "");
+            const parsed = Number(normalized);
+            return Number.isFinite(parsed) ? parsed : 0;
+        };
+
+        const formatNumber = (value) => Math.max(0, Math.round(value)).toLocaleString("en-US");
+
+        const addLevelReward = (data) => {
+            const rewardType = String(data?.rewardType || "").toUpperCase();
+            const rewardValue = toNumber(data?.rewardValue);
+
+            runRewards.score = Math.max(runRewards.score, toNumber(data?.score));
+            if (rewardType === "DATA") runRewards.data += rewardValue;
+            if (rewardType === "LOYALTY" || rewardType === "SCORE") runRewards.point += rewardValue;
+        };
+
+        const closeEmbeddedGame = () => {
+            gameStarted = false;
+            playGameRoot.classList.remove("is-game-running");
+            gameShell?.setAttribute("aria-hidden", "true");
+            if (gameFrame) gameFrame.removeAttribute("src");
+        };
+
+        const showGameOverSummary = () => {
+            if (gameOverScore) gameOverScore.textContent = formatNumber(runRewards.score);
+            if (gameOverData) gameOverData.textContent = `${formatNumber(runRewards.data)} MB`;
+            if (gameOverPoint) gameOverPoint.textContent = formatNumber(runRewards.point);
+            gameOverModal?.classList.add("is-open");
+            gameOverModal?.setAttribute("aria-hidden", "false");
+        };
+
+        const hideGameOverSummary = () => {
+            gameOverModal?.classList.remove("is-open");
+            gameOverModal?.setAttribute("aria-hidden", "true");
+        };
+
+        const saveGameOver = async (data) => {
+            if (finishingGame) return;
+            finishingGame = true;
+
+            try {
+                await fetch(`${playGameRoot.dataset.root || ""}/Game/SaveLevelResult`, {
+                    method: "POST",
+                    headers: {
+                        "Content-Type": "application/json"
+                    },
+                    body: JSON.stringify({
+                        msisdn: playGameRoot.dataset.msisdn || "",
+                        token: playGameRoot.dataset.token || "",
+                        transId: playGameRoot.dataset.transId || "",
+                        gameType: selectedLevel,
+                        level: data?.level || 1,
+                        score: data?.score || runRewards.score || 0,
+                        gameOver: 1
+                    })
+                });
+            } catch (error) {
+                console.error("Save game over error:", error);
+            } finally {
+                closeEmbeddedGame();
+                showGameOverSummary();
+                finishingGame = false;
+            }
+        };
+
         levelButtons.forEach((button) => {
             button.addEventListener("click", () => setSelectedLevel(button));
         });
 
+        gameOverClose?.addEventListener("click", hideGameOverSummary);
+
         const sendGameInit = () => {
             if (!gameFrame?.contentWindow) return;
 
@@ -289,6 +376,12 @@ const initKittyFall = () => {
             if (event.data?.type === "GAME_READY") {
                 sendGameInit();
             }
+            if (event.data?.type === "LEVEL_REWARD") {
+                addLevelReward(event.data);
+            }
+            if (event.data?.type === "GAME_OVER") {
+                saveGameOver(event.data);
+            }
         });
 
         const startSession = async () => {
@@ -324,6 +417,7 @@ const initKittyFall = () => {
                     return;
                 }
 
+                resetRunRewards();
                 gameStarted = true;
                 playGameRoot.classList.add("is-game-running");
                 gameShell.setAttribute("aria-hidden", "false");

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