From 082b532d52fe05fd40e31399a32cab7e45bfea28 Mon Sep 17 00:00:00 2001 From: DoarMihai Date: Sat, 4 Jul 2026 17:09:55 +0300 Subject: [PATCH] a bit of progress --- GAME_ANALYSIS.md | 1116 +++++++++++++++++ .../Commands/Game/ProcessCooldowns.php | 41 + .../Controllers/Game/AttackController.php | 330 +++++ .../Controllers/Game/CharacterController.php | 41 +- app/Http/Controllers/Game/CrimeController.php | 13 +- app/Http/Controllers/Game/GymController.php | 9 +- .../Controllers/Game/InventoryController.php | 4 +- app/Http/Controllers/Game/JailController.php | 4 +- app/Http/Middleware/Game/CheckAlive.php | 45 + app/Http/Middleware/Game/CheckCooldown.php | 40 + app/Http/Requests/Game/AttackRequest.php | 41 + .../Requests/Game/AttemptCrimeRequest.php | 41 + .../Requests/Game/CharacterDepositRequest.php | 43 + .../Game/CharacterWithdrawRequest.php | 43 + app/Http/Requests/Game/TrainRequest.php | 42 + app/Models/Attack.php | 136 ++ app/Models/Character.php | 16 + app/Models/Cooldown.php | 67 + app/Services/Game/ActivityLogService.php | 49 + app/Services/Game/CombatService.php | 301 +++++ app/Services/Game/CooldownService.php | 123 ++ app/Services/Game/CrimeService.php | 22 +- app/Services/Game/EconomyService.php | 15 +- bootstrap/app.php | 2 + config/game.php | 14 + ...2026_07_04_000001_create_attacks_table.php | 46 + ...7_04_000002_add_indexes_to_game_tables.php | 92 ++ ...26_07_04_000003_create_cooldowns_table.php | 35 + routes/game.php | 10 + 29 files changed, 2734 insertions(+), 47 deletions(-) create mode 100644 GAME_ANALYSIS.md create mode 100644 app/Console/Commands/Game/ProcessCooldowns.php create mode 100644 app/Http/Controllers/Game/AttackController.php create mode 100644 app/Http/Middleware/Game/CheckAlive.php create mode 100644 app/Http/Middleware/Game/CheckCooldown.php create mode 100644 app/Http/Requests/Game/AttackRequest.php create mode 100644 app/Http/Requests/Game/AttemptCrimeRequest.php create mode 100644 app/Http/Requests/Game/CharacterDepositRequest.php create mode 100644 app/Http/Requests/Game/CharacterWithdrawRequest.php create mode 100644 app/Http/Requests/Game/TrainRequest.php create mode 100644 app/Models/Attack.php create mode 100644 app/Models/Cooldown.php create mode 100644 app/Services/Game/ActivityLogService.php create mode 100644 app/Services/Game/CombatService.php create mode 100644 app/Services/Game/CooldownService.php create mode 100644 database/migrations/2026_07_04_000001_create_attacks_table.php create mode 100644 database/migrations/2026_07_04_000002_add_indexes_to_game_tables.php create mode 100644 database/migrations/2026_07_04_000003_create_cooldowns_table.php diff --git a/GAME_ANALYSIS.md b/GAME_ANALYSIS.md new file mode 100644 index 0000000..3628b6a --- /dev/null +++ b/GAME_ANALYSIS.md @@ -0,0 +1,1116 @@ +# The Crime Syndicate - Code Analysis & Improvement Plan + +**Generated:** 2026-07-04 +**Project:** Laravel-based Omerta/Torn City-style browser game +**Current Status:** Phase 1 mostly complete, Phase 2 partially started + +--- + +## 📊 Current Implementation Overview + +### ✅ What's Already Implemented + +#### Core Systems +- [x] **User Authentication** - Laravel Breeze setup with registration, login, password reset +- [x] **Character System** - Full character model with stats (strength, defense, speed, dexterity) +- [x] **Energy System** - Regeneration, costs, service layer +- [x] **Health System** - Damage, healing, hospital mechanics +- [x] **Nerve System** - Separate resource for crimes, midnight refill +- [x] **Money & Bank** - Cash, bank deposits/withdrawals, daily interest +- [x] **Experience & Leveling** - EXP curve, level up rewards, stat increases + +#### Gameplay Features +- [x] **Crime System** - Crimes with difficulties, success rates, rewards, cooldowns +- [x] **Inventory System** - Items, equipment, consumables with equip/use functionality +- [x] **Gym/Training** - Stat training with daily caps +- [x] **Jail System** - Sentencing, bust-out mechanic (recently added) +- [x] **Hospital System** - Death, revival with costs +- [x] **Location System** - Basic location model +- [x] **Notifications** - System notifications for game events +- [x] **Activity Logging** - Comprehensive action tracking + +#### Infrastructure +- [x] **Service Layer** - CrimeService, EconomyService, EnergyService, HealthService, NerveService, JailService, TrainingService, NotificationService +- [x] **Middleware** - VerifyCharacter, CheckEnergy, CheckInHospital, CheckInJail +- [x] **Configuration** - Comprehensive game.php config with all mechanics +- [x] **Enums** - CharacterStatus, CrimeDifficulty, ItemType, AttackResult +- [x] **Scheduled Commands** - Energy regen, health regen, nerve midnight refill +- [x] **Database** - 14+ migrations covering all core tables +- [x] **Routes** - Well-structured web.php and game.php routing + +#### Frontend +- [x] **Blade Templates** - Game views for crimes, character, dashboard, jail, hospital +- [x] **Tailwind CSS** - Styling framework +- [x] **Alpine.js** - Frontend interactivity + +--- + +## 🎯 Strengths of Current Implementation + +### 1. **Excellent Architecture** +- Clean separation of concerns with dedicated service classes +- Proper use of Laravel features (middlewares, enums, policies) +- Well-organized configuration in game.php +- Good use of model methods and accessors + +### 2. **Solid Core Mechanics** +- Energy/Nerve/Health systems are well-implemented +- Crime system with configurable difficulties and rewards +- Character progression with EXP and leveling +- Comprehensive economy with bank interest + +### 3. **Good Code Quality** +- Type hints where appropriate +- Clear method naming and responsibilities +- Proper error handling +- Good use of Laravel's ORM relationships + +### 4. **Extensible Design** +- Configuration-driven mechanics +- Service-based architecture makes adding features easy +- Middleware system allows for flexible access control +- Event logging is comprehensive + +--- + +## ⚠️ Code Improvements Needed + +### 🔴 High Priority Issues + +#### 1. **Service Injection Improvements** +**Problem:** Services are manually instantiated with `new` instead of using dependency injection. + +```php +// Current (in CrimeService.php line 20-22) +public function __construct() +{ + $this->energyService = new EnergyService(); + $this->economyService = new EconomyService(); + $this->nerveService = new NerveService(); +} +``` + +**Solution:** Use Laravel's dependency injection container: +```php +public function __construct( + EnergyService $energyService, + EconomyService $economyService, + NerveService $nerveService +) { + $this->energyService = $energyService; + $this->economyService = $economyService; + $this->nerveService = $nerveService; +} +``` + +**Files to update:** +- `app/Services/Game/CrimeService.php` +- `app/Services/Game/EconomyService.php` (remove circular dependency) +- `app/Http/Controllers/Game/CrimeController.php` +- `app/Http/Controllers/Game/CharacterController.php` +- All other controllers and services + +#### 2. **Circular Dependency Issue** +**Problem:** `EconomyService` depends on `ActivityLog` model, which may cause issues. Also, `Character` model methods are used directly by services. + +**Solution:** +- Move ActivityLog methods to a dedicated `ActivityLogService` +- Use interfaces for service dependencies +- Extract character stat modifications to a `CharacterStatService` + +#### 3. **Missing Attack/PvP System** +**Problem:** Character model has `getAttackPowerAttribute()` and `getDefenseAttribute()` but no actual PvP combat implementation. + +**Files needed:** +- `app/Models/Attack.php` (migration and model) +- `app/Services/Game/CombatService.php` +- `app/Http/Controllers/Game/AttackController.php` +- `app/Http/Middleware/Game/CheckAlive.php` +- `app/Http/Middleware/Game/CheckCooldown.php` + +#### 4. **Hardcoded Character Status Checks** +**Problem:** Status checking logic is duplicated across models and services. + +**Example:** `Character::canAct()` checks status, but middleware and services repeat this logic. + +**Solution:** Centralize status checking in a `CharacterStatusService` or use the existing enum methods consistently. + +#### 5. **Missing Cooldown System** +**Problem:** Crime cooldown is implemented in CrimeService, but there's no generic cooldown system for other actions. + +**Solution:** Create a `CooldownService` that handles: +- Attack cooldowns +- Travel cooldowns +- Crime cooldowns +- Training cooldowns + +#### 6. **Inconsistent Money Handling** +**Problem:** Some methods use `addMoney()` / `removeMoney()` directly on Character, others use EconomyService. + +**Solution:** Always use EconomyService for money operations to ensure proper logging and validation. + +### 🟡 Medium Priority Improvements + +#### 7. **Missing Indexes on Database Tables** +**Problem:** Database migrations don't include proper indexes for performance. + +**Tables needing indexes:** +```php +// characters table +$table->index('user_id'); +$table->index('location_id'); +$table->index('faction_id'); +$table->index('status'); +$table->index('level'); + +// crime_attempts table +$table->index('character_id'); +$table->index('crime_id'); +$table->index('attempted_at'); +$table->index('success'); + +// inventories table +$table->index('character_id'); +$table->index('item_id'); +$table->index('equipped'); +``` + +#### 8. **No Request Validation** +**Problem:** Controllers use manual validation instead of Form Request classes. + +**Solution:** Create Form Request classes: +- `app/Http/Requests/Game/AttemptCrimeRequest.php` +- `app/Http/Requests/Game/CharacterDepositRequest.php` +- `app/Http/Requests/Game/CharacterWithdrawRequest.php` +- And others for all user input + +#### 9. **Missing Rate Limiting** +**Problem:** No rate limiting on game actions to prevent abuse. + +**Solution:** +- Use Laravel's `throttle` middleware +- Create custom `RateLimitActions` middleware +- Add to config/game.php: +```php +'rate_limits' => [ + 'crime_attempts' => 60, // per minute + 'attacks' => 10, // per minute + 'messages' => 20, // per minute +], +``` + +#### 10. **Hardcoded Values** +**Problem:** Some values are hardcoded in services instead of using config. + +**Examples:** +- `CrimeService.php` line 69: `0.10` should be `config('game.crime.failure_penalty')` +- Various other magic numbers in services + +**Solution:** Move all hardcoded values to `config/game.php` + +#### 11. **No API Rate Limiting** +**Problem:** If/when API is added, there's no rate limiting infrastructure. + +**Solution:** Use Laravel Sanctum with rate limiting. + +#### 12. **Missing Tests** +**Problem:** No unit or feature tests for game logic. + +**Solution:** Create comprehensive test suite: +- `tests/Feature/Game/CrimeTest.php` +- `tests/Feature/Game/CharacterTest.php` +- `tests/Feature/Game/CombatTest.php` +- `tests/Unit/Services/CrimeServiceTest.php` +- etc. + +### 🟢 Low Priority Improvements + +#### 13. **Code Style Consistency** +**Issues:** +- Mixed use of snake_case and camelCase in config keys +- Inconsistent method naming (sometimes `getX`, sometimes `fetchX`) +- Some methods are too long (> 20 lines) + +**Solution:** +- Run `php artisan pint` to auto-format +- Create code style guidelines +- Use consistent naming conventions + +#### 14. **Missing DocBlocks** +**Problem:** Some methods lack proper PHPDoc comments. + +**Solution:** Add comprehensive DocBlocks for all public methods. + +#### 15. **Error Handling** +**Problem:** Some services throw generic `Exception` instead of custom exceptions. + +**Solution:** Create custom exceptions: +- `app/Exceptions/GameException.php` (base) +- `app/Exceptions/NotEnoughEnergyException.php` +- `app/Exceptions/CharacterCannotActException.php` +- `app/Exceptions/CooldownException.php` +- etc. + +#### 16. **Logging Improvements** +**Problem:** Inconsistent logging approach. + +**Solution:** +- Use Laravel's logging channels +- Create a `GameLogger` service +- Add structured logging for analytics + +--- + +## 🚀 Missing Features (Prioritized) + +### 🔴 Phase 1 - Critical (Should be implemented first) + +#### 1. **Attack/PvP Combat System** +**Priority:** ⭐⭐⭐⭐⭐ +**Estimated Time:** 2-3 days + +**Components needed:** +- `Attack` model and migration +- `CombatService` with damage calculation +- `AttackController` with attack logic +- Hit list system +- Revenge attack tracking +- Hospital/send to hospital on death +- Jail sentence on attack loss +- Attack cooldowns + +**Mechanics:** +- Initiative based on speed +- Damage = (strength * 0.5 + weapon_bonus) * (0.8 + random(0, 0.4)) +- Critical chance based on dexterity +- Defense reduction based on defense stat +- Money reward for winning (steal % of victim's cash) +- EXP reward for winning/losing + +#### 2. **Faction System** +**Priority:** ⭐⭐⭐⭐⭐ +**Estimated Time:** 3-5 days + +**Components needed:** +- `Faction` model and migration +- `FactionMember` pivot model +- `FactionRank` model +- `FactionChat` model +- `FactionService` +- `FactionController` +- Faction bank with tax system +- Respect system +- Faction creation requirements (level 10, $10,000) + +**Mechanics:** +- Create/join/leave factions +- Faction ranks with permissions +- Faction bank with daily interest +- Faction chat +- Faction crimes (organised crimes) + +#### 3. **Mail/Message System** +**Priority:** ⭐⭐⭐⭐ +**Estimated Time:** 1-2 days + +**Components needed:** +- `Mail` model and migration +- `MailController` +- Inbox view +- Compose message view +- Read message view +- Delete message functionality +- Unread message count in notification bar + +**Fields:** +- sender_id, recipient_id, subject, body, read_at, is_system + +#### 4. **Shop System** +**Priority:** ⭐⭐⭐⭐ +**Estimated Time:** 1-2 days + +**Components needed:** +- `Shop` model and migration +- `ShopItem` pivot model +- `ShopController` +- Shop view with item listings +- Purchase functionality +- Shop restocking (scheduled command) + +**Mechanics:** +- Multiple shops per location +- Different items in different locations +- Restock on schedule +- Dynamic pricing + +### 🟡 Phase 2 - Important + +#### 5. **Market/Player Trading** +**Priority:** ⭐⭐⭐ +**Estimated Time:** 2-3 days + +**Components needed:** +- `MarketListing` model and migration +- `MarketService` +- `MarketController` +- Market listing view +- Purchase from market view +- My listings view + +**Mechanics:** +- Players can list items for sale +- Buyers can purchase items +- Market fee (5-10%) +- Listing duration (24-48 hours) +- Cancel listing + +#### 6. **Travel System** +**Priority:** ⭐⭐⭐ +**Estimated Time:** 1-2 days + +**Components needed:** +- Expand `Location` model +- `TravelService` +- Travel view with map +- Travel cooldowns + +**Mechanics:** +- Travel between locations costs energy and money +- Different locations have different items/crimes/shops +- Travel cooldown based on distance +- Location level requirements + +#### 7. **Item Crafting** +**Priority:** ⭐⭐⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `Recipe` model +- `CraftingService` +- `CraftingController` +- Crafting view + +**Mechanics:** +- Combine items to create new items +- Recipes with requirements +- Crafting skill/level requirements +- Success rates + +#### 8. **Casino/Gambling** +**Priority:** ⭐⭐⭐ +**Estimated Time:** 2-3 days + +**Components needed:** +- `CasinoGame` model +- `Bet` model +- `CasinoService` +- `CasinoController` + +**Games to implement:** +- Slots +- Roulette +- Blackjack +- Dice +- Lottery + +**Mechanics:** +- House always has slight edge +- Bet amounts +- Win/lose notifications +- Gambling addiction warnings + +#### 9. **Daily Login Bonus** +**Priority:** ⭐⭐⭐ +**Estimated Time:** 1 day + +**Components needed:** +- Add `last_login_at` to characters table +- Add `login_streak` to characters table +- `DailyBonusService` +- Scheduled command for daily reset +- Login bonus view/notification + +**Mechanics:** +- Reward for first login of the day +- Streak bonuses (3 day, 7 day, 30 day) +- Reset on missed day + +#### 10. **Achievements/Merits System** +**Priority:** ⭐⭐⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `Achievement` model +- `CharacterAchievement` pivot +- `AchievementService` +- Achievement progress tracking + +**Mechanics:** +- Unlock achievements for milestones +- Permanent stat bonuses +- Achievement display on profile +- Notification on unlock + +### 🟢 Phase 3 - Nice to Have + +#### 11. **Faction Wars** +**Priority:** ⭐⭐ +**Estimated Time:** 3-4 days + +**Components needed:** +- `FactionWar` model +- `WarAttack` model +- `WarService` +- `WarController` + +**Mechanics:** +- Declare war (costs respect) +- War duration (7 days) +- Attack enemy faction members +- Respect gains/losses +- War victory conditions +- War rewards (bank loot) + +#### 12. **Territory System** +**Priority:** ⭐⭐ +**Estimated Time:** 3 days + +**Components needed:** +- `Territory` model +- `TerritoryControl` model +- `TerritoryService` + +**Mechanics:** +- Factions control territories +- Territory provides bonuses +- Territory can be contested +- Territory income +- Territory defense bonuses + +#### 13. **Missions/Quests** +**Priority:** ⭐⭐ +**Estimated Time:** 2-3 days + +**Components needed:** +- `Mission` model +- `MissionStep` model +- `CharacterMission` pivot +- `MissionService` + +**Mechanics:** +- Storyline missions +- Daily missions +- Mission steps +- Mission rewards (money, EXP, items) + +#### 14. **Stock Market** +**Priority:** ⭐⭐ +**Estimated Time:** 3 days + +**Components needed:** +- `Stock` model +- `Portfolio` model +- `StockHistory` model +- `StockService` + +**Mechanics:** +- Buy/sell stocks +- Price fluctuations +- Dividends +- Company information + +#### 15. **Properties** +**Priority:** ⭐⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `Property` model +- `CharacterProperty` pivot +- `PropertyService` + +**Mechanics:** +- Buy/sell properties +- Property income (hourly/daily) +- Property upgrades +- Property types (houses, businesses) + +#### 16. **Vehicles** +**Priority:** ⭐⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `Vehicle` model +- `CharacterVehicle` pivot +- `VehicleService` + +**Mechanics:** +- Buy/sell vehicles +- Vehicle travel speed bonuses +- Vehicle fuel system +- Vehicle combat bonuses + +#### 17. **Bounties/Hit Contracts** +**Priority:** ⭐⭐ +**Estimated Time:** 1-2 days + +**Components needed:** +- `Bounty` model +- `BountyService` +- `BountyController` + +**Mechanics:** +- Place bounty on player +- Bounty amount +- Claim bounty by attacking target +- Bounty expiration +- Bounty leaderboard + +#### 18. **Marriage/Partnerships** +**Priority:** ⭐ +**Estimated Time:** 1 day + +**Components needed:** +- `Marriage` model +- `MarriageService` + +**Mechanics:** +- Propose marriage +- Accept/reject proposals +- Marriage bonuses +- Divorce system + +### 🔵 Phase 4 - Advanced Features + +#### 19. **Drug System** +**Priority:** ⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `Drug` model (or use Item type) +- `DrugEffect` model +- `DrugService` + +**Mechanics:** +- Temporary stat boosts +- Addiction system +- Overdose penalties +- Withdrawal effects + +#### 20. **Loan System** +**Priority:** ⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `Loan` model +- `LoanService` + +**Mechanics:** +- Borrow money +- Interest rates +- Repayment schedules +- Late fees +- Default penalties + +#### 21. **Education System** +**Priority:** ⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `Course` model +- `Enrollment` model +- `EducationService` + +**Mechanics:** +- Enroll in courses +- Course duration (real time) +- Permanent stat boosts +- Course requirements + +#### 22. **Job System** +**Priority:** ⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `Job` model +- `JobApplication` model +- `JobService` + +**Mechanics:** +- Apply for jobs +- Daily pay +- Skill XP +- Job requirements + +#### 23. **Real-time Chat** +**Priority:** ⭐ +**Estimated Time:** 2-3 days + +**Components needed:** +- Laravel Echo setup +- Pusher/Ably integration +- `ChatMessage` model +- `ChatService` +- Frontend Vue/React components + +**Mechanics:** +- Global chat +- Faction chat +- Private messages +- Chat moderation + +#### 24. **Leaderboards** +**Priority:** ⭐ +**Estimated Time:** 1-2 days + +**Components needed:** +- `Leaderboard` model +- `LeaderboardService` +- Scheduled command for updates +- Leaderboard views + +**Categories:** +- Level +- Money +- Respect +- Crimes committed +- Attacks won +- Faction rank + +#### 25. **Events System** +**Priority:** ⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `GameEvent` model +- `EventService` + +**Mechanics:** +- Holiday events +- Double XP weekends +- Special crimes +- Event rewards + +#### 26. **Referral System** +**Priority:** ⭐ +**Estimated Time:** 1 day + +**Components needed:** +- `Referral` model +- `ReferralService` + +**Mechanics:** +- Referral codes +- Referral rewards +- Referral tracking + +#### 27. **Admin Panel** +**Priority:** ⭐⭐ +**Estimated Time:** 3-5 days + +**Components needed:** +- Nova or custom admin +- User management +- Item management +- Crime management +- Game settings +- Moderation tools + +**Features:** +- Ban/mute users +- Edit items/crimes +- Game configuration +- Player support + +#### 28. **Moderation Tools** +**Priority:** ⭐⭐ +**Estimated Time:** 2 days + +**Components needed:** +- `ModerationLog` model +- `ModerationService` + +**Features:** +- Warn users +- Temporary bans +- Permanent bans +- Mute chat +- Freeze accounts + +#### 29. **API for Mobile** +**Priority:** ⭐ +**Estimated Time:** 3-5 days + +**Components needed:** +- Sanctum setup +- API routes +- API resources +- Authentication + +#### 30. **Web Push Notifications** +**Priority:** ⭐ +**Estimated Time:** 2 days + +**Components needed:** +- Service worker +- Push notification service +- User subscription management + +**Notifications for:** +- Energy full +- Cooldown expired +- Jail release +- Hospital release +- New messages +- Attacked by someone + +--- + +## 📋 Implementation Roadmap + +### Week 1-2: Core Gameplay Completion +1. **Day 1-2:** Implement Attack/PvP system +2. **Day 3-5:** Implement Faction system +3. **Day 6:** Implement Mail system +4. **Day 7:** Implement Shop system + +### Week 3-4: Economy & Social +5. **Day 8-9:** Implement Market/Player trading +6. **Day 10:** Implement Travel system +7. **Day 11-12:** Implement Casino/gambling +8. **Day 13:** Implement Daily login bonus +9. **Day 14:** Implement Achievements system + +### Week 5-6: Advanced Features +10. **Day 15-16:** Implement Faction Wars +11. **Day 17-18:** Implement Territory system +12. **Day 19-20:** Implement Missions/Quests +13. **Day 21:** Implement Stock Market +14. **Day 22:** Implement Properties +15. **Day 23:** Implement Vehicles + +### Week 7-8: Polish & Scale +16. **Day 24-25:** Implement Bounties +17. **Day 26:** Implement Marriage +18. **Day 27-28:** Implement Admin Panel +19. **Day 29:** Implement Moderation Tools +20. **Day 30:** Testing and bug fixes + +### Week 9+: Optional Features +- Drug system +- Loan system +- Education system +- Job system +- Real-time chat +- Leaderboards +- Events system +- Referral system +- API for Mobile +- Web Push Notifications + +--- + +## 🎯 Quick Wins (Can be implemented in < 1 day) + +Based on the existing codebase, these features can be implemented quickly: + +1. **Daily Login Bonus** - Just needs `last_login_at` column and a check on login +2. **Merit/Achievement Badges** - Use existing activity log data +3. **Gambling Mini-game** - Simple dice roll using existing money logic +4. **Player Profile Pages** - Read-only view of character stats +5. **Consumable Items with Effects** - Extend existing Item model with effect fields +6. **Friends/Enemy List** - Simple relationship table +7. **Cooldown Notifications** - Use existing notification system +8. **Item Categories** - Extend ItemType enum + +--- + +## 🛠️ Technical Debt Priority + +### Immediate (Fix before adding new features) +1. **Service Dependency Injection** - Fix the `new Service()` pattern +2. **Circular Dependencies** - Resolve EconomyService dependencies +3. **Attack System** - Implement basic PvP +4. **Cooldown System** - Create generic cooldown service +5. **Request Validation** - Create Form Request classes + +### Short-term (Fix within next 2 weeks) +6. **Database Indexes** - Add missing indexes +7. **Rate Limiting** - Implement action rate limiting +8. **Hardcoded Values** - Move to config +9. **Custom Exceptions** - Better error handling +10. **Tests** - Add unit/feature tests + +### Long-term (Fix before production) +11. **Code Style** - Consistent formatting +12. **DocBlocks** - Complete documentation +13. **Logging** - Structured logging +14. **Caching** - Implement caching for leaderboards, etc. +15. **Performance Optimization** - Query optimization + +--- + +## 📊 Feature Comparison with Competitors + +### What Torn City Has That We Need +- [ ] Attacking other players +- [ ] Mugging +- [ ] Bounties/Hit contracts +- [ ] Revenge attacks +- [ ] War mode/Gang wars +- [ ] Escape from jail +- [ ] Item market/P2P trading +- [ ] Stock market +- [ ] Businesses +- [ ] Drug mechanics +- [ ] Loan sharks +- [ ] Casino/Gambling +- [ ] Points/Premium currency +- [ ] Gangs/Factions/Families +- [ ] Gang crimes +- [ ] Messaging system +- [ ] Forums/Bulletin boards +- [ ] Friends list/Enemy list +- [ ] Profile pages +- [ ] Marriage/Partnerships +- [ ] Nerve/Action points +- [ ] Gym/Training +- [ ] Education system +- [ ] Job/Working +- [ ] Merit badges/Achievements +- [ ] Rank/Prestige system +- [ ] Happy stat +- [ ] Travel/City system +- [ ] Item crafting +- [ ] Temporary consumables +- [ ] Weapon/armor enhancement +- [ ] Car/Vehicle system +- [ ] Passive income timers +- [ ] Daily login bonus +- [ ] Cooldown notifications +- [ ] Events/Seasonal content + +### What We Have That's Better +- [x] **Modern Tech Stack** - Laravel 12, Tailwind CSS, Alpine.js +- [x] **Clean Architecture** - Well-organized services and models +- [x] **Transparent Probabilities** - Show success rates (can be extended) +- [x] **Configurable Mechanics** - Everything in config/game.php +- [x] **Comprehensive Logging** - ActivityLog tracks everything +- [x] **Type Safety** - PHP 8.3 with proper typing +- [x] **Enum Support** - Proper enum usage for game states + +--- + +## 🎨 UI/UX Improvements + +### Immediate +1. **Add cooldown timers** on action buttons +2. **Show success probabilities** for crimes +3. **Add progress bars** for EXP, energy, health, nerve +4. **Improve mobile responsiveness** +5. **Add keyboard shortcuts** for common actions + +### Short-term +6. **Dark mode** toggle +7. **Sound effects** for actions +8. **Animations** for combat, level ups, etc. +9. **Tooltips** for game mechanics explanation +10. **Tutorial** for new players + +### Long-term +11. **Custom themes** +12. **Avatar system** +13. **Map visualization** for travel +14. **Inventory sorting/filtering** +15. **Quick action buttons** on dashboard + +--- + +## 🔧 Configuration Improvements + +### Add to config/game.php + +```php +// Attack system +'attack' => [ + 'base_damage' => 5, + 'damage_variation' => 0.20, + 'crit_chance' => 0.10, + 'crit_multiplier' => 2.0, + 'defense_reduction' => 0.5, + 'min_damage' => 1, + 'attack_cooldown' => 300, // 5 minutes + 'hit_list_cost' => 5000, + 'mug_chance' => 0.30, // 30% chance to mug instead of hospitalize + 'mug_percent' => 0.20, // Steal 20% of victim's cash +], + +// Faction system +'factions' => [ + 'create_cost' => 10000, + 'create_level' => 10, + 'max_members' => 50, + 'respect_per_win' => 10, + 'respect_per_loss' => -5, + 'bank_tax' => 0.05, + 'war_cost' => 5000, // Respect cost to declare war + 'war_duration' => 604800, // 7 days in seconds + 'max_ranks' => 10, +], + +// Travel system +'travel' => [ + 'base_cost' => 100, + 'cost_per_level' => 10, + 'energy_cost' => 5, + 'cooldown_base' => 60, // 1 minute + 'cooldown_per_level' => 30, // +30 seconds per level difference +], + +// Rate limits +'rate_limits' => [ + 'crime_attempts' => 60, // per minute + 'attacks' => 10, // per minute + 'messages' => 20, // per minute + 'market_listings' => 10, // per minute +], + +// Daily bonuses +'daily' => [ + 'login_reward' => 1000, // Money + 'login_exp' => 50, // EXP + 'streak_3_reward' => 5000, + 'streak_7_reward' => 15000, + 'streak_30_reward' => 50000, +], + +// Casino +'casino' => [ + 'house_edge' => 0.05, // 5% + 'min_bet' => 100, + 'max_bet' => 100000, +], +``` + +--- + +## 📈 Performance Considerations + +### Database Optimization +1. **Add indexes** to all foreign key columns +2. **Cache frequently accessed data**: + - Character stats (1 minute cache) + - Leaderboards (5-10 minute cache) + - Shop items (1 hour cache) +3. **Use eager loading** for relationships in controllers +4. **Paginate** all lists (crimes, inventory, etc.) +5. **Optimize queries** with proper selects and where clauses + +### Queue Optimization +1. **Separate queues** for different job types: + - High priority: energy regen, health regen + - Medium priority: notifications, logging + - Low priority: daily interest, cleanup +2. **Use database queue** for development, Redis for production +3. **Monitor queue performance** with Horizon + +### Caching Strategy +1. **Redis** for session and cache +2. **Cache tags** for invalidation +3. **Cache warmup** on deploy + +--- + +## 🔒 Security Considerations + +### Immediate +1. **Rate limiting** on all game actions +2. **Input validation** for all user input +3. **CSRF protection** (Laravel default) +4. **SQL injection prevention** (Eloquent ORM) +5. **XSS protection** (Blade escaping) + +### Short-term +6. **Two-factor authentication** for admin accounts +7. **Password complexity requirements** +8. **Account lockout** after failed login attempts +9. **IP tracking** for moderation +10. **Session security** settings + +### Long-term +11. **HTTPS enforcement** +12. **Security headers** (CSP, X-Frame-Options, etc.) +13. **Regular backups** +14. **Audit logging** for admin actions +15. **Fail2Ban** for brute force protection + +--- + +## 🎓 Learning Resources + +### Laravel +- [Laravel Documentation](https://laravel.com/docs) +- [Laravel News](https://laravel-news.com/) +- [Laracasts](https://laracasts.com/) + +### Game Development +- [Text-Based Game Design](https://gamedev.net/forums/forum/12-gamedesign/) +- [MUD/MMO Architecture](https://www.mudconnect.net/) + +### Competitor Analysis +- [Torn City](https://www.torn.com/) +- [Omerta Game](https://www.omertagame.com/) + +--- + +## 📝 Next Steps + +### For the Development Team + +1. **Review this document** and prioritize based on your needs +2. **Start with Quick Wins** - Implement features that can be done in < 1 day +3. **Fix Technical Debt** - Especially service injection and circular dependencies +4. **Implement Core Features** - Attack system, Factions, Mail, Shops +5. **Test Thoroughly** - Each feature before moving to the next +6. **Document** - Keep this document updated as you implement features + +### For the Project Lead + +1. **Set Milestones** - Use GitHub/GitLab milestones to track progress +2. **Assign Tasks** - Use issues to track individual features +3. **Code Reviews** - Ensure quality with pull requests and reviews +4. **Regular Testing** - Manual and automated testing +5. **Community Feedback** - Get player feedback on new features + +--- + +## 📊 Success Metrics + +### Player Retention +- Daily Active Users (DAU) +- Monthly Active Users (MAU) +- Average session duration +- Return rate (players coming back) + +### Engagement +- Average actions per session +- Average crimes committed per day +- Average attacks per day +- Average playtime per day + +### Economy +- Total money in circulation +- Bank balance distribution +- Market activity +- Casino profits + +### Community +- Faction count +- Average faction size +- Chat activity +- Forum posts + +--- + +**Document Version:** 1.0 +**Last Updated:** 2026-07-04 +**Next Review:** After Phase 1 completion diff --git a/app/Console/Commands/Game/ProcessCooldowns.php b/app/Console/Commands/Game/ProcessCooldowns.php new file mode 100644 index 0000000..de4c30a --- /dev/null +++ b/app/Console/Commands/Game/ProcessCooldowns.php @@ -0,0 +1,41 @@ +info('Processing cooldowns...'); + + // Delete expired cooldowns + $maxAge = now()->subHours(24); // Keep cooldowns for 24 hours max + $deleted = Cooldown::where('last_action_at', '<', $maxAge)->delete(); + + $this->info("Deleted {$deleted} expired cooldown records."); + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Game/AttackController.php b/app/Http/Controllers/Game/AttackController.php new file mode 100644 index 0000000..56cc539 --- /dev/null +++ b/app/Http/Controllers/Game/AttackController.php @@ -0,0 +1,330 @@ +combatService = $combatService; + $this->healthService = $healthService; + $this->jailService = $jailService; + $this->notificationService = $notificationService; + } + + /** + * Display the attack page with potential targets + */ + public function index(Request $request): View + { + $character = $request->user()->character; + + if (!$character) { + return redirect()->route('game.character.create'); + } + + // Check if character is in jail or hospital + if ($character->isInJail() || $character->isInHospital() || !$character->canAct()) { + return redirect()->route('game.dashboard') + ->with('error', 'You cannot attack while in jail, hospital, or unable to act.'); + } + + // Get potential targets (other characters in same location, not in jail/hospital) + $potentialTargets = Character::where('location_id', $character->location_id) + ->where('id', '!=', $character->id) + ->where('status', 'active') + ->where('health', '>', 0) + ->orderBy('level', 'desc') + ->get(); + + // Get attack cooldown + $cooldownRemaining = $this->combatService->getAttackCooldownRemaining($character); + + return view('game.attack.index', compact( + 'character', + 'potentialTargets', + 'cooldownRemaining' + )); + } + + /** + * Show attack preview for a specific target + */ + public function preview(Request $request, Character $target): View + { + $attacker = $request->user()->character; + + if (!$attacker) { + return redirect()->route('game.character.create'); + } + + // Check if can attack this target + if (!$this->combatService->canAttack($attacker, $target)) { + return redirect()->route('game.attack.index') + ->with('error', 'You cannot attack this target right now.'); + } + + // Calculate preview stats + $winProbability = $this->combatService->getWinProbability($attacker, $target); + $hitChance = $this->combatService->getHitChance($attacker, $target); + $attackerPower = $attacker->getAttackPowerAttribute(); + $defenderDefense = $target->getDefenseAttribute(); + $cooldownRemaining = $this->combatService->getAttackCooldownRemaining($attacker); + + // Check for weapon and armor + $attackerWeapon = $attacker->inventory() + ->where('equipped', true) + ->whereHas('item', function($q) { + $q->where('type', 'weapon'); + }) + ->first(); + + $defenderArmor = $target->inventory() + ->where('equipped', true) + ->whereHas('item', function($q) { + $q->where('type', 'armor'); + }) + ->first(); + + return view('game.attack.preview', compact( + 'attacker', + 'target', + 'winProbability', + 'hitChance', + 'attackerPower', + 'defenderDefense', + 'cooldownRemaining', + 'attackerWeapon', + 'defenderArmor' + )); + } + + /** + * Process an attack on another character + */ + public function attack(Request $request, Character $target): RedirectResponse + { + $attacker = $request->user()->character; + + if (!$attacker) { + return redirect()->route('game.character.create'); + } + + $isMugging = $request->has('mug') && $request->boolean('mug'); + + // Check if can attack this target + if (!$this->combatService->canAttack($attacker, $target)) { + $cooldown = $this->combatService->getAttackCooldownRemaining($attacker); + if ($cooldown > 0) { + return redirect()->route('game.attack.index') + ->with('error', "You must wait {$cooldown} seconds before attacking again."); + } + return redirect()->route('game.attack.index') + ->with('error', 'You cannot attack this target right now.'); + } + + try { + // Process the attack + $result = $this->combatService->processAttack($attacker, $target, $isMugging); + + // Record the attack + $attackRecord = AttackModel::create([ + 'attacker_id' => $attacker->id, + 'defender_id' => $target->id, + 'attacker_damage' => $result['attacker_damage'], + 'defender_damage' => $result['defender_damage'], + 'attacker_health_after' => $attacker->health, + 'defender_health_after' => $target->health, + 'result' => $result['result']->value, + 'money_stolen' => $result['money_stolen'], + 'experience_gained' => $result['experience_gained'], + 'was_critical' => $result['was_critical'], + 'was_mugging' => $result['was_mugging'], + 'attacker_weapon_id' => $attacker->inventory() + ->where('equipped', true) + ->whereHas('item', function($q) { $q->where('type', 'weapon'); }) + ->first()?->item_id, + 'defender_armor_id' => $target->inventory() + ->where('equipped', true) + ->whereHas('item', function($q) { $q->where('type', 'armor'); }) + ->first()?->item_id, + 'attacked_at' => now(), + ]); + + // Handle death consequences + if ($result['defender_died']) { + // Defender died - send to hospital + $this->healthService->sendToHospital($target); + + // If not mugging, send attacker to jail for killing + if (!$isMugging) { + $sentenceLength = config('game.jail.sentence_length', 3600); + $this->jailService->sendToJail($attacker, $sentenceLength); + } + } + + if ($result['attacker_died']) { + // Attacker died - send to hospital + $this->healthService->sendToHospital($attacker); + } + + // Send notifications + if ($result['result']->value === 'win') { + if ($isMugging) { + $this->notificationService->send($request->user(), 'mug_success', [ + 'target_name' => $target->name, + 'money_stolen' => $result['money_stolen'], + ]); + } else { + $this->notificationService->send($request->user(), 'attack_win', [ + 'target_name' => $target->name, + 'experience_gained' => $result['experience_gained'], + ]); + } + } else { + $this->notificationService->send($request->user(), 'attack_result', [ + 'target_name' => $target->name, + 'result' => $result['result']->value, + ]); + } + + // Send notification to defender + if ($target->user) { + $this->notificationService->send($target->user, 'attacked', [ + 'attacker_name' => $attacker->name, + 'result' => $result['result']->value, + 'damage_taken' => $result['attacker_damage'], + ]); + } + + $successMessage = match ($result['result']->value) { + 'win' => $isMugging ? "You mugged {$target->name} and stole $" . number_format($result['money_stolen']) . "!" : "You defeated {$target->name}!", + 'lose' => "You lost to {$target->name}!", + 'draw' => "Your attack against {$target->name} ended in a draw.", + default => "Attack result: {$result['result']->label()}", + }; + + return redirect()->route('game.attack.index') + ->with('success', $successMessage); + + } catch (\Exception $e) { + return redirect()->route('game.attack.index') + ->with('error', $e->getMessage()); + } + } + + /** + * Place a hit/bounty on a character + */ + public function placeHit(Request $request, Character $target): RedirectResponse + { + $attacker = $request->user()->character; + + if (!$attacker) { + return redirect()->route('game.character.create'); + } + + $validated = $request->validate([ + 'amount' => ['required', 'integer', 'min:1000'], + 'reason' => ['nullable', 'string', 'max:255'], + ]); + + $hitCost = config('game.attack.hit_list_cost', 5000); + + if (!$attacker->hasMoney($validated['amount'] + $hitCost)) { + return redirect()->back() + ->with('error', 'You need at least $' . number_format($validated['amount'] + $hitCost) . ' to place a hit.'); + } + + // Place the hit - TODO: Implement Hit model + $attacker->removeMoney($validated['amount'] + $hitCost); + + $this->notificationService->send($request->user(), 'hit_placed', [ + 'target_name' => $target->name, + 'amount' => $validated['amount'], + ]); + + return redirect()->route('game.attack.index') + ->with('success', "Hit placed on {$target->name} for $" . number_format($validated['amount']) . "!"); + } + + /** + * Get recent attacks for a character + */ + public function history(Request $request): View + { + $character = $request->user()->character; + + if (!$character) { + return redirect()->route('game.character.create'); + } + + // Attacks made by this character + $myAttacks = AttackModel::where('attacker_id', $character->id) + ->with(['defender', 'defender.user']) + ->latest('attacked_at') + ->paginate(20); + + // Attacks made against this character + $attacksOnMe = AttackModel::where('defender_id', $character->id) + ->with(['attacker', 'attacker.user']) + ->latest('attacked_at') + ->paginate(20); + + return view('game.attack.history', compact( + 'character', + 'myAttacks', + 'attacksOnMe' + )); + } + + /** + * Get characters who have attacked you recently (for revenge) + */ + public function revengeList(Request $request): View + { + $character = $request->user()->character; + + if (!$character) { + return redirect()->route('game.character.create'); + } + + // Get characters who attacked this character and are still alive + $revengeTargets = AttackModel::where('defender_id', $character->id) + ->where('attacker_id', '!=', $character->id) + ->whereHas('attacker', function($q) { + $q->where('health', '>', 0) + ->where('status', 'active'); + }) + ->with(['attacker', 'attacker.user']) + ->latest('attacked_at') + ->get() + ->unique('attacker_id') + ->pluck('attacker'); + + return view('game.attack.revenge', compact( + 'character', + 'revengeTargets' + )); + } +} diff --git a/app/Http/Controllers/Game/CharacterController.php b/app/Http/Controllers/Game/CharacterController.php index e4ea38e..c276f44 100644 --- a/app/Http/Controllers/Game/CharacterController.php +++ b/app/Http/Controllers/Game/CharacterController.php @@ -3,9 +3,11 @@ namespace App\Http\Controllers\Game; use App\Http\Controllers\Controller; +use App\Http\Requests\Game\CharacterDepositRequest; +use App\Http\Requests\Game\CharacterWithdrawRequest; use App\Models\Character; use App\Models\Location; -use App\Models\ActivityLog; +use App\Services\Game\ActivityLogService; use App\Services\Game\EconomyService; use App\Services\Game\EnergyService; use App\Services\Game\HealthService; @@ -21,13 +23,20 @@ class CharacterController extends Controller protected EnergyService $energyService; protected HealthService $healthService; protected NotificationService $notificationService; + protected ActivityLogService $activityLogService; - public function __construct() - { - $this->economyService = new EconomyService(); - $this->energyService = new EnergyService(); - $this->healthService = new HealthService(); - $this->notificationService = new NotificationService(); + public function __construct( + EconomyService $economyService, + EnergyService $energyService, + HealthService $healthService, + NotificationService $notificationService, + ActivityLogService $activityLogService + ) { + $this->economyService = $economyService; + $this->energyService = $energyService; + $this->healthService = $healthService; + $this->notificationService = $notificationService; + $this->activityLogService = $activityLogService; } /** @@ -85,7 +94,7 @@ class CharacterController extends Controller ]); // Log character creation - ActivityLog::logCharacterCreated($character); + $this->activityLogService->logCharacterCreated($character); return redirect()->route('game.dashboard') ->with('success', 'Character created successfully! Welcome to the game.'); @@ -144,15 +153,11 @@ class CharacterController extends Controller /** * Deposit money to bank */ - public function deposit(Request $request): RedirectResponse + public function deposit(CharacterDepositRequest $request): RedirectResponse { $character = $request->user()->character; - $validated = $request->validate([ - 'amount' => ['required', 'integer', 'min:1'], - ]); - - if ($this->economyService->depositToBank($character, $validated['amount'])) { + if ($this->economyService->depositToBank($character, $request->amount)) { return redirect()->back() ->with('success', 'Money deposited to bank successfully!'); } @@ -164,15 +169,11 @@ class CharacterController extends Controller /** * Withdraw money from bank */ - public function withdraw(Request $request): RedirectResponse + public function withdraw(CharacterWithdrawRequest $request): RedirectResponse { $character = $request->user()->character; - $validated = $request->validate([ - 'amount' => ['required', 'integer', 'min:1'], - ]); - - if ($this->economyService->withdrawFromBank($character, $validated['amount'])) { + if ($this->economyService->withdrawFromBank($character, $request->amount)) { return redirect()->back() ->with('success', 'Money withdrawn from bank successfully!'); } diff --git a/app/Http/Controllers/Game/CrimeController.php b/app/Http/Controllers/Game/CrimeController.php index a30dbf6..b911a38 100644 --- a/app/Http/Controllers/Game/CrimeController.php +++ b/app/Http/Controllers/Game/CrimeController.php @@ -18,11 +18,14 @@ class CrimeController extends Controller protected EnergyService $energyService; protected NotificationService $notificationService; - public function __construct() - { - $this->crimeService = new CrimeService(); - $this->energyService = new EnergyService(); - $this->notificationService = new NotificationService(); + public function __construct( + CrimeService $crimeService, + EnergyService $energyService, + NotificationService $notificationService + ) { + $this->crimeService = $crimeService; + $this->energyService = $energyService; + $this->notificationService = $notificationService; } /** diff --git a/app/Http/Controllers/Game/GymController.php b/app/Http/Controllers/Game/GymController.php index 9ffd1ec..b94a179 100644 --- a/app/Http/Controllers/Game/GymController.php +++ b/app/Http/Controllers/Game/GymController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Game; use App\Http\Controllers\Controller; +use App\Http\Requests\Game\TrainRequest; use App\Services\Game\TrainingService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -12,9 +13,9 @@ class GymController extends Controller { protected TrainingService $trainingService; - public function __construct() + public function __construct(TrainingService $trainingService) { - $this->trainingService = new TrainingService(); + $this->trainingService = $trainingService; } public function index(Request $request): View @@ -35,10 +36,8 @@ class GymController extends Controller return view('game.gym.index', compact('character', 'stats', 'energyCost', 'dailyCap')); } - public function train(Request $request): RedirectResponse + public function train(TrainRequest $request): RedirectResponse { - $request->validate(['stat' => 'required|in:strength,defense,speed,dexterity']); - $character = $request->user()->character; $result = $this->trainingService->train($character, $request->stat); diff --git a/app/Http/Controllers/Game/InventoryController.php b/app/Http/Controllers/Game/InventoryController.php index 8b32d92..cc0ad45 100644 --- a/app/Http/Controllers/Game/InventoryController.php +++ b/app/Http/Controllers/Game/InventoryController.php @@ -14,9 +14,9 @@ class InventoryController extends Controller { protected EconomyService $economyService; - public function __construct() + public function __construct(EconomyService $economyService) { - $this->economyService = new EconomyService(); + $this->economyService = $economyService; } /** diff --git a/app/Http/Controllers/Game/JailController.php b/app/Http/Controllers/Game/JailController.php index e2594f3..553508b 100644 --- a/app/Http/Controllers/Game/JailController.php +++ b/app/Http/Controllers/Game/JailController.php @@ -11,9 +11,9 @@ class JailController extends Controller { protected JailService $jailService; - public function __construct() + public function __construct(JailService $jailService) { - $this->jailService = new JailService(); + $this->jailService = $jailService; } /** diff --git a/app/Http/Middleware/Game/CheckAlive.php b/app/Http/Middleware/Game/CheckAlive.php new file mode 100644 index 0000000..7a40d13 --- /dev/null +++ b/app/Http/Middleware/Game/CheckAlive.php @@ -0,0 +1,45 @@ +user()) { + return redirect()->route('login'); + } + + // Check if user has a character + $character = $request->user()->character; + + if (!$character) { + return redirect()->route('game.character.create'); + } + + // Check if character is alive and can act + if (!$character->isAlive() || !$character->canAct()) { + if ($character->isInHospital()) { + return redirect()->route('game.hospital'); + } + if ($character->isDead()) { + return redirect()->route('game.hospital'); + } + return redirect()->route('game.dashboard') + ->with('error', 'Your character cannot perform actions right now.'); + } + + // Character is alive and can act, proceed with request + return $next($request); + } +} diff --git a/app/Http/Middleware/Game/CheckCooldown.php b/app/Http/Middleware/Game/CheckCooldown.php new file mode 100644 index 0000000..ae29fdb --- /dev/null +++ b/app/Http/Middleware/Game/CheckCooldown.php @@ -0,0 +1,40 @@ +user()) { + return redirect()->route('login'); + } + + // Check if user has a character + $character = $request->user()->character; + + if (!$character) { + return redirect()->route('game.character.create'); + } + + $cooldownService = app(\App\Services\Game\CooldownService::class); + + if ($cooldownService->isOnCooldown($character, $actionType)) { + $remaining = $cooldownService->getRemainingCooldown($character, $actionType); + return redirect()->back() + ->with('error', "You must wait {$remaining} seconds before performing this action again."); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/Game/AttackRequest.php b/app/Http/Requests/Game/AttackRequest.php new file mode 100644 index 0000000..f4c95d7 --- /dev/null +++ b/app/Http/Requests/Game/AttackRequest.php @@ -0,0 +1,41 @@ +|string> + */ + public function rules(): array + { + return [ + 'mug' => ['sometimes', 'boolean'], + 'hit' => ['sometimes', 'boolean'], + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'mug.boolean' => 'The mug option must be a boolean value.', + ]; + } +} diff --git a/app/Http/Requests/Game/AttemptCrimeRequest.php b/app/Http/Requests/Game/AttemptCrimeRequest.php new file mode 100644 index 0000000..7806a10 --- /dev/null +++ b/app/Http/Requests/Game/AttemptCrimeRequest.php @@ -0,0 +1,41 @@ +|string> + */ + public function rules(): array + { + return [ + 'crime_id' => ['required', 'exists:crimes,id'], + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'crime_id.required' => 'You must select a crime to attempt.', + 'crime_id.exists' => 'The selected crime does not exist.', + ]; + } +} diff --git a/app/Http/Requests/Game/CharacterDepositRequest.php b/app/Http/Requests/Game/CharacterDepositRequest.php new file mode 100644 index 0000000..2ddc328 --- /dev/null +++ b/app/Http/Requests/Game/CharacterDepositRequest.php @@ -0,0 +1,43 @@ +|string> + */ + public function rules(): array + { + return [ + 'amount' => ['required', 'integer', 'min:1', 'max:1000000'], + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'amount.required' => 'You must enter an amount to deposit.', + 'amount.integer' => 'The amount must be a whole number.', + 'amount.min' => 'The minimum deposit amount is :min.', + 'amount.max' => 'The maximum deposit amount is :max.', + ]; + } +} diff --git a/app/Http/Requests/Game/CharacterWithdrawRequest.php b/app/Http/Requests/Game/CharacterWithdrawRequest.php new file mode 100644 index 0000000..bd42576 --- /dev/null +++ b/app/Http/Requests/Game/CharacterWithdrawRequest.php @@ -0,0 +1,43 @@ +|string> + */ + public function rules(): array + { + return [ + 'amount' => ['required', 'integer', 'min:1', 'max:1000000'], + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'amount.required' => 'You must enter an amount to withdraw.', + 'amount.integer' => 'The amount must be a whole number.', + 'amount.min' => 'The minimum withdrawal amount is :min.', + 'amount.max' => 'The maximum withdrawal amount is :max.', + ]; + } +} diff --git a/app/Http/Requests/Game/TrainRequest.php b/app/Http/Requests/Game/TrainRequest.php new file mode 100644 index 0000000..abdb2dd --- /dev/null +++ b/app/Http/Requests/Game/TrainRequest.php @@ -0,0 +1,42 @@ +|string> + */ + public function rules(): array + { + return [ + 'stat' => ['required', 'string', 'in:strength,defense,speed,dexterity'], + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'stat.required' => 'You must select a stat to train.', + 'stat.string' => 'The stat must be a string.', + 'stat.in' => 'The selected stat is invalid. Valid stats are: strength, defense, speed, dexterity.', + ]; + } +} diff --git a/app/Models/Attack.php b/app/Models/Attack.php new file mode 100644 index 0000000..db92c8d --- /dev/null +++ b/app/Models/Attack.php @@ -0,0 +1,136 @@ + AttackResult::class, + 'was_critical' => 'boolean', + 'was_mugging' => 'boolean', + 'attacked_at' => 'datetime', + ]; + + /** + * Get the attacker character. + */ + public function attacker(): BelongsTo + { + return $this->belongsTo(Character::class, 'attacker_id'); + } + + /** + * Get the defender character. + */ + public function defender(): BelongsTo + { + return $this->belongsTo(Character::class, 'defender_id'); + } + + /** + * Get the weapon used by attacker. + */ + public function attackerWeapon(): BelongsTo + { + return $this->belongsTo(Item::class, 'attacker_weapon_id'); + } + + /** + * Get the armor used by defender. + */ + public function defenderArmor(): BelongsTo + { + return $this->belongsTo(Item::class, 'defender_armor_id'); + } + + /** + * Check if this was a successful attack (attacker won) + */ + public function isWin(): bool + { + return $this->result === AttackResult::WIN; + } + + /** + * Check if this was a loss (attacker lost) + */ + public function isLose(): bool + { + return $this->result === AttackResult::LOSE; + } + + /** + * Check if this was a draw + */ + public function isDraw(): bool + { + return $this->result === AttackResult::DRAW; + } + + /** + * Check if attacker escaped + */ + public function isEscape(): bool + { + return $this->result === AttackResult::ESCAPE; + } + + /** + * Check if this attack resulted in defender's death + */ + public function defenderDied(): bool + { + return $this->defender_health_after <= 0; + } + + /** + * Check if this attack resulted in attacker's death + */ + public function attackerDied(): bool + { + return $this->attacker_health_after <= 0; + } +} diff --git a/app/Models/Character.php b/app/Models/Character.php index 84ae96c..d2641b0 100644 --- a/app/Models/Character.php +++ b/app/Models/Character.php @@ -114,6 +114,22 @@ class Character extends Model return $this->hasMany(CrimeAttempt::class); } + /** + * Get the attacks made by this character. + */ + public function attacks(): HasMany + { + return $this->hasMany(Attack::class, 'attacker_id'); + } + + /** + * Get the attacks received by this character. + */ + public function attacksReceived(): HasMany + { + return $this->hasMany(Attack::class, 'defender_id'); + } + /** * Get the activity logs for the character. */ diff --git a/app/Models/Cooldown.php b/app/Models/Cooldown.php new file mode 100644 index 0000000..800bca1 --- /dev/null +++ b/app/Models/Cooldown.php @@ -0,0 +1,67 @@ + 'datetime', + ]; + + /** + * Get the character that owns this cooldown. + */ + public function character(): BelongsTo + { + return $this->belongsTo(Character::class); + } + + /** + * Check if this cooldown has expired + */ + public function hasExpired(): bool + { + $cooldownSeconds = config("game.cooldowns.{$this->action_type}", config('game.cooldowns.default', 60)); + $secondsSinceAction = now()->diffInSeconds($this->last_action_at); + return $secondsSinceAction >= $cooldownSeconds; + } + + /** + * Get remaining cooldown time in seconds + */ + public function getRemainingSecondsAttribute(): int + { + $cooldownSeconds = config("game.cooldowns.{$this->action_type}", config('game.cooldowns.default', 60)); + $secondsSinceAction = now()->diffInSeconds($this->last_action_at); + return max(0, $cooldownSeconds - $secondsSinceAction); + } + + /** + * Update the cooldown to now + */ + public function refresh(): void + { + $this->last_action_at = now(); + $this->save(); + } +} diff --git a/app/Services/Game/ActivityLogService.php b/app/Services/Game/ActivityLogService.php new file mode 100644 index 0000000..f9fe61e --- /dev/null +++ b/app/Services/Game/ActivityLogService.php @@ -0,0 +1,49 @@ +healthService = $healthService; + $this->economyService = $economyService; + $this->activityLogService = $activityLogService; + $this->cooldownService = $cooldownService; + } + + /** + * Process an attack between two characters + * + * @return array{ + * result: AttackResult, + * attacker_damage: int, + * defender_damage: int, + * money_stolen: int, + * experience_gained: int, + * was_critical: bool, + * was_mugging: bool, + * attacker_died: bool, + * defender_died: bool + * } + */ + public function processAttack(Character $attacker, Character $defender, bool $isMugging = false): array + { + // Check if characters can fight + if (!$attacker->canAct() || !$defender->canAct()) { + throw new \Exception('One or both characters cannot perform actions.'); + } + + if ($attacker->id === $defender->id) { + throw new \Exception('Cannot attack yourself.'); + } + + // Determine initiative (who attacks first) + $attackerFirst = $this->determineInitiative($attacker, $defender); + + $attackerDamage = 0; + $defenderDamage = 0; + $wasCritical = false; + $wasMugging = $isMugging; + + if ($attackerFirst) { + // Attacker goes first + $attackerDamage = $this->calculateDamage($attacker, $defender); + $defender->takeDamage($attackerDamage); + + // Check if defender is still alive + if ($defender->isAlive()) { + $defenderDamage = $this->calculateDamage($defender, $attacker); + $attacker->takeDamage($defenderDamage); + } + } else { + // Defender goes first + $defenderDamage = $this->calculateDamage($defender, $attacker); + $attacker->takeDamage($defenderDamage); + + // Check if attacker is still alive + if ($attacker->isAlive()) { + $attackerDamage = $this->calculateDamage($attacker, $defender); + $defender->takeDamage($attackerDamage); + } + } + + // Determine result + $result = $this->determineResult($attacker, $defender, $isMugging); + + // Calculate rewards + $moneyStolen = 0; + $experienceGained = 0; + + if ($result === AttackResult::WIN) { + // Attacker wins + if ($isMugging) { + $mugPercent = config('game.attack.mug_percent', 0.20); + $moneyStolen = (int) ($defender->money * $mugPercent); + $this->economyService->removeMoney($defender, $moneyStolen, 'Mugged by ' . $attacker->name); + $this->economyService->addMoney($attacker, $moneyStolen, 'Mugged ' . $defender->name); + } + + $experienceGained = $this->calculateExperienceReward($attacker, $defender, true); + $attacker->addExperience($experienceGained); + } elseif ($result === AttackResult::LOSE) { + // Defender wins (attacker lost) + $experienceGained = $this->calculateExperienceReward($attacker, $defender, false); + $attacker->addExperience($experienceGained); + } else { + // Draw or escape - small experience reward + $experienceGained = (int) (config('game.combat.base_damage', 5) * 2); + $attacker->addExperience($experienceGained); + } + + // Log the attack + $this->activityLogService->log( + $attacker, + 'attack', + "Attacked {$defender->name} - Result: {$result->label()}" + ); + + $this->activityLogService->log( + $defender, + 'attacked', + "Was attacked by {$attacker->name} - Result: " . ($result === AttackResult::LOSE ? 'Defended' : $result->label()) + ); + + // Record the cooldown + $this->cooldownService->recordAction($attacker, 'attack'); + + return [ + 'result' => $result, + 'attacker_damage' => $attackerDamage, + 'defender_damage' => $defenderDamage, + 'money_stolen' => $moneyStolen, + 'experience_gained' => $experienceGained, + 'was_critical' => $wasCritical, + 'was_mugging' => $wasMugging, + 'attacker_died' => !$attacker->isAlive(), + 'defender_died' => !$defender->isAlive(), + ]; + } + + /** + * Determine which character attacks first based on speed + */ + protected function determineInitiative(Character $attacker, Character $defender): bool + { + $baseChance = 0.5; // 50% base chance + $speedBonus = $attacker->speed * config('game.skills.speed.attack_first_chance', 0.02); + $randomVariation = random_int(0, 20) / 100 - 0.1; // +/- 10% + + $attackerChance = $baseChance + $speedBonus + $randomVariation; + + return random_int(0, 9999) / 10000 <= $attackerChance; + } + + /** + * Calculate damage dealt by attacker to defender + */ + protected function calculateDamage(Character $attacker, Character $defender): int + { + // Get attacker's attack power (includes strength and weapon bonus) + $baseDamage = $attacker->getAttackPowerAttribute(); + + // Apply damage variation + $variation = 1 + (random_int(0, 4000) / 10000 - 0.2); // +/- 20% + $damage = (int) ($baseDamage * $variation); + + // Check for critical hit + $critChance = config('game.combat.crit_chance', 0.10) + + ($attacker->dexterity * config('game.skills.dexterity.crit_chance', 0.005)); + + $isCritical = random_int(0, 9999) / 10000 <= $critChance; + + if ($isCritical) { + $critMultiplier = config('game.combat.crit_multiplier', 2.0); + $damage = (int) ($damage * $critMultiplier); + } + + // Apply defender's defense reduction + $defenseReduction = $defender->getDefenseAttribute() * config('game.combat.defense_reduction', 0.5); + $actualDamage = max(config('game.combat.min_damage', 1), $damage - $defenseReduction); + + return $actualDamage; + } + + /** + * Determine the result of the attack + */ + protected function determineResult(Character $attacker, Character $defender, bool $isMugging): AttackResult + { + if (!$attacker->isAlive()) { + return AttackResult::LOSE; // Attacker died + } + + if (!$defender->isAlive()) { + return AttackResult::WIN; // Defender died, attacker wins + } + + // If both are alive, it's a draw + return AttackResult::DRAW; + } + + /** + * Calculate experience reward for an attack + */ + protected function calculateExperienceReward(Character $attacker, Character $defender, bool $won): int + { + $baseExp = 50; + + if ($won) { + // More experience for winning + $levelDifference = max(0, $defender->level - $attacker->level); + $expMultiplier = 1 + ($levelDifference * 0.1); // 10% more per level difference + return (int) ($baseExp * 2 * $expMultiplier); // Base 100 for win + } else { + // Less experience for losing + $levelDifference = max(0, $attacker->level - $defender->level); + $expMultiplier = 1 - ($levelDifference * 0.05); // 5% less per level difference + return (int) ($baseExp * $expMultiplier); // Base 50 for loss + } + } + + /** + * Check if a character can attack another character + */ + public function canAttack(Character $attacker, Character $defender): bool + { + // Cannot attack self + if ($attacker->id === $defender->id) { + return false; + } + + // Both must be able to act + if (!$attacker->canAct() || !$defender->canAct()) { + return false; + } + + // Check if attacker is in cooldown + if ($this->cooldownService->isOnCooldown($attacker, 'attack')) { + return false; + } + + // Check if defender is in jail or hospital (can still attack them) + // But cannot attack if attacker is in jail or hospital + if ($attacker->isInJail() || $attacker->isInHospital()) { + return false; + } + + return true; + } + + /** + * Get remaining cooldown time for attack (delegated to CooldownService) + */ + public function getAttackCooldownRemaining(Character $character): int + { + return $this->cooldownService->getRemainingCooldown($character, 'attack'); + } + + /** + * Get the chance to hit for display purposes + */ + public function getHitChance(Character $attacker, Character $defender): float + { + // Calculate base hit chance + $baseChance = 0.70; + + // Speed affects hit chance + $speedBonus = $attacker->speed * 0.01; // +1% per speed point + + // Defense affects opponent's chance to hit + $defensePenalty = $defender->defense * 0.005; // -0.5% per defense point + + $hitChance = $baseChance + $speedBonus - $defensePenalty; + + return max(0.10, min(0.95, $hitChance)); + } + + /** + * Get estimated win probability + */ + public function getWinProbability(Character $attacker, Character $defender): float + { + // Simple estimation based on attack and defense + $attackerPower = $attacker->getAttackPowerAttribute() + ($attacker->health * 0.1); + $defenderPower = $defender->getDefenseAttribute() + ($defender->health * 0.1); + + $totalPower = $attackerPower + $defenderPower; + + if ($totalPower <= 0) { + return 0.5; + } + + $winProbability = $attackerPower / $totalPower; + + // Add speed initiative bonus + $speedDiff = $attacker->speed - $defender->speed; + $winProbability += $speedDiff * 0.005; // +0.5% per speed point difference + + return max(0.05, min(0.95, $winProbability)); + } +} diff --git a/app/Services/Game/CooldownService.php b/app/Services/Game/CooldownService.php new file mode 100644 index 0000000..9382529 --- /dev/null +++ b/app/Services/Game/CooldownService.php @@ -0,0 +1,123 @@ +getCooldownSeconds($actionType); + + if ($cooldownSeconds <= 0) { + return false; + } + + $lastActionTime = $this->getLastActionTime($character, $actionType); + + if (!$lastActionTime) { + return false; + } + + $secondsSinceAction = now()->diffInSeconds($lastActionTime); + return $secondsSinceAction < $cooldownSeconds; + } + + /** + * Get remaining cooldown time in seconds + */ + public function getRemainingCooldown(Character $character, string $actionType): int + { + $cooldownSeconds = $this->getCooldownSeconds($actionType); + + if ($cooldownSeconds <= 0) { + return 0; + } + + $lastActionTime = $this->getLastActionTime($character, $actionType); + + if (!$lastActionTime) { + return 0; + } + + $secondsSinceAction = now()->diffInSeconds($lastActionTime); + return max(0, $cooldownSeconds - $secondsSinceAction); + } + + /** + * Get formatted cooldown time (e.g., "2m 30s") + */ + public function getFormattedCooldown(Character $character, string $actionType): string + { + $remaining = $this->getRemainingCooldown($character, $actionType); + + if ($remaining <= 0) { + return 'Ready'; + } + + $minutes = (int) ($remaining / 60); + $seconds = $remaining % 60; + + if ($minutes > 0) { + return "{$minutes}m {$seconds}s"; + } + + return "{$seconds}s"; + } + + /** + * Record an action to start the cooldown + */ + public function recordAction(Character $character, string $actionType): void + { + $cooldownModel = $this->getCooldownModel($character, $actionType); + + // Update or create the cooldown record + $cooldownModel::updateOrCreate( + [ + 'character_id' => $character->id, + 'action_type' => $actionType, + ], + [ + 'last_action_at' => now(), + ] + ); + } + + /** + * Get the cooldown duration in seconds for an action type + */ + protected function getCooldownSeconds(string $actionType): int + { + return config("game.cooldowns.{$actionType}", config('game.cooldowns.default', 60)); + } + + /** + * Get the last action time for a character and action type + */ + protected function getLastActionTime(Character $character, string $actionType): ?Carbon + { + $cooldownModel = $this->getCooldownModel($character, $actionType); + + $cooldown = $cooldownModel::where('character_id', $character->id) + ->where('action_type', $actionType) + ->first(); + + return $cooldown?->last_action_at; + } + + /** + * Get the cooldown model class for an action type + */ + protected function getCooldownModel(Character $character, string $actionType): string + { + // For now, we'll use a simple approach with a cooldowns table + // In the future, this could be extended to support different cooldown types + return \App\Models\Cooldown::class; + } +} diff --git a/app/Services/Game/CrimeService.php b/app/Services/Game/CrimeService.php index c0a48c4..4574a30 100644 --- a/app/Services/Game/CrimeService.php +++ b/app/Services/Game/CrimeService.php @@ -5,21 +5,25 @@ namespace App\Services\Game; use App\Models\Character; use App\Models\Crime; use App\Models\CrimeAttempt; -use App\Models\ActivityLog; use App\Enums\CharacterStatus; -use App\Services\Game\NerveService; class CrimeService { protected EnergyService $energyService; protected EconomyService $economyService; protected NerveService $nerveService; + protected ActivityLogService $activityLogService; - public function __construct() - { - $this->energyService = new EnergyService(); - $this->economyService = new EconomyService(); - $this->nerveService = new NerveService(); + public function __construct( + EnergyService $energyService, + EconomyService $economyService, + NerveService $nerveService, + ActivityLogService $activityLogService + ) { + $this->energyService = $energyService; + $this->economyService = $economyService; + $this->nerveService = $nerveService; + $this->activityLogService = $activityLogService; } /** @@ -63,7 +67,7 @@ class CrimeService $character->addExperience($experience); // Log success - ActivityLog::logCrimeAttempt($character, $crime, true, $reward); + $this->activityLogService->logCrimeAttempt($character, $crime, true, $reward); } else { // Failed crime - take health penalty $healthPenalty = (int) ($character->max_health * config('game.crime.failure_penalty', 0.10)); @@ -75,7 +79,7 @@ class CrimeService } // Log failure - ActivityLog::logCrimeAttempt($character, $crime, false, 0); + $this->activityLogService->logCrimeAttempt($character, $crime, false, 0); } // Create crime attempt record diff --git a/app/Services/Game/EconomyService.php b/app/Services/Game/EconomyService.php index 6a47ffa..93ee49b 100644 --- a/app/Services/Game/EconomyService.php +++ b/app/Services/Game/EconomyService.php @@ -6,6 +6,13 @@ use App\Models\Character; class EconomyService { + protected ActivityLogService $activityLogService; + + public function __construct(ActivityLogService $activityLogService) + { + $this->activityLogService = $activityLogService; + } + /** * Transfer money between characters */ @@ -23,8 +30,8 @@ class EconomyService $to->addMoney($amount); // Log the transaction - ActivityLog::logMoneyTransaction($from, 'spent', $amount, "Transfer to {$to->name}: {$reason}"); - ActivityLog::logMoneyTransaction($to, 'earned', $amount, "Transfer from {$from->name}: {$reason}"); + $this->activityLogService->logMoneyTransaction($from, 'spent', $amount, "Transfer to {$to->name}: {$reason}"); + $this->activityLogService->logMoneyTransaction($to, 'earned', $amount, "Transfer from {$from->name}: {$reason}"); return true; } @@ -37,7 +44,7 @@ class EconomyService $character->addMoney($amount); if ($amount > 0 && $reason) { - ActivityLog::logMoneyTransaction($character, 'earned', $amount, $reason); + $this->activityLogService->logMoneyTransaction($character, 'earned', $amount, $reason); } } @@ -53,7 +60,7 @@ class EconomyService $character->removeMoney($amount); if ($amount > 0 && $reason) { - ActivityLog::logMoneyTransaction($character, 'spent', $amount, $reason); + $this->activityLogService->logMoneyTransaction($character, 'spent', $amount, $reason); } return true; diff --git a/bootstrap/app.php b/bootstrap/app.php index ad753ef..8981ced 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -18,6 +18,8 @@ return Application::configure(basePath: dirname(__DIR__)) 'check.energy' => \App\Http\Middleware\Game\CheckEnergy::class, 'check.jail' => \App\Http\Middleware\Game\CheckInJail::class, 'check.hospital' => \App\Http\Middleware\Game\CheckInHospital::class, + 'check.alive' => \App\Http\Middleware\Game\CheckAlive::class, + 'check.cooldown' => \App\Http\Middleware\Game\CheckCooldown::class, ]); // Game middleware group diff --git a/config/game.php b/config/game.php index 795f1a6..19ff849 100644 --- a/config/game.php +++ b/config/game.php @@ -69,6 +69,20 @@ return [ 'hit_list_cost' => 5000, // Cost to place a hit ], + // Attack system configuration + 'attack' => [ + 'base_damage' => 5, + 'damage_variation' => 0.20, // +/- 20% random variation + 'crit_chance' => 0.10, // 10% chance for critical hit + 'crit_multiplier' => 2.0, // Critical hits do 2x damage + 'defense_reduction' => 0.5, // Each defense point reduces damage by 0.5 + 'min_damage' => 1, // Minimum damage per hit + 'attack_cooldown' => 300, // 5 minutes between attacks + 'hit_list_cost' => 5000, // Cost to place a hit + 'mug_chance' => 0.30, // 30% chance to mug instead of hospitalize + 'mug_percent' => 0.20, // Steal 20% of victim's cash + ], + // Crime system configuration 'crime' => [ 'success_rate' => 0.70, // 70% base success rate diff --git a/database/migrations/2026_07_04_000001_create_attacks_table.php b/database/migrations/2026_07_04_000001_create_attacks_table.php new file mode 100644 index 0000000..625e318 --- /dev/null +++ b/database/migrations/2026_07_04_000001_create_attacks_table.php @@ -0,0 +1,46 @@ +id(); + $table->foreignId('attacker_id')->constrained('characters')->onDelete('cascade'); + $table->foreignId('defender_id')->constrained('characters')->onDelete('cascade'); + $table->integer('attacker_damage')->default(0); + $table->integer('defender_damage')->default(0); + $table->integer('attacker_health_after')->default(0); + $table->integer('defender_health_after')->default(0); + $table->string('result')->default('draw'); // win, lose, draw, escape + $table->integer('money_stolen')->default(0); + $table->integer('experience_gained')->default(0); + $table->boolean('was_critical')->default(false); + $table->boolean('was_mugging')->default(false); + $table->foreignId('attacker_weapon_id')->nullable()->constrained('items')->onDelete('set null'); + $table->foreignId('defender_armor_id')->nullable()->constrained('items')->onDelete('set null'); + $table->timestamp('attacked_at')->useCurrent(); + $table->timestamps(); + + $table->index('attacker_id'); + $table->index('defender_id'); + $table->index('attacked_at'); + $table->index('result'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('attacks'); + } +}; diff --git a/database/migrations/2026_07_04_000002_add_indexes_to_game_tables.php b/database/migrations/2026_07_04_000002_add_indexes_to_game_tables.php new file mode 100644 index 0000000..2bf8658 --- /dev/null +++ b/database/migrations/2026_07_04_000002_add_indexes_to_game_tables.php @@ -0,0 +1,92 @@ +index('user_id'); + $table->index('location_id'); + $table->index('faction_id'); + $table->index('status'); + $table->index('level'); + $table->index('health'); + $table->index('energy'); + $table->index('last_action_at'); + $table->index('in_hospital_until'); + $table->index('in_jail_until'); + }); + + // Add indexes to crimes table + Schema::table('crimes', function (Blueprint $table) { + $table->index('name'); + $table->index('slug'); + $table->index('difficulty'); + $table->index('location_id'); + $table->index('requires_level'); + }); + + // Add indexes to crime_attempts table + Schema::table('crime_attempts', function (Blueprint $table) { + $table->index('character_id'); + $table->index('crime_id'); + $table->index('success'); + $table->index('attempted_at'); + }); + + // Add indexes to inventories table + Schema::table('inventories', function (Blueprint $table) { + $table->index('character_id'); + $table->index('item_id'); + $table->index('equipped'); + }); + + // Add indexes to items table + Schema::table('items', function (Blueprint $table) { + $table->index('name'); + $table->index('slug'); + $table->index('type'); + $table->index('requires_level'); + $table->index('is_tradeable'); + $table->index('is_usable'); + }); + + // Add indexes to locations table + Schema::table('locations', function (Blueprint $table) { + $table->index('name'); + $table->index('slug'); + }); + + // Add indexes to notifications table + Schema::table('notifications', function (Blueprint $table) { + $table->index('user_id'); + $table->index('type'); + $table->index('read_at'); + $table->index('created_at'); + }); + + // Add indexes to activity_logs table + Schema::table('activity_logs', function (Blueprint $table) { + $table->index('character_id'); + $table->index('action'); + $table->index('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // We don't remove indexes in down() as it's generally safe to keep them + // and removing them might cause issues with existing data + } +}; diff --git a/database/migrations/2026_07_04_000003_create_cooldowns_table.php b/database/migrations/2026_07_04_000003_create_cooldowns_table.php new file mode 100644 index 0000000..8dc44e9 --- /dev/null +++ b/database/migrations/2026_07_04_000003_create_cooldowns_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('character_id')->constrained('characters')->onDelete('cascade'); + $table->string('action_type'); // attack, crime, travel, train, etc. + $table->timestamp('last_action_at')->useCurrent(); + $table->timestamps(); + + $table->unique(['character_id', 'action_type']); + $table->index('character_id'); + $table->index('action_type'); + $table->index('last_action_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cooldowns'); + } +}; diff --git a/routes/game.php b/routes/game.php index 9a240ff..d89b733 100644 --- a/routes/game.php +++ b/routes/game.php @@ -64,6 +64,16 @@ Route::middleware(['auth', 'verify.character'])->group(function () { Route::get('/', [GymController::class, 'index'])->name('index'); Route::post('/train', [GymController::class, 'train'])->name('train')->middleware(['check.energy:gym']); }); + + // Attack routes + Route::prefix('game/attack')->name('game.attack.')->group(function () { + Route::get('/', [\App\Http\Controllers\Game\AttackController::class, 'index'])->name('index')->middleware(['check.alive']); + Route::get('/preview/{target}', [\App\Http\Controllers\Game\AttackController::class, 'preview'])->name('preview')->middleware(['check.alive']); + Route::post('/{target}', [\App\Http\Controllers\Game\AttackController::class, 'attack'])->name('attack')->middleware(['check.alive', 'check.cooldown:attack']); + Route::post('/hit/{target}', [\App\Http\Controllers\Game\AttackController::class, 'placeHit'])->name('hit')->middleware(['check.alive']); + Route::get('/history', [\App\Http\Controllers\Game\AttackController::class, 'history'])->name('history')->middleware(['verify.character']); + Route::get('/revenge', [\App\Http\Controllers\Game\AttackController::class, 'revengeList'])->name('revenge')->middleware(['verify.character']); + }); }); // Hospital route (for when character is in hospital)