A Perk implementation named PerkChatInstruction (Voice of God) that intermittently issues in-game tasks via chat (e.g. say a message, don't move, touch arena edge). It tracks timers, validates player actions (chat, kills, hits, XP pickup, movement, position) and grants rewards or punishments using game APIs (spawning items/enemies, modifying player stats, playing SFX, showing messages).
using System;
using Sandbox;
[Perk( Rarity.Unique, locked: true, alwaysOfferDebug: false )]
public class PerkChatInstruction : Perk
{
private int _maxHpAdded;
private float _instrTimer;
private float _instrDelay;
private string _reqMessage;
private bool _isTaskActive;
private ChatTaskType _taskType;
private float _countdown;
private float _countdownTotal;
float CountdownTimeElapsed => _countdownTotal - _countdown;
private ArenaSide _targetArenaSide;
private enum ChatTaskType { SayMessage, DontMove, DontKill, ChopTree, TouchArenaSide, LetBasicZombieHurt, DontCollectXp, }
private enum ArenaSide { Left, Right, Bottom, Top }
private bool _isLockedInPlace;
private float _lockedInPlaceCountdown;
private const float LOCKED_IN_PLACE_TIME = 10f;
private bool _areControlsReversed;
private float _reversedControlsCountdown;
private const float REVERSED_CONTROLS_TIME = 10f;
public override float ImportanceMultiplier => 1.2f;
static PerkChatInstruction()
{
Register<PerkChatInstruction>(
name: "Voice of God",
imagePath: "textures/icons/vector/chat_instructions.png",
description: level => $"Obey God's instructions"
);
}
public override void Start()
{
base.Start();
ShouldUpdate = true;
_isTaskActive = false;
DisplayCooldownColor = new Color( 0.7f, 0.7f, 0.7f, 2f );
}
public override void IncreaseLevel()
{
base.IncreaseLevel();
_instrDelay = Game.Random.Float( 0.5f, 1.2f );
}
public override void Refresh()
{
base.Refresh();
}
public override void Update( float dt )
{
base.Update( dt );
if ( _isTaskActive )
{
switch ( _taskType )
{
case ChatTaskType.DontMove:
if ( Player.IsMoving && CountdownTimeElapsed > 2f )
{
Fail( ranOutOfTime: false );
}
break;
case ChatTaskType.TouchArenaSide:
float BUFFER = Player.Radius * 1.2f;
var pos = Player.Position2D;
if ( (_targetArenaSide == ArenaSide.Left && pos.x < Manager.Instance.BOUNDS_MIN.x + BUFFER) ||
(_targetArenaSide == ArenaSide.Right && pos.x > Manager.Instance.BOUNDS_MAX.x - BUFFER) ||
(_targetArenaSide == ArenaSide.Bottom && pos.y < Manager.Instance.BOUNDS_MIN.x + BUFFER) ||
(_targetArenaSide == ArenaSide.Top && pos.y > Manager.Instance.BOUNDS_MAX.y - BUFFER) )
{
Succeed();
}
break;
}
if ( _isTaskActive )
{
_countdown -= dt;
if ( _countdown <= 0f )
{
CountdownFinished();
}
}
}
else
{
_instrTimer += dt;
if ( _instrTimer > _instrDelay )
{
StartRandomTask();
_instrTimer = 0f;
_instrDelay = Game.Random.Float( 8f, 32f );
// todo: better sfx
Manager.Instance.PlaySfxUI( "god.question", pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f );
HighlightColor = new Color( 1f, 1f, 1f );
HighlightDuration = 1.25f;
HighlightOpacity = 3f;
Highlight();
}
}
if ( _isTaskActive )
{
DisplayText = MathX.CeilToInt( _countdown ).ToString();
DisplayCooldown = Utils.Map( _countdown, _countdownTotal, 0f, 1f, 0f );
}
if ( _isLockedInPlace )
{
_lockedInPlaceCountdown -= dt;
if ( _lockedInPlaceCountdown < 0f )
{
Player.StopModifying( this, PlayerStat.CantMove );
_isLockedInPlace = false;
}
}
if ( _areControlsReversed )
{
_reversedControlsCountdown -= dt;
if ( _reversedControlsCountdown < 0f )
{
Player.StopModifying( this, PlayerStat.ReverseMoveControls );
_areControlsReversed = false;
}
}
}
void StartRandomTask()
{
_isTaskActive = true;
// don't give chat typing instructions if game pauses while choosing or if player is using a controller
if ( (Manager.Instance.IsUnpausedChoosing && !Input.UsingController) && Game.Random.Float(0f, 1f) < 0.125f )
{
StartSayMessageTask();
}
else
{
int rand = Game.Random.Int( 0, 7 );
while ( rand == 2 && Player.Scene.GetAll<Tree>().Count() == 0 )
rand = Game.Random.Int( 0, 7 );
switch ( rand )
{
case 0: default: StartDontMoveTask(); break;
case 1: StartDontKillTask(); break;
case 2: StartChopTreeTask(); break;
case 3: StartTouchArenaSideTask(); break;
case 4: StartLetBasicZombieHurtTask(); break;
case 5: StartDontCollectXpTask(); break;
}
}
}
void StartSayMessageTask()
{
_taskType = ChatTaskType.SayMessage;
_countdown = _countdownTotal = Game.Random.Float( 9f, 17f );
bool mathQuestion = Game.Random.Int( 0, 4 ) == 0;
if ( mathQuestion )
{
int a = Game.Random.Int( 2, 16 );
int b = Game.Random.Int( 2, 16 );
string mathStr;
int operation = Game.Random.Int( 0, 2 );
switch ( operation )
{
case 0:
default:
_reqMessage = (a + b).ToString();
mathStr = $"{a} + {b}";
break;
case 1:
_reqMessage = (a - b).ToString();
mathStr = $"{a} - {b}";
break;
case 2:
_reqMessage = (a * b).ToString();
mathStr = $"{a} x {b}";
break;
}
AddLocalChatMessage( $"{SelectStr( "", "", "Math time! " )}What is {mathStr}?" );
}
else
{
_reqMessage = GetRandomReqMessage();
AddLocalChatMessage( $"{SelectStr( "Say", "Type" )} \"{_reqMessage}\" in chat!{SelectStr( "", " Don't misspell it!" )}" );
}
}
string GetRandomReqMessage()
{
int rand = Game.Random.Int( 0, 2 );
switch ( rand )
{
case 0: default: return $"{SelectStr( "", "", "hi ", "hello " )}{SelectStr( ":)", ":-)", ":>)", ":>]", ":-]", ":3" )}";
case 1: return SelectStr( $"i farted", $"{SelectStr( "i", "i", "u", "you" )} {SelectStr( "suck" )}", $"this game is {SelectStr( "cool", "fun", "dope", "awesome" )}", SelectStr( "sausage", "Sausage", "SAUSAGE", RandomizeCapitalization( "sausage" ) ) );
case 2: return SelectStr( $"{SelectStr( "i'm", "im" )} not {SelectStr( "crazy", "crazy!" )}", $"{SelectStr( "HAHAHA", "HAHAHAHA" )}" );
}
}
void StartDontMoveTask()
{
_taskType = ChatTaskType.DontMove;
_countdown = _countdownTotal = Game.Random.Float( 6f, 10f );
AddLocalChatMessage( SelectStr( $"Don't move!", $"Stop moving!", $"Don't you dare move an inch{SelectStr( ".", "!" )}", $"Don't move for {MathX.FloorToInt( _countdown )}s!" ) );
}
void StartDontKillTask()
{
_taskType = ChatTaskType.DontKill;
_countdown = _countdownTotal = Game.Random.Float( 8f, 14f );
AddLocalChatMessage( SelectStr( $"Don't kill anything!", $"Don't kill anything for {MathX.FloorToInt( _countdown )}s!" ) );
}
void StartChopTreeTask()
{
_taskType = ChatTaskType.ChopTree;
_countdown = _countdownTotal = Game.Random.Float( 16f, 24f );
AddLocalChatMessage( SelectStr( $"Destroy a tree{SelectStr( ".", "!" )}", $"Destroy a tree immediately{SelectStr( ".", "!" )}", $"Destroy a tree, now{SelectStr( ".", "!" )}", $"Your quest: destroy a tree{SelectStr( ".", "!" )}" ) );
}
void StartTouchArenaSideTask()
{
_taskType = ChatTaskType.TouchArenaSide;
_countdown = _countdownTotal = Game.Random.Float( 18f, 27f );
_targetArenaSide = (ArenaSide)Game.Random.Int( 0, Enum.GetValues( typeof( ArenaSide ) ).Length - 1 );
float BIG_BUFFER = Player.Radius * 5f;
var pos = Player.Position2D;
if ( _targetArenaSide == ArenaSide.Left && pos.x < Manager.Instance.BOUNDS_MIN.x + BIG_BUFFER )
_targetArenaSide = ArenaSide.Right;
else if ( _targetArenaSide == ArenaSide.Right && pos.x > Manager.Instance.BOUNDS_MAX.x - BIG_BUFFER )
_targetArenaSide = ArenaSide.Left;
else if ( _targetArenaSide == ArenaSide.Bottom && pos.y < Manager.Instance.BOUNDS_MIN.x + BIG_BUFFER )
_targetArenaSide = ArenaSide.Top;
else if ( _targetArenaSide == ArenaSide.Top && pos.y > Manager.Instance.BOUNDS_MAX.y - BIG_BUFFER )
_targetArenaSide = ArenaSide.Bottom;
AddLocalChatMessage( $"Touch the {GetStringForSide( _targetArenaSide )} edge of the arena{SelectStr( ".", "!" )}" );
}
void StartLetBasicZombieHurtTask()
{
_taskType = ChatTaskType.LetBasicZombieHurt;
_countdown = _countdownTotal = Game.Random.Float( 10f, 15f );
AddLocalChatMessage( $"Let a basic Zombie {SelectStr( "hit", "punch", "bite" )} you{SelectStr( ".", "!" )}" );
}
void StartDontCollectXpTask()
{
_taskType = ChatTaskType.DontCollectXp;
_countdown = _countdownTotal = Game.Random.Float( 10f, 16f );
AddLocalChatMessage( $"Don't {SelectStr( "get", "collect", "pick up" )} any coins{SelectStr( ".", "!" )}" );
}
string GetStringForSide( ArenaSide arenaSide )
{
switch ( arenaSide )
{
case ArenaSide.Left: default: return "left";
case ArenaSide.Right: return "right";
case ArenaSide.Bottom: return "bottom";
case ArenaSide.Top: return "top";
}
}
public override void OnSayChat( string message )
{
if ( _isTaskActive && _taskType == ChatTaskType.SayMessage )
{
if ( _reqMessage.Equals( message ) )
{
Succeed();
}
else
{
Fail( ranOutOfTime: false );
}
}
}
public override void OnKill( Enemy enemy, DamageType damageType, bool countsAsKill )
{
base.OnKill( enemy, damageType, countsAsKill );
if ( !_isTaskActive )
return;
if ( _taskType == ChatTaskType.DontKill && countsAsKill && CountdownTimeElapsed > 2f )
{
Fail( ranOutOfTime: false );
}
else if ( _taskType == ChatTaskType.ChopTree && enemy is Tree )
{
Succeed();
}
}
public override void OnHit( float amount, DamageType damageType, bool isSelfInflicted, Vector2 dir, float force, Enemy enemySource, EnemyType enemyType, float previousHealth )
{
base.OnHit( amount, damageType, isSelfInflicted, dir, force, enemySource, enemyType, previousHealth );
if ( !_isTaskActive )
return;
if ( _taskType == ChatTaskType.LetBasicZombieHurt && enemySource is Zombie )
{
Succeed();
}
}
public override void OnGainXpCoin( float xp )
{
base.OnGainXpCoin( xp );
if ( !_isTaskActive )
return;
if ( _taskType == ChatTaskType.DontCollectXp )
{
Fail( ranOutOfTime: false );
}
}
void CountdownFinished()
{
switch ( _taskType )
{
case ChatTaskType.SayMessage: default: Fail( ranOutOfTime: true ); break;
case ChatTaskType.DontMove: Succeed(); break;
case ChatTaskType.DontKill: Succeed(); break;
case ChatTaskType.DontCollectXp: Succeed(); break;
}
}
void Succeed()
{
GiveRandomReward();
// todo: better sfx
Manager.Instance.PlaySfxUI( "god.correct", pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f );
DisplayText = "✔️";
DisplayCooldown = 0f;
_isTaskActive = false;
HighlightColor = new Color( 0f, 1f, 0f );
HighlightDuration = 0.35f;
HighlightOpacity = 3f;
Highlight();
}
void Fail( bool ranOutOfTime )
{
GiveRandomPunishment( ranOutOfTime );
// todo: better sfx
Manager.Instance.PlaySfxUI( "god.wrong", pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f );
DisplayText = "❌";
DisplayCooldown = 0f;
_isTaskActive = false;
HighlightColor = new Color( 1f, 0f, 0f );
HighlightDuration = 0.35f;
HighlightOpacity = 3f;
Highlight();
}
// todo: sometimes msg is too long and becomes 2 lines
void GiveRandomReward()
{
string msgStart;
switch ( _taskType )
{
case ChatTaskType.SayMessage:
default:
msgStart = SelectStr( $"Correct{SelectStr( ".", "!" )}", $"That's right{SelectStr( ".", "!" )}", $"That's correct{SelectStr( ".", "!" )}", $"Well done{SelectStr( ".", "!" )}", "You did it!" );
break;
case ChatTaskType.DontMove:
msgStart = SelectStr( $"Well done{SelectStr( ".", "!" )}", $"Nice{SelectStr( ".", "!" )}" );
break;
case ChatTaskType.DontKill:
msgStart = SelectStr( $"Well done{SelectStr( ".", "!" )}", $"Nice{SelectStr( ".", "!" )}" );
break;
case ChatTaskType.ChopTree:
msgStart = SelectStr( $"Well done{SelectStr( ".", "!" )}", $"You showed that tree{SelectStr( ".", "!" )}" );
break;
case ChatTaskType.TouchArenaSide:
msgStart = SelectStr( $"Good hustle{SelectStr( ".", "!" )}", $"Nice moves{SelectStr( ".", "!" )}", $"Well done{SelectStr( ".", "!" )}" );
break;
case ChatTaskType.LetBasicZombieHurt:
msgStart = SelectStr( "How'd that feel?", "You took that hit well!", $"I'm proud of you{SelectStr( ".", "!" )}" );
break;
case ChatTaskType.DontCollectXp:
msgStart = SelectStr( $"Well done{SelectStr( ".", "!" )}", $"Nice{SelectStr( ".", "!" )}", $"Good job{SelectStr( ".", "!" )}" );
break;
}
string msgEnd = "";
int rand = Game.Random.Int( 0, 10 );
switch ( rand )
{
case 0:
default:
int MAX_HP = Game.Random.Int( 3, 6 );
msgEnd = $"{SelectStr( "Gain", "Plus" )} {MAX_HP} max hp{SelectStr( ".", "!" )}";
AddLocalChatMessage( $"✝️🤗 {msgStart} {msgEnd}" );
AdjustMaxHp( MAX_HP );
break;
case 1:
msgEnd = $"{SelectStr( "Have", "You get", "Here's" )} some xp{SelectStr( ".", "!" )}";
AddLocalChatMessage( $"✝️🤗 {msgStart} {msgEnd}" );
for ( int i = 0; i < 2; i++ )
{
Manager.Instance.SpawnCoinRpc( Player.Position2D, value: Game.Random.Int( 2, 4 ), Utils.GetRandomVectorInCone( -Player.FacingDir, coneDegrees: 150f ) );
}
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f );
Player.DodgeDuckRpc( dir: Utils.GetRandomVectorInCone( -Player.FacingDir, coneDegrees: 150f ), time: Game.Random.Float( 0.04f, 0.06f ) );
break;
case 2:
msgEnd = SelectStr( $"{SelectStr( "Have", "You get", "Here's" )} a healthpack{SelectStr( ".", "!" )}", "Need a healthpack?" );
AddLocalChatMessage( $"✝️🤗 {msgStart} {msgEnd}" );
Player.GiveItemRpc( "health_pack", Utils.GetRandomVectorInCone( -Player.FacingDir, coneDegrees: 200f ) );
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f );
break;
case 3:
msgEnd = $"{SelectStr( "Have", "You get", "Here's" )} a random perk:";
Player.GiveRandomPerkItemWithMessage( sourcePerkType: TypeLibrary.GetType( this.GetType() ), message: $"✝️🤗 {msgStart} {msgEnd}", isReward: true );
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f );
break;
case 4:
// todo: make this one more rare
msgEnd = $"{SelectStr( "Have", "You get", "Here's" )} a random Unique perk:";
Player.GiveRandomPerkItemWithMessage( sourcePerkType: TypeLibrary.GetType( this.GetType() ), message: $"✝️🤗 {msgStart} {msgEnd}", Rarity.Unique, isReward: true );
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 1.25f, 1.3f ), volume: 1f, maxDist: 300f );
break;
case 5:
msgEnd = $"{SelectStr( "Have", "You get", "Here are" )} some rerolls{SelectStr( ".", "!" )}";
AddLocalChatMessage( $"✝️🤗 {msgStart} {msgEnd}" );
for ( int i = 0; i < 3; i++ )
{
Manager.Instance.SpawnItemRpc( "reroll_item", Player.Position2D, Utils.GetRandomVectorInCone( -Player.FacingDir, coneDegrees: 150f ) );
}
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f );
Player.DodgeDuckRpc( Utils.GetRandomVectorInCone( -Player.FacingDir, coneDegrees: 150f ), time: Game.Random.Float( 0.04f, 0.06f ) );
break;
case 6:
msgEnd = $"{SelectStr( "Have", "You get", "Here's" )} a banish{SelectStr( ".", "!" )}";
AddLocalChatMessage( $"✝️🤗 {msgStart} {msgEnd}" );
Player.GiveItemRpc( "banish_item", Utils.GetRandomVectorInCone( -Player.FacingDir, coneDegrees: 150f ) );
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f );
break;
case 7:
msgEnd = $"{SelectStr( "Have", "You get", "Here's" )} a magnet{SelectStr( ".", "!" )}";
AddLocalChatMessage( $"✝️🤗 {msgStart} {msgEnd}" );
Player.GiveItemRpc( "magnet", Utils.GetRandomVectorInCone( -Player.FacingDir, coneDegrees: 150f ) );
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f );
break;
case 8:
msgEnd = $"{SelectStr( "Have", "You get", "Here's" )} a chest{SelectStr( ".", "!" )}";
AddLocalChatMessage( $"✝️🤗 {msgStart} {msgEnd}" );
{
Vector2 pos = Manager.Instance.ClampPosToBounds( Player.Position2D + Utils.GetRandomVector() * Game.Random.Float( 30f, 50f ) );
Manager.Instance.SpawnEnemyRpc( EnemyType.Chest, pos, rotAngle: -90f + Game.Random.Float( -30f, 30f ) );
}
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f );
break;
case 9:
msgEnd = SelectStr( $"{SelectStr( "Have", "You get", "Here are" )} some barrels{SelectStr( ".", "!" )}", $"Hope you like barrels{SelectStr( ".", "!" )}" );
AddLocalChatMessage( $"✝️🤗 {msgStart} {msgEnd}" );
for ( int i = 0; i < 2; i++ )
{
Vector2 pos = Manager.Instance.ClampPosToBounds( Player.Position2D + Utils.GetRandomVector() * Game.Random.Float( 30f, 60f ) );
Manager.Instance.SpawnEnemyRpc( EnemyType.Barrel, pos );
}
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f );
break;
case 10:
msgEnd = $"{SelectStr( "Have", "You get", "Here's" )} some armor{SelectStr( ".", "!" )}";
AddLocalChatMessage( $"✝️🤗 {msgStart} {msgEnd}" );
Player.GiveItemRpc( "armor_item", Utils.GetRandomVectorInCone( -Player.FacingDir, coneDegrees: 150f ) );
Manager.Instance.PlaySfxNearbyRpc( "scuffle", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1.05f ), volume: 1f, maxDist: 300f );
break;
}
}
void GiveRandomPunishment( bool ranOutOfTime )
{
string msgStart;
switch ( _taskType )
{
case ChatTaskType.SayMessage:
default:
msgStart = (ranOutOfTime ? SelectStr( $"Too slow{SelectStr( ".", "!" )}", "Took too long!" ) : SelectStr( $"Wrong{SelectStr( ".", "!" )}", $"No{SelectStr( ".", "!" )}", $"NO{SelectStr( ".", "!" )}" ));
break;
case ChatTaskType.DontMove:
msgStart = SelectStr( $"You moved{SelectStr( ".", "!" )}", "I told you not to move!", "Why did you move?" );
break;
case ChatTaskType.DontKill:
msgStart = SelectStr( "You killed something!", $"I told you not to kill anything{SelectStr( ".", "!" )}" );
break;
case ChatTaskType.ChopTree:
msgStart = SelectStr( "You didn't destroy a tree!", $"I told you to destroy a tree{SelectStr( ".", "!" )}", "Why didn't you destroy a tree?", $"You took too long{SelectStr( ".", "!" )}" );
break;
case ChatTaskType.TouchArenaSide:
msgStart = SelectStr( $"You are too slow{SelectStr( ".", "!" )}", "You took too long!", $"Stop making me wait{SelectStr( ".", "!" )}" );
break;
case ChatTaskType.LetBasicZombieHurt:
msgStart = SelectStr( "Scared of a basic Zombie?", $"Coward{SelectStr( ".", "!" )}" );
break;
case ChatTaskType.DontCollectXp:
msgStart = SelectStr( $"You have no patience{SelectStr( ".", "!" )}", $"You're too greedy{SelectStr( ".", "!" )}", $"Covetous sinner{SelectStr( ".", "!" )}" );
break;
}
string msgEnd = "";
int rand = Game.Random.Int( 0, 9 );
while ( rand == 2 && Player.Scene.GetAll<Tree>().Count() > 20 )
rand = Game.Random.Int( 0, 9 );
switch ( rand )
{
case 0:
default:
int DMG = Game.Random.Int( 1, 35 );
msgEnd = $"Take {DMG} damage{SelectStr( ".", "!" )}";
Player.Damage( DMG, DamageType.Self, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: Game.Random.Float(0f, 100f), ragdollForce: 1f, enemySource: null, enemyType: EnemyType.None );
break;
case 1:
msgEnd = $"Get burnt{SelectStr( ".", "!", "..." )}";
Manager.Instance.SpawnFireRingRpc( Player.Position2D, 150f, 15, damage: 5f, lifetime: Game.Random.Float( 8f, 10f ), startDegrees: 0f, scale: 1f, Color.Red, Color.Yellow, playerSource: null, enemySource: null, enemyType: EnemyType.None );
Manager.Instance.SpawnFireGroundRpc( Player.Position2D, player: null, enemySource: null, enemyType: EnemyType.None, damage: 5f, lifetime: Game.Random.Float( 8f, 10f ), spreadChance: 0.05f, canStack: false, scale: 1f, colorA: Color.Red, colorB: Color.Yellow );
Manager.Instance.PlaySfxNearbyRpc( "burn", Player.Position2D, pitch: Game.Random.Float( 0.95f, 1f ), volume: 0.9f, maxDist: 300f );
break;
case 2:
msgEnd = $"{SelectStr( "You're in", "Take a" )} time-out{SelectStr( ".", "!", "..." )}";
Manager.Instance.SpawnEnemyRingRpc( EnemyType.Tree, Player.Position2D, 80f, 8 );
break;
case 3:
msgEnd = $"Watch your {SelectStr( "step.", "step!" )}";
Manager.Instance.SpawnLandmineRingRpc( Player.Position2D, Game.Random.Float( 80f, 120f ), Game.Random.Int( 7, 13 ) );
break;
case 4:
msgEnd = $"You must {SelectStr( "be punished.", "be punished!", "feel pain." )}";
{
float currAngle = Game.Random.Float( 0f, 360f );
Manager.Instance.SpawnAcidPuddleRingRpc( Player.Position2D, radius: 90f, num: Game.Random.Int( 5, 6 ), scale: Game.Random.Float( 1.1f, 1.25f ), lifetime: Game.Random.Float( 8f, 12f ), damage: 8f, playerSource: null, enemySource: null, enemyType: EnemyType.None, currAngle );
currAngle += Game.Random.Float( 90f, 110f );
Manager.Instance.SpawnAcidPuddleRingRpc( Player.Position2D, radius: Game.Random.Float( 160f, 190f ), num: Game.Random.Int( 6, 7 ), scale: Game.Random.Float( 1.35f, 1.45f ), lifetime: Game.Random.Float( 11f, 13f ), damage: 8f, playerSource: null, enemySource: null, enemyType: EnemyType.None, currAngle );
}
break;
case 5:
int MAX_HP = Game.Random.Int( 1, 10 );
Player.Damage( MAX_HP, DamageType.Self, Player.Position2D, Utils.GetRandomVector(), upwardAmount: 0f, force: Game.Random.Float( 0f, 100f ), ragdollForce: 1f, enemySource: null, enemyType: EnemyType.None );
msgEnd = $"Lose {MAX_HP} max hp!";
AdjustMaxHp( -MAX_HP );
break;
case 6:
msgEnd = SelectStr( "Have a curse!", "Enjoy a curse!", "Free curse!" );
// todo: curse punishment
//var curseType = GetRandomCurseType();
//int tries = 0;
//while ( Player.HasUpgrade( curseType ) && tries < 15 )
//{
// curseType = GetRandomCurseType();
// tries++;
//}
//Player.GiveUpgradePowerup( curseType );
break;
case 7:
msgEnd = $"Now you can't move for {LOCKED_IN_PLACE_TIME}s!";
// todo: kneel on ground while can't move
Player.Modify( this, PlayerStat.CantMove, 1f, ModifierType.Add );
_isLockedInPlace = true;
_lockedInPlaceCountdown = LOCKED_IN_PLACE_TIME;
break;
case 8: // todo: don't pick this one if PlayerStat.ReverseMoveControls is already > 0f
msgEnd = $"{SelectStr( "Reversed movement", "Confusion" )} for {REVERSED_CONTROLS_TIME}s!";
Player.Modify( this, PlayerStat.ReverseMoveControls, 1f, ModifierType.Add );
_areControlsReversed = true;
_reversedControlsCountdown = REVERSED_CONTROLS_TIME;
break;
case 9:
msgEnd = $"Time to die!";
Manager.Instance.SpawnMiniboss( Player.Position2D + Utils.GetRandomVector() * Game.Random.Float( 90f, 120f ) );
break;
}
AddLocalChatMessage($"✝️😡 {msgStart} {msgEnd}");
}
// todo:
//TypeDescription GetRandomCurseType()
//{
// int rand = Game.Random.Int( 0, 10 );
// switch ( rand )
// {
// case 0: default: return TypeLibrary.GetType( typeof( CurseBacksideUpgrade ) );
// case 1: return TypeLibrary.GetType( typeof( CurseCommonUpgrade ) );
// case 2: return TypeLibrary.GetType( typeof( CurseMaxHpUpgrade ) );
// case 3: return TypeLibrary.GetType( typeof( CurseRerollUpgrade ) );
// case 4: return TypeLibrary.GetType( typeof( CurseTurnSpeedUpgrade ) );
// case 5: return TypeLibrary.GetType( typeof( CurseRegenUpgrade ) );
// case 6: return TypeLibrary.GetType( typeof( CurseLessChoicesUpgrade ) );
// case 7: return TypeLibrary.GetType( typeof( CursePiercingUpgrade ) );
// case 8: return TypeLibrary.GetType( typeof( CurseCritChanceUpgrade ) );
// case 9: return TypeLibrary.GetType( typeof( CurseCritMultiplierUpgrade ) );
// case 10: return TypeLibrary.GetType( typeof( CurseTeleportUpgrade ) );
// }
//}
void AdjustMaxHp( int amount)
{
if ( amount == 0 )
return;
_maxHpAdded += amount;
Player.Modify( this, PlayerStat.MaxHp, _maxHpAdded, ModifierType.Add );
var color = amount > 0 ? new Color( 0f, 0.8f, 0f ) : new Color( 0.8f, 0.4f, 0.4f );
Manager.Instance.SpawnFloaterTextRpc( Player.WorldPosition.WithZ( 65f ), $"{(amount > 0 ? "+" : "-")}{amount} MAX HP!", color, size: 1.2f, floaterType: amount > 0 ? FloaterType.PositiveMessage : FloaterType.NegativeMessage );
}
public static string RandomizeCapitalization( string input )
{
char[] characters = input.ToCharArray();
for ( int i = 0; i < characters.Length; i++ )
{
if ( Char.IsLetter( characters[i] ) )
{
characters[i] = Game.Random.Int( 0, 1 ) == 0 ? Char.ToLower( characters[i] ) : Char.ToUpper( characters[i] );
}
}
return new string( characters );
}
public static string SelectStr( params string[] strings )
{
if ( strings == null || strings.Length == 0 )
return "";
return strings[Game.Random.Int( 0, strings.Length - 1 )];
}
public override void OnDie()
{
base.OnDie();
Player.StopModifying( this, PlayerStat.CantMove );
Player.StopModifying( this, PlayerStat.ReverseMoveControls );
}
void AddLocalChatMessage( string message )
{
Manager.Instance.Chat.AddLocalChatMessage( $"{Perk.GetRichTextToken( GetType() )} {message}", from: "" );
}
}