amar-tui 2.1.1 → 2.1.5

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/amar-tui.rb +8198 -0
  3. data/cli_enc_output.rb +87 -0
  4. data/cli_enc_output_new.rb +433 -0
  5. data/cli_enc_output_new_3tier.rb +198 -0
  6. data/cli_enc_output_new_compact.rb +238 -0
  7. data/cli_name_gen.rb +21 -0
  8. data/cli_npc_output.rb +279 -0
  9. data/cli_npc_output_new.rb +752 -0
  10. data/cli_town_output.rb +39 -0
  11. data/cli_weather_output.rb +36 -0
  12. data/includes/class_enc.rb +341 -0
  13. data/includes/class_enc_new.rb +512 -0
  14. data/includes/class_monster_new.rb +551 -0
  15. data/includes/class_npc.rb +1378 -0
  16. data/includes/class_npc_new.rb +1322 -0
  17. data/includes/class_npc_new.rb.backup +706 -0
  18. data/includes/class_npc_new_skills.rb +153 -0
  19. data/includes/class_town.rb +237 -0
  20. data/includes/d6s.rb +40 -0
  21. data/includes/equipment_tables.rb +120 -0
  22. data/includes/functions.rb +67 -0
  23. data/includes/includes.rb +30 -0
  24. data/includes/randomizer.rb +15 -0
  25. data/includes/spell_catalog.rb +446 -0
  26. data/includes/tables/armour.rb +13 -0
  27. data/includes/tables/chartype.rb +4412 -0
  28. data/includes/tables/chartype_new.rb +765 -0
  29. data/includes/tables/chartype_new_full.rb +2713 -0
  30. data/includes/tables/enc_specific.rb +168 -0
  31. data/includes/tables/enc_type.rb +17 -0
  32. data/includes/tables/encounters.rb +99 -0
  33. data/includes/tables/magick.rb +169 -0
  34. data/includes/tables/melee.rb +36 -0
  35. data/includes/tables/missile.rb +17 -0
  36. data/includes/tables/monster_stats_new.rb +264 -0
  37. data/includes/tables/month.rb +18 -0
  38. data/includes/tables/names.rb +21 -0
  39. data/includes/tables/personality.rb +12 -0
  40. data/includes/tables/race_templates.rb +318 -0
  41. data/includes/tables/religions.rb +266 -0
  42. data/includes/tables/spells_new.rb +496 -0
  43. data/includes/tables/tier_system.rb +104 -0
  44. data/includes/tables/town.rb +71 -0
  45. data/includes/tables/weather.rb +41 -0
  46. data/includes/town_relations.rb +127 -0
  47. data/includes/weather.rb +108 -0
  48. data/includes/weather2latex.rb +114 -0
  49. data/lib/rcurses.rb +33 -0
  50. metadata +116 -10
