Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e6c1825
perf: stop rebuilding whole map source per frame; fix stale initial c…
gaidheal1 Aug 9, 2026
8c56f75
feat: import watabou "squares" as communal-space Subzones
gaidheal1 Aug 10, 2026
4151639
feat: auto-pick an unused village_layout slot for import_village
gaidheal1 Aug 10, 2026
ddde5c0
perf: prefetch initial-centre/world-bounds map data at login
gaidheal1 Aug 10, 2026
4317385
Merge pull request #745 from progressrpg/perf/map-animation-and-initi…
gaidheal1 Aug 10, 2026
3ddc2f8
Merge pull request #746 from progressrpg/feat/watabou-square-subzones
gaidheal1 Aug 10, 2026
fed33bd
Merge pull request #747 from progressrpg/feat/import-village-auto-slot
gaidheal1 Aug 10, 2026
592c6e4
Merge remote-tracking branch 'origin/staging' into development
gaidheal1 Aug 10, 2026
8aec23c
Merge pull request #748 from progressrpg/development
gaidheal1 Aug 10, 2026
ee3e482
Replace map-view badge with daily goals + completion bonus (#751)
claude Aug 10, 2026
a4f0d0d
Change dailyGoalsBadge flag from 'all' to 'testers'
gaidheal1 Aug 11, 2026
4e29ccf
Merge pull request #752 from progressrpg/feat/daily-goals-badge
gaidheal1 Aug 11, 2026
708f759
fix: standardize timer duration formatting
Harshit15-coder Aug 12, 2026
4982652
Merge pull request #768 from Harshit15-coder/main
gaidheal1 Aug 13, 2026
62d3f91
feat: derive Character.can_link instead of storing it (#682)
gaidheal1 Aug 13, 2026
91af1db
Merge pull request #769 from progressrpg/feat/character-can-link-deri…
gaidheal1 Aug 13, 2026
a8202b0
feat: broadcast activity-timer updates to a player's other sessions
gaidheal1 Aug 13, 2026
3a170ed
Merge pull request #770 from progressrpg/feat/multi-session-timer-sync
gaidheal1 Aug 13, 2026
97cf8e0
Autosave the item-name field in PlayerItemList's edit modal
claude Aug 13, 2026
d14ea30
fix: keep parent-task field editable and render subtasks as indented …
gaidheal1 Aug 13, 2026
04b8ead
Merge pull request #772 from progressrpg/feat/task-parent-editable
gaidheal1 Aug 13, 2026
c49dacf
fix: correct save-status indicator position, add saving-delay, align …
gaidheal1 Aug 13, 2026
062abc6
Merge remote-tracking branch 'origin/development' into claude/issue-7…
gaidheal1 Aug 13, 2026
89b06dc
Merge pull request #771 from progressrpg/claude/issue-763-t3l7dz
gaidheal1 Aug 13, 2026
c47dd00
feat: extend formatDueAt with weeks/months granularity
gaidheal1 Aug 13, 2026
704f8dd
Merge pull request #773 from progressrpg/feat/format-due-at-granularity
gaidheal1 Aug 13, 2026
37f09c6
fix: simplify resident line display by removing activity status
gaidheal1 Aug 13, 2026
ca7adfd
style: standardize form control dimensions and radius across components
gaidheal1 Aug 13, 2026
44ce250
test: update resident-display tests for name-only rendering
gaidheal1 Aug 13, 2026
c254fbb
feat: add offline activity logging modal
gaidheal1 Aug 13, 2026
1e82866
Merge pull request #775 from progressrpg/feat/offline-activity-logging
gaidheal1 Aug 13, 2026
899c245
Merge pull request #776 from progressrpg/development
gaidheal1 Aug 13, 2026
2133d2e
feat: parent task chip, due-date split, timestamp tooltip, subtask cr…
gaidheal1 Aug 14, 2026
9f7f8d1
Merge pull request #777 from progressrpg/feat/parent-task-chip-display
gaidheal1 Aug 14, 2026
5d894c2
Re-enable player-presence XP modifiers and add live character product…
claude Aug 14, 2026
0cc4123
feat: story bucket-1 components for Storybook coverage (#749)
gaidheal1 Aug 14, 2026
13e7d37
feat: story layout chrome for Storybook coverage (#749)
gaidheal1 Aug 14, 2026
d572f1d
Merge pull request #779 from progressrpg/feat/storybook-coverage-buck…
gaidheal1 Aug 14, 2026
e6f8ba1
Merge pull request #778 from progressrpg/claude/issue-750-vfjrzu
gaidheal1 Aug 14, 2026
eb6a3e3
refactor: optimize imports and streamline activity timer functionality
gaidheal1 Aug 14, 2026
8cfd1c7
task: bump mkdocs and -material
gaidheal1 Aug 14, 2026
ca90bfd
fix: update pr templates
gaidheal1 Aug 14, 2026
fd4aa91
Merge pull request #780 from progressrpg/development
gaidheal1 Aug 14, 2026
0f49f83
fix: prevent duration fields wrapping on small screens
gaidheal1 Aug 14, 2026
fbcf641
fix: add note to credit contributors
gaidheal1 Aug 14, 2026
23c53cb
fix: ignore models Manager typing mypy limitation
gaidheal1 Aug 14, 2026
c2e8a7c
feat: broadcast announcement badge updates in real time
gaidheal1 Aug 14, 2026
82932f3
Merge pull request #783 from progressrpg/feat/announcement-published-…
gaidheal1 Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions .github/PULL_REQUEST_TEMPLATE/development-to-staging.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Leave a category empty (or delete its header) if nothing applies.
-->

## User-visible improvements (UVIs)
## Summary

### Features
-
Expand All @@ -19,9 +19,8 @@

---

## Contributors
<!-- Credit anyone who contributed a PR in this batch besides yourself (e.g. @username). -->

## Technical notes
<!-- Migrations, env vars, feature flags, anything a reviewer needs to know -->

## Test plan
- [ ] CI passes
- [ ] Smoke test on staging after deploy
7 changes: 3 additions & 4 deletions .github/PULL_REQUEST_TEMPLATE/staging-to-main.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@

---

## Contributors
<!-- Credit anyone who contributed a PR in this release besides yourself (e.g. @username). -->

## Technical notes
<!-- Migrations, env vars, feature flags, rollout/rollback risk -->

## Test plan
- [ ] CI passes
- [ ] Verified on staging
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ ds:

t:
docker compose exec web python manage.py test $(t) --keepdb --buffer
tnk:
docker compose exec web python manage.py test $(t) --buffer

vt:
cd ./frontend
Expand Down
62 changes: 54 additions & 8 deletions api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def player_for(user) -> Player:

class TestMeViewSet(APITestCase):
def setUp(self):
self.character = Character.objects.create(given_name="Hero", can_link=True)
self.character = Character.objects.create(given_name="Hero")
self.user = create_test_user(
email="duncan@example.com",
password="pass12345",
Expand Down Expand Up @@ -194,32 +194,78 @@ def test_complete_onboarding_sets_flag(self):
self.assertTrue(player_for(self.user).onboarding_completed)
self.assertEqual(res.data, {"onboarding_completed": True})

def test_today_points_null_when_no_active_link(self):
def test_daily_goals_null_when_no_active_link(self):
self.authenticate()
player = player_for(self.user)
self.assertIsNone(player.active_link)

res = self.client.get(reverse("me-today-points"))
res = self.client.get(reverse("me-daily-goals"))

self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertIsNone(res.data["points_today"])
self.assertIsNone(res.data["goals"])

def test_today_points_reflects_todays_completed_activities(self):
def test_daily_goals_all_false_when_linked_but_no_activity_or_login(self):
self.authenticate()
player = player_for(self.user)
PlayerCharacterLink.objects.create(player=player, character=self.character)

res = self.client.get(reverse("me-daily-goals"))

self.assertEqual(res.status_code, status.HTTP_200_OK)
goals = res.data["goals"]
self.assertFalse(goals["logged_in_today"])
self.assertFalse(goals["completed_activity_today"])
self.assertEqual(goals["activity_minutes_today"], 0)
self.assertFalse(goals["minutes_goal_met"])
self.assertFalse(goals["all_goals_met"])
self.assertFalse(goals["bonus_awarded_today"])
self.assertEqual(goals["bonus_ap"], 0)

def test_daily_goals_reflects_todays_completed_activities(self):
self.authenticate()
player = player_for(self.user)
PlayerCharacterLink.objects.create(player=player, character=self.character)

PlayerActivity.objects.create(
player=player,
is_complete=True,
duration=1200, # 20 minutes
completed_at=datetime.now(timezone.utc),
)

res = self.client.get(reverse("me-daily-goals"))

self.assertEqual(res.status_code, status.HTTP_200_OK)
goals = res.data["goals"]
self.assertTrue(goals["completed_activity_today"])
self.assertEqual(goals["activity_minutes_today"], 20)
self.assertTrue(goals["minutes_goal_met"])

def test_daily_goals_all_met_awards_bonus_once(self):
from users.models import UserLogin

self.authenticate()
player = player_for(self.user)
PlayerCharacterLink.objects.create(player=player, character=self.character)
UserLogin.objects.create(user=self.user)

PlayerActivity.objects.create(
player=player,
is_complete=True,
duration=1200, # 20 minutes -> 2 points
duration=1200,
completed_at=datetime.now(timezone.utc),
)

res = self.client.get(reverse("me-today-points"))
res = self.client.get(reverse("me-daily-goals"))

self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(res.data["points_today"], 2)
goals = res.data["goals"]
self.assertTrue(goals["all_goals_met"])
# Reads never award the bonus themselves - only activity completion
# does (via check_and_award_daily_goals), so a plain GET here
# shouldn't have paid it out.
self.assertFalse(goals["bonus_awarded_today"])
self.assertEqual(goals["bonus_ap"], 0)


class CustomTokenObtainPairViewTests(APITestCase):
Expand Down
6 changes: 6 additions & 0 deletions api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@

from locations.views import (
PopulationCentreMapView,
InitialMapCentreView,
MapCharacterDetailView,
MapViewportView,
MapWorldBoundsView,
Expand Down Expand Up @@ -135,6 +136,11 @@ def to_url(self, value):
PopulationCentreMapView.as_view(),
name="populationcentre-map",
),
path(
"map/initial-centre/",
InitialMapCentreView.as_view(),
name="map-initial-centre",
),
path(
"map/viewport/",
MapViewportView.as_view(),
Expand Down
51 changes: 44 additions & 7 deletions api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,24 +363,61 @@ def character(self, request):

@extend_schema(
responses=inline_serializer(
name="TodayPointsResponse",
name="DailyGoalsResponse",
fields={
"points_today": drf_serializers.IntegerField(allow_null=True),
"goals": inline_serializer(
name="DailyGoalsStateResponse",
fields={
"logged_in_today": drf_serializers.BooleanField(),
"completed_activity_today": drf_serializers.BooleanField(),
"activity_minutes_today": drf_serializers.IntegerField(),
"minutes_goal_threshold": drf_serializers.IntegerField(),
"minutes_goal_met": drf_serializers.BooleanField(),
"all_goals_met": drf_serializers.BooleanField(),
"bonus_awarded_today": drf_serializers.BooleanField(),
"bonus_ap": drf_serializers.IntegerField(),
},
allow_null=True,
),
},
)
)
@action(detail=False, methods=["get"])
def today_points(self, request):
def daily_goals(self, request):
"""
Personal "points earned today" for the map view's badge (issue #673).
`points_today` is null - not zero - when the player has no active
Daily goals for the map view's badge (issue #751, replacing the old
`today_points`/`points_today` mechanic from issue #673). `goals` is
null - not a set of all-false goals - when the player has no active
PlayerCharacterLink, so the frontend can tell "no link" apart from
"linked but nothing earned yet today" and hide the badge entirely.
"linked but no goals cleared yet today" and hide the badge entirely.
"""
from progression.daily_goals import (
MINUTES_GOAL_THRESHOLD,
get_daily_goals_state,
)

player = request.user.player
link = player.active_link

return Response({"points_today": link.points_today if link else None})
if not link:
return Response({"goals": None})

state = get_daily_goals_state(player)

return Response(
{
"goals": {
"logged_in_today": state.logged_in_today,
"completed_activity_today": state.completed_activity_today,
"activity_minutes_today": state.activity_minutes_today,
"minutes_goal_threshold": MINUTES_GOAL_THRESHOLD,
"minutes_goal_met": state.minutes_goal_met,
"all_goals_met": state.all_goals_met,
"bonus_awarded_today": state.bonus_awarded_today,
"bonus_ap": state.bonus_ap,
}
}
)

@extend_schema(
responses=inline_serializer(
Expand Down
39 changes: 26 additions & 13 deletions character/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,27 @@ def get_other_members(self, obj):
return ", ".join(str(c) for c in others)


class CanLinkListFilter(admin.SimpleListFilter):
"""
can_link is a derived property, not a DB column, so it can't be listed
in list_filter directly - filter via Character.objects.linkable()
(the queryset-level equivalent) instead.
"""

title = "can link"
parameter_name = "can_link"

def lookups(self, request, model_admin):
return (("yes", "Yes"), ("no", "No"))

def queryset(self, request, queryset):
if self.value() == "yes":
return queryset.filter(pk__in=Character.objects.linkable())
if self.value() == "no":
return queryset.exclude(pk__in=Character.objects.linkable())
return queryset


@admin.action(description="Mark selected characters as NPCs and unlink from players")
def mark_as_npc(modeladmin, request, queryset):
for character in queryset:
Expand All @@ -73,17 +94,6 @@ def mark_as_npc(modeladmin, request, queryset):
)


@admin.action(description="Mark selected characters as available to link")
def mark_as_canlink(modeladmin, request, queryset):
for character in queryset:
character.can_link = True
character.save(update_fields=["can_link"])

messages.success(
request, f"{queryset.count()} character(s) marked as available to link."
)


@admin.register(Character)
class CharacterAdmin(admin.ModelAdmin):
fieldsets = (
Expand All @@ -93,6 +103,7 @@ class CharacterAdmin(admin.ModelAdmin):
"fields": (
"given_name",
"can_link",
"is_reserved",
"sex",
)
},
Expand Down Expand Up @@ -145,7 +156,8 @@ class CharacterAdmin(admin.ModelAdmin):
"birth_date",
]
list_filter = [
"can_link",
CanLinkListFilter,
"is_reserved",
"birth_date",
"death_date",
"sex",
Expand All @@ -156,6 +168,7 @@ class CharacterAdmin(admin.ModelAdmin):
"links__player__name",
]
readonly_fields = [
"can_link",
"get_player",
"get_age",
"created_at",
Expand All @@ -168,7 +181,7 @@ class CharacterAdmin(admin.ModelAdmin):
CharacterRelationshipMembershipInline,
CharacterCurrencyInline,
]
actions = [mark_as_npc, mark_as_canlink]
actions = [mark_as_npc]

@admin.display(description="Player")
def get_player(self, obj):
Expand Down
14 changes: 13 additions & 1 deletion character/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class CharacterFilter(django_filters.FilterSet):
level = django_filters.RangeFilter(field_name="level")
xp = django_filters.RangeFilter(field_name="xp")
is_npc = django_filters.BooleanFilter(method="filter_is_npc")
can_link = django_filters.BooleanFilter(field_name="can_link")
can_link = django_filters.BooleanFilter(method="filter_can_link")

class Meta:
model = Character
Expand All @@ -21,3 +21,15 @@ def filter_is_npc(self, queryset, name, value):
else:
# is_npc=False means HAS active player link
return queryset.filter(links__is_active=True)

def filter_can_link(self, queryset, name, value):
"""
can_link is derived, not a DB column - defer to
Character.objects.linkable(), the queryset-level equivalent of
Character.can_link, instead of duplicating its logic here.
"""
linkable = Character.objects.linkable()
if value:
return queryset.filter(pk__in=linkable)
else:
return queryset.exclude(pk__in=linkable)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 5.2.17 on 2026-08-13 16:31

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("character", "0020_remove_character_building"),
]

operations = [
migrations.RemoveField(
model_name="character",
name="can_link",
),
migrations.AddField(
model_name="character",
name="is_reserved",
field=models.BooleanField(
default=False,
help_text="Manually held back from linking (e.g. reserved for a future storyline), independent of age or link status.",
verbose_name="Reserved",
),
),
]
Loading
Loading