I worked as a game designer at Carbine Studios for 3 years. I shipped WildStar as a premium subscription-based MMO, and I also worked on the free-to-play relaunch of the game.
Mission Design
Worked on multiple teams to implement scalable end-game instances, holiday events, daily quests, tutorial areas, and training quests. Responsibilities included gameplay, scripting, cinematics, FX, dialogue, and NPC spawning.
Shade’s Eve Forest – Design Doc
Cryo Awakening Protocol – Script
BACK TO TOP
Level Design
Designed play spaces from paper design through implementation, including “Cryo Awakening Protocol” tutorial level and “Whitevale Wipeout” mount race track.
BACK TO TOP
Combat Design
Created and balanced the AI/spells for enemies, including “Winterfest” event bosses and “Guardian Pyralos” raid attunement encounter.
BACK TO TOP
Scripting
Wrote new “Race_Quest” template script to allow for flexible, rapid creation of a new quest type. Wrote many scripts to handle cinematics, unique player mechanics, and combat encounters. Trusted as a code reviewer for the scripting team.
Carbine has a proprietary C-based language they use in conjunction with a visual scripting tool for all their scripting needs. Because the language is so robust, designers must receive special training before they are allowed to move beyond visual scripting and work directly with the code. I received that training and additional permissions to use the more complex features of that language (e.g., arrays). In addition to writing dozens of scripts for various features, I’ve also been tapped to help the scripting team audit designer scripts, review code, and evaluate new programming tests for potential applicants.
I created the Race_Quest template script as part of the Hoverboard zPrix event. We had basic systems in place for trigger-based objectives, so I leveraged that to create a robust general script with 12 public properties. Those allow designers to almost instantly create a race-type quest and provides the flexibility to add things like optional countdowns, race spells, required vehicles, etc.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | //Race_Quest //@Functionality for a race-type quest where the first objective is the starting line and the final objective is the finish line. actor unit Player; actor quest Quest; actor unit Obj0_Volume; actor unit Obj1_Volume; actor unit Obj2_Volume; actor unit Obj3_Volume; actor unit Obj4_Volume; actor unit Obj5_Volume; //@Trigger volume to complete the first objective property trigger_volume OBJ0_VOLUME = 0; //@Trigger volume to complete the second objective property trigger_volume OBJ1_VOLUME = 0; //@Trigger volume to complete the third objective property trigger_volume OBJ2_VOLUME = 0; //@Trigger volume to complete the fourth objective property trigger_volume OBJ3_VOLUME = 0; //@Trigger volume to complete the fifth objective property trigger_volume OBJ4_VOLUME = 0; //@Trigger volume to complete the sixth objective property trigger_volume OBJ5_VOLUME = 0; //@Optional prerequisite that must be met for each objective to increment property prerequisite OBJ_COMPLETE_PREREQ = 0; //@Optional spell to apply to player when race begins (first obj completes) and persists throughout the race. Removed if player finishes, botches, or abandons the quest. property spell RACE_START_SPELL = 0; //@Optional spell to apply to player when each objective is complete property spell OBJ_COMPLETE_SPELL = 0; //@Optional spell to apply to player when quest is achieved (in place of objective complete spell) property spell RACE_COMPLETE_SPELL = 0; //@Set to true if you want players to remain in OBJ0_VOLUME for 3 seconds (with accompanying story panels) before the race begins. property bool COUNTDOWN = false //@Optional spell to apply only when the countdown is active. Removes when the countdown is interrupted or the player botches/abandons the quest (useful for anim states). property spell COUNTDOWN_SPELL = 0; const story_panel COUNTDOWN_READY = 2518; // HA15 - Race Countdown Ready - JDT const story_panel COUNTDOWN_3 = 2508; // HA15 - Race Countdown 3 - JDT const story_panel COUNTDOWN_2 = 2509; // HA15 - Race Countdown 2 - JDT const story_panel COUNTDOWN_1 = 2510; // HA15 - Race Countdown 1 - JDT const story_panel COUNTDOWN_GO = 2511; // HA15 - Race Countdown GO - JDT const story_panel COUNTDOWN_EXIT = 2512; // HA15 - Race Countdown Exit Warning - JDT const story_panel COUNTDOWN_PREREQ_FAIL = 2513; // HA15 - Race Countdown Prereq Fail - JDT const story_panel CHECKPOINT_NOT_ACTIVE = 2515; // HA15 - Race Checkpoint Not Active - JDT const story_panel CHECKPOINT_ALREADY_REACHED = 2516; // HA15 - Race Checkpoint Already Completed - JDT const story_panel CHECKPOINT_PREREQ_FAIL = 2517; // HA15 - Race Checkpoint Prereq Fail - JDT const text RACE_TIME_MESSAGE = 719181; // You completed the race in $c(timer) seconds! const text RACE_TIME_INVALID = 719340; // You completed the race! const spell TELEPORT_SCIENTIST = 69685; // Scientist Path Ability - Summon Player Teleport CSI - dmarsh - Tier 1 const spell TELEPORT_EXPLORER_1 = 58701; // Translocate Beacon - Explorer Path Ability - Tier 1 const spell TELEPORT_EXPLORER_2 = 58805; // Translocate Beacon - Explorer Path Ability - Tier 2 const spell TELEPORT_EXPLORER_3 = 58806; // Translocate Beacon - Explorer Path Ability - Tier 3 variable int obj_count_total = 0 variable int obj_count_current = 1 variable int race_time_start variable int race_time_total variable bool cheated = false message Player::EvaluateObjective(int obj) message Player::CheatDetection() message bool Player::CountdownValid(story_panel countdown_message) Player::OnStart() { //Define trigger volume actors and increment the objective count to get the total number of objectives if (OBJ0_VOLUME != null) { Obj0_Volume = GetVolumeUnit(OBJ0_VOLUME) obj_count_total++ } if (OBJ1_VOLUME != null) { Obj1_Volume = GetVolumeUnit(OBJ1_VOLUME) obj_count_total++ } if (OBJ2_VOLUME != null) { Obj2_Volume = GetVolumeUnit(OBJ2_VOLUME) obj_count_total++ } if (OBJ3_VOLUME != null) { Obj3_Volume = GetVolumeUnit(OBJ3_VOLUME) obj_count_total++ } if (OBJ4_VOLUME != null) { Obj4_Volume = GetVolumeUnit(OBJ4_VOLUME) obj_count_total++ } if (OBJ5_VOLUME != null) { Obj5_Volume = GetVolumeUnit(OBJ5_VOLUME) obj_count_total++ } //If the player has logged out in the middle of the race, increment their current obj count to match that if (Player.IsObjectiveCompleted(Quest, 0)) obj_count_current++ if (Player.IsObjectiveCompleted(Quest, 1)) obj_count_current++ if (Player.IsObjectiveCompleted(Quest, 2)) obj_count_current++ if (Player.IsObjectiveCompleted(Quest, 3)) obj_count_current++ if (Player.IsObjectiveCompleted(Quest, 4)) obj_count_current++ if (Player.IsObjectiveCompleted(Quest, 5)) obj_count_current++ //If the player has started the race and logs back in, re-apply the race spell. if (Player.IsObjectiveCompleted(Quest, 0) && RACE_START_SPELL != null && Player.UnderSpell(RACE_START_SPELL)== 0) Player.ApplySpell(Player, RACE_START_SPELL) } //Cleanup for if the player finishes, botches, or abandons the quest Player::OnEnd() { if (Player.UnderSpell(COUNTDOWN_SPELL) != 0) Player.RemoveSpell(COUNTDOWN_SPELL) if (Player.UnderSpell(RACE_START_SPELL) != 0) Player.RemoveSpell(RACE_START_SPELL) } //Evaluates whether the player meets the prereqs to complete the obj when they enter its accompanying trigger Obj0_Volume::OnVolumeEnter(unit player) { if (player != Player) return if (!Player.IsObjectiveActive(Quest, 0)) return //Counts down from 3, checking to make sure the player still meets the prereq and is inside the trigger volume before starting the race if (COUNTDOWN) { //Spell used to determine if player is currently already in the countdown process if (Player.UnderSpell(COUNTDOWN_SPELL) != 0) return //Countdown wait for animations if ((OBJ_COMPLETE_PREREQ == null) || ((OBJ_COMPLETE_PREREQ != null) && (Player.MeetsPrerequisite(OBJ_COMPLETE_PREREQ)))) { Player.ApplySpell(Player, COUNTDOWN_SPELL) Player.ShowStoryPanel(COUNTDOWN_READY) wait(1300) } else { Player.ShowStoryPanel(COUNTDOWN_PREREQ_FAIL) return } //Countdown from three, if player still meets conditions then advance obj if (!Player.CountdownValid(COUNTDOWN_3)) return wait(1000) if (!Player.CountdownValid(COUNTDOWN_2)) return wait(1000) if (!Player.CountdownValid(COUNTDOWN_1)) return wait(1000) if (!Player.CountdownValid(COUNTDOWN_GO)) return Player.EvaluateObjective(0) Player.RemoveSpell(COUNTDOWN_SPELL) } else Player.EvaluateObjective(0) } Obj1_Volume::OnVolumeEnter(unit player) { if (player != Player) return Player.EvaluateObjective(1) } Obj2_Volume::OnVolumeEnter(unit player) { if (player != Player) return Player.EvaluateObjective(2) } Obj3_Volume::OnVolumeEnter(unit player) { if (player != Player) return Player.EvaluateObjective(3) } Obj4_Volume::OnVolumeEnter(unit player) { if (player != Player) return Player.EvaluateObjective(4) } Obj5_Volume::OnVolumeEnter(unit player) { if (player != Player) return Player.EvaluateObjective(5) } //Apply the race spell when the player enters the starting area (objective 0) Player::OnQuestObjectiveCompleted(int obj) { if (RACE_START_SPELL != null) Player.ApplySpell(Player, RACE_START_SPELL) race_time_start = GetTime() } Player::OnQuestAdvanced(int obj) { //If players complete an obj that is not the final obj, ensure they have the race spell and then apply the obj complete spell if (obj_count_current < obj_count_total) { if ((RACE_START_SPELL != null) && (Player.UnderSpell(RACE_START_SPELL) == 0)) Player.ApplySpell(Player, RACE_START_SPELL) obj_count_current++ if (OBJ_COMPLETE_SPELL != null) Player.ApplySpell(Player, OBJ_COMPLETE_SPELL) } //If it is the final obj, apply the quest complete spell instead else if (obj_count_current == obj_count_total) { obj_count_current++ if (RACE_COMPLETE_SPELL != null) Player.ApplySpell(Player, RACE_COMPLETE_SPELL) if (race_time_start != null && !cheated) { LocalizedText_Clear() race_time_total = GetTime() - race_time_start if (race_time_total > 999) race_time_total = 999 LocalizedText_AddActorTextKeyed("timer", null, race_time_total) Player.MakeStoryPanel(RACE_TIME_MESSAGE, StoryPanel_Urgent, 10000) } else { Player.MakeStoryPanel(RACE_TIME_INVALID, StoryPanel_Urgent, 10000) } } } //If the objective is not complete, is active, and there is either no objective prereq or there is and the player meets it, increment the objective. Player::EvaluateObjective(int obj) { if (Player.GetQuestState(Quest) == QUEST_ACHIEVED) return if (Player.IsObjectiveCompleted(Quest, obj)) Player.ShowStoryPanel(CHECKPOINT_ALREADY_REACHED) else if (!Player.IsObjectiveActive(Quest, obj)) Player.ShowStoryPanel(CHECKPOINT_NOT_ACTIVE) else { if (OBJ_COMPLETE_PREREQ == null) Player.AdvanceObjective(Quest, obj) else if (Player.MeetsPrerequisite(OBJ_COMPLETE_PREREQ)) Player.AdvanceObjective(Quest, obj) else Player.ShowStoryPanel(CHECKPOINT_PREREQ_FAIL) } } //Evaluates whether the player meets the defined prereq and is still inside the volume. If not, send the appropriate messages. bool Player::CountdownValid(story_panel countdown_message) { if (OBJ_COMPLETE_PREREQ != null && !Player.MeetsPrerequisite(OBJ_COMPLETE_PREREQ)) { Player.ShowStoryPanel(COUNTDOWN_PREREQ_FAIL) Player.RemoveSpell(COUNTDOWN_SPELL) return false } else if (Player.IsInsideTriggerVolume(Obj0_Volume)) { Player.ShowStoryPanel(countdown_message) return true } else { Player.ShowStoryPanel(COUNTDOWN_EXIT) Player.RemoveSpell(COUNTDOWN_SPELL) return false } } Player::OnSpellCast(spell spellId, unit target) { Player.CheatDetection() } Player::OnSpellCast(spell spellId, unit target) { Player.CheatDetection() } Player::OnSpellCast(spell spellId, unit target) { Player.CheatDetection() } Player::OnSpellCast(spell spellId, unit target) { Player.CheatDetection() } Player::CheatDetection() { if (Player.IsObjectiveCompleted(Quest, 0)) cheated = true } //EOF |
The Proto-Present Turret toy for Winterfest is one of the most unique consumables in the game. Players can summon the turret, and then any player can activate it to fire a random present a variable distance. My script ensures there are no repeat presents from the array of possible choices. It also has a failsafe in place if there is no room to shoot presents, whereupon it will destroy the turret and spawn the remaining turrets around the creature’s position.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | //Proto_Present_Turret_Toy //@Fires three randomly selected, non-repeating present spells and prevents overlapping of activations actor unit NPC; const signal TURRET_CAST = 29101; // WF15_PRESENT_TURRET_CAST_JDT const signal TURRET_ACTION = 29110; // WF15_PRESENT_TURRET_ACTION_JDT const int PRESENTS_MAX = 3 array spell PRESENTS = { PRESENT_0, PRESENT_1, PRESENT_2, PRESENT_3, PRESENT_4 } array creature PRESENT_CREATURES = { PRESENT_CREATURE_0, PRESENT_CREATURE_1, PRESENT_CREATURE_2, PRESENT_CREATURE_3, PRESENT_CREATURE_4 } variable int present_count = 0 message NPC::ShuffleArray() //Shuffle the present array to get three randomly selected, non-repeating presents NPC::OnStart() { ShuffleArray() } //When the turret is activated, set it busy and cast the summon present spell NPC::OnSignal(signal id, unit sender, int data) { NPC.SetBusy(true) //The summon spell sets the unit unbusy if it casts successfully, meaning there was room to spawn the present NPC.CastSpell(NPC, PRESENTS[BucketGet(present_count)]) wait(2000) //Check to see if the unit is still busy, meaning the spell was unable to spawn presents because of world collision if (NPC.IsValid() && NPC.IsBusy() && present_count<3) { if (present_count < 2) SpawnNear(PRESENT_CREATURES[BucketGet(2)], NPC.GetPosition(), (1), cast(240~270)) if (present_count < 1) SpawnNear(PRESENT_CREATURES[BucketGet(1)], NPC.GetPosition(), (1), cast(120~150)) SpawnNear(PRESENT_CREATURES[BucketGet(0)], NPC.GetPosition(), (1), cast(0~30)) Kill(NPC) } } //If the spell makes it to the action phase, a present has been summoned and we can increment the count NPC::OnSignal(signal id, unit sender, int data) { present_count++ if (present_count < PRESENTS_MAX) NPC.SetBusy(false) } //Shuffle the presents from the array NPC::ShuffleArray() { BucketAlloc(PRESENTS.count); BucketSet(0, 0); variable int i; for (i = 1; i < PRESENTS.count; i++) { variable int j = 0 ~ i; BucketSet(i, BucketGet(j)); BucketSet(j, i); } } //EOF |
The Creature Summoner toy script searches nearby for a specified creature counterpart. If found, the creature attached to this script will turn to its counterpart and deliver specific lines with a designated animation. Otherwise, it will turn to and address the player. The creature delivers two lines and then despawns.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | //Creature_Summoner_Toy //@Summons either the Caretaker or Avatus, spawns them facing the player, and then delivers one of 5 different lines actor void Spell; //@The creature to be summoned property creature SUMMONED_CREATURE; const creature CARETAKER_CREATURE = 70269; // Levian Bay PCP - Toys - Caretaker - JDT const creature AVATUS_CREATURE = 70270; // Levian Bay PCP - Toys - Avatus - JDT const ravel DESPAWN_SCRIPT = 264907; // s_caretaker_toy_despawn_JDT const signal DESPAWN_SIGNAL = 28249; // TOYS_CARETAKER_DESPAWN_SIGNAL_JDT const visual_effect CARETAKER_DESPAWN = 4635; // Default_Death_Fast array text CARETAKER_INTROS = { CARETAKER_INTRO_1, CARETAKER_INTRO_2, CARETAKER_INTRO_3, CARETAKER_INTRO_4, CARETAKER_INTRO_5 } array text AVATUS_INTROS = { AVATUS_INTRO_1, AVATUS_INTRO_2, AVATUS_INTRO_3, AVATUS_INTRO_4, AVATUS_INTRO_5 } array text CARETAKER_OUTROS = { CARETAKER_OUTRO_1, CARETAKER_OUTRO_2, CARETAKER_OUTRO_3 } array text AVATUS_OUTROS = { AVATUS_OUTRO_1, AVATUS_OUTRO_2, AVATUS_OUTRO_3 } array text CARETAKER_ENCOUNTERS = { CARETAKER_ENCOUNTER_1, CARETAKER_ENCOUNTER_2, CARETAKER_ENCOUNTER_3, CARETAKER_ENCOUNTER_4, CARETAKER_ENCOUNTER_5 } array text AVATUS_ENCOUNTERS = { AVATUS_ENCOUNTER_1, AVATUS_ENCOUNTER_2, AVATUS_ENCOUNTER_3, AVATUS_ENCOUNTER_4, AVATUS_ENCOUNTER_5 } //@If true, this is the Caretaker speaking. If false, it is Avatus. property bool CARETAKER message bool Spell::ValidTarget(unit target) Spell::OnSpellAction(unit source, unit target, spell id, int data) { variable unit spawned_creature = Spawn(SUMMONED_CREATURE, target.GetPosition(), (source.GetFacing() - 180a)) variable unit nemesis variable bool nemesis_encountered = false variable int search_distance = 10 //This wait exists because both the Caretaker and Avatus creatures can receive the same despawn signal from the player //They also share the same stack group of 1 per caster //If a player summons the caretaker and then avatus, the first spell will end (because of the stack group) //This will send the despawn signal to both creatures, removing the just spawned avatus as well //Thus, the wait exists to allow for the signal to be sent before spawning in the new unit wait(1) spawned_creature.AttachScript(DESPAWN_SCRIPT) spawned_creature.SetScriptProperty(DESPAWN_SCRIPT, "PLAYER", cast(source)) spawned_creature.SetScriptProperty(DESPAWN_SCRIPT, "CARETAKER", cast(CARETAKER)) spawned_creature.FaceUnit(source) wait(5000) if (!ValidTarget(target)) return //Check to see if the spawned creature's nemesis type is spawned nearby if (CARETAKER) nemesis = FindNearestCreature(AVATUS_CREATURE, target.GetPosition(), search_distance) else nemesis = FindNearestCreature(CARETAKER_CREATURE, target.GetPosition(), search_distance) //If there is no nemesis nearby or the creature is not ready for interaction, proceed with normal text if ((nemesis == null) || nemesis.GetScriptProperty(DESPAWN_SCRIPT, "INTERACT_READY") == 0) { spawned_creature.FaceUnit(source) if (CARETAKER) spawned_creature.BubbleSay(source, CARETAKER_INTROS[0 ~ (CARETAKER_INTROS.count - 1)]) else spawned_creature.BubbleSay(source, AVATUS_INTROS[0 ~ (AVATUS_INTROS.count - 1)]) } else { spawned_creature.FaceUnit(nemesis) if (CARETAKER) spawned_creature.BubbleSay(source, CARETAKER_ENCOUNTERS[0 ~ (CARETAKER_ENCOUNTERS.count - 1)]) else spawned_creature.BubbleSay(source, AVATUS_ENCOUNTERS[0 ~ (AVATUS_ENCOUNTERS.count - 1)]) nemesis = null nemesis_encountered = true } wait(10000) if (!ValidTarget(target)) return //If the nemesis has not already been encountered, check to see if the spawned creature's nemesis type is spawned nearby if (!nemesis_encountered) { if (CARETAKER) nemesis = FindNearestCreature(AVATUS_CREATURE, target.GetPosition(), search_distance) else nemesis = FindNearestCreature(CARETAKER_CREATURE, target.GetPosition(), search_distance) } //If there is no nemesis nearby, it has already been interacted with, or the creature is not ready for interaction, proceed with normal text if ((nemesis == null) || (nemesis.GetScriptProperty(DESPAWN_SCRIPT, "INTERACT_READY") == 0) || (nemesis_encountered == true)) { spawned_creature.FaceUnit(source) if (CARETAKER) spawned_creature.BubbleSay(source, CARETAKER_OUTROS[0 ~ (CARETAKER_OUTROS.count - 1)]) else spawned_creature.BubbleSay(source, AVATUS_OUTROS[0 ~ (AVATUS_OUTROS.count - 1)]) } //Otherwise, face nemesis and deliver encounter text else { spawned_creature.FaceUnit(nemesis) if (CARETAKER) spawned_creature.BubbleSay(source, CARETAKER_ENCOUNTERS[0 ~ (CARETAKER_ENCOUNTERS.count - 1)]) else spawned_creature.BubbleSay(source, AVATUS_ENCOUNTERS[0 ~ (AVATUS_ENCOUNTERS.count - 1)]) } wait (3500) if (!ValidTarget(target)) return //The creature will soon despawn, so make sure no other creatures try to interact with it spawned_creature.SetScriptProperty(DESPAWN_SCRIPT, "INTERACT_READY", 0) wait(5000) if (!ValidTarget(target)) return //Checks to see if the summoned creature is the caretaker //and if it's currently in the process of despawning because the spell was removed if ((CARETAKER) && (!cast(spawned_creature.GetScriptProperty(DESPAWN_SCRIPT, "DESPAWNING")))) { spawned_creature.SetScriptProperty(DESPAWN_SCRIPT, "DESPAWNING", 1) spawned_creature.AttachVisual(CARETAKER_DESPAWN) wait(1500) Despawn(spawned_creature) } else if (!CARETAKER) Kill(spawned_creature) } //If the spell is removed by the player, send a signal to despawn the summoned creature Spell::OnSpellEnd(unit source, unit target, spell id, int data) { source.BroadcastAll(DESPAWN_SIGNAL, cast(source)) } //Check to see if the target is both valid and alive bool Spell::ValidTarget(unit target) { if ((target.IsValid()) && (!target.IsDead())) return true else return false } //EOF |
BACK TO TOP
Itemization
Created the first toys and pets for WildStar and the majority of all other existing toys. Balanced loot tables and reward structure for “Anniversary” events.
Specific loot tables and MTX reward structures are not shown here because of their sensitive nature, but additional information can be provided on request.
BACK TO TOP
Feature Lead
Wrote and updated feature documentation on Confluence, worked with other departments to create new assets and systems to support WildStar’s first live event, and tracked progress and dependencies using JIRA.
Wildstar Anniversary – Design Doc
BACK TO TOP
Domain Owner
Implemented new workflow to minimize confusion, rework, and last-minute requests that would typically cost engineers and UI artists days of unexpected work. Standardized system implementation with new how-to documentation, best practices guidelines, and constant communication across departments.
Before I was Domain Owner for Tutorial Systems, there was no set process or standards for player onboarding or training. Designers would create tutorial panels and teach concepts in various ways, and they would often implement them with expensive scripting that didn’t take full advantage of our systems. I created a new Confluence page on our internal wiki with a step-by-step how-to guide for making tutorials, and I included naming conventions and standards for how tutorials should look and work.
The final component was to mitigate last-minute tutorial requests that plagued our engineering and UI teams. Most tutorials require an engineer to create a new code event to fire the tutorial and an anchor point placed by a UI artist to designate which part of the UI the tutorial should point to. I implemented a new workflow to ensure tutorial costs are factored into all future features at kickoff, got all producers and leads on board with the process, and then communicated out the new workflow to all relevant departments. Since then, we’ve severely reduced the unexpected workload on those teams.
BACK TO TOP
Free-to-Play Experience
Spearheaded the redesign of the 1-6 new player experience and led external focus groups in preparation for our free-to-play transition. Reworked several events in their second year to integrate MTX rewards, lockbox support, and player retention and monetization initiatives.
Our transition to a free-to-play model also came with an increased emphasis on metrics and data. We were seeing high churn in the 1-6 new player experience, so I spearheaded the redesign of the new player experience ahead of our FTP relaunch by focusing on three key points:
- Early Combat – showcase what makes WildStar unique in the very first quest
- Streamlined Tutorials – push non-essential information out of the starting zones and cut down tutorial play time in half
- 3 Tutorial Entry Points – minimize unnecessary teaching to experts and provide a welcoming environment for casual players unfamiliar with MMOs
I also led external focus group testing to identify problem areas and provide additional data (e.g., heat maps, likert scale data, etc.) to the design team. Churn was expected to worsen significantly with the onslaught of mildly curious players that always flock to FTP games, but instead all indicators either remained consistent or improved with the relaunch.
My work on creating MTX-focused events for WildStar are still unannounced, so additional information can only be provided on request at this time.