Search the source of every open source package.
3653 results
using Sandbox;
using System.Drawing;
public class FloaterLock : Component
{
public int NumTurns { get; set; }
private TimeSince _timeSinceSpawn;
public float Lifetime { get; set; }
public bool ShowLocked { get; set ; }
private float _startScale;
private float _endScale;
private Vector2 _velocity;
private float _deceleration;
public float Scale { get; set; }
public float Opacity { get; set; }
public void Init( int numTurns )
{
NumTurns = numTurns;
Lifetime = 1.25f;
_velocity = new Vector2( 0f, 12f );
_deceleration = 4f;
Scale = _startScale = 1f;
_endScale = 1f;
}
protected override void OnStart()
{
base.OnStart();
_timeSinceSpawn = 0f;
Manager.Instance.PlaySfx( "lock_start", new Vector3( WorldPosition.x, WorldPosition.y, Manager.Instance.Camera.WorldPosition.z - 100f ), volume: 0.9f, pitch: Game.Random.Float( 1.05f, 1.13f ) );
}
protected override void OnUpdate()
{
Opacity = Utils.Map( _timeSinceSpawn, 0f, 0.3f, 0f, 1f, EasingType.QuadOut ) * Utils.Map( _timeSinceSpawn, 0f, Lifetime - 0.1f, 1f, 0.1f, EasingType.ExpoIn ) * Utils.Map( _timeSinceSpawn, Lifetime - 0.3f, Lifetime - 0.05f, 1f, 0f, EasingType.Linear );
Scale = Utils.Map( _timeSinceSpawn, 0f, Lifetime, _startScale, _endScale, EasingType.SineOut );
WorldPosition += new Vector3( _velocity.x, _velocity.y, 0f ) * Time.Delta;
_velocity *= (1f - _deceleration * Time.Delta);
if( !ShowLocked && _timeSinceSpawn > 0.5f )
{
ShowLocked = true;
_velocity = new Vector2( 0f, -20f );
_deceleration = 7f;
Manager.Instance.PlaySfx( "lock", new Vector3( WorldPosition.x, WorldPosition.y, Manager.Instance.Camera.WorldPosition.z - 100f ), volume: 2f, pitch: Game.Random.Float( 0.97f, 1.03f ) );
}
if ( _timeSinceSpawn > Lifetime )
{
Manager.Instance.NumFloaters--;
GameObject.Destroy();
}
}
}
using Microsoft.VisualBasic;
using Sandbox;
using Sandbox.UI;
using System.Runtime;
using System.Threading.Tasks;
public enum CardType { None, Apple, Bomb, Rocket, Telephone, Trumpet, Pawn, Balloon, Spider, Umbrella, UmbrellaClosed, Juggler, Coin, Vampire, Maracas, Microwave, Raccoon, Rock, Bear, Candle,
CrystalBall, Snake, Rose, RoseWilted, Bell, Ant, Twins, Compass, Antenna, Wizard, AncientScroll, JumpingSpider, Bento, Ninja, Flashlight, Bodybuilder, Helicopter, FlyingMoney,
Syringe, Cellphone, Crab, Magnet, Stethoscope, UFO, Teacher, Tractor, FortuneCookie, Dolphin, Detective, Genie, Potion, Mirror, FaxMachine, Dice, Coffee, Tree, Chipmunk,
Dancer, Wine, King, Police, Ogre, Clown, MoneyBag, Bank, Steak, MagicWand, Carrot, Caterpillar, Butterfly, Tornado, Map, Cockroach, Broccoli, Donut, Cheese, CheeseMoldy, GuideDog, Taco, Burrito,
Skunk, CarePackage, Cash, BirthdayCake, Gem, Pickaxe, Mountain, DiamondRing, Rabbit, RedEnvelope,
}
// StockMarket, StockMarketDown, Key, AlarmClock, ATM
public class Card : Component, IEventHandler
{
public GameObject Model { get; set; }
public ModelRenderer ModelRenderer { get; set; }
public GameObject IconObj { get; set; }
public GameObject IconBackObj { get; set; }
public bool IsFlagShown { get; set; }
public float CardIconScale { get; set; } = 1f;
public bool IsHovered { get; set; }
public bool IsRevealed { get; set; }
public bool IsSpawning { get; set; }
public int NumTimesRevealed { get; set; }
public int NumTimesChosen { get; set; }
public CardType CardType { get; private set; }
public CardType OriginalCardType { get; private set; }
public virtual bool IsFoodOrBeverage => false;
public virtual bool IsAlive => false;
public virtual bool CantBeMoved => false;
public IntVector2 GridPos { get; set; }
public bool IsShaking { get; private set; }
private TimeSince _timeSinceShakeStart; // todo: replace with timer? this can become negative somehow
private float _shakeTime;
private EasingType _shakeEasingType;
public Vector2 ShakeOffset { get; set; }
private float _shakeStrength;
public bool IsMovementControlled { get; set; }
private Vector3 _moveStartPos;
private Vector3 _moveTargetPos;
private float _moveTime;
private TimeSince _timeSinceMoveStart;
private EasingType _moveEasingType;
private bool _moveRemoveControlAfter;
public int HP { get; set; }
public bool IsLocked { get; set; }
public int LockTurnsRemaining { get; set; }
public bool WasJustLocked { get; set; }
public int CardTypeID { get; set; }
public float IconOpacity { get; set; }
public bool IsExploding;
private TimeSince _timeSinceStartExploding;
private float _explodeTime;
private EasingType _explodeEasingType;
private Vector3 _explodeStartPos;
private EasingType _explodeShakeEasingType;
public bool IsScaling;
private TimeSince _timeSinceScale;
private float _scaleTime;
private float _scaleAmount;
private EasingType _scaleEasingType;
public bool AreRevealMarksShown { get; set; }
protected override void OnAwake()
{
base.OnAwake();
}
public virtual void Init(CardType cardType)
{
CardType = OriginalCardType = cardType;
Model = GameObject.Children.Find( x => x.Name == "model" );
ModelRenderer = Model.Components.Get<ModelRenderer>();
IconOpacity = 1f;
IconObj = GameObject.Children.Find( x => x.Name == "icon" );
var cardIcon = IconObj.Components.Get<CardIcon>( includeDisabled: true );
cardIcon.Card = this;
IconBackObj = GameObject.Children.Find( x => x.Name == "icon_back" );
var cardIconBack = IconBackObj.Components.Get<CardIconBack>( includeDisabled: true );
cardIconBack.Card = this;
SetMaterial( cardType );
}
public void SetMaterial(CardType cardType)
{
var matFilename = GetMaterialFilename( cardType );
if ( !string.IsNullOrEmpty( matFilename ) )
ModelRenderer.MaterialOverride = Material.Load( matFilename );
}
public void SetCardType(CardType newType)
{
CardType = newType;
SetMaterial( newType );
}
protected override void OnUpdate()
{
//if ( Input.Down( "Duck" ) )
//{
// var emoji = Card.GetIconEmoji( CardType );
// if ( string.IsNullOrEmpty( emoji ) )
// {
// Gizmo.Draw.Color = Color.Black.WithAlpha( 1f );
// Gizmo.Draw.Text( $"{CardType}", new global::Transform( WorldPosition + new Vector3( -0.5f, -0.5f, -1f ) ), size: 24f );
// Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
// Gizmo.Draw.Text( $"{CardType}", new global::Transform( WorldPosition ), size: 24f );
// }
// else
// {
// Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
// Gizmo.Draw.Text( emoji, new global::Transform( WorldPosition ), size: 60f );
// }
// if ( IsMovementControlled )
// {
// Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
// Gizmo.Draw.Text( "➡️", new global::Transform( WorldPosition + new Vector3( -15.5f, -19f, -1f ) ), size: 30f );
// }
// if ( IsLocked )
// {
// Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
// Gizmo.Draw.Text( "🔒", new global::Transform( WorldPosition + new Vector3( 15.5f, -19f, -1f ) ), size: 30f );
// Gizmo.Draw.Color = Color.Red.WithAlpha( 1f );
// Gizmo.Draw.Text( $"{LockTurnsRemaining}", new global::Transform( WorldPosition + new Vector3( 15.5f, -21f, 1f ) ), size: 20f );
// }
// Gizmo.Draw.Color = Color.Black.WithAlpha( 1f );
// Gizmo.Draw.Text( $"ID: {CardTypeID}", new global::Transform( WorldPosition + new Vector3( -13.5f, 19f, -1f ) + new Vector3( -0.5f, -0.5f, -1f ) ), size: 16 );
// Gizmo.Draw.Color = Color.White.WithAlpha( 1f );
// Gizmo.Draw.Text( $"ID: {CardTypeID}", new global::Transform( WorldPosition + new Vector3( -13.5f, 19f, -1f ) ), size: 16 );
//}
HandleMoving();
HandleShaking();
if(IsExploding)
{
HandleExploding();
return;
}
if ( IsScaling )
{
HandleScaling();
}
//if(!IsScaling && !IsExploding)
//{
// Transform.LocalScale = new Vector3( Utils.DynamicEaseTo( Transform.LocalScale.x, IsHovered ? 1.05f : 1f, 0.3f, Time.Delta ) );
//}
if( IsSpawning )
{
// do nothing
}
else if( IsMovementControlled )
{
LocalPosition += new Vector3( ShakeOffset.x, ShakeOffset.y, 0f ) * Time.Delta * 10f;
}
else
{
var targetPos = Manager.GetCardPos( GridPos );
if ( IsShaking )
targetPos += new Vector3( ShakeOffset.x, ShakeOffset.y, 0f );
var height = 10f + ((IsRevealed || IsHovered) ? Globals.CARD_ADD_HEIGHT_REVEALED_OR_HOVERED : 0f) + ((IsRevealed && IsHovered) ? Globals.CARD_ADD_HEIGHT_REVEALED_AND_HOVERED : 0f);
var zPos = MathX.Lerp( LocalPosition.z, height, Time.Delta * 20f );
LocalPosition = Vector3.Lerp( LocalPosition, targetPos, Time.Delta * 10f ).WithZ( zPos );
}
var targetRot = Rotation.FromPitch( IsRevealed ? 0f : 180f );
WorldRotation = Rotation.Lerp( WorldRotation, targetRot, Time.Delta * 15f );
}
public void MoveToPos( Vector3 pos, float time, EasingType easingType = EasingType.Linear, bool removeControlAfter = false)
{
//Log.Info( $"Card - MoveToPos: {pos}" );
IsMovementControlled = true;
_moveStartPos = LocalPosition;
_moveTargetPos = pos;
_moveTime = time;
_timeSinceMoveStart = 0f;
_moveEasingType = easingType;
_moveRemoveControlAfter = removeControlAfter;
}
void HandleMoving()
{
if ( !IsMovementControlled )
return;
if(_timeSinceMoveStart > _moveTime)
{
LocalPosition = _moveTargetPos;
if( _moveRemoveControlAfter )
IsMovementControlled = false;
}
else
{
LocalPosition = Vector3.Lerp( _moveStartPos, _moveTargetPos, Utils.Map( _timeSinceMoveStart, 0f, _moveTime, 0f, 1f, _moveEasingType ) );
}
}
protected void HandleShaking()
{
if ( IsShaking )
{
if ( _timeSinceShakeStart > _shakeTime )
IsShaking = false;
else
ShakeOffset = Utils.GetRandomVector() * _shakeStrength * Utils.Map( _timeSinceShakeStart, 0f, _shakeTime, 1f, 0f, _shakeEasingType );
}
}
void HandleExploding()
{
LocalScale = new Vector3( Utils.Map(_timeSinceStartExploding, 0f, _explodeTime, 1f, Globals.CARD_EXPLODE_SCALE, _explodeEasingType) );
IconOpacity = Utils.Map( _timeSinceStartExploding, 0f, _explodeTime, 1f, 0f, EasingType.SineIn );
LocalPosition = _explodeStartPos + Vector3.Random * Utils.Map( _timeSinceStartExploding, 0f, _explodeTime, 0f, 2f, _explodeShakeEasingType) * Utils.Map( _timeSinceStartExploding, _explodeTime * 0.9f, _explodeTime, 1f, 0f );
}
void HandleScaling()
{
if(_timeSinceScale > 0.25f)
{
IsScaling = false;
LocalScale = new Vector3( 1f );
}
else
{
LocalScale = Utils.MapReturn( _timeSinceScale, 0f, _scaleTime, 1f, _scaleAmount, _scaleEasingType);
}
}
public static string GetName( CardType cardType )
{
switch ( cardType )
{
case CardType.CrystalBall: return "Crystal Ball";
case CardType.AncientScroll: return "Ancient Scroll";
case CardType.JumpingSpider: return "Jumping Spider";
case CardType.Rock: return "Boulder";
case CardType.Syringe: return "Morphine";
case CardType.FlyingMoney: return "Flying Money";
case CardType.FortuneCookie: return "Fortune Cookie";
//case CardType.AlarmClock: return "Alarm Clock";
case CardType.FaxMachine: return "Fax Machine";
case CardType.MoneyBag: return "Money Bag";
case CardType.MagicWand: return "Magic Wand";
//case CardType.StockMarket: return "Stock Market";
case CardType.GuideDog: return "Guide Dog";
case CardType.CarePackage: return "Care Package";
case CardType.BirthdayCake: return "Birthday Cake";
case CardType.DiamondRing: return "Diamond Ring";
case CardType.RedEnvelope: return "Red Envelope";
}
return cardType.ToString();
}
public static string GetFilename( CardType cardType )
{
switch ( cardType )
{
case CardType.Apple: return "apple";
case CardType.Bomb: return "bomb";
case CardType.Rocket: return "rocket";
case CardType.Telephone: return "telephone";
case CardType.Trumpet: return "trumpet";
case CardType.Pawn: return "pawn";
case CardType.Balloon: return "balloon";
case CardType.Spider: return "spider";
case CardType.Umbrella: return "umbrella";
case CardType.UmbrellaClosed: return "umbrella_closed";
case CardType.Juggler: return "juggler";
case CardType.Coin: return "coin";
case CardType.Vampire: return "vampire";
case CardType.Maracas: return "maracas";
case CardType.Microwave: return "microwave";
case CardType.Raccoon: return "raccoon";
case CardType.Rock: return "rock";
case CardType.Bear: return "bear";
case CardType.Candle: return "candle";
case CardType.CrystalBall: return "crystal_ball";
case CardType.Snake: return "snake";
case CardType.Rose: return "rose";
case CardType.RoseWilted: return "rose_wilted";
case CardType.Bell: return "bell";
case CardType.Ant: return "ant";
case CardType.Twins: return "twins";
case CardType.Compass: return "compass";
case CardType.Antenna: return "antenna";
case CardType.Wizard: return "wizard";
case CardType.AncientScroll: return "scroll";
case CardType.JumpingSpider: return "spider_jumping";
case CardType.Bento: return "bento";
//case CardType.StockMarket: return "investment";
//case CardType.StockMarketDown: return "investment_down";
case CardType.Ninja: return "ninja";
case CardType.Flashlight: return "flashlight";
case CardType.Bodybuilder: return "bodybuilder";
case CardType.Helicopter: return "helicopter";
case CardType.FlyingMoney: return "flying_money";
case CardType.Syringe: return "syringe";
case CardType.Cellphone: return "cellphone";
case CardType.Crab: return "crab";
case CardType.Magnet: return "magnet";
case CardType.Stethoscope: return "stethoscope";
case CardType.UFO: return "ufo";
case CardType.Teacher: return "teacher";
case CardType.Tractor: return "tractor";
case CardType.FortuneCookie: return "fortune_cookie";
case CardType.Dolphin: return "dolphin";
case CardType.Detective: return "detective";
case CardType.Genie: return "genie";
case CardType.Potion: return "potion";
case CardType.Mirror: return "mirror";
//case CardType.AlarmClock: return "alarm_clock";
case CardType.FaxMachine: return "fax_machine";
case CardType.Dice: return "dice";
case CardType.Coffee: return "coffee";
case CardType.Tree: return "tree";
case CardType.Chipmunk: return "chipmunk";
case CardType.Dancer: return "dancer";
case CardType.Wine: return "wine";
case CardType.King: return "king";
case CardType.Police: return "police";
case CardType.Ogre: return "ogre";
case CardType.Clown: return "clown";
case CardType.MoneyBag: return "money_bag";
case CardType.Bank: return "bank";
case CardType.Steak: return "steak";
case CardType.MagicWand: return "magic_wand";
case CardType.Carrot: return "carrot";
case CardType.Caterpillar: return "caterpillar";
case CardType.Butterfly: return "butterfly";
case CardType.Tornado: return "tornado";
case CardType.Map: return "map";
case CardType.Cockroach: return "cockroach";
//case CardType.Key: return "key";
case CardType.Broccoli: return "broccoli";
//case CardType.ATM: return "atm";
case CardType.Donut: return "donut";
case CardType.Cheese: return "cheese";
case CardType.CheeseMoldy: return "cheese_moldy";
case CardType.GuideDog: return "guide_dog";
case CardType.Taco: return "taco";
case CardType.Burrito: return "burrito";
case CardType.Skunk: return "skunk";
case CardType.CarePackage: return "care_package";
case CardType.Cash: return "cash";
case CardType.BirthdayCake: return "birthday_cake";
case CardType.Gem: return "gem";
case CardType.Pickaxe: return "pickaxe";
case CardType.Mountain: return "mountain";
case CardType.DiamondRing: return "diamond_ring";
case CardType.Rabbit: return "rabbit";
case CardType.RedEnvelope: return "red_envelope";
}
return "";
}
public static string GetIconFilename( CardType cardType )
{
return $"textures/{GetFilename( cardType )}.png";
}
public static string GetMaterialFilename( CardType cardType )
{
return $"materials/cards/{GetFilename( cardType )}.vmat";
}
public static string GetIconEmoji( CardType cardType )
{
switch ( cardType )
{
case CardType.Apple: return "🍎";
case CardType.Bomb: return "💣";
case CardType.Rocket: return "🚀";
case CardType.Telephone: return "☎️";
case CardType.Trumpet: return "🎺";
case CardType.Pawn: return "♟️";
case CardType.Balloon: return "🎈";
case CardType.Spider: return "🕷️";
case CardType.Umbrella: return "☂️";
case CardType.UmbrellaClosed: return "🌂";
case CardType.Juggler: return "🤹";
case CardType.Coin: return "🟡";
case CardType.Vampire: return "🧛";
case CardType.Raccoon: return "🦝";
case CardType.Rock: return "🗿";
case CardType.Bear: return "🐻";
case CardType.Candle: return "🕯️";
case CardType.CrystalBall: return "🔮";
case CardType.Snake: return "🐍";
case CardType.Rose: return "🌹";
case CardType.RoseWilted: return "🥀";
case CardType.Bell: return "🔔";
case CardType.Ant: return "🐜";
case CardType.Twins: return "👯";
case CardType.Compass: return "🧭";
case CardType.Antenna: return "📡";
case CardType.Wizard: return "🧙";
case CardType.AncientScroll: return "📜";
case CardType.JumpingSpider: return "🕷️";
case CardType.Bento: return "🍱";
//case CardType.StockMarket: return "📈";
//case CardType.StockMarketDown: return "📉";
case CardType.Ninja: return "🐱👤";
case CardType.Flashlight: return "🔦";
case CardType.Bodybuilder: return "🏋️";
case CardType.Helicopter: return "🚁";
case CardType.FlyingMoney: return "💸";
case CardType.Syringe: return "💉";
case CardType.Cellphone: return "📱";
case CardType.Crab: return "🦀";
case CardType.Magnet: return "🧲";
case CardType.Stethoscope: return "🩺";
case CardType.UFO: return "🛸";
case CardType.Teacher: return "👩🏫";
case CardType.Tractor: return "🚜";
case CardType.FortuneCookie: return "🥠";
case CardType.Dolphin: return "🐬";
case CardType.Detective: return "🕵️";
case CardType.Genie: return "🧞";
case CardType.Potion: return "🧪";
case CardType.Mirror: return "🔘";
//case CardType.AlarmClock: return "⏰";
case CardType.FaxMachine: return "📠";
case CardType.Dice: return "🎲";
case CardType.Coffee: return "☕";
case CardType.Tree: return "🌳";
case CardType.Chipmunk: return "🐿️";
case CardType.Dancer: return "💃";
case CardType.Wine: return "🍷";
case CardType.King: return "🤴";
case CardType.Police: return "👮";
case CardType.Ogre: return "👹";
case CardType.Clown: return "🤡";
case CardType.MoneyBag: return "💰";
case CardType.Bank: return "🏦";
case CardType.Steak: return "🥩";
case CardType.MagicWand: return "✨";
case CardType.Carrot: return "🥕";
case CardType.Caterpillar: return "🐛";
case CardType.Butterfly: return "🦋";
case CardType.Tornado: return "🌪️";
case CardType.Map: return "🗺️";
case CardType.Cockroach: return "𓆣";
//case CardType.Key: return "🔑";
case CardType.Broccoli: return "🥦";
//case CardType.ATM: return "🏧";
case CardType.Donut: return "🍩";
case CardType.Cheese: return "🧀";
case CardType.CheeseMoldy: return "🤮";
case CardType.GuideDog: return "🦮";
case CardType.Taco: return "🌮";
case CardType.Burrito: return "🌯";
case CardType.Skunk: return "🦨";
case CardType.CarePackage: return "📦";
case CardType.Cash: return "💵";
case CardType.BirthdayCake: return "🎂";
case CardType.Gem: return "💎";
case CardType.Pickaxe: return "⛏️";
case CardType.Mountain: return "⛰️";
case CardType.DiamondRing: return "💍";
case CardType.Rabbit: return "🐇";
case CardType.RedEnvelope: return "🧧";
}
return "";
}
public static string GetCardDescription( CardType cardType )
{
switch ( cardType )
{
case CardType.Apple: return "✅Match: +1 HP";
case CardType.Bomb: return "✅Match: reveal nearby cards then shuffle them";
case CardType.Rocket: return "❌Mismatch: swap position";
case CardType.Telephone: return "✅Match: shake nearby pairs";
case CardType.Trumpet: return "Match other cards: 30% chance to reveal";
case CardType.Pawn: return "➡️Turn start: 50% to try to move upward";
case CardType.Balloon: return "Floats upward";
case CardType.Spider: return "Starts in a corner";
case CardType.Umbrella: return "👁️Revealed: toggle open/closed";
case CardType.Juggler: return "❌Mismatch: rearrange nearby cards";
case CardType.Coin: return "✅Match: +$2";
case CardType.Vampire: return "While hovered: shake if you lose HP";
case CardType.Maracas: return "Makes a distinctive sound when shook";
case CardType.Microwave: return "➡️Turn start: shake a nearby food/drink";
case CardType.Raccoon: return "➡️Turn start: shake an adjacent card";
case CardType.Rock: return "Falls downward, pushing other cards";
case CardType.Bear: return "Food/drink revealed: 20% chance to shake";
case CardType.Candle: return "✅Match: each reveal a nearby card";
case CardType.CrystalBall: return "❌Mismatch: shake cards, including other card's match";
case CardType.Snake: return "Shake when an adjacent card is matched";
case CardType.Rose: return "When moved: shake and wilt";
case CardType.Bell: return "Rings when moved or shook";
case CardType.Ant: return "➡️Turn start: move to nearby empty space";
case CardType.Twins: return "👁️Revealed: nudge toward match";
case CardType.Compass: return "❌Mismatch: nudge toward other card's match";
case CardType.Antenna: return "👁️Revealed: sound indicates distance to match";
case CardType.Wizard: return "⏩Every 3 turns: shuffle some cards\n💔Damage Wizard: respawn some cards and shuffle them";
case CardType.AncientScroll: return "✅Match: reveal all cards (after next turn, shuffle all cards)";
case CardType.JumpingSpider: return "Starts in a corner\n❌Mismatch: swaps with an unknown card";
case CardType.Bento: return "✅Match: +1 HP for each consecutive match";
//case CardType.StockMarket: return "✅Match: gain half your money if the market is up, lose half if the market is down";
case CardType.Ninja: return "➡️Turn start: 25% chance to swap position";
case CardType.Flashlight: return "👆Chosen: reveal a nearby card";
case CardType.Bodybuilder: return "➡️Turn start: move adjacent card upwards, pushing other cards";
case CardType.Helicopter: return "❌Mismatch: relocate along with an adjacent card";
case CardType.FlyingMoney: return "✅Match: +$3\n❌Mismatch: shuffle with some nearby cards";
case CardType.Syringe: return "✅Match: heal all HP, but time is halved until end of level";
case CardType.Cellphone: return "❌Mismatch: vibrate nearest cellphone";
case CardType.Crab: return "Moves sideways to empty spaces";
case CardType.Magnet: return "➡️Turn start: pull card toward it";
case CardType.Stethoscope: return "✅Match/❌Mismatch: shake nearby cards with a heartbeat";
case CardType.UFO: return "✅Match: abduct and shuffle nearby cards";
case CardType.Teacher: return "✅Match: arrange row alphabetically";
case CardType.Tractor: return "✅Match/❌Mismatch: shift row one space";
case CardType.FortuneCookie: return "✅Match: +1 HP, and reveal 5 cards that you haven't seen before";
case CardType.Dolphin: return "Revealed on 4 consecutive mismatches";
case CardType.Detective: return "➡️Turn start: 50% chance to reveal a nearby card";
case CardType.Genie: return "✅Match: next card chosen shakes its match";
case CardType.Potion: return "✅Match: your HP is set to half";
case CardType.Mirror: return "✅Match: flip the level horizontally";
//case CardType.AlarmClock: return "70% chance to shake when timer goes below 1s";
case CardType.FaxMachine: return "👆Chosen: shake 3 cards, including another fax machine";
case CardType.Dice: return "❌Mismatch: roll a 6 to get +$2 (rolling 1 loses $1)\n✅Match: Roll a 12 to get +$5 (rolling 2 loses $2)";
case CardType.Coffee: return "✅Match: +2 HP, and your next turn lasts 3 seconds";
case CardType.Tree: return "✅Match: reveal an apple";
case CardType.Chipmunk: return "➡️Turn start: move toward a tree";
case CardType.Dancer: return "❌Mismatch: dance with multiple partners";
case CardType.Wine: return "✅Match: +2 HP\nShake if an adjacent card shakes";
case CardType.King: return "💔Damage King: he decides your next choice";
case CardType.Police: return "❌Mismatch: Reveal and lock a nearby card for 2 turns\n💔Damage Police: Lock some nearby cards for 1 turn, then shuffle";
case CardType.Ogre: return "💔Damage Ogre: shuffle with nearby cards";
case CardType.Clown: return "💔Damage Clown: swap multiple times";
case CardType.MoneyBag: return "✅Match: +$5";
case CardType.Bank: return "✅Match: transform all coins into money bags";
case CardType.Steak: return "✅Match: -$1 and +4 HP";
case CardType.MagicWand: return "❌Mismatch: send other card adjacent to its match";
case CardType.Carrot: return "✅Match: +3 HP, and timer is doubled next turn";
case CardType.Caterpillar: return "➡️Turn start: 20% chance to shake and transform into a butterfly";
case CardType.Tornado: return "➡️Turn start: swap 2 adjacent cards";
case CardType.Map: return "✅Match: reveal all cards between";
case CardType.Cockroach: return "Can't be matched until no other cards remain";
//case CardType.Key: return "❌Mismatch: unlock other card\n✅Match: unlock all cards";
case CardType.Broccoli: return "✅Match: +3 HP";
//case CardType.ATM: return "✅Match: +$1 for every 2 matches you've made this level (max: $7)";
case CardType.Donut: return "✅Match: +1 HP for each adjacent empty space";
case CardType.Cheese: return "✅Match: +3 HP if not moldy\nGoes moldy after 7 turns";
case CardType.GuideDog: return "When you mismatch other cards, 50% chance to point toward one of their matches";
case CardType.Taco: return "✅Match: +1 Max HP";
case CardType.Burrito: return "✅Match: +2 Max HP";
case CardType.Skunk: return "Can't be matched while adjacent to any non-skunks";
case CardType.CarePackage: return "✅Match: +$1 for every 2 HP missing";
case CardType.Cash: return "✅Match: +$3 if both cash are nearby eachother, otherwise +$1";
case CardType.BirthdayCake: return "✅Match: +1 HP and +$2";
case CardType.Gem: return "✅Match: +$4";
case CardType.Pickaxe: return "✅Match: transform all boulders into gems";
case CardType.Mountain: return "Can't be moved";
case CardType.DiamondRing: return "✅Match: +$3 if no other cards remain, otherwise +$1";
case CardType.Rabbit: return "➡️Turn start: 50% chance to move towards its match";
case CardType.RedEnvelope: return "✅Match: if you have less than $5, set your money to $5";
}
return "";
}
public static string GetCardExplanation( CardType cardType )
{
switch ( cardType )
{
case CardType.Ant: return "\"nearby\" means 1 space away\n(including diagonal)";
case CardType.Snake: return "\"adjacent\" means 1 space away\n(NOT including diagonal)";
}
return "";
}
public static bool HasHP( CardType cardType )
{
switch ( cardType )
{
case CardType.Ogre: return true;
case CardType.Clown: return true;
case CardType.Police: return true;
case CardType.Wizard: return true;
case CardType.King: return true;
}
return false;
}
public static int GetMaxHP( CardType cardType )
{
switch ( cardType )
{
case CardType.Ogre: return 2;
case CardType.Clown: return 2;
case CardType.Police: return 2;
case CardType.Wizard: return 3;
case CardType.King: return 3;
}
return 0;
}
public void SetRevealedFromAsync(bool revealed)
{
IsRevealed = revealed;
if( Card.HasHP(CardType) || IsLocked )
IconObj.Enabled = revealed;
if ( revealed )
{
NumTimesRevealed++;
if(Manager.Instance.Stats[StatType.MaxCrayonMarks] > 0f && !AreRevealMarksShown)
ShowRevealMarks();
}
}
public void SetRevealedInstant()
{
IsRevealed = true;
if ( Card.HasHP( CardType ) || IsLocked )
IconObj.Enabled = true;
WorldRotation = Rotation.FromPitch( 0f );
NumTimesRevealed++;
if ( Manager.Instance.Stats[StatType.MaxCrayonMarks] > 0f && !AreRevealMarksShown )
ShowRevealMarks();
}
public void SetHiddenInstant()
{
IsRevealed = false;
if ( Card.HasHP( CardType ) )
IconObj.Enabled = false;
WorldRotation = Rotation.FromPitch( 180f );
}
public void Shake( float strength, float time = 0.6f, EasingType easingType = EasingType.QuadOut, bool playSfx = true )
{
IsShaking = true;
_timeSinceShakeStart = 0f;
_shakeTime = time;
_shakeEasingType = easingType;
_shakeStrength = strength;
if ( playSfx || ( OverrideShakeSfx && ( IsRevealed || !OnlyOverrideShakeSfxWhenRevealed ) ) )
PlayShakeSfx();
}
public virtual void PlayShakeSfx()
{
//Manager.Instance.PlayCardSfx( "shake_1", this, volume: 1.1f, pitch: Game.Random.Float( 1.4f, 1.6f ) );
Manager.Instance.PlayCardSfx( "shake_3", this, volume: 1.35f, pitch: Game.Random.Float( 0.94f, 1.06f ) );
}
public virtual bool OverrideShakeSfx => false;
public virtual bool OnlyOverrideShakeSfxWhenRevealed => false;
public virtual void PlayHurtSfx(Card card0, Card card1)
{
Manager.Instance.PlayCardSfxBetween( "shake_1", card0, card1, volume: 1.3f, pitch: Game.Random.Float( 1.4f, 1.6f ) );
}
// return false if needed to change start pos
public virtual bool ValidateStartingGridPos()
{
return true;
}
public virtual bool ShouldHandleEvent( EventType eventType )
{
return false;
}
public virtual async Task HandleEventAsync(EventType eventType)
{
await Task.Frame();
}
public virtual string GetEventText( EventType eventType )
{
return GetCardDescription( CardType );
}
public async Task Hurt()
{
if(!Card.HasHP(CardType))
{
Log.Error( $"Trying to hurt card {CardType} with no HP!" );
return;
}
HP--;
await Manager.Instance.ShakeCard(this, playSfx: false);
}
public void Lock(int numTurns)
{
IsLocked = true;
LockTurnsRemaining = numTurns;
WasJustLocked = true;
}
public void Unlock()
{
IsLocked = false;
}
public virtual bool CanBeMatched()
{
return true;
}
public void StartExploding(float explodeTime)
{
IsExploding = true;
_timeSinceStartExploding = 0f;
_explodeTime = explodeTime;
_explodeStartPos = LocalPosition;
int rand = Game.Random.Int( 0, 5 );
switch(rand)
{
case 0: _explodeEasingType = EasingType.SineIn; break;
case 1: _explodeEasingType = EasingType.SineIn; break;
case 2: _explodeEasingType = EasingType.QuadIn; break;
case 3: _explodeEasingType = EasingType.QuadIn; break;
case 4: _explodeEasingType = EasingType.QuartIn; break;
case 5: _explodeEasingType = EasingType.Linear; break;
}
rand = Game.Random.Int( 0, 7 );
switch ( rand )
{
case 0: _explodeShakeEasingType = EasingType.SineIn; break;
case 1: _explodeShakeEasingType = EasingType.QuadIn; break;
case 2: _explodeShakeEasingType = EasingType.QuartIn; break;
case 3: _explodeShakeEasingType = EasingType.ExpoIn; break;
case 4: _explodeShakeEasingType = EasingType.ExpoIn; break;
case 5: _explodeShakeEasingType = EasingType.ExpoIn; break;
case 6: _explodeShakeEasingType = EasingType.ExpoIn; break;
case 7: _explodeShakeEasingType = EasingType.Linear; break;
}
}
public void Explode( int breakNum )
{
var scale = LocalScale.x * Model.LocalScale.x;
Manager.Instance.SpawnCardBreak( WorldPosition, LocalRotation, scale, ModelRenderer.MaterialOverride, breakNum );
Manager.Instance.PlayCardSfx( "explosion", this, volume: Game.Random.Float( 0.4f, 0.55f ), pitch: Game.Random.Float( 0.4f, 2f ) );
Manager.Instance.CardBreakParticlesPrefab.Clone( WorldPosition.WithZ( 25f ) );
Manager.Instance.CardBreakShockwavePrefab.Clone( WorldPosition.WithZ( 55f ) );
GameObject.Destroy();
}
public void StartScaling(float time, float amount, EasingType easingType)
{
IsScaling = true;
_timeSinceScale = 0f;
_scaleTime = time;
_scaleAmount = amount;
_scaleEasingType = easingType;
}
public void ShowFlag()
{
IsFlagShown = true;
IconBackObj.Enabled = true;
}
public void HideFlag()
{
IsFlagShown = false;
if ( !AreRevealMarksShown )
IconBackObj.Enabled = false;
}
public void ShowRevealMarks()
{
AreRevealMarksShown = true;
IconBackObj.Enabled = true;
}
}
using Sandbox;
using System.Net;
using System.Threading.Tasks;
public class CardBank : Card
{
public override bool ShouldHandleEvent( EventType eventType )
{
switch ( eventType )
{
case EventType.Match:
return Manager.Instance.ChosenCards[1] == this;
}
return false;
}
public override async Task HandleEventAsync( EventType eventType )
{
Manager.Instance.PushEventMessage( this, eventType );
await Task.DelayRealtime( 150 );
var coins = Manager.Instance.Cards.Where( x => x.CardType == CardType.Coin ).ToList();
if(coins.Count > 0)
{
Manager.Instance.PlayCardSfxBetween( "bank", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 1f, pitch: Game.Random.Float( 0.9f, 1.1f ) );
await Task.DelayRealtime( 50 );
foreach ( var coin in coins )
coin.SetCardType( CardType.MoneyBag );
await Task.DelayRealtime( 300 );
}
await Task.DelayRealtime( 800 );
Manager.Instance.PopEventMessage();
}
}
using Sandbox;
using System.Threading.Tasks;
public class CardCockroach : Card
{
public override bool IsAlive => true;
public override bool ShouldHandleEvent( EventType eventType )
{
return eventType == EventType.Mismatch && Manager.Instance.ChosenCards[0].CardType == CardType.Cockroach && Manager.Instance.ChosenCards[1].CardType == CardType.Cockroach && !IsLocked;
}
public override async Task HandleEventAsync( EventType eventType )
{
Manager.Instance.PushEventMessage( this, eventType );
await Task.DelayRealtime( 850 );
Manager.Instance.PopEventMessage();
}
public override bool CanBeMatched()
{
foreach(var card in Manager.Instance.Cards)
{
if ( card.CardType != CardType.Cockroach )
return false;
}
return true;
}
}
using Sandbox;
using System.Threading.Tasks;
public class CardDancer : Card
{
public override bool IsAlive => true;
public override bool ShouldHandleEvent( EventType eventType )
{
return eventType == EventType.Mismatch && Manager.Instance.ChosenCards.Contains( this ) && !Manager.Instance.IsMismatchALockedMatch;
}
public override async Task HandleEventAsync( EventType eventType )
{
Manager.Instance.PushEventMessage( this, eventType );
List<Card> neighbourCards = new();
List<IntVector2> neighboursEmpty = new();
Card lastPartnerCard = null;
await Task.DelayRealtime( 300 );
Manager.Instance.PlayCardSfx( "dancer", this, volume: 1.1f, pitch: Game.Random.Float( 0.98f, 1.02f ) );
for (int i = 0; i < 4; i++)
{
neighbourCards.Clear();
neighboursEmpty.Clear();
EvaluateNeighbour( GridPos + new IntVector2( -1, 0 ), neighbourCards, neighboursEmpty );
EvaluateNeighbour( GridPos + new IntVector2( 1, 0 ), neighbourCards, neighboursEmpty );
EvaluateNeighbour( GridPos + new IntVector2( 0, -1 ), neighbourCards, neighboursEmpty );
EvaluateNeighbour( GridPos + new IntVector2( 0, 1 ), neighbourCards, neighboursEmpty );
IntVector2 targetGridPos;
Card targetCard = null;
var validNeighbourCards = neighbourCards.Where( x => x != lastPartnerCard ).ToList();
if ( validNeighbourCards.Count > 0 )
{
validNeighbourCards.Shuffle();
targetCard = validNeighbourCards.First();
targetGridPos = targetCard.GridPos;
}
else
{
neighboursEmpty.Shuffle();
targetGridPos = neighboursEmpty.First();
}
if ( targetCard != null )
{
await Manager.Instance.ShakeCard( this );
await Manager.Instance.ShakeCard( targetCard );
lastPartnerCard = targetCard;
await Task.DelayRealtime( 650 );
if(i < 3)
{
await Manager.Instance.SwapCardPositions( this, targetCard );
await Task.DelayRealtime( 250 );
}
}
else
{
lastPartnerCard = null;
if ( i < 3 )
{
Manager.Instance.RemoveCardGridPos( this );
await Manager.Instance.SetCardGridPos( this, targetGridPos );
await Task.DelayRealtime( 400 );
}
}
}
await Task.DelayRealtime( 100 );
Manager.Instance.PopEventMessage();
await Manager.Instance.EventHappened( EventType.AfterCardsMoved );
}
void EvaluateNeighbour(IntVector2 gridPos, List<Card> neighbourCards, List<IntVector2> empty)
{
if ( !Manager.Instance.IsGridPosInBounds( gridPos ) )
return;
var card = Manager.Instance.GetCardAtGridPos( gridPos );
if ( card != null )
neighbourCards.Add( card );
else
empty.Add( gridPos );
}
}
using Sandbox;
using System.Threading.Tasks;
using System.Xml.Linq;
public class CardDolphin : Card
{
public override bool IsAlive => true;
public override bool ShouldHandleEvent( EventType eventType )
{
return eventType == EventType.Mismatch && !Manager.Instance.ChosenCards.Contains(this) && Manager.Instance.Stats[StatType.ConsecutiveMismatches] == 3;
}
public override async Task HandleEventAsync( EventType eventType )
{
Manager.Instance.PushEventMessage( this, eventType );
await Task.DelayRealtime( 250 );
Manager.Instance.PlayCardSfx( "card_flip", this, volume: 0.8f, pitch: Game.Random.Float( 1.15f, 1.2f ) );
await Task.DelayRealtime( 100 );
Manager.Instance.PlayCardSfx( "dolphin", this, volume: 0.8f, pitch: Game.Random.Float( 1f, 1.15f ) );
await Manager.Instance.RevealCard( this );
await Task.DelayRealtime( Game.Random.Int(400, 700) );
Manager.Instance.HideCard( this );
Manager.Instance.PlayCardSfx( "card_flip", this, volume: 0.7f, pitch: Game.Random.Float( 0.65f, 0.75f ) );
Manager.Instance.PopEventMessage();
}
}
using Sandbox;
using System.Threading.Tasks;
public class CardGenie : Card
{
public override bool IsAlive => true;
public override bool ShouldHandleEvent( EventType eventType )
{
return eventType == EventType.Match && Manager.Instance.ChosenCards[0] == this;
}
public override async Task HandleEventAsync( EventType eventType )
{
Manager.Instance.PushEventMessage( this, eventType );
await Task.DelayRealtime( 250 );
Manager.Instance.PlayCardSfxBetween( "genie", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 0.6f, pitch: Game.Random.Float( 1.25f, 1.26f ) );
Manager.Instance.AddStatus( "StatusGenie" );
await Task.DelayRealtime( 1750 );
Manager.Instance.PopEventMessage();
}
}
public class StatusGenie : StatusEffect
{
public float TimerReductionAmount { get; set; }
public override bool ShouldHandleEvent( EventType eventType )
{
return eventType == EventType.Choose;
}
public override async Task HandleEventAsync( EventType eventType )
{
var validCards = Manager.Instance.Cards.Where(x => x.CardType == Manager.Instance.ChosenCard.CardType && x != Manager.Instance.ChosenCard).ToList();
if ( validCards.Count == 0 )
return;
Manager.Instance.PushEventMessage( this, eventType );
await Task.DelayRealtime( 250 );
Manager.Instance.PlayCardSfx( "genie_choose", Manager.Instance.ChosenCard, volume: 0.9f, pitch: Game.Random.Float( 0.95f, 1.05f ) );
await Task.DelayRealtime( 750 );
validCards.Shuffle();
await Manager.Instance.ShakeCard( validCards.FirstOrDefault() );
await Task.DelayRealtime( 750 );
Manager.Instance.PopEventMessage();
Manager.Instance.RemoveStatus( this );
}
public override string GetEventIcon( EventType eventType )
{
return "textures/genie.png";
}
public override string GetEventText( EventType eventType )
{
return "Shake matching card";
}
}
using Sandbox;
using System.Threading.Tasks;
public class CardKey : Card
{
//public override bool ShouldHandleEvent( EventType eventType )
//{
// return eventType == EventType.Mismatch && Manager.Instance.ChosenCards.Contains(this) || eventType == EventType.Match && Manager.Instance.ChosenCards[1] == this;
//}
//public override async Task HandleEventAsync( EventType eventType )
//{
// if ( eventType == EventType.Mismatch )
// {
// var key = Manager.Instance.ChosenCards[0].CardType == CardType.Key ? Manager.Instance.ChosenCards[0] : Manager.Instance.ChosenCards[1];
// var otherCard = Manager.Instance.ChosenCards[0].CardType == CardType.Key ? Manager.Instance.ChosenCards[1] : Manager.Instance.ChosenCards[0];
// if ( key.IsLocked || !otherCard.IsLocked )
// return;
// Manager.Instance.PushEventMessage( this, eventType );
// await Task.DelayRealtime( 50 );
// Manager.Instance.PlayCardSfxBetween( "key", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 1.5f, pitch: Game.Random.Float( 0.9f, 1.1f ) );
// await Task.DelayRealtime( 50 );
// await Manager.Instance.UnlockCard( otherCard );
// await Task.DelayRealtime( 900 );
// Manager.Instance.PopEventMessage();
// }
// else if (eventType == EventType.Match)
// {
// Manager.Instance.PushEventMessage( this, eventType );
// await Task.DelayRealtime( 50 );
// var lockedCards = Manager.Instance.Cards.Where( x => x.IsLocked ).ToList();
// if ( lockedCards.Count == 0 )
// {
// await Task.DelayRealtime( 800 );
// Manager.Instance.PopEventMessage();
// return;
// }
// Manager.Instance.PlayCardSfxBetween( "key", Manager.Instance.ChosenCards[0], Manager.Instance.ChosenCards[1], volume: 1.5f, pitch: Game.Random.Float( 0.9f, 1.1f ) );
// await Task.DelayRealtime( 50 );
// foreach ( var card in lockedCards )
// {
// await Manager.Instance.UnlockCard( card );
// }
// await Task.DelayRealtime( 1000 );
// Manager.Instance.PopEventMessage();
// }
//}
//public override string GetEventText( EventType eventType )
//{
// if ( eventType == EventType.Mismatch )
// {
// return "❌Mismatch: unlock other card";
// }
// else
// {
// return "✅Match: unlock all cards";
// }
//}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;
public class RelicGuideDog : Relic
{
public override void Init()
{
base.Init();
MaxLevel = 1;
}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;
public class RelicMammothMeat : Relic
{
public override void LevelUp()
{
base.LevelUp();
var mouse = Mouse.Position;
var camera = Scene.Camera;
var ray = camera.ScreenPixelToRay( mouse );
var tr = Scene.Trace.Ray( ray, 10000f ).Run();
Manager.Instance.PlaySfxCenter( "mammoth_meat", volume: 0.75f, pitch: Game.Random.Float( 0.85f, 0.9f ) );
int maxHpAmount = 8;
Manager.Instance.SpawnMaxHPFloater( maxHpAmount, tr.EndPosition );// + new Vector3(0f, -30f, 0f) );
Manager.Instance.MaxHP += maxHpAmount;
//int hpAmount = 5;
//Manager.Instance.HP = Math.Min( Manager.Instance.HP + hpAmount, Manager.Instance.MaxHP );
Manager.Instance.TimeSinceHPChanged = 0f;
//Manager.Instance.SpawnHealHPFloater( hpAmount, tr.EndPosition );
}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;
public class RelicMantlepieceClock : Relic
{
public override void LevelUp()
{
base.LevelUp();
Manager.Instance.Stats[StatType.NumExtraBountyTurns] += (Level == 1 ? 2f : 1f);
}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;
public class RelicMedicalLicense : Relic
{
public override void Init()
{
base.Init();
MaxLevel = 1;
}
public override bool ShouldHandleEvent( EventType eventType )
{
return eventType == EventType.OverhealHP;
}
public override async Task HandleEventAsync( EventType eventType )
{
await Task.DelayRealtime( 1000 );
Manager.Instance.PushEventMessage( this, eventType );
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();
Manager.Instance.PlaySfx( "medical_license", tr.EndPosition.WithZ( Scene.Camera.WorldPosition.z - Globals.CARD_SFX_DEPTH_DIFF ), volume: 1.2f, pitch: Game.Random.Float( 0.95f, 1.05f ) );
await Task.DelayRealtime( 350 );
int moneyAmount = (int)Manager.Instance.Stats[StatType.LatestOverhealAmount] + (int)Manager.Instance.Stats[StatType.EarnExtraMoney];
Manager.Instance.SpawnGainMoneyFloater( moneyAmount, tr.EndPosition );
await Manager.Instance.GainMoney( moneyAmount );
await Task.DelayRealtime( 650 );
Manager.Instance.PopEventMessage();
}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;
public class RelicMouseTrap : Relic
{
public override bool ShouldHandleEvent( EventType eventType )
{
return eventType == EventType.ClaimBounty;
}
public override async Task HandleEventAsync(EventType eventType)
{
Manager.Instance.PushEventMessage( this, eventType );
await Task.DelayRealtime( 100 );
Manager.Instance.PlaySfxCenter( "mouse_trap", volume: 0.8f, pitch: Game.Random.Float( 0.95f, 1.05f ) );
await Task.DelayRealtime( 400 );
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();
int maxHP = Level;
Manager.Instance.SpawnMaxHPFloater( maxHP, tr.EndPosition );
Manager.Instance.MaxHP += maxHP;
Manager.Instance.TimeSinceHPChanged = 0f;
await Task.DelayRealtime( 500 );
Manager.Instance.PopEventMessage();
}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Threading.Tasks;
public class RelicSacrificialBlade : Relic
{
public override bool ShouldHandleEvent( EventType eventType )
{
return eventType == EventType.HurtCards;
}
public override async Task HandleEventAsync(EventType eventType)
{
Manager.Instance.PushEventMessage( this, eventType );
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();
Manager.Instance.PlaySfx( "sacrificial_blade", tr.EndPosition.WithZ( Scene.Camera.WorldPosition.z - Globals.CARD_SFX_DEPTH_DIFF ), volume: 0.75f, pitch: Game.Random.Float( 1.6f, 1.65f ) );
await Task.DelayRealtime( 450 );
int healAmount = Level;
Manager.Instance.SpawnHealHPFloater( healAmount, tr.EndPosition );
await Task.DelayRealtime( 350 );
await Manager.Instance.GainHP( healAmount );
await Task.DelayRealtime( 250 );
Manager.Instance.PopEventMessage();
}
}
using Sandbox;
using System.Reflection.PortableExecutable;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
public class RelicSign : Relic
{
public override bool ShouldHandleEvent( EventType eventType )
{
return eventType == EventType.Mismatch && Game.Random.Float(0f, 1f) < Level * 0.25f;
}
public override async Task HandleEventAsync( EventType eventType )
{
List<Card> validCards = new();
foreach ( var card in Manager.Instance.Cards )
{
if ( card.NumTimesRevealed > 0 && !Manager.Instance.ChosenCards.Contains(card) && !card.IsRevealed )
validCards.Add( card );
}
if ( validCards.Count == 0 )
return;
Manager.Instance.PushEventMessage( this, eventType );
// todo: sfx
await Task.DelayRealtime( 250 );
validCards.Shuffle();
var cardToReveal = validCards.First();
Manager.Instance.PlayCardSfx( "card_flip", cardToReveal, volume: 0.8f, pitch: Game.Random.Float( 1.15f, 1.2f ) );
await Manager.Instance.RevealCard( cardToReveal );
await Task.DelayRealtime( 750 );
Manager.Instance.HideCard( cardToReveal );
Manager.Instance.PlayCardSfx( "card_flip", cardToReveal, volume: 0.7f, pitch: Game.Random.Float( 0.65f, 0.75f ) );
await Task.DelayRealtime( 300 );
Manager.Instance.PopEventMessage();
}
}
using Sandbox.Diagnostics;
public sealed class PlayerInventory : Component, IPlayerEvent, ILocalPlayerEvent
{
[RequireComponent] public Player Player { get; set; }
public List<BaseWeapon> Weapons => Scene.Components.GetAll<BaseWeapon>( FindMode.EverythingInSelfAndDescendants ).Where( x => x.Network.OwnerId == Network.OwnerId ).OrderBy( x => x.InventorySlot ).ThenBy( x => x.InventoryOrder ).ToList();
public BaseWeapon ActiveWeapon { get; private set; }
public void GiveDefaultWeapons()
{
Pickup( "weapons/hands.prefab" );
Pickup( "weapons/camera.prefab" );
}
void Pickup( string prefabName )
{
var prefab = GameObject.Clone( prefabName, new CloneConfig { Parent = GameObject, StartEnabled = false } );
prefab.NetworkSpawn( false, Network.Owner );
var weapon = prefab.Components.Get<BaseWeapon>( true );
Assert.NotNull( weapon );
IPlayerEvent.PostToGameObject( Player.GameObject, e => e.OnWeaponAdded( weapon ) );
ILocalPlayerEvent.Post( e => e.OnWeaponAdded( weapon ) );
}
protected override void OnUpdate()
{
if ( ActiveWeapon.IsValid() )
{
ActiveWeapon.OnPlayerUpdate( Player );
}
}
public void SwitchWeapon( BaseWeapon weapon )
{
if ( ActiveWeapon.IsValid() )
{
ActiveWeapon.GameObject.Enabled = false;
}
ActiveWeapon = weapon;
if ( ActiveWeapon.IsValid() )
{
ActiveWeapon.GameObject.Enabled = true;
}
}
void IPlayerEvent.OnSpawned()
{
GiveDefaultWeapons();
}
void ILocalPlayerEvent.OnCameraMove( ref Angles angles )
{
if ( ActiveWeapon.IsValid() )
{
ActiveWeapon.OnCameraMove( Player, ref angles );
}
}
void ILocalPlayerEvent.OnCameraPostSetup( Sandbox.CameraComponent camera )
{
if ( ActiveWeapon.IsValid() )
{
ActiveWeapon.OnCameraSetup( Player, camera );
}
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
<root>
<div class="table">
<div class="header row">
<div class="name">Name</div>
<div class="stat">⏲️</div>
</div>
@foreach ( var entry in Connection.All )
{
string specialClass = "";
if (entry == Connection.Local) specialClass = "me";
<div class="row @specialClass">
<div class="name">
@if ( entry.IsHost )
{
<div>👑</div>
}
@entry.DisplayName
</div>
<div class="stat">@GetTime( entry )</div>
</div>
}
</div>
</root>
@code
{
string GetTime( Connection c )
{
TimeSpan time = DateTime.UtcNow - c.ConnectionTime;
if (time.TotalMinutes < 60)
return time.ToString("mm\\m\\ s\\s");
return time.ToString("hh\\h\\ \\m\\m");
}
protected override void OnUpdate()
{
SetClass( "hidden", !Input.Down( "score" ) );
}
/// <summary>
/// update every second
/// </summary>
protected override int BuildHash() => System.HashCode.Combine( RealTime.Now.CeilToInt() );
}
using Sandbox;
public sealed class SoundEmitter : Component
{
[Property] public string SoundString { get; set; }
protected override void OnStart()
{
base.OnStart();
Sound.Play( SoundString, Transform.Position );
}
protected override void OnUpdate()
{
}
}
using Sandbox;
using Sandbox.Citizen;
public sealed class MapOverrider : Component
{
[Property] GameObject FrogPrefab { get; set; }
protected override void OnEnabled()
{
base.OnEnabled();
/*
{
var obj = Scene.GetAllComponents<SkinnedModelRenderer>();
foreach ( var item in obj )
{
if ( item.Model.ResourceName == "frog_test_subject_01a" )
item.Set( "sit", 1 );
item.Set( "sit_pose", Random.Shared.Float( 3 ) );
item.Set( "scale_height", Random.Shared.Float( 0.75f, 1.5f ) );
var ran = Random.Shared.Int( 0, 1 );
item.MaterialGroup = ran == 1 ? "default" : "Orange";
}
}
*/
{
var npc = Scene.GetAllObjects( true ).Where( x => x.Name == "js_npc_text" ).ToList();
foreach ( var item in npc )
{
item.DestroyImmediate();
Log.Info( "Destroyed" );
}
}
}
protected override void OnUpdate()
{
}
}
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@implements Component.INetworkListener
<root>
<div class="output">
@foreach (var entry in Entries)
{
<div class="chat_entry">
<div class="author">@entry.author</div>
<div class="message">@entry.message</div>
</div>
}
</div>
<div class="input">
<TextEntry @ref="InputBox" onsubmit="@ChatFinished"></TextEntry>
</div>
</root>
@code
{
TextEntry InputBox;
public record Entry( ulong steamid, string author, string message, RealTimeSince timeSinceAdded );
List<Entry> Entries = new();
protected override void OnUpdate()
{
if (InputBox is null)
return;
Panel.AcceptsFocus = false;
if ( Input.Pressed( "chat" ) )
{
InputBox.Focus();
}
if ( Entries.RemoveAll( x => x.timeSinceAdded > 20.0f ) > 0 )
{
StateHasChanged();
}
SetClass( "open", InputBox.HasFocus );
}
void ChatFinished()
{
var text = InputBox.Text;
InputBox.Text = "";
if (string.IsNullOrWhiteSpace(text))
return;
AddText( text );
}
[Broadcast]
public void AddText( string message )
{
message = message.Truncate( 300 );
if (string.IsNullOrWhiteSpace(message))
return;
var author = Rpc.Caller.DisplayName;
var steamid = Rpc.Caller.SteamId;
Log.Info($"{author}: {message}");
Entries.Add(new Entry(steamid, author, message, 0.0f));
StateHasChanged();
}
[Broadcast] // todo: only from host/owner
public void AddSystemText(string message)
{
message = message.Truncate(300);
if (string.IsNullOrWhiteSpace(message))
return;
Entries.Add(new Entry(0, "ℹ️", message, 0.0f));
StateHasChanged();
}
void Component.INetworkListener.OnConnected( Connection channel )
{
if ( IsProxy ) return;
AddSystemText( $"{channel.DisplayName} has joined the game" );
}
void Component.INetworkListener.OnDisconnected( Connection channel )
{
if ( IsProxy ) return;
AddSystemText( $"{channel.DisplayName} has left the game" );
}
}@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@if (Network.OwnerConnection is Connection owner && owner == Connection.Local)
{
<root class=@(IsOpen ? "open" : "closed")>
<div class="bar">
<label class="heightbar"></label>
<label class="flag"></label>
<div class="heightbarpos" style="height:@(CalculateProgress(Height) * 100)%;">
<label class="heighttext" text="< @(Height)" />
</div>
<div class="maxheightbarpos" style="height:@(CalculateProgress(MaxHeight) * 100)%;">
<label class="maxheighttext" text="@(MaxHeight)>" />
</div>
@foreach (var player in Scene.GetAllComponents<JumperPlayerStuff>())
{
if (owner == player.Network.OwnerConnection) continue;
<div class="maxheightotherbarpos" style="border-top: 4px solid @player.rndColor; height:@(CalculateProgress(player.Height) * 100)%;">
<img class="maxheightothertext" style=" box-shadow: 3px 3px @player.rndColor; background-image: url( avatar:@player.Network.OwnerConnection.SteamId )">
</div>
}
</div>
</root>
}
@code
{
float TotalHeight { get; set; }
float Height { get; set; }
float MaxHeight { get; set; }
bool IsOpen { get; set; }
// Find first player stats class that isn't owned by a proxy (is ours)
JumperPlayerStuff PlayerStats => GameObject.Components.Get<JumperPlayerStuff>(FindMode.InParent);
JumperDistanceRuler Ruler { get; set; }
public Color32 rndColor = Color.Random;
public string hexColor;
protected override void OnUpdate()
{
base.OnUpdate();
if (IsProxy) return;
Height = PlayerStats.Height;
MaxHeight = PlayerStats.MaxHeight;
TotalHeight = Scene.GetAllComponents<JumperDistanceRuler>().FirstOrDefault().Distance;
}
private float CalculateProgress(float height)
{
return height.LerpInverse(0, Scene.GetAllComponents<JumperDistanceRuler>().FirstOrDefault().Distance);
}
protected override int BuildHash()
{
IsOpen ^= Input.Pressed("slot1");
return HashCode.Combine(Time.Delta);
}
}@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
<root class=@(Visible ? "visible" : "")>
<label class="message">@OutputText</label>
<label class="name">@NPCName</label>
<label class="background"></label>
</root>
@code {
public string Message { get; set; } = "";
private bool Visible => TimeSinceDisplayed < 4;
private RealTimeSince TimeSinceDisplayed = 999;
public string OutputText { get; set; }
public float Delay { get; set; } = .1f;
public string NPCName { get; set; } = "Ben";
public string Voice { get; set; } = "beep1";
protected override void OnEnabled()
{
base.OnEnabled();
TimeSinceDisplayed = 999;
}
public static bool IsTalking()
{
return true;
}
private async Task RevealTextAsync(string message)
{
Random rand = new Random();
foreach (char c in message)
{
IsTalking();
OutputText += c;
TimeSinceDisplayed = 0f;
await Task.DelaySeconds((float)GetRandomNumber(0.05f, 0.2f));
var snd = Sound.Play(Voice);
snd.Pitch = (float)GetRandomNumber(0.9f, 1.1f);
snd.Volume = 0.25f;
}
}
static Random random = new Random();
public double GetRandomNumber(double minimum, double maximum)
{
return random.NextDouble() * (maximum - minimum) + minimum;
}
public void DisplayMessage(string message)
{
Message = message;
OutputText = null;
}
protected override void OnUpdate()
{
base.OnUpdate();
if (OutputText == Message)
{
return;
}
if (OutputText == null)
{
RevealTextAsync(Message);
}
Message = OutputText;
}
protected override int BuildHash()
{
return HashCode.Combine(OutputText, Visible ? 1 : 0);
}
}using Sandbox;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace SpriteTools;
[Category( "2D" )]
[Title( "2D Tileset" )]
[Icon( "calendar_view_month" )]
[Tint( EditorTint.Yellow )]
public partial class TilesetComponent : Component, Component.ExecuteInEditor
{
/// <summary>
/// The Layers within the TilesetComponent
/// </summary>
[Property, Group( "Layers" )]
public List<Layer> Layers
{
get => _layers;
set
{
_layers = value;
foreach ( var layer in _layers )
{
layer.TilesetComponent = this;
}
}
}
List<Layer> _layers;
[Property, WideMode( HasLabel = false )]
ComponentControls InternalControls { get; set; }
/// <summary>
/// Whether or not the component should generate a collider based on the specified Collision Layer
/// </summary>
[Property, FeatureEnabled( "Collision" )]
public bool HasCollider
{
get => _hasCollider;
set
{
if ( value == _hasCollider ) return;
_hasCollider = value;
if ( value ) CreateCollider();
else DestroyCollider();
}
}
bool _hasCollider;
/// <inheritdoc cref="Collider.Static" />
[Property, Feature( "Collision" )]
public bool Static
{
get => _static;
set
{
if ( value == _static ) return;
_static = value;
if ( Collider.IsValid() ) Collider.Static = value;
}
}
private bool _static = true;
/// <inheritdoc cref="Collider.IsTrigger" />
[Property, Feature( "Collision" )]
public bool IsTrigger
{
get => _isTrigger;
set
{
if ( value == _isTrigger ) return;
_isTrigger = value;
if ( Collider.IsValid() ) Collider.IsTrigger = value;
}
}
private bool _isTrigger = false;
/// <summary>
/// The width of the generated collider
/// </summary>
[Property, Feature( "Collision" )]
public float ColliderWidth
{
get => _colliderWidth;
set
{
if ( value < 0f ) _colliderWidth = 0f;
else if ( value == _colliderWidth ) return;
_colliderWidth = value;
Collider?.RebuildMesh();
}
}
float _colliderWidth;
/// <inheritdoc cref="Collider.Friction" />
[Property, Feature( "Collision" ), Group( "Surface Properties" )]
[Range( 0f, 1f, true, true ), Step( 0.01f )]
public float? Friction
{
get => _friction;
set
{
if ( value == _friction ) return;
_friction = value;
if ( Collider.IsValid() ) Collider.Friction = value;
}
}
private float? _friction;
/// <inheritdoc cref="Collider.Surface" />
[Property, Feature( "Collision" ), Group( "Surface Properties" )]
public Surface Surface
{
get => _surface;
set
{
if ( value == _surface ) return;
_surface = value;
if ( Collider.IsValid() ) Collider.Surface = value;
}
}
private Surface _surface;
/// <inheritdoc cref="Collider.SurfaceVelocity" />
[Property, Feature( "Collision" ), Group( "Surface Properties" )]
public Vector3 SurfaceVelocity
{
get => _surfaceVelocity;
set
{
if ( value == _surfaceVelocity ) return;
_surfaceVelocity = value;
if ( Collider.IsValid() ) Collider.SurfaceVelocity = value;
}
}
private Vector3 _surfaceVelocity;
[Property, Feature( "Collision" ), Group( "Trigger Actions" ), ShowIf( nameof( IsTrigger ), true )]
public Action<Collider> OnTriggerEnter { get; set; }
[Property, Feature( "Collision" ), Group( "Trigger Actions" ), ShowIf( nameof( IsTrigger ), true )]
public Action<Collider> OnTriggerExit { get; set; }
/// <summary>
/// Whether or not the associated Collider is dirty. Setting this to true will rebuild the Collider on the next frame.
/// </summary>
public bool IsDirty
{
get => Collider?.IsDirty ?? false;
set
{
if ( !Collider.IsValid() ) return;
Collider.IsDirty = value;
}
}
TilesetCollider Collider;
internal List<TilesetSceneObject> _sos = new();
protected override void OnEnabled ()
{
base.OnEnabled();
CreateCollider();
if ( Layers is null ) return;
foreach ( var layer in Layers )
{
layer.TilesetComponent = this;
}
}
protected override void OnDisabled ()
{
base.OnDisabled();
DestroyCollider();
foreach ( var _so in _sos )
{
_so.Delete();
}
_sos.Clear();
}
protected override void OnUpdate ()
{
base.OnUpdate();
_sos ??= new();
Layers ??= new();
var _newSos = new List<TilesetSceneObject>();
foreach ( var sos in _sos )
{
if ( sos is not null || sos.IsValid() )
{
_newSos.Add( sos );
}
else
{
sos?.Delete();
}
}
_sos = _newSos;
if ( Layers.Count != _sos.Count )
{
RebuildSceneObjects();
}
}
protected override void OnTagsChanged ()
{
base.OnTagsChanged();
foreach ( var _so in _sos )
_so?.Tags.SetFrom( Tags );
}
protected override void OnPreRender ()
{
base.OnPreRender();
if ( Layers is null ) return;
if ( Layers.Count == 0 )
{
return;
}
foreach ( var _so in _sos )
{
if ( !_so.IsValid() ) continue;
_so.RenderingEnabled = true;
_so.Transform = Transform.World;
_so.Flags.CastShadows = false;
_so.Flags.IsOpaque = false;
_so.Flags.IsTranslucent = true;
}
}
protected override void DrawGizmos ()
{
base.DrawGizmos();
var bounds = GetBounds();
Gizmo.Hitbox.BBox( bounds );
if ( !Gizmo.IsSelected ) return;
using ( Gizmo.Scope( "tileset", new Transform( 0, WorldRotation.Inverse, 1 ) ) )
{
Gizmo.Draw.Color = Color.Yellow;
Gizmo.Draw.LineThickness = 1f;
Gizmo.Draw.LineBBox( bounds );
}
}
public BBox GetBounds ()
{
var bounds = BBox.FromPositionAndSize( 0, 0 );
foreach ( var _so in _sos )
{
if ( !_so.IsValid() ) continue;
var boundSize = _so.Bounds.Size;
if ( ( boundSize.x + boundSize.y + boundSize.z ) > ( bounds.Size.x + bounds.Size.y + bounds.Size.z ) )
{
bounds = _so.Bounds.Translate( -_so.Position );
}
}
return bounds;
}
void RebuildSceneObjects ()
{
foreach ( var _so in _sos )
{
_so.Delete();
}
_sos = new List<TilesetSceneObject>();
for ( int i = 0; i < Layers.Count; i++ )
{
_sos.Add( new TilesetSceneObject( this, Scene.SceneWorld, i ) );
}
}
void CreateCollider ()
{
if ( !HasCollider ) return;
if ( Collider.IsValid() ) return;
Collider = AddComponent<TilesetCollider>();
Collider.Flags |= ComponentFlags.Hidden | ComponentFlags.NotSaved;
Collider.Tileset = this;
Collider.Static = Static;
Collider.IsTrigger = IsTrigger;
Collider.Friction = Friction;
Collider.Surface = Surface;
Collider.SurfaceVelocity = SurfaceVelocity;
Collider.OnTriggerEnter += OnTriggerEnter;
Collider.OnTriggerExit += OnTriggerExit;
}
void DestroyCollider ()
{
if ( Collider.IsValid() )
Collider.Destroy();
Collider = null;
}
/// <summary>
/// Returns the Layer with the specified name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public Layer GetLayerFromName ( string name )
{
return Layers.FirstOrDefault( x => x.Name == name );
}
/// <summary>
/// Returns the Layer at the specified index
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public Layer GetLayerFromIndex ( int index )
{
if ( index < 0 || index >= Layers.Count ) return null;
return Layers[index];
}
public class Layer
{
/// <summary>
/// The name of the Layer
/// </summary>
public string Name { get; set; }
/// <summary>
/// Whether or not this Layer is currently being rendered
/// </summary>
public bool IsVisible { get; set; }
/// <summary>
/// Whether or not this Layer is locked. Locked Layers will ignore any attempted changes
/// </summary>
public bool IsLocked { get; set; }
/// <summary>
/// The Tileset that this Layer uses
/// </summary>
[Property, Group( "Selected Layer" )] public TilesetResource TilesetResource { get; set; }
/// <summary>
/// The height of the Layer
/// </summary>
[Property, Group( "Selected Layer" )] public float? Height { get; set; } = null;
/// <summary>
/// Whether or not this Layer dictates the collision mesh
/// </summary>
[Group( "Selected Layer" ), Title( "Has Collisions" )] public bool IsCollisionLayer { get; set; }
/// <summary>
/// A dictionary of all Tiles in the layer by their position.
/// </summary>
public Dictionary<Vector2Int, Tile> Tiles { get; set; }
/// <summary>
/// A dictionary containing a list of positions for each Autotile Brush by their ID.
/// </summary>
public Dictionary<Guid, List<AutotilePosition>> Autotiles { get; set; }
/// <summary>
/// The TilesetComponent that this Layer belongs to
/// </summary>
[JsonIgnore, Hide] public TilesetComponent TilesetComponent { get; set; }
public Layer ( string name = "Untitled Layer" )
{
Name = name;
IsVisible = true;
IsLocked = false;
Tiles = new();
}
/// <summary>
/// Returns an exact copy of the Layer
/// </summary>
/// <returns></returns>
public Layer Copy ()
{
var layer = new Layer( Name )
{
IsVisible = IsVisible,
IsLocked = IsLocked,
Tiles = new(),
IsCollisionLayer = false,
TilesetComponent = TilesetComponent,
};
foreach ( var tile in Tiles )
{
layer.Tiles[tile.Key] = tile.Value.Copy();
}
return layer;
}
/// <summary>
/// Set a tile at the specified position. Will fail if IsLocked is true.
/// </summary>
/// <param name="position"></param>
/// <param name="tileId"></param>
/// <param name="cellPosition"></param>
/// <param name="angle"></param>
/// <param name="flipX"></param>
/// <param name="flipY"></param>
/// <param name="rebuild"></param>
public void SetTile ( Vector2Int position, Guid tileId, Vector2Int cellPosition = default, int angle = 0, bool flipX = false, bool flipY = false, bool rebuild = true, bool removeAutotile = true )
{
if ( IsLocked ) return;
var tile = new Tile( tileId, cellPosition, angle, flipX, flipY );
Tiles[position] = tile;
if ( rebuild && TilesetComponent.IsValid() )
TilesetComponent.IsDirty = true;
if ( removeAutotile && Autotiles is not null )
{
foreach ( var group in Autotiles )
{
foreach ( var autotile in group.Value )
{
if ( autotile.Position == position )
{
Autotiles[group.Key].Remove( autotile );
break;
}
}
}
}
}
/// <summary>
/// Get the Tile at the specified position
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
public Tile GetTile ( Vector2Int position )
{
return Tiles[position];
}
/// <summary>
/// Get the Tile at the specified position
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
public Tile GetTile ( Vector3 position )
{
return Tiles[new Vector2Int( (int)position.x, (int)position.y )];
}
/// <summary>
/// Remove the Tile at the specified position. Will fail if IsLocked is true.
/// </summary>
/// <param name="position"></param>
public void RemoveTile ( Vector2Int position )
{
if ( IsLocked ) return;
Tiles.Remove( position );
if ( Autotiles is not null )
{
foreach ( var group in Autotiles )
{
foreach ( var autotile in group.Value )
{
if ( autotile.Position == position )
{
Autotiles[group.Key].Remove( autotile );
break;
}
}
}
}
}
/// <summary>
/// Set an Autotile at the specified position. Will fail if IsLocked is true.
/// </summary>
/// <param name="autotileBrush"></param>
/// <param name="position"></param>
/// <param name="enabled"></param>
/// <param name="update"></param>
/// <param name="isMerging"></param>
public void SetAutotile ( AutotileBrush autotileBrush, Vector2Int position, bool enabled = true, bool update = true, bool isMerging = false )
{
SetAutotile( autotileBrush.Id, position, enabled, update, isMerging );
}
/// <summary>
/// Set an Autotile at the specified position. Will fail if IsLocked is true.
/// </summary>
/// <param name="autotileId"></param>
/// <param name="position"></param>
/// <param name="enabled"></param>
/// <param name="update"></param>
/// <param name="isMerging"></param>
public void SetAutotile ( Guid autotileId, Vector2Int position, bool enabled = true, bool update = true, bool isMerging = false )
{
if ( IsLocked ) return;
Autotiles ??= new();
foreach ( var group in Autotiles )
{
if ( group.Key == autotileId ) continue;
foreach ( var autotile in group.Value )
{
if ( autotile.Position == position )
{
Autotiles[group.Key].Remove( autotile );
break;
}
}
}
if ( !Autotiles.ContainsKey( autotileId ) )
Autotiles[autotileId] = new List<AutotilePosition>();
bool shouldUpdate = false;
if ( enabled )
{
if ( !Autotiles[autotileId].Any( x => x.Position == position ) )
{
Autotiles[autotileId].Add( new( position, isMerging ) );
shouldUpdate = true;
}
}
else
{
var foundPos = Autotiles[autotileId].FirstOrDefault( x => x.Position == position );
if ( foundPos is not null )
{
Tiles.Remove( position );
Autotiles[autotileId].Remove( foundPos );
shouldUpdate = true;
}
else
{
RemoveTile( position );
}
}
if ( update && shouldUpdate )
{
UpdateAutotile( autotileId, position, !enabled, shouldMerge: isMerging );
}
}
/// <summary>
/// Update the Autotile at the specified position. Used when manually modifying the placed autotiles.
/// </summary>
/// <param name="autotileId"></param>
/// <param name="position"></param>
/// <param name="checkErased"></param>
/// <param name="updateSurrounding"></param>
/// <param name="shouldMerge"></param>
public void UpdateAutotile ( Guid autotileId, Vector2Int position, bool checkErased, bool updateSurrounding = true, bool shouldMerge = false )
{
if ( !Autotiles.ContainsKey( autotileId ) ) return;
var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
var autotile = Autotiles[autotileId].FirstOrDefault( x => x.Position == position );
if ( autotile is not null )
{
if ( shouldMerge ) autotile.ShouldMerge = true;
if ( autotile.ShouldMerge ) shouldMerge = true;
var bitmask = GetAutotileBitmask( autotileId, position, shouldMerge );
if ( bitmask == -1 )
{
if ( checkErased ) RemoveTile( position );
}
else
{
if ( brush is not null )
{
var tile = brush.GetTileFromBitmask( bitmask );
if ( tile is not null )
{
SetTile( position, tile.Id, Vector2Int.Zero, 0, false, false, false, removeAutotile: false );
}
else
{
Log.Warning( $"Tile not found for bitmask {bitmask} in AutotileBrush {brush.Name}" );
}
}
}
}
if ( updateSurrounding )
{
var up = position.WithY( position.y + 1 );
var down = position.WithY( position.y - 1 );
var left = position.WithX( position.x - 1 );
var right = position.WithX( position.x + 1 );
var upLeft = up.WithX( left.x );
var upRight = up.WithX( right.x );
var downLeft = down.WithX( left.x );
var downRight = down.WithX( right.x );
if ( brush is not null && brush.AutotileType == AutotileType.Bitmask2x2Edge )
{
ClearInvalidAutotile( autotileId, up );
ClearInvalidAutotile( autotileId, down );
ClearInvalidAutotile( autotileId, left );
ClearInvalidAutotile( autotileId, right );
ClearInvalidAutotile( autotileId, upLeft );
ClearInvalidAutotile( autotileId, upRight );
ClearInvalidAutotile( autotileId, downLeft );
ClearInvalidAutotile( autotileId, downRight );
}
UpdateAutotile( autotileId, up, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, down, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, left, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, right, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, upLeft, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, upRight, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, downLeft, checkErased, false, shouldMerge );
UpdateAutotile( autotileId, downRight, checkErased, false, shouldMerge );
}
}
void ClearInvalidAutotile ( Guid autotileId, Vector2Int position )
{
if ( !Tiles.TryGetValue( position, out var tile ) ) return;
var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
if ( brush is null ) return;
if ( brush.AutotileType != AutotileType.Bitmask2x2Edge ) return;
if ( !brush.Tiles.Any( x => x.Tiles.Any( y => y.Id == tile.TileId ) ) ) return;
if ( GetAutotileBitmask( autotileId, position ) != -1 ) return;
RemoveTile( position );
}
public int GetAutotileBitmask ( Guid autotileId, Vector2Int position, bool mergeAll = false )
{
if ( Autotiles is null || ( !mergeAll && !Autotiles.ContainsKey( autotileId ) ) ) return -1;
List<AutotilePosition> positions = new();
if ( mergeAll )
{
foreach ( var kvp in Autotiles )
{
positions.AddRange( kvp.Value );
}
}
else
{
positions = Autotiles[autotileId];
}
int value = 0;
var up = position.WithY( position.y + 1 );
var down = position.WithY( position.y - 1 );
var left = position.WithX( position.x - 1 );
var right = position.WithX( position.x + 1 );
var brush = TilesetResource.AutotileBrushes.FirstOrDefault( x => x.Id == autotileId );
if ( brush is null ) return 0;
bool is2x2 = brush.AutotileType == AutotileType.Bitmask2x2Edge;
if ( is2x2 )
{
foreach ( var pos in positions )
{
if ( pos.Position == up ) value += 1;
if ( pos.Position == left ) value += 2;
if ( pos.Position == right ) value += 4;
if ( pos.Position == down ) value += 8;
}
switch ( value )
{
case 0:
case 1:
case 2:
case 4:
case 8:
case 9:
case 6:
return -1;
}
value = 0;
}
var upLeft = up.WithX( left.x );
var upRight = up.WithX( right.x );
var downLeft = down.WithX( left.x );
var downRight = down.WithX( right.x );
foreach ( var thing in positions )
{
var pos = thing.Position;
if ( pos == upLeft ) value += 1;
if ( pos == up ) value += 2;
if ( pos == upRight ) value += 4;
if ( pos == left ) value += 8;
if ( pos == right ) value += 16;
if ( pos == downLeft ) value += 32;
if ( pos == down ) value += 64;
if ( pos == downRight ) value += 128;
}
if ( is2x2 )
{
switch ( value )
{
case 46:
case 116:
case 147:
case 201:
return -1;
}
}
return value;
}
public int GetAutotileBitmask ( Guid autotileId, Vector2Int position, Dictionary<Vector2Int, bool> overrides, bool mergeAll = false )
{
if ( Autotiles is null ) return -1;
var positions = new List<Vector2Int>();
foreach ( var thing in Autotiles )
{
if ( !mergeAll && thing.Key != autotileId ) continue;
foreach ( var pos in thing.Value )
{
if ( !positions.Contains( pos.Position ) )
positions.Add( pos.Position );
}
}
int value = 0;
foreach ( var ride in overrides )
{
if ( ride.Value )
{
if ( !positions.Contains( ride.Key ) )
{
positions.Add( ride.Key );
}
}
else
{
if ( positions.Contains( ride.Key ) )
{
positions.Remove( ride.Key );
}
}
}
var up = position.WithY( position.y + 1 );
var down = position.WithY( position.y - 1 );
var left = position.WithX( position.x - 1 );
var right = position.WithX( position.x + 1 );
var upLeft = up.WithX( left.x );
var upRight = up.WithX( right.x );
var downLeft = down.WithX( left.x );
var downRight = down.WithX( right.x );
foreach ( var pos in positions )
{
if ( pos == upLeft ) value += 1;
if ( pos == up ) value += 2;
if ( pos == upRight ) value += 4;
if ( pos == left ) value += 8;
if ( pos == right ) value += 16;
if ( pos == downLeft ) value += 32;
if ( pos == down ) value += 64;
if ( pos == downRight ) value += 128;
}
return value;
}
public class AutotilePosition
{
public Vector2Int Position { get; set; }
public bool ShouldMerge { get; set; } = false;
public AutotilePosition ( Vector2Int position, bool shouldMerge = false )
{
Position = position;
ShouldMerge = shouldMerge;
}
}
}
public class Tile
{
public Guid TileId { get; set; } = Guid.NewGuid();
public Vector2Int CellPosition { get; set; }
public bool HorizontalFlip { get; set; }
public bool VerticalFlip { get; set; }
public int Rotation { get; set; }
public Vector2Int BakedPosition { get; set; }
public Tile () { }
public Tile ( Guid tileId, Vector2Int cellPosition, int rotation, bool flipX, bool flipY )
{
TileId = tileId;
CellPosition = cellPosition;
HorizontalFlip = flipX;
VerticalFlip = flipY;
Rotation = rotation;
}
public Tile Copy ()
{
return new Tile( TileId, CellPosition, Rotation, HorizontalFlip, VerticalFlip );
}
}
public class ComponentControls { }
}
internal sealed class TilesetSceneObject : SceneCustomObject
{
TilesetComponent Component;
Dictionary<TilesetResource, (TileAtlas, Material)> Materials = new();
Material MissingMaterial;
int LayerIndex;
public TilesetSceneObject ( TilesetComponent component, SceneWorld world, int layerIndex ) : base( world )
{
Component = component;
LayerIndex = layerIndex;
MissingMaterial = Material.Load( "materials/sprite_2d.vmat" ).CreateCopy();
MissingMaterial.Set( "Texture", Texture.Load( "images/missing-tile.png" ) );
Tags.SetFrom( Component.Tags );
}
public override void RenderSceneObject ()
{
if ( Component?.Layers is null ) return;
var Layer = Component.Layers.ElementAtOrDefault( LayerIndex );
if ( Layer is null )
{
return;
}
var layers = Component.Layers.ToList();
layers.Reverse();
if ( layers.Count == 0 ) return;
Dictionary<Vector2Int, TilesetComponent.Tile> missingTiles = new();
if ( Layer?.IsVisible != true ) return;
int i = 0;
int layerIndex = layers.IndexOf( Layer );
{
var tileset = Layer.TilesetResource;
if ( tileset is null ) return;
var tilemap = tileset.TileMap;
var combo = GetMaterial( tileset );
if ( combo.Item1 is null || combo.Item2 is null ) return;
var tiling = combo.Item1.GetTiling();
var totalTiles = Layer.Tiles.Where( x => x.Value.TileId == default || tilemap.ContainsKey( x.Value.TileId ) );
var vertex = ArrayPool<Vertex>.Shared.Rent( totalTiles.Count() * 6 );
var minPosition = new Vector3( int.MaxValue, int.MaxValue, int.MaxValue );
var maxPosition = new Vector3( int.MinValue, int.MinValue, int.MinValue );
foreach ( var tile in Layer.Tiles )
{
var pos = tile.Key;
Vector2Int offsetPos = Vector2Int.Zero;
if ( tile.Value.TileId == default ) offsetPos = tile.Value.BakedPosition;
else
{
if ( !tilemap.ContainsKey( tile.Value.TileId ) )
{
missingTiles[pos] = tile.Value;
continue;
}
offsetPos = tilemap[tile.Value.TileId].Position;
}
var offset = combo.Item1.GetOffset( offsetPos + tile.Value.CellPosition );
if ( tile.Value.HorizontalFlip )
offset.x = -offset.x - tiling.x;
if ( !tile.Value.VerticalFlip )
offset.y = -offset.y - tiling.y;
var size = tileset.GetTileSize();
var position = new Vector3( pos.x, pos.y, Layer.Height ?? ( Component.Layers.Count - Component.Layers.IndexOf( Layer ) ) ) * new Vector3( size.x, size.y, 1 );
minPosition = Vector3.Min( minPosition, position );
maxPosition = Vector3.Max( maxPosition, position );
var topLeft = new Vector3( position.x, position.y, position.z );
var topRight = new Vector3( position.x + size.x, position.y, position.z );
var bottomRight = new Vector3( position.x + size.x, position.y + size.y, position.z );
var bottomLeft = new Vector3( position.x, position.y + size.y, position.z );
var uvTopLeft = new Vector2( offset.x, offset.y );
var uvTopRight = new Vector2( offset.x + tiling.x, offset.y );
var uvBottomRight = new Vector2( offset.x + tiling.x, offset.y + tiling.y );
var uvBottomLeft = new Vector2( offset.x, offset.y + tiling.y );
if ( tile.Value.Rotation == 90 )
{
var tempUv = uvTopLeft;
uvTopLeft = uvBottomLeft;
uvBottomLeft = uvBottomRight;
uvBottomRight = uvTopRight;
uvTopRight = tempUv;
}
else if ( tile.Value.Rotation == 180 )
{
var tempUv = uvTopLeft;
uvTopLeft = uvBottomRight;
uvBottomRight = tempUv;
tempUv = uvTopRight;
uvTopRight = uvBottomLeft;
uvBottomLeft = tempUv;
}
else if ( tile.Value.Rotation == 270 )
{
var tempUv = uvTopLeft;
uvTopLeft = uvTopRight;
uvTopRight = uvBottomRight;
uvBottomRight = uvBottomLeft;
uvBottomLeft = tempUv;
}
vertex[i] = new Vertex( topLeft );
vertex[i].TexCoord0 = uvTopLeft;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( topRight );
vertex[i].TexCoord0 = uvTopRight;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( bottomRight );
vertex[i].TexCoord0 = uvBottomRight;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( topLeft );
vertex[i].TexCoord0 = uvTopLeft;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( bottomRight );
vertex[i].TexCoord0 = uvBottomRight;
vertex[i].Normal = Vector3.Up;
i++;
vertex[i] = new Vertex( bottomLeft );
vertex[i].TexCoord0 = uvBottomLeft;
vertex[i].Normal = Vector3.Up;
i++;
}
Graphics.Draw( vertex, totalTiles.Count() * 6, combo.Item2, Attributes );
ArrayPool<Vertex>.Shared.Return( vertex );
var siz = tileset.GetTileSize();
maxPosition += new Vector3( siz.x, siz.y, 0 );
Bounds = new BBox( minPosition, maxPosition + Vector3.Down * 0.01f ).Rotate( Rotation ).Translate( Position );
}
if ( missingTiles.Count > 0 )
{
var uvTopLeft = new Vector2( 0, 0 );
var uvTopRight = new Vector2( 1, 0 );
var uvBottomRight = new Vector2( 1, 1 );
var uvBottomLeft = new Vector2( 0, 1 );
foreach ( var tile in missingTiles )
{
var material = MissingMaterial;
var pos = tile.Key;
var size = Component.Layers[0].TilesetResource.TileSize;
var position = new Vector3( pos.x, pos.y, 0 ) * new Vector3( size.x, size.y, 1 );
var topLeft = new Vector3( position.x, position.y, position.z );
var topRight = new Vector3( position.x + size.x, position.y, position.z );
var bottomRight = new Vector3( position.x + size.x, position.y + size.y, position.z );
var bottomLeft = new Vector3( position.x, position.y + size.y, position.z );
var vertex = new Vertex[]
{
new Vertex(topLeft) { TexCoord0 = uvTopLeft, Normal = Vector3.Up },
new Vertex(topRight) { TexCoord0 = uvTopRight, Normal = Vector3.Up },
new Vertex(bottomRight) { TexCoord0 = uvBottomRight, Normal = Vector3.Up },
new Vertex(topLeft) { TexCoord0 = uvTopLeft, Normal = Vector3.Up },
new Vertex(bottomRight) { TexCoord0 = uvBottomRight, Normal = Vector3.Up },
new Vertex(bottomLeft) { TexCoord0 = uvBottomLeft, Normal = Vector3.Up },
};
Graphics.Draw( vertex, 6, material, Attributes );
}
}
}
(TileAtlas, Material) GetMaterial ( TilesetResource resource )
{
var texture = TileAtlas.FromTileset( resource );
if ( Materials.TryGetValue( resource, out var combo ) )
{
combo.Item1 = texture;
combo.Item2.Set( "Texture", texture );
}
else
{
var material = Material.Load( "materials/sprite_2d.vmat" ).CreateCopy();
material.Set( "Texture", texture );
combo.Item1 = texture;
combo.Item2 = material;
Materials.Add( resource, combo );
}
return combo;
}
}
namespace SpriteTools.Converters
{
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>
/// Json collection converter.
/// </summary>
/// <typeparam name="TDatatype">Type of item to convert.</typeparam>
/// <typeparam name="TConverterType">Converter to use for individual items.</typeparam>
public class JsonCollectionItemConverter<TDatatype, TConverterType> : JsonConverter<IEnumerable<TDatatype>>
where TConverterType : JsonConverter
{
/// <summary>
/// Reads a json string and deserializes it into an object.
/// </summary>
/// <param name="reader">Json reader.</param>
/// <param name="typeToConvert">Type to convert.</param>
/// <param name="options">Serializer options.</param>
/// <returns>Created object.</returns>
public override IEnumerable<TDatatype> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return default(IEnumerable<TDatatype>);
}
JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(options);
jsonSerializerOptions.Converters.Clear();
jsonSerializerOptions.Converters.Add(Activator.CreateInstance<TConverterType>());
List<TDatatype> returnValue = new List<TDatatype>();
while (reader.TokenType != JsonTokenType.EndArray)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
returnValue.Add((TDatatype)JsonSerializer.Deserialize(ref reader, typeof(TDatatype), jsonSerializerOptions));
}
reader.Read();
}
return returnValue;
}
/// <summary>
/// Writes a json string.
/// </summary>
/// <param name="writer">Json writer.</param>
/// <param name="value">Value to write.</param>
/// <param name="options">Serializer options.</param>
public override void Write(Utf8JsonWriter writer, IEnumerable<TDatatype> value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions(options);
jsonSerializerOptions.Converters.Clear();
jsonSerializerOptions.Converters.Add(Activator.CreateInstance<TConverterType>());
writer.WriteStartArray();
foreach (TDatatype data in value)
{
JsonSerializer.Serialize(writer, data, jsonSerializerOptions);
}
writer.WriteEndArray();
}
}
}using Sandbox;
public class Camera2D : Component
{
[Property] public CameraComponent Camera { get; set; }
public Vector2 TargetPos { get; set; }
public Vector2 CameraPos { get; set; }
protected override void OnUpdate()
{
base.OnUpdate();
//Vector2 newPos = Vector2.Lerp( (Vector2)WorldPosition, TargetPos, Time.Delta * 3f );
CameraPos = Utils.DynamicEaseTo( CameraPos, TargetPos, 0.2f, Time.Delta );
var newPos = CameraPos + Utils.GetRandomVector() * Manager.Instance.Player.CamShakeAmount;
float bounds_zoom = 1f + Manager.Instance.Player.Stats[PlayerStat.ZoomAmount] * 0.44f;
var XDIST = 10.75f / bounds_zoom;
var Y_MIN = -8.8f / bounds_zoom;
var Y_MAX = 8.9f / bounds_zoom;
newPos = new Vector2( MathX.Clamp( newPos.x, -XDIST, XDIST ), MathX.Clamp( newPos.y, Y_MIN, Y_MAX ) );
WorldPosition = ((Vector3)newPos).WithZ( WorldPosition.z );
}
public void SetPos( Vector2 pos )
{
CameraPos = pos;
TargetPos = pos;
WorldPosition = ((Vector3)pos).WithZ( WorldPosition.z );
}
}
using Sandbox;
public class FloatingDamageNumber : Component
{
private RealTimeSince _timeSince;
protected override void OnAwake()
{
base.OnAwake();
_timeSince = 0f;
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( _timeSince > 1f )
GameObject.Destroy();
}
}
using Sandbox.UI;
using SpriteTools;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using static Manager;
public class LavaPuddle : Component
{
[Property] public SpriteRendererLayer Sprite { get; set; }
public TimeSince TimeSinceSpawn { get; set; }
public float Lifetime { get; set; }
public float Radius { get; set; }
public float FullRadius { get; set; }
private float _timeOffset;
private TimeSince _timeSinceDamagePlayer;
private const float DAMAGE_INTERVAL = 0.25f;
private float _expandTime;
public float DamageToPlayer { get; set; }
public Color ColorA { get; set; }
public Color ColorB { get; set; }
public Vector2 Position2D
{
get { return (Vector2)WorldPosition; }
set { WorldPosition = new Vector3( value.x, value.y, WorldPosition.z ); }
}
protected override void OnAwake()
{
base.OnAwake();
if ( Game.Random.Float( 0f, 1f ) < 0.5f )
Sprite.SpriteFlags = SpriteFlags.HorizontalFlip;
Radius = 0f;
LocalScale = new Vector3( Radius * 1f, Radius * 1.06f, 1f ) * 2f * Globals.SPRITE_SCALE;
//LocalScale = new Vector3( Game.Random.Float( 0.4f, 0.6f ), Game.Random.Float( 0.15f, 0.25f ), 1f ) * Globals.SPRITE_SCALE;
//LocalScale = new Vector3( Game.Random.Float( 0.33f, 0.48f ), Game.Random.Float( 0.4f, 0.63f ), 1f ) * 5f * Globals.SPRITE_SCALE;
_timeOffset = Game.Random.Float( 0f, 99f );
TimeSinceSpawn = 0f;
_expandTime = Game.Random.Float( 0.4f, 0.5f );
DamageToPlayer = 2f + Math.Min(Manager.Instance.Difficulty, 5);
}
protected override void OnUpdate()
{
base.OnUpdate();
float FADE_IN_TIME = 0.3f;
var color = Color.Lerp( ColorA, ColorB, 0.5f + Utils.FastSin( TimeSinceSpawn * 16f ) * 0.5f );
var opacity = 0.3f
* Utils.Map( TimeSinceSpawn, 0f, FADE_IN_TIME, 0f, 1f, EasingType.SineOut )
* Utils.Map( TimeSinceSpawn, Lifetime - 1f, Lifetime, 1f, 0f, EasingType.SineIn )
* ( 0.85f + Utils.FastSin( _timeOffset + Time.Now * 16f ) * 0.15f );
Sprite.Tint = color.WithAlpha( opacity );
//Gizmo.Draw.Color = Color.White.WithAlpha( 0.05f );
//Gizmo.Draw.LineSphere( WorldPosition, Radius, 20 );
if (TimeSinceSpawn < _expandTime )
{
Radius = Utils.Map( TimeSinceSpawn, 0f, _expandTime, FullRadius * 0.05f, FullRadius, EasingType.QuadOut );
LocalScale = new Vector3( Radius * 0.98f, Radius * 1.06f, 1f ) * 2f * Globals.SPRITE_SCALE;
}
if ( TimeSinceSpawn > Lifetime )
{
Manager.Instance.RemoveLavaPuddle( this );
GameObject.Destroy();
return;
}
if( TimeSinceSpawn > FADE_IN_TIME && TimeSinceSpawn < Lifetime - 0.5f )
{
var player = Manager.Instance.Player;
float distSqr = (player.Position2D - Position2D).LengthSquared;
if( distSqr < MathF.Pow(Radius, 1.9f) && _timeSinceDamagePlayer > DAMAGE_INTERVAL && player.TimeSinceHurtLava > DAMAGE_INTERVAL && !player.IsDead )
{
float currDmg = Utils.Map( TimeSinceSpawn, FADE_IN_TIME, FADE_IN_TIME * 2f, 1f, DamageToPlayer );
float dmg = player.CheckDamageAmount( currDmg, DamageType.LavaPuddle );
if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
{
player.Damage( dmg );
_timeSinceDamagePlayer = 0f;
player.TimeSinceHurtLava = 0f;
Manager.Instance.PlaySfxNearby( "lava_puddle_03", player.Position2D, pitch: Game.Random.Float( 0.9f, 1.1f ), volume: 1f, maxDist: 4f );
}
}
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status( 5, 0, 1f, 0, false )]
public class BulletLifetimeDamageStatus : Status
{
public BulletLifetimeDamageStatus()
{
Title = "Growing Bullets";
IconPath = "textures/icons/bullet_lifetime_damage.png";
}
public override void Init( Player player )
{
base.Init( player );
}
public override void Refresh()
{
Description = GetDescription( Level );
Player.Modify( this, PlayerStat.BulletDamageGrow, GetAddForLevel( Level ), ModifierType.Add );
}
public override string GetDescription( int newLevel )
{
return string.Format( "Bullets grow their damage by {0} per second", GetPrintAmountForLevel( Level ) );
}
public override string GetUpgradeDescription( int newLevel )
{
return newLevel > 1 ? string.Format( "Bullets grow their damage by {0}→{1} per second", GetPrintAmountForLevel( newLevel - 1 ), GetPrintAmountForLevel( newLevel ) ) : GetDescription( newLevel );
}
public float GetAddForLevel( int level )
{
return 0.2f + 1.5f * level + (level == 7 ? 1.5f : 0f);
}
public string GetPrintAmountForLevel( int level )
{
return string.Format( "{0:0.0}", GetAddForLevel( level ) );
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status( 7, 0, 1f, 0, false )]
public class CritChanceStatus : Status
{
public CritChanceStatus()
{
Title = "Sharp Bullets";
IconPath = "textures/icons/crit_chance.png";
}
public override void Init( Player player )
{
base.Init( player );
}
public override void Refresh()
{
Description = GetDescription( Level );
Player.Modify( this, PlayerStat.CritChance, GetAddForLevel( Level ), ModifierType.Add );
}
public override string GetDescription( int newLevel )
{
return string.Format( "Increase critical chance from 5%→{0}%", GetPercentForLevel( Level ) );
}
public override string GetUpgradeDescription( int newLevel )
{
return newLevel > 1 ? string.Format( "Increase critical chance from {0}%→{1}%", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
}
public float GetAddForLevel( int level )
{
return 0.05f + 0.08f * level + (level == 7 ? 0.04f : 0f);
}
public float GetPercentForLevel( int level )
{
return 5 + 8 * level + (level == 7 ? 4 : 0);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status(4, 0, 0.5f, 0, true)]
public class CurseAimDir : Status
{
public CurseAimDir()
{
Title = "Random Aim";
IconPath = "textures/icons/curse_shoot_random_dir.png";
}
public override void Init(Player player)
{
base.Init(player);
}
public override void Refresh()
{
Description = GetDescription(Level);
Player.Modify(this, PlayerStat.ShootRandomDirChance, GetChanceForLevel(Level), ModifierType.Add);
}
public override string GetDescription( int newLevel )
{
return string.Format( "{0}% chance to shoot in a random direction", GetPercentForLevel( Level ) );
}
public override string GetUpgradeDescription( int newLevel )
{
return newLevel > 1 ? string.Format( "{0}%→{1}% chance to shoot in a random direction", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
}
public float GetChanceForLevel( int level )
{
return 0.25f * level;
}
public float GetPercentForLevel( int level )
{
return 25 * level;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status( 7, 0, 1f, 0, true )]
public class CurseAttackSpeedStatus : Status
{
public CurseAttackSpeedStatus()
{
Title = "Lazy Shooting";
IconPath = "textures/icons/curse_attack_speed.png";
}
public override void Init( Player player )
{
base.Init( player );
}
public override void Refresh()
{
Description = GetDescription( Level );
Player.Modify( this, PlayerStat.AttackSpeed, GetMultForLevel( Level ), ModifierType.Mult );
}
public override string GetDescription( int newLevel )
{
return string.Format( "Decrease attack speed by {0}%", GetPercentForLevel( Level ) );
}
public override string GetUpgradeDescription( int newLevel )
{
return newLevel > 1 ? string.Format( "Decrease attack speed by {0}%→{1}%", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
}
public float GetMultForLevel( int level )
{
return 1f - 0.20f * level;
}
public float GetPercentForLevel( int level )
{
return 20 * level;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status( 4, 0, 1f, 0, false )]
public class DashStrengthStatus : Status
{
public DashStrengthStatus()
{
Title = "Leg Day";
IconPath = "textures/icons/dash_strength.png";
}
public override void Init( Player player )
{
base.Init( player );
}
public override void Refresh()
{
Description = GetDescription( Level );
Player.Modify( this, PlayerStat.DashStrength, GetMultForLevel( Level ), ModifierType.Mult );
Player.Modify( this, PlayerStat.DashInvulnTime, GetMultForLevel( Level ), ModifierType.Mult );
}
public override string GetDescription( int newLevel )
{
return string.Format( "You dash {0}% longer", GetPercentForLevel( Level ) );
}
public override string GetUpgradeDescription( int newLevel )
{
return newLevel > 1 ? string.Format( "You dash {0}%→{1}% longer", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
}
public float GetMultForLevel( int level )
{
return 1f + 0.18f * level + (level == 4 ? 0.03f : 0f);
}
public float GetPercentForLevel( int level )
{
return 18 * level + (level == 4 ? 3 : 0);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status( 7, 0, 1f, 0, false, typeof( DashFearStatus ), typeof( GrenadeFearStatus ) )]
public class FearDropGrenadeStatus : Status
{
public FearDropGrenadeStatus()
{
Title = "Bomb Curse";
IconPath = "textures/icons/fear_drop_grenade.png";
}
public override void Init( Player player )
{
base.Init( player );
}
public override void Refresh()
{
Description = GetDescription( Level );
Player.Modify( this, PlayerStat.FearDropGrenadeChance, GetAddForLevel( Level ), ModifierType.Add ); ;
}
public override string GetDescription( int newLevel )
{
return string.Format( "Enemies you scare have a {0}% chance to drop a grenade on death", GetPercentForLevel( Level ) );
}
public override string GetUpgradeDescription( int newLevel )
{
return newLevel > 1 ? string.Format( "Enemies you scare have a {0}%→{1}% chance to drop a grenade on death", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
}
public float GetAddForLevel( int level )
{
return level * 0.07f;
}
public float GetPercentForLevel( int level )
{
return level * 7;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status( 5, 0, 1f, 0, false, typeof( DashFearStatus ), typeof( GrenadeFearStatus ) )]
public class FearPainStatus : Status
{
public FearPainStatus()
{
Title = "Killer Stress";
IconPath = "textures/icons/fear_pain.png";
}
public override void Init( Player player )
{
base.Init( player );
}
public override void Refresh()
{
Description = GetDescription( Level );
Player.Modify( this, PlayerStat.FearPainPercent, GetAddForLevel( Level ), ModifierType.Add );
}
public override string GetDescription( int newLevel )
{
return string.Format( "Enemies you scare lose {0}% of their remaining HP each second", GetPercentForLevel( Level ) );
}
public override string GetUpgradeDescription( int newLevel )
{
return newLevel > 1 ? string.Format( "Enemies you scare lose {0}%→{1}% of their remaining HP each second", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
}
public float GetAddForLevel( int level )
{
return 0.02f + level * 0.07f + (level == 5 ? 0.01f : 0f);
}
public float GetPercentForLevel( int level )
{
return 2 + level * 7 + (level == 5 ? 1 : 0);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status(4, 0, 1f, 0, false, typeof(GrenadeShootReloadStatus), typeof(FearDropGrenadeStatus))]
public class GrenadeFearStatus : Status
{
public GrenadeFearStatus()
{
Title = "Terrorism";
IconPath = "textures/icons/grenade_fear.png";
}
public override void Init(Player player)
{
base.Init(player);
}
public override void Refresh()
{
Description = GetDescription(Level);
Player.Modify(this, PlayerStat.GrenadeFearChance, GetAddForLevel(Level), ModifierType.Add); ;
}
public override string GetDescription(int newLevel)
{
return string.Format("Your grenades have a {0}% chance to scare enemies they hurt", GetPercentForLevel(Level));
}
public override string GetUpgradeDescription(int newLevel)
{
return newLevel > 1 ? string.Format("Your grenades have a {0}%→{1}% chance to scare enemies they hurt", GetPercentForLevel(newLevel - 1), GetPercentForLevel(newLevel)) : GetDescription(newLevel);
}
public float GetAddForLevel(int level)
{
return level == 4 ? 1f : 0.3f * level;
}
public float GetPercentForLevel(int level)
{
return level == 4 ? 100 : 30 * level;
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status(5, 3, 1f, 0, false )]
public class KillHealStatus : Status
{
public KillHealStatus()
{
Title = "Vampire";
IconPath = "textures/icons/kill_heal.png";
}
public override void Init(Player player)
{
base.Init(player);
}
public override void Refresh()
{
Description = GetDescription(Level);
Player.Modify(this, PlayerStat.HealthRegen, -GetHpDrainAmountForLevel(Level), ModifierType.Add);
}
public override string GetDescription(int newLevel)
{
return string.Format("Heal for {0} whenever you kill an enemy within 2 meters but lose {1} HP/s", GetPrintAmountForLevel(Level), GetHpDrainPrintAmountForLevel(Level));
}
public override string GetUpgradeDescription(int newLevel)
{
return newLevel > 1 ? string.Format( "Heal for {0}→{1} whenever you kill an enemy within 2 meters but lose {2}→{3} HP/s", GetPrintAmountForLevel(newLevel - 1), GetPrintAmountForLevel(newLevel), GetHpDrainPrintAmountForLevel(newLevel - 1), GetHpDrainPrintAmountForLevel(newLevel)) : GetDescription(newLevel);
}
public override void OnKill(Enemy enemy)
{
var distSqr = (enemy.Position2D - Player.Position2D).LengthSquared;
if ( distSqr > 2f * 2f )
return;
var amount = GetAmountForLevel( Level );
if ( Player.Health < Player.Stats[PlayerStat.MaxHp] )
Player.TimeSinceChangeHP = 0f;
Player.RegenHealth( amount );
//DamageNumbersLegacy.Create( amount, Player.Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Player.Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: Color.Green, sizeMultiplier: 0.7f );
}
public float GetAmountForLevel(int level)
{
return 0.1f + level * 0.5f + (level == 5 ? 0.05f : 0f);
}
public string GetPrintAmountForLevel(int level)
{
return string.Format("{0:0.00}", GetAmountForLevel(level));
}
public float GetHpDrainAmountForLevel(int level)
{
return level * 0.4f;
}
public string GetHpDrainPrintAmountForLevel(int level)
{
return string.Format("{0:0.00}", GetHpDrainAmountForLevel(level));
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
[Status(3, 0, 1f, maxDifficulty: 2, isCurse: false )]
public class MoreRerollsStatus : Status
{
public MoreRerollsStatus()
{
Title = "More Rerolls";
IconPath = "textures/icons/more_rerolls.png";
}
public override void Init(Player player)
{
base.Init(player);
}
public override void Refresh()
{
Description = GetDescription(Level);
Player.Modify(this, PlayerStat.NumRerollsPerLevel, GetAddForLevel(Level), ModifierType.Add);
}
public override string GetDescription(int newLevel)
{
return string.Format("Gain {0} addition reroll each level", GetAddForLevel(Level));
}
public override string GetUpgradeDescription(int newLevel)
{
return newLevel > 1 ? string.Format("Gain {0}→{1} addition rerolls each level", GetAddForLevel(newLevel - 1), GetAddForLevel(newLevel)) : GetDescription(newLevel);
}
public float GetAddForLevel(int level)
{
return 1f * level;
}
}
using Sandbox;
[Status( 7, 0, 1f, 0, false )]
public class MovespeedStatus : Status
{
public MovespeedStatus()
{
Title = "Fast Shoes";
IconPath = "textures/icons/shoe.png";
}
public override void Init( Player player )
{
base.Init( player );
}
public override void Refresh()
{
Description = GetDescription( Level );
Player.Modify( this, PlayerStat.MoveSpeed, GetMultForLevel( Level ), ModifierType.Mult );
}
public override string GetDescription( int newLevel )
{
return string.Format( "Increase movespeed by {0}%", GetPercentForLevel( Level ) );
}
public override string GetUpgradeDescription( int newLevel )
{
return newLevel > 1 ? string.Format( "Increase movespeed by {0}%→{1}%", GetPercentForLevel( newLevel - 1 ), GetPercentForLevel( newLevel ) ) : GetDescription( newLevel );
}
public float GetMultForLevel( int level )
{
return 1f + 0.2f * level + (level == 7 ? 0.2f : 0f);
}
public float GetPercentForLevel( int level )
{
return 20 * level + (level == 7 ? 20 : 0);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
public class Status
{
public bool ShouldUpdate { get; protected set; }
public Player Player { get; protected set; }
public int Level { get; set; }
public int MaxLevel { get; set; }
public TimeSince ElapsedTime { get; protected set; }
public string Title { get; protected set; }
public string Description { get; protected set; }
public string IconPath { get; protected set; }
public string DataString { get; protected set; }
public Status()
{
Level = 1;
}
public virtual void Init( Player player )
{
Player = player;
ElapsedTime = 0f;
ShouldUpdate = false;
}
// when gaining or leveling up
public virtual void Refresh()
{
}
public virtual void Update( float dt )
{
//if (ElapsedTime > 10f)
// Player.RemoveStatus(this);
}
public virtual void Remove()
{
}
public virtual string GetDescription( int newLevel )
{
return "...";
}
public virtual string GetUpgradeDescription( int newLevel )
{
return "...";
}
public virtual void Colliding( Thing other, float percent, float dt ) { }
public virtual void OnDashStarted() { }
public virtual void OnDashFinished() { }
public virtual void OnDashRecharged() { }
public virtual void OnReload() { }
//public virtual void OnBurn( Enemy enemy ) { }
//public virtual void OnFreeze( Enemy enemy ) { }
//public virtual void OnFear( Enemy enemy ) { }
public virtual void OnKill( Enemy enemy ) { }
public virtual void OnHurt( float amount ) { }
public virtual void OnGainExperience( int xp ) { }
public virtual void OnLevelUp() { }
public virtual void OnReroll() { }
public virtual void OnGainShield() { }
public virtual void OnLoseShield() { }
public virtual void OnAddStatus( int typeIdentity ) { }
}
using SpriteTools;
using static Manager;
public enum ModifierType { Set, Add, Mult }
public class ModifierData
{
public float value;
public ModifierType type;
public float priority;
public ModifierData( float _value, ModifierType _type, float _priority = 0f )
{
value = _value;
type = _type;
priority = _priority;
}
}
public enum PlayerStat
{
AttackTime, AttackSpeed, ReloadTime, ReloadSpeed, MaxAmmoCount, BulletDamage, BulletForce, Recoil, MoveSpeed, NumProjectiles, BulletSpread, BulletInaccuracy, BulletSpeed, BulletLifetime,
BulletNumPiercing, CritChance, CritMultiplier, LowHealthDamageMultiplier, NumUpgradeChoices, HealthRegen, HealthRegenStill, DamageReductionPercent, PushStrength, CoinAttractRange, CoinAttractStrength, Luck, MaxHp,
NumDashes, DashInvulnTime, DashCooldown, DashProgress, DashStrength, ThornsPercent, ShootFireIgniteChance, FireDamage, FireLifetime, FireSpreadChance, ShootFreezeChance, FreezeLifetime,
FreezeTimeScale, FreezeOnMeleeChance, FreezeFireDamageMultiplier, LastAmmoDamageMultiplier, FearLifetime, FearDamageMultiplier, FearOnMeleeChance, BulletDamageGrow, BulletDamageShrink,
BulletDistanceDamage, NumRerollsPerLevel, FullHealthDamageMultiplier, DamagePerEarlierShot, DamageForSpeed, OverallDamageMultiplier, ExplosionSizeMultiplier, GrenadeVelocity, ExplosionDamageMultiplier,
BulletDamageMultiplier, ExplosionDamageReductionPercent, NonExplosionDamageIncreasePercent, GrenadeStickyPercent, GrenadeFearChance, FearDrainPercent, FearPainPercent, CrateChanceAdditional,
AttackSpeedStill, FearDropGrenadeChance, FrozenShardsNum, NoDashInvuln, BulletFlatDamageAddition, GrenadesCanCrit, BulletHealTeammateAmount, HomingBulletChance, PauseWhileChoosing,
BulletNumBouncing, DashCharm, CharmedEnemyDmgTakenMultiplier, CharmedEnemyDmgDealtMultiplier, BossArrivalTime, RadiusMultiplier, SpecialistStatusAmount, ZoomAmount, MaxFireStacks, XpRepel, HealthPackAmount,
MaxBulletSpread, ShootRandomDirChance, DashRandomDirChance, MoveSelfDmg, MoveSelfDmgReqDist, MoveSelfDmgAmount, SelfDmgDistanceMoved, IncreasedDmgTaken, ReverseControls, SelfCritChance,
}
public enum DamageType { Melee, Ranged, Explosion, Fire, PlayerBullet, Self, Generic, LavaPuddle, FearPain, }
public enum PlayerDamageType { Enemy, Self, Grenade }
public struct CamShakeData
{
public float strength;
public float startTime;
public float time;
public EasingType easingType;
public bool useRealTime;
public CamShakeData( float _strength, float _startTime, float _time, EasingType _easingType, bool _useRealTime )
{
strength = _strength;
startTime = _startTime;
time = _time;
easingType = _easingType;
useRealTime = _useRealTime;
}
}
public class Player : Thing
{
[Property] public GameObject Body { get; set; }
[Property] public GameObject ArrowAimerPrefab { get; set; }
[Property] public GameObject BulletPrefab { get; set; }
[Property] public Sprite EasySprite { get; set; }
[Property] public Sprite Difficulty1Sprite { get; set; }
[Property] public Sprite Difficulty2Sprite { get; set; }
[Property] public Sprite Difficulty3Sprite { get; set; }
[Property] public Sprite Difficulty4Sprite { get; set; }
[Property] public Sprite Difficulty5Sprite { get; set; }
[Property] public Sprite Difficulty6Sprite { get; set; }
[Property] public Sprite Difficulty7Sprite { get; set; }
[Property] public Sprite Difficulty8Sprite { get; set; }
[Property] public Sprite Difficulty9Sprite { get; set; }
[Property] public Sprite Difficulty10Sprite { get; set; }
[Property] public Sprite Difficulty11Sprite { get; set; }
[Property] public Sprite Difficulty12Sprite { get; set; }
[Property] public Sprite Difficulty13Sprite { get; set; }
[Property] public Sprite Difficulty14Sprite { get; set; }
[Property] public Sprite Difficulty15Sprite { get; set; }
[Sync] public float Health { get; set; }
private float _regenHpAccumulated;
private float _drainHpAccumulated;
[Sync] public Vector2 InputVector { get; set; }
public GameObject ArrowAimer { get; private set; }
public SpriteRendererLayer ArrowSprite { get; private set; }
public Vector2 AimDir { get; private set; }
[Sync] public bool IsDead { get; private set; }
public float Timer { get; protected set; }
[Sync] public bool IsReloading { get; protected set; }
[Sync] public float ReloadProgress { get; protected set; }
public const float BASE_MOVE_SPEED = 15f;
private int _shotNum;
[Sync] public int Level { get; protected set; }
public int ExperienceTotal { get; protected set; }
public int ExperienceCurrent { get; protected set; }
public int ExperienceRequired { get; protected set; }
public bool IsChoosingLevelUpReward { get; protected set; }
public TimeSince TimeSinceLevelUp { get; set; }
public RealTimeSince RealTimeSinceChoseUpgrade { get; set; }
public List<Status> LevelUpChoices { get; private set; }
[Sync] public float DashTimer { get; private set; }
[Sync] public bool IsDashing { get; private set; }
[Sync] public Vector2 DashVelocity { get; private set; }
[Sync] public float DashInvulnTimer { get; private set; }
private TimeSince _dashCloudTime;
public float DashProgress { get; protected set; }
[Sync] public float DashRechargeProgress { get; protected set; }
[Sync] public int NumDashesAvailable { get; set; }
public int AmmoCount { get; protected set; }
public bool IsMoving => Velocity.LengthSquared > 0.05f && !IsDashing;
//public bool IsMoving => (Position2D - _lastPos2D).LengthSquared > 0.000001f;
public bool IsInputtingMove => Input.AnalogMove.LengthSquared > 0.01f;
public bool IsInvulnerable => IsDashing && Stats[PlayerStat.NoDashInvuln] <= 0f;
public bool IsTimePausedForChoosing => IsChoosingLevelUpReward && Stats[PlayerStat.PauseWhileChoosing] > 0f;
private float _flashTimer;
private bool _isFlashing;
public TimeSince TimeSinceHurt { get; private set; }
public TimeSince TimeSinceChangeHP { get; set; }
private GameObject _shieldVfx;
[Sync] public int NumRerollAvailable { get; set; }
[Sync] public NetDictionary<PlayerStat, float> Stats { get; private set; } = new();
public Dictionary<int, Status> Statuses { get; private set; }
private Dictionary<Status, Dictionary<PlayerStat, ModifierData>> _modifiers_stat = new Dictionary<Status, Dictionary<PlayerStat, ModifierData>>();
private Dictionary<PlayerStat, float> _original_properties_stat = new Dictionary<PlayerStat, float>();
private bool _doneFirstUpdate;
private TimeSince _timeSinceShoot;
private TimeSince _timeSinceSpawn;
private Vector2 _lastPos2D;
private TimeSince _timeSinceTouchLeftSide;
private bool _hasUnlockedSprinterAchievement;
private bool _hasUnlockedExperiencedAchievement;
public TimeSince TimeSinceInputMove { get; set; }
private bool _hasUnlockedNoMoveAchievement;
private List<CamShakeData> _camShakeDatas = new();
public float CamShakeAmount { get; set; }
public int ChoiceHash { get; set; }
public TimeSince TimeSinceHurtLava { get; set; }
public RealTimeSince RealTimeSinceDeath { get; set; }
private float _arrowDeathAlphaStart;
private bool _hasPlayedDeathSfx;
protected override void OnAwake()
{
base.OnAwake();
Scale = 1f;
ShadowOpacity = 0.8f;
ShadowScale = 1.12f;
Statuses = new Dictionary<int, Status>();
LevelUpChoices = new List<Status>();
InitializeStats();
if ( IsProxy )
return;
CollideWith.Add( typeof( Enemy ) );
CollideWith.Add( typeof( Player ) );
ArrowAimer = ArrowAimerPrefab.Clone( WorldPosition );
ArrowAimer.SetParent( GameObject );
ArrowAimer.NetworkMode = NetworkMode.Never;
ArrowSprite = ArrowAimer.Components.Get<SpriteRendererLayer>();
ArrowSprite.Tint = Color.White.WithAlpha( 0f );
_timeSinceShoot = 999f;
_timeSinceSpawn = 0f;
if ( Manager.Instance.Difficulty < 0 ) Sprite.Sprite = EasySprite;
else if ( Manager.Instance.Difficulty == 1 ) Sprite.Sprite = Difficulty1Sprite;
else if ( Manager.Instance.Difficulty == 2 ) Sprite.Sprite = Difficulty2Sprite;
else if ( Manager.Instance.Difficulty == 3 ) Sprite.Sprite = Difficulty3Sprite;
else if ( Manager.Instance.Difficulty == 4 ) Sprite.Sprite = Difficulty4Sprite;
else if ( Manager.Instance.Difficulty == 5 ) Sprite.Sprite = Difficulty5Sprite;
else if ( Manager.Instance.Difficulty == 6 ) Sprite.Sprite = Difficulty6Sprite;
else if ( Manager.Instance.Difficulty == 7 ) Sprite.Sprite = Difficulty7Sprite;
else if ( Manager.Instance.Difficulty == 8 ) Sprite.Sprite = Difficulty8Sprite;
else if ( Manager.Instance.Difficulty == 9 ) Sprite.Sprite = Difficulty9Sprite;
else if ( Manager.Instance.Difficulty == 10 ) Sprite.Sprite = Difficulty10Sprite;
else if ( Manager.Instance.Difficulty == 11 ) Sprite.Sprite = Difficulty11Sprite;
else if ( Manager.Instance.Difficulty == 12 ) Sprite.Sprite = Difficulty12Sprite;
else if ( Manager.Instance.Difficulty == 13 ) Sprite.Sprite = Difficulty13Sprite;
else if ( Manager.Instance.Difficulty == 14 ) Sprite.Sprite = Difficulty14Sprite;
else if ( Manager.Instance.Difficulty == 15 ) Sprite.Sprite = Difficulty15Sprite;
//Sprite.LocalScale = 0.5f * Globals.SPRITE_SCALE;
}
public void InitializeStats()
{
_original_properties_stat.Clear();
if ( Network.Active )
{
RemoveShieldVfx();
}
else
{
if ( _shieldVfx != null )
{
_shieldVfx.Destroy();
_shieldVfx = null;
}
}
Level = 0;
ExperienceRequired = GetExperienceReqForLevel( Level + 1 );
ExperienceTotal = 0;
ExperienceCurrent = 0;
Stats[PlayerStat.AttackTime] = 0.15f;
AmmoCount = 5;
Stats[PlayerStat.MaxAmmoCount] = AmmoCount;
Stats[PlayerStat.ReloadTime] = 1.5f;
Stats[PlayerStat.ReloadSpeed] = 1f;
Stats[PlayerStat.AttackSpeed] = 1f;
Stats[PlayerStat.BulletDamage] = 5f;
Stats[PlayerStat.BulletForce] = 0.55f;
Stats[PlayerStat.Recoil] = 0f;
Stats[PlayerStat.MoveSpeed] = 1f;
Stats[PlayerStat.NumProjectiles] = 1f;
Stats[PlayerStat.BulletSpread] = 35f;
Stats[PlayerStat.BulletInaccuracy] = 5f;
Stats[PlayerStat.BulletSpeed] = 4.5f;
Stats[PlayerStat.BulletLifetime] = 0.8f;
Stats[PlayerStat.Luck] = 1f;
Stats[PlayerStat.CritChance] = 0.05f;
Stats[PlayerStat.CritMultiplier] = 1.5f;
Stats[PlayerStat.LowHealthDamageMultiplier] = 1f;
Stats[PlayerStat.FullHealthDamageMultiplier] = 1f;
Stats[PlayerStat.ThornsPercent] = 0f;
Stats[PlayerStat.NumDashes] = 1f;
NumDashesAvailable = (int)MathF.Round( Stats[PlayerStat.NumDashes] );
Stats[PlayerStat.DashCooldown] = 3f;
Stats[PlayerStat.DashInvulnTime] = 0.25f;
Stats[PlayerStat.DashStrength] = 3f;
Stats[PlayerStat.BulletNumPiercing] = 0f;
Stats[PlayerStat.BulletNumBouncing] = 0f;
Stats[PlayerStat.DashCharm] = 0f;
Stats[PlayerStat.CharmedEnemyDmgTakenMultiplier] = 1f;
Stats[PlayerStat.CharmedEnemyDmgDealtMultiplier] = 1f;
Stats[PlayerStat.BossArrivalTime] = 15 * 60f;
//Stats[PlayerStat.BossArrivalTime] = (Manager.Instance.Difficulty >= 5 ? 10 : 15) * 60f;
Stats[PlayerStat.RadiusMultiplier] = 1f;
Stats[PlayerStat.SpecialistStatusAmount] = 0f;
Stats[PlayerStat.ZoomAmount] = 0f;
Stats[PlayerStat.MaxFireStacks] = 0f;
Stats[PlayerStat.XpRepel] = 0f;
Stats[PlayerStat.HealthPackAmount] = 20f;
Stats[PlayerStat.MaxBulletSpread] = 0f;
Stats[PlayerStat.ShootRandomDirChance] = 0f;
Stats[PlayerStat.DashRandomDirChance] = 0f;
Stats[PlayerStat.MoveSelfDmg] = 0f;
Stats[PlayerStat.MoveSelfDmgReqDist] = 0f;
Stats[PlayerStat.MoveSelfDmgAmount] = 0f;
Stats[PlayerStat.SelfDmgDistanceMoved] = 0f;
Stats[PlayerStat.IncreasedDmgTaken] = 0f;
Stats[PlayerStat.ReverseControls] = 0f;
Stats[PlayerStat.SelfCritChance] = 0f;
Health = 100f;
Stats[PlayerStat.MaxHp] = 100f;
//Health = 1f;
_regenHpAccumulated = 0f;
_drainHpAccumulated = 0f;
IsDead = false;
Radius = 0.10f;
GridPos = Manager.Instance.GetGridSquareForPos( Position2D );
AimDir = new Vector2( 0f, 1f );
NumRerollAvailable = Manager.Instance.Difficulty < 0 ? 4 : (Manager.Instance.Difficulty >= 3 ? 3 : 2);
Stats[PlayerStat.FireDamage] = 1f;
Stats[PlayerStat.FireLifetime] = 3f;
Stats[PlayerStat.ShootFireIgniteChance] = 0f;
Stats[PlayerStat.FireSpreadChance] = 0f;
Stats[PlayerStat.ShootFreezeChance] = 0f;
Stats[PlayerStat.FreezeLifetime] = 4f;
Stats[PlayerStat.FreezeTimeScale] = 0.55f;
Stats[PlayerStat.FreezeOnMeleeChance] = 0f;
Stats[PlayerStat.FreezeFireDamageMultiplier] = 1f;
Stats[PlayerStat.FearLifetime] = 4.5f;
Stats[PlayerStat.FearDamageMultiplier] = 1f;
Stats[PlayerStat.FearOnMeleeChance] = 0f;
Stats[PlayerStat.CoinAttractRange] = 1.7f;
Stats[PlayerStat.CoinAttractStrength] = 3.5f;
Stats[PlayerStat.NumUpgradeChoices] = 3f;
Stats[PlayerStat.HealthRegen] = Manager.Instance.Difficulty == -1 ? 0.4f : 0f;
Stats[PlayerStat.HealthRegenStill] = 0f;
//Stats[PlayerStat.HealthDrain] = 0f;
Stats[PlayerStat.DamageReductionPercent] = 0f;
Stats[PlayerStat.PushStrength] = 30f;
Stats[PlayerStat.LastAmmoDamageMultiplier] = 1f;
Stats[PlayerStat.BulletDamageGrow] = 0f;
Stats[PlayerStat.BulletDamageShrink] = 0f;
Stats[PlayerStat.BulletDistanceDamage] = 0f;
Stats[PlayerStat.NumRerollsPerLevel] = 1f;
Stats[PlayerStat.DamagePerEarlierShot] = 0f;
Stats[PlayerStat.DamageForSpeed] = 0f;
Stats[PlayerStat.OverallDamageMultiplier] = 1f;
Stats[PlayerStat.ExplosionSizeMultiplier] = 1f;
Stats[PlayerStat.GrenadeVelocity] = 8f;
Stats[PlayerStat.ExplosionDamageMultiplier] = 1f;
Stats[PlayerStat.BulletDamageMultiplier] = 1f;
Stats[PlayerStat.ExplosionDamageReductionPercent] = 0f;
Stats[PlayerStat.NonExplosionDamageIncreasePercent] = 0f;
Stats[PlayerStat.GrenadeStickyPercent] = 0f;
Stats[PlayerStat.GrenadeFearChance] = 0f;
Stats[PlayerStat.FearDrainPercent] = 0f;
Stats[PlayerStat.FearPainPercent] = 0f;
Stats[PlayerStat.CrateChanceAdditional] = 0f;
Stats[PlayerStat.AttackSpeedStill] = 1f;
Stats[PlayerStat.FearDropGrenadeChance] = 0f;
Stats[PlayerStat.FrozenShardsNum] = 0f;
Stats[PlayerStat.NoDashInvuln] = 0f;
Stats[PlayerStat.BulletFlatDamageAddition] = 0f;
Stats[PlayerStat.GrenadesCanCrit] = 0f;
Stats[PlayerStat.BulletHealTeammateAmount] = 0f;
Stats[PlayerStat.HomingBulletChance] = 0f;
Stats[PlayerStat.PauseWhileChoosing] = 0f;
Statuses.Clear();
_modifiers_stat.Clear();
_isFlashing = false;
Sprite.FlashTint = Color.White.WithAlpha( 0f );
Sprite.Tint = Color.White;
IsChoosingLevelUpReward = false;
IsDashing = false;
IsReloading = true;
Timer = Stats[PlayerStat.ReloadTime];
ReloadProgress = 0f;
DashProgress = 0f;
DashRechargeProgress = 1f;
TempWeight = 0f;
_shotNum = 0;
TimeSinceHurt = 999f;
TimeSinceChangeHP = 999f;
TimeSinceHurtLava = 999f;
ShadowOpacity = 0.8f;
ShadowSpriteDirty = true;
_timeSinceTouchLeftSide = 999f;
TimeSinceInputMove = 0f;
TimeSinceLevelUp = 999f;
RealTimeSinceChoseUpgrade = 999f;
_camShakeDatas.Clear();
ChoiceHash = 0;
}
public Vector2 AverageVelocity { get; private set; }
protected override void OnUpdate()
{
base.OnUpdate();
AverageVelocity += Velocity * Time.Delta * 8f;
//AverageVelocity = Utils.DynamicEaseTo( AverageVelocity, Vector2.Zero, 0.02f, Time.Delta );
AverageVelocity *= (1f - Time.Delta * 2.7f);
//Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );
//Gizmo.Draw.Line( Position2D, Position2D + AverageVelocity );
//Gizmo.Draw.Color = Color.Red.WithAlpha( 0.5f );
//Gizmo.Draw.Line( Position2D, Position2D + AverageVelocity );
Vector2 anchor = Position2D + AverageVelocity * 0.1f;
Vector2 perp = Utils.GetPerpendicularVector( AverageVelocity ).Normal;
var perpA = anchor - perp * 10f;
var perpB = anchor + perp * 10f;
//Gizmo.Draw.Color = Color.Green.WithAlpha( 0.3f );
//Gizmo.Draw.Line( perpA, perpB );
//Gizmo.Draw.Color = Color.White.WithAlpha( 0.03f );
//Gizmo.Draw.LineSphere( (Vector3)Position2D, 2, 16 );
//Gizmo.Draw.Color = Color.White.WithAlpha( 0.5f );
//Gizmo.Draw.Text( $"{Stats[PlayerStat.FearPainPercent]}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );
if ( !_doneFirstUpdate )
{
SpawnShadow( ShadowScale, ShadowOpacity );
Manager.Instance.Camera2D.SetPos( Position2D );
_doneFirstUpdate = true;
}
InputVector = new Vector2( -Input.AnalogMove.y, Input.AnalogMove.x );
if ( Stats[PlayerStat.ReverseControls] > 0f )
InputVector *= -1f;
//if ( Input.Pressed( "Menu" ) )
//{
// Manager.Instance.Restart();
// return;
//}
HandleCamShaking();
if ( IsDead )
{
Sprite.FlashTint = RealTimeSinceDeath < 0.1f
? Color.Red
: Color.White.WithAlpha( 0f );
ShadowOpacity = Utils.Map( RealTimeSinceDeath, 0f, Manager.FINAL_PANEL_WAIT_TIME, 0.8f, 0f, EasingType.Linear );
ShadowSprite.Tint = Color.Black.WithAlpha( ShadowOpacity );
ArrowSprite.Tint = Color.White.WithAlpha( Utils.Map( RealTimeSinceDeath, 0f, 0.4f, _arrowDeathAlphaStart, 0f, EasingType.SineOut ) );
if ( !_hasPlayedDeathSfx && RealTimeSinceDeath > 0.7f )
{
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Game.Random.Float( 0.55f, 0.65f ), volume: 1f, maxDist: 5.5f );
_hasPlayedDeathSfx = true;
}
}
if ( !Manager.Instance.ShouldUpdatePlayer )
return;
float dt = Time.Delta;
if ( MathF.Abs( Velocity.x ) > 0.01f )
Sprite.SpriteFlags = Velocity.x > 0f ? SpriteFlags.HorizontalFlip : SpriteFlags.None;
bool hurting = TimeSinceHurt < 0.25f;
bool attacking = !IsReloading;
bool moving = Velocity.LengthSquared > 0.01f && InputVector.LengthSquared > 0.1f;
string stateStr = "";
if ( IsDead )
stateStr = "ghost_";
else if ( hurting && attacking )
stateStr = "hurt_attack_";
else if ( hurting )
stateStr = "hurt_";
else if ( attacking )
stateStr = "attack_";
Sprite.PlayAnimation( $"{stateStr}{(moving ? "walk" : "idle")}" );
Sprite.PlaybackSpeed = moving ? Utils.Map( Velocity.Length, 0f, 2f, 1.5f, 2f ) : 0.66f;
Sprite.LocalRotation = new Angles( 0f, -90f + (Velocity.Length * Utils.FastSin( Time.Now * MathF.PI * 6f ) * 1.6f) * (Sprite.SpriteFlags.HasFlag( SpriteFlags.HorizontalFlip ) ? -1f : 1f), 0f );
if ( !IsDead )
{
HandleFlashing( dt );
}
if ( IsProxy )
return;
// ACHIEVEMENTS
if ( Manager.Instance.Difficulty >= 0 )
{
if ( _lastPos2D.x < -15.7f && WorldPosition.x >= -15.7f )
{
_timeSinceTouchLeftSide = 0f;
}
if ( _lastPos2D.x < 15.7f && WorldPosition.x >= 15.7f )
{
Log.Info( $"_timeSinceTouchLeftSide: {_timeSinceTouchLeftSide}" );
if ( _timeSinceTouchLeftSide < 5.5f )
{
if ( !_hasUnlockedSprinterAchievement )
Sandbox.Services.Achievements.Unlock( "sprinter" );
_hasUnlockedSprinterAchievement = true;
}
}
if ( !_hasUnlockedNoMoveAchievement )
{
if ( IsInputtingMove )
{
TimeSinceInputMove = 0f;
}
else if ( TimeSinceInputMove > 60f * 10f )
{
Sandbox.Services.Achievements.Unlock( "stand_ground" );
_hasUnlockedNoMoveAchievement = true;
}
}
}
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"ArrowAimer: {ArrowAimer}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );
//Gizmo.Draw.ScreenText( $"_timeSinceTouchLeftSide: {_timeSinceTouchLeftSide}", new Vector2( 50, 50 ) );
_lastPos2D = Position2D;
var velocity = Velocity + (IsDashing ? DashVelocity : Vector2.Zero);
Position2D += velocity * dt;
if ( Stats[PlayerStat.MoveSelfDmg] > 0f )
{
Stats[PlayerStat.SelfDmgDistanceMoved] += velocity.Length * dt;
if ( !IsInvulnerable && Stats[PlayerStat.SelfDmgDistanceMoved] > Stats[PlayerStat.MoveSelfDmgReqDist] )
{
Stats[PlayerStat.SelfDmgDistanceMoved] -= Stats[PlayerStat.MoveSelfDmgReqDist];
Damage( Stats[PlayerStat.MoveSelfDmgAmount], PlayerDamageType.Self );
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Game.Random.Float( 1.25f, 1.45f ), volume: 0.9f, maxDist: 3f );
}
}
WorldPosition = WorldPosition.WithZ( Globals.GetZPos( Position2D.y ) );
//Velocity = Utils.DynamicEaseTo( Velocity, Vector2.Zero, 0.2f, dt );
Velocity *= Math.Max( 1f - dt * 12.9f, 0f );
if ( Velocity.LengthSquared < 0.001f )
Velocity = Vector2.Zero;
TempWeight *= (1f - dt * 4.7f);
if ( InputVector.LengthSquared > 0f )
{
Velocity += InputVector.Normal * Stats[PlayerStat.MoveSpeed] * BASE_MOVE_SPEED * dt;
//Log.Info( $"dt: {dt}" );
}
HandleBounds();
Manager.Instance.Camera2D.TargetPos = Position2D;
if ( Input.UsingController )
{
}
else
{
AimDir = (Manager.Instance.MouseWorldPos - (Position2D + new Vector2( 0f, 0.5f ))).Normal;
if ( Stats[PlayerStat.ReverseControls] > 0f )
AimDir *= -1f;
}
if ( ArrowAimer != null && !Manager.Instance.IsPauseMenuOpen && !IsTimePausedForChoosing )
{
ArrowAimer.LocalRotation = new Angles( 0f, MathF.Atan2( AimDir.y, AimDir.x ) * (180f / MathF.PI) - 180f, 0f );
ArrowAimer.LocalPosition = new Vector2( 0f, 0.4f ) + AimDir * Utils.Map( _timeSinceShoot, 0f, 0.25f, 0.6f, 0.55f, EasingType.QuadOut );
ArrowAimer.LocalScale = new Vector3( Utils.Map( _timeSinceShoot, 0f, 0.25f, 1.25f, 0.75f, EasingType.QuadOut ), 1f, 1f ) * 0.005f;
ArrowSprite.Tint = Color.White.WithAlpha( Utils.Map( _timeSinceShoot, 0f, 0.3f, 1f, 0.3f, EasingType.QuadOut ) * Utils.Map( _timeSinceSpawn, 0f, 1f, 0f, 1f, EasingType.Linear ) );
}
for ( int dx = -1; dx <= 1; dx++ )
{
for ( int dy = -1; dy <= 1; dy++ )
{
Manager.Instance.HandleThingCollisionForGridSquare( this, new GridSquare( GridPos.x + dx, GridPos.y + dy ), dt );
}
}
if ( !IsDead )
{
HandleDashing( dt );
HandleStatuses( dt );
HandleShooting( dt );
HandleRegen( dt );
}
if ( IsChoosingLevelUpReward && !Manager.Instance.IsPauseMenuOpen )
{
if ( Input.Pressed( "reload" ) ) UseReroll();
else if ( Input.Pressed( "Slot1" ) ) UseChoiceHotkey( 1 );
else if ( Input.Pressed( "Slot2" ) ) UseChoiceHotkey( 2 );
else if ( Input.Pressed( "Slot3" ) ) UseChoiceHotkey( 3 );
else if ( Input.Pressed( "Slot4" ) ) UseChoiceHotkey( 4 );
else if ( Input.Pressed( "Slot5" ) ) UseChoiceHotkey( 5 );
else if ( Input.Pressed( "Slot6" ) ) UseChoiceHotkey( 6 );
}
if ( Input.Pressed( "use" ) )
{
//AddExperience( 10 );
}
}
void HandleRegen( float dt )
{
if ( Math.Abs( Stats[PlayerStat.HealthRegen] ) > 0f )
RegenHealth( Stats[PlayerStat.HealthRegen] * dt );
//if ( Math.Abs( Stats[PlayerStat.HealthDrain] ) > 0f )
// RegenHealth( Stats[PlayerStat.HealthDrain] * dt );
if ( Stats[PlayerStat.HealthRegenStill] > 0f && !IsMoving )
{
RegenHealth( Stats[PlayerStat.HealthRegenStill] * dt );
if ( !IsDashing && Health < Stats[PlayerStat.MaxHp] )
TimeSinceChangeHP = 0f;
}
}
public void RegenHealth( float amount )
{
float maxHp = Stats[PlayerStat.MaxHp];
float hpMissing = maxHp - Health;
if ( amount > 0f )
{
if ( hpMissing <= 0f )
return;
_regenHpAccumulated += amount;
if ( _regenHpAccumulated < 0.85f && hpMissing > _regenHpAccumulated )
return;
float hpRecovered = Math.Min( _regenHpAccumulated, hpMissing );
Health += hpRecovered;
//var particle = DamageNumbersLegacy.Create( hpRecovered, Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: Color.Green, sizeMultiplier: 0.8f );
//Vector3 velocity = new Vector3( Game.Random.Float(-0.5f, 0.5f), 0f, 0f );
//Vector3 gravity = new Vector3( 0f, 1.5f, 0f );
//particle.SetVector( 1, velocity );
//particle.SetNamedValue( "Gravity", gravity );
var pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );
float size = 1.1f;
Manager.Instance.SpawnDamageNumber( pos, amount, Color.Green, size, FloaterType.Heal );
_regenHpAccumulated = 0f;
}
else
{
_drainHpAccumulated += amount;
if ( _drainHpAccumulated < -1f )
{
var dmgAmount = MathF.Truncate( _drainHpAccumulated );
_drainHpAccumulated -= dmgAmount;
Damage( MathF.Abs( dmgAmount ), PlayerDamageType.Self );
Manager.Instance.PlaySfxNearby( "lava_puddle_03", Position2D, pitch: Game.Random.Float( 1.7f, 1.75f ), volume: 0.15f, maxDist: 4f );
}
}
}
void HandleDashing( float dt )
{
int numDashes = (int)MathF.Round( Stats[PlayerStat.NumDashes] );
if ( NumDashesAvailable < numDashes )
{
DashTimer -= dt;
DashRechargeProgress = Utils.Map( DashTimer, Stats[PlayerStat.DashCooldown], 0f, 0f, 1f );
if ( DashTimer <= 0f )
{
DashRecharged();
}
}
if ( DashInvulnTimer > 0f )
{
DashInvulnTimer -= dt;
DashProgress = Utils.Map( DashInvulnTimer, Stats[PlayerStat.DashInvulnTime], 0f, 0f, 1f );
if ( DashInvulnTimer <= 0f )
{
IsDashing = false;
//Sprite.Tint = Color.White;
Sprite.FlashTint = Color.White.WithAlpha( 0f );
DashFinished();
}
else
{
if ( Stats[PlayerStat.DashCharm] > 0f )
{
if ( IsInvulnerable )
Sprite.FlashTint = new Color( Game.Random.Float( 0.8f, 1f ), Game.Random.Float( 0f, 0.1f ), Game.Random.Float( 0.8f, 1f ), 0.9f );
else if ( !_isFlashing )
Sprite.FlashTint = new Color( Game.Random.Float( 0.9f, 1f ), Game.Random.Float( 0.1f, 0.3f ), Game.Random.Float( 0.9f, 1f ), 0.8f );
}
else
{
if ( IsInvulnerable )
Sprite.FlashTint = new Color( Game.Random.Float( 0f, 0.25f ), Game.Random.Float( 0f, 0.25f ), Game.Random.Float( 0.8f, 1f ), 0.9f );
}
if ( _dashCloudTime > Game.Random.Float( 0.1f, 0.2f ) )
{
SpawnDashCloudClient();
_dashCloudTime = 0f;
}
}
}
if ( Input.Pressed( "Jump" ) || Input.Pressed( "attack1" ) )
{
//Position2D = Manager.Instance.MouseWorldPos;
////Manager.Instance.Camera2D.SetPos( Position2D );
//Transform.ClearInterpolation();
//return;
Dash();
}
}
public void Dash()
{
if ( NumDashesAvailable <= 0 || IsTimePausedForChoosing )
return;
Vector2 dashDir = Velocity.LengthSquared > 0f ? Velocity.Normal : AimDir;
if ( Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.DashRandomDirChance] )
dashDir = Utils.GetRandomVector();
DashVelocity = dashDir * Stats[PlayerStat.DashStrength];
TempWeight = 2f;
if ( NumDashesAvailable == (int)Stats[PlayerStat.NumDashes] )
DashTimer = Stats[PlayerStat.DashCooldown];
NumDashesAvailable--;
IsDashing = true;
DashInvulnTimer = Stats[PlayerStat.DashInvulnTime];
DashProgress = 0f;
DashRechargeProgress = 0f;
Manager.Instance.PlaySfxNearby( "player.dash", Position2D + dashDir * 0.5f, pitch: Utils.Map( NumDashesAvailable, 0, 5, 1f, 0.9f ), volume: 1f, maxDist: 4f );
SpawnDashCloudClient();
_dashCloudTime = 0f;
ForEachStatus( status => status.OnDashStarted() );
}
public void DashFinished()
{
ForEachStatus( status => status.OnDashFinished() );
}
public void DashRecharged()
{
NumDashesAvailable++;
var numDashes = (int)MathF.Round( Stats[PlayerStat.NumDashes] );
if ( NumDashesAvailable > numDashes )
NumDashesAvailable = numDashes;
if ( NumDashesAvailable < numDashes )
{
DashTimer = Stats[PlayerStat.DashCooldown];
DashRechargeProgress = 0f;
}
else
{
DashRechargeProgress = 1f;
}
ForEachStatus( status => status.OnDashRecharged() );
Manager.Instance.PlaySfxNearby( "player.dash.recharge", Position2D, pitch: Utils.Map( NumDashesAvailable, 1, numDashes, 1f, 1.2f ), volume: 0.2f, maxDist: 5f );
}
void HandleBounds()
{
var x_min = Manager.Instance.BOUNDS_MIN.x + Radius;
var x_max = Manager.Instance.BOUNDS_MAX.x - Radius;
var y_min = Manager.Instance.BOUNDS_MIN.y;
var y_max = Manager.Instance.BOUNDS_MAX.y - Radius;
if ( Position2D.x < x_min )
Position2D = new Vector2( x_min, Position2D.y );
else if ( Position2D.x > x_max )
Position2D = new Vector2( x_max, Position2D.y );
if ( Position2D.y < y_min )
Position2D = new Vector2( Position2D.x, y_min );
else if ( Position2D.y > y_max )
Position2D = new Vector2( Position2D.x, y_max );
}
public int GetExperienceReqForLevel( int level )
{
switch ( Manager.Instance.Difficulty )
{
case -1:
return (int)MathF.Round( Utils.Map( level, 1, 150, 3f, 240f, EasingType.SineIn ) );
case 0:
default:
return (int)MathF.Round( Utils.Map( level, 1, 150, 3f, 320f, EasingType.SineIn ) );
}
}
public void Flash( float time )
{
if ( _isFlashing )
return;
//Sprite.Tint = new Color( 1f, 0f, 0f );
Sprite.FlashTint = new Color( 1f, 0f, 0f, 1f );
_isFlashing = true;
_flashTimer = time;
}
public void Heal( float amount, float flashTime )
{
//Sprite.Tint = new Color( 0f, 1f, 0f );
Sprite.FlashTint = new Color( 0f, 1f, 0f, 1f );
_isFlashing = true;
_flashTimer = flashTime;
if ( IsProxy )
return;
if ( Health < Stats[PlayerStat.MaxHp] )
TimeSinceChangeHP = 0f;
Health += amount;
if ( Health > Stats[PlayerStat.MaxHp] )
Health = Stats[PlayerStat.MaxHp];
}
void HandleFlashing( float dt )
{
if ( _isFlashing )
{
_flashTimer -= dt;
if ( _flashTimer < 0f )
{
_isFlashing = false;
//Sprite.Tint = Color.White;
Sprite.FlashTint = Color.White.WithAlpha( 0f );
}
}
}
[ConCmd( "give_status" )]
public static void GiveStatus( string name )
{
// Cheat only works in the editor
if ( !Game.IsEditor )
return;
var type = TypeLibrary.GetType( name );
if ( type == null )
{
Log.Info( $"No status with name '{name}' found!" );
return;
}
Manager.Instance?.GetLocalPlayer()?.AddStatus( type );
}
public void AddStatus( TypeDescription type )
{
Status status = null;
var typeIdentity = type.Identity;
ForEachStatus( status => status.OnAddStatus( typeIdentity ) );
if ( Statuses.ContainsKey( typeIdentity ) )
{
status = Statuses[typeIdentity];
status.Level++;
}
if ( status == null )
{
status = StatusManager.CreateStatus( type );
Statuses.Add( typeIdentity, status );
status.Init( this );
}
//Sandbox.Services.Stats.Increment( Client, "status", 1, $"{type.Name.ToLowerInvariant()}", new { Status = type.Name.ToLowerInvariant(), Level = status.Level } );
status.Refresh();
Manager.Instance.PlaySfxNearby( "click", Position2D, 0.9f, 0.75f, 5f );
LevelUpChoices.Clear();
IsChoosingLevelUpReward = false;
RealTimeSinceChoseUpgrade = 0f;
CheckForLevelUp();
}
public bool HasStatus( TypeDescription type )
{
return Statuses.ContainsKey( type.Identity );
}
public Status GetStatus( TypeDescription type )
{
if ( Statuses.ContainsKey( type.Identity ) )
return Statuses[type.Identity];
return null;
}
public int GetStatusLevel( TypeDescription type )
{
if ( Statuses.ContainsKey( type.Identity ) )
return Statuses[type.Identity].Level;
return 0;
}
public void Modify( Status caller, PlayerStat statType, float value, ModifierType type, float priority = 0f, bool update = true )
{
if ( !_modifiers_stat.ContainsKey( caller ) )
_modifiers_stat.Add( caller, new Dictionary<PlayerStat, ModifierData>() );
_modifiers_stat[caller][statType] = new ModifierData( value, type, priority );
if ( update )
UpdateProperty( statType );
}
public void AdjustBaseStat( PlayerStat statType, float amount, bool update = true )
{
if ( !_original_properties_stat.ContainsKey( statType ) )
_original_properties_stat.Add( statType, Stats[statType] );
_original_properties_stat[statType] += amount;
if ( update )
UpdateProperty( statType );
}
void UpdateProperty( PlayerStat statType )
{
if ( !_original_properties_stat.ContainsKey( statType ) )
{
_original_properties_stat.Add( statType, Stats[statType] );
}
float curr_value = _original_properties_stat[statType];
float curr_set = curr_value;
bool should_set = false;
float curr_priority = 0f;
float total_add = 0f;
float total_mult = 1f;
foreach ( Status caller in _modifiers_stat.Keys )
{
var dict = _modifiers_stat[caller];
if ( dict.ContainsKey( statType ) )
{
var mod_data = dict[statType];
switch ( mod_data.type )
{
case ModifierType.Set:
if ( mod_data.priority >= curr_priority )
{
curr_set = mod_data.value;
curr_priority = mod_data.priority;
should_set = true;
}
break;
case ModifierType.Add:
total_add += mod_data.value;
break;
case ModifierType.Mult:
total_mult *= mod_data.value;
break;
}
}
}
if ( should_set )
curr_value = curr_set;
curr_value += total_add;
curr_value *= total_mult;
Stats[statType] = curr_value;
if ( statType == PlayerStat.MaxHp )
Stats[statType] = Math.Max( curr_value, 1f );
}
public void AddExperience( int xp )
{
ExperienceTotal += xp;
ExperienceCurrent += xp;
//var particle = DamageNumbersLegacy.Create( xp, Position2D + new Vector2( 0.2f + Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) ), color: new Color(0.1f, 0.1f, 1f), sizeMultiplier: 0.8f );
//Vector3 velocity = new Vector3( 0f, 0f, 0f );
//Vector3 gravity = new Vector3( 0f, 1f, 0f );
//particle.SetVector( 1, velocity );
//particle.SetNamedValue( "Gravity", gravity );
var pos = Position2D + new Vector2( Game.Random.Float( -0.1f, 0.1f ), Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) );
float size = Utils.Map( xp, 1f, 4f, 0.95f, 1.1f, EasingType.Linear );
var color = new Color( 0.4f, 0.4f, 1f );
Manager.Instance.SpawnDamageNumber( pos, xp, color, size, FloaterType.Xp );
ForEachStatus( status => status.OnGainExperience( xp ) );
if ( !IsChoosingLevelUpReward )
CheckForLevelUp();
}
public void LoseExperience( int amount )
{
ExperienceCurrent = Math.Max( ExperienceCurrent - amount, 0 );
}
public void CheckForLevelUp()
{
//Log.Info("CheckForLevelUp: " + ExperienceCurrent + " / " + ExperienceRequired + " IsServer: " + Sandbox.Game.IsServer + " Level: " + Level);
if ( ExperienceCurrent >= ExperienceRequired && Manager.Instance.ShouldUpdatePlayer )
LevelUp();
}
public void LevelUp()
{
ExperienceCurrent -= ExperienceRequired;
Level++;
ExperienceRequired = GetExperienceReqForLevel( Level + 1 );
if ( Manager.Instance.Difficulty < 3 )
NumRerollAvailable += (int)Stats[PlayerStat.NumRerollsPerLevel];
Manager.Instance.PlaySfxNearby( "levelup", Position2D, Game.Random.Float( 0.95f, 1.05f ), 0.5f, 5f );
ForEachStatus( status => status.OnLevelUp() );
GenerateLevelUpChoices();
IsChoosingLevelUpReward = true;
TimeSinceLevelUp = 0f;
if ( Manager.Instance.Difficulty >= 0 && !_hasUnlockedExperiencedAchievement && Level >= 85 )
{
Sandbox.Services.Achievements.Unlock( "experienced" );
_hasUnlockedExperiencedAchievement = true;
}
}
public void UseReroll()
{
if ( NumRerollAvailable <= 0 )
{
// todo: sfx
return;
}
Manager.Instance.PlaySfxNearby( "reroll", Position2D, Utils.Map( NumRerollAvailable, 0, 20, 0.9f, 1.4f, EasingType.QuadIn ), 0.6f, 5f );
NumRerollAvailable--;
GenerateLevelUpChoices();
ForEachStatus( status => status.OnReroll() );
}
public void UseChoiceHotkey( int num )
{
var index = num - 1;
if ( !IsChoosingLevelUpReward || index >= LevelUpChoices.Count )
return;
AddStatus( TypeLibrary.GetType( LevelUpChoices[index].GetType() ) );
}
public float CheckDamageAmount( float damage, DamageType damageType )
{
if ( IsInvulnerable )
{
return 0f;
}
if ( HasStatus( TypeLibrary.GetType( typeof( ShieldStatus ) ) ) && damageType != DamageType.LavaPuddle )
{
var shieldStatus = GetStatus( TypeLibrary.GetType( typeof( ShieldStatus ) ) ) as ShieldStatus;
if ( shieldStatus != null && shieldStatus.IsShielded )
{
shieldStatus.LoseShield();
return 0f;
}
}
if ( Stats[PlayerStat.DamageReductionPercent] > 0f )
damage *= (1f - MathX.Clamp( Stats[PlayerStat.DamageReductionPercent], 0f, 1f ));
if ( Stats[PlayerStat.IncreasedDmgTaken] > 0f )
damage *= (1f + MathX.Clamp( Stats[PlayerStat.IncreasedDmgTaken], 0f, 1f ));
if ( damageType == DamageType.Explosion && Stats[PlayerStat.ExplosionDamageReductionPercent] > 0f )
damage *= (1f - MathX.Clamp( Stats[PlayerStat.ExplosionDamageReductionPercent], 0f, 1f ));
if ( damageType != DamageType.Explosion && Stats[PlayerStat.NonExplosionDamageIncreasePercent] > 0f )
damage *= (1f + Stats[PlayerStat.NonExplosionDamageIncreasePercent]);
if ( Manager.Instance.Difficulty < 0 )
damage *= 0.571429f; // so zombie's 7 dmg becomes 4 dmg
return damage;
}
public void Damage( float damage, PlayerDamageType playerDamageType = PlayerDamageType.Enemy )
{
if ( !Manager.Instance.ShouldUpdatePlayer )
return;
TimeSinceHurt = 0f;
bool isCrit = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.SelfCritChance];
if ( isCrit )
damage *= 2f;
if ( damage > 0f )
{
TimeSinceChangeHP = 0f;
Flash( 0.125f );
}
var offset = new Vector2(
Game.Random.Float( -0.1f, 0.1f ),
Radius * 3f + Game.Random.Float( -0.2f, 0.3f ) + (Health - damage < 0f ? -0.7f : 0f)
);
//DamageNumbers.Add( (int)damage, Position2D + Vector2.Up * Radius * 3f + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.2f, color: Color.Red );
//DamageNumbersLegacy.Create( damage, Position2D + offset, color: isCrit ? Color.Orange : Color.Red );
var pos = Position2D + offset;
float size;
if ( damage < 5f ) size = Utils.Map( damage, 1f, 5f, 1.1f, 1.25f, EasingType.QuadOut );
else if ( damage < 20f ) size = Utils.Map( damage, 5f, 20f, 1.25f, 1.6f, EasingType.Linear );
else size = Utils.Map( damage, 20f, 100f, 1.6f, 1.8f, EasingType.Linear );
var color = isCrit ? Color.Orange : Color.Red;
Manager.Instance.SpawnDamageNumber( pos, damage, color, size );
ForEachStatus( status => status.OnHurt( damage ) );
Health -= damage;
//if ( damage > 3f )
// ShakeCam( Utils.Map( damage, 3f, 25f, 0f, 0.1f ), Utils.Map( damage, 3f, 20f, 0.1f, 0.25f ), EasingType.QuadOut );
if ( Health <= 0f )
{
Die();
SpawnBlood( damage, sizeMultiplier: Game.Random.Float( 2.5f, 3f ), playbackSpeed: Game.Random.Float( 20f, 25f ), shouldUseRealTime: true );
}
else
{
SpawnBlood( damage );
}
}
public void AddVelocity( Vector2 vel )
{
if ( !Manager.Instance.ShouldUpdatePlayer )
return;
Velocity += vel;
}
public void SpawnBlood( float damage, float sizeMultiplier = 1f, float playbackSpeed = 0f, bool shouldUseRealTime = false )
{
var blood = Manager.Instance.SpawnBloodSplatter( Position2D );
if ( blood != null )
{
blood.LocalScale *= Utils.Map( damage, 1f, 20f, 0.5f, 1.2f, EasingType.QuadIn ) * Game.Random.Float( 0.8f, 1.2f ) * sizeMultiplier;
blood.Lifetime *= 0.3f;
blood.ShouldUseRealTime = shouldUseRealTime;
if ( playbackSpeed > 0f )
blood.Sprite.PlaybackSpeed = playbackSpeed;
}
}
public void Die()
{
if ( IsDead )
return;
IsDead = true;
_hasPlayedDeathSfx = false;
Sprite.Tint = new Color( 1f, 1f, 1f, 1f );
Sprite.FlashTint = new Color( 1f, 1f, 1f, 0f );
//ShadowOpacity = 0.2f;
_isFlashing = false;
IsReloading = false;
RealTimeSinceDeath = 0f;
_arrowDeathAlphaStart = ArrowSprite.Tint.a;
var pitch = Game.Random.Float( 1.25f, 1.3f ) * (Manager.Instance.Difficulty < 0 ? 2f : 1f);
Manager.Instance.PlaySfxNearby( "die", Position2D, pitch, volume: 1.5f, maxDist: 12f );
Sprite.LocalScale *= 2f;
Sprite.PlayAnimation( $"death" );
//Sprite.PlayAnimation( "ghost_idle" );
ShakeCam( Game.Random.Float( 0.025f, 0.065f ), Game.Random.Float( 0.3f, 0.5f ), EasingType.SineOut, useRealTime: true );
if ( IsProxy )
return;
Manager.Instance.PlayerDied( this );
}
public void Revive()
{
if ( !IsDead )
return;
IsDead = false;
IsChoosingLevelUpReward = false;
IsDashing = false;
IsReloading = true;
Sprite.Tint = Color.White;
ShadowOpacity = 0.8f;
if ( IsProxy )
return;
Timer = Stats[PlayerStat.ReloadTime];
ReloadProgress = 0f;
DashProgress = 0f;
ExperienceCurrent = 0;
Health = Stats[PlayerStat.MaxHp] * 0.33f;
}
public void ForEachStatus( Action<Status> action )
{
if ( IsProxy )
return;
foreach ( var (_, status) in Statuses )
{
action( status );
}
}
void HandleStatuses( float dt )
{
foreach ( KeyValuePair<int, Status> pair in Statuses )
{
Status status = pair.Value;
if ( status.ShouldUpdate )
status.Update( dt );
}
}
void HandleShooting( float dt )
{
if ( IsReloading )
{
ReloadProgress = Utils.Map( Timer, Stats[PlayerStat.ReloadTime], 0f, 0f, 1f );
Timer -= dt * Stats[PlayerStat.ReloadSpeed];
if ( Timer <= 0f )
{
Reload();
}
}
else
{
Timer -= dt * Stats[PlayerStat.AttackSpeed] * (IsMoving ? 1f : Stats[PlayerStat.AttackSpeedStill]);
if ( Timer <= 0f )
{
Shoot( isLastAmmo: AmmoCount == 1 );
AmmoCount--;
if ( AmmoCount <= 0 )
{
IsReloading = true;
Timer += Stats[PlayerStat.ReloadTime];
}
else
{
Timer += Stats[PlayerStat.AttackTime];
}
}
}
}
public void Shoot( bool isLastAmmo = false )
{
int num_bullets_int = (int)Stats[PlayerStat.NumProjectiles];
var aimDir = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootRandomDirChance]
? Utils.GetRandomVector()
: AimDir;
var pos = Position2D + AimDir * 0.3f;
if ( Stats[PlayerStat.MaxBulletSpread] > 0f )
{
float increment = 360f / num_bullets_int;
for ( int i = 0; i < num_bullets_int; i++ )
{
var dir = Utils.RotateVector( aimDir, i * increment );
SpawnBullet( pos, dir, isLastAmmo );
}
}
else
{
float start_angle = MathF.Sin( -_shotNum * 2f ) * Stats[PlayerStat.BulletInaccuracy];
var spread = Stats[PlayerStat.BulletSpread] * num_bullets_int;
float currAngleOffset = num_bullets_int == 1 ? 0f : -spread * 0.5f;
float increment = num_bullets_int == 1 ? 0f : spread / (float)(num_bullets_int - 1);
for ( int i = 0; i < num_bullets_int; i++ )
{
var dir = Utils.RotateVector( aimDir, start_angle + currAngleOffset + increment * i );
SpawnBullet( pos, dir, isLastAmmo );
}
}
Manager.Instance.PlaySfxNearby( "shoot", pos, pitch: Utils.Map( _shotNum, 0f, (float)Stats[PlayerStat.MaxAmmoCount], 1f, 1.25f ), volume: 1f, maxDist: 4f );
Velocity -= aimDir * Stats[PlayerStat.Recoil];
_shotNum++;
_timeSinceShoot = 0f;
}
void SpawnBullet( Vector2 pos, Vector2 dir, bool isLastAmmo = false, float damageMult = 1f )
{
var damage = (Stats[PlayerStat.BulletDamage] * Stats[PlayerStat.BulletDamageMultiplier] + Stats[PlayerStat.BulletFlatDamageAddition]) * GetDamageMultiplier() * damageMult;
if ( isLastAmmo )
damage *= Stats[PlayerStat.LastAmmoDamageMultiplier];
if ( Stats[PlayerStat.DamagePerEarlierShot] > 0f )
damage += _shotNum * Stats[PlayerStat.DamagePerEarlierShot];
if ( Stats[PlayerStat.DamageForSpeed] > 0f )
{
damage += Stats[PlayerStat.DamageForSpeed] * Velocity.Length;
if ( IsDashing )
damage += Stats[PlayerStat.DamageForSpeed] * DashVelocity.Length;
}
var bulletObj = BulletPrefab.Clone( (Vector3)pos );
var bullet = bulletObj.Components.Get<Bullet>();
bullet.Velocity = dir * Stats[PlayerStat.BulletSpeed];
bullet.Shooter = this;
bullet.TempWeight = 3f;
bullet.Stats[BulletStat.Damage] = damage;
bullet.Stats[BulletStat.Force] = Stats[PlayerStat.BulletForce];
bullet.Stats[BulletStat.Lifetime] = Stats[PlayerStat.BulletLifetime];
bullet.Stats[BulletStat.NumPiercing] = (int)MathF.Round( Stats[PlayerStat.BulletNumPiercing] );
bullet.Stats[BulletStat.NumBouncing] = (int)MathF.Round( Stats[PlayerStat.BulletNumBouncing] );
bullet.Stats[BulletStat.WillIgnite] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootFireIgniteChance] ? 1f : 0f;
bullet.Stats[BulletStat.WillFreeze] = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.ShootFreezeChance] ? 1f : 0f;
bullet.Stats[BulletStat.GrowDamageAmount] = Stats[PlayerStat.BulletDamageGrow];
bullet.Stats[BulletStat.ShrinkDamageAmount] = Stats[PlayerStat.BulletDamageShrink];
bullet.Stats[BulletStat.DistanceDamageAmount] = Stats[PlayerStat.BulletDistanceDamage];
bullet.Stats[BulletStat.HealTeammateAmount] = Stats[PlayerStat.BulletHealTeammateAmount];
bullet.IsHoming = Game.Random.Float( 0f, 1f ) < Stats[PlayerStat.HomingBulletChance];
if ( Stats[PlayerStat.GrenadesCanCrit] <= 0f )
{
bullet.Stats[BulletStat.CriticalChance] = Stats[PlayerStat.CritChance];
bullet.Stats[BulletStat.CriticalMultiplier] = Stats[PlayerStat.CritMultiplier];
}
bullet.Init();
//bullet.GameObject.NetworkSpawn( Network.Owner );
}
void Reload()
{
AmmoCount = (int)Stats[PlayerStat.MaxAmmoCount];
IsReloading = false;
_shotNum = 0;
ReloadProgress = 0f;
ForEachStatus( status => status.OnReload() );
}
public float GetDamageMultiplier()
{
float damageMultiplier = Stats[PlayerStat.OverallDamageMultiplier];
if ( Stats[PlayerStat.LowHealthDamageMultiplier] > 1f )
damageMultiplier *= Utils.Map( Health, Stats[PlayerStat.MaxHp], 0f, 1f, Stats[PlayerStat.LowHealthDamageMultiplier] );
if ( Stats[PlayerStat.FullHealthDamageMultiplier] > 1f && !(Health < Stats[PlayerStat.MaxHp]) )
damageMultiplier *= Stats[PlayerStat.FullHealthDamageMultiplier];
return damageMultiplier;
}
public override void Colliding( Thing other, float percent, float dt )
{
base.Colliding( other, percent, dt );
if ( IsDead )
return;
ForEachStatus( status => status.Colliding( other, percent, dt ) );
if ( other is Enemy enemy && !enemy.IsDying )
{
if ( !Position2D.Equals( other.Position2D ) )
{
var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
Velocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 100f ) * (1f + other.TempWeight) * spawnFactor * dt;
}
}
else if ( other is Player player )
{
if ( !player.IsDead && !Position2D.Equals( other.Position2D ) )
{
Velocity += (Position2D - other.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 100f ) * (1f + other.TempWeight) * dt;
}
}
}
public void SpawnDashCloudClient()
{
Manager.Instance.SpawnCloud( Position2D + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.05f );
}
public void GenerateLevelUpChoices()
{
LevelUpChoices.Clear();
//if( Level == 1)
//{
// LevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( PauseWhileChoosingStatus ) ) ) );
// LevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( MysteryBoxStatus ) ) ) );
// LevelUpChoices.Add( CreateStatus( GetRandomStartingPerk() ) );
// LevelUpChoices.Shuffle();
// return;
//}
bool offerCurses = false;
if ( Manager.Instance.Difficulty >= 6 )
offerCurses = IsLevelCursed( Level );
int numChoices = Math.Clamp( (int)MathF.Round( Stats[PlayerStat.NumUpgradeChoices] ), 1, 6 );
List<TypeDescription> statusTypes = StatusManager.GetRandomStatuses( this, numChoices, offerCurses );
for ( int i = 0; i < statusTypes.Count; i++ )
LevelUpChoices.Add( CreateStatus( statusTypes[i] ) );
if ( Level == 1 )
{
bool alreadyOffered = false;
foreach ( var status in LevelUpChoices )
{
if ( status is PauseWhileChoosingStatus )
{
alreadyOffered = true;
break;
}
}
if ( !alreadyOffered )
{
LevelUpChoices.RemoveAt( 1 );
LevelUpChoices.Add( CreateStatus( TypeLibrary.GetType( typeof( PauseWhileChoosingStatus ) ) ) );
LevelUpChoices.Shuffle();
}
}
ChoiceHash++;
}
public bool IsLevelCursed( int level )
{
if ( Manager.Instance.Difficulty < 6 || level <= 1 )
return false;
switch ( Manager.Instance.Difficulty )
{
case 6: return level % 10 == 0;
case 7: return level % 9 == 0;
case 8: return level % 8 == 0;
case 9: return level % 7 == 0;
case 10: return level % 6 == 0;
case 11: return level % 5 == 0;
case 12: return level % 4 == 0;
case 13: return level % 3 == 0;
case 14: return level % 2 == 0;
case 15: return level % 3 != 1;
}
return false;
}
TypeDescription GetRandomStartingPerk()
{
List<(TypeDescription Type, float Weight)> perks = new List<(TypeDescription, float)>
{
(TypeLibrary.GetType( typeof( DamageStatus ) ), 5f),
(TypeLibrary.GetType( typeof( MovespeedStatus ) ), 3f),
(TypeLibrary.GetType( typeof( AttackSpeedStatus ) ), 2f),
(TypeLibrary.GetType( typeof( NumProjectileStatus ) ), 2f),
(TypeLibrary.GetType( typeof( PiercingStatus ) ), 3f),
(TypeLibrary.GetType( typeof( GrenadeShootReloadStatus ) ), 1f),
(TypeLibrary.GetType( typeof( NumDashesStatus ) ), 2f),
(TypeLibrary.GetType( typeof( FireIgniteStatus ) ), 2f),
(TypeLibrary.GetType( typeof( FreezeShootStatus ) ), 2f),
(TypeLibrary.GetType( typeof( FullHealthDamageStatus ) ), 2f),
(TypeLibrary.GetType( typeof( MoreRerollsStatus ) ), 4f),
(TypeLibrary.GetType( typeof( MoreChoicesStatus ) ), 3f),
(TypeLibrary.GetType( typeof( ReloadSpeedStatus ) ), 1f),
(TypeLibrary.GetType( typeof( XpDamageStatus ) ), 1f),
(TypeLibrary.GetType( typeof( HomingBulletStatus ) ), 4f),
(TypeLibrary.GetType( typeof( ThornsStatus ) ), 1f),
(TypeLibrary.GetType( typeof( BouncingBulletStatus ) ), 3f),
};
TypeDescription chosenPerk = null;
float totalWeight = perks.Sum( x => x.Weight );
var rand = Game.Random.Float( 0f, totalWeight );
for ( int i = perks.Count - 1; i >= 0; i-- )
{
var (type, weight) = perks[i];
rand -= weight;
if ( rand < 0f )
{
chosenPerk = type;
break;
}
}
return chosenPerk;
}
Status CreateStatus( TypeDescription type )
{
var status = StatusManager.CreateStatus( type );
var currLevel = GetStatusLevel( type );
status.Level = currLevel + 1;
return status;
}
public void Restart()
{
Sprite.PlayAnimation( "idle" );
Sprite.PlaybackSpeed = 0.66f;
Sprite.Tint = new Color( 1f, 1f, 1f, 1f );
Sprite.LocalScale = new Vector3( Globals.SPRITE_SCALE );
if ( IsProxy )
return;
Position2D = new Vector3( Game.Random.Float( -3f, 3f ), Game.Random.Float( -3f, 3f ) );
Manager.Instance.Camera2D.SetPos( Position2D );
InitializeStats();
Manager.Instance.PlaySfxNearby( "restart", Position2D, Game.Random.Float( 0.95f, 1.05f ), 0.66f, 4f );
}
public void SpawnBulletRing( Vector2 pos, int numBullets, Vector2 aimDir, float damageMultMin = 1f, float damageMultMax = 1f )
{
float increment = 360f / numBullets;
for ( int i = 0; i < numBullets; i++ )
{
var dir = Utils.RotateVector( aimDir, i * increment );
float damageMult = Game.Random.Float( damageMultMin, damageMultMax );
SpawnBullet( pos, dir, false, damageMult );
}
Manager.Instance.PlaySfxNearby( "shoot", pos, pitch: 1f, volume: 1f, maxDist: 3f );
}
public Grenade SpawnGrenade( Vector2 pos, Vector2 vel )
{
var grenadeObj = Manager.Instance.GrenadePrefab.Clone();
var grenade = grenadeObj.Components.Get<Grenade>();
grenade.Velocity = vel;
grenade.ExplosionSizeMultiplier = Stats[PlayerStat.ExplosionSizeMultiplier];
grenade.Player = this;
grenade.StickyPercent = Stats[PlayerStat.GrenadeStickyPercent];
grenade.FearChance = Stats[PlayerStat.GrenadeFearChance];
if ( Stats[PlayerStat.GrenadesCanCrit] > 0f )
{
grenade.CriticalChance = Stats[PlayerStat.CritChance];
grenade.CriticalMultiplier = Stats[PlayerStat.CritMultiplier];
}
//grenadeObj.NetworkSpawn( Network.Owner );
grenadeObj.WorldPosition = new Vector3( pos.x, pos.y, Globals.GetZPos( pos.y ) );
Manager.Instance.AddThing( grenade );
return grenade;
}
public void CreateShieldVfx()
{
_shieldVfx = Manager.Instance.ShieldVfxPrefab.Clone( WorldPosition );
_shieldVfx.Parent = GameObject;
_shieldVfx.LocalPosition = new Vector3( 0f, 0f, 0.1f );
_shieldVfx.LocalScale = new Vector3( 1f ) * 1.8f * Globals.SPRITE_SCALE;
_shieldVfx.LocalRotation = new Angles( 0f, -90f, 0f );
}
public void RemoveShieldVfx()
{
if ( _shieldVfx != null )
{
_shieldVfx.Destroy();
_shieldVfx = null;
}
}
public void PlaySfx( string name, Vector2 pos, float pitch, float volume )
{
var sfx = Sound.Play( name, new Vector3( pos.x, pos.y, Globals.SFX_DEPTH ) );
if ( sfx != null )
{
sfx.Volume = volume;
sfx.Pitch = pitch;
}
}
protected override void OnStart()
{
base.OnStart();
Manager.Instance.AddPlayer( this );
}
protected override void OnDestroy()
{
base.OnDestroy();
Manager.Instance.RemovePlayer( this );
}
void HandleCamShaking()
{
CamShakeAmount = 0f;
for ( int i = _camShakeDatas.Count - 1; i >= 0; i-- )
{
var data = _camShakeDatas[i];
var time = data.useRealTime ? RealTime.Now : Time.Now;
if ( time > data.startTime + data.time )
{
_camShakeDatas.RemoveAt( i );
}
else
{
float amount = Utils.Map( time, data.startTime, data.startTime + data.time, data.strength, 0f, data.easingType );
CamShakeAmount = MathF.Max( amount, CamShakeAmount );
}
}
}
public void ShakeCam( float strength, float time, EasingType easingType = EasingType.Linear, bool useRealTime = false )
{
var timeNow = useRealTime ? RealTime.Now : Time.Now;
_camShakeDatas.Add( new CamShakeData( strength, timeNow, time, easingType, useRealTime ) );
}
}
using Sandbox;
using Sandbox.ModelEditor.Nodes;
using System.Net.NetworkInformation;
public class Charger : Enemy
{
private TimeSince _damageTime;
private const float DAMAGE_TIME = 1f;
protected float _chargeDelayTimer;
private const float CHARGE_DELAY_MIN = 2f;
private const float CHARGE_DELAY_MAX = 6f;
public bool IsPreparingToCharge { get; private set; }
public bool IsCharging { get; private set; }
private float _prepareTimer;
private const float PREPARE_TIME = 1f;
protected float _chargeTimer;
protected float CHARGE_TIME_MIN = 1.8f;
protected float CHARGE_TIME_MAX = 2.5f;
private float _chargeTime;
private float _nextRedirectTime;
private TimeSince _timeSinceRedirect;
protected Vector2 _chargeDir;
protected Vector2 _chargeVel;
private TimeSince _chargeCloudTimer;
public override float HeightVariance => 0.03f;
public override float WidthVariance => 0.015f;
public float ChargeRange { get; set; }
protected float REDIRECT_DELAY_MIN = 0.3f;
protected float REDIRECT_DELAY_MAX = 2.7f;
protected override void OnAwake()
{
//OffsetY = -0.57f;
ShadowScale = 1.25f;
ShadowFullOpacity = 0.8f;
ShadowOpacity = 0f;
Scale = 1.25f;
base.OnAwake();
//Sprite.Texture = Texture.Load("textures/sprites/charger.vtex");
//Sprite.Size = new Vector2( 1f, 1f ) * Scale;
PushStrength = 25f;
Radius = 0.275f;
Health = 75f;
if ( Manager.Instance.Difficulty < 0 )
Health = 55f;
MaxHealth = Health;
DamageToPlayer = 10f;
CoinValueMin = 2;
CoinValueMax = 5;
CoinChance = 0.7f;
Sprite.PlayAnimation( AnimSpawnPath );
if ( IsProxy )
return;
CollideWith.Add( typeof( Enemy ) );
CollideWith.Add( typeof( Player ) );
_damageTime = DAMAGE_TIME;
_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX );
ChargeRange = 4.2f;
}
protected override void UpdatePosition( float dt )
{
//Gizmo.Draw.Color = Color.White.WithAlpha(0.5f);
//Gizmo.Draw.Text( $"IsCharging: {IsCharging}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.7f, 0f ) ) );
base.UpdatePosition( dt );
var targetPos = Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);
if ( IsPreparingToCharge )
{
_prepareTimer -= dt;
if ( _prepareTimer < 0f )
{
Charge();
return;
}
}
else if ( IsCharging )
{
_chargeTimer -= dt;
if ( _chargeTimer < 0f )
{
IsCharging = false;
Sprite.PlayAnimation( AnimIdlePath );
CanTurn = true;
DontChangeAnimSpeed = false;
}
else
{
HandleCharging( dt );
}
WorldPosition += (Vector3)(_chargeVel + Velocity) * dt;
if ( Manager.Instance.Difficulty >= 1 && _timeSinceRedirect > _nextRedirectTime )
{
_chargeDir = (targetPos - Position2D).Normal;
if(Math.Abs( targetPos.x - Position2D.x ) > 0.15f )
FlipX = targetPos.x > Position2D.x;
_nextRedirectTime = Game.Random.Float( REDIRECT_DELAY_MIN, REDIRECT_DELAY_MAX );
_timeSinceRedirect = 0f;
}
if ( _chargeCloudTimer > 0.25f )
{
SpawnCloudClient( Position2D + new Vector2( 0f, 0.25f ), -_chargeDir * Game.Random.Float( 0.2f, 0.8f ) );
_chargeCloudTimer = Game.Random.Float( 0f, 0.075f );
}
}
else
{
Velocity += (targetPos - Position2D).Normal * dt * (IsFeared ? -1f : 1f);
float speed = 0.75f * (IsAttacking ? 1.3f : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) ) * (IsAttacking ? 0.66f : 0.35f);
if ( Manager.Instance.Difficulty < 0 )
speed *= 0.85f;
WorldPosition += (Vector3)Velocity * speed * dt;
}
var player_dist_sqr = (targetPos - Position2D).LengthSquared;
if ( !IsPreparingToCharge && !IsCharging && !IsAttacking && player_dist_sqr < ChargeRange * ChargeRange && Target.IsValid() )
{
_chargeDelayTimer -= dt;
if ( _chargeDelayTimer < 0f )
{
PrepareToCharge();
}
}
}
protected virtual void HandleCharging(float dt)
{
float chargeSpeed = Manager.Instance.Difficulty >= 1 ? 12f : 4f; ;
_chargeVel += _chargeDir * chargeSpeed * Utils.MapReturn( _chargeTimer, _chargeTime, 0f, 0f, 1f, EasingType.Linear ) * dt;
TempWeight += Utils.MapReturn( _chargeTimer, _chargeTime, 0f, 1f, 6f, EasingType.Linear ) * dt;
float BUFFER = 0.01f;
var x_min = Manager.Instance.BOUNDS_MIN.x + Radius + BUFFER;
var x_max = Manager.Instance.BOUNDS_MAX.x - Radius - BUFFER;
var y_min = Manager.Instance.BOUNDS_MIN.y + BUFFER;
var y_max = Manager.Instance.BOUNDS_MAX.y - Radius - BUFFER;
if ( Position2D.x < x_min && _chargeDir.x < 0f )
{
_chargeDir = _chargeDir.WithX( Math.Abs( _chargeDir.x ) );
_chargeVel = _chargeVel.WithX( Math.Abs( _chargeVel.x ) * 0.1f );
FlipX = true;
Sprite.SpriteFlags = SpriteFlags.HorizontalFlip;
}
else if ( Position2D.x > x_max && _chargeDir.x > 0f )
{
_chargeDir = _chargeDir.WithX( -Math.Abs( _chargeDir.x ) );
_chargeVel = _chargeVel.WithX( -Math.Abs( _chargeVel.x ) * 0.1f );
FlipX = false;
Sprite.SpriteFlags = SpriteFlags.None;
}
if ( Position2D.y < y_min && _chargeDir.y < 0f )
{
_chargeDir = _chargeDir.WithY( Math.Abs( _chargeDir.y ) );
_chargeVel = _chargeDir.WithY( Math.Abs( _chargeVel.y ) * 0.1f );
}
else if ( Position2D.y > y_max && _chargeDir.y > 0f )
{
_chargeDir = _chargeDir.WithY( -Math.Abs( _chargeDir.y ) );
_chargeVel = _chargeDir.WithY( -Math.Abs( _chargeVel.y ) * 0.1f );
}
}
protected override void HandleDeceleration( float dt )
{
if ( IsCharging )
{
Velocity *= (1f - dt * 1.75f);
float decel = Manager.Instance.Difficulty >= 1 ? 3f : 0.5f;
_chargeVel *= (1f - dt * decel);
}
else
{
base.HandleDeceleration( dt );
}
}
protected override void UpdateSprite( Thing target )
{
if ( !IsCharging )
base.UpdateSprite( target );
}
protected override void HandleAttacking( Thing target, float dt )
{
if ( !IsPreparingToCharge && !IsCharging )
base.HandleAttacking( target, dt );
}
public void PrepareToCharge()
{
_prepareTimer = PREPARE_TIME;
IsPreparingToCharge = true;
Manager.Instance.PlaySfxNearby( "enemy.roar.prepare", Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 5f );
Sprite.PlayAnimation( "charge_start" );
CanTurn = false;
CanAttack = false;
CanAttackAnim = false;
DontChangeAnimSpeed = true;
if ( Manager.Instance.Difficulty >= 1 )
{
_nextRedirectTime = Game.Random.Float( REDIRECT_DELAY_MIN, REDIRECT_DELAY_MAX );
_timeSinceRedirect = 0f;
}
}
public void Charge()
{
var target_pos = Target.IsValid() && !IsFeared
? Target.Position2D + Target.Velocity * Game.Random.Float( 0.5f, 1.75f )
: Position2D + Utils.GetRandomVector() * 0.5f;
_chargeDir = Utils.RotateVector( (target_pos - Position2D).Normal, Game.Random.Float( -10f, 10f ) );
IsPreparingToCharge = false;
IsCharging = true;
_chargeTime = Game.Random.Float( CHARGE_TIME_MIN, CHARGE_TIME_MAX ) * (Manager.Instance.Difficulty >= 1 ? Game.Random.Float(1.25f, Utils.Map(Manager.Instance.Difficulty, 1, 10, 1.75f, 3f, EasingType.SineIn)) : 1f);
_chargeTimer = _chargeTime;
CanAttack = true;
CanAttackAnim = true;
_chargeDelayTimer = Game.Random.Float( CHARGE_DELAY_MIN, CHARGE_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 1.4f : 1f);
_chargeVel = Vector2.Zero;
Sprite.PlayAnimation( "charge_loop" );
AnimSpeed = 3f;
FlipX = _chargeDir.x > 0f;
Sprite.SpriteFlags = FlipX ? SpriteFlags.HorizontalFlip : SpriteFlags.None;
//Sprite.SpriteFlags = target_pos.x > Position2D.x ? SpriteFlags.HorizontalFlip : SpriteFlags.None;
Manager.Instance.PlaySfxNearby( "enemy.roar", Position2D, pitch: Game.Random.Float( 0.925f, 1.075f ), volume: 1f, maxDist: 8f );
}
public override void Colliding( Thing other, float percent, float dt )
{
base.Colliding( other, percent, dt );
if ( other is Enemy enemy && !enemy.IsDying )
{
var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
Velocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;
if ( (IsAttacking || IsCharging) && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )
{
var dmg = DamageToPlayer;
if ( IsCharmed )
dmg *= CharmDamageDealtMultiplier;
enemy.Damage( dmg, null, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false, DamageType.Melee );
enemy.Target = this;
_damageTime = 0f;
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );
}
}
// todo: move collision check to player instead to prevent laggy hits?
else if ( other is Player player )
{
if ( !player.IsDead )
{
Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;
if ( (IsAttacking || IsCharging) && _damageTime > (DAMAGE_TIME / TimeScale) && !IsCharmed )
{
float dmg = player.CheckDamageAmount( DamageToPlayer, DamageType.Melee );
if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
{
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );
player.Damage( dmg );
if ( dmg > 0f )
OnDamagePlayer( player, dmg );
}
_damageTime = 0f;
}
}
}
}
public override void Celebrate()
{
base.Celebrate();
CelebrateAsync();
}
async void CelebrateAsync()
{
await Task.Delay( Game.Random.Int( 0, 500 ) );
Sprite.PlaybackSpeed = Game.Random.Float( 1f, 2.5f );
Sprite.PlayAnimation( "cheer_start" );
await Task.Delay( Game.Random.Int( 200, 400 ) );
Sprite.PlayAnimation( "cheer" );
}
}
using Sandbox;
public class RunnerEliteSpecial : RunnerElite
{
public override float FullOpacity => 0f;
protected override void OnAwake()
{
base.OnAwake();
Radius = 0.5075f;
Scale = 2.275f;
Sprite.LocalScale = new Vector3( Scale * Game.Random.Float( 1f - HeightVariance, 1f + HeightVariance ), Scale * Game.Random.Float( 1f - WidthVariance, 1f + WidthVariance ), 1f ) * Globals.SPRITE_SCALE;
Health = 300f;
MaxHealth = Health;
ShadowScale = 2.275f;
ShadowFullOpacity = 0f;
ShadowSpriteDirty = true;
PushStrength = 55f;
AggroRange = 2.25f;
DamageToPlayer = 5f;
CoinChance = 1f;
CoinValueMin = 4;
CoinValueMax = 8;
Sprite.Tint = Color.White.WithAlpha( FullOpacity );
MoveSpeed = 0.35f;
JUMP_DELAY_MIN = 4f;
JUMP_DELAY_MAX = 10f;
}
protected override void SpawnLandingClouds()
{
for ( int i = 0; i < Game.Random.Int( 2, 3 ); i++ )
{
var dir = Utils.GetRandomVector();
SpawnCloudClient( Position2D + dir * Game.Random.Float(0.4f, 0.6f), dir * Game.Random.Float( 0.3f, 1.5f ) );
}
}
public override void DealDamage()
{
Flash( 0.1f, Color.Red.WithAlpha( 0.25f ) );
}
public override void Celebrate()
{
if ( IsSpawning )
FinishSpawning();
if ( _isJumping )
return;
CelebrateAsync();
}
async void CelebrateAsync()
{
await Task.Delay( Game.Random.Int( 0, 500 ) );
Sprite.PlaybackSpeed = Game.Random.Float( 2f, 4f );
Sprite.Tint = Color.Red.WithAlpha( 0.25f );
Sprite.PlayAnimation( "cheer" );
}
}
using Sandbox;
using static Sandbox.VertexLayout;
public class Spiker : Enemy
{
private TimeSince _damageTime;
private const float DAMAGE_TIME = 0.75f;
private float _shootDelayTimer;
private const float SHOOT_DELAY_MIN = 2f;
private const float SHOOT_DELAY_MAX = 4f;
public bool IsShooting { get; private set; }
private float _shotTimer;
private const float SHOOT_TIME = 4f;
private bool _hasShot;
private TimeSince _prepareStartTime;
private bool _hasReversed;
public override float HeightVariance => 0.02f;
public override float WidthVariance => 0.01f;
private bool _moveClockwise;
public static int SpikerNum { get; set; }
private float _perpendicularMaxDist;
private float _digDelayTimer;
private const float DIG_DELAY_MIN = 4f;
private const float DIG_DELAY_MAX = 13f;
public bool IsDigging { get; private set; }
private TimeSince _timeSinceStartDigging;
private const float DIG_TIME = 1.2f;
public float ShootRange { get; set; }
public float MoveSpeed { get; set; }
protected override void OnAwake()
{
//OffsetY = -0.58f;
ShadowScale = 1.2f;
ShadowFullOpacity = 0.8f;
ShadowOpacity = 0f;
Scale = 1.4f;
base.OnAwake();
//AnimSpeed = 4f;
//Sprite.Texture = Texture.Load("textures/sprites/spiker.vtex");
PushStrength = 8f;
Deceleration = 2.57f;
DecelerationAttacking = 2.35f;
AggroRange = 0.75f;
Radius = 0.27f;
Health = 80f;
if ( Manager.Instance.Difficulty < 0 )
Health = 70f;
MaxHealth = Health;
DamageToPlayer = 14f;
CoinValueMin = 2;
CoinValueMax = 4;
CoinChance = 0.75f;
Sprite.PlayAnimation( AnimSpawnPath );
if ( IsProxy )
return;
CollideWith.Add( typeof( Enemy ) );
CollideWith.Add( typeof( Player ) );
_damageTime = DAMAGE_TIME;
_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX );
_digDelayTimer = Game.Random.Float( DIG_DELAY_MIN, DIG_DELAY_MAX );
_moveClockwise = SpikerNum % 2 == 0;
SpikerNum++;
_perpendicularMaxDist = Game.Random.Float( 3.5f, 6.5f );
ShootRange = 4.5f;
MoveSpeed = 0.9f;
}
protected override void UpdatePosition( float dt )
{
base.UpdatePosition( dt );
//if(!IsDigging)
//{
// Gizmo.Draw.Color = Color.White;
// Gizmo.Draw.Text( $"{_digDelayTimer}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );
//}
var targetPos = GetTargetPos();
if ( IsShooting )
{
Velocity *= (1f - dt * (IsAttacking ? DecelerationAttacking : Deceleration));
if ( !_hasShot && _prepareStartTime > 1f )
{
CreateSpike();
_hasShot = true;
}
if ( !_hasReversed && _prepareStartTime > 3f )
{
_hasReversed = true;
Sprite.PlayAnimation( "shoot_reverse" );
}
Velocity *= (1f - dt * 4f);
_shotTimer -= dt;
if ( _shotTimer < 0f )
{
FinishShooting();
return;
}
}
else if ( IsDigging )
{
Velocity *= (1f - dt * 6f);
if ( _timeSinceStartDigging > DIG_TIME )
{
Vector2 pos;
if ( Target.IsValid() )
{
pos = targetPos + Target.Velocity * Game.Random.Float( 0f, 2f ) + Utils.GetRandomVector() * Game.Random.Float( 1.5f, 5.5f);
if ( (Position2D - pos).LengthSquared < MathF.Pow( 0.5f, 2f ) )
pos = Position2D + Utils.GetRandomVector() * Game.Random.Float( 3f, 5f );
}
else
{
pos = Position2D + Game.Random.Float( 4f, 5f );
}
FinishDigging( Manager.Instance.ClampToBounds( pos ) );
}
else
{
float progress = Utils.Map( _timeSinceStartDigging, 0f, DIG_TIME, 0f, 1f );
//ZPos = Utils.Map( progress, 0f, 1f, 0f, DIG_DEPTH );
//PlaybackRate = Utils.Map( progress, 0f, 1f, 0f, 1f ) * _personalSpeedScale;
float shadowOpacity = Utils.Map( progress, 0f, 1f, ShadowOpacity, 0f, EasingType.QuadIn );
ShadowSprite.Tint = Color.Black.WithAlpha( shadowOpacity );
VfxOpacity = Utils.Map( progress, 0f, 1f, 1f, 0f, EasingType.QuadIn );
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"{progress}", new global::Transform( (Vector3)Position2D + new Vector3( 0f, -0.4f, 0f ) ) );
IgnoreCollision = progress > 0.3f;
if ( _spawnCloudTime > (0.3f / TimeScale) )
{
var cloud = Manager.Instance.SpawnCloud( Position2D + new Vector2( 0f, 0.05f ) );
cloud.Velocity = new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ).Normal * Game.Random.Float( 0.2f, 0.6f );
_spawnCloudTime = Game.Random.Float( 0f, 0.15f );
}
}
return;
}
else
{
Vector2 toTarget = (targetPos - Position2D).Normal;
if(Manager.Instance.Difficulty >= 1)
{
if ( (targetPos - Position2D).LengthSquared < MathF.Pow( _perpendicularMaxDist, 2f ) )
toTarget = Vector2.Lerp( toTarget, new Vector2( toTarget.y, -toTarget.x ) * (_moveClockwise ? -1f : 1f), Utils.Map( (targetPos - Position2D).LengthSquared, MathF.Pow( _perpendicularMaxDist, 2f ), MathF.Pow( 1.5f, 2f ), 0f, 1f ) );
}
Velocity += toTarget * 1.0f * dt * (IsFeared ? -1f : 1f);
}
float speed = MoveSpeed * (IsAttacking ? 1.3f : 0.7f) + Utils.FastSin( MoveTimeOffset + Time.Now * (IsAttacking ? 15f : 7.5f) ) * (IsAttacking ? 0.66f : 0.35f);
if ( Manager.Instance.Difficulty < 0 )
speed *= 0.85f;
WorldPosition += (Vector3)Velocity * speed * dt;
if ( !IsShooting && !IsDigging && (!IsAttacking || IsCharmed) && !Manager.Instance.IsGameOver )
{
var target_dist_sqr = ((Target.IsValid() ? Target.Position2D : targetPos) - Position2D).LengthSquared;
var rangeSqr = ShootRange * ShootRange;
if ( Manager.Instance.Difficulty < 0 )
rangeSqr *= 0.7f;
if ( target_dist_sqr < rangeSqr )
{
_shootDelayTimer -= dt;
if ( _shootDelayTimer < 0f )
StartShooting();
}
if ( target_dist_sqr < MathF.Pow( 12f, 2f ) && Manager.Instance.Difficulty > 0 )
{
_digDelayTimer -= dt;
if ( _digDelayTimer < 0f )
StartDigging();
}
}
}
protected virtual Vector2 GetTargetPos()
{
return Target.IsValid() ? Target.Position2D : (IsCharmed ? Manager.Instance.Player.Position2D : Position2D);
}
protected override void UpdateSprite( Thing target )
{
if ( Sprite.CurrentAnimation.Name.Contains( "shoot" ) || IsDigging )
return;
base.UpdateSprite( target );
}
public void StartShooting()
{
_shotTimer = SHOOT_TIME;
IsShooting = true;
CanAttack = false;
CanTurn = false;
CanAttackAnim = false;
_hasShot = false;
_hasReversed = false;
_prepareStartTime = 0f;
Velocity *= 0.25f;
DontChangeAnimSpeed = true;
AnimSpeed = 1f;
Sprite.PlayAnimation( "shoot" );
ShouldUpdateAfterGameOver = true;
}
public virtual void CreateSpike()
{
var target_pos = Target.IsValid()
? Target.Position2D + Target.Velocity * Game.Random.Float( 0.2f, 2f ) + new Vector2( Game.Random.Float( -1f, 1f ), Game.Random.Float( -1f, 1f ) ) * 0.8f
: Position2D + Utils.GetRandomVector() * Game.Random.Float( 3f, 6f );
var BUFFER = 0.1f;
var spike = Manager.Instance.SpawnEnemySpike( new Vector2( Math.Clamp( target_pos.x, Manager.Instance.BOUNDS_MIN.x + BUFFER, Manager.Instance.BOUNDS_MAX.x - BUFFER ), Math.Clamp( target_pos.y, Manager.Instance.BOUNDS_MIN.y + BUFFER, Manager.Instance.BOUNDS_MAX.y - BUFFER ) ) );
if(IsCharmed)
{
spike.BecomeCharmed();
}
Manager.Instance.PlaySfxNearby( "spike.prepare", target_pos, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1.5f, maxDist: 5f );
}
public void FinishShooting()
{
_shootDelayTimer = Game.Random.Float( SHOOT_DELAY_MIN, SHOOT_DELAY_MAX ) * (Manager.Instance.Difficulty < 0 ? 2.5f : 1f);
IsShooting = false;
CanAttack = true;
CanAttackAnim = true;
CanTurn = true;
DontChangeAnimSpeed = false;
Sprite.PlayAnimation( AnimIdlePath );
ShouldUpdateAfterGameOver = false;
if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
Celebrate();
}
public override void Colliding( Thing other, float percent, float dt )
{
base.Colliding( other, percent, dt );
if ( other is Enemy enemy && !enemy.IsDying )
{
var spawnFactor = Utils.Map( enemy.TimeSinceSpawn, 0f, enemy.SpawnTime, 0f, 1f, EasingType.QuadIn );
Velocity += (Position2D - enemy.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * enemy.PushStrength * (1f + enemy.TempWeight) * spawnFactor * dt;
if ( IsAttacking && IsCharmed != enemy.IsCharmed && _damageTime > (DAMAGE_TIME / TimeScale) )
{
var dmg = DamageToPlayer;
if ( IsCharmed )
dmg *= CharmDamageDealtMultiplier;
enemy.Damage( dmg, null, addVel: Vector2.Zero, addTempWeight: 0f, isCrit: false, DamageType.Melee );
enemy.Target = this;
_damageTime = 0f;
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( enemy.Health, enemy.MaxHealth, 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 0.6f, maxDist: 4.5f );
}
}
// todo: move collision check to player instead to prevent laggy hits?
else if ( other is Player player )
{
if ( !player.IsDead )
{
Velocity += (Position2D - player.Position2D).Normal * Utils.Map( percent, 0f, 1f, 0f, 1f ) * player.Stats[PlayerStat.PushStrength] * (1f + player.TempWeight) * dt;
if ( IsAttacking && _damageTime > (DAMAGE_TIME / TimeScale) && !IsCharmed )
{
float dmg = player.CheckDamageAmount( DamageToPlayer, DamageType.Melee );
if ( !player.IsInvulnerable && !player.IsTimePausedForChoosing )
{
Manager.Instance.PlaySfxNearby( "zombie.attack.player", Position2D, pitch: Utils.Map( player.Health, player.Stats[PlayerStat.MaxHp], 0f, 0.95f, 1.15f, EasingType.QuadIn ), volume: 1f, maxDist: 5.5f );
player.Damage( dmg );
if( dmg > 0f )
OnDamagePlayer( player, dmg );
}
_damageTime = 0f;
}
}
}
}
public override void Damage( float damage, Player player, Vector2 addVel, float addTempWeight, bool isCrit = false, DamageType damageType = DamageType.PlayerBullet )
{
base.Damage( damage, player, addVel, addTempWeight, isCrit, damageType );
if ( Game.Random.Float( 0f, 1f ) < Utils.Map( damage, 1f, 20f, 0.1f, 0.7f ) )
_digDelayTimer *= Game.Random.Float( 0.6f, 0.95f );
}
public void StartDigging()
{
IsDigging = true;
_timeSinceStartDigging = 0f;
CanAttack = false;
CanAttackAnim = false;
CanTurn = false;
Velocity *= 0.5f;
Sprite.PlayAnimation( "dig" );
Manager.Instance.PlaySfxNearby( "zombie.dirt", Position2D, pitch: Game.Random.Float( 0.5f, 0.55f ), volume: 0.6f, maxDist: 7.5f );
//SS2Game.PlaySfx( "zombie.dirt", Position, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f );
//SS2Game.Current.DustCloudClient( Position2D );
ShouldUpdateAfterGameOver = true;
}
void FinishDigging( Vector2 pos )
{
Position2D = pos;
//WorldPosition = ((Vector3)Position2D).WithZ( ZPos );
Transform.ClearInterpolation();
IsDigging = false;
CanAttack = true;
CanAttackAnim = true;
CanTurn = true;
IgnoreCollision = false;
//SetAnim( "Attack1" );
_moveClockwise = !_moveClockwise;
_digDelayTimer = Game.Random.Float( DIG_DELAY_MIN, DIG_DELAY_MAX );
//SS2Game.PlaySfx( "zombie.dirt", Position, pitch: Game.Random.Float( 0.6f, 0.8f ), volume: 0.7f );
//SS2Game.Current.DustCloudClient( Position2D );
IsSpawning = true;
//_hasDug = true;
TimeSinceSpawn = 0f;
//SpawnProgress = 0f;
//ShadowRadiusModifier = 1.5f;
//ShadowOpacityModifier = 0f;
Manager.Instance.PlaySfxNearby( "zombie.dirt", Position2D, pitch: Game.Random.Float( 0.85f, 0.9f ), volume: 0.6f, maxDist: 7.5f );
ShadowSprite.Tint = Color.Black.WithAlpha( 0f );
VfxOpacity = 0f;
}
public override void Celebrate()
{
base.Celebrate();
if ( IsShooting || IsDigging )
return;
CelebrateAsync();
}
async void CelebrateAsync()
{
await Task.Delay( Game.Random.Int( 0, 500 ) );
Sprite.PlaybackSpeed = Game.Random.Float( 1.5f, 3.5f );
Sprite.PlayAnimation( "cheer_start" );
await Task.Delay( Game.Random.Int( 200, 400 ) );
Sprite.PlayAnimation( "cheer" );
}
protected override void FinishSpawning()
{
base.FinishSpawning();
ShouldUpdateAfterGameOver = false;
if ( Manager.Instance.IsGameOver && !Manager.Instance.ShouldUpdateThings )
Celebrate();
}
}
@using Sandbox;
@using Sandbox.UI;
@namespace SS1
@inherits Panel
@attribute [StyleSheet("BossNametag.razor.scss")]
<root>
@{
var currHp = Manager.Instance.Difficulty >= 5
? ( (Boss.IsValid() ? Math.Max(Boss.Health, 0f) : 0f) + (OtherBoss.IsValid() ? Math.Max(OtherBoss.Health, 0f) : 0f) )
: Boss.Health;
var maxHp = Manager.Instance.Difficulty >= 5
? 14000f
: Boss.MaxHealth;
var hpPercent = currHp / maxHp;
var bgColor = Lerp3(new Color(0f, 0.75f, 0f), new Color(0.75f, 0.75f, 0f), new Color(1f, 0f, 0f), 1f - hpPercent);
}
<div class="hpbar">
<div class="hpbardelta" style="width:@(hpPercent * 100f)%;"></div>
<div class="hpbaroverlay" style="width:@(hpPercent * 100f)%; background-color:@(bgColor.Rgba);"></div>
<div class="name_label">@(Manager.Instance.Difficulty >= 5 ? "BOSSES" : "BOSS")</div>
<div class="hp_label">
<div class="label">@($"{(int)Math.Ceiling(currHp)}")</div>
<div class="label">/</div>
<div class="label">@($"{(int)maxHp}")</div>
</div>
</div>
</root>
@code
{
public Boss Boss { get; set; }
public Boss OtherBoss { get; set; }
protected override int BuildHash()
{
var currHp = Manager.Instance.Difficulty >= 5
? ( (Boss.IsValid() ? Math.Max(Boss.Health, 0f) : 0f) + (OtherBoss.IsValid() ? Math.Max(OtherBoss.Health, 0f) : 0f) )
: Boss.Health;
return HashCode.Combine(
currHp
);
}
Color Lerp3(Color a, Color b, Color c, float t)
{
if(t < 0.5f) // 0.0 to 0.5 goes to a -> b
return Color.Lerp(a, b, t / 0.5f);
else // 0.5 to 1.0 goes to b -> c
return Color.Lerp(b, c, (t - 0.5f) / 0.5f);
}
}
/// <summary>
/// Handles firing (hitscan bullets or spawning projectiles) for a weapon.
/// </summary>
[Title( "Shoot Component" ), Icon( "gps_fixed" )]
public sealed class ShootComponent : WeaponComponent
{
[Property] public string FireButton { get; set; } = "Attack1";
[Property] public GameObject MuzzlePoint { get; set; }
[Property] public WeaponData Data { get; set; }
// Driven by WeaponData at runtime
public float BaseDamage { get; set; }
public float BulletRange { get; set; } = 5000f;
public int BulletCount { get; set; } = 1;
public float BulletForce { get; set; } = 1f;
public float BulletSize { get; set; } = 2f;
public float BulletSpread { get; set; } = 0f;
public float FireDelay { get; set; } = 0.1f;
public bool DisableBulletImpacts { get; set; }
public SoundEvent FireSound { get; set; }
public bool FireSoundLoop { get; set; }
public bool FireSoundOnlyOnStart { get; set; }
public string ActivateSound { get; set; }
public string DryFireSound { get; set; }
public string ProjectilePrefab { get; set; }
public string ShootEffectPrefab { get; set; }
public string ImpactEffectPrefab { get; set; }
public string MuzzleFlashPrefab { get; set; }
public Color TracerColor { get; set; } = Color.Yellow;
public float TracerSpeed { get; set; } = 8000f;
public float TracerLength { get; set; } = 300f;
private TimeUntil _timeUntilCanFire;
private SoundHandle _activeSound;
private SoundHandle _fireSoundHandle;
private bool _isFiring;
protected override void OnStart()
{
if ( Data == null ) return;
BaseDamage = Data.BaseDamage;
BulletRange = Data.BulletRange;
BulletCount = Data.BulletCount;
BulletForce = Data.BulletForce;
BulletSize = Data.BulletSize;
BulletSpread = Data.BulletSpread;
FireDelay = Data.FireDelay;
DisableBulletImpacts = Data.DisableBulletImpacts;
FireSoundLoop = Data.FireSoundLoop;
FireSoundOnlyOnStart = Data.FireSoundOnlyOnStart;
TracerColor = Data.TracerColor;
TracerSpeed = Data.TracerSpeed;
TracerLength = Data.TracerLength;
if ( !string.IsNullOrEmpty( Data.ProjectilePrefab ) ) ProjectilePrefab = Data.ProjectilePrefab;
if ( Data.FireSound.IsValid() ) FireSound = Data.FireSound;
if ( !string.IsNullOrEmpty( Data.ActivateSound ) ) ActivateSound = Data.ActivateSound;
if ( !string.IsNullOrEmpty( Data.DryFireSound ) ) DryFireSound = Data.DryFireSound;
if ( !string.IsNullOrEmpty( Data.ShootEffectPrefab ) ) ShootEffectPrefab = Data.ShootEffectPrefab;
if ( !string.IsNullOrEmpty( Data.ImpactEffectPrefab ) ) ImpactEffectPrefab = Data.ImpactEffectPrefab;
if ( !string.IsNullOrEmpty( Data.MuzzleFlashPrefab ) ) MuzzleFlashPrefab = Data.MuzzleFlashPrefab;
}
public override void Simulate()
{
base.Simulate();
if ( Player == null ) return;
if ( WishesToFire() )
{
if ( CanFire() )
{
if ( !_isFiring )
{
_isFiring = true;
if ( FireSound.IsValid() )
{
if ( FireSoundLoop )
_fireSoundHandle = GameObject.PlaySound( FireSound );
else if ( FireSoundOnlyOnStart )
GameObject.PlaySound( FireSound );
}
}
Fire();
}
else
{
// Dry fire if no ammo
var ammo = GetComponent<AmmoComponent>();
if ( ammo != null && !ammo.HasEnoughAmmo() && Input.Pressed( FireButton ) )
{
if ( !string.IsNullOrEmpty( DryFireSound ) )
Sound.Play( DryFireSound, Player.WorldPosition );
}
}
}
else
{
StopFiring();
}
}
private void StopFiring()
{
if ( !_isFiring ) return;
_isFiring = false;
_activeSound?.Stop();
_activeSound = null;
if ( _fireSoundHandle.IsValid() )
{
_fireSoundHandle.Stop();
_fireSoundHandle = default;
}
}
protected override void OnDeactivate() => StopFiring();
private void Fire()
{
_timeUntilCanFire = FireDelay;
TimeSinceActivated = 0;
RunGameEvent( $"{Name}.fire" );
// Local feedback: fire sound and activate effects
if ( FireSound.IsValid() && !FireSoundOnlyOnStart && !FireSoundLoop )
GameObject.PlaySound( FireSound );
// Muzzle flash
if ( !string.IsNullOrEmpty( MuzzleFlashPrefab ) )
{
var muzzlePos = MuzzlePoint?.WorldPosition ?? Player.WorldPosition;
var muzzleRot = MuzzlePoint?.WorldRotation ?? Player.WorldRotation;
var flashFile = ResourceLibrary.Get<PrefabFile>( MuzzleFlashPrefab );
if ( flashFile != null )
{
var flash = SceneUtility.GetPrefabScene( flashFile )?.Clone();
if ( flash != null )
{
flash.WorldPosition = muzzlePos;
flash.WorldRotation = muzzleRot;
flash.NetworkSpawn();
}
}
}
// Shoot effect for projectile weapons (no hit endpoint available)
if ( !string.IsNullOrEmpty( ShootEffectPrefab ) && Data?.Mode == WeaponData.FiringMode.Projectile )
{
var muzzlePos = MuzzlePoint?.WorldPosition ?? Player.WorldPosition;
var muzzleRot = MuzzlePoint?.WorldRotation ?? Player.WorldRotation;
BroadcastShootEffect( muzzlePos, muzzleRot, muzzlePos + muzzleRot.Forward * BulletRange );
}
if ( _activeSound == null && !string.IsNullOrEmpty( ActivateSound ) )
_activeSound = Sound.Play( ActivateSound, Player.WorldPosition );
// Route shot processing through host
if ( Data?.Mode == WeaponData.FiringMode.Projectile )
{
if ( Networking.IsHost )
SpawnProjectile();
else
ServerSpawnProjectile();
}
else
{
ShootBullet();
}
}
private bool WishesToFire() => (Player?.IsBot == true ? Player.BotFirePrimary : Input.Down( FireButton )) && Player?.ActiveWeapon == Weapon;
private bool CanFire()
{
if ( _timeUntilCanFire > 0 ) return false;
if ( Weapon != null && Weapon.GameObject.Tags.Has( "reloading" ) ) return false;
var ammo = GetComponent<AmmoComponent>();
if ( ammo != null && !ammo.HasEnoughAmmo() ) return false;
return true;
}
private void ShootBullet()
{
if ( Player == null ) return;
Game.SetRandomSeed( (int)Time.Now );
var aimRay = Player.AimRay;
for ( int i = 0; i < BulletCount; i++ )
{
var forward = aimRay.Forward;
if ( BulletSpread > 0 )
forward += (Vector3.Random + Vector3.Random + Vector3.Random + Vector3.Random) * BulletSpread * 0.25f;
forward = forward.Normal;
var start = aimRay.Position;
if ( Networking.IsHost )
ProcessShot( start, forward );
else
ServerShootBullet( start, forward );
}
}
/// <summary>Sent from owner client to host for authoritative shot processing.</summary>
[Rpc.Host]
private void ServerShootBullet( Vector3 start, Vector3 forward )
{
ProcessShot( start, forward );
}
private void ProcessShot( Vector3 start, Vector3 forward )
{
var end = start + forward.Normal * BulletRange;
var tr = Scene.Trace.Ray( start, end )
.WithAnyTags( "solid", "player" )
.IgnoreGameObject( Player?.GameObject )
.Size( BulletSize )
.Run();
if ( !DisableBulletImpacts && tr.Hit )
{
BroadcastImpactEffect( tr.EndPosition, tr.Normal );
}
// Shoot effect for hitscan — use ray start and actual hit endpoint
if ( !string.IsNullOrEmpty( ShootEffectPrefab ) )
{
BroadcastShootEffect( start, Rotation.Identity, tr.EndPosition );
}
if ( tr.Hit )
{
var hitPawn = tr.GameObject?.Components.Get<PlayerPawn>( FindMode.EnabledInSelfAndDescendants );
if ( hitPawn != null )
hitPawn.TakeDamage( BaseDamage, Player );
}
if ( string.IsNullOrEmpty( ShootEffectPrefab ) )
BroadcastTracerEffect( start, tr.EndPosition );
}
private void SpawnProjectile()
{
if ( string.IsNullOrEmpty( ProjectilePrefab ) ) return;
var prefabFile = ResourceLibrary.Get<PrefabFile>( ProjectilePrefab );
if ( prefabFile == null )
{
Log.Warning( $"[ShootComponent] ProjectilePrefab not found: {ProjectilePrefab}" );
return;
}
var go = SceneUtility.GetPrefabScene( prefabFile )?.Clone();
if ( go != null )
{
var proj = go.Components.Get<Projectile>( FindMode.EnabledInSelfAndDescendants );
if ( proj != null && Data != null )
proj.Data = Data;
proj?.Launch( Player, null );
go.NetworkSpawn();
}
}
/// <summary>Sent from owner client to host to spawn and simulate the projectile authoritatively.</summary>
[Rpc.Host]
private void ServerSpawnProjectile()
{
SpawnProjectile();
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void BroadcastShootEffect( Vector3 position, Rotation rotation, Vector3 endPosition )
{
if ( string.IsNullOrEmpty( ShootEffectPrefab ) ) return;
var prefabFile = ResourceLibrary.Get<PrefabFile>( ShootEffectPrefab );
if ( prefabFile == null ) return;
var go = SceneUtility.GetPrefabScene( prefabFile )?.Clone();
if ( go == null ) return;
go.WorldPosition = position;
go.WorldRotation = rotation;
// Find BeamEffect in prefab (configured in editor), fallback to creating one
var beam = go.GetComponent<BeamEffect>( true );
//Log.Info( $"Shoot effect beam: {go}, start: {position}, end: {endPosition}" );
beam.TargetGameObject.WorldPosition = endPosition;
beam.TargetGameObject.Transform.ClearInterpolation();
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void BroadcastImpactEffect( Vector3 position, Vector3 normal )
{
if ( string.IsNullOrEmpty( ImpactEffectPrefab ) ) return;
var prefabFile = ResourceLibrary.Get<PrefabFile>( ImpactEffectPrefab );
if ( prefabFile == null ) return;
var go = SceneUtility.GetPrefabScene( prefabFile )?.Clone();
if ( go == null ) return;
go.WorldPosition = position;
go.WorldRotation = Rotation.LookAt( normal );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void BroadcastTracerEffect( Vector3 start, Vector3 end )
{
var go = new GameObject( true, "BulletTracer" );
go.WorldPosition = start;
var tracer = go.Components.Create<Tracer>();
tracer.EndPoint = end;
tracer.DistancePerSecond = TracerSpeed;
tracer.Length = TracerLength;
tracer.LineColor = new Gradient(
new Gradient.ColorFrame( 0, TracerColor ),
new Gradient.ColorFrame( 1, TracerColor.WithAlpha( 0 ) )
);
}
public override void OnGameEvent( string eventName )
{
if ( eventName == "sprint.stop" ) _timeUntilCanFire = 0.2f;
if ( eventName == "aimcomponent.start" ) _timeUntilCanFire = 0.15f;
if ( eventName == "shootcomponent.fire" ) TimeSinceActivated = 0;
}
}
@using Sandbox.UI
@using Sandbox.UI.Components
@inherits Panel
<style>
Info {
position: absolute;
width: 100%;
bottom: 0;
height: 100%;
align-items: center;
justify-content: center;
transition: all 0.5s sin-ease-in-out;
flex-direction: row;
}
.arc-root {
position: absolute;
width: 600px;
height: 600px;
margin-bottom: 16px;
margin-right: 12px;
}
.seg {
position: absolute;
opacity: 0.22;
width: 10px;
height: 18px;
}
.seg.health {
background-color: rgba(95, 230, 120, 0.95);
}
.seg.health.active {
opacity: 1;
box-shadow: 0 0 8px rgba(95, 230, 120, 0.45);
}
.seg.armour {
background-color: rgba(65, 195, 255, 0.95);
}
.seg.armour.active {
opacity: 1;
box-shadow: 0 0 8px rgba(65, 195, 255, 0.4);
}
.arc-label {
position: absolute;
font-family: "Wallpoet";
font-size: 24px;
color: rgba(210, 245, 255, 0.9);
text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
}
.arc-label.hp {
left: 144px;
bottom: 122px;
}
.arc-label.sh {
right: 144px;
bottom: 122px;
}
.sWarning {
text-align: center;
position: absolute;
font-family: "Wallpoet";
font-size: 44px;
top: 22%;
left: 50%;
transform: translate(-50%, -50%);
color: rgba(39, 181, 238, 1);
z-index: 30;
}
</style>
<root>
@if ( _showWarning )
{
<label class="sWarning">- -[WARNING SHIELD DOWN]- -</label>
}
<div class="arc-root">
@foreach ( var s in GetHealthArc() )
{
<div class="seg health @(s.Active ? "active" : "")"
style="left:@($"{s.X:F1}px"); top:@($"{s.Y:F1}px"); transform:rotate(@($"{s.Angle + 90f:F1}deg"));"></div>
}
@foreach ( var s in GetShieldArc() )
{
<div class="seg armour @(s.Active ? "active" : "")"
style="left:@($"{s.X:F1}px"); top:@($"{s.Y:F1}px"); transform:rotate(@($"{s.Angle + 90f:F1}deg"));"></div>
}
<div class="arc-label hp">HP @MathF.Round(_health)</div>
<div class="arc-label sh">SH @MathF.Round(_shield)</div>
</div>
</root>
@code
{
private PlayerPawn Pawn => LocalPlayer.Pawn;
private float _health => Pawn?.Health ?? 0f;
private float _healthMax => Pawn?.MaxHealth ?? 100f;
private float _shield => Pawn?.Shield ?? 0f;
private float _shieldMax => Pawn?.MaxShield ?? 0f;
private bool _showWarning =>
PilotGame.Gamemode != FPGameMode.Instagib &&
(Pawn?.IsAlive ?? false) &&
_health > 0f &&
_shieldMax > 0f &&
_shield <= 0f;
private float HealthProgress => _healthMax > 0f ? Math.Clamp( _health / _healthMax, 0f, 1f ) : 0f;
private float ShieldProgress => _shieldMax > 0f ? Math.Clamp( _shield / _shieldMax, 0f, 1f ) : 0f;
private readonly struct ArcSeg
{
public ArcSeg( float x, float y, float angle, bool active )
{
X = x;
Y = y;
Angle = angle;
Active = active;
}
public float X { get; }
public float Y { get; }
public float Angle { get; }
public bool Active { get; }
}
private IEnumerable<ArcSeg> GetHealthArc() => BuildArc( HealthProgress, 256f, 135, 225f, 28);
private IEnumerable<ArcSeg> GetShieldArc() => BuildArc( ShieldProgress, 256f, 45, -45f, 28 );
private static IEnumerable<ArcSeg> BuildArc( float progress, float radius, float start, float end, int count )
{
const float cx = 300f;
const float cy = 300f;
for ( int i = 0; i < count; i++ )
{
var t = count <= 1 ? 1f : i / (float)(count - 1);
var angle = start + (end - start) * t;
var rad = angle.DegreeToRadian();
var x = cx + MathF.Cos( rad ) * radius;
var y = cy + MathF.Sin( rad ) * radius;
yield return new ArcSeg( x, y, angle, (i + 1) / (float)count <= progress );
}
}
protected override int BuildHash()
=> HashCode.Combine( _health, _shield, _healthMax, _shieldMax, Pawn?.IsAlive );
}
@using Sandbox.UI
@inherits Panel
<style>
ScreenOverlay {
position: absolute;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
transition: all 0.5s sin-ease-in-out;
flex-direction: row;
z-index: 10;
&.hidden {
opacity: 0;
pointer-events: none;
}
.inner {
width: 95%;
height: 95%;
border: 5px solid rgba(255, 255, 255, 1);
background-image: url("ui/overlay/grid.png");
background-size: 12.5% 25%;
opacity: 0.02;
image-rendering: nearest-neighbor;
}
.icon {
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
transition: opacity 0.1s ease;
opacity: 0.025;
&.crosshair {
aspect-ratio: 1;
width: 5%;
opacity: 0.075;
}
}
}
</style>
<root>
<img class="icon" src="ui/crosshair/crosshair.png" />
<img class="icon crosshair" src="ui/crosshair/crosshair2.png" />
<label class="inner"></label>
</root>
@code
{
public override void Tick()
{
SetClass( "hidden", !(LocalPlayer.Pawn?.IsAlive ?? false) );
}
}
using Sandbox.UI;
namespace GuessIt;
public partial class GameCanvas
{
// Drawing Variables
bool IsDrawing = false;
List<Vector2> DrawingPoints = new List<Vector2>();
// UI Variables
Image Canvas { get; set; }
public void SetTexture( Texture texture )
{
Canvas.Texture = texture;
}
public void MarkDirty()
{
Canvas?.MarkRenderDirty();
}
protected override void OnAfterTreeRender( bool firstRender )
{
base.OnAfterTreeRender( firstRender );
if ( Canvas is not null && GameMenu.Instance?.Canvas is not null )
{
Canvas.Texture = GameMenu.Instance.Canvas;
}
}
protected override void OnMouseDown( MousePanelEvent e )
{
base.OnMouseDown( e );
Log.Info( $"[GameCanvas] OnMouseDown LocalPos={e.LocalPosition} CanvasSize={Canvas?.Box?.Rect.Size} PlayerIsDrawing={Player.Local?.IsDrawing}" );
DrawingPoints.Clear();
IsDrawing = true;
AddPoint( e.LocalPosition );
}
protected override void OnMouseMove( MousePanelEvent e )
{
base.OnMouseMove( e );
if ( IsDrawing )
{
bool returnVal = AddPoint( e.LocalPosition );
if ( returnVal == false )
{
Log.Info( $"[GameCanvas] AddPoint failed during move, stopping draw" );
StopDrawing();
}
}
}
protected override void OnMouseUp( MousePanelEvent e )
{
base.OnMouseUp( e );
StopDrawing();
}
void StopDrawing()
{
if ( !IsDrawing ) return;
IsDrawing = false;
if ( DrawingPoints.Count > 0 )
{
GameMenu.Instance.BroadcastDraw( DrawingPoints, GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
}
else
{
var canvasSize = Canvas?.Box?.Rect.Size ?? Vector2.One;
var pos = MousePosition / canvasSize * new Vector2( 320, 240 );
GameMenu.Instance.BroadcastDraw( pos, GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
}
DrawingPoints.Clear();
}
bool AddPoint( Vector2 vec2 )
{
if ( !(Player.Local?.IsDrawing ?? false) ) return false;
if ( !IsDrawing ) return false;
var canvasSize = Canvas?.Box?.Rect.Size ?? Vector2.Zero;
if ( canvasSize.x <= 0 || canvasSize.y <= 0 )
{
Log.Warning( $"[GameCanvas] Canvas size is zero or invalid: {canvasSize}" );
return false;
}
Vector2 pos = (vec2 / canvasSize) * new Vector2( 320, 240 );
if ( pos.x < 0 || pos.x > 320 || pos.y < 0 || pos.y > 240 ) return false;
DrawingPoints.Add( pos );
if ( DrawingPoints.Count > 1 )
{
GameMenu.Instance.Draw( DrawingPoints, GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
}
else
{
GameMenu.Instance.Draw( DrawingPoints[0], GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
}
if ( DrawingPoints.Count >= 5 )
{
var last = DrawingPoints.LastOrDefault();
GameMenu.Instance.BroadcastDraw( DrawingPoints, GameMenu.Instance.BrushColor, GameMenu.Instance.BrushSize );
DrawingPoints.Clear();
DrawingPoints.Add( last );
}
return true;
}
}
@using Sandbox;
@using Sandbox.UI;
@attribute [StyleSheet]
@namespace GuessIt
<root class="message">
@if (!string.IsNullOrEmpty(Name))
{
<label class="name">@Name</label>
}
<label class="content">@Content</label>
</root>
@code
{
public string Name { get; set; }
public string Content { get; set; }
public void SetMessage(string name, string content)
{
Name = name;
Content = content;
}
}@using System.Threading.Tasks;
@using Sandbox;
@using Sandbox.UI;
@using Sandbox.Network;
@namespace Battlebugs
@inherits Panel
@attribute [StyleSheet]
<root>
<label class="header">Lobbies</label>
<div class="lobbies">
@if (!refreshing && list.Count > 0)
{
@foreach (var lobby in list)
{
<div class="lobby" onclick=@(() => JoinLobby(lobby))>
<img src="ui/gamepad.png" />
<div class="info">
<label class="name">@lobby.Name</label>
@* <label class="desc">Looking for opponent...</label> *@
</div>
<div class="players">
<i>person</i>
<label>@lobby.Members/2</label>
</div>
</div>
}
}
else
{
<div class="no-lobbies">
No lobbies found...
</div>
}
@* Uncomment the block below to preview a lobby entry *@
@* <div class="lobby">
<img src="ui/gamepad.png" />
<div class="info">
<label class="name">Carson vs Bakscratch</label>
<label class="desc">Waiting for players...</label>
</div>
<div class="players">
<i>person</i>
<label>1/2</label>
</div>
</div> *@
</div>
</root>
@code
{
List<LobbyInformation> list = new();
bool refreshing = true;
protected override void OnAfterTreeRender(bool firstTime)
{
base.OnAfterTreeRender(firstTime);
if (firstTime)
{
_ = RefreshLobbyList();
}
}
async Task RefreshLobbyList()
{
while (true)
{
await Refresh();
await Task.DelayRealtimeSeconds(5f);
}
}
async Task Refresh()
{
refreshing = true;
StateHasChanged();
list = await Networking.QueryLobbies();
refreshing = false;
StateHasChanged();
}
void JoinLobby(LobbyInformation lobby)
{
Networking.Connect(lobby.LobbyId);
}
protected override int BuildHash() => System.HashCode.Combine("");
}