diff --git a/.flake8 b/.flake8 index 8b5d05f..2e663a7 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,5 @@ [flake8] import-order-style = google exclude = .git,__pycache__,migrations,.dev-venv,.prod-venv,.venv -max-line-length = 79 \ No newline at end of file +max-line-length = 79 +ignore = FNE003,FNE008 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ab991c7..4e91397 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ local_settings.py db.sqlite3 db.sqlite3-journal fixtures/ +game/ # Flask stuff: instance/ diff --git a/NoobRPG/NoobRPG/settings.py b/NoobRPG/NoobRPG/settings.py index b7c86e2..2a7f0d0 100644 --- a/NoobRPG/NoobRPG/settings.py +++ b/NoobRPG/NoobRPG/settings.py @@ -39,6 +39,8 @@ def load_bool_from_env(name: str, default: bool): 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', + 'rest_framework.authtoken', + 'djoser', 'entities.apps.EntitiesConfig', 'items.apps.ItemsConfig', 'locations.apps.LocationsConfig', @@ -124,3 +126,12 @@ def load_bool_from_env(name: str, default: bool): DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.BasicAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ], +} diff --git a/NoobRPG/NoobRPG/urls.py b/NoobRPG/NoobRPG/urls.py index cf7c7a5..929220e 100644 --- a/NoobRPG/NoobRPG/urls.py +++ b/NoobRPG/NoobRPG/urls.py @@ -1,9 +1,12 @@ from django.contrib import admin -from django.urls import include, path +from django.urls import include, path, re_path urlpatterns = [ path('admin/', admin.site.urls), + path('api/v1/drf-auth/', include('rest_framework.urls')), + path('api/v1/auth/', include('djoser.urls')), + re_path(r'^auth/', include('djoser.urls.authtoken')), path('api/v1/entities/', include('entities.urls')), path('api/v1/items/', include('items.urls')), path('api/v1/locations/', include('locations.urls')), diff --git a/NoobRPG/entities/apps.py b/NoobRPG/entities/apps.py index 3748dcc..f611540 100644 --- a/NoobRPG/entities/apps.py +++ b/NoobRPG/entities/apps.py @@ -6,3 +6,6 @@ class EntitiesConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'entities' + + def ready(self): + import entities.signals # noqa diff --git a/NoobRPG/entities/migrations/0002_player_user.py b/NoobRPG/entities/migrations/0002_player_user.py new file mode 100644 index 0000000..fdda0ec --- /dev/null +++ b/NoobRPG/entities/migrations/0002_player_user.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.8 on 2025-11-27 11:15 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('entities', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='player', + name='user', + field=models.OneToOneField( + blank=True, + help_text='The user account associated with this player', + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='player_profile', + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/NoobRPG/entities/models.py b/NoobRPG/entities/models.py index c65f941..e8a09a5 100644 --- a/NoobRPG/entities/models.py +++ b/NoobRPG/entities/models.py @@ -3,6 +3,7 @@ import random from core.models import EntityBaseModel +from django.contrib.auth.models import User from django.db import models from items.models import Items from locations.models import Location @@ -72,17 +73,22 @@ def __str__(self) -> str: return f'{self.name}' def drop_items(self) -> models.QuerySet: - return random.choice(self.items_to_drop) + items = list(self.items_to_drop.all()) + if items: + return random.choice(items) + return None def taking_damage(self, damage: int) -> str | None: if self.hp > damage: self.hp -= damage else: self.hp = 0 + self.save() + if self.hp == 0: return self.drop_items() return None - def dealing_damage(self) -> int: + def self_damage(self) -> int: return self.base_damage @@ -106,6 +112,14 @@ def all_fields(self) -> models.QuerySet: class Player(EntityBaseModel): objects = PlayerManager() + user = models.OneToOneField( + User, + on_delete=models.CASCADE, + null=True, + blank=True, + related_name='player_profile', + help_text='The user account associated with this player', + ) inventory = models.ManyToManyField( Items, blank=True, @@ -133,18 +147,20 @@ class Meta: def __str__(self): return f'{self.name}' - def to_start_location(self) -> None: - self.location = self.start_location + def to_start_location(self) -> str: + self.current_location = self.start_location self.save() + return f'You are now in start location: {self.current_location}' - def equip_weapon(self, item: models.QuerySet) -> None: + def equip_weapon(self, item: models.QuerySet) -> str: if item is None: self.weapon = None - return + return 'Cannot equip weapon: Inventory is empty.' if item not in self.inventory.all(): - raise ValueError('Cannot equip weapon: item not in inventory.') + return 'Cannot equip weapon: Item not in inventory.' self.weapon = item self.save() + return f'Equipped weapon: {self.weapon}.' def taking_damage(self, damage_hp: int, damage_mana: int) -> str: if self.hp >= damage_hp or self.mana >= damage_mana: @@ -183,7 +199,12 @@ def healing(self, hp_heal_amount: int, mana_heal_amount: int) -> str: ) return f'Your health is now {self.hp} and your mana is {self.mana}.' - def dealing_damage(self) -> int: + def self_damage(self) -> int: if self.weapon: return self.base_damage + self.weapon.damage return self.base_damage + + def attack(self, enemy: models.QuerySet) -> str: + damage = self.self_damage() + enemy.taking_damage(damage) + return f'You dealt {damage} damage to an enemy.' diff --git a/NoobRPG/entities/serializers.py b/NoobRPG/entities/serializers.py index 268e3a1..960de53 100644 --- a/NoobRPG/entities/serializers.py +++ b/NoobRPG/entities/serializers.py @@ -65,6 +65,7 @@ def build_field(self, field_name, info, model_class, nested_depth): class PlayerSerializer(serializers.HyperlinkedModelSerializer): + user = serializers.StringRelatedField() inventory = serializers.StringRelatedField(many=True, read_only=True) weapon = serializers.StringRelatedField(read_only=True) @@ -81,6 +82,7 @@ class Meta: Player.base_damage.field.name, Player.is_in_battle.field.name, Player.current_location.field.name, + Player.user.field.name, Player.inventory.field.name, Player.weapon.field.name, Player.start_location.field.name, diff --git a/NoobRPG/entities/signals.py b/NoobRPG/entities/signals.py new file mode 100644 index 0000000..83a8357 --- /dev/null +++ b/NoobRPG/entities/signals.py @@ -0,0 +1,51 @@ +__all__ = () + +from django.contrib.auth.models import User +from django.db.models.signals import post_save +from django.dispatch import receiver +from entities.models import Player +from locations.models import Location + + +@receiver(post_save, sender=User) +def create_player_profile(sender, instance, created, **kwargs): + if created: + if not hasattr(instance, 'player_profile'): + default_location = Location.objects.first() + + if default_location: + Player.objects.create( + user=instance, + name=instance.username, + start_location=default_location, + hp=100, + max_hp=100, + mana=20, + max_mana=20, + base_damage=10, + current_location=default_location, + is_in_battle=False, + ) + else: + default_location = Location.objects.create( + name='Default Location', + slug='default-location', + ) + Player.objects.create( + user=instance, + name=instance.username, + start_location=default_location, + hp=100, + max_hp=100, + mana=20, + max_mana=20, + base_damage=10, + current_location=default_location, + is_in_battle=False, + ) + + +@receiver(post_save, sender=User) +def save_player_profile(sender, instance, **kwargs): + if hasattr(instance, 'player_profile'): + instance.player_profile.save() diff --git a/NoobRPG/entities/tests.py b/NoobRPG/entities/tests.py deleted file mode 100644 index e4990b4..0000000 --- a/NoobRPG/entities/tests.py +++ /dev/null @@ -1,61 +0,0 @@ -__all__ = () - -from django.contrib.auth.models import User -from django.test import RequestFactory, TestCase -from entities.models import NonPlayerCharacter as NPCModel -from entities.views import NPCsViewSet -from locations.models import Location -from rest_framework.test import force_authenticate - - -class NPCLocationQueryTest(TestCase): - def setUp(self): - self.factory = RequestFactory() - self.user = User.objects.create_user( - username='testuser', - password='testpass', - ) - - self.location1 = Location.objects.create(name='Forest', slug='forest') - self.location2 = Location.objects.create(name='Castle', slug='castle') - - self.npc1 = NPCModel.objects.create( - name='Forest Guardian', - current_location=self.location1, - ) - self.npc2 = NPCModel.objects.create( - name='Castle Guard', - current_location=self.location2, - ) - - def test_npc_list_without_query_param(self): - view = NPCsViewSet.as_view({'get': 'list'}) - request = self.factory.get('/api/v1/entities/npcs/') - force_authenticate(request, user=self.user) - response = view(request) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 2) - - def test_npc_list_with_location_query_param(self): - view = NPCsViewSet.as_view({'get': 'list'}) - request = self.factory.get( - '/api/v1/entities/npcs/?current_location__slug=forest', - ) - force_authenticate(request, user=self.user) - response = view(request) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data[0]['name'], 'Forest Guardian') - - def test_npc_list_with_nonexistent_location(self): - view = NPCsViewSet.as_view({'get': 'list'}) - request = self.factory.get( - '/api/v1/entities/npcs/?current_location__slug=nonexistent', - ) - force_authenticate(request, user=self.user) - response = view(request) - - self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 0) diff --git a/NoobRPG/entities/urls.py b/NoobRPG/entities/urls.py index 758b275..7ed3d53 100644 --- a/NoobRPG/entities/urls.py +++ b/NoobRPG/entities/urls.py @@ -10,11 +10,4 @@ urlpatterns = [ path('', include(router.urls)), - path( - 'api-auth/', - include( - 'rest_framework.urls', - namespace='rest_framework', - ), - ), ] diff --git a/NoobRPG/entities/utils.py b/NoobRPG/entities/utils.py new file mode 100644 index 0000000..560954f --- /dev/null +++ b/NoobRPG/entities/utils.py @@ -0,0 +1,45 @@ +__all__ = () + +from entities.models import Player +from locations.models import Location + + +def create_player_for_user(user, player_name=None, location=None): + try: + player = user.player_profile + return player + except AttributeError: + pass + + name = player_name or user.username + + start_location = location + if not start_location: + start_location = Location.objects.first() + + if not start_location: + raise ValueError('No location available to assign to the new player.') + + player = Player.objects.create( + user=user, + name=name, + start_location=start_location, + hp=100, + max_hp=100, + mana=20, + max_mana=20, + base_damage=10, + current_location=start_location, + is_in_battle=False, + ) + + return player + + +def get_or_create_player_for_user(user, player_name=None, location=None): + try: + player = user.player_profile + return player, False + except AttributeError: + player = create_player_for_user(user, player_name, location) + return player, True diff --git a/NoobRPG/items/urls.py b/NoobRPG/items/urls.py index b33083d..8cf0824 100644 --- a/NoobRPG/items/urls.py +++ b/NoobRPG/items/urls.py @@ -7,5 +7,5 @@ router.register(r'', views.ItemViewSet, basename='items') urlpatterns = [ - path('items/', include(router.urls)), + path('', include(router.urls)), ] diff --git a/NoobRPG/locations/serializers.py b/NoobRPG/locations/serializers.py index 62436c5..10ebc7b 100644 --- a/NoobRPG/locations/serializers.py +++ b/NoobRPG/locations/serializers.py @@ -23,6 +23,6 @@ def build_field(self, field_name, info, model_class, nested_depth): ) if field_name == 'url': - field_kwargs['view_name'] = 'locations-detail' + field_kwargs['view_name'] = 'location-detail' return field_class, field_kwargs diff --git a/NoobRPG/users/__init__.py b/NoobRPG/users/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/NoobRPG/users/apps.py b/NoobRPG/users/apps.py deleted file mode 100644 index e3b65cc..0000000 --- a/NoobRPG/users/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -__all__ = () - -from django.apps import AppConfig - - -class UsersConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'users' diff --git a/NoobRPG/users/migrations/__init__.py b/NoobRPG/users/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/requirements/prod.txt b/requirements/prod.txt index c226786..de1f664 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -1,2 +1,3 @@ djangorestframework==3.16.1 +djoser==2.3.3 python-dotenv==1.2.1 \ No newline at end of file