amar-rpg 2.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE +675 -0
- data/README.md +155 -0
- data/amar-tui.rb +8195 -0
- data/cli_enc_output.rb +87 -0
- data/cli_enc_output_new.rb +433 -0
- data/cli_enc_output_new_3tier.rb +198 -0
- data/cli_enc_output_new_compact.rb +238 -0
- data/cli_name_gen.rb +21 -0
- data/cli_npc_output.rb +279 -0
- data/cli_npc_output_new.rb +700 -0
- data/cli_town_output.rb +39 -0
- data/cli_weather_output.rb +36 -0
- data/includes/class_enc.rb +341 -0
- data/includes/class_enc_new.rb +512 -0
- data/includes/class_monster_new.rb +551 -0
- data/includes/class_npc.rb +1378 -0
- data/includes/class_npc_new.rb +1187 -0
- data/includes/class_npc_new.rb.backup +706 -0
- data/includes/class_npc_new_skills.rb +153 -0
- data/includes/class_town.rb +237 -0
- data/includes/d6s.rb +40 -0
- data/includes/equipment_tables.rb +120 -0
- data/includes/functions.rb +67 -0
- data/includes/includes.rb +30 -0
- data/includes/randomizer.rb +15 -0
- data/includes/spell_catalog.rb +441 -0
- data/includes/tables/armour.rb +13 -0
- data/includes/tables/chartype.rb +4412 -0
- data/includes/tables/chartype_new.rb +765 -0
- data/includes/tables/chartype_new_full.rb +2713 -0
- data/includes/tables/enc_specific.rb +168 -0
- data/includes/tables/enc_type.rb +17 -0
- data/includes/tables/encounters.rb +99 -0
- data/includes/tables/magick.rb +169 -0
- data/includes/tables/melee.rb +36 -0
- data/includes/tables/missile.rb +17 -0
- data/includes/tables/monster_stats_new.rb +264 -0
- data/includes/tables/month.rb +18 -0
- data/includes/tables/names.rb +21 -0
- data/includes/tables/personality.rb +12 -0
- data/includes/tables/race_templates.rb +318 -0
- data/includes/tables/religions.rb +266 -0
- data/includes/tables/spells_new.rb +496 -0
- data/includes/tables/tier_system.rb +104 -0
- data/includes/tables/town.rb +71 -0
- data/includes/tables/weather.rb +41 -0
- data/includes/town_relations.rb +127 -0
- data/includes/weather.rb +108 -0
- data/includes/weather2latex.rb +114 -0
- data/lib/rcurses.rb +33 -0
- metadata +157 -0
@@ -0,0 +1,700 @@
|
|
1
|
+
# Compact output module for new 3-tier NPC system with full details
|
2
|
+
require 'io/console'
|
3
|
+
|
4
|
+
# Try to load string_extensions for color support
|
5
|
+
begin
|
6
|
+
require 'string_extensions'
|
7
|
+
rescue LoadError
|
8
|
+
# Define a fallback pure method if string_extensions is not available
|
9
|
+
class String
|
10
|
+
def pure
|
11
|
+
self.gsub(/\e\[[0-9;]*m/, '')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def npc_output_new(n, cli, custom_width = nil)
|
17
|
+
# Clear screen before displaying character (only for true CLI mode)
|
18
|
+
if cli == "cli_direct"
|
19
|
+
system("clear") || system("cls") # Works on both Unix and Windows
|
20
|
+
end
|
21
|
+
|
22
|
+
f = ""
|
23
|
+
|
24
|
+
# Use UTF-8 box drawing characters (no side borders for easy copy-paste)
|
25
|
+
# Adjust width based on output mode
|
26
|
+
if custom_width
|
27
|
+
width = custom_width
|
28
|
+
elsif cli == "cli_direct"
|
29
|
+
width = 120
|
30
|
+
else
|
31
|
+
width = 80 # Default for other modes
|
32
|
+
end
|
33
|
+
|
34
|
+
# Define colors if terminal output
|
35
|
+
if cli == "cli"
|
36
|
+
# Colors for different elements
|
37
|
+
@header_color = "\e[1;36m" # Bright cyan
|
38
|
+
@char_color = "\e[1;33m" # Bright yellow
|
39
|
+
@attr_color = "\e[1;35m" # Bright magenta
|
40
|
+
@skill_color = "\e[0;37m" # White
|
41
|
+
@stat_color = "\e[1;32m" # Bright green
|
42
|
+
@weapon_color = "\e[38;5;202m" # Brighter red (202)
|
43
|
+
@spell_color = "\e[0;34m" # Blue
|
44
|
+
@desc_color = "\e[38;5;229m" # Light color for description
|
45
|
+
@reset = "\e[0m"
|
46
|
+
else
|
47
|
+
@header_color = @char_color = @attr_color = @skill_color = ""
|
48
|
+
@stat_color = @weapon_color = @spell_color = @desc_color = @reset = ""
|
49
|
+
end
|
50
|
+
|
51
|
+
# Compact header: Name (sex age) H/W: height/weight with right-justified type
|
52
|
+
name_hw_part = "#{n.name} (#{n.sex} #{n.age}) H/W: #{n.height}cm/#{n.weight}kg"
|
53
|
+
type_part = "#{n.type} (#{n.level})"
|
54
|
+
spaces_needed = width - name_hw_part.length - type_part.length
|
55
|
+
f += "#{@header_color}#{name_hw_part}#{' ' * spaces_needed}#{type_part}#{@reset}\n"
|
56
|
+
|
57
|
+
# Area and Social Status on same line, with status right-justified
|
58
|
+
social_status = generate_social_status(n.type, n.level)
|
59
|
+
area_part = "Area: #{n.area}"
|
60
|
+
status_part = "Social Status: #{social_status}"
|
61
|
+
spaces_needed = width - area_part.length - status_part.length
|
62
|
+
f += "#{area_part}#{' ' * spaces_needed}#{status_part}\n"
|
63
|
+
|
64
|
+
|
65
|
+
# Add religion line for priests and anyone with Attunement
|
66
|
+
attunement_level = 0
|
67
|
+
if n.tiers && n.tiers["SPIRIT"] && n.tiers["SPIRIT"]["Attunement"]
|
68
|
+
attunement_level = n.tiers["SPIRIT"]["Attunement"]["level"] || 0
|
69
|
+
end
|
70
|
+
|
71
|
+
# Check if NPC should have religion info (skip for monsters)
|
72
|
+
is_humanoid = !n.type.to_s.match(/Monster:|Animal:|monster/i)
|
73
|
+
if is_humanoid && (["Priest", "Clergyman", "Monk"].include?(n.type) || (n.respond_to?(:has_magic?) && n.has_magic?) || attunement_level > 0)
|
74
|
+
cult_info = generate_cult_info(n.type, n.level, attunement_level, n.sex)
|
75
|
+
f += "#{cult_info}\n" if cult_info
|
76
|
+
end
|
77
|
+
|
78
|
+
# Add description line with correct light color (229)
|
79
|
+
description = n.description && !n.description.empty? ? n.description : generate_description(n.type, n.level, n.sex)
|
80
|
+
if cli == "cli"
|
81
|
+
f += "#{@desc_color}Description: #{description}#{@reset}\n"
|
82
|
+
else
|
83
|
+
f += "Description: #{description}\n"
|
84
|
+
end
|
85
|
+
f += "─" * width + "\n"
|
86
|
+
|
87
|
+
# Three-column layout for skills
|
88
|
+
col_width = 40
|
89
|
+
columns = [[], [], []]
|
90
|
+
|
91
|
+
# BODY hierarchy with non-zero skills
|
92
|
+
body_char = n.get_characteristic('BODY')
|
93
|
+
body_lines = ["#{@char_color}BODY (#{body_char.to_s.rjust(2)})#{@reset}"]
|
94
|
+
n.tiers["BODY"].each do |attr_name, attr_data|
|
95
|
+
# Skip if attr_data is not a hash or doesn't have skills
|
96
|
+
next unless attr_data.is_a?(Hash) && attr_data["skills"].is_a?(Hash)
|
97
|
+
|
98
|
+
non_zero_skills = attr_data["skills"].select { |_, v| v > 0 }
|
99
|
+
next if non_zero_skills.empty?
|
100
|
+
|
101
|
+
attr_level = attr_data["level"]
|
102
|
+
body_lines << " #{@attr_color}#{attr_name} (#{attr_level.to_s.rjust(2)})#{@reset}"
|
103
|
+
non_zero_skills.each do |skill_name, skill_level|
|
104
|
+
total = body_char + attr_level + skill_level
|
105
|
+
skill_display = " #{skill_name} (#{skill_level.to_s.rjust(2)})"
|
106
|
+
body_lines << "#{@skill_color}#{skill_display.ljust(30)}: #{total.to_s.rjust(2)}#{@reset}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# MIND hierarchy with non-zero skills
|
111
|
+
mind_char = n.get_characteristic('MIND')
|
112
|
+
mind_lines = ["#{@char_color}MIND (#{mind_char.to_s.rjust(2)})#{@reset}"]
|
113
|
+
n.tiers["MIND"].each do |attr_name, attr_data|
|
114
|
+
# Skip if attr_data is not a hash or doesn't have skills
|
115
|
+
next unless attr_data.is_a?(Hash) && attr_data["skills"].is_a?(Hash)
|
116
|
+
|
117
|
+
non_zero_skills = attr_data["skills"].select { |_, v| v > 0 }
|
118
|
+
next if non_zero_skills.empty?
|
119
|
+
|
120
|
+
attr_level = attr_data["level"]
|
121
|
+
mind_lines << " #{@attr_color}#{attr_name} (#{attr_level.to_s.rjust(2)})#{@reset}"
|
122
|
+
non_zero_skills.each do |skill_name, skill_level|
|
123
|
+
total = mind_char + attr_level + skill_level
|
124
|
+
skill_display = " #{skill_name} (#{skill_level.to_s.rjust(2)})"
|
125
|
+
mind_lines << "#{@skill_color}#{skill_display.ljust(30)}: #{total.to_s.rjust(2)}#{@reset}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# SPIRIT hierarchy with non-zero skills
|
130
|
+
spirit_char = n.get_characteristic("SPIRIT")
|
131
|
+
spirit_lines = []
|
132
|
+
spirit_attrs_with_skills = n.tiers["SPIRIT"].select do |_, attr_data|
|
133
|
+
attr_data.is_a?(Hash) && attr_data["skills"].is_a?(Hash) && attr_data["skills"].any? { |_, v| v > 0 }
|
134
|
+
end
|
135
|
+
|
136
|
+
if !spirit_attrs_with_skills.empty?
|
137
|
+
spirit_lines << "#{@char_color}SPIRIT (#{spirit_char.to_s.rjust(2)})#{@reset}"
|
138
|
+
n.tiers["SPIRIT"].each do |attr_name, attr_data|
|
139
|
+
# Skip if attr_data is not a hash or doesn't have skills
|
140
|
+
next unless attr_data.is_a?(Hash) && attr_data["skills"].is_a?(Hash)
|
141
|
+
|
142
|
+
non_zero_skills = attr_data["skills"].select { |_, v| v > 0 }
|
143
|
+
next if non_zero_skills.empty?
|
144
|
+
|
145
|
+
attr_level = attr_data["level"]
|
146
|
+
spirit_lines << " #{@attr_color}#{attr_name} (#{attr_level.to_s.rjust(2)})#{@reset}"
|
147
|
+
non_zero_skills.each do |skill_name, skill_level|
|
148
|
+
total = spirit_char + attr_level + skill_level
|
149
|
+
skill_display = " #{skill_name} (#{skill_level.to_s.rjust(2)})"
|
150
|
+
spirit_lines << "#{@skill_color}#{skill_display.ljust(30)}: #{total.to_s.rjust(2)}#{@reset}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Distribute content across three columns more evenly
|
156
|
+
all_lines = []
|
157
|
+
all_lines.concat(body_lines) unless body_lines.length == 1
|
158
|
+
all_lines.concat(mind_lines) unless mind_lines.length == 1
|
159
|
+
all_lines.concat(spirit_lines) unless spirit_lines.empty?
|
160
|
+
|
161
|
+
# Calculate lines per column for even distribution
|
162
|
+
total_lines = all_lines.length
|
163
|
+
lines_per_col = (total_lines / 3.0).ceil
|
164
|
+
|
165
|
+
# Fill columns
|
166
|
+
all_lines.each_with_index do |line, idx|
|
167
|
+
col_idx = idx / lines_per_col
|
168
|
+
col_idx = 2 if col_idx > 2
|
169
|
+
columns[col_idx] << line
|
170
|
+
end
|
171
|
+
|
172
|
+
# Balance columns
|
173
|
+
max_lines = columns.map(&:length).max
|
174
|
+
columns.each { |col| col.concat([""] * (max_lines - col.length)) }
|
175
|
+
|
176
|
+
# Print three columns with vertical separators
|
177
|
+
max_lines.times do |i|
|
178
|
+
line = ""
|
179
|
+
columns.each_with_index do |col, idx|
|
180
|
+
# Strip color codes for length calculation
|
181
|
+
plain_text = col[i].to_s.gsub(/\e\[[0-9;]*m/, '')
|
182
|
+
padding = col_width - plain_text.length
|
183
|
+
line += col[i].to_s + " " * padding
|
184
|
+
line += "│" if idx < 2
|
185
|
+
end
|
186
|
+
f += line.rstrip + "\n"
|
187
|
+
end
|
188
|
+
|
189
|
+
# Stats line
|
190
|
+
f += "─" * width + "\n"
|
191
|
+
|
192
|
+
# Calculate proper encumbrance based on weight carried
|
193
|
+
total_weight = calculate_total_weight(n)
|
194
|
+
|
195
|
+
# Calculate carrying capacity based on Strength
|
196
|
+
strength = n.tiers["BODY"]["Strength"]["level"] || 2
|
197
|
+
base_capacity = strength * 5 # Base carrying capacity in kg
|
198
|
+
|
199
|
+
# Calculate ENC penalty based on Amar rules
|
200
|
+
enc_penalty = if total_weight <= base_capacity
|
201
|
+
0 # No penalty under normal load
|
202
|
+
elsif total_weight <= base_capacity * 2
|
203
|
+
-1 # Light encumbrance
|
204
|
+
elsif total_weight <= base_capacity * 5
|
205
|
+
-3 # Medium encumbrance
|
206
|
+
elsif total_weight <= base_capacity * 10
|
207
|
+
-5 # Heavy encumbrance
|
208
|
+
else
|
209
|
+
-7 # Extreme encumbrance (not -10)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Format SIZE for display (3.5 becomes "3½")
|
213
|
+
size_display = n.SIZE % 1 == 0.5 ? "#{n.SIZE.floor}½" : n.SIZE.to_s
|
214
|
+
# Calculate combat totals for the summary line
|
215
|
+
dodge_total = n.get_skill_total("BODY", "Athletics", "Dodge") rescue 0
|
216
|
+
reaction_total = n.get_skill_total("BODY", "Athletics", "Reaction Speed") rescue 0
|
217
|
+
|
218
|
+
f += "#{@stat_color}SIZE: #{size_display} BP:#{n.BP.to_s} DB: #{n.DB.to_s} MD: #{n.MD.to_s} Reaction: #{reaction_total} Dodge: #{dodge_total} Carried: #{total_weight}kg ENC: #{enc_penalty}#{@reset}\n"
|
219
|
+
|
220
|
+
# Armor section - use original format if available
|
221
|
+
f += "─" * width + "\n"
|
222
|
+
if n.respond_to?(:armour) && !n.armour.to_s.empty?
|
223
|
+
# Use original armor format
|
224
|
+
f += "#{@weapon_color}ARMOR: #{n.armour.ljust(20)} AP: #{n.ap.to_s.rjust(2)} Weight: #{get_armor_weight(n.armour).to_s.rjust(2)}kg#{@reset}\n"
|
225
|
+
elsif n.armor && n.armor[:name] && !n.armor[:name].empty?
|
226
|
+
# Fall back to new format
|
227
|
+
f += "#{@weapon_color}ARMOR: #{n.armor[:name].ljust(20)} AP: #{n.armor[:ap].to_s.rjust(2)} Weight: #{get_armor_weight(n.armor[:name]).to_s.rjust(2)}kg#{@reset}\n"
|
228
|
+
else
|
229
|
+
# No armor
|
230
|
+
f += "#{@weapon_color}ARMOR: None AP: 0 Weight: 0kg#{@reset}\n"
|
231
|
+
end
|
232
|
+
|
233
|
+
# Check if we have original weapon data and display it
|
234
|
+
if n.respond_to?(:melee1) && !n.melee1.to_s.empty?
|
235
|
+
# Display ORIGINAL weapon format
|
236
|
+
f += "─" * width + "\n"
|
237
|
+
f += "#{@weapon_color}WEAPON SKILL INI OFF DEF DAM HP RANGE#{@reset}\n"
|
238
|
+
|
239
|
+
# Melee weapon 1
|
240
|
+
if n.melee1s && n.melee1s > 0
|
241
|
+
f += "#{n.melee1.ljust(19)}"
|
242
|
+
f += "#{n.melee1s.to_s.ljust(9)}"
|
243
|
+
f += "#{n.melee1i.to_s.ljust(8)}"
|
244
|
+
f += "#{n.melee1o.to_s.ljust(7)}"
|
245
|
+
f += "#{n.melee1d.to_s.ljust(7)}"
|
246
|
+
f += "#{n.melee1dam.to_s.ljust(7)}"
|
247
|
+
f += "#{n.melee1hp.to_s.ljust(6)}"
|
248
|
+
f += "\n"
|
249
|
+
end
|
250
|
+
|
251
|
+
# Melee weapon 2
|
252
|
+
if n.respond_to?(:melee2s) && n.melee2s && n.melee2s > 0
|
253
|
+
f += "#{n.melee2.ljust(19)}"
|
254
|
+
f += "#{n.melee2s.to_s.ljust(9)}"
|
255
|
+
f += "#{n.melee2i.to_s.ljust(8)}"
|
256
|
+
f += "#{n.melee2o.to_s.ljust(7)}"
|
257
|
+
f += "#{n.melee2d.to_s.ljust(7)}"
|
258
|
+
f += "#{n.melee2dam.to_s.ljust(7)}"
|
259
|
+
f += "#{n.melee2hp.to_s.ljust(6)}"
|
260
|
+
f += "\n"
|
261
|
+
end
|
262
|
+
|
263
|
+
# Missile weapon
|
264
|
+
if n.respond_to?(:missiles) && n.missiles && n.missiles > 0
|
265
|
+
f += "#{n.missile.ljust(19)}"
|
266
|
+
f += "#{n.missiles.to_s.ljust(9)}"
|
267
|
+
f += "#{' '.ljust(8)}" # No init for missile
|
268
|
+
f += "#{n.missileo.to_s.ljust(7)}"
|
269
|
+
f += "#{' '.ljust(7)}" # No def for missile
|
270
|
+
f += "#{n.missiledam.to_s.ljust(7)}"
|
271
|
+
f += "#{n.melee1hp.to_s.ljust(6)}" # Use melee1 HP as placeholder
|
272
|
+
f += "#{n.missilerange}m"
|
273
|
+
f += "\n"
|
274
|
+
end
|
275
|
+
else
|
276
|
+
# Fall back to 3-tier weapon display
|
277
|
+
melee_weapons = n.tiers["BODY"]["Melee Combat"]["skills"].select { |_, v| v > 0 }
|
278
|
+
missile_weapons = n.tiers["BODY"]["Missile Combat"]["skills"].select { |_, v| v > 0 }
|
279
|
+
|
280
|
+
if melee_weapons.any? || missile_weapons.any?
|
281
|
+
f += "─" * width + "\n"
|
282
|
+
f += "#{@weapon_color}WEAPONS:#{@reset}\n"
|
283
|
+
|
284
|
+
# Headers
|
285
|
+
melee_header = "Melee Weapon".ljust(15) + "Skill Init Off Def Damage"
|
286
|
+
missile_header = "Missile Weapon".ljust(15) + "Skill Range Damage"
|
287
|
+
f += "#{melee_header.ljust(60)} │ #{missile_header}\n"
|
288
|
+
f += "─" * 61 + "┼" + "─" * 58 + "\n"
|
289
|
+
|
290
|
+
# Format weapons with calculated totals
|
291
|
+
melee_lines = []
|
292
|
+
missile_lines = []
|
293
|
+
|
294
|
+
body_char = n.get_characteristic("BODY")
|
295
|
+
|
296
|
+
# Calculate Dodge bonus for defense (Dodge/5 rounded down)
|
297
|
+
dodge_total = n.get_skill_total("BODY", "Athletics", "Dodge") || 0
|
298
|
+
dodge_bonus = (dodge_total / 5).to_i
|
299
|
+
|
300
|
+
melee_weapons.each do |weapon, skill|
|
301
|
+
wpn_stats = get_weapon_stats(weapon)
|
302
|
+
attr = n.get_attribute("BODY", "Melee Combat") || 0
|
303
|
+
skill_total = body_char + attr + skill
|
304
|
+
|
305
|
+
# Calculate weapon totals with correct initiative formula
|
306
|
+
# Initiative = Weapon Init + Reaction Speed skill total
|
307
|
+
reaction_speed = n.get_skill_total("MIND", "Awareness", "Reaction speed") || 0
|
308
|
+
init = reaction_speed + (wpn_stats[:init] || 0)
|
309
|
+
off = skill_total + (wpn_stats[:off] || 0)
|
310
|
+
defense = skill_total + (wpn_stats[:def] || 0) + dodge_bonus # Include Dodge/5
|
311
|
+
dmg_mod = wpn_stats[:dmg].to_s =~ /special/ ? 0 : (wpn_stats[:dmg].to_s.to_i || 0)
|
312
|
+
dmg = (n.DB || 0) + dmg_mod
|
313
|
+
|
314
|
+
line = "#{weapon.ljust(15)} #{skill_total.to_s.rjust(3)} #{init.to_s.rjust(4)} #{off.to_s.rjust(3)} #{defense.to_s.rjust(3)} #{dmg.to_s.rjust(3)}"
|
315
|
+
melee_lines << line
|
316
|
+
end
|
317
|
+
|
318
|
+
missile_weapons.each do |weapon, skill|
|
319
|
+
wpn_stats = get_missile_stats(weapon)
|
320
|
+
attr = n.get_attribute("BODY", "Missile Combat") || 0
|
321
|
+
skill_total = body_char + attr + skill
|
322
|
+
|
323
|
+
range = wpn_stats[:range] || "30m"
|
324
|
+
dmg_mod = wpn_stats[:dmg].to_s =~ /special/ ? 0 : (wpn_stats[:dmg].to_s.to_i || 0)
|
325
|
+
dmg = (n.DB || 0) + dmg_mod
|
326
|
+
|
327
|
+
line = "#{weapon.ljust(15)} #{skill_total.to_s.rjust(3)} #{range.ljust(10)} #{dmg.to_s.rjust(3)}"
|
328
|
+
missile_lines << line
|
329
|
+
end
|
330
|
+
|
331
|
+
# Balance weapon columns
|
332
|
+
max_wpn_lines = [melee_lines.length, missile_lines.length].max
|
333
|
+
melee_lines += [""] * (max_wpn_lines - melee_lines.length)
|
334
|
+
missile_lines += [""] * (max_wpn_lines - missile_lines.length)
|
335
|
+
|
336
|
+
max_wpn_lines.times do |i|
|
337
|
+
f += "#{melee_lines[i].ljust(60)} │ #{missile_lines[i]}\n"
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end # Close the original/3-tier weapon display if/else block
|
341
|
+
|
342
|
+
# Equipment section with money
|
343
|
+
equipment = generate_equipment(n.type, n.level)
|
344
|
+
social_status = generate_social_status(n.type, n.level)
|
345
|
+
money = generate_money(social_status, n.level)
|
346
|
+
|
347
|
+
# Convert money format from "X silver" to currency abbreviations
|
348
|
+
money_value = money.split(' ').first.to_i
|
349
|
+
money_str = if money_value >= 100
|
350
|
+
gp = money_value / 100
|
351
|
+
sp = (money_value % 100) / 10
|
352
|
+
cp = money_value % 10
|
353
|
+
parts = []
|
354
|
+
parts << "#{gp}gp" if gp > 0
|
355
|
+
parts << "#{sp}sp" if sp > 0
|
356
|
+
parts << "#{cp}cp" if cp > 0
|
357
|
+
parts.join(" ")
|
358
|
+
elsif money_value >= 10
|
359
|
+
sp = money_value / 10
|
360
|
+
cp = money_value % 10
|
361
|
+
parts = []
|
362
|
+
parts << "#{sp}sp" if sp > 0
|
363
|
+
parts << "#{cp}cp" if cp > 0
|
364
|
+
parts.join(" ")
|
365
|
+
else
|
366
|
+
"#{money_value}cp"
|
367
|
+
end
|
368
|
+
|
369
|
+
f += "─" * width + "\n"
|
370
|
+
equipment_list = equipment + [money_str]
|
371
|
+
f += "EQUIPMENT: #{equipment_list.join(", ")}\n"
|
372
|
+
|
373
|
+
# Creative Spells section using new spell catalog
|
374
|
+
require_relative 'includes/spell_catalog'
|
375
|
+
spells = assign_spells_to_npc(n)
|
376
|
+
|
377
|
+
if !spells.empty?
|
378
|
+
f += "─" * width + "\n"
|
379
|
+
# Use color 165 for SPELLS title (purple for magic)
|
380
|
+
spell_title_color = cli == "cli" ? "\e[38;5;165m\e[1m" : ""
|
381
|
+
f += "#{spell_title_color}SPELLS (#{spells.length}):#{@reset}\n"
|
382
|
+
|
383
|
+
spells.each do |spell|
|
384
|
+
# Spell name and basic info
|
385
|
+
spell_name_color = cli == "cli" ? "\e[38;5;226m\e[1m" : ""
|
386
|
+
difficulty_color = cli == "cli" ? "\e[38;5;202m" : ""
|
387
|
+
desc_color = cli == "cli" ? "\e[38;5;51m" : ""
|
388
|
+
|
389
|
+
f += " • #{spell_name_color}#{spell[:name]}#{@reset} (Difficulty: #{difficulty_color}#{spell[:difficulty]}#{@reset}, Cooldown: #{difficulty_color}#{spell[:cooldown]}h#{@reset})\n"
|
390
|
+
f += " #{desc_color}#{spell[:description]}#{@reset}\n"
|
391
|
+
|
392
|
+
# Casting path and details
|
393
|
+
path_color = cli == "cli" ? "\e[38;5;111m" : ""
|
394
|
+
detail_color = cli == "cli" ? "\e[38;5;240m" : ""
|
395
|
+
|
396
|
+
f += " #{detail_color}Cast via: #{path_color}#{spell[:skill_path]}#{@reset}\n"
|
397
|
+
f += " #{detail_color}Range: #{@reset}#{spell[:range]}, #{detail_color}Duration: #{@reset}#{spell[:duration]}\n"
|
398
|
+
|
399
|
+
if spell[:side_effects] && !spell[:side_effects].empty?
|
400
|
+
side_effect_color = cli == "cli" ? "\e[38;5;196m" : ""
|
401
|
+
f += " #{detail_color}Side effect: #{side_effect_color}#{spell[:side_effects]}#{@reset}\n"
|
402
|
+
end
|
403
|
+
f += "\n"
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
# Equipment section
|
408
|
+
|
409
|
+
# Output handling
|
410
|
+
if cli == "cli_direct"
|
411
|
+
# This is the direct CLI mode - print and handle editing
|
412
|
+
# Save clean version without ANSI codes for editing
|
413
|
+
File.write("saved/temp_new.npc", f.pure, perm: 0644)
|
414
|
+
# Display version with colors
|
415
|
+
print f
|
416
|
+
|
417
|
+
# Options
|
418
|
+
puts "\nPress 'e' to edit, any other key to continue"
|
419
|
+
begin
|
420
|
+
key = STDIN.getch
|
421
|
+
rescue Errno::ENOTTY
|
422
|
+
# Not in a terminal, skip the interactive part
|
423
|
+
key = nil
|
424
|
+
end
|
425
|
+
|
426
|
+
if key == "e"
|
427
|
+
# Use vim with settings to avoid binary file warnings
|
428
|
+
if $editor.include?("vim") || $editor.include?("vi")
|
429
|
+
system("#{$editor} -c 'set fileformat=unix' saved/temp_new.npc")
|
430
|
+
else
|
431
|
+
system("#{$editor} saved/temp_new.npc")
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
return f
|
436
|
+
elsif cli == "cli"
|
437
|
+
# This is TUI mode - just return the colored string without printing
|
438
|
+
return f
|
439
|
+
else
|
440
|
+
# Plain mode - return without colors
|
441
|
+
return f
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
# Helper functions
|
446
|
+
|
447
|
+
def generate_cult_info(type, level, attunement_level = 0, sex = nil)
|
448
|
+
# Generate religion and cult standing for religious types
|
449
|
+
# Format: "Cult: [Religion], [Standing] (CS)"
|
450
|
+
|
451
|
+
# Load religion table if not loaded
|
452
|
+
unless defined?($CharacterReligions)
|
453
|
+
load File.join($pgmdir, "includes/tables/religions.rb")
|
454
|
+
end
|
455
|
+
|
456
|
+
# Get appropriate deity from table - pass sex for nobility handling
|
457
|
+
cult = get_character_religion(type, sex)
|
458
|
+
|
459
|
+
# Determine cult standing based on level and attunement
|
460
|
+
# Higher attunement means deeper religious involvement
|
461
|
+
standing, cs = case level
|
462
|
+
when 1..2
|
463
|
+
if attunement_level >= 3
|
464
|
+
["Initiate", rand(3..5)]
|
465
|
+
else
|
466
|
+
["Lay Member", rand(1..3)]
|
467
|
+
end
|
468
|
+
when 3..4
|
469
|
+
if attunement_level >= 4
|
470
|
+
["Priest", rand(6..8)]
|
471
|
+
else
|
472
|
+
["Initiate", rand(4..7)]
|
473
|
+
end
|
474
|
+
when 5..6
|
475
|
+
if attunement_level >= 5
|
476
|
+
["High Priest", rand(10..12)]
|
477
|
+
else
|
478
|
+
["Priest", rand(8..11)]
|
479
|
+
end
|
480
|
+
else
|
481
|
+
["High Priest", rand(12..15)]
|
482
|
+
end
|
483
|
+
|
484
|
+
# Special handling for non-priest types (they progress slower in cult)
|
485
|
+
if !["Priest", "Clergyman", "Monk"].include?(type)
|
486
|
+
standing, cs = case level
|
487
|
+
when 1..3
|
488
|
+
if attunement_level >= 3
|
489
|
+
["Initiate", rand(2..3)]
|
490
|
+
else
|
491
|
+
["Lay Member", rand(1..2)]
|
492
|
+
end
|
493
|
+
when 4..5
|
494
|
+
if attunement_level >= 4
|
495
|
+
["Initiate", rand(4..6)]
|
496
|
+
else
|
497
|
+
["Initiate", rand(3..5)]
|
498
|
+
end
|
499
|
+
else
|
500
|
+
if attunement_level >= 5
|
501
|
+
["Priest", rand(7..9)]
|
502
|
+
else
|
503
|
+
["Initiate", rand(6..8)]
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
"Cult: #{cult}, #{standing} (#{cs})"
|
509
|
+
end
|
510
|
+
|
511
|
+
def generate_description(type, level, sex)
|
512
|
+
# Generate random description if none provided
|
513
|
+
build = ["lean", "muscular", "stocky", "thin", "athletic", "sturdy", "wiry", "robust"].sample
|
514
|
+
hair = ["dark", "brown", "blonde", "red", "grey", "black", "auburn", "silver"].sample
|
515
|
+
eyes = ["blue", "brown", "green", "grey", "hazel", "dark", "amber"].sample
|
516
|
+
feature = ["scarred", "weathered", "youthful", "stern", "friendly", "intense", "calm", "sharp"].sample
|
517
|
+
|
518
|
+
pronoun = sex == "F" ? "She" : "He"
|
519
|
+
pronoun_pos = sex == "F" ? "Her" : "His"
|
520
|
+
|
521
|
+
case type
|
522
|
+
when "Warrior", "Guard", "Soldier"
|
523
|
+
"#{build.capitalize} build with #{hair} hair. #{pronoun} has #{eyes} eyes and a #{feature} face. Battle-hardened."
|
524
|
+
when "Mage", "Wizard", "Scholar"
|
525
|
+
"#{pronoun} has #{eyes} eyes that show intelligence. #{hair.capitalize} hair, #{build} build. Scholarly demeanor."
|
526
|
+
when "Thief", "Bandit", "Assassin"
|
527
|
+
"#{feature.capitalize} features with #{eyes} eyes. #{build.capitalize} and agile. Moves with quiet confidence."
|
528
|
+
when "Noble", "Merchant"
|
529
|
+
"Well-groomed with #{hair} hair and #{eyes} eyes. #{build.capitalize} build. #{pronoun_pos} bearing shows status."
|
530
|
+
when "Priest", "Monk"
|
531
|
+
"#{pronoun} has a serene face with #{eyes} eyes. #{hair.capitalize} hair. #{feature.capitalize} presence."
|
532
|
+
when "Ranger", "Hunter"
|
533
|
+
"Weather-beaten with #{hair} hair. #{eyes.capitalize} eyes scan surroundings. #{build.capitalize} and outdoorsy."
|
534
|
+
else
|
535
|
+
"#{build.capitalize} build with #{hair} hair and #{eyes} eyes. #{feature.capitalize} appearance."
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
def generate_social_status(type, level)
|
540
|
+
# Generate social status using old system abbreviations
|
541
|
+
# S=Slave, LC=Lower Class, LMC=Lower Middle Class, MC=Middle Class, UC=Upper Class, N=Noble
|
542
|
+
case type
|
543
|
+
when "Noble"
|
544
|
+
"N"
|
545
|
+
when "Priest", "Mage", "Wizard (air)", "Wizard (earth)", "Wizard (fire)", "Wizard (prot.)", "Wizard (water)", "Sorcerer", "Summoner"
|
546
|
+
level > 3 ? "UC" : "MC"
|
547
|
+
when "Merchant", "Scholar", "Sage", "Seer"
|
548
|
+
level > 2 ? "MC" : "LMC"
|
549
|
+
when "Warrior", "Guard", "Soldier", "Army officer", "Body guard"
|
550
|
+
level > 4 ? "MC" : "LMC"
|
551
|
+
when "Bandit", "Thief", "Assassin", "Prostitute", "Highwayman"
|
552
|
+
"LC"
|
553
|
+
when "Commoner", "Farmer", "House wife", "Nanny"
|
554
|
+
"LMC"
|
555
|
+
when "Executioner", "Gladiator"
|
556
|
+
"LC"
|
557
|
+
else
|
558
|
+
level > 3 ? "LMC" : "LC"
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
def generate_money(status, level)
|
563
|
+
base = case status
|
564
|
+
when "N" then 100 * level # Noble
|
565
|
+
when "UC" then 50 * level # Upper Class
|
566
|
+
when "MC" then 30 * level # Middle Class
|
567
|
+
when "LMC" then 15 * level # Lower Middle Class
|
568
|
+
when "LC" then 8 * level # Lower Class
|
569
|
+
when "S" then 2 * level # Slave
|
570
|
+
else 10 * level
|
571
|
+
end
|
572
|
+
|
573
|
+
variance = rand(base / 2) - base / 4
|
574
|
+
total = base + variance
|
575
|
+
"#{total} silver"
|
576
|
+
end
|
577
|
+
|
578
|
+
# Load the new equipment tables if not already loaded
|
579
|
+
def generate_equipment(type, level)
|
580
|
+
unless defined?(generate_npc_equipment)
|
581
|
+
load File.join($pgmdir, "includes/equipment_tables.rb")
|
582
|
+
end
|
583
|
+
generate_npc_equipment(type, level)
|
584
|
+
end
|
585
|
+
|
586
|
+
def calculate_total_weight(n)
|
587
|
+
weight = 0
|
588
|
+
|
589
|
+
# Armor weight
|
590
|
+
if n.armor
|
591
|
+
weight += get_armor_weight(n.armor[:name])
|
592
|
+
end
|
593
|
+
|
594
|
+
# Weapon weights (approximate)
|
595
|
+
melee_count = n.tiers["BODY"]["Melee Combat"]["skills"].select { |_, v| v > 0 }.size
|
596
|
+
missile_count = n.tiers["BODY"]["Missile Combat"]["skills"].select { |_, v| v > 0 }.size
|
597
|
+
weight += melee_count * 2 # Average 2kg per melee weapon
|
598
|
+
weight += missile_count * 1.5 # Average 1.5kg per missile weapon
|
599
|
+
|
600
|
+
# Basic equipment
|
601
|
+
weight += 5 # Basic gear
|
602
|
+
|
603
|
+
weight.round(1)
|
604
|
+
end
|
605
|
+
|
606
|
+
def get_armor_weight(armor_name)
|
607
|
+
case armor_name
|
608
|
+
when /Leather/i then 5
|
609
|
+
when /Padded|Quilt/i then 7
|
610
|
+
when /Chain shirt/i then 12
|
611
|
+
when /Scale/i then 15
|
612
|
+
when /Chain mail/i then 19
|
613
|
+
when /Plate/i then 25
|
614
|
+
when /Full plate/i then 30
|
615
|
+
else 0
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
# Get weapon stats from tables with init modifier
|
620
|
+
def get_weapon_stats(weapon)
|
621
|
+
# Ensure weapon is a string and handle nil
|
622
|
+
weapon = weapon.to_s.downcase
|
623
|
+
|
624
|
+
# Return default stats if weapon is empty
|
625
|
+
if weapon.empty?
|
626
|
+
return { init: 0, off: 0, def: 0, dmg: "0" }
|
627
|
+
end
|
628
|
+
|
629
|
+
# Default values for weapons based on name patterns
|
630
|
+
stats = case weapon
|
631
|
+
when /sword/
|
632
|
+
{ init: 0, off: 0, def: 0, dmg: "+1" }
|
633
|
+
when /dagger/, /knife/
|
634
|
+
{ init: 2, off: 1, def: -1, dmg: "-1" }
|
635
|
+
when /axe/
|
636
|
+
{ init: -1, off: 0, def: -1, dmg: "+2" }
|
637
|
+
when /spear/, /pike/
|
638
|
+
{ init: 1, off: 0, def: 1, dmg: "0" }
|
639
|
+
when /mace/, /club/, /hammer/
|
640
|
+
{ init: -1, off: -1, def: 0, dmg: "+1" }
|
641
|
+
when /staff/, /quarterstaff/
|
642
|
+
{ init: 0, off: 0, def: 2, dmg: "-1" }
|
643
|
+
when /shield/
|
644
|
+
{ init: 0, off: -2, def: 3, dmg: "-2" }
|
645
|
+
when /net/
|
646
|
+
{ init: -2, off: 0, def: 0, dmg: "special" }
|
647
|
+
when /unarmed/
|
648
|
+
{ init: 1, off: -2, def: -4, dmg: "-4" }
|
649
|
+
else
|
650
|
+
{ init: 0, off: 0, def: 0, dmg: "0" }
|
651
|
+
end
|
652
|
+
|
653
|
+
# Make absolutely sure all values are present
|
654
|
+
stats ||= {}
|
655
|
+
stats[:init] ||= 0
|
656
|
+
stats[:off] ||= 0
|
657
|
+
stats[:def] ||= 0
|
658
|
+
stats[:dmg] ||= "0"
|
659
|
+
|
660
|
+
stats
|
661
|
+
end
|
662
|
+
|
663
|
+
def get_missile_stats(weapon)
|
664
|
+
# Ensure weapon is a string
|
665
|
+
weapon = weapon.to_s.downcase
|
666
|
+
|
667
|
+
# Return default stats if weapon is empty
|
668
|
+
if weapon.empty?
|
669
|
+
return { range: "30m", dmg: "0" }
|
670
|
+
end
|
671
|
+
|
672
|
+
# Default values for missile weapons
|
673
|
+
stats = case weapon
|
674
|
+
when /longbow/
|
675
|
+
{ range: "150m", dmg: "+1" }
|
676
|
+
when /bow/
|
677
|
+
{ range: "100m", dmg: "0" }
|
678
|
+
when /x-bow/, /crossbow/
|
679
|
+
{ range: "150m", dmg: "+2" }
|
680
|
+
when /sling/
|
681
|
+
{ range: "50m", dmg: "-1" }
|
682
|
+
when /throwing/, /javelin/
|
683
|
+
{ range: "30m", dmg: "0" }
|
684
|
+
when /net/
|
685
|
+
{ range: "10m", dmg: "special" }
|
686
|
+
when /spear/
|
687
|
+
{ range: "30m", dmg: "0" }
|
688
|
+
when /blowgun/
|
689
|
+
{ range: "20m", dmg: "-5" }
|
690
|
+
else
|
691
|
+
{ range: "30m", dmg: "0" }
|
692
|
+
end
|
693
|
+
|
694
|
+
# Ensure all values are present
|
695
|
+
stats ||= {}
|
696
|
+
stats[:range] ||= "30m"
|
697
|
+
stats[:dmg] ||= "0"
|
698
|
+
|
699
|
+
stats
|
700
|
+
end
|