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,1187 @@
|
|
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
|
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 square root for more realistic progression
|
410
|
+
level = (base * Math.sqrt(npc_level + 1) * growth_rate).to_i
|
411
|
+
|
412
|
+
# Add minimal variation ONLY if base > 0
|
413
|
+
if base > 0
|
414
|
+
variation = rand(3) - 1 # -1, 0, or 1
|
415
|
+
level += variation
|
416
|
+
end
|
417
|
+
|
418
|
+
# Ensure minimum competence for trained individuals
|
419
|
+
# Skills should rarely be below 3 for anyone with training
|
420
|
+
if tier_modifier == 0.6 # Skills
|
421
|
+
min_skill = case npc_level
|
422
|
+
when 1..2 then 2
|
423
|
+
when 3..4 then 3
|
424
|
+
else 4
|
425
|
+
end
|
426
|
+
level = min_skill if level < min_skill && base > 0
|
427
|
+
elsif tier_modifier == 0.8 # Attributes
|
428
|
+
min_attr = case npc_level
|
429
|
+
when 1..2 then 1
|
430
|
+
when 3..4 then 2
|
431
|
+
else 3
|
432
|
+
end
|
433
|
+
level = min_attr if level < min_attr && base > 0
|
434
|
+
end
|
435
|
+
|
436
|
+
# Apply training reality - most people plateau
|
437
|
+
# Only exceptional individuals (high level + good base) reach max
|
438
|
+
if tier_modifier == 1.0 # Characteristics rarely exceed 3
|
439
|
+
level = 3 if level > 3 && rand(100) > 20 # 80% plateau at 3
|
440
|
+
elsif tier_modifier == 0.8 # Attributes occasionally reach 6
|
441
|
+
level = 5 if level > 5 && rand(100) > 40 # 60% plateau at 5
|
442
|
+
end
|
443
|
+
|
444
|
+
# Ensure within bounds
|
445
|
+
level = 0 if level < 0
|
446
|
+
level = max_value if level > max_value
|
447
|
+
|
448
|
+
level
|
449
|
+
end
|
450
|
+
|
451
|
+
def add_weapon_skills(template)
|
452
|
+
# Add melee weapon skills
|
453
|
+
if template["melee_weapons"]
|
454
|
+
@tiers["BODY"]["Melee Combat"]["skills"] ||= {}
|
455
|
+
template["melee_weapons"].each do |weapon, skill_level|
|
456
|
+
base_level = calculate_tier_level(skill_level, @level, 0.6)
|
457
|
+
@tiers["BODY"]["Melee Combat"]["skills"][weapon] = base_level
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# Add missile weapon skills
|
462
|
+
if template["missile_weapons"]
|
463
|
+
@tiers["BODY"]["Missile Combat"]["skills"] ||= {}
|
464
|
+
template["missile_weapons"].each do |weapon, skill_level|
|
465
|
+
base_level = calculate_tier_level(skill_level, @level, 0.6)
|
466
|
+
@tiers["BODY"]["Missile Combat"]["skills"][weapon] = base_level
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
# Add Unarmed combat for all NPCs (everyone can fight with fists)
|
471
|
+
@tiers["BODY"]["Melee Combat"]["skills"] ||= {}
|
472
|
+
if !@tiers["BODY"]["Melee Combat"]["skills"]["Unarmed"]
|
473
|
+
# Base unarmed skill based on type and level
|
474
|
+
unarmed_bonus = case @type
|
475
|
+
when /Monk|Martial|Barbarian/
|
476
|
+
2 # Better at unarmed
|
477
|
+
when /Warrior|Guard|Soldier|Gladiator/
|
478
|
+
1 # Decent at unarmed
|
479
|
+
when /Wizard|Sage|Scholar|Scribe/
|
480
|
+
-1 # Poor at unarmed
|
481
|
+
else
|
482
|
+
0 # Average
|
483
|
+
end
|
484
|
+
unarmed_skill = calculate_tier_level(1 + unarmed_bonus, @level, 0.5)
|
485
|
+
@tiers["BODY"]["Melee Combat"]["skills"]["Unarmed"] = [unarmed_skill, 0].max
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
def select_equipment
|
490
|
+
@weapons = []
|
491
|
+
@ENC = 0
|
492
|
+
|
493
|
+
# Get melee combat skills
|
494
|
+
melee_skills = @tiers["BODY"]["Melee Combat"]["skills"] || {}
|
495
|
+
missile_skills = @tiers["BODY"]["Missile Combat"]["skills"] || {}
|
496
|
+
|
497
|
+
# Check if has shield skill
|
498
|
+
has_shield = melee_skills["Shield"] && melee_skills["Shield"] > 0
|
499
|
+
|
500
|
+
# Determine weapon loadout based on character type and skills
|
501
|
+
case @type
|
502
|
+
when /Warrior|Guard|Soldier|Knight/
|
503
|
+
# Warriors typically have weapon + shield or two-handed weapon
|
504
|
+
if has_shield && rand(100) < 70
|
505
|
+
# Weapon + shield combo (70% chance if has shield skill)
|
506
|
+
primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Mace", "Spear"])
|
507
|
+
@weapons << primary if primary
|
508
|
+
@weapons << "Shield"
|
509
|
+
elsif rand(100) < 40
|
510
|
+
# Two-handed weapon (40% chance)
|
511
|
+
primary = select_best_weapon(melee_skills, ["2H Sword", "2H Axe", "Polearm", "Spear"])
|
512
|
+
@weapons << (primary || "Spear")
|
513
|
+
else
|
514
|
+
# Dual wield
|
515
|
+
primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Mace"])
|
516
|
+
secondary = select_best_weapon(melee_skills, ["Short sword", "Dagger", "Hatchet"])
|
517
|
+
@weapons << (primary || "Sword")
|
518
|
+
@weapons << (secondary || "Dagger")
|
519
|
+
end
|
520
|
+
when /Thief|Assassin|Rogue/
|
521
|
+
# Thieves prefer light weapons, often dual wield
|
522
|
+
primary = select_best_weapon(melee_skills, ["Short sword", "Dagger", "Rapier"])
|
523
|
+
@weapons << (primary || "Dagger")
|
524
|
+
if rand(100) < 60
|
525
|
+
# Often carry a second weapon
|
526
|
+
@weapons << "Dagger"
|
527
|
+
end
|
528
|
+
when /Ranger|Hunter|Scout/
|
529
|
+
# Rangers typically have melee + ranged
|
530
|
+
primary = select_best_weapon(melee_skills, ["Sword", "Axe", "Spear"])
|
531
|
+
@weapons << (primary || "Sword")
|
532
|
+
if rand(100) < 30
|
533
|
+
@weapons << "Dagger" # Backup weapon
|
534
|
+
end
|
535
|
+
when /Priest|Cleric|Monk/
|
536
|
+
# Religious types often use blunt weapons
|
537
|
+
primary = select_best_weapon(melee_skills, ["Mace", "Staff", "Hammer"])
|
538
|
+
@weapons << (primary || "Staff")
|
539
|
+
if has_shield && rand(100) < 40
|
540
|
+
@weapons << "Shield"
|
541
|
+
end
|
542
|
+
when /Wizard|Mage|Sorcerer/
|
543
|
+
# Mages usually just have a staff or dagger
|
544
|
+
primary = select_best_weapon(melee_skills, ["Staff", "Dagger"])
|
545
|
+
@weapons << (primary || "Staff")
|
546
|
+
when /Noble/
|
547
|
+
# Nobles have fancy weapons
|
548
|
+
primary = select_best_weapon(melee_skills, ["Sword", "Rapier"])
|
549
|
+
@weapons << (primary || "Sword")
|
550
|
+
if rand(100) < 50
|
551
|
+
@weapons << "Dagger" # Ornamental backup
|
552
|
+
end
|
553
|
+
when /Barbarian/
|
554
|
+
# Barbarians use heavy weapons
|
555
|
+
if rand(100) < 60
|
556
|
+
primary = select_best_weapon(melee_skills, ["2H Axe", "2H Sword", "Spear"])
|
557
|
+
@weapons << (primary || "2H Axe")
|
558
|
+
else
|
559
|
+
# Dual wield
|
560
|
+
@weapons << select_best_weapon(melee_skills, ["Axe", "Sword"]) || "Axe"
|
561
|
+
@weapons << select_best_weapon(melee_skills, ["Axe", "Mace"]) || "Hatchet"
|
562
|
+
end
|
563
|
+
when /Gladiator/
|
564
|
+
# Gladiators have varied weapon combos
|
565
|
+
combo = rand(100)
|
566
|
+
if combo < 33
|
567
|
+
# Sword and shield
|
568
|
+
@weapons << select_best_weapon(melee_skills, ["Sword", "Spear"]) || "Sword"
|
569
|
+
@weapons << "Shield"
|
570
|
+
elsif combo < 66
|
571
|
+
# Dual wield
|
572
|
+
@weapons << "Sword"
|
573
|
+
@weapons << "Short sword"
|
574
|
+
else
|
575
|
+
# Exotic weapon
|
576
|
+
@weapons << select_best_weapon(melee_skills, ["Trident", "Net", "Spear"]) || "Spear"
|
577
|
+
if has_shield
|
578
|
+
@weapons << "Shield"
|
579
|
+
end
|
580
|
+
end
|
581
|
+
when /Smith/
|
582
|
+
# Smiths have varied hammers and tools - add randomization
|
583
|
+
weapon_choice = rand(100)
|
584
|
+
if weapon_choice < 40
|
585
|
+
@weapons << select_best_weapon(melee_skills, ["Hammer", "War hammer"]) || "Hammer"
|
586
|
+
elsif weapon_choice < 70
|
587
|
+
@weapons << select_best_weapon(melee_skills, ["Mace", "Hammer"]) || "Mace"
|
588
|
+
elsif weapon_choice < 85
|
589
|
+
# Smith with axe
|
590
|
+
@weapons << select_best_weapon(melee_skills, ["Axe", "Hatchet"]) || "Axe"
|
591
|
+
else
|
592
|
+
# Smith with sword (forged their own)
|
593
|
+
@weapons << select_best_weapon(melee_skills, ["Sword", "Short sword"]) || "Sword"
|
594
|
+
end
|
595
|
+
# Sometimes add a shield
|
596
|
+
if has_shield && rand(100) < 30
|
597
|
+
@weapons << "Shield"
|
598
|
+
end
|
599
|
+
when /Sailor|Seaman|Mariner/
|
600
|
+
# Sailors typically have cutlass or short sword
|
601
|
+
primary = select_best_weapon(melee_skills, ["Cutlass", "Short sword", "Hatchet"])
|
602
|
+
@weapons << (primary || "Cutlass")
|
603
|
+
if rand(100) < 40
|
604
|
+
@weapons << "Dagger" # Utility knife
|
605
|
+
end
|
606
|
+
when /Farmer|Commoner/
|
607
|
+
# Common folk have simple weapons
|
608
|
+
@weapons << select_best_weapon(melee_skills, ["Pitchfork", "Staff", "Hatchet"]) || "Staff"
|
609
|
+
else
|
610
|
+
# Default: pick best available weapon with more variety
|
611
|
+
if melee_skills.any?
|
612
|
+
# Get all weapons with decent skill
|
613
|
+
good_weapons = melee_skills.select { |k, v| v > 0 && k != "Shield" }
|
614
|
+
if good_weapons.any?
|
615
|
+
# Sort weapons by skill level and pick from the better ones
|
616
|
+
sorted_weapons = good_weapons.sort_by { |_, v| -v }
|
617
|
+
# Take top weapons (those within 2 skill points of best)
|
618
|
+
best_skill = sorted_weapons.first[1]
|
619
|
+
top_weapons = sorted_weapons.select { |_, v| v >= best_skill - 2 }
|
620
|
+
|
621
|
+
# Prefer weapons other than Dagger if available
|
622
|
+
preferred = top_weapons.reject { |k, _| k =~ /Dagger/ }
|
623
|
+
if preferred.any?
|
624
|
+
@weapons << preferred.sample[0]
|
625
|
+
else
|
626
|
+
@weapons << top_weapons.sample[0]
|
627
|
+
end
|
628
|
+
|
629
|
+
# Sometimes add a secondary weapon
|
630
|
+
if rand(100) < 30 && good_weapons.size > 1
|
631
|
+
secondary = good_weapons.keys - [@weapons.first]
|
632
|
+
@weapons << secondary.sample
|
633
|
+
end
|
634
|
+
end
|
635
|
+
else
|
636
|
+
# No skills, give more varied basic weapons
|
637
|
+
basic_weapons = ["Staff", "Short sword", "Hatchet", "Mace", "Spear", "Knife"]
|
638
|
+
@weapons << basic_weapons.sample
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
# Add missile weapon if applicable
|
643
|
+
if missile_skills.any?
|
644
|
+
best_missile = missile_skills.max_by { |_, v| v }
|
645
|
+
if best_missile && rand(100) < 70 # 70% chance to actually carry it
|
646
|
+
@missile_weapon = best_missile[0]
|
647
|
+
@ENC += 1
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
# Set primary melee weapon for compatibility
|
652
|
+
@melee_weapon = @weapons.first if @weapons.any?
|
653
|
+
|
654
|
+
# Calculate encumbrance
|
655
|
+
@ENC += @weapons.size
|
656
|
+
|
657
|
+
# Use ORIGINAL armor selection logic from class_npc.rb
|
658
|
+
# Determine armor level based on strength (exact original logic)
|
659
|
+
strng = get_characteristic("BODY") || 3
|
660
|
+
arm_level = case strng
|
661
|
+
when 1..2 then 1
|
662
|
+
when 3 then 2
|
663
|
+
when 4 then 3
|
664
|
+
when 5 then 5
|
665
|
+
when 6 then 6
|
666
|
+
when 7 then 7
|
667
|
+
else 8
|
668
|
+
end
|
669
|
+
|
670
|
+
# Pick armor from ORIGINAL $Armour table
|
671
|
+
armor_index = rand(arm_level) + 1
|
672
|
+
armor_data = $Armour[armor_index]
|
673
|
+
|
674
|
+
@armour = armor_data[0] # Armor name from original table
|
675
|
+
@ap = armor_data[1] # Armor points from original table
|
676
|
+
|
677
|
+
# Set armor in new format for compatibility with output
|
678
|
+
@armor = {
|
679
|
+
name: @armour,
|
680
|
+
ap: @ap,
|
681
|
+
enc: armor_data[4] || 0 # Weight as encumbrance
|
682
|
+
}
|
683
|
+
|
684
|
+
# Add armor encumbrance
|
685
|
+
@ENC += @armor[:enc] if @armor
|
686
|
+
|
687
|
+
# Add ORIGINAL weapon selection using $Melee and $Missile tables
|
688
|
+
generate_original_weapons
|
689
|
+
end
|
690
|
+
|
691
|
+
def generate_original_weapons
|
692
|
+
# Use EXACT original CLI weapon selection logic
|
693
|
+
# Determine weapon level based on strength (like original)
|
694
|
+
strng = get_characteristic("BODY") || 3
|
695
|
+
wpn_level = case strng
|
696
|
+
when 1 then 2
|
697
|
+
when 2 then 4
|
698
|
+
when 3 then 11
|
699
|
+
when 4 then 18
|
700
|
+
when 5 then 22
|
701
|
+
when 7..8 then 28
|
702
|
+
else 30
|
703
|
+
end
|
704
|
+
|
705
|
+
# Initialize weapon variables
|
706
|
+
@melee1 = @melee2 = @melee3 = ""
|
707
|
+
@melee1s = @melee2s = @melee3s = 0
|
708
|
+
@melee1i = @melee1o = @melee1d = @melee1dam = @melee1hp = 0
|
709
|
+
@melee2i = @melee2o = @melee2d = @melee2dam = @melee2hp = 0
|
710
|
+
@melee3i = @melee3o = @melee3d = @melee3dam = @melee3hp = 0
|
711
|
+
@missile = ""
|
712
|
+
@missiles = @missileo = @missiledam = @missilerange = 0
|
713
|
+
|
714
|
+
# Get reaction speed for initiative calculations
|
715
|
+
reaction_speed = get_skill_total("MIND", "Awareness", "Reaction speed") || 0
|
716
|
+
dodge_total = get_skill_total("BODY", "Athletics", "Dodge") || 0
|
717
|
+
|
718
|
+
# Select melee weapon 1 from ORIGINAL $Melee table
|
719
|
+
melee1_idx = rand(wpn_level) + 1
|
720
|
+
melee1_data = $Melee[melee1_idx]
|
721
|
+
@melee1 = melee1_data[0] # Weapon name like "Longsword/Buc"
|
722
|
+
@melee1s = calculate_weapon_skill_for_name(@melee1)
|
723
|
+
@melee1i = melee1_data[4] + reaction_speed # Init = weapon init + reaction
|
724
|
+
@melee1o = melee1_data[5] + @melee1s # Off = weapon off + skill
|
725
|
+
@melee1d = melee1_data[6] + @melee1s + (dodge_total / 5) # Def = weapon def + skill + dodge/5
|
726
|
+
@melee1dam = melee1_data[3] + self.DB # Damage = weapon dam + DB
|
727
|
+
@melee1hp = melee1_data[7] # Weapon hit points
|
728
|
+
|
729
|
+
# Select melee weapon 2 (if different)
|
730
|
+
melee2_idx = rand(wpn_level) + 1
|
731
|
+
if melee2_idx != melee1_idx
|
732
|
+
melee2_data = $Melee[melee2_idx]
|
733
|
+
@melee2 = melee2_data[0]
|
734
|
+
@melee2s = calculate_weapon_skill_for_name(@melee2)
|
735
|
+
@melee2i = melee2_data[4] + reaction_speed
|
736
|
+
@melee2o = melee2_data[5] + @melee2s
|
737
|
+
@melee2d = melee2_data[6] + @melee2s + (dodge_total / 5)
|
738
|
+
@melee2dam = melee2_data[3] + self.DB
|
739
|
+
@melee2hp = melee2_data[7]
|
740
|
+
end
|
741
|
+
|
742
|
+
# Select missile weapon from ORIGINAL $Missile table
|
743
|
+
msl_level = wpn_level
|
744
|
+
missile_idx = rand(msl_level) + 1
|
745
|
+
missile_data = $Missile[missile_idx]
|
746
|
+
@missile = missile_data[0] # Weapon name like "Bow(H) [1]"
|
747
|
+
@missiles = calculate_missile_skill_for_name(@missile)
|
748
|
+
@missileo = missile_data[4] + @missiles # Off = weapon off + skill
|
749
|
+
@missiledam = missile_data[3] + self.DB # Damage = weapon dam + DB
|
750
|
+
@missilerange = missile_data[5] # Range from table
|
751
|
+
|
752
|
+
# Apply strength bonus for throwing weapons (original logic)
|
753
|
+
if @missile && missile_data[1] != "Crossbow" && missile_data[1] != "Bow"
|
754
|
+
@missiledam += (strng / 5)
|
755
|
+
end
|
756
|
+
end
|
757
|
+
|
758
|
+
def calculate_weapon_skill_for_name(weapon_name)
|
759
|
+
# Map weapon names to 3-tier skills
|
760
|
+
case weapon_name.to_s.downcase
|
761
|
+
when /sword/
|
762
|
+
get_skill_total("BODY", "Melee Combat", "Sword")
|
763
|
+
when /axe/
|
764
|
+
get_skill_total("BODY", "Melee Combat", "Axe")
|
765
|
+
when /mace|club|hammer/
|
766
|
+
get_skill_total("BODY", "Melee Combat", "Club")
|
767
|
+
when /spear|polearm/
|
768
|
+
get_skill_total("BODY", "Melee Combat", "Spear")
|
769
|
+
when /staff/
|
770
|
+
get_skill_total("BODY", "Melee Combat", "Staff")
|
771
|
+
when /dagger|knife/
|
772
|
+
get_skill_total("BODY", "Melee Combat", "Dagger")
|
773
|
+
when /unarmed/
|
774
|
+
get_skill_total("BODY", "Melee Combat", "Unarmed")
|
775
|
+
else
|
776
|
+
get_skill_total("BODY", "Melee Combat", "Sword") || 0
|
777
|
+
end
|
778
|
+
end
|
779
|
+
|
780
|
+
def calculate_missile_skill_for_name(weapon_name)
|
781
|
+
case weapon_name.to_s.downcase
|
782
|
+
when /bow/
|
783
|
+
get_skill_total("BODY", "Missile Combat", "Bow")
|
784
|
+
when /crossbow|x-bow/
|
785
|
+
get_skill_total("BODY", "Missile Combat", "Crossbow")
|
786
|
+
when /sling/
|
787
|
+
get_skill_total("BODY", "Missile Combat", "Sling")
|
788
|
+
when /javelin/
|
789
|
+
get_skill_total("BODY", "Missile Combat", "Javelin")
|
790
|
+
when /rock|stone|throwing|th\s|knife/
|
791
|
+
get_skill_total("BODY", "Missile Combat", "Throwing")
|
792
|
+
else
|
793
|
+
get_skill_total("BODY", "Missile Combat", "Bow") || 0
|
794
|
+
end
|
795
|
+
end
|
796
|
+
|
797
|
+
def select_best_weapon(skills, preferred_weapons)
|
798
|
+
# Find the best weapon from preferred list that character has skill in
|
799
|
+
best_weapon = nil
|
800
|
+
best_skill = 0
|
801
|
+
|
802
|
+
preferred_weapons.each do |weapon|
|
803
|
+
if skills[weapon] && skills[weapon] > best_skill
|
804
|
+
best_weapon = weapon
|
805
|
+
best_skill = skills[weapon]
|
806
|
+
end
|
807
|
+
end
|
808
|
+
|
809
|
+
# If no preferred weapon found, check for any weapon skill
|
810
|
+
if !best_weapon && skills.any?
|
811
|
+
# Filter out Shield as it's not a primary weapon
|
812
|
+
weapon_skills = skills.reject { |k, _| k == "Shield" }
|
813
|
+
if weapon_skills.any?
|
814
|
+
best = weapon_skills.max_by { |_, v| v }
|
815
|
+
best_weapon = best[0]
|
816
|
+
end
|
817
|
+
end
|
818
|
+
|
819
|
+
best_weapon
|
820
|
+
end
|
821
|
+
|
822
|
+
def has_template_magic?(template)
|
823
|
+
# Check if template indicates magical ability
|
824
|
+
return false unless template
|
825
|
+
|
826
|
+
# Check if template has Casting attribute > 0
|
827
|
+
casting_attr = template["attributes"]["SPIRIT/Casting"] || 0
|
828
|
+
return true if casting_attr > 0
|
829
|
+
|
830
|
+
# Check for specific magic-using types
|
831
|
+
magic_types = ["Mage", "Wizard", "Witch (white)", "Witch (black)", "Sorcerer",
|
832
|
+
"Summoner", "Priest", "Sage", "Seer"]
|
833
|
+
type_str = @type.to_s
|
834
|
+
magic_types.include?(type_str) || type_str.include?("Wizard")
|
835
|
+
end
|
836
|
+
|
837
|
+
# Moved to public section below
|
838
|
+
|
839
|
+
def generate_spells
|
840
|
+
# Generate spell cards based on character type and level
|
841
|
+
spirit = get_characteristic("SPIRIT")
|
842
|
+
casting_attr = get_attribute("SPIRIT", "Casting")
|
843
|
+
casting_total = spirit + casting_attr
|
844
|
+
|
845
|
+
# Restrict spells to appropriate creatures
|
846
|
+
# Require SPIRIT >= 2 AND total Casting >= 5
|
847
|
+
# Exclude specific races and creature types
|
848
|
+
excluded_types = ["Araxi", "Troll", "Dwarf", "Ogre", "Lizard", "Animal", "Zombie", "Skeleton"]
|
849
|
+
type_lower = @type.to_s.downcase
|
850
|
+
|
851
|
+
is_excluded = excluded_types.any? { |ex| type_lower.include?(ex.downcase) }
|
852
|
+
|
853
|
+
# Only generate spells for intelligent magical beings
|
854
|
+
if spirit >= 2 && casting_total >= 5 && !is_excluded
|
855
|
+
# Load spell database if not already loaded
|
856
|
+
unless defined?($SpellDatabase)
|
857
|
+
load File.join($pgmdir, "includes/tables/spells_new.rb")
|
858
|
+
end
|
859
|
+
|
860
|
+
@spells = generate_spell_cards(@type, @level, casting_total)
|
861
|
+
end
|
862
|
+
end
|
863
|
+
|
864
|
+
# Dice rolling methods (matching old system)
|
865
|
+
def d6
|
866
|
+
rand(1..6)
|
867
|
+
end
|
868
|
+
|
869
|
+
def oD6
|
870
|
+
# Open-ended D6 roll
|
871
|
+
result = d6
|
872
|
+
return result if (2..5).include?(result)
|
873
|
+
|
874
|
+
if result == 1
|
875
|
+
down = d6
|
876
|
+
while down <= 3
|
877
|
+
result -= 1
|
878
|
+
down = d6
|
879
|
+
end
|
880
|
+
elsif result == 6
|
881
|
+
up = d6
|
882
|
+
while up >= 4
|
883
|
+
result += 1
|
884
|
+
up = d6
|
885
|
+
end
|
886
|
+
end
|
887
|
+
result
|
888
|
+
end
|
889
|
+
|
890
|
+
def aD6
|
891
|
+
# Average D6 roll (average of regular d6 and open-ended d6)
|
892
|
+
((d6 + oD6) / 2.0).to_i
|
893
|
+
end
|
894
|
+
|
895
|
+
# Public methods for accessing tier data
|
896
|
+
public
|
897
|
+
|
898
|
+
def has_magic?
|
899
|
+
# Check if character has any casting ability
|
900
|
+
casting_level = @tiers["SPIRIT"]["Casting"]["level"] || 0
|
901
|
+
casting_level > 0
|
902
|
+
end
|
903
|
+
|
904
|
+
# Calculate derived stats
|
905
|
+
def SIZE
|
906
|
+
# SIZE system based on weight with half-sizes
|
907
|
+
case @weight
|
908
|
+
when 0...10 then 0.5
|
909
|
+
when 10...15 then 1
|
910
|
+
when 15...20 then 1.5
|
911
|
+
when 20...35 then 2
|
912
|
+
when 35...50 then 2.5
|
913
|
+
when 50...75 then 3
|
914
|
+
when 75...100 then 3.5
|
915
|
+
when 100...125 then 4
|
916
|
+
when 125...150 then 4.5
|
917
|
+
when 150...188 then 5
|
918
|
+
when 188...225 then 5.5
|
919
|
+
when 225...263 then 6
|
920
|
+
when 263...300 then 6.5
|
921
|
+
when 300...350 then 7
|
922
|
+
when 350...400 then 7.5
|
923
|
+
when 400...450 then 8
|
924
|
+
when 450...500 then 8.5
|
925
|
+
when 500...550 then 9
|
926
|
+
when 550...600 then 9.5
|
927
|
+
when 600...663 then 10
|
928
|
+
when 663...725 then 10.5
|
929
|
+
when 725...788 then 11
|
930
|
+
when 788...850 then 11.5
|
931
|
+
when 850...925 then 12
|
932
|
+
when 925...1000 then 12.5
|
933
|
+
when 1000...1075 then 13
|
934
|
+
when 1075...1150 then 13.5
|
935
|
+
when 1150...1225 then 14
|
936
|
+
when 1225...1300 then 14.5
|
937
|
+
when 1300...1375 then 15
|
938
|
+
when 1375...1450 then 15.5
|
939
|
+
when 1450...1525 then 16
|
940
|
+
when 1525...1600 then 16.5
|
941
|
+
else
|
942
|
+
# For very large creatures, add 0.5 per 100kg
|
943
|
+
16.5 + ((@weight - 1600) / 100.0).floor * 0.5
|
944
|
+
end
|
945
|
+
end
|
946
|
+
|
947
|
+
def BP
|
948
|
+
# Body Points: SIZE * 2 + Fortitude / 3
|
949
|
+
# Fortitude is under Endurance
|
950
|
+
fortitude = get_skill("BODY", "Endurance", "Fortitude")
|
951
|
+
endurance = get_attribute("BODY", "Endurance")
|
952
|
+
body = get_characteristic("BODY")
|
953
|
+
total_fortitude = body + endurance + fortitude
|
954
|
+
(self.SIZE * 2 + total_fortitude / 3.0).round
|
955
|
+
end
|
956
|
+
|
957
|
+
def DB
|
958
|
+
# Damage Bonus: (SIZE + Strength total) / 3
|
959
|
+
strength = get_attribute("BODY", "Strength")
|
960
|
+
body = get_characteristic("BODY")
|
961
|
+
total_strength = body + strength
|
962
|
+
((self.SIZE + total_strength) / 3.0).round
|
963
|
+
end
|
964
|
+
|
965
|
+
def MD
|
966
|
+
# Magic Defense: (Mental Fortitude + Attunement Self) / 3
|
967
|
+
mental_fortitude = get_skill("MIND", "Willpower", "Mental Fortitude")
|
968
|
+
willpower = get_attribute("MIND", "Willpower")
|
969
|
+
mind = get_characteristic("MIND")
|
970
|
+
total_mental_fortitude = mind + willpower + mental_fortitude
|
971
|
+
|
972
|
+
attunement_self = get_skill("SPIRIT", "Attunement", "Self")
|
973
|
+
attunement = get_attribute("SPIRIT", "Attunement")
|
974
|
+
spirit = get_characteristic("SPIRIT")
|
975
|
+
total_attunement_self = spirit + attunement + attunement_self
|
976
|
+
|
977
|
+
((total_mental_fortitude + total_attunement_self) / 3.0).round
|
978
|
+
end
|
979
|
+
|
980
|
+
def get_characteristic(name)
|
981
|
+
# Calculate characteristic as weighted average of its attributes
|
982
|
+
# Reflects years of broad training across all aspects
|
983
|
+
total = 0
|
984
|
+
count = 0
|
985
|
+
@tiers[name].each do |attr_name, attr_data|
|
986
|
+
if attr_data["level"] && attr_data["level"] > 0
|
987
|
+
total += attr_data["level"]
|
988
|
+
count += 1
|
989
|
+
end
|
990
|
+
end
|
991
|
+
|
992
|
+
return 0 if count == 0
|
993
|
+
|
994
|
+
# Characteristics are very hard to improve - most people stay at 2-3
|
995
|
+
char_level = (total.to_f / count / 1.5).round # Scaled down to reflect training difficulty
|
996
|
+
|
997
|
+
# Apply realistic caps based on NPC level
|
998
|
+
max_char = case @level
|
999
|
+
when 1..2 then 2 # Novices rarely exceed 2
|
1000
|
+
when 3..4 then 3 # Experienced typically max at 3
|
1001
|
+
when 5..6 then 4 # Veterans might reach 4
|
1002
|
+
else 5 # Only masters achieve 5
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
char_level = max_char if char_level > max_char
|
1006
|
+
char_level
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
def get_attribute(char_name, attr_name)
|
1010
|
+
@tiers[char_name][attr_name]["level"] || 0
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
def get_skill(char_name, attr_name, skill_name)
|
1014
|
+
@tiers[char_name][attr_name]["skills"][skill_name] || 0
|
1015
|
+
end
|
1016
|
+
|
1017
|
+
def get_skill_total(char_name, attr_name, skill_name)
|
1018
|
+
# Calculate total: Characteristic + Attribute + Skill
|
1019
|
+
char_level = get_characteristic(char_name)
|
1020
|
+
attr_level = get_attribute(char_name, attr_name)
|
1021
|
+
skill_level = get_skill(char_name, attr_name, skill_name)
|
1022
|
+
|
1023
|
+
# Natural progression based on training difficulty:
|
1024
|
+
# - Getting to 10 is achievable with focused skill training
|
1025
|
+
# - Getting to 15 requires years of dedicated work
|
1026
|
+
# - Getting to 18+ is legendary, requiring lifetime mastery
|
1027
|
+
total = char_level + attr_level + skill_level
|
1028
|
+
|
1029
|
+
# Apply soft cap at 18 for game balance (only true masters exceed)
|
1030
|
+
if total > 18 && @level < 7
|
1031
|
+
# Small chance for exceptional individuals to exceed 18
|
1032
|
+
total = 18 unless rand(100) < 5
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
total
|
1036
|
+
end
|
1037
|
+
|
1038
|
+
# Mark system for progression
|
1039
|
+
|
1040
|
+
def add_mark(char_name, attr_name, skill_name = nil)
|
1041
|
+
if skill_name
|
1042
|
+
# Add mark to skill
|
1043
|
+
key = "#{attr_name}/#{skill_name}"
|
1044
|
+
@marks[char_name][key] ||= 0
|
1045
|
+
@marks[char_name][key] += 1
|
1046
|
+
|
1047
|
+
# Check if ready to advance
|
1048
|
+
check_advancement(char_name, attr_name, skill_name)
|
1049
|
+
else
|
1050
|
+
# Add mark to attribute
|
1051
|
+
@marks[char_name][attr_name] ||= 0
|
1052
|
+
@marks[char_name][attr_name] += 1
|
1053
|
+
end
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
def check_advancement(char_name, attr_name, skill_name)
|
1057
|
+
current_level = get_skill(char_name, attr_name, skill_name)
|
1058
|
+
required_marks = (current_level + 1) * 5
|
1059
|
+
key = "#{attr_name}/#{skill_name}"
|
1060
|
+
|
1061
|
+
if @marks[char_name][key] >= required_marks
|
1062
|
+
# Roll for advancement (all but a 1)
|
1063
|
+
if oD6 > 1
|
1064
|
+
@tiers[char_name][attr_name]["skills"][skill_name] += 1
|
1065
|
+
@marks[char_name][key] = 0
|
1066
|
+
|
1067
|
+
# Add mark to attribute above
|
1068
|
+
add_mark(char_name, attr_name)
|
1069
|
+
|
1070
|
+
return true
|
1071
|
+
end
|
1072
|
+
end
|
1073
|
+
false
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
private
|
1077
|
+
|
1078
|
+
def generate_random_name(sex)
|
1079
|
+
# Use the proper name generator based on race
|
1080
|
+
race = @type.to_s.sub(/(:| ).*/, '').capitalize
|
1081
|
+
|
1082
|
+
# Use naming function from functions.rb which uses the actual name generator
|
1083
|
+
name = naming(race, sex)
|
1084
|
+
|
1085
|
+
# Fallback to basic names if name generator fails
|
1086
|
+
if name.nil? || name.empty?
|
1087
|
+
if sex == "F" || (sex.empty? && rand(2) == 0)
|
1088
|
+
female_names = ["Aria", "Luna", "Sera", "Mira", "Lyra", "Nova", "Kira", "Zara", "Elara", "Thalia"]
|
1089
|
+
name = female_names.sample + " " + ["Starweaver", "Moonwhisper", "Brightblade", "Swiftwind", "Ironheart"].sample
|
1090
|
+
else
|
1091
|
+
male_names = ["Gareth", "Marcus", "Aldric", "Kael", "Doran", "Lucian", "Theron", "Cassius", "Orion", "Zephyr"]
|
1092
|
+
name = male_names.sample + " " + ["Ironforge", "Stormcaller", "Darkbane", "Goldhand", "Steelclaw"].sample
|
1093
|
+
end
|
1094
|
+
end
|
1095
|
+
|
1096
|
+
name
|
1097
|
+
end
|
1098
|
+
|
1099
|
+
def ensure_essential_skills
|
1100
|
+
# Ensure all NPCs have essential combat/stealth awareness skills (even if at 0)
|
1101
|
+
# These skills are frequently used in encounters
|
1102
|
+
|
1103
|
+
# Ensure Athletics attribute exists with essential skills
|
1104
|
+
@tiers["BODY"]["Athletics"] ||= {"level" => 0, "skills" => {}}
|
1105
|
+
@tiers["BODY"]["Athletics"]["skills"] ||= {}
|
1106
|
+
|
1107
|
+
# Add Move Quietly if missing
|
1108
|
+
unless @tiers["BODY"]["Athletics"]["skills"].key?("Move Quietly")
|
1109
|
+
@tiers["BODY"]["Athletics"]["skills"]["Move Quietly"] = 0
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
# Add Hide if missing
|
1113
|
+
unless @tiers["BODY"]["Athletics"]["skills"].key?("Hide")
|
1114
|
+
@tiers["BODY"]["Athletics"]["skills"]["Hide"] = 0
|
1115
|
+
end
|
1116
|
+
|
1117
|
+
# Add Dodge if missing (often used in combat)
|
1118
|
+
unless @tiers["BODY"]["Athletics"]["skills"].key?("Dodge")
|
1119
|
+
@tiers["BODY"]["Athletics"]["skills"]["Dodge"] = 0
|
1120
|
+
end
|
1121
|
+
|
1122
|
+
# Ensure Awareness attribute exists with essential skills
|
1123
|
+
@tiers["MIND"]["Awareness"] ||= {"level" => 0, "skills" => {}}
|
1124
|
+
@tiers["MIND"]["Awareness"]["skills"] ||= {}
|
1125
|
+
|
1126
|
+
# Add Alertness if missing (this is the general awareness skill)
|
1127
|
+
unless @tiers["MIND"]["Awareness"]["skills"].key?("Alertness")
|
1128
|
+
@tiers["MIND"]["Awareness"]["skills"]["Alertness"] = 0
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
# Reaction speed should always be present for initiative calculations
|
1132
|
+
unless @tiers["MIND"]["Awareness"]["skills"].key?("Reaction speed")
|
1133
|
+
@tiers["MIND"]["Awareness"]["skills"]["Reaction speed"] = 0
|
1134
|
+
end
|
1135
|
+
end
|
1136
|
+
|
1137
|
+
def apply_predetermined_stats
|
1138
|
+
# Apply predetermined stats to preserve encounter consistency
|
1139
|
+
# This allows encounters to pass specific stats that should not be randomized
|
1140
|
+
|
1141
|
+
return unless @predetermined_stats.is_a?(Hash)
|
1142
|
+
|
1143
|
+
# Override characteristics if provided
|
1144
|
+
if @predetermined_stats["characteristics"]
|
1145
|
+
@predetermined_stats["characteristics"].each do |char_name, value|
|
1146
|
+
# This would require modifying the SIZE calculation system
|
1147
|
+
# For now, we'll focus on weapons/armor/skills
|
1148
|
+
end
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
# Override specific weapon skills if provided
|
1152
|
+
if @predetermined_stats["weapon_skills"]
|
1153
|
+
@predetermined_stats["weapon_skills"].each do |weapon, skill_level|
|
1154
|
+
# Add to melee combat skills
|
1155
|
+
@tiers["BODY"]["Melee Combat"]["skills"] ||= {}
|
1156
|
+
@tiers["BODY"]["Melee Combat"]["skills"][weapon] = skill_level.to_i
|
1157
|
+
end
|
1158
|
+
end
|
1159
|
+
|
1160
|
+
# Override missile skills if provided
|
1161
|
+
if @predetermined_stats["missile_skills"]
|
1162
|
+
@predetermined_stats["missile_skills"].each do |weapon, skill_level|
|
1163
|
+
@tiers["BODY"]["Missile Combat"]["skills"] ||= {}
|
1164
|
+
@tiers["BODY"]["Missile Combat"]["skills"][weapon] = skill_level.to_i
|
1165
|
+
end
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
# Override armor if provided
|
1169
|
+
if @predetermined_stats["armor"]
|
1170
|
+
@armor = @predetermined_stats["armor"]
|
1171
|
+
end
|
1172
|
+
|
1173
|
+
# Override other skills if provided
|
1174
|
+
if @predetermined_stats["skills"]
|
1175
|
+
@predetermined_stats["skills"].each do |skill_path, value|
|
1176
|
+
parts = skill_path.split("/")
|
1177
|
+
next unless parts.length == 3
|
1178
|
+
|
1179
|
+
char_name, attr_name, skill_name = parts
|
1180
|
+
next unless @tiers[char_name] && @tiers[char_name][attr_name]
|
1181
|
+
|
1182
|
+
@tiers[char_name][attr_name]["skills"] ||= {}
|
1183
|
+
@tiers[char_name][attr_name]["skills"][skill_name] = value.to_i
|
1184
|
+
end
|
1185
|
+
end
|
1186
|
+
end
|
1187
|
+
end
|