322 lines
15 KiB
C#
322 lines
15 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using BlockOutReplica.Presentation;
|
|
using UnityEngine;
|
|
|
|
namespace BlockOutReplica.Gameplay
|
|
{
|
|
public sealed partial class BlockOutGameController
|
|
{
|
|
private BlockOutPlayerState player;
|
|
private BlockOutEconomy economy;
|
|
private GameModal modalReturn;
|
|
private bool lifeCharged, completionStarted, completionCommitted, winReturnsHome;
|
|
private int playOnCount, wonLevel, awardedReward;
|
|
private float freezeRemaining;
|
|
private BlockOutGameAudio gameAudio;
|
|
public GameModal Modal { get; private set; }
|
|
public BoosterType SelectedBooster { get; private set; }
|
|
public BoosterType PurchaseBooster { get; private set; }
|
|
public string Notice { get; private set; }
|
|
public float FreezeRemaining => freezeRemaining;
|
|
public bool HasStarted => timerRunning;
|
|
public int WonLevel => wonLevel;
|
|
public int WinReward => awardedReward;
|
|
public int LevelRewardPreview => economy.RegularLevelWinCoinReward;
|
|
public int ContinueCost => economy.ContinuePrice(playOnCount);
|
|
public int LifeRefillCost => economy.refillPrice;
|
|
public int BoosterCost => economy.Price(PurchaseBooster);
|
|
public int PriceOfBooster(BoosterType type) => economy.Price(type);
|
|
public bool AllLevelsCompleted => progress != null && progress.CatalogCompleted;
|
|
public BlockOutPlayerState Player
|
|
{
|
|
get
|
|
{
|
|
if (player == null) { economy = BlockOutEconomy.Load(); player = new BlockOutPlayerState(economy); }
|
|
return player;
|
|
}
|
|
}
|
|
|
|
private void InitializeInteractions()
|
|
{
|
|
_ = Player;
|
|
gameAudio = GetComponent<BlockOutGameAudio>();
|
|
if (gameAudio == null) gameAudio = gameObject.AddComponent<BlockOutGameAudio>();
|
|
gameAudio.Initialize(player);
|
|
}
|
|
|
|
private void ResetRunInteractions()
|
|
{
|
|
Modal = GameModal.None; SelectedBooster = BoosterType.None;
|
|
BoosterPhase = "";
|
|
BoosterPlaybackTime=0; FreezeAnimationTime=0;
|
|
ActiveBoosterParticleCount=0;ActiveFreezeParticleCount=0;HasNativeFreezeScreen=false;
|
|
Notice = ""; freezeRemaining = 0f; playOnCount = 0;
|
|
lifeCharged = completionStarted = completionCommitted = false;
|
|
awardedReward = 0;
|
|
if (gameAudio != null) gameAudio.SetHome(false);
|
|
}
|
|
|
|
private void AdvanceClock(float seconds)
|
|
{
|
|
if (player != null) player.RefreshLives();
|
|
if (!timerRunning || IsPaused || ScreenState != BlockOutScreenState.Playing ||
|
|
Modal != GameModal.None || SelectedBooster != BoosterType.None || pieces.Count == 0) return;
|
|
float frozen = Mathf.Min(freezeRemaining, seconds);
|
|
freezeRemaining -= frozen;
|
|
TimeLeft = Mathf.Max(0f, TimeLeft - (seconds - frozen));
|
|
if (TimeLeft <= 0f && activeExits == 0 && !completionStarted)
|
|
{
|
|
CancelDragForMenu();
|
|
SelectedBooster = BoosterType.None;
|
|
OpenModal(GameModal.OutOfTime);
|
|
Status = "时间到,可以续关或放弃本局";
|
|
PlayUiSound("Timer_Warning");
|
|
}
|
|
}
|
|
|
|
private void CancelDragForMenu()
|
|
{
|
|
if (draggingPiece == null) return;
|
|
draggingPiece.SetDragging(false);
|
|
SetDoorHighlight(draggingPiece.ColorId, false);
|
|
draggingPiece = null; hasPreviousPointerPosition = false;
|
|
}
|
|
|
|
private void SetPaused(bool value)
|
|
{
|
|
IsPaused = value;
|
|
foreach (DoorView door in doors) if (door != null) door.IsEffectPaused = value;
|
|
if(boardRoot!=null)
|
|
foreach(var curtain in boardRoot.GetComponentsInChildren<OriginalCurtainLockView>())curtain.IsPaused=value;
|
|
}
|
|
|
|
private void OpenModal(GameModal modal)
|
|
{
|
|
CancelDragForMenu();
|
|
Modal = modal; Notice = "";
|
|
SetPaused(ScreenState == BlockOutScreenState.Playing);
|
|
}
|
|
|
|
public void CloseModal()
|
|
{
|
|
if (Modal == GameModal.OutOfTime) { OpenModal(GameModal.GiveUp); return; }
|
|
if (Modal == GameModal.Failed) { GoHome(); return; }
|
|
if (Modal == GameModal.Restart || Modal == GameModal.Quit)
|
|
{ GameModal back = modalReturn; Modal = GameModal.None; if (back != GameModal.None) { OpenModal(back); return; } }
|
|
else if (Modal == GameModal.GiveUp) { OpenModal(GameModal.OutOfTime); return; }
|
|
else Modal = GameModal.None;
|
|
Notice = ""; SetPaused(false);
|
|
}
|
|
|
|
public void RequestPause()
|
|
{
|
|
if (ScreenState != BlockOutScreenState.Playing || completionStarted) return;
|
|
if (Modal == GameModal.Pause) { CloseModal(); return; }
|
|
if (Modal != GameModal.None) return;
|
|
SelectedBooster = BoosterType.None;
|
|
OpenModal(GameModal.Pause);
|
|
}
|
|
|
|
public void RequestRestart()
|
|
{
|
|
if (ScreenState != BlockOutScreenState.Playing || completionStarted) return;
|
|
if (Modal != GameModal.None && Modal != GameModal.Pause) return;
|
|
modalReturn = Modal; SelectedBooster = BoosterType.None; OpenModal(GameModal.Restart);
|
|
}
|
|
|
|
public void RequestQuit()
|
|
{
|
|
if (ScreenState != BlockOutScreenState.Playing || completionStarted) return;
|
|
if (Modal != GameModal.None && Modal != GameModal.Pause) return;
|
|
modalReturn = Modal; SelectedBooster = BoosterType.None; OpenModal(GameModal.Quit);
|
|
}
|
|
|
|
private void ChargeLifeOnce(bool force=false)
|
|
{
|
|
if (lifeCharged || (!timerRunning&&!force)) return;
|
|
Player.LoseLife(); lifeCharged = true;
|
|
}
|
|
|
|
public void ConfirmRestart()
|
|
{
|
|
if (Modal != GameModal.Restart) return;
|
|
ChargeLifeOnce();
|
|
if (Player.Lives <= 0 && !Player.HasInfiniteLives) { GoHome(); OpenModal(GameModal.Lives); return; }
|
|
LoadCurrentLevel();
|
|
}
|
|
|
|
public void ConfirmQuit()
|
|
{
|
|
if (Modal != GameModal.Quit) return;
|
|
// Original L10 runtime (2026-10-20): even a freshly entered board
|
|
// with no drag goes through FAILED; closing FAILED returns Home.
|
|
ChargeLifeOnce(true);
|
|
SelectedBooster=BoosterType.None;
|
|
ScreenState=BlockOutScreenState.Failed;Modal=GameModal.Failed;SetPaused(true);
|
|
}
|
|
|
|
public void ContinueWithCoins()
|
|
{
|
|
if (Modal != GameModal.OutOfTime && Modal != GameModal.GiveUp) return;
|
|
if (!Player.SpendCoins(ContinueCost)) { Notice = "Not enough coins"; return; }
|
|
playOnCount++; TimeLeft += economy.PlayOnExtraTimeSeconds;
|
|
Modal = GameModal.None; Notice = ""; SetPaused(false); timerRunning = true;
|
|
Status = "+30 seconds";
|
|
}
|
|
|
|
public void GiveUpRun()
|
|
{
|
|
if (Modal != GameModal.GiveUp) return;
|
|
ChargeLifeOnce();
|
|
SelectedBooster = BoosterType.None;
|
|
ScreenState = BlockOutScreenState.Failed;
|
|
Modal = GameModal.Failed; SetPaused(true);
|
|
}
|
|
|
|
public void RetryFailedLevel()
|
|
{
|
|
if (Modal != GameModal.Failed || ScreenState != BlockOutScreenState.Failed) return;
|
|
if (Player.Lives <= 0 && !Player.HasInfiniteLives) { GoHome(); OpenModal(GameModal.Lives); return; }
|
|
LoadCurrentLevel();
|
|
}
|
|
|
|
public void RequestEnterLevel()
|
|
{
|
|
if (ScreenState != BlockOutScreenState.Home || Modal != GameModal.None) return;
|
|
if (AllLevelsCompleted) { OpenModal(GameModal.Info); Notice = "All recovered levels completed"; return; }
|
|
OpenModal(Player.Lives > 0 || Player.HasInfiniteLives ? GameModal.PreLevel : GameModal.Lives);
|
|
}
|
|
|
|
public void EnterPreparedLevel()
|
|
{
|
|
if (ScreenState != BlockOutScreenState.Home || Modal != GameModal.PreLevel) return;
|
|
if (Player.Lives <= 0 && !Player.HasInfiniteLives) { OpenModal(GameModal.Lives); return; }
|
|
LoadCurrentLevel();
|
|
#if UNITY_EDITOR
|
|
if(IsManualLoopSandbox)TimeLeft=1800; // UI review only; automated real-board loop retains 180s.
|
|
#endif
|
|
}
|
|
|
|
public void GoHome(bool settleJourney = true)
|
|
{
|
|
StopAllCoroutines(); CancelDragForMenu(); ClearGenerated();
|
|
pieces.Clear(); doors.Clear(); activeExits = 0;
|
|
SelectedBooster = BoosterType.None; Modal = GameModal.None;
|
|
InputLocked = false; SetPaused(false); ScreenState = BlockOutScreenState.Home;
|
|
JourneyTab = false; JourneyRevision++;
|
|
timerRunning = false; freezeRemaining = 0f; Status = "";
|
|
ActiveBoosterParticleCount=0;ActiveFreezeParticleCount=0;HasNativeFreezeScreen=false;
|
|
if (gameAudio != null) gameAudio.SetHome(true);
|
|
if (settleJourney) SettleJourneyOnHome();
|
|
}
|
|
|
|
public void RequestLives()
|
|
{ if (ScreenState == BlockOutScreenState.Home && Modal == GameModal.None) OpenModal(GameModal.Lives); }
|
|
public void BuyLifeRefill()
|
|
{
|
|
if (Modal != GameModal.Lives) return;
|
|
Notice = Player.Lives == Player.MaxLives ? "Lives are full" : Player.RefillLives() ? "Lives refilled" : "Not enough coins";
|
|
}
|
|
public void RequestSettings()
|
|
{ if (ScreenState == BlockOutScreenState.Home && Modal == GameModal.None) OpenModal(GameModal.Settings); }
|
|
public void RequestCoinInfo()
|
|
{ if (ScreenState == BlockOutScreenState.Home && Modal == GameModal.None) { OpenModal(GameModal.Info); Notice = "Coins are earned by winning levels. Real-money purchases are not connected."; } }
|
|
public void ToggleSounds() { if (Modal != GameModal.Settings && Modal != GameModal.Pause) return; Player.ToggleSounds(); gameAudio?.ApplySettings(); }
|
|
public void ToggleMusic() { if (Modal != GameModal.Settings && Modal != GameModal.Pause) return; Player.ToggleMusic(); gameAudio?.ApplySettings(); }
|
|
public void ToggleHaptics() { if (Modal != GameModal.Settings && Modal != GameModal.Pause) return; Player.ToggleHaptics(); }
|
|
public void PlayUiSound(string name = "ui-click-v1") { if (gameAudio != null) gameAudio.Play(name); }
|
|
|
|
public void ClickBooster(BoosterType type)
|
|
{
|
|
if (type == BoosterType.None || ScreenState != BlockOutScreenState.Playing || Modal != GameModal.None ||
|
|
IsPaused || InputLocked || activeExits != 0 || TimeLeft <= 0f || completionStarted) return;
|
|
CancelDragForMenu();
|
|
if (SelectedBooster == type) { SelectedBooster = BoosterType.None; Status = ""; return; }
|
|
SelectedBooster = BoosterType.None;
|
|
if (type == BoosterType.Freeze && freezeRemaining > 0f) { Status = "Freeze is already active"; return; }
|
|
if (!Player.CanUseBooster(type))
|
|
{ ShowBoosterFundsNotice(type); return; }
|
|
if (type == BoosterType.Freeze)
|
|
{
|
|
if (!Player.SpendBoosterUse(type)) return;
|
|
freezeRemaining = economy.TimeFreezeDurationAsSeconds; timerRunning = true;
|
|
Status = "Timer frozen for 10 seconds"; PlayUiSound("Timer_Freeze");
|
|
StartCoroutine(PlayFreezeBooster());
|
|
return;
|
|
}
|
|
SelectedBooster = type;
|
|
Status = type == BoosterType.Rocket ? "Select a block to eliminate" : "Select a color to eliminate";
|
|
}
|
|
|
|
public void CancelBoosterSelection()
|
|
{ if (!InputLocked) { SelectedBooster = BoosterType.None; Status = ""; } }
|
|
|
|
private void ShowBoosterFundsNotice(BoosterType type)
|
|
{
|
|
PurchaseBooster=type;
|
|
OpenModal(GameModal.Info);
|
|
// Observed original L10 opens Shop here. Billing is intentionally
|
|
// unconnected; do not imitate a working purchase or grant currency.
|
|
Notice="Not enough coins\n"+economy.Price(type)+" coins required\nShop is not connected.";
|
|
}
|
|
|
|
public void BuySelectedBooster()
|
|
{
|
|
if (Modal != GameModal.BoosterPurchase) return;
|
|
if (!Player.BuyBooster(PurchaseBooster)) { Notice = "Not enough coins"; return; }
|
|
CloseModal(); Status = "Booster added to inventory";
|
|
}
|
|
|
|
private void ApplyBoosterTarget(BlockPieceView target)
|
|
{
|
|
BoosterType type = SelectedBooster;
|
|
if (target == null || !pieces.Contains(target) || (type != BoosterType.Rocket && type != BoosterType.Ufo) ||
|
|
activeExits != 0) return;
|
|
if(!SpecialCanTargetBooster(target))return;
|
|
if(!Player.SpendBoosterUse(type)){SelectedBooster=BoosterType.None;ShowBoosterFundsNotice(type);return;}
|
|
SelectedBooster = BoosterType.None; timerRunning = true; InputLocked = true;
|
|
#if UNITY_ANDROID || UNITY_IOS
|
|
if (Player.Haptics) Handheld.Vibrate();
|
|
#endif
|
|
List<BlockPieceView> targets = type == BoosterType.Rocket ? new List<BlockPieceView> {target} :
|
|
pieces.FindAll(item => item.ColorId == target.ColorId && SpecialCanTargetBooster(item));
|
|
foreach (var item in targets)
|
|
{
|
|
SpecialBeginRemoval(item, true);
|
|
pieces.Remove(item); activeExits++;
|
|
foreach (Collider collider in item.GetComponentsInChildren<Collider>()) collider.enabled = false;
|
|
}
|
|
StartCoroutine(UseTargetBooster(type, targets));
|
|
}
|
|
|
|
private IEnumerator UseTargetBooster(BoosterType type, List<BlockPieceView> targets)
|
|
{
|
|
yield return PlayRecoveredTargetBooster(type,targets);
|
|
}
|
|
|
|
private void CloseUnusedDoors()
|
|
{
|
|
foreach (DoorView door in doors)
|
|
if (door != null && !SpecialHasRemainingColor(door.ColorId)) door.MarkCleared();
|
|
}
|
|
|
|
private IEnumerator FinishBoardIfCleared()
|
|
{
|
|
if (pieces.Count != 0 || activeExits != 0 || completionStarted || !SpecialCanFinishLevel()) yield break;
|
|
completionStarted = true; timerRunning = false; InputLocked = true;
|
|
while (doors.Exists(item => item != null && item.IsExitEffectPlaying)) yield return null;
|
|
SpawnCompletionBurst(); yield return CompleteLevelFlow();
|
|
}
|
|
|
|
public void ContinueAfterWin()
|
|
{
|
|
if (ScreenState != BlockOutScreenState.LevelComplete || !completionCommitted) return;
|
|
// GoHome automatically settles queued Journey rewards. Do not force
|
|
// the Journey tab on every ordinary win; original tab order unverified.
|
|
if (winReturnsHome || AllLevelsCompleted) GoHome();
|
|
else LoadCurrentLevel();
|
|
}
|
|
}
|
|
}
|