Sylver 9e5f83df37 Large initial push
7 factions already implemented:

- Norther Realms
- Nilfgaard Empire
- Scoia'Tael
- Monsters
- Skellige
- Redenia
- Toussaint

In progress:
- Velen

To do:
- Witchers
- Wild Hunt
2023-11-02 02:02:29 +01:00

5775 lines
223 KiB
JavaScript

"use strict"
class Controller { }
var nilfgaard_wins_draws = false;
// Makes decisions for the AI opponent player
class ControllerAI {
constructor(player) {
this.player = player;
}
// Computes the weights of the given cards
getWeights(cards) {
let data_max = this.getMaximums();
let data_board = this.getBoardData();
return cards.map(c =>
({
weight: this.weightCard(c, data_max, data_board),
card: c
}));
}
// Return the card among the provided list having the highest weight
getHighestWeightCard(cards) {
let w = this.getWeights(cards).sort((a, b) => (b.weight - a.weight));
if (w.length > 0) {
return w[0].card;
}
return null;
}
// Return the card among the provided list having the lowest weight
getLowestWeightCard(cards) {
let w = this.getWeights(cards).sort((a, b) => (a.weight - b.weight));
if (w.length > 0) {
return w[0].card;
}
return null;
}
// Collects data and weighs options before taking a weighted random action
async startTurn(player) {
let data_max = this.getMaximums();
let data_board = this.getBoardData();
// In case of a forced action, play it if positive weight, pass otherwise
if (player.forcedActions.length > 0) {
let card = player.forcedActions.splice(0, 1)[0];
let w = this.weightCard(card, data_max, data_board);
if (w > 0) {
await this.playCard(card, data_max, data_board)
} else {
await player.passRound();
}
return;
}
if (player.opponent().passed && (player.winning || player.deck.faction === "nilfgaard" && player.total === player.opponent().total)) {
nilfgaard_wins_draws = player.deck.faction === "nilfgaard" && player.total === player.opponent().total;
await player.passRound();
return;
}
let weights = player.hand.cards.map(c =>
({
weight: this.weightCard(c, data_max, data_board),
action: async () => await this.playCard(c, data_max, data_board),
card: c
}));
// If the opponent has passed and is not too far ahead, let's try to take the win
let diff = player.opponent().total - player.total;
if (player.opponent().passed && diff < 16) {
// Looking for a one shot that isn't overkill
let oneshot = weights.filter(w => (w.card.basePower > diff && w.card.basePower < diff + 5) || (w.weight > diff && w.weight > diff + 5));
if (oneshot.length > 0) {
let oneshot_card = oneshot.sort((a, b) => (a.weight - b.weight))[0];
await oneshot_card.action();
return;
}
// Can we catch up in 2 plays?
let playable = weights.filter(w => w.weight > 0).sort((a, b) => (b.weight - a.weight));
if (playable.length > 2)
playable = playable.slice(0, 2);
let weightTotal = playable.reduce((a, c) => a + c.weight, 0);
if (weightTotal > diff) {
await playable[0].action();
return;
}
}
if (player.leaderAvailable)
weights.push({
name: "Leader Ability",
weight: this.weightLeader(player.leader, data_max, data_board),
action: async () => {
await ui.notification("op-leader", 1200);
await player.activateLeader();
}
});
if (player.factionAbilityUses > 0) {
let factionAbility = factions[player.deck.faction];
weights.push({
name: "Faction ability",
weight: factionAbility.weight(player),
action: async () => {
await player.useFactionAbility();
}
});
}
weights.push({
name: "Pass",
weight: this.weightPass(),
action: async () => await player.passRound()
});
let weightTotal = weights.reduce((a, c) => a + c.weight, 0);
if (weightTotal === 0) {
for (let i = 0; i < player.hand.cards.length; ++i) {
let card = player.hand.cards[i];
if (card.row === "weather" && this.weightWeather(card) > -1 || card.abilities.includes("avenger")) {
await weights[i].action();
return;
}
}
await player.passRound();
} else {
for (var i = 0; i < weights.length; ++i) {
if (weights[i].card)
console.log("[" + weights[i].card.name + "] Weight: " + weights[i].weight);
else
console.log("[" + weights[i].name + "] Weight: " + weights[i].weight);
}
let rand = randomInt(weightTotal);
console.log("Chosen weight: " + rand);
for (var i = 0; i < weights.length; ++i) {
rand -= weights[i].weight;
if (rand < 0)
break;
}
console.log(weights[i]);
await weights[i].action();
}
}
isSelfRowIndex(i) {
return (this.player === player_me && i > 2) || (this.player === player_op && i < 3);
}
getSelfRowIndexes() {
if (this.player === player_me)
return [3, 4, 5];
return [0, 1, 2];
}
// Collects data about card with the hightest power on the board
getMaximums() {
let rmax = board.row.map(r => ({
row: r,
cards: r.cards.filter(c => c.isUnit() && !c.isImmortal()).reduce((a, c) =>
(!a.length || a[0].power < c.power) ? [c] : a[0].power === c.power ? a.concat([c]) : a, [])
}));
let max = rmax.filter((r, i) => r.cards.length && this.isSelfRowIndex(i)).reduce((a, r) => Math.max(a, r.cards[0].power), 0);
let max_me = rmax.filter((r, i) => this.isSelfRowIndex(i) && r.cards.length && r.cards[0].power === max).reduce((a, r) =>
a.concat(r.cards.map(c => ({
row: r,
card: c
}))), []);
max = rmax.filter((r, i) => r.cards.length && !this.isSelfRowIndex(i)).reduce((a, r) => Math.max(a, r.cards[0].power), 0);
let max_op = rmax.filter((r, i) => !this.isSelfRowIndex(i) && r.cards.length && r.cards[0].power === max).reduce((a, r) =>
a.concat(r.cards.map(c => ({
row: r,
card: c
}))), []);
// Also compute for rows without a shield
let rmax_noshield = rmax.filter((r, i) => !r.row.isShielded());
let max_noshield = rmax_noshield.filter((r, i) => r.cards.length && this.isSelfRowIndex(i)).reduce((a, r) => Math.max(a, r.cards[0].power), 0);
let max_me_noshield = rmax_noshield.filter((r, i) => this.isSelfRowIndex(i) && r.cards.length && r.cards[0].power === max_noshield).reduce((a, r) =>
a.concat(r.cards.map(c => ({
row: r,
card: c
}))), []);
max_noshield = rmax_noshield.filter((r, i) => r.cards.length && !this.isSelfRowIndex(i)).reduce((a, r) => Math.max(a, r.cards[0].power), 0);
let max_op_noshield = rmax_noshield.filter((r, i) => !this.isSelfRowIndex(i) && r.cards.length && r.cards[0].power === max_noshield).reduce((a, r) =>
a.concat(r.cards.map(c => ({
row: r,
card: c
}))), []);
return {
rmax: rmax,
me: max_me,
op: max_op,
rmax_noshield: rmax_noshield,
me_noshield: max_me_noshield,
op_noshield: max_op_noshield
};
}
// Collects data about the types of cards on the board and in each player's graves
getBoardData() {
let data = this.countCards(new CardContainer());
this.player.getAllRows().forEach(r => this.countCards(r, data));
data.grave_me = this.countCards(this.player.grave);
data.grave_op = this.countCards(this.player.opponent().grave);
return data;
}
// Catalogs the kinds of cards in a given CardContainer
countCards(container, data) {
data = data ? data : {
spy: [],
medic: [],
bond: {},
scorch: []
};
container.cards.filter(c => c.isUnit()).forEach(c => {
for (let x of c.abilities) {
if (!c.isLocked()) {
switch (x) {
case "spy":
case "medic":
data[x].push(c);
break;
case "scorch_r":
case "scorch_c":
case "scorch_s":
data["scorch"].push(c);
break;
case "bond":
if (!data.bond[c.target])
data.bond[c.target] = 0;
data.bond[c.target]++;
}
}
}
});
return data;
}
// Swaps a card from the hand with the deck if beneficial
redraw() {
let card = this.discardOrder({
holder: this.player
}).shift();
if (card && card.power < 15) {
this.player.deck.swap(this.player.hand, this.player.hand.removeCard(card));
}
}
// Orders discardable cards from most to least discardable
discardOrder(card, src = null, force = false) {
let cards = [];
let groups = {};
let source = src ? src : card.holder.hand;
let musters = source.cards.filter(c => c.abilities.includes("muster"));
// Grouping Musters together
musters.forEach(curr => {
let name = curr.target;
if (!groups[name])
groups[name] = [];
groups[name].push(curr);
});
// Keeping one in hand for each muster group
for (let group of Object.values(groups)) {
group.sort(Card.compare);
group.pop();
cards.push(...group);
}
// Targets of muster that do not have the ability themselves for which we already have the "summoner"
let tmusters = source.cards.filter(c => Object.keys(groups).includes(c.target) && !c.abilities.includes("muster"));
cards.push(...tmusters);
// Discarding randomly weather cards to keep only 1 in hand
let weathers = source.cards.filter(c => c.row === "weather");
if (weathers.length > 1) {
weathers.splice(randomInt(weathers.length), 1);
cards.push(...weathers);
}
// Discarding cards with no abilities by order of strength, unless they are of 7+
let normal = source.cards.filter(c => c.abilities.length === 0 && c.basePower < 7);
normal.sort(Card.compare);
cards.push(...normal);
// Grouping bonds together
let bonds = source.cards.filter(c => c.abilities.includes("bond"));
groups = {};
bonds.forEach(curr => {
let name = curr.target;
if (!groups[name])
groups[name] = [];
groups[name].push(curr);
});
// Discarding those that are alone and weak (< 6)
for (let group of Object.values(groups)) {
if (group.length === 1 && group[0].basePower < 6) {
cards.push(group[0]);
}
}
// In this mode, we force all cards to be ordered - let's add remaining ones by order of basePower
if (force) {
source.cards.sort((a, b) => a.basePower - b.basePower).forEach(c => {
if (cards.indexOf(c) < 0)
cards.push(c);
});
}
return cards;
}
// Tells the Player that this object controls to play a card
async playCard(c, max, data) {
if (c.key === "spe_horn")
await this.horn(c);
else if (c.key === "spe_mardroeme")
await this.mardroeme(c);
else if (c.abilities.includes("decoy"))
await this.decoy(c, max, data);
else if (c.faction === "special" && c.abilities.includes("scorch"))
await this.scorch(c, max, data);
else if (c.faction === "special" && c.abilities.includes("cintra_slaughter"))
await this.slaughterCintra(c);
else if (c.faction === "special" && c.abilities.includes("seize"))
await this.seizeCards(c);
else if (c.faction === "special" && (c.abilities.includes("shield") || c.abilities.includes("shield_c") || c.abilities.includes("shield_r") || c.abilities.includes("shield_s")))
await this.shieldCards(c);
else if (c.faction === "special" && c.abilities.includes("lock"))
await this.lock(c);
else if (c.faction === "special" && c.abilities.includes("curse"))
await this.curse(c);
else if (c.faction === "special" && c.abilities.includes("knockback"))
await this.knockback(c);
else if (c.faction === "special" && c.abilities.includes("toussaint_wine"))
await this.toussaintWine(c);
else if ((c.isUnit() || c.hero) && c.abilities.includes("witch_hunt"))
await this.witchHunt(c);
else if ((c.isUnit() || c.hero) && c.row.includes("agile") && (c.abilities.includes("morale") || c.abilities.includes("horn") || c.abilities.includes("bond") ))
await this.player.playCardToRow(c, this.bestAgileRowChange(c).row);
else if (c.faction === "special" && c.abilities.includes("bank"))
await this.bank(c);
else if (c.faction === "special" && c.abilities.includes("skellige_fleet"))
await this.skelligeFleet(c);
else if (c.faction === "special" && c.abilities.includes("royal_decree"))
await this.royalDecree(c);
else
await this.player.playCard(c);
}
// Plays a Commander's Horn to the most beneficial row. Assumes at least one viable row.
async horn(card) {
let rows = this.player.getAllRows().filter(r => !r.special.containsCardByKey("spe_horn"));
let max_row;
let max = 0;
for (let i = 0; i < rows.length; ++i) {
let r = rows[i];
let dif = [0, 0];
this.calcRowPower(r, dif, true);
r.effects.horn++;
this.calcRowPower(r, dif, false);
r.effects.horn--;
let score = dif[1] - dif[0];
if (max < score) {
max = score;
max_row = r;
}
}
await this.player.playCardToRow(card, max_row);
}
// Plays a Mardroeme to the most beneficial row. Assumes at least one viable row.
async mardroeme(card) {
let row, max = 0;
this.getSelfRowIndexes().forEach(i => {
let curr = this.weightMardroemeRow(card, board.row[i]);
if (curr > max) {
max = curr;
row = board.row[i];
}
});
await this.player.playCardToRow(card, row);
}
// Selects a card to remove from a Grave. Assumes at least one valid card.
medic(card, grave) {
let data = this.countCards(grave);
let targ;
if (data.spy.length) {
let min = data.spy.reduce((a, c) => Math.min(a, c.power), Number.MAX_VALUE);
targ = data.spy.filter(c => c.power === min)[0];
} else if (data.medic.length) {
let max = data.medic.reduce((a, c) => Math.max(a, c.power), 0);
targ = data.medic.filter(c => c.power === max)[0];
} else if (data.scorch.length) {
targ = data.scorch[randomInt(data.scorch.length)];
} else {
let units = grave.findCards(c => c.isUnit());
targ = units.reduce((a, c) => a.power < c.power ? c : a, units[0]);
}
return targ;
}
// Selects a card to return to the Hand and replaces it with a Decoy. Assumes at least one valid card.
async decoy(card, max, data) {
let targ, row;
if (game.decoyCancelled)
return;
let usable_data;
if (card.row.length > 0) {
// Units with decoy ability only work on a specific row
if (["close", "agile","agile_cr","agile_cs","agile_crs"].includes(card.row))
usable_data = this.countCards(board.getRow(card, "close", this.player), usable_data);
if (["ranged", "agile", "agile_cr", "agile_rs", "agile_crs"].includes(card.row))
usable_data = this.countCards(board.getRow(card, "ranged", this.player), usable_data);
if (["siege", "agile_rs", "agile_cs", "agile_crs"].includes(card.row))
usable_data = this.countCards(board.getRow(card, "siege", this.player), usable_data);
} else {
usable_data = data;
}
if (usable_data.spy.length) {
let min = usable_data.spy.reduce((a, c) => Math.min(a, c.power), Number.MAX_VALUE);
targ = usable_data.spy.filter(c => c.power === min)[0];
} else if (usable_data.medic.length) {
targ = usable_data.medic[randomInt(usable_data.medic.length)];
} else if (usable_data.scorch.length) {
targ = usable_data.scorch[randomInt(usable_data.scorch.length)];
} else {
let pairs = max.rmax.filter((r, i) => this.isSelfRowIndex(i) && r.cards.length)
.filter((r, i) => card.row.length === 0 || (["close", "agile", "agile_cr", "agile_cs", "agile_crs"].includes(card.row) && (i === 2 || i === 3))
|| (["ranged", "agile", "agile_cr", "agile_rs", "agile_crs"].includes(card.row) && (i === 1 || i === 4))
|| (["siege", "agile_rs", "agile_cs", "agile_crs"].includes(card.row) && (i === 0 || i === 5)))
.reduce((a, r) =>
r.cards.map(c => ({
r: r.row,
c: c
})).concat(a), []);
if (pairs.length) {
let pair = pairs[randomInt(pairs.length)];
targ = pair.c;
row = pair.r;
}
}
if (targ) {
for (let i = 0; !row; ++i) {
if (board.row[i].cards.indexOf(targ) !== -1) {
row = board.row[i];
break;
}
}
targ.decoyTarget = true;
setTimeout(() => board.toHand(targ, row), 1000);
} else {
row = ["close", "agile", "agile_cr", "agile_cs", "agile_crs"].includes(card.row) ? board.getRow(card, "close", this.player) : ["ranged", "agile_rs"].includes(card.row) ? board.getRow(card, "ranged", this.player) : board.getRow(card, "siege", this.player);
}
await this.player.playCardToRow(card, row);
}
// Tells the controlled Player to play the Scorch card
async scorch(card, max, data) {
await this.player.playScorch(card);
}
// Tells the controlled Player to play the Scorch card
async slaughterCintra(card) {
await this.player.playSlaughterCintra(card);
}
// Tells the controlled Player to play the seize special card
async seizeCards(card) {
await this.player.playSeize(card);
}
// Plays a shield special card to the most beneficial row. Assumes at least one viable row.
// Also applicable for shield cards which affect only one row.
async shieldCards(card) {
if (card.abilities.includes("shield_c")) {
await this.player.playCardToRow(card, board.getRow(card, "close", this.player));
return;
} else if (card.abilities.includes("shield_r")) {
await this.player.playCardToRow(card, board.getRow(card, "ranged", this.player));
return;
}
if (card.abilities.includes("shield_s")) {
await this.player.playCardToRow(card, board.getRow(card, "siege", this.player));
return;
}
let units = card.holder.getAllRowCards().concat(card.holder.hand.cards).filter(c => c.isUnit()).filter(c => !c.abilities.includes("spy"));
let rowStats = {
"close": 0,
"ranged": 0,
"siege": 0,
"agile": 0,
"agile_cr": 0,
"agile_rs": 0,
"agile_cs": 0,
"agile_crs": 0
};
units.forEach(c => {
rowStats[c.row] += c.power;
});
rowStats["close"] += (rowStats["agile"] + rowStats["agile_cr"] + rowStats["agile_cs"] + rowStats["agile_crs"]);
rowStats["ranged"] += rowStats["agile_rs"]
let max_row;
if (rowStats["close"] >= rowStats["ranged"] && rowStats["close"] >= rowStats["siege"])
max_row = board.getRow(card, "close", this.player);
else if (rowStats["ranged"] > rowStats["close"] && rowStats["ranged"] >= rowStats["siege"])
max_row = board.getRow(card, "ranged", this.player);
else
max_row = board.getRow(card, "siege", this.player);
await this.player.playCardToRow(card, max_row);
}
// Plays the lock special card in the enemy melee row
async lock(card) {
await this.player.playCardToRow(card, board.getRow(card, "close", this.player.opponent()));
}
// Plays the curse special card in the enemy melee row
async curse(card) {
await this.player.playCardToRow(card, board.getRow(card, "close", this.player.opponent()));
}
// Plays the knockback special card in the most beneficial row - by default enemy melee row
async knockback(card) {
await this.player.playKnockback(card);
}
// Play special wine card to the most beneficial row.
async toussaintWine(card) {
await this.player.playCardToRow(card, this.bestRowToussaintWine(card));
}
// Play special wine card to the most beneficial row.
async witchHunt(card) {
await this.player.playCardToRow(card, this.bestWitchHuntRow(card).getOppositeRow());
}
// Plays the bank special card
async bank(card) {
await this.player.playBank(card);
}
// Plays the Skellige Fleet special card
async skelligeFleet(card) {
await this.player.playSkelligeFleet(card);
}
// Plays the Royal Decree special card
async royalDecree(card) {
await this.player.playRoyalDecree(card);
}
bestWitchHuntRow(card) {
if (card.row.includes("agile")) {
let r = board.getAgileRows(card, this.player.opponent());
let rows = r.filter(r => !r.isShielded() && !game.scorchCancelled).map(r => ({
row: r,
value: r.minUnits().reduce((a, c) => a + c.power, 0)
}));
if (rows.length > 0)
return rows.sort((a, b) => b.value - a.value)[0].row;
else
return r[0];
} else {
return board.getRow(card, card.row, card.holder.opponent());
}
}
bestRowToussaintWine(card) {
let units = card.holder.getAllRowCards().concat(card.holder.hand.cards).filter(c => c.isUnit()).filter(c => !c.abilities.includes("spy"));
let rowStats = {
"close": 0,
"ranged": 0,
"siege": 0,
"agile": 0,
"agile_cr": 0,
"agile_rs": 0,
"agile_cs": 0,
"agile_crs": 0
};
units.forEach(c => {
rowStats[c.row] += 1;
});
rowStats["close"] += (rowStats["agile"] + rowStats["agile_cr"] + rowStats["agile_cs"] + rowStats["agile_crs"]);
rowStats["ranged"] += rowStats["agile_rs"]
let rows = card.holder.getAllRows();
rowStats["close"] = board.getRow(card, "close", this.player).effects.toussaint_wine > 0 ? 0 : rowStats["close"];
rowStats["ranged"] = board.getRow(card, "ranged", this.player).effects.toussaint_wine > 0 ? 0 : rowStats["ranged"];
rowStats["siege"] = board.getRow(card, "siege", this.player).effects.toussaint_wine > 0 ? 0 : rowStats["siege"];
let max_row;
if (rowStats["close"] >= rowStats["ranged"] && rowStats["close"] >= rowStats["siege"])
max_row = board.getRow(card, "close", this.player);
else if (rowStats["ranged"] > rowStats["close"] && rowStats["ranged"] >= rowStats["siege"])
max_row = board.getRow(card, "ranged", this.player);
else
max_row = board.getRow(card, "siege", this.player);
return max_row;
}
// Gives the list of cards on the board for which we can find a similar card in the grave or deck (either same name or target)
// Returns for each entry: target card on bard / card in deck or grave / target row / weight
bestSimilarCards(card) {
let units = card.holder.getAllRowCards().filter(c => c.isUnit());
let candidates = new Array();
units.forEach(srcCard => {
let targets = srcCard.holder.grave.cards.concat(srcCard.holder.deck.cards).filter((c => srcCard.name === c.name || ("target" in srcCard && srcCard.target !== "" && srcCard.target === c.target)));
if (targets.length > 0) {
targets.forEach(c => {
let pRows = c.getPlayableRows();
pRows.forEach(r => {
let weight = this.weightRowChange(c, r);
candidates.push([srcCard, c, r, weight]);
});
});
}
});
return candidates.sort((a, b) => b[3] - a[3]);
}
// Assigns a weight for how likely the conroller is to Pass the round
weightPass() {
if (this.player.health === 1)
return 0;
let dif = this.player.opponent().total - this.player.total;
if (dif > 30)
return 100;
if (dif < -30 && this.player.opponent().hand.cards.length - this.player.hand.cards.length > 2)
return 100;
return Math.floor(Math.abs(dif));
}
// Assigns a weight for how likely the controller is to activate its leader ability
weightLeader(card, max, data) {
let w = ability_dict[card.abilities[0]].weight;
if (ability_dict[card.abilities[0]].weight) {
let score = w(card, this, max, data);
return score;
}
return 10 + (game.roundCount - 1) * 15;
}
// Assigns a weight for how likely the controller will use a scorch-row card
weightScorchRow(card, max, row_name) {
if (game.scorchCancelled)
return 0;
let index = 3 + (row_name === "close" ? 0 : row_name === "ranged" ? 1 : 2);
if (this.player === player_me)
index = 2 - (row_name === "close" ? 0 : row_name === "ranged" ? 1 : 2);
if (board.row[index].total < 10 || board.row[index].isShielded())
return 0;
let score = max.rmax[index].cards.reduce((a, c) => a + c.power, 0);
return score;
}
// Calculates a weight for how likely the controller will use horn on this row
weightHornRow(card, row) {
return row.effects.horn ? 0 : this.weightRowChange(card, row);
}
// Calculates weight for playing a card on a given row, min 0
weightRowChange(card, row) {
return Math.max(0, this.weightRowChangeTrue(card, row));
}
bestAgileRowChange(card) {
let rows = [];
let r = board.getAgileRows(card, card.holder);
r.forEach(row => rows.push({ row: row, score: 0 }));
for (var i = 0; i < rows.length ; i++) {
rows[i].score = this.weightRowChange(card, rows[i].row);
}
return rows.sort((a, b) => b.score - a.score)[0];
}
// Calculates weight for playing a card on the given row
weightRowChangeTrue(card, row) {
let dif = [0, 0];
this.calcRowPower(row, dif, true);
row.updateState(card, true);
this.calcRowPower(row, dif, false);
if (!card.isSpecial())
dif[0] -= row.calcCardScore(card);
row.updateState(card, false);
return dif[1] - dif[0];
}
// Weights the diff of changing a card from a row to another
weightRowSwap(card, src, dest) {
if (src === dest)
return 0;
return (this.weightRowChangeTrue(card, dest) - this.weightRowChangeTrue(card, src));
}
// Calculates for all cards on the given player's side of the board the diff of score if changing location to another row
// Returns results sorted by best or worst first, depending if "best" parameter is on or off
weightAllCardsRowChanges(player, best = true) {
let weights = [];
let cards = player.getAllRowCards();
let rows = player.getAllRows();
cards.filter(c => c.hero || c.isUnit()).forEach(c => {
rows.forEach(r => {
if (r !== c.currentLocation) {
weights.push({ row: r, card: c, score: this.weightRowSwap(c, c.currentLocation, r) })
}
});
});
if (best) {
return weights.sort((a, b) => b.score - a.score);
} else {
return weights.sort((a, b) => a.score - b.score);
}
}
// Calculates the weight for playing a weather card
weightWeather(card) {
let rows;
// This specific weather card has 2 modes
if (card.key === "spe_storm") {
return Math.max(this.weightWeather({ key: "storm", abilities: ["frost", "fog"] }), this.weightWeather({ key: "storm", abilities: ["rain", "fog"] }));
}
if (card.abilities) {
if (card.key === "spe_clear")
rows = Object.values(weather.types).filter(t => t.count > 0).flatMap(t => t.rows);
else
rows = Object.values(weather.types).filter(t => t.count === 0 && card.abilities.includes(t.name)).flatMap(t => t.rows);
} else {
if (card.ability == "clear")
rows = Object.values(weather.types).filter(t => t.count > 0).flatMap(t => t.rows);
else
rows = Object.values(weather.types).filter(t => t.count === 0 && t.name === card.ability).flatMap(t => t.rows);
}
if (!rows.length)
return 1;
let dif = [0, 0];
rows.forEach(r => {
let state = r.effects.weather;
this.calcRowPower(r, dif, true);
r.effects.weather = !state;
this.calcRowPower(r, dif, false);
r.effects.weather = state;
});
return dif[1] - dif[0];
}
// Calculates the weight for playing a mardroeme card
weightMardroemeRow(card, row) {
if (card.key === "spe_mardroeme" && row.special.containsCardByKey("spe_mardroeme"))
return 0;
let ermion = card.holder.hand.cards.filter(c => c.key === "sk_ermion").length > 0;
if (ermion && card.key !== "sk_ermion" && row === board.getRow(card, "ranged", this.player))
return 0;
let bers_cards = row.cards.filter(c => c.abilities.includes("berserker"));
let weightData = {
bond: {},
strength: 0,
scorch: 0
};
for (var i = 0; i < bers_cards.length; i++) {
var c = bers_cards[i];
var ctarget = card_dict[c.target];
weightData.strength -= c.power;
if (ctarget.ability.includes("morale"))
weightData.strength += Number(ctarget["strength"]) + row.cards.filter(c => c.isUnit()).length - 1;
if (ctarget.ability.includes("bond")) {
if (!weightData.bond[c.target])
weightData.bond[c.target] = [0, Number(ctarget["strength"])];
weightData.bond[c.target][0]++;
}
if (ctarget.ability.includes("scorch_c"))
weightData.scorch += this.weightScorchRow(card, this.getMaximums(), "close");
}
let weight = weightData.strength + Object.keys(weightData.bond).reduce((s, c) => s + Math.pow(weightData.bond[c][0], 2) * weightData.bond[c][1], 0) + weightData.scorch;
return Math.max(1, weight);
}
// Calculates the weight for cards with the medic ability
weightMedic(data, score, owner) {
let units = owner.grave.findCards(c => c.isUnit());
let grave = data["grave_" + owner.opponent().tag];
return !units.length ? Math.min(1, score) : score + (grave.spy.length ? 50 : grave.medic.length ? 15 : grave.scorch.length ? 10 : this.player.health === 1 ? 1 : 0);
}
// Calculates the weight for cards with the berserker ability
weightBerserker(card, row, score) {
if (card.holder.hand.cards.filter(c => c.abilities.includes("mardroeme")).length < 1 && !row.effects.mardroeme > 0)
return score;
score -= card.basePower;
let ctarget = card_dict[card.target];
if (ctarget.ability.includes("morale")) {
score += Number(ctarget["strength"]) + row.cards.filter(c => c.isUnit()).length - 1;
} else if (ctarget.ability.includes("bond")) {
let n = 1;
if (!row.effects.mardroeme)
n += row.cards.filter(c => c.key === card.key).filter(c => !c.isLocked()).length;
else
n += row.cards.filter(c => c.key === card.target).filter(c => !c.isLocked()).length;
score += Number(ctarget["strength"]) * (n * n);
} else if (ctarget.ability.includes("scorch_c")) {
score += this.weightScorchRow(card, this.getMaximums(), "close");
} else {
score += Number(ctarget["strength"]);
}
return Math.max(1, score);
}
// Calculates the weight for a weather card if played from the deck
weightWeatherFromDeck(card, weather_id) {
if (card.holder.deck.findCard(c => c.abilities.includes(weather_id)) === undefined)
return 0;
return this.weightCard({
abilities: [weather_id],
row: "weather"
});
}
// Assigns a weights for how likely the controller with play a card from its hand
weightCard(card, max, data) {
let abi
if (card.abilities) {
abi = card.abilities;
} else if (card["ability"]) {
abi = card["ability"].split(" ");
} else {
abi = [];
console.log("Missing abilities for card:");
console.log(card);
}
if (abi.includes("decoy")) {
if (card.row.length > 0) {
let row_data;
if (["close","agile","agile_cr","agile_cs","agile_crs"].includes(card.row))
row_data = this.countCards(board.getRow(card, "close", this.player), row_data);
if (["ranged", "agile", "agile_cr", "agile_rs", "agile_crs"].includes(card.row))
row_data = this.countCards(board.getRow(card, "ranged", this.player), row_data);
if (["agile_cs", "agile_rs", "agile_crs"].includes(card.row))
row_data = this.countCards(board.getRow(card, "siege", this.player), row_data);
return game.decoyCancelled ? 0 : row_data.spy.length ? 50 : row_data.medic.length ? 15 : row_data.scorch.length ? 10 : max.me.length ? card.power : 0;
} else
return game.decoyCancelled ? 0 : data.spy.length ? 50 : data.medic.length ? 15 : data.scorch.length ? 10 : max.me.length ? 1 : 0;
}
if (abi.includes("horn")) {
let rows = this.player.getAllRows().filter(r => !r.special.containsCardByKey("spe_horn"));
if (!rows.length)
return 0;
rows = rows.map(r => this.weightHornRow(card, r));
return Math.max(...rows) / 2;
}
if (abi) {
if (abi.includes("scorch")) {
if (game.scorchCancelled)
return Math.max(0, card.power);
let power_op = max.op_noshield.length ? max.op_noshield[0].card.power : 0;
let power_me = max.me_noshield.length ? max.me_noshield[0].card.power : 0;
let total_op = power_op * max.op_noshield.length;
let total_me = power_me * max.me_noshield.length;
return power_me > power_op ? 0 : power_me < power_op ? total_op : Math.max(0, total_op - total_me);
}
if (abi.includes("decoy")) {
return game.decoyCancelled ? 0 : data.spy.length ? 50 : data.medic.length ? 15 : data.scorch.length ? 10 : max.me.length ? 1 : 0;
}
if (abi.includes("mardroeme")) {
let rows = this.player.getAllRows();
return Math.max(...rows.map(r => this.weightMardroemeRow(card, r)));
}
if (["cintra_slaughter", "seize", "lock", "shield", "knockback", "shield_c", "shield_r", "shield_s", "bank", "skellige_fleet","immortal","royal_decree","summon_one_of","curse"].includes(abi.at(-1))) {
return ability_dict[abi.at(-1)].weight(card);
}
if (abi.includes("witch_hunt")) {
if (game.scorchCancelled)
return card.power;
let best_row = this.bestWitchHuntRow(card)
if (best_row) {
let dmg = best_row.minUnits().reduce((a, c) => a + c.power, 0);
if (dmg < 6) // Let's not waste it on isolated weak units
dmg = 0;
return dmg + card.power;
}
return card.power
}
if (abi.includes("toussaint_wine")) {
let units = card.holder.getAllRowCards().concat(card.holder.hand.cards).filter(c => c.isUnit()).filter(c => !c.abilities.includes("spy"));
let rowStats = {
"close": 0,
"ranged": 0,
"siege": 0,
"agile": 0,
"agile_cr": 0,
"agile_rs": 0,
"agile_cs": 0,
"agile_crs": 0
};
units.forEach(c => {
rowStats[c.row] += 1;
});
rowStats["close"] += (rowStats["agile"] + rowStats["agile_cr"] + rowStats["agile_cs"] + rowStats["agile_crs"]);
rowStats["ranged"] += rowStats["agile_rs"]
let rows = card.holder.getAllRows();
rowStats["close"] = board.getRow(card, "close", this.player).effects.toussaint_wine > 0 ? 0 : rowStats["close"];
rowStats["ranged"] = board.getRow(card, "ranged", this.player).effects.toussaint_wine > 0 ? 0 : rowStats["ranged"];
rowStats["siege"] = board.getRow(card, "siege", this.player).effects.toussaint_wine > 0 ? 0 : rowStats["siege"];
return 2 * Math.max(rowStats["close"], rowStats["ranged"], rowStats["siege"]);
}
if (abi.at(-1) && abi.at(-1).startsWith("witcher_")) {
let witchers = card.holder.getAllRowCards().filter(c => c.abilities.includes(abi.at(-1)));
let keep = witchers.filter(c => c.hero);
return card.power + (2 * witchers.length * 2) + (keep.length > 0 ? keep[0].power : 0);
}
if (abi.includes("inspire")) {
let insp = card.holder.getAllRowCards().filter(c => c.abilities.includes("inspire"));
let best_power = 0;
if (insp.length > 0)
best_power = insp.sort((a, b) => b.power - a.power)[0].power;
let max_power = Math.max(card.power, best_power);
if (card.power === max_power)
return max_power + insp.map(c => max_power - c.power).reduce((a, c) => a + c, 0);
return max_power;
}
}
if (card.row === "weather" || (card.deck && card.deck.startsWith("weather"))) {
return Math.max(0, this.weightWeather(card));
}
let row = board.getRow(card, ["agile", "agile_cr", "agile_cs", "agile_crs"].includes(card.row) ? "close" : ["ranged","agile_rs"].includes(card.row) ? "ranged" : card.row, this.player);
let score = row.calcCardScore(card);
switch (abi[abi.length - 1]) {
case "bond":
case "morale":
case "toussaint_wine":
case "horn":
score = card.row.includes("agile") ? this.bestAgileRowChange(card).score : this.weightRowChange(card, row);
break;
case "medic":
score = this.weightMedic(data, score, card.holder);
break;
case "spy":
score = 15 + score;
break;
case "muster":
let pred = c => c.target === card.target;
let units = card.holder.hand.cards.filter(pred).concat(card.holder.deck.cards.filter(pred));
score *= units.length;
break;
case "scorch_c":
score = Math.max(1, this.weightScorchRow(card, max, "close"));
break;
case "scorch_r":
score = Math.max(1, this.weightScorchRow(card, max, "ranged"));
break;
case "scorch_s":
score = Math.max(1, this.weightScorchRow(card, max, "siege"));
break;
case "berserker":
score = this.weightBerserker(card, row, score);
break;
case "avenger":
case "avenger_kambi":
case "whorshipper":
case "necrophage":
case "goetia":
case "ambush":
return score + ability_dict[abi.at(-1)].weight(card);
}
return score;
}
// Calculates the current power of a row associated with each Player
calcRowPower(r, dif, add) {
r.findCards(c => c.isUnit()).forEach(c => {
let p = r.calcCardScore(c);
c.holder === this.player ? (dif[0] += add ? p : -p) : (dif[1] += add ? p : -p);
});
}
}
var may_leader = true,
exibindo_lider = false;
// Can make actions during turns like playing cards that it owns
class Player {
constructor(id, name, deck, isAI = true) {
this.id = id;
this.tag = (id === 0) ? "me" : "op";
//this.controller = (id === 0) ? new Controller() : new ControllerAI(this);
if (isAI) {
this.controller = new ControllerAI(this);
} else {
this.controller = new Controller();
this.ai = new ControllerAI(this); // Exposes AI features which can be used by the actual AI to make estimations
}
if (game.mode === 2) {
// AI vs AI
this.hand = (id === 0) ? new Hand(document.getElementById("hand-row"), this.tag) : new Hand(document.getElementById("op-hand-row"), this.tag);
} else if (game.mode === 3) {
// Player vs Player
if (id === 0) {
this.hand = new Hand(document.getElementById("hand-row"), this.tag);
} else {
this.hand = new Hand(document.getElementById("op-hand-row"), this.tag);
document.getElementById("op-hand-row").classList.add("human-op"); // This a playable opponent hand
}
} else {
this.hand = (id === 0) ? new Hand(document.getElementById("hand-row"), this.tag) : new HandAI(this.tag);
}
this.hand.player = this;
this.grave = new Grave(document.getElementById("grave-" + this.tag));
this.deck = new Deck(deck.faction, document.getElementById("deck-" + this.tag));
this.deck_data = deck;
this.leader = new Card(deck.leader.index, deck.leader.card, this);
this.elem_leader = document.getElementById("leader-" + this.tag);
this.elem_leader.children[0].appendChild(this.leader.elem);
this.reset();
this.name = name;
document.getElementById("name-" + this.tag).innerHTML = name;
if (deck.title)
document.getElementById("deck-name-" + this.tag).innerHTML = deck.title;
else
document.getElementById("deck-name-" + this.tag).innerHTML = factions[deck.faction].name;
document.getElementById("stats-" + this.tag).getElementsByClassName("profile-img")[0].children[0].children[0];
let x = document.querySelector("#stats-" + this.tag + " .profile-img > div > div");
x.style.backgroundImage = iconURL("deck_shield_" + deck.faction);
}
getAIController() {
if (this.controller instanceof ControllerAI) {
return this.controller;
} else {
return this.ai;
}
}
// Sets default values
reset() {
this.grave.reset();
this.hand.reset();
this.deck.reset();
this.deck.initializeFromID(this.deck_data.cards, this);
this.health = 2;
this.total = 0;
this.passed = false;
this.handsize = 10;
this.mulliganCount = 2;
this.winning = false;
this.factionAbilityUses = 0;
this.effects = {
"witchers": {},
"whorshippers": 0,
"inspire": 0
};
this.capabilities = {
"drawOPdeck": 0,
"endTurnRetake": 0,
"cardEdit": 0
};
this.forcedActions = [];
this.endturn_action = null;
// Handling Faction abilities: active or passive
let factionAbility = factions[this.deck.faction];
if (factionAbility["activeAbility"]) {
// Init ability if need be
if (factionAbility.factionAbilityInit) {
factionAbility.factionAbilityInit(this);
}
this.updateFactionAbilityUses(factionAbility["abilityUses"]);
document.getElementById("faction-ability-" + this.tag).classList.remove("hide");
if (this.tag === "me" || game.isPvP())
document.getElementById("faction-ability-" + this.tag).addEventListener("click", () => this.activateFactionAbility(), false);
} else {
document.getElementById("faction-ability-" + this.tag).classList.add("hide");
}
this.enableLeader();
this.setPassed(false);
document.getElementById("gem1-" + this.tag).classList.add("gem-on");
document.getElementById("gem2-" + this.tag).classList.add("gem-on");
}
roundStartReset() {
this.effects = {
"witchers": {},
"whorshippers": 0,
"inspire": 0
};
this.forcedActions = [];
}
// Returns the opponent Player
opponent() {
return board.opponent(this);
}
// Updates the player's total score and notifies the gamee
updateTotal(n) {
this.total += n;
document.getElementById("score-total-" + this.tag).children[0].innerHTML = this.total;
board.updateLeader();
}
// Puts the player in the winning state
setWinning(isWinning) {
if (this.winning ^ isWinning)
document.getElementById("score-total-" + this.tag).classList.toggle("score-leader");
this.winning = isWinning;
}
// Puts the player in the passed state
setPassed(hasPassed) {
if (this.passed ^ hasPassed)
document.getElementById("passed-" + this.tag).classList.toggle("passed");
this.passed = hasPassed;
}
// Sets up board for turn
async startTurn() {
document.getElementById("stats-" + this.tag).classList.add("current-turn");
if (this.leaderAvailable)
this.elem_leader.children[1].classList.remove("hide");
if (this === player_me || game.isPvP()) {
document.getElementById("pass-button").classList.remove("noclick");
may_pass1 = true;
}
if (this.controller instanceof ControllerAI) {
await this.controller.startTurn(this);
} else {
// If there is a pending forced action, do it or pass
if (this.forcedActions.length > 0) {
let card = this.forcedActions.splice(0, 1)[0];
ui.showPreviewVisuals(card);
// Ask if the player wants to play the card or pass
let c = await ui.popup("Play card [E]", (p) => p.choice = true, "Pass [Q]", (p) => p.choice = false, "Play card or pass?", "You are forced to play this card or pass for this round, which option do you choose?");
ui.enablePlayer(true);
if (c) {
document.getElementById("click-background").classList.add("noclick");
this.hand.cards.forEach(c => c.elem.classList.add("noclick"));
ui.setSelectable(card, true);
} else {
this.passRound();
}
}
}
}
// Passes the round and ends the turn
async passRound() {
this.setPassed(true);
ui.notification("op-pass", 1200);
await this.endTurn();
}
// Plays a scorch card
async playScorch(card) {
if (!game.scorchCancelled)
await this.playCardAction(card, async () => await ability_dict["scorch"].activated(card));
}
// Plays a Slaughter of Cintra card
async playSlaughterCintra(card) {
await this.playCardAction(card, async () => await ability_dict["cintra_slaughter"].activated(card));
}
// Plays a Seize special card card
async playSeize(card) {
await this.playCardAction(card, async () => await ability_dict["seize"].activated(card));
}
// Plays a Knockback special card card, assuming 1 valid
async playKnockback(card) {
let best_row = board.getRow(card, "close", this.opponent());
//If melee row is empty, better target ranged
if (board.getRow(card, "close", this.opponent()).cards.length === 0)
best_row = board.getRow(card, "ranged", this.opponent());
// If siege has an active weather effect, better target ranged
if (board.getRow(card, "ranged", this.opponent()).cards.length > 1 && board.getRow(card, "siege", this.opponent()).effects.weather)
best_row = board.getRow(card, "ranged", this.opponent());
// If ranged has a horn or shield effect, better target it
if ((board.getRow(card, "ranged", this.opponent()).isShielded() || board.getRow(card, "ranged", this.opponent()).effects.horn > 0) && board.getRow(card, "ranged", this.opponent()).cards.length > 0)
best_row = board.getRow(card, "ranged", this.opponent());
// If there are some bond units in the ranged row, better try to break it before it grows
if (Object.keys(board.getRow(card, "ranged", this.opponent()).effects.bond).length > 0 && board.getRow(card, "siege", this.opponent()).effects.horn === 0)
best_row = board.getRow(card, "ranged", this.opponent());
await this.playCardAction(card, async () => await ability_dict["knockback"].activated(card, best_row));
}
// Play the bank card
async playBank(card) {
await this.playCardAction(card, async () => await ability_dict["bank"].activated(card));
}
// Play the skellige fleet card
async playSkelligeFleet(card) {
await this.playCardAction(card, async () => await ability_dict["skellige_fleet"].activated(card));
}
// Play the royal decree card
async playRoyalDecree(card) {
await this.playCardAction(card, async () => await ability_dict["royal_decree"].activated(card));
}
// Plays a card to a specific row
async playCardToRow(card, row, endTurn=true) {
await this.playCardAction(card, async () => await board.moveTo(card, row, this.hand), endTurn);
}
// Plays a card to the board
async playCard(card) {
await this.playCardAction(card, async () => await card.autoplay(this.hand));
}
// Shows a preview of the card being played, plays it to the board and ends the turn
async playCardAction(card, action, endTurn = true) {
ui.showPreviewVisuals(card);
await sleep(1000);
ui.hidePreview(card);
await action();
if(endTurn)
await this.endTurn();
}
// Handles end of turn visuals and behavior the notifies the game
async endTurn(noEffects=false) {
if (this.endturn_action) {
// Call action instead of ending turn
await this.endturn_action();
return;
}
if (!this.passed && !this.canPlay()) {
this.setPassed(true);
ui.notification("op-pass", 1200);
}
if (this === player_me) {
document.getElementById("pass-button").classList.add("noclick");
may_pass1 = false;
}
document.getElementById("stats-" + this.tag).classList.remove("current-turn");
this.elem_leader.children[1].classList.add("hide");
game.endTurn(noEffects)
}
// Tells the the Player if it won the round. May damage health.
endRound(win) {
if (!win) {
if (this.health < 1)
return;
document.getElementById("gem" + this.health + "-" + this.tag).classList.remove("gem-on");
this.health--;
}
this.setPassed(false);
this.setWinning(false);
}
// Returns true if the Player can make any action other than passing
canPlay() {
return this.hand.cards.length > 0 || this.leaderAvailable || this.factionAbilityUses > 0;
}
// Use a leader's Activate ability, then disable the leader
// Option to not end turn and disable leader - useful when we want to trigger the leader ability in other circumstances
async activateLeader(endTurn=true,disableLeader=true) {
try {
Carousel.curr.cancel();
} catch (err) { }
if (this.leaderAvailable) {
this.endTurnAfterAbilityUse = endTurn;
let res = await this.leader.activated[0](this.leader, this);
// If the leader activity signaled it couldn't be actually used, we stop right here
if (res == false) {
ui.enablePlayer(true);
return false;
}
if (disableLeader)
this.disableLeader();
// Some abilities require further actions before ending the turn, such as selecting a card
if (this.endTurnAfterAbilityUse)
await this.endTurn();
else {
// Make selections for AI player
if (this.controller instanceof ControllerAI) {
if (this.leader.key === "wu_alzur_maker") {
let worse_unit = this.getAllRowCards().filter(c => c.isUnit()).sort((a, b) => a.power - b.power)[0];
ui.selectCard(worse_unit);
} else if (this.leader.key === "to_anna_henrietta_duchess") {
let horns = player_me.getAllRows().filter(r => r.special.findCards(c => c.abilities.includes("horn")).length > 0).sort((a, b) => b.total - a.total);
if (horns[0])
ui.selectRow(horns[0]);
} else if (this.leader.key === "lr_meve_princess" || this.leader.key === "sy_carlo_varese") {
let max = this.controller.getMaximums();
let rows = [this.controller.weightScorchRow(this.leader, max, "close"), this.controller.weightScorchRow(this.leader, max, "ranged"), this.controller.weightScorchRow(this.leader, max, "siege")];
let maxv = 0,
max_row;
let offset = 3;
if (this === player_me) {
offset = 0;
rows = rows.reverse();
}
for (var i = 0; i < 3; i++) {
if (rows[i] > maxv) {
maxv = rows[i];
max_row = board.row[offset + i];
}
}
if (max_row)
ui.selectRow(max_row);
} else if (this.leader.key === "sy_cyrus_hemmelfart") {
// We select a random row to put shackles on
let offset = 3;
if (this === player_me)
offset = 0;
ui.selectRow(board.row[offset + randomInt(2)]);
}
}
}
}
}
// Disable access to leader ability and toggles leader visuals to off state
disableLeader() {
this.leaderAvailable = false;
let elem = this.elem_leader.cloneNode(true);
this.elem_leader.parentNode.replaceChild(elem, this.elem_leader);
this.elem_leader = elem;
this.elem_leader.children[0].classList.add("fade");
this.elem_leader.children[1].classList.add("hide");
this.elem_leader.addEventListener("click", async () => await ui.viewCard(this.leader), false);
}
// Enable access to leader ability and toggles leader visuals to on state
enableLeader() {
this.leaderAvailable = this.leader.activated.length > 0;
let elem = this.elem_leader.cloneNode(true);
this.elem_leader.parentNode.replaceChild(elem, this.elem_leader);
this.elem_leader = elem;
this.elem_leader.children[0].classList.remove("fade");
this.elem_leader.children[1].classList.remove("hide");
if ((this.id === 0 || game.isPvP()) && this.leader.activated.length > 0) {
this.elem_leader.children[0].addEventListener("click",
async () => await ui.viewCard(this.leader, async () => await this.activateLeader()),
false
);
this.elem_leader.children[0].addEventListener("mouseover", function () {
tocar("card", false);
this.style.boxShadow = "0 0 1.5vw #6d5210"
});
this.elem_leader.children[0].addEventListener("mouseout", function () {
this.style.boxShadow = "0 0 0 #6d5210"
});
window.addEventListener("keydown", function (e) {
if (may_leader && may_pass1) {
if (e.keyCode == 88) {
if (exibindo_lider) {
exibindo_lider = false;
player_me.activateLeader();
} else if (player_me.leaderAvailable) {
may_leader = false;
exibindo_lider = true;
player_me.callLeader();
}
}
}
});
window.addEventListener("keyup", function (e) {
if (player_me.leaderAvailable) may_leader = true;
});
} else {
this.elem_leader.children[0].addEventListener("click", async () => await ui.viewCard(this.leader), false);
this.elem_leader.children[0].addEventListener("mouseover", function () { });
this.elem_leader.children[0].addEventListener("mouseout", function () { });
}
// TODO set crown color
}
async callLeader() {
await ui.viewCard(player_me.leader, async () => await player_me.activateLeader());
}
async activateFactionAbility() {
let factionData = factions[this.deck.faction];
if (factionData.activeAbility && this.factionAbilityUses > 0) {
await ui.popup("Use faction ability [E]", () => this.useFactionAbility(), "Cancel [Q]", () => this.escapeFactionAbility(), "Would you like to use your faction ability?", "Faction ability: " + factionData.description);
}
return;
}
async useFactionAbility() {
let factionData = factions[this.deck.faction];
if (factionData.activeAbility && this.factionAbilityUses > 0) {
this.endTurnAfterAbilityUse = true;
await factionData.factionAbility(this);
this.updateFactionAbilityUses(this.factionAbilityUses - 1);
// Some faction abilities require extra interractions
if (this.endTurnAfterAbilityUse)
await this.endTurn();
if (this.controller instanceof ControllerAI) {
if (this.deck.faction === "lyria_rivia") {
let best_row = this.controller.bestRowToussaintWine(ui.previewCard); // Reusing bestRowToussaintWine because it is nearly the same principle
ui.selectRow(best_row, true);
}
}
}
return;
}
// Called when chose to not play the faction ability
async escapeFactionAbility() {
ui.enablePlayer(true);
}
updateFactionAbilityUses(count) {
this.factionAbilityUses = Math.max(0, count);
document.getElementById("faction-ability-count-" + this.tag).innerHTML = this.factionAbilityUses;
if (this.factionAbilityUses === 0) {
document.getElementById("faction-ability-" + this.tag).classList.add("fade");
} else {
document.getElementById("faction-ability-" + this.tag).classList.remove("fade");
}
}
// Get all rows for this player, sorted to have close > ranged > siege
getAllRows() {
if (this === player_me) {
return board.row.filter((r, i) => i > 2);
}
return board.row.filter((r, i) => i < 3).reverse();
}
// Get all special rows for this player, sorted to have close > ranged > siege
getAllSpecialRows() {
return this.getAllRows().map(r => r.special);
}
// Get all cards in rows for this player
getAllRowCards() {
return this.getAllRows().reduce((a, r) => r.cards.concat(a), []);
}
// Get all cards in special rows for this player
getAllSpecialRowCards() {
return this.getAllSpecialRows().reduce((a, r) => r.cards.concat(a), []);
}
// Prepare game and UI to let the player select where on the board to play the given card
selectCardDestination(card,src=null,callback=null) {
// Move from deck to hand
if(src)
src.removeCard(card);
this.hand.addCard(card);
// Enable board interaction - Force to select a destination
ui.showPreviewVisuals(card);
document.getElementById("click-background").classList.add("noclick");
this.hand.cards.forEach(c => c.elem.classList.add("noclick"));
ui.setSelectable(card, true);
// Prevent the end of turn while selecting cards
this.endturn_action = async () => {
this.endturn_action = null;
if (callback)
callback();
}
ui.enablePlayer(true);
}
}
function alteraClicavel(obj, add) {
try {
if (!add && fileira_clicavel.elem.id == obj.elem.id) fileira_clicavel = null;
else fileira_clicavel = obj;
} catch (err) { }
}
// Handles the adding, removing and formatting of cards in a container
class CardContainer {
constructor(elem) {
this.elem = elem;
this.cards = [];
}
// Indicates whether or not this container contains any card
isEmpty() {
return this.cards.length === 0;
}
// Returns the first card that satisfies the predcicate. Does not modify container.
findCard(predicate) {
for (let i = this.cards.length - 1; i >= 0; --i)
if (predicate(this.cards[i]))
return this.cards[i];
}
// Returns a list of cards that satisfy the predicate. Does not modify container.
findCards(predicate) {
return this.cards.filter(predicate);
}
// Indicates whether or not the card with given Key can be found in container
containsCardByKey(key) {
return (this.findCards(c => c.key === key).length) > 0;
}
// Returns a list of up to n cards that satisfy the predicate. Does not modify container.
findCardsRandom(predicate, n) {
let valid = predicate ? this.cards.filter(predicate) : this.cards;
if (valid.length === 0)
return [];
if (!n || n === 1)
return [valid[randomInt(valid.length)]];
// Randum shuffle then select first n items
valid = [...valid].sort(() => 0.5 - Math.random())
return valid.slice(0, n);
}
// Removes and returns a list of cards that satisy the predicate.
getCards(predicate) {
return this.cards.reduce((a, c, i) => (predicate(c, i) ? [i] : []).concat(a), []).map(i => this.removeCard(i));
}
// Removes and returns a card that satisfies the predicate.
getCard(predicate) {
for (let i = this.cards.length - 1; i >= 0; --i)
if (predicate(this.cards[i]))
return this.removeCard(i);
}
// Removes and returns any cards up to n that satisfy the predicate.
getCardsRandom(predicate, n) {
return this.findCardsRandom(predicate, n).map(c => this.removeCard(c));
}
// Adds a card to the container along with its associated HTML element.
addCard(card, index) {
this.cards.push(card);
this.addCardElement(card, index ? index : 0);
this.resize();
card.currentLocation = this;
}
// Removes a card from the container along with its associated HTML element.
removeCard(card, index) {
if (this.cards.length === 0)
throw "Cannot draw from empty " + this.constructor.name;
card = this.cards.splice(isNumber(card) ? card : this.cards.indexOf(card), 1)[0];
this.removeCardElement(card, index ? index : 0);
this.resize();
if (card.temporaryPower) {
card.basePower = card.originalBasePower;
card.originalBasePower = null;
card.temporaryPower = false;
}
return card;
}
// Adds a card to a pre-sorted CardContainer
addCardSorted(card) {
let i = this.getSortedIndex(card);
this.cards.splice(i, 0, card);
return i;
}
// Returns the expected index of a card in a sorted CardContainer
getSortedIndex(card) {
for (var i = 0; i < this.cards.length; ++i)
if (Card.compare(card, this.cards[i]) < 0)
break;
return i;
}
// Adds a card to a random index of the CardContainer
addCardRandom(card) {
this.cards.push(card);
let index = randomInt(this.cards.length);
if (index !== this.cards.length - 1) {
let t = this.cards[this.cards.length - 1];
this.cards[this.cards.length - 1] = this.cards[index];
this.cards[index] = t;
}
card.currentLocation = this;
return index;
}
// Removes the HTML element associated with the card from this CardContainer
removeCardElement(card, index) {
if (this.elem)
this.elem.removeChild(card.elem);
}
// Adds the HTML element associated with the card to this CardContainer
addCardElement(card, index) {
if (this.elem) {
if (index === this.cards.length)
this.elem.appendChild(card.elem);
else
this.elem.insertBefore(card.elem, this.elem.children[index]);
}
}
// Empty function to be overried by subclasses that resize their content
resize() { }
// Modifies the margin of card elements inside a row-like container to stack properly
resizeCardContainer(overlap_count, gap, coef) {
let n = this.elem.children.length;
let param = (n < overlap_count) ? "" + gap + "vw" : defineCardRowMargin(n, coef);
let children = this.elem.getElementsByClassName("card");
for (let x of children)
x.style.marginLeft = x.style.marginRight = param;
function defineCardRowMargin(n, coef = 0) {
return "calc((100% - (4.45vw * " + n + ")) / (2*" + n + ") - (" + coef + "vw * " + n + "))";
}
}
// Allows the row to be clicked
setSelectable() {
this.elem.classList.add("row-selectable");
alteraClicavel(this, true);
}
// Disallows the row to be clicked
clearSelectable() {
this.elem.classList.remove("row-selectable");
alteraClicavel(this, false);
for (card in this.cards)
card.elem.classList.add("noclick");
}
// Returns the container to its default, empty state
reset() {
while (this.cards.length)
this.removeCard(0);
if (this.elem)
while (this.elem.firstChild)
this.elem.removeChild(this.elem.firstChild);
this.cards = [];
}
}
// Contians all used cards in the order that they were discarded
class Grave extends CardContainer {
constructor(elem) {
super(elem)
elem.addEventListener("click", () => ui.viewCardsInContainer(this), false);
}
// Override
addCard(card) {
this.setCardOffset(card, this.cards.length);
super.addCard(card, this.cards.length);
card.destructionRound = game.roundCount;
}
// Override
removeCard(card) {
let n = isNumber(card) ? card : this.cards.indexOf(card);
return super.removeCard(card, n);
}
// Override
removeCardElement(card, index) {
card.elem.style.left = "";
for (let i = index; i < this.cards.length; ++i) this.setCardOffset(this.cards[i], i);
super.removeCardElement(card, index);
}
// Offsets the card element in the deck
setCardOffset(card, n) {
card.elem.style.left = -0.03 * n + "vw";
}
}
// Contians all special cards for a given row
class RowSpecial extends CardContainer {
constructor(elem, row) {
super(elem)
this.row = row;
}
// Override
addCard(card) {
this.setCardOffset(card, this.cards.length);
super.addCard(card, this.cards.length);
}
// Override
removeCard(card) {
let n = isNumber(card) ? card : this.cards.indexOf(card);
if (card.removed) {
for (let x of card.removed)
x(card);
}
return super.removeCard(card, n);
}
// Override
removeCardElement(card, index) {
card.elem.style.left = "";
for (let i = index; i < this.cards.length; ++i) this.setCardOffset(this.cards[i], i);
super.removeCardElement(card, index);
}
// Offsets the card element in the deck
setCardOffset(card, n) {
card.elem.style.left = (1 + n) + "vw";
}
}
// Contains a randomized set of cards to be drawn from
class Deck extends CardContainer {
constructor(faction, elem) {
super(elem);
this.faction = faction;
this.counter = document.createElement("div");
this.counter.classList = "deck-counter center";
this.counter.appendChild(document.createTextNode(this.cards.length));
this.elem.appendChild(this.counter);
}
// Creates duplicates of cards with a count of more than one, then initializes deck
initializeFromID(card_id_list, player) {
this.initialize(card_id_list.reduce((a, c) => a.concat(clone(c.count, c)), []), player);
function clone(n, elem) {
for (var i = 0, a = []; i < n; ++i) a.push(elem);
return a;
}
}
// Populates a this deck with a list of card data and associated those cards with the owner of this deck.
initialize(card_data_list, player) {
this.player = player;
for (let i = 0; i < card_data_list.length; ++i) {
let card = new Card(card_data_list[i].index, card_dict[card_data_list[i].index], player);
card.holder = player;
this.addCardRandom(card);
this.addCardElement();
}
this.resize();
}
// Override
addCard(card) {
this.addCardRandom(card);
this.addCardElement();
this.resize();
card.currentLocation = this;
}
// Sends the top card to the passed hand
async draw(hand) {
tocar("game_buy", false);
// In case a player draws from opponent deck
if (hand.player && this.player !== hand.player)
this.cards[0].holder = hand.player;
if (hand instanceof HandAI)
hand.addCard(this.removeCard(0));
else
await board.toHand(this.cards[0], this, hand);
}
// Draws a card and sends it to the container before adding a card from the container back to the deck.
swap(container, card) {
container.addCard(this.removeCard(0));
this.addCard(card);
}
// Override
addCardElement() {
let elem = document.createElement("div");
elem.classList.add("deck-card");
elem.style.backgroundImage = iconURL("deck_back_" + this.faction, "jpg");
this.setCardOffset(elem, this.cards.length - 1);
this.elem.insertBefore(elem, this.counter);
}
// Override
removeCardElement() {
this.elem.removeChild(this.elem.children[this.cards.length]).style.left = "";
}
// Offsets the card element in the deck
setCardOffset(elem, n) {
elem.style.left = -0.03 * n + "vw";
}
// Override
resize() {
this.counter.innerHTML = this.cards.length;
this.setCardOffset(this.counter, this.cards.length);
}
// Override
reset() {
super.reset();
this.elem.appendChild(this.counter);
}
}
// Hand used by computer AI. Has an offscreen HTML element for card transitions.
class HandAI extends CardContainer {
constructor(tag) {
super(undefined, tag);
this.player = null;
if (this.tag === "me") {
this.counter = document.getElementById("hand-count-me");
this.hidden_elem = document.getElementById("hand-me");
} else {
this.counter = document.getElementById("hand-count-op");
this.hidden_elem = document.getElementById("hand-op");
}
}
resize() {
this.counter.innerHTML = this.cards.length;
}
}
// Hand used by current player
class Hand extends CardContainer {
constructor(elem, tag) {
super(elem);
this.tag = tag;
this.player = null;
if (this.tag === "me") {
this.counter = document.getElementById("hand-count-me");
} else {
this.counter = document.getElementById("hand-count-op");
}
}
// Override
addCard(card) {
let i = this.addCardSorted(card);
this.addCardElement(card, i);
this.resize();
card.currentLocation = this;
}
// Override
resize() {
this.counter.innerHTML = this.cards.length;
this.resizeCardContainer(11, 0.075, .00225);
}
toggleDisplay() {
if (this.elem.hidden) {
this.elem.style.visibility = "visible";
} else {
this.elem.style.visibility = "hidden";
}
this.elem.hidden = !this.elem.hidden;
}
hide() {
this.elem.style.visibility = "hidden";
}
show() {
this.elem.style.visibility = "visible";
}
}
var may_act_card = true;
// Contains active cards and effects. Calculates the current score of each card and the row.
class Row extends CardContainer {
constructor(elem) {
super(elem.getElementsByClassName("row-cards")[0]);
this.elem_parent = elem;
this.special = new RowSpecial(elem.getElementsByClassName("row-special")[0], this);
this.total = 0;
this.effects = {
weather: false,
weather_type: "",
bond: {},
morale: 0,
horn: 0,
mardroeme: 0,
shield: 0,
lock: 0,
curse: 0,
toussaint_wine: 0,
ambush: false
};
this.halfWeather = false;
this.elem.addEventListener("click", () => ui.selectRow(this), true);
this.elem.addEventListener("mouseover", function () {
if (hover_row) {
tocar("card", false);
this.style.boxShadow = "0 0 1.5vw #6d5210";
}
});
this.elem.addEventListener("mouseout", function () {
this.style.boxShadow = "0 0 0 #6d5210"
});
window.addEventListener("keydown", function (e) {
if (e.keyCode == 13 && fileira_clicavel !== null && may_act_card) {
ui.selectRow(fileira_clicavel);
may_act_card = false;
fileira_clicavel = null;
}
});
window.addEventListener("keyup", function (e) {
if (e.keyCode == 13) may_act_card = true;
});
this.special.elem.addEventListener("click", () => ui.selectRow(this, true), false, true);
}
// Override
async addCard(card, runEffect = true) {
if (card.isSpecial()) {
this.special.addCard(card);
} else {
let index = this.addCardSorted(card);
this.addCardElement(card, index);
this.resize();
}
card.currentLocation = this;
if (this.effects.curse && card.isUnit()) {
this.effects.curse = Math.max(this.effects.curse - 1, 0);
await card.animate("curse");
await board.toGrave(card, this);
let curse_card = this.special.findCard(c => c.abilities.includes("curse"));
await board.toGrave(curse_card, this.special);
return;
} else if (this.effects.lock && card.isUnit() && card.abilities.length) {
card.locked = true;
this.effects.lock = Math.max(this.effects.lock - 1, 0);
let lock_card = this.special.findCard(c => c.abilities.includes("lock"));
// If several units arrive at the same time, it can be triggered several times, so we first remove the lock before doing animations
if (lock_card)
await board.toGrave(lock_card, this.special);
await card.animate("lock");
}
// Some cards have abilities with permanent row effects - such has horn, bond or morale
if (!runEffect && card.abilities.length > 0) {
for (var i = 0; i < card.abilities.length; i++) {
if ("effectAfterMove" in ability_dict[card.abilities[i]] && ability_dict[card.abilities[i]]["effectAfterMove"])
runEffect = true;
}
}
if (runEffect && !card.isLocked()) {
this.updateState(card, true);
for (let x of card.placed)
await x(card, this);
}
if (runEffect && this.effects.ambush) {
let ambushCards = this.cards.filter(c => c.abilities.includes("ambush") &&
(c.holder !== card.holder && !card.abilities.includes("spy") && !card.abilities.includes("emissary")) ||
(c.holder === card.holder && (card.abilities.includes("spy") || card.abilities.includes("emissary")))); // Spy/Emissaries switch sides before being placed and would have triggerd when played by the owner of an ambush card
if (ambushCards.length > 0 && ambushCards[0] !== card) {
let targetCard = ambushCards[0];
// Remove status first before animations to avoid triggering the ambush several times when several cards arrive at the same time
if (ambushCards.length < 2)
this.effects.ambush = false;
// Owner draws 2 cards
await targetCard.animate("ambush");
targetCard.holder.deck.draw(targetCard.holder.hand);
targetCard.holder.deck.draw(targetCard.holder.hand);
// Cards goes to the grave
await board.toGrave(targetCard, targetCard.currentLocation);
}
}
card.elem.classList.add("noclick");
await sleep(600);
//this.updateScore();
// Let's update all rows for better accuracy
board.updateScores();
}
// Override
removeCard(card, runEffect = true) {
// TODO: This case should no longer happen
if (isNumber(card) && card === -1) {
card = this.special.cards[0];
this.special.reset();
return card;
}
card = isNumber(card) ? this.cards[card] : card;
if (card.isSpecial()) {
this.special.removeCard(card);
} else {
super.removeCard(card);
card.resetPower();
card.locked = false;
}
this.updateState(card, false);
if (runEffect) {
if (!card.decoyTarget) {
for (let x of card.removed)
x(card);
} else {
card.decoyTarget = false;
}
}
this.updateScore();
return card;
}
// Override
removeCardElement(card, index) {
super.removeCardElement(card, index);
let x = card.elem;
x.style.marginLeft = x.style.marginRight = "";
x.classList.remove("noclick");
}
// Updates a card's effect on the row
updateState(card, activate) {
for (let x of card.abilities) {
if (!card.isLocked()) {
switch (x) {
case "morale":
case "horn":
case "mardroeme":
case "lock":
case "curse":
case "toussaint_wine":
this.effects[x] += activate ? 1 : -1;
break;
case "shield":
case "shield_c":
case "shield_r":
case "shield_s":
if (activate)
Promise.all(this.cards.filter(c => c.isUnit()).map(c => c.animate("shield")));
this.effects["shield"] += activate ? 1 : -1;
break;
case "bond":
if (!this.effects.bond[card.target])
this.effects.bond[card.target] = 0;
this.effects.bond[card.target] += activate ? 1 : -1;
break;
}
}
}
}
// Activates weather effect and visuals
addOverlay(overlay) {
var som = overlay == "fog" || overlay == "rain" ? overlay : overlay == "frost" ? "cold" : "";
if (som != "") tocar(som, false);
this.effects.weather = true;
this.effects.weather_type = overlay;
this.elem_parent.getElementsByClassName("row-weather")[0].classList.add(overlay);
this.updateScore();
}
// Deactivates weather effect and visuals
removeOverlay(overlay) {
this.effects.weather = false;
this.effects.weather_type = "";
this.elem_parent.getElementsByClassName("row-weather")[0].classList.remove(overlay);
this.updateScore();
}
// Override
resize() {
this.resizeCardContainer(10, 0.075, .00325);
}
// Updates the row's score by summing the current power of its cards
updateScore() {
let total = 0;
for (let card of this.cards) {
total += this.cardScore(card);
}
let player = this.elem_parent.parentElement.id === "field-op" ? player_op : player_me;
player.updateTotal(total - this.total);
this.total = total;
this.elem_parent.getElementsByClassName("row-score")[0].innerHTML = this.total;
}
// Calculates and set the card's current power
cardScore(card) {
let total = this.calcCardScore(card);
card.setPower(total);
return total;
}
// Calculates the current power of a card affected by row affects
calcCardScore(card) {
if (card.key === "spe_decoy")
return 0;
let total = card.basePower * card.multiplier;
if (card.hero)
return total;
if (card.abilities.includes("spy") || card.abilities.includes("emissary"))
total = Math.floor(game.spyPowerMult * total);
// Inspire - changes base strength, before weather
if (card.abilities.includes("inspire") && !card.isLocked()) {
let inspires = card.holder.getAllRowCards().filter(c => !c.isLocked() && c.abilities.includes("inspire"));
if (inspires.length > 1) {
let maxBase = inspires.reduce((a, b) => a.power > b.power ? a : b);
total = maxBase.power;
}
}
if (this.effects.weather)
if (!(card.abilities.includes("fog_summoning") && this.effects.weather_type === "fog")) {
if (this.halfWeather)
total = Math.max(Math.min(1, total), Math.floor(total / 2)); // 2 special cases, if intially 1, we want to keep one, not 0 (floor(0.5)). If 0, we want to keep 0, not 1
else
total = Math.min(1, total);
}
// Bond
if (card.abilities.includes("bond")) {
let bond = this.effects.bond[card.target];
if (isNumber(bond) && bond > 1 && !card.isLocked())
total *= Number(bond);
}
// Morale
total += Math.max(0, this.effects.morale + (card.abilities.includes("morale") ? -1 : 0));
// Toussiant Wine
total += Math.max(0, 3 * this.effects.toussaint_wine);
// Witcher Schools
if (card.abilities.at(-1) && card.abilities.at(-1).startsWith("witcher_") && !card.isLocked()) {
let school = card.abilities.at(-1);
if (card.holder.effects["witchers"][school]) {
total += (card.holder.effects["witchers"][school] - 1) * 2;
}
}
// Whorshipped
if (card.abilities.includes("whorshipped") && card.holder.effects["whorshippers"] > 0 && !card.isLocked())
total += card.holder.effects["whorshippers"] * game.whorshipBoost;
// Horn
if (this.effects.horn - (card.abilities.includes("horn") ? 1 : 0) > 0)
total *= 2;
return total;
}
// Applies a temporary leader horn affect that is removed at the end of the round
async leaderHorn(card) {
if (this.special.containsCardByKey("spe_horn"))
return;
let horn = new Card("spe_horn", card_dict["spe_horn"], card.holder);
await this.addCard(horn);
game.roundEnd.push(() => this.removeCard(horn));
}
// Applies a local scorch effect to this row
async scorch() {
if (this.total >= 10 && !this.isShielded() && !game.scorchCancelled)
await Promise.all(this.maxUnits().map(async c => {
await c.animate("scorch", true, false);
await board.toGrave(c, this);
}));
}
// Removes all cards and effects from this row
clear() {
this.special.cards.filter(c => !c.noRemove).forEach(c => board.toGrave(c, this, true));
this.cards.filter(c => !c.noRemove).forEach(c => board.toGrave(c, this, true));
}
// Returns all regular unit cards with the heighest power
maxUnits() {
let max = [];
for (let i = 0; i < this.cards.length; ++i) {
let card = this.cards[i];
if (!card.isUnit() || card.isImmortal())
continue;
if (!max[0] || max[0].power < card.power)
max = [card];
else if (max[0].power === card.power)
max.push(card);
}
return max;
}
minUnits() {
let min = [];
for (let i = 0; i < this.cards.length; ++i) {
let card = this.cards[i];
if (!card.isUnit() || card.isImmortal())
continue;
if (!min[0] || min[0].power > card.power)
min = [card];
else if (min[0].power === card.power)
min.push(card);
}
return min;
}
// Override
reset() {
super.reset();
this.special.reset();
this.total = 0;
this.effects = {
weather: false,
bond: {},
morale: 0,
horn: 0,
mardroeme: 0,
shield: 0,
lock: 0,
curse: 0,
toussaint_wine: 0,
ambush: false
};
}
// Indicates whether or not a shield is protecting that row from abilities (does not protect from weather effects though)
isShielded() {
return (this.effects.shield > 0);
}
// True if at least 1 unit and total of power >= 10
canBeScorched() {
if (game.scorchCancelled)
return false;
return (this.cards.reduce((a, c) => a + c.power, 0) >= 10) && (this.cards.filter(c => c.isUnit()).length > 0);
}
// Return the index of the current row in the list of rows on the board
getRowIndex() {
for (let i = 0; i < board.row.length; i++) {
if (board.row[i] === this)
return i;
}
return -1;
}
// Returns the opposite Row object - the one on the opponent's side of the field
getOppositeRow() {
let index = 5 - this.getRowIndex();
if (index >= 0 && index < board.row.length)
return board.row[index]
return null;
}
// Debug/cheat - Invokes any given card to the row
invokeCard(key) {
let player = this.elem_parent.parentElement.id === "field-op" ? player_op : player_me;
this.addCard(new Card(key, card_dict[key], player), true)
}
}
// Handles how weather effects are added and removed
class Weather extends CardContainer {
constructor(elem) {
super(document.getElementById("weather"));
this.types = {
rain: {
name: "rain",
count: 0,
rows: []
},
fog: {
name: "fog",
count: 0,
rows: []
},
frost: {
name: "frost",
count: 0,
rows: []
}
}
let i = 0;
for (let key of Object.keys(this.types))
this.types[key].rows = [board.row[i], board.row[5 - i++]];
this.elem.addEventListener("click", () => ui.selectRow(this), false);
}
// Adds a card if unique and clears all weather if 'clear weather' card added
async addCard(card, withEffects = true) {
super.addCard(card);
card.elem.classList.add("noclick");
if (!withEffects)
return;
// Run possible actions
if (withEffects && !card.isLocked()) {
for (let x of card.placed)
await x(card, this);
}
if (card.key === "spe_clear") {
// TODO Sunlight animation
tocar("clear", false);
await sleep(500);
this.clearWeather();
} else {
this.changeWeather(card, x => ++this.types[x].count === 1, (r, t) => r.addOverlay(t.name));
for (let i = this.cards.length - 2; i >= 0; --i) {
if (card.abilities.at(-1) === this.cards[i].abilities.at(-1)) {
await sleep(750);
await board.toGrave(card, this);
break;
}
}
}
await sleep(750);
}
// Override
removeCard(card, withEffects = true) {
card = super.removeCard(card);
card.elem.classList.remove("noclick");
if (withEffects) {
this.changeWeather(card, x => --this.types[x].count === 0, (r, t) => r.removeOverlay(t.name));
// Run possible actions
if (withEffects && !card.isLocked()) {
for (let x of card.removed) {
x(card, this);
}
}
}
return card;
}
// Checks if a card's abilities are a weather type. If the predicate is met, perfom the action
// on the type's associated rows
changeWeather(card, predicate, action) {
for (let x of card.abilities) {
if (x in this.types && predicate(x)) {
for (let r of this.types[x].rows)
action(r, this.types[x]);
}
}
}
// Removes all weather effects and cards
async clearWeather() {
await Promise.all(this.cards.map((c, i) => this.cards[this.cards.length - i - 1]).map(c => board.toGrave(c, this)));
}
// Override
resize() {
this.resizeCardContainer(4, 0.075, .045);
}
// Override
reset() {
super.reset();
Object.keys(this.types).map(t => this.types[t].count = 0);
}
}
//
class Board {
constructor() {
this.op_score = 0;
this.me_score = 0;
this.row = [];
for (let x = 0; x < 6; ++x) {
let elem = document.getElementById((x < 3) ? "field-op" : "field-me").children[x % 3];
this.row[x] = new Row(elem);
}
}
// Get the opponent of this Player
opponent(player) {
return player === player_me ? player_op : player_me;
}
// Sends and translates a card from the source to the Deck of the card's holder
async toDeck(card, source) {
tocar("discard", false);
await this.moveTo(card, "deck", source);
}
// Sends and translates a card from the source to the Grave of the card's holder
async toGrave(card, source, turnEnd=false) {
let destroy = true;
let protectors = null;
if (card.isUnit() && source instanceof Row) {
// Checking Protection abilities such as Comrade
protectors = card.holder.getAllRowCards().filter(c => c.abilities.includes("comrade") && c.protects);
if (protectors.length > 0) {
let choice = false;
if (!(card.holder.controller instanceof ControllerAI)) {
choice = await ui.popup("Save it [E]", () => true, "Let it die [Q]", () => false, "Do you want to save this unit?", "Comrade ability can prevent the destruction of the following card: " + card.name + " (strength: " + card.power + "). Do you want to save it?");
if (choice) {
destroy = false;
protectors[0].protects = false;
}
} else {
// AI saves the unit only if its value is > 5
if (card.power > 5) {
protectors[0].protects = false;
destroy = false;
}
}
}
}
if (destroy) {
await this.moveTo(card, "grave", source);
if (game.unitDestroyed && game.unitDestroyed.length > 0 && !turnEnd) {
for (var i = 0; i < game.unitDestroyed.length; i++) {
let cb = game.unitDestroyed[i];
if (await cb(card))
game.unitDestroyed.splice(i, 1)
}
}
} else {
card.animate("comrade");
await protectors[0].animate("comrade");
}
}
// Sends and translates a card from the source to the Hand of the card's holder
// Possible to specify a different destination
async toHand(card, source, dest = null) {
await this.moveTo(card, dest ? dest : "hand", source);
}
// Sends and translates a card from the source to Weather
async toWeather(card, source) {
await this.moveTo(card, weather, source);
}
// Sends and translates a card from the source to the Deck of the card's combat row
async toRow(card, source) {
let row = ["close", "agile", "agile_cr", "agile_cs", "agile_crs"].includes(card.row) ? "close" : ["ranged", "agile_rs"].includes(card.row) ? "ranged" : card.row ? card.row : "close";
await this.moveTo(card, row, source);
}
// Sends and translates a card from the source to a specified row name or CardContainer
async moveTo(card, dest, source = null) {
if (isString(dest)) dest = this.getRow(card, dest);
try {
cartaNaLinha(dest.elem.id, card);
} catch (err) { }
await translateTo(card, source ? source : null, dest);
if (dest instanceof Row || dest instanceof Weather)
await dest.addCard(source ? source.removeCard(card) : card); //Only the override in the Row/Weather classes are asynchronous
else
dest.addCard(source ? source.removeCard(card) : card);
}
// Sends and translates a card from the source to a specified row name or CardContainer - NO EFFECTS/ABILITIES
async moveToNoEffects(card, dest, source = null) {
if (isString(dest)) dest = this.getRow(card, dest);
try {
cartaNaLinha(dest.elem.id, card);
} catch (err) { }
await translateTo(card, source ? source : null, dest);
if (dest instanceof Row || dest instanceof Weather)
await dest.addCard(source ? source.removeCard(card, false) : card, false); //Only the override in the Row/Weather classes are asynchronous
else
dest.addCard(source ? source.removeCard(card) : card);
}
// Sends and translates a card from the source to a row name associated with the passed player
async addCardToRow(card, row_name, player, source) {
let row;
if (row_name instanceof Row) {
row = row_name;
} else {
row = this.getRow(card, row_name, player);
}
try {
cartaNaLinha(row.elem.id, card);
} catch (err) { }
await translateTo(card, source, row);
await row.addCard(card);
}
// Returns the Card associated with the row name that the card would be sent to
getRow(card, row_name, player) {
player = player ? player : card ? card.holder : player_me;
let isMe = player === player_me;
let isSpy = (card.abilities.includes("spy") || card.abilities.includes("emissary") || card.abilities.includes("ambush"));
switch (row_name) {
case "weather":
return weather;
break;
case "close":
return this.row[isMe ^ isSpy ? 3 : 2];
case "ranged":
return this.row[isMe ^ isSpy ? 4 : 1];
case "siege":
return this.row[isMe ^ isSpy ? 5 : 0];
case "grave":
return player.grave;
case "deck":
return player.deck;
case "hand":
return player.hand;
default:
console.error(card.name + " sent to incorrect row \"" + row_name + "\" by " + card.holder.name);
}
}
// Return the list of rows possible for the given card
getAgileRows(card, player) {
switch (card.row) {
case "agile":
case "agile_cr":
return [board.getRow(card, "close", player), board.getRow(card, "ranged", player)];
case "agile_cs":
return [board.getRow(card, "close", player), board.getRow(card, "siege", player)];
case "agile_rs":
return [board.getRow(card, "ranged", player), board.getRow(card, "siege", player)];
case "agile_crs":
return [board.getRow(card, "close", player), board.getRow(card, "ranged", player), board.getRow(card, "siege", player)];
default:
return [board.getRow(card, card.row, player)];
}
}
// Updates which player currently is in the lead
updateLeader() {
let dif = player_me.total - player_op.total;
player_me.setWinning(dif > 0);
player_op.setWinning(dif < 0);
}
updateScores() {
//this.row.map(r => r.cards.map(c => r.cardScore(c)));
this.row.map(r => r.updateScore());
}
}
function limpar() {
fileira_clicavel = null;
load_pass = load_passT;
may_pass1 = false;
may_pass2 = "";
may_pass3 = true;
timer2 = null;
lCard = null;
}
class Game {
constructor() {
this.endScreen = document.getElementById("end-screen");
let buttons = this.endScreen.getElementsByTagName("button");
this.customize_elem = buttons[0];
this.replay_elem = buttons[1];
this.customize_elem.addEventListener("click", () => this.returnToCustomization(), false);
this.replay_elem.addEventListener("click", () => this.restartGame(), false);
this.reset();
this.randomOPDeck = true;
this.mode = 1;
}
reset() {
this.firstPlayer;
this.currPlayer = null;
this.gameStart = [];
this.roundStart = [];
this.roundEnd = [];
this.turnStart = [];
this.turnEnd = [];
this.unitDestroyed = [];
this.roundCount = 0;
this.roundHistory = [];
this.over = false;
this.randomRespawn = false;
this.medicCount = 1;
this.whorshipBoost = 1;
this.spyPowerMult = 1;
this.decoyCancelled = false;
this.scorchCancelled = false;
// Also resetting some board/row properties affected during the course of a game
if (board) {
if (board.row) {
board.row.forEach(r => {
r.halfWeather = false;
});
}
}
weather.reset();
board.row.forEach(r => r.reset());
}
// Sets up player faction abilities and psasive leader abilities
initPlayers(p1, p2) {
let l1 = ability_dict[p1.leader.abilities[0]];
let l2 = ability_dict[p2.leader.abilities[0]];
let special_abilities = {
meve_white_queen: false
};
initLeader(p1, l1);
initLeader(p2, l2);
if (l1 === ability_dict["meve_white_queen"] || l2 === ability_dict["meve_white_queen"])
special_abilities["meve_white_queen"] = true;
if (p1.deck.faction === p2.deck.faction && p1.deck.faction === "scoiatael")
return special_abilities;
initFaction(p1);
initFaction(p2);
function initLeader(player, leader) {
if (leader.placed)
leader.placed(player.leader);
Object.keys(leader).filter(key => game[key]).map(key => game[key].push(leader[key]));
}
function initFaction(player) {
//Only passive faction abilities
if (factions[player.deck.faction] && factions[player.deck.faction].factionAbility && !factions[player.deck.faction].activeAbility)
factions[player.deck.faction].factionAbility(player);
}
return special_abilities;
}
// Sets initializes player abilities, player hands and redraw
async startGame() {
ui.toggleMusic_elem.classList.remove("music-customization");
var special_abilities = this.initPlayers(player_me, player_op);
await Promise.all([...Array(10).keys()].map(async () => {
await player_me.deck.draw(player_me.hand);
await player_op.deck.draw(player_op.hand);
}));
await this.runEffects(this.gameStart);
if (!this.firstPlayer)
this.firstPlayer = await this.coinToss();
if (special_abilities["meve_white_queen"]) await ui.notification("meve_white_queen", 1200);
this.initialRedraw();
somCarta();
}
// Simulated coin toss to determine who starts game
async coinToss() {
this.firstPlayer = (Math.random() < 0.5) ? player_me : player_op;
await ui.notification(this.firstPlayer.tag + "-coin", 1200);
return this.firstPlayer;
}
// Allows the player to swap out up to two cards from their iniitial hand
async initialRedraw() {
if (player_op.controller instanceof ControllerAI) {
if (player_op.leader.key === "sc_francesca_daisy") {
let cards = player_op.controller.discardOrder(player_op.leader, player_op.hand, true).splice(0, player_op.mulliganCount);
cards.forEach(c => {
board.toDeck(c, player_op.hand);
});
} else {
for (let i = 0; i < player_op.mulliganCount; i++)
player_op.controller.redraw();
}
}
if (player_me.controller instanceof ControllerAI) {
if (player_me.leader.key === "sc_francesca_daisy") {
let cards = player_me.controller.discardOrder(player_me.leader, player_me.hand, true).splice(0, player_me.mulliganCount);
cards.forEach(c => {
board.toDeck(c, player_me.hand);
});
} else {
for (let i = 0; i < player_me.mulliganCount; i++)
player_me.controller.redraw();
}
} else {
// player vs player - both have a redraw - player 1 first
if (this.mode === 3) {
if (player_me.leader.key === "sc_francesca_daisy") {
await ui.queueCarousel(player_me.hand, player_me.mulliganCount, async (c, i) => await board.toDeck(c.cards[i], c), c => true, true, false, "Player 1 - Choose " + player_me.mulliganCount +" cards to put back to deck.");
} else {
await ui.queueCarousel(player_me.hand, player_me.mulliganCount, async (c, i) => await player_me.deck.swap(c, c.removeCard(i)), c => true, true, true, "Player 1 - Choose up to " + player_me.mulliganCount +" cards to redraw.");
}
if (player_op.leader.key === "sc_francesca_daisy") {
await ui.queueCarousel(player_op.hand, player_op.mulliganCount, async (c, i) => await board.toDeck(c.cards[i], c), c => true, true, false, "Player 2 - Choose " + player_op.mulliganCount +" cards to put back to deck.");
} else {
await ui.queueCarousel(player_op.hand, player_op.mulliganCount, async (c, i) => await player_op.deck.swap(c, c.removeCard(i)), c => true, true, true, "Player 2 - Choose up to " + player_op.mulliganCount +" cards to redraw.");
}
} else {
if (player_me.leader.key === "sc_francesca_daisy") {
await ui.queueCarousel(player_me.hand, player_me.mulliganCount, async (c, i) => await board.toDeck(c.cards[i], c), c => true, true, false, "Choose " + player_me.mulliganCount +" cards to put back to deck.");
} else {
await ui.queueCarousel(player_me.hand, player_me.mulliganCount, async (c, i) => await player_me.deck.swap(c, c.removeCard(i)), c => true, true, true, "Choose up to " + player_me.mulliganCount +" cards to redraw.");
}
}
ui.enablePlayer(false);
}
game.startRound();
}
// Initiates a new round of the game
async startRound(verdict = false) {
this.roundCount++;
if (verdict && verdict.winner) {
//Last round winner starts the round, verdict.winner can be null if draw
this.currPlayer = verdict.winner.opponent();
} else {
this.currPlayer = (this.roundCount % 2 === 0) ? this.firstPlayer : this.firstPlayer.opponent();
}
player_me.roundStartReset();
player_op.roundStartReset();
await this.runEffects(this.roundStart);
board.row.map(r => r.updateScore());
if (!player_me.canPlay())
player_me.setPassed(true);
if (!player_op.canPlay())
player_op.setPassed(true);
if (player_op.passed && player_me.passed)
return this.endRound();
if (this.currPlayer.passed)
this.currPlayer = this.currPlayer.opponent();
await ui.notification("round-start", 1200);
if (this.currPlayer.opponent().passed)
await ui.notification(this.currPlayer.tag + "-turn", 1200);
this.startTurn();
}
// Starts a new turn. Enables client interraction in client's turn.
async startTurn() {
await this.runEffects(this.turnStart);
if (!this.currPlayer.opponent().passed) {
this.currPlayer = this.currPlayer.opponent();
await ui.notification(this.currPlayer.tag + "-turn", 1200);
}
ui.enablePlayer(this.currPlayer === player_me);
// Player vs player - hide opponent's hand
if (game.isPvP()) {
this.currPlayer.opponent().hand.hide();
this.currPlayer.hand.show();
}
this.currPlayer.startTurn();
}
// Ends the current turn and may end round. Disables client interraction in client's turn.
async endTurn(noEffects=false) {
if (this.currPlayer === player_me)
ui.enablePlayer(false);
if (!noEffects)
await this.runEffects(this.turnEnd);
// Player might have "end turn" events which delay the actual end of the turn
if (this.currPlayer.endturn_action) {
// Call action instead of ending turn
await this.currPlayer.endturn_action();
return;
}
if (this.currPlayer.passed)
await ui.notification(this.currPlayer.tag + "-pass", 1200);
board.updateScores();
if (player_op.passed && player_me.passed)
this.endRound();
else
this.startTurn();
}
// Ends the round and may end the game. Determines final scores and the round winner.
async endRound() {
limpar();
// Clean and update scores
board.row.forEach(r => {
r.cards.forEach(c => {
if (c.temporaryPower)
c.basePower = c.originalBasePower;
});
r.updateScore();
});
board.updateScores();
let dif = player_me.total - player_op.total;
if (dif === 0) {
let nilf_me = player_me.deck.faction === "nilfgaard",
nilf_op = player_op.deck.faction === "nilfgaard";
dif = nilf_me ^ nilf_op ? nilf_me ? 1 : -1 : 0;
}
let winner = dif > 0 ? player_me : dif < 0 ? player_op : null;
let verdict = {
winner: winner,
score_me: player_me.total,
score_op: player_op.total
}
this.roundHistory.push(verdict);
await this.runEffects(this.roundEnd);
player_me.endRound(dif > 0);
player_op.endRound(dif < 0);
if (player_me.health === 0 || player_op.health === 0)
this.over = true;
weather.clearWeather();
// In case some cards stay on the board, we want to reset their power
board.row.forEach(row => {
row.clear();
row.cards.forEach(c => {
c.power = c.basePower;
});
});
if (dif > 0) {
await ui.notification("win-round", 1200);
} else if (dif < 0) {
if (nilfgaard_wins_draws) {
nilfgaard_wins_draws = false;
await ui.notification("nilfgaard-wins-draws", 1200);
}
await ui.notification("lose-round", 1200);
} else
await ui.notification("draw-round", 1200);
if (player_me.health === 0 || player_op.health === 0)
this.endGame();
else
this.startRound(verdict);
}
// Sets up and displays the end-game screen
async endGame() {
this.over = true;
let endScreen = document.getElementById("end-screen");
let rows = endScreen.getElementsByTagName("tr");
rows[1].children[0].innerHTML = player_me.name;
rows[2].children[0].innerHTML = player_op.name;
for (let i = 1; i < 4; ++i) {
let round = this.roundHistory[i - 1];
rows[1].children[i].innerHTML = round ? round.score_me : 0;
rows[1].children[i].style.color = round && round.winner === player_me ? "goldenrod" : "";
rows[2].children[i].innerHTML = round ? round.score_op : 0;
rows[2].children[i].style.color = round && round.winner === player_op ? "goldenrod" : "";
}
endScreen.children[0].className = "";
if (player_op.health <= 0 && player_me.health <= 0) {
tocar("");
endScreen.getElementsByTagName("p")[0].classList.remove("hide");
endScreen.children[0].classList.add("end-draw");
} else if (player_op.health === 0) {
tocar("game_win", true);
endScreen.children[0].classList.add("end-win");
} else {
tocar("game_lose", true);
endScreen.children[0].classList.add("end-lose");
}
fadeIn(endScreen, 300);
ui.enablePlayer(true);
}
// Returns the client to the deck customization screen
returnToCustomization() {
iniciarMusica();
this.reset();
player_me.reset();
player_op.reset();
ui.toggleMusic_elem.classList.add("music-customization");
this.endScreen.classList.add("hide");
document.getElementById("deck-customization").classList.remove("hide");
}
// Restarts the last game with the same decks
restartGame() {
iniciarMusica();
limpar();
this.reset();
player_me.reset();
player_op.reset();
this.endScreen.classList.add("hide");
this.startGame();
}
// Executes effects in list. If effect returns true, effect is removed.
async runEffects(effects) {
for (let i = effects.length - 1; i >= 0; --i) {
let effect = effects[i];
if (await effect())
effects.splice(i, 1)
}
}
isPvP() {
return (this.mode === 3);
}
}
// Contians information and behavior of a Card
class Card {
constructor(key, card_data, player) {
if (!card_data) {
console.log("Invalid card data for: " + key);
}
this.id;
if (card_data.id)
this.id = Number(card_data.id);
this.key = key;
this.name = card_data.name;
this.basePower = this.power = Number(card_data.strength);
this.faction = card_data.deck;
// To clean the field in case it is a faction specific weather/special card
if (this.faction.startsWith("weather") || this.faction.startsWith("special")) {
this.faction = this.faction.split(" ")[0];
}
this.abilities = (card_data.ability === "") ? [] : card_data.ability.split(" ");
this.row = (this.faction === "weather") ? this.faction : card_data.row;
this.filename = card_data.filename;
this.placed = [];
this.removed = [];
this.activated = [];
this.holder = player;
this.locked = false;
this.decoyTarget = false;
this.target = "";
this.currentLocation = board; // By default, updated later
this.temporaryPower = false;
this.multiplier = 1;
if ("target" in card_data) {
this.target = card_data.target;
}
this.quote = "";
if ("quote" in card_data) {
this.quote = card_data.quote;
}
this.meta = [];
if ("meta" in card_data) {
if (Array.isArray(card_data.meta)) {
this.meta = card_data.meta;
} else {
this.meta = card_data.meta.split(" ");
}
}
this.hero = false;
if (this.abilities.length > 0) {
if (this.abilities[0] === "hero") {
this.hero = true;
this.abilities.splice(0, 1);
}
for (let x of this.abilities) {
let ab = ability_dict[x];
if ("placed" in ab) this.placed.push(ab.placed);
if ("removed" in ab) this.removed.push(ab.removed);
if ("activated" in ab) this.activated.push(ab.activated);
}
}
if (this.row === "leader")
this.desc_name = "Leader Ability";
else if (this.abilities.length > 0) {
this.desc_name = ability_dict[this.abilities[this.abilities.length - 1]].name;
if (this.abilities.length > 1)
this.desc_name += " / " + ability_dict[this.abilities[this.abilities.length - 2]].name;
} else if (this.row === "agile" || this.row === "agile_cr")
this.desc_name = "Agile Close / Ranged";
else if (this.row === "agile_cs")
this.desc_name = "Agile Close / Siege";
else if (this.row === "agile_rs")
this.desc_name = "Agile Ranged / Siege";
else if (this.row === "agile_crs")
this.desc_name = "Agile Close / Ranged / Siege";
else if (this.hero)
this.desc_name = "Hero";
else
this.desc_name = "";
this.desc = this.row.includes("agile") ? "<p><b>Agile:</b> " + ability_dict[this.row].description + "</p>" : "";
for (let i = this.abilities.length - 1; i >= 0; --i) {
let abi_name = (ability_dict[this.abilities[i]].name ? ability_dict[this.abilities[i]].name : "Leader Ability");
let faction_abi_desc = "description_" + this.faction;
if (ability_dict[this.abilities[i]][faction_abi_desc]) {
// If there is a faction specific description/behaviour
this.desc += "<p><b>" + abi_name + " (" + factions[this.faction].name + "):</b> " + ability_dict[this.abilities[i]][faction_abi_desc] + "</p>";
} else {
this.desc += "<p><b>" + abi_name + ":</b> " + ability_dict[this.abilities[i]].description + "</p>";
}
}
// If Summon Avenger or Invoke card, give information about the card being summoned
if (this.abilities.includes("avenger") && this.target) {
let target = card_dict[this.target];
this.desc += "<p>Summons <b>" + target["name"] + "</b> with strength " + target["strength"];
if (target["ability"].length > 0)
this.desc += " and abilities " + target["ability"].split(" ").map(a => ability_dict[a]["name"]).join(" / ");
this.desc += "</p>";
} else if (this.abilities.includes("muster") && this.target) {
let units = Object.keys(card_dict).filter(cid => card_dict[cid].target === this.target).map(cid => card_dict[cid]);
let units_summary = {};
units.forEach(function (u) {
let key = "<b>" + u.name + "</b> (str: " + u.strength + ")";
if (!(key in units_summary))
units_summary[key] = 0;
units_summary[key] = units_summary[key] + 1;
});
this.desc += "<p><u>Summons</u> " + Object.keys(units_summary).map(t => units_summary[t] + " * " + t).join(", ");
} else if (this.abilities.includes("invoke") && this.target) {
let target = Object.keys(card_dict).filter(cid => card_dict[cid].target === this.target && cid !== this.key).map(cid => card_dict[cid]);
if (target.length > 0) {
target = target[0];
this.desc += "<p>Invokes <b>" + target["name"] + "</b> with strength " + target["strength"];
if (target["ability"].length > 0)
this.desc += " and abilities " + target["ability"].split(" ").map(a => ability_dict[a]["name"]).join(" / ");
this.desc += "</p>";
}
} else if ((this.abilities.includes("berserker") || this.abilities.includes("monster_toussaint")) && this.target) {
let target = card_dict[this.target];
this.desc += "<p>Turns into <b>" + target["name"] + "</b> with strength " + target["strength"];
if (target["ability"].length > 0)
this.desc += " and abilities " + target["ability"].split(" ").map(a => ability_dict[a]["name"]).join(" / ");
this.desc += "</p>";
}
if (this.hero)
this.desc += "<p><b>Hero:</b> " + ability_dict["hero"].description + "</p>";
this.elem = this.createCardElem(this);
}
// Returns the identifier for this type of card
getId() {
return this.key;
}
// Sets and displays the current power of this card
setPower(n) {
if (this.key === "spe_decoy")
return;
let elem = this.elem.children[0].children[0];
if (n !== this.power) {
this.power = n;
elem.innerHTML = this.power;
}
elem.style.color = (n > this.basePower) ? "goldenrod" : (n < this.basePower) ? "red" : "";
if (this.temporaryPower)
elem.style.color = "green";
}
// Resets the power of this card to default
resetPower() {
this.setPower(this.basePower);
this.multiplier = 1;
}
// Automatically sends and translates this card to its apropriate row from the passed source
async autoplay(source) {
await board.toRow(this, source);
}
// Animates an ability effect
async animate(name, bFade = true, bExpand = true) {
if (!may_pass1 && playingOnline) await sleep(600);
var guia = {
"medic": "med",
"muster": "ally",
"morale": "moral",
"bond": "moral"
}
var temSom = new Array();
for (var x in guia) temSom[temSom.length] = x;
var literais = ["scorch", "spy", "horn", "shield", "lock", "seize", "knockback", "resilience", "curse", "immortal", "aerondight", "ambush", "necrophage", "comrade", "emissary", "invoke","monster_toussaint"];
var som = literais.indexOf(name) > -1 ? literais[literais.indexOf(name)] : temSom.indexOf(name) > -1 ? guia[name] : "";
if (som != "") tocar(som, false);
if (name === "scorch") {
return await this.scorch(name);
}
let anim = this.elem.children[this.elem.children.length - 1];
anim.style.backgroundImage = iconURL("anim_" + name);
await sleep(50);
if (bFade) fadeIn(anim, 300);
if (bExpand) anim.style.backgroundSize = "100% auto";
await sleep(300);
if (bExpand) anim.style.backgroundSize = "80% auto";
await sleep(1000);
if (bFade) fadeOut(anim, 300);
if (bExpand) anim.style.backgroundSize = "40% auto";
await sleep(300);
anim.style.backgroundImage = "";
}
// Animates the scorch effect
async scorch(name) {
let anim = this.elem.children[this.elem.children.length - 1];
anim.style.backgroundSize = "cover";
anim.style.backgroundImage = iconURL("anim_" + name);
await sleep(50);
fadeIn(anim, 300);
await sleep(1300);
fadeOut(anim, 300);
await sleep(300);
anim.style.backgroundSize = "";
anim.style.backgroundImage = "";
}
// Returns true if this is a combat card that is not a Hero
isUnit() {
return !this.hero && (this.row === "close" || this.row === "ranged" || this.row === "siege" || this.row.includes("agile"));
}
// Returns true if card is sent to a Row's special slot
isSpecial() {
return ["spe_horn", "spe_mardroeme", "spe_sign_quen", "spe_sign_yrden", "spe_toussaint_wine", "spe_lyria_rivia_morale", "spe_wyvern_shield", "spe_mantlet", "spe_garrison", "spe_dimeritium_shackles", "spe_curse"].includes(this.key);
}
// Compares by type then power then name
static compare(a, b) {
var dif = factionRank(a) - factionRank(b);
if (dif !== 0)
return dif;
// Muster/Bond cards
if (a.target && b.target && a.target === b.target) {
if (a.id && b.id)
return Number(a.id) - Number(b.id);
if (a.key && b.key)
return a.key.localeCompare(b.key);
}
dif = a.basePower - b.basePower;
if (dif && dif !== 0)
return dif;
return a.name.localeCompare(b.name);
function factionRank(c) {
return c.faction === "special" ? -2 : (c.faction === "weather") ? -1 : 0;
}
}
getPlayableRows() {
if (this.row.includes("agile")) {
return board.getAgileRows(this, this.holder);
} else if (this.isSpecial()) {
return this.getAllRows();
}
return [board.getRow(this, this.row, this.holder)];
}
// Creates an HTML element based on the card's properties
createCardElem(card) {
let elem = document.createElement("div");
elem.style.backgroundImage = smallURL(card.faction + "_" + card.filename);
elem.classList.add("card");
elem.addEventListener("click", () => ui.selectCard(card), false);
if (card.row === "leader")
return elem;
let power = document.createElement("div");
elem.appendChild(power);
let bg;
if (card.hero) {
bg = "power_hero";
elem.classList.add("hero");
} else if (card.faction === "weather") {
bg = "power_" + card.abilities[0];
} else if (card.faction === "special") {
let str = card.abilities[0];
if (str === "shield_c" || str === "shield_r" || str === "shield_s")
str = "shield";
bg = "power_" + str;
elem.classList.add("special");
} else {
bg = "power_normal";
}
power.style.backgroundImage = iconURL(bg);
let row = document.createElement("div");
elem.appendChild(row);
if (card.row === "close" || card.row === "ranged" || card.row === "siege" || card.row.includes("agile")) {
let num = document.createElement("div");
num.appendChild(document.createTextNode(card.basePower));
num.classList.add("center");
power.appendChild(num);
row.style.backgroundImage = iconURL("card_row_" + card.row);
}
let abi = document.createElement("div");
elem.appendChild(abi);
if (card.faction !== "special" && card.faction !== "weather" && card.abilities.length > 0) {
let str = card.abilities[card.abilities.length - 1];
if (str === "cerys")
str = "muster";
if (str.startsWith("avenger"))
str = "avenger";
if (str === "scorch_c" || str == "scorch_r" || str === "scorch_s")
str = "scorch_combat";
if (str === "shield_c" || str == "shield_r" || str === "shield_s")
str = "shield";
abi.style.backgroundImage = iconURL("card_ability_" + str);
} else if (card.row.includes("agile"))
abi.style.backgroundImage = iconURL("card_ability_" + card.row);
// For cards with 2 abilities
if (card.abilities.length > 1) {
let abi2 = document.createElement("div");
abi2.classList.add("card-ability-2");
elem.appendChild(abi2);
let str = card.abilities[card.abilities.length - 2];
if (str === "cerys")
str = "muster";
if (str.startsWith("avenger"))
str = "avenger";
if (str === "scorch_c" || str == "scorch_r" || str === "scorch_s")
str = "scorch_combat";
if (str === "shield_c" || str == "shield_r" || str === "shield_s")
str = "shield";
abi2.style.backgroundImage = iconURL("card_ability_" + str);
}
elem.appendChild(document.createElement("div")); // animation overlay
return elem;
}
// Indicates whether or not the abilities of this card are locked
isLocked() {
return this.locked;
}
isImmortal() {
return this.abilities.includes("immortal");
}
}
function passBreak() {
clearInterval(timer2);
load_pass = load_passT;
may_pass2 = "";
document.getElementById("pass-button").innerHTML = original;
}
function passStart(input) {
if (may_pass1 && may_pass2 == "") {
may_pass2 = input;
ui.passLoad();
timer2 = setInterval(function () {
ui.passLoad();
}, 750);
}
}
var original = "Pass";
var fileira_clicavel = null;
const load_passT = 3;
var cache_notif = ["op-leader"];
var load_pass = load_passT,
may_pass1 = false,
may_pass2 = "",
may_pass3 = true,
fimU = false,
carta_c = false,
hover_row = true,
timer2, lCard;
// Handles notifications and client interration with menus
class UI {
constructor() {
this.carousels = [];
this.notif_elem = document.getElementById("notification-bar");
this.preview = document.getElementsByClassName("card-preview")[0];
this.previewCard = null;
this.lastRow = null;
this.underRearrangement = false;
this.underCardPowerEdit = false;
this.arrangementMoves = 0;
if (!isMobile()) {
document.getElementById("pass-button").addEventListener("mousedown", function (e) {
if (e.button == 0) {
passStart("mouse");
may_pass3 = false;
} else if (may_pass2 == "mouse") passBreak();
});
document.getElementById("pass-button").addEventListener("mouseup", () => {
if (may_pass2 == "mouse") passBreak();
}, false);
document.getElementById("pass-button").addEventListener("mouseout", () => {
if (may_pass2 == "mouse") passBreak();
}, false);
window.addEventListener("keydown", function (e) {
switch (e.keyCode) {
case 81:
e.preventDefault();
try {
ui.cancel();
} catch (err) { }
break;
case 32:
if (may_pass3) passStart("keyboard");
break;
}
});
window.addEventListener("keyup", function (e) {
if (e.keyCode == 32 && may_pass1) {
may_pass3 = true;
if (may_pass2 == "keyboard") passBreak();
}
});
} else document.getElementById("pass-button").addEventListener("click", function (e) {
if (game.isPvP()) {
game.currPlayer.passRound();
} else {
player_me.passRound();
}
});
document.getElementById("click-background").addEventListener("click", () => ui.cancel(), false);
this.youtube;
this.ytActive;
this.toggleMusic_elem = document.getElementById("toggle-music");
this.toggleMusic_elem.classList.add("fade");
this.toggleMusic_elem.addEventListener("click", () => this.toggleMusic(), false);
document.getElementById("arrangementWindow-button").addEventListener("click", () => {
this.updateArrangementCounter(0);
this.underRearrangement = false;
game.currPlayer.endTurn();
}, false);
this.helper = new HelperBox();
}
passLoad() {
load_pass--;
if (load_pass == -1) {
document.getElementById("pass-button").innerHTML = original;
load_pass = load_passT;
if (game.isPvP()) {
game.currPlayer.passRound();
} else {
player_me.passRound();
}
passBreak();
} else document.getElementById("pass-button").innerHTML = load_pass + 1;
}
// Enables or disables client interration
enablePlayer(enable) {
// Player vs player
if (game.isPvP()) {
document.getElementsByTagName("main")[0].classList.remove("noclick");
} else {
let main = document.getElementsByTagName("main")[0].classList;
if (enable) main.remove("noclick");
else main.add("noclick");
}
}
// Initializes the youtube background music object
initYouTube() {
this.youtube = new YT.Player('youtube', {
videoId: "UE9fPWy1_o4",
playerVars: {
"autoplay": 1,
"controls": 0,
"loop": 1,
"playlist": "UE9fPWy1_o4",
"rel": 0,
"version": 3,
"modestbranding": 1
},
events: {
'onStateChange': initButton
}
});
function initButton() {
if (ui.ytActive !== undefined)
return;
ui.ytActive = true;
ui.youtube.playVideo();
let timer = setInterval(() => {
if (ui.youtube.getPlayerState() !== YT.PlayerState.PLAYING)
ui.youtube.playVideo();
else {
clearInterval(timer);
ui.toggleMusic_elem.classList.remove("fade");
}
}, 500);
}
}
// Called when client toggles the music
toggleMusic() {
if (this.youtube.getPlayerState() !== YT.PlayerState.PLAYING) iniciarMusica();
else {
this.youtube.pauseVideo();
this.toggleMusic_elem.classList.add("fade");
}
}
// Enables or disables backgorund music
setYouTubeEnabled(enable) {
if (this.ytActive === enable)
return;
if (enable && !this.mute)
ui.youtube.playVideo();
else
ui.youtube.pauseVideo();
this.ytActive = enable;
}
// Called when the player selects a selectable card
async selectCard(card) {
let row = this.lastRow;
let pCard = this.previewCard;
if (this.underRearrangement) {
this.showPreviewVisuals(card);
return;
}
if (this.underCardPowerEdit) {
if (this.previewCard == null) {
this.showPreviewVisuals(card);
ui.editCardPower(card);
}
return;
}
if (card === pCard)
return;
if (pCard === null || card.holder.hand.cards.includes(card)) {
this.setSelectable(null, false);
this.showPreview(card);
} else if (pCard.abilities.includes("decoy")) {
this.hidePreview(card);
this.enablePlayer(false);
card.decoyTarget = true;
board.toHand(card, row);
await board.moveTo(pCard, row, pCard.holder.hand);
await pCard.holder.endTurn();
} else if (pCard.abilities.includes("alzur_maker")) {
this.hidePreview(card);
this.enablePlayer(false);
await board.toGrave(card, row);
let target = new Card(ability_dict["alzur_maker"].target, card_dict[ability_dict["alzur_maker"].target], card.holder);
//target.removed.push(() => setTimeout(() => target.holder.grave.removeCard(target), 1001));
await board.addCardToRow(target, target.row, card.holder);
await pCard.holder.endTurn();
}
}
// Called when the player selects a selectable CardContainer
async selectRow(row, isSpecial = false) {
this.lastRow = row;
if (this.underRearrangement) {
if (this.previewCard !== null) {
if (row !== this.previewCard.currentLocation) {
board.moveToNoEffects(this.previewCard, row, this.previewCard.currentLocation);
this.updateArrangementCounter(this.arrangementMoves - 1);
}
this.preview.classList.add("hide");
let holder = this.previewCard.holder;
this.previewCard = null;
this.lastRow = null;
if (this.arrangementMoves < 1) {
this.underRearrangement = false;
ui.helper.hide();
await holder.endTurn();
}
}
return;
}
if (this.underCardPowerEdit)
return;
if (this.previewCard === null) {
if (isSpecial)
await ui.viewCardsInContainer(row.special);
else
await ui.viewCardsInContainer(row);
return;
}
if (this.previewCard.key === "spe_decoy" || this.previewCard.abilities.includes("alzur_maker"))
return;
if (this.previewCard.abilities.includes("decoy") && row.cards.filter(c => c.isUnit()).length > 0)
return; // If a unit can be selected, we cannot select the whole row
let card = this.previewCard;
let holder = card.holder;
this.hidePreview();
this.enablePlayer(false);
if (card.faction === "special" && card.abilities.includes("scorch")) {
this.hidePreview();
if (game.scorchCancelled)
return;
await ability_dict["scorch"].activated(card);
} else if (card.faction === "special" && card.abilities.includes("cintra_slaughter")) {
this.hidePreview();
await ability_dict["cintra_slaughter"].activated(card);
} else if (card.faction === "special" && card.abilities.includes("seize")) {
this.hidePreview();
await ability_dict[card.abilities.at(-1)].activated(card);
} else if (card.faction === "special" && card.abilities.includes("knockback")) {
this.hidePreview();
await ability_dict[card.abilities.at(-1)].activated(card, row);
} else if (card.key === "spe_decoy" || card.abilities.includes("alzur_maker")) {
return;
} else if (card.abilities.includes("decoy") && row.cards.filter(c => c.isUnit()).length > 0) {
return; // If a unit can be selected, we cannot select the whole row
} else if (card.abilities.includes("anna_henrietta_duchess")) {
this.hidePreview(card);
this.enablePlayer(false);
let horn = row.special.cards.filter(c => c.abilities.includes("horn"))[0];
if (horn)
await board.toGrave(horn, row);
} else if (card.key === "spe_lyria_rivia_morale") {
await board.moveTo(card, row);
} else if (card.abilities.includes("meve_princess") || card.abilities.includes("carlo_varese")) {
this.hidePreview(card);
this.enablePlayer(false);
if (game.scorchCancelled)
return;
await row.scorch();
} else if (card.abilities.includes("cyrus_hemmelfart")) {
this.hidePreview(card);
this.enablePlayer(false);
let new_card = new Card("spe_dimeritium_shackles", card_dict["spe_dimeritium_shackles"], card.holder);
await board.moveTo(new_card, row);
} else if (card.faction === "special" && card.abilities.includes("bank")) {
this.hidePreview();
await ability_dict["bank"].activated(card);
} else if (card.faction === "special" && card.abilities.includes("skellige_fleet")) {
this.hidePreview();
await ability_dict["skellige_fleet"].activated(card);
} else if (card.faction === "special" && card.abilities.includes("royal_decree")) {
this.hidePreview();
await ability_dict["royal_decree"].activated(card);
} else {
await board.moveTo(card, row, card.holder.hand);
}
await holder.endTurn();
}
// Called when the client cancels out of a card-preview
cancel() {
if (!fimU) {
fimU = true;
tocar("discard", false);
lCard = null;
exibindo_lider = false;
carta_c = false;
this.hidePreview();
}
}
// Displays a card preview then enables and highlights potential card destinations
showPreview(card) {
fimU = false;
tocar("explaining", false);
this.showPreviewVisuals(card);
this.setSelectable(card, true);
document.getElementById("click-background").classList.remove("noclick");
}
// Sets up the graphics and description for a card preview
showPreviewVisuals(card) {
this.previewCard = card;
this.preview.classList.remove("hide");
getPreviewElem(this.preview.getElementsByClassName("card-lg")[0], card)
this.preview.getElementsByClassName("card-lg")[0].addEventListener("mousedown", function () {
if (fileira_clicavel !== null && may_act_card) {
ui.selectRow(fileira_clicavel);
may_act_card = false;
fileira_clicavel = null;
}
});
this.preview.getElementsByClassName("card-lg")[0].addEventListener("mouseup", function () {
may_act_card = true;
});
let desc_elem = this.preview.getElementsByClassName("card-description")[0];
this.setDescription(card, desc_elem);
}
// Hides the card preview then disables and removes highlighting from card destinations
hidePreview() {
document.getElementById("click-background").classList.add("noclick");
player_me.hand.cards.forEach(c => c.elem.classList.remove("noclick"));
this.preview.classList.add("hide");
this.setSelectable(null, false);
this.previewCard = null;
this.lastRow = null;
}
// Sets up description window for a card
setDescription(card, desc) {
if (card.hero || card.row.includes("agile") || card.abilities.length > 0 || card.faction === "faction") {
desc.classList.remove("hide");
let str = card.row.includes("agile") ? card.row : "";
if (card.abilities.length)
str = card.abilities[card.abilities.length - 1];
if (str === "cerys")
str = "muster";
if (str.startsWith("avenger"))
str = "avenger";
if (str === "scorch_c" || str == "scorch_r" || str === "scorch_s")
str = "scorch_combat";
if (str === "shield_c" || str == "shield_r" || str === "shield_s")
str = "shield";
if (card.faction === "faction" || card.abilities.length === 0 && card.row !== "agile")
desc.children[0].style.backgroundImage = "";
else if (card.row === "leader")
desc.children[0].style.backgroundImage = iconURL("deck_shield_" + card.faction);
else
desc.children[0].style.backgroundImage = iconURL("card_ability_" + str);
desc.children[1].innerHTML = card.desc_name;
desc.children[2].innerHTML = card.desc;
} else {
desc.classList.add("hide");
}
}
// Displayed a timed notification to the client
async notification(name, duration) {
var guia1 = {
"notif-nilfgaard-wins-draws": "Nilfgaard wins draws",
"notif-op-white-flame": "The opponent's leader cancel your opponent's Leader Ability",
"notif-op-leader": "Opponent uses leader",
"notif-me-first": "You will go first",
"notif-op-first": "Your opponent will go first",
"notif-me-coin": "You will go first",
"notif-op-coin": "Your opponent will go first",
"notif-round-start": "Round Start",
"notif-me-pass": "Round passed",
"notif-op-pass": "Your opponent has passed",
"notif-win-round": "You won the round!",
"notif-lose-round": "Your opponent won the round",
"notif-draw-round": "The round ended in a draw",
"notif-me-turn": "Your turn!",
"notif-op-turn": "Opponent's turn",
"notif-north": "Northern Realms faction ability triggered - North draws an additional card.",
"notif-monsters": "Monsters faction ability triggered - monsters retake one card to their hand",
"notif-scoiatael": "Opponent used the Scoia'tael faction perk to go first.",
"notif-skellige-op": "Opponent Skellige Ability Triggered!",
"notif-skellige-me": "Skellige Ability Triggered!",
"notif-witcher_universe": "Witcher Universe used its faction ability and skipped a turn",
"notif-toussaint": "Toussaint faction ability triggered - Toussaint draws an additional card.",
"notif-toussaint-decoy-cancelled": "Toussaint Leader ability used - Decoy ability cancelled for the rest of the round.",
"notif-lyria_rivia": "Lyria & Rivia ability used - Morale Boost effect applied to a row.",
"notif-meve_white_queen": "Lyria & Rivia leader allows both players to restore 2 units when using the medic ability.",
"notif-north-scorch-cancelled": "Northern Realms Leader ability used - Scorch ability cancelled for the rest of the round.",
"notif-zerrikania": "Zerrikania ability used - Unit restored from discard pile.",
"notif-redania": "Redania used its faction ability and skipped a turn",
"notif-velen": "Velen ability triggered: Player will draw a card"
}
var guia2 = {
"me-pass": "pass",
"win-round": "round_win",
"lose-round": "round_lose",
"me-turn": "turn_me",
"op-turn": "turn_op",
"op-leader": "turn_op",
"op-white-flame": "turn_op",
"nilfgaard-wins-draws": "turn_op"
}
var temSom = new Array();
for (var x in guia2) temSom[temSom.length] = x;
var som = temSom.indexOf(name) > -1 ? guia2[name] : name == "round-start" && game.roundHistory.length == 0 ? "round1_start" : "";
if (som != "") tocar(som, false);
this.notif_elem.children[0].id = "notif-" + name;
this.notif_elem.children[0].style.backgroundImage = name == "op-leader" ? "url(img/icons/notif_" + player_op.deck.faction + ".png)" : "";
var caracteres = guia1[this.notif_elem.children[0].id].length;
var palavras = guia1[this.notif_elem.children[0].id].split(" ").length;
duration = parseInt(0.7454878 * Math.max(parseInt((1e3 / 17) * caracteres), parseInt((6e4 / 300) * palavras)) + 211.653152) + 1;
const fadeSpeed = 150;
fadeIn(this.notif_elem, fadeSpeed);
var ch = playingOnline && duration < 1000 & cache_notif.indexOf(name) == -1 ? 800 : 0;
cache_notif[cache_notif.length] = name;
duration += ch;
let d = new Date().getTime();
fadeOut(this.notif_elem, fadeSpeed, duration - fadeSpeed - 50); // Removing some delay to avoid weird behaviours if the fadeOut starts late and has not ended when the next fadeIn starts
await sleep(duration);
}
// Displays a cancellable Carousel for a single card
async viewCard(card, action) {
if (card === null) return;
if (lCard !== card.name) {
lCard = card.name;
let container = new CardContainer();
container.cards.push(card);
await this.viewCardsInContainer(container, action);
}
}
// Displays a cancellable Carousel for all cards in a container
async viewCardsInContainer(container, action) {
action = action ? action : function () {
return this.cancel();
};
await this.queueCarousel(container, 1, action, () => true, false, true);
}
// Displays a Carousel menu of filtered container items that match the predicate.
// Suspends gameplay until the Carousel is closed. Automatically picks random card if activated for AI player
async queueCarousel(container, count, action, predicate, bSort, bQuit, title) {
if (game.currPlayer && game.currPlayer.controller instanceof ControllerAI) {
for (let i = 0; i < count; ++i) {
let cards = container.cards.reduce((a, c, i) => !predicate || predicate(c) ? a.concat([i]) : a, []);
await action(container, cards[randomInt(cards.length)]);
}
return;
}
let carousel = new Carousel(container, count, action, predicate, bSort, bQuit, title);
if (Carousel.curr === undefined || Carousel.curr === null) {
carousel.start();
} else {
this.carousels.push(carousel);
return;
}
await sleepUntil(() => this.carousels.length === 0 && !Carousel.curr, 100);
}
// Starts the next queued Carousel
quitCarousel() {
if (this.carousels.length > 0) {
this.carousels.shift().start();
}
}
// Displays a custom confirmation menu
async popup(yesName, yes, noName, no, title, description) {
let p = new Popup(yesName, yes, noName, no, title, description);
await sleepUntil(() => !Popup.curr);
return p.choice;
}
// Displays a custom menu to select a number
async numberPopup(v, min, max, callback, title, description) {
let p = new NumberValuePopup(v, min, max, callback, title, description);
await sleepUntil(() => !NumberValuePopup.curr);
return parseInt(p.value);
}
async startDeckSorter(cards, player, action, title, bottomAllowed = false) {
let deckSorter = new DeckSorter(cards, player, action, title, bottomAllowed);
deckSorter.start();
await sleepUntil(() => deckSorter.isCompleted(), 100);
}
// Enables or disables selection and highlighting of rows specific to the card
setSelectable(card, enable) {
if (!enable) {
for (let row of board.row) {
row.elem.classList.remove("row-selectable");
row.elem.classList.remove("noclick");
row.special.elem.classList.remove("row-selectable");
row.special.elem.classList.remove("noclick");
alteraClicavel(row, false);
for (let card of row.cards) {
card.elem.classList.add("noclick");
}
}
weather.elem.classList.remove("row-selectable");
weather.elem.classList.remove("noclick");
alteraClicavel(weather, false);
return;
}
if (card.faction === "weather") {
for (let row of board.row) {
row.elem.classList.add("noclick");
row.special.elem.classList.add("noclick");
}
weather.elem.classList.add("row-selectable");
carta_c = true;
document.getElementById("field-op").addEventListener("click", function () {
cancelaClima();
});
document.getElementById("field-me").addEventListener("click", function () {
cancelaClima();
});
alteraClicavel(weather, true);
return;
}
weather.elem.classList.add("noclick");
// Affects all board
if (card.faction === "special" && card.abilities.includes("scorch")) {
for (let r of board.row) {
if (r.isShielded() || game.scorchCancelled) {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
} else {
r.elem.classList.add("row-selectable");
r.special.elem.classList.add("row-selectable");
alteraClicavel(r, true);
}
}
return;
}
// Affects only own side of board
if (card.faction === "special" && (card.abilities.includes("cintra_slaughter") || card.abilities.includes("bank") || card.abilities.includes("skellige_fleet") || card.abilities.includes("royal_decree"))) {
for (let i = 0; i < 6; i++) {
let r = board.row[i];
if ((!game.isPvP() && i > 2) || (game.isPvP() && ((card.holder.tag === player_me.tag && i > 2) || (card.holder.tag === player_op.tag && i < 3)))) {
r.elem.classList.add("row-selectable");
r.special.elem.classList.add("row-selectable");
alteraClicavel(r, true);
}
}
return;
}
// Affects enemy side of the board
// Affects only opponent melee and ranged row
if (card.faction === "special" && card.abilities.includes("knockback")) {
let rows = [1, 2];
if (game.isPvP() && card.holder.tag === player_op.tag) {
rows = [3, 4];
}
for (i of rows) {
let r = board.row[i];
if (!r.isShielded()) {
r.elem.classList.add("row-selectable");
r.special.elem.classList.add("row-selectable");
alteraClicavel(r, true);
}
}
return;
}
// Affects only opponent melee row
if (card.faction === "special" && card.abilities.includes("seize")) {
let r = board.row[2];
if (game.isPvP() && card.holder.tag === player_op.tag) {
r = board.row[3];
}
if (!r.isShielded()) {
r.elem.classList.add("row-selectable");
r.special.elem.classList.add("row-selectable");
alteraClicavel(r, true);
}
return;
}
//Affects only own rows that are available
if (card.isSpecial()) {
for (let i = 0; i < 6; i++) {
let r = board.row[i];
//Affects OP side
if (card.abilities.includes("lock")) {
if (r.special.containsCardByKey(card.key) || r.isShielded() || (!game.isPvP() && i > 2) ||
(game.isPvP() && ((card.holder.tag === player_me.tag && i > 2) || (card.holder.tag === player_op.tag && i < 3)))) {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
} else {
r.special.elem.classList.add("row-selectable");
fileira_clicavel = null;
}
} else if (card.abilities.includes("curse")) {
//Affects both sides
if (r.isShielded()) {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
} else {
r.special.elem.classList.add("row-selectable");
fileira_clicavel = null;
}
} else if (card.abilities.includes("shield_c") || card.abilities.includes("shield_r") || card.abilities.includes("shield_s")) {
if (((card.abilities.includes("shield_c") && i == 3) || (card.abilities.includes("shield_r") && i == 4) || (card.abilities.includes("shield_s") && i == 5)) &&
(!game.isPvP() || (game.isPvP() && card.holder.tag === player_me.tag))) {
r.special.elem.classList.add("row-selectable");
fileira_clicavel = null;
} else if ((game.isPvP() && card.holder.tag === player_op.tag && ((card.abilities.includes("shield_c") && i == 2) || (card.abilities.includes("shield_r") && i == 1) || (card.abilities.includes("shield_s") && i == 0)))) { } else {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
}
} else {
// Affects own side - Toussaint Wine does not affect siege row
if (r.special.containsCardByKey(card.key) || (!game.isPvP() && ((card.abilities.includes("toussaint_wine") && i == 5) || i < 3)) ||
(game.isPvP() && ((card.holder.tag === player_me.tag && ((card.abilities.includes("toussaint_wine") && i == 5) || i < 3)) ||
(card.holder.tag === player_op.tag && ((card.abilities.includes("toussaint_wine") && i == 0) || i > 2))))) {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
} else {
r.special.elem.classList.add("row-selectable");
fileira_clicavel = null;
}
}
}
return;
}
if (card.abilities.includes("decoy") || card.abilities.includes("alzur_maker")) {
for (let i = 0; i < 6; i++) {
let r = board.row[i];
let units = r.cards.filter(c => c.isUnit());
if ((card.key === "spe_decoy" && units.length === 0) || (card.abilities.includes("decoy") && game.decoyCancelled) || (!game.isPvP() && i < 3) ||
(game.isPvP() && ((card.holder.tag === player_me.tag && i < 3) || (card.holder.tag === player_op.tag && i > 2)))) {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
r.elem.classList.remove("card-selectable");
} else {
// For unit cards with Decoy ability, filter by the appropriate row
if (card.abilities.includes("decoy") && card.row.length > 0) {
if (((!game.isPvP() || (game.isPvP() && card.holder.tag === player_me.tag)) && ((i === 3 && ["close", "agile", "agile_cr", "agile_cs", "agile_crs"].includes(card.row)) || (i === 4 && ["ranged", "agile", "agile_cr", "agile_rs", "agile_crs"].includes(card.row)) || (i === 5 && ["siege", "agile_cs", "agile_rs", "agile_crs"].includes(card.row)))) ||
(game.isPvP() && card.holder.tag === player_op.tag && ((i === 2 && ["close", "agile", "agile_cr", "agile_cs", "agile_crs"].includes(card.row)) || (i === 1 && ["ranged", "agile", "agile_cr", "agile_rs", "agile_crs"].includes(card.row)) || (i === 0 && ["siege", "agile_cs", "agile_rs", "agile_crs"].includes(card.row))))) {
r.elem.classList.add("row-selectable");
// Row is selectable if it contains no unit to select, in order to play the unit itself without its effect
if (units.length === 0)
r.elem.classList.remove("noclick");
alteraClicavel(r, true);
units.forEach(c => c.elem.classList.remove("noclick"));
} else {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
r.elem.classList.remove("card-selectable");
}
} else {
r.elem.classList.add("row-selectable");
alteraClicavel(r, true);
units.forEach(c => c.elem.classList.remove("noclick"));
}
}
}
return;
}
if (card.abilities.includes("anna_henrietta_duchess")) {
let rows = [0, 1, 2];
if (game.isPvP() && card.holder.tag === player_op.tag) {
rows = [3, 4, 5];
}
for (i of rows) {
let r = board.row[i];
if (r.effects.horn > 0) {
r.elem.classList.add("row-selectable");
alteraClicavel(r, true);
} else {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
r.elem.classList.remove("card-selectable");
}
}
return;
}
// Target only enemy rows
if (card.abilities.includes("meve_princess") || card.abilities.includes("carlo_varese")) {
let rows = [0, 1, 2];
if (game.isPvP() && card.holder.tag === player_op.tag) {
rows = [3, 4, 5];
}
for (i of rows) {
let r = board.row[i];
if (r.isShielded() || !r.canBeScorched()) {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
r.elem.classList.remove("card-selectable");
} else {
r.elem.classList.add("row-selectable");
alteraClicavel(r, true);
}
}
return;
}
// Play special card on any opponent row, provided it doesn't already have one
if (card.abilities.includes("cyrus_hemmelfart")) {
let rows = [0, 1, 2];
if (game.isPvP() && card.holder.tag === player_op.tag) {
rows = [3, 4, 5];
}
for (i of rows) {
let r = board.row[i];
if (r.containsCardByKey("spe_dimeritium_shackles") || r.isShielded()) {
r.elem.classList.add("noclick");
r.special.elem.classList.add("noclick");
r.elem.classList.remove("card-selectable");
} else {
r.elem.classList.add("row-selectable");
alteraClicavel(r, true);
}
}
return;
}
let currRows = card.row.includes("agile") ? board.getAgileRows(card, card.holder) : [board.getRow(card, card.row, card.holder)];
for (let i = 0; i < 6; i++) {
let row = board.row[i];
if (currRows.includes(row)) {
row.elem.classList.add("row-selectable");
if (!card.row.includes("agile")) alteraClicavel(row, true);
else fileira_clicavel = null;
} else if (card.abilities.includes("ambush") && currRows.includes(row.getOppositeRow())) {
// Ambush cards are symetrical, they affect available rows on each side of the battlefield
row.elem.classList.add("row-selectable");
} else {
row.elem.classList.add("noclick");
}
}
}
// Make UI enter a mode where the player can re-arrange the cards on one side of the board (the one associated to the provided player)
// In this mode, when a player clicks a card, it displays the preview
// When the player clicks a row when a preview is displayed, move the card there (unless it was already there) and decrease remaining moves by one
enableBoardRearrangement(player,moves) {
if (this.underRearrangement)
return;
this.underRearrangement = true;
this.updateArrangementCounter(moves);
this.setSelectable(null, false);
let rows = (player === player_op) ? board.row.slice(0, 3) : board.row.slice(3);
player.hand.cards.forEach(c => c.elem.classList.add("noclick"));
for (let i = 0; i < 6; i++) {
let row = board.row[i];
//Valid side of board
if (rows.includes(row) ) {
row.elem.classList.remove("noclick");
if (row.cards.length > 0) {
alteraClicavel(row, true);
row.cards.filter(c => c.key !== "spe_decoy").forEach(c => c.elem.classList.remove("noclick"));
}
// Other side of board
} else {
row.elem.classList.add("noclick");
row.cards.forEach(c => c.elem.classList.add("noclick"));
}
}
ui.helper.showMessage("Select cards on the board to re-arrange.");
this.enablePlayer(true);
}
updateArrangementCounter(cnt) {
if (cnt > 0) {
this.arrangementMoves = cnt;
document.getElementById("arrangementWindow").classList.remove("hide");
document.getElementById("arrangementWindow-counter").innerText = cnt;
} else {
this.arrangementMoves = 0;
document.getElementById("arrangementWindow").classList.add("hide");
}
}
enableCardPowerEdit(player) {
if (this.underCardPowerEdit || !player.capabilities["cardEdit"] || player.capabilities["cardEdit"] < 1)
return;
this.underCardPowerEdit = true;
this.setSelectable(null, false);
let rows = (player === player_op) ? board.row.slice(0, 3) : board.row.slice(3);
player.hand.cards.forEach(c => c.elem.classList.add("noclick"));
for (let i = 0; i < 6; i++) {
let row = board.row[i];
//Valid side of board
if (rows.includes(row)) {
row.elem.classList.remove("noclick");
if (row.cards.length > 0) {
alteraClicavel(row, true);
row.cards.filter(c => c.hero || c.isUnit()).forEach(c => c.elem.classList.remove("noclick"));
}
// Other side of board
} else {
row.elem.classList.add("noclick");
row.cards.forEach(c => c.elem.classList.add("noclick"));
}
}
// Prevent the end of turn while selecting cards
player.endturn_action = async () => {
player.endturn_action = null;
}
ui.helper.showMessage("Select a card on your side of the board.");
this.enablePlayer(true);
}
async editCardPower(card) {
ui.helper.hide();
let newValue = await this.numberPopup(card.power, 0, 999, null, "Select a new base power", "Select the new base power (before other effects) for the selected card. Currently: " + String(card.power));
if (!card.originalBasePower)
card.originalBasePower = card.basePower;
card.basePower = newValue;
card.temporaryPower = true;
card.holder.capabilities["cardEdit"] -= 1;
this.underCardPowerEdit = false;
this.preview.classList.add("hide");
this.previewCard = null;
card.holder.hand.cards.forEach(c => c.elem.classList.remove("noclick"));
card.holder.endTurn(true);
}
}
var fimC = false;
// Displays up to 5 cards for the client to cycle through and select to perform an action
// Clicking the middle card performs the action on that card "count" times
// Clicking adejacent cards shifts the menu to focus on that card
class Carousel {
constructor(container, count, action, predicate, bSort, bExit = false, title) {
if (count <= 0 || !container || !action || container.cards.length === 0)
return;
this.container = container;
this.count = count;
this.action = action ? action : () => this.cancel();
this.predicate = predicate;
this.bSort = bSort;
this.indices = [];
this.index = 0;
this.bExit = bExit;
this.title = title;
this.cancelled = false;
this.selection = [];
if (!Carousel.elem) {
Carousel.elem = document.getElementById("carousel");
Carousel.elem.children[0].addEventListener("click", () => Carousel.curr.cancel(), false);
window.addEventListener("keydown", function (e) {
if (e.keyCode == 81) {
e.preventDefault();
try {
Carousel.curr.cancel();
} catch (err) { }
}
});
}
this.elem = Carousel.elem;
document.getElementsByTagName("main")[0].classList.remove("noclick");
this.elem.children[0].classList.remove("noclick");
this.previews = this.elem.getElementsByClassName("card-lg");
this.desc = this.elem.getElementsByClassName("card-description")[0];
this.title_elem = this.elem.children[2];
}
// Initializes the current Carousel
start() {
if (!this.elem)
return;
this.indices = this.container.cards.reduce((a, c, i) => (!this.predicate || this.predicate(c)) ? a.concat([i]) : a, []);
if (this.indices.length <= 0)
return this.exit();
if (this.bSort)
this.indices.sort((a, b) => Card.compare(this.container.cards[a], this.container.cards[b]));
this.update();
Carousel.setCurrent(this);
if (this.title) {
this.title_elem.innerHTML = this.title;
this.title_elem.classList.remove("hide");
} else {
this.title_elem.classList.add("hide");
}
this.elem.classList.remove("hide");
ui.enablePlayer(true);
tocar("explaining", false);
fimC = false;
setTimeout(function () {
var label = document.getElementById("carousel_label");
if (label.innerText.indexOf("redraw") > -1 && label.className.indexOf("hide") == -1) tocar("game_start", false);
}, 50);
}
// Called by the client to cycle cards displayed by n
shift(event, n) {
try {
(event || window.event).stopPropagation();
} catch (err) { }
tocar("card", false);
this.index = Math.max(0, Math.min(this.indices.length - 1, this.index + n));
this.update();
}
// Called by client to perform action on the middle card in focus
async select(event) {
try {
(event || window.event).stopPropagation();
} catch (err) { }
// In case of multiple selections, we only do action if not already selected
if (this.selection.indexOf(this.indices[this.index]) < 0) {
var label = document.getElementById("carousel_label");
if (label.innerText.indexOf("redraw") > -1 && label.className.indexOf("hide") == -1) {
tocar("redraw", false);
} else {
this.selection.push(this.indices[this.index]);
}
--this.count;
if (this.isLastSelection())
this.elem.classList.add("hide");
if (this.count <= 0)
ui.enablePlayer(false);
// For redraw, we run the action right away
if (label.innerText.indexOf("redraw") > -1 && label.className.indexOf("hide") == -1)
await this.action(this.container, this.indices[this.index]);
if (this.isLastSelection() && !this.cancelled) {
this.exit();
this.selection.map(async s => await this.action(this.container, s));
this.selection = [];
return;
}
} else {
// If already selected, remove from selection
this.selection.splice(this.selection.indexOf(this.indices[this.index]), 1);
this.count++;
}
this.update();
}
// Called by client to exit out of the current Carousel if allowed. Enables player interraction.
cancel() {
if (!fimC) {
fimC = true;
tocar("discard", false);
lCard = null;
exibindo_lider = false;
if (this.bExit) {
this.cancelled = true;
this.exit();
}
ui.enablePlayer(true);
}
}
// Returns true if there are no more cards to view or select
isLastSelection() {
return this.count <= 0 || this.indices.length === 0;
}
// Updates the visuals of the current selection of cards
update() {
this.indices = this.container.cards.reduce((a, c, i) => (!this.predicate || this.predicate(c)) ? a.concat([i]) : a, []);
if (this.index >= this.indices.length)
this.index = this.indices.length - 1;
for (let i = 0; i < this.previews.length; i++) {
let curr = this.index - 2 + i;
if (curr >= 0 && curr < this.indices.length) {
let card = this.container.cards[this.indices[curr]];
getPreviewElem(this.previews[i], card);
this.previews[i].classList.remove("hide");
this.previews[i].classList.remove("noclick");
if (this.selection.indexOf(this.indices[curr]) >= 0)
this.previews[i].classList.add("selection");
else
this.previews[i].classList.remove("selection");
} else {
this.previews[i].style.backgroundImage = "";
this.previews[i].classList.add("hide");
this.previews[i].classList.add("noclick");
this.previews[i].classList.remove("selection");
}
}
ui.setDescription(this.container.cards[this.indices[this.index]], this.desc);
}
// Clears and quits the current carousel
exit() {
for (let x of this.previews) {
x.style.backgroundImage = "";
x.classList.remove("selection");
}
this.elem.classList.add("hide");
Carousel.clearCurrent();
ui.quitCarousel();
}
// Statically sets the current carousel
static setCurrent(curr) {
this.curr = curr;
}
// Statically clears the current carousel
static clearCurrent() {
this.curr = null;
}
}
// Custom confirmation windows
class Popup {
constructor(yesName, yes, noName, no, header, description) {
this.yes = yes ? yes : () => { };
this.no = no ? no : () => { };
this.choice = false;
this.elem = document.getElementById("popup");
let main = this.elem.children[0];
main.children[0].innerHTML = header ? header : "";
main.children[1].innerHTML = description ? description : "";
main.children[2].children[0].innerHTML = (yesName) ? yesName : "Yes";
main.children[2].children[1].innerHTML = (noName) ? noName : "No";
this.elem.classList.remove("hide");
Popup.setCurrent(this);
ui.enablePlayer(true);
}
// Sets this as the current popup window
static setCurrent(curr) {
this.curr = curr;
}
// Unsets this as the current popup window
static clearCurrent() {
this.curr = null;
}
// Called when client selects the positive aciton
selectYes() {
this.clear();
this.choice = true;
this.yes(this);
return true;
}
// Called when client selects the negative option
selectNo() {
this.clear();
this.choice = false;
this.no(this);
return false;
}
// Clears the popup and diables player interraction
clear() {
ui.enablePlayer(false);
this.elem.classList.add("hide");
Popup.clearCurrent();
}
}
class NumberValuePopup {
constructor(value, min=0, max=999, callback, header, description) {
this.callback = callback ? callback : () => { };
this.choice = false;
this.elem = document.getElementById("number-popup");
let main = this.elem.children[0];
main.children[0].innerHTML = header ? header : "";
main.children[1].innerHTML = description ? description : "";
this.numberElem = document.getElementById("number-popup-value");
main.children[2].children[1].innerHTML = "Done";
this.numberElem.setAttribute("value", value);
this.numberElem.value = value;
this.numberElem.setAttribute("min", min);
this.numberElem.setAttribute("max", max);
this.elem.classList.remove("hide");
NumberValuePopup.setCurrent(this);
ui.enablePlayer(true);
}
// Sets this as the current popup window
static setCurrent(curr) {
this.curr = curr;
}
// Unsets this as the current popup window
static clearCurrent() {
this.curr = null;
}
// Called when client confirms value
done() {
this.value = this.numberElem.value;
if (!isNaN(this.value) && this.value != "") {
this.clear();
this.callback(this);
return true;
}
return false;
}
// Clears the popup and diables player interraction
clear() {
ui.enablePlayer(false);
this.elem.classList.add("hide");
NumberValuePopup.clearCurrent();
}
}
class HelperBox {
constructor() {
this.text = "";
this.elem = document.getElementById("helper-box");
document.getElementById("help-box-close").addEventListener("click", () => this.hide(), false);
HelperBox.setCurrent(this);
}
// Sets this as the current helper box
static setCurrent(curr) {
this.curr = curr;
}
// Unsets this as the current helper box
static clearCurrent() {
this.curr = null;
}
showMessage(text, timer=0) {
this.text = text;
document.getElementById("helper-box-message").innerText = text;
this.elem.classList.remove("hide");
if (timer > 0) {
timer = Math.min(timer, 30);
var opacity = 1; // Initial opacity
var h = this;
setTimeout(function () {
// Fading out - 1s
var interval = setInterval(function () {
if (opacity > 0) {
opacity -= 0.1;
h.elem.style.opacity = opacity;
} else {
clearInterval(interval); // Stop the interval when opacity reaches 0
h.hide(); // Hide the element
h.elem.style.opacity = 1;
}
}, 100);
},timer*1000);
}
}
hide() {
this.elem.classList.add("hide");
}
}
var carta_selecionada = null;
// Screen used to customize, import and export deck contents
class DeckMaker {
constructor() {
this.elem = document.getElementById("deck-customization");
this.bank_elem = document.getElementById("card-bank");
this.deck_elem = document.getElementById("card-deck");
this.leader_elem = document.getElementById("card-leader");
this.leader_elem.children[1].addEventListener("click", () => this.selectLeader(), false);
this.leader_elem.children[1].addEventListener("mouseover", function () {
tocar("card", false);
this.style.boxShadow = "0 0 1.5vw #6d5210"
});
this.leader_elem.children[1].addEventListener("mouseout", function () {
this.style.boxShadow = "0 0 0 #6d5210"
});
this.faction = "realms";
this.setFaction(this.faction, true);
let start_deck = JSON.parse(JSON.stringify(premade_deck[0]));
start_deck.cards = start_deck.cards.map(c => ({
index: c[0],
count: c[1]
}));
this.me_deck_title = start_deck.title;
this.setLeader(start_deck.leader);
this.makeBank(this.faction, start_deck.cards);
this.start_op_deck;
this.me_deck_index = 0;
this.op_deck_index = 0;
this.change_elem = document.getElementById("change-faction");
this.change_elem.addEventListener("click", () => this.selectFaction(), false);
document.getElementById("select-deck").addEventListener("click", () => this.selectDeck(), false);
document.getElementById("select-op-deck").addEventListener("click", () => this.selectOPDeck(), false);
document.getElementById("download-deck").addEventListener("click", () => this.downloadDeck(), false);
document.getElementById("add-file").addEventListener("change", () => this.uploadDeck(), false);
document.getElementById("start-game").addEventListener("click", () => this.startNewGame(1), false);
document.getElementById("start-ai-game").addEventListener("click", () => this.startNewGame(2), false);
document.getElementById("start-pvp-game").addEventListener("click", () => this.startNewGame(3), false);
window.addEventListener("keydown", function (e) {
if (document.getElementById("deck-customization").className.indexOf("hide") == -1) {
switch (e.keyCode) {
case 69:
try {
Carousel.curr.cancel();
} catch (err) { }
if (isLoaded && iniciou) dm.startNewGame();
break;
case 88:
dm.selectLeader();
break;
}
}
});
somCarta();
this.update();
}
// Called when client selects a deck faction. Clears previous cards and makes valid cards available.
async setFaction(faction_name, silent) {
if (!silent && this.faction === faction_name)
return false;
if (!silent) {
tocar("warning", false);
if (!confirm("Changing factions will clear the current deck. Continue? ")) {
tocar("warning", false);
return false;
}
}
this.elem.getElementsByTagName("h1")[0].innerHTML = factions[faction_name].name;
this.elem.getElementsByTagName("h1")[0].style.backgroundImage = iconURL("deck_shield_" + faction_name);
document.getElementById("faction-description").innerHTML = factions[faction_name].description;
this.leaders =
Object.keys(card_dict).map(cid => ({
card: card_dict[cid],
index: cid
}))
.filter(c => c.card.deck === faction_name && c.card.row === "leader");
if (!this.leader || this.faction !== faction_name) {
this.leader = this.leaders[0];
getPreviewElem(this.leader_elem.children[1], this.leader.card)
}
this.faction = faction_name;
setTimeout(function () {
somCarta();
}, 300);
return true;
}
// Called when client selects a leader for their deck
setLeader(index) {
this.leader = this.leaders.filter(l => l.index == index)[0];
getPreviewElem(this.leader_elem.children[1], this.leader.card)
}
// Constructs a bank of cards that can be used by the faction's deck.
// If a deck is provided, will not add cards to bank that are already in the deck.
makeBank(faction, deck) {
this.clear();
let cards = Object.keys(card_dict).map(cid => ({
card: card_dict[cid],
index: cid
})).filter(
p => (([faction, "neutral", "weather", "special"].includes(p.card.deck) ||
(["weather", "special"].includes(p.card.deck.split(" ")[0]) && p.card.deck.split(" ").includes(faction))) &&
p.card.row !== "leader" && !factions[faction].unavailableSpecials.includes(p.index)));
cards.sort(function (id1, id2) {
let a = card_dict[id1.index],
b = card_dict[id2.index];
let c1 = {
name: a.name,
basePower: -a.strength,
faction: a.deck.split(" ")[0] // Cleaning for faction specific special/weather cards
};
let c2 = {
name: b.name,
basePower: -b.strength,
faction: b.deck.split(" ")[0] // Cleaning for faction specific special/weather cards
};
return Card.compare(c1, c2);
});
let deckMap = {};
if (deck) {
for (let i of Object.keys(deck)) deckMap[deck[i].index] = deck[i].count;
}
cards.forEach(p => {
let count = deckMap[p.index] !== undefined ? Number(deckMap[p.index]) : 0;
this.makePreview(p.index, Number.parseInt(p.card.count) - count, this.bank_elem, this.bank,);
this.makePreview(p.index, count, this.deck_elem, this.deck);
});
}
// Creates HTML elements for the card previews
makePreview(index, num, container_elem, cards) {
let card_data = card_dict[index];
let elem = document.createElement("div");
elem.classList.add("card-lg");
elem = getPreviewElem(elem, card_data, num);
container_elem.appendChild(elem);
let bankID = {
index: index,
count: num,
elem: elem
};
let isBank = cards === this.bank;
cards.push(bankID);
let cardIndex = cards.length - 1;
elem.addEventListener("dblclick", () => this.select(cardIndex, isBank), false);
elem.addEventListener("mouseover", () => {
var aux = this;
carta_selecionada = function () {
aux.select(cardIndex, isBank);
}
}, false);
window.addEventListener("keydown", function (e) {
if (e.keyCode == 13 && carta_selecionada !== null) carta_selecionada();
});
// Right click allows to see more details about the selected card
elem.addEventListener('contextmenu', async (e) => {
e.preventDefault();
let container = new CardContainer();
container.cards = [new Card(index, card_data, null)];
try {
Carousel.curr.cancel();
} catch (err) { }
await ui.viewCardsInContainer(container);
}, false);
return bankID;
}
// Updates the card preview elements when any changes are made to the deck
update() {
for (let x of this.bank) {
if (x.count)
x.elem.classList.remove("hide");
else
x.elem.classList.add("hide");
}
let total = 0,
units = 0,
special = 0,
strength = 0,
hero = 0;
for (let x of this.deck) {
let card_data = card_dict[x.index];
if (x.count)
x.elem.classList.remove("hide");
else
x.elem.classList.add("hide");
total += x.count;
if (card_data.deck.startsWith("special") || card_data.deck.startsWith("weather")) {
special += x.count;
continue;
}
units += x.count;
strength += card_data.strength * x.count;
if (card_data.ability.split(" ").includes("hero"))
hero += x.count;
}
this.stats = {
total: total,
units: units,
special: special,
strength: strength,
hero: hero
};
this.updateStats();
}
// Updates and displays the statistics describing the cards currently in the deck
updateStats() {
let stats = document.getElementById("deck-stats");
stats.children[1].innerHTML = this.stats.total;
stats.children[3].innerHTML = this.stats.units + (this.stats.units < 22 ? "/22" : "");
stats.children[5].innerHTML = this.stats.special + "/10";
stats.children[7].innerHTML = this.stats.strength;
stats.children[9].innerHTML = this.stats.hero;
stats.children[3].style.color = this.stats.units < 22 ? "red" : "";
stats.children[5].style.color = (this.stats.special > 10) ? "red" : "";
}
// Opens a Carousel to allow the client to select a leader for their deck
selectLeader() {
let container = new CardContainer();
container.cards = this.leaders.map(c => {
let card = new Card(c.index, c.card, player_me);
card.data = c;
return card;
});
let index = this.leaders.indexOf(this.leader);
ui.queueCarousel(container, 1, (c, i) => {
let data = c.cards[i].data;
this.leader = data;
getPreviewElem(this.leader_elem.children[1], data.card);
}, () => true, false, true);
Carousel.curr.index = index;
Carousel.curr.update();
}
// Opens a Carousel to allow the client to select a faction for their deck
selectFaction() {
let container = new CardContainer();
container.cards = Object.keys(factions).map(f => {
return {
abilities: [f],
filename: f,
desc_name: factions[f].name,
desc: factions[f].description,
faction: "faction"
};
});
let index = container.cards.reduce((a, c, i) => c.filename === this.faction ? i : a, 0);
ui.queueCarousel(container, 1, (c, i) => {
let change = this.setFaction(c.cards[i].filename);
if (!change)
return;
this.makeBank(c.cards[i].filename);
this.update();
}, () => true, false, true);
Carousel.curr.index = index;
Carousel.curr.update();
}
// Called when client selects s a preview card. Moves it from bank to deck or vice-versa then updates;
select(index, isBank) {
carta_selecionada = null;
if (isBank) {
tocar("menu_buy", false);
this.add(index, this.deck);
this.remove(index, this.bank);
} else {
tocar("discard", false);
this.add(index, this.bank);
this.remove(index, this.deck);
}
this.update();
}
// Adds a card to container (Bank or deck)
add(index, cards) {
let id = cards[index];
id.elem.getElementsByClassName("card-count")[0].innerHTML = ++id.count;
id.elem.getElementsByClassName("card-count")[0].classList.remove("hide");
}
// Removes a card from container (bank or deck)
remove(index, cards) {
let id = cards[index];
id.elem.getElementsByClassName("card-count")[0].innerHTML = --id.count;
if (id.count === 0)
id.elem.getElementsByClassName("card-count")[0].classList.add("hide");
}
// Removes all elements in the bank and deck
clear() {
while (this.bank_elem.firstChild)
this.bank_elem.removeChild(this.bank_elem.firstChild);
while (this.deck_elem.firstChild)
this.deck_elem.removeChild(this.deck_elem.firstChild);
this.bank = [];
this.deck = [];
this.stats = {};
}
// Verifies current deck, creates the players and their decks, then starts a new game
// Modes:
// 1 - player VS AI
// 2 - AI vs AI
// 3 - player VS player (hotseat)
startNewGame(mode = 1) {
game.mode = mode;
openFullscreen();
let warning = "";
if (this.stats.units < 22)
warning += "Your deck must have at least 22 unit cards. \n";
if (this.stats.special > 10)
warning += "Your deck must have no more than 10 special cards. \n";
if (warning != "") {
return aviso(warning);
}
let me_deck = {
faction: this.faction,
leader: this.leader,
cards: this.deck.filter(x => x.count > 0),
title: this.me_deck_title
};
if (game.randomOPDeck || !this.start_op_deck) {
this.start_op_deck = JSON.parse(JSON.stringify(premade_deck[randomInt(Object.keys(premade_deck).length)]));
this.start_op_deck.cards = this.start_op_deck.cards.map(c => ({
index: c[0],
count: c[1]
}));
let leaders = Object.keys(card_dict).map(cid => {
return {
index: cid,
card: card_dict[cid]
};
}).filter(c => c.card.row === "leader" && c.card.deck === this.start_op_deck.faction);
this.start_op_deck.leader = leaders[randomInt(leaders.length)];
}
if (game.mode === 1) {
player_me = new Player(0, "Player 1", me_deck, false);
player_op = new Player(1, "Player 2", this.start_op_deck, true);
} else if (game.mode === 2) {
// AI vs AI
player_me = new Player(0, "Player 1", me_deck, true);
player_op = new Player(1, "Player 2", this.start_op_deck, true);
} else {
// PVP
player_me = new Player(0, "Player 1", me_deck, false);
player_op = new Player(1, "Player 2", this.start_op_deck, false);
}
this.elem.classList.add("hide");
tocar("game_opening", false);
game.startGame();
}
// Converts the current deck to a JSON string
deckToJSON() {
let obj = {
faction: this.faction,
leader: this.leader.index,
cards: this.deck.filter(x => x.count > 0).map(x => [x.index, x.count])
};
return JSON.stringify(obj);
}
// Select a premade deck
selectDeck() {
let container = new CardContainer();
container.cards = Object.values(premade_deck).map(d => {
let deck = d;
return {
abilities: [deck["faction"]],
name: card_dict[deck["leader"]]["name"],
row: "leader",
filename: card_dict[deck["leader"]]["filename"],
desc_name: deck["title"],
desc: "<p><b>Faction ability:</b> " + factions[deck["faction"]]["description"] + "</p><p><b>Leader ability:</b> " + ability_dict[card_dict[deck["leader"]]["ability"]].description + "</p><p><b>Deck description:</b> " + deck["description"],
faction: deck["faction"]
};
});
let index = container.cards.reduce((a, c, i) => c.faction === this.faction ? i : a, 0);
ui.queueCarousel(container, 1, (c, i) => {
this.me_deck_index = i;
this.setFaction(c.cards[i].faction, true);
this.deckFromJSON(premade_deck[i], false);
}, () => true, false, true);
Carousel.curr.index = this.me_deck_index;
Carousel.curr.update();
}
selectOPDeck() {
let container = new CardContainer();
// Adding first the option to select a random deck
container.cards = [{
abilities: [],
name: "Random deck",
row: "faction",
filename: "random",
desc_name: "Random deck",
desc: "A random deck from the pool that will change every game.",
faction: "faction"
}];
container.cards = container.cards.concat(Object.values(premade_deck).map(d => {
let deck = d;
return {
abilities: [deck["faction"]],
name: card_dict[deck["leader"]]["name"],
row: "leader",
filename: card_dict[deck["leader"]]["filename"],
desc_name: deck["title"],
desc: "<p><b>Faction ability:</b> " + factions[deck["faction"]]["description"] + "</p><p><b>Leader ability:</b> " + ability_dict[card_dict[deck["leader"]]["ability"]].description + "</p><p><b>Deck description:</b> " + deck["description"],
faction: deck["faction"]
};
}));
ui.queueCarousel(container, 1, (c, i) => {
this.op_deck_index = i;
if (i === 0) {
game.randomOPDeck = true;
document.getElementById("op-deck-name").innerHTML = "Random deck";
} else {
this.start_op_deck = JSON.parse(JSON.stringify(premade_deck[i - 1]));
this.start_op_deck.cards = this.start_op_deck.cards.map(c => ({
index: c[0],
count: c[1]
}));
this.start_op_deck.leader = {
index: this.start_op_deck.leader,
card: card_dict[this.start_op_deck.leader]
};
document.getElementById("op-deck-name").innerHTML = premade_deck[i - 1]["title"];
game.randomOPDeck = false;
}
}, () => true, false, true);
Carousel.curr.index = this.op_deck_index;
Carousel.curr.update();
}
// Called by the client to downlaod the current deck as a JSON file
downloadDeck() {
let json = this.deckToJSON();
let str = "data:text/json;charset=utf-8," + encodeURIComponent(json);
let hidden_elem = document.getElementById('download-json');
hidden_elem.href = str;
hidden_elem.download = "MyGwentDeck.json";
hidden_elem.click();
}
// Called by the client to upload a JSON file representing a new deck
uploadDeck() {
let files = document.getElementById("add-file").files;
if (files.length <= 0)
return false;
let fr = new FileReader();
fr.onload = e => {
try {
this.deckFromJSON(e.target.result, true);
} catch (e) {
aviso("Uploaded deck is not formatted correctly!");
}
}
fr.readAsText(files.item(0));
document.getElementById("add-file").value = "";
openFullscreen();
}
// Creates a deck from a JSON file's contents and sets that as the current deck
// Notifies client with warnings if the deck is invalid
deckFromJSON(json, parse) {
let deck;
if (parse) {
try {
deck = JSON.parse(json);
} catch (e) {
aviso("Uploaded deck is not parsable!");
return;
}
} else {
deck = JSON.parse(JSON.stringify(json));
}
let warning = "";
if (card_dict[deck.leader].row !== "leader")
warning += "'" + card_dict[deck.leader].name + "' is cannot be used as a leader\n";
if (deck.faction != card_dict[deck.leader].deck)
warning += "Leader '" + card_dict[deck.leader].name + "' doesn't match deck faction '" + deck.faction + "'.\n";
let cards = deck.cards.filter(c => {
let card = card_dict[c[0]];
if (!card) {
warning += "ID " + c[0] + " does not correspond to a card.\n";
return false
}
if (!([deck.faction, "neutral", "special", "weather"].includes(card.deck) ||
(["special", "weather"].includes(card.deck.split(" ")[0]) && card.deck.split(" ").includes(deck.faction)))) {
warning += "'" + card.name + "' cannot be used in a deck of faction type '" + deck.faction + "'\n";
return false;
}
if (card.count < c[1]) {
console.log(card);
warning += "Deck contains " + c[1] + "/" + card.count + " available " + card_dict[c[0]].name + " cards\n";
return false;
}
return true;
})
.map(c => ({
index: c[0],
count: Math.min(c[1], card_dict[c[0]].count)
}));
if (warning) {
tocar("warning", false);
if (!confirm(warning + "\n\n\Continue importing deck?")) {
tocar("warning", false);
return;
}
}
this.setFaction(deck.faction, true);
if (card_dict[deck.leader].row === "leader" && deck.faction === card_dict[deck.leader].deck) {
this.leader = this.leaders.filter(c => c.index === deck.leader)[0];
getPreviewElem(this.leader_elem.children[1], this.leader.card);
}
this.me_deck_title = deck.title;
this.makeBank(deck.faction, cards);
this.update();
}
}
class DeckSorter {
constructor(cards, player, action, title, bottomAllowed = false) {
if (!player || !cards || cards.length === 0)
return;
this.cards = cards;
this.player = player;
this.bottomAllowed = bottomAllowed;
this.action = action ? action : () => this.close();
this.title = title;
this.completed = false;
this.target_id = null;
if (!DeckSorter.elem) {
DeckSorter.elem = document.getElementById("deck-sorter");
}
this.elem = DeckSorter.elem;
document.getElementsByTagName("main")[0].classList.remove("noclick");
this.elem.children[0].classList.remove("noclick");
this.title_elem = document.getElementById("deck-sorter-title");
}
// Initializes the current DeckSorter
start() {
if (!this.elem || !this.cards | this.cards.length == 0)
return;
let bankRow = document.getElementById("drop-bank");
let deckTop = document.getElementById("drop-deck-top");
let deckBottom = document.getElementById("drop-deck-bottom");
//Remove old card boxes if any
this.elem.querySelectorAll('.drop-box').forEach(oi => {
oi.remove()
});
this.elem.querySelectorAll('.drop-susp').forEach(oi => {
oi.remove()
});
// Add a "..." at the beginning of the bottom row
let box = null;
if (this.bottomAllowed) {
deckBottom.style.visibility = 'visible';
box = document.createElement("div");
box.classList.add("drop-susp");
box.innerHTML = "&centerdot;&centerdot;&centerdot;";
deckBottom.appendChild(box);
} else {
deckBottom.style.visibility = 'hidden';
}
for (var i = 0; i < this.cards.length; i++) {
// Create the bank box
box = document.createElement("div");
box.classList.add("drop-box");
bankRow.appendChild(box);
// Create the sortable item
let item = document.createElement("div");
item.classList.add("drop-item");
item.setAttribute("draggable", "true");
item.setAttribute("id", "item-" + i.toString());
item.setAttribute("data-pos", i.toString());
item.setAttribute("data-card-key", this.cards[i].key);
box.appendChild(item);
// Add card container
let cardContainer = document.createElement("div");
cardContainer.classList.add("card-lg");
// Add the card content
getPreviewElem(cardContainer, this.cards[i]);
cardContainer.classList.remove("hide");
cardContainer.classList.remove("noclick");
item.appendChild(cardContainer);
// Prevent all children elements from being draggable instead of the parent node
let children = item.querySelectorAll('*');
children.forEach(n => {
n.setAttribute("draggable", "false")
});
item.addEventListener('dragstart', dragStart);
item.addEventListener('dragend', dragEnd);
// Add a box in the Top row
box = document.createElement("div");
box.classList.add("drop-box");
deckTop.appendChild(box);
// Add a box in the Bottom row
if (this.bottomAllowed) {
box = document.createElement("div");
box.classList.add("drop-box");
deckBottom.appendChild(box);
}
}
// Add a "..." at the end of the bottom row
box = document.createElement("div");
box.classList.add("drop-susp");
box.innerHTML = "&centerdot;&centerdot;&centerdot;";
deckTop.appendChild(box);
// Add done button
box = document.createElement("div");
box.classList.add("drop-susp");
//box.innerHTML = "DONE";
bankRow.appendChild(box);
let submitBtn = document.createElement("button");
submitBtn.setAttribute("id", "drop-submit");
submitBtn.disabled = true;
submitBtn.innerText = "DONE";
submitBtn.addEventListener('click', submitDeckSorter);
box.appendChild(submitBtn);
function dragStart(e) {
//e.dataTransfer.setData('text/plain', e.target.id);
if (e.target) {
DeckSorter.curr.target_id = e.target.id;
setTimeout(() => {
e.target.classList.add('hide');
}, 0);
}
}
function dragEnd(e) {
// get the draggable element
//let id = e.dataTransfer.getData('text/plain');
let id = DeckSorter.curr.target_id;
let draggable = document.getElementById(id);
// display the draggable element - by default
if (draggable) {
draggable.classList.remove('hide');
}
}
/* drop targets */
let boxes = document.querySelectorAll('.drop-box');
boxes.forEach(box => {
box.addEventListener('dragenter', dragEnter)
box.addEventListener('dragover', dragOver);
box.addEventListener('dragleave', dragLeave);
box.addEventListener('drop', drop);
});
function dragEnter(e) {
e.preventDefault();
if (e.target) {
e.target.classList.add('drag-over');
}
}
function dragOver(e) {
e.preventDefault();
if (e.target) {
e.target.classList.add('drag-over');
}
}
function dragLeave(e) {
if (e.target) {
e.target.classList.remove('drag-over');
}
}
function drop(e) {
if (e.target) {
e.target.classList.remove('drag-over');
/*let children = e.target.querySelectorAll('*');
children.forEach(n => { n.setAttribute("draggable", "false") });*/
//let id = e.dataTransfer.getData('text/plain');
let id = DeckSorter.curr.target_id;
let draggable = document.getElementById(id);
// get the draggable element
if (e.target.querySelectorAll('.drop-item').length == 0 && e.target.classList.contains("drop-box")) {
// add it to the drop target
e.target.appendChild(draggable);
}
// display the draggable element
if (draggable) {
draggable.classList.remove('hide');
}
//Enable/disable submit button if all cards have been sorted
var bank = document.getElementById("drop-bank").querySelectorAll('.drop-item');
document.getElementById("drop-submit").disabled = (bank.length > 0);
}
}
function submitDeckSorter(e) {
var bank = document.getElementById("drop-bank").querySelectorAll('.drop-item');
var deckTop = document.getElementById("drop-deck-top").querySelectorAll('.drop-item');
var deckBottom = document.getElementById("drop-deck-bottom").querySelectorAll('.drop-item');
// If bank has been sorted, ready to go
if (bank.length == 0 && (deckTop.length + deckBottom.length) == DeckSorter.curr.cards.length) {
DeckSorter.curr.applyChanges();
DeckSorter.curr.exit();
}
}
if (this.title) {
this.title_elem.innerHTML = this.title;
this.title_elem.classList.remove("hide");
} else {
this.title_elem.classList.add("hide");
}
DeckSorter.setCurrent(this);
this.elem.classList.remove("hide");
ui.enablePlayer(true);
}
// Closes the deck sorter interface
exit() {
let bankRow = document.getElementById("drop-bank");
let deckTop = document.getElementById("drop-deck-top");
let deckBottom = document.getElementById("drop-deck-bottom");
//Remove old card boxes if any
bankRow.querySelectorAll('.drop-item').forEach(oi => {
oi.remove()
});
deckTop.querySelectorAll('.drop-susp').forEach(oi => {
oi.remove()
});
deckBottom.querySelectorAll('.drop-susp').forEach(oi => {
oi.remove()
});
this.elem.classList.add("hide");
DeckSorter.clearCurrent();
ui.enablePlayer(true);
}
isCompleted() {
return this.completed;
}
applyChanges() {
let deckTop = Array.from(document.getElementById("drop-deck-top").querySelectorAll('.drop-item'));
let deckBottom = Array.from(document.getElementById("drop-deck-bottom").querySelectorAll('.drop-item'));
// Init new deck without the first X cards being sorted
let newdeck = this.player.deck.cards.slice(this.cards.length);
let head = [];
//Adding cards at the top
let cards_bank = this.cards;
deckTop.forEach(function (el) {
let pos = parseInt(el.dataset.pos);
head.push(cards_bank[pos]);
});
newdeck.unshift(...head);
//Adding cards at the bottom
deckBottom.forEach(function (el) {
let pos = parseInt(el.dataset.pos);
newdeck.push(cards_bank[pos]);
});
// set new deck order
this.player.deck.cards = newdeck;
this.completed = true;
}
// Statically sets the current carousel
static setCurrent(curr) {
this.curr = curr;
}
// Statically clears the current carousel
static clearCurrent() {
this.curr = null;
}
}
// Translates a card between two containers
async function translateTo(card, container_source, container_dest) {
if (!container_dest || !container_source)
return;
// When solo vs AI, do not display the translations between hand and deck for the AI
if (game.mode == 1 && (container_dest === player_op.hand && container_source === player_op.deck) || (container_dest === player_op.deck && container_source === player_op.hand) )
return;
let elem = card.elem;
let source = !container_source ? card.elem : getSourceElem(card, container_source, container_dest);
let dest = getDestinationElem(card, container_source, container_dest);
if (!isInDocument(elem))
source.appendChild(elem);
let x = trueOffsetLeft(dest) - trueOffsetLeft(elem) + dest.offsetWidth / 2 - elem.offsetWidth;
let y = trueOffsetTop(dest) - trueOffsetTop(elem) + dest.offsetHeight / 2 - elem.offsetHeight / 2;
if (container_dest instanceof Row && container_dest.cards.length !== 0 && !card.isSpecial()) {
x += (container_dest.getSortedIndex(card) === container_dest.cards.length) ? elem.offsetWidth / 2 : -elem.offsetWidth / 2;
}
if (card.holder.controller instanceof ControllerAI)
x += elem.offsetWidth / 2;
if (container_source instanceof Row && container_dest instanceof Grave && !card.isSpecial()) {
let mid = trueOffset(container_source.elem, true) + container_source.elem.offsetWidth / 2;
x += trueOffset(elem, true) - mid;
}
if (container_source instanceof Row && container_dest === player_me.hand)
y *= 7 / 8;
await translate(elem, x, y);
// Returns true if the element is visible in the viewport
function isInDocument(elem) {
return elem.getBoundingClientRect().width !== 0;
}
// Returns the true offset of a nested element in the viewport
function trueOffset(elem, left) {
let total = 0;
let curr = elem;
while (curr) {
total += (left ? curr.offsetLeft : curr.offsetTop);
curr = curr.parentElement;
}
return total;
}
function trueOffsetLeft(elem) {
return trueOffset(elem, true);
}
function trueOffsetTop(elem) {
return trueOffset(elem, false);
}
// Returns the source container's element to transition from
function getSourceElem(card, source, dest) {
if (source instanceof HandAI)
return source.hidden_elem;
if (source instanceof Deck)
return source.elem.children[source.elem.children.length - 2];
return source.elem;
}
// Returns the destination container's element to transition to
function getDestinationElem(card, source, dest) {
if (dest instanceof HandAI)
return dest.hidden_elem;
if (card.isSpecial() && dest instanceof Row)
//return dest.elem_special;
return dest.special.elem;
if (dest instanceof Row || dest instanceof Hand || dest instanceof Weather) {
if (dest.cards.length === 0)
return dest.elem;
let index = dest.getSortedIndex(card);
let dcard = dest.cards[index === dest.cards.length ? index - 1 : index];
return dcard.elem;
}
return dest.elem;
}
}
// Translates an element by x from the left and y from the top
async function translate(elem, x, y) {
let vw100 = 100 / document.getElementById("dimensions").offsetWidth;
x *= vw100;
y *= vw100;
elem.style.transform = "translate(" + x + "vw, " + y + "vw)";
let margin = elem.style.marginLeft;
elem.style.marginRight = -elem.offsetWidth * vw100 + "vw";
elem.style.marginLeft = "";
await sleep(499);
elem.style.transform = "";
elem.style.position = "";
elem.style.marginLeft = margin;
elem.style.marginRight = margin;
}
// Fades out an element until hidden over the duration
async function fadeOut(elem, duration, delay) {
await fade(false, elem, duration, delay);
}
// Fades in an element until opaque over the duration
async function fadeIn(elem, duration, delay) {
await fade(true, elem, duration, delay);
}
// Fades an element over a duration
async function fade(fadeIn, elem, dur, delay) {
if (delay)
await sleep(delay);
let op = fadeIn ? 0.1 : 1;
elem.style.opacity = op;
elem.style.filter = "alpha(opacity=" + (op * 100) + ")";
if (fadeIn)
elem.classList.remove("hide");
let timer = setInterval(async function () {
op += (fadeIn ? 0.1 : -0.1);
if (op >= 1) {
clearInterval(timer);
return;
} else if (op <= 0.1) {
elem.classList.add("hide");
elem.style.opacity = "";
elem.style.filter = "";
clearInterval(timer);
return;
}
elem.style.opacity = op;
elem.style.filter = "alpha(opacity=" + (op * 100) + ")";
}, dur / 10);
}
// Get Image paths
function iconURL(name, ext = "png") {
return imgURL("icons/" + name, ext);
}
function largeURL(name, ext = "jpg") {
return imgURL("lg/" + name, ext)
}
function smallURL(name, ext = "jpg") {
return imgURL("sm/" + name, ext);
}
function bottomBgURL() {
return imgURL("icons/gwent_bottom_bg", "png");
}
function imgURL(path, ext) {
return "url('img/" + path + "." + ext + "')";
}
function getPreviewElem(elem, card, nb = 0) {
// Cleaning existing child nodes
while (elem.hasChildNodes()) {
elem.removeChild(elem.lastChild);
}
elem.classList.remove("hero");
elem.classList.remove("faction");
let c_abilities = "";
if ("ability" in card) {
c_abilities = card.ability.split(" ");
} else {
c_abilities = card.abilities;
}
let faction = ""
if ("deck" in card) {
faction = card.deck.split(" ")[0]; // Cleaning in case of special/weather cards being faction specific
} else {
faction = card.faction;
}
elem.style.backgroundImage = smallURL(faction + "_" + card.filename);
if (faction == "faction") {
elem.classList.add("faction");
return elem;
}
if (card.row != "leader" && !faction.startsWith("special") && faction != "neutral" && !faction.startsWith("weather")) {
let factionBand = document.createElement("div");
factionBand.style.backgroundImage = iconURL("faction-band-" + faction);
factionBand.classList.add("card-large-faction-band");
elem.appendChild(factionBand);
}
let cardbg = document.createElement("div");
cardbg.style.backgroundImage = bottomBgURL();
cardbg.classList.add("card-large-bg");
elem.appendChild(cardbg);
let card_name = document.createElement("div");
card_name.classList.add("card-large-name");
card_name.appendChild(document.createTextNode(card.name));
elem.appendChild(card_name);
if ("quote" in card) {
let quote_elem = document.createElement("div");
quote_elem.classList.add("card-large-quote");
quote_elem.appendChild(document.createTextNode(card.quote));
elem.appendChild(quote_elem);
}
// Nothing else to display for leaders
if (card.row === "leader") {
return elem;
}
let count = document.createElement("div");
count.innerHTML = nb;
count.classList.add("card-count");
cardbg.appendChild(count);
if (nb == 0) {
count.classList.add("hide");
}
let power = document.createElement("div");
power.classList.add("card-large-power");
elem.appendChild(power);
let bg;
if (c_abilities[0] === "hero" || ("hero" in card && card.hero)) {
bg = "power_hero";
elem.classList.add("hero");
} else if (faction.startsWith("weather")) {
bg = "power_" + c_abilities[0];
} else if (faction.startsWith("special")) {
let str = c_abilities[0];
if (str === "shield_c" || str == "shield_r" || str === "shield_s")
str = "shield";
bg = "power_" + str;
elem.classList.add("special");
} else {
bg = "power_normal";
}
power.style.backgroundImage = iconURL(bg);
let row = document.createElement("div");
row.classList.add("card-large-row");
elem.appendChild(row);
if (card.row === "close" || card.row === "ranged" || card.row === "siege" || card.row.includes("agile")) {
let num = document.createElement("div");
if ("strength" in card) {
num.appendChild(document.createTextNode(card.strength));
} else {
num.appendChild(document.createTextNode(card.basePower));
}
num.classList.add("card-large-power-strength");
power.appendChild(num);
row.style.backgroundImage = iconURL("card_row_" + card.row);
}
if (c_abilities.length > 0) {
let abi = document.createElement("div");
abi.classList.add("card-large-ability");
elem.appendChild(abi);
if (!faction.startsWith("special") && !faction.startsWith("weather") && c_abilities.length > 0 && c_abilities[c_abilities.length - 1] != "hero") {
let str = c_abilities[c_abilities.length - 1];
if (str === "cerys")
str = "muster";
if (str.startsWith("avenger"))
str = "avenger";
if (str === "scorch_c" || str == "scorch_r" || str === "scorch_s")
str = "scorch_combat";
if (str === "shield_c" || str == "shield_r" || str === "shield_s")
str = "shield";
abi.style.backgroundImage = iconURL("card_ability_" + str);
} else if (card.row.includes("agile")) {
abi.style.backgroundImage = iconURL("card_ability_" + "agile");
}
// In case of double abilities
if ((c_abilities.length > 1 && !(c_abilities[0] === "hero")) || (c_abilities.length > 2 && c_abilities[0] === "hero")) {
let abi2 = document.createElement("div");
abi2.classList.add("card-large-ability-2");
elem.appendChild(abi2);
let str = c_abilities[c_abilities.length - 2];
if (str === "cerys")
str = "muster";
if (str.startsWith("avenger"))
str = "avenger";
if (str === "scorch_c" || str == "scorch_r" || str === "scorch_s")
str = "scorch_combat";
if (str === "shield_c" || str == "shield_r" || str === "shield_s")
str = "shield";
abi2.style.backgroundImage = iconURL("card_ability_" + str);
}
}
return elem;
}
// Returns true if n is an Number
function isNumber(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}
// Returns true if s is a String
function isString(s) {
return typeof (s) === 'string' || s instanceof String;
}
// Returns a random integer in the range [0,n)
function randomInt(n) {
return Math.floor(Math.random() * n);
}
// Pauses execution until the passed number of milliseconds as expired
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Suspends execution until the predicate condition is met, checking every ms milliseconds
function sleepUntil(predicate, ms) {
return new Promise(resolve => {
let timer = setInterval(function () {
if (predicate()) {
clearInterval(timer);
resolve();
}
}, ms)
});
}
// Initializes the interractive YouTube object
function onYouTubeIframeAPIReady() {
ui.initYouTube();
}
/*----------------------------------------------------*/
var ui = new UI();
var board = new Board();
var weather = new Weather();
var game = new Game();
var player_me, player_op;
ui.enablePlayer(false);
let dm = new DeckMaker();
document.addEventListener('contextmenu', event => event.preventDefault());
document.onkeydown = function (e) {
if (e.keyCode != 123) {
if (document.getElementById("carousel").className != "hide") {
switch (e.keyCode) {
case 13:
Carousel.curr.select(e);
break;
case 37:
Carousel.curr.shift(e, -1);
break;
case 39:
Carousel.curr.shift(e, 1);
break;
}
} else if (document.getElementsByClassName("hover_un")[0].innerText.length > 1) {
switch (e.keyCode) {
case 69:
Popup.curr.selectYes();
break;
case 81:
Popup.curr.selectNo();
break;
}
} else if (!iniciou && isLoaded && (e.keyCode == 13 || e.keyCode == 69)) inicio();
} else return false;
}
var elem_principal = document.documentElement;
function openFullscreen() {
try {
if (elem_principal.requestFullscreen) elem_principal.requestFullscreen();
else if (elem_principal.webkitRequestFullscreen) elem_principal.webkitRequestFullscreen();
else if (elem_principal.msRequestFullscreen) elem_principal.msRequestFullscreen();
window.screen.orientation.lock("landscape");
} catch (err) { }
}
var lastSound = "";
function tocar(arquivo, pararMusica) {
if (arquivo != lastSound && arquivo != "") {
var s = new Audio("sfx/" + arquivo + ".mp3");
if (pararMusica && ui.youtube && ui.youtube.getPlayerState() === YT.PlayerState.PLAYING) {
ui.youtube.pauseVideo();
ui.toggleMusic_elem.classList.add("fade");
}
lastSound = arquivo;
if (iniciou) s.play();
setTimeout(function () {
lastSound = "";
}, 50);
}
}
function aviso(texto) {
tocar("warning", false);
setTimeout(function () {
alert(texto);
document.getElementById("start-game").blur();
tocar("warning", false);
}, 150);
}
function somCarta() {
var classes = ["card", "card-lg"];
for (var i = 0; i < classes.length; i++) {
var cartas = document.getElementsByClassName(classes[i]);
for (var j = 0; j < cartas.length; j++) {
if (cartas[j].id != "no_sound" && cartas[j].id != "no_hover") cartas[j].addEventListener("mouseover", function () {
tocar("card", false);
});
}
}
var tags = ["label", "a", "button"];
for (var i = 0; i < tags.length; i++) {
var rec = document.getElementsByTagName(tags[i]);
for (var j = 0; j < rec.length; j++) rec[j].addEventListener("mouseover", function () {
tocar("card", false);
});
}
var ids = ["pass-button", "toggle-music"];
for (var i = 0; i < ids.length; i++) document.getElementById(ids[i]).addEventListener("mouseover", function () {
tocar("card", false);
});
}
function cartaNaLinha(id, carta) {
if (id.charAt(0) == "f") {
if (!carta.hero) {
if (carta.name != "Decoy") {
var linha = parseInt(id.charAt(1));
if (linha == 1 || linha == 6) tocar("common3", false);
else if (linha == 2 || linha == 5) tocar("common2", false);
else if (linha == 3 || linha == 4) tocar("common1", false);
} else tocar("menu_buy", false);
} else tocar("hero", false);
}
}
function inicio() {
var classe = document.getElementsByClassName("abs");
for (var i = 0; i < classe.length; i++) classe[i].style.display = "none";
iniciou = true;
tocar("menu_opening", false);
openFullscreen();
iniciarMusica();
}
function iniciarMusica() {
try {
if (ui.youtube.getPlayerState() !== YT.PlayerState.PLAYING) {
ui.youtube.playVideo();
ui.toggleMusic_elem.classList.remove("fade");
}
} catch (err) { }
}
function cancelaClima() {
if (carta_c) {
ui.cancel();
hover_row = false;
setTimeout(function () {
hover_row = true;
}, 100);
}
}
var iniciou = false,
isLoaded = false;
var playingOnline;
window.onload = function () {
dimensionar();
playingOnline = window.location.href == "https://randompianist.github.io/gwent-classic-v2.0/";
document.getElementById("load_text").style.display = "none";
document.getElementById("button_start").style.display = "inline-block";
document.getElementById("deck-customization").style.display = "";
document.getElementById("toggle-music").style.display = "";
document.getElementsByTagName("main")[0].style.display = "";
document.getElementById("button_start").addEventListener("click", function () {
inicio();
});
isLoaded = true;
}
window.onresize = function () {
dimensionar();
}
function dimensionar() {
var prop = window.innerWidth / window.innerHeight;
var dim = document.getElementById("dimensions").offsetHeight;
document.getElementById("very_start_bg2").style.height = prop < 1.8 ? (parseInt(dim * 0.94) - 8) + "px" : "";
document.getElementById("very_start").style.paddingTop = "";
document.getElementById("very_start").style.paddingTop = parseInt(
(document.getElementById("very_start_bg2").offsetHeight - document.getElementById("very_start").offsetHeight) / 2
) + "px";
}
setTimeout(dimensionar(), 300);
function isMobile() {
if (navigator.userAgentData)
return navigator.userAgentData.mobile;
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}