Last active
June 28, 2022 17:23
-
-
Save benanil/9ce2d7a68e56177026acac4c21e55b57 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#undef UNITY_EDITOR | |
using System; | |
using System.Collections.Generic; | |
using System.Runtime.CompilerServices; | |
using UnityEngine; | |
using UnityEngine.SceneManagement; | |
using UnityEngine.UI; | |
public class Character : MonoBehaviour | |
{ | |
public static Character instance; | |
public Transform firstStair; | |
public Transform secondStair; | |
public Transform TrapPrefab; | |
public Transform skullPrefab; | |
public Transform RecordLineTransform; | |
public TMPro.TextMeshProUGUI comboTextPrefab; | |
public Transform[] chunkPrefabs = new Transform[Biom.BiomCount]; | |
public PrayerGroup[] prayerPrefabs = new PrayerGroup[3]; | |
public Image CoinPrefab; | |
[SerializeField] | |
private uint state = State.playing; | |
[Header("stair")] | |
public float stepInterval = 0.6f; | |
public float moveSpeed = 1; | |
public float blockSpeed = 1; | |
public Vector3 characterOffset; | |
private float stepTimer; | |
public int money; | |
public AudioClip stairStepSound; | |
private AudioSource audioSource; | |
public ParticleSystem checkpointParticle; | |
[Header("tiredness")] | |
public float tiredness; | |
public float tirednessSpeed = 0.5f; | |
public float maxTiredness = 30; | |
public float regeneration = 0.1f; | |
public Material characterMaterial; | |
private readonly TirednessStage[] tirednessStages = new TirednessStage[5]; | |
private int tirednessStage; | |
private int lastTirednessStage; | |
public float tirednessSinceLastStartup = 30; | |
public ParticleSystem sweatingParticle; | |
private Vector3 characterStartScale; | |
private FootTracer[] footTracers; | |
[Header("UI")] | |
[SerializeField] private GameObject checkpointUI; | |
[SerializeField] private GameObject pauseMenu; | |
internal void PlayFootstepSound() | |
{ | |
audioSource.PlayOneShot(stairStepSound); | |
} | |
[SerializeField] private GameObject playerUI; | |
[SerializeField] private GameObject failMenu; | |
[SerializeField] private GameObject HighScoreGO; | |
[SerializeField] private Transform coinImage; | |
[SerializeField] private Image stepsToNextLevelSlider; | |
[SerializeField] private TMPro.TextMeshProUGUI moneyText; | |
[SerializeField] private TMPro.TextMeshProUGUI MettersPassedText; | |
[SerializeField] private TMPro.TextMeshProUGUI MettersRecordText; | |
[Header("Upgrade")] | |
public int staminaLevel; | |
public int moneyEarningLevel; | |
public int speedLevel; | |
private int staminaPrice = 250, incomePrice = 250, speedPrice = 250; | |
[SerializeField] TMPro.TextMeshProUGUI staminaPriceText, incomePriceText, speedPriceText; | |
[SerializeField] TMPro.TextMeshProUGUI staminaLevelText, incomeLevelText, speedLevelText; | |
[SerializeField] TMPro.TextMeshProUGUI LevelText; | |
public int moneyPerStair = 50; | |
public float speedPerUpgrade = 0.05f; | |
public float staminaPerLevel = 5; | |
public Button staminaButton, incomeButton, speedButton; | |
public ParticleSystem upgradeParticle; | |
public ParticleSystem windParticle; | |
private Vector3 stairDiff; | |
// Systems | |
private WalkSystem walkSystem; | |
private CoinSystem coinSystem; | |
private ComboTextSystem comboTextSystem; | |
private InfiniteSystem infiniteSystem; | |
private Animator animator; | |
private bool startedMoving; | |
private Vector3 coinStartScale; | |
private Vector3 coinSmallScale; | |
private float targetFov; | |
private float startFov; | |
private float characterStartXPos; | |
private static float highestMeter = 100; | |
private bool recordPassed = true; | |
private AnimListenner listenner; | |
private float targetWalkAnimValue; | |
private float animSpeedAdition; | |
// Combo | |
private int ComboIndex = 0; | |
private Dictionary<int, Combo> combos = new Dictionary<int, Combo>(3); // unordered_map , map | |
private Combo currentCombo; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
private static float EaseIn(float x) { return x * x * x; } | |
private void SetCharacterRedness() | |
{ | |
float tirednessMap = tirednessSinceLastStartup / maxTiredness; | |
float t = tiredness / (maxTiredness * tirednessMap); | |
characterMaterial.SetFloat("_Value", EaseIn(t) * 10.0f); | |
listenner.SetColor(t); | |
} | |
//////// | |
private void Awake() { instance = this; } | |
//////// | |
private void Start() | |
{ | |
tirednessSinceLastStartup = maxTiredness; | |
listenner = FindObjectOfType<AnimListenner>(); | |
targetWalkAnimValue = 1.0f; | |
tirednessStage = lastTirednessStage = 0; | |
characterStartScale = transform.localScale; | |
targetFov = Camera.main.fieldOfView; | |
startFov = targetFov; | |
coinStartScale = coinImage.localScale; | |
coinSmallScale = coinImage.localScale / 1.4f; | |
characterStartXPos = transform.position.x; | |
stairDiff = firstStair.position - secondStair.position; | |
secondStair.position = transform.position - characterOffset; | |
characterMaterial.SetFloat("_Value", 0); | |
transform.GetChild(4).TryGetComponent(out animator); | |
TryGetComponent(out audioSource); | |
animator.SetFloat("Speed", 0); | |
Load(); | |
float targetChunkMeterRecord = Mathf.Approximately(lastChunkMeterRecord, 0) ? 400 : lastChunkMeterRecord; | |
RecordLineTransform.position = new Vector3(Mathf.Min(-targetChunkMeterRecord, -400) , -3.79999995f, -0.0799999982f); | |
RecordLineTransform.GetChild(0).GetComponent<TMPro.TextMeshPro>().text = Mathf.Max(targetChunkMeterRecord + 300, 700).ToString() + 'm'; | |
recordPassed = false; | |
footTracers = FindObjectsOfType<FootTracer>(); | |
// initialize systems | |
// | |
walkSystem = new WalkSystem(transform); | |
coinSystem = new CoinSystem(); | |
infiniteSystem = new InfiniteSystem(); | |
comboTextSystem = new ComboTextSystem(); | |
// | |
infiniteSystem.SetCurrentBiom((uint)PlayerPrefs.GetInt("BiomIndex" + Character.PV)); | |
tirednessStages[0] = new TirednessStage( | |
onActivate : () => | |
{ | |
sweatingParticle.Stop(); | |
targetWalkAnimValue = 1; | |
characterMaterial.color = Color.white; | |
characterMaterial.SetFloat("_Value", 0); | |
}, | |
onDeactivate: () => { }, | |
onEvaluete : (float dt) => { SetCharacterRedness(); } | |
); | |
tirednessStages[1] = new TirednessStage( | |
onActivate : () => { targetWalkAnimValue = 2; sweatingParticle.Play(); }, | |
onDeactivate: () => { sweatingParticle.Stop(); }, | |
onEvaluete : (float dt) => { SetCharacterRedness(); } | |
); | |
tirednessStages[2] = new TirednessStage( | |
onActivate : () => { targetWalkAnimValue = 3; sweatingParticle.Play(); }, | |
onDeactivate: () => { | |
sweatingParticle.Stop(); | |
transform.localScale = characterStartScale; | |
}, | |
onEvaluete : (float dt) => { | |
SetCharacterRedness(); | |
transform.localScale = characterStartScale + (0.025f * Mathf.Sin(Time.timeSinceLevelLoad * 8) * Vector3.one); | |
} | |
); | |
tirednessStages[3] = tirednessStages[2]; | |
tirednessStages[4] = new TirednessStage( | |
onActivate : () => {targetWalkAnimValue = 3; sweatingParticle.Play(); }, | |
onDeactivate: () => { | |
sweatingParticle.Stop(); | |
transform.localScale = characterStartScale; | |
}, | |
onEvaluete : (float dt) => { | |
SetCharacterRedness(); | |
transform.localScale = characterStartScale + (0.045f * Mathf.Sin(Time.timeSinceLevelLoad * 14) * Vector3.one); | |
} | |
); | |
combos.Add(16, new Combo( | |
onActivate : () => { | |
animSpeedAdition += 0.4f; | |
moveSpeed += 2.5f; | |
targetFov = startFov + 2.5f; | |
windParticle.gameObject.SetActive(true); | |
windParticle.Play(); | |
stepInterval -= 0.02f; | |
moneyPerStair += 5; | |
}, | |
onDeactivate: () => { | |
animSpeedAdition -= 0.4f; | |
moveSpeed -= 2.5f; | |
targetFov = startFov; | |
windParticle.gameObject.SetActive(false); | |
stepInterval += 0.02f; | |
moneyPerStair -= 5; | |
}, | |
onEvaluete : (float dt) => {} | |
)); | |
combos.Add(32, new Combo( | |
onActivate : () => { | |
animSpeedAdition += 0.4f; | |
moveSpeed += 2.5f; | |
targetFov = startFov + 5f; | |
stepInterval -= 0.02f; | |
moneyPerStair += 5; | |
}, | |
onDeactivate: () => { | |
animSpeedAdition -= 0.4f; | |
moveSpeed -= 2.5f; | |
targetFov = startFov; | |
windParticle.gameObject.SetActive(false); | |
stepInterval += 0.02f; | |
moneyPerStair -= 5; | |
}, | |
onEvaluete : (float dt) => { } | |
)); | |
combos.Add(64, new Combo( | |
onActivate : () => { | |
animSpeedAdition += 0.4f; | |
moveSpeed += 2.5f; | |
targetFov = startFov + 7.5f; | |
stepInterval -= 0.02f; | |
moneyPerStair += 10; | |
}, | |
onDeactivate: () => { | |
animSpeedAdition -= 0.4f; | |
moveSpeed -= 2.5f; | |
targetFov = startFov; | |
windParticle.gameObject.SetActive(false); | |
stepInterval += 0.02f; | |
moneyPerStair -= 10; | |
}, // also revert first and second combo upgrades | |
onEvaluete : (float dt) => { } | |
)); | |
Canvas canvas = FindObjectOfType<Canvas>(); | |
for (int i = 0; i < CoinSystem.MaximumCoins; i++) | |
coinSystem.PoolCoin(Instantiate(CoinPrefab, parent: canvas.transform)); | |
for (int i = 0; i < ComboTextSystem.MaximumCoins; i++) | |
comboTextSystem.PoolCoin(Instantiate(comboTextPrefab, parent: canvas.transform)); | |
for (uint biomIndex = 0; biomIndex < Biom.BiomCount; ++biomIndex) | |
for (int i = 0; i < InfiniteSystem.MaximumChunks; i++) | |
infiniteSystem.AddChunk(biomIndex, Instantiate(chunkPrefabs[biomIndex], new Vector3(0, -8.2f, 0), Quaternion.identity)); | |
for (int i = 0; i < 3; i++) | |
infiniteSystem.AddPrayer(Instantiate(prayerPrefabs[i])); | |
infiniteSystem.PlaceChunks(); | |
firstStair.gameObject.SetActive(false); | |
secondStair.gameObject.SetActive(false); | |
UpdateUpgradeMenu(); | |
} | |
public void AddStair() | |
{ | |
coinSystem.MoveCoin(transform.position - characterOffset, moneyPerStair); | |
money += moneyPerStair; | |
moneyText.text = FormatNumber(money); | |
startedMoving = true; | |
ComboIndex++; | |
coinImage.localScale = coinStartScale; | |
if (combos.TryGetValue(ComboIndex, out Combo combo)) | |
{ | |
comboTextSystem.MoveCoin(transform.position + new Vector3(0,0,2), ComboIndex); | |
combo.Activate(); | |
currentCombo = combo; | |
} | |
} | |
private void EliminateCombos() | |
{ | |
if (currentCombo != null && ComboIndex != 0) | |
{ | |
foreach (var combo in combos) | |
if (combo.Key <= ComboIndex) | |
combo.Value.Deactivate(); | |
} | |
ComboIndex = 0; | |
currentCombo = null; | |
} | |
private float lastChunkMeterRecord; | |
private float stopTimer; | |
public void CheckPoint() | |
{ | |
// open upgrade ui, play dance animation, blow up confetti, | |
playerUI.SetActive(false); | |
checkpointParticle.gameObject.SetActive(true); | |
checkpointParticle.Play(); | |
ToggleCheckpointUI(true); | |
float mettersPassed = Mathf.Abs(characterStartXPos - transform.position.x); | |
lastChunkMeterRecord = mettersPassed + 100; | |
TirednessStage currentTirednessStage = tirednessStages[tirednessStage]; | |
currentTirednessStage.Deactivate(); | |
tirednessStages[0].Activate(); | |
coinSystem.DisposeCurrentMovingCoins(); | |
EliminateCombos(); | |
ContinuePressed = false; | |
int.TryParse(LevelText.text[5..], out int level); | |
LevelText.text = "Level " + (level+1).ToString(); | |
animator.SetFloat("Speed", 0.0f); | |
ComboIndex = tirednessStage = 0; | |
tiredness = 0.0f; | |
state = State.paused; | |
} | |
private void CloseHighScore() { HighScoreGO.SetActive(false); } | |
private void Update() | |
{ | |
// handle coins | |
coinSystem.Update(Time.deltaTime); | |
float mettersPassed = Mathf.Abs(characterStartXPos - transform.position.x); | |
MettersPassedText.text = (mettersPassed + lastChunkMeterRecord).ToString(".00"); | |
if ((state & State.playing) == 0 || state == State.dead || !ContinuePressed) return; | |
stepsToNextLevelSlider.fillAmount = | |
infiniteSystem.MettersPassedSinceLastBiom(transform.position.x) | |
/ infiniteSystem.TotalMetersToNextBiom; | |
float deltaTime = Time.deltaTime; | |
if (transform.position.x < RecordLineTransform.position.x) | |
{ | |
highestMeter = mettersPassed; | |
if (!recordPassed) | |
{ | |
PlayerPrefs.SetFloat("lastChunkMeterRecord" + PV, lastChunkMeterRecord + 400); | |
HighScoreGO.SetActive(true); | |
Invoke(nameof(CloseHighScore), 2); | |
recordPassed = true; | |
} | |
} | |
if (MettersRecordText) MettersRecordText.text = highestMeter.ToString(".00"); | |
float tirednessMap = tirednessSinceLastStartup / maxTiredness; | |
TirednessStage currentTirednessStage; | |
{ | |
float oneStagePercent = tirednessStages.Length / (maxTiredness / tirednessMap); // 4 / 40 = 0.1 = s; | |
tirednessStage = Mathf.Max(Mathf.FloorToInt((oneStagePercent * tiredness) - 1), 0); | |
currentTirednessStage = tirednessStages[tirednessStage]; | |
currentTirednessStage.Evaluete(deltaTime); | |
} | |
currentCombo?.Evaluate(deltaTime); | |
if (Input.GetMouseButton(0)) | |
{ | |
animator.speed = 1 + animSpeedAdition + (speedLevel * 0.12f); | |
stepTimer += deltaTime; // stEP | |
tiredness += deltaTime * tirednessSpeed; | |
stopTimer += deltaTime; // stOP | |
if (stopTimer > 0.21f) | |
{ | |
animator.SetFloat("Speed", Mathf.Lerp(animator.GetFloat("Speed"), targetWalkAnimValue, Time.deltaTime * 8)); | |
walkSystem.Walk(moveSpeed); | |
foreach (FootTracer tracer in footTracers) tracer.UpdateThisThing(); | |
} | |
if (tirednessStage != lastTirednessStage) | |
{ | |
currentTirednessStage.Deactivate(); | |
lastTirednessStage = tirednessStage; | |
TirednessStage newTirednessStage = tirednessStages[tirednessStage]; | |
newTirednessStage.Activate(); | |
} | |
const float bias = 3.0f; | |
if (tiredness - bias >= tirednessSinceLastStartup * tirednessMap) | |
{ | |
Fail(); | |
return; | |
} | |
} | |
else | |
{ | |
stopTimer = 0; | |
animator.SetFloat("Speed", Mathf.Lerp(animator.GetFloat("Speed"), 0.0f, Time.deltaTime * 12)); | |
tiredness = Mathf.Max(tiredness - (deltaTime * regeneration), 0); | |
if (!Mathf.Approximately(tiredness, 0)) | |
{ | |
// reduce maximum stamina | |
tirednessSinceLastStartup = MathF.Max(tirednessSinceLastStartup - (deltaTime * 0.6f), 1); | |
} | |
} | |
if (stepTimer >= stepInterval) | |
{ | |
AddStair(); | |
stepTimer = 0; | |
} | |
if (Input.GetMouseButtonUp(0)) | |
{ | |
stepTimer = 0; | |
EliminateCombos(); | |
} | |
Camera.main.fieldOfView = Mathf.LerpUnclamped(Camera.main.fieldOfView, targetFov, Time.deltaTime * 3); | |
coinImage.localScale = Vector3.LerpUnclamped(coinImage.localScale, coinSmallScale, deltaTime * 3); | |
{ | |
walkSystem.Update(moveSpeed); | |
infiniteSystem.Update(transform.position.x); | |
comboTextSystem.Update(Time.deltaTime); | |
} | |
} | |
public void Fail() | |
{ | |
if (!startedMoving || state != State.playing) return; | |
animator.SetTrigger("Die"); | |
currentCombo?.Deactivate(); | |
tirednessStages[^1].Deactivate(); | |
Invoke(nameof(Fail2), 3.5f); | |
playerUI.SetActive(false); | |
animator.SetFloat("Speed", 0); | |
Save(); | |
state = State.dead; | |
if (transform.position.x < RecordLineTransform.position.x) | |
{ | |
HighScoreGO.SetActive(true); | |
Invoke(nameof(CloseHighScore), 2); | |
recordPassed = true; | |
} | |
} | |
public GameObject LevelGOLeftUpper; | |
public GameObject CoinGORightUpper; | |
private void Fail2() | |
{ | |
LevelGOLeftUpper.SetActive(false); | |
CoinGORightUpper.SetActive(false); | |
failMenu.SetActive(true); | |
} | |
public void Quit() { Application.Quit(); } | |
// player pref version | |
public const char PV = '5'; | |
// save load | |
private void Save(int biomeAddition = 0) | |
{ | |
PlayerPrefs.SetInt("Money" + PV, (int)money); | |
PlayerPrefs.SetInt("Stamina" + PV, staminaLevel); | |
PlayerPrefs.SetInt("Speed" + PV, speedLevel); | |
PlayerPrefs.SetInt("Income" + PV, moneyEarningLevel); | |
PlayerPrefs.SetFloat("MeterRecore" + PV, highestMeter); | |
int.TryParse(LevelText.text[5..], out int level); | |
PlayerPrefs.SetInt("Level" + PV, level); | |
} | |
// returns false if there is no save | |
private bool Load() | |
{ | |
// if (!PlayerPrefs.HasKey("Money")) return false; | |
money = Mathf.Max(PlayerPrefs.GetInt("Money" + PV), 0); | |
int level = PlayerPrefs.GetInt("Level" + PV); | |
LevelText.text = "Level " + (level == 0 ? 1 : level); | |
highestMeter = PlayerPrefs.GetFloat("MeterRecore" + PV); | |
lastChunkMeterRecord = PlayerPrefs.GetFloat("lastChunkMeterRecord" + PV) ; | |
LoadSkills(PlayerPrefs.GetInt("Stamina" + PV), PlayerPrefs.GetInt("Income" + PV), PlayerPrefs.GetInt("Speed" + PV)); | |
return true; | |
} | |
public void Restart() { SceneManager.LoadScene(0); } | |
public void ResetGame()// button event | |
{ | |
PlayerPrefs.SetInt("Stamina" + PV, 0); | |
PlayerPrefs.SetInt("Speed" + PV, 0); | |
PlayerPrefs.SetInt("Income" + PV, 0); | |
PlayerPrefs.SetInt("CheckpointIndex" + PV, 0); | |
PlayerPrefs.SetInt("Money" + PV, 0); | |
SceneManager.LoadScene(0); | |
} | |
// Upgrade | |
[ContextMenu("UpgradeStamina")] | |
public void UpgradeStamina() { // button event | |
if (money < staminaPrice || staminaLevel >= 8) return; | |
money -= staminaPrice; | |
maxTiredness += staminaPerLevel; | |
staminaPrice += staminaPrice / 2; | |
tirednessSinceLastStartup = maxTiredness; | |
staminaLevel++; | |
staminaLevelText.text = staminaLevel.ToString() + "lvl"; | |
UpdateUpgradeMenu(); | |
} | |
[ContextMenu("UpgradeIncome")] | |
public void UpgradeIncome() { // button event | |
if (money < incomePrice || moneyEarningLevel >= 8) return; | |
money -= incomePrice; | |
moneyPerStair += moneyPerStair / 2; | |
incomePrice += incomePrice / 2; | |
moneyEarningLevel++; | |
incomeLevelText.text = moneyEarningLevel.ToString() + "lvl"; | |
UpdateUpgradeMenu(); | |
} | |
[ContextMenu("UpgradeSpeed")] | |
public void UpgradeSpeed() { // button event | |
if (money < speedPrice || speedLevel >= 8) return; | |
money -= speedPrice; | |
stepInterval = Mathf.Max(stepInterval - speedPerUpgrade, 0.1f) ; | |
moveSpeed += 2.0f; | |
animSpeedAdition += 0.4f; | |
speedPrice += speedPrice / 2; | |
speedLevel++; | |
speedLevelText.text = speedLevel.ToString() + "lvl"; | |
UpdateUpgradeMenu(); | |
} | |
public void LoadSkills(int stamina, int income, int speed) { | |
staminaLevel = stamina; moneyEarningLevel = income; speedLevel = speed; | |
speedLevelText.text = speedLevel.ToString() + "lvl"; | |
incomeLevelText.text = moneyEarningLevel.ToString() + "lvl"; | |
staminaLevelText.text = staminaLevel.ToString() + "lvl"; | |
while (income -- > 0) { | |
moneyPerStair += moneyPerStair / 2; | |
incomePrice += incomePrice / 2; | |
} | |
while (stamina-- > 0) { | |
maxTiredness += staminaPerLevel; | |
staminaPrice += staminaPrice / 2; | |
} | |
while (speed-- > 0) { | |
animSpeedAdition += 0.4f; | |
stepInterval = Mathf.Max(stepInterval - speedPerUpgrade, 0.1f); | |
moveSpeed += 2.0f; | |
speedPrice += speedPrice / 2; | |
} | |
tirednessSinceLastStartup = maxTiredness; | |
UpdateUpgradeMenu(); | |
} | |
private bool ContinuePressed = false; | |
// UI | |
public void ToggleCheckpointUI(bool open) { checkpointUI.SetActive(open); UpdateUpgradeMenu(); } | |
public void TogglePlayerUI() { playerUI.SetActive(!playerUI.activeSelf); } | |
public void TogglePauseMenu() { pauseMenu.SetActive(!pauseMenu.activeSelf); } | |
public void Continue() | |
{ | |
ContinuePressed = true; | |
state = State.playing; | |
Save(1); | |
AddStair(); | |
playerUI.SetActive(true); | |
checkpointParticle.Stop(); | |
checkpointParticle.gameObject.SetActive(false); | |
} | |
public void TogleTimeScale() | |
{ | |
Time.timeScale = Mathf.Approximately(Time.timeScale, 1.0f) ? 0.0f : 1.0f; | |
state = state == State.playing ? State.paused : State.playing; | |
} | |
public void UpdateUpgradeMenu() | |
{ | |
staminaButton.enabled = money >= staminaPrice && staminaLevel < 8; | |
incomeButton .enabled = money >= incomePrice && moneyEarningLevel < 8; | |
speedButton .enabled = money >= speedPrice && speedLevel < 8; | |
staminaPriceText.text = FormatNumber(staminaPrice); | |
incomePriceText.text = FormatNumber(incomePrice); | |
speedPriceText.text = FormatNumber(speedPrice); | |
moneyText.text = FormatNumber(money); | |
} | |
public static string FormatNumber(int num) | |
{ | |
if (num >= 100000000) | |
return (num / 1000000).ToString("#,0M"); | |
if (num >= 10000000) | |
return (num / 1000000).ToString("0.#") + "M"; | |
if (num >= 100000) | |
return (num / 1000).ToString("#,0K"); | |
if (num >= 10000) | |
return (num / 1000).ToString("0.#") + "K"; | |
return num.ToString("#,0"); | |
} | |
} // character class end | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment