· 8 years ago · Aug 10, 2018, 10:34 AM
1// All player logic was put into this class. We could also split it into several
2// smaller components, but this would result in many GetComponent calls and a
3// more complex syntax.
4//
5// The default Player class takes care of the basic player logic like the state
6// machine and some properties like damage and defense.
7//
8// The Player class stores the maximum experience for each level in a simple
9// array. So the maximum experience for level 1 can be found in expMax[0] and
10// the maximum experience for level 2 can be found in expMax[1] and so on. The
11// player's health and mana are also level dependent in most MMORPGs, hence why
12// there are hpMax and mpMax arrays too. We can find out a players's max health
13// in level 1 by using hpMax[0] and so on.
14//
15// The class also takes care of selection handling, which detects 3D world
16// clicks and then targets/navigates somewhere/interacts with someone.
17//
18// Animations are not handled by the NetworkAnimator because it's still very
19// buggy and because it can't really react to movement stops fast enough, which
20// results in moonwalking. Not synchronizing animations over the network will
21// also save us bandwidth.
22//
23// Note: unimportant commands should use the Unreliable channel to reduce load.
24// (it doesn't matter if a player has to click the respawn button twice if under
25// heavy load)
26using UnityEngine;
27using UnityEngine.Networking;
28using UnityEngine.AI;
29using System;
30using System.Linq;
31using System.Collections.Generic;
32
33public enum TradeStatus {Free, Locked, Accepted}
34public enum CraftingState {None, InProgress, Success, Failed}
35
36[Serializable]
37public struct SkillbarEntry
38{
39 public string reference;
40 public KeyCode hotKey;
41}
42
43[Serializable]
44public struct EquipmentInfo
45{
46 public string requiredCategory;
47 public SubAnimation location;
48 public ScriptableItem defaultItem;
49}
50
51[Serializable]
52public struct ItemMallCategory
53{
54 public string category;
55 public ScriptableItem[] items;
56}
57
58[RequireComponent(typeof(Animator))]
59[RequireComponent(typeof(Chat))]
60[RequireComponent(typeof(NetworkName))]
61public partial class Player : Entity
62{
63 [Header("Components")]
64 public Chat chat;
65 public Camera avatarCamera;
66
67 [Header("Text Meshes")]
68 public TextMesh guildOverlay;
69 public string guildOverlayPrefix = "[";
70 public string guildOverlaySuffix = "]";
71 public Color nameOverlayDefaultColor = Color.white;
72 public Color nameOverlayOffenderColor = Color.magenta;
73 public Color nameOverlayMurdererColor = Color.red;
74 public Color nameOverlayPartyColor = new Color(0.341f, 0.965f, 0.702f);
75
76 [Header("Icons")]
77 public Sprite classIcon; // for character selection
78 public Sprite portraitIcon; // for top left portrait
79
80 // some meta info
81 [HideInInspector] public string account = "";
82 [HideInInspector] public string className = "";
83
84 // health
85 public override int healthMax
86 {
87 get
88 {
89 // calculate equipment bonus
90 int equipmentBonus = (from slot in equipment
91 where slot.amount > 0
92 select ((EquipmentItem)slot.item.data).healthBonus).Sum();
93
94 // calculate strength bonus (1 strength means 1% of hpMax bonus)
95 int attributeBonus = Convert.ToInt32(_healthMax.Get(level) * (strength * 0.01f));
96
97 // base (health + buff) + equip + attributes
98 return base.healthMax + equipmentBonus + attributeBonus;
99 }
100 }
101
102 // mana
103 public override int manaMax
104 {
105 get
106 {
107 // calculate equipment bonus
108 int equipmentBonus = (from slot in equipment
109 where slot.amount > 0
110 select ((EquipmentItem)slot.item.data).manaBonus).Sum();
111
112 // calculate intelligence bonus (1 intelligence means 1% of hpMax bonus)
113 int attributeBonus = Convert.ToInt32(_manaMax.Get(level) * (intelligence * 0.01f));
114
115 // base (mana + buff) + equip + attributes
116 return base.manaMax + equipmentBonus + attributeBonus;
117 }
118 }
119
120 // damage
121 public override int damage
122 {
123 get
124 {
125 // calculate equipment bonus
126 int equipmentBonus = (from slot in equipment
127 where slot.amount > 0
128 select ((EquipmentItem)slot.item.data).damageBonus).Sum();
129
130 // return base (damage + buff) + equip
131 return base.damage + equipmentBonus;
132 }
133 }
134
135 // defense
136 public override int defense
137 {
138 get
139 {
140 // calculate equipment bonus
141 int equipmentBonus = (from slot in equipment
142 where slot.amount > 0
143 select ((EquipmentItem)slot.item.data).defenseBonus).Sum();
144
145 // return base (defense + buff) + equip
146 return base.defense + equipmentBonus;
147 }
148 }
149
150 // block
151 public override float blockChance
152 {
153 get
154 {
155 // calculate equipment bonus
156 float equipmentBonus = (from slot in equipment
157 where slot.amount > 0
158 select ((EquipmentItem)slot.item.data).blockChanceBonus).Sum();
159
160 // return base (blockChance + buff) + equip
161 return base.blockChance + equipmentBonus;
162 }
163 }
164
165 // crit
166 public override float criticalChance
167 {
168 get
169 {
170 // calculate equipment bonus
171 float equipmentBonus = (from slot in equipment
172 where slot.amount > 0
173 select ((EquipmentItem)slot.item.data).criticalChanceBonus).Sum();
174
175 // return base (criticalChance + buff) + equip
176 return base.criticalChance + equipmentBonus;
177 }
178 }
179
180 [Header("Attributes")]
181 [SyncVar] public int strength = 0;
182 [SyncVar] public int intelligence = 0;
183
184 [Header("Experience")] // note: int is not enough (can have > 2 mil. easily)
185 public int maxLevel = 1;
186 [SyncVar, SerializeField] long _experience = 0;
187 public long experience
188 {
189 get { return _experience; }
190 set
191 {
192 if (value <= _experience)
193 {
194 // decrease
195 _experience = Math.Max(value, 0);
196 }
197 else
198 {
199 // increase with level ups
200 // set the new value (which might be more than expMax)
201 _experience = value;
202
203 // now see if we leveled up (possibly more than once too)
204 // (can't level up if already max level)
205 while (_experience >= experienceMax && level < maxLevel)
206 {
207 // subtract current level's required exp, then level up
208 _experience -= experienceMax;
209 ++level;
210
211 // addon system hooks
212 Utils.InvokeMany(typeof(Player), this, "OnLevelUp_");
213 }
214
215 // set to expMax if there is still too much exp remaining
216 if (_experience > experienceMax) _experience = experienceMax;
217 }
218 }
219 }
220 [SerializeField] protected LevelBasedLong _experienceMax = new LevelBasedLong{baseValue=10, bonusPerLevel=10};
221 public long experienceMax { get { return _experienceMax.Get(level); } }
222
223 [Header("Skill Experience")]
224 [SyncVar] public long skillExperience = 0;
225
226 [Header("Indicator")]
227 public GameObject indicatorPrefab;
228 GameObject indicator;
229
230 [Header("Inventory")]
231 [SerializeField, SyncVar]
232 [Tooltip("The default number of inventory slots the player has without any EquipInventorySlot items equipped.")]
233 int baseInventorySize = 10;
234 public int inventorySize
235 {
236 get
237 {
238 //return the base inventory size plus the sum of any
239 return baseInventorySize + equipment.Sum(item => item.amount > 0 ? item.item.equipInventorySlots : 0);
240 }
241 }
242
243 public ScriptableItem[] defaultItems;
244 public KeyCode[] inventorySplitKeys = {KeyCode.LeftShift, KeyCode.RightShift};
245
246 [Header("Trash")]
247 [SyncVar] public ItemSlot trash = new ItemSlot();
248
249 [Header("Equipment")]
250 public EquipmentInfo[] equipmentInfo = new EquipmentInfo[]
251 {
252 new EquipmentInfo{requiredCategory="Weapon", location=null, defaultItem=null},
253 new EquipmentInfo{requiredCategory="Head", location=null, defaultItem=null},
254 new EquipmentInfo{requiredCategory="Chest", location=null, defaultItem=null},
255 new EquipmentInfo{requiredCategory="Legs", location=null, defaultItem=null},
256 new EquipmentInfo{requiredCategory="Shield", location=null, defaultItem=null},
257 new EquipmentInfo{requiredCategory="Shoulders", location=null, defaultItem=null},
258 new EquipmentInfo{requiredCategory="Hands", location=null, defaultItem=null},
259 new EquipmentInfo{requiredCategory="Feet", location=null, defaultItem=null}
260 };
261 public SyncListItemSlot equipment = new SyncListItemSlot();
262
263 [Header("Skillbar")]
264 public SkillbarEntry[] skillbar = new SkillbarEntry[]
265 {
266 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha1},
267 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha2},
268 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha3},
269 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha4},
270 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha5},
271 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha6},
272 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha7},
273 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha8},
274 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha9},
275 new SkillbarEntry{reference="", hotKey=KeyCode.Alpha0},
276 };
277
278 [Header("Quests")] // contains active and completed quests (=all)
279 public int activeQuestLimit = 10;
280 public SyncListQuest quests = new SyncListQuest();
281
282 [Header("Interaction")]
283 public float interactionRange = 1;
284 public KeyCode targetNearestKey = KeyCode.Tab;
285 public bool localPlayerClickThrough = true; // click selection goes through localplayer. feels best.
286
287 [Header("PvP")]
288 public BuffSkill offenderBuff;
289 public BuffSkill murdererBuff;
290
291 [Header("Trading")]
292 [SyncVar, HideInInspector] public string tradeRequestFrom = "";
293 [SyncVar, HideInInspector] public TradeStatus tradeStatus = TradeStatus.Free;
294 [SyncVar, HideInInspector] public long tradeOfferGold = 0;
295 public SyncListInt tradeOfferItems = new SyncListInt(); // inventory indices
296
297 [Header("Crafting")]
298 public List<int> craftingIndices = Enumerable.Repeat(-1, ScriptableRecipe.recipeSize).ToList();
299 [HideInInspector] public CraftingState craftingState = CraftingState.None; // // client sided
300
301 [Header("Item Mall")]
302 public ItemMallCategory[] itemMallCategories; // the items that can be purchased in the item mall
303 [SyncVar] public long coins = 0;
304 public float couponWaitSeconds = 3;
305
306 [Header("Guild")]
307 [SyncVar, HideInInspector] public string guildName = ""; // syncvar so that all observers see it
308 [SyncVar, HideInInspector] public string guildInviteFrom = "";
309 [HideInInspector] public Guild guild; // no syncvar, only owner sees it via TargetRPC (saves lots of bandwidth)
310 public float guildInviteWaitSeconds = 3;
311
312 [Header("Party")]
313 [HideInInspector] public Party party;
314 [SyncVar, HideInInspector] public string partyInviteFrom = "";
315 public float partyInviteWaitSeconds = 3;
316
317 [Header("Pet")]
318 [SyncVar] GameObject _activePet;
319 public Pet activePet
320 {
321 get { return _activePet != null ? _activePet.GetComponent<Pet>() : null; }
322 set { _activePet = value != null ? value.gameObject : null; }
323 }
324 // pet's destination should always be right next to player, not inside him
325 // -> we use a helper property so we don't have to recalculate it each time
326 // -> we offset the position by exactly 1 x bounds to the left because dogs
327 // are usually trained to walk on the left of the owner. looks natural.
328 public Vector2 petDestination
329 {
330 get
331 {
332 Bounds bounds = collider.bounds;
333 return transform.position - transform.right * bounds.size.x;
334 }
335 }
336
337 [Header("Death")]
338 public float deathExperienceLossPercent = 0.05f;
339
340 // some commands should have delays to avoid DDOS, too much database usage
341 // or brute forcing coupons etc. we use one riskyAction timer for all.
342 [SyncVar, HideInInspector] public float nextRiskyActionTime = 0;
343
344 // the next skill to be set if we try to set it while casting
345 int nextSkill = -1;
346
347 // the next target to be set if we try to set it while casting
348 // 'Entity' can't be SyncVar and NetworkIdentity causes errors when null,
349 // so we use [SyncVar] GameObject and wrap it for simplicity
350 [SyncVar] GameObject _nextTarget;
351 public Entity nextTarget
352 {
353 get { return _nextTarget != null ? _nextTarget.GetComponent<Entity>() : null; }
354 set { _nextTarget = value != null ? value.gameObject : null; }
355 }
356
357 // cache players to save lots of computations
358 // (otherwise we'd have to iterate NetworkServer.objects all the time)
359 // => on server: all online players
360 // => on client: all observed players
361 public static Dictionary<string, Player> onlinePlayers = new Dictionary<string, Player>();
362
363 // networkbehaviour ////////////////////////////////////////////////////////
364 protected override void Awake()
365 {
366 // cache base components
367 base.Awake();
368
369 // addon system hooks
370 Utils.InvokeMany(typeof(Player), this, "Awake_");
371 }
372
373 public override void OnStartLocalPlayer()
374 {
375 // make camera follow the local player. we don't just set .parent
376 // because the player might be destroyed, but the camera never should be
377 Camera.main.GetComponent<CameraMMO2D>().target = transform;
378 GameObject.FindWithTag("MinimapCamera").GetComponent<CopyPosition>().target = transform;
379 if (avatarCamera) avatarCamera.enabled = true; // avatar camera for local player
380
381 // load skillbar after player data was loaded
382 LoadSkillbar();
383
384 // addon system hooks
385 Utils.InvokeMany(typeof(Player), this, "OnStartLocalPlayer_");
386 }
387
388 public override void OnStartClient()
389 {
390 base.OnStartClient();
391
392 // setup synclist callbacks on client. no need to update and show and
393 // animate equipment on server
394 equipment.Callback += OnEquipmentChanged;
395
396 // refresh all locations once (on synclist changed won't be called
397 // for initial lists)
398 // -> needs to happen before ProximityChecker's initial SetVis call,
399 // otherwise we get a hidden character with visible equipment
400 // (hence OnStartClient and not Start)
401 for (int i = 0; i < equipment.Count; ++i)
402 RefreshLocation(i);
403 }
404
405 public override void OnStartServer()
406 {
407 base.OnStartServer();
408
409 // initialize trade item indices
410 for (int i = 0; i < 6; ++i) tradeOfferItems.Add(-1);
411
412 InvokeRepeating("ProcessCoinOrders", 5, 5);
413
414 // addon system hooks
415 Utils.InvokeMany(typeof(Player), this, "OnStartServer_");
416 }
417
418 protected override void Start()
419 {
420 base.Start();
421 onlinePlayers[name] = this;
422
423 // spawn effects for any buffs that might still be active after loading
424 // (OnStartServer is too early)
425 // note: no need to do that in Entity.Start because we don't load them
426 // with previously casted skills
427 if (isServer)
428 for (int i = 0; i < buffs.Count; ++i)
429 if (buffs[i].BuffTimeRemaining() > 0)
430 buffs[i].data.SpawnEffect(this, this);
431
432 // notify guild members that we are online. this also updates the client's
433 // own guild info via targetrpc automatically
434 // -> OnStartServer is too early because it's not spawned there yet
435 if (isServer)
436 SetGuildOnline(true);
437
438 // addon system hooks
439 Utils.InvokeMany(typeof(Player), this, "Start_");
440 }
441
442 void LateUpdate()
443 {
444 // pass parameters to animation state machine
445 // => passing the states directly is the most reliable way to avoid all
446 // kinds of glitches like movement sliding, attack twitching, etc.
447 // => make sure to import all looping animations like idle/run/attack
448 // with 'loop time' enabled, otherwise the client might only play it
449 // once
450 // => only play moving animation while the agent is actually moving. the
451 // MOVING state might be delayed due to latency or we might be in
452 // MOVING while a path is still pending, etc.
453 // => skill names are assumed to be boolean parameters in animator
454 // so we don't need to worry about an animation number etc.
455 if (isClient) // no need for animations on the server
456 {
457 animator.SetBool("MOVING", state == "MOVING" && agent.velocity != Vector2.zero);
458 animator.SetBool("CASTING", state == "CASTING");
459 foreach (Skill skill in skills)
460 if (skill.level > 0)
461 animator.SetBool(skill.name, skill.CastTimeRemaining() > 0);
462 animator.SetBool("DEAD", state == "DEAD");
463 animator.SetFloat("LookX", lookDirection.x);
464 animator.SetFloat("LookY", lookDirection.y);
465 }
466
467 // addon system hooks
468 Utils.InvokeMany(typeof(Player), this, "LateUpdate_");
469 }
470
471 void OnDestroy()
472 {
473 // Unity bug: isServer is false when called in host mode. only true when
474 // called in dedicated mode. so we need a workaround:
475 if (NetworkServer.active) // isServer
476 {
477 // leave party (if any)
478 if (InParty())
479 {
480 // dismiss if master, leave otherwise
481 if (party.members[0] == name)
482 PartyDismiss();
483 else
484 PartyLeave();
485 }
486
487 // notify guild members that we are offline
488 SetGuildOnline(false);
489 }
490
491 if (isLocalPlayer) // requires at least Unity 5.5.1 bugfix to work
492 {
493 Destroy(indicator);
494 SaveSkillbar();
495 }
496
497 onlinePlayers.Remove(name);
498
499 // addon system hooks
500 Utils.InvokeMany(typeof(Player), this, "OnDestroy_");
501 }
502
503 // finite state machine events - status based //////////////////////////////
504 // status based events
505 bool EventDied()
506 {
507 return health == 0;
508 }
509
510 bool EventTargetDisappeared()
511 {
512 return target == null;
513 }
514
515 bool EventTargetDied()
516 {
517 return target != null && target.health == 0;
518 }
519
520 bool EventSkillRequest()
521 {
522 return 0 <= currentSkill && currentSkill < skills.Count;
523 }
524
525 bool EventSkillFinished()
526 {
527 return 0 <= currentSkill && currentSkill < skills.Count &&
528 skills[currentSkill].CastTimeRemaining() == 0;
529 }
530
531 // setting agent.velocity takes one frame to apply and is still Vector3.zero
532 // immediately after setting it. so we need a little helper to make sure
533 // that EventMoveEnd doesn't fire immediately after.
534 bool velocityPending;
535 bool EventMoveEnd()
536 {
537 bool result = state == "MOVING" && !velocityPending && !IsMoving();
538 velocityPending = false; // always reset before returning
539 return result;
540 }
541
542 bool EventTradeStarted()
543 {
544 // did someone request a trade? and did we request a trade with him too?
545 Player player = FindPlayerFromTradeInvitation();
546 return player != null && player.tradeRequestFrom == name;
547 }
548
549 bool EventTradeDone()
550 {
551 // trade canceled or finished?
552 return state == "TRADING" && tradeRequestFrom == "";
553 }
554
555 // finite state machine events - command based /////////////////////////////
556 // client calls command, command sets a flag, event reads and resets it
557 // => we use a set so that we don't get ultra long queues etc.
558 // => we use set.Return to read and clear values
559 HashSet<string> cmdEvents = new HashSet<string>();
560
561 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
562 public void CmdRespawn() { cmdEvents.Add("Respawn"); }
563 bool EventRespawn() { return cmdEvents.Remove("Respawn"); }
564
565 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
566 public void CmdCancelAction() { cmdEvents.Add("CancelAction"); }
567 bool EventCancelAction() { return cmdEvents.Remove("CancelAction"); }
568
569 Vector2 navigatePosition = Vector2.zero;
570 float navigateStop = 0;
571 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
572 void CmdNavigateDestination(Vector2 position, float stoppingDistance)
573 {
574 navigatePosition = position; navigateStop = stoppingDistance;
575 cmdEvents.Add("NavigateDestination");
576 }
577 bool EventNavigateDestination()
578 { return cmdEvents.Remove("NavigateDestination"); }
579
580 Vector2 navigateVelocity = Vector2.zero;
581 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
582 void CmdNavigateVelocity(Vector2 velocity)
583 {
584 navigateVelocity = velocity.magnitude > 1 ? velocity.normalized : velocity; // prevent speedhacks
585 cmdEvents.Add("NavigateVelocity");
586 }
587 bool EventNavigateVelocity() { return cmdEvents.Remove("NavigateVelocity"); }
588
589 // finite state machine - server ///////////////////////////////////////////
590 [Server]
591 string UpdateServer_IDLE()
592 {
593 // events sorted by priority (e.g. target doesn't matter if we died)
594 if (EventDied())
595 {
596 // we died.
597 OnDeath();
598 currentSkill = nextSkill = -1; // in case we died while trying to cast
599 return "DEAD";
600 }
601 if (EventCancelAction())
602 {
603 // the only thing that we can cancel is the target
604 target = null;
605 return "IDLE";
606 }
607 if (EventTradeStarted())
608 {
609 // cancel casting (if any), set target, go to trading
610 currentSkill = nextSkill = -1; // just in case
611 target = FindPlayerFromTradeInvitation();
612 return "TRADING";
613 }
614 if (EventNavigateDestination())
615 {
616 // cancel casting (if any) and start moving
617 currentSkill = nextSkill = -1;
618 agent.stoppingDistance = navigateStop;
619 agent.destination = navigatePosition;
620 return "MOVING";
621 }
622 if (EventNavigateVelocity())
623 {
624 // cancel casting (if any) and start moving
625 currentSkill = nextSkill = -1;
626 agent.ResetPath(); // needed after click movement before we can use .velocity
627 agent.velocity = navigateVelocity * agent.speed;
628 velocityPending = true; // takes 1 frame to apply velocity
629 return "MOVING";
630 }
631 if (EventSkillRequest())
632 {
633 // user wants to cast a skill.
634 // check self (alive, mana, weapon etc.) and target
635 Skill skill = skills[currentSkill];
636 nextTarget = target; // return to this one after any corrections by CastCheckTarget
637 if (CastCheckSelf(skill) && CastCheckTarget(skill))
638 {
639 // check distance between self and target
640 Vector2 destination;
641 if (CastCheckDistance(skill, out destination))
642 {
643 // start casting
644 StartCastSkill(skill);
645 return "CASTING";
646 }
647 else
648 {
649 // move to the target first
650 // (use collider point(s) to also work with big entities)
651 agent.stoppingDistance = skill.castRange;
652 agent.destination = destination;
653 return "MOVING";
654 }
655 }
656 else
657 {
658 // checks failed. stop trying to cast.
659 currentSkill = nextSkill = -1;
660 return "IDLE";
661 }
662 }
663 if (EventSkillFinished()) {} // don't care
664 if (EventMoveEnd()) {} // don't care
665 if (EventTradeDone()) {} // don't care
666 if (EventRespawn()) {} // don't care
667 if (EventTargetDied()) {} // don't care
668 if (EventTargetDisappeared()) {} // don't care
669
670 return "IDLE"; // nothing interesting happened
671 }
672
673 [Server]
674 string UpdateServer_MOVING()
675 {
676 // agent.velocity needs to be set constantly during wasd movement
677 if (navigateVelocity != Vector2.zero)
678 agent.velocity = navigateVelocity * agent.speed;
679
680 // events sorted by priority (e.g. target doesn't matter if we died)
681 if (EventDied())
682 {
683 // we died.
684 OnDeath();
685 currentSkill = nextSkill = -1; // in case we died while trying to cast
686 return "DEAD";
687 }
688 if (EventMoveEnd())
689 {
690 // finished moving. do whatever we did before.
691 return "IDLE";
692 }
693 if (EventCancelAction())
694 {
695 // cancel casting (if any) and stop moving
696 currentSkill = nextSkill = -1;
697 agent.ResetPath();
698 return "IDLE";
699 }
700 if (EventTradeStarted())
701 {
702 // cancel casting (if any), stop moving, set target, go to trading
703 currentSkill = nextSkill = -1;
704 agent.ResetPath();
705 target = FindPlayerFromTradeInvitation();
706 return "TRADING";
707 }
708 if (EventNavigateDestination())
709 {
710 // cancel casting (if any) and start moving
711 currentSkill = nextSkill = -1;
712 agent.stoppingDistance = navigateStop;
713 agent.destination = navigatePosition;
714 return "MOVING";
715 }
716 if (EventNavigateVelocity())
717 {
718 // cancel casting (if any) and start moving
719 currentSkill = nextSkill = -1;
720 agent.ResetPath(); // needed after click movement before we can use .velocity
721 agent.velocity = navigateVelocity * agent.speed;
722 velocityPending = true; // takes 1 frame to apply velocity
723 return "MOVING";
724 }
725 if (EventSkillRequest())
726 {
727 // if and where we keep moving depends on the skill and the target
728 // check self (alive, mana, weapon etc.) and target
729 Skill skill = skills[currentSkill];
730 nextTarget = target; // return to this one after any corrections by CastCheckTarget
731 if (CastCheckSelf(skill) && CastCheckTarget(skill))
732 {
733 // check distance between self and target
734 Vector2 destination;
735 if (CastCheckDistance(skill, out destination))
736 {
737 // stop moving, start casting
738 agent.ResetPath();
739 StartCastSkill(skill);
740 return "CASTING";
741 }
742 else
743 {
744 // keep moving towards the target
745 // (use collider point(s) to also work with big entities)
746 agent.stoppingDistance = skill.castRange;
747 agent.destination = destination;
748 return "MOVING";
749 }
750 }
751 else
752 {
753 // invalid target. stop trying to cast, but keep moving.
754 currentSkill = nextSkill = -1;
755 return "MOVING";
756 }
757 }
758 if (EventSkillFinished()) {} // don't care
759 if (EventTradeDone()) {} // don't care
760 if (EventRespawn()) {} // don't care
761 if (EventTargetDied()) {} // don't care
762 if (EventTargetDisappeared()) {} // don't care
763
764 return "MOVING"; // nothing interesting happened
765 }
766
767 void UseNextTargetIfAny()
768 {
769 // use next target if the user tried to target another while casting
770 // (target is locked while casting so skill isn't applied to an invalid
771 // target accidentally)
772 if (nextTarget != null)
773 {
774 target = nextTarget;
775 nextTarget = null;
776 }
777 }
778
779 [Server]
780 string UpdateServer_CASTING()
781 {
782 // events sorted by priority (e.g. target doesn't matter if we died)
783 //
784 // IMPORTANT: nextTarget might have been set while casting, so make sure
785 // to handle it in any case here. it should definitely be null again
786 // after casting was finished.
787 // => this way we can reliably display nextTarget on the client if it's
788 // != null, so that UITarget always shows nextTarget>target
789 // (this just feels better)
790 if (EventDied())
791 {
792 // we died.
793 OnDeath();
794 currentSkill = nextSkill = -1; // in case we died while trying to cast
795 UseNextTargetIfAny(); // if user selected a new target while casting
796 return "DEAD";
797 }
798 if (EventNavigateDestination())
799 {
800 // cancel casting and start moving
801 currentSkill = nextSkill = -1;
802 agent.stoppingDistance = navigateStop;
803 agent.destination = navigatePosition;
804 UseNextTargetIfAny(); // if user selected a new target while casting
805 return "MOVING";
806 }
807 if (EventNavigateVelocity())
808 {
809 // cancel casting (if any) and start moving
810 currentSkill = nextSkill = -1;
811 agent.ResetPath(); // needed after click movement before we can use .velocity
812 agent.velocity = navigateVelocity * agent.speed;
813 velocityPending = true; // takes 1 frame to apply velocity
814 UseNextTargetIfAny(); // if user selected a new target while casting
815 return "MOVING";
816 }
817 if (EventCancelAction())
818 {
819 // cancel casting
820 currentSkill = nextSkill = -1;
821 UseNextTargetIfAny(); // if user selected a new target while casting
822 return "IDLE";
823 }
824 if (EventTradeStarted())
825 {
826 // cancel casting (if any), stop moving, set target, go to trading
827 currentSkill = nextSkill = -1;
828 agent.ResetPath();
829
830 // set target to trade target instead of next target (clear that)
831 target = FindPlayerFromTradeInvitation();
832 nextTarget = null;
833 return "TRADING";
834 }
835 if (EventTargetDisappeared())
836 {
837 // cancel if the target matters for this skill
838 if (skills[currentSkill].cancelCastIfTargetDied)
839 {
840 currentSkill = nextSkill = -1;
841 UseNextTargetIfAny(); // if user selected a new target while casting
842 return "IDLE";
843 }
844 }
845 if (EventTargetDied())
846 {
847 // cancel if the target matters for this skill
848 if (skills[currentSkill].cancelCastIfTargetDied)
849 {
850 currentSkill = nextSkill = -1;
851 UseNextTargetIfAny(); // if user selected a new target while casting
852 return "IDLE";
853 }
854 }
855 if (EventSkillFinished())
856 {
857 // apply the skill after casting is finished
858 // note: we don't check the distance again. it's more fun if players
859 // still cast the skill if the target ran a few steps away
860 Skill skill = skills[currentSkill];
861
862 // apply the skill on the target
863 FinishCastSkill(skill);
864
865 // casting finished for now. user pressed another skill button?
866 if (nextSkill != -1)
867 {
868 currentSkill = nextSkill;
869 nextSkill = -1;
870 }
871 // skill should be followed with default attack? otherwise clear
872 else currentSkill = skill.followupDefaultAttack ? 0 : -1;
873
874 // use next target if the user tried to target another while casting
875 UseNextTargetIfAny();
876
877 // go back to IDLE
878 return "IDLE";
879 }
880 if (EventMoveEnd()) {} // don't care
881 if (EventTradeDone()) {} // don't care
882 if (EventRespawn()) {} // don't care
883 if (EventSkillRequest()) {} // don't care
884
885 return "CASTING"; // nothing interesting happened
886 }
887
888 [Server]
889 string UpdateServer_TRADING()
890 {
891 // events sorted by priority (e.g. target doesn't matter if we died)
892 if (EventDied())
893 {
894 // we died, stop trading. other guy will receive targetdied event.
895 OnDeath();
896 currentSkill = nextSkill = -1; // in case we died while trying to cast
897 TradeCleanup();
898 return "DEAD";
899 }
900 if (EventCancelAction())
901 {
902 // stop trading
903 TradeCleanup();
904 return "IDLE";
905 }
906 if (EventTargetDisappeared())
907 {
908 // target disconnected, stop trading
909 TradeCleanup();
910 return "IDLE";
911 }
912 if (EventTargetDied())
913 {
914 // target died, stop trading
915 TradeCleanup();
916 return "IDLE";
917 }
918 if (EventTradeDone())
919 {
920 // someone canceled or we finished the trade. stop trading
921 TradeCleanup();
922 return "IDLE";
923 }
924 if (EventMoveEnd()) {} // don't care
925 if (EventSkillFinished()) {} // don't care
926 if (EventRespawn()) {} // don't care
927 if (EventTradeStarted()) {} // don't care
928 if (EventNavigateDestination()) {} // don't care
929 if (EventNavigateVelocity()) {} // don't care
930 if (EventSkillRequest()) {} // don't care
931
932 return "TRADING"; // nothing interesting happened
933 }
934
935 [Server]
936 string UpdateServer_DEAD()
937 {
938 // events sorted by priority (e.g. target doesn't matter if we died)
939 if (EventRespawn())
940 {
941 // revive to closest spawn, with 50% health, then go to idle
942 Transform start = NetworkManager.singleton.GetNearestStartPosition(transform.position);
943 agent.Warp(start.position); // recommended over transform.position
944 Revive(0.5f);
945 return "IDLE";
946 }
947 if (EventMoveEnd()) {} // don't care
948 if (EventSkillFinished()) {} // don't care
949 if (EventDied()) {} // don't care
950 if (EventCancelAction()) {} // don't care
951 if (EventTradeStarted()) {} // don't care
952 if (EventTradeDone()) {} // don't care
953 if (EventTargetDisappeared()) {} // don't care
954 if (EventTargetDied()) {} // don't care
955 if (EventNavigateDestination()) {} // don't care
956 if (EventNavigateVelocity()) {} // don't care
957 if (EventSkillRequest()) {} // don't care
958
959 return "DEAD"; // nothing interesting happened
960 }
961
962 [Server]
963 protected override string UpdateServer()
964 {
965 if (state == "IDLE") return UpdateServer_IDLE();
966 if (state == "MOVING") return UpdateServer_MOVING();
967 if (state == "CASTING") return UpdateServer_CASTING();
968 if (state == "TRADING") return UpdateServer_TRADING();
969 if (state == "DEAD") return UpdateServer_DEAD();
970 Debug.LogError("invalid state:" + state);
971 return "IDLE";
972 }
973
974 // finite state machine - client ///////////////////////////////////////////
975 [Client]
976 protected override void UpdateClient()
977 {
978 if (state == "IDLE" || state == "MOVING")
979 {
980 if (isLocalPlayer)
981 {
982 // simply accept input
983 SelectionHandling();
984 WSADHandling();
985 TargetNearest();
986
987 // canel action if escape key was pressed
988 if (Input.GetKeyDown(KeyCode.Escape)) CmdCancelAction();
989 }
990 }
991 else if (state == "CASTING")
992 {
993 if (isLocalPlayer)
994 {
995 // simply accept input
996 SelectionHandling();
997 WSADHandling();
998 TargetNearest();
999
1000 // canel action if escape key was pressed
1001 if (Input.GetKeyDown(KeyCode.Escape)) CmdCancelAction();
1002 }
1003 }
1004 else if (state == "TRADING") {}
1005 else if (state == "DEAD") {}
1006 else Debug.LogError("invalid state:" + state);
1007
1008 if (nameOverlay != null)
1009 {
1010 // find local player (null while in character selection)
1011 Player player = Utils.ClientLocalPlayer();
1012 if (player != null)
1013 {
1014 // note: murderer has higher priority (a player can be a murderer and an
1015 // offender at the same time)
1016 if (IsMurderer())
1017 nameOverlay.color = nameOverlayMurdererColor;
1018 else if (IsOffender())
1019 nameOverlay.color = nameOverlayOffenderColor;
1020 // member of the same party
1021 else if (player.InParty() && player.party.GetMemberIndex(name) != -1)
1022 nameOverlay.color = nameOverlayPartyColor;
1023 // otherwise default
1024 else
1025 nameOverlay.color = nameOverlayDefaultColor;
1026 }
1027 }
1028 if (guildOverlay != null)
1029 guildOverlay.text = guildName != "" ? guildOverlayPrefix + guildName + guildOverlaySuffix : "";
1030
1031 // addon system hooks
1032 Utils.InvokeMany(typeof(Player), this, "UpdateClient_");
1033 }
1034
1035 // attributes //////////////////////////////////////////////////////////////
1036 public int AttributesSpendable()
1037 {
1038 // calculate the amount of attribute points that can still be spent
1039 // -> one point per level
1040 // -> we don't need to store the points in an extra variable, we can
1041 // simply decrease the attribute points spent from the level
1042 return level - (strength + intelligence);
1043 }
1044
1045 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1046 public void CmdIncreaseStrength()
1047 {
1048 // validate
1049 if (health > 0 && AttributesSpendable() > 0) ++strength;
1050 }
1051
1052 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1053 public void CmdIncreaseIntelligence()
1054 {
1055 // validate
1056 if (health > 0 && AttributesSpendable() > 0) ++intelligence;
1057 }
1058
1059 // combat //////////////////////////////////////////////////////////////////
1060 // helper function to calculate the experience rewards for sharing parties
1061 public static long CalculatePartyExperienceShare(long total, int memberCount, float bonusPercentagePerMember, int memberLevel, int killedLevel)
1062 {
1063 // bonus percentage based on how many members there are
1064 float bonusPercentage = (memberCount-1) * bonusPercentagePerMember;
1065
1066 // calculate the share via ceil, so that uneven numbers still result in
1067 // at least 'total' in the end.
1068 // e.g. 4/2=2 (good); 5/2=2 (1 point got lost)
1069 long share = (long)Mathf.Ceil(total / (float)memberCount);
1070
1071 // balance experience reward for the receiver's level. this is important
1072 // to avoid crazy power leveling where a level 1 hero would get a LOT of
1073 // level ups if his friend kills a level 100 monster once.
1074 long balanced = BalanceExpReward(share, memberLevel, killedLevel);
1075 long bonus = Convert.ToInt64(balanced * bonusPercentage);
1076
1077 return balanced + bonus;
1078 }
1079
1080 [Server]
1081 public void OnDamageDealtToMonster(Monster monster)
1082 {
1083 // did we kill it?
1084 if (monster.health == 0)
1085 {
1086 // share kill rewards with party or only for self
1087 List<Player> closeMembers = InParty() ? GetPartyMembersInProximity() : new List<Player>();
1088
1089 // share experience & skill experience
1090 // note: bonus only applies to exp. share parties, otherwise
1091 // there's an unnecessary pressure to always join a
1092 // party when leveling alone too.
1093 // note: if monster.rewardExp is 10 then it's possible that
1094 // two members only receive 2 exp each (= 4 total).
1095 // this happens because of exp balancing by level and
1096 // is as intended.
1097 if (InParty() && party.shareExperience)
1098 {
1099 foreach (Player member in closeMembers)
1100 {
1101 member.experience += CalculatePartyExperienceShare(
1102 monster.rewardExperience,
1103 closeMembers.Count,
1104 Party.BonusExperiencePerMember,
1105 member.level,
1106 monster.level
1107 );
1108 member.skillExperience += CalculatePartyExperienceShare(
1109 monster.rewardSkillExperience,
1110 closeMembers.Count,
1111 Party.BonusExperiencePerMember,
1112 member.level,
1113 monster.level
1114 );
1115 }
1116 }
1117 else
1118 {
1119 skillExperience += BalanceExpReward(monster.rewardSkillExperience, level, monster.level);
1120 experience += BalanceExpReward(monster.rewardExperience, level, monster.level);
1121 }
1122
1123 // give pet the same exp without dividing it, but balance it
1124 // => AFTER player exp reward! pet can only ever level up to player
1125 // level, so it's best if the player gets exp and level-ups
1126 // first, then afterwards we try to level up the pet.
1127 if (activePet != null)
1128 activePet.experience += BalanceExpReward(monster.rewardExperience, activePet.level, monster.level);
1129
1130 // increase quest kill counter for all party members
1131 if (InParty())
1132 {
1133 foreach (Player member in closeMembers)
1134 member.IncreaseQuestKillCounterFor(monster.name);
1135 }
1136 else IncreaseQuestKillCounterFor(monster.name);
1137 }
1138 }
1139
1140 [Server]
1141 public void OnDamageDealtToPlayer(Player player)
1142 {
1143 // was he innocent?
1144 if (!player.IsOffender() && !player.IsMurderer())
1145 {
1146 // did we kill him? then start/reset murder status
1147 // did we just attack him? then start/reset offender status
1148 // (unless we are already a murderer)
1149 if (player.health == 0) StartMurderer();
1150 else if (!IsMurderer()) StartOffender();
1151 }
1152 }
1153
1154 [Server]
1155 public void OnDamageDealtToPet(Pet pet)
1156 {
1157 // was he innocent?
1158 if (!pet.owner.IsOffender() && !pet.owner.IsMurderer())
1159 {
1160 // did we kill him? then start/reset murder status
1161 // did we just attack him? then start/reset offender status
1162 // (unless we are already a murderer)
1163 if (pet.health == 0) StartMurderer();
1164 else if (!IsMurderer()) StartOffender();
1165 }
1166 }
1167
1168 // custom DealDamageAt function that also rewards experience if we killed
1169 // the monster
1170 [Server]
1171 public override void DealDamageAt(Entity entity, int amount)
1172 {
1173 // deal damage with the default function
1174 base.DealDamageAt(entity, amount);
1175
1176 // a monster?
1177 if (entity is Monster)
1178 {
1179 OnDamageDealtToMonster((Monster)entity);
1180 }
1181 // a player?
1182 // (see murder code section comments to understand the system)
1183 else if (entity is Player)
1184 {
1185 OnDamageDealtToPlayer((Player)entity);
1186 }
1187 // a pet?
1188 // (see murder code section comments to understand the system)
1189 else if (entity is Pet)
1190 {
1191 OnDamageDealtToPet((Pet)entity);
1192 }
1193
1194 // let pet know that we attacked something
1195 if (activePet != null && activePet.autoAttack)
1196 activePet.OnAggro(entity);
1197
1198 // addon system hooks
1199 Utils.InvokeMany(typeof(Player), this, "DealDamageAt_", entity, amount);
1200 }
1201
1202 // experience //////////////////////////////////////////////////////////////
1203 public float ExperiencePercent()
1204 {
1205 return (experience != 0 && experienceMax != 0) ? (float)experience / (float)experienceMax : 0;
1206 }
1207
1208 // players gain exp depending on their level. if a player has a lower level
1209 // than the monster, then he gains more exp (up to 100% more) and if he has
1210 // a higher level, then he gains less exp (up to 100% less)
1211 // -> test with monster level 20 and expreward of 100:
1212 // BalanceExpReward( 1, 20, 100)); => 200
1213 // BalanceExpReward( 9, 20, 100)); => 200
1214 // BalanceExpReward(10, 20, 100)); => 200
1215 // BalanceExpReward(11, 20, 100)); => 190
1216 // BalanceExpReward(12, 20, 100)); => 180
1217 // BalanceExpReward(13, 20, 100)); => 170
1218 // BalanceExpReward(14, 20, 100)); => 160
1219 // BalanceExpReward(15, 20, 100)); => 150
1220 // BalanceExpReward(16, 20, 100)); => 140
1221 // BalanceExpReward(17, 20, 100)); => 130
1222 // BalanceExpReward(18, 20, 100)); => 120
1223 // BalanceExpReward(19, 20, 100)); => 110
1224 // BalanceExpReward(20, 20, 100)); => 100
1225 // BalanceExpReward(21, 20, 100)); => 90
1226 // BalanceExpReward(22, 20, 100)); => 80
1227 // BalanceExpReward(23, 20, 100)); => 70
1228 // BalanceExpReward(24, 20, 100)); => 60
1229 // BalanceExpReward(25, 20, 100)); => 50
1230 // BalanceExpReward(26, 20, 100)); => 40
1231 // BalanceExpReward(27, 20, 100)); => 30
1232 // BalanceExpReward(28, 20, 100)); => 20
1233 // BalanceExpReward(29, 20, 100)); => 10
1234 // BalanceExpReward(30, 20, 100)); => 0
1235 // BalanceExpReward(31, 20, 100)); => 0
1236 public static long BalanceExpReward(long reward, int attackerLevel, int victimLevel)
1237 {
1238 int levelDiff = Mathf.Clamp(victimLevel - attackerLevel, -10, 10);
1239 float multiplier = 1 + levelDiff * 0.1f;
1240 return Convert.ToInt64(reward * multiplier);
1241 }
1242
1243 // aggro ///////////////////////////////////////////////////////////////////
1244 // this function is called by entities that attack us
1245 [ServerCallback]
1246 public override void OnAggro(Entity entity)
1247 {
1248 // forward to pet if it's supposed to defend us
1249 if (activePet != null && activePet.defendOwner)
1250 activePet.OnAggro(entity);
1251 }
1252
1253 // death ///////////////////////////////////////////////////////////////////
1254 protected override void OnDeath()
1255 {
1256 // take care of entity stuff
1257 base.OnDeath();
1258
1259 // lose experience
1260 long loss = Convert.ToInt64(experienceMax * deathExperienceLossPercent);
1261 experience -= loss;
1262
1263 // send an info chat message
1264 string message = "You died and lost " + loss + " experience.";
1265 chat.TargetMsgInfo(connectionToClient, message);
1266
1267 // addon system hooks
1268 Utils.InvokeMany(typeof(Player), this, "OnDeath_");
1269 }
1270
1271 // loot ////////////////////////////////////////////////////////////////////
1272 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1273 public void CmdTakeLootGold()
1274 {
1275 // validate: dead monster and close enough?
1276 // use collider point(s) to also work with big entities
1277 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1278 target != null && target is Monster && target.health == 0 &&
1279 Utils.ClosestDistance(collider, target.collider) <= interactionRange)
1280 {
1281 // distribute reward through party or to self
1282 if (InParty() && party.shareGold)
1283 {
1284 // find all party members in observer range
1285 // (we don't distribute it all across the map. standing
1286 // next to each other is a better experience. players
1287 // can't just stand safely in a city while gaining exp)
1288 List<Player> closeMembers = GetPartyMembersInProximity();
1289
1290 // calculate the share via ceil, so that uneven numbers
1291 // still result in at least total gold in the end.
1292 // e.g. 4/2=2 (good); 5/2=2 (1 gold got lost)
1293 long share = (long)Mathf.Ceil((float)target.gold / (float)closeMembers.Count);
1294
1295 // now distribute
1296 foreach (Player member in closeMembers)
1297 member.gold += share;
1298 ShowItemCollections(share.ToString() + " gold gained", this);
1299 } else {
1300 gold += target.gold;
1301 ShowItemCollections(target.gold.ToString() + " gold gained", this);
1302 }
1303
1304 // reset target gold
1305 target.gold = 0;
1306 }
1307 }
1308
1309 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1310 public void CmdTakeLootItem(int index)
1311 {
1312 // validate: dead monster and close enough and valid loot index?
1313 // use collider point(s) to also work with big entities
1314 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1315 target != null && target is Monster && target.health == 0 &&
1316 Utils.ClosestDistance(collider, target.collider) <= interactionRange &&
1317 0 <= index && index < target.inventory.Count &&
1318 target.inventory[index].amount > 0)
1319 {
1320 ItemSlot slot = target.inventory[index];
1321
1322 // try to add it to the inventory, clear monster slot if it worked
1323 if (InventoryAdd(slot.item, slot.amount))
1324 {
1325 ShowItemCollections(slot.item.name + " gained", this);
1326 slot.amount = 0;
1327 target.inventory[index] = slot;
1328 }
1329 }
1330 }
1331
1332 // inventory ///////////////////////////////////////////////////////////////
1333 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1334 public void CmdSwapInventoryTrash(int inventoryIndex)
1335 {
1336 // dragging an inventory item to the trash always overwrites the trash
1337 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1338 0 <= inventoryIndex && inventoryIndex < inventory.Count)
1339 {
1340 // inventory slot has to be valid and destroyable and not summoned
1341 ItemSlot slot = inventory[inventoryIndex];
1342 if (slot.amount > 0 && slot.item.destroyable && !slot.item.petSummoned)
1343 {
1344 // overwrite trash
1345 trash = slot;
1346
1347 // clear inventory slot
1348 slot.amount = 0;
1349 inventory[inventoryIndex] = slot;
1350 }
1351 }
1352 }
1353
1354 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1355 public void CmdSwapTrashInventory(int inventoryIndex)
1356 {
1357 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1358 0 <= inventoryIndex && inventoryIndex < inventory.Count)
1359 {
1360 // inventory slot has to be empty or destroyable
1361 ItemSlot slot = inventory[inventoryIndex];
1362 if (slot.amount == 0 || slot.item.destroyable)
1363 {
1364 // swap them
1365 inventory[inventoryIndex] = trash;
1366 trash = slot;
1367 }
1368 }
1369 }
1370
1371 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1372 public void CmdSwapInventoryInventory(int fromIndex, int toIndex)
1373 {
1374 // note: should never send a command with complex types!
1375 // validate: make sure that the slots actually exist in the inventory
1376 // and that they are not equal
1377 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1378 0 <= fromIndex && fromIndex < inventory.Count &&
1379 0 <= toIndex && toIndex < inventory.Count &&
1380 fromIndex != toIndex)
1381 {
1382 // swap them
1383 ItemSlot temp = inventory[fromIndex];
1384 inventory[fromIndex] = inventory[toIndex];
1385 inventory[toIndex] = temp;
1386 }
1387 }
1388
1389 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1390 public void CmdInventorySplit(int fromIndex, int toIndex)
1391 {
1392 // note: should never send a command with complex types!
1393 // validate: make sure that the slots actually exist in the inventory
1394 // and that they are not equal
1395 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1396 0 <= fromIndex && fromIndex < inventory.Count &&
1397 0 <= toIndex && toIndex < inventory.Count &&
1398 fromIndex != toIndex)
1399 {
1400 // slotFrom needs at least two to split, slotTo has to be empty
1401 ItemSlot slotFrom = inventory[fromIndex];
1402 ItemSlot slotTo = inventory[toIndex];
1403 if (slotFrom.amount >= 2 && slotTo.amount == 0)
1404 {
1405 // split them serversided (has to work for even and odd)
1406 slotTo = slotFrom; // copy the value
1407
1408 slotTo.amount = slotFrom.amount / 2;
1409 slotFrom.amount -= slotTo.amount; // works for odd too
1410
1411 // put back into the list
1412 inventory[fromIndex] = slotFrom;
1413 inventory[toIndex] = slotTo;
1414 }
1415 }
1416 }
1417
1418 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1419 public void CmdInventoryMerge(int fromIndex, int toIndex)
1420 {
1421 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1422 0 <= fromIndex && fromIndex < inventory.Count &&
1423 0 <= toIndex && toIndex < inventory.Count &&
1424 fromIndex != toIndex)
1425 {
1426 // both items have to be valid
1427 ItemSlot slotFrom = inventory[fromIndex];
1428 ItemSlot slotTo = inventory[toIndex];
1429 if (slotFrom.amount > 0 && slotTo.amount > 0)
1430 {
1431 // make sure that items are the same type
1432 // note: .Equals because name AND dynamic variables matter (petLevel etc.)
1433 if (slotFrom.item.Equals(slotTo.item))
1434 {
1435 // merge from -> to
1436 // put as many as possible into 'To' slot
1437 int put = slotTo.IncreaseAmount(slotFrom.amount);
1438 slotFrom.DecreaseAmount(put);
1439
1440 // put back into the list
1441 inventory[fromIndex] = slotFrom;
1442 inventory[toIndex] = slotTo;
1443 }
1444 }
1445 }
1446 }
1447
1448 [ClientRpc(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1449 public void RpcUsedItem(Item item)
1450 {
1451 // validate
1452 if (item.data is UsableItem)
1453 {
1454 UsableItem itemData = (UsableItem)item.data;
1455 itemData.OnUsed(this);
1456 }
1457 }
1458
1459 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1460 public void CmdUseInventoryItem(int index)
1461 {
1462 // validate
1463 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1464 0 <= index && index < inventory.Count && inventory[index].amount > 0 &&
1465 inventory[index].item.data is UsableItem)
1466 {
1467 // use item
1468 // note: we don't decrease amount / destroy in all cases because
1469 // some items may swap to other slots in .Use()
1470 UsableItem itemData = (UsableItem)inventory[index].item.data;
1471 if (itemData.CanUse(this, index))
1472 {
1473 // .Use might clear the slot, so we backup the Item first for the Rpc
1474 Item item = inventory[index].item;
1475 itemData.Use(this, index);
1476 RpcUsedItem(item);
1477 }
1478 }
1479 }
1480
1481 // equipment ///////////////////////////////////////////////////////////////
1482 public int GetEquipmentIndexByName(string itemName)
1483 {
1484 return equipment.FindIndex(slot => slot.amount > 0 && slot.item.name == itemName);
1485 }
1486
1487 void OnEquipmentChanged(SyncListItemSlot.Operation op, int index)
1488 {
1489 // update the equipment
1490 RefreshLocation(index);
1491 }
1492
1493 void RefreshLocation(int index)
1494 {
1495 ItemSlot slot = equipment[index];
1496 EquipmentInfo info = equipmentInfo[index];
1497
1498 // valid cateogry and valid location? otherwise don't bother
1499 if (info.requiredCategory != "" && info.location != null)
1500 info.location.spritesToAnimate = slot.amount > 0 ? ((EquipmentItem)slot.item.data).sprites : null;
1501 }
1502
1503 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1504 public void CmdSwapInventoryEquip(int inventoryIndex, int equipmentIndex)
1505 {
1506 // validate: make sure that the slots actually exist in the inventory
1507 // and in the equipment
1508 if (health > 0 &&
1509 0 <= inventoryIndex && inventoryIndex < inventory.Count &&
1510 0 <= equipmentIndex && equipmentIndex < equipment.Count)
1511 {
1512 // item slot has to be empty (unequip) or equipabable
1513 ItemSlot slot = inventory[inventoryIndex];
1514 if (slot.amount == 0 ||
1515 slot.item.data is EquipmentItem &&
1516 ((EquipmentItem)slot.item.data).CanEquip(this, inventoryIndex, equipmentIndex))
1517 {
1518 // swap them
1519 ItemSlot temp = equipment[equipmentIndex];
1520 equipment[equipmentIndex] = slot;
1521 inventory[inventoryIndex] = temp;
1522 }
1523 }
1524 }
1525
1526 // skills //////////////////////////////////////////////////////////////////
1527 public override bool HasCastWeapon()
1528 {
1529 // equipped any 'Weapon...' item?
1530 return equipment.FindIndex(slot => slot.amount > 0 &&
1531 ((EquipmentItem)slot.item.data).category.StartsWith("Weapon")
1532 ) != -1;
1533 }
1534
1535 public override bool CanAttack(Entity entity)
1536 {
1537 return health > 0 &&
1538 entity.health > 0 &&
1539 entity != this &&
1540 (entity.GetType() == typeof(Monster) ||
1541 entity.GetType() == typeof(Player) ||
1542 (entity.GetType() == typeof(Pet) && entity != activePet));
1543
1544 }
1545
1546 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1547 public void CmdUseSkill(int skillIndex)
1548 {
1549 // validate
1550 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1551 0 <= skillIndex && skillIndex < skills.Count)
1552 {
1553 // skill learned and can be casted?
1554 if (skills[skillIndex].level > 0 && skills[skillIndex].IsReady())
1555 {
1556 // add as current or next skill, unless casting same one already
1557 // (some players might hammer the key multiple times, which
1558 // doesn't mean that they want to cast it afterwards again)
1559 // => also: always set currentSkill when moving or idle or whatever
1560 // so that the last skill that the player tried to cast while
1561 // moving is the first skill that will be casted when attacking
1562 // the enemy.
1563 if (currentSkill == -1 || state != "CASTING")
1564 currentSkill = skillIndex;
1565 else if (currentSkill != skillIndex)
1566 nextSkill = skillIndex;
1567 }
1568 }
1569 }
1570
1571 public bool HasLearnedSkill(string skillName)
1572 {
1573 return skills.Any(skill => skill.name == skillName && skill.level > 0);
1574 }
1575
1576 // helper function for command and UI
1577 // -> this is for learning and upgrading!
1578 public bool CanUpgradeSkill(Skill skill)
1579 {
1580 return skill.level < skill.maxLevel &&
1581 level >= skill.upgradeRequiredLevel &&
1582 skillExperience >= skill.upgradeRequiredSkillExperience &&
1583 (skill.predecessor == null || HasLearnedSkill(skill.predecessor.name));
1584 }
1585
1586 // -> this is for learning and upgrading!
1587 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1588 public void CmdUpgradeSkill(int skillIndex)
1589 {
1590 // validate
1591 if ((state == "IDLE" || state == "MOVING" || state == "CASTING") &&
1592 0 <= skillIndex && skillIndex < skills.Count)
1593 {
1594 // can be upgraded?
1595 Skill skill = skills[skillIndex];
1596 if (CanUpgradeSkill(skill))
1597 {
1598 // decrease skill experience
1599 skillExperience -= skill.upgradeRequiredSkillExperience;
1600
1601 // upgrade
1602 ++skill.level;
1603 skills[skillIndex] = skill;
1604 }
1605 }
1606 }
1607
1608 // skillbar ////////////////////////////////////////////////////////////////
1609 //[Client] <- disabled while UNET OnDestroy isLocalPlayer bug exists
1610 void SaveSkillbar()
1611 {
1612 // save skillbar to player prefs (based on player name, so that
1613 // each character can have a different skillbar)
1614 for (int i = 0; i < skillbar.Length; ++i)
1615 PlayerPrefs.SetString(name + "_skillbar_" + i, skillbar[i].reference);
1616
1617 // force saving playerprefs, otherwise they aren't saved for some reason
1618 PlayerPrefs.Save();
1619 }
1620
1621 [Client]
1622 void LoadSkillbar()
1623 {
1624 print("loading skillbar for " + name);
1625 List<Skill> learned = skills.Where(skill => skill.level > 0).ToList();
1626 for (int i = 0; i < skillbar.Length; ++i)
1627 {
1628 // try loading an existing entry. otherwise fill with default skills
1629 // for a better first impression
1630 if (PlayerPrefs.HasKey(name + "_skillbar_" + i))
1631 {
1632 // only if learned (might be old character's playerprefs etc.)
1633 string skillName = PlayerPrefs.GetString(name + "_skillbar_" + i, "");
1634 if (HasLearnedSkill(skillName))
1635 skillbar[i].reference = skillName;
1636 }
1637 else if (i < learned.Count)
1638 {
1639 skillbar[i].reference = learned[i].name;
1640 }
1641 }
1642 }
1643
1644 // quests //////////////////////////////////////////////////////////////////
1645 public int GetQuestIndexByName(string questName)
1646 {
1647 return quests.FindIndex(quest => quest.name == questName);
1648 }
1649
1650 // helper function to check if the player has completed a quest before
1651 public bool HasCompletedQuest(string questName)
1652 {
1653 return quests.Any(q => q.name == questName && q.completed);
1654 }
1655
1656 // helper function to check if a player has an active (not completed) quest
1657 public bool HasActiveQuest(string questName)
1658 {
1659 return quests.Any(q => q.name == questName && !q.completed);
1660 }
1661
1662 [Server]
1663 public void IncreaseQuestKillCounterFor(string monsterName)
1664 {
1665 for (int i = 0; i < quests.Count; ++i)
1666 {
1667 // active quest and not completed yet?
1668 if (!quests[i].completed && quests[i].killTarget != null && quests[i].killTarget.name == monsterName)
1669 {
1670 Quest quest = quests[i];
1671 quest.killed = Mathf.Min(quest.killed + 1, quest.killAmount);
1672 quests[i] = quest;
1673 }
1674 }
1675 }
1676
1677 // helper function to check if the player can accept a new quest
1678 // note: no quest.completed check needed because we have a'not accepted yet'
1679 // check
1680 public bool CanAcceptQuest(ScriptableQuest quest)
1681 {
1682 // not too many quests yet?
1683 // has required level?
1684 // not accepted yet?
1685 // has finished predecessor quest (if any)?
1686 return quests.Count(q => !q.completed) < activeQuestLimit &&
1687 level >= quest.requiredLevel && // has required level?
1688 GetQuestIndexByName(quest.name) == -1 && // not accepted yet?
1689 (quest.predecessor == null || HasCompletedQuest(quest.predecessor.name));
1690 }
1691
1692 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1693 public void CmdAcceptQuest(int npcQuestIndex)
1694 {
1695 // validate
1696 // use collider point(s) to also work with big entities
1697 if (state == "IDLE" &&
1698 target != null &&
1699 target.health > 0 &&
1700 target is Npc &&
1701 0 <= npcQuestIndex && npcQuestIndex < ((Npc)target).quests.Length &&
1702 Utils.ClosestDistance(collider, target.collider) <= interactionRange &&
1703 CanAcceptQuest(((Npc)target).quests[npcQuestIndex]))
1704 {
1705 ScriptableQuest npcQuest = ((Npc)target).quests[npcQuestIndex];
1706 quests.Add(new Quest(npcQuest));
1707 }
1708 }
1709
1710 // helper function to check if the player can complete a quest
1711 public bool CanCompleteQuest(string questName)
1712 {
1713 // has the quest and not completed yet?
1714 int index = GetQuestIndexByName(questName);
1715 if (index != -1 && !quests[index].completed)
1716 {
1717 // fulfilled?
1718 Quest quest = quests[index];
1719 int gathered = quest.gatherItem != null ? InventoryCount(new Item(quest.gatherItem)) : 0;
1720 if(quest.IsFulfilled(gathered))
1721 {
1722 // enough space for reward item (if any)?
1723 return quest.rewardItem == null || InventoryCanAdd(new Item(quest.rewardItem), 1);
1724 }
1725 }
1726 return false;
1727 }
1728
1729 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1730 public void CmdCompleteQuest(int npcQuestIndex)
1731 {
1732 // validate
1733 // use collider point(s) to also work with big entities
1734 if (state == "IDLE" &&
1735 target != null &&
1736 target.health > 0 &&
1737 target is Npc &&
1738 0 <= npcQuestIndex && npcQuestIndex < ((Npc)target).quests.Length &&
1739 Utils.ClosestDistance(collider, target.collider) <= interactionRange)
1740 {
1741 ScriptableQuest npcQuest = ((Npc)target).quests[npcQuestIndex];
1742 int index = GetQuestIndexByName(npcQuest.name);
1743 if (index != -1)
1744 {
1745 // can complete it? (also checks inventory space for reward, if any)
1746 Quest quest = quests[index];
1747 if (CanCompleteQuest(quest.name))
1748 {
1749 // remove gathered items from player's inventory
1750 if (quest.gatherItem != null)
1751 InventoryRemove(new Item(quest.gatherItem), quest.gatherAmount);
1752
1753 // gain rewards
1754 gold += quest.rewardGold;
1755 experience += quest.rewardExperience;
1756 if (quest.rewardItem != null)
1757 InventoryAdd(new Item(quest.rewardItem), 1);
1758
1759 // complete quest
1760 quest.completed = true;
1761 quests[index] = quest;
1762 }
1763 }
1764 }
1765 }
1766
1767 // npc trading /////////////////////////////////////////////////////////////
1768 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1769 public void CmdNpcBuyItem(int index, int amount)
1770 {
1771 // validate: close enough, npc alive and valid index?
1772 // use collider point(s) to also work with big entities
1773 if (state == "IDLE" &&
1774 target != null &&
1775 target.health > 0 &&
1776 target is Npc &&
1777 Utils.ClosestDistance(collider, target.collider) <= interactionRange &&
1778 0 <= index && index < ((Npc)target).saleItems.Length)
1779 {
1780 // valid amount?
1781 Item npcItem = new Item(((Npc)target).saleItems[index]);
1782 if (1 <= amount && amount <= npcItem.maxStack)
1783 {
1784 long price = npcItem.buyPrice * amount;
1785
1786 // enough gold and enough space in inventory?
1787 if (gold >= price && InventoryCanAdd(npcItem, amount))
1788 {
1789 // pay for it, add to inventory
1790 gold -= price;
1791 InventoryAdd(npcItem, amount);
1792 ShowItemCollections(npcItem.name + " gained", this);
1793 }
1794 }
1795 }
1796 }
1797
1798 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1799 public void CmdNpcSellItem(int index, int amount)
1800 {
1801 // validate: close enough, npc alive and valid index and valid item?
1802 // use collider point(s) to also work with big entities
1803 if (state == "IDLE" &&
1804 target != null &&
1805 target.health > 0 &&
1806 target is Npc &&
1807 Utils.ClosestDistance(collider, target.collider) <= interactionRange &&
1808 0 <= index && index < inventory.Count)
1809 {
1810 // sellable?
1811 ItemSlot slot = inventory[index];
1812 if (slot.amount > 0 && slot.item.sellable && !slot.item.petSummoned)
1813 {
1814 // valid amount?
1815 if (1 <= amount && amount <= slot.amount)
1816 {
1817 // sell the amount
1818 long price = slot.item.sellPrice * amount;
1819 gold += price;
1820 slot.DecreaseAmount(amount);
1821 inventory[index] = slot;
1822 }
1823 }
1824 }
1825 }
1826
1827 // npc teleport ////////////////////////////////////////////////////////////
1828 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1829 public void CmdNpcTeleport()
1830 {
1831 // validate
1832 if (state == "IDLE" &&
1833 target != null &&
1834 target.health > 0 &&
1835 target is Npc &&
1836 Utils.ClosestDistance(collider, target.collider) <= interactionRange &&
1837 ((Npc)target).teleportTo != null)
1838 {
1839 // using agent.Warp is recommended over transform.position
1840 // (the latter can cause weird bugs when using it with an agent)
1841 agent.Warp(((Npc)target).teleportTo.position);
1842 }
1843 }
1844
1845 // player to player trading ////////////////////////////////////////////////
1846 // how trading works:
1847 // 1. A invites his target with CmdTradeRequest()
1848 // -> sets B.tradeInvitationFrom = A;
1849 // 2. B sees a UI window and accepts (= invites A too)
1850 // -> sets A.tradeInvitationFrom = B;
1851 // 3. the TradeStart event is fired, both go to 'TRADING' state
1852 // 4. they lock the trades
1853 // 5. they accept, then items and gold are swapped
1854
1855 public bool CanStartTrade()
1856 {
1857 // a player can only trade if he is not trading already and alive
1858 return health > 0 && state != "TRADING";
1859 }
1860
1861 public bool CanStartTradeWith(Entity entity)
1862 {
1863 // can we trade? can the target trade? are we close enough?
1864 return entity != null && entity is Player && entity != this &&
1865 CanStartTrade() && ((Player)entity).CanStartTrade() &&
1866 Utils.ClosestDistance(collider, entity.collider) <= interactionRange;
1867 }
1868
1869 // request a trade with the target player.
1870 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1871 public void CmdTradeRequestSend()
1872 {
1873 // validate
1874 if (CanStartTradeWith(target))
1875 {
1876 // send a trade request to target
1877 ((Player)target).tradeRequestFrom = name;
1878 print(name + " invited " + target.name + " to trade");
1879 }
1880 }
1881
1882 // helper function to find the guy who sent us a trade invitation
1883 [Server]
1884 Player FindPlayerFromTradeInvitation()
1885 {
1886 if (tradeRequestFrom != "" && onlinePlayers.ContainsKey(tradeRequestFrom))
1887 return onlinePlayers[tradeRequestFrom];
1888 return null;
1889 }
1890
1891 // accept a trade invitation by simply setting 'requestFrom' for the other
1892 // person to self
1893 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1894 public void CmdTradeRequestAccept()
1895 {
1896 Player sender = FindPlayerFromTradeInvitation();
1897 if (sender != null) {
1898 if (CanStartTradeWith(sender)) {
1899 // also send a trade request to the person that invited us
1900 sender.tradeRequestFrom = name;
1901 print(name + " accepted " + sender.name + "'s trade request");
1902 }
1903 }
1904 }
1905
1906 // decline a trade invitation
1907 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1908 public void CmdTradeRequestDecline()
1909 {
1910 tradeRequestFrom = "";
1911 }
1912
1913 [Server]
1914 void TradeCleanup()
1915 {
1916 // clear all trade related properties
1917 tradeOfferGold = 0;
1918 for (int i = 0; i < tradeOfferItems.Count; ++i) tradeOfferItems[i] = -1;
1919 tradeStatus = TradeStatus.Free;
1920 tradeRequestFrom = "";
1921 }
1922
1923 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1924 public void CmdTradeCancel()
1925 {
1926 // validate
1927 if (state == "TRADING")
1928 {
1929 // clear trade request for both guys. the FSM event will do the rest
1930 Player player = FindPlayerFromTradeInvitation();
1931 if (player != null) player.tradeRequestFrom = "";
1932 tradeRequestFrom = "";
1933 }
1934 }
1935
1936 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1937 public void CmdTradeOfferLock()
1938 {
1939 // validate
1940 if (state == "TRADING")
1941 tradeStatus = TradeStatus.Locked;
1942 }
1943
1944 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1945 public void CmdTradeOfferGold(long amount)
1946 {
1947 // validate
1948 if (state == "TRADING" && tradeStatus == TradeStatus.Free &&
1949 0 <= amount && amount <= gold)
1950 tradeOfferGold = amount;
1951 }
1952
1953 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1954 public void CmdTradeOfferItem(int inventoryIndex, int offerIndex)
1955 {
1956 // validate
1957 if (state == "TRADING" && tradeStatus == TradeStatus.Free &&
1958 0 <= offerIndex && offerIndex < tradeOfferItems.Count &&
1959 !tradeOfferItems.Contains(inventoryIndex) && // only one reference
1960 0 <= inventoryIndex && inventoryIndex < inventory.Count)
1961 {
1962 ItemSlot slot = inventory[inventoryIndex];
1963 if (slot.amount > 0 && slot.item.tradable && !slot.item.petSummoned)
1964 tradeOfferItems[offerIndex] = inventoryIndex;
1965 }
1966 }
1967
1968 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
1969 public void CmdTradeOfferItemClear(int offerIndex)
1970 {
1971 // validate
1972 if (state == "TRADING" && tradeStatus == TradeStatus.Free &&
1973 0 <= offerIndex && offerIndex < tradeOfferItems.Count)
1974 tradeOfferItems[offerIndex] = -1;
1975 }
1976
1977 [Server]
1978 bool IsTradeOfferStillValid()
1979 {
1980 // enough gold and all offered items are -1 or valid?
1981 return gold >= tradeOfferGold &&
1982 tradeOfferItems.All(index => index == -1 ||
1983 (0 <= index && index < inventory.Count && inventory[index].amount > 0));
1984 }
1985
1986 [Server]
1987 int TradeOfferItemSlotAmount()
1988 {
1989 return tradeOfferItems.Count(i => i != -1);
1990 }
1991
1992 [Server]
1993 int InventorySlotsNeededForTrade()
1994 {
1995 // if other guy offers 2 items and we offer 1 item then we only need
1996 // 2-1 = 1 slots. and the other guy would need 1-2 slots and at least 0.
1997 if (target != null && target is Player)
1998 {
1999 Player other = (Player)target;
2000 int otherAmount = other.TradeOfferItemSlotAmount();
2001 int myAmount = TradeOfferItemSlotAmount();
2002 return Mathf.Max(otherAmount - myAmount, 0);
2003 }
2004 return 0;
2005 }
2006
2007 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2008 public void CmdTradeOfferAccept()
2009 {
2010 // validate
2011 // note: distance check already done when starting the trade
2012 if (state == "TRADING" && tradeStatus == TradeStatus.Locked &&
2013 target != null && target is Player)
2014 {
2015 Player other = (Player)target;
2016
2017 // other has locked?
2018 if (other.tradeStatus == TradeStatus.Locked)
2019 {
2020 // simply accept and wait for the other guy to accept too
2021 tradeStatus = TradeStatus.Accepted;
2022 print("first accept by " + name);
2023 }
2024 // other has accepted already? then both accepted now, start trade.
2025 else if (other.tradeStatus == TradeStatus.Accepted)
2026 {
2027 // accept
2028 tradeStatus = TradeStatus.Accepted;
2029 print("second accept by " + name);
2030
2031 // both offers still valid?
2032 if (IsTradeOfferStillValid() && other.IsTradeOfferStillValid())
2033 {
2034 // both have enough inventory slots?
2035 // note: we don't use InventoryCanAdd here because:
2036 // - current solution works if both have full inventories
2037 // - InventoryCanAdd only checks one slot. here we have
2038 // multiple slots though (it could happen that we can
2039 // not add slot 2 after we did add slot 1's items etc)
2040 if (InventorySlotsFree() >= InventorySlotsNeededForTrade() &&
2041 other.InventorySlotsFree() >= other.InventorySlotsNeededForTrade())
2042 {
2043 // exchange the items by first taking them out
2044 // into a temporary list and then putting them
2045 // in. this guarantees that exchanging even
2046 // works with full inventories
2047
2048 // take them out
2049 Queue<ItemSlot> tempMy = new Queue<ItemSlot>();
2050 foreach (int index in tradeOfferItems)
2051 {
2052 if (index != -1)
2053 {
2054 ItemSlot slot = inventory[index];
2055 tempMy.Enqueue(slot);
2056 slot.amount = 0;
2057 inventory[index] = slot;
2058 }
2059 }
2060
2061 Queue<ItemSlot> tempOther = new Queue<ItemSlot>();
2062 foreach (int index in other.tradeOfferItems)
2063 {
2064 if (index != -1)
2065 {
2066 ItemSlot slot = other.inventory[index];
2067 tempOther.Enqueue(slot);
2068 slot.amount = 0;
2069 other.inventory[index] = slot;
2070 }
2071 }
2072
2073 // put them into the free slots
2074 for (int i = 0; i < inventory.Count; ++i)
2075 if (inventory[i].amount == 0 && tempOther.Count > 0)
2076 inventory[i] = tempOther.Dequeue();
2077
2078 for (int i = 0; i < other.inventory.Count; ++i)
2079 if (other.inventory[i].amount == 0 && tempMy.Count > 0)
2080 other.inventory[i] = tempMy.Dequeue();
2081
2082 // did it all work?
2083 if (tempMy.Count > 0 || tempOther.Count > 0)
2084 Debug.LogWarning("item trade problem");
2085
2086 // exchange the gold
2087 gold -= tradeOfferGold;
2088 other.gold -= other.tradeOfferGold;
2089
2090 gold += other.tradeOfferGold;
2091 other.gold += tradeOfferGold;
2092 }
2093 }
2094 else print("trade canceled (invalid offer)");
2095
2096 // clear trade request for both guys. the FSM event will do the
2097 // rest
2098 tradeRequestFrom = "";
2099 other.tradeRequestFrom = "";
2100 }
2101 }
2102 }
2103
2104 // crafting ////////////////////////////////////////////////////////////////
2105 // the crafting system is designed to work with all kinds of commonly known
2106 // crafting options:
2107 // - item combinations: wood + stone = axe
2108 // - weapon upgrading: axe + gem = strong axe
2109 // - recipe items: axerecipe(item) + wood(item) + stone(item) = axe(item)
2110 //
2111 // players can craft at all times, not just at npcs, because that's the most
2112 // realistic option
2113
2114 // craft the current combination of items and put result into inventory
2115 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2116 public void CmdCraft(int[] indices)
2117 {
2118 // validate: between 1 and 6, all valid, no duplicates?
2119 if ((state == "IDLE" || state == "MOVING") &&
2120 0 < indices.Length && indices.Length <= ScriptableRecipe.recipeSize &&
2121 indices.All(index => 0 <= index && index < inventory.Count && inventory[index].amount > 0) &&
2122 !indices.ToList().HasDuplicates())
2123 {
2124 // build list of item templates from indices
2125 List<ScriptableItem> items = indices.Select(index => inventory[index].item.data).ToList();
2126
2127 // find recipe
2128 ScriptableRecipe recipe = ScriptableRecipe.dict.Values.ToList().Find(r => r.CanCraftWith(items)); // good enough for now
2129 if (recipe != null && recipe.result != null)
2130 {
2131 // enough space?
2132 Item result = new Item(recipe.result);
2133 if (InventoryCanAdd(result, 1))
2134 {
2135 // remove the ingredients from inventory in any case
2136 foreach (int index in indices)
2137 {
2138 // decrease item amount
2139 ItemSlot slot = inventory[index];
2140 slot.DecreaseAmount(1);
2141 inventory[index] = slot;
2142 }
2143
2144 // roll the dice to decide if we add the result or not
2145 // IMPORTANT: we use rand() < probability to decide.
2146 // => UnityEngine.Random.value is [0,1] inclusive:
2147 // for 0% probability it's fine because it's never '< 0'
2148 // for 100% probability it's not because it's not always '< 1', it might be == 1
2149 // and if we use '<=' instead then it won't work for 0%
2150 // => C#'s Random value is [0,1) exclusive like most random
2151 // functions. this works fine.
2152 if (new System.Random().NextDouble() < recipe.probability)
2153 {
2154 // add result item to inventory
2155 InventoryAdd(new Item(recipe.result), 1);
2156 TargetCraftingSuccess(connectionToClient);
2157 }
2158 else
2159 {
2160 TargetCraftingFailed(connectionToClient);
2161 }
2162 }
2163 }
2164 }
2165 }
2166
2167 // two rpcs for results to save 1 byte for the actual result
2168 [TargetRpc(channel=Channels.DefaultUnreliable)] // only send to one client
2169 public void TargetCraftingSuccess(NetworkConnection target)
2170 {
2171 craftingState = CraftingState.Success;
2172 }
2173
2174 [TargetRpc(channel=Channels.DefaultUnreliable)] // only send to one client
2175 public void TargetCraftingFailed(NetworkConnection target)
2176 {
2177 craftingState = CraftingState.Failed;
2178 }
2179
2180 // pvp murder system ///////////////////////////////////////////////////////
2181 // attacking someone innocent results in Offender status
2182 // (can be attacked without penalty for a short time)
2183 // killing someone innocent results in Murderer status
2184 // (can be attacked without penalty for a long time + negative buffs)
2185 // attacking/killing a Offender/Murderer has no penalty
2186 //
2187 // we use buffs for the offender/status because buffs have all the features
2188 // that we need here.
2189 public bool IsOffender()
2190 {
2191 return offenderBuff != null && buffs.Any(buff => buff.name == offenderBuff.name);
2192 }
2193
2194 public bool IsMurderer()
2195 {
2196 return murdererBuff != null && buffs.Any(buff => buff.name == murdererBuff.name);
2197 }
2198
2199 public void StartOffender()
2200 {
2201 if (offenderBuff != null) AddOrRefreshBuff(new Buff(offenderBuff, 1));
2202 }
2203
2204 public void StartMurderer()
2205 {
2206 if (murdererBuff != null) AddOrRefreshBuff(new Buff(murdererBuff, 1));
2207 }
2208
2209 // item mall ///////////////////////////////////////////////////////////////
2210 [Command]
2211 public void CmdEnterCoupon(string coupon)
2212 {
2213 // only allow entering one coupon every few seconds to avoid brute force
2214 if (Time.time >= nextRiskyActionTime)
2215 {
2216 // YOUR COUPON VALIDATION CODE HERE
2217 // coins += ParseCoupon(coupon);
2218 Debug.Log("coupon: " + coupon + " => " + name + "@" + Time.time);
2219 nextRiskyActionTime = Time.time + couponWaitSeconds;
2220 }
2221 }
2222
2223 [Command]
2224 public void CmdUnlockItem(int categoryIndex, int itemIndex)
2225 {
2226 // validate: only if alive so people can't buy resurrection potions
2227 // after dieing in a PvP fight etc.
2228 if (health > 0 &&
2229 0 <= categoryIndex && categoryIndex <= itemMallCategories.Length &&
2230 0 <= itemIndex && itemIndex <= itemMallCategories[categoryIndex].items.Length)
2231 {
2232 Item item = new Item(itemMallCategories[categoryIndex].items[itemIndex]);
2233 if (0 < item.itemMallPrice && item.itemMallPrice <= coins)
2234 {
2235 // try to add it to the inventory, subtract costs from coins
2236 if (InventoryAdd(item, 1))
2237 {
2238 coins -= item.itemMallPrice;
2239 Debug.Log(name + " unlocked " + item.name);
2240 }
2241 }
2242 }
2243 }
2244
2245 // coins can't be increased by an external application while the player is
2246 // ingame. we use an additional table to store new orders in and process
2247 // them every few seconds from here. this way we can even notify the player
2248 // after his order was processed successfully.
2249 //
2250 // note: the alternative is to keep player.coins in the database at all
2251 // times, but then we need RPCs and the client needs a .coins value anyway.
2252 [Server]
2253 void ProcessCoinOrders()
2254 {
2255 List<long> orders = Database.GrabCharacterOrders(name);
2256 foreach (long reward in orders)
2257 {
2258 coins += reward;
2259 Debug.Log("Processed order for: " + name + ";" + reward);
2260 string message = "Processed order for: " + reward;
2261 chat.TargetMsgInfo(connectionToClient, message);
2262 }
2263 }
2264
2265 // guild ///////////////////////////////////////////////////////////////////
2266 public bool InGuild()
2267 {
2268 // only if both are true, otherwise we might be in the middle of leaving
2269 return guildName != "" && guild.GetMemberIndex(name) != -1;
2270 }
2271
2272 [Server]
2273 static void BroadcastGuildChanges(string guildName, Guild guild)
2274 {
2275 // save in database
2276 Database.SaveGuild(guildName, guild.notice, guild.members.ToList());
2277
2278 // copy to every online member. we don't just reload from db because the
2279 // online status is only available in the members list
2280 // (call TargetRpc on that GameObject for that connection)
2281 foreach (GuildMember member in guild.members)
2282 {
2283 if (onlinePlayers.ContainsKey(member.name))
2284 {
2285 Player player = onlinePlayers[member.name];
2286 player.guildName = guildName;
2287 player.guild = guild;
2288 player.TargetGuildSync(player.connectionToClient, guild);
2289 }
2290 }
2291 }
2292
2293 // sending guild notice and members to all observers would be bandwidth
2294 // overkill, so we use a targetrpc
2295 [TargetRpc(channel=Channels.DefaultUnreliable)] // only send to one client
2296 public void TargetGuildSync(NetworkConnection target, Guild guild)
2297 {
2298 this.guild = guild;
2299 }
2300
2301 // helper function to clear guild variables sync for kick/leave/terminate
2302 [Server]
2303 void ClearGuild()
2304 {
2305 guildName = "";
2306 guild = new Guild();
2307 TargetGuildSync(connectionToClient, guild);
2308 }
2309
2310 [Server]
2311 public void SetGuildOnline(bool online)
2312 {
2313 if (InGuild())
2314 {
2315 guild.SetOnline(name, online);
2316 BroadcastGuildChanges(guildName, guild);
2317 }
2318 }
2319
2320 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2321 public void CmdGuildInviteTarget()
2322 {
2323 // validate
2324 if (target != null && target is Player &&
2325 InGuild() && !((Player)target).InGuild() &&
2326 guild.CanInvite(name, target.name) &&
2327 Time.time >= nextRiskyActionTime &&
2328 Utils.ClosestDistance(collider, target.collider) <= interactionRange)
2329 {
2330 // send a invite and reset risky time
2331 ((Player)target).guildInviteFrom = name;
2332 nextRiskyActionTime = Time.time + guildInviteWaitSeconds;
2333 print(name + " invited " + target.name + " to guild");
2334 }
2335 }
2336
2337 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2338 public void CmdGuildInviteAccept()
2339 {
2340 // valid invitation?
2341 // note: no distance check because sender might be far away already
2342 if (!InGuild() && guildInviteFrom != "" &&
2343 onlinePlayers.ContainsKey(guildInviteFrom))
2344 {
2345 // can sender actually invite us?
2346 Player sender = onlinePlayers[guildInviteFrom];
2347 if (sender.InGuild() && sender.guild.CanInvite(sender.name, name))
2348 {
2349 // add self to sender's guild members list
2350 sender.guild.AddMember(name, level);
2351
2352 // broadcast and save changes from sender to everyone
2353 BroadcastGuildChanges(sender.guildName, sender.guild);
2354 print(sender.name + " added " + name + " to guild: " + guildName);
2355 }
2356 }
2357
2358 // reset guild invite in any case
2359 guildInviteFrom = "";
2360 }
2361
2362 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2363 public void CmdGuildInviteDecline()
2364 {
2365 guildInviteFrom = "";
2366 }
2367
2368 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2369 public void CmdGuildKick(string memberName)
2370 {
2371 // validate
2372 if (InGuild() && guild.CanKick(name, memberName))
2373 {
2374 // remove from member list
2375 guild.RemoveMember(memberName);
2376
2377 // broadcast and save changes
2378 BroadcastGuildChanges(guildName, guild);
2379
2380 // clear variables for the kicked person
2381 if (onlinePlayers.ContainsKey(memberName))
2382 onlinePlayers[memberName].ClearGuild();
2383 print(name + " kicked " + memberName + " from guild: " + guildName);
2384 }
2385 }
2386
2387 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2388 public void CmdGuildPromote(string memberName)
2389 {
2390 // validate
2391 if (InGuild() && guild.CanPromote(name, memberName))
2392 {
2393 // promote the member
2394 guild.PromoteMember(memberName);
2395
2396 // broadcast and save changes
2397 BroadcastGuildChanges(guildName, guild);
2398 print(name + " promoted " + memberName + " in guild: " + guildName);
2399 }
2400 }
2401
2402 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2403 public void CmdGuildDemote(string memberName)
2404 {
2405 // validate
2406 if (InGuild() && guild.CanDemote(name, memberName))
2407 {
2408 // demote the member
2409 guild.DemoteMember(memberName);
2410
2411 // broadcast and save changes
2412 BroadcastGuildChanges(guildName, guild);
2413 print(name + " demoted " + memberName + " in guild: " + guildName);
2414 }
2415 }
2416
2417 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2418 public void CmdSetGuildNotice(string notice)
2419 {
2420 // validate
2421 // (only allow changes every few seconds to avoid bandwidth issues)
2422 if (InGuild() && guild.CanNotify(name) &&
2423 notice.Length < Guild.NoticeMaxLength &&
2424 Time.time >= nextRiskyActionTime)
2425 {
2426 // set notice and reset next time
2427 guild.notice = notice;
2428 nextRiskyActionTime = Time.time + Guild.NoticeWaitSeconds;
2429
2430 // broadcast and save changes
2431 BroadcastGuildChanges(guildName, guild);
2432 print(name + " changed guild notice to: " + guild.notice);
2433 }
2434 }
2435
2436 // helper function to check if we are near a guild manager npc
2437 public bool IsGuildManagerNear()
2438 {
2439 return target != null &&
2440 target is Npc &&
2441 ((Npc)target).offersGuildManagement &&
2442 Utils.ClosestDistance(collider, target.collider) <= interactionRange;
2443 }
2444
2445 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2446 public void CmdTerminateGuild()
2447 {
2448 // validate
2449 if (InGuild() && IsGuildManagerNear() && guild.CanTerminate(name))
2450 {
2451 // remove guild from database
2452 Database.RemoveGuild(guildName);
2453
2454 // clear player variables
2455 ClearGuild();
2456 }
2457 }
2458
2459 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2460 public void CmdCreateGuild(string newGuildName)
2461 {
2462 // validate
2463 if (health > 0 && IsGuildManagerNear() && !InGuild() && gold >= Guild.CreationPrice)
2464 {
2465 if (Guild.IsValidGuildName(newGuildName) &&
2466 !Database.GuildExists(newGuildName)) // db check only on server, no Guild.CanCreate function because client has no DB.
2467 {
2468 // remove gold
2469 gold -= Guild.CreationPrice;
2470
2471 // set guild and add self to members list as highest rank
2472 guildName = newGuildName;
2473 guild.notice = ""; // avoid null
2474 guild.AddMember(name, level, GuildRank.Master);
2475
2476 // (broadcast and) save changes
2477 BroadcastGuildChanges(guildName, guild);
2478 print(name + " created guild: " + guildName);
2479 }
2480 else
2481 {
2482 string message = "Guild name invalid!"; // exists or invalid regex
2483 chat.TargetMsgInfo(connectionToClient, message);
2484 }
2485 }
2486 }
2487
2488 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2489 public void CmdLeaveGuild()
2490 {
2491 // validate
2492 if (InGuild() && guild.CanLeave(name))
2493 {
2494 // remove self from members list
2495 guild.RemoveMember(name);
2496
2497 // broadcast and save changes
2498 BroadcastGuildChanges(guildName, guild);
2499
2500 // reset guild info and members list for the person that left
2501 ClearGuild();
2502 }
2503 }
2504
2505 // party ///////////////////////////////////////////////////////////////////
2506 public bool InParty()
2507 {
2508 return party.members != null && party.members.Length > 0;
2509 }
2510
2511 [Server]
2512 static void BroadcastPartyChanges(Party party)
2513 {
2514 // copy to every online member. we don't just reload from db because the
2515 // online status is only available in the members list
2516 // (call TargetRpc on that GameObject for that connection)
2517 foreach (string member in party.members)
2518 {
2519 if (onlinePlayers.ContainsKey(member))
2520 {
2521 Player player = onlinePlayers[member];
2522 player.party = party;
2523 player.TargetPartySync(player.connectionToClient, party);
2524 }
2525 }
2526 }
2527
2528 // sending party info to all observers would be bandwidth overkill, so we
2529 // use a targetrpc
2530 [TargetRpc(channel=Channels.DefaultUnreliable)] // only send to one client
2531 public void TargetPartySync(NetworkConnection target, Party party)
2532 {
2533 this.party = party;
2534 }
2535
2536 // helper function to clear party variables sync for kick/leave/dismiss
2537 [Server]
2538 void ClearParty()
2539 {
2540 party = new Party();
2541 TargetPartySync(connectionToClient, party);
2542 }
2543
2544 // find party members in proximity for item/exp sharing etc.
2545 public List<Player> GetPartyMembersInProximity()
2546 {
2547 if (InParty())
2548 {
2549 return netIdentity.observers.Select(conn => Utils.GetGameObjectFromPlayerControllers(conn.playerControllers).GetComponent<Player>())
2550 .Where(p => party.GetMemberIndex(p.name) != -1)
2551 .ToList();
2552 }
2553 return new List<Player>();
2554 }
2555
2556 // party invite by name (not by target) so that chat commands are possible
2557 // if needed
2558 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2559 public void CmdPartyInvite(string otherName)
2560 {
2561 // validate: is there someone with that name, and not self?
2562 if (otherName != name && onlinePlayers.ContainsKey(otherName) &&
2563 Time.time >= nextRiskyActionTime)
2564 {
2565 Player other = onlinePlayers[otherName];
2566
2567 // can only send invite if no party yet or party isn't full and
2568 // have invite rights and other guy isn't in party yet
2569 if ((!InParty() || party.CanInvite(name)) && !other.InParty())
2570 {
2571 // send a invite and reset risky time
2572 other.partyInviteFrom = name;
2573 nextRiskyActionTime = Time.time + partyInviteWaitSeconds;
2574 print(name + " invited " + other.name + " to party");
2575 }
2576 }
2577 }
2578
2579 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2580 public void CmdPartyInviteAccept()
2581 {
2582 // valid invitation?
2583 // note: no distance check because sender might be far away already
2584 if (!InParty() && partyInviteFrom != "" &&
2585 onlinePlayers.ContainsKey(partyInviteFrom))
2586 {
2587 // can sender actually invite us?
2588 Player sender = onlinePlayers[partyInviteFrom];
2589
2590 // -> either he is in a party and can still invite someone
2591 if (sender.InParty() && sender.party.CanInvite(sender.name))
2592 {
2593 sender.party.AddMember(name);
2594 BroadcastPartyChanges(sender.party);
2595 print(sender.name + " added " + name + " to " + sender.party.members[0] + "'s party");
2596 }
2597 // -> or he is not in a party and forms a new one
2598 else if (!sender.InParty())
2599 {
2600 sender.party.AddMember(sender.name); // master
2601 sender.party.AddMember(name);
2602 BroadcastPartyChanges(sender.party);
2603 print(sender.name + " formed a new party with " + name);
2604 }
2605 }
2606
2607 // reset party invite in any case
2608 partyInviteFrom = "";
2609 }
2610
2611 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2612 public void CmdPartyInviteDecline()
2613 {
2614 partyInviteFrom = "";
2615 }
2616
2617 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2618 public void CmdPartyKick(int memberIndex)
2619 {
2620 // validate: party master and index exists and not master?
2621 if (InParty() && party.members[0] == name &&
2622 0 < memberIndex && memberIndex < party.members.Length)
2623 {
2624 string member = party.members[memberIndex];
2625
2626 // kick
2627 party.RemoveMember(member);
2628
2629 // still enough people in it for a party?
2630 if (party.members.Length > 1)
2631 {
2632 BroadcastPartyChanges(party);
2633 }
2634 else
2635 {
2636 // a party requires at least two people, otherwise it's not
2637 // really a party anymore. if we'd keep it alive with one player
2638 // then he can't be invited to another party until he dismisses
2639 // the empty one.
2640 ClearParty();
2641 }
2642
2643 // clear for the kicked person too
2644 if (onlinePlayers.ContainsKey(member))
2645 onlinePlayers[member].ClearParty();
2646
2647 print(name + " kicked " + member + " from party");
2648 }
2649 }
2650
2651 // version without cmd because we need to call it from the server too
2652 public void PartyLeave()
2653 {
2654 // validate: in party and not master?
2655 if (InParty() && party.members[0] != name)
2656 {
2657 // remove self from party
2658 party.RemoveMember(name);
2659
2660 // still enough people in it for a party?
2661 if (party.members.Length > 1)
2662 {
2663 BroadcastPartyChanges(party);
2664 }
2665 else
2666 {
2667 // a party requires at least two people, otherwise it's not
2668 // really a party anymore. if we'd keep it alive with one player
2669 // then he can't be invited to another party until he dismisses
2670 // the empty one.
2671 if (onlinePlayers.ContainsKey(party.members[0]))
2672 onlinePlayers[party.members[0]].ClearParty();
2673 }
2674
2675 // clear for self
2676 ClearParty();
2677 print(name + " left the party");
2678 }
2679 }
2680 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2681 public void CmdPartyLeave() { PartyLeave(); }
2682
2683 // version without cmd because we need to call it from the server too
2684 public void PartyDismiss()
2685 {
2686 // validate: is master?
2687 if (InParty() && party.members[0] == name)
2688 {
2689 // clear party for everyone
2690 foreach (string member in party.members)
2691 {
2692 if (onlinePlayers.ContainsKey(member))
2693 onlinePlayers[member].ClearParty();
2694 }
2695 print(name + " dismissed the party");
2696 }
2697 }
2698 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2699 public void CmdPartyDismiss() { PartyDismiss(); }
2700
2701 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2702 public void CmdPartySetExperienceShare(bool value)
2703 {
2704 // validate: is party master?
2705 if (InParty() && party.members[0] == name)
2706 {
2707 // set new value, sync to everyone else
2708 party.shareExperience = value;
2709 BroadcastPartyChanges(party);
2710 }
2711 }
2712
2713 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2714 public void CmdPartySetGoldShare(bool value)
2715 {
2716 // validate: is party master?
2717 if (InParty() && party.members[0] == name)
2718 {
2719 // set new value, sync to everyone else
2720 party.shareGold = value;
2721 BroadcastPartyChanges(party);
2722 }
2723 }
2724
2725 // pet /////////////////////////////////////////////////////////////////////
2726 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2727 public void CmdPetSetAutoAttack(bool value)
2728 {
2729 // validate
2730 if (activePet != null)
2731 activePet.autoAttack = value;
2732 }
2733
2734 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2735 public void CmdPetSetDefendOwner(bool value)
2736 {
2737 // validate
2738 if (activePet != null)
2739 activePet.defendOwner = value;
2740 }
2741
2742 // helper function for command and UI
2743 public bool CanUnsummonPet()
2744 {
2745 // only while pet and owner aren't fighting
2746 return activePet != null &&
2747 ( state == "IDLE" || state == "MOVING") &&
2748 (activePet.state == "IDLE" || activePet.state == "MOVING");
2749 }
2750
2751 [Command(channel = Channels.DefaultUnreliable)] // unimportant => unreliable
2752 public void CmdPetUnsummon()
2753 {
2754 // validate
2755 if (CanUnsummonPet())
2756 {
2757 AddOn_MinimapUI.DeleteMinimapObject(activePet.gameObject);
2758 // destroy from world. item.summoned and activePet will be null.
2759 NetworkServer.Destroy(activePet.gameObject);
2760 }
2761 }
2762
2763 [Command(channel=Channels.DefaultUnreliable)] // unimportant => unreliable
2764 public void CmdNpcRevivePet(int index)
2765 {
2766 // validate: close enough, npc alive and valid index and valid item?
2767 // use collider point(s) to also work with big entities
2768 if (state == "IDLE" &&
2769 target != null &&
2770 target.health > 0 &&
2771 target is Npc &&
2772 ((Npc)target).offersPetRevive &&
2773 Utils.ClosestDistance(collider, target.collider) <= interactionRange &&
2774 0 <= index && index < inventory.Count)
2775 {
2776 ItemSlot slot = inventory[index];
2777 if (slot.amount > 0 && slot.item.data is PetItem)
2778 {
2779 // verify the pet status
2780 PetItem itemData = (PetItem)slot.item.data;
2781 if (slot.item.petHealth == 0 && itemData.petPrefab != null)
2782 {
2783 // enough gold?
2784 if (gold >= itemData.petPrefab.revivePrice)
2785 {
2786 // pay for it, revive it
2787 gold -= itemData.petPrefab.revivePrice;
2788 slot.item.petHealth = itemData.petPrefab.healthMax;
2789 inventory[index] = slot;
2790 }
2791 }
2792 }
2793 }
2794 }
2795
2796 // selection handling //////////////////////////////////////////////////////
2797 void SetIndicatorViaParent(Transform parent)
2798 {
2799 if (!indicator) indicator = Instantiate(indicatorPrefab);
2800 indicator.transform.SetParent(parent, true);
2801 indicator.transform.position = parent.position;
2802 }
2803
2804 void SetIndicatorViaPosition(Vector2 pos)
2805 {
2806 if (!indicator) indicator = Instantiate(indicatorPrefab);
2807 indicator.transform.parent = null;
2808 indicator.transform.position = pos;
2809 }
2810
2811 [Command(channel=Channels.DefaultReliable)] // important for skills etc.
2812 public void CmdSetTarget(NetworkIdentity ni)
2813 {
2814 // validate
2815 if (ni != null)
2816 {
2817 // can directly change it, or change it after casting?
2818 if (state == "IDLE" || state == "MOVING")
2819 target = ni.GetComponent<Entity>();
2820 else if (state == "CASTING")
2821 nextTarget = ni.GetComponent<Entity>();
2822 }
2823 }
2824
2825 [Client]
2826 void SelectionHandling()
2827 {
2828 // click raycasting if not over a UI element & not pinching on mobile
2829 // note: this only works if the UI's CanvasGroup blocks Raycasts
2830 if (Input.GetMouseButtonDown(0) && !Utils.IsCursorOverUserInterface() && Input.touchCount <= 1)
2831 {
2832 // cast a 3D ray from the camera towards the 2D scene.
2833 // Physics2D.Raycast isn't made for that, we use GetRayIntersection.
2834 Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
2835
2836 // raycast with local player ignore option
2837 RaycastHit2D hit = localPlayerClickThrough ? Utils.Raycast2DWithout(ray, gameObject) : Physics2D.GetRayIntersection(ray);
2838
2839 // valid target?
2840 Entity entity = hit.transform != null ? hit.transform.GetComponent<Entity>() : null;
2841 if (entity)
2842 {
2843 // set indicator
2844 SetIndicatorViaParent(hit.transform);
2845
2846 // clicked last target again? and is not self or pet?
2847 if (entity == target && entity != this && entity != activePet)
2848 {
2849 // attackable? => attack
2850 if (CanAttack(entity))
2851 {
2852 // cast the first skill (if any, and if ready)
2853 if (skills.Count > 0 && skills[0].IsReady())
2854 CmdUseSkill(0);
2855 // otherwise walk there if still on cooldown etc
2856 // use collider point(s) to also work with big entities
2857 else
2858 CmdNavigateDestination(entity.collider.ClosestPointOnBounds(transform.position), skills.Count > 0 ? skills[0].castRange : 0f);
2859 }
2860 // npc & alive => talk
2861 else if (entity is Npc && entity.health > 0 && ((Npc)entity).UCE_ValidateNpcRestrictions(this))
2862 {
2863 // close enough to talk?
2864 // use collider point(s) to also work with big entities
2865 if (Utils.ClosestDistance(collider, entity.collider) <= interactionRange)
2866 FindObjectOfType<UINpcDialogue>().Show();
2867 // otherwise walk there
2868 // use collider point(s) to also work with big entities
2869 else
2870 CmdNavigateDestination(entity.collider.ClosestPointOnBounds(transform.position), interactionRange);
2871 }
2872 // monster & dead => loot
2873 else if (entity is Monster && entity.health == 0)
2874 {
2875 // has loot? and close enough?
2876 // use collider point(s) to also work with big entities
2877 if (((Monster)entity).HasLoot() &&
2878 Utils.ClosestDistance(collider, entity.collider) <= interactionRange)
2879 FindObjectOfType<UILoot>().Show();
2880 // otherwise walk there
2881 // use collider point(s) to also work with big entities
2882 else
2883 CmdNavigateDestination(entity.collider.ClosestPointOnBounds(transform.position), interactionRange);
2884 }
2885
2886 // addon system hooks
2887 Utils.InvokeMany(typeof(Player), this, "OnSelect_", entity);
2888 // clicked a new target
2889 }
2890 else
2891 {
2892 // target it
2893 CmdSetTarget(entity.netIdentity);
2894 }
2895 }
2896 // if we hit nothing then we want to move somewhere
2897 else
2898 {
2899 // set indicator and navigate to the nearest walkable
2900 // destination. this prevents twitching when destination is
2901 // accidentally in a room without a door etc.
2902 Vector2 worldPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
2903 Vector2 bestDestination = agent.NearestValidDestination(worldPos);
2904 SetIndicatorViaPosition(bestDestination);
2905 CmdNavigateDestination(bestDestination, 0);
2906 }
2907 }
2908 }
2909
2910 // simple WSAD movement without prediction
2911 Vector2 lastDirection;
2912 [Client]
2913 void WSADHandling()
2914 {
2915 // don't move if currently typing in an input
2916 // we check this after checking h and v to save computations
2917 if (!UIUtils.AnyInputActive())
2918 {
2919 // get horizontal and vertical input
2920 // note: no != 0 check because it's 0 when we stop moving rapidly
2921 float horizontal = Input.GetAxis("Horizontal");
2922 float vertical = Input.GetAxis("Vertical");
2923
2924 // create direction, normalize in case of diagonal movement
2925 Vector2 direction = new Vector2(horizontal, vertical);
2926 if (direction.magnitude > 1) direction = direction.normalized;
2927
2928 // draw direction for debugging
2929 Debug.DrawLine(transform.position, transform.position + (Vector3)direction, Color.green, 0, false);
2930
2931 // clear indicator if there is one, and if it's not on a target
2932 // (simply looks better)
2933 if (direction != Vector2.zero && indicator != null && indicator.transform.parent == null)
2934 Destroy(indicator);
2935
2936 // send to server - if changed since last time to save bandwidth
2937 if (direction != lastDirection)
2938 {
2939 CmdNavigateVelocity(direction);
2940 lastDirection = direction;
2941 }
2942 }
2943 }
2944
2945 // simple tab targeting
2946 [Client]
2947 void TargetNearest()
2948 {
2949 if (Input.GetKeyDown(targetNearestKey))
2950 {
2951 // find all monsters that are alive, sort by distance
2952 GameObject[] objects = GameObject.FindGameObjectsWithTag("Monster");
2953 List<Monster> monsters = objects.Select(go => go.GetComponent<Monster>()).Where(m => m.health > 0).ToList();
2954 List<Monster> sorted = monsters.OrderBy(m => Vector2.Distance(transform.position, m.transform.position)).ToList();
2955
2956 // target nearest one
2957 if (sorted.Count > 0)
2958 {
2959 SetIndicatorViaParent(sorted[0].transform);
2960 CmdSetTarget(sorted[0].netIdentity);
2961 }
2962 }
2963 }
2964
2965 // drag and drop ///////////////////////////////////////////////////////////
2966 void OnDragAndDrop_InventorySlot_InventorySlot(int[] slotIndices)
2967 {
2968 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
2969
2970 // merge? check Equals because name AND dynamic variables matter (petLevel etc.)
2971 if (inventory[slotIndices[0]].amount > 0 && inventory[slotIndices[1]].amount > 0 &&
2972 inventory[slotIndices[0]].item.Equals(inventory[slotIndices[1]].item))
2973 {
2974 CmdInventoryMerge(slotIndices[0], slotIndices[1]);
2975 }
2976 // split?
2977 else if (Utils.AnyKeyPressed(inventorySplitKeys))
2978 {
2979 CmdInventorySplit(slotIndices[0], slotIndices[1]);
2980 }
2981 // swap?
2982 else
2983 {
2984 CmdSwapInventoryInventory(slotIndices[0], slotIndices[1]);
2985 }
2986 }
2987
2988 void OnDragAndDrop_InventorySlot_TrashSlot(int[] slotIndices)
2989 {
2990 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
2991 CmdSwapInventoryTrash(slotIndices[0]);
2992 }
2993
2994 void OnDragAndDrop_InventorySlot_EquipmentSlot(int[] slotIndices)
2995 {
2996 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
2997 CmdSwapInventoryEquip(slotIndices[0], slotIndices[1]);
2998 }
2999
3000 void OnDragAndDrop_InventorySlot_SkillbarSlot(int[] slotIndices)
3001 {
3002 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3003 skillbar[slotIndices[1]].reference = inventory[slotIndices[0]].item.name; // just save it clientsided
3004 }
3005
3006 void OnDragAndDrop_InventorySlot_NpcSellSlot(int[] slotIndices)
3007 {
3008 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3009 ItemSlot slot = inventory[slotIndices[0]];
3010 if (slot.item.sellable && !slot.item.petSummoned) {
3011 FindObjectOfType<UINpcTrading>().sellIndex = slotIndices[0];
3012 FindObjectOfType<UINpcTrading>().sellAmountInput.text = slot.amount.ToString();
3013 }
3014 }
3015
3016 void OnDragAndDrop_InventorySlot_TradingSlot(int[] slotIndices)
3017 {
3018 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3019 if (inventory[slotIndices[0]].item.tradable)
3020 CmdTradeOfferItem(slotIndices[0], slotIndices[1]);
3021 }
3022
3023 void OnDragAndDrop_InventorySlot_CraftingIngredientSlot(int[] slotIndices)
3024 {
3025 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3026 // only if not crafting right now
3027 if (craftingState != CraftingState.InProgress)
3028 {
3029 if (!craftingIndices.Contains(slotIndices[0]))
3030 {
3031 craftingIndices[slotIndices[1]] = slotIndices[0];
3032 craftingState = CraftingState.None; // reset state
3033 }
3034 }
3035 }
3036
3037 void OnDragAndDrop_TrashSlot_InventorySlot(int[] slotIndices)
3038 {
3039 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3040 CmdSwapTrashInventory(slotIndices[1]);
3041 }
3042
3043 void OnDragAndDrop_EquipmentSlot_InventorySlot(int[] slotIndices)
3044 {
3045 if (CanUnEquip(equipment[slotIndices[0]]))
3046 {
3047 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3048 CmdSwapInventoryEquip(slotIndices[1], slotIndices[0]); // reversed
3049 }
3050 else { return; }
3051 }
3052
3053 void OnDragAndDrop_EquipmentSlot_SkillbarSlot(int[] slotIndices)
3054 {
3055 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3056 skillbar[slotIndices[1]].reference = equipment[slotIndices[0]].item.name; // just save it clientsided
3057 }
3058
3059 void OnDragAndDrop_SkillsSlot_SkillbarSlot(int[] slotIndices)
3060 {
3061 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3062 skillbar[slotIndices[1]].reference = skills[slotIndices[0]].name; // just save it clientsided
3063 }
3064
3065 void OnDragAndDrop_SkillbarSlot_SkillbarSlot(int[] slotIndices)
3066 {
3067 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3068 // just swap them clientsided
3069 string temp = skillbar[slotIndices[0]].reference;
3070 skillbar[slotIndices[0]].reference = skillbar[slotIndices[1]].reference;
3071 skillbar[slotIndices[1]].reference = temp;
3072 }
3073
3074 void OnDragAndDrop_CraftingIngredientSlot_CraftingIngredientSlot(int[] slotIndices)
3075 {
3076 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3077 // only if not crafting right now
3078 if (craftingState != CraftingState.InProgress)
3079 {
3080 // just swap them clientsided
3081 int temp = craftingIndices[slotIndices[0]];
3082 craftingIndices[slotIndices[0]] = craftingIndices[slotIndices[1]];
3083 craftingIndices[slotIndices[1]] = temp;
3084 craftingState = CraftingState.None; // reset state
3085 }
3086 }
3087
3088 void OnDragAndDrop_InventorySlot_NpcPetReviveSlot(int[] slotIndices)
3089 {
3090 // slotIndices[0] = slotFrom; slotIndices[1] = slotTo
3091 if (inventory[slotIndices[0]].item.data is PetItem)
3092 FindObjectOfType<UINpcPetRevive>().itemIndex = slotIndices[0];
3093 }
3094
3095 void OnDragAndClear_SkillbarSlot(int slotIndex)
3096 {
3097 skillbar[slotIndex].reference = "";
3098 }
3099
3100 void OnDragAndClear_TradingSlot(int slotIndex)
3101 {
3102 CmdTradeOfferItemClear(slotIndex);
3103 }
3104
3105 void OnDragAndClear_NpcSellSlot(int slotIndex)
3106 {
3107 FindObjectOfType<UINpcTrading>().sellIndex = -1;
3108 }
3109
3110 void OnDragAndClear_CraftingIngredientSlot(int slotIndex)
3111 {
3112 // only if not crafting right now
3113 if (craftingState != CraftingState.InProgress)
3114 {
3115 craftingIndices[slotIndex] = -1;
3116 craftingState = CraftingState.None; // reset state
3117 }
3118 }
3119
3120 void OnDragAndClear_NpcPetReviveSlot(int slotIndex)
3121 {
3122 FindObjectOfType<UINpcPetRevive>().itemIndex = -1;
3123 }
3124}