2021-12-24 03:36:16 +00:00
using System.Collections ;
using System.Collections.Generic ;
using UnityEngine ;
2023-01-05 04:04:31 +00:00
using HeavenStudio.Util ;
2023-03-11 04:51:22 +00:00
using HeavenStudio.Common ;
2023-10-29 19:44:47 +00:00
using HeavenStudio.InputSystem ;
using System ;
2023-12-05 22:38:52 +00:00
using System.Linq ;
2024-04-07 04:54:06 +00:00
using BurstLinq ;
2024-01-14 07:18:46 +00:00
using Jukebox ;
2023-01-05 04:04:31 +00:00
2022-03-14 14:21:05 +00:00
namespace HeavenStudio.Games
2021-12-24 03:36:16 +00:00
{
public class Minigame : MonoBehaviour
{
2024-05-12 22:45:23 +00:00
// root timing window values
2023-12-26 05:22:51 +00:00
public static double ngEarlyTimeBase = 0.1 , justEarlyTimeBase = 0.05 , aceEarlyTimeBase = 0.01 , aceLateTimeBase = 0.01 , justLateTimeBase = 0.05 , ngLateTimeBase = 0.1 ;
2024-05-12 22:45:23 +00:00
// recommended added margin for release inputs
public static double releaseMargin = 0.01 ;
2023-12-26 05:22:51 +00:00
public static double rankHiThreshold = 0.8 , rankOkThreshold = 0.6 ;
public static double ngEarlyTime = > ngEarlyTimeBase * Conductor . instance ? . SongPitch ? ? 1 ;
public static double justEarlyTime = > justEarlyTimeBase * Conductor . instance ? . SongPitch ? ? 1 ;
public static double aceEarlyTime = > aceEarlyTimeBase * Conductor . instance ? . SongPitch ? ? 1 ;
public static double aceLateTime = > aceLateTimeBase * Conductor . instance ? . SongPitch ? ? 1 ;
public static double justLateTime = > justLateTimeBase * Conductor . instance ? . SongPitch ? ? 1 ;
public static double ngLateTime = > ngLateTimeBase * Conductor . instance ? . SongPitch ? ? 1 ;
2023-01-05 04:04:31 +00:00
[SerializeField] public SoundSequence . SequenceKeyValue [ ] SoundSequences ;
2024-01-14 07:18:46 +00:00
[NonSerialized] public string minigameName ;
2024-01-19 04:52:38 +00:00
[NonSerialized] public GameManager gameManager ;
[NonSerialized] public Conductor conductor ;
2023-10-29 19:44:47 +00:00
#region Premade Input Actions
protected const int IAEmptyCat = - 1 ;
protected const int IAPressCat = 0 ;
protected const int IAReleaseCat = 1 ;
protected const int IAPressingCat = 2 ;
protected const int IAFlickCat = 3 ;
protected const int IAMAXCAT = 4 ;
2021-12-25 12:16:40 +00:00
2023-10-29 19:44:47 +00:00
protected static bool IA_Empty ( out double dt )
2021-12-25 12:16:40 +00:00
{
2023-10-29 19:44:47 +00:00
dt = 0 ;
return false ;
2021-12-25 12:16:40 +00:00
}
2021-12-25 02:37:03 +00:00
2023-10-29 19:44:47 +00:00
protected static bool IA_PadBasicPress ( out double dt )
{
return PlayerInput . GetPadDown ( InputController . ActionsPad . East , out dt ) ;
}
protected static bool IA_TouchBasicPress ( out double dt )
{
return PlayerInput . GetTouchDown ( InputController . ActionsTouch . Tap , out dt ) ;
}
protected static bool IA_BatonBasicPress ( out double dt )
{
return PlayerInput . GetBatonDown ( InputController . ActionsBaton . Face , out dt ) ;
}
protected static bool IA_PadBasicRelease ( out double dt )
{
return PlayerInput . GetPadUp ( InputController . ActionsPad . East , out dt ) ;
}
protected static bool IA_TouchBasicRelease ( out double dt )
{
return PlayerInput . GetTouchUp ( InputController . ActionsTouch . Tap , out dt ) & & ! PlayerInput . GetFlick ( out _ ) ;
}
protected static bool IA_BatonBasicRelease ( out double dt )
{
return PlayerInput . GetBatonUp ( InputController . ActionsBaton . Face , out dt ) ;
}
protected static bool IA_PadBasicPressing ( out double dt )
{
dt = 0 ;
return PlayerInput . GetPad ( InputController . ActionsPad . East ) ;
}
protected static bool IA_TouchBasicPressing ( out double dt )
{
dt = 0 ;
return PlayerInput . GetTouch ( InputController . ActionsTouch . Tap ) ;
}
protected static bool IA_BatonBasicPressing ( out double dt )
{
dt = 0 ;
return PlayerInput . GetBaton ( InputController . ActionsBaton . Face ) ;
}
protected static bool IA_TouchFlick ( out double dt )
{
return PlayerInput . GetFlick ( out dt ) ;
}
public static PlayerInput . InputAction InputAction_BasicPress =
new ( "BasicPress" , new int [ ] { IAPressCat , IAPressCat , IAPressCat } ,
IA_PadBasicPress , IA_TouchBasicPress , IA_BatonBasicPress ) ;
public static PlayerInput . InputAction InputAction_BasicRelease =
new ( "BasicRelease" , new int [ ] { IAReleaseCat , IAReleaseCat , IAReleaseCat } ,
IA_PadBasicRelease , IA_TouchBasicRelease , IA_BatonBasicRelease ) ;
public static PlayerInput . InputAction InputAction_BasicPressing =
new ( "BasicRelease" , new int [ ] { IAReleaseCat , IAReleaseCat , IAReleaseCat } ,
IA_PadBasicPressing , IA_TouchBasicPressing , IA_BatonBasicPressing ) ;
public static PlayerInput . InputAction InputAction_FlickPress =
new ( "FlickPress" , new int [ ] { IAPressCat , IAFlickCat , IAPressCat } ,
IA_PadBasicPress , IA_TouchFlick , IA_BatonBasicPress ) ;
public static PlayerInput . InputAction InputAction_FlickRelease =
new ( "FlickRelease" , new int [ ] { IAReleaseCat , IAFlickCat , IAReleaseCat } ,
IA_PadBasicRelease , IA_TouchFlick , IA_BatonBasicRelease ) ;
#endregion
2024-05-12 22:45:23 +00:00
[NonSerialized] public List < PlayerActionEvent > scheduledInputs = new List < PlayerActionEvent > ( ) ;
2022-05-04 18:05:51 +00:00
2023-10-29 19:44:47 +00:00
/// <summary>
/// Schedule an Input for a later time in the minigame. Executes the methods put in parameters
/// </summary>
/// <param name="startBeat">When the scheduling started (in beats)</param>
/// <param name="timer">How many beats later should the input be expected</param>
/// <param name="inputAction">The input action that's expected</param>
/// <param name="OnHit">Method to run if the Input has been Hit</param>
/// <param name="OnMiss">Method to run if the Input has been Missed</param>
/// <param name="OnBlank">Method to run whenever there's an Input while this is Scheduled (Shouldn't be used too much)</param>
2024-05-12 02:17:46 +00:00
/// <param name="ignoreSwing">Pretty much what it says on the tin, ignores swing when active. This'll probably end up being used for a whole one game lmao</param>
2023-10-29 19:44:47 +00:00
/// <returns></returns>
public PlayerActionEvent ScheduleInput (
double startBeat ,
double timer ,
PlayerInput . InputAction inputAction ,
PlayerActionEvent . ActionEventCallbackState OnHit ,
PlayerActionEvent . ActionEventCallback OnMiss ,
PlayerActionEvent . ActionEventCallback OnBlank ,
2024-05-12 02:17:46 +00:00
PlayerActionEvent . ActionEventHittableQuery HittableQuery = null ,
bool ignoreSwing = false
2023-10-29 19:44:47 +00:00
)
{
2024-01-14 07:18:46 +00:00
// List<RiqEntity> gameSwitches = GameManager.instance.Beatmap.Entities.FindAll(c => c.beat <= startBeat + timer && c.datamodel.Split("/")[0] == "switchGame");
// if (gameSwitches != null && gameSwitches[^1].datamodel.Split("/")[1] != gameObject.name)
// {
// return null;
// }
2023-10-29 19:44:47 +00:00
GameObject evtObj = new ( "ActionEvent" + ( startBeat + timer ) ) ;
PlayerActionEvent evt = evtObj . AddComponent < PlayerActionEvent > ( ) ;
2024-05-12 02:17:46 +00:00
if ( ignoreSwing ) evt . startBeat = Conductor . instance . GetSwungBeat ( startBeat ) ;
else evt . startBeat = startBeat ;
2023-10-29 19:44:47 +00:00
evt . timer = timer ;
evt . InputAction = inputAction ;
evt . OnHit = OnHit ;
evt . OnMiss = OnMiss ;
evt . OnBlank = OnBlank ;
evt . IsHittable = HittableQuery ;
evt . OnDestroy = RemoveScheduledInput ;
evt . canHit = true ;
evt . enabled = true ;
evt . transform . parent = this . transform . parent ;
2024-01-14 07:18:46 +00:00
evt . minigame = minigameName ;
2023-10-29 19:44:47 +00:00
evtObj . SetActive ( true ) ;
scheduledInputs . Add ( evt ) ;
return evt ;
}
2024-05-12 22:45:23 +00:00
public PlayerActionEvent ScheduleInput (
double startBeat ,
double timer ,
double margin ,
PlayerInput . InputAction inputAction ,
PlayerActionEvent . ActionEventCallbackState OnHit ,
PlayerActionEvent . ActionEventCallback OnMiss ,
PlayerActionEvent . ActionEventCallback OnBlank ,
PlayerActionEvent . ActionEventHittableQuery HittableQuery = null
)
{
PlayerActionEvent evt = ScheduleInput ( startBeat , timer , inputAction , OnHit , OnMiss , OnBlank , HittableQuery ) ;
evt . margin = margin ;
return evt ;
}
2023-10-29 19:44:47 +00:00
public PlayerActionEvent ScheduleAutoplayInput ( double startBeat ,
double timer ,
PlayerInput . InputAction inputAction ,
PlayerActionEvent . ActionEventCallbackState OnHit ,
PlayerActionEvent . ActionEventCallback OnMiss ,
PlayerActionEvent . ActionEventCallback OnBlank )
{
PlayerActionEvent evt = ScheduleInput ( startBeat , timer , inputAction , OnHit , OnMiss , OnBlank ) ;
evt . autoplayOnly = true ;
2024-01-14 07:18:46 +00:00
evt . minigame = minigameName ;
2023-10-29 19:44:47 +00:00
return evt ;
}
public PlayerActionEvent ScheduleUserInput ( double startBeat ,
double timer ,
PlayerInput . InputAction inputAction ,
PlayerActionEvent . ActionEventCallbackState OnHit ,
PlayerActionEvent . ActionEventCallback OnMiss ,
PlayerActionEvent . ActionEventCallback OnBlank ,
PlayerActionEvent . ActionEventHittableQuery HittableQuery = null )
{
PlayerActionEvent evt = ScheduleInput ( startBeat , timer , inputAction , OnHit , OnMiss , OnBlank , HittableQuery ) ;
evt . noAutoplay = true ;
2024-01-14 07:18:46 +00:00
evt . minigame = minigameName ;
2022-05-06 20:05:19 +00:00
return evt ;
}
2022-05-04 18:37:52 +00:00
//Clean up method used whenever a PlayerActionEvent has finished
2022-05-04 18:05:51 +00:00
public void RemoveScheduledInput ( PlayerActionEvent evt )
{
scheduledInputs . Remove ( evt ) ;
}
2023-10-29 19:44:47 +00:00
public PlayerActionEvent GetClosestScheduledInput ( int [ ] actionCats )
{
int catIdx = ( int ) PlayerInput . CurrentControlStyle ;
int cat = actionCats [ catIdx ] ;
PlayerActionEvent closest = null ;
foreach ( PlayerActionEvent toCompare in scheduledInputs )
{
// ignore inputs that are for sequencing in autoplay
if ( toCompare . autoplayOnly ) continue ;
if ( toCompare . InputAction = = null ) continue ;
if ( closest = = null )
{
if ( toCompare . InputAction . inputLockCategory [ catIdx ] = = cat )
closest = toCompare ;
}
else
{
double t1 = closest . startBeat + closest . timer ;
double t2 = toCompare . startBeat + toCompare . timer ;
if ( t2 < t1 )
{
if ( toCompare . InputAction . inputLockCategory [ catIdx ] = = cat )
closest = toCompare ;
}
}
}
return closest ;
}
public bool IsExpectingInputNow ( int [ ] wantActionCategory )
{
PlayerActionEvent input = GetClosestScheduledInput ( wantActionCategory ) ;
if ( input = = null ) return false ;
2024-05-12 22:45:23 +00:00
return input . IsExpectingInputNow ( conductor ) ;
2023-10-29 19:44:47 +00:00
}
public bool IsExpectingInputNow ( PlayerInput . InputAction wantAction )
{
return IsExpectingInputNow ( wantAction . inputLockCategory ) ;
}
2022-06-04 03:15:56 +00:00
// now should fix the fast bpm problem
2024-05-12 22:45:23 +00:00
public static double NgEarlyTime ( float pitch = - 1 , double margin = 0 )
2021-12-25 02:37:03 +00:00
{
2023-12-26 05:22:51 +00:00
if ( pitch < 0 )
2024-05-12 22:45:23 +00:00
return 1 - ( ngEarlyTime + margin ) ;
return 1 - ( ( ngEarlyTime + margin ) * pitch ) ;
2021-12-25 02:37:03 +00:00
}
2024-05-12 22:45:23 +00:00
public static double NgLateTime ( float pitch = - 1 , double margin = 0 )
2021-12-25 02:37:03 +00:00
{
2023-12-26 05:22:51 +00:00
if ( pitch < 0 )
2024-05-12 22:45:23 +00:00
return 1 + ( ngLateTime + margin ) ;
return 1 + ( ( ngLateTime + margin ) * pitch ) ;
2021-12-25 02:37:03 +00:00
}
2024-05-12 22:45:23 +00:00
public static double JustEarlyTime ( float pitch = - 1 , double margin = 0 )
2021-12-25 02:37:03 +00:00
{
2023-12-26 05:22:51 +00:00
if ( pitch < 0 )
2024-05-12 22:45:23 +00:00
return 1 - ( justEarlyTime + margin ) ;
return 1 - ( ( justEarlyTime + margin ) * pitch ) ;
2021-12-25 02:37:03 +00:00
}
2024-05-12 22:45:23 +00:00
public static double JustLateTime ( float pitch = - 1 , double margin = 0 )
2021-12-25 02:37:03 +00:00
{
2023-12-26 05:22:51 +00:00
if ( pitch < 0 )
2024-05-12 22:45:23 +00:00
return 1 + ( justLateTime + margin ) ;
return 1 + ( ( justLateTime + margin ) * pitch ) ;
2022-06-04 03:15:56 +00:00
}
2024-05-12 22:45:23 +00:00
public static double AceEarlyTime ( float pitch = - 1 , double margin = 0 )
2023-01-14 04:53:25 +00:00
{
2023-12-26 05:22:51 +00:00
if ( pitch < 0 )
2024-05-12 22:45:23 +00:00
return 1 - ( aceEarlyTime + margin ) ;
return 1 - ( ( aceEarlyTime + margin ) * pitch ) ;
2023-01-14 04:53:25 +00:00
}
2024-05-12 22:45:23 +00:00
public static double AceLateTime ( float pitch = - 1 , double margin = 0 )
2023-01-14 04:53:25 +00:00
{
2023-12-26 05:22:51 +00:00
if ( pitch < 0 )
2024-05-12 22:45:23 +00:00
return 1 + ( aceLateTime + margin ) ;
return 1 + ( ( aceLateTime + margin ) * pitch ) ;
2023-01-14 04:53:25 +00:00
}
2023-06-10 19:13:29 +00:00
public virtual void OnGameSwitch ( double beat )
2021-12-24 03:36:16 +00:00
{
}
2021-12-26 05:11:54 +00:00
public virtual void OnTimeChange ( )
{
}
2022-01-17 05:00:26 +00:00
2023-06-10 19:13:29 +00:00
public virtual void OnPlay ( double beat )
2022-09-18 20:48:14 +00:00
{
}
2023-06-10 19:13:29 +00:00
public virtual void OnStop ( double beat )
2023-05-07 20:33:15 +00:00
{
foreach ( var evt in scheduledInputs )
{
evt . Disable ( ) ;
}
}
2023-11-23 16:19:39 +00:00
// mainly for bopping logic
public virtual void OnBeatPulse ( double beat )
{
}
2023-12-31 04:06:31 +00:00
// added because OnBeatPulse had some animation issues going on
// if your bopping overlaps with other animations, use this instead
public virtual void OnLateBeatPulse ( double beat )
{
}
2023-06-10 19:13:29 +00:00
public static MultiSound PlaySoundSequence ( string game , string name , double startBeat , params SoundSequence . SequenceParams [ ] args )
2023-01-05 04:04:31 +00:00
{
2023-01-12 01:42:12 +00:00
Minigames . Minigame gameInfo = GameManager . instance . GetGameInfo ( game ) ;
foreach ( SoundSequence . SequenceKeyValue pair in gameInfo . LoadedSoundSequences )
2023-01-05 04:04:31 +00:00
{
if ( pair . name = = name )
{
2024-01-15 02:04:10 +00:00
// Debug.Log($"Playing sound sequence {pair.name} at beat {startBeat}");
2023-01-05 04:04:31 +00:00
return pair . sequence . Play ( startBeat ) ;
}
}
2023-01-12 01:42:12 +00:00
Debug . LogWarning ( $"Sound sequence {name} not found in game {game} (did you build AssetBundles?)" ) ;
2023-01-05 04:04:31 +00:00
return null ;
}
2023-01-16 03:05:25 +00:00
2023-12-26 05:22:51 +00:00
public void ScoreMiss ( float weight = 1f )
2023-01-25 03:54:19 +00:00
{
2023-12-26 05:22:51 +00:00
double beat = Conductor . instance ? . songPositionInBeatsAsDouble ? ? - 1 ;
2024-05-12 22:45:23 +00:00
GameManager . instance . ScoreInputAccuracy ( beat , 0 , true , NgLateTime ( ) , weight : weight , doDisplay : false ) ;
2023-01-25 03:54:19 +00:00
}
2023-10-29 19:44:47 +00:00
public void ToggleSplitColoursDisplay ( bool on )
{
}
2023-12-05 22:38:52 +00:00
#region Bop
protected enum DefaultBopEnum
{
Off = 0 ,
On = 1 ,
}
2023-12-12 16:54:04 +00:00
protected Dictionary < double , int > bopRegion = new ( ) ;
2023-12-05 22:38:52 +00:00
public bool BeatIsInBopRegion ( double beat )
{
if ( bopRegion . Count = = 0 ) return true ;
int bop = 0 ;
foreach ( var item in bopRegion )
{
if ( beat < item . Key ) break ;
if ( beat > = item . Key ) bop = item . Value ;
}
return ( DefaultBopEnum ) bop = = DefaultBopEnum . On ;
}
public int BeatIsInBopRegionInt ( double beat )
{
if ( bopRegion . Count = = 0 ) return 0 ;
int bop = 0 ;
foreach ( var item in bopRegion )
{
if ( beat < item . Key ) break ;
if ( beat > = item . Key ) bop = item . Value ;
}
return bop ;
}
protected void SetupBopRegion ( string gameName , string eventName , string toggleName , bool isBool = true )
{
var allEvents = EventCaller . GetAllInGameManagerList ( gameName , new string [ ] { eventName } ) ;
2023-12-12 16:54:04 +00:00
if ( allEvents . Count = = 0 ) return ;
2023-12-05 22:38:52 +00:00
allEvents . Sort ( ( x , y ) = > x . beat . CompareTo ( y . beat ) ) ;
foreach ( var e in allEvents )
{
2023-12-12 16:54:04 +00:00
if ( bopRegion . ContainsKey ( e . beat ) )
{
2024-01-15 02:04:10 +00:00
// Debug.Log("Two bops on the same beat, ignoring this one");
2023-12-12 16:54:04 +00:00
continue ;
}
2023-12-05 22:38:52 +00:00
if ( isBool )
{
bopRegion . Add ( e . beat , e [ toggleName ] ? 1 : 0 ) ;
}
else
{
bopRegion . Add ( e . beat , e [ toggleName ] ) ;
}
}
}
protected void AddBopRegionEvents ( string gameName , string eventName , bool allowBop )
{
var allEvents = EventCaller . GetAllInGameManagerList ( gameName , new string [ ] { eventName } ) ;
foreach ( var e in allEvents )
{
bopRegion . Add ( e . beat , allowBop ? 1 : 0 ) ;
}
bopRegion = bopRegion . OrderBy ( pair = > pair . Value ) . ToDictionary ( pair = > pair . Key , pair = > pair . Value ) ;
}
protected void AddBopRegionEventsInt ( string gameName , string eventName , int allowBop )
{
var allEvents = EventCaller . GetAllInGameManagerList ( gameName , new string [ ] { eventName } ) ;
foreach ( var e in allEvents )
{
bopRegion . Add ( e . beat , allowBop ) ;
}
bopRegion = bopRegion . OrderBy ( pair = > pair . Value ) . ToDictionary ( pair = > pair . Key , pair = > pair . Value ) ;
}
#endregion
2024-03-04 03:50:46 +00:00
#region Color
// truly a moment in history. documentation in heaven studio :)
public class ColorEase
{
/// <summary>
/// Gets the eased color from the variables inside a <c>ColorEase</c>. <br/>
/// Use this in <c>Update()</c>.
/// </summary>
/// <returns>A new color, based on <c>startColor</c> and <c>endColor</c>.</returns>
public Color GetColor ( ) = > MakeNewColor ( startBeat , length , startColor , endColor , easeFunc ) ;
public static Color MakeNewColor ( double beat , float length , Color start , Color end , Util . EasingFunction . Function func )
{
2024-05-12 22:45:23 +00:00
if ( length ! = 0 )
{
2024-03-04 03:50:46 +00:00
float normalizedBeat = length = = 0 ? 1 : Mathf . Clamp01 ( Conductor . instance . GetPositionFromBeat ( beat , length ) ) ;
float newR = func ( start . r , end . r , normalizedBeat ) ;
float newG = func ( start . g , end . g , normalizedBeat ) ;
float newB = func ( start . b , end . b , normalizedBeat ) ;
return new Color ( newR , newG , newB ) ;
2024-05-12 22:45:23 +00:00
}
else
{
2024-03-04 03:50:46 +00:00
return end ;
}
}
public double startBeat = 0 ;
public float length = 0 ;
public Color startColor , endColor = Color . white ;
public Util . EasingFunction . Ease ease = Util . EasingFunction . Ease . Instant ;
public readonly Util . EasingFunction . Function easeFunc ;
/// <summary>
/// The constructor to use when constructing a ColorEase from a block.
/// </summary>
/// <param name="startBeat">The start beat of the ease.</param>
/// <param name="length">The length of the ease.</param>
/// <param name="startColor">The beginning color of the ease.</param>
/// <param name="endColor">The end color of the ease.</param>
/// <param name="ease">
/// The ease to use to transition between <paramref name="startColor"/> and <paramref name="endColor"/>.<br/>
/// Should be derived from <c>Util.EasingFunction.Ease</c>,
/// </param>
2024-05-12 22:45:23 +00:00
public ColorEase ( double startBeat , float length , Color startColor , Color endColor , int ease )
{
2024-03-04 03:50:46 +00:00
this . startBeat = startBeat ;
this . length = length ;
( this . startColor , this . endColor ) = ( startColor , endColor ) ;
this . ease = ( Util . EasingFunction . Ease ) ease ;
this . easeFunc = Util . EasingFunction . GetEasingFunction ( this . ease ) ;
}
2024-05-12 22:45:23 +00:00
2024-03-04 03:50:46 +00:00
/// <summary>
/// The constructor to use when initializing the ColorEase variable.
/// </summary>
/// <param name="defaultColor">The default color to initialize with.</param>
2024-05-12 22:45:23 +00:00
public ColorEase ( Color ? defaultColor = null )
{
2024-03-04 03:50:46 +00:00
startColor = endColor = defaultColor ? ? Color . white ;
easeFunc = Util . EasingFunction . Instant ;
}
}
#endregion
2023-10-29 19:44:47 +00:00
private void OnDestroy ( )
{
2023-01-16 03:05:25 +00:00
foreach ( var evt in scheduledInputs )
{
evt . Disable ( ) ;
}
}
2023-03-11 04:51:22 +00:00
2023-10-29 19:44:47 +00:00
protected void OnDrawGizmos ( )
{
2023-03-11 04:51:22 +00:00
Gizmos . color = Color . magenta ;
Gizmos . DrawWireCube ( Vector3 . zero , new Vector3 ( 17.77695f , 10 , 0 ) ) ;
}
2021-12-24 03:36:16 +00:00
}
2022-06-24 00:05:27 +00:00
}