Manager.cs
using Microsoft.VisualBasic;
using Sandbox;
using Sandbox.Audio;
using Sandbox.Services;
using System.IO.Pipes;
using System.Numerics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
public enum CheckMatchPhase { Begin, Halfway, Finish }
public enum EventType { None, Choose, Match, Mismatch, LevelStartBoss, LevelStart, TurnStart, Reveal, GainHP, LoseHP, OverhealHP, TimerRanOut, GainMoney, LoseMoney, MoveCard, SwapCards, HurtCards, AfterCardsMoved, FinishLevel, Shake,
ClaimBounty, ClaimBountyAfter, }
public enum GameState { Menu, Playing, BuyPhase, Victory, Failure }
public enum StatType { TurnNum, NumMatches, NumMismatches, ConsecutiveMatches, ConsecutiveMismatches, TimerTotalTime, TimerTotalTimeOverride, KingMoveForced, LatestHealAmount, LatestOverhealAmount, TimerRunOutHPLoss, ShieldAmount,
TimerResetAfterBothChoices, FreeItem, NumFlags, MaxFlags, MaxCrayonMarks, FoodAdditionalHeal, EarnExtraMoney, ShopExtraItems, ExistingItemExtraChance, BuyItemHealChance, NumAdditionalBounties, NumExtraBountyTurns, ExtraBountyReward,
StealItemChance, ClaimBountiesBeforeReady, AllItemsCostHP, HPHealedThisLevel,
CardSpawnVal0, CardSpawnVal1, CardSpawnVal2, CardSpawnVal3, CardSpawnVal4, CardSpawnVal5, CardSpawnVal6, CardSpawnVal7, CardSpawnVal8, CardSpawnVal9,
}
public interface IEventHandler
{
public bool ShouldHandleEvent( EventType eventType );
public Task HandleEventAsync( EventType eventType );
public string GetEventText( EventType eventType );
}
public struct BountyData
{
public int startTurn;
public int endTurn;
public int moneyAmount;
}
public sealed class Manager : Component
{
public static Manager Instance { get; private set; }
[Property] public CameraComponent Camera;
[Property] public GameObject CardPrefab;
[Property] public GameObject CardOutlinePrefab;
[Property] public List<GameObject> CardBreakPrefabs;
[Property] public GameObject CardBreakParticlesPrefab;
[Property] public GameObject CardBreakShockwavePrefab;
[Property] public GameObject MenuCardPrefab;
[Property] public SoundEvent SongStart;
[Property] public SoundEvent SongB;
[Property] public SoundEvent SongC;
[Property] public SoundEvent SongD;
[Property] public SoundEvent SongE;
[Property] public SoundEvent SongLastLevel;
[Property] public SoundEvent SongShop;
[Property] public SoundEvent SongVictory;
[Property] public SoundEvent SongDefeat;
public Hud Hud { get; set; }
public SoundPointComponent MusicSoundPoint { get; set; }
public Mixer MusicMixer { get; set; }
public float MusicVolume { get; set; } = 30f;
public Mixer SfxMixer { get; set; }
public float SfxVolume { get; set; } = 50f;
public bool IsMuted { get; set; }
public bool IsSettingsOpen { get; set; }
public bool IsLeaderboardOpen { get; set; }
public Card HoveredCard { get; private set; }
public Card ChosenCard { get; private set; }
public Card RevealedCard { get; private set; }
public Card MovedCard { get; private set; }
public Card ShakenCard { get; private set; }
public List<Card> SwappedCards { get; private set; } = new();
public bool IsMismatchALockedMatch { get; private set; }
public int GridWidth { get; set; }
public int GridHeight { get; set; }
private GameObject _cardContainer;
private GameObject _relicContainer;
private GameObject _statusContainer;
public const float GRID_X_SPACING = 50f;
public const float GRID_Y_SPACING = 60f;
public int NumPairs => (GridWidth * GridHeight) / 2;
public List<Card> Cards = new();
private Dictionary<IntVector2, Card> _cardPositions = new();
public Dictionary<CardType, int> CardPairsExisting = new(); // pairs that exist on the current level
public Dictionary<CardType, int> CardPairsMatched = new(); // pairs that have been matched on the current level
public List<Card> ChosenCards = new();
public bool IsMoveResolving { get; private set; }
public int CardTypePanelHash { get; set; }
public int HP { get; set; }
public int MaxHP { get; set; }
public TimeSince TimeSinceHPChanged { get; set; }
public bool IsDead { get; set; }
private int _currRemainingSeconds;
public float TimerElapsed { get; set; }
public float TimerMoveStartTime { get; set; }
public TimeSince TimeSinceTimerChanged { get; set; }
private bool _hasMadeFirstMove;
public Stack<(IEventHandler, EventType)> EventMessageStack = new();
public int EventMessageHash { get; private set; }
public CardType HoveredPanelType { get; set; }
public int HoveredPanelIndex { get; set; }
public bool IsHoveringBounty { get; set; }
public RelicType HoveredRelicType { get; set; }
public int LevelNum { get; private set; }
public const int MAX_LEVEL = 10;
public bool IsSpawningLevel { get; private set; }
public bool IsLevelActive { get; set; }
public bool ShouldRestart { get; set; }
public bool ShouldReturnToMenu { get; set; }
public int Money { get; set; }
public TimeSince TimeSinceMoneyChanged { get; set; }
public GameState GameState { get; set; }
public List<Relic> Relics = new();
public List<RelicType> BuyPhaseOfferedRelics = new();
public int NumRelicsToShow { get; set; }
public int BuyPhaseHash { get; set; }
public List<bool> BoughtItems = new();
public int RelicHash { get; set; }
public Dictionary<StatType, float> Stats = new();
public TimeSince TimeSinceHoverSfx { get; set; }
public List<StatusEffect> Statuses = new();
public Vector3 CenterPos => new Vector3( GridWidth, GridHeight, 0f ) * 20f;
public bool ShouldShowOverlayColor { get; private set; }
public TimeSince TimeSinceOverlayStart { get; private set; }
public float OverlayTime { get; private set; }
public Color OverlayColor { get; private set; }
private Vector3 _cameraStartPos;
private bool _isCameraShaking;
private TimeSince _timeSinceCameraShakeStart;
private float _cameraShakeTime;
private float _cameraShakeStrength;
public bool ForceCursor { get; set; }
public int CursorClockNum { get; set; }
private TimeSince _timeSinceCursorClockChange;
public TimeSince TimeSinceMoveStartResolving { get; private set; }
public int NumFloaters { get; set; }
public Dictionary<CardType, BountyData> Bounties = new();
public TimeSince TimeSinceTurnStart { get; set; }
public TimeSince TimeSinceRunStart { get; set; }
public float FinalRunTime { get; set; }
public float BeatPreviousLevelTime { get; set; }
private TimeSince _timeSinceSpawnMenuCard;
private float _spawnNextMenuCardDelay;
public bool IsFadingIn { get; set; }
public TimeSince TimeSinceStartFadingIn { get; set; }
public const float FADE_IN_TIME = 1f;
public bool StartFromMenu { get; set; }
private List<CardType> SboxStatsMatchedCardTypes { get; set; } = new();
private List<RelicType> SboxStatsBoughtRelicTypes { get; set; } = new();
private bool _finishedGettingSboxStats;
protected override void OnStart()
{
base.OnStart();
Hud = Components.Get<Hud>();
MusicSoundPoint = Scene.Directory.FindByName( "music player" ).First().Components.Get<SoundPointComponent>();
MusicMixer = Mixer.FindMixerByName( "Music" );
SfxMixer = Mixer.FindMixerByName( "Game" );
SfxMixer.Volume = 1f;
Instance = this;
_cardContainer = new GameObject();
_cardContainer.Name = "cards";
_relicContainer = new GameObject();
_relicContainer.Name = "relics";
_statusContainer = new GameObject();
_statusContainer.Name = "statuses";
_cameraStartPos = Camera.WorldPosition;
InitSboxStats();
StartMenu();
//StartNewRun();
}
public void PlaySong(SoundEvent soundEvent)
{
if(soundEvent == null)
{
MusicSoundPoint.StopSound();
return;
}
MusicSoundPoint.SoundEvent = soundEvent;
MusicSoundPoint.StopSound();
MusicSoundPoint.StartSound();
}
public void StartMenu()
{
foreach ( var child in _cardContainer.Children )
child.Destroy();
IsLevelActive = false;
GameState = GameState.Menu;
ForceCursor = true;
if (!IsMuted)
MusicMixer.Volume = 0f;
_timeSinceSpawnMenuCard = 0f;
_spawnNextMenuCardDelay = 0f;
int numMenuCards = Game.Random.Int( 10, 20 );
for ( int i = 0; i < numMenuCards; i++ )
CreateMenuCard( new Vector3( Game.Random.Float( -275f, 275f ), Game.Random.Float( -150f, 150f ), Game.Random.Float( 10f, 80f ) ) );
SetBgTint( Color.FromRgb( 0x00972D ) );
FadeIn();
PlaySong( SongStart );
}
public void StartNewRun()
{
foreach ( var component in Scene.GetAllComponents<MenuCard>() )
component.GameObject.Destroy();
foreach ( var component in Scene.GetAllComponents<CardBreakPiece>() )
component.GameObject.Destroy();
foreach ( var component in Scene.GetAllComponents<ParticleEffect>() )
component.GameObject.Destroy();
ResetRunVars();
ResetLevelVars();
//HP = 1;
//Money = 10;
//IncreaseRelicLevel( RelicType.ShoppingCart );
//IncreaseRelicLevel( RelicType.ShoppingCart );
//IncreaseRelicLevel( RelicType.ShoppingCart );
//GameState = GameState.Menu;
//GameState = GameState.Failure;
//GameState = GameState.Victory;
SetLevel( 1, startNewSong: !StartFromMenu );
StartFromMenu = false;
//_ = FinishedLevel();
}
public void SetLevel(int levelNum, bool startNewSong = true)
{
LevelNum = levelNum;
GameState = GameState.Playing;
var cardTypes = LevelCreator.GetCardTypesForLevel( levelNum );
InitializeStage( cardTypes );
GenerateBounties();
//PlaySong( SongLevel1 );
//PlaySong( null );
if(levelNum == 1)
SetBgTint( Color.FromRgb( 0x00972D ) );
else if (levelNum == 3)
SetBgTint( Color.FromRgb( 0x086D0D ) );
else if ( levelNum == 5 )
SetBgTint( Color.FromRgb( 0xA4389D ) );
else if ( levelNum == 7 )
SetBgTint( Color.FromRgb( 0xA03B3B ) );
else if ( levelNum == 9 )
SetBgTint( Color.FromRgb( 0x2B44B1 ) );
FadeIn();
//_ = FinishedLevel();
if( startNewSong )
{
if ( levelNum == 1 || levelNum == 2 )
PlaySong( SongStart );
if ( levelNum == 3 || levelNum == 4 )
PlaySong( SongB );
else if ( levelNum == 5 || levelNum == 6 )
PlaySong( SongC );
else if ( levelNum == 7 || levelNum == 8 )
PlaySong( SongE );
else if ( levelNum == 9 )
PlaySong( SongD );
else if ( levelNum == 10 )
PlaySong( SongLastLevel );
}
foreach ( var component in Scene.GetAllComponents<CardBreakPiece>() )
component.GameObject.Destroy();
foreach ( var component in Scene.GetAllComponents<ParticleEffect>() )
component.GameObject.Destroy();
}
public void InitializeStage( List<CardType> cardPairTypes )
{
ResetLevelVars();
List<CardType> replacementCardTypes = new();
if ( GetRelic( RelicType.GuideDog ) != null )
replacementCardTypes.Add( CardType.GuideDog );
for(int i = 0; i < replacementCardTypes.Count; i++)
cardPairTypes.RemoveAt( cardPairTypes.Count - 1 );
foreach(var cardType in replacementCardTypes)
cardPairTypes.Add( cardType );
IsLevelActive = true;
var numCards = cardPairTypes.Count() * 2;
var gridSize = FindBestGridSize( numCards );
GridWidth = gridSize.Item1;
GridHeight = gridSize.Item2;
List<CardType> cardTypes = new();
foreach (var cardPairType in cardPairTypes )
{
for ( int i = 0; i < 2; i++ )
cardTypes.Add( cardPairType );
if ( CardPairsExisting.ContainsKey( cardPairType ) )
CardPairsExisting[cardPairType]++;
else
CardPairsExisting.Add( cardPairType, 1 );
}
cardTypes.Shuffle();
Dictionary<CardType, int> cardTypeIDs = new();
int count = 0;
for ( int x = 0; x < GridWidth; x++ )
{
for ( int y = 0; y < GridHeight; y++ )
{
var gridPos = new IntVector2( x, y );
var cardType = cardTypes[count];
if(cardTypeIDs.ContainsKey(cardType))
cardTypeIDs[cardType]++;
else
cardTypeIDs.Add( cardType, 0 );
CreateCard( gridPos, cardType, cardTypeIDs[cardType] );
count++;
}
}
_cardContainer.WorldPosition = new Vector3(
-GridWidth * GRID_X_SPACING * 0.5f + GRID_X_SPACING * 0.5f + (GridWidth >= 10 ? 10f : 0f),
-GridHeight * GRID_Y_SPACING * 0.5f + GRID_Y_SPACING * 0.5f + 7f,
0f
);
var maxSize = Math.Max( GridWidth * 0.85f, GridHeight * 1.3f );
//Camera.WorldPosition = new Vector3( 0f, 0f, Utils.Map( maxSize, 2.55f, 8.5f, 360f, 540f, EasingType.QuadOut) );
Camera.OrthographicHeight = Utils.Map( maxSize, 2.55f, 8.5f, 260f, 345f, EasingType.QuadOut );
// validate that each card likes its starting position
int MAX_TRIES = 100;
bool allValid = false;
for (int i = 0; i < MAX_TRIES; i++)
{
allValid = true;
foreach ( var card in Cards )
{
if ( !card.ValidateStartingGridPos() )
allValid = false;
}
if ( allValid )
break;
}
if(!allValid)
{
Log.Error( "Unable to validate card starting positions!" );
}
_ = SpawnLevelAsync();
}
async Task SpawnLevelAsync()
{
IsSpawningLevel = true;
IsMoveResolving = true;
await ForceCursorSwitch();
PlaySfx( "card_shuffle", new Vector3( 0f, 0f, Camera.WorldPosition.z - 100f ), volume: 0.6f, pitch: Game.Random.Float( 1.3f, 1.45f ) );
var startPos = new Vector3( -GridWidth * GRID_X_SPACING * 0.5f - GRID_X_SPACING, -GridHeight * GRID_Y_SPACING * 0.5f + GRID_Y_SPACING, 10f );
Cards.Shuffle();
int count = 0;
foreach ( var card in Cards )
{
card.WorldPosition = startPos + new Vector3(-0.75f, 0.75f, 0.1f) * count;
card.IsSpawning = true;
count++;
}
await Task.DelayRealtime( 200 );
// shuffle from deck
count = 0;
for(int i = Cards.Count - 1; i >= 0; i--)
{
var card = Cards[i];
card.IsSpawning = false;
await Task.DelayRealtime( (int)(Utils.Map(Cards.Count, 6, 50, 80f, 20f ) * Utils.Map(count, 0, Cards.Count, 1f, 0.3f)));
count++;
}
await Task.DelayRealtime( 250 );
IsSpawningLevel = false;
await EventHappened( EventType.LevelStartBoss );
await EventHappened( EventType.LevelStart );
//await EventHappened( EventType.TurnStart );
//_updateTaskActive = true;
TimerElapsed = 0f;
TimerMoveStartTime = Time.Now;
IsMoveResolving = false;
await ForceCursorSwitch();
UpdateAsync();
}
void GenerateBounties()
{
if ( LevelNum == 1 )
return;
List<CardType> cardTypes = new();
cardTypes.AddRange( CardPairsExisting.Keys.ToList() );
for( int i = cardTypes.Count - 1; i >= 0; i-- )
{
var cardType = cardTypes[i];
if ( Card.HasHP( cardType ) || cardType == CardType.Skunk || cardType == CardType.Cockroach )
cardTypes.RemoveAt( i );
}
cardTypes.Shuffle();
int numBounties = (LevelNum < 5 ? 1 : 2) + (int)Stats[StatType.NumAdditionalBounties];
for( int i = 0; i < Math.Min(numBounties, cardTypes.Count); i++ )
{
int start = Game.Random.Int( 2, 4 ) + Game.Random.Int( 0, (int)Math.Round( Utils.Map( LevelNum, 2, 10, 0f, 5f, EasingType.SineIn ) ) );
int end = start + Game.Random.Int( 5, (int)Math.Round( Utils.Map( LevelNum, 2, 10, 6f, 9f, EasingType.SineIn ) ) ) + (int)Stats[StatType.NumExtraBountyTurns];
int money = Game.Random.Int( 1, (int)Math.Round( Utils.Map( LevelNum, 2, 10, 3f, 4f, EasingType.QuadIn ) ) ) + (int)Stats[StatType.ExtraBountyReward];
Bounties.Add( cardTypes[i], new BountyData() { startTurn = start, endTurn = end, moneyAmount = money } );
}
}
async Task ForceCursorSwitch()
{
ForceCursor = true;
await Task.DelayRealtime( 70 );
ForceCursor = false;
}
public string GetCursor()
{
return IsMoveResolving && !IsDead && GameState == GameState.Playing && TimeSinceMoveStartResolving > 0.07f
? $"clock_{CursorClockNum}"
: (TimeSinceMoveStartResolving < 0.01f ? "pointer3_small" : "pointer3");
//: "pointer3";
}
void ResetRunVars()
{
LevelNum = 1;
HP = MaxHP = 10;
Money = 0;
TimeSinceRunStart = 0f;
TimeSinceHPChanged = 999f;
TimeSinceMoneyChanged = 999f;
TimeSinceTurnStart = 999f;
TimeSinceTimerChanged = 999f;
Relics.Clear();
RelicHash = 0;
var relicComponents = _relicContainer.Components.GetAll();
for (int i = relicComponents.Count() - 1; i >= 0; i--)
relicComponents.ElementAt( i ).Destroy();
Stats[StatType.TimerTotalTime] = 10f;
Stats[StatType.TimerRunOutHPLoss] = 1f;
Stats[StatType.ShieldAmount] = 0f;
Stats[StatType.TimerResetAfterBothChoices] = 0f;
Stats[StatType.FreeItem] = 0f;
Stats[StatType.CardSpawnVal0] = Game.Random.Int( 0, 10 );
Stats[StatType.CardSpawnVal1] = Game.Random.Int( 0, 10 );
Stats[StatType.CardSpawnVal2] = Game.Random.Int( 0, 10 );
Stats[StatType.CardSpawnVal3] = Game.Random.Int( 0, 10 );
Stats[StatType.CardSpawnVal4] = Game.Random.Int( 0, 10 );
Stats[StatType.CardSpawnVal5] = Game.Random.Int( 0, 10 );
Stats[StatType.CardSpawnVal6] = Game.Random.Int( 0, 10 );
Stats[StatType.CardSpawnVal7] = Game.Random.Int( 0, 10 );
Stats[StatType.CardSpawnVal8] = Game.Random.Int( 0, 10 );
Stats[StatType.CardSpawnVal9] = Game.Random.Int( 0, 10 );
Stats[StatType.MaxFlags] = 0f;
Stats[StatType.MaxCrayonMarks] = 0f;
Stats[StatType.FoodAdditionalHeal] = 0f;
Stats[StatType.EarnExtraMoney] = 0f;
Stats[StatType.ShopExtraItems] = 0f;
Stats[StatType.ExistingItemExtraChance] = 0f;
Stats[StatType.BuyItemHealChance] = 0f;
Stats[StatType.NumAdditionalBounties] = 0f;
Stats[StatType.NumExtraBountyTurns] = 0f;
Stats[StatType.ExtraBountyReward] = 0f;
Stats[StatType.StealItemChance] = 0f;
Stats[StatType.ClaimBountiesBeforeReady] = 0f;
Stats[StatType.AllItemsCostHP] = 0f;
}
void ResetLevelVars()
{
IsDead = false;
TimerElapsed = 0f;
_currRemainingSeconds = (int)Stats[StatType.TimerTotalTime];
_hasMadeFirstMove = false;
ShouldShowOverlayColor = false;
_isCameraShaking = false;
ChosenCards.Clear();
ChosenCard = null;
HoveredCard = null;
RevealedCard = null;
MovedCard = null;
ShakenCard = null;
SwappedCards.Clear();
IsMismatchALockedMatch = false;
IsMoveResolving = false;
HoveredPanelType = CardType.None;
HoveredPanelIndex = -1;
IsHoveringBounty = false;
foreach ( var child in _cardContainer.Children )
child.Destroy();
Cards.Clear();
_cardPositions.Clear();
CardPairsExisting.Clear();
CardPairsMatched.Clear();
CardTypePanelHash++;
Bounties.Clear();
Statuses.Clear();
var statusComponents = _statusContainer.Components.GetAll();
for ( int i = statusComponents.Count() - 1; i >= 0; i-- )
statusComponents.ElementAt( i ).Destroy();
Stats[StatType.TurnNum] = 0f;
Stats[StatType.ConsecutiveMatches] = 0f;
Stats[StatType.ConsecutiveMismatches] = 0f;
Stats[StatType.NumMatches] = 0f;
Stats[StatType.NumMismatches] = 0f;
Stats[StatType.TimerTotalTimeOverride] = 0f;
Stats[StatType.KingMoveForced] = 0f;
Stats[StatType.LatestHealAmount] = 0f;
Stats[StatType.LatestOverhealAmount] = 0f;
Stats[StatType.NumFlags] = 0f;
Stats[StatType.HPHealedThisLevel] = 0f;
}
protected override void OnUpdate()
{
if ( GameState == GameState.Menu )
HandleMenu();
if(Input.EscapePressed)
{
if ( IsLeaderboardOpen )
IsLeaderboardOpen = false;
else
IsSettingsOpen = !IsSettingsOpen;
Input.EscapePressed = false;
PlaySfx( "click_0" );
}
if ( Input.Pressed("Mute") )
{
IsMuted = !IsMuted;
SetMute( IsMuted );
PlaySfx( "click_0" );
}
//Gizmo.Draw.ScreenText( $"{IsLevelActive}", new Vector2( 260, 1260 ), size: 16 );
//if(Input.Pressed("use"))
//{
// DetermineOfferedRelics();
//}
//if (Input.Down( "Duck" ) )
//{
// for ( int x = 0; x < GridWidth; x++ )
// {
// for ( int y = 0; y < GridHeight; y++ )
// {
// var gridPos = new IntVector2( x, y );
// Gizmo.Draw.Color = Color.White.WithAlpha( 0.9f );
// var card = GetCardAtGridPos( gridPos );
// Gizmo.Draw.Text( $"{gridPos}: {(card != null ? card.CardType : "")}", new global::Transform( _cardContainer.WorldPosition + GetCardPos( gridPos ) + new Vector3( -21f, -26f, 0f ) ), size: 12f, flags: TextFlag.Left );
// }
// }
//}
if ( IsMoveResolving && !IsDead && IsLevelActive )
{
if ( _timeSinceCursorClockChange > 0.075f )
{
CursorClockNum = (CursorClockNum + 1) % 12;
_timeSinceCursorClockChange = 0f;
}
//if ( Input.Pressed( "attack1" ) )
// PlaySfxScreenPos( "error", Mouse.Position, volume: 1f, pitch: Game.Random.Float( 0.95f, 1.05f ) );
}
//ChosenCard = null;
HandleOverlay();
HandleCameraShake();
if ( Scene.TimeScale < 1f )
Scene.TimeScale = Utils.DynamicEaseTo( Scene.TimeScale, 1f, 0.2f, Time.Delta );
if(IsFadingIn)
{
if ( TimeSinceStartFadingIn > FADE_IN_TIME )
IsFadingIn = false;
if(!IsMuted)
RefreshMusicMixerVolume();
}
if ( IsSpawningLevel || IsDead || !IsLevelActive )
return;
//var prevHoveredCard = HoveredCard;
if ( HoveredCard != null )
{
HoveredCard.IsHovered = false;
HoveredCard = null;
}
var tr = Scene.Trace.Ray( Camera.ScreenPixelToRay( Mouse.Position ), 1000f ).HitTriggersOnly().Run();
if ( tr.Hit )
{
if(tr.GameObject.Parent != null)
{
var card = tr.GameObject.Parent.Components.Get<Card>();
if ( card != null && !card.IsMovementControlled )
{
HoveredCard = card;
card.IsHovered = true;
//if ( prevHoveredCard != card && !card.IsRevealed && TimeSinceHoverSfx > 0.025f )
//{
// PlayCardSfx( "click_1", card, volume: 0.3f, pitch: 0.4f );
// TimeSinceHoverSfx = 0f;
//}
}
}
}
if ( Input.Pressed( "attack2" ) && HoveredCard != null && !HoveredCard.IsRevealed && !HoveredCard.IsExploding )
{
if ( HoveredCard.IsFlagShown )
{
HoveredCard.HideFlag();
Stats[StatType.NumFlags]--;
PlayCardSfx( "pop", HoveredCard, volume: 1f, pitch: Game.Random.Float( 0.9f, 0.95f ) );
HoveredCard.StartScaling( 0.1f, 1.05f, EasingType.SineOut );
}
else
{
if( (int)Stats[StatType.NumFlags] < (int)Stats[StatType.MaxFlags] )
{
HoveredCard.ShowFlag();
Stats[StatType.NumFlags]++;
PlayCardSfx( "pop", HoveredCard, volume: 1f, pitch: Game.Random.Float( 1.1f, 1.15f ) );
HoveredCard.StartScaling( 0.1f, 1.075f, EasingType.SineOut );
}
else
{
PlayCardSfx( "error_1", HoveredCard, volume: 0.5f, pitch: Game.Random.Float( 0.9f, 0.92f ) );
HoveredCard.StartScaling( 0.1f, 1.033f, EasingType.SineOut );
}
}
}
//if ( Input.Pressed( "attack1" ) )
//{
// Log.Info( "1" );
// if ( HoveredCard != null )
// {
// Log.Info( "2" );
// if(IsMoveResolving)
// {
// Log.Info( $"3" );
// PlayCardSfx( "error", HoveredCard, volume: 1f, pitch: Game.Random.Float( 0.95f, 1.05f ) );
// }
// }
//}
if ( IsMoveResolving )
return;
if ( Input.Pressed( "attack1" ) && HoveredCard != null && !ChosenCards.Contains( HoveredCard ) && !HoveredCard.IsMovementControlled && ChosenCard == null )
{
ChosenCard = HoveredCard;
}
//if ( Input.Pressed( "Left" ) && Manager.Instance.LevelNum >= 0 )
//{
// SetLevel( LevelNum - 1 );
//}
//if ( Input.Pressed( "Right" ) && Manager.Instance.LevelNum < MAX_LEVEL )
//{
// SetLevel( LevelNum + 1 );
//}
//if (Input.Pressed("Menu"))
//{
// StartNewRun();
//}
}
void HandleMenu()
{
if(TimeSinceStartFadingIn > 0.075f)
ForceCursor = false;
if (_timeSinceSpawnMenuCard > _spawnNextMenuCardDelay)
{
CreateMenuCard( new Vector3( Game.Random.Float( -275f, 275f ), 230f, Game.Random.Float( 10f, 80f ) ) );
_timeSinceSpawnMenuCard = 0f;
_spawnNextMenuCardDelay = Game.Random.Float( 0.05f, 0.5f );
}
var mousePos = Mouse.Position * Hud.Panel.ScaleFromScreen;
//Log.Info( $"{MathF.Round( mousePos.x)}, {MathF.Round(mousePos.y)}" );
bool allowHover = true;
//if ( IsLeaderboardOpen )
// allowHover = mousePos.x < 479f || mousePos.x > 1437f || mousePos.y < 81f || mousePos.y > 998f;
//else if ( IsSettingsOpen )
// allowHover = mousePos.x < 691f || mousePos.x > 1227f || mousePos.y < 324f || mousePos.y > 754f;
//else
// allowHover = mousePos.x < 650f || mousePos.x > 1265f || mousePos.y < 314f || mousePos.y > 765f;
if ( allowHover )
{
MenuCard hoveredCard = null;
var tr = Scene.Trace.Ray( Camera.ScreenPixelToRay( Mouse.Position ), 1000f ).HitTriggersOnly().Run();
if ( tr.Hit )
{
if ( tr.GameObject.Parent != null )
{
var menuCard = tr.GameObject.Parent.Components.Get<MenuCard>();
if ( menuCard != null && !menuCard.IsMovementControlled )
{
hoveredCard = menuCard;
hoveredCard.IsHovered = true;
}
}
}
if ( Input.Pressed( "attack1" ) && hoveredCard.IsValid() )
{
if ( hoveredCard.HP > 1 )
{
hoveredCard.Shake( strength: Game.Random.Float( 1f, 2f ), time: Game.Random.Float( 0.3f, 0.45f ), easingType: EasingType.QuadOut, playSfx: false );
hoveredCard.HP--;
//PlayCardSfx( Card.GetHurtSfxFilename( hoveredCard.CardType ), hoveredCard );
hoveredCard.PlayMenuHurtSfx();
}
else
{
hoveredCard.PlayMenuDeathSfx();
var breakNum = Game.Random.Int( 0, CardBreakPrefabs.Count - 1 );
hoveredCard.Explode( breakNum );
}
//Scene.TimeScale = 0.5f;
}
}
}
async void UpdateAsync()
{
while ( IsLevelActive )
{
if ( ChosenCard != null && !ChosenCards.Contains( ChosenCard ) )
{
//ChosenCard = null;
await ChooseCardAsync( ChosenCard );
}
ChosenCard = null;
if ( _hasMadeFirstMove && !IsDead )
await HandleTimerAsync();
if ( IsDead )
{
Manager.Instance.PlaySfxCenter( "player_death", volume: 0.33f, pitch: Game.Random.Float( 0.95f, 1.05f ) );
foreach ( var card in Cards )
card.SetRevealedFromAsync( true );
await Task.DelayRealtime( 1500 );
GameOver();
}
await Task.Frame();
}
//Log.Info( "UpdateAsync Ended" );
if ( ShouldRestart )
{
StartNewRun();
ShouldRestart = false;
}
if ( ShouldReturnToMenu )
{
StartMenu();
ShouldReturnToMenu = false;
}
}
async Task HandleTimerAsync()
{
TimerElapsed = Time.Now - TimerMoveStartTime;
float totalTime = GetTimerTotalTime();
if ( TimerElapsed > (int)totalTime )
{
var camera = Scene.Camera;
//var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width * 0.05f, Screen.Height * 0.85f, 0f ) );
var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width * 0.5f, Screen.Height * 0.5f, 0f ) );
var tr = Scene.Trace.Ray( ray, 10000f ).Run();
int lossAmount = (int)Stats[StatType.TimerRunOutHPLoss];
SpawnFloaterText(
pos: (tr.EndPosition + new Vector3( 0f, 25f, 0f )).WithZ( 100f ),
text: $"Time ran out!",
emojiText: "",
lifetime: 2f,
color: new Color( 1f, 1f, 1f ),
velocity: new Vector2( 0f, -32f ),
deceleration: 2.2f,
fontSize: 50f,
startScale: 1.1f,
endScale: 0.9f
);
Manager.Instance.SpawnFloaterText(
pos: tr.EndPosition.WithZ( 100f ),
text: $"-{lossAmount}",
emojiText: "❤️",
lifetime: 2f,
color: new Color( 0.8f, 0f, 0f ),
velocity: new Vector2( 0f, -32f ),
deceleration: 2.2f,
fontSize: 60f,
startScale: 1.1f,
endScale: 0.9f
);
TimeSinceTimerChanged = 0f;
await LoseHP( lossAmount );
await EventHappened( EventType.TimerRanOut );
TimerElapsed = 0f;
TimerMoveStartTime = Time.Now;
_currRemainingSeconds = (int)totalTime;
}
else
{
int remainingSeconds = MathX.FloorToInt(MathF.Max( totalTime - TimerElapsed, 0 ) );
if ( remainingSeconds != _currRemainingSeconds )
{
float volume = Utils.Map( remainingSeconds, totalTime, 0f, 0f, 1f, EasingType.SineIn );
if( _currRemainingSeconds % 2 == ( (int)totalTime % 2 == 0 ? 1 : 0 ) )
PlaySfx("clock_tick", new Vector3( -35f, 0f, Camera.WorldPosition.z - 100f ), volume, pitch: 1.1f);
else
PlaySfx( "clock_tock", new Vector3( -35f, 0f, Camera.WorldPosition.z - 100f ), volume, pitch: 0.95f );
_currRemainingSeconds = remainingSeconds;
}
}
//await Task.Frame();
}
public async Task EventHappened( EventType eventType )
{
//Cards.Shuffle();
for(int i = Cards.Count - 1; i >= 0; i-- )
{
var card = Cards[i];
if ( card.ShouldHandleEvent( eventType ) )
await card.HandleEventAsync( eventType );
}
await HandleStatusEvent( eventType );
await HandleRelicEvent( eventType );
}
async Task EventHappened( EventType eventType, Card card0 )
{
if ( card0.ShouldHandleEvent( eventType ) )
await card0.HandleEventAsync( eventType );
//Cards.Shuffle();
for ( int i = Cards.Count - 1; i >= 0; i-- )
{
var card = Cards[i];
if ( card == card0 )
continue;
if ( card.ShouldHandleEvent( eventType ) )
await card.HandleEventAsync( eventType );
}
await HandleStatusEvent( eventType );
await HandleRelicEvent( eventType );
}
async Task EventHappened( EventType eventType, Card card0, Card card1 )
{
if ( card0.ShouldHandleEvent( eventType ) )
await card0.HandleEventAsync( eventType );
if ( card1.ShouldHandleEvent( eventType ) )
await card1.HandleEventAsync( eventType );
//Cards.Shuffle();
for ( int i = Cards.Count - 1; i >= 0; i-- )
{
var card = Cards[i];
if ( card == card0 || card == card1 )
continue;
if ( card.ShouldHandleEvent( eventType ) )
await card.HandleEventAsync( eventType );
}
await HandleStatusEvent( eventType );
await HandleRelicEvent( eventType );
}
async Task HandleRelicEvent(EventType eventType)
{
for ( int i = Relics.Count - 1; i >= 0; i-- )
{
var relic = Relics[i];
if ( relic.ShouldHandleEvent( eventType ) )
await relic.HandleEventAsync( eventType );
}
}
async Task HandleStatusEvent( EventType eventType )
{
for ( int i = Statuses.Count - 1; i >= 0; i-- )
{
var status = Statuses[i];
if ( status.ShouldHandleEvent( eventType ) )
await status.HandleEventAsync( eventType );
}
}
async Task CheckMatchAsync()
{
var card0 = ChosenCards[0];
var card1 = ChosenCards[1];
bool isMatch = (card0.CardType == card1.CardType) && !card0.IsLocked && !card1.IsLocked && card0.CanBeMatched() && card1.CanBeMatched();
await Task.DelayRealtime( 300 );
if (isMatch)
{
Stats[StatType.NumMatches]++;
Stats[StatType.ConsecutiveMatches]++;
Stats[StatType.ConsecutiveMismatches] = 0f;
StatsMatchCard( card0.CardType );
PlayCardSfxBetween( "match", card0, card1, volume: 1.75f, pitch: Game.Random.Float( 0.95f, 1.1f ), depthDiff: 60f );
card0.StartScaling( time: 0.25f, amount: 1.1f, easingType: EasingType.SineInOut );
card1.StartScaling( time: 0.25f, amount: 1.1f, easingType: EasingType.SineInOut );
await CheckBounty( card0.OriginalCardType );
await EventHappened( EventType.Match, card0, card1 );
}
else
{
Stats[StatType.NumMismatches]++;
Stats[StatType.ConsecutiveMatches] = 0f;
Stats[StatType.ConsecutiveMismatches]++;
await Manager.Instance.ShakeCard( card0, time: Game.Random.Float( 0.3f, 0.5f ), easingType: EasingType.QuadOut, playSfx: false );
await Manager.Instance.ShakeCard( card1, time: Game.Random.Float( 0.3f, 0.5f ), easingType: EasingType.QuadOut, playSfx: false );
PlayCardSfxBetween( "wrong", card0, card1, volume: 1.75f, pitch: Game.Random.Float( 0.9f, 1.1f ), depthDiff: 150f );
await Task.DelayRealtime( 250 );
if( (int)Stats[StatType.NumMismatches] <= (int)Stats[StatType.ShieldAmount] )
{
// do nothing
}
else
{
await LoseHP( 1 );
}
await Task.DelayRealtime( 150 );
IsMismatchALockedMatch = (card0.CardType == card1.CardType) && (card0.IsLocked || card1.IsLocked);
await EventHappened( EventType.Mismatch, card0, card1 );
}
await Task.DelayRealtime( 200 );
if ( isMatch )
{
bool shouldRemove = true;
if(Card.HasHP(card0.CardType))
{
await card0.Hurt();
await card1.Hurt();
card0.PlayHurtSfx( card0, card1 );
await EventHappened( EventType.HurtCards, card0, card1 );
await Task.DelayRealtime( 300 );
shouldRemove = card0.HP <= 0 || card1.HP <= 0;
}
if(shouldRemove)
{
var numRemainingCardPairs = CardPairsExisting.Count - CardPairsMatched.Count;
//Log.Info( $"numRemainingCardPairs: {numRemainingCardPairs}" );
int explodeTime = (int)(Utils.Map( Stats[StatType.ConsecutiveMatches], 0f, 4f, Game.Random.Float( 300f, 350f ), 100f, EasingType.SineOut ) * Utils.Map( numRemainingCardPairs, 8, 1, 1f, 0.4f, EasingType.SineIn));
card0.StartExploding( explodeTime / 1000f );
card1.StartExploding( explodeTime / 1000f );
await Task.DelayRealtime( explodeTime );
if ( CardPairsMatched.ContainsKey( card0.OriginalCardType ) )
CardPairsMatched[card0.OriginalCardType]++;
else
CardPairsMatched.Add( card0.OriginalCardType, 1 );
CardTypePanelHash++;
Cards.Remove( card0 );
Cards.Remove( card1 );
_cardPositions.Remove( card0.GridPos );
_cardPositions.Remove( card1.GridPos );
Scene.TimeScale = 0.15f;
var breakNum0 = Game.Random.Int( 0, CardBreakPrefabs.Count - 1 );
var breakNum1 = Game.Random.Int( 0, CardBreakPrefabs.Count - 1 );
while ( breakNum1 == breakNum0 )
breakNum1 = Game.Random.Int( 0, CardBreakPrefabs.Count - 1 );
if ( card0.IsFlagShown )
Stats[StatType.NumFlags]--;
if ( card1.IsFlagShown )
Stats[StatType.NumFlags]--;
card0.Explode( breakNum0 );
card1.Explode( breakNum1 );
}
else
{
if(card0.IsRevealed)
{
HideCard( card0 );
HideCard( card1 );
PlayCardSfxBetween( "card_flip", card0, card1, volume: 0.9f, pitch: Game.Random.Float( 0.65f, 0.75f ) );
}
}
}
else
{
await Task.DelayRealtime( 150 );
HideCard( card0 );
HideCard( card1 );
PlayCardSfxBetween( "card_flip", card0, card1, volume: 0.9f, pitch: Game.Random.Float( 0.65f, 0.75f ) );
}
ChosenCards.Clear();
Stats[StatType.TurnNum]++;
UpdateBounties();
TimeSinceTurnStart = 0f;
if ( !IsDead )
{
if ( Cards.Count == 0 )
{
await FinishedLevel();
}
else
{
await CheckLockedCards();
await EventHappened( EventType.TurnStart );
}
}
}
void UpdateBounties()
{
bool bountyStarted = false;
bool bountyRemoved = false;
int turnNum = (int)Stats[StatType.TurnNum];
for ( int i = Bounties.Count - 1; i >= 0; i-- )
{
var pair = Bounties.ElementAt( i );
var bountyData = pair.Value;
if(turnNum == bountyData.startTurn)
{
bountyStarted = true;
}
if ( turnNum >= bountyData.endTurn )
{
var cardType = pair.Key;
Bounties.Remove( cardType );
bountyRemoved = true;
}
}
if ( bountyStarted )
PlaySfxScreenPos( "bounty", new Vector2( 1800f, 400f ), volume: 1f, pitch: Game.Random.Float( 1f, 1.05f ) );
if ( bountyRemoved )
PlaySfxScreenPos( "bounty", new Vector2( 1800f, 600f ), volume: 0.85f, pitch: Game.Random.Float( 0.8f, 0.85f ) );
}
async Task CheckBounty( CardType cardType )
{
if ( Bounties.ContainsKey( cardType ) )
{
var bountyData = Bounties[cardType];
if( Stats[StatType.TurnNum] >= bountyData.startTurn || Stats[StatType.ClaimBountiesBeforeReady] > 0f )
{
var camera = Scene.Camera;
var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width / 2, Screen.Height / 2, 0f ) );
var tr = Scene.Trace.Ray( ray, 10000f ).Run();
PlaySfxCenter( "coin", volume: 1.3f, pitch: Game.Random.Float( 0.8f, 0.9f ) );
await Task.DelayRealtime( 150 );
int moneyAmount = bountyData.moneyAmount + (int)Stats[StatType.EarnExtraMoney];
SpawnFloaterText(
pos: (tr.EndPosition + new Vector3( 0f, 25f, 0f )).WithZ( 100f ),
text: $"Bounty Claimed",
emojiText: "",
lifetime: 2f,
color: new Color( 1f, 1f, 1f ),
velocity: new Vector2( 0f, 35f ),
deceleration: 2.1f,
fontSize: 50f,
startScale: 1f,
endScale: 1.1f
);
SpawnGainMoneyFloater( moneyAmount, tr.EndPosition );
await GainMoney( moneyAmount );
Bounties.Remove( cardType );
await Task.DelayRealtime( 500 );
await EventHappened( EventType.ClaimBounty );
await EventHappened( EventType.ClaimBountyAfter );
await Task.DelayRealtime( 250 );
}
}
}
public async Task RevealCard( Card card )
{
card.SetRevealedFromAsync( true );
RevealedCard = card;
await EventHappened( EventType.Reveal, card );
}
public void HideCard( Card card )
{
card.SetRevealedFromAsync( false );
}
public async Task ShakeCard( Card card, float time = 0.6f, EasingType easingType = EasingType.QuadOut, bool playSfx = true )
{
card.Shake( strength: Game.Random.Float( 20f, 30f ), time, easingType, playSfx );
ShakenCard = card;
await EventHappened( EventType.Shake, card );
}
public async Task ShakeCard( Card card, float time, float strength, EasingType easingType = EasingType.QuadOut, bool playSfx = true )
{
card.Shake( strength, time, easingType, playSfx );
ShakenCard = card;
await EventHappened( EventType.Shake, card );
}
public async Task LoseHP(int amount)
{
HP = Math.Max( HP - amount, 0 );
if(amount > 0)
{
PlaySfx( "hurt", new Vector3( -50f, 0f, Camera.WorldPosition.z - 100f ) );
ShakeCamera( strength: Utils.Map( HP, MaxHP, 0, 1.5f, 2.5f, EasingType.SineIn ), time: Utils.Map( HP, MaxHP, 0, 0.45f, 0.6f, EasingType.SineIn ) );
ShowOverlayColor( Color.Red.WithAlpha( Utils.Map( HP, MaxHP, 0, 0.4f, 0.7f, EasingType.SineIn ) ), time: Utils.Map( HP, MaxHP, 0, 0.175f, 0.25f, EasingType.SineIn ) );
}
TimeSinceHPChanged = 0f;
await EventHappened( EventType.LoseHP );
if (HP <= 0)
{
Die();
//InitializeRandomStageSize();
}
}
public void Die()
{
IsDead = true;
foreach(var card in Cards)
{
card.IsHovered = false;
}
}
public async Task GainHP( int amount )
{
int overhealAmount = Math.Max( HP + amount - MaxHP, 0 );
var healAmount = Math.Min( amount, MaxHP - HP );
Stats[StatType.LatestHealAmount] = healAmount;
if(healAmount > 0f)
{
Stats[StatType.HPHealedThisLevel] += amount;
if ( Stats[StatType.HPHealedThisLevel] > 10f )
Sandbox.Services.Achievements.Unlock( "healer" );
}
//Log.Info( $"GainHP - HP: {HP} amount: {amount} MaxHP: {MaxHP} healAmount: {Stats[StatType.LatestHealAmount]} overheal: {overhealAmount}" );
HP = Math.Min( HP + amount, MaxHP );
TimeSinceHPChanged = 0f;
await EventHappened( EventType.GainHP );
if(overhealAmount > 0)
{
Stats[StatType.LatestOverhealAmount] = overhealAmount;
await EventHappened( EventType.OverhealHP );
}
}
public async Task GainMoney( int amount )
{
Money += amount;
TimeSinceMoneyChanged = 0f;
await EventHappened( EventType.GainMoney );
}
public async Task LoseMoney( int amount )
{
Money = Math.Max( Money - amount, 0 );
TimeSinceMoneyChanged = 0f;
await EventHappened( EventType.LoseMoney );
}
public void SpendMoney( int amount )
{
if ( amount <= 0 )
return;
if(Money < amount)
{
Log.Error( $"SpendMoney - not enough money!" );
return;
}
Money -= amount;
TimeSinceMoneyChanged = 0f;
}
//void InitializeRandomStageSize()
//{
// int numCardPairs = Game.Random.Int( 3, 25 );
// while(numCardPairs == 11 || numCardPairs == 13 || numCardPairs == 17 || numCardPairs == 18 || numCardPairs == 19 || numCardPairs == 21 || numCardPairs == 22 || numCardPairs == 23 || numCardPairs == 24 )
// numCardPairs = Game.Random.Int( 3, 25 );
// //numCardPairs = 25;
// var cardPairTypes = new List<CardType>();
// for (int i = 0; i < numCardPairs; i++ )
// {
// CardType cardType = CardType.None;
// while(!AllowStartingCardType(cardType))
// cardType = (CardType)Game.Random.Int( 1, Enum.GetValues( typeof( CardType ) ).Length - 1 );
// cardPairTypes.Add( cardType );
// }
// InitializeStage( cardPairTypes );
//}
bool AllowStartingCardType(CardType cardType)
{
return cardType != CardType.None && cardType != CardType.UmbrellaClosed;
}
public Card CreateCard( IntVector2 gridPos, CardType cardType, int cardTypeID, bool createOutline = true )
{
//Log.Info( $"CreateCard: {gridPos} {cardType}" );
//var cardObj = CardPrefab.Clone( _cardContainer.WorldPosition, Rotation.FromPitch(180f) );
var cardObj = CardPrefab.Clone( new Vector3( 999f, 999f, 999f ), Rotation.FromPitch( 180f ) );
cardObj.Parent = _cardContainer;
//cardObj.LocalPosition = GetCardPos( gridPos );
cardObj.Name = $"card ({cardType}) - {cardTypeID}";
var className = $"Card{cardType}";
var typeDesc = TypeLibrary.GetType<Card>( className );
var card = (Card)cardObj.Components.Create( typeDesc );
if ( card == null )
{
Log.Error( $"Failed to create card of type {className}!" );
return null;
}
card.Init(cardType);
card.GridPos = gridPos;
card.CardTypeID = cardTypeID;
Cards.Add( card );
_cardPositions.Add( gridPos, card );
if(createOutline)
{
var outlineObj = CardOutlinePrefab.Clone();
outlineObj.Parent = _cardContainer;
var cardPos = GetCardPos( gridPos );
outlineObj.LocalPosition = new Vector3( cardPos.x, cardPos.y - 1f, 2f );
}
return card;
}
public MenuCard CreateMenuCard( Vector3 pos )
{
var cardObj = MenuCardPrefab.Clone( pos );
var menuCard = cardObj.Components.Get<MenuCard>();
return menuCard;
}
public async Task ChooseCardAsync( Card card )
{
IsMoveResolving = true;
TimeSinceMoveStartResolving = 0f;
await ForceCursorSwitch();
_hasMadeFirstMove = true;
card.StartScaling( time: 0.25f, amount: 1.15f, easingType: EasingType.SineInOut );
card.NumTimesChosen++;
ChosenCards.Add( card );
PlayCardSfx( "card_flip", card, pitch: Game.Random.Float(0.9f, 1.1f) );
await RevealCard( card );
if ( card.IsFlagShown )
{
card.HideFlag();
Stats[StatType.NumFlags]--;
}
await EventHappened( EventType.Choose, card );
bool resetTimer = Stats[StatType.TimerResetAfterBothChoices] > 0f ? (ChosenCards.Count == 2) : true;
if ( ChosenCards.Count == 2 )
await CheckMatchAsync();
IsMoveResolving = false;
await ForceCursorSwitch();
if( resetTimer )
{
TimerElapsed = 0f;
TimerMoveStartTime = Time.Now;
}
}
public static Vector3 GetCardPos(IntVector2 gridPos)
{
return new Vector3( gridPos.x * GRID_X_SPACING, gridPos.y * GRID_Y_SPACING, Globals.CARD_DEFAULT_HEIGHT);
}
public Card GetCardAtGridPos(IntVector2 gridPos)
{
if ( !IsGridPosInBounds( gridPos ) )
return null;
if(_cardPositions.ContainsKey(gridPos))
return _cardPositions[gridPos];
return null;
}
public Card GetCardAtGridPos( int x, int y )
{
return GetCardAtGridPos( new IntVector2( x, y ) );
}
public Card GetRandomCard()
{
if ( Cards.Count == 0 )
return null;
return Cards[Game.Random.Int( 0, Cards.Count - 1 )];
}
public Card GetRandomCard(Card except0, Card except1)
{
if ( Cards.Count == 0 )
return null;
var cards = Cards.Where(x => x != except0 && x != except1);
if ( cards.Count() == 0 )
return null;
return cards.ElementAt(Game.Random.Int( 0, cards.Count() - 1 ));
}
public Card GetRandomCard( Card except0, Card except1, Card except2 )
{
if ( Cards.Count == 0 )
return null;
var cards = Cards.Where( x => x != except0 && x != except1 && x != except2 );
if ( cards.Count() == 0 )
return null;
return cards.ElementAt( Game.Random.Int( 0, cards.Count() - 1 ) );
}
public Card GetRandomCard( List<Card> except )
{
if ( Cards.Count == 0 )
return null;
var cards = Cards.ToList();
cards.Shuffle();
foreach( var card in cards )
{
if ( !except.Contains( card ) )
return card;
}
return null;
}
public List<IntVector2> GetAllGridPositions(bool empty)
{
List<IntVector2> allEmptyGridPositions = new();
for ( int x = 0; x < GridWidth; x++ )
{
for ( int y = 0; y < GridHeight; y++ )
{
var gridPos = new IntVector2( x, y );
var card = GetCardAtGridPos( gridPos );
if ( (empty && card == null) || (!empty && card != null) )
allEmptyGridPositions.Add( gridPos );
}
}
return allEmptyGridPositions;
}
public List<IntVector2> GetRandomEmptyGridPositions(int num)
{
List<IntVector2> output = new();
List<IntVector2> allEmptyGridPositions = new();
for ( int x = 0; x < GridWidth; x++ )
{
for ( int y = 0; y < GridHeight; y++ )
{
var gridPos = new IntVector2( x, y );
if(GetCardAtGridPos(gridPos) == null )
allEmptyGridPositions.Add( gridPos );
}
}
allEmptyGridPositions.Shuffle();
for( int i = 0; i < Math.Min(num, allEmptyGridPositions.Count); i++ )
{
output.Add( allEmptyGridPositions[i]);
}
return output;
}
public IntVector2 GetRandomGridPos(IntVector2 except)
{
IntVector2 gridPos = new IntVector2( Game.Random.Int( 0, GridWidth - 1 ), Game.Random.Int( 0, GridHeight - 1 ) );
while(gridPos.x == except.x && gridPos.y == except.y )
gridPos = new IntVector2( Game.Random.Int( 0, GridWidth - 1 ), Game.Random.Int( 0, GridHeight - 1 ) );
return gridPos;
}
public List<Card> GetCardsOfType(CardType cardType, Card except = null)
{
return Cards.Where( x => x.CardType == cardType && x != except ).ToList();
}
public bool IsGridPosEmpty( IntVector2 gridPos )
{
return !_cardPositions.ContainsKey( gridPos );
}
public bool IsGridPosCorner(IntVector2 gridPos)
{
return (gridPos.x == 0 && gridPos.y == 0) || (gridPos.x == 0 && gridPos.y == GridHeight - 1) || (gridPos.x == GridWidth - 1 && gridPos.y == 0) || (gridPos.x == GridWidth - 1 && gridPos.y == GridHeight - 1);
}
public async Task SetCardGridPos(Card card, IntVector2 gridPos)
{
if ( !IsGridPosInBounds( gridPos ) )
{
Log.Error( $"SetCardGridPos - gridpos {gridPos} is not in bounds!" );
return;
}
if ( _cardPositions.ContainsKey( gridPos ) )
{
Log.Error( $"SetCardGridPos - {gridPos} isn't empty!" );
return;
}
if ( _cardPositions.ContainsKey( card.GridPos ) && _cardPositions[card.GridPos] == card )
{
Log.Error( $"SetCardGridPos - remove {card.CardType}'s old gridposition first!" );
return;
}
_cardPositions[gridPos] = card;
card.GridPos = gridPos;
MovedCard = card;
await EventHappened( EventType.MoveCard, card );
}
public void SetCardGridPosNonAsync( Card card, IntVector2 gridPos, bool instant = false )
{
if ( !IsGridPosInBounds( gridPos ) )
{
Log.Error( $"SetCardGridPosNonAsync - gridpos {gridPos} is not in bounds!" );
return;
}
if ( _cardPositions.ContainsKey( gridPos ) )
{
Log.Error( $"SetCardGridPosNonAsync - {gridPos} isn't empty!" );
return;
}
if ( _cardPositions.ContainsKey( card.GridPos ) && _cardPositions[card.GridPos] == card )
{
Log.Error( $"SetCardGridPosNonAsync - remove {card.CardType}'s old gridposition first!" );
return;
}
_cardPositions[gridPos] = card;
card.GridPos = gridPos;
if ( instant )
card.LocalPosition = GetCardPos( gridPos );
}
public void RemoveCardGridPos(Card card)
{
if(!_cardPositions.ContainsKey(card.GridPos))
{
Log.Error( $"RemoveCardPosition - {card.GridPos} doesn't have a card!" );
return;
}
var existingCard = _cardPositions[card.GridPos];
if(existingCard != card)
{
Log.Error( $"RemoveCardGridPos ({card.CardType}) - A different card ({existingCard.CardType}) exists at {card.GridPos}!" );
}
_cardPositions.Remove( card.GridPos );
}
public async Task SwapCardPositions( Card card0, Card card1, bool instant = false )
{
var gridPos0 = card0.GridPos;
var gridPos1 = card1.GridPos;
//Log.Info( $"SwapCardPositions - {card0.CardType} {card0.GridPos} {GetCardAtGridPos( card0.GridPos )}, {card1.CardType} {card1.GridPos} {GetCardAtGridPos( card1.GridPos )}" );
RemoveCardGridPos( card0 );
RemoveCardGridPos( card1 );
SetCardGridPosNonAsync( card0, gridPos1, instant );
SetCardGridPosNonAsync( card1, gridPos0, instant );
SwappedCards.Clear();
SwappedCards.Add( card0 );
SwappedCards.Add( card1 );
await EventHappened( EventType.SwapCards, card0, card1 );
}
public void SwapCardPositionsNonAsync( Card card0, Card card1, bool instant = false )
{
var gridPos0 = card0.GridPos;
var gridPos1 = card1.GridPos;
RemoveCardGridPos( card0 );
RemoveCardGridPos( card1 );
SetCardGridPosNonAsync( card0, gridPos1, instant );
SetCardGridPosNonAsync( card1, gridPos0, instant );
}
public bool IsGridPosInBounds(IntVector2 gridPos)
{
return gridPos.x >= 0 && gridPos.x < GridWidth && gridPos.y >= 0 && gridPos.y < GridHeight;
}
public List<IntVector2> GetNearbyGridPositions( IntVector2 gridPos, bool adjacentOnly = false)
{
List<IntVector2> offsets = adjacentOnly
? new() { new IntVector2( -1, 0 ), new IntVector2( 0, 1 ), new IntVector2( 1, 0 ), new IntVector2( 0, -1 ) }
: new() { new IntVector2( -1, -1 ), new IntVector2( -1, 0 ), new IntVector2( -1, 1 ), new IntVector2( 0, 1 ), new IntVector2( 1, 1 ), new IntVector2( 1, 0 ), new IntVector2( 1, -1 ), new IntVector2( 0, -1 ) };
List<IntVector2> gridPositions = new();
foreach ( var offset in offsets )
{
if( IsGridPosInBounds( gridPos + offset ) )
gridPositions.Add( gridPos + offset );
}
return gridPositions;
}
public List<Card> GetNearbyCards(IntVector2 gridPos, bool adjacentOnly = false )
{
List<Card> nearbyCards = new();
var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );
foreach ( var nearbyGridPos in nearbyGridPositions )
{
var card = GetCardAtGridPos( nearbyGridPos );
if ( card != null )
nearbyCards.Add( card );
}
return nearbyCards;
}
public int GetNumNearbyCards( IntVector2 gridPos, bool adjacentOnly = false )
{
var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );
int count = 0;
foreach ( var nearbyGridPos in nearbyGridPositions )
{
var card = GetCardAtGridPos( nearbyGridPos );
if ( card != null )
count++;
}
return count;
}
public Card GetRandomNearbyCard( IntVector2 gridPos, bool adjacentOnly = false )
{
List<Card> nearbyCards = new();
var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );
foreach ( var nearbyGridPos in nearbyGridPositions )
{
var card = GetCardAtGridPos( nearbyGridPos );
if ( card != null )
nearbyCards.Add( card );
}
if ( nearbyCards.Count == 0 )
return null;
return nearbyCards[Game.Random.Int(0, nearbyCards.Count - 1)];
}
public int GetNumNearbyEmptyGridPositions( IntVector2 gridPos, bool adjacentOnly = false )
{
var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );
int count = 0;
foreach ( var nearbyGridPos in nearbyGridPositions )
{
var card = GetCardAtGridPos( nearbyGridPos );
if ( card == null )
count++;
}
return count;
}
public IntVector2 GetRandomNearbyGridPos( IntVector2 gridPos, bool empty, bool adjacentOnly = false )
{
List<IntVector2> validGridPositions = new();
var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );
foreach ( var nearbyGridPos in nearbyGridPositions )
{
var card = GetCardAtGridPos( nearbyGridPos );
if ( (empty && card == null) || (!empty && card != null) )
validGridPositions.Add( nearbyGridPos );
}
if ( validGridPositions.Count == 0 )
return new IntVector2(-1, -1);
return validGridPositions[Game.Random.Int( 0, validGridPositions.Count - 1 )];
}
public List<IntVector2> GetNearbyGridPositions( IntVector2 gridPos, bool empty, bool adjacentOnly = false )
{
List<IntVector2> validGridPositions = new();
var nearbyGridPositions = GetNearbyGridPositions( gridPos, adjacentOnly );
foreach ( var nearbyGridPos in nearbyGridPositions )
{
var card = GetCardAtGridPos( nearbyGridPos );
if ( (empty && card == null) || (!empty && card != null) )
validGridPositions.Add( nearbyGridPos );
}
return validGridPositions;
}
public static bool IsAdjacent(IntVector2 posA, IntVector2 posB)
{
return (posA - posB).ManhattanLength == 1;
}
public static bool IsNearby( IntVector2 posA, IntVector2 posB )
{
return MathF.Abs( posA.x - posB.x ) <= 1 && MathF.Abs( posA.y - posB.y ) <= 1;
}
static Tuple<int, int> FindBestGridSize( int numCards )
{
int maxHeight = 5;
int maxWidth = 10;
int bestWidth = 0;
int bestHeight = 0;
int bestDiff = int.MaxValue;
// Loop through possible widths in descending order
for ( int width = Math.Min( maxWidth, numCards ); width >= 1; width-- )
{
if ( numCards % width == 0 )
{
int height = numCards / width;
if ( height <= maxHeight )
{
int diff = Math.Abs( width - height );
if ( diff < bestDiff || (diff == bestDiff && width > bestWidth) )
{
bestDiff = diff;
bestWidth = width;
bestHeight = height;
}
}
}
}
if(bestWidth == 0 || bestHeight == 0)
{
Log.Error( $"No grid size found for {numCards / 2} pairs!" );
}
return Tuple.Create( bestWidth, bestHeight );
}
async Task FinishedLevel()
{
await Task.DelayRealtime( 750 );
BeatPreviousLevelTime = TimeSinceRunStart.Relative;
//Log.Info( $"BeatPreviousLevelTime: {BeatPreviousLevelTime} LevelNum: {LevelNum}" );
if (LevelNum == 10)
{
foreach ( var child in _cardContainer.Children )
child.Destroy();
Victory();
}
else
{
await EventHappened( EventType.FinishLevel );
await Task.DelayRealtime( 300 );
foreach ( var child in _cardContainer.Children )
child.Destroy();
switch(LevelNum)
{
case 2:
Sandbox.Services.Achievements.Unlock( "defeat_ogre" );
break;
case 4:
Sandbox.Services.Achievements.Unlock( "defeat_clown" );
break;
case 6:
Sandbox.Services.Achievements.Unlock( "defeat_police" );
break;
case 8:
Sandbox.Services.Achievements.Unlock( "defeat_king" );
break;
}
TimerElapsed = 0f;
StartBuyPhase();
}
}
public void StartBuyPhase()
{
DetermineOfferedRelics();
IsLevelActive = false;
GameState = GameState.BuyPhase;
Manager.Instance.PlaySfxCenter( "shop", volume: 0.3f, pitch: Game.Random.Float( 0.95f, 1.05f ) );
PlaySong( SongShop );
FadeIn();
}
public void DetermineOfferedRelics()
{
BuyPhaseOfferedRelics.Clear();
List<(RelicType, float)> relicWeights = new();
bool offerBloodDonation = LevelNum == 1 ? true : Game.Random.Float( 0f, 1f ) < Utils.Map( LevelNum, 2, 9, 0.35f, 0.2f );
AddRelicWeight( RelicType.Bandage, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 5f, 0.8f, EasingType.QuadOut ), extraMoney: offerBloodDonation ? 3 : 0 );
//AddRelicWeight( RelicType.Bandage, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.4f, 0.2f, EasingType.QuadIn ), extraMoney: offerBloodDonation ? 3 : 0);
AddRelicWeight( RelicType.Salad, relicWeights, weight: Utils.Map( LevelNum, 1, 4, 1f, 0.45f ), extraMoney: offerBloodDonation ? 3 : 0 );
//AddRelicWeight( RelicType.Salad, relicWeights, weight: Utils.Map( LevelNum, 1, 4, 0.25f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0);
AddRelicWeight( RelicType.Chocolate, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.55f, 2f, EasingType.QuadIn ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.BloodDonation, relicWeights, weight: offerBloodDonation ? float.MaxValue : 0f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Shield, relicWeights, weight: Utils.Map( LevelNum, 1, 8, 0.5f, 0.4f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Socks, relicWeights, weight: Utils.Map( LevelNum, 1, 3, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Pill, relicWeights, minLevel: 2, weight: 0.7f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Butter, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.2f, 0.3f, EasingType.QuadIn ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.ShoppingCart, relicWeights, weight: Utils.Map( LevelNum, 1, 5, 0.15f, 0.4f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Trophy, relicWeights, weight: Utils.Map( LevelNum, 1, 5, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Flag, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.2f, 0.4f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Crayon, relicWeights, minLevel: 2, weight: Utils.Map( LevelNum, 1, 6, 0.1f, 0.5f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Wristwatch, relicWeights, weight: Utils.Map( LevelNum, 1, 6, 0.4f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.PrayerBeads, relicWeights, minLevel: 2, weight: Utils.Map( LevelNum, 2, 8, 0.2f, 0.6f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.MedicHelmet, relicWeights, weight: Utils.Map( LevelNum, 1, 4, 0.1f, 0.6f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.MagnifyingGlass, relicWeights, weight: Utils.Map( LevelNum, 1, 8, 0.5f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Battery, relicWeights, minLevel: 2, weight: 0.225f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.SlotMachine, relicWeights, weight: Utils.Map( LevelNum, 1, 6, 0.1f, 0.2f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.MedicalLicense, relicWeights, minLevel: 2, weight: 0.25f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Magic8Ball, relicWeights, weight: Utils.Map( LevelNum, 1, 6, 0.15f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.RoboAssistant, relicWeights, weight: 0.175f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.GuideDog, relicWeights, minLevel: 3, weight: 0.2f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.LoveLetter, relicWeights, weight: Utils.Map( LevelNum, 1, 6, 0.08f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Taxi, relicWeights, minLevel: 4, weight: 0.2f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Microscope, relicWeights, minLevel: 3, weight: Utils.Map( LevelNum, 3, 0, 0.08f, 0.3f, EasingType.SineIn ), extraMoney: offerBloodDonation ? 3 : 0 );
//AddRelicWeight( RelicType.Satellite, relicWeights, minLevel: 2, weight: 0.3f, extraMoney: offerBloodDonation ? 3 : 0);
AddRelicWeight( RelicType.Satellite, relicWeights, minLevel: 2, weight: Utils.Map( LevelNum, 2, 9, 0.1f, 0.6f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Coupon, relicWeights, maxLevel: 8, weight: Utils.Map( LevelNum, 1, 8, 0f, 0.05f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.WarMedal, relicWeights, weight: Utils.Map( LevelNum, 1, 7, 0.25f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.HotPot, relicWeights, weight: Utils.Map( LevelNum, 1, 5, 0.1f, 0.3f, EasingType.QuadIn ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Abacus, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
//AddRelicWeight( RelicType.Briefcase, relicWeights, weight: Utils.Map( LevelNum, 1, 7, 0.1f, 0.2f ), extraMoney: offerBloodDonation ? 3 : 0);
AddRelicWeight( RelicType.Saw, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Sign, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.12f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.TriangleRuler, relicWeights, minLevel: 3, weight: Utils.Map( LevelNum, 3, 9, 0.1f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Chopsticks, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.RecycleBin, relicWeights, weight: 0.17f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Clipboard, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.35f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Juicebox, relicWeights, weight: 0.1f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.SacrificialBlade, relicWeights, weight: 0.125f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Scale, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.075f, 0.2f, EasingType.SineOut ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Telescope, relicWeights, weight: Utils.Map( LevelNum, 3, 9, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.BowAndArrow, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.25f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.MantlepieceClock, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.2f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Dartboard, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.25f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.PirateFlag, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.15f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.Revolver, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.2f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.MammothMeat, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.8f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.GrandfatherClock, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.8f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.PersonalChef, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.5f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.MouseTrap, relicWeights, weight: 0.15f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.KitchenKnife, relicWeights, weight: 0.15f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.LightBulb, relicWeights, weight: 0.125f, extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.RunningShoes, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.1f, 0.3f ), extraMoney: offerBloodDonation ? 3 : 0 );
AddRelicWeight( RelicType.CreditCard, relicWeights, weight: Utils.Map( LevelNum, 1, 9, 0.025f, 0.1f ), extraMoney: offerBloodDonation ? 3 : 0 );
NumRelicsToShow = MathX.FloorToInt( Utils.Map( LevelNum, 1, 7, 6f, 12f ) );
//NumRelicsToShow = 12;
for ( int i = 0; i < NumRelicsToShow + 3; i++ )
{
var relicType = GetOfferedRelicType( relicWeights );
if ( relicType != RelicType.None )
{
BuyPhaseOfferedRelics.Add( relicType );
int index = relicWeights.FindIndex( x => x.Item1.Equals( relicType ) );
if ( index >= 0 )
relicWeights.RemoveAt( index );
}
}
// shuffle BloodDonation
if( LevelNum == 1 && BuyPhaseOfferedRelics.Count >= 6 && BuyPhaseOfferedRelics[0] == RelicType.BloodDonation )
{
int otherRelicIndex = Game.Random.Int( 1, 5 );
RelicType otherRelicType = BuyPhaseOfferedRelics[otherRelicIndex];
BuyPhaseOfferedRelics[otherRelicIndex] = RelicType.BloodDonation;
BuyPhaseOfferedRelics[0] = otherRelicType;
}
BuyPhaseHash++;
BoughtItems.Clear();
for ( int i = 0; i < BuyPhaseOfferedRelics.Count; i++ )
BoughtItems.Add( false );
}
void AddRelicWeight( RelicType relicType, List<(RelicType, float)> relicWeights, int minLevel = 0, int maxLevel = MAX_LEVEL, float weight = 1f, int extraMoney = 0)
{
if ( LevelNum < minLevel || LevelNum > maxLevel )
return;
int currLevel = GetRelicLevel( relicType );
var price = Relic.GetRelicPrice( relicType, currLevel + 1 );
var costsHP = Relic.DoesRelicCostHP( relicType ) || Stats[StatType.AllItemsCostHP] > 0f;
int currency = costsHP ? HP - 1: Money;
if ( currency < (costsHP ? price : price - extraMoney) && currency > 0 )
return;
foreach ( var pair in relicWeights )
{
if ( pair.Item1 == relicType )
currLevel++;
}
var existingRelic = GetRelic( relicType );
if ( existingRelic != null && currLevel >= existingRelic.MaxLevel )
return;
var existingLevel = existingRelic?.Level ?? 0;
if(existingLevel > 0 && Stats[StatType.ExistingItemExtraChance] > 0f )
weight *= (1f + Stats[StatType.ExistingItemExtraChance]);
relicWeights.Add( (relicType, weight) );
}
RelicType GetOfferedRelicType( List<(RelicType, float)> relicWeights )
{
var totalWeight = 0f;
foreach(var pair in relicWeights)
{
totalWeight += pair.Item2;
}
var randWeight = Game.Random.Float( 0f, totalWeight );
totalWeight = 0f;
foreach ( var pair in relicWeights )
{
totalWeight += pair.Item2;
if ( totalWeight > randWeight )
{
return pair.Item1;
}
}
return RelicType.None;
}
async Task CheckLockedCards()
{
foreach(var card in Cards)
{
if(card.IsLocked)
{
if(card.WasJustLocked)
{
card.WasJustLocked = false;
}
else
{
card.LockTurnsRemaining--;
if ( card.LockTurnsRemaining <= 0 )
{
await UnlockCard( card );
}
}
}
}
}
public void GameOver()
{
IsLevelActive = false;
GameState = GameState.Failure;
FinalRunTime = LevelNum > 1 ? BeatPreviousLevelTime : TimeSinceRunStart.Relative;
PlaySong( SongDefeat );
SubmitScore( levelsBeat: LevelNum - 1, runTime: FinalRunTime );
Sandbox.Services.Stats.Increment( $"died-level-{LevelNum}", 1 );
}
public void Victory()
{
IsLevelActive = false;
GameState = GameState.Victory;
FinalRunTime = TimeSinceRunStart.Relative;
PlaySong( SongVictory );
SubmitScore( levelsBeat: 10, runTime: TimeSinceRunStart.Relative );
Sandbox.Services.Achievements.Unlock( "defeat_wizard" );
Sandbox.Services.Stats.Increment( "game_wins", 1 );
}
public void SubmitScore(int levelsBeat, float runTime)
{
if ( levelsBeat <= 0 )
return;
// lower score is better
float scorePerLevel = 1000000f;
float baseScore = scorePerLevel * (10 - levelsBeat);
float totalScore = baseScore + runTime;
Sandbox.Services.Stats.SetValue( "total_score", totalScore );
//Log.Info( $"submitting score: {totalScore}" );
}
public void PlayCardSfx( string name, Card card, float volume = 1f, float pitch = 1f, float depthDiff = Globals.CARD_SFX_DEPTH_DIFF )
{
var pos = card.WorldPosition.WithZ( Camera.WorldPosition.z - depthDiff );
PlaySfx( name, pos, volume, pitch );
}
public void PlayCardSfxBetween( string name, Card card0, Card card1, float volume = 1f, float pitch = 1f, float depthDiff = Globals.CARD_SFX_DEPTH_DIFF )
{
var pos = ((card0.WorldPosition + card1.WorldPosition) / 2f).WithZ( Camera.WorldPosition.z - depthDiff );
PlaySfx( name, pos, volume, pitch );
}
public void PlayCardSfxBetweenGridPositions( string name, IntVector2 gridPos0, IntVector2 gridPos1, float volume = 1f, float pitch = 1f, float depthDiff = Globals.CARD_SFX_DEPTH_DIFF )
{
var pos = (((GetCardPos(gridPos0) + GetCardPos(gridPos1)) / 2f) + _cardContainer.WorldPosition).WithZ( Camera.WorldPosition.z - depthDiff );
PlaySfx( name, pos, volume, pitch );
}
public void PlaySfxCenter( string name, float volume = 1f, float pitch = 1f )
{
var camera = Scene.Camera;
var ray = camera.ScreenPixelToRay( new Vector3( Screen.Width / 2, Screen.Height / 2, 0f ) );
var tr = Scene.Trace.Ray( ray, 10000f ).Run();
var sfx = Sound.Play( name, SfxMixer );
if ( sfx != null )
{
sfx.Position = tr.EndPosition.WithZ( Scene.Camera.WorldPosition.z - Globals.CARD_SFX_DEPTH_DIFF );
sfx.Volume = volume;
sfx.Pitch = pitch;
}
}
public void PlaySfxScreenPos( string name, Vector2 screenPos, float volume = 1f, float pitch = 1f )
{
var camera = Scene.Camera;
var ray = camera.ScreenPixelToRay( new Vector3(screenPos.x, screenPos.y, 0f) );
var tr = Scene.Trace.Ray( ray, 10000f ).Run();
var sfx = Sound.Play( name, SfxMixer );
if ( sfx != null )
{
sfx.Position = tr.EndPosition.WithZ( Scene.Camera.WorldPosition.z - Globals.CARD_SFX_DEPTH_DIFF );
sfx.Volume = volume;
sfx.Pitch = pitch;
}
}
public void PlaySfx( string name, Vector3 pos, float volume = 1f, float pitch = 1f )
{
var sfx = Sound.Play( name, SfxMixer );
if ( sfx != null )
{
sfx.Position = pos;
sfx.Volume = volume;
sfx.Pitch = pitch;
}
}
public void PlaySfx(string name)
{
Sound.Play( name, SfxMixer );
}
public void PushEventMessage(IEventHandler eventHandler, EventType eventType)
{
EventMessageStack.Push((eventHandler, eventType));
EventMessageHash++;
}
public void PopEventMessage()
{
EventMessageStack.Pop();
EventMessageHash++;
}
public void IncreaseRelicLevel(RelicType relicType)
{
Relic relic = null;
foreach( var r in Relics )
{
if ( r.RelicType == relicType )
relic = r;
}
if(relic == null)
{
var className = $"Relic{relicType}";
var typeDesc = TypeLibrary.GetType<Relic>( className );
relic = (Relic)_relicContainer.Components.Create( typeDesc );
relic.RelicType = relicType;
relic.Init();
Relics.Add( relic );
if(Relics.Count >= 30)
{
Sandbox.Services.Achievements.Unlock( "item_hoarder" );
}
}
relic.LevelUp();
RelicHash++;
StatsBuyItem( relicType );
}
public Relic GetRelic(RelicType relicType)
{
foreach(var relic in Relics)
{
if ( relic.RelicType == relicType )
return relic;
}
return null;
}
public int GetRelicLevel( RelicType relicType )
{
foreach ( var relic in Relics )
{
if ( relic.RelicType == relicType )
return relic.Level;
}
return 0;
}
public void RemoveRelic(Relic relic)
{
if ( Relics.Contains( relic ) )
{
Relics.Remove( relic );
RelicHash++;
}
}
public StatusEffect AddStatus(string statusClassName)
{
var typeDesc = TypeLibrary.GetType<StatusEffect>( statusClassName );
var status = (StatusEffect)_statusContainer.Components.Create( typeDesc );
Statuses.Add( status );
return status;
}
public void RemoveStatus(StatusEffect status)
{
if( Statuses.Contains( status ) )
{
Statuses.Remove( status );
var component = _statusContainer.Components.Get(status.GetType());
component?.Destroy();
}
}
public void ShowOverlayColor(Color color, float time)
{
ShouldShowOverlayColor = true;
TimeSinceOverlayStart = 0f;
OverlayTime = time;
OverlayColor = color;
}
void HandleOverlay()
{
if ( !ShouldShowOverlayColor )
return;
if(TimeSinceOverlayStart > OverlayTime)
ShouldShowOverlayColor = false;
}
public void ShakeCamera(float strength, float time)
{
_isCameraShaking = true;
_cameraShakeTime = time;
_timeSinceCameraShakeStart = 0f;
_cameraShakeStrength = strength;
}
void HandleCameraShake()
{
if ( !_isCameraShaking )
return;
if(_timeSinceCameraShakeStart > _cameraShakeTime)
{
_isCameraShaking = false;
Camera.WorldPosition = _cameraStartPos;
}
else
{
var randomVec = Utils.GetRandomVector()
* _cameraShakeStrength
* Utils.Map( _timeSinceCameraShakeStart, 0f, 0.05f, 0f, 1f, EasingType.SineIn)
* Utils.Map(_timeSinceCameraShakeStart, 0f, _cameraShakeTime, 1f, 0f, EasingType.SineOut);
Camera.WorldPosition = _cameraStartPos + new Vector3( randomVec.x, randomVec.y, 0f );
}
}
public void SpawnFloaterText( Vector3 pos, string text, string emojiText, float lifetime, Color color, Vector2 velocity, float deceleration, float fontSize, float startScale = 1f, float endScale = 1f )
{
var textObj = new GameObject();
textObj.Name = "floater text";
textObj.WorldPosition = pos;
var floaterText = textObj.Components.Create<FloaterText>();
floaterText.Init( text, emojiText, lifetime, color, velocity, deceleration, fontSize, startScale, endScale );
NumFloaters++;
}
public void SpawnFloaterImage( Vector3 pos, string filename, float lifetime, Vector2 velocity, float deceleration, float startScale = 1f, float endScale = 1f )
{
var textObj = new GameObject();
textObj.Name = filename;
textObj.WorldPosition = pos;
var floaterImage = textObj.Components.Create<FloaterImage>();
floaterImage.Init( filename, lifetime, velocity, deceleration, startScale, endScale );
NumFloaters++;
}
public void SpawnHealHPFloater( int healAmount, Vector3 pos )
{
SpawnFloaterText(
pos: pos.WithZ( 100f ),
text: $"+{healAmount}",
emojiText: "❤️",
lifetime: 2f,
color: new Color( 1f, 0f, 0f ),
velocity: new Vector2( 0f, 35f ),
deceleration: 2.1f,
fontSize: 60f,
startScale: 1f,
endScale: 1.1f
);
}
public void SpawnLoseHPFloater( int lossAmount, Vector3 pos )
{
SpawnFloaterText(
pos: pos.WithZ( 100f ),
text: $"-{lossAmount}",
emojiText: "❤️",
lifetime: 2f,
color: new Color( 0.8f, 0f, 0f ),
velocity: new Vector2( 0f, -32f ),
deceleration: 1.1f,
fontSize: 60f,
startScale: 1.1f,
endScale: 0.9f
);
}
public void SpawnMaxHPFloater( int amount, Vector3 pos )
{
SpawnFloaterText(
pos: pos.WithZ( 100f ),
text: $"+{amount} Max HP",
emojiText: "",
lifetime: 2f,
color: new Color( 1f, 0f, 0f ),
velocity: new Vector2( 0f, 35f ),
deceleration: 2.1f,
fontSize: 40f,
startScale: 1f,
endScale: 1.1f
);
}
public void SpawnGainMoneyFloater( int moneyAmount, Vector3 pos )
{
SpawnFloaterText(
pos: pos.WithZ( 100f ),
text: $"+${moneyAmount}",
emojiText: "",
lifetime: 2f,
color: new Color( 1f, 1f, 0f ),
velocity: new Vector2( 0f, 35f ),
deceleration: 2.1f,
fontSize: 60f,
startScale: 1f,
endScale: 1.1f
);
}
public void SpawnLoseMoneyFloater( int moneyAmount, Vector3 pos )
{
SpawnFloaterText(
pos: pos.WithZ( 100f ),
text: $"-${moneyAmount}",
emojiText: "",
lifetime: 1.5f,
color: new Color( 0.7f, 0.7f, 0f ),
velocity: new Vector2( 0f, -25f ),
deceleration: 2f,
fontSize: 60f,
startScale: 1f,
endScale: 1.1f
);
}
public void SpawnFloaterLock( Vector3 pos, int numTurns )
{
var lockObj = new GameObject();
lockObj.Name = "floater lock";
lockObj.WorldPosition = pos;
var floaterLock = lockObj.Components.Create<FloaterLock>();
floaterLock.Init( numTurns );
NumFloaters++;
}
public async Task LockCard( Card card, int numTurns )
{
Manager.Instance.SpawnFloaterLock(
pos: card.WorldPosition.WithZ( 100f ),
numTurns
);
card.Lock( numTurns );
await Task.DelayRealtime( 900 );
await ShakeCard( card );
}
public async Task UnlockCard( Card card )
{
card.Unlock();
await Task.DelayRealtime( 0 );
}
public float GetTimerTotalTime()
{
return Stats[StatType.TimerTotalTimeOverride] > 0f
? Stats[StatType.TimerTotalTimeOverride]
: Stats[StatType.TimerTotalTime];
}
public void SpawnCardBreak(Vector3 pos, Rotation rot, float scale, Material material, int breakNum)
{
//var prefab = CardBreakPrefabs[Game.Random.Int( 0, CardBreakPrefabs.Count - 1 )];
//var prefab = CardBreakPrefabs[2];
var prefab = CardBreakPrefabs[breakNum];
var cardBreakObj = prefab.Clone( pos, rot );
var cardBreaker = cardBreakObj.Components.Get<CardBreaker>();
cardBreaker.Init( material, rot, scale );
}
private List<IntVector2> _gridPath;
private List<IntVector2> _walkable;
public List<IntVector2> GetPathTo( IntVector2 a, IntVector2 b )
{
if ( _gridPath == null )
_gridPath = new List<IntVector2>();
else
_gridPath.Clear();
_gridPath.Clear();
if ( (a - b).ManhattanLength <= 1 )
{
_gridPath.Add( b );
return _gridPath;
}
List<IntVector2> tempPath = new List<IntVector2>();
if ( Utils.AStar<IntVector2>( a, b, tempPath, GetEdges, GetHScoreFromGridPosToGridPos ) )
{
_gridPath.AddRange( tempPath );
// remove start pos
_gridPath.RemoveAt( 0 );
}
return _gridPath;
}
public List<IntVector2> GetWalkableAdjacentGridPositions( IntVector2 start )
{
if ( _walkable == null )
_walkable = new List<IntVector2>();
else
_walkable.Clear();
IntVector2 left = start + new IntVector2( -1, 0 );
if ( IsGridPosInBounds( left ) )
_walkable.Add( left );
IntVector2 right = start + new IntVector2( 1, 0 );
if ( IsGridPosInBounds( right ) )
_walkable.Add( right );
IntVector2 down = start + new IntVector2( 0, -1 );
if ( IsGridPosInBounds( down ) )
_walkable.Add( down );
IntVector2 up = start + new IntVector2( 0, 1 );
if ( IsGridPosInBounds( up ) )
_walkable.Add( up );
return _walkable;
}
static float GetHScoreFromGridPosToGridPos( IntVector2 a, IntVector2 b )
{
return (b - a).ManhattanLength;
}
IEnumerable<AStarEdge<IntVector2>> GetEdges( IntVector2 start )
{
var walkable = GetWalkableAdjacentGridPositions( start );
return walkable.Select( gridPos => Utils.Edge( gridPos, GetCostToMoveFromGridPosToAdjacentGridPos( start, gridPos ) ) );
}
float GetCostToMoveFromGridPosToAdjacentGridPos( IntVector2 a, IntVector2 b )
{
return 1f;
}
public void FadeIn()
{
IsFadingIn = true;
TimeSinceStartFadingIn = 0f;
}
public void RefreshMusicMixerVolume()
{
float volume = MusicVolume * 0.01f * (IsFadingIn ? Utils.Map( TimeSinceStartFadingIn, 0f, FADE_IN_TIME, 0f, 1f ) : 1f);
MusicMixer.Volume = Math.Min( volume, 1f );
}
public void RefreshSfxMixerVolume()
{
float volume = SfxVolume * 0.01f * 2f;
SfxMixer.Volume = Math.Min( volume, 2f );
}
public void SetMute(bool mute)
{
if( mute )
{
SfxMixer.Volume = 0f;
MusicMixer.Volume = 0f;
}
else
{
RefreshMusicMixerVolume();
RefreshSfxMixerVolume();
}
}
public void SetBgTint(Color color)
{
var bg = Scene.Directory.FindByName( "bg" ).First();
foreach(var child in bg.Children)
{
child.Components.Get<ModelRenderer>().Tint = color;
}
}
async void InitSboxStats()
{
//Log.Info( "InitSboxStats start" );
var stats = Sandbox.Services.Stats.GetLocalPlayerStats( "facepunch.memory" );
await stats.Refresh();
for ( int i = 1; i < Enum.GetValues( typeof( CardType ) ).Length - 1; i++ )
{
var cardType = (CardType)i;
var value = stats.Get( $"matched-{cardType}" ).Sum;
if ( value > 0 )
SboxStatsMatchedCardTypes.Add( cardType );
}
for ( int i = 1; i < Enum.GetValues( typeof( RelicType ) ).Length - 1; i++ )
{
var relicType = (RelicType)i;
var value = stats.Get( $"bought-{relicType}" ).Sum;
if ( value > 0 )
SboxStatsBoughtRelicTypes.Add( relicType );
}
//Log.Info( "InitSboxStats finished" );
string matched = "";
foreach ( var cardType in SboxStatsMatchedCardTypes )
matched += $"{cardType}, ";
//Log.Info( $"matched: {matched}" );
string bought = "";
foreach ( var relicType in SboxStatsBoughtRelicTypes )
bought += $"{relicType}, ";
//Log.Info( $"bought: {bought}" );
_finishedGettingSboxStats = true;
}
public void StatsMatchCard(CardType cardType)
{
Sandbox.Services.Stats.Increment( "cards_matched", 2 );
if ( !_finishedGettingSboxStats )
return;
if(!SboxStatsMatchedCardTypes.Contains(cardType))
{
SboxStatsMatchedCardTypes.Add( cardType );
Sandbox.Services.Stats.Increment( $"matched-{cardType}", 1 );
Sandbox.Services.Stats.Increment( "card_types_matched", 1 );
}
}
public void StatsBuyItem( RelicType relicType )
{
if ( !_finishedGettingSboxStats )
return;
if ( !SboxStatsBoughtRelicTypes.Contains( relicType ) )
{
SboxStatsBoughtRelicTypes.Add( relicType );
Sandbox.Services.Stats.Increment( $"bought-{relicType}", 1 );
Sandbox.Services.Stats.Increment( "item_types_bought", 1 );
}
}
}