613 lines
21 KiB
C#
613 lines
21 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
/// <summary>
|
|
/// Załozenia na przyszłość
|
|
/// - player atakując powinien wysyłać informacje o akcji ataku z wartością obrażeń
|
|
/// enemy przymując ją powinien kalkulować szanse uniku / bloku (i odskakiwać w bokj lub do tyłu) albo przyjmować obrazenia
|
|
/// WSZĘDZIE ANIMACJA blokuąca inne akcje na czas odtwarzania efektu
|
|
/// </summary>
|
|
public enum WizardBattleStepsEnum
|
|
{
|
|
First, // have 100% hp, lasts until the value will be reduced to 7%%
|
|
Second, // have 75%% hp, lasts until the value will be reduced to 50%
|
|
Third, // have 50%% hp, lasts until the value will be reduced to 25%
|
|
Last,
|
|
Summoning, // after reducing hp to 75, 50, 25 per cent wizard start to summon minions
|
|
Escaping // when player have 25% he starts escape - go to teleport
|
|
}
|
|
|
|
[RequireComponent(typeof(NPC))]
|
|
[RequireComponent(typeof(AStarPathfindingAgent))]
|
|
class BattleWizard : MonoBehaviour
|
|
{
|
|
public const string BATTLE_STATE = "BattleState";
|
|
|
|
public WizardBattleStepsEnum BattleState = WizardBattleStepsEnum.First;
|
|
|
|
[Header("Health")]
|
|
public float defence = 0;
|
|
public int maxHealth = 100;
|
|
public float currentHealth;
|
|
|
|
public bool canTakeDamage = true;
|
|
|
|
public float attackValue = 5;
|
|
public float attackingRadius = 6;
|
|
|
|
[Header("Summoned Minions")]
|
|
public GameObject MinionRespowner;
|
|
public int SummonedMinionsCounter = 3;
|
|
public int KilledMinionsCounter = 0;
|
|
public bool IsAfterSummoning = false;
|
|
|
|
[Header("Step points")]
|
|
public bool approaching = false; // mean - we can go closer to Player
|
|
[Space]
|
|
public int CurrentPoint = 0;
|
|
public Transform[] Points;
|
|
public Vector3 TargetPosition;
|
|
|
|
|
|
[Space]
|
|
[Header("Attacking Logic")]
|
|
public bool hit = false;
|
|
|
|
public bool firstAttack = false;
|
|
|
|
public float timerDmg = 0f;
|
|
public float timeToWaitBeforeNextAttack = 1.0f; // time which npc must wait before he can atack player again
|
|
|
|
public float timerHit = 0f;
|
|
public float timeToWaitBeforeNextHitFromPlayer = 0.55f; // should be moved to player script !!!!
|
|
|
|
public bool isPanelEnabled = true; // flag about some panel status.... (probably youDied / respown)
|
|
|
|
private void Start()
|
|
{
|
|
MinionRespowner.GetComponent<CounterRespowner>().Respown = false;
|
|
MinionRespowner.GetComponent<CounterRespowner>().Counter = 0;
|
|
|
|
currentHealth = maxHealth;
|
|
|
|
|
|
if (HasProggress())
|
|
{
|
|
BattleState = GetProggress();
|
|
|
|
switch(BattleState)
|
|
{
|
|
case WizardBattleStepsEnum.First:
|
|
{
|
|
// to do nothing to do here
|
|
break;
|
|
}
|
|
case WizardBattleStepsEnum.Second:
|
|
{
|
|
BattleState = WizardBattleStepsEnum.First; // go one stop earlier to propertly go throught Escaping and Summoning
|
|
|
|
gameObject.transform.position = Points[0].position;
|
|
|
|
currentHealth = maxHealth * 0.75f;
|
|
|
|
SummonedMinionsCounter = 3;
|
|
|
|
CurrentPoint = 1;
|
|
break;
|
|
}
|
|
case WizardBattleStepsEnum.Third:
|
|
{
|
|
BattleState = WizardBattleStepsEnum.Second;
|
|
|
|
gameObject.transform.position = Points[1].position;
|
|
|
|
currentHealth = maxHealth * 0.50f;
|
|
|
|
SummonedMinionsCounter = 6;
|
|
|
|
CurrentPoint = 2;
|
|
|
|
break;
|
|
}
|
|
case WizardBattleStepsEnum.Last:
|
|
{
|
|
BattleState = WizardBattleStepsEnum.Third;
|
|
|
|
gameObject.transform.position = Points[2].position;
|
|
|
|
currentHealth = maxHealth * 0.25f;
|
|
|
|
SummonedMinionsCounter = 12;
|
|
|
|
CurrentPoint = 3;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gameObject.GetComponent<NPC>().State = NPCStateEnum.None;
|
|
|
|
BattleState = WizardBattleStepsEnum.First;
|
|
CurrentPoint = 0;
|
|
SummonedMinionsCounter = 3; // deffault value at start
|
|
}
|
|
|
|
defecnceCalculate();
|
|
speedCalculate();
|
|
|
|
// control battle proggress
|
|
ManageBehaviourScenario();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (gameObject.GetComponent<NPC>().State == NPCStateEnum.Walking)
|
|
{
|
|
var dir = TargetPosition - transform.position;
|
|
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
|
|
dir.Normalize();
|
|
|
|
gameObject.GetComponent<Animator>().SetBool("isRunning", new Vector2( dir.x, dir.y) != Vector2.zero);
|
|
}
|
|
|
|
// taking demage by time logic
|
|
TakingDamageManagment();
|
|
|
|
// take action based on npc state
|
|
HandleState();
|
|
|
|
|
|
// config battle depending on state
|
|
if (IsAfterSummoning)
|
|
{
|
|
//wait for killing minions by player
|
|
if (MinionRespowner.GetComponent<CounterRespowner>().killedMinions >= SummonedMinionsCounter * 3/5f)
|
|
{
|
|
SummonedMinionsCounter *= 2;
|
|
|
|
MinionRespowner.GetComponent<CounterRespowner>().killedMinions = 0;
|
|
IsAfterSummoning = false;
|
|
|
|
if ((WizardBattleStepsEnum)GetProggress() == WizardBattleStepsEnum.First)
|
|
BattleState = WizardBattleStepsEnum.Second;
|
|
else if ((WizardBattleStepsEnum)GetProggress() == WizardBattleStepsEnum.Second)
|
|
BattleState = WizardBattleStepsEnum.Third;
|
|
else if ((WizardBattleStepsEnum)GetProggress() == WizardBattleStepsEnum.Third)
|
|
BattleState = WizardBattleStepsEnum.Last;
|
|
|
|
approaching = true; // flag whivh tell to go in player direction
|
|
canTakeDamage = true;
|
|
gameObject.GetComponent<NPC>().State = NPCStateEnum.Walking;
|
|
|
|
SaveProggress(BattleState);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
public void OnTriggerEnter2D(Collider2D collision)
|
|
{
|
|
// after reaching teleport position remove component
|
|
if (collision.gameObject.tag == "SceneTransition")
|
|
{
|
|
collision.gameObject.GetComponent<DoorBehaviour>().isEnabled = true;
|
|
|
|
SaveProggress(WizardBattleStepsEnum.First);
|
|
|
|
Destroy(gameObject);
|
|
}
|
|
|
|
// Hit logic
|
|
if (collision.gameObject.tag == "AttackHitbox" || collision.gameObject.tag == "PickaxeHitbox")
|
|
{
|
|
hit = true;
|
|
}
|
|
}
|
|
|
|
|
|
public void OnTriggerExit2D(Collider2D collision)
|
|
{
|
|
// Hit logic
|
|
if (collision.gameObject.tag == "AttackHitbox" || collision.gameObject.tag == "PickaxeHitbox")
|
|
{
|
|
timerDmg = 0f;
|
|
|
|
hit = false;
|
|
}
|
|
}
|
|
|
|
// script controlling battle scenario by listening on change, depending on:
|
|
// - Wizard life
|
|
// - Wizard state
|
|
// - battle state
|
|
public void ManageBehaviourScenario()
|
|
{
|
|
// at first
|
|
if (BattleState == WizardBattleStepsEnum.First &&
|
|
gameObject.GetComponent<NPC>().State == NPCStateEnum.None)
|
|
{
|
|
Debug.Log("First");
|
|
approaching = true;
|
|
|
|
gameObject.GetComponent<NPC>().State = NPCStateEnum.Walking; // HandleState make rest - makes sure the wizard walks up to the player and cxhange state to attacking
|
|
} else
|
|
|
|
|
|
|
|
// detect health status after each taked damage from player (invoked in TakeDamage)
|
|
if ((BattleState == WizardBattleStepsEnum.First || BattleState == WizardBattleStepsEnum.Second || BattleState == WizardBattleStepsEnum.Third) &&
|
|
(gameObject.GetComponent<NPC>().State & NPCStateEnum.Attacking) > 0)
|
|
{
|
|
if (IsAfterSummoning == true)
|
|
return;
|
|
|
|
|
|
} else
|
|
|
|
|
|
|
|
// 1 Change state to pending & block damage taking & go to safe position
|
|
|
|
if (BattleState == WizardBattleStepsEnum.Escaping)
|
|
{
|
|
gameObject.GetComponent<AStarPathfindingAgent>().speed = 5f;
|
|
|
|
canTakeDamage = false;
|
|
|
|
approaching = false;
|
|
|
|
|
|
if (currentHealth <= maxHealth * 0.10f) // summon before
|
|
{
|
|
Debug.Log("Wizard HP critical");
|
|
TargetPosition = Points[Points.Count() - 1].position;
|
|
SummonManagment();
|
|
|
|
}
|
|
else
|
|
{
|
|
TargetPosition = Points[CurrentPoint].position;
|
|
|
|
CurrentPoint++;
|
|
}
|
|
|
|
gameObject.GetComponent<NPC>().State = NPCStateEnum.Walking; // go to newxt base point
|
|
} else
|
|
|
|
if (BattleState == WizardBattleStepsEnum.Summoning)
|
|
{
|
|
if(!IsAfterSummoning)
|
|
{
|
|
speedCalculate();
|
|
SummonManagment();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void HandleState()
|
|
{
|
|
switch (gameObject.GetComponent<NPC>().State)
|
|
{
|
|
case NPCStateEnum.Walking: // to player, to next point, to exit xd
|
|
{
|
|
WalkingManagment();
|
|
break;
|
|
}
|
|
case NPCStateEnum.Attacking:
|
|
{
|
|
gameObject.GetComponent<Animator>().SetBool("isRunning", false);
|
|
|
|
AttackManagment();
|
|
break;
|
|
}
|
|
case NPCStateEnum.Pending:
|
|
{
|
|
gameObject.GetComponent<Animator>().SetBool("isRunning", false);
|
|
|
|
BattleState = WizardBattleStepsEnum.Summoning;
|
|
|
|
ManageBehaviourScenario();
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void WalkingManagment()
|
|
{
|
|
StopAllCoroutines();
|
|
|
|
if(approaching)
|
|
{
|
|
// when we can go closer to player - we go closer and when we are close enought we start attacking
|
|
if (!IsInAttackRadious())
|
|
{
|
|
gameObject.GetComponent<AStarPathfindingAgent>().FindPath();
|
|
StartCoroutine(gameObject.GetComponent<AStarPathfindingAgent>().FollowPath());
|
|
}
|
|
else
|
|
{
|
|
timeToWaitBeforeNextAttack = 0; // to allowa to first hit withot waiting
|
|
|
|
// in this script we set attacking mode
|
|
gameObject.GetComponent<NPC>().State = NPCStateEnum.Attacking;
|
|
|
|
gameObject.GetComponent<AStarPathfindingAgent>().path.Clear(); // if we are able to talgk we dont want go go further player
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// we summon minions and go to next save point, else we escape
|
|
// IMPOSRANT make summoning and walking independent and unbinded in code
|
|
|
|
// first focus on targetting next step from list
|
|
//Debug.Log(Vector2.Distance(transform.position, TargetPosition));
|
|
if (Vector2.Distance(transform.position, TargetPosition) > 0.95f) // count value - path finding stop moving them before reachin position well..
|
|
{
|
|
gameObject.GetComponent<AStarPathfindingAgent>().point = TargetPosition;
|
|
|
|
gameObject.GetComponent<AStarPathfindingAgent>().FindPoint();
|
|
|
|
StartCoroutine(gameObject.GetComponent<AStarPathfindingAgent>().FollowPath());
|
|
}
|
|
else
|
|
{
|
|
// ------ anim.SetBool("isRunning", false);
|
|
|
|
gameObject.GetComponent<Animator>().SetBool("isRunning", false);
|
|
|
|
// set next point for future
|
|
gameObject.GetComponent<NPC>().State = NPCStateEnum.Pending; // decide what next
|
|
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public void SummonManagment()
|
|
{
|
|
|
|
// 2. Spown minions (depending on iteration increase spowned amount
|
|
|
|
BlockRespowners();
|
|
|
|
MinionRespowner.GetComponent<CounterRespowner>().Counter = SummonedMinionsCounter;
|
|
MinionRespowner.GetComponent<CounterRespowner>().Respown = true;
|
|
|
|
IsAfterSummoning = true;
|
|
Debug.Log("After summoning");
|
|
}
|
|
|
|
#region damage managment
|
|
/// <summary>
|
|
/// Take damage only once in a time and only when IT IS ALLOWED
|
|
/// </summary>
|
|
public void TakingDamageManagment()
|
|
{
|
|
// COPIED FROM Following Enemy scripts
|
|
// Taking hit logic
|
|
timerHit += Time.deltaTime;
|
|
if (hit == true && canTakeDamage)
|
|
{
|
|
if (timerHit >= timeToWaitBeforeNextHitFromPlayer)
|
|
{
|
|
|
|
TakeDamage(PlayerPrefs.GetFloat("attackValue"));
|
|
hit = false;
|
|
timerHit = 0f;
|
|
TakeKnockback();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TakeDamage(float damage)
|
|
{
|
|
var healthBeforeDamage = currentHealth;
|
|
|
|
damage = damage - defence;
|
|
damage = damage < 0 ? 0 : damage;
|
|
Debug.Log("Gamage from player: " + damage + "(defence: " + defence + ")");
|
|
currentHealth -= damage;
|
|
|
|
/* if (gameObject.GetComponent<Enemy>().health <= 0)
|
|
{
|
|
gameObject.SetActive(false);
|
|
gameObject.GetComponent<Enemy>().isKilled = 1;
|
|
GameObject.FindGameObjectWithTag("Player").GetComponent<Player>().GetExp(30);
|
|
|
|
// pass info about killing assigned enemy to mission manager listener
|
|
// pass enemy name from script NOT object name (thats allow to have many different objects variantsa with this same aggregate key (private name - not preffab name) )
|
|
ConditionManager.Instance.UpdateKillCondition(gameObject.GetComponent<Enemy>().MinionName);
|
|
}*/
|
|
|
|
if (
|
|
(healthBeforeDamage > maxHealth * 0.75f && currentHealth <= maxHealth * 0.75f) ||
|
|
(healthBeforeDamage > maxHealth * 0.50f && currentHealth <= maxHealth * 0.50f) ||
|
|
(healthBeforeDamage > maxHealth * 0.25f && currentHealth <= maxHealth * 0.25f) ||
|
|
(healthBeforeDamage > maxHealth * 0.10f && currentHealth <= maxHealth * 0.10f)
|
|
){
|
|
Debug.Log("escaping");
|
|
BattleState = WizardBattleStepsEnum.Escaping;
|
|
Debug.Log(BattleState);
|
|
|
|
// re-calculate after each damage
|
|
defecnceCalculate();
|
|
speedCalculate();
|
|
|
|
ManageBehaviourScenario();
|
|
}
|
|
}
|
|
|
|
private void TakeKnockback()
|
|
{
|
|
Rigidbody2D enemy = gameObject.GetComponent<Rigidbody2D>();
|
|
Rigidbody2D player = GameObject.FindGameObjectWithTag("Player").GetComponent<Rigidbody2D>();
|
|
|
|
if (enemy != null)
|
|
{
|
|
enemy.isKinematic = false;
|
|
Vector2 difference = enemy.transform.position - player.transform.position;
|
|
difference = difference.normalized * 5; // thrust
|
|
enemy.AddForce(difference, ForceMode2D.Impulse);
|
|
//StartCoroutine(KnockCo(enemy));
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region attack managment
|
|
|
|
public void AttackManagment()
|
|
{
|
|
// if during Attacking mode player GO OUT of the attacking radious
|
|
if (!IsInAttackRadious())
|
|
{
|
|
gameObject.GetComponent<NPC>().State = NPCStateEnum.Walking;
|
|
return;
|
|
}
|
|
|
|
// Attack logic
|
|
if (timerDmg >= timeToWaitBeforeNextAttack)
|
|
{
|
|
timerDmg = 0f;
|
|
|
|
GameObject.FindGameObjectWithTag("Player").GetComponent<Player>().TakeDamage(
|
|
attackValue,
|
|
isPanelEnabled
|
|
);
|
|
}
|
|
speedCalculate(); // to restore property timeToWaitBeforeNextAttack walue
|
|
|
|
timerDmg += Time.deltaTime;
|
|
}
|
|
|
|
public bool IsInAttackRadious()
|
|
{
|
|
if (Vector2.Distance(GameObject.FindGameObjectWithTag("Player").transform.position, transform.position) >= attackingRadius)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
#endregion
|
|
|
|
// jesli gracz ma "x" pkt siły
|
|
// Ze wzgledu na wyniki EXCEL'a zakładajac że
|
|
// - gracz ma 4/5 pkt sily (skile + bonus naszyjnika)
|
|
// obrona nie moze przekroczyc 3 - 3.2f
|
|
public void defecnceCalculate()
|
|
{
|
|
if (currentHealth > maxHealth * 0.75f)
|
|
{
|
|
defence = 1.5f;
|
|
}else if(currentHealth <= maxHealth * 0.75f && currentHealth > maxHealth * 0.50f)
|
|
{
|
|
defence = 2.5f;
|
|
}
|
|
else if (currentHealth <= maxHealth * 0.50f && currentHealth > maxHealth * 0.25f)
|
|
{
|
|
defence = 3.15f;
|
|
} else if (currentHealth <= maxHealth * 0.25f && currentHealth > maxHealth * 0.10f)
|
|
{
|
|
defence = 4f; // or higher ???
|
|
}
|
|
}
|
|
|
|
public void speedCalculate()
|
|
{
|
|
if (currentHealth > maxHealth * 0.75f)
|
|
{
|
|
gameObject.GetComponent<AStarPathfindingAgent>().speed = 1f;
|
|
timeToWaitBeforeNextAttack = 1f;
|
|
}
|
|
else if (currentHealth <= maxHealth * 0.75f && currentHealth > maxHealth * 0.50f)
|
|
{
|
|
gameObject.GetComponent<AStarPathfindingAgent>().speed = 1.15f;
|
|
timeToWaitBeforeNextAttack = 0.8f;
|
|
}
|
|
else if (currentHealth <= maxHealth * 0.50f && currentHealth > maxHealth * 0.25f)
|
|
{
|
|
gameObject.GetComponent<AStarPathfindingAgent>().speed = 1.30f;
|
|
timeToWaitBeforeNextAttack = 0.7f;
|
|
|
|
}
|
|
else if (currentHealth <= maxHealth * 0.25f && currentHealth > maxHealth * 0.10f)
|
|
{
|
|
gameObject.GetComponent<AStarPathfindingAgent>().speed = 1.45f;
|
|
timeToWaitBeforeNextAttack = 0.6f;
|
|
}
|
|
}
|
|
|
|
#region Respown minions managments
|
|
private void BlockRespowners()
|
|
{
|
|
ResetRespowners();
|
|
|
|
if (SummonedMinionsCounter == 3) // use only 1
|
|
{
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(1).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(2).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(3).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(4).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(5).GetComponent<MinionRespowner>().Blocked = true;
|
|
}
|
|
if (SummonedMinionsCounter == 6) // use 2 & 3
|
|
{
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(0).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(3).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(4).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(5).GetComponent<MinionRespowner>().Blocked = true;
|
|
}
|
|
if (SummonedMinionsCounter == 12) // use 2 & 4 & 5
|
|
{
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(0).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(1).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(5).GetComponent<MinionRespowner>().Blocked = true;
|
|
}
|
|
if (SummonedMinionsCounter > 12) // use 4 & 5 & 6
|
|
{
|
|
SummonedMinionsCounter = 15;
|
|
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(0).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(1).GetComponent<MinionRespowner>().Blocked = true;
|
|
MinionRespowner.GetComponent<CounterRespowner>().respownPoints.ElementAtOrDefault(2).GetComponent<MinionRespowner>().Blocked = true;
|
|
}
|
|
}
|
|
|
|
private void ResetRespowners()
|
|
{
|
|
foreach(var respownPointObject in MinionRespowner.GetComponent<CounterRespowner>().respownPoints)
|
|
{
|
|
respownPointObject.GetComponent<MinionRespowner>().Blocked = false;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region proggress API
|
|
public void SaveProggress(WizardBattleStepsEnum status)
|
|
{
|
|
Debug.Log("Save: " + status);
|
|
|
|
PlayerPrefs.SetInt(SceneManager.GetActiveScene().name + "." + BATTLE_STATE, (int)status);
|
|
}
|
|
|
|
public WizardBattleStepsEnum GetProggress()
|
|
{
|
|
return (WizardBattleStepsEnum)PlayerPrefs.GetInt(SceneManager.GetActiveScene().name + "." + BATTLE_STATE);
|
|
}
|
|
|
|
public bool HasProggress()
|
|
{
|
|
return PlayerPrefs.HasKey(SceneManager.GetActiveScene().name + "." + BATTLE_STATE);
|
|
}
|
|
#endregion
|
|
}
|