Wildstar

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.

Tutorial: The Cryo Awakening Protocol area is designed for players completely unfamiliar with standard MMO conventions. It exists as a place of minimal distraction so new players can focus on learning the basics of movement and combat. Instancing this area was not possible due to technical limitations, but I was able to limit player distractions in other ways. Virtual walls for each section prevent players from moving too far forward or back without completing that section's objective, minimizing player confusion. I also created a custom script to control visibility as players move through different sections of the map. While in a section, players can only see objects and other players in that same section. This prevents unnecessary visual clutter while still providing the social assistance that comes from watching someone else complete the same objective.
Shade's Eve: My goal for the Forest section of the Halloween event instance was to establish a creepy playspace not often found in MMOs that rewarded multiple playthroughs. This meant taking away standard conventions players usually rely on and providing limited resources. Players have no access to a map or their standard objective directional arrows here, only a vague objective to "Escape the Forest" and several optional objectives to escape in a limited amount of time and avoid all Shadeling enemies. Players must then rely on a limited quantity of flares and signal guns to reunite with their lost group members, combat Shadelings, and escape the Forest.
Shade's Eve: Players attacked by Shadelings transform into a Shadeling themselves, complete with custom music and VFX. They must either find and consume a specific NPC or have a friend shoot them with their limited flare ammo to return to normal and continue their escape. The video above begins with a playthrough of the Forest section of the Shade's Eve instance (courtesy of WildWeave).
Shade's Eve: Aside from the Shadelings, environmental traps are the other main challenge players must contend with in the Forest. Players encounter mists that disorient, roots that grasp, bear traps that stun, spirits that slow, and mysterious packages that may provide rewards for the adventurous. Each of the 10 traps has a visual tell that’s not as obvious as a standard telegraph, placed in a combination of specific spawns per location and random spawns to encourage replayability throughout the holiday.
Starting Zones: I made dozens of quests and cinematics for each faction's starting zones, all designed to teach common concepts while still showcasing WildStar's quirky personality. The quest "Green Thumb" in the Exile Arkship teaches creature activation and go-to objectives in a fun way by tasking players to uproot sentient vegetables and lead them to safety. The quest was the most positively received tutorial quest in all focus groups, and the fan-favorite veggies have become some of the most iconic creatures in the game.
Winterfest: I worked on four events for the Christmas event instance: BOGO Boards, Deals Going Fast, Fire Sale, and the Main Event final boss encounter. My work included custom spells, objectives, and scripting to make the content fun and scalable for 1-5 players. The video above showcases Deals Going Fast and BOGO Boards as the first two events, and it concludes with the Main Event encounter (courtesy of WildWeave).
Veteran Shiphands: When we created end-level versions of our scalable instances, I made additional content to provide greater challenge and variation for veteran players. My work included sudden spaceship invasions, laser jump puzzles, and experimental weaponry players could use on enemies (all pictured above).

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.

Cryo Awakening Protocol: Right when players spawn into the starting zone, they see their ultimate goal. The steady incline through this zone coupled with the beam of light at the highest point guide players forward and provide a constant landmark.
Cryo Awakening Protocol: The minimalist environment and simple level design began were consistent throughout the design process (see design doc below). The tron-esque visuals provide an enticing playspace that can feel open but still contain player movement. One of the few changes from design to implementation was to increase the height of the last jump in the final platform section to encourage players to double jump (Video courtesy of WildWeave).
Whitevale Wipeout Hoverboard Track: The entire zPrix hoverboard event was created very quickly with almost no art bandwidth, so I had to design my race track around an existing zone. I chose to highlight the massive peaks of the Whitevale zone that players were otherwise unable to access. Players speed over frozen waterfalls and across icy peaks, culminating in a massive jump off the tallest mountain in the zone.
Whitevale Wipeout Hoverboard Track: The track has several high risk, high reward shortcuts to encourage replayability. When players jump off the massive peak at the mid-point of the track, speed rings lead them continually downward. Experienced players eventually discover an alternate path (highlighted by speed boosts) that leads through a valley filled with tornados. Players can shave several seconds off their time with this shortcut, but hitting a tornado wastes precious seconds. The track has several other optional shortcuts and alternate routes on the second half of the track (Video courtesy of WildWeave).

Cryo Awakening Protocol – Design Doc

BACK TO TOP

Combat Design

Created and balanced the AI/spells for enemies, including “Winterfest” event bosses and “Guardian Pyralos” raid attunement encounter.

