Files
blockout/BlockOutReplica/Assets/Scripts/Gameplay/BlockOutInteractions.cs
T
2026-10-20 13:21:04 +08:00

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();
}
}
}