Drill the militia; drill sergeant replaces rallying
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fd66b69401
commit
1c80bd80f7
6 changed files with 102 additions and 31 deletions
|
|
@ -50,7 +50,11 @@ Alpha. One season, bandit raids plus two friendly events, six buildings.
|
|||
replaced from the bank automatically.
|
||||
- **Events:** raids are announced a day or two ahead. Reconnaissance from
|
||||
scouts, informants and the spymaster's guild narrows down when, how strong,
|
||||
and finally where their camp is, so fighters can sabotage it. Each intel
|
||||
and finally where their camp is, so fighters can sabotage it. Fighters can
|
||||
also drill the militia before any raid, up to half a defence per
|
||||
townsperson. Favour never buys results directly: master builders,
|
||||
informants and drill sergeants double what players put in for a while,
|
||||
XP included. Each intel
|
||||
level takes more scouting than the last and more for a bigger band, and
|
||||
levels 2 and 3 only open about a day and eight hours before the raid
|
||||
arrives. Defeat kills or drives
|
||||
|
|
|
|||
3
app.py
3
app.py
|
|
@ -102,7 +102,7 @@ def player_view(world: dict, player: dict, now: float) -> dict:
|
|||
view["blessing"] = game.blessing(player)
|
||||
blessed = game.with_potion(view["blessing"], game.potion_effect(player, "combat"))
|
||||
view["fight_damage"] = {a["id"]: round(game.fight_damage(player, a, blessed), 1)
|
||||
for a in game.ACTIONS if a["skill"] == "combat"}
|
||||
for a in game.ACTIONS if "difficulty" in a}
|
||||
view["pp"] = int(player["pp"])
|
||||
view["labour_per_action"] = game.labour_per_action(player)
|
||||
view["levels"] = {skill: game.skill_level(player, skill) for skill in game.SKILL_IDS}
|
||||
|
|
@ -116,6 +116,7 @@ def player_view(world: dict, player: dict, now: float) -> dict:
|
|||
view["action"] = {**player["action"], "duration": game.action_time(world, player, action)}
|
||||
view["times"] = {action["id"]: game.action_time(world, player, action) for action in game.ACTIONS}
|
||||
view["blocked"] = {action["id"]: game.can_start(world, player, action) for action in game.ACTIONS}
|
||||
view["drill_per_action"] = game.drill_per_action(player)
|
||||
view["recon_gain"] = {action["id"]: game.recon_gain(world, action) for action in game.ACTIONS
|
||||
if action.get("special") == "recon"}
|
||||
return view
|
||||
|
|
|
|||
67
game.py
67
game.py
|
|
@ -371,6 +371,7 @@ ACTIONS = [
|
|||
_action("cook_meat", "cooking", "Cooked meat", 5, 20, 7, {"raw_meat": 1, "logs": 1}, {"cooked_meat": 1}),
|
||||
_action("cook_stew", "cooking", "Stew", 15, 35, 16,
|
||||
{"raw_meat": 1, "vegetables": 2, "logs": 1}, {"stew": 2}),
|
||||
_action("drill_militia", "combat", "Drill the militia", 1, 50, 10, special="drill"),
|
||||
_action("fight_wolves", "combat", "Wolves in the woods", 1, 60, 14, difficulty=2, damage=2, gold=2,
|
||||
drops=[["raw_meat", 1, 0.6], ["hide", 1, 0.5], ["bones", 1, 1.0]]),
|
||||
_action("fight_scouts", "combat", "Bandit scouts", 10, 75, 30, difficulty=14, damage=6, gold=10,
|
||||
|
|
@ -1016,7 +1017,13 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
|
|||
blocked = skill_mods(world, action)["blocked"]
|
||||
if blocked:
|
||||
return blocked
|
||||
if action["skill"] == "combat" and player["hp"] < max_hp(player) * TOO_HURT and not edible(player):
|
||||
if action.get("special") == "drill":
|
||||
threat = upcoming_raid(world)
|
||||
if not threat:
|
||||
return "No raid to prepare for"
|
||||
if threat.get("drilled", 0) >= drill_cap(world):
|
||||
return "The militia is as drilled as it can be"
|
||||
if "difficulty" in action and player["hp"] < max_hp(player) * TOO_HURT and not edible(player):
|
||||
return "Too hurt to fight. Rest, or bring food"
|
||||
return None
|
||||
|
||||
|
|
@ -1047,7 +1054,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
outputs[item] = int(exact) + (1 if rng.random() < exact - int(exact) else 0)
|
||||
xp *= mods["xp"]
|
||||
|
||||
if action["skill"] == "combat":
|
||||
if "difficulty" in action:
|
||||
blessed = blessing(player)
|
||||
if player["prayers"] and not blessed["cost"]:
|
||||
summary["prayer_out"] = True
|
||||
|
|
@ -1105,6 +1112,15 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
event["recon"] += gain + boost
|
||||
if action.get("special") == "problem" and action["skill"] != "combat":
|
||||
_work_problem(world, player, action, now)
|
||||
threat = upcoming_raid(world) if action.get("special") == "drill" else None
|
||||
if threat:
|
||||
city = world["city"]
|
||||
gain = drill_per_action(player)
|
||||
# A drill sergeant doubles the drilling (and the XP) of whoever does it while the boost lasts.
|
||||
boost = min(city.get("drill_boost", 0), gain)
|
||||
city["drill_boost"] = city.get("drill_boost", 0) - boost
|
||||
xp = DRILL_XP * (gain + boost) * mods["xp"]
|
||||
threat["drilled"] = min(drill_cap(world), threat.get("drilled", 0) + gain + boost)
|
||||
if action.get("special") == "labour":
|
||||
city = world["city"]
|
||||
site = city["construction"]
|
||||
|
|
@ -1125,6 +1141,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
|
|||
if action.get("special") == "offer":
|
||||
city = world["city"]
|
||||
city["devotion"] = city.get("devotion", 0) + action["devotion"] * (1 + 0.25 * building_level(world, "temple")) * bonus
|
||||
if action.get("special") != "drill":
|
||||
_use_charge(player, action["skill"], summary)
|
||||
|
||||
for item, qty in outputs.items():
|
||||
|
|
@ -1514,7 +1531,8 @@ def defence(world: dict) -> dict:
|
|||
"weapons": round(0.35 * best_kit(stock, "weapon", "power", armed), 1),
|
||||
"shields": round(0.5 * best_kit(stock, "shield", "armour", armed), 1),
|
||||
"arrows": min(100, stock.get("arrows", 0) // 10),
|
||||
"rallied": city.get("rally", 0),
|
||||
"drilled": round(drilled(world), 1),
|
||||
"rallied": city.get("rally", 0), # favour bought before drilling existed; gone after the next raid
|
||||
"ward": ward(world),
|
||||
"mercenaries": city.get("hired", 0),
|
||||
"garrison": 0,
|
||||
|
|
@ -2492,8 +2510,26 @@ def happenings_view(world: dict, now: float) -> dict:
|
|||
MASTER_BUILDER_LABOUR = 50 # boosted labour per hire: doubled labour and XP for whoever works it
|
||||
MASTER_BUILDER_FAVOUR = 150 # +50% for every further hire on the same site
|
||||
INFORMANT_FAVOUR = 250
|
||||
RALLY_FAVOUR = 150
|
||||
RALLY_DEFENCE = 10
|
||||
# Drilling: fighters train the townsfolk before a raid, a little defence per
|
||||
# session (more for a seasoned fighter), up to half the population.
|
||||
DRILL_PER_ACTION = 0.5 # plus as much again every 20 combat levels
|
||||
DRILL_XP = 20 # combat XP per point of defence drilled
|
||||
DRILL_SHARE = 0.5 # most drilled defence: this much per townsperson
|
||||
DRILL_SERGEANT_POINTS = 15 # boosted drilling per hire: doubled defence and XP
|
||||
DRILL_SERGEANT_FAVOUR = 150 # +50% for every further hire against the same threat
|
||||
|
||||
|
||||
def drill_per_action(player: dict) -> float:
|
||||
return DRILL_PER_ACTION * (1 + skill_level(player, "combat") // 20)
|
||||
|
||||
|
||||
def drill_cap(world: dict) -> float:
|
||||
return round(DRILL_SHARE * world["city"]["population"], 1)
|
||||
|
||||
|
||||
def drilled(world: dict) -> float:
|
||||
threat = upcoming_raid(world)
|
||||
return min(drill_cap(world), threat.get("drilled", 0)) if threat else 0
|
||||
|
||||
|
||||
def favours(world: dict, now: float) -> dict:
|
||||
|
|
@ -2517,10 +2553,13 @@ def favours(world: dict, now: float) -> dict:
|
|||
"recon": INFORMANT_RECON,
|
||||
"boost": round(city.get("recon_boost", 0)),
|
||||
},
|
||||
"militia": {
|
||||
"cost": RALLY_FAVOUR * (city.get("rallies", 0) + 1),
|
||||
"why": None if raid else "No raid to prepare for",
|
||||
"rallied": city.get("rally", 0),
|
||||
"sergeant": {
|
||||
"cost": round(DRILL_SERGEANT_FAVOUR * (1 + 0.5 * (raid or {}).get("sergeants", 0))),
|
||||
"why": "No raid to prepare for" if not raid else
|
||||
"The militia is as drilled as it can be" if drilled(world) >= drill_cap(world) else None,
|
||||
"drill": DRILL_SERGEANT_POINTS,
|
||||
"boost": round(city.get("drill_boost", 0), 1),
|
||||
"drilled": round(drilled(world), 1), "cap": drill_cap(world),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -2550,9 +2589,11 @@ def spend_favour(world: dict, player: dict, kind: str, now: float) -> str | None
|
|||
log(world, now, "favour", f"{player['name']} paid an informant: the next {INFORMANT_RECON} "
|
||||
f"reconnaissance counts double", player["id"])
|
||||
else:
|
||||
city["rally"] = city.get("rally", 0) + RALLY_DEFENCE
|
||||
city["rallies"] = city.get("rallies", 0) + 1
|
||||
log(world, now, "favour", f"{player['name']} rallied the militia (+{RALLY_DEFENCE} defence for the coming raid)", player["id"])
|
||||
raid = upcoming_raid(world)
|
||||
raid["sergeants"] = raid.get("sergeants", 0) + 1
|
||||
city["drill_boost"] = city.get("drill_boost", 0) + DRILL_SERGEANT_POINTS
|
||||
log(world, now, "favour", f"{player['name']} hired a drill sergeant: the next {DRILL_SERGEANT_POINTS} "
|
||||
f"drilled defence counts double", player["id"])
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -2625,7 +2666,7 @@ ACTION_GLYPHS = {
|
|||
"fight_ruins": ("castle-ruins", "#a8a29e"), "fight_stronghold": ("castle", "#8a8f98"),
|
||||
"fight_green_dragon": ("dragon-head", "#5fae5a"), "fight_blue_dragon": ("dragon-head", "#4f86d6"),
|
||||
"fight_red_dragon": ("dragon-head", "#d0503c"), "fight_black_dragon": ("dragon-head", "#8a8f98"),
|
||||
"sabotage": ("fire-bomb", "#e0703a"), "recon_roads": ("spyglass", "#6f8fec"), "recon_camps": ("spyglass", "#8a7fd0"),
|
||||
"sabotage": ("fire-bomb", "#e0703a"), "drill_militia": ("sword-brandish", "#e0a050"), "recon_roads": ("spyglass", "#6f8fec"), "recon_camps": ("spyglass", "#8a7fd0"),
|
||||
"labour": ("hammer-nails", "#d1a46a"), "clear_wolves": ("wolf-head", "#9aa1ab"),
|
||||
"catch_goblins": ("goblin-head", "#8fbf6a"), "drive_bear": ("bear-head", "#8a5a32"), "cull_boars": ("boar", "#a06a4a"),
|
||||
"catch_rats": ("rat", "#9aa1ab"), "clear_rockslide": ("falling-rocks", "#a8a29e"),
|
||||
|
|
|
|||
|
|
@ -487,12 +487,17 @@ function actionCard(a) {
|
|||
}
|
||||
io += Object.entries(a.outputs).map(([id, q]) => itemChip(id, q)).join("");
|
||||
let extra = "";
|
||||
if (a.skill === "combat") {
|
||||
if ("difficulty" in a) {
|
||||
const chance = Math.min(0.97, Math.max(0.15, 0.5 + (me.power - a.difficulty) * 0.04));
|
||||
extra = `<div class="small">Win chance <b>${Math.round(chance * 100)}%</b> · about <b>${me.fight_damage[a.id]}</b> damage a fight${a.gold ? ` · ${a.gold} gold` : ""}</div>`;
|
||||
if (a.drops) io += a.drops.map(([id, q, odds]) => `<span class="item-chip" title="${esc(item(id).name)}: ${Math.round(odds * 100)}% chance">${item(id).icon} ${q} <span class="have">${Math.round(odds * 100)}%</span></span>`).join("");
|
||||
if (a.special === "sabotage") extra += `<div class="small muted">Each win weakens the coming raid by 3%, up to 30%.</div>`;
|
||||
}
|
||||
if (a.special === "drill") {
|
||||
const s = S.favours.sergeant, boost = s.boost;
|
||||
extra = `<div class="small makes">${skillOf("combat").icon} +${me.drill_per_action} defence for the coming raid · ${fmt(s.drilled)} / ${fmt(s.cap)} drilled${boost ? ` <b class="boosted">×2 defence and XP for the next ${fmt(boost)}</b>` : ""}</div>
|
||||
<div class="small muted">More per session every 20 combat levels. The militia can take up to half a defence per townsperson.</div>`;
|
||||
}
|
||||
if (a.special === "recon") {
|
||||
const boost = S.favours.informant.boost;
|
||||
extra = `<div class="small makes">${skillOf("reconnaissance").icon} +${+me.recon_gain[a.id].toFixed(2)} recon on the coming threat${boost ? ` <b class="boosted">×2 recon and XP for the next ${fmt(boost)}</b>` : ""}</div>`;
|
||||
|
|
@ -519,7 +524,7 @@ function actionCard(a) {
|
|||
<div class="foot">
|
||||
<span class="why">${!active && why ? esc(why) : ""}</span>
|
||||
${active ? `<button class="btn danger" data-stop>Stop</button>`
|
||||
: `<span class="btns"><button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Do this once, then go back to what you were doing">${a.skill === "combat" ? "Fight once" : Object.keys(a.outputs).length ? "Make one" : "Once"}</button>
|
||||
: `<span class="btns"><button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Do this once, then go back to what you were doing">${"difficulty" in a ? "Fight once" : Object.keys(a.outputs).length ? "Make one" : "Once"}</button>
|
||||
<button class="btn primary" data-start="${a.id}" ${why ? "disabled" : ""}>Start</button></span>`}
|
||||
</div>
|
||||
</div>`;
|
||||
|
|
@ -617,7 +622,7 @@ function favourPanel() {
|
|||
return `<div class="panel"><h2>Favour with the council</h2>
|
||||
<p class="favour">${FAVOUR} ${fmt(me.favour)} favour · ${fmt(me.donated)} given in total</p>
|
||||
<div class="btns"><button class="btn" data-perk="bank_slot" ${me.favour < me.perk_costs.bank_slot ? "disabled" : ""}>+1 bank slot (${fmt(me.perk_costs.bank_slot)} favour)</button></div>
|
||||
<p class="small muted">Favour can also hire builders, pay informants and rally the militia: see <button class="linkish" data-nav="council">Council & threats</button>.</p></div>`;
|
||||
<p class="small muted">Favour can also hire master builders, informants and drill sergeants: see <button class="linkish" data-nav="council">Council & threats</button>.</p></div>`;
|
||||
}
|
||||
|
||||
// --- town -----------------------------------------------------------------
|
||||
|
|
@ -857,13 +862,13 @@ function favourSpends() {
|
|||
return `<div class="panel"><h2>Spend favour on the city · you have ${FAVOUR} ${fmt(me.favour)}</h2>
|
||||
${option("builders", "🏗️", "Hire a master builder", f.builders.why ? esc(f.builders.why) + "." : `The next ${f.builders.labour} labour anyone puts into the site counts double, with double Construction XP${f.builders.boost ? ` (${fmt(f.builders.boost)} still to use)` : ""}. Costs more each time on the same site.`)}
|
||||
${option("informant", "🕵️", "Pay an informant", f.informant.why ? esc(f.informant.why) + "." : `Leads for the scouts: the next ${f.informant.recon} reconnaissance on the coming threat counts double, with double XP${f.informant.boost ? ` (${fmt(f.informant.boost)} still to use)` : ""}. Costs more each time for the same threat.`)}
|
||||
${option("militia", "📯", "Rally the militia", f.militia.why ? esc(f.militia.why) + "." : `+10 defence for the coming raid${f.militia.rallied ? ` (+${f.militia.rallied} so far)` : ""}. Costs more each time.`)}
|
||||
${option("sergeant", "📯", "Hire a drill sergeant", f.sergeant.why ? esc(f.sergeant.why) + "." : `The next ${f.sergeant.drill} defence fighters drill into the militia counts double, with double combat XP${f.sergeant.boost ? ` (${fmt(f.sergeant.boost)} still to use)` : ""}. Costs more each time for the same threat.`)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function raidPrep(ev) {
|
||||
const city = S.city, me = S.me, d = city.defence;
|
||||
const names = { mercenaries: "Hired sellswords", ward: "Divine ward (temple)", rallied: "Rallied militia (favour)", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Militia (barracks)", weapons: "Swords in the armoury", shields: "Shields in the armoury", arrows: "Arrows", garrison: "Garrison" };
|
||||
const names = { mercenaries: "Hired sellswords", ward: "Divine ward (temple)", rallied: "Rallied militia (favour)", drilled: "Drilled militia", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Militia (barracks)", weapons: "Swords in the armoury", shields: "Shields in the armoury", arrows: "Arrows", garrison: "Garrison" };
|
||||
const reqs = (ev.requests || []).map((r) => {
|
||||
const mine = me.bank[r.item] || 0;
|
||||
return `<div class="req"><span>${item(r.item).icon}</span><span>${esc(r.label)} <span class="muted small">${fmt(r.have)} / ${fmt(r.want)}</span></span>
|
||||
|
|
@ -874,7 +879,7 @@ function raidPrep(ev) {
|
|||
<div class="panel"><h2>Council requests</h2>${reqs}
|
||||
<p class="small muted">Arrows, swords and shields in the stockpile arm the defenders: one sword and shield each, best first. Every fight uses up arrows and breaks some of the gear, more when it's close. Food keeps them fighting.</p></div>
|
||||
<div class="panel"><h2>Defence</h2>
|
||||
<table class="parts">${Object.entries(d.parts).map(([k, v]) => `<tr><td>${names[k]}</td><td>${fmt(v)}</td></tr>`).join("")}
|
||||
<table class="parts">${Object.entries(d.parts).filter(([k, v]) => k !== "rallied" || v).map(([k, v]) => `<tr><td>${names[k]}</td><td>${fmt(v)}</td></tr>`).join("")}
|
||||
${d.starving ? `<tr><td class="bad">Hungry (×0.8)</td><td></td></tr>` : ""}</table>
|
||||
<h2 style="margin-top:16px">Garrison</h2>
|
||||
<p class="small">${d.garrison.length ? d.garrison.map((g) => `${esc(g.name)} (${g.power})`).join(", ") : `<span class="muted">Nobody has signed up yet.</span>`}</p>
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@
|
|||
<symbol id="stone-block" viewBox="0 0 512 512"><path fill="currentColor" d="m209.875 44.156l-182 106.47l119.625 54.31l148.344 11.72l41.97-24.312l17.342 11.562L309 230.656V379.53l53.563-14.624l-64.625 51.97l-110.875-59.626l-2.157-1.53l-71.28 6.56l75.936-31.967l100.75 52.125v-147.5l-145.906-11.5l-1.625-.125l-1.5-.688l-121.093-55V391.47L44 423.186l82 20.97l21.875-21.282l11.156 29.72l131.282 33.592V434l4.25 2.28l5.47 2.94l4.812-3.908L309 431.97v52.155L491.375 377.78v-96.405L466.78 269.47l24.595-38.75V125l-90.25 52.28l-1.094 34.095l-88-58.688l84.97 5.375L476.5 112L291.562 64.937l1.625.563l-64.406 5.78l5.345-20.936l-24.25-6.188z"/></symbol>
|
||||
<symbol id="stone-stack" viewBox="0 0 512 512"><path fill="currentColor" d="M278.735 112.385c-6.45-.022-13.001.204-19.541.683c-26.16 1.918-52.24 7.912-71.295 17.338c-19.054 9.426-30.102 21.32-31.21 35.473c-.491 6.258 1.674 12.378 6.886 18.9s13.484 13.055 23.86 18.725c20.75 11.34 49.723 19.217 77.925 20.578s55.456-3.89 73.447-16.592c17.992-12.701 28.163-31.942 23.477-64.853c-.647-4.546-3.149-8.605-8.393-12.748c-5.244-4.144-13.144-7.946-22.834-10.828c-14.534-4.324-32.974-6.611-52.322-6.676m-58.385 27.773a15.021 30.242 71.137 0 1 16.541 7.426a15.021 30.242 71.137 0 1-23.646 24.008a15.021 30.242 71.137 0 1-33.557-4.356a15.021 30.242 71.137 0 1 23.647-24.006a15.021 30.242 71.137 0 1 17.015-3.072m41.203 101.729c-37.759-.033-75.243 5.18-102.252 16.006c-13.504 5.412-24.3 12.2-31.423 19.904c-7.123 7.703-10.792 16.021-10.834 26.406c-.027 6.585 2.827 11.537 10.013 16.832c7.187 5.295 18.653 9.967 32.887 13.277c28.467 6.621 67.726 8.057 106.684 5.051c38.957-3.005 77.792-10.437 105.572-20.668c13.89-5.115 24.986-11.006 31.701-16.691c6.715-5.686 8.59-10.002 8.066-14.201c-.602-4.834-4.196-10.34-12.113-16.06s-19.702-11.174-33.931-15.651c-28.459-8.955-66.61-14.172-104.37-14.205m-76.722 28a15.623 30.966 72.083 0 1 16.52 8.297a15.623 30.966 72.083 0 1-25.08 24.324a15.623 30.966 72.083 0 1-33.964-5.639a15.623 30.966 72.083 0 1 25.078-24.324a15.623 30.966 72.083 0 1 17.446-2.658m105.074 87.545a737 737 0 0 0-36.129.824c-48.367 2.29-96.637 9.383-132.682 20.898c-18.022 5.758-32.96 12.664-43.14 20.12c-10.181 7.455-15.225 14.927-16.135 22.431c-.771 6.358 2.69 13.296 12.693 21.38c10.004 8.086 25.876 16.155 45.407 23.048c39.06 13.786 92.694 23.167 145.267 24.806c52.574 1.64 104.231-4.626 139.022-20.488c17.395-7.93 30.393-18.095 37.974-30.289c7.582-12.194 10.317-26.587 6.166-45.414c-.984-4.464-5.912-10.175-16.506-15.58c-10.593-5.405-25.998-10.126-44.365-13.654c-27.55-5.292-61.777-8.017-97.572-8.082m-159.727 42.4a18.747 35.889 83.498 0 1 28.702 14.086a18.747 35.889 83.498 0 1-31.659 23.371a18.747 35.889 83.498 0 1-39.367-13.441a18.747 35.889 83.498 0 1 31.658-23.371a18.747 35.889 83.498 0 1 10.666-.645"/></symbol>
|
||||
<symbol id="stone-wall" viewBox="0 0 512 512"><path fill="currentColor" d="m18.08 23l.1 18H494V23zM254.3 54.93L133 61.6l30.1 68.5l98.1-3.6zm115.9.28l-92.9.53l3 71.96l96.8-.9zM18.23 56.43l.35 74.67l72.27-2.4l24.35-72.27zM393 64.24l-.5 47.16l15.7 21.5l85.8-6.3V64.93zM120.8 84.68l-17.6 49.82l41.1-1l-8.2-34.02zm354.7 56.02l-4 75.5l22.5-.6v-74.9zm-25.7 3.5l-97.3 10.7L346 219l110.7-3.2zm-243.8.7l-4 75.5l126.1-3.9l7-71.1zm-145.33 2.7l-3.9 75.5l126.13-3.9l-6.9-71.6zm-42 1.5l.33 72.4l24.77-.8l-6.9-71.6zm278.93 84.3L276 277.2l2.1 42l128.7-1.9l-7.6-79.7zm-40.2 1.4l-127.2 6l8 69.5l124.7 6.6zm236.6 3.1l-66.5 2.9l-7.5 75.8l74-5.2zm-474.89 7l.31 65.8l100.78-3.1l-6.9-62.7zm470.29 81.2l-115.9 10.5l-3.3 65l121.5-3.5zm-422.36 4.1l-47.52 2l.34 72.9l51.1-1.3zm17.84 0l3.98 68.5l138.64 9.6l-3-78.1zm161.22 4.3l-5.5 69l107.5 5.4l-1-73.5zm185.4 81.1l-4 75.5l66.5-.6v-73.8zm-9.8 1.2l-39 3.6l27.4 28zm-399.46 1.8l-2.13 40.6l.16 34.8H145.4v-68.9l-46.56-2.5l-18.4 14L68.56 421zm247.96.3l-111.3 1.5l3.5 72.5l114.7-2.4zm101.2 4.3l-72.2 5.9l-8.9 62.1l125.6.6l-6.2-27z"/></symbol>
|
||||
<symbol id="sword-brandish" viewBox="0 0 512 512"><path fill="currentColor" d="M122.1 17.52L186.6 161l104.5-44.9l-44.5-98.58h-51.1l35.8 80.75l-16.4 7.23l-39-87.98zm265.2 77.75L110.4 215.2l12 28.2l278.1-119.8zm-69.7 98.53l-23.9 11.5c12.8 5.4 26 13.7 38.3 15.6c18.1-13.1-4.9-30.5-14.4-27.1m-60.7 11.1l-36.2 16.7c-11.9 18.4-14.8 46-18.2 69.7c20.6 30.1 30.5 62.4 39.5 94c17.9-3.7 32.2-6.8 48.7-11.5c-24.4-38.7-42.2-78.4-53.6-118.9c0 0 5.6-10.5 8.4-15.8c11.9 7.3 24.9 12.4 36.6 9.9c10-4.1 21.5-7.9 27.7-16.6c-17.6-6.9-37.1-17.5-52.9-27.5m92.6 26.6c-10.8 5.8-23 11.2-31.9 18.9c-5.3 4.7-12.1 8.8-20.5 12.3c1.1 7.3-.1 19.6 8.1 18.8c20.5-6.2 40.5-15 59.7-28.4c-.1-9.2-8-24.1-15.4-21.6m-90.2 34.2c11.3 34.8 27.6 69 49.3 102.6c4.7-1.6 9.3-3.3 13.5-5c-7.3-6.2-11.7-15.9-12-25.3c-16.6-4.1-20.8-29.4-17.2-41.6c-9.5-7-13.6-18.1-14.2-28.7c-7.2.7-13.6-.1-19.4-2m103.3 10.4c-13.6 8.1-27.6 14.3-41.6 19.2l-9 4.4c-4.4 4.6-4.5 15.6 1 20.3c9.7 3.5 50.1-21.2 57.7-27.9c1.3-5.8-3.5-14.9-8.1-16m-178.1 21.2L19.1 335.8V495c95.7-25.3 146.9-47.1 209.7-90.2c-10.8-38-20.5-74.8-44.3-107.5m187.8 16.9c-13.6 10.2-27.2 16.2-42.9 21.4c-.4 6.7 2.5 18.1 12.1 16c19.9-4.3 36.3-17.5 36.6-24.6c.2-3.7-2-10.5-5.8-12.8m-7.5 48.6c-14.3 8.2-31.5 15.9-46.7 21.4l18.7 31.8l39.3-18.8zm22.2 49.1L342.9 433l3.1 21.9c15.1.5 46.6-11.3 55.4-26.1v-.1z"/></symbol>
|
||||
<symbol id="temple-gate" viewBox="0 0 512 512"><path fill="currentColor" d="M256 26.92L28.98 183H483zm0 26.29L427.9 167H84.1zm0 21.58L143.9 149h224.2zM73 201v286h30V201zm48 0v286h126V377H137v-54h110V201zm144 0v122h110v54H265v110h126V201zm144 0v286h30V201zM155 341v18h202v-18zm37 50c13.7 0 25 11.3 25 25s-11.3 25-25 25s-25-11.3-25-25s11.3-25 25-25m128 0c13.7 0 25 11.3 25 25s-11.3 25-25 25s-25-11.3-25-25s11.3-25 25-25m-128 18c-4 0-7 3-7 7s3 7 7 7s7-3 7-7s-3-7-7-7m128 0c-4 0-7 3-7 7s3 7 7 7s7-3 7-7s-3-7-7-7"/></symbol>
|
||||
<symbol id="tombstone" viewBox="0 0 512 512"><path fill="currentColor" d="M256 46c-81.822 0-150 63.366-150 150v150c0 6.668-.757 23.558 0 30h300c.757-6.442 0-23.332 0-30V196c0-86.634-68.178-150-150-150M16 406v60h480v-60z"/></symbol>
|
||||
<symbol id="treasure-map" viewBox="0 0 512 512"><path fill="currentColor" d="M227.4 34.7c-10.1 0-20.2.2-30.2.5l6.1 65.6l-61.1-62.5c-31.3 2.5-62.5 6.6-93.8 12.5l34.2 28.4l-48-.6c35.1 100.2 6.9 182.6-.3 292.1L130 476.5c10-1.3 19.9-2.4 29.6-3.3l21.5-42.2l18.6 28.8l41.5-33.5l.8 43c82.9-.2 157.7 9.1 235.7 7.9c-28.2-73-31.2-143.6-31.9-209.2l-33.3-19.1l32.7-33.9c-.4-21.3-1.3-42-3.6-61.9l-57.4.7l50.2-41.7c-3.8-15.5-9-30.4-16.1-44.7l-29.5-23.9C335 38 281.2 34.6 227.4 34.7m58.7 37c10.6 24.75 21.1 49.5 31.7 74.3c7.5-10.5 14.9-21 22.4-31.5c16 27.2 32 54.3 48 81.5l-16.2 9.5l-33.3-56.7l-42.5 59.4l-15.2-10.9l24-33.5l-21.9-51.5l-24.6 40.1l12 22.6l-16.5 8.8l-18.3-34.5l-24.8 58.2l-17.2-7.4l32.5-76.2l7.7-18c4.8 9.2 9.6 18.3 14.5 27.4c12.5-20.6 25.1-41.11 37.7-61.6M91.2 128c6.72 1.6 13.4 3.4 19.2 5.3c-2.1 5.9-4.1 11.8-6.2 17.6c-5.79-1.6-11.72-3.4-16.9-4.7c1.39-6 2.62-12.1 3.9-18.2m37.9 13.4c6.3 3.8 12 7.2 17 12.8L132.6 167c-4-3.7-8.6-7-12.8-9.4zm28.7 32.3c2.1 7.4 2.1 15.7 1.6 22.5l-18.5-2.4c.1-5.1.3-10-1-14.5zm-21.2 35.7l17.2 7.1c-3.3 6.6-5.1 12.7-8.6 17.8l-16.3-9c2.6-5.4 5.6-10.8 7.7-15.9m-16.5 34.1l17.7 6.1c-1.5 5.4-3 11.2-3.6 16.2l-18.6-2c1.3-7.5 2.1-14 4.5-20.3m207.8 17.4c8.5 1 14.6 3 21.7 7.1l-9.8 16c-4.1-2.8-9.4-3.8-13.5-4.5zm-21.2 1.5c1.1 6.1 2.5 12.2 3.9 18.3c-5.9 1.3-11.7 3.3-16.5 5.1l-6.8-17.4c6.7-2.4 13.5-4.7 19.4-6m-37.9 15.9l11 15.1c-5.6 4-11.8 7.8-16.8 10.6l-8.9-16.4c5.1-2.9 10.6-6.3 14.7-9.3M135.3 281c1.5 4.7 4.2 9.2 6.9 12.1l-13.8 12.6c-5.5-5.7-9.5-13.5-11.2-20.1zm230.3 3.3c3.5 6.4 6.8 12.7 8.7 19.1l-17.8 5.6c-2-5.4-4.3-10.8-6.8-14.8zm-127.4 10.9l6.9 17.3c-6.4 2.7-12.9 4.8-18.6 6.5l-5-18c5.9-1.6 11.3-3.8 16.7-5.8m-83.8 6.2c5.3 1.7 10.8 3.4 15.7 4.2c-1.2 6.1-2 12.3-2.8 18.5c-7-1-14.5-3.3-20.5-5.7zm50 3.5l2.8 18.5c-7.2 1.3-13.4 1.6-19.8 1.9l-.4-18.7c5.9-.2 11.6-.8 17.4-1.7m174.5 18c1 6.4 1.6 12.9 2.2 19.3l-18.7 1.5c-.4-6-.9-11.9-2-17.8zm-67.6 30.8c18.9 3.5 44.9 16.2 68.9 33.9c7.4-9.9 14.4-20.4 21.3-31.1l30.1 12.9c-4.7 12.3-15 25.6-28.6 37.2c17 16.2 30.9 34.5 37 53c-13.8-18.1-31.1-31.8-50.3-42.8c-23.4 15.8-52.7 25.9-79.6 20.4c22.9-4.4 40.6-16.6 55.8-32.6c-16.5-7.5-33.8-13.9-51.3-20.1z"/></symbol>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 164 KiB |
|
|
@ -592,20 +592,39 @@ class FavourTest(unittest.TestCase):
|
|||
event["recon"] = game.intel_costs(event)[-1]
|
||||
self.assertEqual(game.spend_favour(world, p, "informant", T0), "Nothing more to learn")
|
||||
|
||||
def test_rallies_cost_more_each_time_and_end_with_the_raid(self):
|
||||
def test_drilling_adds_defence_up_to_half_the_population(self):
|
||||
world, p = self.world, self.p
|
||||
self.assertEqual(game.spend_favour(world, p, "militia", T0), "No raid to prepare for")
|
||||
self.assertEqual(game.start_action(world, p, "drill_militia", T0), "No raid to prepare for")
|
||||
world["next_event_at"] = T0
|
||||
game.tick(world, T0, random.Random(2))
|
||||
world["city"]["population"] = 20.0
|
||||
before = game.defence(world)["total"]
|
||||
game.spend_favour(world, p, "militia", T0)
|
||||
self.assertEqual(game.favours(world, T0)["militia"]["cost"], 300)
|
||||
game.spend_favour(world, p, "militia", T0)
|
||||
self.assertEqual(game.defence(world)["total"], before + 20)
|
||||
self.assertEqual(p["favour"], 5000 - 150 - 300)
|
||||
self.assertIsNone(game.start_action(world, p, "drill_militia", T0))
|
||||
game.process_player(world, p, T0 + 50 * 4 + 1, random.Random(1))
|
||||
self.assertEqual(game.defence(world)["parts"]["drilled"], 2.0)
|
||||
self.assertEqual(game.defence(world)["total"], before + 2)
|
||||
self.assertEqual(p["xp"]["combat"], 40)
|
||||
self.assertEqual(p["hp"], 44) # drilling is not a fight
|
||||
game.process_player(world, p, T0 + 50 * 40, random.Random(1))
|
||||
self.assertEqual(game.defence(world)["parts"]["drilled"], 10.0)
|
||||
self.assertIsNone(p["action"]) # stops once the militia is fully drilled
|
||||
game.tick(world, game.upcoming_raid(world)["fire_at"], random.Random(1))
|
||||
self.assertEqual(game.defence(world)["parts"]["rallied"], 0)
|
||||
self.assertEqual(game.favours(world, T0)["militia"]["cost"], 150)
|
||||
self.assertEqual(game.defence(world)["parts"]["drilled"], 0)
|
||||
|
||||
def test_drill_sergeant_doubles_drilling_for_a_while(self):
|
||||
world, p = self.world, self.p
|
||||
self.assertEqual(game.spend_favour(world, p, "sergeant", T0), "No raid to prepare for")
|
||||
world["next_event_at"] = T0
|
||||
game.tick(world, T0, random.Random(2))
|
||||
self.assertIsNone(game.spend_favour(world, p, "sergeant", T0))
|
||||
self.assertEqual(game.defence(world)["parts"]["drilled"], 0) # a sergeant drills nobody by himself
|
||||
self.assertEqual(game.favours(world, T0)["sergeant"]["cost"], 225)
|
||||
game.start_action(world, p, "drill_militia", T0)
|
||||
game.process_player(world, p, T0 + 50 * 4 + 1, random.Random(1))
|
||||
self.assertEqual(game.upcoming_raid(world)["drilled"], 4.0)
|
||||
self.assertEqual(p["xp"]["combat"], 80)
|
||||
self.assertEqual(world["city"]["drill_boost"], 13)
|
||||
self.assertEqual(p["favour"], 5000 - 150)
|
||||
|
||||
def test_not_enough_favour(self):
|
||||
self.p["favour"] = 10
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue