arkparser 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. arkparser/__init__.py +117 -0
  2. arkparser/common/__init__.py +72 -0
  3. arkparser/common/binary_reader.py +402 -0
  4. arkparser/common/exceptions.py +99 -0
  5. arkparser/common/map_config.py +166 -0
  6. arkparser/common/types.py +249 -0
  7. arkparser/common/version_detection.py +195 -0
  8. arkparser/data_models.py +801 -0
  9. arkparser/export.py +485 -0
  10. arkparser/files/__init__.py +25 -0
  11. arkparser/files/base.py +309 -0
  12. arkparser/files/cloud_inventory.py +259 -0
  13. arkparser/files/profile.py +205 -0
  14. arkparser/files/tribe.py +155 -0
  15. arkparser/files/world_save.py +699 -0
  16. arkparser/game_objects/__init__.py +32 -0
  17. arkparser/game_objects/container.py +180 -0
  18. arkparser/game_objects/game_object.py +273 -0
  19. arkparser/game_objects/location.py +87 -0
  20. arkparser/models/__init__.py +29 -0
  21. arkparser/models/character.py +227 -0
  22. arkparser/models/creature.py +642 -0
  23. arkparser/models/item.py +207 -0
  24. arkparser/models/player.py +263 -0
  25. arkparser/models/stats.py +226 -0
  26. arkparser/models/structure.py +176 -0
  27. arkparser/models/tribe.py +291 -0
  28. arkparser/properties/__init__.py +77 -0
  29. arkparser/properties/base.py +329 -0
  30. arkparser/properties/byte_property.py +230 -0
  31. arkparser/properties/compound.py +1125 -0
  32. arkparser/properties/primitives.py +803 -0
  33. arkparser/properties/registry.py +236 -0
  34. arkparser/py.typed +0 -0
  35. arkparser/structs/__init__.py +60 -0
  36. arkparser/structs/base.py +63 -0
  37. arkparser/structs/colors.py +108 -0
  38. arkparser/structs/misc.py +133 -0
  39. arkparser/structs/property_list.py +101 -0
  40. arkparser/structs/registry.py +140 -0
  41. arkparser/structs/vectors.py +221 -0
  42. arkparser-0.1.0.dist-info/METADATA +833 -0
  43. arkparser-0.1.0.dist-info/RECORD +46 -0
  44. arkparser-0.1.0.dist-info/WHEEL +5 -0
  45. arkparser-0.1.0.dist-info/licenses/LICENSE +21 -0
  46. arkparser-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,642 @@
1
+ """
2
+ Creature model classes - TamedCreature and WildCreature.
3
+
4
+ Wraps GameObject with intuitive attribute access for creature data.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import typing as t
10
+ from dataclasses import dataclass, field
11
+
12
+ from .stats import CreatureStats, Location
13
+
14
+
15
+ @dataclass
16
+ class Creature:
17
+ """
18
+ Base creature class with common attributes.
19
+
20
+ This is the base class for both tamed and wild creatures.
21
+ It wraps a GameObject and provides intuitive property access.
22
+
23
+ Attributes:
24
+ class_name: Blueprint class name (e.g., "Dodo_Character_BP_C").
25
+ guid: Unique identifier (ASA only, ASE will be empty).
26
+ is_female: True if the creature is female.
27
+ is_baby: True if the creature is a baby.
28
+ is_neutered: True if neutered/spayed.
29
+ colors: List of 6 color region indices.
30
+ base_level: Wild/base level of the creature.
31
+ base_stats: Wild stat points (before taming).
32
+ location: World position.
33
+ """
34
+
35
+ _game_object: t.Any = field(default=None, repr=False)
36
+ _status_object: t.Any = field(default=None, repr=False)
37
+
38
+ # Cached values
39
+ _class_name: str | None = field(default=None, repr=False)
40
+ _colors: list[int] | None = field(default=None, repr=False)
41
+ _base_stats: CreatureStats | None = field(default=None, repr=False)
42
+
43
+ @property
44
+ def class_name(self) -> str:
45
+ """Blueprint class name (e.g., 'Dodo_Character_BP_C')."""
46
+ if self._class_name is None:
47
+ self._class_name = self._game_object.class_name if self._game_object else ""
48
+ return self._class_name or ""
49
+
50
+ @property
51
+ def guid(self) -> str:
52
+ """Unique identifier (ASA only)."""
53
+ return self._game_object.guid if self._game_object else ""
54
+
55
+ @property
56
+ def dino_id(self) -> int:
57
+ """
58
+ Unique dino ID (combination of DinoID1 and DinoID2).
59
+
60
+ Returns:
61
+ 64-bit dino ID, or 0 if not available.
62
+ """
63
+ if not self._game_object:
64
+ return 0
65
+ id1 = self._game_object.get_property_value("DinoID1", default=0)
66
+ id2 = self._game_object.get_property_value("DinoID2", default=0)
67
+ if id1 and id2:
68
+ return (int(id1) << 32) | (int(id2) & 0xFFFFFFFF)
69
+ return 0
70
+
71
+ @property
72
+ def is_female(self) -> bool:
73
+ """True if the creature is female."""
74
+ if not self._game_object:
75
+ return False
76
+ return self._game_object.get_property_value("bIsFemale", default=False)
77
+
78
+ @property
79
+ def gender(self) -> str:
80
+ """Gender as string ('Female' or 'Male')."""
81
+ return "Female" if self.is_female else "Male"
82
+
83
+ @property
84
+ def is_baby(self) -> bool:
85
+ """True if the creature is a baby."""
86
+ if not self._game_object:
87
+ return False
88
+ return self._game_object.get_property_value("bIsBaby", default=False)
89
+
90
+ @property
91
+ def is_neutered(self) -> bool:
92
+ """True if the creature is neutered/spayed."""
93
+ if not self._game_object:
94
+ return False
95
+ return self._game_object.get_property_value("bNeutered", default=False)
96
+
97
+ @property
98
+ def colors(self) -> list[int]:
99
+ """
100
+ Color region indices (6 values).
101
+
102
+ Returns:
103
+ List of 6 color indices for regions 0-5.
104
+ """
105
+ if self._colors is None:
106
+ self._colors = []
107
+ if self._game_object:
108
+ for i in range(6):
109
+ color = self._game_object.get_property_value("ColorSetIndices", index=i, default=0)
110
+ self._colors.append(int(color) if color else 0)
111
+ else:
112
+ self._colors = [0] * 6
113
+ return self._colors
114
+
115
+ @property
116
+ def base_level(self) -> int:
117
+ """Wild/base level (before any tamed levels)."""
118
+ if self._status_object:
119
+ return self._status_object.get_property_value("BaseCharacterLevel", default=1)
120
+ return 1
121
+
122
+ @property
123
+ def base_stats(self) -> CreatureStats:
124
+ """
125
+ Wild stat points (points applied at spawn).
126
+
127
+ These are the stat points the creature had before taming.
128
+ """
129
+ if self._base_stats is None:
130
+ points = []
131
+ if self._status_object:
132
+ for i in range(12):
133
+ val = self._status_object.get_property_value("NumberOfLevelUpPointsApplied", index=i, default=0)
134
+ points.append(int(val) if val else 0)
135
+ self._base_stats = CreatureStats.from_array(points)
136
+ return self._base_stats
137
+
138
+ @property
139
+ def location(self) -> Location | None:
140
+ """World position and rotation."""
141
+ if self._game_object and self._game_object.location:
142
+ loc = self._game_object.location
143
+ return Location(
144
+ x=loc.x,
145
+ y=loc.y,
146
+ z=loc.z,
147
+ pitch=getattr(loc, "pitch", 0.0),
148
+ yaw=getattr(loc, "yaw", 0.0),
149
+ roll=getattr(loc, "roll", 0.0),
150
+ )
151
+ return None
152
+
153
+ @property
154
+ def wild_scale(self) -> float:
155
+ """Wild random scale factor (size variation)."""
156
+ if not self._game_object:
157
+ return 1.0
158
+ return self._game_object.get_property_value("WildRandomScale", default=1.0)
159
+
160
+ @property
161
+ def maturation(self) -> float:
162
+ """
163
+ Baby maturation progress (0.0 - 1.0).
164
+
165
+ Only meaningful for babies. Returns 1.0 for adults.
166
+ """
167
+ if not self._game_object or not self.is_baby:
168
+ return 1.0
169
+ baby_age = self._game_object.get_property_value("BabyAge", default=1.0)
170
+ return float(baby_age) if baby_age else 1.0
171
+
172
+ @property
173
+ def maturation_percent(self) -> str:
174
+ """Baby maturation as a percentage string (e.g., '75' or '100')."""
175
+ return str(int(self.maturation * 100))
176
+
177
+ def get_property(self, name: str, index: int = 0, default: t.Any = None) -> t.Any:
178
+ """
179
+ Get a raw property value from the underlying game object.
180
+
181
+ Args:
182
+ name: Property name.
183
+ index: Array index for repeated properties.
184
+ default: Value to return if not found.
185
+
186
+ Returns:
187
+ The property value.
188
+ """
189
+ if self._game_object:
190
+ return self._game_object.get_property_value(name, default=default, index=index)
191
+ return default
192
+
193
+ def to_dict(self) -> dict[str, t.Any]:
194
+ """Convert to dictionary matching C# ASV export format."""
195
+ result: dict[str, t.Any] = {
196
+ "id": self.dino_id,
197
+ "creature": self.class_name,
198
+ "sex": self.gender,
199
+ "base": self.base_level,
200
+ "colors": self.colors,
201
+ "c0": self.colors[0] if len(self.colors) > 0 else 0,
202
+ "c1": self.colors[1] if len(self.colors) > 1 else 0,
203
+ "c2": self.colors[2] if len(self.colors) > 2 else 0,
204
+ "c3": self.colors[3] if len(self.colors) > 3 else 0,
205
+ "c4": self.colors[4] if len(self.colors) > 4 else 0,
206
+ "c5": self.colors[5] if len(self.colors) > 5 else 0,
207
+ "dinoid": str(self.dino_id),
208
+ "base_stats": self.base_stats.to_dict(),
209
+ # Flat stat fields matching C# wild export
210
+ "hp": self.base_stats.health,
211
+ "stam": self.base_stats.stamina,
212
+ "melee": self.base_stats.melee,
213
+ "weight": self.base_stats.weight,
214
+ "speed": self.base_stats.speed,
215
+ "food": self.base_stats.food,
216
+ "oxy": self.base_stats.oxygen,
217
+ "craft": self.base_stats.crafting,
218
+ }
219
+ if self.location:
220
+ result["location"] = self.location.to_dict()
221
+ result["ccc"] = self.location.ccc
222
+ if self.location.latitude is not None:
223
+ result["lat"] = self.location.latitude
224
+ if self.location.longitude is not None:
225
+ result["lon"] = self.location.longitude
226
+ return result
227
+
228
+
229
+ @dataclass
230
+ class TamedCreature(Creature):
231
+ """
232
+ A tamed creature with full attribute access.
233
+
234
+ Extends Creature with taming-specific attributes like name,
235
+ tribe, imprint quality, tamed stats, and mutations.
236
+
237
+ Example:
238
+ >>> creature = TamedCreature.from_game_object(obj, status_obj)
239
+ >>> print(f"{creature.name} - Level {creature.level}")
240
+ >>> print(f"Imprint: {creature.imprint_quality:.1%}")
241
+ >>> print(f"HP: {creature.base_stats.health} + {creature.tamed_stats.health}")
242
+
243
+ Attributes:
244
+ name: Tamed name given by player.
245
+ tribe_name: Name of the owning tribe.
246
+ tamer_name: Name of the player who tamed it.
247
+ level: Total level (base + extra).
248
+ imprint_quality: Imprint percentage (0.0 - 1.0).
249
+ imprinter_name: Name of the player who imprinted.
250
+ tamed_stats: Tamed stat points (added after taming).
251
+ is_clone: True if this is a cloned creature.
252
+ is_cryo: True if stored in a cryopod.
253
+ mutations_female: Number of mutations from female line.
254
+ mutations_male: Number of mutations from male line.
255
+ """
256
+
257
+ # Cached values
258
+ _tamed_stats: CreatureStats | None = field(default=None, repr=False)
259
+ _mutated_stats: CreatureStats | None = field(default=None, repr=False)
260
+
261
+ @classmethod
262
+ def from_game_object(
263
+ cls,
264
+ game_object: t.Any,
265
+ status_object: t.Any = None,
266
+ ) -> TamedCreature:
267
+ """
268
+ Create a TamedCreature from a GameObject.
269
+
270
+ Args:
271
+ game_object: The creature's main game object.
272
+ status_object: The creature's status component (for stats).
273
+
274
+ Returns:
275
+ A TamedCreature instance.
276
+ """
277
+ return cls(_game_object=game_object, _status_object=status_object)
278
+
279
+ @property
280
+ def name(self) -> str:
281
+ """Tamed name given by player."""
282
+ if not self._game_object:
283
+ return ""
284
+ return self._game_object.get_property_value("TamedName", default="") or ""
285
+
286
+ @property
287
+ def tribe_name(self) -> str:
288
+ """Name of the owning tribe."""
289
+ if not self._game_object:
290
+ return ""
291
+ return self._game_object.get_property_value("TribeName", default="") or ""
292
+
293
+ @property
294
+ def tamer_name(self) -> str:
295
+ """Name of the player who tamed this creature."""
296
+ if not self._game_object:
297
+ return ""
298
+ return self._game_object.get_property_value("TamerString", default="") or ""
299
+
300
+ @property
301
+ def extra_level(self) -> int:
302
+ """Extra levels gained after taming."""
303
+ if self._status_object:
304
+ val = self._status_object.get_property_value("ExtraCharacterLevel", default=0)
305
+ return int(val) if val else 0
306
+ return 0
307
+
308
+ @property
309
+ def level(self) -> int:
310
+ """Total level (base level + extra levels)."""
311
+ return self.base_level + self.extra_level
312
+
313
+ @property
314
+ def imprint_quality(self) -> float:
315
+ """
316
+ Imprint percentage (0.0 to 1.0).
317
+
318
+ A value of 1.0 means 100% imprinted.
319
+ """
320
+ if self._status_object:
321
+ val = self._status_object.get_property_value("DinoImprintingQuality", default=0.0)
322
+ return float(val) if val else 0.0
323
+ return 0.0
324
+
325
+ @property
326
+ def imprinter_name(self) -> str:
327
+ """Name of the player who imprinted this creature."""
328
+ if not self._game_object:
329
+ return ""
330
+ return self._game_object.get_property_value("ImprinterName", default="") or ""
331
+
332
+ @property
333
+ def imprinter_id(self) -> int:
334
+ """Player ID of the imprinter."""
335
+ if not self._game_object:
336
+ return 0
337
+ val = self._game_object.get_property_value("ImprinterPlayerDataID", default=0)
338
+ return int(val) if val else 0
339
+
340
+ @property
341
+ def tamed_stats(self) -> CreatureStats:
342
+ """
343
+ Tamed stat points (points added after taming).
344
+
345
+ These are the stat points allocated by the player.
346
+ """
347
+ if self._tamed_stats is None:
348
+ points = []
349
+ if self._status_object:
350
+ for i in range(12):
351
+ val = self._status_object.get_property_value(
352
+ "NumberOfLevelUpPointsAppliedTamed", index=i, default=0
353
+ )
354
+ points.append(int(val) if val else 0)
355
+ self._tamed_stats = CreatureStats.from_array(points)
356
+ return self._tamed_stats
357
+
358
+ @property
359
+ def mutated_stats(self) -> CreatureStats:
360
+ """
361
+ Mutated stat points (NumberOfMutationsAppliedTamed).
362
+
363
+ These are the stat points gained through mutations.
364
+ """
365
+ if self._mutated_stats is None:
366
+ points = []
367
+ if self._status_object:
368
+ for i in range(12):
369
+ val = self._status_object.get_property_value("NumberOfMutationsAppliedTamed", index=i, default=0)
370
+ points.append(int(val) if val else 0)
371
+ self._mutated_stats = CreatureStats.from_array(points)
372
+ return self._mutated_stats
373
+
374
+ @property
375
+ def experience(self) -> float:
376
+ """Current experience points."""
377
+ if self._status_object:
378
+ val = self._status_object.get_property_value("ExperiencePoints", default=0.0)
379
+ return float(val) if val else 0.0
380
+ return 0.0
381
+
382
+ @property
383
+ def is_clone(self) -> bool:
384
+ """True if this creature was cloned."""
385
+ if not self._game_object:
386
+ return False
387
+ is_clone = self._game_object.get_property_value("bIsClone", default=False)
388
+ if is_clone:
389
+ return True
390
+ return self._game_object.get_property_value("bIsCloneDino", default=False)
391
+
392
+ @property
393
+ def is_cryo(self) -> bool:
394
+ """True if this creature is stored in a cryopod."""
395
+ if not self._game_object:
396
+ return False
397
+ return self._game_object.get_property_value("IsInCryo", default=False)
398
+
399
+ @property
400
+ def is_wandering(self) -> bool:
401
+ """True if wandering is enabled."""
402
+ if not self._game_object:
403
+ return False
404
+ return self._game_object.get_property_value("bEnableTamedWandering", default=False)
405
+
406
+ @property
407
+ def is_mating(self) -> bool:
408
+ """True if mating is enabled."""
409
+ if not self._game_object:
410
+ return False
411
+ return self._game_object.get_property_value("bEnableTamedMating", default=False)
412
+
413
+ @property
414
+ def mutations_female(self) -> int:
415
+ """Number of mutations from the female line."""
416
+ if not self._game_object:
417
+ return 0
418
+ val = self._game_object.get_property_value("RandomMutationsFemale", default=0)
419
+ return int(val) if val else 0
420
+
421
+ @property
422
+ def mutations_male(self) -> int:
423
+ """Number of mutations from the male line."""
424
+ if not self._game_object:
425
+ return 0
426
+ val = self._game_object.get_property_value("RandomMutationsMale", default=0)
427
+ return int(val) if val else 0
428
+
429
+ @property
430
+ def total_mutations(self) -> int:
431
+ """Total mutations (female + male)."""
432
+ return self.mutations_female + self.mutations_male
433
+
434
+ @property
435
+ def targeting_team(self) -> int:
436
+ """Targeting team ID (tribe ID)."""
437
+ if not self._game_object:
438
+ return 0
439
+ val = self._game_object.get_property_value("TargetingTeam", default=0)
440
+ return int(val) if val else 0
441
+
442
+ @property
443
+ def tamed_server(self) -> str:
444
+ """Server name where the creature was tamed."""
445
+ if not self._game_object:
446
+ return ""
447
+ return self._game_object.get_property_value("TamedOnServerName", default="") or ""
448
+
449
+ @property
450
+ def uploaded_server(self) -> str:
451
+ """Server name from which the creature was uploaded."""
452
+ if not self._game_object:
453
+ return ""
454
+ return self._game_object.get_property_value("UploadedFromServerName", default="") or ""
455
+
456
+ @property
457
+ def father_id(self) -> int | None:
458
+ """
459
+ Father's dino ID, if bred.
460
+
461
+ Parses DinoAncestors struct array to extract the father's
462
+ combined DinoID1/DinoID2 values.
463
+ """
464
+ ancestors = self._get_ancestors("DinoAncestors")
465
+ if ancestors:
466
+ return self._extract_dino_id(ancestors[0])
467
+ return None
468
+
469
+ @property
470
+ def mother_id(self) -> int | None:
471
+ """
472
+ Mother's dino ID, if bred.
473
+
474
+ Parses DinoAncestorsMale struct array to extract the mother's
475
+ combined DinoID1/DinoID2 values.
476
+ """
477
+ ancestors = self._get_ancestors("DinoAncestorsMale")
478
+ if ancestors:
479
+ return self._extract_dino_id(ancestors[0])
480
+ return None
481
+
482
+ @property
483
+ def father_name(self) -> str:
484
+ """Father's name, if bred."""
485
+ ancestors = self._get_ancestors("DinoAncestors")
486
+ if ancestors:
487
+ return self._extract_ancestor_name(ancestors[0])
488
+ return ""
489
+
490
+ @property
491
+ def mother_name(self) -> str:
492
+ """Mother's name, if bred."""
493
+ ancestors = self._get_ancestors("DinoAncestorsMale")
494
+ if ancestors:
495
+ return self._extract_ancestor_name(ancestors[0])
496
+ return ""
497
+
498
+ def _get_ancestors(self, prop_name: str) -> list[t.Any]:
499
+ """Get ancestor list from a DinoAncestors property."""
500
+ if not self._game_object:
501
+ return []
502
+ val = self._game_object.get_property_value(prop_name, default=None)
503
+ if isinstance(val, list) and val:
504
+ return val
505
+ return []
506
+
507
+ def _extract_dino_id(self, ancestor: t.Any) -> int | None:
508
+ """Extract dino ID from an ancestor struct/dict."""
509
+ if isinstance(ancestor, dict):
510
+ id1 = ancestor.get("DinoID1", 0)
511
+ id2 = ancestor.get("DinoID2", 0)
512
+ if id1 or id2:
513
+ return (int(id1) << 32) | (int(id2) & 0xFFFFFFFF)
514
+ return None
515
+
516
+ def _extract_ancestor_name(self, ancestor: t.Any) -> str:
517
+ """Extract name from an ancestor struct/dict."""
518
+ if isinstance(ancestor, dict):
519
+ name = ancestor.get("DinoName", "") or ancestor.get("MaleName", "")
520
+ return str(name) if name else ""
521
+ return ""
522
+
523
+ def to_dict(self) -> dict[str, t.Any]:
524
+ """Convert to dictionary matching C# ASV_Tamed export format."""
525
+ result = super().to_dict()
526
+ result.update(
527
+ {
528
+ "name": self.name,
529
+ "tribeid": self.targeting_team,
530
+ "tribe": self.tribe_name or None,
531
+ "tamer": self.tamer_name,
532
+ "imprinter": self.imprinter_name,
533
+ "imprint": self.imprint_quality,
534
+ "lvl": self.level,
535
+ "extra_level": self.extra_level,
536
+ "tamed_stats": self.tamed_stats.to_dict(),
537
+ # Flat tamed stat fields matching C# export
538
+ "hp-w": self.base_stats.health,
539
+ "stam-w": self.base_stats.stamina,
540
+ "melee-w": self.base_stats.melee,
541
+ "weight-w": self.base_stats.weight,
542
+ "speed-w": self.base_stats.speed,
543
+ "food-w": self.base_stats.food,
544
+ "oxy-w": self.base_stats.oxygen,
545
+ "craft-w": self.base_stats.crafting,
546
+ "hp-m": self.mutated_stats.health,
547
+ "stam-m": self.mutated_stats.stamina,
548
+ "melee-m": self.mutated_stats.melee,
549
+ "weight-m": self.mutated_stats.weight,
550
+ "speed-m": self.mutated_stats.speed,
551
+ "food-m": self.mutated_stats.food,
552
+ "oxy-m": self.mutated_stats.oxygen,
553
+ "craft-m": self.mutated_stats.crafting,
554
+ "hp-t": self.tamed_stats.health,
555
+ "stam-t": self.tamed_stats.stamina,
556
+ "melee-t": self.tamed_stats.melee,
557
+ "weight-t": self.tamed_stats.weight,
558
+ "speed-t": self.tamed_stats.speed,
559
+ "food-t": self.tamed_stats.food,
560
+ "oxy-t": self.tamed_stats.oxygen,
561
+ "craft-t": self.tamed_stats.crafting,
562
+ "mut-f": self.mutations_female,
563
+ "mut-m": self.mutations_male,
564
+ "cryo": self.is_cryo,
565
+ "isMating": self.is_mating,
566
+ "isNeutered": self.is_neutered,
567
+ "isClone": self.is_clone,
568
+ "tamedServer": self.tamed_server,
569
+ "uploadedServer": self.uploaded_server,
570
+ "maturation": self.maturation_percent,
571
+ "experience": self.experience,
572
+ }
573
+ )
574
+ return result
575
+
576
+ def __repr__(self) -> str:
577
+ name = self.name or self.class_name
578
+ return f"TamedCreature({name!r}, level={self.level}, gender={self.gender!r})"
579
+
580
+
581
+ @dataclass
582
+ class WildCreature(Creature):
583
+ """
584
+ A wild (untamed) creature.
585
+
586
+ Wild creatures have simpler attributes than tamed ones.
587
+
588
+ Attributes:
589
+ level: Same as base_level for wild creatures.
590
+ """
591
+
592
+ @classmethod
593
+ def from_game_object(
594
+ cls,
595
+ game_object: t.Any,
596
+ status_object: t.Any = None,
597
+ ) -> WildCreature:
598
+ """
599
+ Create a WildCreature from a GameObject.
600
+
601
+ Args:
602
+ game_object: The creature's main game object.
603
+ status_object: The creature's status component (for stats).
604
+
605
+ Returns:
606
+ A WildCreature instance.
607
+ """
608
+ return cls(_game_object=game_object, _status_object=status_object)
609
+
610
+ @property
611
+ def level(self) -> int:
612
+ """Creature level (same as base level for wild)."""
613
+ return self.base_level
614
+
615
+ @property
616
+ def tameable(self) -> bool:
617
+ """
618
+ Whether this creature can be tamed.
619
+
620
+ Checks for the RequiredTameAffinity property — if present and > 0,
621
+ the creature is tameable.
622
+ """
623
+ if not self._game_object:
624
+ return False
625
+ val = self._game_object.get_property_value("RequiredTameAffinity", default=None)
626
+ if val is not None:
627
+ return float(val) > 0
628
+ if self._status_object:
629
+ val = self._status_object.get_property_value("RequiredTameAffinity", default=None)
630
+ if val is not None:
631
+ return float(val) > 0
632
+ return False
633
+
634
+ def to_dict(self) -> dict[str, t.Any]:
635
+ """Convert to dictionary matching C# ASV_Wild export format."""
636
+ result = super().to_dict()
637
+ result["lvl"] = self.level
638
+ result["tameable"] = self.tameable
639
+ return result
640
+
641
+ def __repr__(self) -> str:
642
+ return f"WildCreature({self.class_name!r}, level={self.level}, gender={self.gender!r})"