-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathScriptableSkill.cs
More file actions
210 lines (185 loc) · 9.23 KB
/
ScriptableSkill.cs
File metadata and controls
210 lines (185 loc) · 9.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// Saves the skill info in a ScriptableObject that can be used ingame by
// referencing it from a MonoBehaviour. It only stores an skill's static data.
//
// We also add each one to a dictionary automatically, so that all of them can
// be found by name without having to put them all in a database. Note that we
// have to put them all into the Resources folder and use Resources.LoadAll to
// load them. This is important because some skills may not be referenced by any
// entity ingame (e.g. after a special event). But all skills should still be
// loadable from the database, even if they are not referenced by anyone
// anymore. So we have to use Resources.Load. (before we added them to the dict
// in OnEnable, but that's only called for those that are referenced in the
// game. All others will be ignored by Unity.)
//
// Entity animation controllers will need one bool parameter for each skill name
// and they can use the same animation for different skill templates by using
// multiple transitions. (this is way easier than keeping track of a skillindex)
//
// A Skill can be created by right clicking the Resources folder and selecting
// Create -> uMMORPG Skill. Existing skills can be found in the Resources folder
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace uMMORPG
{
public abstract partial class ScriptableSkill : ScriptableObject
{
[Header("Info")]
public bool loopAttack;
[SerializeField, TextArea(1, 30)] protected string toolTip; // not public, use ToolTip()
public Sprite image;
public bool learnDefault; // normal attack etc.
public bool showCastBar;
public bool cancelCastIfTargetDied; // direct hit may want to cancel if target died. buffs doesn't care. etc.
public bool allowMovement; // can we move while casting this skill?
[Header("Requirements")]
public ScriptableSkill predecessor; // this skill has to be learned first
public int predecessorLevel = 1; // level of predecessor skill that is required
public string requiredWeaponCategory = ""; // "" = no weapon needed; "Weapon" = requires a weapon, "WeaponSword" = requires a sword weapon, etc.
public LinearInt requiredLevel; // required player level
public LinearLong requiredSkillExperience;
[Header("Properties")]
public int maxLevel = 1;
public LinearInt manaCosts;
public LinearFloat castTime;
public LinearFloat cooldown;
public LinearFloat castRange;
[Header("Sound")]
public AudioClip castSound;
// the skill casting process ///////////////////////////////////////////////
// some skills requires certain weapons.
// -> check the weapon type
// -> check durability if the weapon is a durability item
public bool CheckWeapon(Entity caster)
{
// no weapon requirement at all
if (string.IsNullOrWhiteSpace(requiredWeaponCategory))
return true;
// try equipped weapon first (existing behavior)
int weaponIndex = caster.equipment.GetEquippedWeaponIndex();
if (weaponIndex != -1)
{
if (caster.equipment.GetEquippedWeaponCategory() == requiredWeaponCategory)
return true;
return false;
}
// --- NEW: unarmed fallback ---
if (caster is Player player && player.unarmedWeapon != null)
{
// if unarmed weapon has no category, allow generic skills
if (string.IsNullOrWhiteSpace(player.unarmedWeapon.category))
return true;
// category match
if (player.unarmedWeapon.category == requiredWeaponCategory)
return true;
}
return false;
}
// 1. self check: alive, enough mana, cooldown ready etc.?
// (most skills can only be cast while alive. some maybe while dead or only
// if we have ammo, etc.)
public virtual bool CheckSelf(Entity caster, int skillLevel)
{
// has a weapon (important for projectiles etc.), no cooldown, hp, mp?
// note: only require equipment if requried weapon category != ""
return caster.health.current > 0 &&
caster.mana.current >= manaCosts.Get(skillLevel) &&
CheckWeapon(caster);
}
// 2. target check: can we cast this skill 'here' or on this 'target'?
// => e.g. sword hit checks if target can be attacked
// skill shot checks if the position under the mouse is valid etc.
// buff checks if it's a friendly player, etc.
// ===> IMPORTANT: this function HAS TO correct the target if necessary,
// e.g. for a buff that is cast on 'self' even though we target a NPC
// while casting it
public abstract bool CheckTarget(Entity caster);
// 3. distance check: do we need to walk somewhere to cast it?
// e.g. on a monster that's far away
// => returns 'true' if distance is fine, 'false' if we need to move
// (has corrected target already)
public abstract bool CheckDistance(Entity caster, int skillLevel, out Vector3 destination);
// 4. apply skill: deal damage, heal, launch projectiles, etc.
// (has corrected target already)
public abstract void Apply(Entity caster, int skillLevel);
// events for client sided effects /////////////////////////////////////////
// [Client]
public virtual void OnCastStarted(Entity caster)
{
if (caster.audioSource != null && castSound != null)
caster.audioSource.PlayOneShot(castSound);
}
// [Client]
public virtual void OnCastFinished(Entity caster) {}
// OnCastCanceled doesn't seem worth the Rpc bandwidth, since skill effects
// can check if caster.currentSkill == -1
// tooltip /////////////////////////////////////////////////////////////////
// fill in all variables into the tooltip
// this saves us lots of ugly string concatenation code.
// (dynamic ones are filled in Skill.cs)
// -> note: each tooltip can have any variables, or none if needed
// -> example usage:
/*
<b>{NAME}</b>
Description here...
Damage: {DAMAGE}
Cast Time: {CASTTIME}
Cooldown: {COOLDOWN}
Cast Range: {CASTRANGE}
AoE Radius: {AOERADIUS}
Mana Costs: {MANACOSTS}
*/
public virtual string ToolTip(int level, bool showRequirements = false)
{
// note: caching StringBuilder is worse for GC because .Clear frees the internal array and reallocates.
StringBuilder tip = new StringBuilder(toolTip);
tip.Replace("{NAME}", name);
tip.Replace("{LEVEL}", level.ToString());
tip.Replace("{CASTTIME}", Utils.PrettySeconds(castTime.Get(level)));
tip.Replace("{COOLDOWN}", Utils.PrettySeconds(cooldown.Get(level)));
tip.Replace("{CASTRANGE}", castRange.Get(level).ToString());
tip.Replace("{MANACOSTS}", manaCosts.Get(level).ToString());
// only show requirements if necessary
if (showRequirements)
{
tip.Append("\n<b><i>Required Level: " + requiredLevel.Get(1) + "</i></b>\n" +
"<b><i>Required Skill Exp.: " + requiredSkillExperience.Get(1) + "</i></b>\n");
if (predecessor != null)
tip.Append("<b><i>Required Skill: " + predecessor.name + " Lv. " + predecessorLevel + " </i></b>\n");
}
return tip.ToString();
}
// caching /////////////////////////////////////////////////////////////////
// we can only use Resources.Load in the main thread. we can't use it when
// declaring static variables. so we have to use it as soon as 'dict' is
// accessed for the first time from the main thread.
// -> we save the hash so the dynamic item part doesn't have to contain and
// sync the whole name over the network
static Dictionary<int, ScriptableSkill> cache;
public static Dictionary<int, ScriptableSkill> All
{
get
{
// not loaded yet?
if (cache == null)
{
// get all ScriptableSkills in resources
ScriptableSkill[] skills = Resources.LoadAll<ScriptableSkill>("");
// check for duplicates, then add to cache
List<string> duplicates = skills.ToList().FindDuplicates(skill => skill.name);
if (duplicates.Count == 0)
{
cache = skills.ToDictionary(skill => skill.name.GetStableHashCode(), skill => skill);
}
else
{
foreach (string duplicate in duplicates)
Debug.LogError("Resources folder contains multiple ScriptableSkills with the name " + duplicate + ". If you are using subfolders like 'Warrior/NormalAttack' and 'Archer/NormalAttack', then rename them to 'Warrior/(Warrior)NormalAttack' and 'Archer/(Archer)NormalAttack' instead.");
}
}
return cache;
}
}
}
}