diff options
author | Tor Andersson <tor@ccxvii.net> | 2021-06-21 20:48:31 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2022-11-16 19:19:38 +0100 |
commit | 70dddf939d32eefdb70346febd40fa639ee3d39d (patch) | |
tree | d355915e0db8c89d2e33cfcc792914e29588ddb6 | |
parent | 34d668d46988eccf279cece033f35533aa56904c (diff) | |
download | crusader-rex-70dddf939d32eefdb70346febd40fa639ee3d39d.tar.gz |
crusader: Storming!
-rw-r--r-- | play.html | 26 | ||||
-rw-r--r-- | rules.js | 352 | ||||
-rw-r--r-- | ui.js | 14 |
3 files changed, 310 insertions, 82 deletions
@@ -163,16 +163,26 @@ body.shift .block.known:hover { .block.moved { filter: brightness(80%) grayscale(40%); } .block.highlight.moved { filter: brightness(95%) grayscale(40%); } -.map .block.castle { border-color: #444; } +.map .block.castle.known { border-color: #444; filter: grayscale(50%); } +.map .block.castle:not(.known) { + background-image: url("/crusader-rex/besieged.svg"); + background-size: 60%; + background-position: center; +} +.map .block.besieging:not(.known) { + background-image: url("/crusader-rex/besieging.svg"); + background-size: 60%; + background-position: center; +} .block.r0 { transform: rotate(0deg); } .block.r1 { transform: rotate(-90deg); } .block.r2 { transform: rotate(-180deg); } .block.r3 { transform: rotate(-270deg); } -.block.r0hh { transform: rotate(-30deg); } -.block.r1hh { transform: rotate(-120deg); } -.block.r2hh { transform: rotate(-210deg); } -.block.r3hh { transform: rotate(-300deg); } +.block.r0.halfhit { transform: rotate(-15deg); } +.block.r1.halfhit { transform: rotate(-105deg); } +.block.r2.halfhit { transform: rotate(-195deg); } +.block.r3.halfhit { transform: rotate(-285deg); } .block { transition-property: top, left, transform; @@ -331,10 +341,12 @@ body.shift .block.known:hover { <div id="prompt" class="prompt">$PROMPT</div> <button id="eliminate_button" class="hide" onclick="on_button_eliminate()">Eliminate</button> - <button id="sea_move_button" class="hide" onclick="on_button_sea_move()">Sea Move</button> + <button id="group_move_button" class="hide" onclick="on_button_group_move()">Group move</button> + <button id="end_group_move_button" class="hide" onclick="on_button_end_group_move()">End group move</button> + <button id="sea_move_button" class="hide" onclick="on_button_sea_move()">Sea move</button> + <button id="end_sea_move_button" class="hide" onclick="on_button_end_sea_move()">End sea move</button> <button id="muster_button" class="hide" onclick="on_button_muster()">Muster</button> <button id="end_muster_button" class="hide" onclick="on_button_end_muster()">End muster</button> - <button id="end_sea_move_button" class="hide" onclick="on_button_end_sea_move()">End sea move</button> <button id="end_retreat_button" class="hide" onclick="on_button_end_retreat()">End retreat</button> <button id="end_regroup_button" class="hide" onclick="on_button_end_regroup()">End regroup</button> <button id="end_move_phase_button" class="hide" onclick="on_button_end_move_phase()">End move phase</button> @@ -438,6 +438,15 @@ function can_block_sea_move(who) { return false; } +function can_sea_move_anywhere() { + if (game.moves > 0) { + for (let b in BLOCKS) + if (can_block_sea_move(b)) + return true; + } + return false; +} + function can_block_continue(who, from, to) { if (is_contested_field(to)) return false; @@ -586,6 +595,18 @@ function is_field_combatant(who) { return false; } +function is_storm_attacker(who) { + return game.storming.includes(who); +} + +function is_storm_defender(who) { + return is_block_in_castle_in(who, game.where); +} + +function is_storm_combatant(who) { + return game.storming.includes(who) || is_block_in_castle_in(who, game.where); +} + function disband(who) { if (block_plural(who)) log(block_name(who) + " disband."); @@ -816,7 +837,7 @@ function move_block(who, from, to) { } function goto_move_phase(moves) { - game.state = 'group_move'; + game.state = 'move_phase'; game.moves = moves; game.activated = []; game.mustered = []; @@ -831,11 +852,63 @@ function end_move_phase() { end_player_turn(); } +states.move_phase = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Move Phase: Waiting for " + game.active + "."; + view.prompt = "Move Phase: Group Move, Sea Move, or Muster. " + game.moves + "AP left."; + gen_action_undo(view); + gen_action(view, 'end_move_phase'); + if (game.moves > 0) { + gen_action(view, 'group_move'); + if (can_sea_move_anywhere()) + gen_action(view, 'sea_move'); + if (can_muster_anywhere()) + gen_action(view, 'muster'); + } + }, + group_move: function () { + push_undo(); + --game.moves; + game.state = 'group_move'; + }, + sea_move: function () { + push_undo(); + --game.moves; + game.state = 'sea_move'; + }, + muster: function () { + push_undo(); + --game.moves; + game.state = 'muster'; + }, + end_move_phase: end_move_phase, + undo: pop_undo +} + +// GROUP MOVE + states.group_move = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Move Phase: Waiting for " + game.active + "."; - view.prompt = "Group Move: Choose a block to group move. " + game.moves + "AP left."; + view.prompt = "Group Move: Choose a block to group move."; + gen_action_undo(view); + for (let t in TOWNS) { + } + }, + town: function () { + push_undo(); + game.state = 'muster'; + }, + undo: pop_undo +} + +states.group_move_who = { + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Move Phase: Waiting for " + game.active + "."; + view.prompt = "Group Move: Choose a block to group move."; gen_action_undo(view); gen_action(view, 'end_move_phase'); if (can_muster_anywhere()) @@ -934,29 +1007,19 @@ function end_move() { game.state = 'group_move'; } -function can_sea_move_anywhere() { - if (game.moves > 0) { - for (let b in BLOCKS) - if (can_block_sea_move(b)) - return true; - } - return false; -} +// SEA MOVE states.sea_move = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Move Phase: Waiting for " + game.active + "."; - view.prompt = "Sea Move: Choose a block to sea move. " + game.moves + "AP left."; + view.prompt = "Sea Move: Choose a block to sea move."; gen_action_undo(view); - if (game.moves > 0) { - for (let b in BLOCKS) - if (can_block_sea_move(b)) - gen_action(view, 'block', b); - } + for (let b in BLOCKS) + if (can_block_sea_move(b)) + gen_action(view, 'block', b); }, block: function (who) { - push_undo(); game.who = who; game.state = 'sea_move_to'; }, @@ -976,7 +1039,6 @@ states.sea_move_to = { gen_action(view, 'town', to); }, town: function (to) { - --game.moves; let from = game.location[game.who]; game.location[game.who] = to; game.moved[game.who] = true; @@ -986,6 +1048,7 @@ states.sea_move_to = { lift_siege(from); // English Crusaders attack! + // TODO: attack on field or enter port in tripoli/tyre? if (is_contested_town(to)) { game.attacker[to] = FRANK; game.main_road[to] = "England"; @@ -994,13 +1057,21 @@ states.sea_move_to = { log_move_continue(to); } - game.state = 'group_move'; game.who = null; + game.state = 'move_phase'; + }, + block: function () { + game.who = null; + game.state = 'sea_move'; + }, + undo: function () { + game.who = null; + game.state = 'sea_move'; }, - block: pop_undo, - undo: pop_undo } +// MUSTER + function can_muster_anywhere() { if (game.moves > 0) return true; @@ -1016,7 +1087,7 @@ states.muster = { prompt: function (view, current) { if (is_inactive_player(current)) return view.prompt = "Waiting for " + game.active + " to move."; - view.prompt = "Muster: Choose one friendly or vacant muster town. " + game.moves + "AP left."; + view.prompt = "Muster: Choose one friendly or vacant muster town."; gen_action_undo(view); gen_action(view, 'end_muster'); if (game.moves > 0) { @@ -1312,6 +1383,7 @@ function start_combat(where) { log("Battle in " + where + "."); game.where = where; game.combat_round = 0; + game.halfhit = null; game.storming = []; game.sallying = []; @@ -1336,12 +1408,7 @@ function start_combat(where) { function end_combat() { console.log("END COMBAT IN", game.where); - game.active = game.p1; - if (is_contested_town(game.where)) { - console.log("SIEGE CONTINUES NEXT TURN..."); - } else { - lift_siege(game.where); - } + lift_siege(game.where); delete game.storming; delete game.sallying; @@ -1406,6 +1473,7 @@ function goto_regroup() { lift_siege(game.where); console.log("REGROUP", game.active); reset_road_limits(); + game.moved = {}; game.state = 'regroup'; game.turn_log = []; } @@ -1511,6 +1579,7 @@ function goto_combat_round(combat_round) { console.log("NEW ATTACKER IS", game.attacker[game.where]); log(game.attacker[game.where] + " is now the attacker."); } + return goto_declare_sally(); } return goto_field_battle(); } @@ -1583,7 +1652,7 @@ states.declare_sally = { return view.prompt = "Waiting for " + game.active + " to declare sallying."; view.prompt = "Sally: Declare which blocks should sally onto the field."; for (let b in BLOCKS) { - if (block_owner(b) == game.active && !is_battle_reserve(b)) { + if (block_owner(b) == game.active && !is_battle_reserve(b) && is_block_in_castle(b)) { if (game.location[b] == game.where && !game.sallying.includes(b)) { gen_action(view, 'battle_sally', b); gen_action(view, 'block', b); @@ -1726,7 +1795,7 @@ function goto_siege_attrition() { console.log("SIEGE ATTRITION"); game.active = besieging_player(game.where); for (let b in BLOCKS) { - if (is_block_in_castle_in(game.where)) { + if (is_block_in_castle_in(b, game.where)) { let die = roll_d6(); if (die <= 3) { log("Siege attrition: " + DIE_HIT[die] + "."); @@ -1740,34 +1809,57 @@ function goto_siege_attrition() { log(game.where + " falls to siege attrition."); goto_regroup(); } else { + log("Siege continues."); end_combat(); } } +// FIELD AND STORM BATTLE HELPERS + +function filter_battle_blocks(ci, is_candidate) { + let output = null; + for (let b in BLOCKS) { + if (is_candidate(b) && !game.moved[b]) { + if (block_initiative(b) == ci) { + if (!output) + output = []; + output.push(b); + } + } + } + return output; +} + +function battle_step(active, initiative, candidate) { + game.battle_list = filter_battle_blocks(initiative, candidate); + if (game.battle_list) { + game.active = active; + return true; + } + return false; +} + +function pump_battle_step(is_candidate_attacker, is_candidate_defender) { + let attacker = game.attacker[game.where]; + let defender = ENEMY[attacker]; + + if (battle_step(defender, 'A', is_candidate_defender)) return; + if (battle_step(attacker, 'A', is_candidate_attacker)) return; + if (battle_step(defender, 'B', is_candidate_defender)) return; + if (battle_step(attacker, 'B', is_candidate_attacker)) return; + if (battle_step(defender, 'C', is_candidate_defender)) return; + if (battle_step(attacker, 'C', is_candidate_attacker)) return; + + next_combat_round(); +} + // FIELD BATTLE function goto_field_battle() { - console.log("FIELD BATTLE"); resume_field_battle(); } function resume_field_battle() { - game.state = 'field_battle'; - - function filter_battle_blocks(ci, is_candidate) { - let output = null; - for (let b in BLOCKS) { - if (is_candidate(b) && !game.moved[b]) { - if (block_initiative(b) == ci) { - if (!output) - output = []; - output.push(b); - } - } - } - return output; - } - game.active = game.p1; if (is_friendly_field(game.where)) { @@ -1783,26 +1875,8 @@ function resume_field_battle() { return goto_regroup(); } - function battle_step(active, initiative, candidate) { - game.battle_list = filter_battle_blocks(initiative, candidate); - if (game.battle_list) { - game.active = active; - return true; - } - return false; - } - - let attacker = game.attacker[game.where]; - let defender = ENEMY[attacker]; - - if (battle_step(defender, 'A', is_field_defender)) return; - if (battle_step(attacker, 'A', is_field_attacker)) return; - if (battle_step(defender, 'B', is_field_defender)) return; - if (battle_step(attacker, 'B', is_field_attacker)) return; - if (battle_step(defender, 'C', is_field_defender)) return; - if (battle_step(attacker, 'C', is_field_attacker)) return; - - next_combat_round(); + game.state = 'field_battle'; + pump_battle_step(is_field_attacker, is_field_defender); } states.field_battle = { @@ -1841,17 +1915,53 @@ states.field_battle = { // STORM BATTLE function goto_storm_battle() { - log("TODO: storm battle"); console.log("STORM BATTLE"); - next_combat_round(); + resume_storm_battle(); +} + +function resume_storm_battle() { + game.active = besieging_player(game.where); + + if (is_friendly_town(game.where)) { + console.log("STORM BATTLE WON BY ATTACKER", game.active); + log("Siege battle won by " + game.active); + return goto_regroup(); + } + + if (is_enemy_town(game.where)) { + console.log("STORM BATTLE WON BY DEFENDER", ENEMY[game.active]); + game.halfhit = null; + log("Storming repulsed."); + return next_combat_round(); + } + + game.state = 'storm_battle'; + pump_battle_step(is_storm_attacker, is_storm_defender); } -// BATTLE HITS +states.storm_battle = { + show_battle: true, + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to choose a combat action."; + view.prompt = "Field Battle: Choose a combat action."; + for (let b of game.battle_list) { + gen_action(view, 'block', b); // take default action + gen_action(view, 'battle_fire', b); + if (game.storming.includes(b)) + gen_action(view, 'battle_retreat', b); + } + }, + block: storm_fire_with_block, + battle_fire: storm_fire_with_block, + battle_retreat: storm_withdraw_with_block, +} + +// FIELD BATTLE HITS function goto_field_battle_hits() { game.active = ENEMY[game.active]; game.battle_list = list_field_victims(); - console.log("victim list = ", game.battle_list); if (game.battle_list.length == 0) resume_field_battle(); else @@ -1903,6 +2013,73 @@ function apply_field_battle_hit(who) { } } +// STORM BATTLE HITS + +function goto_storm_battle_hits() { + game.active = ENEMY[game.active]; + game.battle_list = list_storm_victims(); + if (game.battle_list.length == 0) + resume_storm_battle(); + else + game.state = 'storm_battle_hits'; +} + +function list_storm_victims() { + if (game.halfhit) + return [ game.halfhit ]; + let max = 0; + for (let b in BLOCKS) + if (block_owner(b) == game.active && is_storm_combatant(b) && game.steps[b] > max) + max = game.steps[b]; + let list = []; + for (let b in BLOCKS) + if (block_owner(b) == game.active && is_storm_combatant(b) && game.steps[b] == max) + list.push(b); + return list; +} + +states.storm_battle_hits = { + show_battle: true, + prompt: function (view, current) { + if (is_inactive_player(current)) + return view.prompt = "Waiting for " + game.active + " to assign hits."; + view.prompt = "Assign " + game.hits + (game.hits != 1 ? " hits" : " hit") + " to your armies."; + for (let b of game.battle_list) { + gen_action(view, 'battle_hit', b); + gen_action(view, 'block', b); + } + }, + battle_hit: apply_storm_battle_hit, + block: apply_storm_battle_hit, +} + +function apply_storm_battle_hit(who) { + if (block_plural(who)) + game.flash = block_name(who) + " take a hit."; + else + game.flash = block_name(who) + " takes a hit."; + if (game.halfhit == who) { + reduce_block(who, 'combat'); + game.halfhit = null; + } else { + if (is_block_in_castle(who)) + game.halfhit = who; + else + reduce_block(who, 'combat'); + } + game.hits--; + + if (game.hits == 0) + resume_storm_battle(); + else { + game.battle_list = list_storm_victims(); + if (game.battle_list.length == 0) + resume_storm_battle(); + else + game.flash += " " + game.hits + (game.hits == 1 ? " hit left." : " hits left."); + } +} + // BATTLE ACTIONS function roll_attack(active, b, verb, is_charge) { @@ -1973,6 +2150,19 @@ function field_fire_with_block(b) { } } +function storm_fire_with_block(b) { + game.moved[b] = true; + if (block_plural(b)) + roll_attack(game.active, b, "fire", 0); + else + roll_attack(game.active, b, "fires", 0); + if (game.hits > 0) { + goto_storm_battle_hits(); + } else { + resume_storm_battle(); + } +} + function charge_with_block(b) { game.moved[b] = true; if (block_plural(b)) @@ -1997,6 +2187,16 @@ function field_withdraw_with_block(b) { resume_field_battle(); } +function storm_withdraw_with_block(b) { + if (block_plural(b)) + log(game.active[0] + ": " + b + " withdraw."); + else + log(game.active[0] + ": " + b + " withdraws."); + game.moved[b] = true; + remove_from_array(game.storming, b); + resume_storm_battle(); +} + function harry_with_block(b) { // TODO: fire, hits, retreat OR fire, retreat, hits order ? if (block_plural(b)) @@ -2151,6 +2351,7 @@ function make_battle_view() { SA: [], SC: [], SR: [], storming: game.storming, sallying: game.sallying, + halfhit: game.halfhit, flash: game.flash }; @@ -2233,6 +2434,14 @@ exports.resign = function (state, current) { } } +function make_siege_view() { + let list = {}; + for (let t in TOWNS) + if (is_under_siege(t)) + list[t] = besieging_player(t); + return list; +} + exports.view = function(state, current) { game = state; @@ -2249,6 +2458,7 @@ exports.view = function(state, current) { steps: game.steps, reserves: game.reserves1.concat(game.reserves2), moved: game.moved, + sieges: make_siege_view(), battle: null, prompt: null, actions: null, @@ -526,15 +526,14 @@ function update_map() { let town = game.location[b]; if (town in TOWNS) { let moved = game.moved[b] ? " moved" : ""; - let castle = game.castle[b] ? " castle" : ""; - // TODO: show besieging blocks too! if (info.owner == player || info.owner == ASSASSINS) { let image = " known block_" + info.image; let steps = " r" + (info.steps - game.steps[b]); element.classList = info.owner + " block" + image + steps + moved; layout[town].south.push(element); } else { - element.classList = info.owner + " block" + moved; + let besieging = (game.sieges[town] == info.owner) ? " besieging" : ""; + element.classList = info.owner + " block" + moved + besieging; layout[town].north.push(element); } show_block(element); @@ -648,6 +647,11 @@ function update_battle() { ui.battle_menu[block].classList.add('treachery'); update_steps(block, steps, ui.battle_block[block], false); + + if (block == game.battle.halfhit) + ui.battle_block[block].classList.add("halfhit"); + else + ui.battle_block[block].classList.remove("halfhit"); if (reserve) ui.battle_block[block].classList.add("secret"); else @@ -691,10 +695,12 @@ function on_update() { show_action_button("#next_button", "next"); show_action_button("#pass_button", "pass"); show_action_button("#undo_button", "undo"); + show_action_button("#group_move_button", "group_move"); + show_action_button("#end_group_move_button", "end_group_move"); show_action_button("#sea_move_button", "sea_move"); + show_action_button("#end_sea_move_button", "end_sea_move"); show_action_button("#muster_button", "muster"); show_action_button("#end_muster_button", "end_muster"); - show_action_button("#end_sea_move_button", "end_sea_move"); show_action_button("#end_move_phase_button", "end_move_phase"); show_action_button("#end_regroup_button", "end_regroup"); show_action_button("#end_retreat_button", "end_retreat"); |