Winterfest Boss: The final event for the Winterfest instance consists of three boss fights. The first summons four static turrets that all pick the same random target. This required both scripting to select/broadcast the target and AI work to receive the broadcast and simultaneously choose the appropriate attack (Video courtesy of WildWeave).
Winterfest Boss: The second boss consists of two fairly standard bot combat packages, but I spruced them up by adding present visuals to the two different bots. This both distinguishes them for the player (allowing them to pick the appropriate combat response per bot) and looks hilarious.
Winterfest Boss: The final boss spawns his own adds over time. The first spell he casts is an AOE attack that summons a small present bot and places the summon spell on cooldown for about 20 seconds. The bot has its own suite of custom moves and telegraphs, which can be problematic for players if they don't destroy the add before the boss's summon spell goes off cooldown. Players must quickly destroy the bots or risk being overrun more and more adds. The boss also has his own suite of attacks including light and heavy melees and a chasing whirlwind spin that's designed to move the player around the environment and create additional time pressure for removing the adds.
Guardian Pyralos (Raid Attunement): When initially implemented, the first step of our raid attunement was overly tedious and nearly impossible. I was tasked with improving the encounter, so I created three distinct attacks and a more steady ramp in difficulty. When the boss spawns, he is invulnerable in a pit of damaging lava. Players must activate one of the extinguishers positioned around him to cool the lava, makes the boss vulnerable, and deal damage for a short period of time. Extinguishers despawn after each use, so players must continually move around the arena.
Guardian Pyralos (Raid Attunement): Once at the beginning of each phase (i.e., boss reaches a certain HP threshold), the boss spawns fire mines that damage and stun players. These remain where they were cast and only remove when a player runs into them, so players have to avoid more and more fire mines as the fight continues. After the mines spawn, the boss alternates between a a targeted fire cone and a rotating AOE fire attack that circles the arena. The fire cone interrupts players activating the extinguishers, and the rotating AOE attack increases in size with every phase. Players must run around the arena to avoid the damage from the AOE attack and activate extinguishers, which becomes more difficult over time as fire mines increase and available extinguishers decrease.
Shadeling: Shadelings have two primary attacks: a short-range melee and a mid-range stun. Since players have a limited suite of abilities in the forest, the enemies are equally simple. The melee turns players into Shadelings and fails the instance optional objective. The stun acts as a gap-closer for the enemy while showcasing to players one of WildStar's unique CC mechanics: dashing to remove CC effects.

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.

Summon Items: I created a new template for high-value summon items that interact with one another. When summoned, the toy creature searches nearby for its counterpart. If found, it will turn to the other creature and deliver specific lines with a designated animation. Otherwise, it will turn to and address the player. Designers can now use my template to quickly and easily create additional summon items for their content.
Placeable Items: I created another template for placeable toys that anyone can interact with. A prime example is the Winterfest Turret. Players use this toy to place a present-dispensing turret. When spawned, any player can activate the turret to shoot a collectible present several feet away. If the turret does not have enough room to shoot, it explodes as a failsafe and spawns all three presents on its location. The template has since been used to create toys for nearly a dozen other zones and events.
Standardization and Tracking: I standardized the toy creation process for all designers, currently maintain the toy tracking spreadsheets (includes drop rates and loot tables), made the first pets for WildStar, and created more than 80% of all existing toys in the game (including custom scripts, spells, and FX).

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.

Anniversary: I worked with our engineering, art, audio, narrative, and design departments to design the entirety of WildStar's first anniversary event. My work includes the main chase item for WildStar's first Anniversary: the Anniverserowsdower. The majestic end result speaks for itself.
Anniversary: Because this was WildStar's first live event, it required several new systems and extensive testing. I wrote system specs with our engineers for a new live event item distribution system and worked extensively with QA to ensure the event would function as expected. Our robust preparation paid off when an unknown, long-standing bug in the live environment initially prevented the event from turning on as expected. My feature documentation and test plan provided the live investigations team with the information they needed to quickly track down the issue and get the event back up with minimal downtime.
Anniversary: I oversaw the creation of nearly a dozen additional new assets (e.g., toys, decor, FX, etc.) and systems for this event. My work included the initial design documentation, task creation and management, inter-departmental meetings, design implementation, and final signoff. The video above showcases the Danceplosion Device final product, one of the custom deliverables for this feature (courtesy of Christopher Kaster).

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.

Tutorial Workflow

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

New Player Experience: This video contrasts our player onboarding experience before and after our FTP transition (courtesy of cecilandblues).

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.

BACK TO TOP

Resume/Contact