Metadata-Version: 2.4
Name: artifacts-mmo
Version: 0.1.3
Summary: Async Python wrapper for the Artifacts MMO API
License: MIT
Keywords: api,artifacts,async,bot,game,mmo,wrapper
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Games/Entertainment
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: pydantic<3,>=2.0
Provides-Extra: dev
Requires-Dist: aiohttp<4,>=3.9; extra == 'dev'
Requires-Dist: aioresponses; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-asyncio; extra == 'dev'
Provides-Extra: native
Requires-Dist: aiohttp<4,>=3.9; extra == 'native'
Description-Content-Type: text/markdown

# Artifacts MMO - Python Wrapper

Async Python wrapper for the [Artifacts MMO](https://artifactsmmo.com/) API. Control up to 5 characters simultaneously with full type safety and IDE autocompletion.

- **65 endpoints** covered (every single one)
- **Async** built on `aiohttp` -- run multiple characters in parallel
- **Typed** with Pydantic v2 models -- full IDE autocompletion
- **Simple** -- character-centric design, beginner friendly

## Installation

```bash
pip install -e .
```

**Requirements:** Python 3.10+

## Quick Start

```python
import asyncio
from artifacts import ArtifactsClient, wait_for_cooldown

async def main():
    async with ArtifactsClient(token="your_token_here") as client:
        # Get a character controller
        char = client.character("MyCharacter")

        # Fetch character info
        info = await char.get()
        print(f"{info.name} lv{info.level} HP={info.hp}/{info.max_hp}")

        # Fight a monster
        result = await char.fight()
        print(f"Result: {result.fight.result.value}")

        # Wait for cooldown before next action
        await wait_for_cooldown(result.cooldown)

asyncio.run(main())
```

## Getting a Token

You need a JWT token to authenticate. Generate one from your account credentials:

```python
async with ArtifactsClient() as client:
    token = await client.token.generate("your_username", "your_password")
    print(token)  # Use this token from now on
```

Or use a token you already have from the Artifacts website.

## Custom API URL

By default the wrapper connects to `https://api.artifactsmmo.com/`. To use a different server (e.g. sandbox):

```python
client = ArtifactsClient(
    token="your_token",
    base_url="https://api.sandbox.artifactsmmo.com",
)
```

## Architecture Overview

```
ArtifactsClient
├── .character("name")     -> Character (34 action methods)
├── .server                -> ServerAPI
├── .token                 -> TokenAPI
├── .accounts              -> AccountsAPI
├── .my_account            -> MyAccountAPI
├── .characters            -> CharactersAPI
├── .items                 -> ItemsAPI
├── .monsters              -> MonstersAPI
├── .maps                  -> MapsAPI
├── .resources             -> ResourcesAPI
├── .npcs                  -> NPCsAPI
├── .events                -> EventsAPI
├── .achievements          -> AchievementsAPI
├── .badges                -> BadgesAPI
├── .effects               -> EffectsAPI
├── .grand_exchange        -> GrandExchangeAPI
├── .leaderboard           -> LeaderboardAPI
├── .tasks                 -> TasksAPI
├── .simulation            -> SimulationAPI
└── .sandbox               -> SandboxAPI
```

---

## Fetching Game Data

All game data endpoints are read-only and return typed Pydantic models. Paginated results come wrapped in `DataPage[T]`.

### Items

```python
# Get a single item by code
item = await client.items.get("copper_ore")
print(f"{item.name} (lv{item.level}, type={item.type.value})")

# List items with filters
page = await client.items.get_all(min_level=1, max_level=10, type="resource", size=20)
for item in page.data:
    print(f"  {item.code}: {item.name}")
print(f"Page {page.page}/{page.pages} (total: {page.total})")
```

### Monsters

```python
monster = await client.monsters.get("chicken")
print(f"{monster.name} lv{monster.level} HP={monster.hp}")
print(f"Drops: {[(d.code, d.rate) for d in monster.drops]}")

# List all monsters in a level range
page = await client.monsters.get_all(min_level=1, max_level=5)
```

### Maps

```python
# Find maps containing a specific monster
maps = await client.maps.get_all(content_type="monster", content_code="chicken")
for m in maps.data:
    print(f"  ({m.x},{m.y}) layer={m.layer.value}")

# Get a specific map tile
tile = await client.maps.get_by_position("overworld", 0, 1)

# Get a map by its ID
tile = await client.maps.get_by_id(42)
```

### Resources

```python
# All mining resources
page = await client.resources.get_all(skill="mining", min_level=1)
for r in page.data:
    print(f"  {r.code} (lv{r.level}) drops: {[d.code for d in r.drops]}")
```

### NPCs

```python
# List all NPCs
npcs = await client.npcs.get_all()

# Get items sold by an NPC
items = await client.npcs.get_items("merchant_1")
for i in items.data:
    print(f"  {i.code} buy={i.buy_price} sell={i.sell_price}")
```

### Other game data

```python
# Achievements
all_achievements = await client.achievements.get_all()
single = await client.achievements.get("first_kill")

# Badges
badges = await client.badges.get_all()

# Effects
effects = await client.effects.get_all()

# Events
active = await client.events.get_all_active()
all_events = await client.events.get_all()

# Tasks
tasks = await client.tasks.get_all(type="monsters", min_level=1)
rewards = await client.tasks.get_all_rewards()

# Leaderboard
top_chars = await client.leaderboard.get_characters(sort="combat")
top_accounts = await client.leaderboard.get_accounts(sort="gold")

# Grand Exchange
orders = await client.grand_exchange.get_orders(code="copper_ore")
history = await client.grand_exchange.get_history("copper_ore")
```

### Pagination

All list endpoints return a `DataPage[T]`:

```python
page = await client.items.get_all(page=1, size=50)
page.data    # list[ItemSchema] -- the items on this page
page.total   # int -- total number of items across all pages
page.page    # int -- current page number
page.pages   # int -- total number of pages
page.size    # int -- page size
```

To fetch every item across all pages at once, use the HTTP client helper:

```python
all_items = await client._http.get_all_pages("/items", ItemSchema, page_size=100)
# Returns a flat list[ItemSchema] with every item
```

---

## Account & Bank

```python
# Account details
account = await client.my_account.get_details()
print(f"{account.username} -- gems={account.gems}")

# Bank info
bank = await client.my_account.get_bank()
print(f"Gold: {bank.gold}, Slots: {bank.slots}/{bank.slots}")

# Bank items
bank_items = await client.my_account.get_bank_items()
for item in bank_items.data:
    print(f"  {item.code} x{item.quantity}")

# Your characters
chars = await client.my_account.get_characters()

# Your GE orders and history
orders = await client.my_account.get_ge_orders()
history = await client.my_account.get_ge_history()

# Pending items (from achievements, events, etc.)
pending = await client.my_account.get_pending_items()
```

---

## Character Actions

Create a character controller, then call action methods. Every action returns a result containing a `cooldown` field.

```python
char = client.character("MyCharacter")
```

### Movement

```python
# Move by coordinates
result = await char.move(x=2, y=3)
await wait_for_cooldown(result.cooldown)

# Move by map ID
result = await char.move(map_id=42)
await wait_for_cooldown(result.cooldown)

# Layer transition (e.g. enter a building)
result = await char.transition()
await wait_for_cooldown(result.cooldown)
```

### Combat

```python
# Solo fight
result = await char.fight()
fight = result.fight
print(f"{fight.result.value} in {fight.turns} turns")
for cr in fight.characters:
    print(f"  +{cr.xp}xp +{cr.gold}g, drops={[d.code for d in cr.drops]}")
await wait_for_cooldown(result.cooldown)

# Boss fight with other characters (up to 3 total)
result = await char.fight(participants=["Char2", "Char3"])

# Rest to recover HP
result = await char.rest()
print(f"Restored {result.hp_restored} HP")
await wait_for_cooldown(result.cooldown)
```

### Equipment

```python
from artifacts.models import ItemSlot

# Equip
result = await char.equip(code="iron_sword", slot=ItemSlot.WEAPON)
await wait_for_cooldown(result.cooldown)

# Unequip
result = await char.unequip(slot=ItemSlot.WEAPON)
await wait_for_cooldown(result.cooldown)
```

### Gathering & Crafting

```python
# Gather (character must be on a resource tile)
result = await char.gathering()
print(f"+{result.details.xp}xp, got: {[d.code for d in result.details.items]}")
await wait_for_cooldown(result.cooldown)

# Craft (character must be at a workshop)
result = await char.crafting(code="iron_sword", quantity=1)
await wait_for_cooldown(result.cooldown)

# Recycle equipment
result = await char.recycling(code="iron_sword", quantity=1)
await wait_for_cooldown(result.cooldown)
```

### Bank Operations

```python
# Character must be on a bank tile

# Gold
result = await char.bank_deposit_gold(quantity=500)
result = await char.bank_withdraw_gold(quantity=200)

# Items
from artifacts.models import SimpleItemSchema
items = [SimpleItemSchema(code="copper_ore", quantity=50)]
result = await char.bank_deposit_items(items)
result = await char.bank_withdraw_items(items)

# Buy a bank expansion (+20 slots)
result = await char.bank_buy_expansion()
```

### NPC Trading

```python
# Character must be on an NPC tile
result = await char.npc_buy(code="wooden_staff", quantity=1)
result = await char.npc_sell(code="wooden_staff", quantity=1)
```

### Grand Exchange

```python
# Character must be on a GE tile

# Buy from an existing sell order
result = await char.ge_buy(id="order_id_here", quantity=10)

# Create a sell order
result = await char.ge_create_sell_order(code="copper_ore", quantity=100, price=5)

# Create a buy order (gold is locked upfront)
result = await char.ge_create_buy_order(code="copper_ore", quantity=100, price=5)

# Fill someone's buy order by selling to it
result = await char.ge_fill(id="order_id_here", quantity=50)

# Cancel your order
result = await char.ge_cancel_order(id="order_id_here")
```

### Tasks

```python
# Accept a new task (character must be at a tasks master)
result = await char.task_new()
print(f"Task: {result.task.code} ({result.task.type.value}) x{result.task.total}")

# Trade items for the task
result = await char.task_trade(code="copper_ore", quantity=10)

# Complete the task
result = await char.task_complete()

# Exchange 6 task coins for a random reward
result = await char.task_exchange()

# Cancel current task (costs 1 task coin)
result = await char.task_cancel()
```

### Consumables & Items

```python
# Use a consumable
result = await char.use(code="healing_potion", quantity=1)

# Delete an item from inventory
result = await char.delete_item(code="junk_item", quantity=5)
```

### Give Items/Gold to Another Character

```python
# Characters must be on the same map tile

# Give gold
result = await char.give_gold(quantity=100, character="OtherChar")

# Give items
items = [SimpleItemSchema(code="copper_ore", quantity=20)]
result = await char.give_items(items=items, character="OtherChar")
```

### Other

```python
# Claim a pending item
result = await char.claim_item(id=123)

# Change character skin
from artifacts.models import CharacterSkin
result = await char.change_skin(skin=CharacterSkin.MEN2)

# View action logs
logs = await char.get_logs(page=1, size=20)
for log in logs.data:
    print(f"  [{log.type.value}] {log.description}")
```

---

## Cooldown Handling

Every action returns a `cooldown` object. The wrapper **never** waits automatically -- you decide when to wait.

```python
from artifacts import wait_for_cooldown, cooldown_seconds

result = await char.fight()

# Option 1: Helper that sleeps for the remaining duration
await wait_for_cooldown(result.cooldown)

# Option 2: Read the value and handle it yourself
seconds = cooldown_seconds(result.cooldown)
print(f"Cooldown: {seconds}s remaining")
await asyncio.sleep(seconds)

# Option 3: Access the raw CooldownSchema
cd = result.cooldown
print(f"Total: {cd.total_seconds}s")
print(f"Remaining: {cd.remaining_seconds}s")
print(f"Reason: {cd.reason.value}")
print(f"Expires at: {cd.expiration}")
```

---

## Error Handling

The wrapper raises typed exceptions mapped to API error codes:

```python
from artifacts.errors import (
    ArtifactsAPIError,       # Base class for all API errors
    CooldownActiveError,     # 499 -- character is in cooldown
    ActionInProgressError,   # 486 -- action already running
    InventoryFullError,      # 497 -- inventory full
    InsufficientGoldError,   # 492 -- not enough gold
    NotFoundError,           # 404 -- resource not found
    ContentNotOnMapError,    # 598 -- no monster/resource here
    AlreadyAtDestinationError, # 490 -- already at target
    SkillLevelTooLowError,   # 493 -- skill level too low
    EquipmentSlotError,      # 491 -- equipment slot issue
    MapBlockedError,         # 596 -- map is blocked
    NoPathError,             # 595 -- no path to destination
    MemberRequiredError,     # 451 -- member/founder required
    ConditionsNotMetError,   # 496 -- conditions not met
    TaskError,               # 474-489 -- task-related errors
    GrandExchangeError,      # 433-438 -- GE errors
    ValidationError,         # 422 -- invalid request
)
```

Example:

```python
try:
    result = await char.fight()
    await wait_for_cooldown(result.cooldown)
except CooldownActiveError:
    await asyncio.sleep(3)
except InventoryFullError:
    print("Inventory full! Go deposit at the bank.")
except ContentNotOnMapError:
    print("No monster on this tile.")
except ArtifactsAPIError as e:
    print(f"API error [{e.code}]: {e.message}")
```

---

## Running Multiple Characters in Parallel

Use `asyncio.gather()` to run multiple characters at the same time:

```python
import asyncio
from artifacts import ArtifactsClient, wait_for_cooldown

async def combat_loop(char):
    for _ in range(10):
        info = await char.get()
        if info.hp < info.max_hp * 0.3:
            result = await char.rest()
            await wait_for_cooldown(result.cooldown)
            continue
        result = await char.fight()
        print(f"[{char.name}] {result.fight.result.value}")
        await wait_for_cooldown(result.cooldown)

async def main():
    async with ArtifactsClient(token="your_token") as client:
        names = ["Char1", "Char2", "Char3", "Char4", "Char5"]
        chars = [client.character(n) for n in names]
        await asyncio.gather(*[combat_loop(c) for c in chars])

asyncio.run(main())
```

See `examples/combat_loop_5chars.py` for a complete example with error handling, movement, and drop tracking.

---

## Character Management

```python
from artifacts.models import CharacterSkin

# Create a character (max 5 per account)
new_char = await client.characters.create("NewHero", CharacterSkin.MEN1)

# Delete a character
deleted = await client.characters.delete("NewHero")

# List all active characters on the server
active = await client.characters.get_active()

# Get any character's public info
info = await client.characters.get("SomePlayer")
```

---

## Simulation (Members Only)

```python
from artifacts.models import FakeCharacterSchema

fake = FakeCharacterSchema(
    level=20,
    weapon_slot="iron_sword",
    body_armor_slot="iron_armor",
)
result = await client.simulation.fight(
    characters=[fake],
    monster="ogre",
    iterations=100,
)
print(f"Winrate: {result.winrate:.1%} ({result.wins}W / {result.losses}L)")
```

---

## Sandbox (Sandbox Server Only)

When using the sandbox server (`base_url="https://api.sandbox.artifactsmmo.com"`):

```python
await client.sandbox.give_gold("MyChar", 10000)
await client.sandbox.give_item("MyChar", "iron_sword", 5)
await client.sandbox.give_xp("MyChar", "combat", 5000)
await client.sandbox.spawn_event("event_code")
await client.sandbox.reset_account()
```

---

## Complete API Reference

### Client Sub-Accessors

| Accessor | Methods |
|---|---|
| `client.server` | `get_status()` |
| `client.token` | `generate(username, password)` |
| `client.accounts` | `create()`, `forgot_password()`, `reset_password()`, `get()`, `get_achievements()`, `get_characters()` |
| `client.my_account` | `get_details()`, `get_bank()`, `get_bank_items()`, `get_ge_orders()`, `get_ge_history()`, `get_pending_items()`, `change_password()`, `get_characters()`, `get_all_logs()` |
| `client.characters` | `create()`, `delete()`, `get_active()`, `get()` |
| `client.items` | `get_all()`, `get()` |
| `client.monsters` | `get_all()`, `get()` |
| `client.maps` | `get_all()`, `get_layer()`, `get_by_position()`, `get_by_id()` |
| `client.resources` | `get_all()`, `get()` |
| `client.npcs` | `get_all()`, `get()`, `get_all_items()`, `get_items()` |
| `client.events` | `get_all_active()`, `get_all()`, `spawn()` |
| `client.achievements` | `get_all()`, `get()` |
| `client.badges` | `get_all()`, `get()` |
| `client.effects` | `get_all()`, `get()` |
| `client.grand_exchange` | `get_history()`, `get_orders()`, `get_order()` |
| `client.leaderboard` | `get_characters()`, `get_accounts()` |
| `client.tasks` | `get_all()`, `get()`, `get_all_rewards()`, `get_reward()` |
| `client.simulation` | `fight()` |
| `client.sandbox` | `give_gold()`, `give_item()`, `give_xp()`, `spawn_event()`, `reset_account()` |

### Character Methods

| Category | Methods |
|---|---|
| Info | `get()`, `get_logs()` |
| Movement | `move(x, y)`, `move(map_id)`, `transition()` |
| Combat | `fight()`, `rest()` |
| Equipment | `equip(code, slot)`, `unequip(slot)` |
| Skills | `gathering()`, `crafting(code, qty)`, `recycling(code, qty)` |
| Items | `use(code, qty)`, `delete_item(code, qty)` |
| Bank | `bank_deposit_gold(qty)`, `bank_withdraw_gold(qty)`, `bank_deposit_items(items)`, `bank_withdraw_items(items)`, `bank_buy_expansion()` |
| NPC | `npc_buy(code, qty)`, `npc_sell(code, qty)` |
| Grand Exchange | `ge_buy(id, qty)`, `ge_create_sell_order(code, qty, price)`, `ge_create_buy_order(code, qty, price)`, `ge_cancel_order(id)`, `ge_fill(id, qty)` |
| Tasks | `task_new()`, `task_complete()`, `task_exchange()`, `task_trade(code, qty)`, `task_cancel()` |
| Give | `give_gold(qty, character)`, `give_items(items, character)` |
| Misc | `claim_item(id)`, `change_skin(skin)` |