@@ -0,0 +1,1322 @@
1
+ # New NPC Class for 3-Tier System
2
+ # Implements the Characteristics > Attributes > Skills structure
3
+
4
+ class NpcNew
5
+ attr_accessor :name, :type, :level, :area, :sex, :age, :height, :weight
6
+ attr_accessor :tiers, :social_status, :marks, :description
7
+ attr_accessor :melee_weapon, :missile_weapon, :armor, :ENC, :spells
8
+ # Original CLI weapon format accessors
9
+ attr_accessor :melee1, :melee2, :melee3, :missile
10
+ attr_accessor :melee1s, :melee2s, :melee3s, :missiles
11
+ attr_accessor :melee1i, :melee1o, :melee1d, :melee1dam, :melee1hp
12
+ attr_accessor :melee2i, :melee2o, :melee2d, :melee2dam, :melee2hp
13
+ attr_accessor :melee3i, :melee3o, :melee3d, :melee3dam, :melee3hp
14
+ attr_accessor :missileo, :missiledam, :missilerange
15
+ attr_accessor :armour, :ap
16
+
17
+ def initialize(name, type, level, area, sex, age, height, weight, description, predetermined_stats = nil)
18
+ # Generate random values for missing data
19
+ @name = name && !name.empty? ? name : generate_random_name(sex)
20
+ @type = type
21
+ @level = level.to_i > 0 ? level.to_i : rand(1..6) # Random level 1-6 if 0 or invalid
22
+ @area = area && !area.empty? ? area : ["Amaronir", "Merisir", "Calaronir", "Feronir", "Rauinir"].sample
23
+ @sex = sex && !sex.empty? ? sex : ["M", "F"].sample
24
+
25
+ # Calculate realistic age based on experience level
26
+ # Assuming training starts at 15, each level requires years of dedication
27
+ if age.to_i > 0
28
+ @age = age.to_i
29
+ else
30
+ base_age = 15 # Starting age for training
31
+ years_training = case @level
32
+ when 1 then rand(1..3) # 16-18 years old
33
+ when 2 then rand(3..7) # 18-22 years old
34
+ when 3 then rand(7..12) # 22-27 years old
35
+ when 4 then rand(12..20) # 27-35 years old
36
+ when 5 then rand(20..30) # 35-45 years old
37
+ when 6 then rand(30..45) # 45-60 years old
38
+ else rand(45..60) # 60-75 years old
39
+ end
40
+ @age = base_age + years_training
41
+ end
42
+
43
+ # Generate realistic height and weight based on Amar averages
44
+ # Human average: 170cm height, 70kg weight
45
+ # But allowing for strongmen up to 195cm/125kg
46
+ if height.to_i > 0
47
+ @height = height.to_i
48
+ else
49
+ # Base height around 160 + variation using open-ended d6 rolls
50
+ # This gives a bell curve centered around 170cm
51
+ base_height = 160
52
+ variation = oD6 * 2 + oD6 + rand(10)
53
+ @height = base_height + variation
54
+ @height -= 5 if @sex == "F" # Females slightly shorter on average
55
+ @height -= (3 * (16 - @age)) if @age < 17 # Youth adjustment
56
+
57
+ # Type-based adjustments for larger/smaller builds
58
+ case @type
59
+ when /Warrior|Guard|Soldier|Body guard|Barbarian/
60
+ @height += rand(0..10) # Warriors tend to be taller
61
+ when /Thief|Assassin|Rogue/
62
+ @height -= rand(0..5) # Rogues tend to be more average/shorter
63
+ end
64
+ end
65
+
66
+ if weight.to_i > 0
67
+ @weight = weight.to_i
68
+ else
69
+ # Weight based on height with more variation for different builds
70
+ # Base formula: height - 120 + variation
71
+ base_weight = @height - 120
72
+
73
+ # Build variation based on character type
74
+ build_modifier = case @type
75
+ when /Warrior|Guard|Soldier|Body guard|Barbarian|Worker|Farmer/
76
+ # Muscular builds: more weight for same height
77
+ aD6 * 5 + rand(15) # Can add up to 55kg for strongman builds
78
+ when /Noble|Merchant|Scholar|Sage|Priest|Mage|Wizard/
79
+ # Average to lighter builds
80
+ aD6 * 3 + rand(10) # Normal variation
81
+ when /Thief|Assassin|Rogue|Scout/
82
+ # Lean, agile builds
83
+ aD6 * 2 + rand(10) # Lighter variation
84
+ else
85
+ # Default average build
86
+ aD6 * 4 + rand(10)
87
+ end
88
+
89
+ @weight = base_weight + build_modifier
90
+ @weight = [@weight, 40].max # Minimum weight of 40kg
91
+ end
92
+ @description = description
93
+
94
+ # Initialize tier system structures
95
+ @tiers = {
96
+ "BODY" => {},
97
+ "MIND" => {},
98
+ "SPIRIT" => {}
99
+ }
100
+
101
+ # Initialize mark tracking for progression
102
+ @marks = {
103
+ "BODY" => {},
104
+ "MIND" => {},
105
+ "SPIRIT" => {}
106
+ }
107
+
108
+ # Set social status
109
+ @social_status = ["S", "LC", "LMC", "MC", "UC", "N"].sample
110
+
111
+ # Store predetermined stats for later use
112
+ @predetermined_stats = predetermined_stats
113
+
114
+ # Generate based on new tier system
115
+ generate_tiers()
116
+
117
+ # Generate spells if magic user
118
+ generate_spells()
119
+
120
+ # Select equipment
121
+ select_equipment()
122
+
123
+ # Initialize encumbrance
124
+ @ENC = 0
125
+ end
126
+
127
+ private
128
+
129
+ def generate_tiers
130
+ # Load tier system and character templates
131
+ unless defined?($TierSystem)
132
+ load File.join($pgmdir, "includes/tables/tier_system.rb")
133
+ end
134
+ unless defined?($ChartypeNewFull)
135
+ load File.join($pgmdir, "includes/tables/chartype_new_full.rb")
136
+ end
137
+
138
+ # Get template for this character type
139
+ template = $ChartypeNewFull[@type] || $ChartypeNewFull["Commoner"]
140
+
141
+ # Generate characteristics
142
+ ["BODY", "MIND", "SPIRIT"].each do |char_name|
143
+ char_base = template["characteristics"][char_name] || 2
144
+
145
+ # Initialize attributes for this characteristic
146
+ $TierSystem[char_name].each do |attr_name, attr_data|
147
+ @tiers[char_name][attr_name] = {
148
+ "level" => 0,
149
+ "skills" => {}
150
+ }
151
+
152
+ # Set attribute base from template
153
+ attr_key = "#{char_name}/#{attr_name}"
154
+ if template["attributes"] && template["attributes"][attr_key]
155
+ attr_base = template["attributes"][attr_key]
156
+ else
157
+ attr_base = attr_data["base"] || 0
158
+ end
159
+
160
+ # Calculate attribute level based on NPC level
161
+ @tiers[char_name][attr_name]["level"] = calculate_tier_level(attr_base, @level, 0.8)
162
+
163
+ # Initialize skills for this attribute
164
+ if attr_data["skills"]
165
+ # Convert array to hash for skills
166
+ skill_list = attr_data["skills"]
167
+ skill_list = [] unless skill_list.is_a?(Array)
168
+
169
+ skill_list.each do |skill_name|
170
+ # Check if template has specific skill values
171
+ skill_key = "#{char_name}/#{attr_name}/#{skill_name}"
172
+ skill_base = 0
173
+
174
+ if template["skills"] && template["skills"][skill_key]
175
+ skill_base = template["skills"][skill_key]
176
+ end
177
+
178
+ # Calculate skill level
179
+ @tiers[char_name][attr_name]["skills"][skill_name] = calculate_tier_level(skill_base, @level, 0.6)
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ # Add template-specific skills
186
+ if template["skills"]
187
+ template["skills"].each do |skill_path, base_value|
188
+ parts = skill_path.split("/")
189
+ next unless parts.length == 3
190
+
191
+ char_name, attr_name, skill_name = parts
192
+ next unless @tiers[char_name] && @tiers[char_name][attr_name]
193
+
194
+ @tiers[char_name][attr_name]["skills"] ||= {}
195
+ current = @tiers[char_name][attr_name]["skills"][skill_name] || 0
196
+ new_value = calculate_tier_level(base_value, @level, 0.6)
197
+ @tiers[char_name][attr_name]["skills"][skill_name] = [current, new_value].max
198
+ end
199
+ end
200
+
201
+ # Add weapon skills from template
202
+ add_weapon_skills(template)
203
+
204
+ # Add Innate skills ONLY for specific character types
205
+ innate_types = ["Witch (white)", "Witch (black)", "Sorcerer", "Summoner"]
206
+ if innate_types.include?(@type)
207
+ # These types get innate magical abilities
208
+ innate_skills = ["Flying", "Camouflage", "Shape Shifting"]
209
+ innate_skills.sample(rand(1..2)).each do |skill|
210
+ @tiers["SPIRIT"]["Innate"]["skills"][skill] = rand(1..3) + (@level / 2)
211
+ end
212
+ end
213
+
214
+ # For magic users, ensure they have appropriate Casting and Attunement
215
+ if has_template_magic?(template)
216
+ # Ensure minimum Casting level for spell use
217
+ min_casting = (@level / 2) + 1
218
+ if @tiers["SPIRIT"]["Casting"]["level"] < min_casting
219
+ @tiers["SPIRIT"]["Casting"]["level"] = min_casting
220
+ end
221
+
222
+ # Add specific casting skills
223
+ ["Range", "Duration", "Area of Effect"].each do |skill|
224
+ @tiers["SPIRIT"]["Casting"]["skills"][skill] = rand(1..3) + (@level / 2)
225
+ end
226
+
227
+ # Set attunement domains based on type
228
+ domains = case @type
229
+ when /Wizard \((.*?)\)/
230
+ [$1.capitalize]
231
+ when "Priest"
232
+ ["Life", "Body"]
233
+ when "Mage"
234
+ ["Fire", "Air", "Mind"]
235
+ else
236
+ ["Fire", "Water", "Air", "Earth"].sample(2)
237
+ end
238
+
239
+ domains.each do |domain|
240
+ @tiers["SPIRIT"]["Attunement"]["skills"][domain] = rand(2..4) + (@level / 2)
241
+ end
242
+ end
243
+
244
+ # Add experience-based skills for higher level NPCs
245
+ if @level >= 4
246
+ add_experience_skills()
247
+ end
248
+
249
+ # Apply predetermined stats if provided (for encounter consistency)
250
+ apply_predetermined_stats() if @predetermined_stats
251
+
252
+ # Ensure essential skills are always present
253
+ ensure_essential_skills()
254
+ end
255
+
256
+ def add_experience_skills
257
+ # Add additional skills for experienced characters
258
+ experience_bonus = @level - 3 # 1 for level 4, 2 for level 5, etc.
259
+
260
+ # Significantly expand skill generation based on level
261
+ skill_count = case @level
262
+ when 4 then rand(8..12)
263
+ when 5 then rand(12..18)
264
+ when 6 then rand(18..25)
265
+ else rand(20..30)
266
+ end
267
+
268
+ skills_added = 0
269
+
270
+ # Build skill pool based on actual tier system
271
+ skill_pool = []
272
+
273
+ # BODY skills - more varied distribution
274
+ skill_pool += [
275
+ ["BODY", "Athletics", "Hide", rand(1..2) + (experience_bonus / 2)],
276
+ ["BODY", "Athletics", "Move Quietly", rand(1..2) + (experience_bonus / 2)],
277
+ ["BODY", "Athletics", "Climb", rand(0..2) + (experience_bonus / 2)],
278
+ ["BODY", "Athletics", "Swim", rand(0..2) + (experience_bonus / 2)],
279
+ ["BODY", "Athletics", "Ride", rand(0..1) + (experience_bonus / 3)],
280
+ ["BODY", "Athletics", "Jump", rand(0..1) + (experience_bonus / 3)],
281
+ ["BODY", "Athletics", "Balance", rand(1..2) + (experience_bonus / 2)],
282
+ ["BODY", "Endurance", "Running", rand(1..2) + (experience_bonus / 2)],
283
+ ["BODY", "Endurance", "Combat Tenacity", rand(0..2) + (experience_bonus / 2)],
284
+ ["BODY", "Sleight", "Pick pockets", rand(0..1) + (experience_bonus / 3)],
285
+ ["BODY", "Sleight", "Disarm Traps", rand(0..1) + (experience_bonus / 3)]
286
+ ]
287
+
288
+ # MIND skills - more varied distribution
289
+ skill_pool += [
290
+ ["MIND", "Awareness", "Tracking", rand(1..2) + (experience_bonus / 2)],
291
+ ["MIND", "Awareness", "Detect Traps", rand(0..1) + (experience_bonus / 3)],
292
+ ["MIND", "Awareness", "Sense Emotions", rand(0..2) + (experience_bonus / 2)],
293
+ ["MIND", "Awareness", "Sense of Direction", rand(1..2) + (experience_bonus / 2)],
294
+ ["MIND", "Awareness", "Listening", rand(1..2) + (experience_bonus / 2)],
295
+ ["MIND", "Social Knowledge", "Social lore", rand(1..2) + (experience_bonus / 2)],
296
+ ["MIND", "Social Knowledge", "Spoken Language", rand(0..2) + (experience_bonus / 2)],
297
+ ["MIND", "Social Knowledge", "Literacy", rand(0..2) + (experience_bonus / 2)],
298
+ ["MIND", "Nature Knowledge", "Medical lore", rand(0..1) + (experience_bonus / 3)],
299
+ ["MIND", "Nature Knowledge", "Plant Lore", rand(0..1) + (experience_bonus / 3)],
300
+ ["MIND", "Nature Knowledge", "Animal Lore", rand(0..1) + (experience_bonus / 3)],
301
+ ["MIND", "Practical Knowledge", "Survival Lore", rand(1..2) + (experience_bonus / 2)],
302
+ ["MIND", "Practical Knowledge", "Set traps", rand(0..1) + (experience_bonus / 3)],
303
+ ["MIND", "Willpower", "Mental Fortitude", rand(1..2) + (experience_bonus / 2)],
304
+ ["MIND", "Willpower", "Courage", rand(0..2) + (experience_bonus / 2)]
305
+ ]
306
+
307
+ # Type-specific skills
308
+ if has_magic?
309
+ skill_pool += [
310
+ ["MIND", "Awareness", "Sense Magick", rand(3..4) + experience_bonus],
311
+ ["MIND", "Nature Knowledge", "Magick Rituals", rand(3..5) + experience_bonus],
312
+ ["MIND", "Social Knowledge", "Mythology", rand(2..3) + experience_bonus],
313
+ ["MIND", "Social Knowledge", "Legend Lore", rand(2..3) + experience_bonus],
314
+ ["SPIRIT", "Casting", "Range", rand(2..4) + experience_bonus],
315
+ ["SPIRIT", "Casting", "Duration", rand(2..4) + experience_bonus],
316
+ ["SPIRIT", "Casting", "Area of Effect", rand(1..3) + experience_bonus]
317
+ ]
318
+ end
319
+
320
+ # Physical combat types
321
+ type_str = @type.to_s
322
+ if ["Warrior", "Guard", "Soldier", "Gladiator", "Body guard", "Ranger", "Hunter"].any? { |t| type_str.include?(t) }
323
+ skill_pool += [
324
+ ["BODY", "Endurance", "Fortitude", rand(3..4) + experience_bonus],
325
+ ["BODY", "Endurance", "Combat Tenacity", rand(3..4) + experience_bonus],
326
+ ["MIND", "Practical Knowledge", "Ambush", rand(2..3) + experience_bonus],
327
+ ["MIND", "Awareness", "Sense Ambush", rand(2..3) + experience_bonus]
328
+ ]
329
+ end
330
+
331
+ # Scholarly types
332
+ if ["Scholar", "Sage", "Scribe", "Wizard", "Mage"].any? { |t| type_str.include?(t) }
333
+ skill_pool += [
334
+ ["MIND", "Intelligence", "Innovation", rand(3..4) + experience_bonus],
335
+ ["MIND", "Intelligence", "Problem Solving", rand(3..4) + experience_bonus],
336
+ ["MIND", "Social Knowledge", "Literacy", rand(4..5) + experience_bonus],
337
+ ["MIND", "Nature Knowledge", "Alchemy", rand(2..3) + experience_bonus]
338
+ ]
339
+ end
340
+
341
+ # Rogueish types
342
+ if ["Thief", "Rogue", "Assassin", "Scout"].any? { |t| type_str.include?(t) }
343
+ skill_pool += [
344
+ ["BODY", "Athletics", "Hide", rand(4..5) + experience_bonus],
345
+ ["BODY", "Athletics", "Move Quietly", rand(4..5) + experience_bonus],
346
+ ["BODY", "Sleight", "Pick pockets", rand(3..5) + experience_bonus],
347
+ ["BODY", "Sleight", "Disarm Traps", rand(3..4) + experience_bonus],
348
+ ["MIND", "Awareness", "Detect Traps", rand(3..4) + experience_bonus]
349
+ ]
350
+ end
351
+
352
+ # Now randomly select and add skills
353
+ skill_pool.shuffle!
354
+
355
+ skill_pool.each do |char_name, attr_name, skill_name, value|
356
+ break if skills_added >= skill_count
357
+
358
+ # Ensure the tier structure exists
359
+ next unless @tiers[char_name] && @tiers[char_name][attr_name]
360
+
361
+ # Initialize skills hash if needed
362
+ @tiers[char_name][attr_name]["skills"] ||= {}
363
+
364
+ # Add skill if it doesn't exist or is 0
365
+ if !@tiers[char_name][attr_name]["skills"][skill_name] ||
366
+ @tiers[char_name][attr_name]["skills"][skill_name] == 0
367
+ @tiers[char_name][attr_name]["skills"][skill_name] = value
368
+ skills_added += 1
369
+ end
370
+ end
371
+ end
372
+
373
+ def calculate_tier_level(base, npc_level, tier_modifier)
374
+ # Natural caps based on training difficulty
375
+ # The harder to train, the lower the natural cap
376
+
377
+ # Determine tier type and apply realistic caps
378
+ # Adjusted to create proper population distribution
379
+ tier_caps = case tier_modifier
380
+ when 1.0 # Characteristic (hardest to train)
381
+ { normal: 2, experienced: 3, master: 4, hero: 5 }
382
+ when 0.8 # Attribute (moderate training)
383
+ { normal: 3, experienced: 5, master: 6, hero: 7 }
384
+ when 0.6 # Skill (easiest to train)
385
+ { normal: 5, experienced: 7, master: 9, hero: 11 }
386
+ else
387
+ { normal: 2, experienced: 3, master: 4, hero: 5 }
388
+ end
389
+
390
+ # Determine NPC experience level - matches population distribution
391
+ experience = case npc_level
392
+ when 1..2 then :normal # Common folk
393
+ when 3..4 then :experienced # Town champions
394
+ when 5..6 then :master # Regional masters
395
+ else :hero # National/legendary
396
+ end
397
+
398
+ max_value = tier_caps[experience]
399
+
400
+ # Calculate level with diminishing returns
401
+ # Characteristics grow very slowly, skills grow faster
402
+ growth_rate = case tier_modifier
403
+ when 1.0 then 0.4 # Very slow for characteristics
404
+ when 0.8 then 0.6 # Moderate for attributes
405
+ when 0.6 then 0.8 # Faster for skills
406
+ else 0.5
407
+ end
408
+
409
+ # Use scaling that produces desired total skill progression
410
+ # Target totals: L1: 4-5, L2: 6-7, L3: 8-9, L4: 10-11, L5: 12-13, L6: 14+
411
+ # Typical warrior has BODY 1-2, Melee Combat 2-3, Weapon skill 2-3 at mid-levels
412
+ level_multiplier = case npc_level
413
+ when 1 then 0.7
414
+ when 2 then 0.95
415
+ when 3 then 1.2
416
+ when 4 then 1.45
417
+ when 5 then 1.7
418
+ when 6 then 1.95
419
+ else 2.2
420
+ end
421
+
422
+ level = (base * level_multiplier * growth_rate).to_i
423
+
424
+ # Add variation ONLY if base > 0
425
+ if base > 0
426
+ variation = rand(3) - 1 # -1, 0, or 1
427
+ level += variation
428
+ end
429
+
430
+ # Ensure minimum competence for trained individuals
431
+ # Skills should rarely be below 3 for anyone with training
432
+ if tier_modifier == 0.6 # Skills
433
+ min_skill = case npc_level
434
+ when 1..2 then 2
435
+ when 3..4 then 3
436
+ else 4
437
+ end
438
+ level = min_skill if level < min_skill && base > 0
439
+ elsif tier_modifier == 0.8 # Attributes
440
+ min_attr = case npc_level
441
+ when 1..2 then 1
442
+ when 3..4 then 2
443
+ else 3
444
+ end
445
+ level = min_attr if level < min_attr && base > 0
446
+ end
447
+
448
+ # Apply training reality - most people plateau
449
+ # Only exceptional individuals (high level + good base) reach max
450
+ if tier_modifier == 1.0 # Characteristics rarely exceed 3
451
+ level = 3 if level > 3 && rand(100) > 20 # 80% plateau at 3
452
+ elsif tier_modifier == 0.8 # Attributes occasionally reach 6
453
+ level = 5 if level > 5 && rand(100) > 40 # 60% plateau at 5
454
+ end
455
+
456
+ # Ensure within bounds
457
+ level = 0 if level < 0
458
+ level = max_value if level > max_value
459
+
460
+ level
461
+ end
462
+
463
+ def select_actual_weapon_from_table(skill_name, strength_char, is_missile = false)
464
+ # Select actual weapon from $Melee or $Missile table based on skill and strength
465
+ # This mimics the original weapon selection system
466
+ unless defined?($Melee)
467
+ load File.join($pgmdir, "includes/tables/melee.rb")
468
+ end
469
+ unless defined?($Missile)
470
+ load File.join($pgmdir, "includes/tables/missile.rb")
471
+ end
472
+
473
+ if is_missile
474
+ # Select from $Missile table based on skill type
475
+ case skill_name.downcase
476
+ when /bow/
477
+ # Select bow based on strength: L(2), M(4), H(6), H2(8), H3(10)
478
+ if strength_char >= 10
479
+ "Bow(H3) [1]"
480
+ elsif strength_char >= 8
481
+ "Bow(H2) [1]"
482
+ elsif strength_char >= 6
483
+ "Bow(H) [1]"
484
+ elsif strength_char >= 4
485
+ "Bow(M) [1]"
486
+ else
487
+ "Bow(L) [1]"
488
+ end
489
+ when /crossbow|x-bow/
490
+ if strength_char >= 4
491
+ "X-bow(H) [¼]"
492
+ elsif strength_char >= 3
493
+ "X-bow(M) [⅓]"
494
+ else
495
+ "X-bow(L) [½]"
496
+ end
497
+ when /throwing/
498
+ "Th Knife [2]"
499
+ when /javelin/
500
+ "Javelin [1]"
501
+ when /sling/
502
+ "Sling [1]"
503
+ when /net/
504
+ "Net"
505
+ when /spear/
506
+ "Javelin [1]"
507
+ else
508
+ "Rock [2]"
509
+ end
510
+ else
511
+ # Select from $Melee table based on Wield Weapon total (replaces old STRENGTH)
512
+ # This is BODY + Strength + Wield weapon skill
513
+ # Determine weapon level range based on Wield Weapon total
514
+ wpn_level = case strength_char
515
+ when 0..1 then 2
516
+ when 2..3 then 4
517
+ when 4..6 then 11
518
+ when 7..9 then 18
519
+ when 10..12 then 22
520
+ when 13..15 then 26
521
+ when 16..18 then 28
522
+ else 30
523
+ end
524
+
525
+ # Select random weapon from available range
526
+ weapon_idx = rand(wpn_level) + 1
527
+ weapon_data = $Melee[weapon_idx]
528
+
529
+ if weapon_data
530
+ weapon_data[0].strip # Return weapon name like "Longsword/Buc"
531
+ else
532
+ skill_name # Fallback to skill name
533
+ end
534
+ end
535
+ end
536
+
537
+ def add_weapon_skills(template)
538
+ # Add melee weapon skills with primary weapon specialization
539
+ # Also store actual weapon selections from $Melee/$Missile tables
540
+ if template["melee_weapons"]
541
+ @tiers["BODY"]["Melee Combat"]["skills"] ||= {}
542
+ @tiers["BODY"]["Melee Combat"]["actual_weapons"] ||= {}
543
+
544
+ # Find primary weapon (highest base value)
545
+ primary_weapon = template["melee_weapons"].max_by { |_, v| v }
546
+
547
+ # Get Wield Weapon total for weapon table selection (replaces old STRENGTH)
548
+ # This is BODY + Strength + Wield weapon skill
549
+ wield_total = get_skill_total("BODY", "Strength", "Wield weapon") rescue 3
550
+
551
+ template["melee_weapons"].each_with_index do |(weapon, skill_level), index|
552
+ base_level = calculate_tier_level(skill_level, @level, 0.6)
553
+
554
+ # Boost primary weapon by 1-2 points for specialization
555
+ if weapon == primary_weapon[0] && skill_level >= 4
556
+ boost = rand(2) + 1 # +1 or +2
557
+ base_level += boost
558
+ end
559
+
560
+ @tiers["BODY"]["Melee Combat"]["skills"][weapon] = base_level
561
+
562
+ # Select actual weapon from $Melee table based on Wield Weapon total
563
+ actual_weapon = select_actual_weapon_from_table(weapon, wield_total, false)
564
+ @tiers["BODY"]["Melee Combat"]["actual_weapons"][weapon] = actual_weapon
565
+ end
566
+ end
567
+
568
+ # Add missile weapon skills
569
+ if template["missile_weapons"]
570
+ @tiers["BODY"]["Missile Combat"]["skills"] ||= {}
571
+ @tiers["BODY"]["Missile Combat"]["actual_weapons"] ||= {}
572
+
573
+ # Find primary missile weapon
574
+ primary_missile = template["missile_weapons"].max_by { |_, v| v }
575
+
576
+ # Get Wield Weapon total for missile weapon selection (bow strength requirements)
577
+ wield_total = get_skill_total("BODY", "Strength", "Wield weapon") rescue 3
578
+
579
+ template["missile_weapons"].each do |weapon, skill_level|
580
+ base_level = calculate_tier_level(skill_level, @level, 0.6)
581
+
582
+ # Boost primary missile weapon
583
+ if weapon == primary_missile[0] && skill_level >= 3
584
+ boost = rand(2) + 1 # +1 or +2
585
+ base_level += boost
586
+ end
587
+
588
+ @tiers["BODY"]["Missile Combat"]["skills"][weapon] = base_level
589
+
590
+ # Select actual weapon based on Wield Weapon total
591
+ actual_weapon = select_actual_weapon_from_table(weapon, wield_total, true)
592
+ @tiers["BODY"]["Missile Combat"]["actual_weapons"][weapon] = actual_weapon
593
+ end
594
+ end
595
+
596
+ # Add Unarmed combat for all NPCs (everyone can fight with fists)
597
+ @tiers["BODY"]["Melee Combat"]["skills"] ||= {}
598
+ if !@tiers["BODY"]["Melee Combat"]["skills"]["Unarmed"]
599
+ # Base unarmed skill based on type and level
600
+ unarmed_bonus = case @type
601
+ when /Monk|Martial|Barbarian/
602
+ 2 # Better at unarmed
603
+ when /Warrior|Guard|Soldier|Gladiator/
604
+ 1 # Decent at unarmed
605
+ when /Wizard|Sage|Scholar|Scribe/
606
+ -1 # Poor at unarmed
607
+ else
608
+ 0 # Average
609
+ end
610
+ unarmed_skill = calculate_tier_level(1 + unarmed_bonus, @level, 0.5)
611
+ @tiers["BODY"]["Melee Combat"]["skills"]["Unarmed"] = [unarmed_skill, 0].max
612
+ end
613
+ end
614
+
615
+ def select_equipment
616
+ @weapons = []
617
+ @ENC = 0
618
+
619
+ # Get melee combat skills
620
+ melee_skills = @tiers["BODY"]["Melee Combat"]["skills"] || {}
621
+ missile_skills = @tiers["BODY"]["Missile Combat"]["skills"] || {}
622
+
623
+ # Check if has shield skill
624
+ has_shield = melee_skills["Shield"] && melee_skills["Shield"] > 0
625
+
626
+ # Determine weapon loadout based on character type and skills
627
+ case @type
628
+ when /Warrior|Guard|Soldier|Knight/
629
+ # Warriors typically have weapon + shield or two-handed weapon
630
+ if has_shield && rand(100) < 70
631
+ # Weapon + shield combo (70% chance if has shield skill)
632
+ primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Mace", "Spear"])
633
+ @weapons << primary if primary
634
+ @weapons << "Shield"
635
+ elsif rand(100) < 40
636
+ # Two-handed weapon (40% chance)
637
+ primary = select_best_weapon(melee_skills, ["2H Sword", "2H Axe", "Polearm", "Spear"])
638
+ @weapons << (primary || "Spear")
639
+ else
640
+ # Dual wield
641
+ primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Mace"])
642
+ secondary = select_best_weapon(melee_skills, ["Short sword", "Dagger", "Hatchet"])
643
+ @weapons << (primary || "Sword")
644
+ @weapons << (secondary || "Dagger")
645
+ end
646
+ when /Thief|Assassin|Rogue/
647
+ # Thieves prefer light weapons, often dual wield
648
+ primary = select_best_weapon(melee_skills, ["Short sword", "Dagger", "Rapier"])
649
+ @weapons << (primary || "Dagger")
650
+ if rand(100) < 60
651
+ # Often carry a second weapon
652
+ @weapons << "Dagger"
653
+ end
654
+ when /Ranger|Hunter|Scout/
655
+ # Rangers typically have melee + ranged
656
+ primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Spear"])
657
+ @weapons << (primary || "Sword")
658
+ if rand(100) < 30
659
+ @weapons << "Dagger" # Backup weapon
660
+ end
661
+ when /Priest|Cleric|Monk/
662
+ # Religious types often use blunt weapons
663
+ primary = select_best_weapon(melee_skills, ["Mace", "Staff", "Hammer"])
664
+ @weapons << (primary || "Staff")
665
+ if has_shield && rand(100) < 40
666
+ @weapons << "Shield"
667
+ end
668
+ when /Wizard|Mage|Sorcerer/
669
+ # Mages usually just have a staff or dagger
670
+ primary = select_best_weapon(melee_skills, ["Staff", "Dagger"])
671
+ @weapons << (primary || "Staff")
672
+ when /Noble/
673
+ # Nobles have fancy weapons
674
+ primary = select_best_weapon(melee_skills, ["Sword", "Rapier"])
675
+ @weapons << (primary || "Sword")
676
+ if rand(100) < 50
677
+ @weapons << "Dagger" # Ornamental backup
678
+ end
679
+ when /Barbarian/
680
+ # Barbarians use heavy weapons
681
+ if rand(100) < 60
682
+ primary = select_best_weapon(melee_skills, ["2H Axe", "2H Sword", "Spear"])
683
+ @weapons << (primary || "2H Axe")
684
+ else
685
+ # Dual wield
686
+ @weapons << select_best_weapon(melee_skills, ["Axe", "Sword"]) || "Axe"
687
+ @weapons << select_best_weapon(melee_skills, ["Axe", "Mace"]) || "Hatchet"
688
+ end
689
+ when /Gladiator/
690
+ # Gladiators have varied weapon combos
691
+ combo = rand(100)
692
+ if combo < 33
693
+ # Sword and shield
694
+ @weapons << select_best_weapon(melee_skills, ["Sword", "Spear"]) || "Sword"
695
+ @weapons << "Shield"
696
+ elsif combo < 66
697
+ # Dual wield
698
+ @weapons << "Sword"
699
+ @weapons << "Short sword"
700
+ else
701
+ # Exotic weapon
702
+ @weapons << select_best_weapon(melee_skills, ["Trident", "Net", "Spear"]) || "Spear"
703
+ if has_shield
704
+ @weapons << "Shield"
705
+ end
706
+ end
707
+ when /Smith/
708
+ # Smiths have varied hammers and tools - add randomization
709
+ weapon_choice = rand(100)
710
+ if weapon_choice < 40
711
+ @weapons << select_best_weapon(melee_skills, ["Hammer", "War hammer"]) || "Hammer"
712
+ elsif weapon_choice < 70
713
+ @weapons << select_best_weapon(melee_skills, ["Mace", "Hammer"]) || "Mace"
714
+ elsif weapon_choice < 85
715
+ # Smith with axe
716
+ @weapons << select_best_weapon(melee_skills, ["Axe", "Hatchet"]) || "Axe"
717
+ else
718
+ # Smith with sword (forged their own)
719
+ @weapons << select_best_weapon(melee_skills, ["Sword", "Short sword"]) || "Sword"
720
+ end
721
+ # Sometimes add a shield
722
+ if has_shield && rand(100) < 30
723
+ @weapons << "Shield"
724
+ end
725
+ when /Sailor|Seaman|Mariner/
726
+ # Sailors typically have cutlass or short sword
727
+ primary = select_best_weapon(melee_skills, ["Cutlass", "Short sword", "Hatchet"])
728
+ @weapons << (primary || "Cutlass")
729
+ if rand(100) < 40
730
+ @weapons << "Dagger" # Utility knife
731
+ end
732
+ when /Farmer|Commoner/
733
+ # Common folk have simple weapons
734
+ @weapons << select_best_weapon(melee_skills, ["Pitchfork", "Staff", "Hatchet"]) || "Staff"
735
+ else
736
+ # Default: pick best available weapon with more variety
737
+ if melee_skills.any?
738
+ # Get all weapons with decent skill
739
+ good_weapons = melee_skills.select { |k, v| v > 0 && k != "Shield" }
740
+ if good_weapons.any?
741
+ # Sort weapons by skill level and pick from the better ones
742
+ sorted_weapons = good_weapons.sort_by { |_, v| -v }
743
+ # Take top weapons (those within 2 skill points of best)
744
+ best_skill = sorted_weapons.first[1]
745
+ top_weapons = sorted_weapons.select { |_, v| v >= best_skill - 2 }
746
+
747
+ # Prefer weapons other than Dagger if available
748
+ preferred = top_weapons.reject { |k, _| k =~ /Dagger/ }
749
+ if preferred.any?
750
+ @weapons << preferred.sample[0]
751
+ else
752
+ @weapons << top_weapons.sample[0]
753
+ end
754
+
755
+ # Sometimes add a secondary weapon
756
+ if rand(100) < 30 && good_weapons.size > 1
757
+ secondary = good_weapons.keys - [@weapons.first]
758
+ @weapons << secondary.sample
759
+ end
760
+ end
761
+ else
762
+ # No skills, give more varied basic weapons
763
+ basic_weapons = ["Staff", "Short sword", "Hatchet", "Mace", "Spear", "Knife"]
764
+ @weapons << basic_weapons.sample
765
+ end
766
+ end
767
+
768
+ # Add missile weapon if applicable
769
+ if missile_skills.any?
770
+ best_missile = missile_skills.max_by { |_, v| v }
771
+ if best_missile && rand(100) < 70 # 70% chance to actually carry it
772
+ @missile_weapon = best_missile[0]
773
+ @ENC += 1
774
+ end
775
+ end
776
+
777
+ # Set primary melee weapon for compatibility
778
+ @melee_weapon = @weapons.first if @weapons.any?
779
+
780
+ # Calculate encumbrance
781
+ @ENC += @weapons.size
782
+
783
+ # Use ORIGINAL armor selection logic from class_npc.rb
784
+ # Determine armor level based on strength (exact original logic)
785
+ strng = get_characteristic("BODY") || 3
786
+ arm_level = case strng
787
+ when 1..2 then 1
788
+ when 3 then 2
789
+ when 4 then 3
790
+ when 5 then 5
791
+ when 6 then 6
792
+ when 7 then 7
793
+ else 8
794
+ end
795
+
796
+ # Pick armor from ORIGINAL $Armour table
797
+ armor_index = rand(arm_level) + 1
798
+ armor_data = $Armour[armor_index]
799
+
800
+ @armour = armor_data[0] # Armor name from original table
801
+ @ap = armor_data[1] # Armor points from original table
802
+
803
+ # Set armor in new format for compatibility with output
804
+ @armor = {
805
+ name: @armour,
806
+ ap: @ap,
807
+ enc: armor_data[4] || 0 # Weight as encumbrance
808
+ }
809
+
810
+ # Add armor encumbrance
811
+ @ENC += @armor[:enc] if @armor
812
+
813
+ # Add ORIGINAL weapon selection using $Melee and $Missile tables
814
+ generate_original_weapons
815
+ end
816
+
817
+ def generate_original_weapons
818
+ # Use EXACT original CLI weapon selection logic
819
+ # Determine weapon level based on strength (like original)
820
+ strng = get_characteristic("BODY") || 3
821
+ wpn_level = case strng
822
+ when 1 then 2
823
+ when 2 then 4
824
+ when 3 then 11
825
+ when 4 then 18
826
+ when 5 then 22
827
+ when 7..8 then 28
828
+ else 30
829
+ end
830
+
831
+ # Initialize weapon variables
832
+ @melee1 = @melee2 = @melee3 = ""
833
+ @melee1s = @melee2s = @melee3s = 0
834
+ @melee1i = @melee1o = @melee1d = @melee1dam = @melee1hp = 0
835
+ @melee2i = @melee2o = @melee2d = @melee2dam = @melee2hp = 0
836
+ @melee3i = @melee3o = @melee3d = @melee3dam = @melee3hp = 0
837
+ @missile = ""
838
+ @missiles = @missileo = @missiledam = @missilerange = 0
839
+
840
+ # Get reaction speed for initiative calculations
841
+ reaction_speed = get_skill_total("MIND", "Awareness", "Reaction speed") || 0
842
+ dodge_total = get_skill_total("BODY", "Athletics", "Dodge") || 0
843
+
844
+ # Select melee weapon 1 from ORIGINAL $Melee table
845
+ melee1_idx = rand(wpn_level) + 1
846
+ melee1_data = $Melee[melee1_idx]
847
+ @melee1 = melee1_data[0] # Weapon name like "Longsword/Buc"
848
+ @melee1s = calculate_weapon_skill_for_name(@melee1)
849
+ @melee1i = melee1_data[4] + reaction_speed # Init = weapon init + reaction
850
+ @melee1o = melee1_data[5] + @melee1s # Off = weapon off + skill
851
+ @melee1d = melee1_data[6] + @melee1s + (dodge_total / 5) # Def = weapon def + skill + dodge/5
852
+ @melee1dam = melee1_data[3] + self.DB # Damage = weapon dam + DB
853
+ @melee1hp = melee1_data[7] # Weapon hit points
854
+
855
+ # Select melee weapon 2 (if different)
856
+ melee2_idx = rand(wpn_level) + 1
857
+ if melee2_idx != melee1_idx
858
+ melee2_data = $Melee[melee2_idx]
859
+ @melee2 = melee2_data[0]
860
+ @melee2s = calculate_weapon_skill_for_name(@melee2)
861
+ @melee2i = melee2_data[4] + reaction_speed
862
+ @melee2o = melee2_data[5] + @melee2s
863
+ @melee2d = melee2_data[6] + @melee2s + (dodge_total / 5)
864
+ @melee2dam = melee2_data[3] + self.DB
865
+ @melee2hp = melee2_data[7]
866
+ end
867
+
868
+ # Select missile weapon from ORIGINAL $Missile table
869
+ msl_level = wpn_level
870
+ missile_idx = rand(msl_level) + 1
871
+ missile_data = $Missile[missile_idx]
872
+ @missile = missile_data[0] # Weapon name like "Bow(H) [1]"
873
+ @missiles = calculate_missile_skill_for_name(@missile)
874
+ @missileo = missile_data[4] + @missiles # Off = weapon off + skill
875
+ @missiledam = missile_data[3] + self.DB # Damage = weapon dam + DB
876
+ @missilerange = missile_data[5] # Range from table
877
+
878
+ # Apply strength bonus for throwing weapons (original logic)
879
+ if @missile && missile_data[1] != "Crossbow" && missile_data[1] != "Bow"
880
+ @missiledam += (strng / 5)
881
+ end
882
+ end
883
+
884
+ def calculate_weapon_skill_for_name(weapon_name)
885
+ # Map weapon names to 3-tier skills
886
+ case weapon_name.to_s.downcase
887
+ when /sword/
888
+ get_skill_total("BODY", "Melee Combat", "Sword")
889
+ when /axe/
890
+ get_skill_total("BODY", "Melee Combat", "Axe")
891
+ when /mace|club|hammer/
892
+ get_skill_total("BODY", "Melee Combat", "Club")
893
+ when /spear|polearm/
894
+ get_skill_total("BODY", "Melee Combat", "Spear")
895
+ when /staff/
896
+ get_skill_total("BODY", "Melee Combat", "Staff")
897
+ when /dagger|knife/
898
+ get_skill_total("BODY", "Melee Combat", "Dagger")
899
+ when /unarmed/
900
+ get_skill_total("BODY", "Melee Combat", "Unarmed")
901
+ else
902
+ get_skill_total("BODY", "Melee Combat", "Sword") || 0
903
+ end
904
+ end
905
+
906
+ def calculate_missile_skill_for_name(weapon_name)
907
+ case weapon_name.to_s.downcase
908
+ when /bow/
909
+ get_skill_total("BODY", "Missile Combat", "Bow")
910
+ when /crossbow|x-bow/
911
+ get_skill_total("BODY", "Missile Combat", "Crossbow")
912
+ when /sling/
913
+ get_skill_total("BODY", "Missile Combat", "Sling")
914
+ when /javelin/
915
+ get_skill_total("BODY", "Missile Combat", "Javelin")
916
+ when /rock|stone|throwing|th\s|knife/
917
+ get_skill_total("BODY", "Missile Combat", "Throwing")
918
+ else
919
+ get_skill_total("BODY", "Missile Combat", "Bow") || 0
920
+ end
921
+ end
922
+
923
+ def select_best_weapon(skills, preferred_weapons)
924
+ # Find the best weapon from preferred list that character has skill in
925
+ best_weapon = nil
926
+ best_skill = 0
927
+
928
+ preferred_weapons.each do |weapon|
929
+ if skills[weapon] && skills[weapon] > best_skill
930
+ best_weapon = weapon
931
+ best_skill = skills[weapon]
932
+ end
933
+ end
934
+
935
+ # If no preferred weapon found, check for any weapon skill
936
+ if !best_weapon && skills.any?
937
+ # Filter out Shield as it's not a primary weapon
938
+ weapon_skills = skills.reject { |k, _| k == "Shield" }
939
+ if weapon_skills.any?
940
+ best = weapon_skills.max_by { |_, v| v }
941
+ best_weapon = best[0]
942
+ end
943
+ end
944
+
945
+ best_weapon
946
+ end
947
+
948
+ def has_template_magic?(template)
949
+ # Check if template indicates magical ability
950
+ return false unless template
951
+
952
+ # Check if template has Casting attribute > 0
953
+ casting_attr = template["attributes"]["SPIRIT/Casting"] || 0
954
+ return true if casting_attr > 0
955
+
956
+ # Check for specific magic-using types
957
+ magic_types = ["Mage", "Wizard", "Witch (white)", "Witch (black)", "Sorcerer",
958
+ "Summoner", "Priest", "Sage", "Seer"]
959
+ type_str = @type.to_s
960
+ magic_types.include?(type_str) || type_str.include?("Wizard")
961
+ end
962
+
963
+ # Moved to public section below
964
+
965
+ def generate_spells
966
+ # Generate spell cards based on character type and level
967
+ spirit = get_characteristic("SPIRIT")
968
+ casting_attr = get_attribute("SPIRIT", "Casting")
969
+ casting_total = spirit + casting_attr
970
+
971
+ # Restrict spells to appropriate creatures
972
+ # Require SPIRIT >= 2 AND total Casting >= 5
973
+ # Exclude specific races and creature types
974
+ excluded_types = ["Araxi", "Troll", "Dwarf", "Ogre", "Lizard", "Animal", "Zombie", "Skeleton"]
975
+ type_lower = @type.to_s.downcase
976
+
977
+ is_excluded = excluded_types.any? { |ex| type_lower.include?(ex.downcase) }
978
+
979
+ # Only generate spells for intelligent magical beings
980
+ if spirit >= 2 && casting_total >= 5 && !is_excluded
981
+ # Load spell database if not already loaded
982
+ unless defined?($SpellDatabase)
983
+ load File.join($pgmdir, "includes/tables/spells_new.rb")
984
+ end
985
+
986
+ @spells = generate_spell_cards(@type, @level, casting_total)
987
+ end
988
+ end
989
+
990
+ # Dice rolling methods (matching old system)
991
+ def d6
992
+ rand(1..6)
993
+ end
994
+
995
+ def oD6
996
+ # Open-ended D6 roll
997
+ result = d6
998
+ return result if (2..5).include?(result)
999
+
1000
+ if result == 1
1001
+ down = d6
1002
+ while down <= 3
1003
+ result -= 1
1004
+ down = d6
1005
+ end
1006
+ elsif result == 6
1007
+ up = d6
1008
+ while up >= 4
1009
+ result += 1
1010
+ up = d6
1011
+ end
1012
+ end
1013
+ result
1014
+ end
1015
+
1016
+ def aD6
1017
+ # Average D6 roll (average of regular d6 and open-ended d6)
1018
+ ((d6 + oD6) / 2.0).to_i
1019
+ end
1020
+
1021
+ # Public methods for accessing tier data
1022
+ public
1023
+
1024
+ def has_magic?
1025
+ # Check if character has any casting ability
1026
+ casting_level = @tiers["SPIRIT"]["Casting"]["level"] || 0
1027
+ casting_level > 0
1028
+ end
1029
+
1030
+ # Calculate derived stats
1031
+ def SIZE
1032
+ # SIZE system based on weight with half-sizes
1033
+ case @weight
1034
+ when 0...10 then 0.5
1035
+ when 10...15 then 1
1036
+ when 15...20 then 1.5
1037
+ when 20...35 then 2
1038
+ when 35...50 then 2.5
1039
+ when 50...75 then 3
1040
+ when 75...100 then 3.5
1041
+ when 100...125 then 4
1042
+ when 125...150 then 4.5
1043
+ when 150...188 then 5
1044
+ when 188...225 then 5.5
1045
+ when 225...263 then 6
1046
+ when 263...300 then 6.5
1047
+ when 300...350 then 7
1048
+ when 350...400 then 7.5
1049
+ when 400...450 then 8
1050
+ when 450...500 then 8.5
1051
+ when 500...550 then 9
1052
+ when 550...600 then 9.5
1053
+ when 600...663 then 10
1054
+ when 663...725 then 10.5
1055
+ when 725...788 then 11
1056
+ when 788...850 then 11.5
1057
+ when 850...925 then 12
1058
+ when 925...1000 then 12.5
1059
+ when 1000...1075 then 13
1060
+ when 1075...1150 then 13.5
1061
+ when 1150...1225 then 14
1062
+ when 1225...1300 then 14.5
1063
+ when 1300...1375 then 15
1064
+ when 1375...1450 then 15.5
1065
+ when 1450...1525 then 16
1066
+ when 1525...1600 then 16.5
1067
+ else
1068
+ # For very large creatures, add 0.5 per 100kg
1069
+ 16.5 + ((@weight - 1600) / 100.0).floor * 0.5
1070
+ end
1071
+ end
1072
+
1073
+ def BP
1074
+ # Body Points: SIZE * 2 + Fortitude / 3
1075
+ # Fortitude is under Endurance
1076
+ fortitude = get_skill("BODY", "Endurance", "Fortitude")
1077
+ endurance = get_attribute("BODY", "Endurance")
1078
+ body = get_characteristic("BODY")
1079
+ total_fortitude = body + endurance + fortitude
1080
+ (self.SIZE * 2 + total_fortitude / 3.0).round
1081
+ end
1082
+
1083
+ def DB
1084
+ # Damage Bonus: (SIZE + Strength total) / 3
1085
+ strength = get_attribute("BODY", "Strength")
1086
+ body = get_characteristic("BODY")
1087
+ total_strength = body + strength
1088
+ ((self.SIZE + total_strength) / 3.0).round
1089
+ end
1090
+
1091
+ def MD
1092
+ # Magic Defense: (Mental Fortitude + Attunement Self) / 3
1093
+ mental_fortitude = get_skill("MIND", "Willpower", "Mental Fortitude")
1094
+ willpower = get_attribute("MIND", "Willpower")
1095
+ mind = get_characteristic("MIND")
1096
+ total_mental_fortitude = mind + willpower + mental_fortitude
1097
+
1098
+ attunement_self = get_skill("SPIRIT", "Attunement", "Self")
1099
+ attunement = get_attribute("SPIRIT", "Attunement")
1100
+ spirit = get_characteristic("SPIRIT")
1101
+ total_attunement_self = spirit + attunement + attunement_self
1102
+
1103
+ ((total_mental_fortitude + total_attunement_self) / 3.0).round
1104
+ end
1105
+
1106
+ def get_characteristic(name)
1107
+ # Calculate characteristic as weighted average of its attributes
1108
+ # Reflects years of broad training across all aspects
1109
+ total = 0
1110
+ count = 0
1111
+ @tiers[name].each do |attr_name, attr_data|
1112
+ if attr_data["level"] && attr_data["level"] > 0
1113
+ total += attr_data["level"]
1114
+ count += 1
1115
+ end
1116
+ end
1117
+
1118
+ return 0 if count == 0
1119
+
1120
+ # Characteristics are very hard to improve - most people stay at 2-3
1121
+ char_level = (total.to_f / count / 1.5).round # Scaled down to reflect training difficulty
1122
+
1123
+ # Apply realistic caps based on NPC level
1124
+ max_char = case @level
1125
+ when 1..2 then 2 # Novices rarely exceed 2
1126
+ when 3..4 then 3 # Experienced typically max at 3
1127
+ when 5..6 then 4 # Veterans might reach 4
1128
+ else 5 # Only masters achieve 5
1129
+ end
1130
+
1131
+ char_level = max_char if char_level > max_char
1132
+ char_level
1133
+ end
1134
+
1135
+ def get_attribute(char_name, attr_name)
1136
+ @tiers[char_name][attr_name]["level"] || 0
1137
+ end
1138
+
1139
+ def get_skill(char_name, attr_name, skill_name)
1140
+ @tiers[char_name][attr_name]["skills"][skill_name] || 0
1141
+ end
1142
+
1143
+ def get_skill_total(char_name, attr_name, skill_name)
1144
+ # Calculate total: Characteristic + Attribute + Skill
1145
+ char_level = get_characteristic(char_name)
1146
+ attr_level = get_attribute(char_name, attr_name)
1147
+ skill_level = get_skill(char_name, attr_name, skill_name)
1148
+
1149
+ # Natural progression based on training difficulty:
1150
+ # - Getting to 10 is achievable with focused skill training
1151
+ # - Getting to 15 requires years of dedicated work
1152
+ # - Getting to 18+ is legendary, requiring lifetime mastery
1153
+ total = char_level + attr_level + skill_level
1154
+
1155
+ # Apply soft cap at 18 for game balance (only true masters exceed)
1156
+ if total > 18 && @level < 7
1157
+ # Small chance for exceptional individuals to exceed 18
1158
+ total = 18 unless rand(100) < 5
1159
+ end
1160
+
1161
+ total
1162
+ end
1163
+
1164
+ # Mark system for progression
1165
+
1166
+ def add_mark(char_name, attr_name, skill_name = nil)
1167
+ if skill_name
1168
+ # Add mark to skill
1169
+ key = "#{attr_name}/#{skill_name}"
1170
+ @marks[char_name][key] ||= 0
1171
+ @marks[char_name][key] += 1
1172
+
1173
+ # Check if ready to advance
1174
+ check_advancement(char_name, attr_name, skill_name)
1175
+ else
1176
+ # Add mark to attribute
1177
+ @marks[char_name][attr_name] ||= 0
1178
+ @marks[char_name][attr_name] += 1
1179
+ end
1180
+ end
1181
+
1182
+ def check_advancement(char_name, attr_name, skill_name)
1183
+ current_level = get_skill(char_name, attr_name, skill_name)
1184
+ required_marks = (current_level + 1) * 5
1185
+ key = "#{attr_name}/#{skill_name}"
1186
+
1187
+ if @marks[char_name][key] >= required_marks
1188
+ # Roll for advancement (all but a 1)
1189
+ if oD6 > 1
1190
+ @tiers[char_name][attr_name]["skills"][skill_name] += 1
1191
+ @marks[char_name][key] = 0
1192
+
1193
+ # Add mark to attribute above
1194
+ add_mark(char_name, attr_name)
1195
+
1196
+ return true
1197
+ end
1198
+ end
1199
+ false
1200
+ end
1201
+
1202
+ private
1203
+
1204
+ def generate_random_name(sex)
1205
+ # Use the proper name generator based on race
1206
+ race = @type.to_s.sub(/(:| ).*/, '').capitalize
1207
+
1208
+ # Use naming function from functions.rb which uses the actual name generator
1209
+ name = naming(race, sex)
1210
+
1211
+ # Fallback to basic names if name generator fails
1212
+ if name.nil? || name.empty?
1213
+ if sex == "F" || (sex.empty? && rand(2) == 0)
1214
+ female_names = ["Aria", "Luna", "Sera", "Mira", "Lyra", "Nova", "Kira", "Zara", "Elara", "Thalia"]
1215
+ name = female_names.sample + " " + ["Starweaver", "Moonwhisper", "Brightblade", "Swiftwind", "Ironheart"].sample
1216
+ else
1217
+ male_names = ["Gareth", "Marcus", "Aldric", "Kael", "Doran", "Lucian", "Theron", "Cassius", "Orion", "Zephyr"]
1218
+ name = male_names.sample + " " + ["Ironforge", "Stormcaller", "Darkbane", "Goldhand", "Steelclaw"].sample
1219
+ end
1220
+ end
1221
+
1222
+ name
1223
+ end
1224
+
1225
+ def ensure_essential_skills
1226
+ # Ensure all NPCs have essential combat/stealth awareness skills (even if at 0)
1227
+ # These skills are frequently used in encounters
1228
+
1229
+ # Ensure Athletics attribute exists with essential skills
1230
+ @tiers["BODY"]["Athletics"] ||= {"level" => 0, "skills" => {}}
1231
+ @tiers["BODY"]["Athletics"]["skills"] ||= {}
1232
+
1233
+ # Add Move Quietly if missing
1234
+ unless @tiers["BODY"]["Athletics"]["skills"].key?("Move Quietly")
1235
+ @tiers["BODY"]["Athletics"]["skills"]["Move Quietly"] = 0
1236
+ end
1237
+
1238
+ # Add Hide if missing
1239
+ unless @tiers["BODY"]["Athletics"]["skills"].key?("Hide")
1240
+ @tiers["BODY"]["Athletics"]["skills"]["Hide"] = 0
1241
+ end
1242
+
1243
+ # Add Dodge if missing (often used in combat)
1244
+ unless @tiers["BODY"]["Athletics"]["skills"].key?("Dodge")
1245
+ @tiers["BODY"]["Athletics"]["skills"]["Dodge"] = 0
1246
+ end
1247
+
1248
+ # Ensure Awareness attribute exists with essential skills
1249
+ @tiers["MIND"]["Awareness"] ||= {"level" => 0, "skills" => {}}
1250
+ @tiers["MIND"]["Awareness"]["skills"] ||= {}
1251
+
1252
+ # Add Alertness if missing (this is the general awareness skill)
1253
+ unless @tiers["MIND"]["Awareness"]["skills"].key?("Alertness")
1254
+ @tiers["MIND"]["Awareness"]["skills"]["Alertness"] = 0
1255
+ end
1256
+
1257
+ # Reaction speed should always be present for initiative calculations
1258
+ unless @tiers["MIND"]["Awareness"]["skills"].key?("Reaction speed")
1259
+ @tiers["MIND"]["Awareness"]["skills"]["Reaction speed"] = 0
1260
+ end
1261
+
1262
+ # Ensure Social Knowledge attribute exists for Spoken Language
1263
+ @tiers["MIND"]["Social Knowledge"] ||= {"level" => 0, "skills" => {}}
1264
+ @tiers["MIND"]["Social Knowledge"]["skills"] ||= {}
1265
+
1266
+ # Ensure Spoken Language is at least 2 (native tongue)
1267
+ # All characters start with 2 in Spoken Language per character creation rules
1268
+ current_spoken = @tiers["MIND"]["Social Knowledge"]["skills"]["Spoken Language"] || 0
1269
+ @tiers["MIND"]["Social Knowledge"]["skills"]["Spoken Language"] = [current_spoken, 2].max
1270
+ end
1271
+
1272
+ def apply_predetermined_stats
1273
+ # Apply predetermined stats to preserve encounter consistency
1274
+ # This allows encounters to pass specific stats that should not be randomized
1275
+
1276
+ return unless @predetermined_stats.is_a?(Hash)
1277
+
1278
+ # Override characteristics if provided
1279
+ if @predetermined_stats["characteristics"]
1280
+ @predetermined_stats["characteristics"].each do |char_name, value|
1281
+ # This would require modifying the SIZE calculation system
1282
+ # For now, we'll focus on weapons/armor/skills
1283
+ end
1284
+ end
1285
+
1286
+ # Override specific weapon skills if provided
1287
+ if @predetermined_stats["weapon_skills"]
1288
+ @predetermined_stats["weapon_skills"].each do |weapon, skill_level|
1289
+ # Add to melee combat skills
1290
+ @tiers["BODY"]["Melee Combat"]["skills"] ||= {}
1291
+ @tiers["BODY"]["Melee Combat"]["skills"][weapon] = skill_level.to_i
1292
+ end
1293
+ end
1294
+
1295
+ # Override missile skills if provided
1296
+ if @predetermined_stats["missile_skills"]
1297
+ @predetermined_stats["missile_skills"].each do |weapon, skill_level|
1298
+ @tiers["BODY"]["Missile Combat"]["skills"] ||= {}
1299
+ @tiers["BODY"]["Missile Combat"]["skills"][weapon] = skill_level.to_i
1300
+ end
1301
+ end
1302
+
1303
+ # Override armor if provided
1304
+ if @predetermined_stats["armor"]
1305
+ @armor = @predetermined_stats["armor"]
1306
+ end
1307
+
1308
+ # Override other skills if provided
1309
+ if @predetermined_stats["skills"]
1310
+ @predetermined_stats["skills"].each do |skill_path, value|
1311
+ parts = skill_path.split("/")
1312
+ next unless parts.length == 3
1313
+
1314
+ char_name, attr_name, skill_name = parts
1315
+ next unless @tiers[char_name] && @tiers[char_name][attr_name]
1316
+
1317
+ @tiers[char_name][attr_name]["skills"] ||= {}
1318
+ @tiers[char_name][attr_name]["skills"][skill_name] = value.to_i
1319
+ end
1320
+ end
1321
+ end
1322
+ end