Skip to content

Implement symbiotic XP multiplier with activity interruption on player state change - #198

Closed
gaidheal1 with Copilot wants to merge 4 commits into
developmentfrom
copilot/implement-xp-multiplier-system-again
Closed

Implement symbiotic XP multiplier with activity interruption on player state change#198
gaidheal1 with Copilot wants to merge 4 commits into
developmentfrom
copilot/implement-xp-multiplier-system-again

Conversation

Copilot AI commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

✅ Symbiotic XP Multiplier System - Rebased and Regenerated

Successfully rebased on the new development branch (after migration squash) and regenerated proper Django migrations.

Changes Made:

  • Rebased branch on top of new development branch (commit 4f6dd01)
  • Applied XP multiplier code changes to new base
  • Generated proper Django migrations:
    • character/migrations/0004_characterrole_romanticrelationship_and_more.py - Adds player_came_online_at field
    • progression/migrations/0003_characteractivity_xp_multiplier_applied.py - Adds xp_multiplier_applied field
  • Removed obsolete position field that was causing migration errors
  • Added test file character/tests/test_xp_multiplier.py

Model Changes:

Character model:

  • Added get_active_link() method
  • Added player_is_online property
  • Added xp_multiplier property
  • Added on_player_state_change() method

PlayerCharacterLink model:

  • Added player_came_online_at field
  • Added player_is_online property
  • Added xp_multiplier property (2.0x online, 1.0x offline)
  • Added get_link_for_character() classmethod
  • Added get_link_for_player() classmethod

CharacterActivity model:

  • Added xp_multiplier_applied field
  • Override save() to capture multiplier on creation
  • Updated calculate_xp_reward() to use stored multiplier

WebSocket Consumer:

  • Call on_player_state_change() in connect method
  • Call on_player_state_change() in disconnect method

Files Changed:

  • character/models/character.py (+117 lines)
  • progression/models.py (+13 lines)
  • gameplay/consumers.py (+11 lines)
  • character/migrations/0004_*.py (new)
  • progression/migrations/0003_*.py (new)
  • character/tests/test_xp_multiplier.py (new, 348 lines)
Original prompt

Overview

Implement a symbiotic XP multiplier system where character activities are interrupted and restarted with the correct multiplier when player logs in/out. This avoids temporal state complexity by ensuring each activity has a single multiplier throughout its duration.

Goals

  • Player online: Character gets 2.0x XP multiplier
  • Player offline: Character gets 1.0x XP multiplier
  • When player state changes: Interrupt current activity and restart with new multiplier
  • Each activity stores its multiplier at creation time
  • No notifications on state change (silent interruption)

Backend Changes

1. Add Migration for PlayerCharacterLink Model

Create migration in character/migrations/ to add:

  • player_came_online_at field (DateTimeField, null=True, blank=True) with help text "When the player came online while linked to this character"

2. Add Migration for CharacterActivity Model

Create migration in progression/migrations/ to add:

  • xp_multiplier_applied field (FloatField, default=1.0) to store the multiplier that was active when the activity started

3. Update PlayerCharacterLink Model (character/models/link.py)

Add to the PlayerCharacterLink class:

# New field (will be added via migration)
player_came_online_at = models.DateTimeField(
    null=True,
    blank=True,
    help_text="When the player came online while linked to this character"
)

@property
def player_is_online(self):
    """Is the player currently connected?"""
    return self.player.is_online

@property
def xp_multiplier(self):
    """
    Simple multiplier: player online = 2.0x, offline = 1.0x
    
    Returns:
        float: 1.0 (offline), 2.0 (online)
    """
    return 2.0 if self.player_is_online else 1.0

@classmethod
def get_link_for_character(cls, character):
    """Get active link for a character."""
    return cls.objects.filter(
        character=character,
        is_active=True
    ).select_related('player').first()

@classmethod
def get_link_for_player(cls, player):
    """Get active link for a player."""
    return cls.objects.filter(
        player=player,
        is_active=True
    ).select_related('character').first()

4. Update Character Model (character/models/character.py)

