Implemented room blocking in game.
This commit is contained in:
parent
18908b2ae7
commit
1e95378bb1
24 changed files with 25172 additions and 65 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -56,7 +56,7 @@ MonoBehaviour:
|
|||
litRoom: {fileID: 7153068010997067763, guid: f64f7ca52dead154780dd3ce1ef99a90, type: 3}
|
||||
AdjacentRooms: []
|
||||
IsEntrance: 0
|
||||
roomReward: {fileID: 0}
|
||||
roomRewards: {fileID: 0}
|
||||
numberTextObject: {fileID: 8119019481281764985}
|
||||
--- !u!114 &6904757263627452010
|
||||
MonoBehaviour:
|
||||
|
|
@ -70,9 +70,9 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: 686125cb666fff047a08043aa1feb0a8, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
diamonds: 0
|
||||
damage: 0
|
||||
chest: 0
|
||||
Diamonds: 0
|
||||
Damage: 0
|
||||
Chest: 0
|
||||
--- !u!212 &1770509300873928573
|
||||
SpriteRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
|
|||
12531
PuzzleGameProject/Assets/Scenes/GoodMap.unity
Normal file
12531
PuzzleGameProject/Assets/Scenes/GoodMap.unity
Normal file
File diff suppressed because it is too large
Load diff
7
PuzzleGameProject/Assets/Scenes/GoodMap.unity.meta
Normal file
7
PuzzleGameProject/Assets/Scenes/GoodMap.unity.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bc314d02d714775469a85c4b40cca0ac
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
12389
PuzzleGameProject/Assets/Scenes/good.unity
Normal file
12389
PuzzleGameProject/Assets/Scenes/good.unity
Normal file
File diff suppressed because it is too large
Load diff
7
PuzzleGameProject/Assets/Scenes/good.unity.meta
Normal file
7
PuzzleGameProject/Assets/Scenes/good.unity.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 62ea064c9ca6e9644a9a6d448767cf86
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -14,7 +14,7 @@ public class ChestRewardSelection : MonoBehaviour
|
|||
|
||||
private void OnEnable()
|
||||
{
|
||||
RoomReward.ChestRewarded += HandChestRewarded;
|
||||
RoomRewards.ChestRewarded += HandChestRewarded;
|
||||
diamondAndLifeButton.onClick.AddListener(HandleDiamondAndLifeSelected);
|
||||
torchButton.onClick.AddListener(HandleTorchSelected);
|
||||
blackDiceButton.onClick.AddListener(HandleBlackDiceSelected);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ using UnityEngine;
|
|||
|
||||
public static class ColorHelper
|
||||
{
|
||||
public static Color OkayGreen = new Color(12f, 202f, 0f);
|
||||
public static readonly Color OkayGreen = new Color(12f, 202f, 0f);
|
||||
|
||||
public const float TEXT_FADED_OPACITY = .5f;
|
||||
public const float TEXT_FULL_OPACITY = 1f;
|
||||
|
||||
public static Color AddColorTint(Color originalColor, Color tintColor, float tintIntensity)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using DungeonMapGenerator;
|
||||
using DungeonMapGenerator.Rooms;
|
||||
using Unity.VisualScripting;
|
||||
using UnityEditor;
|
||||
using Object = UnityEngine.Object;
|
||||
|
|
@ -25,25 +26,32 @@ namespace DungeonGenerator
|
|||
GameObject bossRoomPrefab = Resources.Load<GameObject>(BOSS_ROOM);
|
||||
GameObject monsterRoomPrefab = Resources.Load<GameObject>(MONSTER_ROOM);
|
||||
GameObject normalRoomPrefab = Resources.Load<GameObject>(NORMAL_ROOM);
|
||||
|
||||
Dictionary<GameObject, DungeonMapGenerator.Rooms.Room> gameObjectRoomPairs = new Dictionary<GameObject, DungeonMapGenerator.Rooms.Room>();
|
||||
|
||||
GameObject bossRoomGO = PrefabUtility.InstantiatePrefab(bossRoomPrefab, gameObject.transform) as GameObject;
|
||||
bossRoomGO.transform.position = ConvertToUnityPosition(map.GetBossRoom().GetCenterOfRoom(), map.Width, map.Height);
|
||||
AddLockToRoomObject(bossRoomGO, map.GetBossRoom().Lock.GetLock());
|
||||
foreach (DungeonMapGenerator.Lock extraLock in map.GetBossRoom().ExtraLocks)
|
||||
BossRoom bossRoom = map.GetBossRoom();
|
||||
gameObjectRoomPairs[bossRoomGO] = bossRoom;
|
||||
AddLockToRoomObject(bossRoomGO, bossRoom.Lock.GetLock());
|
||||
foreach (DungeonMapGenerator.Lock extraLock in bossRoom.ExtraLocks)
|
||||
{
|
||||
AddLockToRoomObject(bossRoomGO, extraLock.GetLock());
|
||||
}
|
||||
_roomIdToGameObject[map.GetBossRoom().Id] = bossRoomGO;
|
||||
AddRewardsToRoomObject(bossRoomGO, bossRoom.GetLoot());
|
||||
_roomIdToGameObject[bossRoom.Id] = bossRoomGO;
|
||||
|
||||
foreach (var monsterRoom in map.GetMonsterRooms())
|
||||
{
|
||||
GameObject monsterRoomGO = PrefabUtility.InstantiatePrefab(monsterRoomPrefab, gameObject.transform) as GameObject;
|
||||
monsterRoomGO.transform.position = ConvertToUnityPosition(monsterRoom.GetCenterOfRoom(), map.Width, map.Height);
|
||||
gameObjectRoomPairs[monsterRoomGO] = monsterRoom;
|
||||
AddLockToRoomObject(monsterRoomGO, monsterRoom.Lock.GetLock());
|
||||
foreach (DungeonMapGenerator.Lock extraLock in monsterRoom.ExtraLocks)
|
||||
{
|
||||
AddLockToRoomObject(monsterRoomGO, extraLock.GetLock());
|
||||
}
|
||||
AddRewardsToRoomObject(monsterRoomGO, monsterRoom.GetLoot());
|
||||
_roomIdToGameObject[monsterRoom.Id] = monsterRoomGO;
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +59,9 @@ namespace DungeonGenerator
|
|||
{
|
||||
GameObject normalRoomGO = PrefabUtility.InstantiatePrefab(normalRoomPrefab, gameObject.transform) as GameObject;
|
||||
normalRoomGO.transform.position = ConvertToUnityPosition(normalRoom.GetCenterOfRoom(), map.Width, map.Height);
|
||||
gameObjectRoomPairs[normalRoomGO] = normalRoom;
|
||||
AddLockToRoomObject(normalRoomGO, normalRoom.Lock.GetLock());
|
||||
AddRewardsToRoomObject(normalRoomGO, normalRoom.GetLoot());
|
||||
_roomIdToGameObject[normalRoom.Id] = normalRoomGO;
|
||||
}
|
||||
|
||||
|
|
@ -59,8 +69,10 @@ namespace DungeonGenerator
|
|||
{
|
||||
GameObject entranceRoomGO = PrefabUtility.InstantiatePrefab(normalRoomPrefab, gameObject.transform) as GameObject;
|
||||
entranceRoomGO.transform.position = ConvertToUnityPosition(entranceRoom.GetCenterOfRoom(), map.Width, map.Height);
|
||||
gameObjectRoomPairs[entranceRoomGO] = entranceRoom;
|
||||
entranceRoomGO.GetComponent<Room>().IsEntrance = true;
|
||||
AddLockToRoomObject(entranceRoomGO, entranceRoom.Lock.GetLock());
|
||||
AddRewardsToRoomObject(entranceRoomGO, entranceRoom.GetLoot());
|
||||
_roomIdToGameObject[entranceRoom.Id] = entranceRoomGO;
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +85,14 @@ namespace DungeonGenerator
|
|||
roomComponent.AdjacentRooms.Add(_roomIdToGameObject[id]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kvp in gameObjectRoomPairs)
|
||||
{
|
||||
if (kvp.Value.GetType() == typeof(DungeonMapGenerator.Rooms.MonsterRoom) || kvp.Value.GetType() == typeof(DungeonMapGenerator.Rooms.BossRoom))
|
||||
{
|
||||
AddBlockingRooms(kvp.Key.GetComponent<Room>(), (DungeonMapGenerator.Rooms.MonsterRoom)kvp.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 ConvertToUnityPosition(Point dungeonPosition, int mapWidth, int mapHeight)
|
||||
|
|
@ -112,5 +132,34 @@ namespace DungeonGenerator
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRewardsToRoomObject(GameObject roomGO, List<LootType> loot)
|
||||
{
|
||||
RoomRewards rewards = roomGO.AddComponent<RoomRewards>();
|
||||
foreach (LootType type in loot)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case LootType.Chest:
|
||||
rewards.Chest = true;
|
||||
break;
|
||||
case LootType.Diamond:
|
||||
rewards.Diamonds++;
|
||||
break;
|
||||
case LootType.Damage:
|
||||
rewards.Damage++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddBlockingRooms(Room roomComponent, DungeonMapGenerator.Rooms.MonsterRoom room)
|
||||
{
|
||||
foreach (var blockingRoomId in room.GetBlockingRoomIds())
|
||||
{
|
||||
roomComponent.AddBlockingRoom(_roomIdToGameObject[blockingRoomId].GetComponent<Room>());
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class GameManager : MonoBehaviour
|
|||
_dicePairTwo = new DicePair();
|
||||
}
|
||||
|
||||
void HandleRoomExploredByDice(object sender, Room room) {
|
||||
void HandleRoomExploredByDice(Room room) {
|
||||
if (State == GameState.PickRoomOne)
|
||||
{
|
||||
ChangeState(GameState.PickDiceTwo);
|
||||
|
|
|
|||
|
|
@ -19,10 +19,7 @@ public class EmptyRoom : Room
|
|||
public override void SetRoomExplored() {
|
||||
_isExplored = true;
|
||||
UnhighlightRoomAsOption();
|
||||
if (roomReward != null)
|
||||
{
|
||||
roomReward.TriggerGetReward();
|
||||
}
|
||||
TriggerRoomRewards();
|
||||
|
||||
SetExploredGUI();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,38 @@ using System;
|
|||
using Unity.VisualScripting;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
|
||||
public abstract class Lock : MonoBehaviour
|
||||
{
|
||||
|
||||
// Room that must be explored before lock can be unlocked.
|
||||
[SerializeField] protected Room blockingRoom;
|
||||
private TextMeshProUGUI _lockText;
|
||||
public event Action Unlocked;
|
||||
|
||||
public abstract override bool Equals(object other);
|
||||
|
||||
public override int GetHashCode() => GetHashCode();
|
||||
|
||||
public void OnEnable()
|
||||
{
|
||||
if (blockingRoom != null)
|
||||
{
|
||||
blockingRoom.ThisRoomExploredByTorch += HandleBlockingRoomExplored;
|
||||
blockingRoom.RoomExploredByDice += HandleBlockingRoomExplored;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDisable()
|
||||
{
|
||||
if (blockingRoom != null)
|
||||
{
|
||||
blockingRoom.ThisRoomExploredByTorch -= HandleBlockingRoomExplored;
|
||||
blockingRoom.RoomExploredByDice -= HandleBlockingRoomExplored;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool CheckIfKeyFits(DicePair dicePair)
|
||||
{
|
||||
if (blockingRoom == null || blockingRoom.GetRoomExplored())
|
||||
|
|
@ -27,8 +52,35 @@ public abstract class Lock : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
public void SetBlockingRoom(Room room)
|
||||
{
|
||||
blockingRoom = room;
|
||||
}
|
||||
|
||||
public virtual void AssignGUI(TextMeshProUGUI text)
|
||||
{
|
||||
_lockText = text;
|
||||
if (blockingRoom != null)
|
||||
{
|
||||
_lockText.alpha = ColorHelper.TEXT_FADED_OPACITY;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleBlockingRoomExplored(Room room)
|
||||
{
|
||||
SetBlockingRoomExploredGUI();
|
||||
}
|
||||
|
||||
protected virtual void OnUnlock()
|
||||
{
|
||||
Unlocked?.Invoke();
|
||||
}
|
||||
|
||||
private void SetBlockingRoomExploredGUI()
|
||||
{
|
||||
if (_lockText != null)
|
||||
{
|
||||
_lockText.alpha = ColorHelper.TEXT_FULL_OPACITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,24 @@
|
|||
using TMPro;
|
||||
using Unity.VisualScripting;
|
||||
using UnityEngine;
|
||||
|
||||
public class MatchingDiceLock : Lock
|
||||
{
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is MatchingDiceLock otherLock)
|
||||
{
|
||||
// Add meaningful comparison logic if there are properties that define equality.
|
||||
return true; // Placeholder, update as needed.
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return GetType().GetHashCode(); // Update if there are meaningful properties to hash.
|
||||
}
|
||||
|
||||
public override bool CheckIfKeyFits(DicePair dicePair)
|
||||
{
|
||||
if (base.CheckIfKeyFits(dicePair) && dicePair.CheckIfResultsMatch())
|
||||
|
|
@ -12,4 +28,10 @@ public class MatchingDiceLock : Lock
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void AssignGUI(TextMeshProUGUI text)
|
||||
{
|
||||
base.AssignGUI(text);
|
||||
text.SetText("=");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,24 @@
|
|||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
public class NumberLock : Lock
|
||||
{
|
||||
[SerializeField] private int number;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is NumberLock otherLock)
|
||||
{
|
||||
return otherLock.GetNumber() == number;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return number.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool CheckIfKeyFits(DicePair dicePair)
|
||||
{
|
||||
if (base.CheckIfKeyFits(dicePair) && dicePair.Sum() == number)
|
||||
|
|
@ -23,4 +38,10 @@ public class NumberLock : Lock
|
|||
{
|
||||
return number;
|
||||
}
|
||||
|
||||
public override void AssignGUI(TextMeshProUGUI text)
|
||||
{
|
||||
base.AssignGUI(text);
|
||||
text.SetText(number.ToString());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public class MonsterRoom : Room
|
|||
for (int i = 1; i < _locks.Length; i++)
|
||||
{
|
||||
lockToDuplicate = DuplicateToTheLeft(lockToDuplicate, ((RectTransform)lockToDuplicate.transform).rect.width);
|
||||
lockToDuplicate.GetComponent<TextMeshProUGUI>().text = ((NumberLock)_locks[i]).GetNumber().ToString();
|
||||
SetLockGUI(lockToDuplicate.GetComponent<TextMeshProUGUI>(), _locks[i]);
|
||||
}
|
||||
|
||||
_healthTicks = new GameObject[_health];
|
||||
|
|
@ -54,10 +54,7 @@ public class MonsterRoom : Room
|
|||
{
|
||||
_isExplored = true;
|
||||
UnhighlightRoomAsOption();
|
||||
if (roomReward != null)
|
||||
{
|
||||
roomReward.TriggerGetReward();
|
||||
}
|
||||
TriggerRoomRewards();
|
||||
|
||||
SetExploredGUI();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections;
|
|||
using TMPro;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Unity.VisualScripting;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
|
|
@ -14,9 +15,10 @@ public abstract class Room : MonoBehaviour
|
|||
[SerializeField] private Sprite litRoom;
|
||||
[FormerlySerializedAs("adjacentRooms")] [SerializeField] public List<GameObject> AdjacentRooms;
|
||||
public bool IsEntrance;
|
||||
[SerializeField] protected RoomReward roomReward;
|
||||
[SerializeField] protected RoomRewards roomRewards;
|
||||
|
||||
public event EventHandler<Room> RoomExploredByDice;
|
||||
public event Action<Room> RoomExploredByDice;
|
||||
public event Action<Room> ThisRoomExploredByTorch;
|
||||
public static event Action<Room> RoomExploredByTorch;
|
||||
private DicePair _diceSelected;
|
||||
|
||||
|
|
@ -54,6 +56,31 @@ public abstract class Room : MonoBehaviour
|
|||
gameObject.GetComponent<SpriteRenderer>().sprite = litRoom;
|
||||
}
|
||||
|
||||
public Lock[] GetLocks()
|
||||
{
|
||||
if (_locks != null)
|
||||
{
|
||||
return _locks;
|
||||
}
|
||||
|
||||
return gameObject.GetComponents<Lock>();
|
||||
}
|
||||
|
||||
public void AddBlockingRoom(Room blockingRoom)
|
||||
{
|
||||
Lock[] locks = gameObject.GetComponents<Lock>();
|
||||
foreach (Lock _lock in locks)
|
||||
{
|
||||
foreach (Lock blockingRoomLocks in blockingRoom.GetLocks())
|
||||
{
|
||||
if (blockingRoomLocks.Equals(_lock))
|
||||
{
|
||||
_lock.SetBlockingRoom(blockingRoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void SetRoomExplored();
|
||||
|
||||
protected void SetExploredGUI()
|
||||
|
|
@ -69,6 +96,7 @@ public abstract class Room : MonoBehaviour
|
|||
SetPropertiesOfEntrance();
|
||||
}
|
||||
_locks = gameObject.GetComponents<Lock>();
|
||||
roomRewards = gameObject.GetComponent<RoomRewards>();
|
||||
}
|
||||
|
||||
protected void HighlightRoomAsOption()
|
||||
|
|
@ -115,24 +143,23 @@ public abstract class Room : MonoBehaviour
|
|||
}
|
||||
|
||||
protected virtual void OnRoomExploredByDice() {
|
||||
RoomExploredByDice?.Invoke(this, this);
|
||||
RoomExploredByDice?.Invoke(this);
|
||||
}
|
||||
|
||||
protected void SetLockGUI(TextMeshProUGUI text, Lock _lock)
|
||||
{
|
||||
if (_lock is NumberLock )
|
||||
{
|
||||
text.SetText(((NumberLock)_locks[0]).GetNumber().ToString());
|
||||
}
|
||||
else if (_lock is MatchingDiceLock)
|
||||
{
|
||||
text.SetText("=");
|
||||
}
|
||||
_lock.AssignGUI(text);
|
||||
}
|
||||
|
||||
protected void TriggerRoomRewards()
|
||||
{
|
||||
roomRewards.TriggerGetReward();
|
||||
}
|
||||
|
||||
private void OnRoomExploredByTorch()
|
||||
{
|
||||
RoomExploredByTorch?.Invoke(this);
|
||||
ThisRoomExploredByTorch?.Invoke(this);
|
||||
}
|
||||
|
||||
// Check if the room is valid to be explored. If so trigger the event.
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class RoomReward : MonoBehaviour
|
||||
{
|
||||
public static event Action<int> DiamondsRewarded;
|
||||
public static event Action ChestRewarded;
|
||||
public static event Action<int> DamageDealt;
|
||||
|
||||
[SerializeField] private int diamonds;
|
||||
[SerializeField] private int damage;
|
||||
[SerializeField] private bool chest;
|
||||
|
||||
public void TriggerGetReward()
|
||||
{
|
||||
if (diamonds > 0)
|
||||
{
|
||||
DiamondsRewarded?.Invoke(diamonds);
|
||||
}
|
||||
|
||||
if (chest)
|
||||
{
|
||||
ChestRewarded?.Invoke();
|
||||
}
|
||||
|
||||
if (damage > 0)
|
||||
{
|
||||
DamageDealt?.Invoke(damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
PuzzleGameProject/Assets/Scripts/Rooms/RoomRewards.cs
Normal file
31
PuzzleGameProject/Assets/Scripts/Rooms/RoomRewards.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class RoomRewards : MonoBehaviour
|
||||
{
|
||||
public static event Action<int> DiamondsRewarded;
|
||||
public static event Action ChestRewarded;
|
||||
public static event Action<int> DamageDealt;
|
||||
|
||||
[SerializeField] public int Diamonds = 0;
|
||||
[SerializeField] public int Damage = 0;
|
||||
[SerializeField] public bool Chest = false;
|
||||
|
||||
public void TriggerGetReward()
|
||||
{
|
||||
if (Diamonds > 0)
|
||||
{
|
||||
DiamondsRewarded?.Invoke(Diamonds);
|
||||
}
|
||||
|
||||
if (Chest)
|
||||
{
|
||||
ChestRewarded?.Invoke();
|
||||
}
|
||||
|
||||
if (Damage > 0)
|
||||
{
|
||||
DamageDealt?.Invoke(Damage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ public class UIManager : MonoBehaviour
|
|||
[SerializeField] private GameObject chestRewardSelectionUI;
|
||||
private void OnEnable()
|
||||
{
|
||||
RoomReward.ChestRewarded += HandleChestRewarded;
|
||||
RoomRewards.ChestRewarded += HandleChestRewarded;
|
||||
}
|
||||
|
||||
private void HandleChestRewarded()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue