{"id":31360554,"url":"https://github.com/dante4rt/tamago-sui","last_synced_at":"2025-09-27T01:53:54.769Z","repository":{"id":313522808,"uuid":"1051719162","full_name":"dante4rt/tamago-sui","owner":"dante4rt","description":"A hands‑on workshop project that builds a Tamagotchi‑style virtual pet game on the Sui blockchain. It demonstrates advanced Move patterns—ownership, dynamic fields, events, and time‑based logic—paired with a modern React + TypeScript frontend using Mysten dApp Kit.","archived":false,"fork":false,"pushed_at":"2025-09-06T17:02:13.000Z","size":240,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-06T17:38:02.438Z","etag":null,"topics":["contract","game","move","sui","tamagochi"],"latest_commit_sha":null,"homepage":"https://tamago-sui.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dante4rt.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-06T15:23:24.000Z","updated_at":"2025-09-06T17:02:16.000Z","dependencies_parsed_at":"2025-09-06T17:49:22.095Z","dependency_job_id":null,"html_url":"https://github.com/dante4rt/tamago-sui","commit_stats":null,"previous_names":["dante4rt/tamago-sui"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/dante4rt/tamago-sui","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dante4rt%2Ftamago-sui","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dante4rt%2Ftamago-sui/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dante4rt%2Ftamago-sui/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dante4rt%2Ftamago-sui/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dante4rt","download_url":"https://codeload.github.com/dante4rt/tamago-sui/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dante4rt%2Ftamago-sui/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":277170766,"owners_count":25773065,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-26T02:00:09.010Z","response_time":78,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["contract","game","move","sui","tamagochi"],"created_at":"2025-09-27T01:53:53.678Z","updated_at":"2025-09-27T01:53:54.756Z","avatar_url":"https://github.com/dante4rt.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Workshop Tamagosui - Pengembangan Smart Contract Move Tingkat Lanjut by Rama\n\n## 🚀 Modul 3: Advanced Smart Contract Development with Tamagosui\n\n### 🎯 Tujuan Pembelajaran\n\nDi akhir modul ini, Anda akan dapat:\n\n- Mempelajari fitur-fitur lanjutan Sui dan Move melalui pembuatan game pet virtual ala Tamagotchi\n- Memahami ownership, dynamic fields, dan logika berbasis waktu dalam praktik\n- Membangun sistem pet virtual lengkap dengan metadata dan aksesori\n- Menerapkan teknik optimasi gas untuk aplikasi gaming\n\n---\n\n## Tamagotchi Smart Contract - Step by Step Implementation\n\n## Prerequisites\n\n- Sui CLI terinstal\n- Text editor (VS Code dengan Move extension direkomendasikan)\n- Pemahaman dasar tentang blockchain dan Move language\n\n---\n\n## Step 1: Create Project Structure\n\n```bash\n# Create main project directory\nmkdir tamagosui\ncd tamagosui\n\n# Create contract directory\nsui move new tamagosui_contract\ncd tamagosui_contract\n```\n\n## Step 2: Configure `Move.toml`:\n\n```toml\n[package]\nname = \"tamagosui\"\nedition = \"2024.beta\"\n\n[dependencies]\nSui = { git = \"https://github.com/MystenLabs/sui.git\", subdir = \"crates/sui-framework/packages/sui-framework\", rev = \"framework/testnet\" }\n\n[addresses]\ntamagosui = \"0x0\"\n\n[dev-dependencies]\n\n[dev-addresses]\n\n```\n\n## Step 3: Implementasi Smart Contract Tamagosui\n\nBuat file `sources/tamagosui.move` dan mulai dengan deklarasi module dan import yang diperlukan:\n\n```move\nmodule 0x0::tamagosui;\n\nuse std::string::{Self, String};\nuse sui::{clock::Clock, display, dynamic_field, event, package};\n```\n\n## Step 4: Constants dan Error Codes\n\nTambahkan konstanta untuk error handling dan asset URLs:\n\n```move\n// === Errors ===\nconst E_NOT_ENOUGH_COINS: u64 = 101;\nconst E_PET_NOT_HUNGRY: u64 = 102;\nconst E_PET_TOO_TIRED: u64 = 103;\nconst E_PET_TOO_HUNGRY: u64 = 104;\nconst E_ITEM_ALREADY_EQUIPPED: u64 = 105;\nconst E_NO_ITEM_EQUIPPED: u64 = 106;\nconst E_NOT_ENOUGH_EXP: u64 = 107;\nconst E_PET_IS_ASLEEP: u64 = 108;\nconst E_PET_IS_ALREADY_ASLEEP: u64 = 109;\n\n// === Constants ===\nconst PET_LEVEL_1_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreidkhjpthergw2tcg6u5r344shgi2cdg5afmhgpf5bv34vqfrr7hni\";\nconst PET_LEVEL_1_IMAGE_WITH_GLASSES_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreibizappmcjaq5a5metl27yc46co4kxewigq6zu22vovwvn5qfsbiu\";\nconst PET_LEVEL_2_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreia5tgsowzfu6mzjfcxagfpbkghfuho6y5ybetxh3wabwrc5ajmlpq\";\nconst PET_LEVEL_2_IMAGE_WITH_GLASSES_URL:vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreif5bkpnqyybq3aqgafqm72x4wfjwcuxk33vvykx44weqzuilop424\";\nconst PET_LEVEL_3_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreidnqerfwxuxkrdsztgflmg5jwuespdkrazl6qmk7ykfgmrfzvinoy\";\nconst PET_LEVEL_3_IMAGE_WITH_GLASSES_URL:vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreigs6r3rdupoji7pqmpwe76z7wysguzdlq43t3wqmzi2654ux5n6uu\";\nconst PET_SLEEP_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreihwofl5stihtzjixfhrtznd7zqkclfhmlshgsg7cbszzjqqpvf7ae\";\nconst ACCESSORY_GLASSES_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreigyivmq45od3jkryryi3w6t5j65hcnfh5kgwpi2ex7llf2i6se7de\";\n\nconst EQUIPPED_ITEM_KEY: vector\u003cu8\u003e = b\"equipped_item\";\nconst SLEEP_STARTED_AT_KEY: vector\u003cu8\u003e = b\"sleep_started_at\";\n```\n\n## Step 5: Game Balance Configuration\n\nTambahkan struct untuk mengatur balance game:\n\n```move\n// === Game Balance ===\npublic struct GameBalance has copy, drop {\n    max_stat: u8,\n\n    // Feed settings\n    feed_coins_cost: u64,\n    feed_experience_gain: u64,\n    feed_hunger_gain: u8,\n\n    // Play settings\n    play_energy_loss: u8,\n    play_hunger_loss: u8,\n    play_experience_gain: u64,\n    play_happiness_gain: u8,\n\n    // Work settings\n    work_energy_loss: u8,\n    work_happiness_loss: u8,\n    work_hunger_loss: u8,\n    work_coins_gain: u64,\n    work_experience_gain: u64,\n\n    // Sleep settings (in milliseconds)\n    sleep_energy_gain_ms: u64,\n    sleep_happiness_loss_ms: u64,\n    sleep_hunger_loss_ms: u64,\n\n    // Level settings\n    exp_per_level: u64,\n}\n\nfun get_game_balance(): GameBalance {\n    GameBalance {\n        max_stat: 100,\n\n        // Feed\n        feed_coins_cost: 5,\n        feed_experience_gain: 5,\n        feed_hunger_gain: 20,\n\n        // Play\n        play_energy_loss: 15,\n        play_hunger_loss: 15,\n        play_experience_gain: 10,\n        play_happiness_gain: 25,\n\n        // Work\n        work_energy_loss: 20,\n        work_hunger_loss: 20,\n        work_happiness_loss: 20,\n        work_coins_gain: 10,\n        work_experience_gain: 15,\n\n        // Sleep (rates per millisecond)\n        sleep_energy_gain_ms: 1000,    // 1 energy per second\n        sleep_happiness_loss_ms: 700, // 1 happiness loss per 0.7 seconds\n        sleep_hunger_loss_ms: 500,    // 1 hunger loss per 0.5 seconds\n\n        // Level\n        exp_per_level: 100,\n    }\n}\n```\n\n## Step 6: Core Data Structures\n\nDefinisikan struct utama untuk Pet dan komponennya:\n\n```move\npublic struct TAMAGOSUI has drop {}\n\npublic struct Pet has key, store {\n    id: UID,\n    name: String,\n    image_url: String,\n    adopted_at: u64,\n    stats: PetStats,\n    game_data: PetGameData,\n}\n\npublic struct PetAccessory has key, store {\n    id: UID,\n    name: String,\n    image_url: String\n}\n\npublic struct PetStats has store {\n    energy: u8,\n    happiness: u8,\n    hunger: u8,\n}\n\npublic struct PetGameData has store {\n    coins: u64,\n    experience: u64,\n    level: u8,\n}\n```\n\n## Step 7: Events\n\nTambahkan struct untuk events:\n\n```move\n// === Events ===\npublic struct PetAdopted has copy, drop {\n    pet_id: ID,\n    name: String,\n    adopted_at: u64\n}\n\npublic struct PetAction has copy, drop {\n    pet_id: ID,\n    action: String,\n    energy: u8,\n    happiness: u8,\n    hunger: u8\n}\n```\n\n## Step 8: Module Initialization\n\nImplement fungsi `init` untuk setup display dan publisher:\n\n```move\nfun init(witness: TAMAGOSUI, ctx: \u0026mut TxContext) {\n    let publisher = package::claim(witness, ctx);\n\n    let pet_keys = vector[\n        string::utf8(b\"name\"),\n        string::utf8(b\"image_url\"),\n        string::utf8(b\"birth_date\"),\n        string::utf8(b\"experience\"),\n        string::utf8(b\"level\"),\n    ];\n\n    let pet_values = vector[\n        string::utf8(b\"{name}\"),\n        string::utf8(b\"{image_url}\"),\n        string::utf8(b\"{adopted_at}\"),\n        string::utf8(b\"{game_data.experience}\"),\n        string::utf8(b\"{game_data.level}\"),\n    ];\n\n    let mut pet_display = display::new_with_fields\u003cPet\u003e(\u0026publisher, pet_keys, pet_values, ctx);\n    pet_display.update_version();\n    transfer::public_transfer(pet_display, ctx.sender());\n\n    let accessory_keys = vector[\n        string::utf8(b\"name\"),\n        string::utf8(b\"image_url\")\n    ];\n    let accessory_values = vector[\n        string::utf8(b\"{name}\"),\n        string::utf8(b\"{image_url}\")\n    ];\n    let mut accessory_display = display::new_with_fields\u003cPetAccessory\u003e(\u0026publisher, accessory_keys, accessory_values, ctx);\n    accessory_display.update_version();\n    transfer::public_transfer(accessory_display, ctx.sender());\n\n    transfer::public_transfer(publisher, ctx.sender());\n}\n```\n\n## Step 9: Pet Adoption Function\n\nImplement fungsi untuk mengadopsi pet:\n\n```move\npublic entry fun adopt_pet(\n    name: String,\n    clock: \u0026Clock,\n    ctx: \u0026mut TxContext\n) {\n    let current_time = clock.timestamp_ms();\n\n    let pet_stats = PetStats {\n        energy: 60,\n        happiness: 50,\n        hunger: 40,\n    };\n\n    let pet_game_data = PetGameData {\n        coins: 20,\n        experience: 0,\n        level: 1\n    };\n\n    let pet = Pet {\n        id: object::new(ctx),\n        name,\n        image_url: string::utf8(PET_LEVEL_1_IMAGE_URL),\n        adopted_at: current_time,\n        stats: pet_stats,\n        game_data: pet_game_data\n    };\n\n    let pet_id = object::id(\u0026pet);\n\n    event::emit(PetAdopted {\n        pet_id: pet_id,\n        name: pet.name,\n        adopted_at: pet.adopted_at\n    });\n\n    transfer::public_transfer(pet, ctx.sender());\n}\n```\n\n## Step 10: Basic Pet Care Functions\n\nImplement fungsi dasar untuk merawat pet:\n\n### Feed Pet Function:\n\n```move\npublic entry fun feed_pet(pet: \u0026mut Pet) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let gb = get_game_balance();\n\n    assert!(pet.stats.hunger \u003c gb.max_stat, E_PET_NOT_HUNGRY);\n    assert!(pet.game_data.coins \u003e= gb.feed_coins_cost, E_NOT_ENOUGH_COINS);\n\n    pet.game_data.coins = pet.game_data.coins - gb.feed_coins_cost;\n    pet.game_data.experience = pet.game_data.experience + gb.feed_experience_gain;\n    pet.stats.hunger = if (pet.stats.hunger + gb.feed_hunger_gain \u003e gb.max_stat)\n        gb.max_stat\n    else\n        pet.stats.hunger + gb.feed_hunger_gain;\n\n    emit_action(pet, b\"fed\");\n}\n```\n\n### Play with Pet Function:\n\n```move\npublic entry fun play_with_pet(pet: \u0026mut Pet) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let gb = get_game_balance();\n    assert!(pet.stats.energy \u003e= gb.play_energy_loss, E_PET_TOO_TIRED);\n    assert!(pet.stats.hunger \u003e= gb.play_hunger_loss, E_PET_TOO_HUNGRY);\n\n    pet.stats.energy = pet.stats.energy - gb.play_energy_loss;\n    pet.stats.hunger = pet.stats.hunger - gb.play_hunger_loss;\n    pet.game_data.experience = pet.game_data.experience + gb.play_experience_gain;\n    pet.stats.happiness = if (pet.stats.happiness + gb.play_happiness_gain \u003e gb.max_stat)\n        gb.max_stat\n    else\n        pet.stats.happiness + gb.play_happiness_gain;\n\n    emit_action(pet, b\"played\");\n}\n```\n\n## Step 11: Work and Level System\n\n### Work Function:\n\n```move\npublic entry fun work_for_coins(pet: \u0026mut Pet) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let gb = get_game_balance();\n\n    assert!(pet.stats.energy \u003e= gb.work_energy_loss, E_PET_TOO_TIRED);\n    assert!(pet.stats.happiness \u003e= gb.work_happiness_loss, E_PET_NOT_HUNGRY);\n    assert!(pet.stats.hunger \u003e= gb.work_hunger_loss, E_PET_TOO_HUNGRY);\n\n    pet.stats.energy = if (pet.stats.energy \u003e= gb.work_energy_loss)\n        pet.stats.energy - gb.work_energy_loss\n    else\n        0;\n    pet.stats.happiness = if (pet.stats.happiness \u003e= gb.work_happiness_loss)\n        pet.stats.happiness - gb.work_happiness_loss\n    else\n        0;\n    pet.stats.hunger = if (pet.stats.hunger \u003e= gb.work_hunger_loss)\n        pet.stats.hunger - gb.work_hunger_loss\n    else\n        0;\n    pet.game_data.coins = pet.game_data.coins + gb.work_coins_gain;\n    pet.game_data.experience = pet.game_data.experience + gb.work_experience_gain;\n\n    emit_action(pet, b\"worked\");\n}\n```\n\n### Level Up Function:\n\n```move\npublic entry fun check_and_level_up(pet: \u0026mut Pet) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let gb = get_game_balance();\n\n    // Calculate required exp: level * exp_per_level\n    let required_exp = (pet.game_data.level as u64) * gb.exp_per_level;\n    assert!(pet.game_data.experience \u003e= required_exp, E_NOT_ENOUGH_EXP);\n\n    // Level up\n    pet.game_data.level = pet.game_data.level + 1;\n    pet.game_data.experience = pet.game_data.experience - required_exp;\n\n    // Update image based on level and equipped accessory\n    update_pet_image(pet);\n\n    emit_action(pet, b\"leveled_up\")\n}\n```\n\n## Step 12: Sleep System\n\n### Sleep Functions:\n\n```move\npublic entry fun let_pet_sleep(pet: \u0026mut Pet, clock: \u0026Clock) {\n    assert!(!is_sleeping(pet), E_PET_IS_ALREADY_ASLEEP);\n\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    dynamic_field::add(\u0026mut pet.id, key, clock.timestamp_ms());\n\n    pet.image_url = string::utf8(PET_SLEEP_IMAGE_URL);\n\n    emit_action(pet, b\"started_sleeping\");\n}\n\npublic entry fun wake_up_pet(pet: \u0026mut Pet, clock: \u0026Clock) {\n    assert!(is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    let sleep_started_at: u64 = dynamic_field::remove\u003cString, u64\u003e(\u0026mut pet.id, key);\n    let duration_ms = clock.timestamp_ms() - sleep_started_at;\n\n    let gb = get_game_balance();\n\n    // Calculate energy gained\n    let energy_gained_u64 = duration_ms / gb.sleep_energy_gain_ms;\n    let energy_gained = if (energy_gained_u64 \u003e (gb.max_stat as u64)) {\n        gb.max_stat\n    } else {\n        (energy_gained_u64 as u8)\n    };\n    pet.stats.energy = if (pet.stats.energy + energy_gained \u003e gb.max_stat) gb.max_stat else pet.stats.energy + energy_gained;\n\n    // Calculate happiness lost\n    let happiness_lost_u64 = duration_ms / gb.sleep_happiness_loss_ms;\n    let happiness_lost = if (happiness_lost_u64 \u003e (gb.max_stat as u64)) {\n        gb.max_stat\n    } else {\n        (happiness_lost_u64 as u8)\n    };\n    pet.stats.happiness = if (pet.stats.happiness \u003e happiness_lost) pet.stats.happiness - happiness_lost else 0;\n\n    // Calculate hunger lost\n    let hunger_lost_u64 = duration_ms / gb.sleep_hunger_loss_ms;\n    let hunger_lost = if (hunger_lost_u64 \u003e (gb.max_stat as u64)) {\n        gb.max_stat\n    } else {\n        (hunger_lost_u64 as u8)\n    };\n    pet.stats.hunger = if (pet.stats.hunger \u003e hunger_lost) pet.stats.hunger - hunger_lost else 0;\n\n    update_pet_image(pet);\n\n    emit_action(pet, b\"woke_up\");\n}\n```\n\n## Step 13: Accessory System\n\n### Mint dan Equip Accessories:\n\n```move\npublic entry fun mint_accessory(ctx: \u0026mut TxContext) {\n    let accessory = PetAccessory {\n        id: object::new(ctx),\n        name: string::utf8(b\"cool glasses\"),\n        image_url: string::utf8(ACCESSORY_GLASSES_IMAGE_URL)\n    };\n    transfer::public_transfer(accessory, ctx.sender());\n}\n\npublic entry fun equip_accessory(pet: \u0026mut Pet, accessory: PetAccessory) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let key = string::utf8(EQUIPPED_ITEM_KEY);\n    assert!(!dynamic_field::exists_\u003cString\u003e(\u0026pet.id, copy key), E_ITEM_ALREADY_EQUIPPED);\n\n    // Add accessory to pet\n    dynamic_field::add(\u0026mut pet.id, key, accessory);\n    // Update image\n    update_pet_image(pet);\n    emit_action(pet, b\"equipped_item\");\n}\n\npublic entry fun unequip_accessory(pet: \u0026mut Pet, ctx: \u0026mut TxContext) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let key = string::utf8(EQUIPPED_ITEM_KEY);\n    assert!(dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key), E_NO_ITEM_EQUIPPED);\n\n    // Remove accessory\n    let accessory: PetAccessory = dynamic_field::remove\u003cString, PetAccessory\u003e(\u0026mut pet.id, key);\n    // Update image\n    update_pet_image(pet);\n\n    transfer::transfer(accessory, ctx.sender());\n    emit_action(pet, b\"unequipped_item\");\n}\n```\n\n## Step 14: Helper Functions\n\n### Emit Action dan Update Image:\n\n```move\n// === Helper Functions ===\nfun emit_action(pet: \u0026Pet, action: vector\u003cu8\u003e) {\n    event::emit(PetAction {\n        pet_id: object::id(pet),\n        action: string::utf8(action),\n        energy: pet.stats.energy,\n        happiness: pet.stats.happiness,\n        hunger: pet.stats.hunger,\n    });\n}\n\nfun update_pet_image(pet: \u0026mut Pet) {\n    let key = string::utf8(EQUIPPED_ITEM_KEY);\n    let has_accessory = dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key);\n\n    if (pet.game_data.level == 1) {\n        if (has_accessory) {\n            pet.image_url = string::utf8(PET_LEVEL_1_IMAGE_WITH_GLASSES_URL);\n        } else {\n            pet.image_url = string::utf8(PET_LEVEL_1_IMAGE_URL);\n        }\n    } else if (pet.game_data.level == 2) {\n        if (has_accessory) {\n            pet.image_url = string::utf8(PET_LEVEL_2_IMAGE_WITH_GLASSES_URL);\n        } else {\n            pet.image_url = string::utf8(PET_LEVEL_2_IMAGE_URL);\n        }\n    } else if (pet.game_data.level \u003e= 3) {\n        if (has_accessory) {\n            pet.image_url = string::utf8(PET_LEVEL_3_IMAGE_WITH_GLASSES_URL);\n        } else {\n            pet.image_url = string::utf8(PET_LEVEL_3_IMAGE_URL);\n        }\n    };\n}\n```\n\n## Step 15: View Functions\n\nTambahkan fungsi untuk membaca data pet:\n\n```move\n// === View Functions ===\npublic fun get_pet_name(pet: \u0026Pet): String { pet.name }\npublic fun get_pet_adopted_at(pet: \u0026Pet): u64 { pet.adopted_at }\npublic fun get_pet_coins(pet: \u0026Pet): u64 { pet.game_data.coins }\npublic fun get_pet_experience(pet: \u0026Pet): u64 { pet.game_data.experience }\npublic fun get_pet_level(pet: \u0026Pet): u8 { pet.game_data.level }\npublic fun get_pet_energy(pet: \u0026Pet): u8 { pet.stats.energy }\npublic fun get_pet_hunger(pet: \u0026Pet): u8 { pet.stats.hunger }\npublic fun get_pet_happiness(pet: \u0026Pet): u8 { pet.stats.happiness }\n\npublic fun get_pet_stats(pet: \u0026Pet): (u8, u8, u8) {\n    (pet.stats.energy, pet.stats.hunger, pet.stats.happiness)\n}\npublic fun get_pet_game_data(pet: \u0026Pet): (u64, u64, u8) {\n    (pet.game_data.coins, pet.game_data.experience, pet.game_data.level)\n}\n\npublic fun is_sleeping(pet: \u0026Pet): bool {\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key)\n}\n```\n\n## Step 16: Test Function\n\nTambahkan fungsi untuk testing:\n\n```move\n// === Test-Only Functions ===\n#[test_only]\npublic fun init_for_testing(ctx: \u0026mut TxContext) {\n    init(TAMAGOSUI {}, ctx);\n}\n```\n\n# ✅ Full Code Implementation\n\n```move\nmodule 0x0::tamagosui;\n\nuse std::string::{Self, String};\nuse sui::{clock::Clock, display, dynamic_field, event, package};\n\n// === Errors ===\nconst E_NOT_ENOUGH_COINS: u64 = 101;\nconst E_PET_NOT_HUNGRY: u64 = 102;\nconst E_PET_TOO_TIRED: u64 = 103;\nconst E_PET_TOO_HUNGRY: u64 = 104;\nconst E_ITEM_ALREADY_EQUIPPED: u64 = 105;\nconst E_NO_ITEM_EQUIPPED: u64 = 106;\nconst E_NOT_ENOUGH_EXP: u64 = 107;\nconst E_PET_IS_ASLEEP: u64 = 108;\nconst E_PET_IS_ALREADY_ASLEEP: u64 = 109;\n\n// === Constants ===\nconst PET_LEVEL_1_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreidkhjpthergw2tcg6u5r344shgi2cdg5afmhgpf5bv34vqfrr7hni\";\nconst PET_LEVEL_1_IMAGE_WITH_GLASSES_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreibizappmcjaq5a5metl27yc46co4kxewigq6zu22vovwvn5qfsbiu\";\nconst PET_LEVEL_2_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreia5tgsowzfu6mzjfcxagfpbkghfuho6y5ybetxh3wabwrc5ajmlpq\";\nconst PET_LEVEL_2_IMAGE_WITH_GLASSES_URL:vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreif5bkpnqyybq3aqgafqm72x4wfjwcuxk33vvykx44weqzuilop424\";\nconst PET_LEVEL_3_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreidnqerfwxuxkrdsztgflmg5jwuespdkrazl6qmk7ykfgmrfzvinoy\";\nconst PET_LEVEL_3_IMAGE_WITH_GLASSES_URL:vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreigs6r3rdupoji7pqmpwe76z7wysguzdlq43t3wqmzi2654ux5n6uu\";\nconst PET_SLEEP_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreihwofl5stihtzjixfhrtznd7zqkclfhmlshgsg7cbszzjqqpvf7ae\";\nconst ACCESSORY_GLASSES_IMAGE_URL: vector\u003cu8\u003e = b\"https://tan-kind-lizard-741.mypinata.cloud/ipfs/bafkreigyivmq45od3jkryryi3w6t5j65hcnfh5kgwpi2ex7llf2i6se7de\";\n\nconst EQUIPPED_ITEM_KEY: vector\u003cu8\u003e = b\"equipped_item\";\nconst SLEEP_STARTED_AT_KEY: vector\u003cu8\u003e = b\"sleep_started_at\";\n\n// === Game Balance ===\npublic struct GameBalance has copy, drop {\n    max_stat: u8,\n\n    // Feed settings\n    feed_coins_cost: u64,\n    feed_experience_gain: u64,\n    feed_hunger_gain: u8,\n\n    // Play settings\n    play_energy_loss: u8,\n    play_hunger_loss: u8,\n    play_experience_gain: u64,\n    play_happiness_gain: u8,\n\n    // Work settings\n    work_energy_loss: u8,\n    work_happiness_loss: u8,\n    work_hunger_loss: u8,\n    work_coins_gain: u64,\n    work_experience_gain: u64,\n\n    // Sleep settings (in milliseconds)\n    sleep_energy_gain_ms: u64,\n    sleep_happiness_loss_ms: u64,\n    sleep_hunger_loss_ms: u64,\n\n    // Level settings\n    exp_per_level: u64,\n}\n\nfun get_game_balance(): GameBalance {\n    GameBalance {\n        max_stat: 100,\n\n        // Feed\n        feed_coins_cost: 5,\n        feed_experience_gain: 5,\n        feed_hunger_gain: 20,\n\n        // Play\n        play_energy_loss: 15,\n        play_hunger_loss: 15,\n        play_experience_gain: 10,\n        play_happiness_gain: 25,\n\n        // Work\n        work_energy_loss: 20,\n        work_hunger_loss: 20,\n        work_happiness_loss: 20,\n        work_coins_gain: 10,\n        work_experience_gain: 15,\n\n        // Sleep (rates per millisecond)\n        sleep_energy_gain_ms: 1000,    // 1 energy per second\n        sleep_happiness_loss_ms: 700, // 1 happiness loss per 0.7 seconds\n        sleep_hunger_loss_ms: 500,    // 1 hunger loss per 0.5 seconds\n\n        // Level\n        exp_per_level: 100,\n    }\n}\n\npublic struct TAMAGOSUI has drop {}\n\npublic struct Pet has key, store {\n    id: UID,\n    name: String,\n    image_url: String,\n    adopted_at: u64,\n    stats: PetStats,\n    game_data: PetGameData,\n}\n\npublic struct PetAccessory has key, store {\n    id: UID,\n    name: String,\n    image_url: String\n}\n\npublic struct PetStats has store {\n    energy: u8,\n    happiness: u8,\n    hunger: u8,\n}\n\npublic struct PetGameData has store {\n    coins: u64,\n    experience: u64,\n    level: u8,\n}\n\n// === Events ===\n\npublic struct PetAdopted has copy, drop {\n    pet_id: ID,\n    name: String,\n    adopted_at: u64\n}\npublic struct PetAction has copy, drop {\n    pet_id: ID,\n    action: String,\n    energy: u8,\n    happiness: u8,\n    hunger: u8\n}\n\nfun init(witness: TAMAGOSUI, ctx: \u0026mut TxContext) {\n    let publisher = package::claim(witness, ctx);\n\n    let pet_keys = vector[\n        string::utf8(b\"name\"),\n        string::utf8(b\"image_url\"),\n        string::utf8(b\"birth_date\"),\n        string::utf8(b\"experience\"),\n        string::utf8(b\"level\"),\n    ];\n\n    let pet_values = vector[\n        string::utf8(b\"{name}\"),\n        string::utf8(b\"{image_url}\"),\n        string::utf8(b\"{adopted_at}\"),\n        string::utf8(b\"{game_data.experience}\"),\n        string::utf8(b\"{game_data.level}\"),\n    ];\n\n    let mut pet_display = display::new_with_fields\u003cPet\u003e(\u0026publisher, pet_keys, pet_values, ctx);\n    pet_display.update_version();\n    transfer::public_transfer(pet_display, ctx.sender());\n\n    let accessory_keys = vector[\n        string::utf8(b\"name\"),\n        string::utf8(b\"image_url\")\n    ];\n    let accessory_values = vector[\n        string::utf8(b\"{name}\"),\n        string::utf8(b\"{image_url}\")\n    ];\n    let mut accessory_display = display::new_with_fields\u003cPetAccessory\u003e(\u0026publisher, accessory_keys, accessory_values, ctx);\n    accessory_display.update_version();\n    transfer::public_transfer(accessory_display, ctx.sender());\n\n    transfer::public_transfer(publisher, ctx.sender());\n}\n\npublic entry fun adopt_pet(\n    name: String,\n    clock: \u0026Clock,\n    ctx: \u0026mut TxContext\n) {\n    let current_time = clock.timestamp_ms();\n\n    let pet_stats = PetStats {\n        energy: 60,\n        happiness: 50,\n        hunger: 40,\n    };\n\n    let pet_game_data = PetGameData {\n        coins: 20,\n        experience: 0,\n        level: 1\n    };\n\n    let pet = Pet {\n        id: object::new(ctx),\n        name,\n        image_url: string::utf8(PET_LEVEL_1_IMAGE_URL),\n        adopted_at: current_time,\n        stats: pet_stats,\n        game_data: pet_game_data\n    };\n\n    let pet_id = object::id(\u0026pet);\n\n    event::emit(PetAdopted {\n        pet_id: pet_id,\n        name: pet.name,\n        adopted_at: pet.adopted_at\n    });\n\n    transfer::public_transfer(pet, ctx.sender());\n}\n\npublic entry fun feed_pet(pet: \u0026mut Pet) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let gb = get_game_balance();\n\n    assert!(pet.stats.hunger \u003c gb.max_stat, E_PET_NOT_HUNGRY);\n    assert!(pet.game_data.coins \u003e= gb.feed_coins_cost, E_NOT_ENOUGH_COINS);\n\n    pet.game_data.coins = pet.game_data.coins - gb.feed_coins_cost;\n    pet.game_data.experience = pet.game_data.experience + gb.feed_experience_gain;\n    pet.stats.hunger = if (pet.stats.hunger + gb.feed_hunger_gain \u003e gb.max_stat)\n        gb.max_stat\n    else\n        pet.stats.hunger + gb.feed_hunger_gain;\n\n    emit_action(pet, b\"fed\");\n}\n\npublic entry fun play_with_pet(pet: \u0026mut Pet) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let gb = get_game_balance();\n    assert!(pet.stats.energy \u003e= gb.play_energy_loss, E_PET_TOO_TIRED);\n    assert!(pet.stats.hunger \u003e= gb.play_hunger_loss, E_PET_TOO_HUNGRY);\n\n    pet.stats.energy = pet.stats.energy - gb.play_energy_loss;\n    pet.stats.hunger = pet.stats.hunger - gb.play_hunger_loss;\n    pet.game_data.experience = pet.game_data.experience + gb.play_experience_gain;\n    pet.stats.happiness = if (pet.stats.happiness + gb.play_happiness_gain \u003e gb.max_stat)\n        gb.max_stat\n    else\n        pet.stats.happiness + gb.play_happiness_gain;\n\n    emit_action(pet, b\"played\");\n}\n\npublic entry fun work_for_coins(pet: \u0026mut Pet) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let gb = get_game_balance();\n\n    assert!(pet.stats.energy \u003e= gb.work_energy_loss, E_PET_TOO_TIRED);\n    assert!(pet.stats.happiness \u003e= gb.work_happiness_loss, E_PET_NOT_HUNGRY);\n    assert!(pet.stats.hunger \u003e= gb.work_hunger_loss, E_PET_TOO_HUNGRY);\n\n    pet.stats.energy = if (pet.stats.energy \u003e= gb.work_energy_loss)\n        pet.stats.energy - gb.work_energy_loss\n    else\n        0;\n    pet.stats.happiness = if (pet.stats.happiness \u003e= gb.work_happiness_loss)\n        pet.stats.happiness - gb.work_happiness_loss\n    else\n        0;\n    pet.stats.hunger = if (pet.stats.hunger \u003e= gb.work_hunger_loss)\n        pet.stats.hunger - gb.work_hunger_loss\n    else\n        0;\n    pet.game_data.coins = pet.game_data.coins + gb.work_coins_gain;\n    pet.game_data.experience = pet.game_data.experience + gb.work_experience_gain;\n\n    emit_action(pet, b\"worked\");\n}\n\npublic entry fun let_pet_sleep(pet: \u0026mut Pet, clock: \u0026Clock) {\n    assert!(!is_sleeping(pet), E_PET_IS_ALREADY_ASLEEP);\n\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    dynamic_field::add(\u0026mut pet.id, key, clock.timestamp_ms());\n\n    pet.image_url = string::utf8(PET_SLEEP_IMAGE_URL);\n\n    emit_action(pet, b\"started_sleeping\");\n}\n\npublic entry fun wake_up_pet(pet: \u0026mut Pet, clock: \u0026Clock) {\n    assert!(is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    let sleep_started_at: u64 = dynamic_field::remove\u003cString, u64\u003e(\u0026mut pet.id, key);\n    let duration_ms = clock.timestamp_ms() - sleep_started_at;\n\n    let gb = get_game_balance();\n\n    // Calculate energy gained\n    let energy_gained_u64 = duration_ms / gb.sleep_energy_gain_ms;\n    // Cap energy gain to max_stat\n    let energy_gained = if (energy_gained_u64 \u003e (gb.max_stat as u64)) {\n        gb.max_stat\n    } else {\n        (energy_gained_u64 as u8)\n    };\n    pet.stats.energy = if (pet.stats.energy + energy_gained \u003e gb.max_stat) gb.max_stat else pet.stats.energy + energy_gained;\n\n    // Calculate happiness lost\n    let happiness_lost_u64 = duration_ms / gb.sleep_happiness_loss_ms;\n    let happiness_lost = if (happiness_lost_u64 \u003e (gb.max_stat as u64)) {\n        gb.max_stat\n    } else {\n        (happiness_lost_u64 as u8)\n    };\n    pet.stats.happiness = if (pet.stats.happiness \u003e happiness_lost) pet.stats.happiness - happiness_lost else 0;\n\n    // Calculate hunger lost\n    let hunger_lost_u64 = duration_ms / gb.sleep_hunger_loss_ms;\n    let hunger_lost = if (hunger_lost_u64 \u003e (gb.max_stat as u64)) {\n        gb.max_stat\n    } else {\n        (hunger_lost_u64 as u8)\n    };\n    pet.stats.hunger = if (pet.stats.hunger \u003e hunger_lost) pet.stats.hunger - hunger_lost else 0;\n\n    update_pet_image(pet);\n\n    emit_action(pet, b\"woke_up\");\n}\n\n\npublic entry fun check_and_level_up(pet: \u0026mut Pet) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let gb = get_game_balance();\n\n    // Calculate required exp: level * exp_per_level\n    let required_exp = (pet.game_data.level as u64) * gb.exp_per_level;\n    assert!(pet.game_data.experience \u003e= required_exp, E_NOT_ENOUGH_EXP);\n\n    // Level up\n    pet.game_data.level = pet.game_data.level + 1;\n    pet.game_data.experience = pet.game_data.experience - required_exp;\n\n    // Update image based on level and equipped accessory\n    update_pet_image(pet);\n\n    emit_action(pet, b\"leveled_up\")\n}\n\npublic entry fun mint_accessory(ctx: \u0026mut TxContext) {\n    let accessory = PetAccessory {\n        id: object::new(ctx),\n        name: string::utf8(b\"cool glasses\"),\n        image_url: string::utf8(ACCESSORY_GLASSES_IMAGE_URL)\n    };\n    transfer::public_transfer(accessory, ctx.sender());\n}\n\npublic entry fun equip_accessory(pet: \u0026mut Pet, accessory: PetAccessory) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let key = string::utf8(EQUIPPED_ITEM_KEY);\n    assert!(!dynamic_field::exists_\u003cString\u003e(\u0026pet.id, copy key), E_ITEM_ALREADY_EQUIPPED);\n\n    // Add accessory to pet\n    dynamic_field::add(\u0026mut pet.id, key, accessory);\n    // Update image\n    update_pet_image(pet);\n    emit_action(pet, b\"equipped_item\");\n}\n\npublic entry fun unequip_accessory(pet: \u0026mut Pet, ctx: \u0026mut TxContext) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let key = string::utf8(EQUIPPED_ITEM_KEY);\n    assert!(dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key), E_NO_ITEM_EQUIPPED);\n\n    // Remove accessory\n    let accessory: PetAccessory = dynamic_field::remove\u003cString, PetAccessory\u003e(\u0026mut pet.id, key);\n    // Update image\n    update_pet_image(pet);\n\n    transfer::transfer(accessory, ctx.sender());\n    emit_action(pet, b\"unequipped_item\");\n}\n\n// === Helper Functions ===\nfun emit_action(pet: \u0026Pet, action: vector\u003cu8\u003e) {\n    event::emit(PetAction {\n        pet_id: object::id(pet),\n        action: string::utf8(action),\n        energy: pet.stats.energy,\n        happiness: pet.stats.happiness,\n        hunger: pet.stats.hunger,\n    });\n}\n\nfun update_pet_image(pet: \u0026mut Pet) {\n    let key = string::utf8(EQUIPPED_ITEM_KEY);\n    let has_accessory = dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key);\n\n    if (pet.game_data.level == 1) {\n        if (has_accessory) {\n            pet.image_url = string::utf8(PET_LEVEL_1_IMAGE_WITH_GLASSES_URL);\n        } else {\n            pet.image_url = string::utf8(PET_LEVEL_1_IMAGE_URL);\n        }\n    } else if (pet.game_data.level == 2) {\n        if (has_accessory) {\n            pet.image_url = string::utf8(PET_LEVEL_2_IMAGE_WITH_GLASSES_URL);\n        } else {\n            pet.image_url = string::utf8(PET_LEVEL_2_IMAGE_URL);\n        }\n    } else if (pet.game_data.level \u003e= 3) {\n        if (has_accessory) {\n            pet.image_url = string::utf8(PET_LEVEL_3_IMAGE_WITH_GLASSES_URL);\n        } else {\n            pet.image_url = string::utf8(PET_LEVEL_3_IMAGE_URL);\n        }\n    };\n}\n\n// === View Functions ===\npublic fun get_pet_name(pet: \u0026Pet): String { pet.name }\npublic fun get_pet_adopted_at(pet: \u0026Pet): u64 { pet.adopted_at }\npublic fun get_pet_coins(pet: \u0026Pet): u64 { pet.game_data.coins }\npublic fun get_pet_experience(pet: \u0026Pet): u64 { pet.game_data.experience }\npublic fun get_pet_level(pet: \u0026Pet): u8 { pet.game_data.level }\npublic fun get_pet_energy(pet: \u0026Pet): u8 { pet.stats.energy }\npublic fun get_pet_hunger(pet: \u0026Pet): u8 { pet.stats.hunger }\npublic fun get_pet_happiness(pet: \u0026Pet): u8 { pet.stats.happiness }\n\npublic fun get_pet_stats(pet: \u0026Pet): (u8, u8, u8) {\n    (pet.stats.energy, pet.stats.hunger, pet.stats.happiness)\n}\npublic fun get_pet_game_data(pet: \u0026Pet): (u64, u64, u8) {\n    (pet.game_data.coins, pet.game_data.experience, pet.game_data.level)\n}\n\npublic fun is_sleeping(pet: \u0026Pet): bool {\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key)\n}\n\n// === Test-Only Functions ===\n#[test_only]\npublic fun init_for_testing(ctx: \u0026mut TxContext) {\n    init(TAMAGOSUI {}, ctx);\n}\n```\n\n## Step 17: Deploy dan Testing\n\n### Compile Contract:\n\n```bash\nsui move build\n```\n\n### Deploy ke Testnet:\n\n```bash\nsui client publish\n```\n\n### Test Functions:\n\n```bash\n# Adopt pet\nsui client call --function adopt_pet --module tamagosui --package [PACKAGE_ID] --args \"My Pet\" [CLOCK_ID]\n\n# Feed pet\nsui client call --function feed_pet --module tamagosui --package [PACKAGE_ID] --args [PET_ID]\n```\n\n# 🧠 Konsep Move Lanjutan dalam Tamagosui\n\n### 1. Model Penyimpanan Object-Centric\n\nBerbeda dengan blockchain tradisional yang menggunakan global storage, Sui menggunakan object-centric storage. Kontrak Tamagosui mendemonstrasikan konsep ini:\n\n**Blockchain Tradisional (Account-Based):**\n\n```solidity\n// Ethereum Style - global mapping\nmapping(address =\u003e Pet) public pets;\nmapping(uint256 =\u003e Item) public items;\n```\n\n**Sui Object-Centric:**\n\n```move\n// Segala sesuatu adalah objek dengan ID unik (wajib memiliki ability key dengan field id: UID )\npublic struct Pet has key, store {\n    id: UID,\n    name: String,\n    image_url: String,\n    adopted_at: u64,\n    stats: PetStats,\n    game_data: PetGameData,\n}\n\npublic struct PetAccessory has key, store {\n    id: UID,\n    name: String,\n    image_url: String\n}\n```\n\n**Keuntungan Utama:**\n\n- **Tidak Ada Konflik Global State:** Setiap pet dapat diproses secara independen\n- **Ownership yang Jelas:** Setiap pet milik owner tertentu\n- **Type Safety:** Pet dan aksesori memiliki tipe yang kuat\n- **Akses Efisien:** Akses langsung ke pet tanpa pencarian global state\n\n\u003e **References:**\n\u003e\n\u003e - [Sui Objects - Object Model](https://docs.sui.io/concepts/object-model)\n\n### 2. Shared Objects vs Owned Objects\n\nMemahami ownership objek sangat penting untuk aplikasi gaming:\n\n```move\n// Owned object - hanya owner yang bisa berinteraksi (tidak perlu konsensus)\ntransfer::public_transfer(pet, ctx.sender());\n\n// Di Tamagosui, pet adalah owned object - hanya owner yang bisa:\n// - Memberi makan pet\n// - Bermain dengan pet\n// - Menyuruh pet bekerja\n// - Memasang/melepas aksesori\n```\n\n**Kapan Menggunakan Masing-masing type object:**\n\n- **Owned:** Pet individual, aksesori, profil user (seperti di Tamagosui)\n- **Shared:** Leaderboard game, marketplace, sistem multi-user\n- **Immutable:** Konfigurasi game, gambar evolusi pet\n\n\u003e **References:**\n\u003e\n\u003e - [Sui Objects - Object Ownership ](https://docs.sui.io/concepts/object-ownership)\n\u003e - [Sui Objects - Object Ownership 2](https://docs.sui.io/guides/developer/sui-101/object-ownership)\n\u003e - [Move Book - Ownership](https://move-book.com/object/ownership/)\n\u003e - [Move Book - Resources](https://move-book.com/reference/structs)\n\u003e - [Sui Objects - Transfer](https://docs.sui.io/concepts/transfers)\n\n### 3. Dynamic Fields: Kustomisasi Pet Tanpa Batas\n\nDynamic fields memungkinkan penyimpanan data tanpa batas tanpa mengetahui nama field saat compile time. Tamagosui menggunakan ini untuk:\n\n```move\nuse sui::dynamic_field as df;\n\nconst EQUIPPED_ITEM_KEY: vector\u003cu8\u003e = b\"equipped_item\";\nconst SLEEP_STARTED_AT_KEY: vector\u003cu8\u003e = b\"sleep_started_at\";\n\n// Simpan aksesori di dalam pet tanpa mengubah struct Pet\npublic entry fun equip_accessory(pet: \u0026mut Pet, accessory: PetAccessory) {\n    let key = string::utf8(EQUIPPED_ITEM_KEY);\n    dynamic_field::add(\u0026mut pet.id, key, accessory);\n    update_pet_image(pet); // Mengubah tampilan berdasarkan item yang dipasang\n}\n\n// Simpan timestamp tidur untuk kalkulasi durasi\npublic entry fun let_pet_sleep(pet: \u0026mut Pet, clock: \u0026Clock) {\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    dynamic_field::add(\u0026mut pet.id, key, clock.timestamp_ms());\n}\n\n// Cek apakah pet memiliki aksesori terpasang\nfun has_accessory_equipped(pet: \u0026Pet): bool {\n    let key = string::utf8(EQUIPPED_ITEM_KEY);\n    dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key)\n}\n```\n\n**Kasus Penggunaan Tamagosui:**\n\n- **Accessories:** Simpan kacamata, topi, atau item lain di dalam pet\n- **Status Tidur:** Lacak kapan pet mulai tidur untuk recovery energy\n- **Ekstensibilitas Masa Depan:** Tambah fitur pet baru tanpa mengubah struct inti\n\n\u003e **References:**\n\u003e\n\u003e - [Sui Dynamic Fields Concept](https://docs.sui.io/concepts/dynamic-fields)\n\u003e - [Move Book - Dynamic Object Fields](https://move-book.com/programmability/dynamic-object-fields/)\n\n### 4. Clock Object: Mekanik Pet Berbasis Waktu\n\nSui memiliki Clock object, semacam \"jam global\" on-chain yang bisa digunakan semua orang untuk handle hal-hal yang butuh timing:\n\n```move\nuse sui::clock::{Self, Clock};\n\n// Adopsi pet dengan timestamp\npublic entry fun adopt_pet(\n    name: String,\n    clock: \u0026Clock,\n    ctx: \u0026mut TxContext\n) {\n    let current_time = clock.timestamp_ms();\n\n    let pet = Pet {\n        id: object::new(ctx),\n        name,\n        image_url: string::utf8(PET_LEVEL_1_IMAGE_URL),\n        adopted_at: current_time, // Catat kapan pet diadopsi\n        stats: PetStats { energy: 60, happiness: 50, hunger: 40 },\n        game_data: PetGameData { coins: 20, experience: 0, level: 1 }\n    };\n\n    transfer::public_transfer(pet, ctx.sender());\n}\n\n// Sistem tidur dengan recovery berbasis durasi\npublic entry fun wake_up_pet(pet: \u0026mut Pet, clock: \u0026Clock) {\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    let sleep_started_at: u64 = dynamic_field::remove\u003cString, u64\u003e(\u0026mut pet.id, key);\n    let duration_ms = clock.timestamp_ms() - sleep_started_at;\n\n    let gb = get_game_balance();\n\n    // Hitung energy yang didapat berdasarkan durasi tidur\n    let energy_gained = duration_ms / gb.sleep_energy_gain_ms; // 1 energy per detik\n    pet.stats.energy = if (pet.stats.energy + energy_gained \u003e gb.max_stat)\n        gb.max_stat\n    else\n        pet.stats.energy + energy_gained;\n\n    // Juga hitung kehilangan happiness dan hunger selama tidur\n    // Ini menciptakan mekanik perawatan pet yang realistis\n}\n```\n\n**Properti Clock Object:**\n\n- **Singleton object** dengan ID 0x6\n- **Akses read-only** ke timestamp saat ini\n- **Waktu berbasis konsensus** (bukan waktu mesin lokal)\n- **Presisi milidetik** untuk mekanik game yang akurat\n\n**Pola Berbasis Waktu Tamagosui:**\n\n- **Adopsi Pet:** Catat waktu adopsi yang tepat\n- **Mekanik Tidur:** Recovery energy seiring waktu\n- **Decay Stat:** Pet menjadi lapar/lelah seiring waktu (fitur masa depan)\n- **Evolusi:** Pertumbuhan pet berbasis waktu (fitur masa depan)\n\n\u003e **References:**\n\u003e\n\u003e - [Clock Module Reference](https://docs.sui.io/references/framework/sui_sui/clock)\n\u003e - [Move Book - Time in Sui](https://move-book.com/programmability/epoch-and-time/#time)\n\n### 5. Module Initializers: Setup Game Satu Kali\n\nFungsi init berjalan tepat sekali ketika module Tamagosui dipublish:\n\n```move\npublic struct TAMAGOSUI has drop {} // One-Time Witness\n\nfun init(witness: TAMAGOSUI, ctx: \u0026mut TxContext) {\n    let publisher = package::claim(witness, ctx);\n\n    // Setup bagaimana pet muncul di wallet dan marketplace\n    let pet_keys = vector[\n        string::utf8(b\"name\"),\n        string::utf8(b\"image_url\"),\n        string::utf8(b\"birth_date\"),\n        string::utf8(b\"experience\"),\n        string::utf8(b\"level\"),\n    ];\n\n    let pet_values = vector[\n        string::utf8(b\"{name}\"),\n        string::utf8(b\"{image_url}\"),\n        string::utf8(b\"{adopted_at}\"),\n        string::utf8(b\"{game_data.experience}\"),\n        string::utf8(b\"{game_data.level}\"),\n    ];\n\n    let mut pet_display = display::new_with_fields\u003cPet\u003e(\u0026publisher, pet_keys, pet_values, ctx);\n    pet_display.update_version();\n    transfer::public_transfer(pet_display, ctx.sender());\n\n    // Setup display aksesori\n    let accessory_keys = vector[\n        string::utf8(b\"name\"),\n        string::utf8(b\"image_url\")\n    ];\n    let accessory_values = vector[\n        string::utf8(b\"{name}\"),\n        string::utf8(b\"{image_url}\")\n    ];\n    let mut accessory_display = display::new_with_fields\u003cPetAccessory\u003e(\u0026publisher, accessory_keys, accessory_values, ctx);\n    accessory_display.update_version();\n    transfer::public_transfer(accessory_display, ctx.sender());\n\n    transfer::public_transfer(publisher, ctx.sender());\n}\n```\n\n\u003e **References:**\n\u003e\n\u003e - [Move Book - Module Initializer](https://move-book.com/programmability/module-initializer/)\n\u003e - [Sui Module Initializer](https://docs.sui.io/concepts/sui-move-concepts#module-initializers)\n\u003e - [Move Book - One-Time Witness](https://move-book.com/programmability/one-time-witness/)\n\u003e - [Module Publishing](https://move-book.com/your-first-move/hello-sui/#publish)\n\n### 6. Entry Functions vs Public Functions dalam Gaming\n\nMemahami visibility fungsi untuk mekanik game:\n\n```move\n// Entry function - pemain panggil langsung dari wallet/CLI\npublic entry fun feed_pet(pet: \u0026mut Pet) {\n    assert!(!is_sleeping(pet), E_PET_IS_ASLEEP);\n\n    let gb = get_game_balance();\n    assert!(pet.stats.hunger \u003c gb.max_stat, E_PET_NOT_HUNGRY);\n    assert!(pet.game_data.coins \u003e= gb.feed_coins_cost, E_NOT_ENOUGH_COINS);\n\n    // Kurangi koin dan tingkatkan hunger\n    pet.game_data.coins = pet.game_data.coins - gb.feed_coins_cost;\n    pet.stats.hunger = if (pet.stats.hunger + gb.feed_hunger_gain \u003e gb.max_stat)\n        gb.max_stat else pet.stats.hunger + gb.feed_hunger_gain;\n\n    emit_action(pet, b\"fed\");\n}\n\n// Public function - modul lain bisa panggil dan dapat return value\npublic fun get_pet_stats(pet: \u0026Pet): (u8, u8, u8) {\n    (pet.stats.energy, pet.stats.hunger, pet.stats.happiness)\n}\n\npublic fun is_sleeping(pet: \u0026Pet): bool {\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key)\n}\n\n// Internal function - hanya dalam modul ini\nfun update_pet_image(pet: \u0026mut Pet) {\n    let key = string::utf8(EQUIPPED_ITEM_KEY);\n    let has_accessory = dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key);\n\n    // Update gambar berdasarkan level dan aksesori terpasang\n    if (pet.game_data.level == 1) {\n        if (has_accessory) {\n            pet.image_url = string::utf8(PET_LEVEL_1_IMAGE_WITH_GLASSES_URL);\n        } else {\n            pet.image_url = string::utf8(PET_LEVEL_1_IMAGE_URL);\n        }\n    }\n    // ... logic level lainnya\n}\n```\n\n\u003e **References:**\n\u003e\n\u003e - [Move Book - Function Visibility](https://move-book.com/move-basics/visibility/)\n\u003e - [Sui Move - Entry Functions](https://docs.sui.io/concepts/sui-move-concepts#entry-functions)\n\n### 7. Teknik Optimasi Gas untuk Gaming\n\n**1. Struktur Data Efisien:**\n\n```move\n// Good: Organisir stat dalam nested struct\npublic struct Pet has key, store {\n    id: UID,\n    name: String,\n    image_url: String,\n    adopted_at: u64,\n    stats: PetStats,        // Kelompokkan data terkait\n    game_data: PetGameData, // Kelompokkan data terkait\n}\n\npublic struct PetStats has store {\n    energy: u8,     // Gunakan tipe terkecil yang memungkinkan\n    happiness: u8,\n    hunger: u8,\n}\n\n// Daripada field terpisah tersebar di struct Pet\n```\n\n**2. Konfigurasi Game Terpusat:**\n\n```move\npublic struct GameBalance has copy, drop {\n    max_stat: u8,\n    feed_coins_cost: u64,\n    feed_experience_gain: u64,\n    feed_hunger_gain: u8,\n    // ... semua parameter game di satu tempat\n}\n\nfun get_game_balance(): GameBalance {\n    GameBalance {\n        max_stat: 100,\n        feed_coins_cost: 5,\n        feed_experience_gain: 5,\n        // ... inisialisasi sekali per pemanggilan fungsi\n    }\n}\n```\n\n**3. Operasi Batch (Next Update):**\n\n```move\n// Daripada aksi pet individual\npublic entry fun batch_feed_pets(pets: vector\u003c\u0026mut Pet\u003e) {\n    let gb = get_game_balance(); // Hitung sekali\n    let mut i = 0;\n    while (i \u003c vector::length(\u0026pets)) {\n        let pet = vector::borrow_mut(\u0026mut pets, i);\n        // Feed setiap pet menggunakan game balance yang sama\n        i = i + 1;\n    };\n}\n```\n\n\u003e **References:**\n\u003e\n\u003e - [Sui Gas Pricing](https://docs.sui.io/concepts/sui-move-concepts#entry-functions)\n\n## ✅ Latihan: Memahami Smart Contract Tamagosui (90 menit)\n\n### Langkah 1: Analisis Struktur Kontrak (20 menit)\n\nPeriksa komponen utama kontrak Tamagosui:\n\n```move\nmodule 0x0::tamagosui;\n\n// === Konstanta Error ===\nconst E_NOT_ENOUGH_COINS: u64 = 101;\nconst E_PET_NOT_HUNGRY: u64 = 102;\n// ... kode error lainnya\n\n// === Struct Inti ===\npublic struct Pet has key, store {\n    id: UID,\n    name: String,\n    image_url: String,\n    adopted_at: u64,\n    stats: PetStats,\n    game_data: PetGameData,\n}\n\n// === Konfigurasi Game Balance ===\npublic struct GameBalance has copy, drop {\n    max_stat: u8,\n    feed_coins_cost: u64,\n    // ... semua parameter game\n}\n```\n\n**Pertanyaan untuk Eksplorasi:**\n\n1. Mengapa kode error didefinisikan sebagai konstanta?\n2. Ability apa yang dimiliki struct `Pet` dan mengapa?\n3. Bagaimana struct `GameBalance` membantu maintainability?\n\n### Langkah 2: Memahami Siklus Hidup Pet (25 menit)\n\nTelusuri siklus hidup pet yang lengkap:\n\n```move\n// 1. Adopsi Pet\npublic entry fun adopt_pet(name: String, clock: \u0026Clock, ctx: \u0026mut TxContext)\n\n// 2. Aksi Perawatan Dasar\npublic entry fun feed_pet(pet: \u0026mut Pet)\npublic entry fun play_with_pet(pet: \u0026mut Pet)\npublic entry fun work_for_coins(pet: \u0026mut Pet)\n\n// 3. Sistem Tidur\npublic entry fun let_pet_sleep(pet: \u0026mut Pet, clock: \u0026Clock)\npublic entry fun wake_up_pet(pet: \u0026mut Pet, clock: \u0026Clock)\n\n// 4. Progression\npublic entry fun check_and_level_up(pet: \u0026mut Pet)\n\n// 5. Kustomisasi\npublic entry fun equip_accessory(pet: \u0026mut Pet, accessory: PetAccessory)\n```\n\n**Tugas Hands-on:**\n\n1. Deploy kontrak Tamagosui ke testnet\n2. Adopsi pet pertama kamu\n3. Berinteraksi dengan pet kamu (feed, play, work)\n4. Tidurkan pet dan bangunkan\n5. Level up pet kamu\n6. Mint dan pasang accessories\n\n### Langkah 3: Deep Dive Dynamic Fields (25 menit)\n\nEksplorasi bagaimana dynamic fields bekerja dalam praktik:\n\n```move\n// Periksa sistem tidur\nconst SLEEP_STARTED_AT_KEY: vector\u003cu8\u003e = b\"sleep_started_at\";\n\npublic entry fun let_pet_sleep(pet: \u0026mut Pet, clock: \u0026Clock) {\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    dynamic_field::add(\u0026mut pet.id, key, clock.timestamp_ms());\n    pet.image_url = string::utf8(PET_SLEEP_IMAGE_URL);\n}\n\npublic fun is_sleeping(pet: \u0026Pet): bool {\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    dynamic_field::exists_\u003cString\u003e(\u0026pet.id, key)\n}\n```\n\n**Eksperimen dengan:**\n\n1. Cek apakah pet sedang tidur menggunakan `is_sleeping()`\n2. Pahami bagaimana durasi tidur mempengaruhi recovery stat\n3. Eksplorasi sistem aksesori menggunakan dynamic fields\n\n### Langkah 4: Analisis Mekanik Berbasis Waktu (20 menit)\n\nPahami bagaimana Clock object memungkinkan mekanik game:\n\n```move\npublic entry fun wake_up_pet(pet: \u0026mut Pet, clock: \u0026Clock) {\n    let key = string::utf8(SLEEP_STARTED_AT_KEY);\n    let sleep_started_at: u64 = dynamic_field::remove\u003cString, u64\u003e(\u0026mut pet.id, key);\n    let duration_ms = clock.timestamp_ms() - sleep_started_at;\n\n    let gb = get_game_balance();\n\n    // Recovery energy: 1 poin per detik\n    let energy_gained = duration_ms / gb.sleep_energy_gain_ms;\n\n    // Decay happiness: 1 poin per 0.7 detik\n    let happiness_lost = duration_ms / gb.sleep_happiness_loss_ms;\n\n    // Decay hunger: 1 poin per 0.5 detik\n    let hunger_lost = duration_ms / gb.sleep_hunger_loss_ms;\n\n    // Aplikasikan perubahan yang dihitung dengan bounds checking\n    // ...\n}\n```\n\n**Skenario Test:**\n\n1. Tidurkan pet untuk durasi berbeda\n2. Observasi bagaimana stat berubah berdasarkan waktu tidur\n3. Hitung durasi tidur optimal untuk situasi berbeda\n\n## 🎮 Pola Gaming Lanjutan dalam Tamagosui\n\n### 1. Pola State Machine\n\n```move\n// Pet bisa dalam state berbeda yang mempengaruhi aksi yang tersedia\npub fun is_sleeping(pet: \u0026Pet): bool // State tidur\n// Aksi dibatasi berdasarkan state:\nassert!(!is_sleeping(pet), E_PET_IS_ASLEEP); // Di feed_pet(), play_with_pet(), dll.\n```\n\n### 2. Progressive Disclosure\n\n```move\n// Kemampuan pet terbuka berdasarkan level\nfun update_pet_image(pet: \u0026mut Pet) {\n    if (pet.game_data.level == 1) {\n        // Opsi tampilan Level 1\n    } else if (pet.game_data.level == 2) {\n        // Opsi tampilan Level 2\n    } else if (pet.game_data.level \u003e= 3) {\n        // Opsi tampilan Level 3+\n    }\n}\n```\n\n### 3. Resource Management\n\n```move\n// Multiple resource yang saling terkait\npublic struct PetStats has store {\n    energy: u8,    // Dikonsumsi oleh play dan work\n    happiness: u8, // Didapat dari play, hilang dari work dan sleep\n    hunger: u8,    // Hilang dari play dan work, didapat dari feeding\n}\n\n// Aksi memiliki multiple cost/benefit resource\npub entry fun play_with_pet(pet: \u0026mut Pet) {\n    pet.stats.energy = pet.stats.energy - gb.play_energy_loss;      // Butuh energy\n    pet.stats.hunger = pet.stats.hunger - gb.play_hunger_loss;      // Butuh hunger\n    pet.stats.happiness = pet.stats.happiness + gb.play_happiness_gain; // Dapat happiness\n    pet.game_data.experience = pet.game_data.experience + gb.play_experience_gain; // Dapat XP\n}\n```\n\n### 4. Event-Driven Architecture\n\n```move\n// Event memungkinkan pengalaman off-chain yang kaya\npublic struct PetAction has copy, drop {\n    pet_id: ID,\n    action: String,\n    energy: u8,\n    happiness: u8,\n    hunger: u8\n}\n\n// Diemit setelah setiap aksi untuk update UI dan analytics\nfun emit_action(pet: \u0026Pet, action: vector\u003cu8\u003e) {\n    event::emit(PetAction {\n        pet_id: object::id(pet),\n        action: string::utf8(action),\n        energy: pet.stats.energy,\n        happiness: pet.stats.happiness,\n        hunger: pet.stats.hunger,\n    });\n}\n```\n\n## 📚 Poin Penting untuk Pengembangan Gaming Move\n\n### 1. **Object Ownership Memungkinkan True Ownership**\n\n- Pemain benar-benar memiliki pet mereka sebagai objek Sui\n- Tidak ada otoritas pusat yang bisa mengambil atau memodifikasi pet\n- Pet bisa ditransfer, dijual, atau digunakan di game lain\n\n### 2. **Dynamic Fields Memungkinkan Ekstensibilitas**\n\n- Tambah fitur baru tanpa mengubah struct inti\n- Simpan aksesori, achievement, atau data custom\n- Desain game yang future-proof\n\n### 3. **Mekanik Berbasis Waktu Terasa Natural**\n\n- Waktu blockchain memungkinkan gameplay yang adil dan berbasis konsensus\n- Siklus tidur/bangun menciptakan pola interaksi harian yang engaging\n- Kalkulasi berbasis durasi reward pemikiran strategis\n\n### 4. **Event Memungkinkan Pengalaman Kaya**\n\n- Service off-chain bisa membangun UI kaya menggunakan data event\n- Analytics dan leaderboard menjadi mungkin\n- Fitur sosial bisa dibangun di sekitar aktivitas pet\n\n### 5. **Resource Management Menciptakan Depth**\n\n- Multiple stat yang saling terkait menciptakan pilihan meaningful\n- Pemain harus menyeimbangkan prioritas yang berkompetisi (energy vs happiness vs hunger)\n- Sistem ekonomi muncul secara natural (koin untuk feeding)\n\n## 🚀 Langkah Selanjutnya: Memperluas Tamagosui\n\n### Ekstensi Pemula\n\n1. **Aksi Pet Baru:** Tambah aksi \"exercise\", \"study\", atau \"rest\"\n2. **Lebih Banyak Aksesori:** Buat topi, pakaian, atau mainan\n3. **Kepribadian Pet:** Tambah trait yang mempengaruhi outcome aksi\n\n### Ekstensi Menengah\n\n1. **Evolusi Pet:** Transform pet pada level tertentu\n2. **Sistem Breeding:** Gabungkan dua pet untuk membuat keturunan\n3. **Marketplace:** Biarkan pemain trade pet dan aksesori\n\n### Ekstensi Lanjutan\n\n1. **Gameplay Multi-Pet:** Battle pet, race, atau kompetisi\n2. **Sistem Guild:** Pemain membentuk grup dengan tujuan bersama\n3. **Integrasi Cross-Game:** Gunakan pet di multiple game\n\n## 🧪 Uji Pemahaman\n\n### Pertanyaan Quiz\n\n1. Apa yang terjadi jika kamu menghapus ability `store` dari `PetStats`?\n2. Mengapa Clock object adalah shared object bukan owned object?\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdante4rt%2Ftamago-sui","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdante4rt%2Ftamago-sui","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdante4rt%2Ftamago-sui/lists"}