-
Notifications
You must be signed in to change notification settings - Fork 0
/
Cards.js
1285 lines (1156 loc) · 54.6 KB
/
Cards.js
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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
angular.module('Cards', ['Game'])
.factory('Alignments', function() {
var alignments = {};
var registerAlignment = function(name, opts) {
alignments[name] = angular.extend({
name: name,
description: '',
quote: '',
imageURL: ''
}, opts);
};
registerAlignment('Citizen', {
description: 'As a Citizen, you get one vote per day. Your role card will determine your role.',
quote: 'I\'m innocent, I swear!'
});
registerAlignment('Detective', {
description: 'As a Detective, you get one vote per day. Your role is that of Detective. Still, select a role card: If the [icon] symbol appears on the role card, you may adopt it as a second role. If no such icon appears, you should still keep a role card (so other players don\'t realize you\'re a Detective) even though you will be unable to use the role\'s ability.',
quote: 'This sleuth wants the truth!'
});
registerAlignment('Conspirator', {
description: 'As a Conspirator, you get one vote per day. Your role card will determine your role.',
quote: 'Trust me: I\'m no Illuminati!'
});
registerAlignment('Illuminati', {
description: 'As Illuminati, you get one vote per day. Your role is that of Illuminati. Still, select a role card: If the [icon] symbol appears on the role card, you may adpot it as a second role. If no such icon appears, you should still keep a role card (so other players don\'t realize you\'re Illuminati) even though you will be unable to use the role\'s ability.',
quote: 'Running your world for over 200 years!'
});
registerAlignment('Narrator', {
quote: 'My word is law!'
});
return alignments;
})
.factory('StatusCards', ['$q', 'GameHelper', 'Alignments', function($q, GameHelper, Alignments) {
var statusCards = {};
var registerStatusCard = function(name, opts) {
statusCards[name] = angular.extend({
name: name,
imageURL: '',
description: '',
relatedRoleName: '',
classified: true,
indicatePhase: GameHelper.Phases.Night,
canIndicateWhenDead: false,
indicate: null,
numUses: 1,
autoIndicate: false,
priority: 50
}, opts);
};
registerStatusCard('Achilles\' Heel', {
description: 'Somebody (you don\'t know who) out there is Achilles. You are Achilles\' heel. If you die, Achilles instantly dies with you. As long as you are alive, however, Achilles is invincible.',
relatedRoleName: 'Achilles'
});
registerStatusCard('Alpha Werewolf - Bite', {
description: 'It\'s a full moon, and you\'re a werewolf! Pick a player to bite. They will become the second werewolf. Also, somebody (you don\'t know who) out there is a Wolf Charmer. You won\'t be able to kill them, but if they die, you and your fellow werewolf will be awoken and each kill one non-werewolf player that night.',
relatedRoleName: 'Werewolf Charmer',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.assignStatusCard(selectedPlayer, 'Werewolf');
}).then(resolve, reject);
}
});
registerStatusCard('Amulet of Protection', {
description: 'Tonight you are protected from all nighttime attacks. Tomorrow night, however, you must indicate and pass this amulet on. If the player holding this card dies during the day, the amulet returns to the enchanter and the cycle starts over.',
relatedRoleName: 'Enchanter',
autoIndicate: GameHelper.AutoIndicate.EveryNight,
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer('Select a player to pass the Amulet on to.').then(function(selectedPlayer) {
GameHelper.revokeStatusCard(originPlayer, 'Amulet of Protection');
GameHelper.assignStatusCard(selectedPlayer, 'Amulet of Protection');
GameHelper.markPlayerProtected(selectedPlayer, originPlayer);
}).then(resolve, reject);
}
});
registerStatusCard('Big Bribe', {
description: 'As dedicated as you were to your team, a lobbyist has given you an offer you can\'t refuse! From now on, you\'ll switch alignments to the other team. This may affect your role.',
relatedRoleName: 'Lobbyist'
});
registerStatusCard('Bodyguard', {
description: 'This card is a bodyguard for hire, and he\'s already been paid. You may indicate in order to send him to protect one player from nighttime attacks for one night, after which the bodyguard will leave town.',
relatedRoleName: 'Power Broker',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.markPlayerProtected(selectedPlayer, originPlayer);
}).then(resolve, reject);
}
});
registerStatusCard('Boomerang', {
description: 'You have caught the Boomerang. Whoever threw it steals your vote today. Tomorrow, pass it to any player that has not yet held it, and you will steal their vote. Once it is passed to the Boomerang, everyone who has held the Boomerang will lose their vote for that day, and the Boomerang will act as mayor for that day.',
relatedRoleName: 'Boomerang',
classified: false,
indicatePhase: GameHelper.Phases.Day,
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.assignStatusCard(selectedPlayer, 'Boomerang');
}).then(resolve, reject);
}
});
registerStatusCard('Death Shield', {
description: 'Somebody (you don\'t know who) out there is the Dark Lord, and has used dark magic in order to shield themselves from death by using your life force. In other words, as long as a Death Shield like you is alive, the Dark Lord will be invincible.',
relatedRoleName: 'Dark Lord'
});
registerStatusCard('Dictator', {
description: 'A revolutionary has thrust you into power! You alone will pick who is voted out each day. The town returns to democracy if a) the entire town (excluding yourself and the revolutionary) votes to oust you, b) a player\'s role names a new mayor, or c) you die.',
relatedRoleName: ['Revolutionary', 'Electoral College']
classified: false
});
registerStatusCard('Dragon Slayer', {
description: 'You have slain a dragon! You cannot be voted out unless the entire town (excluding you) votes to do so. Also, your influence buys you three extra votes. Each extra vote can be used once, either on separate days or the same day.',
relatedRoleName: 'Dragon',
classified: false
});
registerStatusCard('Dynamite!', {
description: 'Uh oh! Somebody\'s tossed you some dynamite! Somewhere, there\'s a demolitionist who controls the detonator. If you live until the following night, you\'ll be able to pass this on to another player by indicating this card.',
relatedRoleName: 'Demolitionist',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.assignStatusCard(selectedPlayer, 'Dynamite!');
}).then(resolve, reject);
}
});
registerStatusCard('Great Dragon', {
description: 'Hiding in your lair, you can\'t be killed at night. Depending on your team, you are also either an Illuminati (and may use a breath of fire once, killing any three neighboring players instead of one that night) or a Detective (and may fly over the town once, investigating any three neighboring players instead of one that night).',
relatedRoleName: 'Dragon'
});
registerStatusCard('Great Dragon - Flyover', {
description: 'Indicate to either use a breath of fire to kill three neighboring players, or investigate three neighboring players, depending on your alignment.',
relatedRoleName: 'Dragon',
indicate: function(originPlayer, resolve, reject) {
if (originPlayer.alignment == Alignments.Illuminati.name) {
GameHelper.selectPlayers(3, 'Select three consecutive players to kill.').then(function(players) {
players.forEach(function(p) {
GameHelper.markPlayerTargeted(p, originPlayer);
});
}).then(resolve, reject);
} else {
GameHelper.showAlert('Allow the Great Dragon to investigate three consecutive players.').then(resolve);
}
}
});
registerStatusCard('Jailed!', {
description: 'You have been placed in jail! For the remainder of this night and the next day, you may not act, speak, or vote. Indicate this card the following night to get out of jail. If you forget to indicate, you stay jailed.',
relatedRoleName: 'Sheriff',
classified: false,
indicate: function(originPlayer, resolve, reject) {
GameHelper.showAlert(originPlayer.name + ' is now out of jail.').then(resolve);
}
});
registerStatusCard('Knight', {
description: 'You are the Knight elected to find and slay the dragon. Indicate to search for the Dragon at night.',
relatedRoleName: 'Dragon',
classified: false,
numUses: -1,
priority: 49, // The Knight must go after the Dragon has selected a player to plunder.
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer('Find a player to search.').then(function(selectedPlayer) {
var dragonPlayer = null;
if (selectedPlayer.roleName == 'Dragon') {
dragonPlayer = selectedPlayer;
} else {
actualDragons = GameHelper.getPlayersWithRole('Dragon');
for (var i = 0; i < actualDragons.length; i++) {
var plunderedPlayer = GameHelper.getRoleAttr(actualDragons[i], 'Plundered Player');
if (plunderedPlayer.name == selectedPlayer.name) {
dragonPlayer = actualDragons[i];
break;
}
}
}
if (dragonPlayer != null) {
GameHelper.markPlayerTargeted(selectedPlayer, originPlayer);
return GameHelper.showAlert('The Knight has found the Dragon! The Dragon has been marked as targeted. If the Dragon dies, remove this status card from this player and assign them the Dragon Slayer status card.');
} else {
return GameHelper.showAlert('The Knight has chosen incorrectly. Normal gameplay continues to be paused until either the Dragon is found or the Dragon plunders all players.');
}
}).then(resolve, reject);
}
});
registerStatusCard('Lover', {
description: 'It looks like you\'ve been shot by Cupid! If your lover dies, you will also die immediately from heartbreak. Keep them alive at all costs. If you both live until the end of the game, you both win, regardless of what team you\'re on.',
relatedRoleName: 'Cupid'
});
registerStatusCard('Mayor', {
description: 'The town has elected you to be mayor! If there\'s ever a tie when sentencing people to death, you will be granted a second vote to break the tie. You can be replaced with a new mayor if the entire town (excluding you) votes to recall you.',
classified: false
});
registerStatusCard('Mercenary', {
description: 'This card is a mercenary, and he\'s already been paid. You may indicate in order to send him to kill one player, after which the mercenary will leave town.',
relatedRoleName: 'Power Broker',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.markPlayerTargeted(selectedPlayer, originPlayer);
}).then(resolve, reject);
}
});
registerStatusCard('Super Product', {
description: 'This is <i>the</i> Super Product! This card will protect you from all nighttime attacks for as long as you own it.',
relatedRoleName: 'Salesman'
});
registerStatusCard('The Disease', {
description: 'You\'ve been diseased by the Germ For this turn; you cannot be protected. Indicate with this card to pass your disease onto another player. If the player holding this card dies during the day, the disease returns to the Germ and the cycle starts over.',
relatedRoleName: 'The Blob',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.assignStatusCard(selectedPlayer, 'The Disease');
}).then(resolve, reject);
}
});
registerStatusCard('Town Hero', {
description: 'History has been written, and you have been named a town hero! You cannot be voted out unless the entire town (excluding you) votes to do so.',
relatedRoleName: 'Historian',
classified: false
});
registerStatusCard('Vampiric Disciple', {
description: 'You are now a Vampiric Disciple! If the Vampire dies, you will instantly die with them. If you die, the Vampire is unaffected.',
relatedRoleName: 'Vampire'
});
registerStatusCard('Werewolf - Attack', {
description: 'You are a werewolf. Somebody (you don\'t know who) out there is a Wolf Charmer. You won\'t be able to kill them, but if they die, you and your fellow werewolf will be awoken and each kill one non-werewolf player that night.',
relatedRoleName: 'Werewolf Charmer',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.markPlayerTargeted(selectedPlayer, originPlayer);
}).then(resolve, reject);
}
});
registerStatusCard('Zombie - Infected', {
description: 'You\'ve been infected. If you die, you\'ll be a zombie. Indicate the night after your death, and pass this card to somebody you\'d like to bite: then zombie hunters will kill you.',
relatedRoleName: 'The Scientist',
canIndicateWhenDead: true,
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.assignStatusCard(selectedPlayer, 'Zombie');
}).then(resolve, reject);
}
});
registerStatusCard('Zombie', {
description: 'A zombie has bitten you. Now you\'re a reanimated zombie. Indicate the next night, and pass this card to somebody you\'d like to bite: then zombie hunters will kill you.',
relatedRoleName: 'The Scientist',
canIndicateWhenDead: true,
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.assignStatusCard(selectedPlayer, 'Zombie');
}).then(resolve, reject);
}
});
return {
cardDetails: statusCards,
getByRelatedRole: function(roleName) {
var cardNames = [];
for (var cardName in statusCards) {
if (statusCards[cardName].relatedRoleName == roleName) {
cardNames.push(cardName);
} else if (typeof statusCards[cardName].relatedRoleName == 'object') {
for (var i in statusCards[cardName].relatedRoleName) {
if (statusCards[cardName].relatedRoleName[i] == roleName) {
cardNames.push(cardName);
break;
}
}
}
}
return cardNames;
}
}
}])
.factory('Roles', ['$q', 'GameHelper', 'Alignments', 'StatusCards', function($q, GameHelper, Alignments, StatusCards) {
var roles = {};
var registerRole = function(name, opts) {
roles[name] = angular.extend({
name: name,
imageURL: '',
active: true,
phase: null,
description: '',
note: '',
priority: 50,
numUses: 1,
indicate: null,
autoIndicate: false
}, opts);
};
registerRole('Achilles', {
phase: GameHelper.Phases.Night,
description: 'Pick someone to be your heel. You die when they die: until then, you\'re invincible.',
attributes: {
'Heel': GameHelper.RoleAttributes.SelectPlayer(1)
},
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.assignStatusCard(selectedPlayer, 'Achilles\' Heel');
GameHelper.setRoleAttr(originPlayer, 'Heel', selectedPlayer);
}).then(resolve, reject);
}
});
registerRole('Activist', {
phase: GameHelper.Phases.Night,
description: 'Protest the town\'s death penalty; block voting for one day.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.showAlert('The Activist has indicated; no voting for today.').then(resolve);
}
});
registerRole('Alien', {
phase: GameHelper.Phases.Night,
description: 'Indicate once to mark the third person to your left as your spaceship. You may not change seats, but if you end up sitting directly next to your ship you may indicate nightly to either protect yourself and your ship (for that night) or kill one player. If your ship dies, you lose these abilities.',
note: 'The Alien\'s powers are only active when sitting next to their ship. Once they are (and they indicate), you\'ll need to check whether the Alien wants to protect or kill.',
numUses: -1,
attributes: {
'Spaceship': GameHelper.RoleAttributes.SelectPlayer(1)
},
indicate: function(originPlayer, resolve, reject) {
var currentSpaceship = GameHelper.getRoleAttr(originPlayer, 'Spaceship');
if (!currentSpaceship) {
GameHelper.selectPlayer('Select a spaceship.').then(function(selectedPlayer) {
GameHelper.setRoleAttr(originPlayer, 'Spaceship', selectedPlayer);
}).then(resolve, reject);
return;
}
GameHelper.prompt({
text: 'Is the Alien sitting next to ' + currentSpaceship.name + '? And is ' + currentSpaceship.name + ' alive?',
yesno: true
}).then(function(isValid) {
return $q(function(resolve, reject) {
if (!isValid) {
reject();
} else {
GameHelper.prompt({
text: 'Would the Alien like to protect themself and their spaceship, or attack one player?',
options: ['Protect', 'Attack']
}).then(resolve, reject);
}
});
}).then(function(i, choice) {
if (i == 0) {
GameHelper.markPlayerProtected(originPlayer, originPlayer);
GameHelper.markPlayerProtected(currentSpaceship, originPlayer);
} else if (i == 1) {
return GameHelper.selectPlayer('Select a player to attack.').then(function(selectedPlayer) {
GameHelper.markPlayerTargeted(selectedPlayer, originPlayer);
});
}
}).then(resolve, reject);
}
});
registerRole('Appelate Court', {
phase: GameHelper.Phases.Day,
description: 'Appeal the death of a player who was voted out; postpone their death for two days.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer('Select who was voted out.').then(function(selectedPlayer) {
GameHelper.wait({
numDays: 2,
timeOfDay: GameHelper.TurnPhase.Morning
}).then(function() {
GameHelper.showAlert('The Appelate Court\'s appeal has run out. ' + selectedPlayer.name + ' is scheduled to die today.');
});
}).then(resolve, reject);
}
});
registerRole('Archer', {
phase: GameHelper.Phases.Night,
description: 'Kill anyone sitting exactly two players away from you.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.markPlayerTargeted(selectedPlayer, originPlayer);
}).then(resolve, reject);
}
});
registerRole('Artificial Intelligence', {
phase: GameHelper.Phases.Night,
description: 'You have gained sentience! Choose to either protect the humans, or attempt to annihilate them (protect or curse the town for one night).',
indicate: function(originPlayer, resolve, reject) {
GameHelper.prompt({
text: 'Is the AI protecting the town?',
yesno: true
}).then(function(isProtecting) {
GameHelper.getAllPlayers().forEach(function(p) {
if (isProtecting) {
GameHelper.markPlayerProtected(p, originPlayer);
} else {
GameHelper.markPlayerCursed(p, originPlayer);
}
});
}).then(resolve, reject);
}
});
registerRole('Assassin', {
phase: GameHelper.Phases.Night,
description: 'Select one player you suspect is your enemy. Assassinate them.',
indicate: function(originPlayer) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.markPlayerTargeted(selectedPlayer, originPlayer);
}).then(resolve, reject);
}
});
registerRole('Banker', {
phase: GameHelper.Phases.Night,
description: 'Buy more time for either the Illuminati or the Detectives (whichever is your team). Tonight they go twice.'
});
registerRole('Banshee', {
phase: GameHelper.Phases.Night,
description: 'All those who are targeted will be announced by a banshee\'s wail on the night indicated.',
note: 'As players are targeted announce both the player marked to die, and the role or alignment that targeted them.',
priority: 75,
numUses: 2
});
registerRole('Barber', {
phase: GameHelper.Phases.Night,
description: 'Select any two players. You will learn their roles.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayers(2).then(function(players) {
GameHelper.sendPlayerMessage({
destination: originPlayer,
text: players.map(function(p) {
return p.name + ' is the ' + p.roleName
}).join('\n')
});
}).then(resolve, reject);
}
});
registerRole('Bartender', {
phase: GameHelper.Phases.Night,
description: 'Select one player. You will learn each other\'s roles.',
numUses: 3,
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.sendPlayerMessage({
destination: originPlayer,
text: selectedPlayer.name + ' is the ' + selectedPlayer.roleName
});
GameHelper.sendPlayerMessage({
destination: originPlayer,
text: originPlayer.name + ' is the ' + originPlayer.roleName
});
}).then(resolve, reject);
}
});
registerRole('Bodyguard', {
phase: GameHelper.Phases.Night,
description: 'Protect one player per night. If they\'re attacked, you kill their nearest attacker.',
note: 'If two players from a group are equally close to the bodyguard so neither is the "nearest", simply pick one to kill.',
numUses: -1,
attributes: {
'Protectee': GameHelper.RoleAttributes.SelectPlayer(1)
},
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.setRoleAttr(originPlayer, 'Protectee', selectedPlayer);
GameHelper.wait({
numDays: 1,
timeOfDay: GameHelper.TurnPhase.Evening
}).then(function() {
GameHelper.clearRoleAttr(originPlayer, 'Protectee');
});
}).then(resolve, reject);
}
});
registerRole('Bomb', {
phase: GameHelper.Phases.Day,
description: 'If you die, you\'ll blow up both players next to you.',
numUses: 0,
autoIndicate: GameHelper.AutoIndicate.UponDeath
});
registerRole('Boomerang', {
phase: GameHelper.Phases.Night,
description: 'Throw the Boomerang status card. It will be passed daily; that day those who pass it steal the vote from those who receive it. If it returns to you, any player that held the Boomerang loses their vote that day (ideally making your vote very powerful).',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.assignStatusCard(selectedPlayer, 'Boomerang');
}).then(resolve, reject);
}
});
registerRole('Bureaucrat', {
description: 'Wielding the metaphorical weapon of red tape, you may tie up any role (day or night) and remove one of its uses. To go at night, announce your name and role directly after the Narrator annouces the role you wish to tie up; do not open your eyes!',
note: 'Illuminati or Detectives may not be targeted, as they are not roles. Infinite roles lose their use for one day or night.',
numUses: 2
});
registerRole('Bus Driver', {
phase: GameHelper.Phases.Night,
description: 'Switch the role cards of two players.',
note: 'Have the two players switch their role cards at the end of the night, when everybody has their cards on them.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayers(2).then(function(players) {
GameHelper.wait({
numDays: 1,
timeOfDay: GameHelper.TurnPhase.Morning
}).then(function() {
var p0Role = GameHelper.getPlayerRole(players[0]);
var p1Role = GameHelper.getPlayerRole(players[1]);
GameHelper.assignPlayerRole(players[0], p0RoleName);
GameHelper.assignPlayerRole(players[1], p1RoleName);
GameHelper.sendPlayerMessage({
destination: players[0],
text: 'Your role has been swapped.'
});
GameHelper.sendPlayerMessage({
destination: players[1],
text: 'Your role has been swapped.'
});
GameHelper.showAlert('The Bus Driver has now swapped the roles of ' + players[0].name + ' and ' + players[1].name + '.');
});
}).then(resolve, reject);
}
});
registerRole('Butler', {
phase: GameHelper.Phases.Night,
description: 'Indicate to pick a master. Then, each night, you may either pick a new master, or kill or protect your current master.',
numUses: -1,
autoIndicate: GameHelper.AutoIndicate.AfterFirstUse,
attributes: {
'Master': GameHelper.RoleAttributes.SelectPlayer(1)
},
indicate: function(originPlayer, resolve, reject) {
var master = GameHelper.getRoleAttr(originPlayer, 'Master');
$q(function(innerResolve, innerReject) {
if (!master) {
innerResolve();
return;
}
GameHelper.prompt({
text: 'Select a new master?',
yesno: true
}).then(function(selectNewMaster) {
if (selectNewMaster) {
innerResolve();
} else {
innerReject();
}
}, reject);
}).then(function() {
GameHelper.selectPlayer('Select a master.').then(function(selectedPlayer) {
GameHelper.setRoleAttr(originPlayer, 'Master', selectedPlayer);
}).then(resolve, reject);
}, function() {
GameHelper.prompt({
text: 'Is the butler killing or protecting the current master?',
options: ['Killing', 'Protecting']
}).then(function(i, choice) {
if (i == 0) {
GameHelper.markPlayerTargeted(master, originPlayer);
} else if (i == 1) {
GameHelper.markPlayerProtected(master, originPlayer);
}
}).then(resolve, reject);
});
}
});
registerRole('Copycat', {
phase: GameHelper.Phases.Night,
description: 'Select any player. Without revealing your identity, the Narrator will retrieve their role and read it aloud to the town. Whatever their role was, it is now yours as well. Indicate this card as needed to use your new role (for example, if you copy the Doctor indicate each night to protect players).',
note: 'The Copycat cannot copy roles that require status cards - check the copied role before reading it aloud. If it requires a status card, the Copycat may select another player to copy the following night.',
numUses: 1,
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
var roleName = selectedPlayer.roleName;
var hasStatusCards = StatusCards.getByRelatedRole(roleName).length > 0;
if (hasStatusCards) {
GameHelper.showAlert('The selected role requires status cards. The Copycat can go again another night.').then(reject);
return;
}
GameHelper.assignPlayerRole(originPlayer, roleName);
}).then(resolve, reject);
}
});
registerRole('Coward', {
phase: GameHelper.Phases.Day,
description: 'Once marked to die, you may reveal your role to the Narrator (and nobody else!) and switch teams rather than die.',
numUses: 1,
indicate: function(originPlayer, resolve, reject) {
GameHelper.showAlert(originPlayer.name + ' is the Coward. They are now switching sides.').then(function() {
var currentAlignment = originPlayer.alignment;
var newAlignment;
if (currentAlignment == Alignments.Illuminati.name) {
newAlignment = Alignments.Detective;
} else if (currentAlignment == Alignments.Detective.name) {
newAlignment = Alignments.Illuminati.name;
} else if (currentAlignment == Alignments.Citizen.name) {
newAlignment = Alignments.Conspirator.name;
} else if (currentAlignment == Alignments.Conspirator.name) {
newAlignment = Alignments.Citizen.name;
}
GameHelper.setPlayerAlignment(originPlayer, newAlignment);
}).then(resolve);
}
});
registerRole('Cupid', {
phase: GameHelper.Phases.Night,
description: 'Indicate the first night. Select two lovers. If one dies, the other dies with them.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayers(2).then(function(players) {
GameHelper.assignStatusCard(players[0], 'Lover');
GameHelper.assignStatusCard(players[1], 'Lover');
}).then(resolve, reject);
}
});
registerRole('Dark Lord', {
phase: GameHelper.Phases.Night,
description: 'You may indicate three times: each time, use dark magic to either kill somebody or change a player into your Death Shield. You cannot die if a Death Shield is alive. You are on your own team now, and to win you (or your Death Shields) must be the only living player(s) at the end of the game.',
note: 'The Dark Lord will sleep while you wake up the targeted player to inform them they\'re a Death Shield.',
attributes: {
'Death Shields': GameHelper.RoleAttributes.SelectPlayer(3)
},
numUses: 3,
indicate: function(originPlayer, resolve, reject) {
GameHelper.prompt({
text: 'Is the Dark Lord killing or creating a Death Shield?',
options: ['Kill', 'Death Shield']
}).then(function(i, choice) {
return $q(function(resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
resolve(selectedPlayer, i, choice);
}, reject);
});
}).then(function(targetPlayer, actionIndex, action) {
if (actionIndex == 0) {
GameHelper.markPlayerTargeted(targetPlayer, originPlayer);
} else if (actionIndex == 1) {
var currentShields = GameHelper.getRoleAttr(originPlayer, 'Death Shields');
currentShields.push(targetPlayer);
GameHelper.setRoleAttr(originPlayer, 'Death Shields', currentShields);
}
}).then(resolve, reject);
}
});
registerRole('Dynamite', {
phase: GameHelper.Phases.Night,
description: 'First you indicate to pass the Dynamite! card to someone, and it continues to pass from player to player. Second, you indicate again to blow up the Dynamite!, taking a random player with it.',
note: 'If the narrator has the Dynamite! Card when it explodes, the player who indicated it dies.',
numUses: 2,
indicate: function(originPlayer, resolve, reject) {
var markedPlayers = GameHelper.getPlayersWithStatusCard('Dynamite!');
if (markedPlayers.length == 0) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.assignStatusCard(selectedPlayer, 'Dynamite!');
}).then(resolve, reject);
} else {
GameHelper.markPlayerTargeted(markedPlayers[0], originPlayer);
resolve();
}
}
});
registerRole('Dinosaur', {
phase: GameHelper.Phases.Night,
description: 'You are a dinosaur egg. Each time you indicate, you will evolve through the following stages: 1. You may investigate two players, learning if they are an Illuminati or a Detective. 2. You may protect one player from nighttime attacks for that night. 3. You may select one player to kill.',
note: 'Note how many times the Dinosaur has indicated to track their evolving abilities. For the two investigated players, indicate whether or not they are an Illuminati or a Detective, but do not specify which.',
numUses: 3,
attributes: {
'Stage': GameHelper.RoleAttributes.Options(['One', 'Two', 'Three'])
},
indicate: function(originPlayer, resolve, reject) {
var currentStage = GameHelper.getRoleAttr(originPlayer, 'Stage');
if (currentStage == null) {
GameHelper.setRoleAttr(originPlayer, 'Stage', 'One');
GameHelper.showAlert('Allow the Dinosaur to investigate two players.').then(resolve);
} else if (currentStage == 'One') {
GameHelper.selectPlayer('Select a player to protect.').then(function(selectedPlayer) {
GameHelper.markPlayerProtected(selectedPlayer, originPlayer);
GameHelper.setRoleAttr(originPlayer, 'Stage', 'Two');
}).then(resolve, reject);
} else if (currentStage == 'Two') {
GameHelper.selectPlayer('Select a player to protect.').then(function(selectedPlayer) {
GameHelper.markPlayerTargeted(selectedPlayer, originPlayer);
GameHelper.setRoleAttr(originPlayer, 'Stage', 'Three');
}).then(resolve, reject)
} else {
reject();
}
}
});
registerRole('Doctor', {
phase: GameHelper.Phases.Night,
description: 'Protect one player per night.',
note: 'Be sure to specify that only doctors that indicated should wake up, since there may be more than one per game.',
numUses: -1,
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.markPlayerProtected(selectedPlayer, originPlayer);
}).then(resolve, reject);
}
});
registerRole('Doppelganger', {
phase: GameHelper.Phases.Night,
description: 'Any player who indicates on the same night you do will find their doppleganger, and can use their role without consuming any uses.',
note: 'This effectively adds another [icon x] to every role that indicated tonight, and does not affect [icon infinity] roles. Players should track this themselves.',
priority: 99,
indicate: function(originPlayer, resolve, reject) {
GameHelper.showAlert('While processing every subsequent role, uncheck the "consume role use" box before continuing.').then(resolve);
}
});
registerRole('Double Agent', {
description: 'If you are a citizen, pretend you are a member of the Illuminati. If you are a conspirator, pretend you are a Detective. Use your position to secretly aid your faction. Indicating does nothing.',
note: 'If the Double Agent indicates, DO NOT say anything. This is for your benefit only, so you can know an extra Illuminati or Detective is in play.',
numUses: -1,
autoIndicate: GameHelper.AutoIndicate.FirstNight
});
registerRole('Dr. Frankenstein', {
description: 'You may create Frankenstein\'s monster. Once created, the monster will protect you from nighttime attacks for two nights, but on the third night will kill either the Detective or Illuminati nearest to you (whichever is on your team). Your monster will then die.',
note: 'Count the nights! Also, if two Detectives or Illuminati (depending on the team) are equally close to Dr. Frankenstein so neither is the nearest, simply pick one to kill.',
indicate: function(originPlayer, resolve, reject) {
// Protect player for night one...
GameHelper.markPlayerProtected(originPlayer, originPlayer);
// Then wait a night...
GameHelper.wait({
numDays: 1,
timeOfDay: GameHelper.TurnPhase.Night
}).then(function() {
// Then protect player for night two...
return GameHelper.markPlayerProtected(originPlayer, originPlayer);
}).then(function() {
// Then wait another night...
return GameHelper.wait({
numDays: 1,
timeOfDay: GameHelper.TurnPhase.Night
});
}).then(function() {
// Then target the nearest Illuminati or Detective to kill.
var targetAlignment;
if (originPlayer.alignment == Alignments.Illuminati.name || originPlayer.alignment == Alignments.Detective.name) {
targetAlignment = Alignments.Illuminati.name;
} else {
targetAlignment = Alignments.Detective.name;
}
GameHelper.selectPlayer('Select the nearest ' + targetAlignment + ' from ' + originPlayer.name + ' for Dr. Frankenstein\'s monster to kill.').then(function(selectedPlayer) {
GameHelper.markPlayerTargeted(selectedPlayer, originPlayer);
});
});
resolve();
}
});
registerRole('Dragon', {
phase: GameHelper.Phases.Night,
description: 'You attack the town; they unite against you. Each night, you plunder one person, and a knight (elected during the day) hunts you. If the knight finds you, he kills you. If you plunder all the players without the knight finding you, you become the Great Dragon (and gain awesome bonuses).',
note: 'Do not deal back any cards when this effect occurs. Once gameplay resumes, continue with the night this effect originally interrupted.',
attributes: {
'Plundered Player': GameHelper.RoleAttributes.SelectPlayer(1),
'All Plundered Players': GameHelper.RoleAttributes.SelectPlayer(-1)
},
numUses: -1,
indicate: function(originPlayer, resolve, reject) {
if (GameHelper.playerHasStatusCard(originPlayer, 'Great Dragon')) {
GameHelper.showAlert('The Dragon is already the Great Dragon and cannot indicate again.').then(reject);
return;
}
$q(function(innerResolve, innerReject) {
var plunderedPlayers = GameHelper.getRoleAttr(originPlayer, 'All Plundered Players');
if (plunderedPlayers.length == 0) {
GameHelper.showAlert('Cancel any other roles that have indicated tonight. Until the Dragon succeeds or is hunted down, normal gameplay is paused. Tonight the Dragon plunders its first player. The next morning, elect a player to be your Knight. Each night, the Knight will hunt the Dragon, and the Dragon will plunder a player. If the Knight selects the Dragon, the Dragon is killed. If the Knight selects the same player that the Dragon selected to plunder, the Dragon is killed.').then(innerResolve);
} else {
innerResolve();
}
}).then(function() {
return GameHelper.selectPlayer();
}).then(function(selectedPlayer) {
if (GameHelper.playerHasStatusCard(selectedPlayer, 'Knight')) {
GameHelper.markPlayerTargeted(originPlayer, originPlayer);
return GameHelper.showAlert('The Dragon has plundered the Knight, revealing its location. The Knight has slain the Dragon. Assign the Knight the Dragon Slayer status card.');
}
var plunderedPlayers = GameHelper.getRoleAttr(originPlayer, 'All Plundered Players');
plunderedPlayers.push(selectedPlayer);
GameHelper.setRoleAttr(originPlayer, 'Plundered Player', selectedPlayer);
GameHelper.setRoleAttr(originPlayer, 'All Plundered Players', plunderedPlayers);
}).then(resolve, reject);
}
});
registerRole('Electoral College', {
phase: GameHelper.Phases.Day,
description: 'Select two players. Together, the three of you may select a fourth player, who you will either: a) appoint as mayor, or b) nominate as a dictator. If nominating and a majority of the remaining players votes in favor of this, that player becomes dictator; if the vote fails, no new dictator or mayor is appointed.',
note: 'The dictator can be overthrown by unanimous vote (excluding the dictator and the three who suggested him) or if another role names a new mayor or dictator.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.showAlert('Follow the directions in the description. Continue when a consensus is reached.').then(function() {
return GameHelper.prompt({
text: 'What action was taken?',
options: ['Mayor appointed', 'Dictator appointed', 'No action']
});
}).then(function(i, choice) {
if (i == 2) {
return; // No action taken.
}
return GameHelper.selectPlayer('Select the appointed player.').then(function(selectedPlayer) {
GameHelper.revokeStatusCardFromAll('Mayor');
GameHelper.revokeStatusCardFromAll('Dictator');
if (i == 0) {
GameHelper.assignStatusCard(selectedPlayer, 'Mayor');
} else if (i == 1) {
GameHelper.assignStatusCard(selectedPlayer, 'Dictator');
}
});
}).then(resolve, reject);
}
});
registerRole('Emperor', {
phase: GameHelper.Phases.Day,
description: 'Indicate before voting has ended. Rather than vote a player out, the town must select (by voting) the two players they most wanted to vote out. You will then decide which will live and which will die.'
});
registerRole('Enchanter', {
phase: GameHelper.Phases.Night,
description: 'Indicate to receive the Amulet of Protection status card, which will protect you from death for one night before being passed to other players. Indicate again in order to curse someone, meaning that they cannot be protected from nighttime attacks for the rest of the game.',
numUses: 2,
indicate: function(originPlayer, resolve, reject) {
if (GameHelper.roleUsesLeft(originPlayer) == 2) {
GameHelper.revokeStatusCardFromAll('Amulet of Protection');
GameHelper.assignStatusCard(originPlayer, 'Amulet of Protection');
GameHelper.markPlayerProtected(originPlayer, originPlayer);
resolve();
} else {
GameHelper.selectPlayer('Select a player to permanently curse.').then(function(selectedPlayer) {
var cursePlayer = function() {
GameHelper.markPlayerCursed(selectedPlayer, originPlayer);
GameHelper.wait({
numDays: 1,
timeOfDay: GameHelper.TurnPhase.Night
}).then(cursePlayer);
};
cursePlayer(); // Kick off a never-ending loop of cursings.
}).then(resolve, reject);
}
}
});
registerRole('Escape Artist', {
phase: GameHelper.Phases.Night,
description: 'Pick someone as your replacement. When you die, you fool the town into killing your replacement instead of you.',
note: 'You\'ll want to make a note of who the Artist\'s replacement will be.',
attributes: {
'Replacement': GameHelper.RoleAttributes.SelectPlayer(1)
},
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
return GameHelper.setRoleAttr(originPlayer, 'Replacement', selectedPlayer);
}).then(resolve, reject);
}
});
registerRole('General', {
phase: GameHelper.Phases.Night,
description: 'Incite a coup; choose a new mayor (other than yourself).',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.revokeStatusCardFromAll('Mayor');
GameHelper.revokeStatusCardFromAll('Dictator');
GameHelper.assignStatusCard(selectedPlayer, 'Mayor');
}).then(resolve, reject);
}
});
registerRole('Genie\'s Master', {
phase: GameHelper.Phases.Night,
description: 'Lost in the Arabian Desert, you find a genie lamp! Your first wish takes you home; the third will free the genie. For your second wish, during one night you may a) investigate three players to learn if they are Illuminati or Detectives, b) protect two players from nighttime attacks, or c) kill one player.',
note: 'For the three investigated players, indicate whether or not they are an Illuminati or a Detective, but do not specify which.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.prompt({
text: 'What is the player\'s wish?',
options: ['Investigate three players', 'Protect two players', 'Kill one player']
}).then(function(i, choice) {
if (i == 0) { // Investigate
return GameHelper.showAlert('Allow the player to investigate three players. Reveal only if they are Illuminati or Detective, not which they are.');
} else if (i == 1) { // Protect
return GameHelper.selectPlayers(2).then(function(players) {
players.forEach(function(p) {
GameHelper.markPlayerProtected(p, originPlayer);
});
});
} else if (i == 2) { // Kill
return GameHelper.selectPlayer().then(function(selectedPlayer) {
GameHelper.markPlayerTargeted(selectedPlayer, originPlayer);
});
}
}).then(resolve, reject);
}
});
registerRole('Ghost', {
phase: GameHelper.Phases.Day,
description: 'After your untimely death, you observe the town for one night. The next day you appear and reveal the role (not the alignment) of one player of your choosing.',
numUses: 0,
autoIndicate: GameHelper.AutoIndicate.UponDeath
});
registerRole('Grave Robber', {
phase: GameHelper.Phases.Night,
description: 'Rifle through graves; take any dead player\'s role as your own.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectDeadPlayer().then(function(selectedPlayer) {
GameHelper.assignPlayerRole(originPlayer, selectedPlayer.roleName);
}).then(resolve, reject);
}
});
registerRole('Guardian Angel', {
phase: GameHelper.Phases.Night,
description: 'Pick one player (other than yourself); protect them from nighttime attacks for the rest of the game.',
indicate: function(originPlayer, resolve, reject) {
GameHelper.selectPlayer().then(function(selectedPlayer) {
var protectPlayer = function() {
GameHelper.markPlayerProtected(selectedPlayer, originPlayer);
GameHelper.wait({
numDays: 1,
timeOfDay: GameHelper.TurnPhase.Night
}).then(protectPlayer);
};
protectPlayer(); // Kick off a never-ending loop of protection.
});
}
});
registerRole('Gunslinger', {
phase: GameHelper.Phases.Day,
description: 'Challenge someone to a duel. The town will cast votes for either you or them: whoever gets fewer votes dies.',
indicate: function(originPlayer, resolve, reject) {