-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathmechanics.js
1670 lines (1619 loc) · 59.1 KB
/
mechanics.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
//MECHANICS
//These are GAME MECHANIC functions, performing acts that are like physical movements, these take inputs that make sense in terms of the game e.g. card object, server object, integer value, player reference.
//These functions make no validity/legality checks or payments, and they Log the result. Usually nothing is returned but that's not a requirement.
/**
* Make a run.<br/>Assumes all checks have been made (e.g. valid server).<br/>Logs the declaration.
*
* @method MakeRun
* @param {Server} server e.g. corp.HQ or corp.remoteServers[0]
*/
function MakeRun(server) {
//Declare attacked server (Nisei 2021 1.1)
attackedServer = server;
Log("Run initiated attacking " + server.serverName);
GainCredits(runner, corp.badPublicity, "bad publicity"); //(Nisei 2021 1.2)
AutomaticTriggers("automaticOnRunBegins", [server]); //(Nisei 2021 1.3) but only automatics at the moment
approachIce = attackedServer.ice.length - 1;
if (attackedServer.ice.length > 0) {
//(Nisei 2021 1.4.1 sends to 2.1)
ChangePhase(phases.runApproachIce);
}
else ChangePhase(phases.runDecideContinue); //(Nisei 2021 1.4.2 sends to 4; 4.1 doesn't apply so go directly to 4.2)
Render(); //to update server glow
}
/**
* Bypass the currently encountered ice.<br/>This is logged.
*
* @method Bypass
*/
function Bypass() {
Log(GetTitle(attackedServer.ice[approachIce], true)+" bypassed");
//clear AI run cache (the bypassed ice needs to be ignored)
if (runner.AI != null) {
runner.AI.cachedBestPath = null; //force a recalculation
}
phases.runEncounterEnd.next = phases.runPassesIce; //this needs to be said because it's not constant what happens when the encounter ends (e.g. the run may end)
ChangePhase(phases.runEncounterEnd);
}
/**
* Advance an installed card.<br/>Makes no checks or payments.<br/>Logs the result.
*
* @method Advance
* @param {Card} card the card to advance
*/
function Advance(card) {
if (typeof card.advancement === "undefined") card.advancement = 0;
card.advancement++;
Log("Card advanced");
AutomaticTriggers("automaticOnAdvance", [card]);
}
/**
* Place advancement tokens on a card.<br/>No checks are performed.</br>Logs the result.
*
* @method PlaceAdvancement
* @param {Card} card to place advancement tokens on
* @param {int} num number of advancement tokens to place
*/
function PlaceAdvancement(card, num) {
if (num < 1) return;
if (typeof card.advancement === "undefined") card.advancement = num;
else card.advancement += num;
if (num == 1) Log("1 advancement token placed on " + GetTitle(card, true));
else Log(num + " advancement tokens placed on " + GetTitle(card, true));
UpdateCounters();
}
/**
* Rez an installed card.<br/>Makes no checks or payments.<br/>Logs the result.
*
* @method Rez
* @param {Card} card the card to rez
* @param {Boolean} [ignoreAllCosts] if set to true, no costs will be paid (except those already paid)
* @param {function} [onRezResolve] fires if the rez is not cancelled
* @param {Object} [context] for onRezResolve
* @param {Boolean} [allowCancel] whether to allow cancel rez when choosing additional costs
*/
function Rez(card, ignoreAllCosts=false, onRezResolve=null, context=null, allowCancel=true) {
//costs need to be paid? pay them then recurse with ignoreAllCosts=true
if (!ignoreAllCosts) {
//forfeit agenda first if relevant
var payCreditsAndRez = function() {
SpendCredits(
corp,
RezCost(card),
"rezzing",
card,
function () {
//true here means ignore all costs (we have already paid them)
Rez(card, true, onRezResolve, context);
},
this
);
};
var cancelCallback = function () {
ChangePhase(oldPhase, true);
activePlayer=oldActivePlayer;
Cancel();
};
if (!allowCancel) cancelCallback = undefined;
if (card.additionalRezCostForfeitAgenda) {
var oldPhase = currentPhase; //in case of cancel
var oldActivePlayer = activePlayer; //also for cancel
var forfdec = DecisionPhase(
corp,
ChoicesArrayCards(corp.scoreArea),
function(fparams) {
Forfeit(fparams.card);
payCreditsAndRez();
},
"Rezzing "+card.title,
"Rezzing "+card.title,
this,
"forfeit",
cancelCallback
);
}
else payCreditsAndRez();
//it's a recursive function so no need to continue here
return;
}
//all costs paid? rez it.
card.rezzed = true;
card.renderer.FaceUp(); //in case Render is not forthcoming
Log("Corp rezzed " + GetTitle(card, true));
var finishRez = function(cardsTrashed) {
//call the resolve callback
if (onRezResolve) onRezResolve.call(context);
//first the automatic triggers
AutomaticTriggers("automaticOnRez", [card]);
//then the Enumerate ones
//currently giving whoever's turn it is priority...not sure this is always going to be right
TriggeredResponsePhase(playerTurn, "responseOnRez", [card], function() {
//run recalculation has to be done AFTER all the rezzing effects in case they change ice/program states
if (runner.AI != null) {
runner.AI.LoseInfoAboutHQCards(card);
//if it was ice that was rezzed, recalculate run (even if ice is in another server e.g. could affect Ravana 1.0)
/*
var cardServer = GetServer(card);
if (GetApproachEncounterIce() == card || (attackedServer == cardServer && cardServer.ice.indexOf(card) > -1 && cardServer.ice.indexOf(card) < approachIce) ) {
*/
if (CheckCardType(card,["ice"])) {
//recalculate run if needed
runner.AI.RecalculateRunIfNeeded();
}
}
}, "Rez");
};
//if unique, old one is immediately and unpreventably trashed (except if facedown, and facedown cards don't count for check)
var uniqueToTrash = null;
if (typeof card.unique !== "undefined") {
if (card.unique == true) {
var installedCards = InstalledCards(card.player);
for (var i = 0; i < installedCards.length; i++) {
if (installedCards[i] != card && installedCards[i].rezzed) {
if (GetTitle(installedCards[i]) == GetTitle(card)) {
uniqueToTrash = installedCards[i];
break;
}
}
}
}
}
if (uniqueToTrash) {
Log(
GetTitle(card) +
" is unique, the older copy will be unpreventably trashed."
);
Trash(uniqueToTrash, false, finishRez, this);
}
else finishRez([]);
}
/**
* Remove a card from the game<br/>Makes no checks or payments.<br/>Logs the result.
*
* @method RemoveFromGame
* @param {Card} card the card to remove from game
*/
function RemoveFromGame(card) {
card.host = null;
MoveCard(card, removedFromGame);
Log(GetTitle(card, true) + " removed from the game");
}
/**
* Forfeit a card<br/>Makes no checks or payments.<br/>Logs the result.
*
* @method Forfeit
* @param {Card} card the card to forfeit
*/
function Forfeit(card) {
card.host = null;
MoveCard(card, removedFromGame);
Log(GetTitle(card, true) + " forfeited");
}
/**
* Trash a card or cards.<br/>Makes no checks or payments.<br/>Logs the result.
*
* @method Trash
* @param {Card or Card[]} cards the card or cards to trash
* @param {Boolean} canBePrevented true if can be prevented, false if not (e.g. is a cost)
* @param {function(cardsTrashed)} [afterTrashing] called after trashing is complete with parameter cardsTrashed
* @param {Object} [context] for afterTrashing
* @param {Object} [fromDamage] to fire damage callbacks
*/
function Trash(cards, canBePrevented, afterTrashing, context, fromDamage) {
//if a single card is passed, make it an array with one element so that the function can run as normal
if (!Array.isArray(cards)) {
cards = [cards];
}
if (cards.length < 1) {
Log("No cards trashed");
return;
}
if (canBePrevented) {
intended.trash = cards;
//currently giving whoever's turn it is priority...not sure this is always going to be right
OpportunityForAvoidPrevent(playerTurn, "responsePreventableTrash", [], function () {
Trash(intended.trash, false, afterTrashing, context);
}, "About to Trash");
return;
}
//prevention opportunity has passed, from here all trashes are unpreventable
//add any hosted cards to also be trashed (this acts recursively as necessary)
for (var i=0; i<cards.length; i++) {
if (typeof cards[i].hostedCards != 'undefined') {
for (var j=0; j<cards[i].hostedCards.length; j++) {
if (!cards.includes(cards[i].hostedCards[j])) {
cards.push(cards[i].hostedCards[j]);
}
}
}
}
//"would trash" triggers:
//first the automatic triggers
AutomaticTriggers("automaticOnWouldTrash", [cards]);
//then the Enumerate ones
//currently giving whoever's turn it is priority...not sure this is always going to be right
TriggeredResponsePhase(playerTurn, "responseOnWouldTrash", [cards], function() {
//loop through all the cards (move cards, lose information about HQ cards, cancel encounters if relevant, set host to null, and log message)
//BUT fire the on trashed trigger just once, when all this is one (i.e. for all cards at once)
for (var i=0; i<cards.length; i++) {
var card = cards[i];
card.host = null;
if (runner.AI != null && card.cardLocation == corp.HQ.cards)
runner.AI.LoseInfoAboutHQCards(card);
//if the currently encountered ice is trashed, it's no longer being encountered
if (GetApproachEncounterIce() == card) {
encountering = false;
subroutine = -1;
}
//now move it
MoveCard(card, PlayerTrashPile(card.player));
if (card.player == runner) card.faceUp = true;
Log(GetTitle(card, true) + " trashed");
}
//first the automatic triggers
AutomaticTriggers("automaticOnTrash", [cards]);
if (fromDamage) {
AutomaticTriggers("automaticOnTakeDamage", [fromDamage.damage, fromDamage.damageType]);
}
//then the Enumerate ones
//currently giving whoever's turn it is priority...not sure this is always going to be right
//note this may be a combined phase, if the trashing is from damage
var pseudoPhaseTitle = "Trashed";
var secondCallbackName="";
var secondEnumerateParams=[];
if (fromDamage) {
pseudoPhaseTitle = "Trashed/Damage";
secondCallbackName="responseOnTakeDamage";
secondEnumerateParams=[fromDamage.damage, fromDamage.damageType];
}
TriggeredResponsePhase(playerTurn, "responseOnTrash", [cards], function() {
if (typeof afterTrashing === "function") {
afterTrashing.call(context, cards);
}
}, pseudoPhaseTitle, null, secondCallbackName, secondEnumerateParams);
}, "Would Trash");
}
/**
* Trash the card being accessed.<br/>Makes no checks or payments.<br/>Logs the result.
*
* @method TrashAccessedCard
* @param {Boolean} canBePrevented true if can be prevented, false if not (e.g. is a cost)
*/
function TrashAccessedCard(canBePrevented) {
if (PlayerCanLook(corp, accessingCard)) accessingCard.faceUp = true;
SetHistoryThumbnail(accessingCard.imageFile, "Trash");
Trash(accessingCard, canBePrevented, function (cardsTrashed) {
ResolveAccess();
});
}
/**
* Install a card.<br/>Makes no checks and spends no clicks, but provides opportunity to trash cards (if relevant), and spends the relevant credit cost.<br/>Logs the result.
*
* @method Install
* @param {Card} installingCard card to install
* @param {Server|Card} [destination] for corp this is the server, for runner this is the host card (default = null)
* @param {Boolean} [ignoreAllCosts] if set to true, no costs will be paid (except those already paid)
* @param {int} [position] insert ice at the given position (null will install outermost)
* @param {Boolean} [returnToPhase] if set to true, phase after install will be what currentPhase was before install (rather than currentPhase.next)
* @param {function} [onInstallResolve] fires if the install is not cancelled
* @param {Object} [context] for onInstallResolve (and onCancelResolve, if relevant)
* @param {function} [onCancelResolve] fires if the install is cancelled
* @param {function} [onPaymentComplete] fires once the credits (if any) are paid
*/
function Install(
installingCard,
destination = null,
ignoreAllCosts = false,
position = null,
returnToPhase = true,
onInstallResolve,
context,
onCancelResolve,
onPaymentComplete,
allowCancel=true
) {
var oldLocation = installingCard.cardLocation; //in case of cancel
var oldPhase = currentPhase; //in case of cancel
MoveCard(installingCard, installingCard.player.installingCards); //installing cards are kept here instead of resolvingCards, so they sit where you put them while they resolve
//update destination so card doesn't ʒoop back after drop
installingCard.renderer.destinationPosition.x = installingCard.renderer.sprite.x;
installingCard.renderer.destinationPosition.y = installingCard.renderer.sprite.y;
var host = null;
//initialise second part of install as a callback in case a server creation is required first
var installCommonHandler = function() {
var installDestination = InstallDestination(installingCard, destination);
var installTrashPhase = {
Enumerate: {
trash: function () {
if (installDestination == runner.rig.resources) return [];
if (installDestination == runner.rig.hardware) return [];
if (installingCard.cardType == "program") {
/*
//for usability we will skip trashing if there is enough MU left
if (
installingCard.cardType == "program" &&
typeof (installingCard.memoryCost !== "undefined")
) {
if (
installingCard.memoryCost + InstalledMemoryCost(destination) <=
MemoryUnits(destination)
)
return [];
}
//but we'll leave this line here just in case, or for later
*/
return ChoicesInstalledCards(runner, function (card) {
return CheckCardType(card, ["program"]);
}); //The Runner can choose to trash any number of his installed programs at the beginning of an install program action. [Core rulebook]
}
return ChoicesArrayCards(installDestination, CheckTrash);
},
n: function () {
if (installingCard.player == corp) {
if (
!CheckCredits(
corp,
InstallCost(
installingCard,
destination,
ignoreAllCosts,
position
),
"installing",
installingCard
)
)
return []; //can't afford to n yet
if (
installingCard.cardType == "agenda" ||
installingCard.cardType == "asset"
) {
var cardlist = InstallDestination(installingCard, destination);
for (var i = 0; i < cardlist.length; i++) {
if (
cardlist[i].cardType == "agenda" ||
cardlist[i].cardType == "asset"
)
return []; //only one asset/agenda allowed
}
} else if (CheckSubType(installingCard, "Region")) {
//upgrade subtype
var cardlist = InstallDestination(installingCard, destination);
for (var i = 0; i < cardlist.length; i++) {
if (CheckSubType(cardlist[i], "Region")) return []; //limit 1 one region per server (even facedown!)
}
}
return [{}];
} else if (installingCard.player == runner) {
//only check to do here is make sure enough available MU (sufficient programs trashed)
if (
installingCard.cardType == "program" &&
typeof (installingCard.memoryCost !== "undefined")
) {
if (
installingCard.memoryCost + InstalledMemoryCost(destination) >
MemoryUnits(destination)
)
return [];
}
return [{}];
}
return [];
},
},
Resolve: {
//see Nisei CR 1.5 8.5.13 for steps of installing (e.g. trash, pay, trigger "when installed")
trash: function (params) {
var storedServer = GetServerByArray(installDestination); //as above, see below
var serverIndex = corp.remoteServers.indexOf(storedServer); //to make sure servers aren't destroyed here (see below)
Trash(params.card, false, function(cardsTrashed) {
//if this move destroyed a remote server, it shouldn't have (see CR1.5 8.5.9)
if (installingCard.player == corp && serverIndex > -1) {
if (GetServerByArray(installDestination) == null)
corp.remoteServers.splice(serverIndex, 0, storedServer);
}
currentPhase.Cancel = undefined; //once trashing begins there is no going back
delete currentPhase.Cancel; //remove the variable completely
Render();
}, context);
},
n: function () {
//card will be installed, callback fires
if (typeof onInstallResolve === "function")
onInstallResolve.call(context);
//if clicks were spent, it was done before trash (or as part of callback) so no need to SpendClicks here
SpendCredits(
installingCard.player,
InstallCost(installingCard, destination, ignoreAllCosts, position),
"installing",
installingCard,
function () {
//payment done, callback fires
if (typeof onPaymentComplete === "function")
onPaymentComplete.call(context);
//move the card, write to the logs, etc
if (installingCard.player == corp) {
//corp cards are installed facedown
if (installingCard.rezzed) {
installingCard.knownToRunner = true;
installingCard.rezzed = false;
}
if (installingCard.faceUp) {
installingCard.knownToRunner = true;
installingCard.faceUp = false;
}
}
MoveCard(installingCard, installDestination, position); //if position not specified, this uses .push (i.e. ice will be installed outermost)
if (runner.AI != null && oldLocation == corp.HQ.cards)
runner.AI.LoseInfoAboutHQCards(null, installingCard.cardType); //one less card in corp hand
if (host != null) installingCard.host = host;
if (typeof installingCard.recurringCredits !== "undefined")
installingCard.credits = installingCard.recurringCredits;
var outStr = GetTitle(installingCard, true);
if (CheckCardType(installingCard, ["agenda", "asset", "upgrade"]))
outStr =
"a card in root of " + CardServerName(installingCard, true);
else if (CheckCardType(installingCard, ["ice"]))
outStr = "ice protecting " + CardServerName(installingCard, true);
Log(PlayerName(installingCard.player) + " installed " + outStr);
//if unique or a console, old one is immediately and unpreventably trashed (except if facedown, and facedown cards don't count for check)
var cardToReplace = null;
if (
typeof installingCard.unique !== "undefined" &&
installingCard.faceUp
) {
var replaceReason = "";
if (CheckSubType(installingCard, "Console")) {
//console subtype
var cardlist = runner.rig.hardware; //assuming for now this is the only place you'll find a console (and chances are a hosted console is probably not installed)
for (var i = 0; i < cardlist.length; i++) {
if (installingCard != cardlist[i] && CheckSubType(cardlist[i], "Console")) {
cardToReplace = cardlist[i];
replaceReason = "a console";
}
}
}
if (installingCard.unique == true) {
var installedCards = InstalledCards(installingCard.player);
for (var i = 0; i < installedCards.length; i++) {
if (
installedCards[i] != installingCard &&
installedCards[i].faceUp
) {
if (
GetTitle(installedCards[i]) == GetTitle(installingCard)
) {
cardToReplace = installedCards[i];
replaceReason = "unique";
}
}
}
}
}
var finishInstall = function(cardsTrashed) {
//install done, card becomes active
//first the automatic triggers
AutomaticTriggers("automaticOnInstall", [installingCard]);
//then the Enumerate ones
//currently giving whoever's turn it is priority...not sure this is always going to be right
TriggeredResponsePhase(playerTurn, "responseOnInstall", [installingCard], function() {
IncrementPhase(returnToPhase);
}, "Installed");
};
if (cardToReplace) {
Log(
GetTitle(installingCard) +
" is "+replaceReason+", the older card will be unpreventably trashed."
);
Trash(cardToReplace, false, finishInstall, this);
}
else finishInstall([]);
},
this
);
},
},
};
if (allowCancel) {
installTrashPhase.Cancel = {
trash: function () {
ChangePhase(oldPhase, true);
MoveCard(installingCard, oldLocation);
if (typeof onCancelResolve === "function")
onCancelResolve.call(context);
Cancel();
Render();
},
};
}
if (returnToPhase) installTrashPhase.next = currentPhase;
else installTrashPhase.next = currentPhase.next;
installTrashPhase.player = installingCard.player;
installTrashPhase.title = "Trash Before Install";
if (installingCard.player == corp)
installTrashPhase.identifier = "Corp Install";
else if (installingCard.player == runner)
installTrashPhase.identifier = "Runner Install";
ChangePhase(installTrashPhase);
executingCommand = "trash";
};
if (installingCard.player == corp) {
if (destination == null) {
destination = NewServer("Remote " + corp.serverIncrementer++, false);
corp.remoteServers.push(destination);
Log("Corp created a new remote server");
//currently giving whoever's turn it is priority...not sure this is always going to be right
TriggeredResponsePhase(playerTurn, "responseOnCreateServer", [destination], installCommonHandler, "Server Created");
}
else installCommonHandler();
} else {
installingCard.faceUp = true;
host = destination;
installCommonHandler();
}
}
/**
* Play a card from hand as an action.<br/>Makes no checks or payments and does not move card to heap/archives (card code should do this after resolving).<br/>Logs the result.
*
* @method Play
* @param {Card} card the card to play
* @param {function(finishResolve)} [onPlayResolve] fires if the play is not cancelled (i.e. right BEFORE the card begins to resolve) and must call finishResolve when done
* @param {Object} [context] for onPlayResolve
* @returns {Phase} the phase object created and changed to
*/
function Play(card, onPlayResolve, context) {
var oldLocation = card.cardLocation; //in case of cancel
var oldPhase = currentPhase; //in case of cancel
MoveCard(card, card.player.resolvingCards);
card.faceUp = true; //this is no secret...
var choices = [{}]; //assume valid by default
if (typeof card.Enumerate === "function") {
choices = card.Enumerate.call(card);
}
var instruction = GetTitle(card, true);
if (typeof card.text !== "undefined") instruction = card.text;
//note these will be called in the context of the card
var cancelCallback = function () {
ChangePhase(oldPhase, true);
MoveCard(card, oldLocation);
Cancel();
Render();
};
var resolveCallback = function (params) {
//handle X cost
//this is done by temporarily defining the printed play cost
var playCostReplaced = false;
if (card.playCost === 'X') {
if (typeof params.playCost == 'undefined') {
LogError(GetTitle(card)+" has X play cost so it must return .playCost for each Enumerate choice");
} else {
card.playCost = params.playCost;
playCostReplaced = true;
}
}
var finishResolve = function() {
//finish handling X
if (playCostReplaced) {
card.playCost = 'X';
}
if (runner.AI != null) runner.AI.LoseInfoAboutHQCards(card);
Log('Played "' + GetTitle(card, true) + '"');
AutomaticTriggers("automaticOnPlay", [card]);
card.Resolve.call(card, params);
};
//card will be played, callback fires
if (typeof onPlayResolve === "function") onPlayResolve.call(context, finishResolve);
else finishResolve();
};
var command = "continue";
if (typeof card.command !== "undefined") command = card.command;
return DecisionPhase(
card.player,
choices,
resolveCallback,
"Playing " + GetTitle(card, true),
instruction,
card,
command,
cancelCallback
);
}
/**
* Break a subroutine.<br/>Makes no checks or payments.<br/>Logs the result.
*
* @method Break
* @param {Subroutine} subroutine the subroutine to break
*/
function Break(subroutine) {
subroutine.broken = true;
//particle effect
var card = GetApproachEncounterIce();
if (card && typeof subroutine.visual !== "undefined") {
particleSystems.breaksubroutine.spawnRect.x =
209 - subroutine.visual.y + 0.5 * subroutine.visual.h; //-209 is half card height
cardRenderer.ParticleEffect(
card.renderer.particleContainer,
particleSystems.breaksubroutine
);
}
Log('Subroutine "' + subroutine.text + '" broken');
}
/**
* Trigger a card's ability or subroutine.<br/>Makes no checks or payments.<br/>Logs the result.
*
* @method Trigger
* @param {Card} card card which the ability is on
* @param {Triggerable} triggerable object which has .text and .Resolve(params) or .Resolve()
* @param {Params} params parameters to use (omit if triggerable.Resolve doesn't require it)
*/
function Trigger(card, triggerable, params, customVerb='Using') {
if (typeof triggerable.text !== "undefined")
Log(customVerb+' "' + triggerable.text + '" on ' + GetTitle(card) + ":");
else Log(customVerb+" " + GetTitle(card) + ":");
triggerable.Resolve.call(card, params); //call in context of the card
}
/**
* Trigger a card's ability, making decisions where necessary.<br/>Logs the result.
*
* @method TriggerAbility
* @param {Card} card card which the ability is on
* @param {Ability} ability object which has .text and either .Enumerate() with .Resolve(params) or just .Resolve()
* @param {function} [onTriggerResolve] fires if the play is not cancelled
* @param {Object} [context] for onTriggerResolve
* @returns {Phase} the phase object created and changed to
*/
function TriggerAbility(card, ability, onTriggerResolve, context) {
var oldPhase = currentPhase; //in case of cancel
var choices = [{}]; //assume valid by default
if (typeof ability.Enumerate === "function") {
choices = ability.Enumerate.call(card); //call in context of the card
}
var instruction = GetTitle(card, true);
if (typeof ability.text !== "undefined") instruction = ability.text;
//note these will be called in the context of the card
var cancelCallback = function () {
ChangePhase(oldPhase, true);
Cancel();
//Render();
};
var resolveCallback = function (params) {
//ability will be triggered, callback fires
if (typeof onTriggerResolve === "function") onTriggerResolve.call(context);
//Log("Triggered \""+ability.text+"\"");
Log("Using " + GetTitle(card, true) + ":");
ability.Resolve.call(card, params); //call in context of the card
};
var player = card.player;
if (ability.opponentOnly) {
if (player == runner) player = corp;
else if (player == corp) player = runner;
}
return DecisionPhase(
player,
choices,
resolveCallback,
instruction,
instruction,
card,
"continue",
cancelCallback
);
}
/**
* Discard a card.<br/>Makes no checks or payments.<br/>Logs the result.
*
* @method Discard
* @param {Card} card card in hand to discard
* @returns {Card} the card discarded
*/
function Discard(card) {
if (card.player == runner) card.faceUp = true;
Log(PlayerName(card.player)+' discarded "' + GetTitle(card, true) + '"');
MoveCard(card, PlayerTrashPile(card.player));
return card;
}
/**
* Runner takes damage (runner randomly trashes cards from grip).<br/>Stops on flatline.<br/>Logs the result.
*
* @method Damage
* @param {String} damageType type of net damage to take
* @param {int} num number of net damage to take
* @param {Boolean} canBePrevented true if can be prevented, false if not (e.g. is a cost)
* @param {function(cardsTrashed)} [afterTrashing] called after trashing is complete (even if no cards are trashed), cards will be an array
* @param {Object} [context] for afterTrashing
*/
function Damage(damageType, num, canBePrevented, afterTrashing, context) {
intended.damageType = damageType;
intended.damage = num;
var applyDamage = function() {
//opportunity for prevention has passed, intended.damage will now be done
Log("Runner takes " + intended.damage + " " + intended.damageType + " damage");
if (intended.damageType == "core") {
runner.coreDamage++;
}
//if damage is greater than grip length, flatline
if (intended.damage > runner.grip.length) {
//move all the cards from grip to heap for the visual effect
while (runner.grip.length > 0) {
MoveCard(runner.grip[0], runner.heap);
}
Render();
PlayerWin(corp, "Runner flatlined");
return;
}
//else select intended.damage random cards from grip
var copyOfGrip = runner.grip.concat([]);
Shuffle(copyOfGrip);
var cardsToTrash = copyOfGrip.slice(0, intended.damage);
//trash all cards simultaneously (note this trashing cannot be prevented since that opportunity has passed)
//notice we combine takedamage triggers with trashed by sending damage info
Trash(cardsToTrash, false, afterTrashing, context, { damage:intended.damage, damageType:intended.damageType });
}
if (canBePrevented) {
OpportunityForAvoidPrevent(runner, "responsePreventableDamage", [], function () {
applyDamage();
}, "About to " + intended.damageType.charAt(0).toUpperCase() + intended.damageType.slice(1) + " Damage");
} else {
applyDamage();
}
}
/**
* Purges all virus counters from all cards.</br>Makes no checks or payments.<br/>Logs the result.
*
* @method Purge
*/
function Purge() {
var numPurged = 0;
ApplyToAllCards(function (card) {
if (typeof (card.virus !== "undefined")) {
numPurged += card.virus;
card.virus = 0;
}
});
Log("Virus counters purged");
TriggeredResponsePhase(playerTurn, "responseOnPurge", [numPurged], function () {}, "Purged");
}
/**
* A player draws cards.<br/>Stops on corp draw from empty R&D.<br/>Logs the result.
*
* @method Draw
* @param {Player} player either corp or runner
* @param {int} num number of cards to attempt to draw
* @param {function()} [afterDraw] called after drawing is complete (even if no cards are drawn)
* @param {Object} [context] for afterDraw
* @returns {int} the number of cards drawn
*/
function Draw(player, num=1, afterDraw, context) {
num += ModifyingTriggers("modifyDraw", player, -num); //lower limit of -num means the total will not be any lower than zero
if (num < 1) return 0;
var cards = [];
//draw for corp (lose if impossible)
if (player == corp) {
var maxDraw = corp.RnD.cards.length;
if (maxDraw < num) {
PlayerWin(runner, "Corp attempted to draw a card from empty R&D");
return 0;
} else {
for (var i = 0; i < num; i++) {
cards.push(corp.RnD.cards[corp.RnD.cards.length - 1]);
MoveCardByIndex(
corp.RnD.cards.length - 1,
corp.RnD.cards,
corp.HQ.cards
);
}
}
}
//draw for runner (break if impossible)
else if (player == runner) {
var maxDraw = runner.stack.length;
if (maxDraw < num) num = maxDraw;
for (var i = 0; i < num; i++) {
cards.push(runner.stack[runner.stack.length - 1]);
MoveCardByIndex(runner.stack.length - 1, runner.stack, runner.grip);
}
}
//write to log
if (num == 1) Log(PlayerName(player) + " drew a card");
else if (num > 1) Log(PlayerName(player) + " drew " + num + " cards");
if (maxDraw == num) {
if (player == corp) Log("R&D is empty");
else if (player == runner) Log("Stack is empty");
else LogError("No player specified for Draw");
}
//currently giving whoever's turn it is priority...not sure this is always going to be right
TriggeredResponsePhase(playerTurn, "responseOnCardsDrawn", [cards], function() {
if (typeof afterDraw == 'function') afterDraw.call(context);
}, "Cards Drawn");
return num;
}
/**
* A player spends clicks.<br/>No checks are performed.</br>Logs the result.
*
* @method SpendClicks
* @param {Player} player either corp or runner
* @param {int} num number of clicks to spend
*/
function SpendClicks(player, num) {
if (num < 1) return;
player.clickTracker -= num;
if (num == 1) Log(PlayerName(player) + " spent one click");
else Log(PlayerName(player) + " spent " + num + " clicks");
}
/**
* A player gain clicks.<br/>No checks are performed.</br>Logs the result.
*
* @method GainClicks
* @param {Player} player either corp or runner
* @param {int} num number of clicks to gain
*/
function GainClicks(player, num) {
if (num < 1) return;
player.clickTracker += num;
if (num == 1) Log(PlayerName(player) + " gained one click");
else Log(PlayerName(player) + " gained " + num + " clicks");
}
/**
* A player spends credits.<br/>No checks are performed.</br>Logs the result.
*
* @method SpendCredits
* @param {Player} player either corp or runner
* @param {int} num number of credits to spend
* @param {String} [doing] for 'recurring credit' checks
* @param {Card} [card] for 'recurring credit' checks
* @param {function} [afterSpend] called after spending complete
* @param {Object} [context] for afterSpend
*/
function SpendCredits(
player,
num,
doing = "",
card = null,
afterSpend,
context
) {
//new version of this function just automatically uses extra credits sources when available
//first, temporary credits (e.g. from bad publicity)
if (player == runner) {
var spendCred_temporary = Math.min(num, runner.temporaryCredits);
if (spendCred_temporary > 0) {
runner.temporaryCredits -= spendCred_temporary;
num -= spendCred_temporary;
if (spendCred_temporary == 1)
Log(PlayerName(player) + " spent one temporary credit");
//TODO specific messages per type?
else
Log(
PlayerName(player) +
" spent " +
spendCred_temporary +
" temporary credits"
);
}
}
//second, card-hosted credits
if (num > 0) {
var oldNum = num;
var activeCards = ActiveCards(player);
for (var i = 0; i < activeCards.length; i++) {
if (typeof activeCards[i].credits !== "undefined") {
if (typeof activeCards[i].canUseCredits === "function") {
if (activeCards[i].canUseCredits(doing, card)) {
var spendCred_card = Math.min(num, activeCards[i].credits);
activeCards[i].credits -= spendCred_card;
num -= spendCred_card;
if (spendCred_card == 1)
Log(
PlayerName(player) +
" spent one credit from " +
GetTitle(activeCards[i], true)
);
else if (spendCred_card > 0)
Log(
PlayerName(player) +
" spent " +
spendCred_card +
" credits from " +
GetTitle(activeCards[i], true)
);
}
}
}
}
if (num != oldNum) UpdateCounters();
}
//lastly, credit pool
if (num > 0) {
player.creditPool -= num; //spend the rest from default pool
if (num == 1) Log(PlayerName(player) + " spent one credit");
else Log(PlayerName(player) + " spent " + num + " credits");
}
//done, do whatever needs to be done after
if (typeof afterSpend === "function") afterSpend.call(context);
//old version of this function below allows player to choose which sources to use and when:
//allow player to use as many credits as desired from recurring sources (continue spends the rest using credit pool)
/*
var spendCreditsPhase = {
Enumerate: {
use: function() {
var ret = [];
//for each available recurring credit source, list from 1 to max(available,required)
var activeCards = ActiveCards(player);
for (var i=0; i<activeCards.length; i++)
{
if (typeof(activeCards[i].credits) !== 'undefined')
{
if (typeof(activeCards[i].canUseCredits) === 'function')
{
if (activeCards[i].canUseCredits(doing,card))
{
for (var j=1; (j<=activeCards[i].credits)&&(j<=num); j++)
{
ret.push({card:activeCards[i],num:j,label:"Use "+j+" credits from "+GetTitle(activeCards[i],true)});
}
}
}
}
}
return ret;
},
n: function() {
if (num>Credits(player)) return []; //need to spend more recurring credits
return [{}];
}
},
Resolve: {
use: function(params) {
params.card.credits-=params.num;
num-=params.num;
if (params.num == 1) Log(PlayerName(player)+" used one credit from "+GetTitle(params.card,true));
else Log(PlayerName(player)+" used "+params.num+" credits from "+GetTitle(params.card,true));
},
n: function() {
IncrementPhase(true); //return to original phase before callback in case the callback needs to change phase