Add to the Character class:

def get_active_link(self):
    """Get active PlayerCharacterLink for this character."""
    from character.models import PlayerCharacterLink
    return PlayerCharacterLink.objects.filter(
        character=self,
        is_active=True
    ).select_related('player').first()

@property
def player_is_online(self):
    """Check if linked player is online."""
    link = self.get_active_link()
    return link.player_is_online if link else False

@property
def xp_multiplier(self):
    """
    Get XP multiplier from link.
    
    Returns:
        float: 1.0 (offline), 2.0 (online)
    """
    link = self.get_active_link()
    return link.xp_multiplier if link else 1.0

def on_player_state_change(self, now=None):
    """
    Called when player online/offline state changes.
    Interrupts current activity and starts a new one with current multiplier.
    
    This method handles both login and logout:
    1. Completes current activity early (with its stored multiplier)
    2. Starts new activity for remaining time (which captures current multiplier)
    
    Returns:
        CharacterActivity: The new activity (or None if none to interrupt)
    """
    now = now or timezone.now()
    
    behaviour = getattr(self, 'behaviour', None)
    if not behaviour:
        return None
    
    # Get current activity
    current = behaviour.sync_to_now(now)
    
    if not current or current.is_complete:
        return None
    
    # Don't interrupt sleep
    if current.kind == 'sleep':
        return current
    
    # Don't interrupt if less than 5 minutes remaining
    remaining = (current.scheduled_end - now).total_seconds()
    if remaining < 300:
        return current
    
    # Complete current activity early
    current.completed_at = now
    current.is_complete = True
    if current.started_at:
        current.duration = int((now - current.started_at).total_seconds())
    current.save()
    
    # Create new activity for remaining time
    # Multiplier will be captured automatically in save()
    from progression.models import CharacterActivity
    
    new_activity = CharacterActivity.objects.create(
        character=self,
        kind=current.kind,
        name=current.name or current.kind,
        scheduled_start=now,
        scheduled_end=current.scheduled_end,
        started_at=now,
        is_complete=False
    )
    
    return new_activity

5. Update CharacterActivity Model (progression/models.py)

Modify the CharacterActivity class:

# Add new field (will be added via migration)
xp_multiplier_applied = models.FloatField(default=1.0)

def save(self, *args, **kwargs):
    """Capture multiplier when activity is created."""
    if not sel...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Copilot AI and others added 3 commits January 23, 2026 18:05
Co-authored-by: gaidheal1 <84990667+gaidheal1@users.noreply.github.com>
Co-authored-by: gaidheal1 <84990667+gaidheal1@users.noreply.github.com>
Co-authored-by: gaidheal1 <84990667+gaidheal1@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement symbiotic XP multiplier system for character activities Implement symbiotic XP multiplier with activity interruption on player state change Jan 23, 2026
Copilot AI requested a review from gaidheal1 January 23, 2026 18:11
@gaidheal1

Copy link
Copy Markdown
Member

@copilot rewrite the migrations and make any other changes needed now that I have regenerated migrations from scratch.

Copilot AI commented Jan 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot rewrite the migrations and make any other changes needed now that I have regenerated migrations from scratch.

Rebased on development and regenerated migrations. Changes:

Migrations regenerated:

  • character/migrations/0004_characterrole_romanticrelationship_and_more.py - Adds player_came_online_at field
  • progression/migrations/0003_characteractivity_xp_multiplier_applied.py - Adds xp_multiplier_applied field

Code changes:

  • All XP multiplier code re-applied to development base
  • Removed obsolete position field that was causing errors
  • Test file preserved

Commit: 0f7a8e4

Note: The branch history has been rewritten to be based on the new development branch (4f6dd01). A force push will be needed to update the remote.

@gaidheal1

Copy link
Copy Markdown
Member

Implemented separately, closing

@gaidheal1 gaidheal1 closed this Feb 9, 2026
@gaidheal1
gaidheal1 deleted the copilot/implement-xp-multiplier-system-again branch April 10, 2026 16:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants