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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +675 -0
  3. data/README.md +155 -0
  4. data/amar-tui.rb +8195 -0
  5. data/cli_enc_output.rb +87 -0
  6. data/cli_enc_output_new.rb +433 -0
  7. data/cli_enc_output_new_3tier.rb +198 -0
  8. data/cli_enc_output_new_compact.rb +238 -0
  9. data/cli_name_gen.rb +21 -0
  10. data/cli_npc_output.rb +279 -0
  11. data/cli_npc_output_new.rb +700 -0
  12. data/cli_town_output.rb +39 -0
  13. data/cli_weather_output.rb +36 -0
  14. data/includes/class_enc.rb +341 -0
  15. data/includes/class_enc_new.rb +512 -0
  16. data/includes/class_monster_new.rb +551 -0
  17. data/includes/class_npc.rb +1378 -0
  18. data/includes/class_npc_new.rb +1187 -0
  19. data/includes/class_npc_new.rb.backup +706 -0
  20. data/includes/class_npc_new_skills.rb +153 -0
  21. data/includes/class_town.rb +237 -0
  22. data/includes/d6s.rb +40 -0
  23. data/includes/equipment_tables.rb +120 -0
  24. data/includes/functions.rb +67 -0
  25. data/includes/includes.rb +30 -0
  26. data/includes/randomizer.rb +15 -0
  27. data/includes/spell_catalog.rb +441 -0
  28. data/includes/tables/armour.rb +13 -0
  29. data/includes/tables/chartype.rb +4412 -0
  30. data/includes/tables/chartype_new.rb +765 -0
  31. data/includes/tables/chartype_new_full.rb +2713 -0
  32. data/includes/tables/enc_specific.rb +168 -0
  33. data/includes/tables/enc_type.rb +17 -0
  34. data/includes/tables/encounters.rb +99 -0
  35. data/includes/tables/magick.rb +169 -0
  36. data/includes/tables/melee.rb +36 -0
  37. data/includes/tables/missile.rb +17 -0
  38. data/includes/tables/monster_stats_new.rb +264 -0
  39. data/includes/tables/month.rb +18 -0
  40. data/includes/tables/names.rb +21 -0
  41. data/includes/tables/personality.rb +12 -0
  42. data/includes/tables/race_templates.rb +318 -0
  43. data/includes/tables/religions.rb +266 -0
  44. data/includes/tables/spells_new.rb +496 -0
  45. data/includes/tables/tier_system.rb +104 -0
  46. data/includes/tables/town.rb +71 -0
  47. data/includes/tables/weather.rb +41 -0
  48. data/includes/town_relations.rb +127 -0
  49. data/includes/weather.rb +108 -0
  50. data/includes/weather2latex.rb +114 -0
  51. data/lib/rcurses.rb +33 -0
  52. 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