summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoël Simoneau <simoneaujoel@gmail.com>2024-12-23 11:42:40 -0500
committerJoël Simoneau <simoneaujoel@gmail.com>2024-12-23 11:42:40 -0500
commit74ecddba5a3c053b1dd57f0f4e330dca1dec0406 (patch)
treebe408030d3d9cd43a1a29ba30ea114321443794e
parent2716d69eea22eb82bdf79cc0eb14c08b920c72c7 (diff)
downloadvijayanagara-74ecddba5a3c053b1dd57f0f4e330dca1dec0406.tar.gz
wip attack
-rw-r--r--images/die_gold_pips.svg37
-rw-r--r--play.css73
-rw-r--r--play.html13
-rw-r--r--play.js35
-rw-r--r--rules.js273
5 files changed, 416 insertions, 15 deletions
diff --git a/images/die_gold_pips.svg b/images/die_gold_pips.svg
new file mode 100644
index 0000000..2ccc0bf
--- /dev/null
+++ b/images/die_gold_pips.svg
@@ -0,0 +1,37 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="336" height="56">
+<g fill="#e7c70d" fill-opacity="0.8" stroke="black" stroke-width="1">
+<g transform="translate(0 0)">
+<circle r="6" cx="28" cy="28"/>
+</g>
+<g transform="translate(56 0)">
+<circle r="6" cx="12" cy="12"/>
+<circle r="6" cx="44" cy="44"/>
+</g>
+<g transform="translate(112 0)">
+<circle r="6" cx="12" cy="44"/>
+<circle r="6" cx="44" cy="12"/>
+<circle r="6" cx="28" cy="28"/>
+</g>
+<g transform="translate(168 0)">
+<circle r="6" cx="12" cy="12"/>
+<circle r="6" cx="12" cy="44"/>
+<circle r="6" cx="44" cy="12"/>
+<circle r="6" cx="44" cy="44"/>
+</g>
+<g transform="translate(224 0)">
+<circle r="6" cx="12" cy="12"/>
+<circle r="6" cx="12" cy="44"/>
+<circle r="6" cx="44" cy="12"/>
+<circle r="6" cx="44" cy="44"/>
+<circle r="6" cx="28" cy="28"/>
+</g>
+<g transform="translate(280 0)">
+<circle r="6" cx="12" cy="12"/>
+<circle r="6" cx="12" cy="28"/>
+<circle r="6" cx="12" cy="44"/>
+<circle r="6" cx="44" cy="12"/>
+<circle r="6" cx="44" cy="28"/>
+<circle r="6" cx="44" cy="44"/>
+</g>
+</g>
+</svg>
diff --git a/play.css b/play.css
index 7dd3022..41a2382 100644
--- a/play.css
+++ b/play.css
@@ -62,6 +62,66 @@ main { background-color: #777; }
}
}
+/* COMBAT TABLE */
+
+.player_pool {
+ width: 500px;
+ background-color: #0003;
+ padding: 8px;
+ border-radius: 24px;
+ margin: 24px auto;
+}
+
+.player_name {
+ color: antiquewhite;
+ text-shadow: 0 0 3px goldenrod;
+ font-size: 20px;
+ text-align: center;
+ font-style: italic;
+ font-family: "Source Serif";
+ padding: 4px;
+}
+
+.table_pool {
+ display: flex;
+ justify-content: center;
+ height: 36px;
+ gap: 12px;
+ align-items: center;
+ margin: 8px;
+}
+
+.die {
+ width: 37px;
+ height: 37px;
+ border-width: 3px;
+ border-style: solid;
+ background-size: 600% 100%;
+ background-repeat: no-repeat;
+ background-image: url(images/die_gold_pips.svg);
+ box-shadow: 0 0 0 1px #333, 1px 2px 3px 1px #0004;
+}
+
+.d0 { display: none; }
+.d1 { background-position: 0% 0; }
+.d2 { background-position: 20% 0; }
+.d3 { background-position: 40% 0; }
+.d4 { background-position: 60% 0; }
+.d5 { background-position: 80% 0; }
+.d6 { background-position: 100% 0; }
+
+.attacker .die {
+ background-color: #9e302f;
+ border-color: #9c3b3a #701d1b #701d1b #9c3b3a;
+}
+
+.defender .die {
+ background-color: #c09e87;
+ border-color: #d6af95 #a18069 #a18069 #d6af95;
+}
+
+.die.action { box-shadow: 0 0 0 1px #333, 0 0 0px 3px white; }
+
/* LOG */
#log .h1 { background-color: silver; font-weight: bold; padding-top:4px; padding-bottom:4px; margin: 8px 0; text-align: center; }
#log .h1 { background-image: linear-gradient(60deg, gray, turquoise, gold); text-shadow: 0 0 4px white; }
@@ -74,6 +134,19 @@ main { background-color: #777; }
#log .h2.mi { background-color: #ebc9be }
#log { font-variant-numeric: tabular-nums; }
+#log .italic { font-style: italic; }
+#log div { padding-left: 20px; text-indent: -12px; }
+#log div.indent { padding-left: 32px; text-indent: -12px; }
+
+#log .adice {
+ font-size: 14px;
+ color: #9e302f;
+}
+
+#log .ddice {
+ font-size: 14px;
+ color: #000000;
+}
/* MAP */
diff --git a/play.html b/play.html
index 79cc6fb..636b314 100644
--- a/play.html
+++ b/play.html
@@ -57,6 +57,19 @@
<div id="table">
+<div id="combat_table">
+
+ <div id="attacker" class="player_pool">
+ <div id="name_attack" class="player_name">Attacker</div>
+ <div id="pool_a" class="table_pool attacker"></div>
+ </div>
+ <div id="defender" class="player_pool defender">
+ <div id="pool_d" class="table_pool"></div>
+ <div id="name_defender" class="player_name">Defender</div>
+ </div>
+
+</div>
+
<div id="mapwrap">
<div id="map">
<svg id="svgmap" width="1275" height="1650" viewBox="0 0 1275 1650">
diff --git a/play.js b/play.js
index d72c78f..0a9a40b 100644
--- a/play.js
+++ b/play.js
@@ -465,6 +465,11 @@ let ui = {
document.getElementById("bk_cylinder"),
document.getElementById("ve_cylinder"),
],
+ pool: [
+ document.getElementById("pool_a"),
+ document.getElementById("pool_d"),
+ ],
+ dice: []
}
function create(t, p, ...c) {
@@ -603,6 +608,15 @@ function init_ui() {
create_piece_list(MI, TROOPS, "piece mongol cube", 2, 0)
+ // dice
+ for (let d = 0; d < 6; ++d) {
+ let e = null
+ let pool = (d < 4) ? "pool_a" : "pool_d"
+ ui.dice[d] = e = create("div", { className: "die d0" })
+ register_action(e, "die", d)
+ document.getElementById(pool).append(e)
+ }
+
}
/* UPDATE */
@@ -907,7 +921,7 @@ function on_update() {
let discs = [ ]
for (let s = 0; s < data.spaces.length; ++s) {
let xy
-
+
if (s <= last_province) {
items.length = discs.length = 0
filter_piece_list(discs, s, DS, DISC)
@@ -1021,6 +1035,14 @@ function on_update() {
for (let i = 0; i < ui.pieces.length; ++i)
ui.pieces[i].classList.toggle("selected", set_has(view.who, i))
+ // Update dice
+ for (let i = 0; i < 6; ++i) {
+ let v = view.dice[i]
+ if (v >= 0)
+ ui.dice[i].className = "die d" + v
+ ui.dice[i].classList.toggle("action", is_action("die", i))
+ }
+
// Influence
layout_influence()
@@ -1041,12 +1063,14 @@ function on_update() {
action_button("end_march", "End March")
// Command buttons
+ action_button("attack", "Attack")
action_button("migrate", "Migrate")
action_button("end_migrate", "End Migrate")
action_button("rally", "Rally")
action_button("end_rally", "End Rally")
action_button("rebel", "Rebel")
action_button("end_rebel", "End Rebel")
+ action_button("roll", "Roll")
// Decree buttons
action_button("build", "Build")
@@ -1072,6 +1096,7 @@ function on_update() {
action_button("end_gifts", "Refuse Compromising Gifts")
// Other buttons
+ action_button("roll", "Roll")
action_button("end_cavalry", "End Cavalry Selection")
action_button("next", "Next")
@@ -1404,6 +1429,14 @@ function on_log(text) {
text = text.substring(3)
p.className = "indent italic"
}
+ else if (text.match(/^\.ad/)) {
+ text = text.substring(4)
+ p.className += " adice"
+ }
+ else if (text.match(/^\.dd/)) {
+ text = text.substring(4)
+ p.className += " ddice"
+ }
text = text.replace(/C(\d+)/g, sub_card)
text = text.replace(/S(\d+)/g, sub_space)
diff --git a/rules.js b/rules.js
index 2c90984..cc4ad9e 100644
--- a/rules.js
+++ b/rules.js
@@ -49,6 +49,10 @@ const last_province = S_TAMILAKAM
const faction_name = [ "Delhi Sultanate", "Bahmani Kingdom", "Vijayanagara Empire", "Mongol Invaders" ]
const faction_acronyms = [ "ds", "bk", "ve", "mi" ]
+const AD = [ "0", '\u2776', '\u2777', '\u2778', '\u2779', '\u277A', '\u277B' ]
+const DD = [ "0", '\u2460', '\u2461', '\u2462', '\u2463', '\u2464', '\u2465' ]
+
+
exports.scenarios = [ "Standard", "Solo" ]
exports.roles = function (scenario, _options) {
@@ -107,6 +111,7 @@ exports.view = function (state, role) {
rebel: game.rebel,
order: game.order,
who: {},
+ dice: game.dice,
}
if (game.result) {
@@ -215,6 +220,7 @@ exports.setup = function (seed, scenario, _options) {
inf_shift: null,
decree: null,
vm: null,
+ dice: [0, 0, 0, 0, 0, 0]
}
if (scenario === "Solo")
@@ -465,6 +471,7 @@ function goto_rebel() {
function goto_tax() {
init_decree("Tax")
+ game.decree.count = 1
game.state = "tax"
}
@@ -536,6 +543,7 @@ states.main_phase = {
view.actions.end_of_turn = 1
}
},
+ attack: goto_attack,
build: goto_build,
campaign: goto_campaign,
collect: goto_collect,
@@ -761,7 +769,7 @@ states.strategic_assistance = {
states.tax = {
prompt() {
- if (game.decree.n === 1) {
+ if (game.decree.count === 1) {
view.prompt = `Tax: Collect ${tax_count()} from Controlled Prosperity and Temples.`
gen_action_resources(VE)
} else {
@@ -774,7 +782,7 @@ states.tax = {
let t = tax_count()
add_resources(game.current, t)
logi_resources(VE, t)
- game.decree.n = 0
+ game.decree.count = 0
},
end_tax: end_decree
}
@@ -880,11 +888,202 @@ function end_decree() {
/* SHARED COMMANDS */
-function can_attack() {}
+function can_attack() {
+ for (let s = first_space; s <= last_space; ++s)
+ if (can_attack_in_space(s))
+ return true
+ return false
+}
-function can_attack_in_space(s) {}
+function can_attack_in_space(s) {
+ if (!has_valid_attackers(s, game.current))
+ return false
-function goto_attack() {}
+ if (game.current === DS) {
+ if (
+ can_attack_rebel_in_space(s, BK) ||
+ can_attack_rebel_in_space(s, VE) ||
+ has_piece(s, MI, TROOPS)
+ )
+ return true
+ } else if (game.current === BK) {
+ if (count_faction_pieces(s, DS) > 0 || count_faction_pieces(s, VE) > 0)
+ return true
+ } else if (game.current === VE) {
+ if (count_faction_pieces(s, DS) > 0 || count_faction_pieces(s, BK) > 0)
+ return true
+ }
+ return false
+}
+
+function can_attack_rebel_in_space(s, faction) {
+ if (
+ count_pieces(s, faction, DISC) === 1 &&
+ count_faction_pieces(s, faction) === 1
+ )
+ return true
+
+ if (has_rebel(s, faction))
+ return true
+
+ return false
+}
+
+function goto_attack() {
+ init_command("Attack")
+ game.cmd.attacker = game.current
+ game.state = "attack"
+}
+
+function goto_attack_select() {
+ game.cmd.target = [0, 0, 0, 0]
+ if (game.current === DS) {
+ game.cmd.target[BK] += can_attack_rebel_in_space(game.cmd.where, BK)
+ game.cmd.target[VE] += can_attack_rebel_in_space(game.cmd.where, VE)
+ game.cmd.target[MI] += has_piece(game.cmd.where, MI, TROOPS)
+ } else if (game.current === BK) {
+ game.cmd.target[DS] += count_faction_pieces(game.cmd.where, DS) > 0
+ game.cmd.target[VE] += count_faction_pieces(game.cmd.where, VE) > 0
+ } else if (game.current === VE) {
+ game.cmd.target[DS] += count_faction_pieces(game.cmd.where, DS) > 0
+ game.cmd.target[BK] += count_faction_pieces(game.cmd.where, BK) > 0
+ }
+
+ let n = game.cmd.target.reduce((a, c) => a + c, 0);
+ if (n === 1) {
+ game.cmd.target = game.cmd.target.indexOf(1)
+ game.state = "attack_space"
+ } else {
+ game.state = "attack_select"
+ }
+}
+
+function roll_attack(faction_d) {
+ for (let d = 0; d < 6; ++d)
+ game.dice[d] = random(6) + 1
+
+ if (faction_d === BK && has_piece(game.cmd.where, BK, DISC))
+ game.dice[3] = 0
+}
+
+function goto_attack_cavalry() {
+ game.cmd.clog = false
+ game.state = "attack_cavalry"
+}
+
+function attack_use_cavalry(d) {
+ use_cavalry(game.current)
+
+ let is_attacker = (game.current === game.cmd.attacker)
+ let is_a_die = (d < 4)
+
+ if (is_a_die && is_attacker) {
+ charge_die(d)
+ } else if (!is_a_die && is_attacker) {
+ screen_die(d)
+ } else if (is_a_die && !is_attacker) {
+ screen_die(d)
+ } else if (!is_a_die && !is_attacker) {
+ charge_die[d]
+ }
+}
+
+function charge_die(d) {
+ if (d < 4) {
+ logi(`.ad ${AD[game.dice[d]]} \u27f6 ${AD[game.dice[d]-1]} charged`)
+ } else {
+ logi(`.dd ${DD[game.dice[d]]} \u27f6 ${DD[game.dice[d]-1]} charged`)
+ }
+ game.dice[d] -= 1
+}
+
+function screen_die(d) {
+ if (d < 4) {
+ logi(`.ad ${AD[game.dice[d]]} screened`)
+ } else {
+ logi(`.dd ${DD[game.dice[d]]} screened`)
+ }
+ game.dice[d] = 0
+}
+
+states.attack = {
+ prompt() {
+ if (game.current === DS)
+ view.prompt = "Attack: Select Spaces with Rebels or Mongol Invaders."
+ else
+ view.prompt = "Attack: Select Provinces with enemy pieces."
+
+ if (can_select_cmd_space(1) && can_attack()) {
+ for (let s = first_space; s <= last_space; ++s) {
+ if (!is_selected_cmd_space(s) && can_attack_in_space(s))
+ gen_action_space(s)
+ }
+ }
+ },
+ space(s) {
+ game.cmd.where = s
+ log_space(game.cmd.where, "Attack")
+ goto_attack_select()
+ }
+}
+
+states.attack_select = {
+ prompt() {
+ view.prompt = "Attack: Choose a Faction to attack"
+ }
+}
+
+states.attack_space = {
+ prompt() {
+ view.prompt = "Attack: "
+
+ view.actions.roll = 1
+ },
+ roll() {
+ roll_attack()
+ log(">.ad " + game.dice.slice(0,4).map(d => AD[d]).join(" "))
+ log(">.dd " + game.dice.slice(4).map(d => DD[d]).join(" "))
+ log_br()
+
+ goto_attack_cavalry()
+ }
+}
+
+states.attack_cavalry = {
+ prompt() {
+ view.prompt = "Attack: Use cavalry to..."
+
+ // game.cmd.attacker
+ // game.cmd.target
+ // game.current
+ //
+
+ // STEP ?
+ // step 1 - attacker cavalry
+ // step 2 - defender calvary
+
+ let curr_die = [0, 1, 2, 3, 4, 5]
+ // TODO: restrict range for MI player
+
+ if (has_cavalry(game.current))
+ for (let d of curr_die)
+ if (game.dice[d] > 1)
+ gen_action_die(d)
+
+ view.actions.next = 1
+ },
+ die(d) {
+ if (!game.cmd.cavalry) {
+ log(`${faction_name[game.current]} is using Cavalry.`)
+ game.cmd.cavalry = true
+ }
+
+ attack_use_cavalry(d)
+ },
+ next() {
+
+ }
+}
/* DELHI SULTANATE COMMANDS */
@@ -1105,6 +1304,7 @@ function goto_migrate_space() {
}
states.migrate = {
+ // TODO: Check Rebel status after migration
prompt() {
view.prompt = "Migrate: Select a Province as the Migrate destination."
@@ -1305,13 +1505,13 @@ function collect_count() {
function goto_collect() {
init_decree("Collect")
- game.decree.n = 1
+ game.decree.count = 1
game.state = "collect"
}
states.collect = {
prompt() {
- if (game.decree.n > 0) {
+ if (game.decree.count > 0) {
view.prompt = `Collect Tribute: Collect ${collect_count()} from half the Tributaries prosperity`
gen_action_resources(DS)
} else {
@@ -1323,7 +1523,7 @@ states.collect = {
let c = collect_count()
add_resources(DS, c)
logi_resources(DS, c)
- game.decree.n = 0
+ game.decree.count = 0
goto_cavalry(2, "collect")
},
end_collect: end_decree,
@@ -1444,7 +1644,7 @@ function can_build_in_space(s) {
function goto_build() {
init_decree("Build")
- game.decree.n = 1
+ game.decree.count = 1
game.state = "build"
}
@@ -1455,7 +1655,7 @@ states.build = {
else if (game.current === VE)
view.prompt = "Build: Select a Province with a Raja."
- if (game.decree.n === 1) {
+ if (game.decree.count === 1) {
for (let s = first_space; s <= last_space; ++s) {
if (can_build_in_space(s))
gen_action_space(s)
@@ -1473,7 +1673,7 @@ states.build = {
place_piece(p, s)
log_space(s, "Build")
pop_summary()
- game.decree.n = 0
+ game.decree.count = 0
},
end_build: end_decree,
}
@@ -1658,12 +1858,13 @@ function trade_count() {
function goto_trade() {
init_decree("Trade")
+ game.decree.count = 1
game.state = "trade"
}
states.trade = {
prompt() {
- if (game.decree.n === 1) {
+ if (game.decree.count === 1) {
view.prompt = `Trade: Collect ${trade_count()} from Provinces with your presence.`
gen_action_resources(BK)
} else {
@@ -1675,7 +1876,7 @@ states.trade = {
let t = trade_count()
add_resources(game.current, t)
logi_resources(BK, t)
- game.decree.n = 0
+ game.decree.count = 0
goto_cavalry(trade_cavalry_count(), "trade")
},
end_trade: end_decree,
@@ -1760,6 +1961,16 @@ function remove_tributary(s) {
update_control()
}
+function has_rebel(s, faction) {
+ let p0 = first_piece[faction][ELITE]
+ let p1 = last_piece[faction][ELITE]
+ for (let p = p0; p <= p1; ++p)
+ if (piece_space(p) === s)
+ if (is_rebel(p))
+ return true
+ return false
+}
+
function is_rebel(p) {
let faction = piece_faction(p)
let piece_index = p - first_piece[faction][ELITE]
@@ -1890,6 +2101,20 @@ function has_unmoved_piece(s, faction) {
return unmoved
}
+function has_valid_attackers(s, faction) {
+ let valid_attacker = false
+ for_each_movable(faction, p => {
+ if (piece_space(p) === s)
+ valid_attacker = true
+ if (piece_space(p) === s || (has_piece(piece_space(p), faction, DISC) && SPACES[s].adjacent.includes(piece_space(p))))
+ if (!game.cmd.pieces || game.cmd.pieces.length === 0)
+ valid_attacker = true
+ else if (!set_has(game.cmd.pieces, p))
+ valid_attacker = true
+ })
+ return valid_attacker
+}
+
function gen_place_piece(faction, type) {
for_each_piece(faction, type, p => {
if (piece_space(p) === AVAILABLE) {
@@ -2004,6 +2229,22 @@ function trade_cavalry_count() {
return 3;
}
+function find_cavalry(faction) {
+ for (let c = 0; c <= LAST_CAVALRY; ++c)
+ if (game.cavalry[c] === faction)
+ return c
+ return -1
+}
+
+function has_cavalry(faction) {
+ return find_cavalry(faction) != -1
+}
+
+function use_cavalry(faction) {
+ let c = find_cavalry(faction)
+ game.cavalry[c] = AVAILABLE
+}
+
/* INFLUENCE */
function add_influence(faction) {
@@ -2127,6 +2368,10 @@ function gen_action(action, argument) {
set_add(view.actions[action], argument)
}
+function gen_action_die(d) {
+ gen_action("die", d)
+}
+
function gen_action_influence(faction) {
gen_action("inf", faction)
}
@@ -2614,7 +2859,7 @@ function for_each_movable(faction, f) {
if (faction === BK)
for_each_piece(BK, ELITE, f)
else if (faction === VE)
- for_each_piece(BK, ELITE, f)
+ for_each_piece(VE, ELITE, f)
else if (faction === DS) {
for_each_piece(DS, TROOPS, f)
for_each_piece(DS, ELITE, f)