diff --git a/st/sei_time/doctype/character/character.js b/st/sei_time/doctype/character/character.js index a06012e..be65eff 100644 --- a/st/sei_time/doctype/character/character.js +++ b/st/sei_time/doctype/character/character.js @@ -1,8 +1,21 @@ // Copyright (c) 2026, Vassili and contributors // For license information, please see license.txt -// frappe.ui.form.on("Character", { -// refresh(frm) { +frappe.ui.form.on("Character", { + // refresh(frm) { -// }, -// }); + // }, + type(frm) { + frm.trigger('last_interaction'); + }, + last_interaction(frm) { + frappe.db.get_value('Character Type', frm.doc.type, 'death_clock') + .then(r => { + if (r && r.message) { + let last_int = moment(frm.doc.last_interaction); + let death = last_int.clone().add(r.message.death_clock, 'days'); + frm.set_value('death', death.format('YYYY-MM-DD HH:mm:ss')); + } + }); + } +}); diff --git a/st/sei_time/doctype/character/character.json b/st/sei_time/doctype/character/character.json index 0acd6e7..43c2c36 100644 --- a/st/sei_time/doctype/character/character.json +++ b/st/sei_time/doctype/character/character.json @@ -8,7 +8,9 @@ "field_order": [ "character_name", "type", - "profile_picture" + "profile_picture", + "last_interaction", + "death" ], "fields": [ { @@ -29,12 +31,24 @@ "fieldname": "profile_picture", "fieldtype": "Attach Image", "label": "Profile Picture" + }, + { + "fieldname": "last_interaction", + "fieldtype": "Datetime", + "label": "Last Interaction", + "permlevel": 2 + }, + { + "fieldname": "death", + "fieldtype": "Datetime", + "label": "Death", + "permlevel": 1 } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-03-24 19:37:53.659927", + "modified": "2026-03-31 19:09:46.094796", "modified_by": "Administrator", "module": "Sei Time", "name": "Character", diff --git a/st/sei_time/doctype/character/character.py b/st/sei_time/doctype/character/character.py index b2bd970..82e7f1e 100644 --- a/st/sei_time/doctype/character/character.py +++ b/st/sei_time/doctype/character/character.py @@ -15,6 +15,8 @@ class Character(Document): from frappe.types import DF character_name: DF.Data + death: DF.Datetime | None + last_interaction: DF.Datetime | None profile_picture: DF.AttachImage | None type: DF.Link | None # end: auto-generated types diff --git a/st/sei_time/doctype/character_type/character_type.json b/st/sei_time/doctype/character_type/character_type.json index 13412ce..7f562d9 100644 --- a/st/sei_time/doctype/character_type/character_type.json +++ b/st/sei_time/doctype/character_type/character_type.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "type", + "death_clock", "frame_image", "background_image" ], @@ -28,12 +29,17 @@ "fieldname": "background_image", "fieldtype": "Attach Image", "label": "Background Image" + }, + { + "fieldname": "death_clock", + "fieldtype": "Int", + "label": "Death Clock (days)" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-03-24 18:56:08.479742", + "modified": "2026-03-31 19:03:42.942277", "modified_by": "Administrator", "module": "Sei Time", "name": "Character Type", diff --git a/st/sei_time/doctype/character_type/character_type.py b/st/sei_time/doctype/character_type/character_type.py index 31e3e3c..354e05a 100644 --- a/st/sei_time/doctype/character_type/character_type.py +++ b/st/sei_time/doctype/character_type/character_type.py @@ -15,6 +15,7 @@ class CharacterType(Document): from frappe.types import DF background_image: DF.AttachImage | None + death_clock: DF.Int frame_image: DF.AttachImage | None type: DF.Data # end: auto-generated types diff --git a/st/sei_time/doctype/game/game.js b/st/sei_time/doctype/game/game.js index d87b9f8..0e70dd3 100644 --- a/st/sei_time/doctype/game/game.js +++ b/st/sei_time/doctype/game/game.js @@ -1,8 +1,16 @@ // Copyright (c) 2026, Vassili and contributors // For license information, please see license.txt -// frappe.ui.form.on("Game", { -// refresh(frm) { +frappe.ui.form.on("Game", { + // refresh(frm) { -// }, -// }); + // }, + session_duration(frm) { + frm.trigger('next_session_start'); + }, + next_session_start(frm) { + let start = moment(frm.doc.next_session_start); + let end = start.clone().add(frm.doc.session_duration, 'hours'); + frm.set_value('next_session_end', end.format('YYYY-MM-DD HH:mm:ss')); + } +}); \ No newline at end of file diff --git a/st/sei_time/doctype/game/game.json b/st/sei_time/doctype/game/game.json index 2987d2d..010ddc0 100644 --- a/st/sei_time/doctype/game/game.json +++ b/st/sei_time/doctype/game/game.json @@ -8,11 +8,18 @@ "field_order": [ "title", "description", - "section_break_nsqq", - "next_session", - "max_seats", + "section_break_sial", "type", - "select_vniu" + "max_seats", + "frequency", + "session_duration", + "section_break_next_session", + "next_session_number", + "next_session_start", + "next_session_end", + "schedule_session", + "all_sessions_section", + "all_sessions" ], "fields": [ { @@ -27,15 +34,7 @@ "label": "Description" }, { - "fieldname": "section_break_nsqq", - "fieldtype": "Section Break" - }, - { - "fieldname": "next_session", - "fieldtype": "Datetime", - "label": "Next Session" - }, - { + "default": "6", "fieldname": "max_seats", "fieldtype": "Int", "label": "Max Seats" @@ -47,15 +46,67 @@ "options": "Game Type" }, { - "fieldname": "select_vniu", + "fieldname": "section_break_sial", + "fieldtype": "Section Break" + }, + { + "default": "Weekly", + "fieldname": "frequency", "fieldtype": "Select", - "options": "January\nFebruary\nMarch" + "label": "Frequency", + "options": "Weekly\nBiweekly\nEtc." + }, + { + "fieldname": "section_break_next_session", + "fieldtype": "Section Break", + "label": "Next Session" + }, + { + "default": "0", + "fieldname": "next_session_number", + "fieldtype": "Int", + "label": "Next Session Number" + }, + { + "default": "now", + "fieldname": "next_session_start", + "fieldtype": "Datetime", + "label": "Next Session Start" + }, + { + "default": "now", + "fieldname": "next_session_end", + "fieldtype": "Datetime", + "label": "Next Session End", + "read_only": 1 + }, + { + "fieldname": "session_duration", + "fieldtype": "Float", + "label": "Session Duration (Hours)", + "length": 3, + "non_negative": 1, + "precision": "1" + }, + { + "fieldname": "schedule_session", + "fieldtype": "Button", + "label": "Schedule Session" + }, + { + "fieldname": "all_sessions_section", + "fieldtype": "Section Break", + "label": "All Sessions" + }, + { + "fieldname": "all_sessions", + "fieldtype": "HTML" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-03-24 19:02:39.174748", + "modified": "2026-04-28 19:43:24.253848", "modified_by": "Administrator", "module": "Sei Time", "name": "Game", @@ -86,7 +137,7 @@ ], "row_format": "Dynamic", "rows_threshold_for_grid_search": 20, - "sort_field": "next_session", + "sort_field": "next_session_start", "sort_order": "DESC", "states": [] } diff --git a/st/sei_time/doctype/game/game.py b/st/sei_time/doctype/game/game.py index ef303aa..ca6b728 100644 --- a/st/sei_time/doctype/game/game.py +++ b/st/sei_time/doctype/game/game.py @@ -4,7 +4,7 @@ import frappe from frappe.model.document import Document from frappe.utils import get_datetime - +from frappe.types import DF class Game(Document): # begin: auto-generated types @@ -16,9 +16,12 @@ class Game(Document): from frappe.types import DF description: DF.SmallText | None + frequency: DF.Literal["Weekly", "Biweekly", "Etc."] max_seats: DF.Int - next_session: DF.Datetime | None - select_vniu: DF.Literal["January", "February", "March"] + next_session_end: DF.Datetime | None + next_session_number: DF.Int + next_session_start: DF.Datetime | None + session_duration: DF.Float title: DF.Data | None type: DF.Link | None # end: auto-generated types @@ -26,23 +29,22 @@ class Game(Document): pass @frappe.whitelist() -def get_events(start, end, filters=None): +def get_events(start:DF.Datetime, end:DF.Datetime, filters:str=None): event_docs = frappe.get_all( "Game", - fields=["name", "title", "next_session"], + fields=["name", "title", "next_session_start", "next_session_end"], filters=[ - ["next_session", 'between', [start, end]], + ["next_session_start", 'between', [start, end]], ] ) events = [] for doc in event_docs: - next_session = get_datetime(doc.next_session) events.append({ "name": doc.name, "title": doc.title, - "start": next_session, - "end": next_session, + "start": get_datetime(doc.next_session_start), + "end": get_datetime(doc.next_session_end), }) return events \ No newline at end of file diff --git a/st/sei_time/doctype/game/game_calendar.js b/st/sei_time/doctype/game/game_calendar.js index 9974cb4..412b583 100644 --- a/st/sei_time/doctype/game/game_calendar.js +++ b/st/sei_time/doctype/game/game_calendar.js @@ -1,7 +1,7 @@ frappe.views.calendar["Game"] = { field_map: { - "start": "next_session", - "end": "next_session", // Optional + "start": "start", + "end": "end", "id": "name", "title": "title" }, diff --git a/st/sei_time/doctype/game_session/__init__.py b/st/sei_time/doctype/game_session/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/st/sei_time/doctype/game_session/game_session.js b/st/sei_time/doctype/game_session/game_session.js new file mode 100644 index 0000000..d887fb2 --- /dev/null +++ b/st/sei_time/doctype/game_session/game_session.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Vassili and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Game Session", { +// refresh(frm) { + +// }, +// }); diff --git a/st/sei_time/doctype/game_session/game_session.json b/st/sei_time/doctype/game_session/game_session.json new file mode 100644 index 0000000..eed09fa --- /dev/null +++ b/st/sei_time/doctype/game_session/game_session.json @@ -0,0 +1,98 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:session_title", + "creation": "2026-04-26 03:29:54.749610", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "game", + "game_title", + "session_title", + "session_number", + "session_start", + "session_end", + "notes_section", + "notes" + ], + "fields": [ + { + "fieldname": "game", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Game", + "options": "Game", + "reqd": 1 + }, + { + "fetch_from": "game.title", + "fieldname": "game_title", + "fieldtype": "Data", + "label": "Game Title", + "read_only": 1 + }, + { + "fieldname": "session_title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Session Title", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "session_number", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Session Number", + "reqd": 1 + }, + { + "fieldname": "session_start", + "fieldtype": "Datetime", + "label": "Session Start" + }, + { + "fieldname": "session_end", + "fieldtype": "Datetime", + "label": "Session End" + }, + { + "collapsible": 1, + "fieldname": "notes_section", + "fieldtype": "Section Break", + "label": "Notes" + }, + { + "fieldname": "notes", + "fieldtype": "Long Text" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-04-28 19:53:33.962004", + "modified_by": "Administrator", + "module": "Sei Time", + "name": "Game Session", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/st/sei_time/doctype/game_session/game_session.py b/st/sei_time/doctype/game_session/game_session.py new file mode 100644 index 0000000..d274b6b --- /dev/null +++ b/st/sei_time/doctype/game_session/game_session.py @@ -0,0 +1,26 @@ +# Copyright (c) 2026, Vassili and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class GameSession(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + game: DF.Link + game_title: DF.Data | None + notes: DF.LongText | None + session_end: DF.Datetime | None + session_number: DF.Int + session_start: DF.Datetime | None + session_title: DF.Data + # end: auto-generated types + + pass diff --git a/st/sei_time/doctype/game_session/test_game_session.py b/st/sei_time/doctype/game_session/test_game_session.py new file mode 100644 index 0000000..47ae88d --- /dev/null +++ b/st/sei_time/doctype/game_session/test_game_session.py @@ -0,0 +1,22 @@ +# Copyright (c) 2026, Vassili and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + + +class IntegrationTestGameSession(IntegrationTestCase): + """ + Integration tests for GameSession. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/st/sei_time/page/characters/characters.css b/st/sei_time/page/characters/characters.css index 2aef4e3..9ade006 100644 --- a/st/sei_time/page/characters/characters.css +++ b/st/sei_time/page/characters/characters.css @@ -18,10 +18,131 @@ } .image-stack .fg { - padding: 35px; + padding: 50px; z-index: 2; } .image-stack .frame { z-index: 3; +} + +.deathclock-wrapper { + min-height: 150px; + margin: 0; + display: grid; + place-items: center; + font-family: "Courier New", monospace; + overflow: hidden; +} + +.deathclock { + display: flex; + gap: 5px; + padding: 5px; +} + +.digit { + position: relative; + width: 50px; + height: 90px; + perspective: 700px; +} + +.flipcard { + position: absolute; + inset: 0; + border-radius: 10px; + background: linear-gradient(#1d1d1d, #050505); + color: #f2e7c9; + font-size: 56px; + font-weight: bold; + line-height: 95px; + text-align: center; + overflow: hidden; +} + +.flipcard::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 50%; + height: 2px; + background: #000; + box-shadow: + 0 -1px rgba(255,255,255,0.08), + 0 1px rgba(255,255,255,0.05); + z-index: 3; +} + +.top, .bottom { + position: absolute; + left: 0; + width: 100%; + height: 50%; + overflow: hidden; + background: linear-gradient(#202020, #080808); + color: #f2e7c9; + font-size: 56px; + font-weight: bold; + text-align: center; + backface-visibility: hidden; +} + +.top { + top: 0; + line-height: 90px; + border-radius: 10px 10px 0 0; + transform-origin: bottom; +} + +.bottom { + bottom: 0; + line-height: 0; + border-radius: 0 0 10px 10px; + transform-origin: top; +} + +.flip-top { + animation: flipTop 0.28s ease-in forwards; + z-index: 5; +} + +.flip-bottom { + animation: flipBottom 0.28s ease-out forwards; + z-index: 5; +} + +@keyframes flipTop { + from { + transform: rotateX(0deg); + } + to { + transform: rotateX(-90deg); + } +} + +@keyframes flipBottom { + from { + transform: rotateX(90deg); + } + to { + transform: rotateX(0deg); + } +} + +.shine { + position: absolute; + inset: 0; + border-radius: 10px; + pointer-events: none; + background: + linear-gradient( + 115deg, + rgba(255,255,255,0.12), + transparent 35%, + transparent 70%, + rgba(255,255,255,0.05) + ); + z-index: 10; } \ No newline at end of file diff --git a/st/sei_time/page/characters/characters.js b/st/sei_time/page/characters/characters.js index 8d19723..d36078a 100644 --- a/st/sei_time/page/characters/characters.js +++ b/st/sei_time/page/characters/characters.js @@ -6,24 +6,117 @@ frappe.pages['characters'].on_page_load = function(wrapper) { }); frappe.db.get_list('Character', { - fields: ['name', 'type', 'profile_picture', 'type.frame_image', 'type.background_image'], + fields: ['name', 'type', 'profile_picture', 'type.frame_image', 'type.background_image', 'death'], filters: { - owner: frappe.session.user + //owner: frappe.session.user } }).then(docs => { - console.log(docs); render_cards(docs, wrapper); + + const deathclocks = document.getElementsByClassName("deathclock"); + + function createDigit() { + const digit = document.createElement("div"); + digit.className = "digit"; + digit.dataset.value = "0"; + + digit.innerHTML = ` +
${doc.type}