diff options
author | Tor Andersson <tor@ccxvii.net> | 2023-06-25 11:40:54 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2023-07-07 19:05:52 +0200 |
commit | d91729e13fd3081907bf38b5a6823aab161c6509 (patch) | |
tree | 5fd098cb0ea4db756a81110b57f851188b25c501 | |
parent | 0648e171052fb2ecc6a9ee94b33dede1846dec99 (diff) | |
download | time-of-crisis-d91729e13fd3081907bf38b5a6823aab161c6509.tar.gz |
Combat overlay to show battle lines expanded.
-rw-r--r-- | play.css | 13 | ||||
-rw-r--r-- | play.html | 4 | ||||
-rw-r--r-- | play.js | 136 | ||||
-rw-r--r-- | rules.js | 53 | ||||
-rw-r--r-- | tools/boxes.svg | 59 |
5 files changed, 228 insertions, 37 deletions
@@ -636,3 +636,16 @@ body.shift #zenobia { background-image: url(images/rival_back.png) } gap: 18px; min-height: 350px; } + +/* COMBAT MASK */ + +#combat_mask { + box-sizing: border-box; + position: absolute; + z-index: 100; + border-radius: 12px; + box-shadow: 0px 2px 8px #000; + background-color: #666; + border: 2px solid #444; + opacity: 0.8; +} @@ -125,9 +125,11 @@ </svg> +<div id="pieces"> + <div id="active_event"></div> -<div id="pieces"> +<div id="combat_mask"></div> <div id="crisis_die_1" class="hide" style="right:175px;top:530px"></div> <div id="crisis_die_2" class="hide" style="right:125px;top:530px"></div> @@ -300,6 +300,11 @@ const BOXES = { "Goths Dice": [1730,195,100,50], "Sassanids Dice": [2380,895,100,50], "Nomads Dice": [570,1520,100,50], + "Franks Battle": [750,210,430,180], + "Alamanni Battle": [1240,210,430,180], + "Goths Battle": [1670,245,430,180], + "Sassanids Battle": [2070,980,430,180], + "Nomads Battle": [670,1400,430,180], } const LAYOUT_XY = [ @@ -326,6 +331,26 @@ const LAYOUT_XY = [ BOXES["Pontus Euxinus XY"], ] +const LAYOUT_BATTLE = [ + BOXES["Italia Capital"], + BOXES["Asia Capital"], + BOXES["Gallia Capital"], + BOXES["Macedonia Capital"], + BOXES["Pannonia Capital"], + BOXES["Thracia Capital"], + BOXES["Britannia Capital"], + BOXES["Galatia Capital"], + BOXES["Syria Capital"], + BOXES["Aegyptus Capital"], + BOXES["Africa Capital"], + BOXES["Hispania Capital"], + BOXES["Alamanni Battle"], + BOXES["Franks Battle"], + BOXES["Goths Battle"], + BOXES["Sassanids Battle"], + BOXES["Nomads Battle"], +] + const LAYOUT_SUPPORT = [ BOXES["Italia Support 1"], BOXES["Asia Support"], @@ -611,6 +636,7 @@ let ui = { hand: document.getElementById("hand"), draw: document.getElementById("draw"), active_event: document.getElementById("active_event"), + combat_mask: document.getElementById("combat_mask"), discard: document.getElementById("discard"), played: document.getElementById("played"), market: document.getElementById("market"), @@ -856,9 +882,39 @@ function on_init() { let stack_count = new Array(21).fill(0) -function layout_stack(id, list, region, in_capital, dx, dy) { +let battle_width = 0 +const BATTLE_MIN = 3 +const BATTLE_H_MARGIN = 12 +const BATTLE_V_MARGIN = 12 +const BATTLE_H_GAP = 8 +const BATTLE_V_GAP = 16 + +function layout_battle_stack(is_att, list, region) { + let [ x, y, w, h ] = LAYOUT_BATTLE[region] + x += w >> 1 + if (region < 12) + y -= 7 + + if (list.length > battle_width) + battle_width = list.length + + w = list.length * 60 + (list.length - 1) * BATTLE_H_GAP + x -= w / 2 + + y += BATTLE_V_MARGIN + if (is_att) + y += 60 + BATTLE_V_GAP + + for (let item of list) { + item.style.left = x + "px" + item.style.top = y + "px" + item.style.zIndex = 101 + x += 60 + BATTLE_H_GAP + } +} + +function layout_stack(id, list, region, in_capital, dx, dy, z = 1) { let [ x, y, w, h ] = LAYOUT_XY[region] - let z = 1 x += w >> 1 y += h >> 1 @@ -885,7 +941,6 @@ function layout_stack(id, list, region, in_capital, dx, dy) { item.style.left = x + "px" item.style.top = y + "px" item.style.zIndex = z - item.my_stack = id x -= dx y -= dy z += 1 @@ -900,7 +955,6 @@ function layout_available(list, dx, x0, y0) { item.style.left = x + "px" item.style.top = y + "px" item.style.zIndex = z - item.my_stack = 0 x += dx z -= 1 } @@ -946,6 +1000,18 @@ function layout_barbarian_dice(black, white, tribe) { } } +function is_battle_stack(region, type, id) { + if (view.battle_region !== region) + return false + if (type === "general" && view.battle.attacker === id) + return true + if (type === "militia" && view.battle.attacker < 0) + return true + if (type === "militia" && view.battle.type === "militia") + return true + return view.battle.type === type && view.battle.target === id +} + function on_update() { let player_count = view.legacy.length @@ -975,6 +1041,8 @@ function on_update() { ui.discard.replaceChildren() ui.market.replaceChildren() + battle_width = BATTLE_MIN + for (let pi = 0; pi < player_count; ++pi) { let legacy = view.legacy[pi] let turns = view.emperor_turns[pi] @@ -1109,15 +1177,16 @@ function on_update() { for (let region = 0; region < 12 + 5; ++region) { for (let tribe = 0; tribe < 5; ++tribe) { + let battle = [] let active_barbarians = [] let inactive_barbarians = [] for (let id = first_barbarian[tribe]; id <= last_barbarian[tribe]; ++id) { - if (id === CNIVA || id === ARDASHIR || id === SHAPUR) - continue let loc = get_barbarian_location(id) let inactive = is_barbarian_inactive(id) if (loc === region) { - if (inactive) + if (is_battle_stack(region, "barbarians", tribe) && (region < 12 || !inactive)) + battle.push(ui.barbarians[id]) + else if (inactive) inactive_barbarians.push(ui.barbarians[id]) else active_barbarians.push(ui.barbarians[id]) @@ -1130,22 +1199,21 @@ function on_update() { layout_stack(-1, inactive_barbarians, region, false, 8, 8) if (active_barbarians.length > 0) layout_stack(-1, active_barbarians, region, false, 8, 8) + if (battle.length > 0) + layout_battle_stack(0, battle, region) } } - for (let id of [ CNIVA, ARDASHIR, SHAPUR ]) { - let loc = get_barbarian_location(id) - if (loc !== AVAILABLE) - layout_stack(-1, [ ui.barbarians[id] ], loc, false, 8, 8) - } - for (let id = 0; id < 3; ++id) { let loc = get_rival_emperor_location(id) if (loc === AVAILABLE) hide(ui.rival_emperors[id]) else { show(ui.rival_emperors[id]) - layout_stack(-1, [ ui.rival_emperors[id] ], loc, false, 8, 8) + if (is_battle_stack(loc, "rival_emperor", id)) + layout_battle_stack(0, [ ui.rival_emperors[id] ], loc) + else + layout_stack(-1, [ ui.rival_emperors[id] ], loc, false, 8, 8) } } @@ -1170,10 +1238,17 @@ function on_update() { } } if (lone_militia) { - if (has_militia_castra(region)) - layout_stack(-1, [ ui.mcastra[region], ui.militia[region] ], region, true, 16, 16) - else - layout_stack(-1, [ ui.militia[region] ], region, true, 16, 16) + if (is_battle_stack(region, "militia", region)) { + if (has_militia_castra(region)) + layout_battle_stack(view.battle.attacker < 0, [ ui.mcastra[region], ui.militia[region] ], region) + else + layout_battle_stack(view.battle.attacker < 0, [ ui.militia[region] ], region, true) + } else { + if (has_militia_castra(region)) + layout_stack(-1, [ ui.mcastra[region], ui.militia[region] ], region, true, 16, 16) + else + layout_stack(-1, [ ui.militia[region] ], region, true, 16, 16) + } } } @@ -1235,10 +1310,10 @@ function on_update() { if (has_militia(region) && inside) stack.push(ui.militia[region]) - if (inside) - layout_stack(pi * 6 + ai, stack, region, true, 16, 16) + if (is_battle_stack(region, "general", pi * 6 + ai)) + layout_battle_stack(pi * 6 + ai === view.battle.attacker, stack, region) else - layout_stack(pi * 6 + ai, stack, region, false, 16, 16) + layout_stack(pi * 6 + ai, stack, region, inside, 16, 16) } else { avail_stack.push(e) } @@ -1278,6 +1353,25 @@ function on_update() { layout_available(avail_stack, 64, pi * 625 + 325, 27) } + if (view.battle) { + let [ x, y, w, h ] = LAYOUT_BATTLE[view.battle_region] + if (view.battle_region < 12) + y -= 7 + x += w >> 1 + + w = battle_width * 60 + (battle_width - 1) * BATTLE_H_GAP + BATTLE_H_MARGIN * 2 + x -= w / 2 + h = 120 + BATTLE_V_GAP + BATTLE_V_MARGIN * 2 + + show(ui.combat_mask) + ui.combat_mask.style.left = x + "px" + ui.combat_mask.style.top = y + "px" + ui.combat_mask.style.width = w + "px" + ui.combat_mask.style.height = h + "px" + } else { + hide(ui.combat_mask) + } + ui.body.classList.toggle("military", view.color === 0) ui.body.classList.toggle("senate", view.color === 1) ui.body.classList.toggle("populace", view.color === 2) @@ -2989,14 +2989,15 @@ function gen_initiate_battle(where) { function format_battle_target() { switch (game.battle.type) { - case "militia": return "militia" + case "militia": return PLAYER_NAMES[get_province_player(game.battle.target)] + " militia" case "barbarians": return BARBARIAN_NAME[game.battle.target] - case "general": return GENERAL_NAME[game.battle.target] + case "general": return PLAYER_NAMES[game.battle.target / 6 | 0] + " army" case "rival_emperor": return RIVAL_EMPEROR_NAME[game.battle.target] } } states.battle = { + show_battle: true, inactive: "Combat", prompt() { prompt("Initiate Battle against " + format_battle_target() + " in " + REGION_NAME[game.where] + ".") @@ -3023,10 +3024,18 @@ states.battle = { } function format_hits() { - return `${game.battle.ahits} hits to attacker vs ${game.battle.dhits} hits to defender` + let s = "Defender rolled " + game.battle.ahits + " hit" + if (game.battle.ahits !== 1) + s += "s" + s += ", attacker rolled " + game.battle.dhits + " hit" + if (game.battle.dhits !== 1) + s += "s" + return s } states.flanking_maneuver = { + show_battle: true, + get inactive() { return "Flanking Maneuver. " + format_hits() }, inactive: "Flanking Maneuver", prompt() { prompt("Flanking Maneuver: " + format_hits() + ".") @@ -3299,13 +3308,14 @@ function goto_assign_hits_on_defender() { if (has_hits_on_defender()) game.state = "assign_hits_on_defender" else - goto_combat_victory() + game.state = "combat_victory" } states.assign_hits_on_attacker = { - inactive: "Combat", + show_battle: true, + get inactive() { return "Combat. " + format_hits() }, prompt() { - prompt("Combat: " + format_hits() + " \u2013 assign " + (game.battle.ahits - game.battle.ataken) + " hits to attacker!") + prompt("Combat: " + format_hits() + ".") if (game.battle.attacker < 0) gen_hits_militia() else @@ -3332,9 +3342,10 @@ states.assign_hits_on_attacker = { } states.assign_hits_on_defender = { - inactive: "Combat", + show_battle: true, + get inactive() { return "Combat. " + format_hits() }, prompt() { - prompt("Combat: " + format_hits() + " \u2013 assign " + (game.battle.dhits - game.battle.dtaken) + " hits to defender!") + prompt("Combat: " + format_hits() + ".") switch (game.battle.type) { case "militia": gen_hits_militia() @@ -3400,6 +3411,25 @@ function is_defender_eliminated() { return false } +states.combat_victory = { + show_battle: true, + inactive: "Combat", + prompt() { + let de = is_defender_eliminated() + let ae = is_attacker_eliminated() + if (de && ae) + prompt("Combat: There is no winner.") + else if (de || game.battle.dtaken > game.battle.ataken) + prompt("Combat: You win the battle!") + else + prompt("Combat: The defenders win the battle!") + view.actions.done = 1 + }, + done() { + goto_combat_victory() + }, +} + function goto_combat_victory() { let de = is_defender_eliminated() let ae = is_attacker_eliminated() @@ -3489,6 +3519,7 @@ function end_battle() { // Deselect eliminated general... if (game.selected_general >= 0 && get_general_location(game.selected_general) === AVAILABLE) game.selected_general = -1 + if (game.battle.killed) { if (can_free_increase_support_level(game.where)) { game.state = "free_increase_support_level" @@ -3498,6 +3529,7 @@ function end_battle() { game.killed |= game.battle.killed } } + game.battle = null game.state = "take_actions" } @@ -4257,6 +4289,11 @@ exports.view = function (state, player_name) { emperor_turns: game.emperor_turns, } + if (game.battle && states[game.state].show_battle) { + view.battle = game.battle + view.battle_region = game.where + } + if (game.state === "game_over") { view.prompt = game.victory } else if (game.current !== player) { diff --git a/tools/boxes.svg b/tools/boxes.svg index 8f81dcb..435d0f7 100644 --- a/tools/boxes.svg +++ b/tools/boxes.svg @@ -40,9 +40,9 @@ inkscape:window-height="480" id="namedview6" showgrid="true" - inkscape:zoom="1.4142136" - inkscape:cx="859.33219" - inkscape:cy="1408.35" + inkscape:zoom="0.89299613" + inkscape:cx="474.18327" + inkscape:cy="1325.4002" inkscape:current-layer="svg4" inkscape:document-rotation="0"> <inkscape:grid @@ -52,11 +52,11 @@ <image sodipodi:absref="/home/tor/src/rally/public/time-of-crisis/map75.png" xlink:href="../map75.png" - style="display:inline;image-rendering:pixelated" - width="2550" - height="1650" + sodipodi:insensitive="true" id="image2" - sodipodi:insensitive="true" /> + height="1650" + width="2550" + style="display:inline;image-rendering:pixelated" /> <rect style="fill:#e60000;fill-opacity:0.325203;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.581301" id="rect873" @@ -426,4 +426,49 @@ y="1520" ry="2.0349298" inkscape:label="Nomads Dice" /> + <rect + style="fill:#0027e6;fill-opacity:0.325203;stroke:none;stroke-width:12.3648;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.581301" + id="rect908-3-3" + width="430" + height="180" + x="750" + y="210" + ry="7.325747" + inkscape:label="Franks Battle" /> + <rect + style="fill:#0027e6;fill-opacity:0.325203;stroke:none;stroke-width:12.3648;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.581301" + id="rect908-3-3-6" + width="430" + height="180" + x="1240" + y="210" + ry="7.325747" + inkscape:label="Alamanni Battle" /> + <rect + style="fill:#0027e6;fill-opacity:0.325203;stroke:none;stroke-width:12.3648;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.581301" + id="rect908-3-3-6-7" + width="430" + height="180" + x="1670" + y="245" + ry="7.325747" + inkscape:label="Goths Battle" /> + <rect + style="fill:#0027e6;fill-opacity:0.325203;stroke:none;stroke-width:12.3648;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.581301" + id="rect908-3-3-6-7-5" + width="430" + height="180" + x="2070" + y="980" + ry="7.325747" + inkscape:label="Sassanids Battle" /> + <rect + style="fill:#0027e6;fill-opacity:0.325203;stroke:none;stroke-width:12.3648;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.581301" + id="rect908-3-3-6-7-5-3" + width="430" + height="180" + x="670" + y="1400" + ry="7.325747" + inkscape:label="Nomads Battle" /> </svg> |