facehard 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- facehard/__init__.py +51 -0
- facehard/cli.py +339 -0
- facehard/emulate.py +1140 -0
- facehard/enums.py +265 -0
- facehard/model.py +3759 -0
- facehard/names.py +165 -0
- facehard-1.0.0.dist-info/METADATA +133 -0
- facehard-1.0.0.dist-info/RECORD +13 -0
- facehard-1.0.0.dist-info/WHEEL +5 -0
- facehard-1.0.0.dist-info/entry_points.txt +2 -0
- facehard-1.0.0.dist-info/licenses/LICENSE +151 -0
- facehard-1.0.0.dist-info/licenses/NOTICE +41 -0
- facehard-1.0.0.dist-info/top_level.txt +1 -0
facehard/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""FACEHARD — native Python port of Nathan Okun's face-hardened armour
|
|
2
|
+
penetration model (FACEHARD 8.0).
|
|
3
|
+
|
|
4
|
+
Public API:
|
|
5
|
+
calc(...) -> Limits ballistic limits only
|
|
6
|
+
results(...) -> Results limits + post-impact outcome/metrics
|
|
7
|
+
render_results(...) -> str original BASIC results narrative
|
|
8
|
+
penetration(...) -> float plate thickness defeated at a velocity
|
|
9
|
+
armor_info, proj_data parameter tables
|
|
10
|
+
names menu names (ARMORS / NATIONS / PROJECTILES)
|
|
11
|
+
Nation, Armor, BackingMetal, *Projectile named menu numbers (IntEnums);
|
|
12
|
+
projectiles(nation) -> that nation's enum
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from . import names
|
|
16
|
+
from .enums import (
|
|
17
|
+
Armor,
|
|
18
|
+
AustroHungarianProjectile,
|
|
19
|
+
BackingMetal,
|
|
20
|
+
BritishProjectile,
|
|
21
|
+
FrenchProjectile,
|
|
22
|
+
GermanProjectile,
|
|
23
|
+
ItalianProjectile,
|
|
24
|
+
JapaneseProjectile,
|
|
25
|
+
Nation,
|
|
26
|
+
RussianProjectile,
|
|
27
|
+
USProjectile,
|
|
28
|
+
projectiles,
|
|
29
|
+
)
|
|
30
|
+
from .model import (
|
|
31
|
+
Limits,
|
|
32
|
+
Results,
|
|
33
|
+
armor_info,
|
|
34
|
+
calc,
|
|
35
|
+
penetration,
|
|
36
|
+
proj_data,
|
|
37
|
+
render_results,
|
|
38
|
+
results,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__version__ = "1.0.0"
|
|
42
|
+
#: Version of Nathan Okun's original QuickBASIC FACEHARD this port reproduces.
|
|
43
|
+
MODEL_VERSION = "8.0"
|
|
44
|
+
__all__ = [
|
|
45
|
+
"calc", "results", "render_results", "penetration", "Limits", "Results",
|
|
46
|
+
"armor_info", "proj_data", "names", "__version__", "MODEL_VERSION",
|
|
47
|
+
"Nation", "Armor", "BackingMetal", "projectiles",
|
|
48
|
+
"USProjectile", "BritishProjectile", "GermanProjectile", "FrenchProjectile",
|
|
49
|
+
"ItalianProjectile", "JapaneseProjectile", "AustroHungarianProjectile",
|
|
50
|
+
"RussianProjectile",
|
|
51
|
+
]
|
facehard/cli.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""FACEHARD command-line interface.
|
|
2
|
+
|
|
3
|
+
facehard interactive wizard (menus)
|
|
4
|
+
facehard run [options] compute one impact; choose --output form
|
|
5
|
+
facehard pen [options] penetration thickness at a striking velocity
|
|
6
|
+
facehard list armors|nations|projectiles [--nation N]
|
|
7
|
+
|
|
8
|
+
Output forms for `run` (--output): narrative | limits | plug | metrics | json | all
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from . import names
|
|
18
|
+
from .model import Limits, Results, calc, penetration, proj_data, results
|
|
19
|
+
|
|
20
|
+
# --------------------------------------------------------------------------- #
|
|
21
|
+
# rendering #
|
|
22
|
+
# --------------------------------------------------------------------------- #
|
|
23
|
+
def _fmt(v, unit="", nd=0):
|
|
24
|
+
if v is None:
|
|
25
|
+
return "n/a"
|
|
26
|
+
if isinstance(v, float):
|
|
27
|
+
return f"{v:.{nd}f}{unit}"
|
|
28
|
+
return f"{v}{unit}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _header(s: dict) -> str:
|
|
32
|
+
a = names.ARMORS.get(s["armor"], "?")
|
|
33
|
+
nat = names.NATIONS.get(s["nation"], "?")
|
|
34
|
+
pj = names.PROJECTILES.get(s["nation"], {}).get(s["proj"], "?")
|
|
35
|
+
# `pen` computes the plate thickness, so it has no TA to show; other commands do.
|
|
36
|
+
ta = s.get("TA")
|
|
37
|
+
plate = f"{ta:g} in {a}" if ta is not None else a
|
|
38
|
+
lines = [
|
|
39
|
+
f"Plate : {plate}",
|
|
40
|
+
f"Projectile: {s['D']:g} in / {s['WT']:g} lb ({nat}, #{s['proj']}: {pj})",
|
|
41
|
+
f"Impact : {s['VS']:g} ft/s at {s['OB']:g} deg obliquity",
|
|
42
|
+
]
|
|
43
|
+
if s.get("wood") or s.get("cement") or s.get("metal"):
|
|
44
|
+
lines.append(f"Backing : wood {s['wood']:g} / cement {s['cement']:g} / "
|
|
45
|
+
f"metal {s['metal']:g} in (type {s['metal_type']}, "
|
|
46
|
+
f"{s['metal_plates']} plate)")
|
|
47
|
+
if s.get("remove", "none") != "none":
|
|
48
|
+
lines.append(f"Nose : {s['remove']} removed")
|
|
49
|
+
return "\n".join(lines)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render_limits(lim: Limits) -> str:
|
|
53
|
+
def row(tag, label, v):
|
|
54
|
+
return f" {tag} {label:<44} {_fmt(v,' ft/s')}"
|
|
55
|
+
eff = lim.vitru if lim.vitru is not None else "usually effective (see narrative)"
|
|
56
|
+
return "\n".join([
|
|
57
|
+
"BALLISTIC LIMITS (velocity to just defeat the plate):",
|
|
58
|
+
row("N1", "Navy BL, no shatter, all damage, given cap", lim.vltru),
|
|
59
|
+
row("N4", "Navy BL, unshattered/undeformed (best case)", lim.vlnd),
|
|
60
|
+
row("N2", "Navy BL with shatter + all damage", lim.vlshat),
|
|
61
|
+
row("N3", "Navy BL with shatter, no other damage", lim.vlshatmax),
|
|
62
|
+
row("H1", "Holing BL, no shatter, given cap", lim.vhtru),
|
|
63
|
+
row("H4", "Holing BL, unshattered/undeformed", lim.vhnd),
|
|
64
|
+
row("H2", "Holing BL with shatter", lim.vhshat),
|
|
65
|
+
row("H3", "Holing BL with shatter, no other damage", lim.vhshatmax),
|
|
66
|
+
f" -- {'shatter on impact?':<44} {'yes' if lim.shat else 'no'}",
|
|
67
|
+
])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def render_plug(res: Results) -> str:
|
|
71
|
+
return "\n".join([
|
|
72
|
+
"EJECTED ARMOUR PLUG:",
|
|
73
|
+
f" normal plug weight : {_fmt(res.norm_plug_wt,' lb',1)}",
|
|
74
|
+
f" delta plug weight : {_fmt(res.delta_plug_wt,' lb',1)}",
|
|
75
|
+
f" total plug weight : {_fmt(res.total_plug_wt,' lb',1)}",
|
|
76
|
+
f" normal plug velocity: {_fmt(res.norm_plug_vel,' ft/s')}",
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def render_metrics(res: Results) -> str:
|
|
81
|
+
eff = (res.effective_bl if isinstance(res.effective_bl, (int, float))
|
|
82
|
+
else res.effective_bl)
|
|
83
|
+
return "\n".join([
|
|
84
|
+
"METRICS:",
|
|
85
|
+
f" outcome : {res.outcome}",
|
|
86
|
+
f" exit angle (Ex) : {_fmt(res.ex,' deg',2)}",
|
|
87
|
+
f" deflection (OB-Ex) : {_fmt(res.obdf,' deg',2)}",
|
|
88
|
+
f" remaining velocity : {_fmt(res.remaining_vel,' ft/s')}",
|
|
89
|
+
f" effective BL : {eff if isinstance(eff,str) else _fmt(eff,' ft/s')}",
|
|
90
|
+
render_plug(res),
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def render_narrative(res: Results, s: dict) -> str:
|
|
95
|
+
lim = res.limits
|
|
96
|
+
out = []
|
|
97
|
+
gun = f"the {s['D']:g}-in shell"
|
|
98
|
+
if res.pen_flag == 2:
|
|
99
|
+
out.append(f"COMPLETE PENETRATION. At {s['VS']:g} ft/s and {s['OB']:g} deg, "
|
|
100
|
+
f"{gun} passes entirely through the {s['TA']:g}-in plate.")
|
|
101
|
+
if res.remaining_vel:
|
|
102
|
+
out.append(f"It exits at ~{res.remaining_vel:.0f} ft/s, deflected to an "
|
|
103
|
+
f"exit angle of {res.ex:.1f} deg (turned {res.obdf:.1f} deg "
|
|
104
|
+
f"from its line of flight).")
|
|
105
|
+
if res.total_plug_wt:
|
|
106
|
+
out.append(f"A ~{res.total_plug_wt:.0f} lb armour plug is punched out of "
|
|
107
|
+
f"the back at ~{res.norm_plug_vel:.0f} ft/s.")
|
|
108
|
+
elif res.pen_flag == 1:
|
|
109
|
+
out.append(f"HOLING ONLY. {gun.capitalize()} makes a hole through the plate "
|
|
110
|
+
f"but does not achieve a clean complete penetration; effect behind "
|
|
111
|
+
f"the plate is reduced.")
|
|
112
|
+
if res.total_plug_wt:
|
|
113
|
+
out.append(f"A ~{res.total_plug_wt:.0f} lb plug/spall is thrown from the back.")
|
|
114
|
+
else:
|
|
115
|
+
out.append(f"NO PENETRATION. At {s['VS']:g} ft/s {gun} fails to hole the "
|
|
116
|
+
f"{s['TA']:g}-in plate; only shock and possible back-spall behind it.")
|
|
117
|
+
if lim.shat:
|
|
118
|
+
if res.body_damage < 2:
|
|
119
|
+
out.append("The projectile suffers nose-only shatter, but its lower body "
|
|
120
|
+
"remains substantially intact.")
|
|
121
|
+
else:
|
|
122
|
+
out.append("The projectile shatters on impact (nose/body break-up).")
|
|
123
|
+
eff = res.effective_bl
|
|
124
|
+
eff_txt = eff if isinstance(eff, str) else f"~{eff:.0f} ft/s"
|
|
125
|
+
out.append(f"Navy (complete-pen) limit is ~{lim.vltru:.0f} ft/s; "
|
|
126
|
+
f"'effective' limit (pen + able to burst) is {eff_txt}.")
|
|
127
|
+
return "PREDICTION:\n " + "\n ".join(out)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def render(res: Results, s: dict, form: str) -> str:
|
|
131
|
+
lim = res.limits
|
|
132
|
+
if form == "limits":
|
|
133
|
+
return render_limits(lim)
|
|
134
|
+
if form == "plug":
|
|
135
|
+
return render_plug(res)
|
|
136
|
+
if form == "metrics":
|
|
137
|
+
return render_metrics(res)
|
|
138
|
+
if form == "json":
|
|
139
|
+
import dataclasses
|
|
140
|
+
d = dataclasses.asdict(res)
|
|
141
|
+
d["scenario"] = s
|
|
142
|
+
return json.dumps(d, indent=2, default=str)
|
|
143
|
+
if form == "all":
|
|
144
|
+
return "\n\n".join([render_narrative(res, s), render_limits(lim),
|
|
145
|
+
render_metrics(res)])
|
|
146
|
+
return render_narrative(res, s) # default
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# --------------------------------------------------------------------------- #
|
|
150
|
+
# scenario extraction #
|
|
151
|
+
# --------------------------------------------------------------------------- #
|
|
152
|
+
SCEN_KEYS = ("armor", "nation", "proj", "TA", "D", "WT", "WB", "OB", "VS",
|
|
153
|
+
"wood", "cement", "metal", "metal_type", "metal_plates",
|
|
154
|
+
"remove", "windscreen_wt", "caphead_wt")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _scen_from_args(a) -> dict:
|
|
158
|
+
return dict(armor=a.armor, nation=a.nation, proj=a.proj, TA=a.thickness,
|
|
159
|
+
D=a.diameter, WT=a.weight, WB=a.body_weight, OB=a.obliquity,
|
|
160
|
+
VS=a.velocity, wood=a.wood, cement=a.cement, metal=a.metal,
|
|
161
|
+
metal_type=a.metal_type, metal_plates=a.metal_plates,
|
|
162
|
+
remove=a.remove, windscreen_wt=a.windscreen_wt,
|
|
163
|
+
caphead_wt=a.caphead_wt)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _add_scenario_args(p, need_velocity=True, need_thickness=True):
|
|
167
|
+
p.add_argument("--armor", type=int, required=True, metavar="1-25")
|
|
168
|
+
p.add_argument("--nation", type=int, required=True, metavar="1-8")
|
|
169
|
+
p.add_argument("--proj", type=int, required=True, metavar="N")
|
|
170
|
+
# `pen` inverts the model to find thickness, so it does not take -t.
|
|
171
|
+
p.add_argument("-t", "--thickness", type=float, required=need_thickness,
|
|
172
|
+
help="plate TA, in")
|
|
173
|
+
p.add_argument("-d", "--diameter", type=float, required=True, help="calibre D, in")
|
|
174
|
+
p.add_argument("-w", "--weight", type=float, required=True, help="total weight WT, lb")
|
|
175
|
+
p.add_argument("-b", "--body-weight", type=float, required=True, dest="body_weight",
|
|
176
|
+
help="body weight WB, lb (< WT)")
|
|
177
|
+
p.add_argument("-o", "--obliquity", type=float, default=0.0, help="obliquity OB, deg")
|
|
178
|
+
if need_velocity:
|
|
179
|
+
p.add_argument("-v", "--velocity", type=float, required=True,
|
|
180
|
+
help="striking velocity VS, ft/s")
|
|
181
|
+
# backing
|
|
182
|
+
p.add_argument("--wood", type=float, default=0.0)
|
|
183
|
+
p.add_argument("--cement", type=float, default=0.0)
|
|
184
|
+
p.add_argument("--metal", type=float, default=0.0)
|
|
185
|
+
p.add_argument("--metal-type", type=int, default=5, dest="metal_type")
|
|
186
|
+
p.add_argument("--metal-plates", type=int, default=1, dest="metal_plates")
|
|
187
|
+
# nose-covering loss
|
|
188
|
+
p.add_argument("--remove", choices=["none", "cap", "windscreen", "caphead"],
|
|
189
|
+
default="none")
|
|
190
|
+
p.add_argument("--windscreen-wt", type=float, default=0.0, dest="windscreen_wt")
|
|
191
|
+
p.add_argument("--caphead-wt", type=float, default=0.0, dest="caphead_wt")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# --------------------------------------------------------------------------- #
|
|
195
|
+
# interactive wizard #
|
|
196
|
+
# --------------------------------------------------------------------------- #
|
|
197
|
+
def _pick(title: str, options: dict[int, str], default: int | None = None) -> int:
|
|
198
|
+
print(f"\n{title}")
|
|
199
|
+
for k in sorted(options):
|
|
200
|
+
print(f" {k:>3}. {options[k]}")
|
|
201
|
+
while True:
|
|
202
|
+
d = f" [{default}]" if default else ""
|
|
203
|
+
raw = input(f"Select{d}: ").strip()
|
|
204
|
+
if raw == "" and default is not None:
|
|
205
|
+
return default
|
|
206
|
+
if raw.isdigit() and int(raw) in options:
|
|
207
|
+
return int(raw)
|
|
208
|
+
print(" invalid selection")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _ask(prompt: str, cast=float, default=None):
|
|
212
|
+
while True:
|
|
213
|
+
d = f" [{default}]" if default is not None else ""
|
|
214
|
+
raw = input(f"{prompt}{d}: ").strip()
|
|
215
|
+
if raw == "" and default is not None:
|
|
216
|
+
return default
|
|
217
|
+
try:
|
|
218
|
+
return cast(raw)
|
|
219
|
+
except ValueError:
|
|
220
|
+
print(" invalid number")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def wizard() -> int:
|
|
224
|
+
print("FACEHARD interactive scenario builder\n" + "=" * 38)
|
|
225
|
+
armor = _pick("ARMOUR TYPE", names.ARMORS, default=14)
|
|
226
|
+
nation = _pick("PROJECTILE NATION", names.NATIONS, default=1)
|
|
227
|
+
proj = _pick("PROJECTILE TYPE", names.PROJECTILES[nation])
|
|
228
|
+
TA = _ask("Plate thickness (in)", float, 12.0)
|
|
229
|
+
D = _ask("Projectile calibre (in)", float, 16.0)
|
|
230
|
+
WT = _ask("Total weight (lb)", float, 2700.0)
|
|
231
|
+
WB = _ask("Body weight (lb, < total)", float, round(WT * 0.82, 1))
|
|
232
|
+
VS = _ask("Striking velocity (ft/s)", float, 2000.0)
|
|
233
|
+
OB = _ask("Impact obliquity (deg)", float, 0.0)
|
|
234
|
+
s = dict(armor=armor, nation=nation, proj=proj, TA=TA, D=D, WT=WT, WB=WB,
|
|
235
|
+
OB=OB, VS=VS, wood=0.0, cement=0.0, metal=0.0, metal_type=5,
|
|
236
|
+
metal_plates=1, remove="none", windscreen_wt=0.0, caphead_wt=0.0)
|
|
237
|
+
if input("\nAdd backing behind the plate? (y/N) ").strip().lower() == "y":
|
|
238
|
+
s["wood"] = _ask(" wood thickness (in)", float, 0.0)
|
|
239
|
+
s["cement"] = _ask(" cement thickness (in)", float, 0.0)
|
|
240
|
+
s["metal"] = _ask(" metal thickness (in)", float, 0.0)
|
|
241
|
+
if s["metal"] > 0:
|
|
242
|
+
s["metal_type"] = _ask(" metal type 1-5", int, 5)
|
|
243
|
+
s["metal_plates"] = _ask(" number of plates", int, 1)
|
|
244
|
+
form = _pick("OUTPUT", {1: "narrative", 2: "limits", 3: "metrics",
|
|
245
|
+
4: "everything", 5: "json"}, default=1)
|
|
246
|
+
form = {1: "narrative", 2: "limits", 3: "metrics", 4: "all", 5: "json"}[form]
|
|
247
|
+
res = results(**s)
|
|
248
|
+
print("\n" + _header(s) + "\n\n" + render(res, s, form))
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# --------------------------------------------------------------------------- #
|
|
253
|
+
# main #
|
|
254
|
+
# --------------------------------------------------------------------------- #
|
|
255
|
+
def main(argv=None) -> int:
|
|
256
|
+
p = argparse.ArgumentParser(
|
|
257
|
+
prog="facehard",
|
|
258
|
+
description="Face-hardened naval armour penetration (Nathan Okun's "
|
|
259
|
+
"FACEHARD 8.0, native Python port).")
|
|
260
|
+
p.add_argument("--version", action="store_true")
|
|
261
|
+
sub = p.add_subparsers(dest="cmd")
|
|
262
|
+
|
|
263
|
+
pr = sub.add_parser("run", help="compute one impact")
|
|
264
|
+
_add_scenario_args(pr)
|
|
265
|
+
pr.add_argument("-O", "--output",
|
|
266
|
+
choices=["narrative", "limits", "plug", "metrics", "json", "all"],
|
|
267
|
+
default="narrative")
|
|
268
|
+
|
|
269
|
+
pp = sub.add_parser("pen", help="penetration thickness at a striking velocity")
|
|
270
|
+
_add_scenario_args(pp, need_thickness=False) # pen computes the thickness
|
|
271
|
+
|
|
272
|
+
sub.add_parser("emulate", help="run the original FACEHARD interactive flow")
|
|
273
|
+
|
|
274
|
+
pl = sub.add_parser("list", help="list menu options")
|
|
275
|
+
pl.add_argument("what", choices=["armors", "nations", "projectiles"])
|
|
276
|
+
pl.add_argument("--nation", type=int, default=1)
|
|
277
|
+
|
|
278
|
+
args = p.parse_args(argv)
|
|
279
|
+
|
|
280
|
+
if args.version:
|
|
281
|
+
from . import MODEL_VERSION, __version__
|
|
282
|
+
print(f"facehard {__version__} (FACEHARD model {MODEL_VERSION})")
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
if args.cmd is None:
|
|
286
|
+
try:
|
|
287
|
+
return wizard()
|
|
288
|
+
except (KeyboardInterrupt, EOFError):
|
|
289
|
+
print("\naborted")
|
|
290
|
+
return 1
|
|
291
|
+
|
|
292
|
+
if args.cmd == "list":
|
|
293
|
+
if args.what == "armors":
|
|
294
|
+
for k in sorted(names.ARMORS):
|
|
295
|
+
print(f"{k:>3}. {names.ARMORS[k]}")
|
|
296
|
+
elif args.what == "nations":
|
|
297
|
+
for k in sorted(names.NATIONS):
|
|
298
|
+
print(f"{k}. {names.NATIONS[k]}")
|
|
299
|
+
else:
|
|
300
|
+
print(f"Projectiles for {names.NATIONS.get(args.nation, '?')}:")
|
|
301
|
+
for k in sorted(names.PROJECTILES.get(args.nation, {})):
|
|
302
|
+
print(f"{k:>3}. {names.PROJECTILES[args.nation][k]}")
|
|
303
|
+
return 0
|
|
304
|
+
|
|
305
|
+
if args.cmd == "emulate":
|
|
306
|
+
from .emulate import main as emulate_main
|
|
307
|
+
return emulate_main()
|
|
308
|
+
|
|
309
|
+
s = _scen_from_args(args)
|
|
310
|
+
if s["WB"] >= s["WT"] and s["remove"] == "none":
|
|
311
|
+
print("warning: body weight >= total weight (shell has no cap/windscreen)",
|
|
312
|
+
file=sys.stderr)
|
|
313
|
+
|
|
314
|
+
if args.cmd == "pen":
|
|
315
|
+
try:
|
|
316
|
+
t = penetration(armor=s["armor"], nation=s["nation"], proj=s["proj"],
|
|
317
|
+
D=s["D"], WT=s["WT"], WB=s["WB"], V=s["VS"], OB=s["OB"])
|
|
318
|
+
except ValueError as e:
|
|
319
|
+
print(f"error: {e}", file=sys.stderr)
|
|
320
|
+
return 1
|
|
321
|
+
a = names.ARMORS.get(s["armor"], "?")
|
|
322
|
+
print(_header(s))
|
|
323
|
+
print(f"\nPENETRATION: {t:.1f} in of [{a}] defeated at {s['VS']:g} ft/s, "
|
|
324
|
+
f"{s['OB']:g} deg (substantially intact, non-immobilised body basis).")
|
|
325
|
+
return 0
|
|
326
|
+
|
|
327
|
+
# run
|
|
328
|
+
res = results(**s)
|
|
329
|
+
if args.output == "json":
|
|
330
|
+
print(render(res, s, "json")) # pure JSON, no header
|
|
331
|
+
else:
|
|
332
|
+
print(_header(s))
|
|
333
|
+
print()
|
|
334
|
+
print(render(res, s, args.output))
|
|
335
|
+
return 0
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
if __name__ == "__main__":
|
|
339
|
+
raise SystemExit(main())
|