From 0e96aa4f7c4716838c9c5dd4f289c60da40e16f2 Mon Sep 17 00:00:00 2001 From: Horoli Date: Tue, 2 Dec 2025 17:52:01 +0900 Subject: [PATCH] update --- assets/data/enemies.json | 24 ++-- assets/data/items.json | 60 ++++++++-- lib/game/data/enemy_table.dart | 4 + lib/game/data/item_table.dart | 42 +++++-- lib/game/model/entity.dart | 27 +++-- lib/game/model/item.dart | 4 +- lib/providers/battle_provider.dart | 5 +- prompt/00_project_context_restore.md | 101 +++++++++------- prompt/23_refactor_item_price_and_images.md | 51 ++++++++ test/character_test.dart | 124 ++++++++++++++------ 10 files changed, 314 insertions(+), 128 deletions(-) create mode 100644 prompt/23_refactor_item_price_and_images.md diff --git a/assets/data/enemies.json b/assets/data/enemies.json index 8048da8..8e5fd3e 100644 --- a/assets/data/enemies.json +++ b/assets/data/enemies.json @@ -4,31 +4,36 @@ "name": "Goblin", "baseHp": 20, "baseAtk": 5, - "baseDefense": 0 + "baseDefense": 0, + "image": "assets/images/enemies/goblin.png" }, { "name": "Slime", "baseHp": 30, "baseAtk": 3, - "baseDefense": 1 + "baseDefense": 1, + "image": "assets/images/enemies/slime.png" }, { "name": "Wolf", "baseHp": 25, "baseAtk": 7, - "baseDefense": 0 + "baseDefense": 0, + "image": "assets/images/enemies/wolf.png" }, { "name": "Bandit", "baseHp": 35, "baseAtk": 6, - "baseDefense": 1 + "baseDefense": 1, + "image": "assets/images/enemies/bandit.png" }, { "name": "Skeleton", "baseHp": 15, "baseAtk": 8, - "baseDefense": 0 + "baseDefense": 0, + "image": "assets/images/enemies/skeleton.png" } ], "elite": [ @@ -36,19 +41,22 @@ "name": "Orc Warrior", "baseHp": 60, "baseAtk": 12, - "baseDefense": 3 + "baseDefense": 3, + "image": "assets/images/enemies/orc_warrior.png" }, { "name": "Giant Spider", "baseHp": 50, "baseAtk": 15, - "baseDefense": 2 + "baseDefense": 2, + "image": "assets/images/enemies/giant_spider.png" }, { "name": "Dark Knight", "baseHp": 80, "baseAtk": 10, - "baseDefense": 5 + "baseDefense": 5, + "image": "assets/images/enemies/dark_knight.png" } ] } diff --git a/assets/data/items.json b/assets/data/items.json index a8ffdba..905baaa 100644 --- a/assets/data/items.json +++ b/assets/data/items.json @@ -4,25 +4,33 @@ "name": "Rusty Dagger", "description": "Old and rusty, but better than nothing.", "baseAtk": 3, - "slot": "weapon" + "slot": "weapon", + "price": 30, + "image": "assets/images/items/rusty_dagger.png" }, { "name": "Iron Sword", "description": "A standard soldier's sword.", "baseAtk": 8, - "slot": "weapon" + "slot": "weapon", + "price": 80, + "image": "assets/images/items/iron_sword.png" }, { "name": "Battle Axe", "description": "Heavy but powerful.", "baseAtk": 12, - "slot": "weapon" + "slot": "weapon", + "price": 120, + "image": "assets/images/items/battle_axe.png" }, { "name": "Stunning Hammer", "description": "A heavy hammer that can stun foes.", "baseAtk": 10, "slot": "weapon", + "price": 150, + "image": "assets/images/items/stunning_hammer.png", "effects": [ { "type": "stun", @@ -36,6 +44,8 @@ "description": "A cruel dagger that causes bleeding.", "baseAtk": 7, "slot": "weapon", + "price": 130, + "image": "assets/images/items/jagged_dagger.png", "effects": [ { "type": "bleed", @@ -50,6 +60,8 @@ "description": "An axe that exposes enemy weaknesses.", "baseAtk": 11, "slot": "weapon", + "price": 160, + "image": "assets/images/items/sunderer_axe.png", "effects": [ { "type": "vulnerable", @@ -64,19 +76,25 @@ "name": "Torn Tunic", "description": "Offers minimal protection.", "baseHp": 10, - "slot": "armor" + "slot": "armor", + "price": 20, + "image": "assets/images/items/torn_tunic.png" }, { "name": "Leather Vest", "description": "Light and flexible.", "baseHp": 30, - "slot": "armor" + "slot": "armor", + "price": 60, + "image": "assets/images/items/leather_vest.png" }, { "name": "Chainmail", "description": "Reliable protection against cuts.", "baseHp": 60, - "slot": "armor" + "slot": "armor", + "price": 120, + "image": "assets/images/items/chainmail.png" } ], "shields": [ @@ -84,25 +102,33 @@ "name": "Pot Lid", "description": "It was used for cooking.", "baseArmor": 1, - "slot": "shield" + "slot": "shield", + "price": 10, + "image": "assets/images/items/pot_lid.png" }, { "name": "Wooden Shield", "description": "Sturdy oak wood.", "baseArmor": 3, - "slot": "shield" + "slot": "shield", + "price": 40, + "image": "assets/images/items/wooden_shield.png" }, { "name": "Kite Shield", "description": "Used by knights.", "baseArmor": 6, - "slot": "shield" + "slot": "shield", + "price": 100, + "image": "assets/images/items/kite_shield.png" }, { "name": "Cursed Shield", "description": "A shield that prevents the wielder from defending themselves.", "baseArmor": 5, "slot": "shield", + "price": 50, + "image": "assets/images/items/cursed_shield.png", "effects": [ { "type": "defenseForbidden", @@ -118,21 +144,27 @@ "description": "A tarnished ring.", "baseAtk": 1, "baseHp": 5, - "slot": "accessory" + "slot": "accessory", + "price": 25, + "image": "assets/images/items/old_ring.png" }, { "name": "Copper Ring", "description": "A simple ring", "baseAtk": 1, "baseHp": 5, - "slot": "accessory" + "slot": "accessory", + "price": 25, + "image": "assets/images/items/copper_ring.png" }, { "name": "Ruby Amulet", "description": "Glows with a faint red light.", "baseAtk": 3, "baseHp": 15, - "slot": "accessory" + "slot": "accessory", + "price": 80, + "image": "assets/images/items/ruby_amulet.png" }, { "name": "Hero's Badge", @@ -140,7 +172,9 @@ "baseAtk": 5, "baseHp": 25, "baseArmor": 1, - "slot": "accessory" + "slot": "accessory", + "price": 150, + "image": "assets/images/items/heros_badge.png" } ] } diff --git a/lib/game/data/enemy_table.dart b/lib/game/data/enemy_table.dart index 3ff194c..5448b77 100644 --- a/lib/game/data/enemy_table.dart +++ b/lib/game/data/enemy_table.dart @@ -7,12 +7,14 @@ class EnemyTemplate { final int baseHp; final int baseAtk; final int baseDefense; + final String? image; const EnemyTemplate({ required this.name, required this.baseHp, required this.baseAtk, required this.baseDefense, + this.image, }); factory EnemyTemplate.fromJson(Map json) { @@ -21,6 +23,7 @@ class EnemyTemplate { baseHp: json['baseHp'] ?? 10, baseAtk: json['baseAtk'] ?? 1, baseDefense: json['baseDefense'] ?? 0, + image: json['image'], ); } @@ -36,6 +39,7 @@ class EnemyTemplate { atk: scaledAtk, baseDefense: scaledDefense, armor: 0, + image: image, ); } } diff --git a/lib/game/data/item_table.dart b/lib/game/data/item_table.dart index a8d86d0..eb0b256 100644 --- a/lib/game/data/item_table.dart +++ b/lib/game/data/item_table.dart @@ -11,6 +11,8 @@ class ItemTemplate { final int baseArmor; final EquipmentSlot slot; final List effects; + final int price; + final String? image; const ItemTemplate({ required this.name, @@ -20,6 +22,8 @@ class ItemTemplate { this.baseArmor = 0, required this.slot, this.effects = const [], + this.price = 0, + this.image, }); factory ItemTemplate.fromJson(Map json) { @@ -30,10 +34,13 @@ class ItemTemplate { baseHp: json['baseHp'] ?? 0, baseArmor: json['baseArmor'] ?? 0, slot: EquipmentSlot.values.firstWhere((e) => e.name == json['slot']), - effects: (json['effects'] as List?) + effects: + (json['effects'] as List?) ?.map((e) => ItemEffect.fromJson(e)) .toList() ?? [], + price: json['price'] ?? 0, + image: json['image'], ); } @@ -45,12 +52,12 @@ class ItemTemplate { int scaledHp = baseHp > 0 ? baseHp + (stage - 1) * 5 : 0; int scaledArmor = baseArmor > 0 ? baseArmor + (stage - 1) : 0; - // Calculate price based on stats - int calculatedPrice = (scaledAtk * 10) + (scaledHp * 2) + (scaledArmor * 5); - if (effects.isNotEmpty) { - calculatedPrice += effects.length * 50; // Bonus value for special effects + // Use fixed price from template + int finalPrice = price; + // Optional: Increase price if stage > 1 (e.g. +10% per stage) + if (stage > 1) { + finalPrice = (price * (1 + (stage - 1) * 0.1)).toInt(); } - if (calculatedPrice < 10) calculatedPrice = 10; // Minimum price return Item( name: "$name${stage > 1 ? ' +${stage - 1}' : ''}", // Append +1, +2 etc. @@ -60,7 +67,8 @@ class ItemTemplate { armorBonus: scaledArmor, slot: slot, effects: effects, // Pass the effects to the Item - price: calculatedPrice, + price: finalPrice, + image: image, ); } } @@ -72,13 +80,23 @@ class ItemTable { static List accessories = []; static Future load() async { - final String jsonString = await rootBundle.loadString('assets/data/items.json'); + final String jsonString = await rootBundle.loadString( + 'assets/data/items.json', + ); final Map data = jsonDecode(jsonString); - weapons = (data['weapons'] as List).map((e) => ItemTemplate.fromJson(e)).toList(); - armors = (data['armors'] as List).map((e) => ItemTemplate.fromJson(e)).toList(); - shields = (data['shields'] as List).map((e) => ItemTemplate.fromJson(e)).toList(); - accessories = (data['accessories'] as List).map((e) => ItemTemplate.fromJson(e)).toList(); + weapons = (data['weapons'] as List) + .map((e) => ItemTemplate.fromJson(e)) + .toList(); + armors = (data['armors'] as List) + .map((e) => ItemTemplate.fromJson(e)) + .toList(); + shields = (data['shields'] as List) + .map((e) => ItemTemplate.fromJson(e)) + .toList(); + accessories = (data['accessories'] as List) + .map((e) => ItemTemplate.fromJson(e)) + .toList(); } static List get allItems => [ diff --git a/lib/game/model/entity.dart b/lib/game/model/entity.dart index 1e43cee..e34b0c4 100644 --- a/lib/game/model/entity.dart +++ b/lib/game/model/entity.dart @@ -8,7 +8,9 @@ class Character { int armor; // Current temporary shield/armor points in battle int baseAtk; int baseDefense; // Base defense stat + int gold; // New: Currency + String? image; // New: Image path Map equipment = {}; List inventory = []; final int maxInventorySize = 16; @@ -24,9 +26,10 @@ class Character { required int atk, this.baseDefense = 0, this.gold = 0, - }) : baseMaxHp = maxHp, - baseAtk = atk, - hp = hp ?? maxHp; + this.image, + }) : baseMaxHp = maxHp, + baseAtk = atk, + hp = hp ?? maxHp; /// Adds a status effect. If it already exists, it refreshes duration or stacks based on logic. /// For now, we'll implement a simple refresh/overwrite logic. @@ -99,7 +102,9 @@ class Character { if (!inventory.contains(newItem)) return false; // 1. Calculate current HP ratio before any changes - double hpRatio = totalMaxHp > 0 ? hp / totalMaxHp : 0.0; // Avoid division by zero + double hpRatio = totalMaxHp > 0 + ? hp / totalMaxHp + : 0.0; // Avoid division by zero // 2. Handle Swap: If slot is occupied, unequip the old item first if (equipment.containsKey(newItem.slot)) { @@ -116,9 +121,10 @@ class Character { hp = (totalMaxHp * hpRatio).toInt(); if (hp < 0) hp = 0; // Ensure HP does not go below zero if (hp > totalMaxHp) { - hp = totalMaxHp; // Final Safety Clamp, though hpRatio <= 1.0 should prevent this + hp = + totalMaxHp; // Final Safety Clamp, though hpRatio <= 1.0 should prevent this } - + return true; } @@ -128,7 +134,9 @@ class Character { if (!equipment.containsValue(item)) return false; // 1. Calculate current HP ratio before any changes - double hpRatio = totalMaxHp > 0 ? hp / totalMaxHp : 0.0; // Avoid division by zero + double hpRatio = totalMaxHp > 0 + ? hp / totalMaxHp + : 0.0; // Avoid division by zero if (inventory.length < maxInventorySize) { equipment.remove(item.slot); @@ -138,11 +146,12 @@ class Character { hp = (totalMaxHp * hpRatio).toInt(); if (hp < 0) hp = 0; // Ensure HP does not go below zero if (hp > totalMaxHp) { - hp = totalMaxHp; // Final Safety Clamp, though hpRatio <= 1.0 should prevent this + hp = + totalMaxHp; // Final Safety Clamp, though hpRatio <= 1.0 should prevent this } return true; } - + return false; } diff --git a/lib/game/model/item.dart b/lib/game/model/item.dart index 9b9c519..a48ba3e 100644 --- a/lib/game/model/item.dart +++ b/lib/game/model/item.dart @@ -46,6 +46,7 @@ class Item { final EquipmentSlot slot; final List effects; // Status effects this item can inflict final int price; // New: Sell/Buy value + final String? image; // New: Image path Item({ required this.name, @@ -56,6 +57,7 @@ class Item { required this.slot, this.effects = const [], // Default to no effects this.price = 0, + this.image, }); String get typeName { @@ -70,4 +72,4 @@ class Item { return "Accessory"; } } -} \ No newline at end of file +} diff --git a/lib/providers/battle_provider.dart b/lib/providers/battle_provider.dart index 7bbc71b..3975a8e 100644 --- a/lib/providers/battle_provider.dart +++ b/lib/providers/battle_provider.dart @@ -515,8 +515,9 @@ class BattleProvider with ChangeNotifier { void sellItem(Item item) { if (player.inventory.remove(item)) { - player.gold += item.price; - _addLog("Sold ${item.name} for ${item.price} G."); + int sellPrice = GameMath.floor(item.price * 0.6); + player.gold += sellPrice; + _addLog("Sold ${item.name} for $sellPrice G."); notifyListeners(); } } diff --git a/prompt/00_project_context_restore.md b/prompt/00_project_context_restore.md index 6de6bb1..1219f16 100644 --- a/prompt/00_project_context_restore.md +++ b/prompt/00_project_context_restore.md @@ -3,70 +3,83 @@ 이 파일은 다른 개발 환경이나 새로운 AI 세션에서 프로젝트의 현재 상태를 빠르게 파악하고 작업을 이어가기 위해 작성되었습니다. ## 1. 프로젝트 개요 -* **프로젝트명:** Colosseum's Choice -* **플랫폼:** Flutter (Android/iOS/Web/Desktop) -* **장르:** 텍스트 기반의 턴제 RPG + GUI (로그라이크 요소 포함) -* **상태:** 프로토타입 단계 (핵심 전투 및 루프 구현 완료) + +- **프로젝트명:** Colosseum's Choice +- **플랫폼:** Flutter (Android/iOS/Web/Desktop) +- **장르:** 텍스트 기반의 턴제 RPG + GUI (로그라이크 요소 포함) +- **상태:** 프로토타입 단계 (핵심 전투, 아이템, 적 시스템 데이터화 완료) ## 2. 현재 구현된 핵심 기능 (Feature Status) ### A. 게임 흐름 (Game Flow) + 1. **메인 메뉴 (`MainMenuScreen`):** 게임 시작 버튼. 2. **캐릭터 선택 (`CharacterSelectionScreen`):** 현재 'Warrior' 직업만 구현됨. 선택 시 게임 초기화. 3. **메인 게임 (`MainWrapper`):** 하단 탭 네비게이션 (Battle / Inventory). ### B. 전투 시스템 (`BattleProvider`) -* **턴제 전투:** 플레이어 턴 -> 적 턴. -* **행동 선택:** 공격(Attack) / 방어(Defend). -* **리스크 시스템:** 행동 시 Safe(100% 성공, 50% 효율), Normal(80% 성공, 100% 효율), Risky(40% 성공, 200% 효율) 중 선택 가능. -* **상태이상 (Status Effects):** - * `Stun`: 행동 불가. - * `Bleed`: 턴 시작 시 지속 피해. - * `Vulnerable`: 받는 피해 1.5배 증가. - * `DefenseForbidden`: 방어 행동 불가. - * *특이사항:* 적에게 건 디버프는 플레이어 턴 시작 시 감소함 (직관성). -### C. 아이템 및 인벤토리 (`Item`, `ItemTable`) -* **장비:** 무기, 방어구, 방패, 장신구 슬롯. -* **아이템 효과 (`ItemEffect`):** 아이템 공격 시 확률적으로 상태이상 부여 (예: 20% 확률로 기절). -* **경제:** - * `Gold` 시스템 구현됨. - * 아이템마다 스탯 기반 `price` 자동 산출. - * 상점 스테이지에서 **판매(Sell)** 기능 구현됨. - * 인벤토리에서 장착(Equip), 버리기(Discard), 판매(Sell) 가능. +- **턴제 전투:** 플레이어 턴 -> 적 턴. +- **행동 선택:** 공격(Attack) / 방어(Defend). +- **리스크 시스템 (Risk System):** + - 플레이어와 적 모두 **Safe / Normal / Risky** 중 하나를 선택하여 행동. + - Safe: 100% 성공, 50% 효율. + - Normal: 80% 성공, 100% 효율. + - Risky: 40% 성공, 200% 효율. +- **적 인공지능 (Enemy AI & Intent):** + - 적은 턴 시작 시 행동(공격/방어)과 리스크 레벨을 무작위로 결정. + - **Intent UI:** 플레이어는 적의 다음 행동(아이콘, 설명)을 미리 볼 수 있음. + - _규칙:_ 적의 `baseDefense`가 0이면 방어 행동을 하지 않음. +- **상태이상 (Status Effects):** + - `Stun`, `Bleed`, `Vulnerable`, `DefenseForbidden` 구현됨. -### D. 스테이지 시스템 (`StageModel`, `StageType`) -* **스테이지 진행:** `currentStage` 객체로 관리. -* **타입 분기:** - * `Battle`: 일반 몬스터 전투. - * `Shop`: (5, 15... 스테이지) 상점 화면. (현재 판매만 가능, 구매 UI 미구현) - * `Rest`: (8, 18... 스테이지) 휴식 화면. (HP 회복) - * `Elite`: (10, 20... 스테이지) 강력한 적 등장. +### C. 데이터 주도 설계 (Data-Driven Design) + +- **JSON 데이터 관리:** `assets/data/` 폴더 내 JSON 파일로 게임 데이터 관리. + - `items.json`: 아이템 정의 (이름, 스탯, 효과, **가격**, **이미지 경로**). + - `enemies.json`: 적 정의 (Normal/Elite, 스탯, **이미지 경로**). +- **데이터 로더:** + - `ItemTable`: `items.json` 로드 및 `ItemTemplate` 관리. + - `EnemyTable`: `enemies.json` 로드 및 `EnemyTemplate` 관리. + +### D. 아이템 및 경제 (`Item`, `Inventory`) + +- **장비:** 무기, 방어구, 방패, 장신구 슬롯. +- **가격 정책:** + - `items.json`에 정의된 고정 `price` 사용. + - **판매(Sell):** 상점 등에서 판매 시 원가의 **60%** (소수점 버림, `GameMath.floor`) 획득. +- **이미지 필드:** 향후 UI 사용을 위해 `Item` 및 `Enemy` 모델에 `image` 필드 추가됨. + +### E. 스테이지 시스템 (`StageModel`, `StageType`) + +- **진행:** `currentStage` 객체로 관리. +- **타입:** Battle, Shop (5단위), Rest (8단위), Elite (10단위). +- **적 생성:** `EnemyTable`에서 현재 스테이지 타입(Normal/Elite)에 맞는 적을 무작위로 스폰하며, 스테이지에 따라 스탯 스케일링 적용. ## 3. 핵심 파일 및 아키텍처 -* **`lib/providers/battle_provider.dart`:** 게임의 **Core Logic**을 담당하는 거대 Provider. 상태 관리, 전투 계산, 스테이지 생성 등을 모두 처리. -* **`lib/game/model/`:** - * `entity.dart`: `Character` 클래스 (Player/Enemy 공용). - * `item.dart`: `Item`, `ItemEffect`. - * `stage.dart`: `StageModel`, `StageType`. - * `status_effect.dart`: 상태이상 정의. -* **`lib/game/data/item_table.dart`:** 아이템 템플릿 데이터. -* **`lib/screens/`:** - * `battle_screen.dart`: 전투 및 스테이지 상황(상점/휴식) UI. - * `inventory_screen.dart`: 인벤토리 및 장비 관리 UI. +- **`lib/providers/battle_provider.dart`:** 게임의 **Core Logic**. 상태 관리, 전투 루프, 적 AI(Intent) 생성, 스테이지 전환 담당. +- **`lib/game/data/`:** + - `item_table.dart`: 아이템 JSON 로더. + - `enemy_table.dart`: 적 JSON 로더. +- **`lib/game/model/`:** + - `entity.dart`: `Character` 클래스 (Player/Enemy 공용). `image` 필드 포함. + - `item.dart`: `Item` 클래스. `price`, `image` 필드 포함. +- **`assets/data/`:** `items.json`, `enemies.json`. ## 4. 작업 컨벤션 (Working Conventions) -* **Prompt Driven Development:** 새로운 기능을 구현할 때마다 `prompt/XX_description.md` 파일을 생성하여 작업 목표와 내용을 기록한다. -* **State Management:** `Provider` 패키지를 사용하며, 로직은 주로 `BattleProvider`에 집중시킨다. -* **Data Preservation:** 하드코딩된 데이터(`ItemTable`)를 사용 중이며, DB는 연동되지 않음. +- **Prompt Driven Development:** `prompt/XX_description.md` 형식을 유지하며 작업. +- **State Management:** `Provider` 사용. +- **Data:** JSON 파일 기반의 데이터 관리. ## 5. 다음 단계 작업 (Next Steps) -1. **상점 구매 기능:** `Shop` 스테이지에서 아이템을 구매하는 UI 및 로직 구현. -2. **밸런싱:** 상태이상 확률, 데미지 공식, 골드 획득량 조정. -3. **UI 개선:** 텍스트 위주의 로그를 시각적 효과(애니메이션, 플로팅 텍스트)로 개선. +1. **상점 구매 기능:** `Shop` 스테이지에서 아이템 목록을 보여주고 구매하는 UI 구현. +2. **이미지 리소스 적용:** JSON에 정의된 경로에 실제 이미지 파일(`assets/images/...`)을 추가하고 UI(`BattleScreen`, `InventoryScreen`)에 표시. +3. **UI 개선:** 텍스트 로그 외에 시각적 피드백(데미지 플로팅, 효과 이펙트) 추가. +4. **밸런싱 및 콘텐츠 확장:** 더 많은 아이템과 적 데이터 추가. --- + **이 프롬프트를 읽은 AI 에이전트는 위 내용을 바탕으로 즉시 개발을 이어가십시오.** diff --git a/prompt/23_refactor_item_price_and_images.md b/prompt/23_refactor_item_price_and_images.md new file mode 100644 index 0000000..e3ae9e0 --- /dev/null +++ b/prompt/23_refactor_item_price_and_images.md @@ -0,0 +1,51 @@ +# 23. 아이템 가격 리팩토링 및 이미지 필드 추가 + +## 목표 + +1. 아이템의 가격(`price`)을 코드 내 동적 계산이 아닌 `items.json`에 명시된 고정 값으로 변경합니다. +2. 상점 구매 시 이 고정 가격을 사용하고, 판매 시에는 60%의 가격을 적용합니다. +3. 향후 UI 개선을 위해 `items.json`과 `enemies.json`에 이미지 경로 필드를 미리 추가합니다. + +## 1. 아이템 가격 리팩토링 + +### A. 데이터 파일 수정 (`assets/data/items.json`) + +- 모든 아이템 항목에 `price` 필드를 추가합니다. (예: `"price": 100`) +- 적절한 가격을 설정합니다. + +### B. 데이터 로더 수정 (`lib/game/data/item_table.dart`) + +- `ItemTemplate` 클래스에서 `price` 필드를 파싱하도록 수정합니다. +- `createItem` 메서드에서 가격을 계산하는 로직을 제거하고, 템플릿의 `price`를 그대로 사용하도록 변경합니다. + - 단, 스테이지 스케일링에 따라 가격이 변동되어야 한다면 그 로직은 유지하거나 수정할 수 있습니다. (현재 요구사항은 "고정된 price"이므로 기본적으로 JSON 값을 따르되, +1 강화된 아이템의 경우 가격 상승 로직이 필요할 수 있음. 일단 기본 가격은 JSON을 따르게 함) + +### C. 상점 및 판매 로직 확인 + +- `BattleProvider` (또는 상점 로직이 있는 곳)에서 아이템 구매 시 `item.price`를 사용하도록 확인합니다. +- 아이템 판매 시 `GameMath.floor(item.price * 0.6)`를 사용하여 소수점을 버리도록 로직을 수정합니다. + +## 2. 이미지 필드 추가 + +### A. JSON 데이터 수정 + +- `assets/data/items.json`: 각 아이템에 `image` 필드 추가 (예: `"image": "assets/images/items/sword.png"`) +- `assets/data/enemies.json`: 각 적에게 `image` 필드 추가 (예: `"image": "assets/images/enemies/goblin.png"`) +- 실제 이미지 파일은 아직 없으므로 필드만 추가합니다. + +### B. 모델 클래스 수정 + +- `lib/game/data/item_table.dart` -> `ItemTemplate`: `image` 필드 파싱 추가. +- `lib/game/model/item.dart` -> `Item`: `image` 필드 추가. +- `lib/game/data/enemy_table.dart` -> `EnemyTemplate`: `image` 필드 파싱 추가. +- `lib/game/model/entity.dart` -> `Character`: `image` 필드 추가 (선택적). + +### C. UI 반영 (BattleScreen, Inventory) + +- `BattleScreen`과 인벤토리 UI에서 해당 이미지 경로를 사용할 수 있도록 준비합니다. +- 이미지가 없는 경우(null 또는 파일 없음) 기존처럼 텍스트나 기본 아이콘을 표시하도록 예외 처리를 해둡니다. + +## 검증 + +- `items.json`의 가격이 게임 내(상점/인벤토리)에 올바르게 반영되는지 확인. +- 판매 시 가격이 60%로 계산되는지 확인. +- 데이터 로딩 시 이미지 필드가 정상적으로 파싱되는지 확인 (테스트 코드 활용). diff --git a/test/character_test.dart b/test/character_test.dart index 738a566..9de2853 100644 --- a/test/character_test.dart +++ b/test/character_test.dart @@ -17,6 +17,7 @@ void main() { atkBonus: 0, hpBonus: 50, slot: EquipmentSlot.armor, + price: 100, ); armorHp100 = Item( name: "Armor +100", @@ -24,6 +25,7 @@ void main() { atkBonus: 0, hpBonus: 100, slot: EquipmentSlot.armor, + price: 200, ); armorHp20 = Item( name: "Armor +20", @@ -31,63 +33,107 @@ void main() { atkBonus: 0, hpBonus: 20, slot: EquipmentSlot.armor, + price: 50, ); - + // Add items to inventory initially player.addToInventory(armorHp50); player.addToInventory(armorHp100); player.addToInventory(armorHp20); }); - test('Equipping item increases MaxHP and scales Current HP proportionally', () { - expect(player.hp, 100); - expect(player.totalMaxHp, 100); + test( + 'Equipping item increases MaxHP and scales Current HP proportionally', + () { + expect(player.hp, 100); + expect(player.totalMaxHp, 100); - player.equip(armorHp50); // From 100/100 (100% HP) to 150 MaxHP + player.equip(armorHp50); // From 100/100 (100% HP) to 150 MaxHP - expect(player.totalMaxHp, 150, reason: "Max HP should increase by 50"); - expect(player.hp, 150, reason: "Current HP should scale to 100% of new MaxHP"); - }); + expect(player.totalMaxHp, 150, reason: "Max HP should increase by 50"); + expect( + player.hp, + 150, + reason: "Current HP should scale to 100% of new MaxHP", + ); + }, + ); - test('Unequipping item decreases MaxHP and scales Current HP proportionally', () { - player.equip(armorHp50); // HP becomes 150/150 - player.unequip(armorHp50); // MaxHP becomes 100 + test( + 'Unequipping item decreases MaxHP and scales Current HP proportionally', + () { + player.equip(armorHp50); // HP becomes 150/150 + player.unequip(armorHp50); // MaxHP becomes 100 - expect(player.totalMaxHp, 100); - expect(player.hp, 100, reason: "Current HP should scale to 100% of new MaxHP"); - }); + expect(player.totalMaxHp, 100); + expect( + player.hp, + 100, + reason: "Current HP should scale to 100% of new MaxHP", + ); + }, + ); - test('Unequipping item clamps Current HP if it exceeds new MaxHP (Already 100% HP)', () { - player.equip(armorHp50); // HP becomes 150/150 - // No need to heal(50) as it's already 150/150. - expect(player.hp, 150); + test( + 'Unequipping item clamps Current HP if it exceeds new MaxHP (Already 100% HP)', + () { + player.equip(armorHp50); // HP becomes 150/150 + // No need to heal(50) as it's already 150/150. + expect(player.hp, 150); - player.unequip(armorHp50); // MaxHP 100 + player.unequip(armorHp50); // MaxHP 100 - expect(player.totalMaxHp, 100); - expect(player.hp, 100, reason: "Current HP should scale to 100% of new MaxHP"); - }); + expect(player.totalMaxHp, 100); + expect( + player.hp, + 100, + reason: "Current HP should scale to 100% of new MaxHP", + ); + }, + ); - test('Swapping items handles HP correctly (Upgrade and maintain percentage)', () { - player.equip(armorHp50); // HP 100/100 -> equip -> 150/150 (100%) - player.hp = 75; // Set HP to 75/150 (50%) - expect(player.hp, 75); - expect(player.totalMaxHp, 150); + test( + 'Swapping items handles HP correctly (Upgrade and maintain percentage)', + () { + player.equip(armorHp50); // HP 100/100 -> equip -> 150/150 (100%) + player.hp = 75; // Set HP to 75/150 (50%) + expect(player.hp, 75); + expect(player.totalMaxHp, 150); - player.equip(armorHp100); // Swap armorHp50 (HP+50) with armorHp100 (HP+100). New MaxHP is 200. + player.equip( + armorHp100, + ); // Swap armorHp50 (HP+50) with armorHp100 (HP+100). New MaxHP is 200. - expect(player.totalMaxHp, 200); - expect(player.hp, 100, reason: "HP should scale to 50% of new MaxHP (200 * 0.5 = 100)"); - expect(player.inventory.contains(armorHp50), true, reason: "Old item returned to inventory"); - }); + expect(player.totalMaxHp, 200); + expect( + player.hp, + 100, + reason: "HP should scale to 50% of new MaxHP (200 * 0.5 = 100)", + ); + expect( + player.inventory.contains(armorHp50), + true, + reason: "Old item returned to inventory", + ); + }, + ); - test('Swapping items handles HP correctly (Downgrade causing clamp due to percentage)', () { - player.equip(armorHp50); // HP 100/100 -> equip -> 150/150 (100%) - - player.equip(armorHp20); // Swap armorHp50 (HP+50) with armorHp20 (HP+20). New MaxHP is 120. + test( + 'Swapping items handles HP correctly (Downgrade causing clamp due to percentage)', + () { + player.equip(armorHp50); // HP 100/100 -> equip -> 150/150 (100%) - expect(player.totalMaxHp, 120); - expect(player.hp, 120, reason: "HP should scale to 100% of new MaxHP (120 * 1.0 = 120)"); - }); + player.equip( + armorHp20, + ); // Swap armorHp50 (HP+50) with armorHp20 (HP+20). New MaxHP is 120. + + expect(player.totalMaxHp, 120); + expect( + player.hp, + 120, + reason: "HP should scale to 100% of new MaxHP (120 * 1.0 = 120)", + ); + }, + ); }); }