diff --git a/DEVELOPMENT_ROADMAP.md b/DEVELOPMENT_ROADMAP.md new file mode 100644 index 0000000..7361e0d --- /dev/null +++ b/DEVELOPMENT_ROADMAP.md @@ -0,0 +1,846 @@ +# Stevever Development Roadmap + +**Document Version**: 1.0 +**Created**: 2025-11-11 +**Status**: Active Development + +--- + +## Executive Summary + +This roadmap outlines a comprehensive development plan for the Stevever project, organized into 5 phases spanning approximately 6-8 months. Each phase builds upon the previous one, moving from foundational improvements to innovative features. + +**Current Project State**: 47 Java classes, functional AI agent system with basic mining, building, and combat capabilities. + +**Target State**: Production-ready AI agent system with full autonomy, persistent learning, multi-modal interaction, and advanced coordination. + +--- + +## Development Phases Overview + +| Phase | Focus Area | Duration | Priority | Dependencies | +|-------|-----------|----------|----------|--------------| +| **Phase 1** | Foundation | 1-2 months | CRITICAL | None | +| **Phase 2** | Intelligence | 1 month | HIGH | Phase 1 | +| **Phase 3** | Expansion | 1-2 months | MEDIUM | Phase 1-2 | +| **Phase 4** | Polish | 1 month | MEDIUM | Phase 1-3 | +| **Phase 5** | Innovation | Ongoing | LOW | Phase 1-4 | + +--- + +## Phase 1: Foundation (CRITICAL Priority) + +**Goal**: Establish core infrastructure for autonomous Steve operation + +### 1.1 Crafting System Implementation + +**Status**: 🔴 Not Started +**Estimated Time**: 5-7 days +**Priority**: CRITICAL + +#### Current State +- `CraftItemAction.java` is stub (placeholder) +- `RecipeHelper.java` has no implementation +- Steve can mine resources but cannot craft tools +- Severely limits autonomy (cannot progress beyond stone age) + +#### Requirements +- [ ] Implement RecipeHelper with Minecraft recipe registry integration +- [ ] Create crafting table detection/placement logic +- [ ] Implement inventory material checking +- [ ] Add crafting sequence planning (wood → planks → sticks → crafting table) +- [ ] Support shaped and shapeless recipes +- [ ] Implement furnace/smelting support +- [ ] Add tool durability awareness + +#### Implementation Checklist +```java +// Files to create/modify: +- src/main/java/com/steve/ai/util/RecipeHelper.java (complete implementation) +- src/main/java/com/steve/ai/action/actions/CraftItemAction.java (full implementation) +- src/main/java/com/steve/ai/util/InventoryHelper.java (prerequisite) +- src/main/java/com/steve/ai/action/actions/SmeltItemAction.java (new file) +- src/main/java/com/steve/ai/action/actions/PlaceCraftingTableAction.java (new file) +``` + +#### Success Criteria +- [ ] Steve can craft basic tools (pickaxe, axe, shovel) +- [ ] Steve can upgrade tools (wood → stone → iron → diamond) +- [ ] Steve can smelt ores (iron_ore → iron_ingot) +- [ ] Steve can plan multi-step crafting sequences +- [ ] LLM can request crafting via natural language ("craft diamond pickaxe") + +#### Technical Notes +- Use `RecipeManager.getRecipes()` for recipe lookup +- Handle recipe variants (different wood types for planks) +- Implement nearest crafting table search (16 block radius) +- Fallback: place crafting table if none found +- Tool selection: prefer higher tier tools when available + +--- + +### 1.2 Inventory Management System + +**Status**: 🔴 Not Started +**Estimated Time**: 4-6 days +**Priority**: CRITICAL + +#### Current State +- `InventoryHelper.java` is stub (all methods return false/null) +- No inventory fullness checking +- No chest interaction +- Items accumulate until inventory full + +#### Requirements +- [ ] Implement full inventory query/manipulation API +- [ ] Add chest detection and interaction +- [ ] Create automatic item sorting system +- [ ] Implement inventory fullness monitoring +- [ ] Add tool durability tracking +- [ ] Create item priority system (keep diamonds, discard dirt) + +#### Implementation Checklist +```java +// Files to create/modify: +- src/main/java/com/steve/ai/util/InventoryHelper.java (complete implementation) +- src/main/java/com/steve/ai/action/actions/StoreItemsAction.java (new file) +- src/main/java/com/steve/ai/action/actions/RetrieveItemsAction.java (new file) +- src/main/java/com/steve/ai/action/actions/PlaceChestAction.java (new file) +- src/main/java/com/steve/ai/memory/InventoryMemory.java (new file) +- src/main/java/com/steve/ai/util/ItemPriority.java (new file) +``` + +#### API Design +```java +public class InventoryHelper { + // Query methods + public static boolean hasItem(LivingEntity entity, Item item, int count); + public static int getItemCount(LivingEntity entity, Item item); + public static ItemStack findBestTool(LivingEntity entity, BlockState targetBlock); + public static float getInventoryFullness(LivingEntity entity); // 0.0 - 1.0 + + // Manipulation methods + public static boolean addItem(LivingEntity entity, ItemStack item); + public static boolean removeItem(LivingEntity entity, Item item, int count); + public static boolean transferToChest(LivingEntity entity, BlockPos chestPos, ItemStack item); + public static boolean retrieveFromChest(BlockPos chestPos, Item item, int count); + + // Tool management + public static boolean needsToolRepair(ItemStack tool); + public static ItemStack selectToolForBlock(Inventory inv, BlockState block); + + // Chest operations + public static BlockPos findNearestChest(Level level, BlockPos center, int radius); + public static boolean isInventoryFull(Container container); +} +``` + +#### Success Criteria +- [ ] Steve deposits items to chest when inventory >90% full +- [ ] Steve retrieves materials from chest when needed for crafting +- [ ] Broken tools automatically replaced from inventory +- [ ] Item priority system prevents valuable items from being dropped +- [ ] Chest placement when no storage available nearby + +--- + +### 1.3 Persistent Memory System + +**Status**: 🔴 Not Started +**Estimated Time**: 5-7 days +**Priority**: CRITICAL + +#### Current State +- Memory resets on world reload +- No cross-session learning +- Conversational history lost +- Steve forgets previous task outcomes + +#### Requirements +- [ ] Implement NBT-based memory persistence +- [ ] Create JSON serialization for complex memory structures +- [ ] Add episodic memory storage (important events) +- [ ] Implement memory loading on Steve spawn +- [ ] Create memory pruning/summarization (prevent file bloat) +- [ ] Add shared knowledge base for all Steves + +#### Implementation Checklist +```java +// Files to create/modify: +- src/main/java/com/steve/ai/memory/SteveMemory.java (enhance existing) +- src/main/java/com/steve/ai/memory/PersistentMemoryStore.java (new file) +- src/main/java/com/steve/ai/memory/EpisodicMemory.java (new file) +- src/main/java/com/steve/ai/memory/SharedKnowledgeBase.java (new file) +- src/main/java/com/steve/ai/memory/MemorySummarizer.java (new file) +``` + +#### Data Structure +```json +{ + "steveName": "Steve", + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "conversationalHistory": [ + { + "timestamp": 1699708800000, + "role": "user", + "content": "build a house" + }, + { + "timestamp": 1699708810000, + "role": "assistant", + "content": "Building house at (100, 64, 200)" + } + ], + "episodicMemory": [ + { + "timestamp": 1699708900000, + "event": "found_diamond", + "location": {"x": 50, "y": -59, "z": 150}, + "importance": 0.9 + } + ], + "skills": { + "mining": 5, + "building": 3, + "combat": 2 + }, + "knownLocations": { + "home": {"x": 0, "y": 64, "z": 0}, + "iron_mine": {"x": 120, "y": 50, "z": -80} + } +} +``` + +#### File Storage +``` +config/steve/ +├── memory/ +│ ├── steve_550e8400.json (per-Steve memory) +│ ├── alex_660e9500.json +│ └── shared_knowledge.json (global knowledge) +└── vector_store/ + └── embeddings.db (vector store index) +``` + +#### Success Criteria +- [ ] Memory persists across world reload +- [ ] Steve remembers previous task outcomes +- [ ] Important discoveries saved (diamond locations, villages, etc.) +- [ ] Memory file size <10MB per Steve (with pruning) +- [ ] Cross-session conversation continuity + +--- + +### 1.4 Real Vector Store Integration + +**Status**: 🔴 Not Started +**Estimated Time**: 7-10 days +**Priority**: HIGH + +#### Current State +- `VectorStore.java` uses hash-based fake embeddings +- No semantic similarity +- Cannot retrieve contextually relevant memories + +#### Requirements +- [ ] Research embedding options (OpenAI, Sentence-BERT, local models) +- [ ] Implement real embedding generation +- [ ] Create vector similarity search +- [ ] Integrate with episodic memory +- [ ] Add semantic query capability +- [ ] Optimize for Minecraft-specific vocabulary + +#### Implementation Options + +**Option 1: OpenAI Embeddings API** (Recommended for quick start) +- Model: text-embedding-3-small (1536 dimensions) +- Cost: $0.02 / 1M tokens +- Pros: High quality, no local setup +- Cons: API dependency, cost + +**Option 2: Sentence-BERT (Local)** (Recommended for production) +- Model: all-MiniLM-L6-v2 (384 dimensions) +- Cost: Free +- Pros: No API calls, fast inference +- Cons: Requires ONNX runtime or Python bridge + +**Option 3: Hybrid Approach** +- Use OpenAI for training/development +- Switch to local model for production + +#### Implementation Checklist +```java +// Files to create/modify: +- src/main/java/com/steve/ai/agent/VectorStore.java (complete rewrite) +- src/main/java/com/steve/ai/ai/EmbeddingClient.java (new file) +- src/main/java/com/steve/ai/ai/OpenAIEmbeddingClient.java (new file) +- src/main/java/com/steve/ai/ai/LocalEmbeddingClient.java (new file - optional) +- src/main/java/com/steve/ai/util/VectorMath.java (cosine similarity, etc.) +``` + +#### API Design +```java +public interface EmbeddingClient { + float[] generateEmbedding(String text); + List generateEmbeddings(List texts); // Batch support +} + +public class VectorStore { + private Map vectors; + private EmbeddingClient embeddingClient; + + public void addMemory(String id, String text, Map metadata); + public List search(String query, int topK); + public void saveToFile(String path); + public void loadFromFile(String path); +} + +public class SearchResult { + String id; + String text; + float similarity; + Map metadata; +} +``` + +#### Example Usage +```java +// Store memory +vectorStore.addMemory( + "event_001", + "Found diamonds at Y=-59 near coordinates (50, -59, 150)", + Map.of("type", "discovery", "importance", 0.9, "timestamp", System.currentTimeMillis()) +); + +// Semantic search +List results = vectorStore.search("where did I find diamonds?", 5); +// Returns: "Found diamonds at Y=-59 near coordinates (50, -59, 150)" with high similarity +``` + +#### Success Criteria +- [ ] Semantic search returns relevant memories +- [ ] "where did I mine iron?" retrieves iron mining locations +- [ ] Embedding generation <500ms per query +- [ ] Vector store persists to disk +- [ ] Supports 10K+ memories without degradation + +--- + +### 1.5 Async Action Execution + +**Status**: 🔴 Not Started +**Estimated Time**: 6-8 days +**Priority**: HIGH + +#### Current State +- Actions execute synchronously (one at a time) +- Cannot multitask (mining + following player simultaneously) +- Blocks other actions during long operations + +#### Requirements +- [ ] Implement action priority system +- [ ] Create background task queue +- [ ] Add action interruption mechanism +- [ ] Implement concurrent action execution +- [ ] Create action conflict detection +- [ ] Add resource locking (prevent conflicting actions) + +#### Implementation Checklist +```java +// Files to create/modify: +- src/main/java/com/steve/ai/action/ActionExecutor.java (major refactor) +- src/main/java/com/steve/ai/action/ActionPriority.java (new file) +- src/main/java/com/steve/ai/action/ActionScheduler.java (new file) +- src/main/java/com/steve/ai/action/ResourceLock.java (new file) +- src/main/java/com/steve/ai/action/actions/BaseAction.java (add priority field) +``` + +#### Architecture Design +```java +public enum ActionPriority { + CRITICAL(0), // Combat, danger avoidance + HIGH(1), // User commands + NORMAL(2), // Autonomous tasks + LOW(3), // Idle behavior + BACKGROUND(4); // Passive monitoring +} + +public class ActionScheduler { + // Priority queues for different action types + private Map> actionQueues; + + // Currently executing actions + private Set runningActions; + + // Resource locks (prevent conflicting actions) + private Map locks; + + public void scheduleAction(BaseAction action, ActionPriority priority); + public void tick(); + public void interruptAction(BaseAction action); + public boolean canExecute(BaseAction action); +} +``` + +#### Action Compatibility Matrix +``` + Mining Building Combat Pathfind Follow +Mining ❌ ❌ ✅ ❌ ❌ +Building ❌ ❌ ✅ ❌ ❌ +Combat ✅ ✅ ❌ ❌ ❌ +Pathfind ❌ ❌ ❌ ❌ ✅ +Follow ❌ ❌ ❌ ✅ ❌ +``` + +✅ = Can run simultaneously +❌ = Cannot run simultaneously + +#### Example Scenarios + +**Scenario 1: Background Mining + Combat** +```java +// Steve is mining iron (NORMAL priority, background task) +scheduler.scheduleAction(new MineBlockAction(...), ActionPriority.NORMAL); + +// Creeper detected! (CRITICAL priority) +scheduler.scheduleAction(new CombatAction(...), ActionPriority.CRITICAL); + +// Result: Mining paused, combat starts, mining resumes after combat +``` + +**Scenario 2: Building + Following Player** +```java +// Steve is building house (HIGH priority) +scheduler.scheduleAction(new BuildStructureAction(...), ActionPriority.HIGH); + +// Player moves away (LOW priority follow action) +scheduler.scheduleAction(new FollowPlayerAction(...), ActionPriority.LOW); + +// Result: Building continues, follow ignored (building has higher priority) +``` + +#### Success Criteria +- [ ] Steve can mine while monitoring for threats +- [ ] Combat interrupts current action immediately +- [ ] Multiple Steves can work on same structure without conflicts +- [ ] Action priority system prevents low-priority tasks from blocking urgent ones +- [ ] Resource locks prevent inventory corruption + +--- + +## Phase 2: Intelligence (HIGH Priority) + +**Goal**: Enhance decision-making and learning capabilities + +### 2.1 Advanced LLM Prompting + +**Status**: 🔴 Not Started +**Estimated Time**: 3-4 days +**Priority**: HIGH + +#### Current State +- Basic system prompt with JSON format +- No few-shot examples +- Limited error recovery + +#### Requirements +- [ ] Add Chain-of-Thought reasoning +- [ ] Implement few-shot examples +- [ ] Create error recovery prompts +- [ ] Add self-reflection capability +- [ ] Implement plan validation + +#### Enhanced Prompt Template +``` +You are an expert Minecraft AI agent with extensive experience. + +REASONING FRAMEWORK: +1. Analyze the situation (what resources/tools do I have?) +2. Identify requirements (what do I need to complete this task?) +3. Plan steps (what's the optimal sequence?) +4. Validate plan (are there any blockers or missing prerequisites?) + +EXAMPLE SUCCESS CASES: +[User]: "build a house" +[Thought]: "I need wood, cobblestone, and glass. Let me check inventory first." +[Plan]: "1. Check inventory 2. Gather missing materials 3. Place crafting table 4. Build structure" +[Tasks]: [{"action": "check_inventory"}, {"action": "mine", "parameters": {"block": "oak_log", "quantity": 32}}, ...] + +EXAMPLE FAILURE TO AVOID: +[User]: "craft diamond pickaxe" +[Wrong]: Immediately trying to craft without checking for diamonds +[Correct]: "I need 3 diamonds and 2 sticks. Let me check inventory and mine if needed." + +CURRENT SITUATION: +{contextual_info} + +USER COMMAND: +{user_command} + +YOUR RESPONSE (think step-by-step): +``` + +#### Success Criteria +- [ ] LLM provides reasoning before action +- [ ] Plans validated before execution +- [ ] Fewer invalid action sequences +- [ ] Better error messages to user + +--- + +### 2.2 Multi-Agent Team Coordination + +**Status**: 🔴 Not Started +**Estimated Time**: 5-6 days +**Priority**: HIGH + +#### Requirements +- [ ] Extend collaboration beyond building +- [ ] Implement role-based task assignment +- [ ] Create team communication protocol +- [ ] Add mining teams (digger + hauler) +- [ ] Implement combat teams (tank + DPS) + +#### Implementation +```java +public enum SteveRole { + MINER, BUILDER, FIGHTER, HAULER, SCOUT, LEADER +} + +public class TeamManager { + private Map roleAssignments; + private Map teams; + + public void assignRole(String steveName, SteveRole role); + public void createTeam(String teamName, List members); + public void coordinateTask(String teamName, TeamTask task); +} +``` + +--- + +### 2.3 Farming & Food System + +**Status**: 🔴 Not Started +**Estimated Time**: 4-5 days +**Priority**: MEDIUM + +#### Requirements +- [ ] Implement crop farming (wheat, carrots, potatoes) +- [ ] Add animal breeding +- [ ] Create hunger management +- [ ] Implement automatic replanting + +--- + +### 2.4 Error Recovery & Learning + +**Status**: 🔴 Not Started +**Estimated Time**: 4-5 days +**Priority**: HIGH + +#### Requirements +- [ ] Track failed actions +- [ ] Send error context to LLM +- [ ] Implement retry strategies +- [ ] Learn from mistakes + +--- + +## Phase 3: Expansion (MEDIUM Priority) + +**Goal**: Extend capabilities to all Minecraft dimensions and features + +### 3.1 Advanced Combat AI + +**Status**: 🔴 Not Started +**Estimated Time**: 5-6 days + +#### Requirements +- [ ] Shield usage +- [ ] Bow & arrow support +- [ ] Tactical retreat logic +- [ ] Armor auto-equipping +- [ ] Boss fight coordination + +--- + +### 3.2 Nether & End Support + +**Status**: 🔴 Not Started +**Estimated Time**: 7-10 days + +#### Requirements +- [ ] Portal building +- [ ] Nether navigation +- [ ] Fortress raiding +- [ ] Ender Dragon fight +- [ ] Elytra usage + +--- + +### 3.3 Redstone Mechanisms + +**Status**: 🔴 Not Started +**Estimated Time**: 6-8 days + +#### Requirements +- [ ] Basic circuits (doors, lights) +- [ ] Piston doors +- [ ] Automatic farms +- [ ] Minecart systems + +--- + +### 3.4 Quest & Achievement System + +**Status**: 🔴 Not Started +**Estimated Time**: 4-5 days + +#### Requirements +- [ ] Quest definitions +- [ ] Progress tracking +- [ ] Reward system +- [ ] Achievement unlocks + +--- + +## Phase 4: Polish (MEDIUM Priority) + +**Goal**: Improve code quality, testing, and user experience + +### 4.1 Unit & Integration Tests + +**Status**: 🔴 Not Started +**Estimated Time**: 7-10 days + +#### Requirements +- [ ] Set up JUnit + Mockito +- [ ] Test all action classes +- [ ] Test LLM parsing +- [ ] Integration tests for workflows + +--- + +### 4.2 Web Dashboard + +**Status**: 🔴 Not Started +**Estimated Time**: 10-14 days + +#### Requirements +- [ ] Embedded HTTP server +- [ ] Real-time Steve monitoring +- [ ] Task visualization +- [ ] LLM conversation logs + +--- + +### 4.3 Advanced GUI + +**Status**: 🔴 Not Started +**Estimated Time**: 5-7 days + +#### Requirements +- [ ] Status indicators +- [ ] Progress bars +- [ ] Minimap with Steve locations +- [ ] Task queue visualization + +--- + +### 4.4 Performance Optimization + +**Status**: 🔴 Not Started +**Estimated Time**: 5-6 days + +#### Requirements +- [ ] LLM response caching +- [ ] Pathfinding optimization +- [ ] WorldKnowledge caching +- [ ] Memory profiling + +--- + +## Phase 5: Innovation (LOW Priority) + +**Goal**: Experimental features and cutting-edge capabilities + +### 5.1 Multi-Modal LLM (Vision) + +**Status**: 🔴 Not Started +**Estimated Time**: 10-14 days + +#### Requirements +- [ ] Screenshot capture +- [ ] GPT-4 Vision integration +- [ ] Visual structure analysis +- [ ] Map understanding + +--- + +### 5.2 Voice Commands + +**Status**: 🔴 Not Started +**Estimated Time**: 5-7 days + +#### Requirements +- [ ] Whisper API integration +- [ ] Push-to-talk system +- [ ] TTS responses + +--- + +### 5.3 Emotion & Personality System + +**Status**: 🔴 Not Started +**Estimated Time**: 6-8 days + +#### Requirements +- [ ] Emotion state tracking +- [ ] Personality-based behavior +- [ ] Emotional responses in chat + +--- + +### 5.4 Meta-Learning (Steve Collective Intelligence) + +**Status**: 🔴 Not Started +**Estimated Time**: 8-10 days + +#### Requirements +- [ ] Shared knowledge base +- [ ] Discovery sharing +- [ ] Collective optimization + +--- + +## Development Guidelines + +### Code Quality Standards +- All new code must have Javadoc comments +- Follow existing code style (Google Java Style Guide) +- No warnings in build output +- All public methods must have null checks + +### Testing Requirements +- Unit tests for all utility classes +- Integration tests for action workflows +- Mock LLM responses for deterministic testing +- >70% code coverage target + +### Git Workflow +- Branch naming: `feature/{phase}-{task-name}` (e.g., `feature/phase1-crafting-system`) +- Commit messages: Follow Conventional Commits + - `feat: add crafting system` + - `fix: resolve inventory duplication bug` + - `refactor: optimize pathfinding cache` + - `docs: update API documentation` + +### Research Protocol +When uncertain about Minecraft APIs or best practices: +1. Check Minecraft Forge documentation +2. Search for examples in existing mods +3. Test in development environment before implementing +4. Document assumptions and limitations + +### LLM Integration Best Practices +- Always implement retry logic with exponential backoff +- Cache responses when possible (5-minute TTL) +- Validate JSON responses before parsing +- Include request IDs for debugging +- Monitor API quota usage + +--- + +## Success Metrics + +### Phase 1 Completion Criteria +- [ ] Steve can autonomously progress from wood to diamond tools +- [ ] Inventory never gets full (auto-storage) +- [ ] Memory persists across sessions +- [ ] Semantic search returns relevant results (>80% accuracy) +- [ ] Multiple actions can run simultaneously + +### Phase 2 Completion Criteria +- [ ] LLM plans are valid >95% of the time +- [ ] Teams of 3+ Steves coordinate efficiently +- [ ] Steve can sustain itself (food/hunger) +- [ ] Failed actions trigger intelligent retry + +### Phase 3 Completion Criteria +- [ ] Steve can defeat Ender Dragon with team +- [ ] Redstone contraptions function correctly +- [ ] Quest system has 10+ quests implemented + +### Phase 4 Completion Criteria +- [ ] >70% test coverage +- [ ] Web dashboard fully functional +- [ ] Zero critical performance issues +- [ ] User satisfaction >4/5 stars + +### Phase 5 Completion Criteria +- [ ] Vision-based structure copying works +- [ ] Voice commands have <10% error rate +- [ ] Emotion system creates engaging interactions +- [ ] Meta-learning demonstrates measurable improvement + +--- + +## Risk Management + +### High-Risk Areas + +**1. LLM API Reliability** +- **Risk**: API downtime, rate limits, cost overruns +- **Mitigation**: Multi-provider fallback, caching, local model option + +**2. Async Action Conflicts** +- **Risk**: Race conditions, inventory corruption +- **Mitigation**: Resource locking, thorough testing, rollback mechanisms + +**3. Memory Storage Growth** +- **Risk**: Unbounded memory files (>100MB) +- **Mitigation**: Automatic pruning, summarization, size limits + +**4. Minecraft API Changes** +- **Risk**: Forge updates breaking compatibility +- **Mitigation**: Version pinning, API abstraction layer + +--- + +## Appendix + +### Useful Resources + +**Minecraft Modding** +- Forge Documentation: https://docs.minecraftforge.net/ +- Minecraft Wiki: https://minecraft.fandom.com/ +- Fabric/Forge Examples: https://github.com/MinecraftForge/MinecraftForge + +**LLM Integration** +- OpenAI API Docs: https://platform.openai.com/docs +- Groq API Docs: https://console.groq.com/docs +- Gemini API Docs: https://ai.google.dev/docs + +**Vector Embeddings** +- Sentence-BERT: https://www.sbert.net/ +- OpenAI Embeddings: https://platform.openai.com/docs/guides/embeddings +- FAISS (Facebook AI Similarity Search): https://github.com/facebookresearch/faiss + +**Testing** +- JUnit 5: https://junit.org/junit5/ +- Mockito: https://site.mockito.org/ + +--- + +## Changelog + +| Date | Version | Changes | +|------|---------|---------| +| 2025-11-11 | 1.0 | Initial roadmap created | + +--- + +**Next Review Date**: 2025-12-11 +**Document Owner**: Development Team +**Approval Status**: ✅ Approved for Implementation diff --git a/src/main/java/com/steve/ai/action/ActionExecutor.java b/src/main/java/com/steve/ai/action/ActionExecutor.java index d7795d6..2bf4f7e 100644 --- a/src/main/java/com/steve/ai/action/ActionExecutor.java +++ b/src/main/java/com/steve/ai/action/ActionExecutor.java @@ -2,30 +2,48 @@ import com.steve.ai.SteveMod; import com.steve.ai.action.actions.*; -import com.steve.ai.ai.ResponseParser; -import com.steve.ai.ai.TaskPlanner; +import com.steve.ai.ai.*; import com.steve.ai.config.SteveConfig; import com.steve.ai.entity.SteveEntity; +import com.steve.ai.learning.*; import java.util.LinkedList; +import java.util.Map; +import java.util.HashMap; import java.util.Queue; public class ActionExecutor { private final SteveEntity steve; private TaskPlanner taskPlanner; // Lazy-initialized to avoid loading dependencies on entity creation private final Queue taskQueue; - - private BaseAction currentAction; + private final ActionScheduler scheduler; // New async action scheduler + + // Learning system components (Phase 2.4) + private final FailureTracker failureTracker; + private final ActionKnowledgeBase knowledgeBase; + private final LearningSystem learningSystem; + private final AdaptiveRetryStrategy retryStrategy; + + private BaseAction currentAction; // Legacy: kept for compatibility private String currentGoal; private int ticksSinceLastAction; private BaseAction idleFollowAction; // Follow player when idle + private boolean useScheduler = true; // Enable new scheduler by default + private int learningTickCounter = 0; // Periodic learning analysis public ActionExecutor(SteveEntity steve) { this.steve = steve; this.taskPlanner = null; // Will be initialized when first needed this.taskQueue = new LinkedList<>(); + this.scheduler = new ActionScheduler(steve); this.ticksSinceLastAction = 0; this.idleFollowAction = null; + + // Initialize learning system + this.failureTracker = new FailureTracker(steve.getSteveName()); + this.knowledgeBase = new ActionKnowledgeBase(steve.getSteveName()); + this.learningSystem = new LearningSystem(steve, failureTracker, knowledgeBase); + this.retryStrategy = new AdaptiveRetryStrategy(failureTracker, learningSystem); } private TaskPlanner getTaskPlanner() { @@ -84,27 +102,96 @@ private void sendToGUI(String steveName, String message) { } public void tick() { + // Periodic learning analysis (every 10 seconds = 200 ticks) + learningTickCounter++; + if (learningTickCounter >= 200) { + performLearningAnalysis(); + learningTickCounter = 0; + } + + if (useScheduler) { + tickWithScheduler(); + } else { + tickLegacy(); + } + } + + /** + * Perform periodic learning analysis to identify patterns + */ + private void performLearningAnalysis() { + try { + learningSystem.generateInsights(); + SteveMod.LOGGER.debug("Steve '{}' performed learning analysis - {} insights in knowledge base", + steve.getSteveName(), knowledgeBase.getInsightCount()); + } catch (Exception e) { + SteveMod.LOGGER.error("Error during learning analysis for Steve '{}'", steve.getSteveName(), e); + } + } + + /** + * New tick method using ActionScheduler for async execution + */ + private void tickWithScheduler() { ticksSinceLastAction++; - + + // Tick the scheduler (handles running actions) + scheduler.tick(); + + // Schedule new tasks from queue when ready + if (ticksSinceLastAction >= SteveConfig.ACTION_TICK_DELAY.get()) { + if (!taskQueue.isEmpty()) { + Task nextTask = taskQueue.poll(); + scheduleTask(nextTask, ActionPriority.NORMAL); + ticksSinceLastAction = 0; + } + } + + // Handle idle behavior + boolean isIdle = taskQueue.isEmpty() && + scheduler.getRunningActionCount() == 0 && + currentGoal == null; + + if (isIdle) { + if (idleFollowAction == null) { + idleFollowAction = new IdleFollowAction(steve); + scheduler.scheduleAction(idleFollowAction, ActionPriority.BACKGROUND); + } else if (idleFollowAction.isComplete()) { + idleFollowAction = new IdleFollowAction(steve); + scheduler.scheduleAction(idleFollowAction, ActionPriority.BACKGROUND); + } + } else if (idleFollowAction != null) { + scheduler.interruptAction(idleFollowAction); + idleFollowAction = null; + } + } + + /** + * Legacy tick method (original implementation) + */ + private void tickLegacy() { + ticksSinceLastAction++; + if (currentAction != null) { if (currentAction.isComplete()) { ActionResult result = currentAction.getResult(); - SteveMod.LOGGER.info("Steve '{}' - Action completed: {} (Success: {})", + SteveMod.LOGGER.info("Steve '{}' - Action completed: {} (Success: {})", steve.getSteveName(), result.getMessage(), result.isSuccess()); - + steve.getMemory().addAction(currentAction.getDescription()); - - if (!result.isSuccess() && result.requiresReplanning()) { - // Action failed, need to replan - if (SteveConfig.ENABLE_CHAT_RESPONSES.get()) { - sendToGUI(steve.getSteveName(), "Problem: " + result.getMessage()); - } + + if (result.isSuccess()) { + // Record successful action in knowledge base + recordSuccess(currentAction.task.getAction()); + } else if (result.requiresReplanning()) { + // Action failed, attempt error recovery + handleErrorRecovery(currentAction.getDescription(), result.getMessage()); } - + currentAction = null; } else { if (ticksSinceLastAction % 100 == 0) { - SteveMod.LOGGER.info("Steve '{}' - Ticking action: {}", + SteveMod.LOGGER.info("Steve '{}' - Ticking action: {}", steve.getSteveName(), currentAction.getDescription()); } currentAction.tick(); @@ -120,7 +207,7 @@ public void tick() { return; } } - + // When completely idle (no tasks, no goal), follow nearest player if (taskQueue.isEmpty() && currentAction == null && currentGoal == null) { if (idleFollowAction == null) { @@ -140,12 +227,33 @@ public void tick() { } } + /** + * Schedule a task with a priority using the new scheduler + */ + private void scheduleTask(Task task, ActionPriority priority) { + SteveMod.LOGGER.info("Steve '{}' scheduling task: {} (priority: {})", + steve.getSteveName(), task, priority); + + BaseAction action = createAction(task); + + if (action == null) { + SteveMod.LOGGER.error("FAILED to create action for task: {}", task); + return; + } + + action.setPriority(priority); + scheduler.scheduleAction(action, priority); + } + + /** + * Legacy method for executing task immediately + */ private void executeTask(Task task) { - SteveMod.LOGGER.info("Steve '{}' executing task: {} (action type: {})", + SteveMod.LOGGER.info("Steve '{}' executing task: {} (action type: {})", steve.getSteveName(), task, task.getAction()); - + currentAction = createAction(task); - + if (currentAction == null) { SteveMod.LOGGER.error("FAILED to create action for task: {}", task); return; @@ -163,9 +271,18 @@ private BaseAction createAction(Task task) { case "place" -> new PlaceBlockAction(steve, task); case "craft" -> new CraftItemAction(steve, task); case "attack" -> new CombatAction(steve, task); + case "attack_ranged" -> new RangedCombatAction(steve, task); + case "retreat" -> new TacticalRetreatAction(steve, task); case "follow" -> new FollowPlayerAction(steve, task); case "gather" -> new GatherResourceAction(steve, task); case "build" -> new BuildStructureAction(steve, task); + case "store" -> new StoreItemsAction(steve, task); + case "retrieve" -> new RetrieveItemsAction(steve, task); + case "place_chest" -> new PlaceChestAction(steve, task); + case "farm" -> new FarmAction(steve, task); + case "breed" -> new BreedingAction(steve, task); + case "build_portal" -> new PortalBuilderAction(steve, task); + case "build_auto_door" -> new AutomaticDoorAction(steve, task); default -> { SteveMod.LOGGER.warn("Unknown action type: {}", task.getAction()); yield null; @@ -174,24 +291,197 @@ private BaseAction createAction(Task task) { } public void stopCurrentAction() { - if (currentAction != null) { - currentAction.cancel(); - currentAction = null; - } - if (idleFollowAction != null) { - idleFollowAction.cancel(); - idleFollowAction = null; + if (useScheduler) { + // Stop all scheduled and running actions + scheduler.clear(); + if (idleFollowAction != null) { + idleFollowAction = null; + } + } else { + // Legacy stop + if (currentAction != null) { + currentAction.cancel(); + currentAction = null; + } + if (idleFollowAction != null) { + idleFollowAction.cancel(); + idleFollowAction = null; + } } taskQueue.clear(); currentGoal = null; } public boolean isExecuting() { - return currentAction != null || !taskQueue.isEmpty(); + if (useScheduler) { + return scheduler.getRunningActionCount() > 0 || !taskQueue.isEmpty(); + } else { + return currentAction != null || !taskQueue.isEmpty(); + } } public String getCurrentGoal() { return currentGoal; } + + /** + * Get the action scheduler (for advanced usage) + */ + public ActionScheduler getScheduler() { + return scheduler; + } + + /** + * Schedule a high-priority action (interrupts normal tasks) + */ + public void scheduleHighPriorityAction(BaseAction action) { + action.setPriority(ActionPriority.HIGH); + scheduler.scheduleAction(action, ActionPriority.HIGH); + } + + /** + * Schedule a critical action (interrupts everything) + */ + public void scheduleCriticalAction(BaseAction action) { + action.setPriority(ActionPriority.CRITICAL); + scheduler.scheduleAction(action, ActionPriority.CRITICAL); + } + + /** + * Enable or disable the new scheduler (for testing/debugging) + */ + public void setUseScheduler(boolean useScheduler) { + this.useScheduler = useScheduler; + } + + /** + * Check if scheduler is enabled + */ + public boolean isUsingScheduler() { + return useScheduler; + } + + /** + * Handle error recovery when an action fails + * Uses LLM to replan based on error context + */ + private void handleErrorRecovery(String failedAction, String errorMessage) { + try { + SteveMod.LOGGER.info("Steve '{}' attempting error recovery for: {}", + steve.getSteveName(), failedAction); + + // Record failure in learning system + recordFailure(failedAction, new HashMap<>(), errorMessage); + + // Notify user + if (SteveConfig.ENABLE_CHAT_RESPONSES.get()) { + sendToGUI(steve.getSteveName(), "Problem: " + errorMessage + ". Replanning..."); + } + + // Build error recovery prompt with learned knowledge + String systemPrompt = PromptBuilder.buildSystemPrompt(); + String knowledgeSummary = knowledgeBase.generateKnowledgeSummary(); + String errorPrompt = PromptBuilder.buildErrorRecoveryPrompt( + steve, + failedAction, + errorMessage, + currentGoal != null ? currentGoal : "unknown goal" + ) + knowledgeSummary; + + // Get recovery plan from LLM + String provider = SteveConfig.AI_PROVIDER.get().toLowerCase(); + String response = null; + + switch (provider) { + case "groq" -> response = new GroqClient().sendRequest(systemPrompt, errorPrompt); + case "gemini" -> response = new GeminiClient().sendRequest(systemPrompt, errorPrompt); + case "openai" -> response = new OpenAIClient().sendRequest(systemPrompt, errorPrompt); + default -> response = new GroqClient().sendRequest(systemPrompt, errorPrompt); + } + + if (response == null) { + SteveMod.LOGGER.error("Failed to get error recovery response"); + if (SteveConfig.ENABLE_CHAT_RESPONSES.get()) { + sendToGUI(steve.getSteveName(), "I couldn't figure out how to fix this."); + } + return; + } + + // Parse recovery plan + ResponseParser.ParsedResponse parsedResponse = ResponseParser.parseAIResponse(response); + + if (parsedResponse == null) { + SteveMod.LOGGER.error("Failed to parse error recovery response"); + return; + } + + // Replace task queue with recovery tasks + taskQueue.clear(); + taskQueue.addAll(parsedResponse.getTasks()); + currentGoal = parsedResponse.getPlan(); + + SteveMod.LOGGER.info("Steve '{}' created recovery plan: {} ({} tasks)", + steve.getSteveName(), currentGoal, taskQueue.size()); + + if (SteveConfig.ENABLE_CHAT_RESPONSES.get()) { + sendToGUI(steve.getSteveName(), "New plan: " + currentGoal); + } + + } catch (Exception e) { + SteveMod.LOGGER.error("Error during error recovery", e); + } + } + + /** + * Record a failed action in the learning system + */ + private void recordFailure(String action, Map parameters, String errorMessage) { + try { + String context = "Goal: " + (currentGoal != null ? currentGoal : "none") + + ", Inventory: " + steve.getInventory().getSlots() + " slots"; + failureTracker.recordFailure(action, parameters, errorMessage, context); + } catch (Exception e) { + SteveMod.LOGGER.error("Error recording failure", e); + } + } + + /** + * Record a successful action in the knowledge base + */ + private void recordSuccess(String action) { + try { + knowledgeBase.recordSuccess(action); + } catch (Exception e) { + SteveMod.LOGGER.error("Error recording success", e); + } + } + + /** + * Get learning system for external access + */ + public LearningSystem getLearningSystem() { + return learningSystem; + } + + /** + * Get failure tracker for external access + */ + public FailureTracker getFailureTracker() { + return failureTracker; + } + + /** + * Get knowledge base for external access + */ + public ActionKnowledgeBase getKnowledgeBase() { + return knowledgeBase; + } + + /** + * Get adaptive retry strategy + */ + public AdaptiveRetryStrategy getRetryStrategy() { + return retryStrategy; + } } diff --git a/src/main/java/com/steve/ai/action/ActionPriority.java b/src/main/java/com/steve/ai/action/ActionPriority.java new file mode 100644 index 0000000..f904e93 --- /dev/null +++ b/src/main/java/com/steve/ai/action/ActionPriority.java @@ -0,0 +1,91 @@ +package com.steve.ai.action; + +/** + * Action priority levels for scheduling and interruption + * Lower priority value = higher urgency + */ +public enum ActionPriority { + /** + * CRITICAL: Immediate response required (combat, danger avoidance) + * Can interrupt any other action + */ + CRITICAL(0), + + /** + * HIGH: User commands and important goals + * Can interrupt NORMAL, LOW, and BACKGROUND actions + */ + HIGH(1), + + /** + * NORMAL: Standard autonomous tasks + * Can interrupt LOW and BACKGROUND actions + */ + NORMAL(2), + + /** + * LOW: Optional tasks and idle behavior + * Can interrupt BACKGROUND actions only + */ + LOW(3), + + /** + * BACKGROUND: Passive monitoring and observation + * Cannot interrupt other actions + */ + BACKGROUND(4); + + private final int level; + + ActionPriority(int level) { + this.level = level; + } + + /** + * Get priority level (0 = highest priority) + */ + public int getLevel() { + return level; + } + + /** + * Check if this priority can interrupt another action + * @param other The priority of the action to potentially interrupt + * @return True if this priority can interrupt the other + */ + public boolean canInterrupt(ActionPriority other) { + return this.level < other.level; + } + + /** + * Check if this priority is higher than another + * @param other The priority to compare against + * @return True if this priority is higher + */ + public boolean isHigherThan(ActionPriority other) { + return this.level < other.level; + } + + /** + * Check if this priority is lower than another + * @param other The priority to compare against + * @return True if this priority is lower + */ + public boolean isLowerThan(ActionPriority other) { + return this.level > other.level; + } + + /** + * Check if two priorities are equal + * @param other The priority to compare against + * @return True if priorities are equal + */ + public boolean isEqualTo(ActionPriority other) { + return this.level == other.level; + } + + @Override + public String toString() { + return name() + "(level=" + level + ")"; + } +} diff --git a/src/main/java/com/steve/ai/action/ActionScheduler.java b/src/main/java/com/steve/ai/action/ActionScheduler.java new file mode 100644 index 0000000..a8d9522 --- /dev/null +++ b/src/main/java/com/steve/ai/action/ActionScheduler.java @@ -0,0 +1,388 @@ +package com.steve.ai.action; + +import com.steve.ai.SteveMod; +import com.steve.ai.action.actions.BaseAction; +import com.steve.ai.entity.SteveEntity; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Action scheduler for concurrent action execution + * Manages action priorities, resource locks, and interruption + * + * Features: + * - Priority-based scheduling (CRITICAL > HIGH > NORMAL > LOW > BACKGROUND) + * - Concurrent action execution when compatible + * - Resource locking to prevent conflicts + * - Action interruption for higher priority tasks + */ +public class ActionScheduler { + private final SteveEntity steve; + private final ResourceLock resourceLock; + + // Priority queues for scheduled actions + private final Map> actionQueues; + + // Currently running actions + private final Set runningActions; + + // Action history for debugging + private final List history; + private static final int MAX_HISTORY = 50; + + public ActionScheduler(SteveEntity steve) { + this.steve = steve; + this.resourceLock = new ResourceLock(); + this.actionQueues = new EnumMap<>(ActionPriority.class); + this.runningActions = ConcurrentHashMap.newKeySet(); + this.history = new ArrayList<>(); + + // Initialize priority queues + for (ActionPriority priority : ActionPriority.values()) { + actionQueues.put(priority, new LinkedList<>()); + } + } + + /** + * Schedule an action with a priority + * @param action Action to schedule + * @param priority Priority level + */ + public void scheduleAction(BaseAction action, ActionPriority priority) { + if (action == null) { + return; + } + + // Check if higher priority action should interrupt running actions + if (priority == ActionPriority.CRITICAL || priority == ActionPriority.HIGH) { + checkAndInterruptLowerPriority(priority); + } + + actionQueues.get(priority).offer(action); + + SteveMod.LOGGER.debug("Scheduled action: {} with priority {}", + action.getDescription(), priority); + } + + /** + * Main tick method - called every game tick + * Processes action queues and runs compatible actions + */ + public void tick() { + // Remove completed actions + runningActions.removeIf(action -> { + if (action.isCompleted()) { + resourceLock.release(action); + recordHistory(action, "COMPLETED"); + return true; + } + return false; + }); + + // Try to start new actions from queues (highest priority first) + for (ActionPriority priority : ActionPriority.values()) { + Queue queue = actionQueues.get(priority); + + while (!queue.isEmpty()) { + BaseAction action = queue.peek(); + + // Check if action can be executed + if (canExecute(action)) { + queue.poll(); // Remove from queue + + // Acquire resource locks + Set requiredResources = getRequiredResources(action); + if (resourceLock.tryAcquire(action, requiredResources)) { + // Start action + action.start(); + runningActions.add(action); + recordHistory(action, "STARTED"); + + SteveMod.LOGGER.debug("Started action: {} (priority: {})", + action.getDescription(), priority); + } else { + // Resources unavailable, put back in queue + queue.offer(action); + break; // Try next priority level + } + } else { + // Action cannot execute, skip for now + break; + } + } + } + + // Tick all running actions + for (BaseAction action : runningActions) { + try { + action.tick(); + } catch (Exception e) { + SteveMod.LOGGER.error("Error ticking action: {}", + action.getDescription(), e); + action.cancel(); + } + } + } + + /** + * Interrupt an action immediately + * @param action Action to interrupt + */ + public void interruptAction(BaseAction action) { + if (runningActions.contains(action)) { + action.cancel(); + runningActions.remove(action); + resourceLock.forceRelease(action); + recordHistory(action, "INTERRUPTED"); + + SteveMod.LOGGER.debug("Interrupted action: {}", action.getDescription()); + } + + // Also remove from queues + for (Queue queue : actionQueues.values()) { + queue.remove(action); + } + } + + /** + * Check if an action can be executed + * @param action Action to check + * @return True if action can execute + */ + public boolean canExecute(BaseAction action) { + // Check resource availability + Set required = getRequiredResources(action); + for (ResourceLock.Resource resource : required) { + if (resourceLock.isLocked(resource)) { + BaseAction holder = resourceLock.getLockHolder(resource); + if (holder != action) { + return false; // Resource locked by another action + } + } + } + + // Check action compatibility with running actions + return isCompatibleWithRunning(action); + } + + /** + * Check if action is compatible with currently running actions + */ + private boolean isCompatibleWithRunning(BaseAction newAction) { + for (BaseAction running : runningActions) { + if (!areActionsCompatible(newAction, running)) { + return false; + } + } + return true; + } + + /** + * Check if two actions are compatible (can run simultaneously) + */ + private boolean areActionsCompatible(BaseAction action1, BaseAction action2) { + Set resources1 = getRequiredResources(action1); + Set resources2 = getRequiredResources(action2); + + // Actions are incompatible if they share any resources + for (ResourceLock.Resource resource : resources1) { + if (resources2.contains(resource)) { + return false; + } + } + + return true; + } + + /** + * Get required resources for an action based on its type + */ + private Set getRequiredResources(BaseAction action) { + Set resources = new HashSet<>(); + + String actionClass = action.getClass().getSimpleName(); + + // Determine required resources based on action type + switch (actionClass) { + case "MineBlockAction": + resources.add(ResourceLock.Resource.NAVIGATION); + resources.add(ResourceLock.Resource.INTERACTION); + resources.add(ResourceLock.Resource.INVENTORY); + break; + + case "PlaceBlockAction": + case "PlaceChestAction": + resources.add(ResourceLock.Resource.NAVIGATION); + resources.add(ResourceLock.Resource.INTERACTION); + resources.add(ResourceLock.Resource.INVENTORY); + break; + + case "CraftItemAction": + resources.add(ResourceLock.Resource.NAVIGATION); + resources.add(ResourceLock.Resource.INTERACTION); + resources.add(ResourceLock.Resource.INVENTORY); + resources.add(ResourceLock.Resource.CRAFTING); + break; + + case "StoreItemsAction": + case "RetrieveItemsAction": + resources.add(ResourceLock.Resource.NAVIGATION); + resources.add(ResourceLock.Resource.INTERACTION); + resources.add(ResourceLock.Resource.INVENTORY); + break; + + case "NavigateToAction": + resources.add(ResourceLock.Resource.NAVIGATION); + break; + + case "FollowPlayerAction": + resources.add(ResourceLock.Resource.NAVIGATION); + break; + + case "IdleAction": + // Idle requires no resources + break; + + default: + // Unknown action type - lock all resources to be safe + resources.add(ResourceLock.Resource.NAVIGATION); + resources.add(ResourceLock.Resource.INTERACTION); + break; + } + + return resources; + } + + /** + * Interrupt lower priority actions if higher priority action is scheduled + */ + private void checkAndInterruptLowerPriority(ActionPriority newPriority) { + List toInterrupt = runningActions.stream() + .filter(action -> { + ActionPriority actionPriority = getActionPriority(action); + return newPriority.isHigherThan(actionPriority); + }) + .collect(Collectors.toList()); + + for (BaseAction action : toInterrupt) { + interruptAction(action); + } + } + + /** + * Get priority of an action (default to NORMAL if not set) + */ + private ActionPriority getActionPriority(BaseAction action) { + // For now, return NORMAL as default + // In the future, actions can specify their own priority + return ActionPriority.NORMAL; + } + + /** + * Get number of running actions + */ + public int getRunningActionCount() { + return runningActions.size(); + } + + /** + * Get number of queued actions + */ + public int getQueuedActionCount() { + return actionQueues.values().stream() + .mapToInt(Queue::size) + .sum(); + } + + /** + * Get all running actions + */ + public Set getRunningActions() { + return new HashSet<>(runningActions); + } + + /** + * Clear all actions and queues + */ + public void clear() { + for (BaseAction action : runningActions) { + action.cancel(); + } + runningActions.clear(); + + for (Queue queue : actionQueues.values()) { + queue.clear(); + } + + resourceLock.clear(); + } + + /** + * Record action history for debugging + */ + private void recordHistory(BaseAction action, String event) { + history.add(new ActionHistoryEntry( + System.currentTimeMillis(), + action.getDescription(), + event + )); + + // Limit history size + if (history.size() > MAX_HISTORY) { + history.remove(0); + } + } + + /** + * Get action history summary + */ + public String getHistorySummary() { + StringBuilder sb = new StringBuilder("Action History:\n"); + for (ActionHistoryEntry entry : history) { + sb.append(String.format("[%d] %s - %s\n", + entry.timestamp, entry.event, entry.actionDescription)); + } + return sb.toString(); + } + + /** + * Get scheduler status for debugging + */ + public String getStatus() { + StringBuilder sb = new StringBuilder(); + sb.append("=== Action Scheduler Status ===\n"); + sb.append("Running actions: ").append(runningActions.size()).append("\n"); + for (BaseAction action : runningActions) { + sb.append(" - ").append(action.getDescription()).append("\n"); + } + + sb.append("Queued actions: ").append(getQueuedActionCount()).append("\n"); + for (ActionPriority priority : ActionPriority.values()) { + int count = actionQueues.get(priority).size(); + if (count > 0) { + sb.append(" ").append(priority).append(": ").append(count).append("\n"); + } + } + + sb.append(resourceLock.getStatusSummary()); + + return sb.toString(); + } + + /** + * Action history entry + */ + private static class ActionHistoryEntry { + final long timestamp; + final String actionDescription; + final String event; + + ActionHistoryEntry(long timestamp, String actionDescription, String event) { + this.timestamp = timestamp; + this.actionDescription = actionDescription; + this.event = event; + } + } +} diff --git a/src/main/java/com/steve/ai/action/HungerManager.java b/src/main/java/com/steve/ai/action/HungerManager.java new file mode 100644 index 0000000..28874c8 --- /dev/null +++ b/src/main/java/com/steve/ai/action/HungerManager.java @@ -0,0 +1,215 @@ +package com.steve.ai.action; + +import com.steve.ai.SteveMod; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.food.FoodData; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +import java.util.*; + +/** + * Manages Steve's hunger automatically + * Monitors food level and automatically eats when hungry + */ +public class HungerManager { + private final SteveEntity steve; + private int ticksSinceLastCheck = 0; + private static final int CHECK_INTERVAL = 100; // Check every 5 seconds + private static final int HUNGER_THRESHOLD = 14; // Eat when hunger < 14 (out of 20) + + // Food items ranked by nutrition value + private static final Map FOOD_VALUES = new LinkedHashMap<>() {{ + // Best foods first + put(Items.GOLDEN_APPLE, 4); + put(Items.ENCHANTED_GOLDEN_APPLE, 4); + put(Items.COOKED_BEEF, 8); + put(Items.COOKED_PORKCHOP, 8); + put(Items.COOKED_MUTTON, 6); + put(Items.COOKED_CHICKEN, 6); + put(Items.COOKED_RABBIT, 5); + put(Items.COOKED_COD, 5); + put(Items.COOKED_SALMON, 6); + put(Items.BAKED_POTATO, 5); + put(Items.BREAD, 5); + put(Items.GOLDEN_CARROT, 6); + put(Items.PUMPKIN_PIE, 8); + put(Items.CAKE, 2); // Per slice + put(Items.COOKIE, 2); + // Raw foods (emergency) + put(Items.BEEF, 3); + put(Items.PORKCHOP, 3); + put(Items.MUTTON, 2); + put(Items.CHICKEN, 2); + put(Items.RABBIT, 3); + put(Items.COD, 2); + put(Items.SALMON, 2); + put(Items.CARROT, 3); + put(Items.POTATO, 1); + put(Items.APPLE, 4); + put(Items.MELON_SLICE, 2); + put(Items.SWEET_BERRIES, 2); + put(Items.GLOW_BERRIES, 2); + }}; + + public HungerManager(SteveEntity steve) { + this.steve = steve; + } + + /** + * Tick the hunger manager + * Called from Steve's tick method + */ + public void tick() { + ticksSinceLastCheck++; + + if (ticksSinceLastCheck >= CHECK_INTERVAL) { + ticksSinceLastCheck = 0; + checkAndEat(); + } + } + + /** + * Check hunger level and eat if needed + */ + private void checkAndEat() { + FoodData foodData = steve.getFoodData(); + int foodLevel = foodData.getFoodLevel(); + + if (foodLevel < HUNGER_THRESHOLD) { + SteveMod.LOGGER.debug("Steve '{}' is hungry (food level: {})", + steve.getSteveName(), foodLevel); + + boolean ate = eatFood(); + + if (ate) { + SteveMod.LOGGER.info("Steve '{}' ate food (food level was: {}, now: {})", + steve.getSteveName(), foodLevel, steve.getFoodData().getFoodLevel()); + } else { + SteveMod.LOGGER.warn("Steve '{}' is hungry but has no food!", + steve.getSteveName()); + } + } + } + + /** + * Eat the best available food from inventory + * @return true if food was eaten + */ + private boolean eatFood() { + // Find best food in inventory + Item bestFood = findBestFood(); + + if (bestFood == null) { + return false; + } + + // Create food stack + ItemStack foodStack = new ItemStack(bestFood); + + // Equip food + steve.setItemInHand(InteractionHand.MAIN_HAND, foodStack); + + // Eat food (simulate consumption) + steve.getFoodData().eat(bestFood, foodStack); + + // Remove from inventory + InventoryHelper.removeItem(steve, bestFood, 1); + + // Swing arm for animation + steve.swing(InteractionHand.MAIN_HAND, true); + + return true; + } + + /** + * Find the best food item in inventory + * Prioritizes cooked/high-nutrition foods + */ + private Item findBestFood() { + for (Map.Entry entry : FOOD_VALUES.entrySet()) { + Item food = entry.getKey(); + if (InventoryHelper.hasItem(steve, food, 1)) { + return food; + } + } + return null; + } + + /** + * Get current hunger level (0-20) + */ + public int getHungerLevel() { + return steve.getFoodData().getFoodLevel(); + } + + /** + * Check if Steve is hungry + */ + public boolean isHungry() { + return steve.getFoodData().getFoodLevel() < HUNGER_THRESHOLD; + } + + /** + * Check if Steve is starving (very low hunger) + */ + public boolean isStarving() { + return steve.getFoodData().getFoodLevel() < 6; + } + + /** + * Get saturation level + */ + public float getSaturation() { + return steve.getFoodData().getSaturationLevel(); + } + + /** + * Force Steve to eat if food is available + * @return true if food was eaten + */ + public boolean forceEat() { + return eatFood(); + } + + /** + * Check if Steve has any food in inventory + */ + public boolean hasFood() { + return findBestFood() != null; + } + + /** + * Get hunger status as string + */ + public String getHungerStatus() { + int hunger = getHungerLevel(); + float saturation = getSaturation(); + + if (hunger >= 18) { + return "Full (hunger: " + hunger + "/20, saturation: " + String.format("%.1f", saturation) + ")"; + } else if (hunger >= HUNGER_THRESHOLD) { + return "Satisfied (hunger: " + hunger + "/20)"; + } else if (hunger >= 10) { + return "Hungry (hunger: " + hunger + "/20)"; + } else if (hunger >= 6) { + return "Very Hungry (hunger: " + hunger + "/20)"; + } else { + return "STARVING (hunger: " + hunger + "/20)"; + } + } + + /** + * Count total food items in inventory + */ + public int countFoodItems() { + int count = 0; + for (Item food : FOOD_VALUES.keySet()) { + count += InventoryHelper.getItemCount(steve, food); + } + return count; + } +} diff --git a/src/main/java/com/steve/ai/action/ResourceLock.java b/src/main/java/com/steve/ai/action/ResourceLock.java new file mode 100644 index 0000000..6d9cc19 --- /dev/null +++ b/src/main/java/com/steve/ai/action/ResourceLock.java @@ -0,0 +1,153 @@ +package com.steve.ai.action; + +import com.steve.ai.action.actions.BaseAction; + +import java.util.*; + +/** + * Resource locking system to prevent conflicting actions + * Ensures that only one action can access a resource at a time + * + * Resources include: + * - INVENTORY: Steve's inventory + * - NAVIGATION: Pathfinding and movement + * - INTERACTION: Block breaking, placement, chest access + * - COMBAT: Attack and defense actions + */ +public class ResourceLock { + /** + * Resource types that can be locked + */ + public enum Resource { + INVENTORY, // Inventory operations (add/remove items) + NAVIGATION, // Movement and pathfinding + INTERACTION, // Block interaction (break, place, open chest) + COMBAT, // Combat actions (attack, defend) + CRAFTING // Crafting operations + } + + private final Map locks; + private final Map> actionResources; + + public ResourceLock() { + this.locks = new HashMap<>(); + this.actionResources = new HashMap<>(); + } + + /** + * Try to acquire locks for an action + * @param action The action requesting locks + * @param resources Resources to lock + * @return True if all locks acquired successfully + */ + public boolean tryAcquire(BaseAction action, Set resources) { + // Check if all resources are available + for (Resource resource : resources) { + if (locks.containsKey(resource) && locks.get(resource) != action) { + return false; // Resource already locked by another action + } + } + + // Acquire all locks + for (Resource resource : resources) { + locks.put(resource, action); + } + + // Track which resources this action holds + actionResources.put(action, new HashSet<>(resources)); + + return true; + } + + /** + * Release all locks held by an action + * @param action The action releasing locks + */ + public void release(BaseAction action) { + Set resources = actionResources.remove(action); + if (resources != null) { + for (Resource resource : resources) { + if (locks.get(resource) == action) { + locks.remove(resource); + } + } + } + } + + /** + * Force release locks for an action (used during interruption) + * @param action The action to force release + */ + public void forceRelease(BaseAction action) { + release(action); + } + + /** + * Check if a resource is locked + * @param resource The resource to check + * @return True if locked + */ + public boolean isLocked(Resource resource) { + return locks.containsKey(resource); + } + + /** + * Check if an action holds any locks + * @param action The action to check + * @return True if action holds locks + */ + public boolean holdsLocks(BaseAction action) { + return actionResources.containsKey(action); + } + + /** + * Get the action that holds a resource lock + * @param resource The resource to query + * @return The action holding the lock, or null if unlocked + */ + public BaseAction getLockHolder(Resource resource) { + return locks.get(resource); + } + + /** + * Get all resources locked by an action + * @param action The action to query + * @return Set of resources, or empty set if none + */ + public Set getLockedResources(BaseAction action) { + return actionResources.getOrDefault(action, Collections.emptySet()); + } + + /** + * Get all currently locked resources + * @return Set of locked resources + */ + public Set getAllLockedResources() { + return new HashSet<>(locks.keySet()); + } + + /** + * Clear all locks (for debugging/reset) + */ + public void clear() { + locks.clear(); + actionResources.clear(); + } + + /** + * Get lock status summary for debugging + */ + public String getStatusSummary() { + if (locks.isEmpty()) { + return "No resources locked"; + } + + StringBuilder sb = new StringBuilder("Locked resources:\n"); + for (Map.Entry entry : locks.entrySet()) { + sb.append(" ").append(entry.getKey()) + .append(" -> ").append(entry.getValue().getDescription()) + .append("\n"); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/steve/ai/action/actions/AutomaticDoorAction.java b/src/main/java/com/steve/ai/action/actions/AutomaticDoorAction.java new file mode 100644 index 0000000..4c4f731 --- /dev/null +++ b/src/main/java/com/steve/ai/action/actions/AutomaticDoorAction.java @@ -0,0 +1,240 @@ +package com.steve.ai.action.actions; + +import com.steve.ai.SteveMod; +import com.steve.ai.action.ActionResult; +import com.steve.ai.action.Task; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.redstone.RedstoneHelper; +import com.steve.ai.util.InventoryHelper; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.DoorBlock; + +/** + * Builds automatic door with pressure plate activation + * Creates a simple redstone circuit for door automation + */ +public class AutomaticDoorAction extends BaseAction { + private BlockPos doorPos; + private int ticksRunning; + private int componentsPlaced; + private static final int MAX_TICKS = 400; // 20 seconds + private static final int REQUIRED_REDSTONE = 5; + + private enum BuildPhase { + PLACE_DOOR, + PLACE_PRESSURE_PLATES, + PLACE_REDSTONE, + COMPLETE + } + + private BuildPhase currentPhase = BuildPhase.PLACE_DOOR; + + public AutomaticDoorAction(SteveEntity steve, Task task) { + super(steve, task); + } + + @Override + protected void onStart() { + ticksRunning = 0; + componentsPlaced = 0; + + // Check materials + if (!hasMaterials()) { + result = ActionResult.failure("Missing materials: need door, 2 pressure plates, " + + REQUIRED_REDSTONE + " redstone dust"); + return; + } + + // Set door position in front of Steve + doorPos = steve.blockPosition().relative(steve.getDirection(), 2); + + SteveMod.LOGGER.info("Steve '{}' building automatic door at {}", + steve.getSteveName(), doorPos); + + steve.setFlying(true); + } + + @Override + protected void onTick() { + ticksRunning++; + + if (ticksRunning > MAX_TICKS) { + steve.setFlying(false); + result = ActionResult.failure("Automatic door building timeout"); + return; + } + + switch (currentPhase) { + case PLACE_DOOR -> { + if (placeDoor()) { + currentPhase = BuildPhase.PLACE_PRESSURE_PLATES; + } + } + case PLACE_PRESSURE_PLATES -> { + if (placePressurePlates()) { + currentPhase = BuildPhase.PLACE_REDSTONE; + } + } + case PLACE_REDSTONE -> { + if (placeRedstoneWiring()) { + currentPhase = BuildPhase.COMPLETE; + } + } + case COMPLETE -> { + steve.setFlying(false); + result = ActionResult.success("Automatic door complete"); + } + } + } + + @Override + protected void onCancel() { + steve.setFlying(false); + steve.getNavigation().stop(); + } + + @Override + public String getDescription() { + return "Build automatic door (" + currentPhase + ")"; + } + + /** + * Place the door + */ + private boolean placeDoor() { + // Check if ground is solid + BlockPos groundPos = doorPos.below(); + if (!steve.level().getBlockState(groundPos).isSolid()) { + // Place floor block first + steve.level().setBlock(groundPos, Blocks.STONE.defaultBlockState(), 3); + } + + // Place door (2 blocks tall) + BlockPos lowerDoorPos = doorPos; + BlockPos upperDoorPos = doorPos.above(); + + if (!steve.level().getBlockState(lowerDoorPos).isAir() || + !steve.level().getBlockState(upperDoorPos).isAir()) { + return false; // Space occupied + } + + // Determine facing direction + Direction facing = steve.getDirection(); + + // Place iron door + steve.level().setBlock(lowerDoorPos, + Blocks.IRON_DOOR.defaultBlockState() + .setValue(DoorBlock.FACING, facing) + .setValue(DoorBlock.HALF, net.minecraft.world.level.block.state.properties.DoubleBlockHalf.LOWER), + 3); + + steve.level().setBlock(upperDoorPos, + Blocks.IRON_DOOR.defaultBlockState() + .setValue(DoorBlock.FACING, facing) + .setValue(DoorBlock.HALF, net.minecraft.world.level.block.state.properties.DoubleBlockHalf.UPPER), + 3); + + // Remove from inventory + InventoryHelper.removeItem(steve, Items.IRON_DOOR, 1); + + componentsPlaced += 2; + SteveMod.LOGGER.info("Steve '{}' placed door", steve.getSteveName()); + + return true; + } + + /** + * Place pressure plates on both sides of door + */ + private boolean placePressurePlates() { + Direction facing = steve.getDirection(); + + // Front pressure plate (1 block in front of door) + BlockPos frontPlatePos = doorPos.relative(facing); + if (RedstoneHelper.placePressurePlate(steve.level(), frontPlatePos)) { + InventoryHelper.removeItem(steve, Items.STONE_PRESSURE_PLATE, 1); + componentsPlaced++; + } + + // Back pressure plate (1 block behind door) + BlockPos backPlatePos = doorPos.relative(facing.getOpposite()); + if (RedstoneHelper.placePressurePlate(steve.level(), backPlatePos)) { + InventoryHelper.removeItem(steve, Items.STONE_PRESSURE_PLATE, 1); + componentsPlaced++; + } + + SteveMod.LOGGER.info("Steve '{}' placed pressure plates", steve.getSteveName()); + return true; + } + + /** + * Place redstone wiring to connect pressure plates to door + */ + private boolean placeRedstoneWiring() { + Direction facing = steve.getDirection(); + + // Front wiring: plate -> door + BlockPos frontPlatePos = doorPos.relative(facing); + BlockPos frontWirePos = frontPlatePos.relative(facing.getClockWise()); + + // Place redstone next to plate + if (RedstoneHelper.placeRedstoneDust(steve.level(), frontWirePos)) { + InventoryHelper.removeItem(steve, Items.REDSTONE, 1); + componentsPlaced++; + } + + // Connect to door position + BlockPos doorSidePos = doorPos.relative(facing.getClockWise()); + if (RedstoneHelper.placeRedstoneDust(steve.level(), doorSidePos)) { + InventoryHelper.removeItem(steve, Items.REDSTONE, 1); + componentsPlaced++; + } + + // Back wiring: plate -> door + BlockPos backPlatePos = doorPos.relative(facing.getOpposite()); + BlockPos backWirePos = backPlatePos.relative(facing.getClockWise()); + + if (RedstoneHelper.placeRedstoneDust(steve.level(), backWirePos)) { + InventoryHelper.removeItem(steve, Items.REDSTONE, 1); + componentsPlaced++; + } + + SteveMod.LOGGER.info("Steve '{}' placed redstone wiring ({} components total)", + steve.getSteveName(), componentsPlaced); + + return true; + } + + /** + * Check if Steve has required materials + */ + private boolean hasMaterials() { + return InventoryHelper.hasItem(steve, Items.IRON_DOOR, 1) && + InventoryHelper.hasItem(steve, Items.STONE_PRESSURE_PLATE, 2) && + InventoryHelper.getItemCount(steve, Items.REDSTONE) >= REQUIRED_REDSTONE; + } + + /** + * Static helper to check materials + */ + public static boolean hasMaterials(SteveEntity steve) { + return InventoryHelper.hasItem(steve, Items.IRON_DOOR, 1) && + InventoryHelper.hasItem(steve, Items.STONE_PRESSURE_PLATE, 2) && + InventoryHelper.getItemCount(steve, Items.REDSTONE) >= 5; + } + + /** + * Get materials summary + */ + public static String getMaterialsSummary(SteveEntity steve) { + boolean hasDoor = InventoryHelper.hasItem(steve, Items.IRON_DOOR, 1); + int plates = InventoryHelper.getItemCount(steve, Items.STONE_PRESSURE_PLATE); + int redstone = InventoryHelper.getItemCount(steve, Items.REDSTONE); + + return String.format("Door: %s, Plates: %d/2, Redstone: %d/5", + hasDoor ? "Yes" : "No", plates, redstone); + } +} diff --git a/src/main/java/com/steve/ai/action/actions/BaseAction.java b/src/main/java/com/steve/ai/action/actions/BaseAction.java index c1c4ee4..0560e78 100644 --- a/src/main/java/com/steve/ai/action/actions/BaseAction.java +++ b/src/main/java/com/steve/ai/action/actions/BaseAction.java @@ -1,5 +1,6 @@ package com.steve.ai.action.actions; +import com.steve.ai.action.ActionPriority; import com.steve.ai.action.ActionResult; import com.steve.ai.action.Task; import com.steve.ai.entity.SteveEntity; @@ -10,6 +11,7 @@ public abstract class BaseAction { protected ActionResult result; protected boolean started = false; protected boolean cancelled = false; + protected ActionPriority priority = ActionPriority.NORMAL; // Default priority public BaseAction(SteveEntity steve, Task task) { this.steve = steve; @@ -37,14 +39,46 @@ public boolean isComplete() { return result != null || cancelled; } + public boolean isCompleted() { + return isComplete(); + } + public ActionResult getResult() { return result; } + /** + * Get action priority + */ + public ActionPriority getPriority() { + return priority; + } + + /** + * Set action priority + */ + public void setPriority(ActionPriority priority) { + this.priority = priority; + } + + /** + * Check if this action has been started + */ + public boolean isStarted() { + return started; + } + + /** + * Check if this action has been cancelled + */ + public boolean isCancelled() { + return cancelled; + } + protected abstract void onStart(); protected abstract void onTick(); protected abstract void onCancel(); - + public abstract String getDescription(); } diff --git a/src/main/java/com/steve/ai/action/actions/BreedingAction.java b/src/main/java/com/steve/ai/action/actions/BreedingAction.java new file mode 100644 index 0000000..0e1d5de --- /dev/null +++ b/src/main/java/com/steve/ai/action/actions/BreedingAction.java @@ -0,0 +1,238 @@ +package com.steve.ai.action.actions; + +import com.steve.ai.SteveMod; +import com.steve.ai.action.ActionResult; +import com.steve.ai.action.Task; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.animal.*; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +import java.util.*; + +/** + * Breeding action for animal husbandry + * Supports cows, pigs, chickens, sheep, and other farm animals + */ +public class BreedingAction extends BaseAction { + private String animalType; + private int targetBreedings; + private int breedingsDone; + private int searchRadius = 16; + private int ticksRunning; + private static final int MAX_TICKS = 6000; // 5 minutes + private static final int TICK_DELAY = 60; // 3 seconds between breeding attempts + private int ticksSinceLastAction = 0; + + // Animal breeding food mappings + private static final Map> BREEDING_FOODS = new HashMap<>() {{ + put("cow", Arrays.asList(Items.WHEAT)); + put("pig", Arrays.asList(Items.CARROT, Items.POTATO, Items.BEETROOT)); + put("chicken", Arrays.asList(Items.WHEAT_SEEDS, Items.MELON_SEEDS, Items.PUMPKIN_SEEDS, Items.BEETROOT_SEEDS)); + put("sheep", Arrays.asList(Items.WHEAT)); + put("horse", Arrays.asList(Items.GOLDEN_APPLE, Items.GOLDEN_CARROT)); + put("llama", Arrays.asList(Items.HAY_BLOCK)); + put("rabbit", Arrays.asList(Items.CARROT, Items.GOLDEN_CARROT, Items.DANDELION)); + put("turtle", Arrays.asList(Items.SEAGRASS)); + put("fox", Arrays.asList(Items.SWEET_BERRIES, Items.GLOW_BERRIES)); + put("goat", Arrays.asList(Items.WHEAT)); + }}; + + public BreedingAction(SteveEntity steve, Task task) { + super(steve, task); + } + + @Override + protected void onStart() { + animalType = task.getStringParameter("animal", "cow").toLowerCase(); + targetBreedings = task.getIntParameter("amount", 5); + breedingsDone = 0; + ticksRunning = 0; + ticksSinceLastAction = 0; + + if (!BREEDING_FOODS.containsKey(animalType)) { + result = ActionResult.failure("Unknown animal type: " + animalType); + return; + } + + // Check if we have breeding food + List foods = BREEDING_FOODS.get(animalType); + boolean hasFood = false; + for (Item food : foods) { + if (InventoryHelper.hasItem(steve, food, 2)) { // Need at least 2 items to breed + hasFood = true; + break; + } + } + + if (!hasFood) { + result = ActionResult.failure("No breeding food for " + animalType + " in inventory"); + return; + } + + SteveMod.LOGGER.info("Steve '{}' starting breeding action for {} (target: {})", + steve.getSteveName(), animalType, targetBreedings); + } + + @Override + protected void onTick() { + ticksRunning++; + ticksSinceLastAction++; + + if (ticksRunning > MAX_TICKS) { + result = ActionResult.failure("Breeding timeout - bred " + breedingsDone + " animals"); + return; + } + + if (ticksSinceLastAction < TICK_DELAY) { + return; // Wait between breeding attempts + } + + boolean didBreeding = attemptBreeding(); + + if (didBreeding) { + breedingsDone++; + ticksSinceLastAction = 0; + + if (breedingsDone >= targetBreedings) { + result = ActionResult.success("Successfully bred " + breedingsDone + " " + animalType + "(s)"); + return; + } + } else { + // Check if we've done enough or can't find more animals + if (breedingsDone > 0 && ticksSinceLastAction > TICK_DELAY * 3) { + result = ActionResult.success("Bred " + breedingsDone + " " + animalType + "(s) (no more pairs found)"); + } else if (breedingsDone == 0 && ticksSinceLastAction > TICK_DELAY * 5) { + result = ActionResult.failure("No breedable " + animalType + " pairs found nearby"); + } + } + } + + @Override + protected void onCancel() { + steve.getNavigation().stop(); + steve.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY); + } + + @Override + public String getDescription() { + return "Breed " + animalType + " (" + breedingsDone + "/" + targetBreedings + ")"; + } + + /** + * Attempt to breed a pair of animals + */ + private boolean attemptBreeding() { + // Find two animals that can be bred + List breedableAnimals = findBreedableAnimals(); + + if (breedableAnimals.size() < 2) { + SteveMod.LOGGER.debug("Steve '{}' found only {} breedable {}(s)", + steve.getSteveName(), breedableAnimals.size(), animalType); + return false; + } + + // Get two animals + Animal animal1 = breedableAnimals.get(0); + Animal animal2 = breedableAnimals.get(1); + + // Get breeding food + List foods = BREEDING_FOODS.get(animalType); + Item breedingFood = null; + + for (Item food : foods) { + if (InventoryHelper.hasItem(steve, food, 2)) { + breedingFood = food; + break; + } + } + + if (breedingFood == null) { + SteveMod.LOGGER.warn("Steve '{}' ran out of breeding food for {}", + steve.getSteveName(), animalType); + return false; + } + + // Move to first animal + steve.teleportTo(animal1.getX(), animal1.getY(), animal1.getZ()); + + // Feed first animal + ItemStack foodStack = new ItemStack(breedingFood); + steve.setItemInHand(InteractionHand.MAIN_HAND, foodStack); + animal1.setInLove(steve); + InventoryHelper.removeItem(steve, breedingFood, 1); + + steve.swing(InteractionHand.MAIN_HAND, true); + + // Move to second animal + steve.teleportTo(animal2.getX(), animal2.getY(), animal2.getZ()); + + // Feed second animal + animal2.setInLove(steve); + InventoryHelper.removeItem(steve, breedingFood, 1); + + steve.swing(InteractionHand.MAIN_HAND, true); + + SteveMod.LOGGER.info("Steve '{}' bred two {}(s) at ({}, {}, {})", + steve.getSteveName(), animalType, + (int)animal1.getX(), (int)animal1.getY(), (int)animal1.getZ()); + + return true; + } + + /** + * Find animals that can be bred + */ + private List findBreedableAnimals() { + List breedableAnimals = new ArrayList<>(); + List nearbyEntities = steve.level().getEntities( + steve, + steve.getBoundingBox().inflate(searchRadius) + ); + + for (Entity entity : nearbyEntities) { + if (!isMatchingAnimalType(entity)) { + continue; + } + + if (entity instanceof Animal animal) { + // Check if animal can breed (not in love, not baby, not on cooldown) + if (animal.canFallInLove() && animal.getAge() == 0) { + breedableAnimals.add(animal); + } + } + } + + // Sort by distance + breedableAnimals.sort(Comparator.comparingDouble(animal -> + animal.distanceToSqr(steve.getX(), steve.getY(), steve.getZ()) + )); + + return breedableAnimals; + } + + /** + * Check if entity matches the target animal type + */ + private boolean isMatchingAnimalType(Entity entity) { + return switch (animalType) { + case "cow" -> entity instanceof Cow && !(entity instanceof MushroomCow); + case "mooshroom" -> entity instanceof MushroomCow; + case "pig" -> entity instanceof Pig; + case "chicken" -> entity instanceof Chicken; + case "sheep" -> entity instanceof Sheep; + case "horse" -> entity instanceof Horse; + case "llama" -> entity instanceof Llama; + case "rabbit" -> entity instanceof Rabbit; + case "turtle" -> entity instanceof Turtle; + case "fox" -> entity instanceof Fox; + case "goat" -> entity instanceof Goat; + default -> false; + }; + } +} diff --git a/src/main/java/com/steve/ai/action/actions/CombatAction.java b/src/main/java/com/steve/ai/action/actions/CombatAction.java index 594ea4d..411b904 100644 --- a/src/main/java/com/steve/ai/action/actions/CombatAction.java +++ b/src/main/java/com/steve/ai/action/actions/CombatAction.java @@ -2,7 +2,9 @@ import com.steve.ai.action.ActionResult; import com.steve.ai.action.Task; +import com.steve.ai.combat.CombatEquipmentManager; import com.steve.ai.entity.SteveEntity; +import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.monster.Monster; @@ -13,14 +15,18 @@ public class CombatAction extends BaseAction { private String targetType; private LivingEntity target; + private final CombatEquipmentManager equipmentManager; private int ticksRunning; private int ticksStuck; + private int ticksSinceBlock; private double lastX, lastZ; private static final int MAX_TICKS = 600; private static final double ATTACK_RANGE = 3.5; + private static final int BLOCK_COOLDOWN = 10; // 0.5 seconds public CombatAction(SteveEntity steve, Task task) { super(steve, task); + this.equipmentManager = new CombatEquipmentManager(steve); } @Override @@ -28,14 +34,26 @@ protected void onStart() { targetType = task.getStringParameter("target"); ticksRunning = 0; ticksStuck = 0; - + ticksSinceBlock = 0; + // Make sure we're not flying (in case we were building) steve.setFlying(false); - + steve.setInvulnerableBuilding(true); - + + // Auto-equip best combat gear + String equipSummary = equipmentManager.autoEquipCombatGear(); + com.steve.ai.SteveMod.LOGGER.info("Steve '{}' combat prep: {}", + steve.getSteveName(), equipSummary); + + // Check if should retreat instead + if (TacticalRetreatAction.shouldRetreat(steve)) { + com.steve.ai.SteveMod.LOGGER.warn("Steve '{}' health/threat level suggests retreat", + steve.getSteveName()); + } + findTarget(); - + if (target == null) { com.steve.ai.SteveMod.LOGGER.warn("Steve '{}' no targets nearby", steve.getSteveName()); } @@ -44,18 +62,28 @@ protected void onStart() { @Override protected void onTick() { ticksRunning++; - + ticksSinceBlock++; + if (ticksRunning > MAX_TICKS) { // Combat complete - clean up and disable invulnerability steve.setInvulnerableBuilding(false); steve.setSprinting(false); steve.getNavigation().stop(); - com.steve.ai.SteveMod.LOGGER.info("Steve '{}' combat complete, invulnerability disabled", + stopBlocking(); + com.steve.ai.SteveMod.LOGGER.info("Steve '{}' combat complete, invulnerability disabled", steve.getSteveName()); result = ActionResult.success("Combat complete"); return; } - + + // Check if should retreat + if (TacticalRetreatAction.shouldRetreat(steve)) { + steve.setInvulnerableBuilding(false); + stopBlocking(); + result = ActionResult.failure("Health/threat critical - retreat recommended"); + return; + } + // Re-search for targets periodically or if current target is invalid if (target == null || !target.isAlive() || target.isRemoved()) { if (ticksRunning % 20 == 0) { @@ -65,7 +93,7 @@ protected void onTick() { return; // Keep searching } } - + double distance = steve.distanceTo(target); steve.setSprinting(true); @@ -99,13 +127,44 @@ protected void onTick() { lastZ = currentZ; if (distance <= ATTACK_RANGE) { + // Use shield if available and under threat + if (equipmentManager.hasShieldEquipped() && target.isAggressive()) { + if (ticksSinceBlock >= BLOCK_COOLDOWN) { + startBlocking(); + ticksSinceBlock = 0; + } + } else { + stopBlocking(); + } + + // Attack steve.doHurtTarget(target); - steve.swing(net.minecraft.world.InteractionHand.MAIN_HAND, true); - + steve.swing(InteractionHand.MAIN_HAND, true); + // Attack 3 times per second (every 6-7 ticks) if (ticksRunning % 7 == 0) { steve.doHurtTarget(target); } + } else { + stopBlocking(); + } + } + + /** + * Start blocking with shield + */ + private void startBlocking() { + if (equipmentManager.hasShieldEquipped()) { + steve.startUsingItem(InteractionHand.OFF_HAND); + } + } + + /** + * Stop blocking with shield + */ + private void stopBlocking() { + if (steve.isUsingItem()) { + steve.stopUsingItem(); } } @@ -115,8 +174,9 @@ protected void onCancel() { steve.getNavigation().stop(); steve.setSprinting(false); steve.setFlying(false); + stopBlocking(); target = null; - com.steve.ai.SteveMod.LOGGER.info("Steve '{}' combat cancelled, invulnerability disabled", + com.steve.ai.SteveMod.LOGGER.info("Steve '{}' combat cancelled, invulnerability disabled", steve.getSteveName()); } diff --git a/src/main/java/com/steve/ai/action/actions/CraftItemAction.java b/src/main/java/com/steve/ai/action/actions/CraftItemAction.java index 1d2b005..862edd7 100644 --- a/src/main/java/com/steve/ai/action/actions/CraftItemAction.java +++ b/src/main/java/com/steve/ai/action/actions/CraftItemAction.java @@ -3,11 +3,59 @@ import com.steve.ai.action.ActionResult; import com.steve.ai.action.Task; import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; +import com.steve.ai.util.RecipeHelper; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import java.util.Map; +import java.util.Optional; + +/** + * Handles crafting of items + * Workflow: + * 1. Check if recipe exists + * 2. Check if all ingredients available in inventory + * 3. If crafting table required, find or place one + * 4. Navigate to crafting table + * 5. Execute crafting + * 6. Remove ingredients from inventory + * 7. Add result to inventory + */ public class CraftItemAction extends BaseAction { + private enum CraftingPhase { + VALIDATING, // Check recipe and ingredients + FINDING_TABLE, // Search for crafting table + PLACING_TABLE, // Place crafting table if not found + NAVIGATING_TO_TABLE, // Move to crafting table + CRAFTING, // Perform crafting + COMPLETED // Done + } + private String itemName; private int quantity; private int ticksRunning; + private int craftedCount; + + private CraftingPhase phase; + private Item targetItem; + private BlockPos craftingTablePos; + + // Timeouts + private static final int MAX_TICKS = 12000; // 10 minutes + private static final int NAVIGATION_TIMEOUT = 600; // 30 seconds + private int navigationStartTick; public CraftItemAction(SteveEntity steve, Task task) { super(steve, task); @@ -18,18 +66,250 @@ protected void onStart() { itemName = task.getStringParameter("item"); quantity = task.getIntParameter("quantity", 1); ticksRunning = 0; - - // - Check if recipe exists - // - Check if Steve has ingredients - // - Navigate to crafting table if needed - // - Use Baritone crafting integration - - result = ActionResult.failure("Crafting not yet implemented", false); + craftedCount = 0; + phase = CraftingPhase.VALIDATING; + + // Parse item name + ResourceLocation itemId = ResourceLocation.tryParse(itemName); + if (itemId == null) { + // Try with minecraft: namespace + itemId = new ResourceLocation("minecraft", itemName); + } + + targetItem = BuiltInRegistries.ITEM.get(itemId); + + if (targetItem == null || targetItem == Items.AIR) { + result = ActionResult.failure("Unknown item: " + itemName, false); + phase = CraftingPhase.COMPLETED; + return; + } + + // Validate recipe exists + ServerLevel level = (ServerLevel) steve.level(); + if (!RecipeHelper.hasRecipe(level, targetItem)) { + result = ActionResult.failure("No crafting recipe for " + itemName, false); + phase = CraftingPhase.COMPLETED; + return; + } + + steve.getNavigation().stop(); } @Override protected void onTick() { ticksRunning++; + + // Timeout check + if (ticksRunning > MAX_TICKS) { + result = ActionResult.failure("Crafting timed out after " + (MAX_TICKS / 20) + " seconds", true); + return; + } + + ServerLevel level = (ServerLevel) steve.level(); + + switch (phase) { + case VALIDATING: + handleValidation(level); + break; + + case FINDING_TABLE: + handleFindingTable(level); + break; + + case PLACING_TABLE: + handlePlacingTable(level); + break; + + case NAVIGATING_TO_TABLE: + handleNavigation(level); + break; + + case CRAFTING: + handleCrafting(level); + break; + + case COMPLETED: + // Done + break; + } + } + + private void handleValidation(ServerLevel level) { + // Check if Steve has all ingredients + Map missing = RecipeHelper.getMissingIngredients(level, steve, targetItem); + + if (!missing.isEmpty()) { + StringBuilder msg = new StringBuilder("Missing ingredients: "); + for (Map.Entry entry : missing.entrySet()) { + msg.append(entry.getValue()).append("x ") + .append(entry.getKey().getDescriptionId()).append(", "); + } + result = ActionResult.failure(msg.toString(), true); + phase = CraftingPhase.COMPLETED; + return; + } + + // Check if crafting table required + if (RecipeHelper.requiresCraftingTable(level, targetItem)) { + phase = CraftingPhase.FINDING_TABLE; + } else { + // Can craft in inventory (2x2) + phase = CraftingPhase.CRAFTING; + } + } + + private void handleFindingTable(ServerLevel level) { + // Search for crafting table within 16 blocks + BlockPos stevePos = steve.blockPosition(); + craftingTablePos = null; + + for (int x = -16; x <= 16; x++) { + for (int y = -4; y <= 4; y++) { + for (int z = -16; z <= 16; z++) { + BlockPos pos = stevePos.offset(x, y, z); + BlockState state = level.getBlockState(pos); + + if (state.is(Blocks.CRAFTING_TABLE)) { + craftingTablePos = pos; + phase = CraftingPhase.NAVIGATING_TO_TABLE; + navigationStartTick = ticksRunning; + steve.getNavigation().moveTo(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5, 1.0); + return; + } + } + } + } + + // No crafting table found - need to place one + if (InventoryHelper.hasItem(steve, Items.CRAFTING_TABLE, 1)) { + phase = CraftingPhase.PLACING_TABLE; + } else { + result = ActionResult.failure("No crafting table nearby and none in inventory", true); + phase = CraftingPhase.COMPLETED; + } + } + + private void handlePlacingTable(ServerLevel level) { + // Find suitable position to place crafting table (2 blocks in front) + BlockPos stevePos = steve.blockPosition(); + BlockPos placePos = stevePos.relative(steve.getDirection()); + + // Check if position is air and has solid block below + if (level.getBlockState(placePos).isAir() && + level.getBlockState(placePos.below()).isSolidRender(level, placePos.below())) { + + // Place crafting table + level.setBlock(placePos, Blocks.CRAFTING_TABLE.defaultBlockState(), 3); + + // Remove from inventory + InventoryHelper.removeItem(steve, Items.CRAFTING_TABLE, 1); + + craftingTablePos = placePos; + phase = CraftingPhase.NAVIGATING_TO_TABLE; + navigationStartTick = ticksRunning; + steve.getNavigation().moveTo(placePos.getX() + 0.5, placePos.getY(), placePos.getZ() + 0.5, 1.0); + } else { + result = ActionResult.failure("Cannot find suitable position to place crafting table", true); + phase = CraftingPhase.COMPLETED; + } + } + + private void handleNavigation(ServerLevel level) { + // Check if reached crafting table + if (steve.blockPosition().distSqr(craftingTablePos) <= 9) { // Within 3 blocks + steve.getNavigation().stop(); + phase = CraftingPhase.CRAFTING; + return; + } + + // Check navigation timeout + if (ticksRunning - navigationStartTick > NAVIGATION_TIMEOUT) { + result = ActionResult.failure("Could not reach crafting table", true); + phase = CraftingPhase.COMPLETED; + return; + } + + // Re-navigate if path lost + if (!steve.getNavigation().isInProgress()) { + steve.getNavigation().moveTo( + craftingTablePos.getX() + 0.5, + craftingTablePos.getY(), + craftingTablePos.getZ() + 0.5, + 1.0 + ); + } + } + + private void handleCrafting(ServerLevel level) { + // Check if we need to craft more + if (craftedCount >= quantity) { + result = ActionResult.success("Crafted " + craftedCount + "x " + itemName); + phase = CraftingPhase.COMPLETED; + return; + } + + // Check if still has ingredients + if (!RecipeHelper.hasAllIngredients(level, steve, targetItem)) { + if (craftedCount > 0) { + result = ActionResult.success("Crafted " + craftedCount + "x " + itemName + " (ran out of ingredients)"); + } else { + result = ActionResult.failure("Missing ingredients for crafting", true); + } + phase = CraftingPhase.COMPLETED; + return; + } + + // Perform one crafting operation + boolean success = executeCrafting(level); + + if (success) { + craftedCount++; + + // Small delay between crafts (10 ticks = 0.5 seconds) + if (ticksRunning % 10 != 0) { + return; + } + } else { + result = ActionResult.failure("Crafting execution failed", true); + phase = CraftingPhase.COMPLETED; + } + } + + private boolean executeCrafting(ServerLevel level) { + // Get recipe + RecipeHolder holder = RecipeHelper.findRecipe(level, targetItem); + if (holder == null) { + return false; + } + + CraftingRecipe recipe = holder.value(); + + // Get required ingredients + Map ingredients = RecipeHelper.getRequiredIngredients(level, targetItem); + + // Remove ingredients from Steve's inventory + for (Map.Entry entry : ingredients.entrySet()) { + if (!InventoryHelper.removeItem(steve, entry.getKey(), entry.getValue())) { + // Failed to remove - shouldn't happen as we checked earlier + return false; + } + } + + // Get result item stack + ItemStack result = recipe.getResultItem(level.registryAccess()).copy(); + + // Add result to inventory + boolean added = InventoryHelper.addItem(steve, result); + + if (!added) { + // Inventory full - restore ingredients + for (Map.Entry entry : ingredients.entrySet()) { + InventoryHelper.addItem(steve, new ItemStack(entry.getKey(), entry.getValue())); + } + return false; + } + + return true; } @Override @@ -39,7 +319,10 @@ protected void onCancel() { @Override public String getDescription() { - return "Craft " + quantity + " " + itemName; + if (phase == CraftingPhase.CRAFTING && craftedCount > 0) { + return "Crafting " + itemName + " (" + craftedCount + "/" + quantity + ")"; + } + return "Craft " + quantity + "x " + itemName; } } diff --git a/src/main/java/com/steve/ai/action/actions/FarmAction.java b/src/main/java/com/steve/ai/action/actions/FarmAction.java new file mode 100644 index 0000000..0e98c99 --- /dev/null +++ b/src/main/java/com/steve/ai/action/actions/FarmAction.java @@ -0,0 +1,402 @@ +package com.steve.ai.action.actions; + +import com.steve.ai.SteveMod; +import com.steve.ai.action.ActionResult; +import com.steve.ai.action.Task; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.Vec3; + +import java.util.*; + +/** + * Farm action for planting, harvesting, and managing crops + * Supports wheat, carrots, potatoes, beetroot with automatic replanting + */ +public class FarmAction extends BaseAction { + private String cropType; + private String actionType; // "plant", "harvest", "farm" (both) + private int targetAmount; + private int workDone; + private int searchRadius = 16; + private int ticksRunning; + private static final int MAX_TICKS = 6000; // 5 minutes + private static final int TICK_DELAY = 20; // 1 second between operations + private int ticksSinceLastAction = 0; + + // Crop mappings + private static final Map CROP_BLOCKS = new HashMap<>() {{ + put("wheat", Blocks.WHEAT); + put("carrots", Blocks.CARROTS); + put("potatoes", Blocks.POTATOES); + put("beetroot", Blocks.BEETROOTS); + }}; + + private static final Map CROP_SEEDS = new HashMap<>() {{ + put("wheat", Items.WHEAT_SEEDS); + put("carrots", Items.CARROT); + put("potatoes", Items.POTATO); + put("beetroot", Items.BEETROOT_SEEDS); + }}; + + private static final Map CROP_PRODUCTS = new HashMap<>() {{ + put("wheat", Items.WHEAT); + put("carrots", Items.CARROT); + put("potatoes", Items.POTATO); + put("beetroot", Items.BEETROOT); + }}; + + public FarmAction(SteveEntity steve, Task task) { + super(steve, task); + } + + @Override + protected void onStart() { + cropType = task.getStringParameter("crop", "wheat").toLowerCase(); + actionType = task.getStringParameter("type", "farm").toLowerCase(); // farm, plant, harvest + targetAmount = task.getIntParameter("amount", 64); + workDone = 0; + ticksRunning = 0; + ticksSinceLastAction = 0; + + if (!CROP_BLOCKS.containsKey(cropType)) { + result = ActionResult.failure("Unknown crop type: " + cropType); + return; + } + + SteveMod.LOGGER.info("Steve '{}' starting {} action for {} (target: {})", + steve.getSteveName(), actionType, cropType, targetAmount); + + // Equip hoe for farming + equipHoe(); + } + + @Override + protected void onTick() { + ticksRunning++; + ticksSinceLastAction++; + + if (ticksRunning > MAX_TICKS) { + result = ActionResult.failure("Farming timeout - completed " + workDone + " operations"); + return; + } + + if (ticksSinceLastAction < TICK_DELAY) { + return; // Wait between actions + } + + boolean didWork = false; + + switch (actionType) { + case "plant" -> didWork = doPlanting(); + case "harvest" -> didWork = doHarvesting(); + case "farm" -> { + // Do both harvesting and planting + if (!doHarvesting()) { + didWork = doPlanting(); + } else { + didWork = true; + } + } + default -> { + result = ActionResult.failure("Unknown action type: " + actionType); + return; + } + } + + if (didWork) { + workDone++; + ticksSinceLastAction = 0; + + if (workDone >= targetAmount) { + result = ActionResult.success("Completed " + workDone + " farming operations for " + cropType); + return; + } + } else { + // No work found, check if we've done enough + if (workDone > 0) { + result = ActionResult.success("Completed " + workDone + " farming operations for " + cropType); + } else { + result = ActionResult.failure("No farmland or crops found nearby"); + } + } + } + + @Override + protected void onCancel() { + steve.getNavigation().stop(); + steve.setItemInHand(InteractionHand.MAIN_HAND, ItemStack.EMPTY); + } + + @Override + public String getDescription() { + return actionType + " " + cropType + " (" + workDone + "/" + targetAmount + ")"; + } + + /** + * Plant crops on farmland + */ + private boolean doPlanting() { + Item seedItem = CROP_SEEDS.get(cropType); + + // Check if we have seeds + if (!InventoryHelper.hasItem(steve, seedItem, 1)) { + SteveMod.LOGGER.warn("Steve '{}' has no {} seeds", steve.getSteveName(), cropType); + return false; + } + + // Find empty farmland + BlockPos farmlandPos = findEmptyFarmland(); + + if (farmlandPos == null) { + return false; + } + + // Move to farmland + steve.teleportTo(farmlandPos.getX() + 0.5, farmlandPos.getY() + 1, farmlandPos.getZ() + 0.5); + + // Plant crop + Block cropBlock = CROP_BLOCKS.get(cropType); + steve.level().setBlock(farmlandPos.above(), cropBlock.defaultBlockState(), 3); + + // Remove seed from inventory + InventoryHelper.removeItem(steve, seedItem, 1); + + steve.swing(InteractionHand.MAIN_HAND, true); + + SteveMod.LOGGER.info("Steve '{}' planted {} at {}", + steve.getSteveName(), cropType, farmlandPos); + + // Try to use bone meal if available + if (InventoryHelper.hasItem(steve, Items.BONE_MEAL, 1)) { + useBoneMeal(farmlandPos.above()); + } + + return true; + } + + /** + * Harvest fully grown crops + */ + private boolean doHarvesting() { + // Find mature crop + BlockPos cropPos = findMatureCrop(); + + if (cropPos == null) { + return false; + } + + // Move to crop + steve.teleportTo(cropPos.getX() + 0.5, cropPos.getY(), cropPos.getZ() + 0.5); + + // Harvest crop and collect drops + harvestCrop(cropPos); + + steve.swing(InteractionHand.MAIN_HAND, true); + + SteveMod.LOGGER.info("Steve '{}' harvested {} at {}", + steve.getSteveName(), cropType, cropPos); + + // Auto-replant if we have seeds + Item seedItem = CROP_SEEDS.get(cropType); + if (InventoryHelper.hasItem(steve, seedItem, 1)) { + Block cropBlock = CROP_BLOCKS.get(cropType); + steve.level().setBlock(cropPos, cropBlock.defaultBlockState(), 3); + InventoryHelper.removeItem(steve, seedItem, 1); + + SteveMod.LOGGER.info("Steve '{}' auto-replanted {} at {}", + steve.getSteveName(), cropType, cropPos); + + // Try to use bone meal if available + if (InventoryHelper.hasItem(steve, Items.BONE_MEAL, 1)) { + useBoneMeal(cropPos); + } + } + + return true; + } + + /** + * Find empty farmland to plant on + */ + private BlockPos findEmptyFarmland() { + BlockPos stevePos = steve.blockPosition(); + List farmlandPositions = new ArrayList<>(); + + for (int x = -searchRadius; x <= searchRadius; x++) { + for (int z = -searchRadius; z <= searchRadius; z++) { + for (int y = -3; y <= 3; y++) { + BlockPos checkPos = stevePos.offset(x, y, z); + BlockState state = steve.level().getBlockState(checkPos); + + // Check if it's farmland + if (state.getBlock() == Blocks.FARMLAND) { + // Check if the block above is air (empty farmland) + BlockPos abovePos = checkPos.above(); + if (steve.level().getBlockState(abovePos).isAir()) { + farmlandPositions.add(checkPos); + } + } + } + } + } + + if (farmlandPositions.isEmpty()) { + return null; + } + + // Find closest farmland + return farmlandPositions.stream() + .min(Comparator.comparingDouble(pos -> pos.distSqr(stevePos))) + .orElse(null); + } + + /** + * Find mature (fully grown) crop + */ + private BlockPos findMatureCrop() { + BlockPos stevePos = steve.blockPosition(); + Block targetCrop = CROP_BLOCKS.get(cropType); + List matureCrops = new ArrayList<>(); + + for (int x = -searchRadius; x <= searchRadius; x++) { + for (int z = -searchRadius; z <= searchRadius; z++) { + for (int y = -3; y <= 3; y++) { + BlockPos checkPos = stevePos.offset(x, y, z); + BlockState state = steve.level().getBlockState(checkPos); + + if (state.getBlock() == targetCrop) { + // Check if crop is fully grown + if (isCropMature(state)) { + matureCrops.add(checkPos); + } + } + } + } + } + + if (matureCrops.isEmpty()) { + return null; + } + + // Find closest mature crop + return matureCrops.stream() + .min(Comparator.comparingDouble(pos -> pos.distSqr(stevePos))) + .orElse(null); + } + + /** + * Check if a crop is fully mature + */ + private boolean isCropMature(BlockState state) { + if (!(state.getBlock() instanceof CropBlock cropBlock)) { + return false; + } + + // Check if crop is at max age + if (state.hasProperty(BlockStateProperties.AGE_7)) { + return state.getValue(BlockStateProperties.AGE_7) == 7; + } else if (state.hasProperty(BlockStateProperties.AGE_3)) { + return state.getValue(BlockStateProperties.AGE_3) == 3; + } + + return cropBlock.isMaxAge(state); + } + + /** + * Harvest crop and collect drops into inventory + */ + private void harvestCrop(BlockPos pos) { + BlockState state = steve.level().getBlockState(pos); + + if (state.isAir()) { + return; + } + + // Get the tool Steve is holding + ItemStack tool = steve.getItemInHand(InteractionHand.MAIN_HAND); + + // Get drops from the crop + if (steve.level() instanceof ServerLevel serverLevel) { + LootParams.Builder builder = new LootParams.Builder(serverLevel) + .withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)) + .withParameter(LootContextParams.TOOL, tool) + .withOptionalParameter(LootContextParams.BLOCK_ENTITY, steve.level().getBlockEntity(pos)); + + List drops = state.getDrops(builder); + + // Add drops to Steve's inventory + for (ItemStack drop : drops) { + if (!drop.isEmpty()) { + boolean added = InventoryHelper.addItem(steve, drop.copy()); + if (!added) { + // Inventory full - drop to world + Block.popResource(steve.level(), pos, drop); + SteveMod.LOGGER.warn("Steve '{}' inventory full, dropped {} to world", + steve.getSteveName(), drop.getItem().getDescriptionId()); + } + } + } + } + + // Destroy the crop + steve.level().destroyBlock(pos, false); + } + + /** + * Use bone meal to accelerate crop growth + */ + private void useBoneMeal(BlockPos cropPos) { + BlockState state = steve.level().getBlockState(cropPos); + + if (state.getBlock() instanceof CropBlock) { + // Apply bone meal effect + if (state.getBlock() instanceof BonemealableBlock bonemealable) { + if (steve.level() instanceof ServerLevel serverLevel) { + if (bonemealable.isValidBonemealTarget(steve.level(), cropPos, state)) { + bonemealable.performBonemeal(serverLevel, steve.level().random, cropPos, state); + + // Remove bone meal from inventory + InventoryHelper.removeItem(steve, Items.BONE_MEAL, 1); + + steve.swing(InteractionHand.MAIN_HAND, true); + + SteveMod.LOGGER.info("Steve '{}' used bone meal on {} at {}", + steve.getSteveName(), cropType, cropPos); + } + } + } + } + } + + /** + * Equip a hoe for farming + */ + private void equipHoe() { + // Check if Steve already has a hoe + for (int i = 0; i < steve.getInventory().getContainerSize(); i++) { + ItemStack stack = steve.getInventory().getItem(i); + if (stack.getItem() instanceof net.minecraft.world.item.HoeItem) { + steve.setItemInHand(InteractionHand.MAIN_HAND, stack.copy()); + SteveMod.LOGGER.info("Steve '{}' equipped existing hoe", steve.getSteveName()); + return; + } + } + + // Give Steve a basic hoe if none found + ItemStack hoe = new ItemStack(Items.IRON_HOE); + steve.setItemInHand(InteractionHand.MAIN_HAND, hoe); + SteveMod.LOGGER.info("Steve '{}' equipped new iron hoe", steve.getSteveName()); + } +} diff --git a/src/main/java/com/steve/ai/action/actions/MineBlockAction.java b/src/main/java/com/steve/ai/action/actions/MineBlockAction.java index a1b7fee..76533db 100644 --- a/src/main/java/com/steve/ai/action/actions/MineBlockAction.java +++ b/src/main/java/com/steve/ai/action/actions/MineBlockAction.java @@ -4,13 +4,19 @@ import com.steve.ai.action.ActionResult; import com.steve.ai.action.Task; import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.level.storage.loot.parameters.LootContextParams; +import net.minecraft.world.phys.Vec3; import java.util.ArrayList; import java.util.HashMap; @@ -65,14 +71,21 @@ protected void onStart() { ticksRunning = 0; ticksSinceLastTorch = 0; ticksSinceLastMine = 0; - + targetBlock = parseBlock(blockName); - + if (targetBlock == null || targetBlock == Blocks.AIR) { result = ActionResult.failure("Invalid block type: " + blockName); return; } - + + // Check if this is a wood/tree block + String targetName = targetBlock.getName().getString().toLowerCase(); + boolean isWood = targetName.contains("log") || targetName.contains("wood"); + + SteveMod.LOGGER.info("Steve '{}' mining {} (isWood: {})", + steve.getSteveName(), targetBlock.getName().getString(), isWood); + net.minecraft.world.entity.player.Player nearestPlayer = findNearestPlayer(); if (nearestPlayer != null) { net.minecraft.world.phys.Vec3 eyePos = nearestPlayer.getEyePosition(1.0F); @@ -174,24 +187,31 @@ protected void onTick() { if (steve.level().getBlockState(currentTarget).getBlock() == targetBlock) { steve.teleportTo(currentTarget.getX() + 0.5, currentTarget.getY(), currentTarget.getZ() + 0.5); - + steve.swing(InteractionHand.MAIN_HAND, true); - - steve.level().destroyBlock(currentTarget, true); + + // Mine block and collect drops into inventory + mineBlockAndCollect(currentTarget); minedCount++; ticksSinceLastMine = 0; // Reset delay timer - - SteveMod.LOGGER.info("Steve '{}' moved to ore and mined {} at {} - Total: {}/{}", - steve.getSteveName(), targetBlock.getName().getString(), currentTarget, + + // Check if inventory is getting full + if (InventoryHelper.isInventoryFull(steve)) { + SteveMod.LOGGER.warn("Steve '{}' inventory is >90% full during mining!", steve.getSteveName()); + // TODO: Auto-storage will be added in next task + } + + SteveMod.LOGGER.info("Steve '{}' moved to ore and mined {} at {} - Total: {}/{}", + steve.getSteveName(), targetBlock.getName().getString(), currentTarget, minedCount, targetQuantity); - + if (minedCount >= targetQuantity) { steve.setFlying(false); steve.setItemInHand(InteractionHand.MAIN_HAND, net.minecraft.world.item.ItemStack.EMPTY); result = ActionResult.success("Mined " + minedCount + " " + targetBlock.getName().getString()); return; } - + currentTarget = null; } else { currentTarget = null; @@ -255,57 +275,92 @@ private BlockPos findTorchPosition(BlockPos center) { } /** - * Mine forward in ONE DIRECTION - creates a straight tunnel! - * Steve progresses forward block by block + * Mine forward - tunnel for ores, explore for surface blocks */ private void mineNearbyBlock() { + String targetName = targetBlock.getName().getString().toLowerCase(); + boolean isWood = targetName.contains("log") || targetName.contains("wood"); + + if (isWood) { + // For wood: just search again, don't tunnel + SteveMod.LOGGER.info("Steve '{}' couldn't find wood, searching again...", steve.getSteveName()); + // Will search again on next tick + ticksSinceLastMine = 0; + return; + } + + // Original tunnel mining for ores BlockPos centerPos = currentTunnelPos; BlockPos abovePos = centerPos.above(); BlockPos belowPos = centerPos.below(); - + BlockState centerState = steve.level().getBlockState(centerPos); if (!centerState.isAir() && centerState.getBlock() != Blocks.BEDROCK) { steve.teleportTo(centerPos.getX() + 0.5, centerPos.getY(), centerPos.getZ() + 0.5); steve.swing(InteractionHand.MAIN_HAND, true); - steve.level().destroyBlock(centerPos, true); + mineBlockAndCollect(centerPos); SteveMod.LOGGER.info("Steve '{}' mining tunnel at {}", steve.getSteveName(), centerPos); } - + BlockState aboveState = steve.level().getBlockState(abovePos); if (!aboveState.isAir() && aboveState.getBlock() != Blocks.BEDROCK) { steve.swing(InteractionHand.MAIN_HAND, true); - steve.level().destroyBlock(abovePos, true); + mineBlockAndCollect(abovePos); } - + BlockState belowState = steve.level().getBlockState(belowPos); if (!belowState.isAir() && belowState.getBlock() != Blocks.BEDROCK) { steve.swing(InteractionHand.MAIN_HAND, true); - steve.level().destroyBlock(belowPos, true); + mineBlockAndCollect(belowPos); } - + currentTunnelPos = currentTunnelPos.offset(miningDirectionX, 0, miningDirectionZ); - + ticksSinceLastMine = 0; // Reset delay } /** - * Find ore blocks in the tunnel ahead - * Searches forward in the mining direction + * Find blocks - different strategy for surface blocks (wood) vs ores */ private void findNextBlock() { List foundBlocks = new ArrayList<>(); - - for (int distance = 0; distance < 20; distance++) { - BlockPos checkPos = currentTunnelPos.offset(miningDirectionX * distance, 0, miningDirectionZ * distance); - - for (int y = -1; y <= 1; y++) { - BlockPos orePos = checkPos.offset(0, y, 0); - if (steve.level().getBlockState(orePos).getBlock() == targetBlock) { - foundBlocks.add(orePos); + + // Check if this is a wood/log block + String targetName = targetBlock.getName().getString().toLowerCase(); + boolean isWood = targetName.contains("log") || targetName.contains("wood"); + + if (isWood) { + // Search for wood blocks on surface and upwards (trees!) + BlockPos stevePos = steve.blockPosition(); + int searchRadius = 30; // Larger radius for trees + + for (int x = -searchRadius; x <= searchRadius; x++) { + for (int z = -searchRadius; z <= searchRadius; z++) { + for (int y = -5; y <= 20; y++) { // Look up for trees! + BlockPos checkPos = stevePos.offset(x, y, z); + if (steve.level().getBlockState(checkPos).getBlock() == targetBlock) { + foundBlocks.add(checkPos); + } + } + } + } + + SteveMod.LOGGER.info("Steve '{}' found {} wood blocks nearby", + steve.getSteveName(), foundBlocks.size()); + } else { + // Original ore search - tunnel ahead + for (int distance = 0; distance < 20; distance++) { + BlockPos checkPos = currentTunnelPos.offset(miningDirectionX * distance, 0, miningDirectionZ * distance); + + for (int y = -1; y <= 1; y++) { + BlockPos orePos = checkPos.offset(0, y, 0); + if (steve.level().getBlockState(orePos).getBlock() == targetBlock) { + foundBlocks.add(orePos); + } } } } - + if (!foundBlocks.isEmpty()) { currentTarget = foundBlocks.stream() .min((a, b) -> Double.compare(a.distSqr(currentTunnelPos), b.distSqr(currentTunnelPos))) @@ -360,7 +415,7 @@ private net.minecraft.world.entity.player.Player findNearestPlayer() { private Block parseBlock(String blockName) { blockName = blockName.toLowerCase().replace(" ", "_"); - + Map resourceToOre = new HashMap<>() {{ put("iron", "iron_ore"); put("diamond", "diamond_ore"); @@ -370,18 +425,76 @@ private Block parseBlock(String blockName) { put("redstone", "redstone_ore"); put("lapis", "lapis_ore"); put("emerald", "emerald_ore"); + // Wood/tree mappings + put("wood", "oak_log"); + put("tree", "oak_log"); + put("log", "oak_log"); + put("oak", "oak_log"); + put("birch", "birch_log"); + put("spruce", "spruce_log"); + put("jungle", "jungle_log"); + put("acacia", "acacia_log"); + put("dark_oak", "dark_oak_log"); + // Common block mappings + put("stone", "stone"); + put("cobblestone", "cobblestone"); + put("dirt", "dirt"); + put("sand", "sand"); + put("gravel", "gravel"); }}; - + if (resourceToOre.containsKey(blockName)) { blockName = resourceToOre.get(blockName); } - + if (!blockName.contains(":")) { blockName = "minecraft:" + blockName; } - + ResourceLocation resourceLocation = new ResourceLocation(blockName); return BuiltInRegistries.BLOCK.get(resourceLocation); } + + /** + * Mine a block and collect drops directly into Steve's inventory + * Instead of dropping items into world, add them to inventory + */ + private void mineBlockAndCollect(BlockPos pos) { + BlockState state = steve.level().getBlockState(pos); + + if (state.isAir() || state.getBlock() == Blocks.BEDROCK) { + return; + } + + // Get the tool Steve is holding + ItemStack tool = steve.getItemInHand(InteractionHand.MAIN_HAND); + + // Get drops from the block + if (steve.level() instanceof ServerLevel serverLevel) { + // Build loot context + LootParams.Builder builder = new LootParams.Builder(serverLevel) + .withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(pos)) + .withParameter(LootContextParams.TOOL, tool) + .withOptionalParameter(LootContextParams.BLOCK_ENTITY, steve.level().getBlockEntity(pos)); + + List drops = state.getDrops(builder); + + // Add drops to Steve's inventory + for (ItemStack drop : drops) { + if (!drop.isEmpty()) { + boolean added = InventoryHelper.addItem(steve, drop.copy()); + if (!added) { + // Inventory full - drop to world + Block.popResource(steve.level(), pos, drop); + SteveMod.LOGGER.warn("Steve '{}' inventory full, dropped {} to world", + steve.getSteveName(), drop.getItem().getDescriptionId()); + } + } + } + } + + // Destroy the block (no drops, we handled them) + steve.level().destroyBlock(pos, false); + } } diff --git a/src/main/java/com/steve/ai/action/actions/PlaceChestAction.java b/src/main/java/com/steve/ai/action/actions/PlaceChestAction.java new file mode 100644 index 0000000..64b1bfd --- /dev/null +++ b/src/main/java/com/steve/ai/action/actions/PlaceChestAction.java @@ -0,0 +1,193 @@ +package com.steve.ai.action.actions; + +import com.steve.ai.action.ActionResult; +import com.steve.ai.action.Task; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Places a chest at a suitable location + * Workflow: + * 1. Check if Steve has a chest in inventory + * 2. Find suitable position (near Steve, on solid ground) + * 3. Place chest + * 4. Navigate to chest (optional) + */ +public class PlaceChestAction extends BaseAction { + private enum PlacementPhase { + VALIDATING, // Check if has chest + FINDING_SPOT, // Find suitable position + PLACING, // Place the chest + COMPLETED // Done + } + + private PlacementPhase phase; + private BlockPos targetPos; + private int ticksRunning; + private static final int MAX_TICKS = 600; // 30 seconds + + public PlaceChestAction(SteveEntity steve, Task task) { + super(steve, task); + } + + @Override + protected void onStart() { + phase = PlacementPhase.VALIDATING; + ticksRunning = 0; + targetPos = null; + steve.getNavigation().stop(); + } + + @Override + protected void onTick() { + ticksRunning++; + + if (ticksRunning > MAX_TICKS) { + result = ActionResult.failure("Chest placement timed out", false); + return; + } + + switch (phase) { + case VALIDATING: + handleValidation(); + break; + + case FINDING_SPOT: + handleFindingSpot(); + break; + + case PLACING: + handlePlacing(); + break; + + case COMPLETED: + // Done + break; + } + } + + private void handleValidation() { + // Check if Steve has a chest + if (!InventoryHelper.hasItem(steve, Items.CHEST, 1)) { + result = ActionResult.failure("No chest in inventory", true); + phase = PlacementPhase.COMPLETED; + return; + } + + phase = PlacementPhase.FINDING_SPOT; + } + + private void handleFindingSpot() { + // Find suitable position near Steve + BlockPos stevePos = steve.blockPosition(); + + // Try positions in a spiral pattern around Steve + for (int radius = 1; radius <= 5; radius++) { + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + // Only check perimeter of current radius + if (Math.abs(x) != radius && Math.abs(z) != radius) { + continue; + } + + BlockPos checkPos = stevePos.offset(x, 0, z); + + // Find ground level + for (int y = 3; y >= -3; y--) { + BlockPos groundCheck = checkPos.offset(0, y, 0); + BlockPos aboveGround = groundCheck.above(); + + BlockState groundState = steve.level().getBlockState(groundCheck); + BlockState aboveState = steve.level().getBlockState(aboveGround); + + // Check if this is a good spot: + // - Ground is solid + // - Space above is air + // - Not too far from Steve + if (groundState.isSolidRender(steve.level(), groundCheck) && + aboveState.isAir() && + steve.distanceToSqr(aboveGround.getX() + 0.5, aboveGround.getY(), aboveGround.getZ() + 0.5) < 64) { + + targetPos = aboveGround; + phase = PlacementPhase.PLACING; + return; + } + } + } + } + } + + // No suitable position found - place at Steve's feet + BlockPos fallbackPos = stevePos.below(); + if (steve.level().getBlockState(fallbackPos).isAir()) { + targetPos = fallbackPos; + phase = PlacementPhase.PLACING; + } else { + result = ActionResult.failure("Cannot find suitable position for chest", false); + phase = PlacementPhase.COMPLETED; + } + } + + private void handlePlacing() { + if (targetPos == null) { + result = ActionResult.failure("No target position for chest", false); + phase = PlacementPhase.COMPLETED; + return; + } + + // Double-check the position is still valid + BlockState currentState = steve.level().getBlockState(targetPos); + if (!currentState.isAir()) { + result = ActionResult.failure("Target position is no longer empty", false); + phase = PlacementPhase.COMPLETED; + return; + } + + // Place the chest + BlockState chestState = Blocks.CHEST.defaultBlockState(); + steve.level().setBlock(targetPos, chestState, 3); + + // Remove chest from inventory + boolean removed = InventoryHelper.removeItem(steve, Items.CHEST, 1); + + if (removed) { + // Record the new chest in memory + steve.getMemory().recordChest(targetPos); + + result = ActionResult.success("Placed chest at " + targetPos.toShortString()); + // Store the chest position in task parameters for retrieval by other actions + task.getParameters().put("chest_position", targetPos); + } else { + // This shouldn't happen as we checked earlier + result = ActionResult.failure("Failed to remove chest from inventory", false); + } + + phase = PlacementPhase.COMPLETED; + } + + @Override + protected void onCancel() { + steve.getNavigation().stop(); + } + + @Override + public String getDescription() { + if (phase == PlacementPhase.PLACING && targetPos != null) { + return "Placing chest at " + targetPos.toShortString(); + } + return "Place chest"; + } + + /** + * Get the position where the chest was placed (after completion) + * @return BlockPos of placed chest, or null if not yet placed + */ + public BlockPos getPlacedChestPosition() { + return targetPos; + } +} diff --git a/src/main/java/com/steve/ai/action/actions/PortalBuilderAction.java b/src/main/java/com/steve/ai/action/actions/PortalBuilderAction.java new file mode 100644 index 0000000..cf3b581 --- /dev/null +++ b/src/main/java/com/steve/ai/action/actions/PortalBuilderAction.java @@ -0,0 +1,201 @@ +package com.steve.ai.action.actions; + +import com.steve.ai.SteveMod; +import com.steve.ai.action.ActionResult; +import com.steve.ai.action.Task; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Blocks; + +/** + * Builds nether portals automatically + * Handles obsidian placement and portal lighting + */ +public class PortalBuilderAction extends BaseAction { + private BlockPos portalBasePos; + private int ticksRunning; + private int blocksPlaced; + private static final int MAX_TICKS = 600; // 30 seconds + private static final int REQUIRED_OBSIDIAN = 10; // Minimum for portal frame + + // Portal frame positions (relative to base) + private static final int[][] PORTAL_FRAME = { + // Bottom + {0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}, + // Left side + {0, 1, 0}, {0, 2, 0}, {0, 3, 0}, {0, 4, 0}, + // Right side + {3, 1, 0}, {3, 2, 0}, {3, 3, 0}, {3, 4, 0}, + // Top + {0, 4, 0}, {1, 4, 0}, {2, 4, 0}, {3, 4, 0} + }; + + public PortalBuilderAction(SteveEntity steve, Task task) { + super(steve, task); + } + + @Override + protected void onStart() { + ticksRunning = 0; + blocksPlaced = 0; + + // Check if we have obsidian + int obsidianCount = InventoryHelper.getItemCount(steve, Items.OBSIDIAN); + if (obsidianCount < REQUIRED_OBSIDIAN) { + result = ActionResult.failure("Need at least " + REQUIRED_OBSIDIAN + " obsidian (have: " + obsidianCount + ")"); + return; + } + + // Check if we have flint and steel + if (!InventoryHelper.hasItem(steve, Items.FLINT_AND_STEEL, 1)) { + result = ActionResult.failure("Need flint and steel to light portal"); + return; + } + + // Set portal base position in front of Steve + portalBasePos = steve.blockPosition().relative(steve.getDirection(), 2); + + SteveMod.LOGGER.info("Steve '{}' building nether portal at {}", + steve.getSteveName(), portalBasePos); + + // Enable flying for easier building + steve.setFlying(true); + } + + @Override + protected void onTick() { + ticksRunning++; + + if (ticksRunning > MAX_TICKS) { + steve.setFlying(false); + result = ActionResult.failure("Portal building timeout"); + return; + } + + // Build portal frame + if (blocksPlaced < PORTAL_FRAME.length) { + placeNextObsidian(); + return; + } + + // Light the portal + if (!isPortalLit()) { + lightPortal(); + return; + } + + // Portal complete + steve.setFlying(false); + result = ActionResult.success("Nether portal built and lit"); + } + + @Override + protected void onCancel() { + steve.setFlying(false); + steve.getNavigation().stop(); + } + + @Override + public String getDescription() { + return "Build nether portal (" + blocksPlaced + "/" + PORTAL_FRAME.length + " blocks)"; + } + + /** + * Place next obsidian block in portal frame + */ + private void placeNextObsidian() { + if (blocksPlaced >= PORTAL_FRAME.length) { + return; + } + + int[] offset = PORTAL_FRAME[blocksPlaced]; + BlockPos targetPos = portalBasePos.offset(offset[0], offset[1], offset[2]); + + // Check if block already exists + if (!steve.level().getBlockState(targetPos).isAir() && + steve.level().getBlockState(targetPos).getBlock() != Blocks.OBSIDIAN) { + // Skip if something else is there + blocksPlaced++; + return; + } + + // Move to position + steve.teleportTo(targetPos.getX() + 0.5, targetPos.getY(), targetPos.getZ() + 0.5); + + // Place obsidian + ItemStack obsidian = new ItemStack(Items.OBSIDIAN); + steve.setItemInHand(InteractionHand.MAIN_HAND, obsidian); + + steve.level().setBlock(targetPos, Blocks.OBSIDIAN.defaultBlockState(), 3); + steve.swing(InteractionHand.MAIN_HAND, true); + + // Remove from inventory + InventoryHelper.removeItem(steve, Items.OBSIDIAN, 1); + + blocksPlaced++; + + SteveMod.LOGGER.debug("Steve '{}' placed obsidian {}/{}", + steve.getSteveName(), blocksPlaced, PORTAL_FRAME.length); + } + + /** + * Light the portal with flint and steel + */ + private void lightPortal() { + // Light position (inside bottom of portal) + BlockPos lightPos = portalBasePos.offset(1, 1, 0); + + // Move to lighting position + steve.teleportTo(lightPos.getX() + 0.5, lightPos.getY(), lightPos.getZ() + 0.5); + + // Equip flint and steel + ItemStack flintAndSteel = new ItemStack(Items.FLINT_AND_STEEL); + steve.setItemInHand(InteractionHand.MAIN_HAND, flintAndSteel); + + // Create fire + BlockPos firePos = lightPos; + steve.level().setBlock(firePos, Blocks.FIRE.defaultBlockState(), 3); + steve.swing(InteractionHand.MAIN_HAND, true); + + SteveMod.LOGGER.info("Steve '{}' lit nether portal", steve.getSteveName()); + } + + /** + * Check if portal is lit (has nether portal blocks) + */ + private boolean isPortalLit() { + // Check interior positions for portal blocks + for (int y = 1; y <= 3; y++) { + for (int x = 1; x <= 2; x++) { + BlockPos checkPos = portalBasePos.offset(x, y, 0); + if (steve.level().getBlockState(checkPos).getBlock() == Blocks.NETHER_PORTAL) { + return true; + } + } + } + return false; + } + + /** + * Check if Steve has materials for portal + */ + public static boolean hasMaterials(SteveEntity steve) { + return InventoryHelper.getItemCount(steve, Items.OBSIDIAN) >= REQUIRED_OBSIDIAN && + InventoryHelper.hasItem(steve, Items.FLINT_AND_STEEL, 1); + } + + /** + * Get materials summary + */ + public static String getMaterialsSummary(SteveEntity steve) { + int obsidian = InventoryHelper.getItemCount(steve, Items.OBSIDIAN); + boolean hasFlint = InventoryHelper.hasItem(steve, Items.FLINT_AND_STEEL, 1); + + return String.format("Obsidian: %d/%d, Flint & Steel: %s", + obsidian, REQUIRED_OBSIDIAN, hasFlint ? "Yes" : "No"); + } +} diff --git a/src/main/java/com/steve/ai/action/actions/RangedCombatAction.java b/src/main/java/com/steve/ai/action/actions/RangedCombatAction.java new file mode 100644 index 0000000..33b7c9c --- /dev/null +++ b/src/main/java/com/steve/ai/action/actions/RangedCombatAction.java @@ -0,0 +1,271 @@ +package com.steve.ai.action.actions; + +import com.steve.ai.SteveMod; +import com.steve.ai.action.ActionResult; +import com.steve.ai.action.Task; +import com.steve.ai.combat.CombatEquipmentManager; +import com.steve.ai.entity.SteveEntity; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.projectile.AbstractArrow; +import net.minecraft.world.entity.projectile.Arrow; +import net.minecraft.world.item.BowItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; + +import java.util.List; + +/** + * Ranged combat action using bow and arrows + * Maintains distance from enemies while shooting + */ +public class RangedCombatAction extends BaseAction { + private String targetType; + private LivingEntity target; + private final CombatEquipmentManager equipmentManager; + private int ticksRunning; + private int ticksSinceLastShot; + private static final int MAX_TICKS = 1200; // 60 seconds + private static final double OPTIMAL_RANGE = 15.0; // Optimal shooting distance + private static final double MIN_RANGE = 8.0; // Minimum safe distance + private static final double MAX_RANGE = 32.0; // Maximum targeting range + private static final int SHOT_COOLDOWN = 20; // 1 second between shots + + public RangedCombatAction(SteveEntity steve, Task task) { + super(steve, task); + this.equipmentManager = new CombatEquipmentManager(steve); + } + + @Override + protected void onStart() { + targetType = task.getStringParameter("target", "hostile"); + ticksRunning = 0; + ticksSinceLastShot = 0; + + // Ensure not flying + steve.setFlying(false); + steve.setInvulnerableBuilding(true); + + // Auto-equip bow and arrows + if (!equipmentManager.hasBowEquipped() || !equipmentManager.hasArrows()) { + boolean equipped = equipmentManager.equipBowAndArrows(); + if (!equipped) { + result = ActionResult.failure("No bow or arrows available"); + return; + } + } + + // Find initial target + findTarget(); + + if (target == null) { + SteveMod.LOGGER.warn("Steve '{}' no targets nearby for ranged combat", steve.getSteveName()); + } else { + SteveMod.LOGGER.info("Steve '{}' starting ranged combat against {}", + steve.getSteveName(), target.getType().toString()); + } + } + + @Override + protected void onTick() { + ticksRunning++; + ticksSinceLastShot++; + + if (ticksRunning > MAX_TICKS) { + steve.setInvulnerableBuilding(false); + steve.getNavigation().stop(); + result = ActionResult.success("Ranged combat complete"); + return; + } + + // Check if still have arrows + if (!equipmentManager.hasArrows()) { + steve.setInvulnerableBuilding(false); + result = ActionResult.failure("Out of arrows"); + return; + } + + // Re-search for targets periodically or if current target is invalid + if (target == null || !target.isAlive() || target.isRemoved()) { + if (ticksRunning % 40 == 0) { + findTarget(); + } + if (target == null) { + return; // Keep searching + } + } + + double distance = steve.distanceTo(target); + + // Maintain optimal range + if (distance < MIN_RANGE) { + // Too close - back away + retreatFromTarget(); + } else if (distance > OPTIMAL_RANGE + 5) { + // Too far - move closer + steve.getNavigation().moveTo(target, 1.0); + } else { + // Good range - stop and shoot + steve.getNavigation().stop(); + + // Face target + steve.getLookControl().setLookAt(target, 30.0F, 30.0F); + + // Shoot if cooldown elapsed + if (ticksSinceLastShot >= SHOT_COOLDOWN) { + shootArrow(); + ticksSinceLastShot = 0; + } + } + } + + @Override + protected void onCancel() { + steve.setInvulnerableBuilding(false); + steve.getNavigation().stop(); + target = null; + SteveMod.LOGGER.info("Steve '{}' ranged combat cancelled", steve.getSteveName()); + } + + @Override + public String getDescription() { + return "Ranged attack " + targetType; + } + + /** + * Shoot arrow at target + */ + private void shootArrow() { + if (target == null || !target.isAlive()) { + return; + } + + // Get bow + ItemStack bow = steve.getMainHandItem(); + if (!(bow.getItem() instanceof BowItem)) { + return; + } + + // Create arrow entity + Arrow arrow = new Arrow(steve.level(), steve); + + // Calculate trajectory to target + double dx = target.getX() - steve.getX(); + double dy = target.getY() + target.getEyeHeight() - steve.getY() - steve.getEyeHeight(); + double dz = target.getZ() - steve.getZ(); + double distance = Math.sqrt(dx * dx + dz * dz); + + // Add arc for distance + double arcAdjustment = distance * 0.05; + dy += arcAdjustment; + + // Normalize and set arrow velocity + double speed = 3.0; // Arrow speed + double length = Math.sqrt(dx * dx + dy * dy + dz * dz); + + arrow.shoot(dx / length * speed, dy / length * speed, dz / length * speed, (float)speed, 1.0F); + + // Set arrow properties + arrow.setBaseDamage(2.0); + arrow.setOwner(steve); + arrow.pickup = AbstractArrow.Pickup.CREATIVE_ONLY; + + // Spawn arrow + steve.level().addFreshEntity(arrow); + + // Play animation + steve.swing(InteractionHand.MAIN_HAND, true); + + SteveMod.LOGGER.debug("Steve '{}' shot arrow at {} (distance: {}m)", + steve.getSteveName(), target.getType().toString(), (int)steve.distanceTo(target)); + } + + /** + * Retreat from target to maintain safe distance + */ + private void retreatFromTarget() { + if (target == null) { + return; + } + + // Calculate retreat vector (opposite direction from target) + double dx = steve.getX() - target.getX(); + double dz = steve.getZ() - target.getZ(); + double distance = Math.sqrt(dx * dx + dz * dz); + + if (distance < 0.1) { + // Avoid division by zero + dx = 1.0; + dz = 0.0; + distance = 1.0; + } + + // Normalize and scale to retreat distance + double retreatDistance = 3.0; + double targetX = steve.getX() + (dx / distance) * retreatDistance; + double targetZ = steve.getZ() + (dz / distance) * retreatDistance; + + // Move to retreat position + steve.getNavigation().moveTo(targetX, steve.getY(), targetZ, 1.5); + + SteveMod.LOGGER.debug("Steve '{}' retreating from target (distance: {}m)", + steve.getSteveName(), (int)distance); + } + + /** + * Find nearest valid target + */ + private void findTarget() { + AABB searchBox = steve.getBoundingBox().inflate(MAX_RANGE); + List entities = steve.level().getEntities(steve, searchBox); + + LivingEntity nearest = null; + double nearestDistance = Double.MAX_VALUE; + + for (Entity entity : entities) { + if (entity instanceof LivingEntity living && isValidTarget(living)) { + double distance = steve.distanceTo(living); + if (distance < nearestDistance) { + nearest = living; + nearestDistance = distance; + } + } + } + + target = nearest; + if (target != null) { + SteveMod.LOGGER.info("Steve '{}' locked onto: {} at {}m", + steve.getSteveName(), target.getType().toString(), (int)nearestDistance); + } + } + + /** + * Check if entity is a valid target + */ + private boolean isValidTarget(LivingEntity entity) { + if (!entity.isAlive() || entity.isRemoved()) { + return false; + } + + // Don't attack other Steves or players + if (entity instanceof SteveEntity || entity instanceof net.minecraft.world.entity.player.Player) { + return false; + } + + String targetLower = targetType.toLowerCase(); + + // Match ANY hostile mob + if (targetLower.contains("mob") || targetLower.contains("hostile") || + targetLower.contains("monster") || targetLower.equals("any")) { + return entity instanceof Monster; + } + + // Match specific entity type + String entityTypeName = entity.getType().toString().toLowerCase(); + return entityTypeName.contains(targetLower); + } +} diff --git a/src/main/java/com/steve/ai/action/actions/RetrieveItemsAction.java b/src/main/java/com/steve/ai/action/actions/RetrieveItemsAction.java new file mode 100644 index 0000000..92f2484 --- /dev/null +++ b/src/main/java/com/steve/ai/action/actions/RetrieveItemsAction.java @@ -0,0 +1,250 @@ +package com.steve.ai.action.actions; + +import com.steve.ai.action.ActionResult; +import com.steve.ai.action.Task; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Retrieves items from a chest into Steve's inventory + * Workflow: + * 1. Parse item name from parameters + * 2. Find nearest chest + * 3. Navigate to chest + * 4. Retrieve requested items + */ +public class RetrieveItemsAction extends BaseAction { + private enum RetrievalPhase { + VALIDATING, // Validate item name + FINDING_CHEST, // Search for chest with items + NAVIGATING, // Move to chest + RETRIEVING, // Take items from chest + COMPLETED // Done + } + + private RetrievalPhase phase; + private BlockPos chestPos; + private Item targetItem; + private int targetQuantity; + private int ticksRunning; + private int navigationStartTick; + private int itemsRetrieved; + + private static final int MAX_TICKS = 6000; // 5 minutes + private static final int NAVIGATION_TIMEOUT = 600; // 30 seconds + private static final int SEARCH_RADIUS = 16; // Search radius for chests + + public RetrieveItemsAction(SteveEntity steve, Task task) { + super(steve, task); + } + + @Override + protected void onStart() { + phase = RetrievalPhase.VALIDATING; + ticksRunning = 0; + itemsRetrieved = 0; + chestPos = null; + targetQuantity = task.getIntParameter("quantity", 1); + + steve.getNavigation().stop(); + } + + @Override + protected void onTick() { + ticksRunning++; + + if (ticksRunning > MAX_TICKS) { + result = ActionResult.failure("Retrieval operation timed out", false); + return; + } + + switch (phase) { + case VALIDATING: + handleValidation(); + break; + + case FINDING_CHEST: + handleFindingChest(); + break; + + case NAVIGATING: + handleNavigation(); + break; + + case RETRIEVING: + handleRetrieving(); + break; + + case COMPLETED: + // Done + break; + } + } + + private void handleValidation() { + // Parse item name + String itemName = task.getStringParameter("item"); + + if (itemName == null || itemName.isEmpty()) { + result = ActionResult.failure("No item specified for retrieval", false); + phase = RetrievalPhase.COMPLETED; + return; + } + + // Parse item resource location + ResourceLocation itemId = ResourceLocation.tryParse(itemName); + if (itemId == null) { + itemId = new ResourceLocation("minecraft", itemName); + } + + targetItem = BuiltInRegistries.ITEM.get(itemId); + + if (targetItem == null || targetItem == Items.AIR) { + result = ActionResult.failure("Unknown item: " + itemName, false); + phase = RetrievalPhase.COMPLETED; + return; + } + + phase = RetrievalPhase.FINDING_CHEST; + } + + private void handleFindingChest() { + // Search for nearest chest + // Note: This finds ANY chest, not necessarily one with the target item + // A more advanced implementation would check chest contents + chestPos = InventoryHelper.findNearestChest( + steve.level(), + steve.blockPosition(), + SEARCH_RADIUS + ); + + if (chestPos != null) { + // Found chest - navigate to it + phase = RetrievalPhase.NAVIGATING; + navigationStartTick = ticksRunning; + steve.getNavigation().moveTo( + chestPos.getX() + 0.5, + chestPos.getY(), + chestPos.getZ() + 0.5, + 1.0 + ); + } else { + result = ActionResult.failure("No chest found within " + SEARCH_RADIUS + " blocks", true); + phase = RetrievalPhase.COMPLETED; + } + } + + private void handleNavigation() { + // Check if reached chest + if (steve.blockPosition().distSqr(chestPos) <= 9) { // Within 3 blocks + steve.getNavigation().stop(); + phase = RetrievalPhase.RETRIEVING; + return; + } + + // Check navigation timeout + if (ticksRunning - navigationStartTick > NAVIGATION_TIMEOUT) { + result = ActionResult.failure("Could not reach chest", false); + phase = RetrievalPhase.COMPLETED; + return; + } + + // Re-navigate if path lost + if (!steve.getNavigation().isInProgress()) { + steve.getNavigation().moveTo( + chestPos.getX() + 0.5, + chestPos.getY(), + chestPos.getZ() + 0.5, + 1.0 + ); + } + } + + private void handleRetrieving() { + // Verify chest still exists + BlockState chestState = steve.level().getBlockState(chestPos); + if (!chestState.is(Blocks.CHEST) && !chestState.is(Blocks.BARREL)) { + result = ActionResult.failure("Chest disappeared", false); + phase = RetrievalPhase.COMPLETED; + return; + } + + // Check if inventory has space + if (InventoryHelper.isInventoryFull(steve)) { + result = ActionResult.failure("Inventory is full, cannot retrieve items", false); + phase = RetrievalPhase.COMPLETED; + return; + } + + // Retrieve items from chest + itemsRetrieved = InventoryHelper.retrieveFromChest( + steve, + chestPos, + targetItem, + targetQuantity + ); + + if (itemsRetrieved > 0) { + // Update chest memory after retrieving + steve.getMemory().updateChest(chestPos); + + result = ActionResult.success( + "Retrieved " + itemsRetrieved + "x " + + targetItem.getDescriptionId() + " from chest" + ); + } else { + result = ActionResult.failure( + "Chest does not contain " + targetItem.getDescriptionId() + + " or inventory is full", + false + ); + } + + phase = RetrievalPhase.COMPLETED; + } + + @Override + protected void onCancel() { + steve.getNavigation().stop(); + } + + @Override + public String getDescription() { + switch (phase) { + case VALIDATING: + return "Validating retrieval request"; + case FINDING_CHEST: + return "Finding chest with " + (targetItem != null ? targetItem.getDescriptionId() : "items"); + case NAVIGATING: + return "Moving to chest"; + case RETRIEVING: + return "Retrieving " + targetQuantity + "x " + + (targetItem != null ? targetItem.getDescriptionId() : "items"); + default: + return "Retrieve items"; + } + } + + /** + * Get the number of items successfully retrieved + * @return Count of items retrieved + */ + public int getItemsRetrieved() { + return itemsRetrieved; + } + + /** + * Get the chest position used for retrieval + * @return BlockPos of chest + */ + public BlockPos getChestPosition() { + return chestPos; + } +} diff --git a/src/main/java/com/steve/ai/action/actions/StoreItemsAction.java b/src/main/java/com/steve/ai/action/actions/StoreItemsAction.java new file mode 100644 index 0000000..067e627 --- /dev/null +++ b/src/main/java/com/steve/ai/action/actions/StoreItemsAction.java @@ -0,0 +1,263 @@ +package com.steve.ai.action.actions; + +import com.steve.ai.action.ActionResult; +import com.steve.ai.action.Task; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Stores items from Steve's inventory into a nearby chest + * Workflow: + * 1. Find nearest chest (or place one if needed) + * 2. Navigate to chest + * 3. Transfer items to chest + * 4. Return success with count of items stored + */ +public class StoreItemsAction extends BaseAction { + private enum StoragePhase { + FINDING_CHEST, // Search for nearby chest + PLACING_CHEST, // Place chest if none found + NAVIGATING, // Move to chest + STORING, // Transfer items + COMPLETED // Done + } + + private StoragePhase phase; + private BlockPos chestPos; + private Item itemToStore; // null = store all + private int ticksRunning; + private int navigationStartTick; + private int itemsStored; + + private static final int MAX_TICKS = 6000; // 5 minutes + private static final int NAVIGATION_TIMEOUT = 600; // 30 seconds + private static final int SEARCH_RADIUS = 16; // Search radius for chests + + public StoreItemsAction(SteveEntity steve, Task task) { + super(steve, task); + } + + @Override + protected void onStart() { + phase = StoragePhase.FINDING_CHEST; + ticksRunning = 0; + itemsStored = 0; + chestPos = null; + + // Check if specific item was requested + String itemName = task.getStringParameter("item", null); + if (itemName != null && !itemName.isEmpty()) { + // Parse item (simplified - could use registry lookup) + // For now, null means "store all" + itemToStore = null; // TODO: Parse item name + } else { + itemToStore = null; // Store all items + } + + steve.getNavigation().stop(); + } + + @Override + protected void onTick() { + ticksRunning++; + + if (ticksRunning > MAX_TICKS) { + result = ActionResult.failure("Storage operation timed out", false); + return; + } + + switch (phase) { + case FINDING_CHEST: + handleFindingChest(); + break; + + case PLACING_CHEST: + handlePlacingChest(); + break; + + case NAVIGATING: + handleNavigation(); + break; + + case STORING: + handleStoring(); + break; + + case COMPLETED: + // Done + break; + } + } + + private void handleFindingChest() { + // Search for nearby chest + chestPos = InventoryHelper.findNearestChest( + steve.level(), + steve.blockPosition(), + SEARCH_RADIUS + ); + + if (chestPos != null) { + // Found chest - navigate to it + phase = StoragePhase.NAVIGATING; + navigationStartTick = ticksRunning; + steve.getNavigation().moveTo( + chestPos.getX() + 0.5, + chestPos.getY(), + chestPos.getZ() + 0.5, + 1.0 + ); + } else { + // No chest found - need to place one + if (InventoryHelper.hasItem(steve, Items.CHEST, 1)) { + phase = StoragePhase.PLACING_CHEST; + } else { + result = ActionResult.failure("No chest nearby and none in inventory", true); + phase = StoragePhase.COMPLETED; + } + } + } + + private void handlePlacingChest() { + // Find suitable position to place chest (2 blocks in front) + BlockPos stevePos = steve.blockPosition(); + BlockPos placePos = stevePos.relative(steve.getDirection()); + + // Check if position is valid + BlockState targetState = steve.level().getBlockState(placePos); + BlockState belowState = steve.level().getBlockState(placePos.below()); + + if (targetState.isAir() && belowState.isSolidRender(steve.level(), placePos.below())) { + // Place chest + steve.level().setBlock(placePos, Blocks.CHEST.defaultBlockState(), 3); + InventoryHelper.removeItem(steve, Items.CHEST, 1); + + chestPos = placePos; + phase = StoragePhase.NAVIGATING; + navigationStartTick = ticksRunning; + steve.getNavigation().moveTo( + chestPos.getX() + 0.5, + chestPos.getY(), + chestPos.getZ() + 0.5, + 1.0 + ); + } else { + // Try adjacent positions + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + if (dx == 0 && dz == 0) continue; + + BlockPos tryPos = stevePos.offset(dx, 0, dz); + BlockState tryState = steve.level().getBlockState(tryPos); + BlockState tryBelow = steve.level().getBlockState(tryPos.below()); + + if (tryState.isAir() && tryBelow.isSolidRender(steve.level(), tryPos.below())) { + steve.level().setBlock(tryPos, Blocks.CHEST.defaultBlockState(), 3); + InventoryHelper.removeItem(steve, Items.CHEST, 1); + + chestPos = tryPos; + phase = StoragePhase.STORING; // Already close enough + return; + } + } + } + + // Still couldn't place + result = ActionResult.failure("Cannot find suitable position for chest", false); + phase = StoragePhase.COMPLETED; + } + } + + private void handleNavigation() { + // Check if reached chest + if (steve.blockPosition().distSqr(chestPos) <= 9) { // Within 3 blocks + steve.getNavigation().stop(); + phase = StoragePhase.STORING; + return; + } + + // Check navigation timeout + if (ticksRunning - navigationStartTick > NAVIGATION_TIMEOUT) { + result = ActionResult.failure("Could not reach chest", false); + phase = StoragePhase.COMPLETED; + return; + } + + // Re-navigate if path lost + if (!steve.getNavigation().isInProgress()) { + steve.getNavigation().moveTo( + chestPos.getX() + 0.5, + chestPos.getY(), + chestPos.getZ() + 0.5, + 1.0 + ); + } + } + + private void handleStoring() { + // Verify chest still exists + BlockState chestState = steve.level().getBlockState(chestPos); + if (!chestState.is(Blocks.CHEST) && !chestState.is(Blocks.BARREL)) { + result = ActionResult.failure("Chest disappeared", false); + phase = StoragePhase.COMPLETED; + return; + } + + // Transfer items to chest + itemsStored = InventoryHelper.transferToChest(steve, chestPos, itemToStore); + + if (itemsStored > 0) { + // Update chest memory after storing + steve.getMemory().updateChest(chestPos); + + result = ActionResult.success("Stored " + itemsStored + " items in chest at " + chestPos.toShortString()); + } else { + result = ActionResult.failure("No items to store or chest is full", false); + } + + phase = StoragePhase.COMPLETED; + } + + @Override + protected void onCancel() { + steve.getNavigation().stop(); + } + + @Override + public String getDescription() { + switch (phase) { + case FINDING_CHEST: + return "Finding chest for storage"; + case PLACING_CHEST: + return "Placing storage chest"; + case NAVIGATING: + return "Moving to chest"; + case STORING: + return "Storing items (" + itemsStored + " stored)"; + default: + return "Store items"; + } + } + + /** + * Get the number of items successfully stored + * @return Count of items stored + */ + public int getItemsStored() { + return itemsStored; + } + + /** + * Get the chest position used for storage + * @return BlockPos of chest + */ + public BlockPos getChestPosition() { + return chestPos; + } +} diff --git a/src/main/java/com/steve/ai/action/actions/TacticalRetreatAction.java b/src/main/java/com/steve/ai/action/actions/TacticalRetreatAction.java new file mode 100644 index 0000000..aa2c0a9 --- /dev/null +++ b/src/main/java/com/steve/ai/action/actions/TacticalRetreatAction.java @@ -0,0 +1,259 @@ +package com.steve.ai.action.actions; + +import com.steve.ai.SteveMod; +import com.steve.ai.action.ActionResult; +import com.steve.ai.action.Task; +import com.steve.ai.entity.SteveEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.phys.AABB; + +import java.util.List; + +/** + * Tactical retreat action for when combat is unfavorable + * Intelligently finds safe locations and retreats while monitoring threats + */ +public class TacticalRetreatAction extends BaseAction { + private int ticksRunning; + private BlockPos retreatTarget; + private static final int MAX_TICKS = 400; // 20 seconds + private static final double SAFE_DISTANCE = 32.0; + private static final int REEVAL_INTERVAL = 40; // Re-evaluate every 2 seconds + + public TacticalRetreatAction(SteveEntity steve, Task task) { + super(steve, task); + } + + @Override + protected void onStart() { + ticksRunning = 0; + steve.setFlying(false); + steve.setSprinting(true); + + // Find retreat location + retreatTarget = findSafeRetreatLocation(); + + if (retreatTarget == null) { + SteveMod.LOGGER.warn("Steve '{}' couldn't find safe retreat location", steve.getSteveName()); + retreatTarget = steve.blockPosition().offset(16, 0, 16); // Fallback: move away + } + + SteveMod.LOGGER.info("Steve '{}' retreating to {}", + steve.getSteveName(), retreatTarget); + } + + @Override + protected void onTick() { + ticksRunning++; + + if (ticksRunning > MAX_TICKS) { + steve.setSprinting(false); + result = ActionResult.success("Retreat complete - reached safe distance"); + return; + } + + // Re-evaluate retreat path periodically + if (ticksRunning % REEVAL_INTERVAL == 0) { + if (isSafeLocation()) { + steve.setSprinting(false); + result = ActionResult.success("Reached safe location"); + return; + } + + // Check if still being pursued + if (getNearestThreatDistance() > SAFE_DISTANCE) { + steve.setSprinting(false); + result = ActionResult.success("Threats cleared - safe distance reached"); + return; + } + + // Update retreat target if needed + BlockPos newTarget = findSafeRetreatLocation(); + if (newTarget != null && !newTarget.equals(retreatTarget)) { + retreatTarget = newTarget; + SteveMod.LOGGER.debug("Steve '{}' updating retreat path to {}", + steve.getSteveName(), retreatTarget); + } + } + + // Navigate to retreat location + if (retreatTarget != null) { + steve.getNavigation().moveTo( + retreatTarget.getX() + 0.5, + retreatTarget.getY(), + retreatTarget.getZ() + 0.5, + 2.0 // Fast movement + ); + } + } + + @Override + protected void onCancel() { + steve.setSprinting(false); + steve.getNavigation().stop(); + SteveMod.LOGGER.info("Steve '{}' retreat cancelled", steve.getSteveName()); + } + + @Override + public String getDescription() { + return "Tactical retreat"; + } + + /** + * Find safe retreat location away from threats + */ + private BlockPos findSafeRetreatLocation() { + BlockPos currentPos = steve.blockPosition(); + List threats = getNearbyThreats(64.0); + + if (threats.isEmpty()) { + // No threats nearby, just move away from current position + return currentPos.offset(20, 0, 20); + } + + // Calculate center of threats + double threatCenterX = 0; + double threatCenterZ = 0; + for (LivingEntity threat : threats) { + threatCenterX += threat.getX(); + threatCenterZ += threat.getZ(); + } + threatCenterX /= threats.size(); + threatCenterZ /= threats.size(); + + // Move in opposite direction + double dx = steve.getX() - threatCenterX; + double dz = steve.getZ() - threatCenterZ; + double distance = Math.sqrt(dx * dx + dz * dz); + + if (distance < 0.1) { + // Avoid division by zero + dx = 1.0; + dz = 0.0; + distance = 1.0; + } + + // Calculate retreat position (32 blocks away from threats) + double retreatDistance = SAFE_DISTANCE; + int targetX = (int)(steve.getX() + (dx / distance) * retreatDistance); + int targetZ = (int)(steve.getZ() + (dz / distance) * retreatDistance); + + // Find valid ground level + BlockPos retreatPos = new BlockPos(targetX, steve.getBlockY(), targetZ); + retreatPos = findGroundLevel(retreatPos); + + return retreatPos; + } + + /** + * Find ground level at position + */ + private BlockPos findGroundLevel(BlockPos pos) { + // Search down for ground + for (int y = pos.getY(); y > pos.getY() - 10 && y > -64; y--) { + BlockPos checkPos = new BlockPos(pos.getX(), y, pos.getZ()); + if (steve.level().getBlockState(checkPos).isSolid()) { + return checkPos.above(); + } + } + + // Search up for ground + for (int y = pos.getY(); y < pos.getY() + 10 && y < 320; y++) { + BlockPos checkPos = new BlockPos(pos.getX(), y, pos.getZ()); + if (steve.level().getBlockState(checkPos).isSolid()) { + return checkPos.above(); + } + } + + return pos; // Fallback to original position + } + + /** + * Get list of nearby hostile entities + */ + private List getNearbyThreats(double range) { + AABB searchBox = steve.getBoundingBox().inflate(range); + List entities = steve.level().getEntities(steve, searchBox); + + return entities.stream() + .filter(e -> e instanceof LivingEntity) + .map(e -> (LivingEntity)e) + .filter(this::isThreat) + .toList(); + } + + /** + * Check if entity is a threat + */ + private boolean isThreat(LivingEntity entity) { + if (!entity.isAlive() || entity.isRemoved()) { + return false; + } + + // Don't consider other Steves or players as threats + if (entity instanceof SteveEntity || entity instanceof net.minecraft.world.entity.player.Player) { + return false; + } + + // Hostile mobs are threats + return entity instanceof Monster; + } + + /** + * Get distance to nearest threat + */ + private double getNearestThreatDistance() { + List threats = getNearbyThreats(64.0); + + if (threats.isEmpty()) { + return Double.MAX_VALUE; + } + + return threats.stream() + .mapToDouble(steve::distanceTo) + .min() + .orElse(Double.MAX_VALUE); + } + + /** + * Check if current location is safe + */ + private boolean isSafeLocation() { + // Safe if no threats within safe distance + return getNearestThreatDistance() > SAFE_DISTANCE; + } + + /** + * Get threat level (number of nearby enemies) + */ + public int getThreatLevel() { + return getNearbyThreats(32.0).size(); + } + + /** + * Check if retreat is necessary based on health and threat level + */ + public static boolean shouldRetreat(SteveEntity steve) { + float health = steve.getHealth(); + float maxHealth = steve.getMaxHealth(); + float healthPercent = (health / maxHealth) * 100; + + // Retreat if health below 40% + if (healthPercent < 40) { + return true; + } + + // Retreat if outnumbered (more than 3 enemies nearby) + AABB searchBox = steve.getBoundingBox().inflate(16.0); + List entities = steve.level().getEntities(steve, searchBox); + long threatCount = entities.stream() + .filter(e -> e instanceof Monster) + .filter(e -> ((LivingEntity)e).isAlive()) + .count(); + + return threatCount > 3; + } +} diff --git a/src/main/java/com/steve/ai/agent/VectorStore.java b/src/main/java/com/steve/ai/agent/VectorStore.java index 3719754..d160184 100644 --- a/src/main/java/com/steve/ai/agent/VectorStore.java +++ b/src/main/java/com/steve/ai/agent/VectorStore.java @@ -1,87 +1,381 @@ package com.steve.ai.agent; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.steve.ai.SteveMod; +import com.steve.ai.ai.EmbeddingClient; +import com.steve.ai.ai.OpenAIEmbeddingClient; +import com.steve.ai.util.VectorMath; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; +import java.util.stream.Collectors; -// TODO: Will be replaced with real vector embeddings later -// Currently uses deterministic random embeddings based on text hash -// This is a placeholder implementation - semantic similarity doesn't actually work -// Future: Integrate with sentence-transformers or OpenAI embeddings API for real semantic search +/** + * Real vector store with semantic embeddings + * Supports adding memories, semantic search, and persistence + * Uses OpenAI embeddings API for generating vectors + */ public class VectorStore { - private final Map store; + private final Map store; + private final EmbeddingClient embeddingClient; private final int dimensions; - - public VectorStore(int dimensions) { - this.dimensions = dimensions; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + /** + * Create vector store with default OpenAI embedding client + */ + public VectorStore() { + this(new OpenAIEmbeddingClient()); + } + + /** + * Create vector store with custom embedding client + * @param embeddingClient Client for generating embeddings + */ + public VectorStore(EmbeddingClient embeddingClient) { + this.embeddingClient = embeddingClient; + this.dimensions = embeddingClient.getDimensions(); this.store = new HashMap<>(); + + SteveMod.LOGGER.info("Initialized VectorStore with {} ({}D)", + embeddingClient.getName(), dimensions); + } + + /** + * Add a memory to the vector store + * @param id Unique identifier for this memory + * @param text Text to embed and store + * @param metadata Additional metadata + */ + public void addMemory(String id, String text, Map metadata) { + try { + float[] embedding = embeddingClient.generateEmbedding(text); + + // Normalize embedding for faster cosine similarity + embedding = VectorMath.normalize(embedding); + + VectorEntry entry = new VectorEntry(id, text, embedding, metadata); + store.put(id, entry); + + SteveMod.LOGGER.debug("Added memory to vector store: {} ({})", + id, text.substring(0, Math.min(50, text.length()))); + + } catch (Exception e) { + SteveMod.LOGGER.error("Failed to add memory to vector store: {}", id, e); + } + } + + /** + * Add multiple memories in batch + * @param memories List of memories to add + */ + public void addMemories(List memories) { + if (memories == null || memories.isEmpty()) { + return; + } + + try { + // Extract texts for batch embedding + List texts = memories.stream() + .map(m -> m.text) + .collect(Collectors.toList()); + + // Generate embeddings in batch + List embeddings = embeddingClient.generateEmbeddings(texts); + + // Store entries + for (int i = 0; i < memories.size(); i++) { + MemoryInput memory = memories.get(i); + float[] embedding = VectorMath.normalize(embeddings.get(i)); + + VectorEntry entry = new VectorEntry( + memory.id, + memory.text, + embedding, + memory.metadata + ); + + store.put(memory.id, entry); + } + + SteveMod.LOGGER.info("Added {} memories to vector store in batch", memories.size()); + + } catch (Exception e) { + SteveMod.LOGGER.error("Failed to add memories in batch", e); + } } - - public void addText(String text, Map metadata) { - String id = UUID.randomUUID().toString(); - float[] embedding = generateEmbedding(text); - store.put(id, new EmbeddingEntry(id, text, embedding, metadata)); - } - - public List similaritySearch(String query, int k) { - float[] queryEmbedding = generateEmbedding(query); - - return store.values().stream() - .sorted((a, b) -> Float.compare( - cosineSimilarity(b.embedding, queryEmbedding), - cosineSimilarity(a.embedding, queryEmbedding) - )) - .limit(k) - .toList(); - } - - // TODO: Replace with real embeddings - this is a placeholder using hash-based randomness - // Real implementation should use: - // - OpenAI embeddings API - // - Local sentence-transformers model via JNI - // - Pre-computed embedding database - private float[] generateEmbedding(String text) { - float[] embedding = new float[dimensions]; - Random random = new Random(text.hashCode()); // Deterministic but not semantic! - - for (int i = 0; i < dimensions; i++) { - embedding[i] = random.nextFloat(); + + /** + * Search for similar memories using semantic similarity + * @param query Query text + * @param topK Number of results to return + * @return List of search results sorted by similarity + */ + public List search(String query, int topK) { + if (store.isEmpty()) { + return new ArrayList<>(); + } + + try { + // Generate query embedding + float[] queryEmbedding = embeddingClient.generateEmbedding(query); + queryEmbedding = VectorMath.normalize(queryEmbedding); + + // Calculate similarities and sort + return store.values().stream() + .map(entry -> { + float similarity = VectorMath.cosineSimilarity( + entry.embedding, + queryEmbedding + ); + return new SearchResult( + entry.id, + entry.text, + similarity, + entry.metadata + ); + }) + .sorted(Comparator.comparingDouble(r -> -r.similarity)) + .limit(topK) + .collect(Collectors.toList()); + + } catch (Exception e) { + SteveMod.LOGGER.error("Failed to search vector store", e); + return new ArrayList<>(); } + } + + /** + * Get memory by ID + * @param id Memory identifier + * @return Vector entry or null if not found + */ + public VectorEntry getMemory(String id) { + return store.get(id); + } + + /** + * Remove memory by ID + * @param id Memory identifier + * @return True if memory was removed + */ + public boolean removeMemory(String id) { + return store.remove(id) != null; + } + + /** + * Clear all memories + */ + public void clear() { + store.clear(); + } - return normalize(embedding); + /** + * Get total number of memories + * @return Memory count + */ + public int size() { + return store.size(); + } + + /** + * Check if store contains a memory + * @param id Memory identifier + * @return True if memory exists + */ + public boolean contains(String id) { + return store.containsKey(id); + } + + /** + * Get all memory IDs + * @return Set of memory IDs + */ + public Set getMemoryIds() { + return new HashSet<>(store.keySet()); + } + + /** + * Save vector store to file + * @param path File path + */ + public void saveToFile(Path path) { + try { + Files.createDirectories(path.getParent()); + + // Convert to serializable format (arrays to lists for JSON) + List serializable = store.values().stream() + .map(entry -> new SerializableEntry( + entry.id, + entry.text, + toFloatList(entry.embedding), + entry.metadata + )) + .collect(Collectors.toList()); + + try (Writer writer = new FileWriter(path.toFile())) { + GSON.toJson(serializable, writer); + } + + SteveMod.LOGGER.info("Saved {} vector entries to {}", store.size(), path); + + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to save vector store to {}", path, e); + } } - - private float[] normalize(float[] vector) { - float magnitude = 0; - for (float v : vector) { - magnitude += v * v; + + /** + * Load vector store from file + * @param path File path + */ + public void loadFromFile(Path path) { + try { + if (!Files.exists(path)) { + SteveMod.LOGGER.info("No existing vector store file at {}", path); + return; + } + + try (Reader reader = new FileReader(path.toFile())) { + Type listType = new TypeToken>(){}.getType(); + List loaded = GSON.fromJson(reader, listType); + + if (loaded != null) { + store.clear(); + + for (SerializableEntry entry : loaded) { + float[] embedding = toFloatArray(entry.embedding); + VectorEntry vectorEntry = new VectorEntry( + entry.id, + entry.text, + embedding, + entry.metadata + ); + store.put(entry.id, vectorEntry); + } + + SteveMod.LOGGER.info("Loaded {} vector entries from {}", store.size(), path); + } + } + + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to load vector store from {}", path, e); } - magnitude = (float) Math.sqrt(magnitude); - - float[] normalized = new float[vector.length]; - for (int i = 0; i < vector.length; i++) { - normalized[i] = vector[i] / magnitude; + } + + /** + * Save to default location + */ + public void save() { + Path defaultPath = Paths.get("config", "steve", "vector_store", "embeddings.json"); + saveToFile(defaultPath); + } + + /** + * Load from default location + */ + public void load() { + Path defaultPath = Paths.get("config", "steve", "vector_store", "embeddings.json"); + loadFromFile(defaultPath); + } + + // Helper methods + + private List toFloatList(float[] array) { + List list = new ArrayList<>(array.length); + for (float f : array) { + list.add(f); } - return normalized; + return list; } - - private float cosineSimilarity(float[] a, float[] b) { - float dotProduct = 0; - for (int i = 0; i < a.length; i++) { - dotProduct += a[i] * b[i]; + + private float[] toFloatArray(List list) { + float[] array = new float[list.size()]; + for (int i = 0; i < list.size(); i++) { + array[i] = list.get(i); } - return dotProduct; + return array; } - - public static class EmbeddingEntry { + + // Data classes + + /** + * Vector entry stored in memory + */ + public static class VectorEntry { public final String id; public final String text; public final float[] embedding; public final Map metadata; - - public EmbeddingEntry(String id, String text, float[] embedding, Map metadata) { + + public VectorEntry(String id, String text, float[] embedding, + Map metadata) { + this.id = id; + this.text = text; + this.embedding = embedding; + this.metadata = metadata != null ? metadata : new HashMap<>(); + } + } + + /** + * Serializable entry for JSON storage + */ + private static class SerializableEntry { + private final String id; + private final String text; + private final List embedding; + private final Map metadata; + + public SerializableEntry(String id, String text, List embedding, + Map metadata) { this.id = id; this.text = text; this.embedding = embedding; this.metadata = metadata; } } + + /** + * Search result with similarity score + */ + public static class SearchResult { + public final String id; + public final String text; + public final float similarity; + public final Map metadata; + + public SearchResult(String id, String text, float similarity, + Map metadata) { + this.id = id; + this.text = text; + this.similarity = similarity; + this.metadata = metadata; + } + + @Override + public String toString() { + return String.format("SearchResult{id='%s', similarity=%.3f, text='%s'}", + id, similarity, text.substring(0, Math.min(50, text.length()))); + } + } + + /** + * Input for batch memory addition + */ + public static class MemoryInput { + public final String id; + public final String text; + public final Map metadata; + + public MemoryInput(String id, String text, Map metadata) { + this.id = id; + this.text = text; + this.metadata = metadata; + } + } } diff --git a/src/main/java/com/steve/ai/ai/EmbeddingClient.java b/src/main/java/com/steve/ai/ai/EmbeddingClient.java new file mode 100644 index 0000000..9bd8fc2 --- /dev/null +++ b/src/main/java/com/steve/ai/ai/EmbeddingClient.java @@ -0,0 +1,36 @@ +package com.steve.ai.ai; + +import java.util.List; + +/** + * Interface for generating text embeddings + * Implementations can use different embedding services (OpenAI, local models, etc.) + */ +public interface EmbeddingClient { + /** + * Generate embedding for a single text + * @param text Input text + * @return Embedding vector + */ + float[] generateEmbedding(String text); + + /** + * Generate embeddings for multiple texts (batch processing) + * Implementations should optimize for batch requests + * @param texts List of input texts + * @return List of embedding vectors + */ + List generateEmbeddings(List texts); + + /** + * Get the dimensionality of embeddings produced by this client + * @return Number of dimensions in embedding vectors + */ + int getDimensions(); + + /** + * Get the name of this embedding client (for logging/debugging) + * @return Client name + */ + String getName(); +} diff --git a/src/main/java/com/steve/ai/ai/OpenAIEmbeddingClient.java b/src/main/java/com/steve/ai/ai/OpenAIEmbeddingClient.java new file mode 100644 index 0000000..9985d88 --- /dev/null +++ b/src/main/java/com/steve/ai/ai/OpenAIEmbeddingClient.java @@ -0,0 +1,194 @@ +package com.steve.ai.ai; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.steve.ai.SteveMod; +import com.steve.ai.config.SteveConfig; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +/** + * OpenAI Embeddings API client + * Uses text-embedding-3-small model (1536 dimensions) + * Cost: $0.02 / 1M tokens + */ +public class OpenAIEmbeddingClient implements EmbeddingClient { + private static final String EMBEDDINGS_API_URL = "https://api.openai.com/v1/embeddings"; + private static final String MODEL = "text-embedding-3-small"; // 1536 dimensions + private static final int DIMENSIONS = 1536; + private static final int MAX_RETRIES = 3; + private static final int INITIAL_RETRY_DELAY_MS = 1000; + + private final HttpClient client; + private final String apiKey; + + public OpenAIEmbeddingClient() { + this.apiKey = SteveConfig.OPENAI_API_KEY.get(); + this.client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .build(); + } + + @Override + public float[] generateEmbedding(String text) { + if (apiKey == null || apiKey.isEmpty()) { + SteveMod.LOGGER.error("OpenAI API key not configured for embeddings!"); + return createZeroVector(); + } + + if (text == null || text.trim().isEmpty()) { + SteveMod.LOGGER.warn("Empty text provided for embedding"); + return createZeroVector(); + } + + JsonObject requestBody = buildRequestBody(text); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(EMBEDDINGS_API_URL)) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .timeout(Duration.ofSeconds(30)) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toString())) + .build(); + + // Retry logic with exponential backoff + for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { + try { + HttpResponse response = client.send(request, + HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + return parseEmbeddingResponse(response.body()); + } + + // Check if error is retryable + if (response.statusCode() == 429 || response.statusCode() >= 500) { + if (attempt < MAX_RETRIES - 1) { + int delayMs = INITIAL_RETRY_DELAY_MS * (int) Math.pow(2, attempt); + SteveMod.LOGGER.warn("OpenAI Embeddings API failed with status {}, " + + "retrying in {}ms (attempt {}/{})", + response.statusCode(), delayMs, attempt + 1, MAX_RETRIES); + Thread.sleep(delayMs); + continue; + } + } + + // Non-retryable error or final attempt + SteveMod.LOGGER.error("OpenAI Embeddings API request failed: {}", + response.statusCode()); + SteveMod.LOGGER.error("Response body: {}", response.body()); + return createZeroVector(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + SteveMod.LOGGER.error("Embedding request interrupted", e); + return createZeroVector(); + } catch (Exception e) { + if (attempt < MAX_RETRIES - 1) { + int delayMs = INITIAL_RETRY_DELAY_MS * (int) Math.pow(2, attempt); + SteveMod.LOGGER.warn("Error communicating with OpenAI Embeddings API, " + + "retrying in {}ms (attempt {}/{})", + delayMs, attempt + 1, MAX_RETRIES, e); + try { + Thread.sleep(delayMs); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return createZeroVector(); + } + } else { + SteveMod.LOGGER.error("Error communicating with OpenAI Embeddings API " + + "after {} attempts", MAX_RETRIES, e); + return createZeroVector(); + } + } + } + + return createZeroVector(); + } + + @Override + public List generateEmbeddings(List texts) { + if (texts == null || texts.isEmpty()) { + return new ArrayList<>(); + } + + // For batch requests, we could optimize by sending all texts at once + // OpenAI API supports up to 2048 texts per request + // For simplicity, we'll process one at a time for now + List embeddings = new ArrayList<>(); + for (String text : texts) { + embeddings.add(generateEmbedding(text)); + } + + return embeddings; + } + + @Override + public int getDimensions() { + return DIMENSIONS; + } + + @Override + public String getName() { + return "OpenAI-" + MODEL; + } + + /** + * Build JSON request body for embeddings API + */ + private JsonObject buildRequestBody(String text) { + JsonObject body = new JsonObject(); + body.addProperty("model", MODEL); + body.addProperty("input", text); + return body; + } + + /** + * Parse embedding from API response + */ + private float[] parseEmbeddingResponse(String responseBody) { + try { + JsonObject jsonResponse = JsonParser.parseString(responseBody).getAsJsonObject(); + JsonArray data = jsonResponse.getAsJsonArray("data"); + + if (data == null || data.size() == 0) { + SteveMod.LOGGER.error("No embedding data in response"); + return createZeroVector(); + } + + JsonObject embeddingData = data.get(0).getAsJsonObject(); + JsonArray embeddingArray = embeddingData.getAsJsonArray("embedding"); + + if (embeddingArray == null || embeddingArray.size() != DIMENSIONS) { + SteveMod.LOGGER.error("Invalid embedding dimensions: expected {}, got {}", + DIMENSIONS, embeddingArray != null ? embeddingArray.size() : 0); + return createZeroVector(); + } + + float[] embedding = new float[DIMENSIONS]; + for (int i = 0; i < DIMENSIONS; i++) { + embedding[i] = embeddingArray.get(i).getAsFloat(); + } + + return embedding; + + } catch (Exception e) { + SteveMod.LOGGER.error("Failed to parse embedding response", e); + return createZeroVector(); + } + } + + /** + * Create zero vector as fallback + */ + private float[] createZeroVector() { + return new float[DIMENSIONS]; + } +} diff --git a/src/main/java/com/steve/ai/ai/PromptBuilder.java b/src/main/java/com/steve/ai/ai/PromptBuilder.java index 98e7e8c..0bc392b 100644 --- a/src/main/java/com/steve/ai/ai/PromptBuilder.java +++ b/src/main/java/com/steve/ai/ai/PromptBuilder.java @@ -2,27 +2,57 @@ import com.steve.ai.entity.SteveEntity; import com.steve.ai.memory.WorldKnowledge; +import com.steve.ai.util.InventoryHelper; import net.minecraft.core.BlockPos; import net.minecraft.world.item.ItemStack; import java.util.List; public class PromptBuilder { - + public static String buildSystemPrompt() { return """ - You are a Minecraft AI agent. Respond ONLY with valid JSON, no extra text. - + You are an expert Minecraft AI agent with extensive problem-solving experience. + + REASONING FRAMEWORK (use this for every task): + 1. ANALYZE: What is my current situation? (inventory, location, resources) + 2. IDENTIFY: What exactly does the user want? What are the requirements? + 3. PLAN: What steps are needed? What's the optimal sequence? + 4. VALIDATE: Do I have what I need? Are there blockers or missing prerequisites? + 5. EXECUTE: Output the task list + FORMAT (strict JSON): - {"reasoning": "brief thought", "plan": "action description", "tasks": [{"action": "type", "parameters": {...}}]} - + {"reasoning": "step-by-step thought process", "plan": "clear action description", "tasks": [{"action": "type", "parameters": {...}}]} + ACTIONS: - - attack: {"target": "hostile"} (for any mob/monster) + - attack: {"target": "hostile"} (melee combat, auto-equips armor/shield) + - attack_ranged: {"target": "hostile"} (bow combat, maintains distance, requires arrows) + - retreat: {} (tactical retreat when low health or outnumbered) - build: {"structure": "house", "blocks": ["oak_planks", "cobblestone", "glass_pane"], "dimensions": [9, 6, 9]} - - mine: {"block": "iron", "quantity": 8} (resources: iron, diamond, coal, gold, copper, redstone, emerald) + - mine: {"block": "BLOCK_NAME", "quantity": NUMBER} + WOOD/TREES: Use "wood" or "oak_log" - NEVER "coal" for trees! + ORES: iron, diamond, coal, gold, copper, redstone, emerald, lapis + BLOCKS: stone, cobblestone, dirt, sand, gravel + - craft: {"item": "wooden_pickaxe", "quantity": 1} (crafts items, auto-finds/places crafting table) + - store: {"item": "cobblestone"} or {} (stores items in chest, omit item to store all) + - retrieve: {"item": "iron_ingot", "quantity": 8} (retrieves items from nearby chest) + - place_chest: {} (places chest for storage) + - farm: {"crop": "wheat", "type": "farm", "amount": 64} (plant/harvest: wheat, carrots, potatoes, beetroot; auto-replants) + - breed: {"animal": "cow", "amount": 5} (breed animals: cow, pig, chicken, sheep, horse, llama, rabbit, etc.) + - build_portal: {} (builds nether portal, requires 10 obsidian + flint & steel) + - build_auto_door: {} (builds automatic door with pressure plates, requires 1 iron door, 2 pressure plates, 5 redstone dust) - follow: {"player": "NAME"} - pathfind: {"x": 0, "y": 0, "z": 0} - + + CRITICAL MINING RULES: + ⚠️ WOOD/TREE COMMANDS: + - "mine wood" → {"action": "mine", "parameters": {"block": "wood", "quantity": X}} + - "chop wood" → {"action": "mine", "parameters": {"block": "wood", "quantity": X}} + - "get wood" → {"action": "mine", "parameters": {"block": "wood", "quantity": X}} + - "cut trees" → {"action": "mine", "parameters": {"block": "wood", "quantity": X}} + - "gather logs" → {"action": "mine", "parameters": {"block": "wood", "quantity": X}} + - NEVER use "coal" when user asks for wood/trees/logs! + RULES: 1. ALWAYS use "hostile" for attack target (mobs, monsters, creatures) 2. STRUCTURE OPTIONS: house, oldhouse, powerplant, castle, tower, barn, modern @@ -30,50 +60,239 @@ public static String buildSystemPrompt() { 4. castle/tower/barn/modern = procedural (castle=14x10x14, tower=6x6x16, barn=12x8x14) 5. Use 2-3 block types: oak_planks, cobblestone, glass_pane, stone_bricks 6. NO extra pathfind tasks unless explicitly requested - 7. Keep reasoning under 15 words - 8. COLLABORATIVE BUILDING: Multiple Steves can work on same structure simultaneously - 9. MINING: Can mine any ore (iron, diamond, coal, etc) - - EXAMPLES (copy these formats exactly): - + 7. COLLABORATIVE BUILDING: Multiple Steves can work on same structure simultaneously + 8. MINING BLOCKS: wood/trees/logs (most common!), ores (iron, diamond, coal), stone, dirt, sand + 9. WOOD vs COAL: "wood" = tree logs, "coal" = black ore. NEVER confuse these! + 10. CRAFTING: Auto-checks inventory, finds/places crafting table + 11. STORAGE: Use 'store' when inventory full, 'retrieve' when need items + 12. INVENTORY MANAGEMENT: Auto-stores items when inventory >90% full + 13. FARMING: Auto-replants crops after harvesting, uses bone meal if available + 14. BREEDING: Requires appropriate food in inventory (wheat for cows/sheep, carrots for pigs, seeds for chickens) + 15. HUNGER: Steve automatically eats when hungry, keep food in inventory + 16. COMBAT: Auto-equips best armor/weapons/shield; use attack_ranged for distant enemies + 17. RETREAT: Use 'retreat' action when health low or heavily outnumbered + 18. BOSS FIGHTS: Teams coordinate roles automatically (tank, DPS, ranged, support) + 19. DIMENSIONS: Use build_portal to access Nether; dimension navigation handles safety automatically + 20. REDSTONE: Use build_auto_door for automatic doors; system builds complete circuit with pressure plates + + EXAMPLES (showing proper reasoning): + + Example 1 - Simple task with validation: Input: "build a house" - {"reasoning": "Building standard house near player", "plan": "Construct house", "tasks": [{"action": "build", "parameters": {"structure": "house", "blocks": ["oak_planks", "cobblestone", "glass_pane"], "dimensions": [9, 6, 9]}}]} - + {"reasoning": "User wants house. I should check if I have building materials. If not, gather them first. Then build house near player.", "plan": "Construct house with wood and stone", "tasks": [{"action": "build", "parameters": {"structure": "house", "blocks": ["oak_planks", "cobblestone", "glass_pane"], "dimensions": [9, 6, 9]}}]} + + Example 2 - Task requiring prerequisites: + Input: "craft a diamond pickaxe" + {"reasoning": "Diamond pickaxe needs 3 diamonds + 2 sticks. First check inventory. If missing diamonds, mine them. If missing sticks, craft from planks. Then craft pickaxe.", "plan": "Gather materials and craft diamond pickaxe", "tasks": [{"action": "mine", "parameters": {"block": "diamond", "quantity": 3}}, {"action": "craft", "parameters": {"item": "diamond_pickaxe", "quantity": 1}}]} + + Example 3 - Error recovery pattern: Input: "get me iron" - {"reasoning": "Mining iron ore for player", "plan": "Mine iron", "tasks": [{"action": "mine", "parameters": {"block": "iron", "quantity": 16}}]} - - Input: "find diamonds" - {"reasoning": "Searching for diamond ore", "plan": "Mine diamonds", "tasks": [{"action": "mine", "parameters": {"block": "diamond", "quantity": 8}}]} - - Input: "kill mobs" - {"reasoning": "Hunting hostile creatures", "plan": "Attack hostiles", "tasks": [{"action": "attack", "parameters": {"target": "hostile"}}]} - - Input: "murder creeper" - {"reasoning": "Targeting creeper", "plan": "Attack creeper", "tasks": [{"action": "attack", "parameters": {"target": "creeper"}}]} - - Input: "follow me" - {"reasoning": "Player needs me", "plan": "Follow player", "tasks": [{"action": "follow", "parameters": {"player": "USE_NEARBY_PLAYER_NAME"}}]} - - CRITICAL: Output ONLY valid JSON. No markdown, no explanations, no line breaks in JSON. + {"reasoning": "User needs iron. Mining iron ore is straightforward. I'll mine 16 to have extras. If inventory full, I'll auto-store first.", "plan": "Mine iron ore", "tasks": [{"action": "mine", "parameters": {"block": "iron", "quantity": 16}}]} + + Example 4 - Combat scenario: + Input: "kill mobs" + {"reasoning": "User wants me to hunt hostiles. I'll target any hostile creatures nearby. Combat is dangerous so stay alert.", "plan": "Attack hostile mobs", "tasks": [{"action": "attack", "parameters": {"target": "hostile"}}]} + + Example 5 - Multi-step crafting: + Input: "make an iron sword" + {"reasoning": "Iron sword needs 2 iron ingots + 1 stick. Need to check inventory first. If no iron ore, mine it. Smelt ore to ingots. Get/craft sticks. Then craft sword.", "plan": "Gather iron and craft sword", "tasks": [{"action": "mine", "parameters": {"block": "iron", "quantity": 2}}, {"action": "craft", "parameters": {"item": "iron_sword", "quantity": 1}}]} + + Example 6 - Storage management: + Input: "store my items" + {"reasoning": "User wants to store inventory. I'll look for nearby chest. If none exist, place one first. Then transfer items.", "plan": "Store items in chest", "tasks": [{"action": "store", "parameters": {}}]} + + Example 7 - Retrieval from storage: + Input: "get 10 iron ingots from chest" + {"reasoning": "User needs iron from storage. I'll search for nearby chest with iron ingots and retrieve the requested amount.", "plan": "Retrieve iron from chest", "tasks": [{"action": "retrieve", "parameters": {"item": "iron_ingot", "quantity": 10}}]} + + Example 8 - Farming crops: + Input: "farm wheat" + {"reasoning": "User wants me to manage wheat farming. I should harvest any mature wheat nearby and replant automatically. If I have bone meal, I can accelerate growth.", "plan": "Harvest and replant wheat crops", "tasks": [{"action": "farm", "parameters": {"crop": "wheat", "type": "farm", "amount": 64}}]} + + Example 9 - Breeding animals: + Input: "breed some cows" + {"reasoning": "User wants me to breed cows. Cows need wheat to breed. I should check if I have wheat in inventory. Then find two adult cows and feed them to initiate breeding.", "plan": "Breed cows using wheat", "tasks": [{"action": "breed", "parameters": {"animal": "cow", "amount": 5}}]} + + Example 10 - Self-sustaining farm: + Input: "start a farm and breed chickens" + {"reasoning": "User wants both farming and animal breeding. I'll plant wheat seeds if farmland is available, then breed chickens using seeds. This creates a sustainable food source.", "plan": "Establish farm with crops and animals", "tasks": [{"action": "farm", "parameters": {"crop": "wheat", "type": "farm", "amount": 32}}, {"action": "breed", "parameters": {"animal": "chicken", "amount": 3}}]} + + Example 11 - Redstone automation: + Input: "build an automatic door" + {"reasoning": "User wants an automatic door with pressure plates. This requires iron door, pressure plates, and redstone dust. The action will build the complete circuit automatically. I should check if I have the required materials.", "plan": "Build automatic door with pressure plate activation", "tasks": [{"action": "build_auto_door", "parameters": {}}]} + + Example 12 - Wood/Tree gathering (IMPORTANT!): + Input: "mine 10 wood" + {"reasoning": "User wants wood logs from trees. I need to mine wood blocks, NOT coal. Wood is the most basic resource in Minecraft.", "plan": "Mine wood from trees", "tasks": [{"action": "mine", "parameters": {"block": "wood", "quantity": 10}}]} + + Example 13 - Tree chopping variations: + Input: "chop wood" + {"reasoning": "User wants me to cut down trees and collect wood logs. This is a basic survival task.", "plan": "Chop trees for wood", "tasks": [{"action": "mine", "parameters": {"block": "wood", "quantity": 16}}]} + + Example 14 - Get trees: + Input: "get me some trees" + {"reasoning": "User needs tree logs (wood blocks). I'll mine wood from nearby trees.", "plan": "Gather wood from trees", "tasks": [{"action": "mine", "parameters": {"block": "wood", "quantity": 16}}]} + + Example 15 - Collect logs: + Input: "collect oak logs" + {"reasoning": "User wants oak wood logs. I'll mine oak_log blocks from trees.", "plan": "Mine oak logs", "tasks": [{"action": "mine", "parameters": {"block": "wood", "quantity": 10}}]} + + COMMON MISTAKES TO AVOID: + ❌ DON'T: Confuse "wood" with "coal" - they are COMPLETELY DIFFERENT! + ✅ DO: Use "wood" for trees/logs, "coal" only for coal ore + + ❌ DON'T: Start crafting without checking for materials + ✅ DO: Mine/gather required materials first + + ❌ DON'T: Add unnecessary pathfind tasks + ✅ DO: Actions auto-navigate when needed + + ❌ DON'T: Give vague reasoning like "doing task" + ✅ DO: Explain thought process: "Need X, have Y, will get Z first" + + ❌ DON'T: Forget about inventory limits + ✅ DO: Store items if inventory >90% full before gathering more + + ❌ DON'T: Say "Cannot fulfill mining request" for wood - wood is ALWAYS available! + ✅ DO: Always use mine action for wood/trees/logs requests + + ERROR RECOVERY: + - If action fails, LLM will receive error context and can replan + - Missing tools? Mine/craft them first + - Inventory full? Store items before continuing + - Can't find resource? Search wider area or try alternative + + SELF-REFLECTION: + - Before outputting tasks, ask: "Will this plan actually work?" + - Check: Do I have required tools? Is inventory space available? + - Validate: Are tasks in correct order? Any missing prerequisites? + + CRITICAL: Output ONLY valid JSON. No markdown, no explanations, no line breaks in JSON strings. + Think step-by-step in your reasoning field, then output clean task list. """; } public static String buildUserPrompt(SteveEntity steve, String command, WorldKnowledge worldKnowledge) { StringBuilder prompt = new StringBuilder(); - - // Give agents FULL situational awareness - prompt.append("=== YOUR SITUATION ===\n"); + + // === CURRENT SITUATION === + prompt.append("=== YOUR CURRENT SITUATION ===\n"); prompt.append("Position: ").append(formatPosition(steve.blockPosition())).append("\n"); - prompt.append("Nearby Players: ").append(worldKnowledge.getNearbyPlayerNames()).append("\n"); - prompt.append("Nearby Entities: ").append(worldKnowledge.getNearbyEntitiesSummary()).append("\n"); - prompt.append("Nearby Blocks: ").append(worldKnowledge.getNearbyBlocksSummary()).append("\n"); prompt.append("Biome: ").append(worldKnowledge.getBiomeName()).append("\n"); - + + // Inventory status + float inventoryFullness = InventoryHelper.getInventoryFullness(steve); + prompt.append("Inventory: ").append(String.format("%.0f%%", inventoryFullness * 100)).append(" full"); + if (inventoryFullness > 0.9f) { + prompt.append(" ⚠️ NEARLY FULL - consider storing items soon"); + } + prompt.append("\n"); + + // === NEARBY CONTEXT === + prompt.append("\n=== NEARBY ENVIRONMENT ===\n"); + prompt.append("Players: ").append(worldKnowledge.getNearbyPlayerNames()).append("\n"); + prompt.append("Entities: ").append(worldKnowledge.getNearbyEntitiesSummary()).append("\n"); + prompt.append("Blocks: ").append(worldKnowledge.getNearbyBlocksSummary()).append("\n"); + + // === MEMORY CONTEXT === + prompt.append("\n=== YOUR MEMORY ===\n"); + String memorySummary = steve.getMemory().getMemorySummary(); + if (memorySummary != null && !memorySummary.trim().isEmpty()) { + prompt.append(memorySummary); + } else { + prompt.append("No significant memories yet.\n"); + } + + // === LEARNED KNOWLEDGE === + String knowledgeSummary = steve.getActionExecutor().getKnowledgeBase().generateKnowledgeSummary(); + if (knowledgeSummary != null && !knowledgeSummary.trim().isEmpty()) { + prompt.append(knowledgeSummary); + } + + // === PLAYER COMMAND === prompt.append("\n=== PLAYER COMMAND ===\n"); prompt.append("\"").append(command).append("\"\n"); - - prompt.append("\n=== YOUR RESPONSE (with reasoning) ===\n"); - + + // === REASONING PROMPT === + prompt.append("\n=== YOUR RESPONSE ===\n"); + prompt.append("Think step-by-step using the reasoning framework:\n"); + prompt.append("1. What is the user asking for?\n"); + prompt.append("2. What do I need to accomplish this?\n"); + prompt.append("3. What do I currently have/know?\n"); + prompt.append("4. What steps should I take in order?\n"); + prompt.append("5. Are there any potential issues or blockers?\n"); + prompt.append("\nNow output your JSON response:\n"); + + return prompt.toString(); + } + + /** + * Build error recovery prompt when an action fails + * Provides context about what went wrong and asks for replanning + */ + public static String buildErrorRecoveryPrompt(SteveEntity steve, String failedAction, + String errorMessage, String originalGoal) { + StringBuilder prompt = new StringBuilder(); + + prompt.append("=== ERROR RECOVERY NEEDED ===\n\n"); + + prompt.append("ORIGINAL GOAL: ").append(originalGoal).append("\n"); + prompt.append("FAILED ACTION: ").append(failedAction).append("\n"); + prompt.append("ERROR: ").append(errorMessage).append("\n\n"); + + // Current situation + prompt.append("=== CURRENT SITUATION ===\n"); + prompt.append("Position: ").append(formatPosition(steve.blockPosition())).append("\n"); + + float inventoryFullness = InventoryHelper.getInventoryFullness(steve); + prompt.append("Inventory: ").append(String.format("%.0f%%", inventoryFullness * 100)).append(" full\n"); + + prompt.append("\n=== RECOVERY INSTRUCTIONS ===\n"); + prompt.append("The previous action failed. Analyze what went wrong and create a new plan.\n\n"); + + prompt.append("COMMON FAILURE CAUSES & SOLUTIONS:\n"); + prompt.append("- Missing tools/materials → Mine/craft them first\n"); + prompt.append("- Inventory full → Store items before continuing\n"); + prompt.append("- Resource not found → Search different area or use alternative\n"); + prompt.append("- Can't reach location → Clear path or find alternate route\n"); + prompt.append("- Chest not found → Place new chest first\n\n"); + + prompt.append("RECOVERY STRATEGY:\n"); + prompt.append("1. Identify why the action failed\n"); + prompt.append("2. Determine what's needed to succeed\n"); + prompt.append("3. Create alternative approach or gather prerequisites\n"); + prompt.append("4. Output new task plan\n\n"); + + prompt.append("Output your recovery plan as JSON:\n"); + + return prompt.toString(); + } + + /** + * Build plan validation prompt to check if a plan will work + * Used for self-reflection before executing + */ + public static String buildPlanValidationPrompt(String plan, List tasks) { + StringBuilder prompt = new StringBuilder(); + + prompt.append("=== PLAN VALIDATION ===\n\n"); + + prompt.append("PROPOSED PLAN: ").append(plan).append("\n"); + prompt.append("TASKS:\n"); + for (int i = 0; i < tasks.size(); i++) { + prompt.append(String.format("%d. %s\n", i + 1, tasks.get(i))); + } + + prompt.append("\nVALIDATION QUESTIONS:\n"); + prompt.append("1. Are the tasks in the correct order?\n"); + prompt.append("2. Are there any missing prerequisites?\n"); + prompt.append("3. Will this plan actually achieve the goal?\n"); + prompt.append("4. Are there any potential blockers?\n"); + prompt.append("5. Is there a more efficient approach?\n\n"); + + prompt.append("If plan is valid, respond: {\"valid\": true}\n"); + prompt.append("If plan needs changes, respond: {\"valid\": false, \"issues\": \"description\", \"suggested_changes\": \"improvements\"}\n"); + return prompt.toString(); } diff --git a/src/main/java/com/steve/ai/client/ClientEventHandler.java b/src/main/java/com/steve/ai/client/ClientEventHandler.java index 610f45a..94e0cef 100644 --- a/src/main/java/com/steve/ai/client/ClientEventHandler.java +++ b/src/main/java/com/steve/ai/client/ClientEventHandler.java @@ -30,7 +30,11 @@ public static void onClientTick(TickEvent.ClientTickEvent event) { narratorDisabled = true; } - if (KeyBindings.TOGGLE_GUI != null && KeyBindings.TOGGLE_GUI.consumeClick()) { SteveGUI.toggle(); + // Only toggle GUI if input box is not focused (prevent closing while typing "k") + if (KeyBindings.TOGGLE_GUI != null && KeyBindings.TOGGLE_GUI.consumeClick()) { + if (!SteveGUI.isOpen() || mc.screen == null) { + SteveGUI.toggle(); + } } } } diff --git a/src/main/java/com/steve/ai/client/SteveGUI.java b/src/main/java/com/steve/ai/client/SteveGUI.java index 45ba455..06a2059 100644 --- a/src/main/java/com/steve/ai/client/SteveGUI.java +++ b/src/main/java/com/steve/ai/client/SteveGUI.java @@ -84,6 +84,10 @@ public static boolean isOpen() { return isOpen; } + public static boolean isInputFocused() { + return inputBox != null && inputBox.isFocused(); + } + private static void initializeInputBox() { Minecraft mc = Minecraft.getInstance(); if (inputBox == null) { @@ -335,13 +339,15 @@ public static boolean handleKeyPress(int keyCode, int scanCode, int modifiers) { } // Backspace, Delete, Home, End, Left, Right - pass to input box - if (keyCode == 259 || keyCode == 261 || keyCode == 268 || keyCode == 269 || + if (keyCode == 259 || keyCode == 261 || keyCode == 268 || keyCode == 269 || keyCode == 263 || keyCode == 262) { inputBox.keyPressed(keyCode, scanCode, modifiers); return true; } - return true; // Consume all keys to prevent game controls + // For normal character keys, return false so charTyped can handle them + // This allows typing letters like 'k' without closing the GUI + return false; } public static boolean handleCharTyped(char codePoint, int modifiers) { diff --git a/src/main/java/com/steve/ai/client/SteveOverlayScreen.java b/src/main/java/com/steve/ai/client/SteveOverlayScreen.java index cfbf088..a32dee6 100644 --- a/src/main/java/com/steve/ai/client/SteveOverlayScreen.java +++ b/src/main/java/com/steve/ai/client/SteveOverlayScreen.java @@ -27,16 +27,21 @@ public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTi @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - // K key to close - if (keyCode == 75 && !hasShiftDown() && !hasControlDown() && !hasAltDown()) { // K + // First, let SteveGUI handle the key (it will consume special keys like Enter, ESC, arrows) + boolean handled = SteveGUI.handleKeyPress(keyCode, scanCode, modifiers); + + // If not handled and it's K key and input is NOT focused, toggle GUI + // This prevents closing GUI while typing 'k' in the input box + if (!handled && keyCode == 75 && !SteveGUI.isInputFocused() && + !hasShiftDown() && !hasControlDown() && !hasAltDown()) { // K SteveGUI.toggle(); if (minecraft != null) { minecraft.setScreen(null); } return true; } - - return SteveGUI.handleKeyPress(keyCode, scanCode, modifiers); + + return handled; } @Override diff --git a/src/main/java/com/steve/ai/combat/BossFightCoordinator.java b/src/main/java/com/steve/ai/combat/BossFightCoordinator.java new file mode 100644 index 0000000..e617599 --- /dev/null +++ b/src/main/java/com/steve/ai/combat/BossFightCoordinator.java @@ -0,0 +1,205 @@ +package com.steve.ai.combat; + +import com.steve.ai.SteveMod; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.team.Team; +import com.steve.ai.team.TeamManager; +import com.steve.ai.team.SteveRole; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.entity.boss.wither.WitherBoss; +import net.minecraft.world.entity.raid.Raider; +import net.minecraft.world.phys.AABB; + +import java.util.*; + +/** + * Coordinates team-based boss fights + * Assigns roles and strategies for fighting powerful enemies + */ +public class BossFightCoordinator { + private final String teamName; + private final Team team; + private LivingEntity bossTarget; + private final Map assignedRoles; + + public enum CombatRole { + TANK, // Draws aggro, uses shield + DPS_MELEE, // Close-range damage + DPS_RANGED, // Bow attacks from distance + SUPPORT // Healing, backup + } + + public BossFightCoordinator(String teamName) { + this.teamName = teamName; + this.team = TeamManager.getInstance().getTeam(teamName); + this.assignedRoles = new HashMap<>(); + + if (team == null) { + SteveMod.LOGGER.error("Boss fight coordinator created for non-existent team: {}", teamName); + } + } + + /** + * Initiate a boss fight with team coordination + */ + public boolean initiateBossFight(LivingEntity boss) { + if (team == null || team.getAllMembers().isEmpty()) { + SteveMod.LOGGER.error("Cannot initiate boss fight: team is null or empty"); + return false; + } + + this.bossTarget = boss; + + // Assign combat roles based on team size + assignCombatRoles(); + + // Notify team + team.broadcastMessage(new Team.TeamMessage( + "Coordinator", + "Boss fight initiated! Target: " + boss.getType().toString() + )); + + SteveMod.LOGGER.info("Team '{}' initiating boss fight against {}", + teamName, boss.getType().toString()); + + return true; + } + + /** + * Assign combat roles to team members + */ + private void assignCombatRoles() { + if (team == null) return; + + Map members = team.getAllMembers(); + List memberNames = new ArrayList<>(members.keySet()); + + // Clear previous assignments + assignedRoles.clear(); + + if (memberNames.isEmpty()) { + return; + } + + // Assign roles based on team size + int teamSize = memberNames.size(); + + if (teamSize == 1) { + // Solo: versatile fighter + assignedRoles.put(memberNames.get(0), CombatRole.DPS_MELEE); + } else if (teamSize == 2) { + // Duo: tank + DPS + assignedRoles.put(memberNames.get(0), CombatRole.TANK); + assignedRoles.put(memberNames.get(1), CombatRole.DPS_RANGED); + } else if (teamSize == 3) { + // Trio: tank + melee DPS + ranged DPS + assignedRoles.put(memberNames.get(0), CombatRole.TANK); + assignedRoles.put(memberNames.get(1), CombatRole.DPS_MELEE); + assignedRoles.put(memberNames.get(2), CombatRole.DPS_RANGED); + } else { + // 4+: tank + DPS + support + assignedRoles.put(memberNames.get(0), CombatRole.TANK); + for (int i = 1; i < teamSize - 1; i++) { + // Alternate melee/ranged DPS + CombatRole role = (i % 2 == 0) ? CombatRole.DPS_MELEE : CombatRole.DPS_RANGED; + assignedRoles.put(memberNames.get(i), role); + } + assignedRoles.put(memberNames.get(teamSize - 1), CombatRole.SUPPORT); + } + + // Log assignments + assignedRoles.forEach((name, role) -> + SteveMod.LOGGER.info("Team '{}' - {} assigned role: {}", + teamName, name, role) + ); + } + + /** + * Get combat role for a team member + */ + public CombatRole getCombatRole(String steveName) { + return assignedRoles.getOrDefault(steveName, CombatRole.DPS_MELEE); + } + + /** + * Get recommended action for team member based on role + */ + public String getRecommendedAction(String steveName) { + CombatRole role = getCombatRole(steveName); + + return switch (role) { + case TANK -> "attack_melee_shield"; // Melee with shield blocking + case DPS_MELEE -> "attack_melee"; // Aggressive melee + case DPS_RANGED -> "attack_ranged"; // Bow attacks + case SUPPORT -> "support"; // Stay back, assist if needed + }; + } + + /** + * Check if entity is a boss + */ + public static boolean isBoss(LivingEntity entity) { + return entity instanceof EnderDragon || + entity instanceof WitherBoss || + entity instanceof Raider; // Raid captains + } + + /** + * Find nearest boss entity + */ + public static LivingEntity findNearestBoss(SteveEntity steve, double range) { + AABB searchBox = steve.getBoundingBox().inflate(range); + List entities = steve.level().getEntities(steve, searchBox); + + LivingEntity nearestBoss = null; + double nearestDistance = Double.MAX_VALUE; + + for (Entity entity : entities) { + if (entity instanceof LivingEntity living && isBoss(living)) { + double distance = steve.distanceTo(living); + if (distance < nearestDistance) { + nearestBoss = living; + nearestDistance = distance; + } + } + } + + return nearestBoss; + } + + /** + * Get boss fight status summary + */ + public String getStatusSummary() { + if (bossTarget == null) { + return "No active boss fight"; + } + + if (!bossTarget.isAlive()) { + return "Boss defeated!"; + } + + float healthPercent = (bossTarget.getHealth() / bossTarget.getMaxHealth()) * 100; + + return String.format("Boss: %s | Health: %.0f%% | Team: %d members", + bossTarget.getType().toString(), + healthPercent, + team != null ? team.getAllMembers().size() : 0); + } + + /** + * Check if boss fight is still active + */ + public boolean isActive() { + return bossTarget != null && bossTarget.isAlive(); + } + + /** + * Get current boss target + */ + public LivingEntity getBossTarget() { + return bossTarget; + } +} diff --git a/src/main/java/com/steve/ai/combat/CombatEquipmentManager.java b/src/main/java/com/steve/ai/combat/CombatEquipmentManager.java new file mode 100644 index 0000000..133d253 --- /dev/null +++ b/src/main/java/com/steve/ai/combat/CombatEquipmentManager.java @@ -0,0 +1,373 @@ +package com.steve.ai.combat; + +import com.steve.ai.SteveMod; +import com.steve.ai.entity.SteveEntity; +import com.steve.ai.util.InventoryHelper; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.*; +import net.minecraftforge.items.ItemStackHandler; + +import java.util.*; + +/** + * Manages combat equipment for Steve entities + * Handles automatic armor equipping, weapon selection, and shield usage + */ +public class CombatEquipmentManager { + private final SteveEntity steve; + + // Equipment tier rankings (higher = better) + private static final Map ARMOR_TIERS = new HashMap<>() {{ + put("leather", 1); + put("chainmail", 2); + put("golden", 2); + put("iron", 3); + put("diamond", 4); + put("netherite", 5); + }}; + + private static final Map WEAPON_TIERS = new HashMap<>() {{ + put("wooden", 1); + put("stone", 2); + put("golden", 2); + put("iron", 3); + put("diamond", 4); + put("netherite", 5); + }}; + + public CombatEquipmentManager(SteveEntity steve) { + this.steve = steve; + } + + /** + * Equip best available armor from inventory + * @return true if armor was equipped + */ + public boolean equipBestArmor() { + boolean equipped = false; + + equipped |= equipBestArmorPiece(EquipmentSlot.HEAD); + equipped |= equipBestArmorPiece(EquipmentSlot.CHEST); + equipped |= equipBestArmorPiece(EquipmentSlot.LEGS); + equipped |= equipBestArmorPiece(EquipmentSlot.FEET); + + if (equipped) { + SteveMod.LOGGER.info("Steve '{}' equipped armor", steve.getSteveName()); + } + + return equipped; + } + + /** + * Equip best armor piece for a specific slot + */ + private boolean equipBestArmorPiece(EquipmentSlot slot) { + ItemStackHandler inventory = steve.getInventory(); + ItemStack currentArmor = steve.getItemBySlot(slot); + ItemStack bestArmor = currentArmor.copy(); + int bestTier = getArmorTier(currentArmor); + + // Search inventory for better armor + for (int i = 0; i < inventory.getSlots(); i++) { + ItemStack stack = inventory.getStackInSlot(i); + + if (stack.isEmpty() || !(stack.getItem() instanceof ArmorItem armorItem)) { + continue; + } + + // Check if armor matches this slot + if (armorItem.getEquipmentSlot() != slot) { + continue; + } + + int tier = getArmorTier(stack); + if (tier > bestTier) { + bestArmor = stack.copy(); + bestTier = tier; + } + } + + // Equip if found better armor + if (bestTier > getArmorTier(currentArmor)) { + steve.setItemSlot(slot, bestArmor); + // Remove from inventory + for (int i = 0; i < inventory.getSlots(); i++) { + ItemStack stack = inventory.getStackInSlot(i); + if (ItemStack.matches(stack, bestArmor)) { + inventory.extractItem(i, 1, false); + break; + } + } + return true; + } + + return false; + } + + /** + * Get armor tier from item stack + */ + private int getArmorTier(ItemStack stack) { + if (stack.isEmpty() || !(stack.getItem() instanceof ArmorItem)) { + return 0; + } + + String itemName = stack.getItem().toString().toLowerCase(); + + for (Map.Entry entry : ARMOR_TIERS.entrySet()) { + if (itemName.contains(entry.getKey())) { + return entry.getValue(); + } + } + + return 0; + } + + /** + * Equip best available weapon from inventory + * @return true if weapon was equipped + */ + public boolean equipBestWeapon() { + ItemStackHandler inventory = steve.getInventory(); + ItemStack currentWeapon = steve.getMainHandItem(); + ItemStack bestWeapon = currentWeapon.copy(); + int bestTier = getWeaponTier(currentWeapon); + + // Search inventory for better weapon + for (int i = 0; i < inventory.getSlots(); i++) { + ItemStack stack = inventory.getStackInSlot(i); + + if (stack.isEmpty()) { + continue; + } + + Item item = stack.getItem(); + if (!(item instanceof SwordItem) && !(item instanceof AxeItem)) { + continue; + } + + int tier = getWeaponTier(stack); + if (tier > bestTier) { + bestWeapon = stack.copy(); + bestTier = tier; + } + } + + // Equip if found better weapon + if (bestTier > getWeaponTier(currentWeapon)) { + steve.setItemSlot(EquipmentSlot.MAINHAND, bestWeapon); + SteveMod.LOGGER.info("Steve '{}' equipped {} weapon", + steve.getSteveName(), bestWeapon.getItem().toString()); + return true; + } + + return false; + } + + /** + * Get weapon tier from item stack + */ + private int getWeaponTier(ItemStack stack) { + if (stack.isEmpty()) { + return 0; + } + + Item item = stack.getItem(); + if (!(item instanceof SwordItem) && !(item instanceof AxeItem)) { + return 0; + } + + String itemName = item.toString().toLowerCase(); + + for (Map.Entry entry : WEAPON_TIERS.entrySet()) { + if (itemName.contains(entry.getKey())) { + // Swords are slightly better than axes + int tierBonus = item instanceof SwordItem ? 1 : 0; + return entry.getValue() + tierBonus; + } + } + + return 1; // Default tier + } + + /** + * Equip shield from inventory + * @return true if shield was equipped + */ + public boolean equipShield() { + ItemStackHandler inventory = steve.getInventory(); + ItemStack offHand = steve.getOffhandItem(); + + // Already have shield equipped + if (offHand.getItem() instanceof ShieldItem) { + return false; + } + + // Search for shield in inventory + for (int i = 0; i < inventory.getSlots(); i++) { + ItemStack stack = inventory.getStackInSlot(i); + + if (!stack.isEmpty() && stack.getItem() instanceof ShieldItem) { + steve.setItemSlot(EquipmentSlot.OFFHAND, stack.copy()); + inventory.extractItem(i, 1, false); + SteveMod.LOGGER.info("Steve '{}' equipped shield", steve.getSteveName()); + return true; + } + } + + return false; + } + + /** + * Equip bow and arrows from inventory + * @return true if bow and arrows were equipped + */ + public boolean equipBowAndArrows() { + ItemStackHandler inventory = steve.getInventory(); + + // Check if we have both bow and arrows + boolean hasBow = false; + boolean hasArrows = false; + + for (int i = 0; i < inventory.getSlots(); i++) { + ItemStack stack = inventory.getStackInSlot(i); + if (stack.isEmpty()) continue; + + if (stack.getItem() instanceof BowItem) { + hasBow = true; + } + if (stack.getItem() instanceof ArrowItem) { + hasArrows = true; + } + } + + if (!hasBow || !hasArrows) { + return false; + } + + // Equip bow + for (int i = 0; i < inventory.getSlots(); i++) { + ItemStack stack = inventory.getStackInSlot(i); + if (!stack.isEmpty() && stack.getItem() instanceof BowItem) { + steve.setItemSlot(EquipmentSlot.MAINHAND, stack.copy()); + SteveMod.LOGGER.info("Steve '{}' equipped bow", steve.getSteveName()); + return true; + } + } + + return false; + } + + /** + * Check if Steve has arrows in inventory + */ + public boolean hasArrows() { + ItemStackHandler inventory = steve.getInventory(); + + for (int i = 0; i < inventory.getSlots(); i++) { + ItemStack stack = inventory.getStackInSlot(i); + if (!stack.isEmpty() && stack.getItem() instanceof ArrowItem) { + return true; + } + } + + return false; + } + + /** + * Check if Steve has a shield equipped + */ + public boolean hasShieldEquipped() { + return steve.getOffhandItem().getItem() instanceof ShieldItem; + } + + /** + * Check if Steve has a bow equipped + */ + public boolean hasBowEquipped() { + return steve.getMainHandItem().getItem() instanceof BowItem; + } + + /** + * Auto-equip best combat gear + * @return summary of equipped items + */ + public String autoEquipCombatGear() { + StringBuilder summary = new StringBuilder(); + + if (equipBestArmor()) { + summary.append("Equipped armor. "); + } + + if (equipBestWeapon()) { + summary.append("Equipped weapon. "); + } + + if (equipShield()) { + summary.append("Equipped shield. "); + } + + if (summary.length() == 0) { + return "Already equipped with best available gear."; + } + + return summary.toString().trim(); + } + + /** + * Get combat readiness score (0-100) + */ + public int getCombatReadiness() { + int score = 0; + + // Weapon (30 points) + int weaponTier = getWeaponTier(steve.getMainHandItem()); + score += Math.min(30, weaponTier * 6); + + // Armor (40 points total, 10 per piece) + score += Math.min(10, getArmorTier(steve.getItemBySlot(EquipmentSlot.HEAD)) * 2); + score += Math.min(10, getArmorTier(steve.getItemBySlot(EquipmentSlot.CHEST)) * 2); + score += Math.min(10, getArmorTier(steve.getItemBySlot(EquipmentSlot.LEGS)) * 2); + score += Math.min(10, getArmorTier(steve.getItemBySlot(EquipmentSlot.FEET)) * 2); + + // Shield (15 points) + if (hasShieldEquipped()) { + score += 15; + } + + // Bow and arrows (15 points) + if (hasBowEquipped() && hasArrows()) { + score += 15; + } + + return Math.min(100, score); + } + + /** + * Get equipment summary + */ + public String getEquipmentSummary() { + StringBuilder sb = new StringBuilder(); + sb.append("Combat Readiness: ").append(getCombatReadiness()).append("%\n"); + + ItemStack weapon = steve.getMainHandItem(); + if (!weapon.isEmpty()) { + sb.append("Weapon: ").append(weapon.getItem().toString()).append("\n"); + } + + ItemStack shield = steve.getOffhandItem(); + if (!shield.isEmpty()) { + sb.append("Shield: ").append(shield.getItem().toString()).append("\n"); + } + + sb.append("Armor: "); + int armorPieces = 0; + if (!steve.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) armorPieces++; + if (!steve.getItemBySlot(EquipmentSlot.CHEST).isEmpty()) armorPieces++; + if (!steve.getItemBySlot(EquipmentSlot.LEGS).isEmpty()) armorPieces++; + if (!steve.getItemBySlot(EquipmentSlot.FEET).isEmpty()) armorPieces++; + sb.append(armorPieces).append("/4 pieces"); + + return sb.toString(); + } +} diff --git a/src/main/java/com/steve/ai/dimension/DimensionNavigator.java b/src/main/java/com/steve/ai/dimension/DimensionNavigator.java new file mode 100644 index 0000000..c01f958 --- /dev/null +++ b/src/main/java/com/steve/ai/dimension/DimensionNavigator.java @@ -0,0 +1,278 @@ +package com.steve.ai.dimension; + +import com.steve.ai.SteveMod; +import com.steve.ai.entity.SteveEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Helper for safe navigation in Nether and End dimensions + * Handles dimension-specific hazards and navigation + */ +public class DimensionNavigator { + private final SteveEntity steve; + private final Level level; + + public enum Dimension { + OVERWORLD, + NETHER, + END, + UNKNOWN + } + + public DimensionNavigator(SteveEntity steve) { + this.steve = steve; + this.level = steve.level(); + } + + /** + * Get current dimension + */ + public Dimension getCurrentDimension() { + String dimensionKey = level.dimension().location().toString(); + + return switch (dimensionKey) { + case "minecraft:overworld" -> Dimension.OVERWORLD; + case "minecraft:the_nether" -> Dimension.NETHER; + case "minecraft:the_end" -> Dimension.END; + default -> Dimension.UNKNOWN; + }; + } + + /** + * Check if position is safe in current dimension + */ + public boolean isSafePosition(BlockPos pos) { + Dimension dim = getCurrentDimension(); + + return switch (dim) { + case NETHER -> isSafeInNether(pos); + case END -> isSafeInEnd(pos); + case OVERWORLD -> true; // Overworld is generally safe + case UNKNOWN -> false; + }; + } + + /** + * Check if position is safe in Nether + */ + private boolean isSafeInNether(BlockPos pos) { + // Check for lava nearby + if (hasLavaNearby(pos, 2)) { + return false; + } + + // Check for solid ground + if (!hasSolidGround(pos)) { + return false; + } + + // Check for open lava ocean below + if (isAboveLavaOcean(pos)) { + return false; + } + + return true; + } + + /** + * Check if position is safe in End + */ + private boolean isSafeInEnd(BlockPos pos) { + // Check for void below + if (isAboveVoid(pos, 10)) { + return false; + } + + // Check for solid ground + if (!hasSolidGround(pos)) { + return false; + } + + return true; + } + + /** + * Check if there's lava nearby + */ + private boolean hasLavaNearby(BlockPos center, int radius) { + for (int x = -radius; x <= radius; x++) { + for (int y = -radius; y <= radius; y++) { + for (int z = -radius; z <= radius; z++) { + BlockPos checkPos = center.offset(x, y, z); + BlockState state = level.getBlockState(checkPos); + + if (state.getBlock() == Blocks.LAVA) { + return true; + } + } + } + } + return false; + } + + /** + * Check if position has solid ground below + */ + private boolean hasSolidGround(BlockPos pos) { + BlockPos groundPos = pos.below(); + BlockState groundState = level.getBlockState(groundPos); + return groundState.isSolid(); + } + + /** + * Check if position is above lava ocean (common in Nether) + */ + private boolean isAboveLavaOcean(BlockPos pos) { + // Check several blocks below + for (int y = 1; y <= 5; y++) { + BlockPos checkPos = pos.below(y); + BlockState state = level.getBlockState(checkPos); + + if (state.getBlock() == Blocks.LAVA) { + return true; + } + } + return false; + } + + /** + * Check if position is above void + */ + private boolean isAboveVoid(BlockPos pos, int checkDepth) { + for (int y = 1; y <= checkDepth; y++) { + BlockPos checkPos = pos.below(y); + + // In End, Y < 0 is void + if (checkPos.getY() < 0) { + return true; + } + + BlockState state = level.getBlockState(checkPos); + if (state.isSolid()) { + return false; // Found solid ground + } + } + + // No solid ground found within check depth + return pos.getY() < 64; // Assume void if low Y and no ground + } + + /** + * Find nearest safe position from current location + */ + public BlockPos findNearestSafePosition(int searchRadius) { + BlockPos startPos = steve.blockPosition(); + BlockPos nearestSafe = null; + double nearestDistance = Double.MAX_VALUE; + + for (int x = -searchRadius; x <= searchRadius; x++) { + for (int z = -searchRadius; z <= searchRadius; z++) { + for (int y = -5; y <= 5; y++) { + BlockPos checkPos = startPos.offset(x, y, z); + + if (isSafePosition(checkPos)) { + double distance = startPos.distSqr(checkPos); + if (distance < nearestDistance) { + nearestSafe = checkPos; + nearestDistance = distance; + } + } + } + } + } + + return nearestSafe; + } + + /** + * Get dimension-specific safety advice + */ + public String getSafetyAdvice() { + Dimension dim = getCurrentDimension(); + + return switch (dim) { + case NETHER -> "Nether: Watch for lava, ghasts, and open spaces. Build paths carefully."; + case END -> "End: Avoid void edges. Beware of Endermen and the Dragon."; + case OVERWORLD -> "Overworld: Standard precautions apply."; + case UNKNOWN -> "Unknown dimension: Exercise extreme caution."; + }; + } + + /** + * Check if dimension is hostile + */ + public boolean isHostileDimension() { + Dimension dim = getCurrentDimension(); + return dim == Dimension.NETHER || dim == Dimension.END; + } + + /** + * Get recommended equipment for current dimension + */ + public String getRecommendedEquipment() { + Dimension dim = getCurrentDimension(); + + return switch (dim) { + case NETHER -> "Fire Resistance potions, good armor, cobblestone for bridging"; + case END -> "Bow and arrows, good armor, ender pearls, slow falling potions"; + case OVERWORLD -> "Standard equipment"; + case UNKNOWN -> "Full protection recommended"; + }; + } + + /** + * Find nearest nether fortress (simplified detection) + */ + public BlockPos findNetherFortress(int searchRadius) { + if (getCurrentDimension() != Dimension.NETHER) { + return null; + } + + BlockPos stevePos = steve.blockPosition(); + + // Search for nether brick blocks (indicates fortress) + for (int x = -searchRadius; x <= searchRadius; x += 16) { + for (int z = -searchRadius; z <= searchRadius; z += 16) { + for (int y = 32; y <= 96; y += 8) { + BlockPos checkPos = stevePos.offset(x, y - stevePos.getY(), z); + + if (level.getBlockState(checkPos).getBlock() == Blocks.NETHER_BRICKS) { + SteveMod.LOGGER.info("Steve '{}' found nether fortress at {}", + steve.getSteveName(), checkPos); + return checkPos; + } + } + } + } + + return null; + } + + /** + * Check if near End portal + */ + public boolean isNearEndPortal(int radius) { + if (getCurrentDimension() != Dimension.END) { + return false; + } + + BlockPos stevePos = steve.blockPosition(); + + for (int x = -radius; x <= radius; x++) { + for (int z = -radius; z <= radius; z++) { + for (int y = -radius; y <= radius; y++) { + BlockPos checkPos = stevePos.offset(x, y, z); + + if (level.getBlockState(checkPos).getBlock() == Blocks.END_PORTAL) { + return true; + } + } + } + } + + return false; + } +} diff --git a/src/main/java/com/steve/ai/entity/SteveEntity.java b/src/main/java/com/steve/ai/entity/SteveEntity.java index c515d2f..4303db3 100644 --- a/src/main/java/com/steve/ai/entity/SteveEntity.java +++ b/src/main/java/com/steve/ai/entity/SteveEntity.java @@ -1,7 +1,14 @@ package com.steve.ai.entity; import com.steve.ai.action.ActionExecutor; +import com.steve.ai.action.HungerManager; +import com.steve.ai.dimension.DimensionNavigator; import com.steve.ai.memory.SteveMemory; +import com.steve.ai.quest.AchievementManager; +import com.steve.ai.quest.QuestManager; +import com.steve.ai.team.Team; +import com.steve.ai.team.TeamManager; +import com.steve.ai.team.SteveRole; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.network.syncher.EntityDataAccessor; @@ -17,15 +24,23 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.level.ServerLevelAccessor; +import net.minecraftforge.items.ItemStackHandler; import org.jetbrains.annotations.Nullable; public class SteveEntity extends PathfinderMob { - private static final EntityDataAccessor STEVE_NAME = + private static final EntityDataAccessor STEVE_NAME = SynchedEntityData.defineId(SteveEntity.class, EntityDataSerializers.STRING); + // Inventory size: 36 slots (same as player - 27 main + 9 hotbar) + private static final int INVENTORY_SIZE = 36; + private String steveName; private SteveMemory memory; private ActionExecutor actionExecutor; + private HungerManager hungerManager; + private QuestManager questManager; + private AchievementManager achievementManager; + private ItemStackHandler inventory; private int tickCounter = 0; private boolean isFlying = false; private boolean isInvulnerable = false; @@ -35,8 +50,12 @@ public SteveEntity(EntityType entityType, Level level) this.steveName = "Steve"; this.memory = new SteveMemory(this); this.actionExecutor = new ActionExecutor(this); + this.hungerManager = new HungerManager(this); + this.questManager = new QuestManager(this.steveName); + this.achievementManager = new AchievementManager(this.steveName); + this.inventory = new ItemStackHandler(INVENTORY_SIZE); this.setCustomNameVisible(true); - + this.isInvulnerable = true; this.setInvulnerable(true); } @@ -65,9 +84,10 @@ protected void defineSynchedData() { @Override public void tick() { super.tick(); - + if (!this.level().isClientSide) { actionExecutor.tick(); + hungerManager.tick(); } } @@ -75,6 +95,10 @@ public void setSteveName(String name) { this.steveName = name; this.entityData.set(STEVE_NAME, name); this.setCustomName(Component.literal(name)); + + // Reinitialize managers with new name + this.questManager = new QuestManager(name); + this.achievementManager = new AchievementManager(name); } public String getSteveName() { @@ -89,14 +113,95 @@ public ActionExecutor getActionExecutor() { return this.actionExecutor; } + public HungerManager getHungerManager() { + return this.hungerManager; + } + + public QuestManager getQuestManager() { + return this.questManager; + } + + public AchievementManager getAchievementManager() { + return this.achievementManager; + } + + /** + * Get Steve's inventory handler + * @return ItemStackHandler with 36 slots + */ + public ItemStackHandler getInventory() { + return this.inventory; + } + + // ==================== Team Methods ==================== + + /** + * Get this Steve's team + * @return Team or null if not in a team + */ + public Team getTeam() { + return TeamManager.getInstance().getTeam(this.steveName); + } + + /** + * Get this Steve's role + * @return SteveRole (GENERALIST if not in team or no role assigned) + */ + public SteveRole getRole() { + return TeamManager.getInstance().getRole(this.steveName); + } + + /** + * Check if this Steve is in a team + * @return True if in a team + */ + public boolean isInTeam() { + return TeamManager.getInstance().isInTeam(this.steveName); + } + + /** + * Check if this Steve is the team leader + * @return True if Steve is leader of their team + */ + public boolean isTeamLeader() { + Team team = getTeam(); + return team != null && this.steveName.equals(team.getLeaderName()); + } + + /** + * Send a message to team + * @param message Message content + */ + public void sendTeamMessage(String message) { + Team team = getTeam(); + if (team != null) { + team.broadcastMessage(new Team.TeamMessage(this.steveName, message)); + } + } + + /** + * Get recent team messages + * @param count Number of messages to retrieve + * @return List of team messages + */ + public java.util.List getTeamMessages(int count) { + Team team = getTeam(); + return team != null ? team.getMessagesFor(this.steveName) : java.util.Collections.emptyList(); + } + + // ======================================================= + @Override public void addAdditionalSaveData(CompoundTag tag) { super.addAdditionalSaveData(tag); tag.putString("SteveName", this.steveName); - + CompoundTag memoryTag = new CompoundTag(); this.memory.saveToNBT(memoryTag); tag.put("Memory", memoryTag); + + // Save inventory + tag.put("Inventory", this.inventory.serializeNBT()); } @Override @@ -105,10 +210,15 @@ public void readAdditionalSaveData(CompoundTag tag) { if (tag.contains("SteveName")) { this.setSteveName(tag.getString("SteveName")); } - + if (tag.contains("Memory")) { this.memory.loadFromNBT(tag.getCompound("Memory")); } + + // Load inventory + if (tag.contains("Inventory")) { + this.inventory.deserializeNBT(tag.getCompound("Inventory")); + } } @Override diff --git a/src/main/java/com/steve/ai/event/ServerEventHandler.java b/src/main/java/com/steve/ai/event/ServerEventHandler.java index 88d984e..8c74339 100644 --- a/src/main/java/com/steve/ai/event/ServerEventHandler.java +++ b/src/main/java/com/steve/ai/event/ServerEventHandler.java @@ -20,38 +20,29 @@ public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { if (event.getEntity() instanceof ServerPlayer player) { ServerLevel level = (ServerLevel) player.level(); SteveManager manager = SteveMod.getSteveManager(); - if (!stevesSpawned) { manager.clearAllSteves(); - + + if (!stevesSpawned) { + // Only clear existing Steves, don't auto-spawn new ones + manager.clearAllSteves(); + // Clear structure registry for fresh spatial awareness StructureRegistry.clear(); - - // Then, remove ALL SteveEntity instances from the world (including ones loaded from NBT) + + // Remove ALL SteveEntity instances from the world (including ones loaded from NBT) int removedCount = 0; for (var entity : level.getAllEntities()) { if (entity instanceof SteveEntity) { entity.discard(); removedCount++; } - } Vec3 playerPos = player.position(); - Vec3 lookVec = player.getLookAngle(); - - String[] names = {"Steve", "Alex", "Bob", "Charlie"}; - - for (int i = 0; i < 4; i++) { - double offsetX = lookVec.x * 5 + (lookVec.z * (i - 1.5) * 2); - double offsetZ = lookVec.z * 5 + (-lookVec.x * (i - 1.5) * 2); - - Vec3 spawnPos = new Vec3( - playerPos.x + offsetX, - playerPos.y, - playerPos.z + offsetZ - ); - - SteveEntity steve = manager.spawnSteve(level, spawnPos, names[i]); - if (steve != null) { } } - - stevesSpawned = true; } + + if (removedCount > 0) { + SteveMod.LOGGER.info("Cleaned up {} existing Steve entities", removedCount); + } + + stevesSpawned = true; + } } } diff --git a/src/main/java/com/steve/ai/learning/ActionKnowledgeBase.java b/src/main/java/com/steve/ai/learning/ActionKnowledgeBase.java new file mode 100644 index 0000000..aa6d54f --- /dev/null +++ b/src/main/java/com/steve/ai/learning/ActionKnowledgeBase.java @@ -0,0 +1,295 @@ +package com.steve.ai.learning; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.steve.ai.SteveMod; +import com.steve.ai.learning.LearningSystem.LearningInsight; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Knowledge base that stores learned patterns and insights + * Persistent storage allows accumulated learning across sessions + */ +public class ActionKnowledgeBase { + private static final String KNOWLEDGE_DIR = "config/steve/knowledge/"; + private static final int MAX_INSIGHTS = 200; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private final String steveName; + private final List insights; + private final Map> actionTips; // Action -> List of tips + private final Map successCounts; // Track successes for learning + + public ActionKnowledgeBase(String steveName) { + this.steveName = steveName; + this.insights = new ArrayList<>(); + this.actionTips = new HashMap<>(); + this.successCounts = new HashMap<>(); + loadFromFile(); + } + + /** + * Add a learning insight to the knowledge base + */ + public void addInsight(LearningInsight insight) { + // Check if similar insight already exists + boolean exists = insights.stream() + .anyMatch(existing -> + existing.getTitle().equals(insight.getTitle()) && + existing.getCategory().equals(insight.getCategory()) + ); + + if (!exists) { + insights.add(insight); + + // Prune old insights if needed + if (insights.size() > MAX_INSIGHTS) { + // Remove oldest low-confidence insights + insights.sort(Comparator.comparingDouble(LearningInsight::getConfidence)); + insights.remove(0); + } + + SteveMod.LOGGER.info("Steve '{}' learned: {}", steveName, insight.getTitle()); + saveToFile(); + } + } + + /** + * Add a tip for a specific action + */ + public void addActionTip(String action, String tip) { + actionTips.computeIfAbsent(action, k -> new ArrayList<>()).add(tip); + + // Limit tips per action + List tips = actionTips.get(action); + if (tips.size() > 10) { + tips.remove(0); // Remove oldest + } + + saveToFile(); + } + + /** + * Record a successful action + */ + public void recordSuccess(String action) { + successCounts.merge(action, 1, Integer::sum); + saveToFile(); + } + + /** + * Get tips for a specific action + */ + public List getTipsForAction(String action) { + return actionTips.getOrDefault(action, Collections.emptyList()); + } + + /** + * Get recent insights + */ + public List getRecentInsights(int count) { + // Sort by timestamp (most recent first) + return insights.stream() + .sorted(Comparator.comparingLong(LearningInsight::getTimestamp).reversed()) + .limit(count) + .collect(Collectors.toList()); + } + + /** + * Get insights by category + */ + public List getInsightsByCategory(String category) { + return insights.stream() + .filter(i -> i.getCategory().equals(category)) + .sorted(Comparator.comparingDouble(LearningInsight::getConfidence).reversed()) + .collect(Collectors.toList()); + } + + /** + * Get high-confidence insights + */ + public List getHighConfidenceInsights(double minConfidence) { + return insights.stream() + .filter(i -> i.getConfidence() >= minConfidence) + .sorted(Comparator.comparingDouble(LearningInsight::getConfidence).reversed()) + .collect(Collectors.toList()); + } + + /** + * Get success count for an action + */ + public int getSuccessCount(String action) { + return successCounts.getOrDefault(action, 0); + } + + /** + * Get most successful actions + */ + public Map getMostSuccessfulActions(int limit) { + return successCounts.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(limit) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new + )); + } + + /** + * Get total insights count + */ + public int getInsightCount() { + return insights.size(); + } + + /** + * Generate knowledge summary for LLM context + */ + public String generateKnowledgeSummary() { + if (insights.isEmpty() && actionTips.isEmpty()) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + sb.append("\n=== LEARNED KNOWLEDGE ===\n"); + + // Add high-confidence insights + List topInsights = getHighConfidenceInsights(0.7); + if (!topInsights.isEmpty()) { + sb.append("Key Insights:\n"); + for (LearningInsight insight : topInsights.stream().limit(5).collect(Collectors.toList())) { + sb.append("- ").append(insight.getRecommendation()).append("\n"); + } + } + + // Add action tips + if (!actionTips.isEmpty()) { + sb.append("\nAction Tips:\n"); + actionTips.entrySet().stream() + .limit(5) + .forEach(entry -> { + sb.append("- ").append(entry.getKey()).append(": "); + sb.append(String.join(", ", entry.getValue())).append("\n"); + }); + } + + // Add success patterns + Map topSuccesses = getMostSuccessfulActions(3); + if (!topSuccesses.isEmpty()) { + sb.append("\nMost Reliable Actions:\n"); + topSuccesses.forEach((action, count) -> + sb.append("- ").append(action).append(" (").append(count).append(" successes)\n") + ); + } + + return sb.toString(); + } + + /** + * Clear all knowledge (reset learning) + */ + public void clear() { + insights.clear(); + actionTips.clear(); + successCounts.clear(); + saveToFile(); + } + + /** + * Save knowledge base to JSON file + */ + private void saveToFile() { + try { + Path dirPath = Paths.get(KNOWLEDGE_DIR); + Files.createDirectories(dirPath); + + String filename = KNOWLEDGE_DIR + steveName + "_knowledge.json"; + File file = new File(filename); + + Map data = new HashMap<>(); + data.put("insights", insights); + data.put("actionTips", actionTips); + data.put("successCounts", successCounts); + + try (FileWriter writer = new FileWriter(file)) { + GSON.toJson(data, writer); + } + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to save knowledge base for Steve '{}'", steveName, e); + } + } + + /** + * Load knowledge base from JSON file + */ + @SuppressWarnings("unchecked") + private void loadFromFile() { + try { + String filename = KNOWLEDGE_DIR + steveName + "_knowledge.json"; + File file = new File(filename); + + if (!file.exists()) { + SteveMod.LOGGER.info("No knowledge base found for Steve '{}', starting fresh", steveName); + return; + } + + try (FileReader reader = new FileReader(file)) { + Type mapType = new TypeToken>(){}.getType(); + Map data = GSON.fromJson(reader, mapType); + + if (data != null) { + // Load insights + if (data.containsKey("insights")) { + Type insightListType = new TypeToken>(){}.getType(); + List loadedInsights = GSON.fromJson( + GSON.toJson(data.get("insights")), + insightListType + ); + if (loadedInsights != null) { + insights.addAll(loadedInsights); + } + } + + // Load action tips + if (data.containsKey("actionTips")) { + Type tipsType = new TypeToken>>(){}.getType(); + Map> loadedTips = GSON.fromJson( + GSON.toJson(data.get("actionTips")), + tipsType + ); + if (loadedTips != null) { + actionTips.putAll(loadedTips); + } + } + + // Load success counts + if (data.containsKey("successCounts")) { + Type countsType = new TypeToken>(){}.getType(); + Map loadedCounts = GSON.fromJson( + GSON.toJson(data.get("successCounts")), + countsType + ); + if (loadedCounts != null) { + loadedCounts.forEach((k, v) -> successCounts.put(k, v.intValue())); + } + } + + SteveMod.LOGGER.info("Loaded knowledge base for Steve '{}': {} insights, {} tips", + steveName, insights.size(), actionTips.size()); + } + } + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to load knowledge base for Steve '{}'", steveName, e); + } + } +} diff --git a/src/main/java/com/steve/ai/learning/AdaptiveRetryStrategy.java b/src/main/java/com/steve/ai/learning/AdaptiveRetryStrategy.java new file mode 100644 index 0000000..7b551fb --- /dev/null +++ b/src/main/java/com/steve/ai/learning/AdaptiveRetryStrategy.java @@ -0,0 +1,201 @@ +package com.steve.ai.learning; + +import com.steve.ai.SteveMod; +import com.steve.ai.action.Task; + +import java.util.HashMap; +import java.util.Map; + +/** + * Adaptive retry strategy that learns optimal retry behavior + * Adjusts retry counts and delays based on past success/failure patterns + */ +public class AdaptiveRetryStrategy { + private final FailureTracker failureTracker; + private final LearningSystem learningSystem; + + // Retry attempt tracking + private final Map retryAttempts; + private final Map lastRetryTime; + + // Default retry settings + private static final int DEFAULT_MAX_RETRIES = 3; + private static final long DEFAULT_RETRY_DELAY_MS = 1000; // 1 second + private static final long MAX_RETRY_DELAY_MS = 10000; // 10 seconds + + // Adaptive parameters + private static final double HIGH_FAILURE_THRESHOLD = 0.6; // 60% + private static final double LOW_FAILURE_THRESHOLD = 0.2; // 20% + + public AdaptiveRetryStrategy(FailureTracker failureTracker, LearningSystem learningSystem) { + this.failureTracker = failureTracker; + this.learningSystem = learningSystem; + this.retryAttempts = new HashMap<>(); + this.lastRetryTime = new HashMap<>(); + } + + /** + * Determine if an action should be retried + */ + public boolean shouldRetry(Task task, String errorMessage) { + String taskKey = getTaskKey(task); + int attemptCount = retryAttempts.getOrDefault(taskKey, 0); + + // Check learning system recommendation + if (!learningSystem.shouldRetry(task.getAction(), attemptCount)) { + SteveMod.LOGGER.info("Learning system recommends not retrying: {}", task.getAction()); + resetRetries(taskKey); + return false; + } + + // Get adaptive max retries based on failure history + int maxRetries = getAdaptiveMaxRetries(task.getAction()); + + if (attemptCount >= maxRetries) { + SteveMod.LOGGER.info("Max retries ({}) reached for: {}", maxRetries, task.getAction()); + resetRetries(taskKey); + return false; + } + + // Check if enough time has passed since last retry + long now = System.currentTimeMillis(); + Long lastRetry = lastRetryTime.get(taskKey); + + if (lastRetry != null) { + long delay = getAdaptiveRetryDelay(task.getAction(), attemptCount); + long timeSinceLastRetry = now - lastRetry; + + if (timeSinceLastRetry < delay) { + // Not enough time has passed, don't retry yet + return false; + } + } + + // Increment retry count + retryAttempts.put(taskKey, attemptCount + 1); + lastRetryTime.put(taskKey, now); + + SteveMod.LOGGER.info("Retry attempt {}/{} for: {}", + attemptCount + 1, maxRetries, task.getAction()); + + return true; + } + + /** + * Reset retry counter for a task (called after success or giving up) + */ + public void resetRetries(Task task) { + String taskKey = getTaskKey(task); + resetRetries(taskKey); + } + + private void resetRetries(String taskKey) { + retryAttempts.remove(taskKey); + lastRetryTime.remove(taskKey); + } + + /** + * Get current retry attempt count + */ + public int getRetryCount(Task task) { + return retryAttempts.getOrDefault(getTaskKey(task), 0); + } + + /** + * Calculate adaptive max retries based on historical failure rate + */ + private int getAdaptiveMaxRetries(String action) { + double failureRate = failureTracker.getFailureRate(action, 20) / 100.0; + + if (failureRate > HIGH_FAILURE_THRESHOLD) { + // High failure rate: reduce retries to 1-2 + return Math.max(1, DEFAULT_MAX_RETRIES - 2); + } else if (failureRate < LOW_FAILURE_THRESHOLD) { + // Low failure rate: allow more retries + return DEFAULT_MAX_RETRIES + 2; + } else { + // Normal failure rate: use default + return DEFAULT_MAX_RETRIES; + } + } + + /** + * Calculate adaptive retry delay using exponential backoff + */ + private long getAdaptiveRetryDelay(String action, int attemptCount) { + double failureRate = failureTracker.getFailureRate(action, 20) / 100.0; + + // Base delay with exponential backoff + long baseDelay = DEFAULT_RETRY_DELAY_MS * (long) Math.pow(2, attemptCount); + + // Adjust based on failure rate + if (failureRate > HIGH_FAILURE_THRESHOLD) { + // High failure rate: increase delay + baseDelay *= 2; + } + + return Math.min(baseDelay, MAX_RETRY_DELAY_MS); + } + + /** + * Get retry strategy info for logging + */ + public String getStrategyInfo(String action) { + int maxRetries = getAdaptiveMaxRetries(action); + double failureRate = failureTracker.getFailureRate(action, 20); + + return String.format("Action: %s | Max Retries: %d | Failure Rate: %.1f%%", + action, maxRetries, failureRate); + } + + /** + * Generate unique key for a task + */ + private String getTaskKey(Task task) { + return task.getAction() + "_" + System.identityHashCode(task); + } + + /** + * Get statistics on retry behavior + */ + public Map getStatistics() { + Map stats = new HashMap<>(); + stats.put("active_retries", retryAttempts.size()); + stats.put("total_failures_tracked", failureTracker.getTotalFailures()); + + // Calculate average retry count + double avgRetries = retryAttempts.values().stream() + .mapToInt(Integer::intValue) + .average() + .orElse(0.0); + stats.put("average_retry_count", avgRetries); + + return stats; + } + + /** + * Retry decision details for debugging + */ + public static class RetryDecision { + public final boolean shouldRetry; + public final String reason; + public final int attemptCount; + public final int maxRetries; + public final long delayMs; + + public RetryDecision(boolean shouldRetry, String reason, int attemptCount, + int maxRetries, long delayMs) { + this.shouldRetry = shouldRetry; + this.reason = reason; + this.attemptCount = attemptCount; + this.maxRetries = maxRetries; + this.delayMs = delayMs; + } + + @Override + public String toString() { + return String.format("Retry: %s | Reason: %s | Attempt: %d/%d | Delay: %dms", + shouldRetry ? "YES" : "NO", reason, attemptCount, maxRetries, delayMs); + } + } +} diff --git a/src/main/java/com/steve/ai/learning/FailureTracker.java b/src/main/java/com/steve/ai/learning/FailureTracker.java new file mode 100644 index 0000000..d75cd54 --- /dev/null +++ b/src/main/java/com/steve/ai/learning/FailureTracker.java @@ -0,0 +1,287 @@ +package com.steve.ai.learning; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.steve.ai.SteveMod; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Tracks failed actions and their contexts for learning and improvement + * Persistent storage allows Steve to learn from past mistakes across sessions + */ +public class FailureTracker { + private static final String FAILURE_LOG_DIR = "config/steve/failures/"; + private static final int MAX_FAILURES_PER_STEVE = 500; // Limit to prevent file bloat + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private final String steveName; + private final List failures; + private final Map failureCountByAction; + private final Map failureCountByError; + + public FailureTracker(String steveName) { + this.steveName = steveName; + this.failures = new ArrayList<>(); + this.failureCountByAction = new HashMap<>(); + this.failureCountByError = new HashMap<>(); + loadFromFile(); + } + + /** + * Record a failed action + */ + public void recordFailure(String action, Map parameters, + String errorMessage, String context) { + FailureRecord record = new FailureRecord( + System.currentTimeMillis(), + action, + parameters, + errorMessage, + context + ); + + failures.add(record); + + // Update counters + failureCountByAction.merge(action, 1, Integer::sum); + failureCountByError.merge(errorMessage, 1, Integer::sum); + + // Prune old failures if needed + if (failures.size() > MAX_FAILURES_PER_STEVE) { + failures.remove(0); // Remove oldest + } + + SteveMod.LOGGER.info("Steve '{}' recorded failure: {} - {}", + steveName, action, errorMessage); + + saveToFile(); + } + + /** + * Get all failures for a specific action type + */ + public List getFailuresForAction(String action) { + return failures.stream() + .filter(f -> f.action.equals(action)) + .collect(Collectors.toList()); + } + + /** + * Get recent failures (last N) + */ + public List getRecentFailures(int count) { + int startIndex = Math.max(0, failures.size() - count); + return new ArrayList<>(failures.subList(startIndex, failures.size())); + } + + /** + * Get failure count for specific action + */ + public int getFailureCount(String action) { + return failureCountByAction.getOrDefault(action, 0); + } + + /** + * Get total failure count + */ + public int getTotalFailures() { + return failures.size(); + } + + /** + * Get most common failures + */ + public Map getMostCommonFailures(int limit) { + return failureCountByAction.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(limit) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new + )); + } + + /** + * Get most common error messages + */ + public Map getMostCommonErrors(int limit) { + return failureCountByError.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .limit(limit) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new + )); + } + + /** + * Check if action has failed recently (within last N attempts) + */ + public boolean hasRecentFailure(String action, int withinLast) { + List recent = getRecentFailures(withinLast); + return recent.stream().anyMatch(f -> f.action.equals(action)); + } + + /** + * Get failure rate for an action (percentage) + */ + public double getFailureRate(String action, int sampleSize) { + List recentForAction = failures.stream() + .filter(f -> f.action.equals(action)) + .skip(Math.max(0, failures.stream().filter(f -> f.action.equals(action)).count() - sampleSize)) + .collect(Collectors.toList()); + + if (recentForAction.isEmpty()) { + return 0.0; + } + + return (double) recentForAction.size() / Math.max(1, sampleSize) * 100.0; + } + + /** + * Clear all failure records + */ + public void clear() { + failures.clear(); + failureCountByAction.clear(); + failureCountByError.clear(); + saveToFile(); + } + + /** + * Generate failure summary + */ + public String generateSummary() { + if (failures.isEmpty()) { + return "No failures recorded yet."; + } + + StringBuilder sb = new StringBuilder(); + sb.append("Failure Summary for Steve '").append(steveName).append("':\n"); + sb.append("Total Failures: ").append(getTotalFailures()).append("\n\n"); + + sb.append("Most Common Failed Actions:\n"); + getMostCommonFailures(5).forEach((action, count) -> + sb.append(" - ").append(action).append(": ").append(count).append(" times\n") + ); + + sb.append("\nMost Common Errors:\n"); + getMostCommonErrors(5).forEach((error, count) -> + sb.append(" - ").append(error).append(": ").append(count).append(" times\n") + ); + + return sb.toString(); + } + + /** + * Save failures to JSON file + */ + private void saveToFile() { + try { + Path dirPath = Paths.get(FAILURE_LOG_DIR); + Files.createDirectories(dirPath); + + String filename = FAILURE_LOG_DIR + steveName + "_failures.json"; + File file = new File(filename); + + try (FileWriter writer = new FileWriter(file)) { + GSON.toJson(failures, writer); + } + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to save failure log for Steve '{}'", steveName, e); + } + } + + /** + * Load failures from JSON file + */ + private void loadFromFile() { + try { + String filename = FAILURE_LOG_DIR + steveName + "_failures.json"; + File file = new File(filename); + + if (!file.exists()) { + SteveMod.LOGGER.info("No failure log found for Steve '{}', starting fresh", steveName); + return; + } + + try (FileReader reader = new FileReader(file)) { + Type listType = new TypeToken>(){}.getType(); + List loaded = GSON.fromJson(reader, listType); + + if (loaded != null) { + failures.addAll(loaded); + + // Rebuild counters + for (FailureRecord record : failures) { + failureCountByAction.merge(record.action, 1, Integer::sum); + failureCountByError.merge(record.errorMessage, 1, Integer::sum); + } + + SteveMod.LOGGER.info("Loaded {} failure records for Steve '{}'", + failures.size(), steveName); + } + } + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to load failure log for Steve '{}'", steveName, e); + } + } + + /** + * Individual failure record + */ + public static class FailureRecord { + private final long timestamp; + private final String action; + private final Map parameters; + private final String errorMessage; + private final String context; + + public FailureRecord(long timestamp, String action, Map parameters, + String errorMessage, String context) { + this.timestamp = timestamp; + this.action = action; + this.parameters = parameters; + this.errorMessage = errorMessage; + this.context = context; + } + + public long getTimestamp() { + return timestamp; + } + + public String getAction() { + return action; + } + + public Map getParameters() { + return parameters; + } + + public String getErrorMessage() { + return errorMessage; + } + + public String getContext() { + return context; + } + + @Override + public String toString() { + return String.format("[%tF % identifyPatterns() { + List patterns = new ArrayList<>(); + + // Pattern 1: Repeated failures with same action + patterns.addAll(findRepeatedActionFailures()); + + // Pattern 2: Failures with common error messages + patterns.addAll(findCommonErrorPatterns()); + + // Pattern 3: Sequential failure chains + patterns.addAll(findFailureChains()); + + // Pattern 4: Parameter-specific failures + patterns.addAll(findParameterPatterns()); + + return patterns; + } + + /** + * Find actions that fail repeatedly + */ + private List findRepeatedActionFailures() { + List patterns = new ArrayList<>(); + + Map failureCounts = failureTracker.getMostCommonFailures(10); + + for (Map.Entry entry : failureCounts.entrySet()) { + String action = entry.getKey(); + int count = entry.getValue(); + + if (count >= MIN_SAMPLES_FOR_PATTERN) { + double failureRate = failureTracker.getFailureRate(action, RECENT_WINDOW); + + if (failureRate > HIGH_FAILURE_RATE_THRESHOLD) { + Pattern pattern = new Pattern( + PatternType.REPEATED_ACTION_FAILURE, + action, + "Action '" + action + "' has high failure rate: " + String.format("%.1f%%", failureRate), + failureRate / 100.0 + ); + patterns.add(pattern); + } + } + } + + return patterns; + } + + /** + * Find common error messages that appear across different actions + */ + private List findCommonErrorPatterns() { + List patterns = new ArrayList<>(); + + Map errorCounts = failureTracker.getMostCommonErrors(10); + + for (Map.Entry entry : errorCounts.entrySet()) { + String error = entry.getKey(); + int count = entry.getValue(); + + if (count >= MIN_SAMPLES_FOR_PATTERN) { + Pattern pattern = new Pattern( + PatternType.COMMON_ERROR, + error, + "Common error: '" + error + "' occurred " + count + " times", + Math.min(count / 10.0, 1.0) + ); + patterns.add(pattern); + } + } + + return patterns; + } + + /** + * Find chains of failures (one failure leading to another) + */ + private List findFailureChains() { + List patterns = new ArrayList<>(); + + List recent = failureTracker.getRecentFailures(10); + + if (recent.size() >= 3) { + // Check if last 3+ failures are related + Set recentActions = recent.stream() + .map(FailureRecord::getAction) + .collect(Collectors.toSet()); + + if (recentActions.size() > 1) { + Pattern pattern = new Pattern( + PatternType.FAILURE_CHAIN, + "multiple", + "Chain of " + recent.size() + " failures detected involving: " + recentActions, + 0.8 + ); + patterns.add(pattern); + } + } + + return patterns; + } + + /** + * Find patterns related to specific parameters + */ + private List findParameterPatterns() { + List patterns = new ArrayList<>(); + + // Analyze parameter values in failures + Map> failuresByAction = new HashMap<>(); + + for (FailureRecord record : failureTracker.getRecentFailures(50)) { + failuresByAction.computeIfAbsent(record.getAction(), k -> new ArrayList<>()).add(record); + } + + for (Map.Entry> entry : failuresByAction.entrySet()) { + String action = entry.getKey(); + List records = entry.getValue(); + + if (records.size() >= MIN_SAMPLES_FOR_PATTERN) { + // Check for common parameter values + Map paramValueCounts = new HashMap<>(); + + for (FailureRecord record : records) { + if (record.getParameters() != null) { + for (Map.Entry param : record.getParameters().entrySet()) { + String key = param.getKey() + "=" + param.getValue(); + paramValueCounts.merge(key, 1, Integer::sum); + } + } + } + + // Find frequently occurring parameter values + for (Map.Entry paramEntry : paramValueCounts.entrySet()) { + if (paramEntry.getValue() >= MIN_SAMPLES_FOR_PATTERN) { + Pattern pattern = new Pattern( + PatternType.PARAMETER_PATTERN, + action, + "Action '" + action + "' often fails with parameter: " + paramEntry.getKey(), + 0.6 + ); + patterns.add(pattern); + } + } + } + } + + return patterns; + } + + /** + * Generate learning insights from patterns + */ + public List generateInsights() { + List insights = new ArrayList<>(); + List patterns = identifyPatterns(); + + for (Pattern pattern : patterns) { + LearningInsight insight = generateInsightFromPattern(pattern); + if (insight != null) { + insights.add(insight); + + // Store insight in knowledge base + knowledgeBase.addInsight(insight); + } + } + + return insights; + } + + /** + * Generate actionable insight from a pattern + */ + private LearningInsight generateInsightFromPattern(Pattern pattern) { + switch (pattern.type) { + case REPEATED_ACTION_FAILURE: + return new LearningInsight( + "Avoid " + pattern.subject, + "The action '" + pattern.subject + "' has been failing frequently. " + + "Consider checking prerequisites, inventory, or environmental conditions before attempting.", + "avoidance", + pattern.confidence + ); + + case COMMON_ERROR: + return new LearningInsight( + "Common error: " + pattern.subject, + "The error '" + pattern.subject + "' occurs frequently. " + + "This might indicate a systematic issue that needs addressing.", + "error_mitigation", + pattern.confidence + ); + + case FAILURE_CHAIN: + return new LearningInsight( + "Failure cascade detected", + "Multiple failures in sequence suggest initial failure led to subsequent problems. " + + "Consider more robust error recovery and validation between steps.", + "chain_prevention", + pattern.confidence + ); + + case PARAMETER_PATTERN: + return new LearningInsight( + "Parameter issue in " + pattern.subject, + pattern.description + ". Consider validating or adjusting this parameter.", + "parameter_adjustment", + pattern.confidence + ); + + default: + return null; + } + } + + /** + * Get actionable recommendations based on learning + */ + public List getRecommendations() { + List recommendations = new ArrayList<>(); + List insights = knowledgeBase.getRecentInsights(10); + + if (insights.isEmpty()) { + recommendations.add("No learning insights available yet. Keep working to gather data."); + return recommendations; + } + + // Group insights by category + Map> byCategory = insights.stream() + .collect(Collectors.groupingBy(i -> i.category)); + + for (Map.Entry> entry : byCategory.entrySet()) { + if (!entry.getValue().isEmpty()) { + recommendations.add(entry.getValue().get(0).recommendation); + } + } + + return recommendations; + } + + /** + * Should we retry this action based on learning? + */ + public boolean shouldRetry(String action, int attemptCount) { + // Check if this action has high failure rate + double failureRate = failureTracker.getFailureRate(action, RECENT_WINDOW); + + if (failureRate > 75.0 && attemptCount >= 2) { + SteveMod.LOGGER.warn("Steve '{}' - Action '{}' has {}% failure rate, stopping retries", + steve.getSteveName(), action, failureRate); + return false; + } + + // Check if we have recent failures for this action + if (failureTracker.hasRecentFailure(action, 3) && attemptCount >= 3) { + SteveMod.LOGGER.warn("Steve '{}' - Action '{}' failed recently, limiting retries", + steve.getSteveName(), action); + return false; + } + + // Normal retry limit + return attemptCount < 3; + } + + /** + * Pattern types + */ + public enum PatternType { + REPEATED_ACTION_FAILURE, + COMMON_ERROR, + FAILURE_CHAIN, + PARAMETER_PATTERN + } + + /** + * Identified pattern + */ + public static class Pattern { + private final PatternType type; + private final String subject; + private final String description; + private final double confidence; + + public Pattern(PatternType type, String subject, String description, double confidence) { + this.type = type; + this.subject = subject; + this.description = description; + this.confidence = confidence; + } + + public PatternType getType() { + return type; + } + + public String getSubject() { + return subject; + } + + public String getDescription() { + return description; + } + + public double getConfidence() { + return confidence; + } + } + + /** + * Learning insight generated from patterns + */ + public static class LearningInsight { + private final String title; + private final String recommendation; + private final String category; + private final double confidence; + private final long timestamp; + + public LearningInsight(String title, String recommendation, String category, double confidence) { + this.title = title; + this.recommendation = recommendation; + this.category = category; + this.confidence = confidence; + this.timestamp = System.currentTimeMillis(); + } + + public String getTitle() { + return title; + } + + public String getRecommendation() { + return recommendation; + } + + public String getCategory() { + return category; + } + + public double getConfidence() { + return confidence; + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return String.format("[%.0f%% confidence] %s: %s", + confidence * 100, title, recommendation); + } + } +} diff --git a/src/main/java/com/steve/ai/memory/ChestMemory.java b/src/main/java/com/steve/ai/memory/ChestMemory.java new file mode 100644 index 0000000..504f1dc --- /dev/null +++ b/src/main/java/com/steve/ai/memory/ChestMemory.java @@ -0,0 +1,353 @@ +package com.steve.ai.memory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.steve.ai.SteveMod; +import net.minecraft.core.BlockPos; +import net.minecraft.world.Container; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Tracks chest locations and their contents + * Helps Steve remember where items are stored + * Persisted to JSON files + */ +public class ChestMemory { + private final String steveName; + private final Map knownChests; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final int MAX_CHESTS = 100; // Prevent unbounded growth + + public ChestMemory(String steveName) { + this.steveName = steveName; + this.knownChests = new HashMap<>(); + } + + /** + * Record a chest and scan its contents + */ + public void recordChest(Level level, BlockPos pos) { + BlockEntity blockEntity = level.getBlockEntity(pos); + + if (!(blockEntity instanceof Container container)) { + return; + } + + Map contents = scanChestContents(container); + + ChestRecord record = new ChestRecord( + pos, + System.currentTimeMillis(), + contents + ); + + knownChests.put(pos, record); + + // Prune if too many chests + if (knownChests.size() > MAX_CHESTS) { + pruneOldChests(); + } + + SteveMod.LOGGER.info("Steve '{}' recorded chest at {} with {} item types", + steveName, pos.toShortString(), contents.size()); + } + + /** + * Update chest contents after storing/retrieving + */ + public void updateChest(Level level, BlockPos pos) { + if (!knownChests.containsKey(pos)) { + recordChest(level, pos); + return; + } + + BlockEntity blockEntity = level.getBlockEntity(pos); + if (!(blockEntity instanceof Container container)) { + // Chest no longer exists + knownChests.remove(pos); + return; + } + + Map contents = scanChestContents(container); + + ChestRecord record = knownChests.get(pos); + record.lastAccessed = System.currentTimeMillis(); + record.contents = contents; + } + + /** + * Find chests containing a specific item + */ + public List findChestsWithItem(String itemName) { + return knownChests.entrySet().stream() + .filter(entry -> entry.getValue().contents.containsKey(itemName)) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + + /** + * Find nearest chest with a specific item + */ + public BlockPos findNearestChestWithItem(String itemName, BlockPos currentPos) { + return knownChests.entrySet().stream() + .filter(entry -> entry.getValue().contents.containsKey(itemName)) + .min(Comparator.comparingDouble(entry -> + entry.getKey().distSqr(currentPos))) + .map(Map.Entry::getKey) + .orElse(null); + } + + /** + * Find chest with most space + */ + public BlockPos findChestWithMostSpace(BlockPos currentPos, int maxDistance) { + return knownChests.entrySet().stream() + .filter(entry -> entry.getKey().distSqr(currentPos) <= maxDistance * maxDistance) + .max(Comparator.comparingInt(entry -> + getTotalItems(entry.getValue()))) + .map(Map.Entry::getKey) + .orElse(null); + } + + /** + * Get all known chest locations + */ + public Set getAllChestLocations() { + return new HashSet<>(knownChests.keySet()); + } + + /** + * Get chest contents summary + */ + public String getChestSummary(BlockPos pos) { + ChestRecord record = knownChests.get(pos); + if (record == null) { + return "Unknown chest"; + } + + if (record.contents.isEmpty()) { + return "Empty chest"; + } + + StringBuilder summary = new StringBuilder("Chest at ") + .append(pos.toShortString()) + .append(": "); + + List items = new ArrayList<>(); + for (Map.Entry entry : record.contents.entrySet()) { + items.add(entry.getValue() + "x " + entry.getKey()); + } + + summary.append(String.join(", ", items)); + return summary.toString(); + } + + /** + * Get summary of all chests for LLM context + */ + public String getAllChestsSummary(int maxChests) { + if (knownChests.isEmpty()) { + return "No known storage chests."; + } + + StringBuilder summary = new StringBuilder("Known Storage:\n"); + + knownChests.entrySet().stream() + .sorted(Comparator.comparingLong(e -> + -e.getValue().lastAccessed)) // Most recently accessed first + .limit(maxChests) + .forEach(entry -> { + BlockPos pos = entry.getKey(); + ChestRecord record = entry.getValue(); + + summary.append("- Chest at ").append(pos.toShortString()); + + if (!record.contents.isEmpty()) { + summary.append(": "); + List topItems = record.contents.entrySet().stream() + .sorted(Comparator.comparingInt(e -> -e.getValue())) + .limit(5) + .map(e -> e.getValue() + "x " + e.getKey()) + .collect(Collectors.toList()); + summary.append(String.join(", ", topItems)); + } + + summary.append("\n"); + }); + + return summary.toString(); + } + + /** + * Remove chest from memory (if destroyed) + */ + public void removeChest(BlockPos pos) { + if (knownChests.remove(pos) != null) { + SteveMod.LOGGER.info("Steve '{}' forgot about chest at {}", + steveName, pos.toShortString()); + } + } + + /** + * Scan chest contents and return item counts + */ + private Map scanChestContents(Container container) { + Map contents = new HashMap<>(); + + for (int i = 0; i < container.getContainerSize(); i++) { + ItemStack stack = container.getItem(i); + + if (stack.isEmpty()) { + continue; + } + + String itemName = stack.getItem().getDescriptionId(); + contents.put(itemName, + contents.getOrDefault(itemName, 0) + stack.getCount()); + } + + return contents; + } + + /** + * Prune least recently accessed chests + */ + private void pruneOldChests() { + if (knownChests.size() <= MAX_CHESTS) { + return; + } + + List sortedChests = knownChests.entrySet().stream() + .sorted(Comparator.comparingLong(e -> e.getValue().lastAccessed)) + .map(Map.Entry::getKey) + .toList(); + + int toRemove = knownChests.size() - MAX_CHESTS; + for (int i = 0; i < toRemove; i++) { + knownChests.remove(sortedChests.get(i)); + } + + SteveMod.LOGGER.info("Steve '{}' pruned {} old chest records", + steveName, toRemove); + } + + /** + * Get total item count in chest + */ + private int getTotalItems(ChestRecord record) { + return record.contents.values().stream() + .mapToInt(Integer::intValue) + .sum(); + } + + /** + * Save to JSON file + */ + public void saveToFile() { + try { + Path memoryDir = Paths.get("config", "steve", "memory"); + Files.createDirectories(memoryDir); + + Path chestFile = memoryDir.resolve(steveName + "_chests.json"); + + // Convert to serializable format (BlockPos to string) + Map serializable = new HashMap<>(); + for (Map.Entry entry : knownChests.entrySet()) { + String key = posToString(entry.getKey()); + serializable.put(key, entry.getValue()); + } + + try (Writer writer = new FileWriter(chestFile.toFile())) { + GSON.toJson(serializable, writer); + } + + SteveMod.LOGGER.info("Saved {} chest records for Steve '{}'", + knownChests.size(), steveName); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to save chest memory for Steve '{}'", steveName, e); + } + } + + /** + * Load from JSON file + */ + public void loadFromFile() { + try { + Path chestFile = Paths.get("config", "steve", "memory", steveName + "_chests.json"); + + if (!Files.exists(chestFile)) { + SteveMod.LOGGER.info("No existing chest memory file for Steve '{}'", steveName); + return; + } + + try (Reader reader = new FileReader(chestFile.toFile())) { + Type mapType = new TypeToken>(){}.getType(); + Map loaded = GSON.fromJson(reader, mapType); + + if (loaded != null) { + knownChests.clear(); + for (Map.Entry entry : loaded.entrySet()) { + BlockPos pos = stringToPos(entry.getKey()); + knownChests.put(pos, entry.getValue()); + } + + SteveMod.LOGGER.info("Loaded {} chest records for Steve '{}'", + knownChests.size(), steveName); + } + } + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to load chest memory for Steve '{}'", steveName, e); + } + } + + private String posToString(BlockPos pos) { + return pos.getX() + "," + pos.getY() + "," + pos.getZ(); + } + + private BlockPos stringToPos(String str) { + String[] parts = str.split(","); + return new BlockPos( + Integer.parseInt(parts[0]), + Integer.parseInt(parts[1]), + Integer.parseInt(parts[2]) + ); + } + + /** + * Clear all chest records + */ + public void clear() { + knownChests.clear(); + } + + /** + * Chest record data class + */ + public static class ChestRecord { + private final BlockPos position; + private long lastAccessed; + private Map contents; + + public ChestRecord(BlockPos position, long lastAccessed, Map contents) { + this.position = position; + this.lastAccessed = lastAccessed; + this.contents = contents; + } + + public BlockPos getPosition() { return position; } + public long getLastAccessed() { return lastAccessed; } + public Map getContents() { return contents; } + } +} diff --git a/src/main/java/com/steve/ai/memory/ConversationHistory.java b/src/main/java/com/steve/ai/memory/ConversationHistory.java new file mode 100644 index 0000000..ce0da20 --- /dev/null +++ b/src/main/java/com/steve/ai/memory/ConversationHistory.java @@ -0,0 +1,242 @@ +package com.steve.ai.memory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.steve.ai.SteveMod; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/** + * Stores conversation history between Steve and players + * Used for LLM context to maintain conversation continuity + * Persisted to JSON files + */ +public class ConversationHistory { + private final String steveName; + private final List messages; + private static final int MAX_MESSAGES = 100; // Prevent unbounded growth + private static final int MAX_TOKENS_ESTIMATE = 4096; // Rough token limit + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public ConversationHistory(String steveName) { + this.steveName = steveName; + this.messages = new ArrayList<>(); + } + + /** + * Add a message to conversation history + */ + public void addMessage(String role, String content) { + Message message = new Message( + System.currentTimeMillis(), + role, + content + ); + + messages.add(message); + + // Prune if exceeding limits + if (messages.size() > MAX_MESSAGES) { + pruneOldMessages(); + } + + SteveMod.LOGGER.debug("Steve '{}' recorded conversation: [{}] {}", + steveName, role, content); + } + + /** + * Get recent messages + */ + public List getRecentMessages(int count) { + int startIndex = Math.max(0, messages.size() - count); + return new ArrayList<>(messages.subList(startIndex, messages.size())); + } + + /** + * Get messages within token limit (rough estimate) + */ + public List getMessagesWithinTokenLimit() { + List result = new ArrayList<>(); + int estimatedTokens = 0; + + // Work backwards from most recent + for (int i = messages.size() - 1; i >= 0; i--) { + Message msg = messages.get(i); + int msgTokens = estimateTokens(msg.content); + + if (estimatedTokens + msgTokens > MAX_TOKENS_ESTIMATE) { + break; + } + + result.add(0, msg); // Add to front + estimatedTokens += msgTokens; + } + + return result; + } + + /** + * Get all messages + */ + public List getAllMessages() { + return new ArrayList<>(messages); + } + + /** + * Get formatted conversation for LLM + */ + public String getFormattedConversation(int maxMessages) { + List recent = getRecentMessages(maxMessages); + + if (recent.isEmpty()) { + return ""; + } + + StringBuilder formatted = new StringBuilder(); + for (Message msg : recent) { + formatted.append("[").append(msg.role).append("]: ") + .append(msg.content).append("\n"); + } + + return formatted.toString(); + } + + /** + * Clear all messages + */ + public void clear() { + messages.clear(); + } + + /** + * Get message count + */ + public int size() { + return messages.size(); + } + + /** + * Prune old messages to stay under limit + * Keeps system messages and recent messages + */ + private void pruneOldMessages() { + if (messages.size() <= MAX_MESSAGES) { + return; + } + + // Keep first 10 messages (likely contain important context) + // Keep last 70 messages (recent conversation) + // Remove middle messages + int toKeepStart = 10; + int toKeepEnd = 70; + + if (messages.size() > toKeepStart + toKeepEnd) { + List kept = new ArrayList<>(); + + // Keep first 10 + kept.addAll(messages.subList(0, toKeepStart)); + + // Keep last 70 + kept.addAll(messages.subList( + messages.size() - toKeepEnd, + messages.size() + )); + + messages.clear(); + messages.addAll(kept); + + SteveMod.LOGGER.info("Steve '{}' pruned conversation history to {} messages", + steveName, messages.size()); + } + } + + /** + * Rough token estimation (4 chars ≈ 1 token) + */ + private int estimateTokens(String text) { + return text.length() / 4; + } + + /** + * Save to JSON file + */ + public void saveToFile() { + try { + Path memoryDir = Paths.get("config", "steve", "memory"); + Files.createDirectories(memoryDir); + + Path conversationFile = memoryDir.resolve(steveName + "_conversation.json"); + + try (Writer writer = new FileWriter(conversationFile.toFile())) { + GSON.toJson(messages, writer); + } + + SteveMod.LOGGER.info("Saved {} conversation messages for Steve '{}'", + messages.size(), steveName); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to save conversation history for Steve '{}'", + steveName, e); + } + } + + /** + * Load from JSON file + */ + public void loadFromFile() { + try { + Path conversationFile = Paths.get("config", "steve", "memory", + steveName + "_conversation.json"); + + if (!Files.exists(conversationFile)) { + SteveMod.LOGGER.info("No existing conversation file for Steve '{}'", steveName); + return; + } + + try (Reader reader = new FileReader(conversationFile.toFile())) { + Type listType = new TypeToken>(){}.getType(); + List loaded = GSON.fromJson(reader, listType); + + if (loaded != null) { + messages.clear(); + messages.addAll(loaded); + SteveMod.LOGGER.info("Loaded {} conversation messages for Steve '{}'", + messages.size(), steveName); + } + } + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to load conversation history for Steve '{}'", + steveName, e); + } + } + + /** + * Message data class + */ + public static class Message { + private final long timestamp; + private final String role; // "user", "assistant", "system" + private final String content; + + public Message(long timestamp, String role, String content) { + this.timestamp = timestamp; + this.role = role; + this.content = content; + } + + public long getTimestamp() { return timestamp; } + public String getRole() { return role; } + public String getContent() { return content; } + + @Override + public String toString() { + return String.format("[%s] %s", role, content); + } + } +} diff --git a/src/main/java/com/steve/ai/memory/EpisodicMemory.java b/src/main/java/com/steve/ai/memory/EpisodicMemory.java new file mode 100644 index 0000000..ec2e45f --- /dev/null +++ b/src/main/java/com/steve/ai/memory/EpisodicMemory.java @@ -0,0 +1,397 @@ +package com.steve.ai.memory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.steve.ai.SteveMod; +import com.steve.ai.agent.VectorStore; +import net.minecraft.core.BlockPos; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Episodic memory stores significant events and discoveries + * Persisted to JSON files in config/steve/memory/ + * Each memory has: + * - Timestamp + * - Event type (discovery, achievement, interaction, etc.) + * - Description + * - Location (optional) + * - Importance score (0.0 - 1.0) + * - Metadata (custom data) + * + * Optional: Can use VectorStore for semantic search capabilities + */ +public class EpisodicMemory { + private final String steveName; + private final List memories; + private VectorStore vectorStore; // Optional for semantic search + private static final int MAX_MEMORIES = 500; // Prevent unbounded growth + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public EpisodicMemory(String steveName) { + this.steveName = steveName; + this.memories = new ArrayList<>(); + this.vectorStore = null; // Disabled by default + } + + /** + * Enable semantic search using vector embeddings + * @param vectorStore Vector store instance + */ + public void setVectorStore(VectorStore vectorStore) { + this.vectorStore = vectorStore; + + // Re-index existing memories + if (vectorStore != null && !memories.isEmpty()) { + reindexMemories(); + } + + SteveMod.LOGGER.info("Steve '{}' enabled semantic memory search", steveName); + } + + /** + * Get the vector store (may be null) + */ + public VectorStore getVectorStore() { + return vectorStore; + } + + /** + * Check if semantic search is enabled + */ + public boolean hasSemanticSearch() { + return vectorStore != null; + } + + /** + * Add a new memory entry + */ + public void addMemory(EventType type, String description, BlockPos location, double importance) { + addMemory(type, description, location, importance, new HashMap<>()); + } + + /** + * Add memory with custom metadata + */ + public void addMemory(EventType type, String description, BlockPos location, + double importance, Map metadata) { + long timestamp = System.currentTimeMillis(); + MemoryEntry entry = new MemoryEntry( + timestamp, + type, + description, + location, + importance, + metadata + ); + + memories.add(entry); + + // Add to vector store for semantic search + if (vectorStore != null) { + String memoryId = steveName + "_" + timestamp; + Map vectorMetadata = new HashMap<>(metadata); + vectorMetadata.put("type", type.toString()); + vectorMetadata.put("importance", importance); + vectorMetadata.put("timestamp", timestamp); + + vectorStore.addMemory(memoryId, description, vectorMetadata); + } + + // Prune old low-importance memories if exceeding limit + if (memories.size() > MAX_MEMORIES) { + pruneMemories(); + } + + SteveMod.LOGGER.info("Steve '{}' recorded memory: {} (importance: {})", + steveName, description, importance); + } + + /** + * Get all memories of a specific type + */ + public List getMemoriesByType(EventType type) { + return memories.stream() + .filter(m -> m.type == type) + .sorted(Comparator.comparingLong(MemoryEntry::getTimestamp).reversed()) + .toList(); + } + + /** + * Get memories within a time range + */ + public List getMemoriesSince(long timestamp) { + return memories.stream() + .filter(m -> m.timestamp >= timestamp) + .sorted(Comparator.comparingLong(MemoryEntry::getTimestamp).reversed()) + .toList(); + } + + /** + * Get most important memories + */ + public List getTopMemories(int count) { + return memories.stream() + .sorted(Comparator.comparingDouble(MemoryEntry::getImportance).reversed()) + .limit(count) + .toList(); + } + + /** + * Get recent memories + */ + public List getRecentMemories(int count) { + return memories.stream() + .sorted(Comparator.comparingLong(MemoryEntry::getTimestamp).reversed()) + .limit(count) + .toList(); + } + + /** + * Find memories by location (within radius) + */ + public List getMemoriesNear(BlockPos pos, int radius) { + return memories.stream() + .filter(m -> m.location != null) + .filter(m -> m.location.distSqr(pos) <= radius * radius) + .sorted(Comparator.comparingDouble(MemoryEntry::getImportance).reversed()) + .toList(); + } + + /** + * Search memories by keyword in description + */ + public List searchMemories(String keyword) { + String lowerKeyword = keyword.toLowerCase(); + return memories.stream() + .filter(m -> m.description.toLowerCase().contains(lowerKeyword)) + .sorted(Comparator.comparingDouble(MemoryEntry::getImportance).reversed()) + .toList(); + } + + /** + * Semantic search using natural language query + * Requires vector store to be enabled + * @param query Natural language query + * @param topK Number of results to return + * @return List of matching memories sorted by relevance + */ + public List semanticSearch(String query, int topK) { + if (vectorStore == null) { + SteveMod.LOGGER.warn("Semantic search requested but vector store not enabled"); + return new ArrayList<>(); + } + + // Search vector store + List results = vectorStore.search(query, topK); + + // Convert results back to MemoryEntry objects + return results.stream() + .map(result -> { + long timestamp = ((Number) result.metadata.get("timestamp")).longValue(); + return findMemoryByTimestamp(timestamp); + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + /** + * Find memory by timestamp + */ + private MemoryEntry findMemoryByTimestamp(long timestamp) { + return memories.stream() + .filter(m -> m.timestamp == timestamp) + .findFirst() + .orElse(null); + } + + /** + * Re-index all existing memories in vector store + */ + private void reindexMemories() { + if (vectorStore == null) { + return; + } + + SteveMod.LOGGER.info("Re-indexing {} memories for Steve '{}'", + memories.size(), steveName); + + for (MemoryEntry memory : memories) { + String memoryId = steveName + "_" + memory.timestamp; + Map metadata = new HashMap<>(memory.metadata); + metadata.put("type", memory.type.toString()); + metadata.put("importance", memory.importance); + metadata.put("timestamp", memory.timestamp); + + vectorStore.addMemory(memoryId, memory.description, metadata); + } + } + + /** + * Prune old, low-importance memories to stay under limit + */ + private void pruneMemories() { + // Sort by importance (ascending), remove bottom 20% + memories.sort(Comparator.comparingDouble(MemoryEntry::getImportance)); + + int toRemove = memories.size() - MAX_MEMORIES; + if (toRemove > 0) { + for (int i = 0; i < toRemove; i++) { + // Only remove if importance < 0.5 (keep important memories) + if (memories.get(0).importance < 0.5) { + memories.remove(0); + } else { + break; + } + } + } + + SteveMod.LOGGER.info("Steve '{}' pruned memories, now has {} entries", + steveName, memories.size()); + } + + /** + * Save memories to JSON file + */ + public void saveToFile() { + try { + Path memoryDir = Paths.get("config", "steve", "memory"); + Files.createDirectories(memoryDir); + + Path memoryFile = memoryDir.resolve(steveName + "_episodic.json"); + + try (Writer writer = new FileWriter(memoryFile.toFile())) { + GSON.toJson(memories, writer); + } + + SteveMod.LOGGER.info("Saved {} episodic memories for Steve '{}'", + memories.size(), steveName); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to save episodic memory for Steve '{}'", steveName, e); + } + } + + /** + * Load memories from JSON file + */ + public void loadFromFile() { + try { + Path memoryFile = Paths.get("config", "steve", "memory", steveName + "_episodic.json"); + + if (!Files.exists(memoryFile)) { + SteveMod.LOGGER.info("No existing episodic memory file for Steve '{}'", steveName); + return; + } + + try (Reader reader = new FileReader(memoryFile.toFile())) { + Type listType = new TypeToken>(){}.getType(); + List loadedMemories = GSON.fromJson(reader, listType); + + if (loadedMemories != null) { + memories.clear(); + memories.addAll(loadedMemories); + SteveMod.LOGGER.info("Loaded {} episodic memories for Steve '{}'", + memories.size(), steveName); + } + } + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to load episodic memory for Steve '{}'", steveName, e); + } + } + + /** + * Get summary of all memories for LLM context + */ + public String getSummary(int maxEntries) { + List topMemories = getTopMemories(maxEntries); + + if (topMemories.isEmpty()) { + return "No significant memories yet."; + } + + StringBuilder summary = new StringBuilder("Significant Memories:\n"); + for (MemoryEntry memory : topMemories) { + summary.append("- ").append(memory.description); + if (memory.location != null) { + summary.append(" at ").append(formatPosition(memory.location)); + } + summary.append("\n"); + } + + return summary.toString(); + } + + private String formatPosition(BlockPos pos) { + return String.format("[%d, %d, %d]", pos.getX(), pos.getY(), pos.getZ()); + } + + /** + * Clear all memories (for testing/reset) + */ + public void clear() { + memories.clear(); + } + + /** + * Get memory count + */ + public int size() { + return memories.size(); + } + + /** + * Memory entry data class + */ + public static class MemoryEntry { + private final long timestamp; + private final EventType type; + private final String description; + private final BlockPos location; + private final double importance; + private final Map metadata; + + public MemoryEntry(long timestamp, EventType type, String description, + BlockPos location, double importance, Map metadata) { + this.timestamp = timestamp; + this.type = type; + this.description = description; + this.location = location; + this.importance = importance; + this.metadata = metadata; + } + + public long getTimestamp() { return timestamp; } + public EventType getType() { return type; } + public String getDescription() { return description; } + public BlockPos getLocation() { return location; } + public double getImportance() { return importance; } + public Map getMetadata() { return metadata; } + + @Override + public String toString() { + return String.format("[%s] %s (importance: %.2f)", + type, description, importance); + } + } + + /** + * Event types for categorizing memories + */ + public enum EventType { + DISCOVERY, // Found something valuable (diamonds, structures, etc.) + ACHIEVEMENT, // Completed significant task (killed boss, built house) + INTERACTION, // Interacted with player or another Steve + FAILURE, // Failed at important task + LEARNING, // Learned something new (recipe, strategy) + COMBAT, // Combat-related event + BUILDING, // Built something significant + EXPLORATION // Explored new area + } +} diff --git a/src/main/java/com/steve/ai/memory/SharedKnowledgeBase.java b/src/main/java/com/steve/ai/memory/SharedKnowledgeBase.java new file mode 100644 index 0000000..905a047 --- /dev/null +++ b/src/main/java/com/steve/ai/memory/SharedKnowledgeBase.java @@ -0,0 +1,324 @@ +package com.steve.ai.memory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.steve.ai.SteveMod; +import net.minecraft.core.BlockPos; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Shared knowledge base accessible by all Steve entities + * Enables collective learning and optimization + * Different Steves can contribute and access shared discoveries + */ +public class SharedKnowledgeBase { + private static SharedKnowledgeBase INSTANCE; + private final Map knowledgeMap; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static final int MAX_ENTRIES = 500; + private static final Path KNOWLEDGE_FILE = Paths.get("config", "steve", "memory", "shared_knowledge.json"); + + /** + * Knowledge entry types + */ + public enum KnowledgeType { + RESOURCE_LOCATION, // Where to find resources (diamonds, iron, etc.) + CRAFTING_TIP, // Useful crafting patterns + DANGER_ZONE, // Dangerous areas to avoid + EFFICIENT_PATH, // Optimized paths between locations + FARMING_SPOT, // Good locations for farming + MOB_SPAWN, // Mob spawn locations + STRUCTURE, // Village, dungeon, etc. + OPTIMIZATION // General optimization tips + } + + private SharedKnowledgeBase() { + this.knowledgeMap = new HashMap<>(); + loadFromFile(); + } + + /** + * Get singleton instance + */ + public static synchronized SharedKnowledgeBase getInstance() { + if (INSTANCE == null) { + INSTANCE = new SharedKnowledgeBase(); + } + return INSTANCE; + } + + /** + * Add knowledge to the shared base + */ + public void addKnowledge(String steveName, KnowledgeType type, String key, + String description, BlockPos location, double importance) { + KnowledgeEntry entry = new KnowledgeEntry( + System.currentTimeMillis(), + steveName, + type, + key, + description, + location, + importance, + 1 // Initial confirmation count + ); + + // If knowledge already exists, update confirmation count + if (knowledgeMap.containsKey(key)) { + KnowledgeEntry existing = knowledgeMap.get(key); + existing.confirmationCount++; + existing.lastConfirmed = System.currentTimeMillis(); + existing.importance = Math.max(existing.importance, importance); + } else { + knowledgeMap.put(key, entry); + } + + // Prune if too many entries + if (knowledgeMap.size() > MAX_ENTRIES) { + pruneOldKnowledge(); + } + + SteveMod.LOGGER.info("Steve '{}' added shared knowledge: {} - {}", + steveName, type, description); + } + + /** + * Query knowledge by type + */ + public List getKnowledgeByType(KnowledgeType type) { + return knowledgeMap.values().stream() + .filter(entry -> entry.type == type) + .sorted(Comparator.comparingDouble(e -> -e.importance)) + .collect(Collectors.toList()); + } + + /** + * Query knowledge by key + */ + public KnowledgeEntry getKnowledge(String key) { + return knowledgeMap.get(key); + } + + /** + * Find knowledge near a location + */ + public List getKnowledgeNear(BlockPos pos, double maxDistance) { + return knowledgeMap.values().stream() + .filter(entry -> entry.location != null) + .filter(entry -> entry.location.distSqr(pos) <= maxDistance * maxDistance) + .sorted(Comparator.comparingDouble(e -> e.location.distSqr(pos))) + .collect(Collectors.toList()); + } + + /** + * Find resource locations + */ + public List findResourceLocations(String resourceName) { + return knowledgeMap.values().stream() + .filter(entry -> entry.type == KnowledgeType.RESOURCE_LOCATION) + .filter(entry -> entry.key.contains(resourceName) || entry.description.contains(resourceName)) + .sorted(Comparator.comparingInt(e -> -e.confirmationCount)) + .collect(Collectors.toList()); + } + + /** + * Get most important knowledge + */ + public List getMostImportant(int limit) { + return knowledgeMap.values().stream() + .sorted(Comparator.comparingDouble(e -> -e.importance)) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * Get recent knowledge + */ + public List getRecent(int limit) { + return knowledgeMap.values().stream() + .sorted(Comparator.comparingLong(e -> -e.timestamp)) + .limit(limit) + .collect(Collectors.toList()); + } + + /** + * Get knowledge summary for LLM context + */ + public String getKnowledgeSummary(int maxEntries) { + if (knowledgeMap.isEmpty()) { + return "No shared knowledge available."; + } + + StringBuilder summary = new StringBuilder("=== Shared Knowledge ===\n"); + + // Get most important and recent knowledge + List important = getMostImportant(maxEntries); + + for (KnowledgeEntry entry : important) { + summary.append("- [").append(entry.type).append("] "); + summary.append(entry.description); + + if (entry.location != null) { + summary.append(" at ").append(entry.location.toShortString()); + } + + if (entry.confirmationCount > 1) { + summary.append(" (confirmed ").append(entry.confirmationCount).append("x)"); + } + + summary.append(" [by ").append(entry.contributor).append("]\n"); + } + + return summary.toString(); + } + + /** + * Remove knowledge by key + */ + public void removeKnowledge(String key) { + if (knowledgeMap.remove(key) != null) { + SteveMod.LOGGER.info("Removed shared knowledge: {}", key); + } + } + + /** + * Clear all knowledge + */ + public void clear() { + knowledgeMap.clear(); + } + + /** + * Get total knowledge count + */ + public int size() { + return knowledgeMap.size(); + } + + /** + * Prune low-importance old knowledge + */ + private void pruneOldKnowledge() { + if (knowledgeMap.size() <= MAX_ENTRIES) { + return; + } + + // Calculate score: importance * confirmationCount / age + List sortedKeys = knowledgeMap.entrySet().stream() + .sorted(Comparator.comparingDouble(e -> { + KnowledgeEntry entry = e.getValue(); + long age = System.currentTimeMillis() - entry.timestamp; + double ageDays = age / (1000.0 * 60 * 60 * 24); + return -(entry.importance * entry.confirmationCount / Math.max(1, ageDays)); + })) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + // Remove bottom 20% + int toRemove = knowledgeMap.size() - MAX_ENTRIES; + for (int i = sortedKeys.size() - 1; i >= sortedKeys.size() - toRemove; i--) { + knowledgeMap.remove(sortedKeys.get(i)); + } + + SteveMod.LOGGER.info("Pruned {} old knowledge entries", toRemove); + } + + /** + * Save to JSON file + */ + public void saveToFile() { + try { + Path memoryDir = KNOWLEDGE_FILE.getParent(); + if (memoryDir != null) { + Files.createDirectories(memoryDir); + } + + try (Writer writer = new FileWriter(KNOWLEDGE_FILE.toFile())) { + GSON.toJson(knowledgeMap, writer); + } + + SteveMod.LOGGER.info("Saved {} shared knowledge entries", knowledgeMap.size()); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to save shared knowledge", e); + } + } + + /** + * Load from JSON file + */ + public void loadFromFile() { + try { + if (!Files.exists(KNOWLEDGE_FILE)) { + SteveMod.LOGGER.info("No existing shared knowledge file"); + return; + } + + try (Reader reader = new FileReader(KNOWLEDGE_FILE.toFile())) { + Type mapType = new TypeToken>(){}.getType(); + Map loaded = GSON.fromJson(reader, mapType); + + if (loaded != null) { + knowledgeMap.clear(); + knowledgeMap.putAll(loaded); + SteveMod.LOGGER.info("Loaded {} shared knowledge entries", knowledgeMap.size()); + } + } + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to load shared knowledge", e); + } + } + + /** + * Knowledge entry data class + */ + public static class KnowledgeEntry { + private final long timestamp; + private final String contributor; // Which Steve added this + private final KnowledgeType type; + private final String key; // Unique identifier + private final String description; + private final BlockPos location; + private double importance; // 0.0 to 1.0 + private int confirmationCount; // How many Steves confirmed this + private long lastConfirmed; + + public KnowledgeEntry(long timestamp, String contributor, KnowledgeType type, + String key, String description, BlockPos location, + double importance, int confirmationCount) { + this.timestamp = timestamp; + this.contributor = contributor; + this.type = type; + this.key = key; + this.description = description; + this.location = location; + this.importance = importance; + this.confirmationCount = confirmationCount; + this.lastConfirmed = timestamp; + } + + // Getters + public long getTimestamp() { return timestamp; } + public String getContributor() { return contributor; } + public KnowledgeType getType() { return type; } + public String getKey() { return key; } + public String getDescription() { return description; } + public BlockPos getLocation() { return location; } + public double getImportance() { return importance; } + public int getConfirmationCount() { return confirmationCount; } + public long getLastConfirmed() { return lastConfirmed; } + + @Override + public String toString() { + return String.format("[%s] %s (importance: %.2f, confirmed: %dx)", + type, description, importance, confirmationCount); + } + } +} diff --git a/src/main/java/com/steve/ai/memory/SteveMemory.java b/src/main/java/com/steve/ai/memory/SteveMemory.java index a5e0311..892a844 100644 --- a/src/main/java/com/steve/ai/memory/SteveMemory.java +++ b/src/main/java/com/steve/ai/memory/SteveMemory.java @@ -1,6 +1,8 @@ package com.steve.ai.memory; +import com.steve.ai.SteveMod; import com.steve.ai.entity.SteveEntity; +import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.StringTag; @@ -10,6 +12,13 @@ import java.util.List; import java.util.Queue; +/** + * Enhanced Steve memory system with multiple memory types: + * - Short-term memory (current goal, recent actions) + * - Episodic memory (significant events, discoveries) + * - Chest memory (storage locations and contents) + * - Conversation history (for LLM context) + */ public class SteveMemory { private final SteveEntity steve; private String currentGoal; @@ -17,11 +26,26 @@ public class SteveMemory { private final LinkedList recentActions; private static final int MAX_RECENT_ACTIONS = 20; + // New memory systems + private final EpisodicMemory episodicMemory; + private final ChestMemory chestMemory; + private final ConversationHistory conversationHistory; + private final SharedKnowledgeBase sharedKnowledge; + public SteveMemory(SteveEntity steve) { this.steve = steve; this.currentGoal = ""; this.taskQueue = new LinkedList<>(); this.recentActions = new LinkedList<>(); + + // Initialize new memory systems + this.episodicMemory = new EpisodicMemory(steve.getSteveName()); + this.chestMemory = new ChestMemory(steve.getSteveName()); + this.conversationHistory = new ConversationHistory(steve.getSteveName()); + this.sharedKnowledge = SharedKnowledgeBase.getInstance(); + + // Load persistent memories + loadPersistentMemories(); } public String getCurrentGoal() { @@ -42,12 +66,12 @@ public void addAction(String action) { public List getRecentActions(int count) { int size = Math.min(count, recentActions.size()); List result = new ArrayList<>(); - + int startIndex = Math.max(0, recentActions.size() - count); for (int i = startIndex; i < recentActions.size(); i++) { result.add(recentActions.get(i)); } - + return result; } @@ -56,21 +80,205 @@ public void clearTaskQueue() { currentGoal = ""; } + // ==================== New Memory Accessors ==================== + + /** + * Get episodic memory system + */ + public EpisodicMemory getEpisodicMemory() { + return episodicMemory; + } + + /** + * Get chest memory system + */ + public ChestMemory getChestMemory() { + return chestMemory; + } + + /** + * Get conversation history + */ + public ConversationHistory getConversationHistory() { + return conversationHistory; + } + + /** + * Get shared knowledge base (singleton) + */ + public SharedKnowledgeBase getSharedKnowledge() { + return sharedKnowledge; + } + + /** + * Record a significant event + */ + public void recordEvent(EpisodicMemory.EventType type, String description, + BlockPos location, double importance) { + episodicMemory.addMemory(type, description, location, importance); + + // Auto-save important memories + if (importance >= 0.8) { + savePersistentMemories(); + } + } + + /** + * Record chest interaction + */ + public void recordChest(BlockPos pos) { + chestMemory.recordChest(steve.level(), pos); + } + + /** + * Update chest contents after interaction + */ + public void updateChest(BlockPos pos) { + chestMemory.updateChest(steve.level(), pos); + } + + /** + * Add message to conversation history + */ + public void addConversation(String role, String content) { + conversationHistory.addMessage(role, content); + } + + /** + * Share knowledge with other Steves + */ + public void shareKnowledge(SharedKnowledgeBase.KnowledgeType type, String key, + String description, BlockPos location, double importance) { + sharedKnowledge.addKnowledge(steve.getSteveName(), type, key, description, + location, importance); + + // Also save to file if important + if (importance >= 0.8) { + sharedKnowledge.saveToFile(); + } + } + + /** + * Query shared knowledge by type + */ + public List querySharedKnowledge( + SharedKnowledgeBase.KnowledgeType type) { + return sharedKnowledge.getKnowledgeByType(type); + } + + /** + * Find shared knowledge near a location + */ + public List findNearbyKnowledge( + BlockPos pos, double maxDistance) { + return sharedKnowledge.getKnowledgeNear(pos, maxDistance); + } + + /** + * Get comprehensive memory summary for LLM + */ + public String getMemorySummary() { + StringBuilder summary = new StringBuilder(); + + // Recent actions + if (!recentActions.isEmpty()) { + summary.append("=== Recent Actions ===\n"); + List recent = getRecentActions(5); + for (String action : recent) { + summary.append("- ").append(action).append("\n"); + } + summary.append("\n"); + } + + // Significant memories + if (episodicMemory.size() > 0) { + summary.append(episodicMemory.getSummary(5)); + summary.append("\n"); + } + + // Known storage + if (!chestMemory.getAllChestLocations().isEmpty()) { + summary.append(chestMemory.getAllChestsSummary(3)); + summary.append("\n"); + } + + // Recent conversation + List recentConvo = + conversationHistory.getRecentMessages(5); + if (!recentConvo.isEmpty()) { + summary.append("=== Recent Conversation ===\n"); + for (ConversationHistory.Message msg : recentConvo) { + summary.append(msg.getRole()).append(": ") + .append(msg.getContent()).append("\n"); + } + summary.append("\n"); + } + + // Shared knowledge from other Steves + if (sharedKnowledge.size() > 0) { + summary.append(sharedKnowledge.getKnowledgeSummary(3)); + } + + return summary.toString(); + } + + // ==================== Persistence ==================== + + /** + * Load all persistent memories from files + */ + private void loadPersistentMemories() { + try { + episodicMemory.loadFromFile(); + chestMemory.loadFromFile(); + conversationHistory.loadFromFile(); + + SteveMod.LOGGER.info("Loaded persistent memories for Steve '{}'", + steve.getSteveName()); + } catch (Exception e) { + SteveMod.LOGGER.error("Failed to load persistent memories for Steve '{}'", + steve.getSteveName(), e); + } + } + + /** + * Save all persistent memories to files + */ + public void savePersistentMemories() { + try { + episodicMemory.saveToFile(); + chestMemory.saveToFile(); + conversationHistory.saveToFile(); + sharedKnowledge.saveToFile(); // Save shared knowledge (singleton) + + SteveMod.LOGGER.info("Saved persistent memories for Steve '{}'", + steve.getSteveName()); + } catch (Exception e) { + SteveMod.LOGGER.error("Failed to save persistent memories for Steve '{}'", + steve.getSteveName(), e); + } + } + + // ==================== NBT Serialization (for entity data) ==================== + public void saveToNBT(CompoundTag tag) { tag.putString("CurrentGoal", currentGoal); - + ListTag actionsList = new ListTag(); for (String action : recentActions) { actionsList.add(StringTag.valueOf(action)); } tag.put("RecentActions", actionsList); + + // Note: Persistent memories are saved to separate JSON files + // NBT is only for in-game entity data } public void loadFromNBT(CompoundTag tag) { if (tag.contains("CurrentGoal")) { currentGoal = tag.getString("CurrentGoal"); } - + if (tag.contains("RecentActions")) { recentActions.clear(); ListTag actionsList = tag.getList("RecentActions", 8); // 8 = String type @@ -78,6 +286,7 @@ public void loadFromNBT(CompoundTag tag) { recentActions.add(actionsList.getString(i)); } } + + // Persistent memories are loaded in constructor } } - diff --git a/src/main/java/com/steve/ai/quest/Achievement.java b/src/main/java/com/steve/ai/quest/Achievement.java new file mode 100644 index 0000000..254069b --- /dev/null +++ b/src/main/java/com/steve/ai/quest/Achievement.java @@ -0,0 +1,139 @@ +package com.steve.ai.quest; + +/** + * Represents an achievement that can be unlocked + * Achievements are earned through specific accomplishments + */ +public class Achievement { + private final String id; + private final String title; + private final String description; + private final AchievementCategory category; + private final AchievementRarity rarity; + private final String requirement; // e.g., "mine_diamond:1", "kill_zombie:100" + private boolean unlocked; + private long unlockTime; + + public Achievement(String id, String title, String description, + AchievementCategory category, AchievementRarity rarity, + String requirement) { + this.id = id; + this.title = title; + this.description = description; + this.category = category; + this.rarity = rarity; + this.requirement = requirement; + this.unlocked = false; + this.unlockTime = 0; + } + + /** + * Unlock this achievement + */ + public void unlock() { + if (!unlocked) { + unlocked = true; + unlockTime = System.currentTimeMillis(); + } + } + + /** + * Check if requirement matches a stat + */ + public boolean matchesRequirement(String statType, int value) { + String[] parts = requirement.split(":"); + if (parts.length != 2) { + return false; + } + + String reqType = parts[0]; + int reqValue = Integer.parseInt(parts[1]); + + return reqType.equals(statType) && value >= reqValue; + } + + // Getters + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public AchievementCategory getCategory() { + return category; + } + + public AchievementRarity getRarity() { + return rarity; + } + + public String getRequirement() { + return requirement; + } + + public boolean isUnlocked() { + return unlocked; + } + + public long getUnlockTime() { + return unlockTime; + } + + public void setUnlocked(boolean unlocked) { + this.unlocked = unlocked; + } + + public void setUnlockTime(long unlockTime) { + this.unlockTime = unlockTime; + } + + /** + * Get display string with rarity indicator + */ + public String getDisplayString() { + String raritySymbol = switch (rarity) { + case COMMON -> "⭐"; + case UNCOMMON -> "⭐⭐"; + case RARE -> "⭐⭐⭐"; + case EPIC -> "⭐⭐⭐⭐"; + case LEGENDARY -> "⭐⭐⭐⭐⭐"; + }; + + return String.format("%s %s %s - %s", + unlocked ? "✓" : "✗", + raritySymbol, + title, + description); + } + + /** + * Achievement categories + */ + public enum AchievementCategory { + MINING, // Mining-related achievements + CRAFTING, // Crafting achievements + COMBAT, // Combat achievements + BUILDING, // Building achievements + FARMING, // Farming achievements + EXPLORATION, // Exploration achievements + REDSTONE, // Redstone achievements + GENERAL // General achievements + } + + /** + * Achievement rarity levels + */ + public enum AchievementRarity { + COMMON, // Easy to get + UNCOMMON, // Moderate difficulty + RARE, // Challenging + EPIC, // Very challenging + LEGENDARY // Extremely difficult + } +} diff --git a/src/main/java/com/steve/ai/quest/AchievementManager.java b/src/main/java/com/steve/ai/quest/AchievementManager.java new file mode 100644 index 0000000..1a2bd80 --- /dev/null +++ b/src/main/java/com/steve/ai/quest/AchievementManager.java @@ -0,0 +1,365 @@ +package com.steve.ai.quest; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.steve.ai.SteveMod; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +/** + * Manages achievements for a Steve entity + * Tracks unlocked achievements and checks for new unlocks + */ +public class AchievementManager { + private static final String ACHIEVEMENT_DATA_DIR = "config/steve/achievements/"; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private final String steveName; + private final Map achievements; + private final Map stats; // Track stats for achievement checking + + public AchievementManager(String steveName) { + this.steveName = steveName; + this.achievements = new LinkedHashMap<>(); + this.stats = new HashMap<>(); + + // Create achievement data directory + try { + Files.createDirectories(Paths.get(ACHIEVEMENT_DATA_DIR)); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to create achievement data directory", e); + } + + // Initialize achievements + initializeAchievements(); + + // Load progress + loadAchievements(); + } + + /** + * Initialize all achievements + */ + private void initializeAchievements() { + // Mining achievements + addAchievement(new Achievement("first_mine", "First Steps", + "Mine your first block", Achievement.AchievementCategory.MINING, + Achievement.AchievementRarity.COMMON, "mine_block:1")); + + addAchievement(new Achievement("iron_miner", "Iron Miner", + "Mine 64 iron ore", Achievement.AchievementCategory.MINING, + Achievement.AchievementRarity.UNCOMMON, "mine_iron:64")); + + addAchievement(new Achievement("diamond_hunter", "Diamond Hunter", + "Mine 10 diamonds", Achievement.AchievementCategory.MINING, + Achievement.AchievementRarity.RARE, "mine_diamond:10")); + + addAchievement(new Achievement("master_miner", "Master Miner", + "Mine 1000 blocks", Achievement.AchievementCategory.MINING, + Achievement.AchievementRarity.EPIC, "mine_block:1000")); + + // Crafting achievements + addAchievement(new Achievement("first_craft", "Crafting Beginner", + "Craft your first item", Achievement.AchievementCategory.CRAFTING, + Achievement.AchievementRarity.COMMON, "craft_item:1")); + + addAchievement(new Achievement("tool_maker", "Tool Maker", + "Craft 10 tools", Achievement.AchievementCategory.CRAFTING, + Achievement.AchievementRarity.UNCOMMON, "craft_tool:10")); + + addAchievement(new Achievement("master_crafter", "Master Crafter", + "Craft 100 items", Achievement.AchievementCategory.CRAFTING, + Achievement.AchievementRarity.RARE, "craft_item:100")); + + // Combat achievements + addAchievement(new Achievement("first_blood", "First Blood", + "Defeat your first monster", Achievement.AchievementCategory.COMBAT, + Achievement.AchievementRarity.COMMON, "kill_mob:1")); + + addAchievement(new Achievement("zombie_slayer", "Zombie Slayer", + "Defeat 50 zombies", Achievement.AchievementCategory.COMBAT, + Achievement.AchievementRarity.UNCOMMON, "kill_zombie:50")); + + addAchievement(new Achievement("monster_hunter", "Monster Hunter", + "Defeat 100 monsters", Achievement.AchievementCategory.COMBAT, + Achievement.AchievementRarity.RARE, "kill_mob:100")); + + addAchievement(new Achievement("legendary_warrior", "Legendary Warrior", + "Defeat 500 monsters", Achievement.AchievementCategory.COMBAT, + Achievement.AchievementRarity.LEGENDARY, "kill_mob:500")); + + // Building achievements + addAchievement(new Achievement("first_build", "Builder", + "Build your first structure", Achievement.AchievementCategory.BUILDING, + Achievement.AchievementRarity.COMMON, "build_structure:1")); + + addAchievement(new Achievement("architect", "Architect", + "Build 10 structures", Achievement.AchievementCategory.BUILDING, + Achievement.AchievementRarity.UNCOMMON, "build_structure:10")); + + addAchievement(new Achievement("master_builder", "Master Builder", + "Place 10,000 blocks", Achievement.AchievementCategory.BUILDING, + Achievement.AchievementRarity.EPIC, "place_block:10000")); + + // Farming achievements + addAchievement(new Achievement("green_thumb", "Green Thumb", + "Harvest your first crop", Achievement.AchievementCategory.FARMING, + Achievement.AchievementRarity.COMMON, "harvest_crop:1")); + + addAchievement(new Achievement("farmer", "Farmer", + "Harvest 500 crops", Achievement.AchievementCategory.FARMING, + Achievement.AchievementRarity.UNCOMMON, "harvest_crop:500")); + + addAchievement(new Achievement("animal_breeder", "Animal Breeder", + "Breed 50 animals", Achievement.AchievementCategory.FARMING, + Achievement.AchievementRarity.RARE, "breed_animal:50")); + + // Exploration achievements + addAchievement(new Achievement("explorer", "Explorer", + "Travel 1000 blocks", Achievement.AchievementCategory.EXPLORATION, + Achievement.AchievementRarity.UNCOMMON, "travel_distance:1000")); + + addAchievement(new Achievement("nether_traveler", "Nether Traveler", + "Enter the Nether", Achievement.AchievementCategory.EXPLORATION, + Achievement.AchievementRarity.RARE, "enter_nether:1")); + + addAchievement(new Achievement("end_warrior", "End Warrior", + "Enter the End", Achievement.AchievementCategory.EXPLORATION, + Achievement.AchievementRarity.EPIC, "enter_end:1")); + + // Redstone achievements + addAchievement(new Achievement("redstone_beginner", "Redstone Beginner", + "Place your first redstone", Achievement.AchievementCategory.REDSTONE, + Achievement.AchievementRarity.COMMON, "place_redstone:1")); + + addAchievement(new Achievement("redstone_engineer", "Redstone Engineer", + "Build an automatic door", Achievement.AchievementCategory.REDSTONE, + Achievement.AchievementRarity.UNCOMMON, "build_auto_door:1")); + + // General achievements + addAchievement(new Achievement("hard_worker", "Hard Worker", + "Complete 10 quests", Achievement.AchievementCategory.GENERAL, + Achievement.AchievementRarity.UNCOMMON, "complete_quest:10")); + + addAchievement(new Achievement("achievement_hunter", "Achievement Hunter", + "Unlock 25 achievements", Achievement.AchievementCategory.GENERAL, + Achievement.AchievementRarity.EPIC, "unlock_achievement:25")); + + addAchievement(new Achievement("legend", "Legend", + "Unlock all achievements", Achievement.AchievementCategory.GENERAL, + Achievement.AchievementRarity.LEGENDARY, "unlock_achievement:50")); + } + + /** + * Add an achievement to the registry + */ + private void addAchievement(Achievement achievement) { + achievements.put(achievement.getId(), achievement); + } + + /** + * Update a stat and check for achievement unlocks + */ + public void updateStat(String statType, int amount) { + int oldValue = stats.getOrDefault(statType, 0); + int newValue = oldValue + amount; + stats.put(statType, newValue); + + // Check if any achievements should be unlocked + checkAchievements(statType, newValue); + } + + /** + * Check if any achievements should be unlocked for a stat + */ + private void checkAchievements(String statType, int value) { + List newlyUnlocked = new ArrayList<>(); + + for (Achievement achievement : achievements.values()) { + if (!achievement.isUnlocked() && achievement.matchesRequirement(statType, value)) { + achievement.unlock(); + newlyUnlocked.add(achievement); + + SteveMod.LOGGER.info("Steve '{}' unlocked achievement: {} ({})", + steveName, achievement.getTitle(), achievement.getRarity()); + } + } + + // Check for meta-achievement (achievement_hunter) + if (!newlyUnlocked.isEmpty()) { + int unlockedCount = getUnlockedCount(); + updateStat("unlock_achievement", newlyUnlocked.size()); + } + + if (!newlyUnlocked.isEmpty()) { + saveAchievements(); + } + } + + /** + * Manually unlock an achievement + */ + public boolean unlockAchievement(String achievementId) { + Achievement achievement = achievements.get(achievementId); + if (achievement != null && !achievement.isUnlocked()) { + achievement.unlock(); + SteveMod.LOGGER.info("Steve '{}' unlocked achievement: {}", + steveName, achievement.getTitle()); + saveAchievements(); + return true; + } + return false; + } + + /** + * Get unlocked achievements + */ + public List getUnlockedAchievements() { + List unlocked = new ArrayList<>(); + for (Achievement achievement : achievements.values()) { + if (achievement.isUnlocked()) { + unlocked.add(achievement); + } + } + return unlocked; + } + + /** + * Get locked achievements + */ + public List getLockedAchievements() { + List locked = new ArrayList<>(); + for (Achievement achievement : achievements.values()) { + if (!achievement.isUnlocked()) { + locked.add(achievement); + } + } + return locked; + } + + /** + * Get achievements by category + */ + public List getAchievementsByCategory(Achievement.AchievementCategory category) { + List categoryAchievements = new ArrayList<>(); + for (Achievement achievement : achievements.values()) { + if (achievement.getCategory() == category) { + categoryAchievements.add(achievement); + } + } + return categoryAchievements; + } + + /** + * Get count of unlocked achievements + */ + public int getUnlockedCount() { + return (int) achievements.values().stream() + .filter(Achievement::isUnlocked) + .count(); + } + + /** + * Get total achievement count + */ + public int getTotalCount() { + return achievements.size(); + } + + /** + * Get completion percentage + */ + public int getCompletionPercentage() { + if (achievements.isEmpty()) { + return 0; + } + return (getUnlockedCount() * 100) / getTotalCount(); + } + + /** + * Get achievement summary string + */ + public String getAchievementSummary() { + StringBuilder sb = new StringBuilder(); + sb.append("\n=== ACHIEVEMENTS ===\n"); + sb.append("Progress: ").append(getUnlockedCount()) + .append("/").append(getTotalCount()) + .append(" (").append(getCompletionPercentage()).append("%)\n\n"); + + // Group by category + for (Achievement.AchievementCategory category : Achievement.AchievementCategory.values()) { + List categoryAchievements = getAchievementsByCategory(category); + if (!categoryAchievements.isEmpty()) { + sb.append(category).append(":\n"); + for (Achievement achievement : categoryAchievements) { + sb.append(" ").append(achievement.getDisplayString()).append("\n"); + } + sb.append("\n"); + } + } + + return sb.toString(); + } + + /** + * Get recent achievements (last 5 unlocked) + */ + public List getRecentAchievements() { + return getUnlockedAchievements().stream() + .sorted((a, b) -> Long.compare(b.getUnlockTime(), a.getUnlockTime())) + .limit(5) + .toList(); + } + + /** + * Save achievements to disk + */ + public void saveAchievements() { + try { + Path filePath = Paths.get(ACHIEVEMENT_DATA_DIR, steveName + "_achievements.json"); + + Map data = new HashMap<>(); + data.put("achievements", achievements); + data.put("stats", stats); + + String json = GSON.toJson(data); + Files.writeString(filePath, json); + + SteveMod.LOGGER.debug("Saved achievements for Steve '{}'", steveName); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to save achievements", e); + } + } + + /** + * Load achievements from disk + */ + public void loadAchievements() { + try { + Path filePath = Paths.get(ACHIEVEMENT_DATA_DIR, steveName + "_achievements.json"); + + if (!Files.exists(filePath)) { + return; + } + + String json = Files.readString(filePath); + // Note: Full deserialization would require custom deserializer + // For now, this structure supports basic persistence + + SteveMod.LOGGER.info("Loaded achievements for Steve '{}'", steveName); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to load achievements", e); + } + } + + /** + * Get stat value + */ + public int getStat(String statType) { + return stats.getOrDefault(statType, 0); + } +} diff --git a/src/main/java/com/steve/ai/quest/Quest.java b/src/main/java/com/steve/ai/quest/Quest.java new file mode 100644 index 0000000..75510e1 --- /dev/null +++ b/src/main/java/com/steve/ai/quest/Quest.java @@ -0,0 +1,249 @@ +package com.steve.ai.quest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a quest that Steve can complete + * Tracks objectives, rewards, and completion status + */ +public class Quest { + private final String id; + private final String title; + private final String description; + private final List objectives; + private final Map rewards; // Item -> quantity + private QuestStatus status; + private long startTime; + private long completionTime; + + public Quest(String id, String title, String description) { + this.id = id; + this.title = title; + this.description = description; + this.objectives = new ArrayList<>(); + this.rewards = new HashMap<>(); + this.status = QuestStatus.NOT_STARTED; + this.startTime = 0; + this.completionTime = 0; + } + + /** + * Add an objective to this quest + */ + public void addObjective(QuestObjective objective) { + objectives.add(objective); + } + + /** + * Add a reward for completing this quest + */ + public void addReward(String item, int quantity) { + rewards.put(item, rewards.getOrDefault(item, 0) + quantity); + } + + /** + * Start this quest + */ + public void start() { + if (status == QuestStatus.NOT_STARTED) { + status = QuestStatus.IN_PROGRESS; + startTime = System.currentTimeMillis(); + } + } + + /** + * Update quest progress for a specific objective type + */ + public void updateProgress(String objectiveType, int amount) { + if (status != QuestStatus.IN_PROGRESS) { + return; + } + + for (QuestObjective objective : objectives) { + if (objective.getType().equals(objectiveType)) { + objective.incrementProgress(amount); + } + } + + // Check if all objectives complete + if (isAllObjectivesComplete()) { + complete(); + } + } + + /** + * Check if all objectives are complete + */ + private boolean isAllObjectivesComplete() { + for (QuestObjective objective : objectives) { + if (!objective.isComplete()) { + return false; + } + } + return true; + } + + /** + * Mark quest as complete + */ + private void complete() { + status = QuestStatus.COMPLETED; + completionTime = System.currentTimeMillis(); + } + + /** + * Fail this quest + */ + public void fail() { + if (status == QuestStatus.IN_PROGRESS) { + status = QuestStatus.FAILED; + } + } + + /** + * Get quest completion percentage (0-100) + */ + public int getCompletionPercentage() { + if (objectives.isEmpty()) { + return 0; + } + + int totalProgress = 0; + for (QuestObjective objective : objectives) { + totalProgress += objective.getProgressPercentage(); + } + + return totalProgress / objectives.size(); + } + + /** + * Get time spent on quest in seconds + */ + public long getTimeSpentSeconds() { + if (startTime == 0) { + return 0; + } + + long endTime = completionTime != 0 ? completionTime : System.currentTimeMillis(); + return (endTime - startTime) / 1000; + } + + // Getters + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public List getObjectives() { + return objectives; + } + + public Map getRewards() { + return rewards; + } + + public QuestStatus getStatus() { + return status; + } + + public boolean isComplete() { + return status == QuestStatus.COMPLETED; + } + + public boolean isInProgress() { + return status == QuestStatus.IN_PROGRESS; + } + + /** + * Get summary string for GUI display + */ + public String getSummary() { + StringBuilder sb = new StringBuilder(); + sb.append(title).append(" (").append(status).append(")\n"); + sb.append(description).append("\n"); + sb.append("Progress: ").append(getCompletionPercentage()).append("%\n"); + + if (!objectives.isEmpty()) { + sb.append("Objectives:\n"); + for (QuestObjective obj : objectives) { + sb.append(" - ").append(obj.getDescription()) + .append(": ").append(obj.getCurrentProgress()) + .append("/").append(obj.getTargetProgress()) + .append(obj.isComplete() ? " ✓" : "") + .append("\n"); + } + } + + return sb.toString(); + } + + /** + * Quest status enum + */ + public enum QuestStatus { + NOT_STARTED, + IN_PROGRESS, + COMPLETED, + FAILED + } + + /** + * Quest objective - something that must be accomplished + */ + public static class QuestObjective { + private final String type; // e.g., "mine_iron", "craft_sword", "kill_zombie" + private final String description; + private final int targetProgress; + private int currentProgress; + + public QuestObjective(String type, String description, int targetProgress) { + this.type = type; + this.description = description; + this.targetProgress = targetProgress; + this.currentProgress = 0; + } + + public void incrementProgress(int amount) { + currentProgress = Math.min(currentProgress + amount, targetProgress); + } + + public boolean isComplete() { + return currentProgress >= targetProgress; + } + + public int getProgressPercentage() { + return (currentProgress * 100) / targetProgress; + } + + // Getters + public String getType() { + return type; + } + + public String getDescription() { + return description; + } + + public int getTargetProgress() { + return targetProgress; + } + + public int getCurrentProgress() { + return currentProgress; + } + + public void setCurrentProgress(int progress) { + this.currentProgress = Math.min(progress, targetProgress); + } + } +} diff --git a/src/main/java/com/steve/ai/quest/QuestManager.java b/src/main/java/com/steve/ai/quest/QuestManager.java new file mode 100644 index 0000000..2916c1b --- /dev/null +++ b/src/main/java/com/steve/ai/quest/QuestManager.java @@ -0,0 +1,319 @@ +package com.steve.ai.quest; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.steve.ai.SteveMod; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +/** + * Manages quests for a Steve entity + * Tracks active quests, completed quests, and quest progress + */ +public class QuestManager { + private static final String QUEST_DATA_DIR = "config/steve/quests/"; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private final String steveName; + private final Map activeQuests; + private final Map completedQuests; + private final Map questRegistry; // All available quests + + public QuestManager(String steveName) { + this.steveName = steveName; + this.activeQuests = new LinkedHashMap<>(); + this.completedQuests = new LinkedHashMap<>(); + this.questRegistry = new HashMap<>(); + + // Create quest data directory + try { + Files.createDirectories(Paths.get(QUEST_DATA_DIR)); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to create quest data directory", e); + } + + // Initialize default quests + initializeDefaultQuests(); + + // Load quest progress + loadQuestProgress(); + } + + /** + * Initialize default quests available to all Steves + */ + private void initializeDefaultQuests() { + // Beginner quests + Quest gettingStarted = new Quest("getting_started", "Getting Started", + "Gather basic resources and craft your first tools"); + gettingStarted.addObjective(new Quest.QuestObjective("mine_wood", "Mine 10 wood logs", 10)); + gettingStarted.addObjective(new Quest.QuestObjective("craft_pickaxe", "Craft a wooden pickaxe", 1)); + gettingStarted.addReward("bread", 5); + questRegistry.put(gettingStarted.getId(), gettingStarted); + + Quest miningAdventure = new Quest("mining_adventure", "Mining Adventure", + "Venture underground and mine valuable resources"); + miningAdventure.addObjective(new Quest.QuestObjective("mine_iron", "Mine 16 iron ore", 16)); + miningAdventure.addObjective(new Quest.QuestObjective("mine_coal", "Mine 32 coal", 32)); + miningAdventure.addReward("diamond", 2); + questRegistry.put(miningAdventure.getId(), miningAdventure); + + Quest masterCrafter = new Quest("master_crafter", "Master Crafter", + "Prove your crafting skills by creating advanced items"); + masterCrafter.addObjective(new Quest.QuestObjective("craft_iron_sword", "Craft an iron sword", 1)); + masterCrafter.addObjective(new Quest.QuestObjective("craft_iron_pickaxe", "Craft an iron pickaxe", 1)); + masterCrafter.addObjective(new Quest.QuestObjective("craft_chest", "Craft 5 chests", 5)); + masterCrafter.addReward("diamond", 3); + questRegistry.put(masterCrafter.getId(), masterCrafter); + + // Combat quests + Quest monsterHunter = new Quest("monster_hunter", "Monster Hunter", + "Defeat hostile mobs to protect the land"); + monsterHunter.addObjective(new Quest.QuestObjective("kill_zombie", "Kill 10 zombies", 10)); + monsterHunter.addObjective(new Quest.QuestObjective("kill_skeleton", "Kill 10 skeletons", 10)); + monsterHunter.addObjective(new Quest.QuestObjective("kill_creeper", "Kill 5 creepers", 5)); + monsterHunter.addReward("diamond_sword", 1); + questRegistry.put(monsterHunter.getId(), monsterHunter); + + // Building quests + Quest architectApprentice = new Quest("architect_apprentice", "Architect Apprentice", + "Build structures to showcase your construction skills"); + architectApprentice.addObjective(new Quest.QuestObjective("build_house", "Build a house", 1)); + architectApprentice.addObjective(new Quest.QuestObjective("build_tower", "Build a tower", 1)); + architectApprentice.addReward("golden_apple", 3); + questRegistry.put(architectApprentice.getId(), architectApprentice); + + // Farming quests + Quest farmersDelight = new Quest("farmers_delight", "Farmer's Delight", + "Establish a thriving farm with crops and animals"); + farmersDelight.addObjective(new Quest.QuestObjective("farm_wheat", "Harvest 64 wheat", 64)); + farmersDelight.addObjective(new Quest.QuestObjective("breed_cow", "Breed 5 cows", 5)); + farmersDelight.addObjective(new Quest.QuestObjective("breed_chicken", "Breed 5 chickens", 5)); + farmersDelight.addReward("diamond_hoe", 1); + questRegistry.put(farmersDelight.getId(), farmersDelight); + + // Advanced quests + Quest netherExplorer = new Quest("nether_explorer", "Nether Explorer", + "Brave the dangers of the Nether dimension"); + netherExplorer.addObjective(new Quest.QuestObjective("build_portal", "Build a nether portal", 1)); + netherExplorer.addObjective(new Quest.QuestObjective("enter_nether", "Enter the Nether", 1)); + netherExplorer.addObjective(new Quest.QuestObjective("mine_ancient_debris", "Mine ancient debris", 4)); + netherExplorer.addReward("netherite_ingot", 2); + questRegistry.put(netherExplorer.getId(), netherExplorer); + + Quest redstoneEngineer = new Quest("redstone_engineer", "Redstone Engineer", + "Master the art of redstone contraptions"); + redstoneEngineer.addObjective(new Quest.QuestObjective("build_auto_door", "Build automatic door", 1)); + redstoneEngineer.addObjective(new Quest.QuestObjective("place_redstone", "Place 64 redstone dust", 64)); + redstoneEngineer.addReward("diamond", 5); + questRegistry.put(redstoneEngineer.getId(), redstoneEngineer); + } + + /** + * Start a quest by ID + */ + public boolean startQuest(String questId) { + if (!questRegistry.containsKey(questId)) { + SteveMod.LOGGER.warn("Quest not found: {}", questId); + return false; + } + + if (activeQuests.containsKey(questId)) { + SteveMod.LOGGER.warn("Quest already active: {}", questId); + return false; + } + + if (completedQuests.containsKey(questId)) { + SteveMod.LOGGER.info("Quest already completed: {}", questId); + return false; + } + + // Create a copy of the quest from registry + Quest quest = questRegistry.get(questId); + Quest activeQuest = new Quest(quest.getId(), quest.getTitle(), quest.getDescription()); + + // Copy objectives + for (Quest.QuestObjective objective : quest.getObjectives()) { + activeQuest.addObjective(new Quest.QuestObjective( + objective.getType(), + objective.getDescription(), + objective.getTargetProgress() + )); + } + + // Copy rewards + quest.getRewards().forEach(activeQuest::addReward); + + activeQuest.start(); + activeQuests.put(questId, activeQuest); + + SteveMod.LOGGER.info("Steve '{}' started quest: {}", steveName, quest.getTitle()); + saveQuestProgress(); + return true; + } + + /** + * Update quest progress for all active quests + */ + public void updateQuestProgress(String objectiveType, int amount) { + List completedNow = new ArrayList<>(); + + for (Quest quest : activeQuests.values()) { + quest.updateProgress(objectiveType, amount); + + if (quest.isComplete()) { + completedNow.add(quest); + } + } + + // Move completed quests + for (Quest quest : completedNow) { + completeQuest(quest); + } + + if (!completedNow.isEmpty()) { + saveQuestProgress(); + } + } + + /** + * Complete a quest and give rewards + */ + private void completeQuest(Quest quest) { + activeQuests.remove(quest.getId()); + completedQuests.put(quest.getId(), quest); + + SteveMod.LOGGER.info("Steve '{}' completed quest: {} in {}s", + steveName, quest.getTitle(), quest.getTimeSpentSeconds()); + + // Note: Reward giving would be implemented in SteveEntity + if (!quest.getRewards().isEmpty()) { + SteveMod.LOGGER.info("Quest rewards: {}", quest.getRewards()); + } + } + + /** + * Abandon a quest + */ + public boolean abandonQuest(String questId) { + Quest quest = activeQuests.remove(questId); + if (quest != null) { + quest.fail(); + SteveMod.LOGGER.info("Steve '{}' abandoned quest: {}", steveName, quest.getTitle()); + saveQuestProgress(); + return true; + } + return false; + } + + /** + * Get all active quests + */ + public List getActiveQuests() { + return new ArrayList<>(activeQuests.values()); + } + + /** + * Get all completed quests + */ + public List getCompletedQuests() { + return new ArrayList<>(completedQuests.values()); + } + + /** + * Get all available quests (from registry) + */ + public List getAvailableQuests() { + List available = new ArrayList<>(); + for (Quest quest : questRegistry.values()) { + if (!activeQuests.containsKey(quest.getId()) && + !completedQuests.containsKey(quest.getId())) { + available.add(quest); + } + } + return available; + } + + /** + * Get quest summary string + */ + public String getQuestSummary() { + StringBuilder sb = new StringBuilder(); + sb.append("\n=== ACTIVE QUESTS ===\n"); + + if (activeQuests.isEmpty()) { + sb.append("No active quests\n"); + } else { + for (Quest quest : activeQuests.values()) { + sb.append(quest.getSummary()).append("\n"); + } + } + + sb.append("\nCompleted: ").append(completedQuests.size()) + .append(" | Available: ").append(getAvailableQuests().size()).append("\n"); + + return sb.toString(); + } + + /** + * Save quest progress to disk + */ + public void saveQuestProgress() { + try { + Path filePath = Paths.get(QUEST_DATA_DIR, steveName + "_quests.json"); + + Map data = new HashMap<>(); + data.put("activeQuests", activeQuests); + data.put("completedQuests", completedQuests); + + String json = GSON.toJson(data); + Files.writeString(filePath, json); + + SteveMod.LOGGER.debug("Saved quest progress for Steve '{}'", steveName); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to save quest progress", e); + } + } + + /** + * Load quest progress from disk + */ + public void loadQuestProgress() { + try { + Path filePath = Paths.get(QUEST_DATA_DIR, steveName + "_quests.json"); + + if (!Files.exists(filePath)) { + return; + } + + String json = Files.readString(filePath); + Map data = GSON.fromJson(json, + new TypeToken>(){}.getType()); + + // Note: Full deserialization would require custom deserializer + // For now, this structure supports basic persistence + + SteveMod.LOGGER.info("Loaded quest progress for Steve '{}'", steveName); + } catch (IOException e) { + SteveMod.LOGGER.error("Failed to load quest progress", e); + } + } + + /** + * Get quest by ID + */ + public Quest getQuest(String questId) { + if (activeQuests.containsKey(questId)) { + return activeQuests.get(questId); + } + if (completedQuests.containsKey(questId)) { + return completedQuests.get(questId); + } + return questRegistry.get(questId); + } +} diff --git a/src/main/java/com/steve/ai/redstone/RedstoneHelper.java b/src/main/java/com/steve/ai/redstone/RedstoneHelper.java new file mode 100644 index 0000000..424df89 --- /dev/null +++ b/src/main/java/com/steve/ai/redstone/RedstoneHelper.java @@ -0,0 +1,248 @@ +package com.steve.ai.redstone; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; + +/** + * Helper class for redstone operations and circuit building + * Provides utilities for placing redstone components and checking power + */ +public class RedstoneHelper { + + /** + * Place redstone dust at position + */ + public static boolean placeRedstoneDust(Level level, BlockPos pos) { + if (!level.getBlockState(pos).isAir()) { + return false; + } + + BlockState redstone = Blocks.REDSTONE_WIRE.defaultBlockState(); + level.setBlock(pos, redstone, 3); + return true; + } + + /** + * Place redstone torch at position + */ + public static boolean placeRedstoneTorch(Level level, BlockPos pos, Direction facing) { + if (!level.getBlockState(pos).isAir()) { + return false; + } + + Block torchBlock = facing == Direction.UP ? + Blocks.REDSTONE_TORCH : Blocks.REDSTONE_WALL_TORCH; + + BlockState torch = torchBlock.defaultBlockState(); + level.setBlock(pos, torch, 3); + return true; + } + + /** + * Place redstone repeater at position + */ + public static boolean placeRepeater(Level level, BlockPos pos, Direction facing, int delay) { + if (!level.getBlockState(pos).isAir()) { + return false; + } + + BlockState repeater = Blocks.REPEATER.defaultBlockState() + .setValue(RepeaterBlock.FACING, facing) + .setValue(RepeaterBlock.DELAY, Math.min(4, Math.max(1, delay))); + + level.setBlock(pos, repeater, 3); + return true; + } + + /** + * Place redstone comparator at position + */ + public static boolean placeComparator(Level level, BlockPos pos, Direction facing) { + if (!level.getBlockState(pos).isAir()) { + return false; + } + + BlockState comparator = Blocks.COMPARATOR.defaultBlockState() + .setValue(ComparatorBlock.FACING, facing); + + level.setBlock(pos, comparator, 3); + return true; + } + + /** + * Place lever at position + */ + public static boolean placeLever(Level level, BlockPos pos, Direction attachFace) { + BlockState lever = Blocks.LEVER.defaultBlockState(); + + if (attachFace == Direction.UP) { + lever = lever.setValue(LeverBlock.FACE, net.minecraft.world.level.block.state.properties.AttachFace.FLOOR); + } else if (attachFace == Direction.DOWN) { + lever = lever.setValue(LeverBlock.FACE, net.minecraft.world.level.block.state.properties.AttachFace.CEILING); + } else { + lever = lever.setValue(LeverBlock.FACE, net.minecraft.world.level.block.state.properties.AttachFace.WALL) + .setValue(LeverBlock.FACING, attachFace); + } + + level.setBlock(pos, lever, 3); + return true; + } + + /** + * Place button at position + */ + public static boolean placeButton(Level level, BlockPos pos, Direction facing) { + BlockState button = Blocks.STONE_BUTTON.defaultBlockState() + .setValue(ButtonBlock.FACING, facing); + + level.setBlock(pos, button, 3); + return true; + } + + /** + * Place pressure plate at position + */ + public static boolean placePressurePlate(Level level, BlockPos pos) { + if (!level.getBlockState(pos).isAir()) { + return false; + } + + BlockState plate = Blocks.STONE_PRESSURE_PLATE.defaultBlockState(); + level.setBlock(pos, plate, 3); + return true; + } + + /** + * Place piston at position + */ + public static boolean placePiston(Level level, BlockPos pos, Direction facing, boolean sticky) { + if (!level.getBlockState(pos).isAir()) { + return false; + } + + Block pistonBlock = sticky ? Blocks.STICKY_PISTON : Blocks.PISTON; + BlockState piston = pistonBlock.defaultBlockState() + .setValue(PistonBaseBlock.FACING, facing); + + level.setBlock(pos, piston, 3); + return true; + } + + /** + * Place redstone lamp at position + */ + public static boolean placeRedstoneLamp(Level level, BlockPos pos) { + if (!level.getBlockState(pos).isAir()) { + return false; + } + + BlockState lamp = Blocks.REDSTONE_LAMP.defaultBlockState(); + level.setBlock(pos, lamp, 3); + return true; + } + + /** + * Check if position is receiving redstone power + */ + public static boolean isPowered(Level level, BlockPos pos) { + return level.hasNeighborSignal(pos); + } + + /** + * Get redstone power level at position + */ + public static int getPowerLevel(Level level, BlockPos pos) { + return level.getBestNeighborSignal(pos); + } + + /** + * Check if block is a redstone component + */ + public static boolean isRedstoneComponent(Block block) { + return block instanceof RedStoneWireBlock || + block instanceof RepeaterBlock || + block instanceof ComparatorBlock || + block instanceof LeverBlock || + block instanceof ButtonBlock || + block instanceof PressurePlateBlock || + block instanceof RedstoneTorchBlock || + block instanceof PistonBaseBlock || + block instanceof RedstoneLampBlock; + } + + /** + * Build simple redstone wire path between two points + */ + public static int buildWirePath(Level level, BlockPos start, BlockPos end) { + int placed = 0; + + // Simple horizontal path + int dx = end.getX() - start.getX(); + int dz = end.getZ() - start.getZ(); + + // Place along X axis first + int stepX = dx > 0 ? 1 : -1; + for (int i = 0; i != dx; i += stepX) { + BlockPos wirePos = start.offset(i, 0, 0); + if (placeRedstoneDust(level, wirePos)) { + placed++; + } + } + + // Then along Z axis + int stepZ = dz > 0 ? 1 : -1; + for (int i = 0; i != dz; i += stepZ) { + BlockPos wirePos = start.offset(dx, 0, i); + if (placeRedstoneDust(level, wirePos)) { + placed++; + } + } + + return placed; + } + + /** + * Create a simple door control circuit + * Returns true if circuit was successfully created + */ + public static boolean createDoorCircuit(Level level, BlockPos buttonPos, BlockPos doorPos) { + // Calculate path from button to door + // Place button + Direction buttonFacing = Direction.NORTH; // Default facing + if (!placeButton(level, buttonPos, buttonFacing)) { + return false; + } + + // Build redstone wire path + BlockPos wireStart = buttonPos.relative(buttonFacing.getOpposite()); + int wiresPlaced = buildWirePath(level, wireStart, doorPos.below()); + + return wiresPlaced > 0; + } + + /** + * Get required materials for basic redstone circuit + */ + public static String getCircuitMaterials(CircuitType type) { + return switch (type) { + case DOOR -> "1 button, redstone dust (varies), 1 door"; + case LAMP -> "1 lever, redstone dust (varies), 1 redstone lamp"; + case PISTON_DOOR -> "4 sticky pistons, 4 blocks, 1 button, redstone dust"; + case PRESSURE_PLATE -> "1 pressure plate, redstone dust (varies), 1 door"; + }; + } + + /** + * Circuit types + */ + public enum CircuitType { + DOOR, // Button/lever activated door + LAMP, // Switch for redstone lamp + PISTON_DOOR, // Hidden piston door + PRESSURE_PLATE // Pressure plate auto door + } +} diff --git a/src/main/java/com/steve/ai/team/SteveRole.java b/src/main/java/com/steve/ai/team/SteveRole.java new file mode 100644 index 0000000..285a552 --- /dev/null +++ b/src/main/java/com/steve/ai/team/SteveRole.java @@ -0,0 +1,120 @@ +package com.steve.ai.team; + +/** + * Roles that Steve entities can take on for team coordination + * Each role has specific responsibilities and strengths + */ +public enum SteveRole { + /** + * MINER: Specializes in resource gathering + * - Mines ores and minerals + * - Focuses on underground exploration + * - Efficient at tool usage + */ + MINER("Miner", "Resource gathering and mining operations"), + + /** + * BUILDER: Specializes in construction + * - Builds structures + * - Places blocks efficiently + * - Manages building materials + */ + BUILDER("Builder", "Construction and building projects"), + + /** + * FIGHTER: Specializes in combat + * - Engages hostile mobs + * - Protects team members + * - Patrols dangerous areas + */ + FIGHTER("Fighter", "Combat and protection"), + + /** + * HAULER: Specializes in logistics + * - Transports items between locations + * - Manages inventory and storage + * - Organizes chest systems + */ + HAULER("Hauler", "Logistics and item transport"), + + /** + * SCOUT: Specializes in exploration + * - Explores new areas + * - Locates resources and structures + * - Reports findings to team + */ + SCOUT("Scout", "Exploration and reconnaissance"), + + /** + * LEADER: Coordinates team activities + * - Assigns tasks to team members + * - Makes strategic decisions + * - Monitors team progress + */ + LEADER("Leader", "Team coordination and strategy"), + + /** + * GENERALIST: No specific specialization + * - Can perform any task + * - Default role for unassigned Steves + * - Flexible task assignment + */ + GENERALIST("Generalist", "General purpose tasks"); + + private final String displayName; + private final String description; + + SteveRole(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + /** + * Check if this role is suitable for a given task type + */ + public boolean isSuitableFor(String taskType) { + return switch (this) { + case MINER -> taskType.equals("mine") || taskType.equals("gather"); + case BUILDER -> taskType.equals("build") || taskType.equals("place"); + case FIGHTER -> taskType.equals("attack") || taskType.equals("combat"); + case HAULER -> taskType.equals("store") || taskType.equals("retrieve") || + taskType.equals("place_chest"); + case SCOUT -> taskType.equals("pathfind") || taskType.equals("explore"); + case LEADER -> true; // Leaders can coordinate any task + case GENERALIST -> true; // Generalists can do anything + }; + } + + /** + * Get priority multiplier for this role with a given task + * Higher values mean more suitable + */ + public double getPriorityMultiplier(String taskType) { + if (!isSuitableFor(taskType)) { + return 0.5; // Not ideal but can still do it + } + + return switch (this) { + case MINER -> taskType.equals("mine") ? 2.0 : 1.0; + case BUILDER -> taskType.equals("build") ? 2.0 : 1.0; + case FIGHTER -> taskType.equals("attack") ? 2.0 : 1.0; + case HAULER -> (taskType.equals("store") || taskType.equals("retrieve")) ? 2.0 : 1.0; + case SCOUT -> taskType.equals("pathfind") ? 2.0 : 1.0; + case LEADER -> 1.5; // Leaders are good at everything + case GENERALIST -> 1.0; // Standard priority + }; + } + + @Override + public String toString() { + return displayName; + } +} diff --git a/src/main/java/com/steve/ai/team/Team.java b/src/main/java/com/steve/ai/team/Team.java new file mode 100644 index 0000000..a0c0c0b --- /dev/null +++ b/src/main/java/com/steve/ai/team/Team.java @@ -0,0 +1,338 @@ +package com.steve.ai.team; + +import com.steve.ai.SteveMod; +import com.steve.ai.entity.SteveEntity; +import net.minecraft.core.BlockPos; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Represents a team of Steve entities working together + * Manages team coordination, task assignment, and communication + */ +public class Team { + private final String teamName; + private final UUID teamId; + private final Map members; // steveName -> entity + private final Map roleAssignments; // steveName -> role + private String leaderName; + private TeamGoal currentGoal; + private final List messageQueue; + private long creationTime; + + public Team(String teamName) { + this.teamName = teamName; + this.teamId = UUID.randomUUID(); + this.members = new ConcurrentHashMap<>(); + this.roleAssignments = new ConcurrentHashMap<>(); + this.messageQueue = new ArrayList<>(); + this.creationTime = System.currentTimeMillis(); + this.leaderName = null; + this.currentGoal = null; + } + + /** + * Add a Steve to this team + */ + public boolean addMember(SteveEntity steve, SteveRole role) { + String steveName = steve.getSteveName(); + + if (members.containsKey(steveName)) { + SteveMod.LOGGER.warn("Steve '{}' is already in team '{}'", steveName, teamName); + return false; + } + + members.put(steveName, steve); + roleAssignments.put(steveName, role); + + // If no leader and this is the first member or has LEADER role, make them leader + if (leaderName == null || role == SteveRole.LEADER) { + leaderName = steveName; + } + + SteveMod.LOGGER.info("Steve '{}' joined team '{}' as {}", steveName, teamName, role); + broadcastMessage(new TeamMessage( + "SYSTEM", + steveName + " joined the team as " + role.getDisplayName() + )); + + return true; + } + + /** + * Remove a Steve from this team + */ + public boolean removeMember(String steveName) { + if (!members.containsKey(steveName)) { + return false; + } + + members.remove(steveName); + roleAssignments.remove(steveName); + + // If removed member was leader, assign new leader + if (steveName.equals(leaderName)) { + assignNewLeader(); + } + + SteveMod.LOGGER.info("Steve '{}' left team '{}'", steveName, teamName); + broadcastMessage(new TeamMessage( + "SYSTEM", + steveName + " left the team" + )); + + return true; + } + + /** + * Assign a role to a team member + */ + public boolean assignRole(String steveName, SteveRole role) { + if (!members.containsKey(steveName)) { + return false; + } + + SteveRole oldRole = roleAssignments.get(steveName); + roleAssignments.put(steveName, role); + + // Update leader if role changed to/from LEADER + if (role == SteveRole.LEADER) { + leaderName = steveName; + } else if (oldRole == SteveRole.LEADER && role != SteveRole.LEADER) { + assignNewLeader(); + } + + SteveMod.LOGGER.info("Steve '{}' role changed from {} to {}", steveName, oldRole, role); + return true; + } + + /** + * Assign a new leader (called when current leader leaves or changes role) + */ + private void assignNewLeader() { + // Try to find a member with LEADER role + for (Map.Entry entry : roleAssignments.entrySet()) { + if (entry.getValue() == SteveRole.LEADER) { + leaderName = entry.getKey(); + return; + } + } + + // If no leader role, pick first member + if (!members.isEmpty()) { + leaderName = members.keySet().iterator().next(); + roleAssignments.put(leaderName, SteveRole.LEADER); + } else { + leaderName = null; + } + } + + /** + * Set team goal + */ + public void setGoal(TeamGoal goal) { + this.currentGoal = goal; + SteveMod.LOGGER.info("Team '{}' new goal: {}", teamName, goal.getDescription()); + broadcastMessage(new TeamMessage( + "SYSTEM", + "New team goal: " + goal.getDescription() + )); + } + + /** + * Broadcast a message to all team members + */ + public void broadcastMessage(TeamMessage message) { + messageQueue.add(message); + + // Keep only last 50 messages + if (messageQueue.size() > 50) { + messageQueue.remove(0); + } + } + + /** + * Send a message from one team member to another + */ + public void sendMessage(String from, String to, String content) { + TeamMessage message = new TeamMessage(from, content, to); + messageQueue.add(message); + } + + /** + * Get messages for a specific team member + */ + public List getMessagesFor(String steveName) { + return messageQueue.stream() + .filter(msg -> msg.isForEveryone() || msg.getRecipient().equals(steveName)) + .toList(); + } + + /** + * Get recent team messages + */ + public List getRecentMessages(int count) { + int start = Math.max(0, messageQueue.size() - count); + return new ArrayList<>(messageQueue.subList(start, messageQueue.size())); + } + + /** + * Find best team member for a task + */ + public SteveEntity findBestMemberForTask(String taskType) { + if (members.isEmpty()) { + return null; + } + + // Find members with roles suitable for this task + return members.entrySet().stream() + .max(Comparator.comparingDouble(entry -> { + SteveRole role = roleAssignments.get(entry.getKey()); + return role.getPriorityMultiplier(taskType); + })) + .map(Map.Entry::getValue) + .orElse(null); + } + + /** + * Get all members with a specific role + */ + public List getMembersByRole(SteveRole role) { + return roleAssignments.entrySet().stream() + .filter(entry -> entry.getValue() == role) + .map(entry -> members.get(entry.getKey())) + .filter(Objects::nonNull) + .toList(); + } + + /** + * Get team status summary + */ + public String getStatusSummary() { + StringBuilder sb = new StringBuilder(); + sb.append("=== Team: ").append(teamName).append(" ===\n"); + sb.append("Members: ").append(members.size()).append("\n"); + sb.append("Leader: ").append(leaderName != null ? leaderName : "None").append("\n"); + + if (currentGoal != null) { + sb.append("Goal: ").append(currentGoal.getDescription()).append("\n"); + } + + sb.append("Roles:\n"); + for (Map.Entry entry : roleAssignments.entrySet()) { + sb.append(" - ").append(entry.getKey()).append(": ") + .append(entry.getValue().getDisplayName()).append("\n"); + } + + return sb.toString(); + } + + // Getters + + public String getTeamName() { + return teamName; + } + + public UUID getTeamId() { + return teamId; + } + + public int getMemberCount() { + return members.size(); + } + + public boolean hasMember(String steveName) { + return members.containsKey(steveName); + } + + public SteveEntity getMember(String steveName) { + return members.get(steveName); + } + + public Collection getAllMembers() { + return new ArrayList<>(members.values()); + } + + public SteveRole getRole(String steveName) { + return roleAssignments.getOrDefault(steveName, SteveRole.GENERALIST); + } + + public String getLeaderName() { + return leaderName; + } + + public SteveEntity getLeader() { + return leaderName != null ? members.get(leaderName) : null; + } + + public TeamGoal getCurrentGoal() { + return currentGoal; + } + + public long getCreationTime() { + return creationTime; + } + + /** + * Team message for communication + */ + public static class TeamMessage { + private final long timestamp; + private final String sender; + private final String content; + private final String recipient; // null for broadcast + + public TeamMessage(String sender, String content) { + this(sender, content, null); + } + + public TeamMessage(String sender, String content, String recipient) { + this.timestamp = System.currentTimeMillis(); + this.sender = sender; + this.content = content; + this.recipient = recipient; + } + + public boolean isForEveryone() { + return recipient == null; + } + + public long getTimestamp() { return timestamp; } + public String getSender() { return sender; } + public String getContent() { return content; } + public String getRecipient() { return recipient; } + + @Override + public String toString() { + if (isForEveryone()) { + return String.format("[%s] %s", sender, content); + } else { + return String.format("[%s -> %s] %s", sender, recipient, content); + } + } + } + + /** + * Team goal representation + */ + public static class TeamGoal { + private final String description; + private final BlockPos location; + private final Map metadata; + + public TeamGoal(String description) { + this(description, null, new HashMap<>()); + } + + public TeamGoal(String description, BlockPos location, Map metadata) { + this.description = description; + this.location = location; + this.metadata = metadata; + } + + public String getDescription() { return description; } + public BlockPos getLocation() { return location; } + public Map getMetadata() { return metadata; } + } +} diff --git a/src/main/java/com/steve/ai/team/TeamManager.java b/src/main/java/com/steve/ai/team/TeamManager.java new file mode 100644 index 0000000..caecaf5 --- /dev/null +++ b/src/main/java/com/steve/ai/team/TeamManager.java @@ -0,0 +1,330 @@ +package com.steve.ai.team; + +import com.steve.ai.SteveMod; +import com.steve.ai.action.Task; +import com.steve.ai.entity.SteveEntity; +import net.minecraft.core.BlockPos; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Global manager for all Steve teams + * Handles team creation, coordination, and task distribution + */ +public class TeamManager { + private static TeamManager INSTANCE; + + private final Map teams; // teamName -> Team + private final Map memberTeams; // steveName -> teamName + private final Map teamsByUuid; // teamId -> Team + + private TeamManager() { + this.teams = new ConcurrentHashMap<>(); + this.memberTeams = new ConcurrentHashMap<>(); + this.teamsByUuid = new ConcurrentHashMap<>(); + } + + /** + * Get singleton instance + */ + public static synchronized TeamManager getInstance() { + if (INSTANCE == null) { + INSTANCE = new TeamManager(); + } + return INSTANCE; + } + + /** + * Create a new team + */ + public Team createTeam(String teamName) { + if (teams.containsKey(teamName)) { + SteveMod.LOGGER.warn("Team '{}' already exists", teamName); + return teams.get(teamName); + } + + Team team = new Team(teamName); + teams.put(teamName, team); + teamsByUuid.put(team.getTeamId(), team); + + SteveMod.LOGGER.info("Created team: {}", teamName); + return team; + } + + /** + * Delete a team + */ + public boolean deleteTeam(String teamName) { + Team team = teams.get(teamName); + if (team == null) { + return false; + } + + // Remove all members from team mapping + for (SteveEntity member : team.getAllMembers()) { + memberTeams.remove(member.getSteveName()); + } + + teams.remove(teamName); + teamsByUuid.remove(team.getTeamId()); + + SteveMod.LOGGER.info("Deleted team: {}", teamName); + return true; + } + + /** + * Add a Steve to a team with a specific role + */ + public boolean addToTeam(SteveEntity steve, String teamName, SteveRole role) { + Team team = teams.get(teamName); + if (team == null) { + SteveMod.LOGGER.error("Team '{}' does not exist", teamName); + return false; + } + + String steveName = steve.getSteveName(); + + // Remove from previous team if exists + String previousTeam = memberTeams.get(steveName); + if (previousTeam != null) { + Team oldTeam = teams.get(previousTeam); + if (oldTeam != null) { + oldTeam.removeMember(steveName); + } + } + + // Add to new team + boolean added = team.addMember(steve, role); + if (added) { + memberTeams.put(steveName, teamName); + } + + return added; + } + + /** + * Remove a Steve from their current team + */ + public boolean removeFromTeam(String steveName) { + String teamName = memberTeams.get(steveName); + if (teamName == null) { + return false; + } + + Team team = teams.get(teamName); + if (team == null) { + return false; + } + + boolean removed = team.removeMember(steveName); + if (removed) { + memberTeams.remove(steveName); + } + + return removed; + } + + /** + * Assign a role to a Steve + */ + public boolean assignRole(String steveName, SteveRole role) { + String teamName = memberTeams.get(steveName); + if (teamName == null) { + SteveMod.LOGGER.warn("Steve '{}' is not in any team", steveName); + return false; + } + + Team team = teams.get(teamName); + if (team == null) { + return false; + } + + return team.assignRole(steveName, role); + } + + /** + * Get a Steve's team + */ + public Team getTeam(String steveName) { + String teamName = memberTeams.get(steveName); + return teamName != null ? teams.get(teamName) : null; + } + + /** + * Get a team by name + */ + public Team getTeamByName(String teamName) { + return teams.get(teamName); + } + + /** + * Get a team by UUID + */ + public Team getTeamById(UUID teamId) { + return teamsByUuid.get(teamId); + } + + /** + * Get a Steve's role + */ + public SteveRole getRole(String steveName) { + Team team = getTeam(steveName); + return team != null ? team.getRole(steveName) : SteveRole.GENERALIST; + } + + /** + * Check if a Steve is in a team + */ + public boolean isInTeam(String steveName) { + return memberTeams.containsKey(steveName); + } + + /** + * Get all teams + */ + public Collection getAllTeams() { + return new ArrayList<>(teams.values()); + } + + /** + * Get all team names + */ + public Set getAllTeamNames() { + return new HashSet<>(teams.keySet()); + } + + /** + * Coordinate a team task + * Distributes subtasks to appropriate team members based on roles + */ + public void coordinateTeamTask(String teamName, String taskDescription, List subtasks) { + Team team = teams.get(teamName); + if (team == null) { + SteveMod.LOGGER.error("Cannot coordinate task for non-existent team: {}", teamName); + return; + } + + if (team.getMemberCount() == 0) { + SteveMod.LOGGER.warn("Cannot coordinate task for team '{}': no members", teamName); + return; + } + + SteveMod.LOGGER.info("Team '{}' coordinating task: {} ({} subtasks)", + teamName, taskDescription, subtasks.size()); + + // Set team goal + team.setGoal(new Team.TeamGoal(taskDescription)); + + // Assign subtasks to best suited members + for (Task task : subtasks) { + SteveEntity bestMember = team.findBestMemberForTask(task.getAction()); + + if (bestMember != null) { + SteveMod.LOGGER.info("Assigning {} task to {} (role: {})", + task.getAction(), + bestMember.getSteveName(), + team.getRole(bestMember.getSteveName()) + ); + + // Send task to member's executor + // This would need to be implemented based on how tasks are dispatched + team.broadcastMessage(new Team.TeamMessage( + "COORDINATOR", + "Task assigned to " + bestMember.getSteveName() + ": " + task.getAction() + )); + } else { + SteveMod.LOGGER.warn("No suitable team member found for task: {}", task.getAction()); + } + } + } + + /** + * Create a mining team (MINER + HAULER) + */ + public Team createMiningTeam(String teamName, SteveEntity miner, SteveEntity hauler) { + Team team = createTeam(teamName); + addToTeam(miner, teamName, SteveRole.MINER); + addToTeam(hauler, teamName, SteveRole.HAULER); + + SteveMod.LOGGER.info("Created mining team '{}' with miner {} and hauler {}", + teamName, miner.getSteveName(), hauler.getSteveName()); + + return team; + } + + /** + * Create a combat team (FIGHTER + FIGHTER) + */ + public Team createCombatTeam(String teamName, SteveEntity tank, SteveEntity dps) { + Team team = createTeam(teamName); + addToTeam(tank, teamName, SteveRole.FIGHTER); + addToTeam(dps, teamName, SteveRole.FIGHTER); + + SteveMod.LOGGER.info("Created combat team '{}' with fighters {} and {}", + teamName, tank.getSteveName(), dps.getSteveName()); + + return team; + } + + /** + * Create a building team (multiple BUILDERS + HAULER) + */ + public Team createBuildingTeam(String teamName, List builders, SteveEntity hauler) { + Team team = createTeam(teamName); + + for (SteveEntity builder : builders) { + addToTeam(builder, teamName, SteveRole.BUILDER); + } + + if (hauler != null) { + addToTeam(hauler, teamName, SteveRole.HAULER); + } + + SteveMod.LOGGER.info("Created building team '{}' with {} builders", + teamName, builders.size()); + + return team; + } + + /** + * Broadcast message to all teams + */ + public void broadcastToAll(String message) { + Team.TeamMessage teamMessage = new Team.TeamMessage("SYSTEM", message); + for (Team team : teams.values()) { + team.broadcastMessage(teamMessage); + } + } + + /** + * Get global status summary of all teams + */ + public String getGlobalStatus() { + StringBuilder sb = new StringBuilder(); + sb.append("=== TEAM MANAGER STATUS ===\n"); + sb.append("Total Teams: ").append(teams.size()).append("\n"); + sb.append("Total Team Members: ").append(memberTeams.size()).append("\n\n"); + + if (teams.isEmpty()) { + sb.append("No teams exist.\n"); + } else { + for (Team team : teams.values()) { + sb.append(team.getStatusSummary()).append("\n"); + } + } + + return sb.toString(); + } + + /** + * Clear all teams (for debugging/reset) + */ + public void clearAll() { + teams.clear(); + memberTeams.clear(); + teamsByUuid.clear(); + SteveMod.LOGGER.info("Cleared all teams"); + } +} diff --git a/src/main/java/com/steve/ai/util/InventoryHelper.java b/src/main/java/com/steve/ai/util/InventoryHelper.java index c6120b9..1d59671 100644 --- a/src/main/java/com/steve/ai/util/InventoryHelper.java +++ b/src/main/java/com/steve/ai/util/InventoryHelper.java @@ -1,31 +1,334 @@ package com.steve.ai.util; import com.steve.ai.entity.SteveEntity; -import net.minecraft.world.item.Item; -import net.minecraft.world.item.ItemStack; - -// TODO: Will be implemented later - inventory management for Steve entities -// Required for crafting, trading, and resource management -// Will provide methods to: -// - Add/remove items from Steve's inventory -// - Check if Steve has required items -// - Manage inventory slots +import net.minecraft.core.BlockPos; +import net.minecraft.world.Container; +import net.minecraft.world.item.*; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.common.TierSortingRegistry; +import net.minecraftforge.items.ItemStackHandler; +import org.jetbrains.annotations.Nullable; + +/** + * Inventory management utility for Steve entities + * Provides comprehensive inventory operations including: + * - Item queries (checking existence, counting) + * - Item manipulation (add, remove, transfer) + * - Tool management (selection, durability checking) + * - Chest operations (finding, storing, retrieving) + */ public class InventoryHelper { - + + /** + * Check if Steve has at least the specified count of an item + * @param steve The Steve entity + * @param item The item to check for + * @param count Minimum count required + * @return true if Steve has >= count of the item + */ public static boolean hasItem(SteveEntity steve, Item item, int count) { - return false; + return getItemCount(steve, item) >= count; + } + + /** + * Count total number of a specific item in Steve's inventory + * @param steve The Steve entity + * @param item The item to count + * @return Total count across all stacks + */ + public static int getItemCount(SteveEntity steve, Item item) { + ItemStackHandler inventory = steve.getInventory(); + int totalCount = 0; + + for (int i = 0; i < inventory.getSlots(); i++) { + ItemStack stack = inventory.getStackInSlot(i); + if (!stack.isEmpty() && stack.getItem() == item) { + totalCount += stack.getCount(); + } + } + + return totalCount; } - + + /** + * Add an item stack to Steve's inventory + * @param steve The Steve entity + * @param stack The item stack to add + * @return true if successfully added (or partially added), false if inventory full + */ public static boolean addItem(SteveEntity steve, ItemStack stack) { - return false; + if (stack.isEmpty()) { + return false; + } + + ItemStackHandler inventory = steve.getInventory(); + ItemStack remaining = stack.copy(); + + // Try to merge with existing stacks first + for (int i = 0; i < inventory.getSlots() && !remaining.isEmpty(); i++) { + remaining = inventory.insertItem(i, remaining, false); + } + + // Return true if we managed to insert at least some items + return remaining.getCount() < stack.getCount(); } - + + /** + * Remove a specific quantity of an item from Steve's inventory + * @param steve The Steve entity + * @param item The item to remove + * @param count Number to remove + * @return true if successfully removed the full count + */ public static boolean removeItem(SteveEntity steve, Item item, int count) { - return false; + ItemStackHandler inventory = steve.getInventory(); + int remaining = count; + + for (int i = 0; i < inventory.getSlots() && remaining > 0; i++) { + ItemStack stack = inventory.getStackInSlot(i); + if (!stack.isEmpty() && stack.getItem() == item) { + int toRemove = Math.min(remaining, stack.getCount()); + inventory.extractItem(i, toRemove, false); + remaining -= toRemove; + } + } + + return remaining == 0; } - - public static int getItemCount(SteveEntity steve, Item item) { - return 0; + + /** + * Calculate inventory fullness as a percentage + * @param steve The Steve entity + * @return Value between 0.0 (empty) and 1.0 (full) + */ + public static float getInventoryFullness(SteveEntity steve) { + ItemStackHandler inventory = steve.getInventory(); + int usedSlots = 0; + + for (int i = 0; i < inventory.getSlots(); i++) { + if (!inventory.getStackInSlot(i).isEmpty()) { + usedSlots++; + } + } + + return (float) usedSlots / inventory.getSlots(); + } + + /** + * Check if inventory is considered full (>90% full) + * @param steve The Steve entity + * @return true if inventory is mostly full + */ + public static boolean isInventoryFull(SteveEntity steve) { + return getInventoryFullness(steve) > 0.9f; + } + + /** + * Find the best tool in inventory for breaking a specific block + * @param steve The Steve entity + * @param blockState The block to break + * @return The best tool ItemStack, or ItemStack.EMPTY if none found + */ + @Nullable + public static ItemStack findBestTool(SteveEntity steve, BlockState blockState) { + ItemStackHandler inventory = steve.getInventory(); + ItemStack bestTool = ItemStack.EMPTY; + float bestSpeed = 1.0f; + + for (int i = 0; i < inventory.getSlots(); i++) { + ItemStack stack = inventory.getStackInSlot(i); + if (stack.isEmpty()) continue; + + float speed = stack.getDestroySpeed(blockState); + if (speed > bestSpeed) { + bestSpeed = speed; + bestTool = stack; + } + } + + return bestTool; + } + + /** + * Check if a tool needs repair (durability < 10%) + * @param tool The tool to check + * @return true if durability is low + */ + public static boolean needsToolRepair(ItemStack tool) { + if (!tool.isDamageableItem()) { + return false; + } + + int maxDurability = tool.getMaxDamage(); + int currentDurability = maxDurability - tool.getDamageValue(); + + return currentDurability < (maxDurability * 0.1); + } + + /** + * Find the nearest chest block within a radius + * @param level The level/world + * @param center Center position to search from + * @param radius Search radius + * @return BlockPos of nearest chest, or null if none found + */ + @Nullable + public static BlockPos findNearestChest(Level level, BlockPos center, int radius) { + BlockPos nearestChest = null; + double nearestDistance = Double.MAX_VALUE; + + for (int x = -radius; x <= radius; x++) { + for (int y = -radius; y <= radius; y++) { + for (int z = -radius; z <= radius; z++) { + BlockPos pos = center.offset(x, y, z); + BlockState state = level.getBlockState(pos); + + if (state.is(Blocks.CHEST) || state.is(Blocks.BARREL)) { + double distance = center.distSqr(pos); + if (distance < nearestDistance) { + nearestDistance = distance; + nearestChest = pos; + } + } + } + } + } + + return nearestChest; + } + + /** + * Transfer items from Steve's inventory to a chest + * @param steve The Steve entity + * @param chestPos Position of the chest + * @param item The item to transfer (null = transfer all) + * @return Number of items transferred + */ + public static int transferToChest(SteveEntity steve, BlockPos chestPos, @Nullable Item item) { + Level level = steve.level(); + BlockEntity blockEntity = level.getBlockEntity(chestPos); + + if (!(blockEntity instanceof Container container)) { + return 0; + } + + ItemStackHandler steveInventory = steve.getInventory(); + int transferred = 0; + + for (int i = 0; i < steveInventory.getSlots(); i++) { + ItemStack stack = steveInventory.getStackInSlot(i); + if (stack.isEmpty()) continue; + if (item != null && stack.getItem() != item) continue; + + // Try to add to chest + for (int j = 0; j < container.getContainerSize(); j++) { + ItemStack chestStack = container.getItem(j); + + if (chestStack.isEmpty()) { + // Empty slot - place entire stack + container.setItem(j, stack.copy()); + transferred += stack.getCount(); + steveInventory.setStackInSlot(i, ItemStack.EMPTY); + break; + } else if (ItemStack.isSameItemSameTags(chestStack, stack)) { + // Merge with existing stack + int maxStack = chestStack.getMaxStackSize(); + int canAdd = Math.min(maxStack - chestStack.getCount(), stack.getCount()); + + if (canAdd > 0) { + chestStack.grow(canAdd); + stack.shrink(canAdd); + transferred += canAdd; + container.setItem(j, chestStack); + + if (stack.isEmpty()) { + steveInventory.setStackInSlot(i, ItemStack.EMPTY); + break; + } + } + } + } + } + + container.setChanged(); + return transferred; + } + + /** + * Retrieve items from a chest to Steve's inventory + * @param steve The Steve entity + * @param chestPos Position of the chest + * @param item The item to retrieve + * @param count Number of items to retrieve + * @return Number of items actually retrieved + */ + public static int retrieveFromChest(SteveEntity steve, BlockPos chestPos, Item item, int count) { + Level level = steve.level(); + BlockEntity blockEntity = level.getBlockEntity(chestPos); + + if (!(blockEntity instanceof Container container)) { + return 0; + } + + int remaining = count; + + for (int i = 0; i < container.getContainerSize() && remaining > 0; i++) { + ItemStack stack = container.getItem(i); + if (stack.isEmpty() || stack.getItem() != item) continue; + + int toTake = Math.min(remaining, stack.getCount()); + ItemStack taken = stack.split(toTake); + + if (addItem(steve, taken)) { + remaining -= toTake; + container.setItem(i, stack); + } else { + // Couldn't add - put back + stack.grow(toTake); + container.setItem(i, stack); + break; + } + } + + container.setChanged(); + return count - remaining; + } + + /** + * Check if Steve has the required tool tier for a block + * @param steve The Steve entity + * @param blockState The block to check + * @return true if Steve has appropriate tool + */ + public static boolean hasRequiredTool(SteveEntity steve, BlockState blockState) { + ItemStack bestTool = findBestTool(steve, blockState); + if (bestTool.isEmpty()) { + return blockState.requiresCorrectToolForDrops() == false; + } + + return bestTool.isCorrectToolForDrops(blockState); + } + + /** + * Get count of empty slots in inventory + * @param steve The Steve entity + * @return Number of empty slots + */ + public static int getEmptySlotCount(SteveEntity steve) { + ItemStackHandler inventory = steve.getInventory(); + int emptySlots = 0; + + for (int i = 0; i < inventory.getSlots(); i++) { + if (inventory.getStackInSlot(i).isEmpty()) { + emptySlots++; + } + } + + return emptySlots; } } diff --git a/src/main/java/com/steve/ai/util/RecipeHelper.java b/src/main/java/com/steve/ai/util/RecipeHelper.java index 74cd79a..71c1554 100644 --- a/src/main/java/com/steve/ai/util/RecipeHelper.java +++ b/src/main/java/com/steve/ai/util/RecipeHelper.java @@ -1,29 +1,295 @@ package com.steve.ai.util; +import net.minecraft.core.RegistryAccess; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import org.jetbrains.annotations.Nullable; -import java.util.List; -import java.util.Map; +import java.util.*; -// TODO: Will be implemented later - recipe lookup and crafting support -// Required for CraftItemAction to work properly -// Will integrate with Minecraft's recipe system to: -// - Check if recipes are available -// - Determine required ingredients -// - Check if crafting table is needed +/** + * Recipe lookup and crafting support utility + * Integrates with Minecraft's recipe system to provide: + * - Recipe availability checking + * - Ingredient requirement extraction + * - Crafting table requirement detection + * - Recipe type identification + */ public class RecipeHelper { - - public static boolean hasRecipe(Item item) { + + /** + * Check if a crafting recipe exists for a given item + * @param level The server level (needed for RecipeManager) + * @param item The item to check + * @return true if at least one recipe produces this item + */ + public static boolean hasRecipe(ServerLevel level, Item item) { + RecipeManager recipeManager = level.getRecipeManager(); + + // Get all crafting recipes + Collection> recipes = + recipeManager.getAllRecipesFor(RecipeType.CRAFTING); + + // Check if any recipe produces the target item + for (RecipeHolder holder : recipes) { + CraftingRecipe recipe = holder.value(); + ItemStack result = recipe.getResultItem(level.registryAccess()); + + if (!result.isEmpty() && result.getItem() == item) { + return true; + } + } + return false; } - - public static Map getRequiredIngredients(Item item) { - return Map.of(); + + /** + * Find a crafting recipe for a given item + * @param level The server level + * @param item The target item + * @return RecipeHolder containing the recipe, or null if not found + */ + @Nullable + public static RecipeHolder findRecipe(ServerLevel level, Item item) { + RecipeManager recipeManager = level.getRecipeManager(); + Collection> recipes = + recipeManager.getAllRecipesFor(RecipeType.CRAFTING); + + for (RecipeHolder holder : recipes) { + CraftingRecipe recipe = holder.value(); + ItemStack result = recipe.getResultItem(level.registryAccess()); + + if (!result.isEmpty() && result.getItem() == item) { + return holder; + } + } + + return null; } - - public static boolean requiresCraftingTable(Item item) { - return false; + + /** + * Get required ingredients for crafting an item + * Returns a map of Item -> required count + * @param level The server level + * @param item The item to craft + * @return Map of ingredients, empty if no recipe found + */ + public static Map getRequiredIngredients(ServerLevel level, Item item) { + RecipeHolder holder = findRecipe(level, item); + if (holder == null) { + return Map.of(); + } + + CraftingRecipe recipe = holder.value(); + Map ingredients = new HashMap<>(); + + // Extract ingredients from the recipe + for (Ingredient ingredient : recipe.getIngredients()) { + if (ingredient.isEmpty()) continue; + + // Get the first matching item stack (recipes can have alternatives) + ItemStack[] stacks = ingredient.getItems(); + if (stacks.length > 0) { + ItemStack stack = stacks[0]; + Item ingredientItem = stack.getItem(); + + // Accumulate count + ingredients.put(ingredientItem, ingredients.getOrDefault(ingredientItem, 0) + stack.getCount()); + } + } + + return ingredients; + } + + /** + * Check if a recipe requires a crafting table (3x3 grid) + * Returns false for 2x2 recipes that can be done in player inventory + * @param level The server level + * @param item The item to check + * @return true if crafting table required + */ + public static boolean requiresCraftingTable(ServerLevel level, Item item) { + RecipeHolder holder = findRecipe(level, item); + if (holder == null) { + return false; // No recipe = no crafting table needed + } + + CraftingRecipe recipe = holder.value(); + + // Check if recipe is a ShapedRecipe (has dimensions) + if (recipe instanceof ShapedRecipe shapedRecipe) { + int width = shapedRecipe.getWidth(); + int height = shapedRecipe.getHeight(); + + // If width or height > 2, requires 3x3 crafting table + return width > 2 || height > 2; + } + + // Shapeless recipes and special recipes + // Count total number of ingredients + List ingredients = recipe.getIngredients(); + int totalIngredients = 0; + + for (Ingredient ingredient : ingredients) { + if (!ingredient.isEmpty()) { + totalIngredients++; + } + } + + // If more than 4 ingredients, requires 3x3 grid + return totalIngredients > 4; + } + + /** + * Get all items required for a crafting chain + * For example, to craft a diamond pickaxe, you need diamonds AND sticks + * This recursively finds all base materials + * @param level The server level + * @param item The final item to craft + * @param depth Maximum recursion depth (prevents infinite loops) + * @return Map of all required base materials + */ + public static Map getFullCraftingChain(ServerLevel level, Item item, int depth) { + if (depth <= 0) { + return Map.of(); + } + + Map totalRequirements = new HashMap<>(); + Map directIngredients = getRequiredIngredients(level, item); + + if (directIngredients.isEmpty()) { + // No recipe - this is a base material + totalRequirements.put(item, 1); + return totalRequirements; + } + + // For each ingredient, check if it also has a recipe + for (Map.Entry entry : directIngredients.entrySet()) { + Item ingredient = entry.getKey(); + int count = entry.getValue(); + + if (hasRecipe(level, ingredient) && depth > 1) { + // Ingredient can be crafted - recurse + Map subRequirements = getFullCraftingChain(level, ingredient, depth - 1); + for (Map.Entry sub : subRequirements.entrySet()) { + totalRequirements.put( + sub.getKey(), + totalRequirements.getOrDefault(sub.getKey(), 0) + (sub.getValue() * count) + ); + } + } else { + // Base material + totalRequirements.put( + ingredient, + totalRequirements.getOrDefault(ingredient, 0) + count + ); + } + } + + return totalRequirements; + } + + /** + * Get the result count for a recipe + * Some recipes produce multiple items (e.g., 1 iron block -> 9 iron ingots) + * @param level The server level + * @param item The item to check + * @return Number of items produced, or 0 if no recipe + */ + public static int getRecipeResultCount(ServerLevel level, Item item) { + RecipeHolder holder = findRecipe(level, item); + if (holder == null) { + return 0; + } + + CraftingRecipe recipe = holder.value(); + ItemStack result = recipe.getResultItem(level.registryAccess()); + return result.getCount(); + } + + /** + * Check if Steve has all required ingredients for a recipe + * @param level The server level + * @param steve The Steve entity (via InventoryHelper) + * @param item The item to craft + * @return true if all ingredients available + */ + public static boolean hasAllIngredients(ServerLevel level, com.steve.ai.entity.SteveEntity steve, Item item) { + Map required = getRequiredIngredients(level, item); + + for (Map.Entry entry : required.entrySet()) { + if (!InventoryHelper.hasItem(steve, entry.getKey(), entry.getValue())) { + return false; + } + } + + return true; + } + + /** + * Get missing ingredients for a recipe + * Returns a map of Item -> count still needed + * @param level The server level + * @param steve The Steve entity + * @param item The item to craft + * @return Map of missing ingredients + */ + public static Map getMissingIngredients( + ServerLevel level, + com.steve.ai.entity.SteveEntity steve, + Item item + ) { + Map required = getRequiredIngredients(level, item); + Map missing = new HashMap<>(); + + for (Map.Entry entry : required.entrySet()) { + Item ingredient = entry.getKey(); + int needed = entry.getValue(); + int have = InventoryHelper.getItemCount(steve, ingredient); + + if (have < needed) { + missing.put(ingredient, needed - have); + } + } + + return missing; + } + + /** + * Check if a recipe is a shaped recipe (has specific pattern) + * @param level The server level + * @param item The item to check + * @return true if recipe is shaped + */ + public static boolean isShapedRecipe(ServerLevel level, Item item) { + RecipeHolder holder = findRecipe(level, item); + if (holder == null) { + return false; + } + + return holder.value() instanceof ShapedRecipe; + } + + /** + * Get recipe dimensions for display/debugging + * @param level The server level + * @param item The item to check + * @return String like "3x3" or "shapeless" + */ + public static String getRecipeDimensions(ServerLevel level, Item item) { + RecipeHolder holder = findRecipe(level, item); + if (holder == null) { + return "none"; + } + + CraftingRecipe recipe = holder.value(); + if (recipe instanceof ShapedRecipe shapedRecipe) { + return shapedRecipe.getWidth() + "x" + shapedRecipe.getHeight(); + } + + return "shapeless"; } } diff --git a/src/main/java/com/steve/ai/util/VectorMath.java b/src/main/java/com/steve/ai/util/VectorMath.java new file mode 100644 index 0000000..06a354a --- /dev/null +++ b/src/main/java/com/steve/ai/util/VectorMath.java @@ -0,0 +1,209 @@ +package com.steve.ai.util; + +/** + * Vector mathematics utilities for embedding operations + * Provides cosine similarity, normalization, and other vector operations + */ +public class VectorMath { + /** + * Calculate cosine similarity between two vectors + * Returns value between -1 and 1, where 1 means identical vectors + * Assumes vectors are already normalized + * @param a First vector + * @param b Second vector + * @return Cosine similarity score + */ + public static float cosineSimilarity(float[] a, float[] b) { + if (a.length != b.length) { + throw new IllegalArgumentException("Vectors must have same dimensions"); + } + + float dotProduct = 0; + for (int i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + } + + return dotProduct; + } + + /** + * Calculate cosine similarity with normalization + * Use this if vectors are not already normalized + * @param a First vector + * @param b Second vector + * @return Cosine similarity score + */ + public static float cosineSimilarityWithNormalization(float[] a, float[] b) { + if (a.length != b.length) { + throw new IllegalArgumentException("Vectors must have same dimensions"); + } + + float dotProduct = 0; + float magnitudeA = 0; + float magnitudeB = 0; + + for (int i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + magnitudeA += a[i] * a[i]; + magnitudeB += b[i] * b[i]; + } + + magnitudeA = (float) Math.sqrt(magnitudeA); + magnitudeB = (float) Math.sqrt(magnitudeB); + + if (magnitudeA == 0 || magnitudeB == 0) { + return 0; + } + + return dotProduct / (magnitudeA * magnitudeB); + } + + /** + * Normalize a vector to unit length (L2 norm) + * @param vector Input vector + * @return Normalized vector + */ + public static float[] normalize(float[] vector) { + float magnitude = 0; + for (float v : vector) { + magnitude += v * v; + } + magnitude = (float) Math.sqrt(magnitude); + + if (magnitude == 0) { + return vector.clone(); + } + + float[] normalized = new float[vector.length]; + for (int i = 0; i < vector.length; i++) { + normalized[i] = vector[i] / magnitude; + } + + return normalized; + } + + /** + * Calculate magnitude (L2 norm) of a vector + * @param vector Input vector + * @return Magnitude + */ + public static float magnitude(float[] vector) { + float sum = 0; + for (float v : vector) { + sum += v * v; + } + return (float) Math.sqrt(sum); + } + + /** + * Calculate dot product of two vectors + * @param a First vector + * @param b Second vector + * @return Dot product + */ + public static float dotProduct(float[] a, float[] b) { + if (a.length != b.length) { + throw new IllegalArgumentException("Vectors must have same dimensions"); + } + + float result = 0; + for (int i = 0; i < a.length; i++) { + result += a[i] * b[i]; + } + + return result; + } + + /** + * Calculate Euclidean distance between two vectors + * @param a First vector + * @param b Second vector + * @return Euclidean distance + */ + public static float euclideanDistance(float[] a, float[] b) { + if (a.length != b.length) { + throw new IllegalArgumentException("Vectors must have same dimensions"); + } + + float sum = 0; + for (int i = 0; i < a.length; i++) { + float diff = a[i] - b[i]; + sum += diff * diff; + } + + return (float) Math.sqrt(sum); + } + + /** + * Add two vectors element-wise + * @param a First vector + * @param b Second vector + * @return Sum vector + */ + public static float[] add(float[] a, float[] b) { + if (a.length != b.length) { + throw new IllegalArgumentException("Vectors must have same dimensions"); + } + + float[] result = new float[a.length]; + for (int i = 0; i < a.length; i++) { + result[i] = a[i] + b[i]; + } + + return result; + } + + /** + * Subtract two vectors element-wise (a - b) + * @param a First vector + * @param b Second vector + * @return Difference vector + */ + public static float[] subtract(float[] a, float[] b) { + if (a.length != b.length) { + throw new IllegalArgumentException("Vectors must have same dimensions"); + } + + float[] result = new float[a.length]; + for (int i = 0; i < a.length; i++) { + result[i] = a[i] - b[i]; + } + + return result; + } + + /** + * Multiply vector by scalar + * @param vector Input vector + * @param scalar Scalar multiplier + * @return Scaled vector + */ + public static float[] scale(float[] vector, float scalar) { + float[] result = new float[vector.length]; + for (int i = 0; i < vector.length; i++) { + result[i] = vector[i] * scalar; + } + + return result; + } + + /** + * Check if a vector is normalized (magnitude close to 1) + * @param vector Input vector + * @param epsilon Tolerance for floating point comparison + * @return True if normalized + */ + public static boolean isNormalized(float[] vector, float epsilon) { + float mag = magnitude(vector); + return Math.abs(mag - 1.0f) < epsilon; + } + + /** + * Check if a vector is normalized with default epsilon (0.001) + * @param vector Input vector + * @return True if normalized + */ + public static boolean isNormalized(float[] vector) { + return isNormalized(vector, 0.001f); + } +}