summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2021-06-21 20:48:31 +0200
committerTor Andersson <tor@ccxvii.net>2022-11-16 19:19:38 +0100
commit70dddf939d32eefdb70346febd40fa639ee3d39d (patch)
treed355915e0db8c89d2e33cfcc792914e29588ddb6
parent34d668d46988eccf279cece033f35533aa56904c (diff)
downloadcrusader-rex-70dddf939d32eefdb70346febd40fa639ee3d39d.tar.gz
crusader: Storming!
-rw-r--r--play.html26
-rw-r--r--rules.js352
-rw-r--r--ui.js14
3 files changed, 310 insertions, 82 deletions
diff --git a/play.html b/play.html
index 87339e9..3ff067d 100644
--- a/play.html
+++ b/play.html
@@ -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>
diff --git a/rules.js b/rules.js
index c37c2f6..7a64ea3 100644
--- a/rules.js
+++ b/rules.js
@@ -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,
diff --git a/ui.js b/ui.js
index a0273a6..80a056a 100644
--- a/ui.js
+++ b/ui.js
@@ -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");