diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a3fe4a2..4cddf05e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,82 @@ # Change Log +## v1.2.0 - Time, Other Modules, Seasons, Moons and Notes + +This update was a long time coming so apologies for making you wait for it. This update with most of the currently requested changes to Simple Calendar + +### Time + +Simple Calendar now supports time of day! What this means is that Simple Calendar can now be tied into FoundryVTTs game world time. This allows any other module that ties into the game world time to be updated whe Simple Calendar updates or update Simple Calendar when they change the game world time. + +There are 4 different options on how to tie into the game world time to achieve the exact level of interaction you want for your world. They are: + +Option|Description|Update Game World Time|When Game World Time is Updated +--------|--------------------|-------------------------------------------------|---------------------------------------------------------- +None (default)|Simple Calendar does not interact with the game world time at all. This setting is ideal if you want to keep Simple Calendar isolated from other modules.|Does not update the game world time|Simple Calendar is not updated when the game world time is updated by something else. +Self|Treats Simple Calendar as the authority source for the game world time. This setting is ideal when you want Simple Calendar to be in control of the games time and don't want other modules updating Simple Calendar|Updates the game world time to match what is in Simple Calendar.|Combat round changes will update Simple Calendars time. Simple Calendar will ignore updates from all others modules. +Third Party Module|This will instruct Simple Calendar to just display the Time in the game world time. All date changing controls are disabled and the changing of time relies 100% on another module. This setting is ideal if you are just want to use Simple Calenar to display the date in calendar form and/or take advantage of the notes.|Does not update the game world time.|Updates it's display everytime the game world time is changed, following what the other modules say the time is. +Mixed|This option is a blend of the self and third party options. Simple calendar can change the game world time and and changes made by other modules are reflected in Simple Calendar. This setting is ideal if you want to use Simple Calendar and another module to change the game time.|Will update the game world time|Will update it's own time based on changes to the game world time, following what other modules say the time is. + +You can check out the [configuration](./docs/Configuration.md#game-world-time-integration) section for more details. + +#### Simple Calendar Clock + +There is now a time of day clock that displays below the calendar to show the current time of the current day. +The GM can manually control this clock if they wish, or they can start the clock and have it update as real time passes. +The clock does also update as combat rounds pass. For more details on the clock check out [here](./docs/UsingTheCalendar.md#simple-calendars-clock). + +#### Configuration + +You can configure the calendar with how many hours in a day, minutes in an hour and seconds in a minute. + +You can also set the ratio of game time to real world seconds. This is used when the built-in clock is running and needs to update the game time as real world seconds pass. This ratio can be a decimal. +A ratio of 1 would mean for every second that passes in the real world 1 second passes in game time, a ratio of 2 would mean for every second of real world time 2 seconds pass in game. + +### Other Modules + +Simple Calendar also now interfaces with other modules. These modules can be used to adjust the game world time and have those changes reflected in Simple Calendar. + +For two of the more common modules Simple Calendar can also import their settings into Simple Calendar or export Simple Calendars settings into those modules. The two modules that support this are about-time and Calendar/Weather. Check out this [documentation](./docs/Configuration.md#third-party-module-importexport) for more details on how it all works. + + +### Seasons + +Simple Calendar now supports seasons. Any number of seasons can be added to a calendar, and you are able to specify the following options for each season: + +Setting | Description +--------|------------ +Season Name | Specify a custom name of the season. +Starting Month | From a drop down choose which month this season starts in. This drop down is populated based on the custom months that have been set up. +Starting Day | From a drop down choose which day of the the starting month this season begins on. This drop down is populated with a list of days based on the staring month selected. +Color | Seasons can be assigned a color, this color is used as the background color for the calendar display when it is the current season. There is a list of predefined colors that work well for standard season and the option to enter a custom color. +Custom Color | If the color option is set to Custom Color an option will appear where you can enter a custom Hex representation of a color to use for the seasons color. + +The calendar display has also been updated so that right below the month and year the name of the current season will be displayed. + +The background color of the calendar is also set based on the current season and its color settings. + +I think this gives the best approach for defining seasons and allowing customization in how they look. + +### Moons + +Simple Calendar now supports the addition of moons. Any number of moons can be added to a calendar, and they can be customized to meet your needs. +For details on how to add and customize a moon please check out the [configuration documentation](./docs/Configuration.md#moon-settings). + +The calendar now also displays the important (single day) moon phases on the calendar as well as the moon phase for the current day and selected day. + +The predefined calendars have been updated to set up their moon(s) for the calendar. + +### Notes + +A configuration option has been added to allow players to add their own notes to the Calendar. If enabled they will see the "Add New Note" button on the main calendar display. + +**Important**: For a user to add their own note a GM must also be logged in at the same time, a warning is displayed if a user attempts to add a note when not GM is logged in. + +### Documentation + +I did a complete re-organization/clean up of all the documentation around Simple Calendar. I also added in links within the Simple Calendar configuration window to this documentation. I hope this will help make configuration and use of the tool easier for all. + + ## v1.1.8 - Bug Fixes - Fixed a rare bug where the day of the week a month starts on would be incorrect. diff --git a/README.md b/README.md index 878d6a52..61fe8a4d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -[![foundry](https://img.shields.io/badge/Foundry-0.7.9-orange)](https://foundryvtt.com/releases/) +[![Foundry Core Compatible Version](https://img.shields.io/badge/dynamic/json.svg?url=https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/master/src/module.json&label=Foundry&query=$.compatibleCoreVersion&colorB=orange)](https://foundryvtt.com/releases/) ![GitHub package.json version](https://img.shields.io/github/package-json/v/vigoren/foundryvtt-simple-calendar) [![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/vigoren/foundryvtt-simple-calendar/blob/main/LICENSE) -[![jest](https://jestjs.io/img/jest-badge.svg)](https://github.com/facebook/jest) +![GitHub Workflow Status](https://img.shields.io/github/workflow/status/vigoren/foundryvtt-simple-calendar/Node.js%20CI) [![codecov](https://codecov.io/gh/vigoren/foundryvtt-simple-calendar/branch/main/graph/badge.svg?token=43TJ117WP1)](https://codecov.io/gh/vigoren/foundryvtt-simple-calendar) +[![GitHub release (latest by date)](https://img.shields.io/github/downloads/vigoren/foundryvtt-simple-calendar/latest/total)](https://github.com/vigoren/foundryvtt-simple-calendar/releases/latest) +[![forge](https://img.shields.io/badge/dynamic/json?label=Forge%20Installs&query=package.installs&suffix=%&url=https://forge-vtt.com/api/bazaar/package/foundryvtt-simple-calendar&colorB=3d8b41)](https://forge-vtt.com/bazaar#package=foundryvtt-simple-calendar) +[![Foundry Hub Endorsements](https://img.shields.io/endpoint?logoColor=white&url=https://www.foundryvtt-hub.com/wp-json/hubapi/v1/package/foundryvtt-simple-calendar/shield/endorsements)](https://www.foundryvtt-hub.com/package/foundryvtt-simple-calendar/) +[![Foundry Hub Comments](https://img.shields.io/endpoint?logoColor=white&url=https://www.foundryvtt-hub.com/wp-json/hubapi/v1/package/foundryvtt-simple-calendar/shield/comments)](https://www.foundryvtt-hub.com/package/foundryvtt-simple-calendar/) ![Logo](https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/logo.png) @@ -10,29 +14,43 @@ A simple calendar module for [FoundryVTT](https://foundryvtt.com/) that is system independent. -This module allows you to create a calendar with any number of months per year and any number of days per month for your game world. +This module allows you to create a calendar with any number of months per year, any number of days per month and customizable hours, minutes and seconds for your game world. It is intended as a way for a GM to show a calendar like interface that maps to their in game world. -The simple calendar module does not keep track of time or tie into any system to advance the day, date changing is a manual process by the GM. -If you are looking for a module that tracks in game time and has weather related effects I recommend you check out the [Calendar/Weather module](https://www.foundryvtt-hub.com/package/calendar-weather/). - -## GM Features: - -* Configure the calendar to meet your worlds needs - * Set the year as well as add any prefix or postfix to the years name - * Define how many months in a year - * Set a custom name for each month - * Set the number of days in each month - * Choose if months are considered intercalary - * Set up your own Leap Year rules -* Set and change the current day as your game story progresses -* Add notes to specific days on the calendar to remind yourself of events or other world related things - * These notes can either be visible to players as well as the GM or just the GM - -## Player Features: - -* Browse a calendar interface to see the years, months and day of the game world -* Select days to see any notes/events specific to that day +## Features + Simple Calendar has a number of features that make it a great time keeping tool for your games! + +### For GMs +* Complete customization of the calendar to meet your worlds needs: + * Set the year as well as add any prefix or postfix to the years name. + * Define how many months in a year. + * Set a custom name, the number of days and days during a leap year (if applicable) for each month. + * Choose if months are considered intercalary (fall outside normal months). + * Set the number hours in a day, minutes in an hour and seconds in a minute. + * Set up your own Leap Year rules. + * Set up different seasons for your calendar and how they are displayed to the users. + * Set up your own custom moons. + * Or choose from a selection of [preset calendars](./docs/Configuration.md#predefined-calendars). +* Set and change the current day and time as your game story progresses or have it automatically advance based on real world time and passing combat rounds. +* Add notes to specific days on the calendar to remind yourself of events or other world related things. + * These notes can either be visible to players as well as the GM or just the GM. + +### For Players + +* Browse a calendar interface to see the years, months and day of the game world. +* See the current day and time of the game world. +* Select days to view any notes/events specific to that day. +* If the GM allows it, the ability to add their own notes to the calendar. + +## Contents + +- [Installation](#installing-the-module) +- [Compatible Modules](#compatible-modules) +- [Accessing and using the calendar](./docs/UsingTheCalendar.md) +- [Changing the date and time](./docs/UpdatingDateTime.md) +- [Notes](./docs/Notes.md) +- [Configuring your Calendar](./docs/Configuration.md) +- [Macros](./docs/Macros.md) ## Installing The Module @@ -47,176 +65,15 @@ To install using the module json file, use this link [https://github.com/vigoren To install the most recent version of the module, view the releases section to the right of the main GitHub page. Selecting the latest release will bring you to a page where you can download the module.zip asset. This will contain everything you need to manually install the module. -## Accessing and using the Calendar -The module adds a calendar button to the basic controls section of the layer controls. Clicking on this will open the module window - -![Calendar Button Location](https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/layers-button.png) - -The above image helps shows the controls, they are detailed out below. - -Control | Description -------- | ----------- -Previous/Next | Allow the user to change which month/year they are currently viewing. -Today Button | Changes the calendar so that the current day (in the game world) is visible and selected. -Blue Circle Day | This indicates the current day in the game world, can be changed by the GM -Green Circle Day | This indicates the day the user currently has selected. This will show any notes on this day. -Red Indicator | This shows on any days that have notes that the user can see. It will show the number of notes on that day up to 99. -Notes List | Any notes that appear in this list can be clicked on to open the note details. - -### Note Details - -The note details shows all the information about a specific note: the date the note is for, the title of the note and the content of the note. - -![Calendar Button Location](https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/note-view.png) - -## Updating the Current Date -The GM version of the module looks a little different, with the addition of controls to change the current date and a button to enter the configuration. - -![GM View](https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/gm-view.png) - -The above image helps shows the controls, they are detailed out below. - -Control | Description -------- | ----------- -Day Back/Forward | This moves the current day forward or back one day. -Month Back/Forward | This moves the current month forward or back one month. The current day will be mapped to the same day as the old month, or the last day of the month if the old month has more days. -Year Back/Forward | This moves the current year forward or back one year. The current month and day will stay the same in the new year. -Apply | This will apply the changes, saving the new current day in the settings and updating all of the players calendars to reflect the new current day. -Configuration | This opens up the configuration dialog to allow the GM to fully customize the calendar. -Add New Note Button | This will open the add notes dialog to add a new note for the selected day. - -## Adding, Editing and Removing notes - -The GM has the ability to add new notes by clicking on the add new note button for a selected date. This will open a dialog where the details of the note can be filled out. - -![Calendar Button Location](https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/note-new.png) - -Field | Description -------- | ----------- -Note Title | The title for the note, this title will appear in the listing of notes for the day. -Player Visible | If this note can be seen by players or if it is for the GM only. -Note Repeats | If this note repeats weekly, monthly, yearly or never. If the note repeats monthly and it is on a day that some months don't have (eg the 31st) months that don't go to that day will not have this note. -Details | Here you can enter the details of a note using the built in text editor. - -After all the details are filled out you can save the note. - -**Important**: If you have not saved the content in the text editor using the text editor save button, a warning will appear when you try to save the note letting you know. - -The GM can also edit or delete existing notes. To do this click on an existing note, at the bottom of the note view 2 additional buttons will be visible, Edit and Delete. - -Button | Description -------- | ----------- -The Edit button | This will load the contents of the note in the same editor as creating a new note. -The Delete button | This will open up a confirmation dialog, where selecting delete again will permanently remove the note. - -## Configuring Your calendar - -Configuration of the calendar is straight forward. As the GM open the configuration dialog and start customizing your calendar! - -### General Settings - -This tab allows you to set some general settings for the entire calendar. - -#### Predefined Calendars - -This setting lets you choose from a list of predefined calendars to get your calendar started with. The following calendars can be selected to configure the game calendar: - -Calendar|Description|Initial Date ---------|-----------|------------- -Gregorian|This the standard real life calendar|The current date will be used -Eberron| This is the calendar from the Eberron setting for Dungeons and Dragons | Zarantyr 1, 998 YK -Exandrian |This is the calendar from the Exandria setting for Dungeons and Dragons | Horisal 1, 812 P.D. -Golarian | This is the calendar from the Pathfinder game | Abadius 1, 4710 AR -Greyhawk | This is the calendar from the Greyhawk setting for Dungeons and Dragons | Needfest 1, 591 cy -Harptos | This is the calendar used across Faerun in the Forgotten Realms | Hammer 1, 1495 DR -Warhammer | This is the calendar used by the Imperium in the Fantasy Warhammer game | Hexenstag 1, 2522 - -All of these calendars can be further customized after they are loaded. They are here to provide a simple starting point for your game. - -#### Other Settings - -These are the other settings availible under the General Settings tab - -Setting | Description --------- | ---------- -Note Default Player Visibility | For new notes, if by default the player visibility option is checked or not. - - -### Year Settings - -This tab allows you to change some settings about the years in your game world - -Setting | Description --------- | ---------- -Current Year | The Current year your game world is in. This can be any positive number. -Year Prefix | Text that will appear before the year number. -Year Postfix | Text that will appear after the year number. - -### Month Settings - -This tab displays all the months that exist in the calendar. Here you can change month names, the number of days in a month, remove a month, add a new month or remove all months. - -Setting | Description --------- | ---------- -Month Name | These text boxes for each month allow you to change the name of an existing month. -Number of Days | These text boxes for each month allow you to change the number of days in each month. A month can have a minimum of 0 days. -Intercalary Month | An intercalary month is one that does not follow the standard month numbering and is skipped.
Example: If we were to add an intercalary month between January and February, January would still be considered the first month and February would be considered the second month. The new month does not get a number.
Intercalary months also do not count towards the years total days nor do they affect the day of the week subsequent months start on. -Include Intercalary Month In Total Day Count | When you select a month to be intercalary, an option will show to include these days as part of the years total days and have its days afect the day of the week subsequent months start on. The month though still is not numbered. -Remove Button | These buttons for each month allow you to remove the month from the list. -Add New Month Button | This button will add a new month to the bottom of the list with a default name and number of days that you can then configure to your liking. -Remove All Months Button | This button will remove all of the months from the list. - - -### Weekday Settings - -This section displays all the weekdays that exist in the calendar. Weekdays are used to determine how wide the calendar display should be and how to make month days to each day of the week. - -Setting | Description --------- | ---------- -Weekday Name | These text boxes for each weekday allow you to change the name of an existing weekday. -Remove Button | These buttons for each weekday allow you to remove the weekday from the list. -Add New Weekday Button | This button will add a new weekday to the bottom of the list with a default name that you can then configure to your liking. -Remove All Weekdays Button | This button will remove all of the weekdays from the list. - -### Leap Year Settings - -This section allows the GM to configure how leap years work for this calendar. - -Setting | Description --------- | ---------- -Leap Year Rule | Which ruleset to follow when determining leap years. The options are -When Leap Years Happen | **This only appears if the Custom leap year rule is selected**.
The number of years when a leap year occurs. Example a value of 5 would mean every 5th year is a leap year. -Months List | **This only appears if the Custom or Gregorian leap year rule is selected**.
A list of months will appear that shows each month, and a textbox where you can change the number of days the corresponding month has during a leap year. A month can have a minimum of 0 leap year days. - -After you have changed the settings to your liking don't forget to save the configuration by hitting the Save Configuration button! - -## Macro's - -You can create macros that will open up the Simple Calendar interface when used. To start create a new script macro and enter this as the command: - -```javascript -SimpleCalendar.show(); -``` - -**Important**: If this macro is intended to be useable by players don't forget to configure the Macros permissions for all players. It will need to be set to at least the "Limited", permission level. +## Compatible Modules +These are other time keeping modules that Simple Calendar can work if they are installed in your world. -The show function can take 3 parameters to set the year, month and day that the calendar opens up to. +**Important**: None of these modules are required, the option to work with them is available to make a GMs life easier if they want to use Simple Calendar but have another of these modules installed. -Parameter|Type|Default|Details ----------|----|-------|------- -Year | number or null | null | The year to open the calendar too. If null is passed in it will open the calendar to the year the user last viewed -Month | number or null | null | The month to open the calendar too.
The month is expected to start at 0, or be the index of the month to show. This way intercalary months can be easily chosen by using their index as they don't have a month number.
-1 can be passed in to view the last month of the year.
If null is passed in the calendar will open to the month the user last viewed. -Day | number or null | null | The day of the month to select.
The day is expected to start at 1.
-1 can be passed in to select the last day of the month.
If null is passed in the selected day will be the last day the user selected, if any. +- [about-time](https://foundryvtt.com/packages/about-time): See the [about-time module configuration for Simple Calendar](./docs/Configuration.md#about-time) for more information. +- [Calendar/Weather](https://foundryvtt.com/packages/calendar-weather): See the [Calendar/Weather module configuration for Simple Calendar](./docs/Configuration.md#calendarweather) for more information. -### Examples -All these examples assume we are using a standard Gregorian calendar. -Open the calendar to August 2003 -```javascript -SimpleCalendar.show(2003, 7); -``` +## Credits -Open the calendar to December 1999 and select the 25th day -```javascript -SimpleCalendar.show(1999, 11, 25); -``` +Moon Icons by [Wolf Böse](https://thenounproject.com/neuedeutsche/) diff --git a/__mocks__/form-application.ts b/__mocks__/form-application.ts index 0520646c..2f0d2059 100644 --- a/__mocks__/form-application.ts +++ b/__mocks__/form-application.ts @@ -41,17 +41,28 @@ class FormApplication extends Application{ weekdayNames.value = 'Z'; + const seasonNames = document.createElement('input'); + seasonNames.setAttribute('data-index', '0'); + seasonNames.value = 'Spring'; + const seasonStart = document.createElement('input'); + seasonStart.setAttribute('data-index', '0'); + seasonStart.value = '2'; + const seasonColor = document.createElement('input'); + seasonColor.setAttribute('data-index', '0'); + seasonColor.value = '#ffffff'; + + this.element = { find: jest.fn() - .mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return '';})};})}) - .mockReturnValueOnce([monthNames]).mockReturnValueOnce([invalidMonthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'none';})};})}) - .mockReturnValueOnce([monthNames]).mockReturnValueOnce([sameMonthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([invalidMonthDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'custom';})};})}) - .mockReturnValueOnce([monthNames]).mockReturnValueOnce([sameMonthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([monthLeapDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'custom';})};})}) - .mockReturnValueOnce([monthNames]).mockReturnValueOnce([monthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([monthLeapDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'none';})};})}) - .mockReturnValueOnce([monthNames]).mockReturnValueOnce([invalidMonthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([monthLeapDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'none';})};})}) - .mockReturnValueOnce([monthNames]).mockReturnValueOnce([monthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([monthLeapDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'none';})};})}) + .mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return '';})};})}).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]).mockReturnValueOnce([noDataAttr]) + .mockReturnValueOnce([monthNames]).mockReturnValueOnce([invalidMonthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'none';})};})}).mockReturnValueOnce([seasonNames]).mockReturnValueOnce([invalidMonthDays]).mockReturnValueOnce([invalidMonthDays]).mockReturnValueOnce([seasonColor]).mockReturnValueOnce([seasonColor]) + .mockReturnValueOnce([monthNames]).mockReturnValueOnce([sameMonthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([invalidMonthDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'custom';})};})}).mockReturnValueOnce([seasonNames]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonColor]).mockReturnValueOnce([seasonColor]) + .mockReturnValueOnce([monthNames]).mockReturnValueOnce([sameMonthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([monthLeapDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'custom';})};})}).mockReturnValueOnce([seasonNames]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonColor]).mockReturnValueOnce([seasonColor]) + .mockReturnValueOnce([monthNames]).mockReturnValueOnce([monthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([monthLeapDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'none';})};})}).mockReturnValueOnce([seasonNames]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonColor]).mockReturnValueOnce([seasonColor]) + .mockReturnValueOnce([monthNames]).mockReturnValueOnce([invalidMonthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([monthLeapDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'none';})};})}).mockReturnValueOnce([seasonNames]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonColor]).mockReturnValueOnce([seasonColor]) + .mockReturnValueOnce([monthNames]).mockReturnValueOnce([monthDays]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([intercalary]).mockReturnValueOnce([monthLeapDays]).mockReturnValueOnce([weekdayNames]).mockReturnValueOnce({find: jest.fn(()=>{return {val: jest.fn(() => {return 'none';})};})}).mockReturnValueOnce([seasonNames]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonStart]).mockReturnValueOnce([seasonColor]).mockReturnValueOnce([seasonColor]) }; } @@ -69,6 +80,7 @@ class FormApplication extends Application{ activateEditor(a: string){} saveEditor(a: string){} + bringToTop(){} } // @ts-ignore diff --git a/__mocks__/game.ts b/__mocks__/game.ts index d0f721e0..02761892 100644 --- a/__mocks__/game.ts +++ b/__mocks__/game.ts @@ -1,7 +1,7 @@ /** * This file mocks the FoundryVTT game global so that it can be used in testing */ -import {SettingNames} from "../src/constants"; +import {GameWorldTimeIntegrations, MoonIcons, MoonYearResetOptions, SettingNames} from "../src/constants"; //@ts-ignore const local: Localization = { @@ -39,12 +39,14 @@ const game = { data: null, i18n: local, user: user, + paused: true, // @ts-ignore settings: { get: jest.fn((moduleName: string, settingName: string): any => { switch (settingName){ case SettingNames.AllowPlayersToAddNotes: case SettingNames.DefaultNoteVisibility: + case SettingNames.ImportRan: return false; case SettingNames.YearConfiguration: return {numericRepresentation: 0, prefix: '', postfix: '', showWeekdayHeadings: true}; @@ -55,17 +57,52 @@ const game = { case SettingNames.LeapYearRule: return {rule: 'none', customMod: 0}; case SettingNames.CurrentDate: - return {year: 0, month: 1, day: 2}; + return {year: 0, month: 1, day: 2, seconds: 3}; case SettingNames.Notes: return [[{year: 0, month: 1, day: 2, title:'', content:'', author:'', playerVisible: false, id: 'abc123'}]]; + case SettingNames.GeneralConfiguration: + return {gameWorldTimeIntegration: GameWorldTimeIntegrations.None, showClock: false, playersAddNotes: false} + case SettingNames.TimeConfiguration: + return {hoursInDay:0, minutesInHour: 1, secondsInMinute: 2, gameTimeRatio: 3}; + case SettingNames.SeasonConfiguration: + return [[{name:'', startingMonth: 1, startingDay: 1, color: '#ffffff', customColor: ''}]]; + case SettingNames.MoonConfiguration: + return [[{"name":"","cycleLength":0,"firstNewMoon":{"yearReset":"none","yearX":0,"year":0,"month":1,"day":1},"phases":[{"name":"","length":3.69,"icon":"new","singleDay":true}],"color":"#ffffff","cycleDayAdjust":0}]]; } }), register: jest.fn((moduleName: string, settingName: string, data: any) => {}), set: jest.fn((moduleName: string, settingName: string, data: any) => {return Promise.resolve(true);}) + }, + time: { + worldTime: 10, + advance: jest.fn() + }, + socket: { + on: jest.fn(), + emit: jest.fn() + }, + combats: { + size: 0, + find: jest.fn((v)=>{ + return v.call(undefined, {started: true}); + }) + }, + modules: { + get: jest.fn() + }, + Gametime: { + DTC: { + saveUserCalendar: jest.fn() + } + }, + users: { + get: jest.fn(), + find: jest.fn((v)=>{ + return v.call(undefined, {isGM: false, active: true}); + }) } - //keyboard: null, - //modules: null }; + // @ts-ignore global.game = game; diff --git a/docs/Configuration.md b/docs/Configuration.md new file mode 100644 index 00000000..827adc32 --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,203 @@ +# Configuring Your calendar + +Configuration of the calendar is straight forward. As the GM open the configuration dialog and start customizing your calendar! + +## General Settings + +This tab allows you to set some general settings for the entire calendar. + +### Quick Setup + +#### Predefined Calendars + +This setting lets you choose from a list of predefined calendars to get your calendar started with. The following calendars can be selected to configure the game calendar: + +Calendar|Description|Initial Date +--------|-----------|------------- +Gregorian|This the standard real life calendar|The current date will be used +Eberron| This is the calendar from the Eberron setting for Dungeons and Dragons | Zarantyr 1, 998 YK +Exandrian |This is the calendar from the Exandria setting for Dungeons and Dragons | Horisal 1, 812 P.D. +Golarian | This is the calendar from the Pathfinder game | Abadius 1, 4710 AR +Greyhawk | This is the calendar from the Greyhawk setting for Dungeons and Dragons | Needfest 1, 591 cy +Harptos | This is the calendar used across Faerun in the Forgotten Realms | Hammer 1, 1495 DR +Warhammer | This is the calendar used by the Imperium in the Fantasy Warhammer game | Hexenstag 1, 2522 + +All of these calendars can be further customized after they are loaded. They are here to provide a simple starting point for your game. + +### Game World Time Integration + +These settings dictate how Simple Calendar interacts with Foundry's game world time (the in game clock). Most other modules that have timed events or deal with time tie into the game world time, so it is a great way to keep everything in sync. The different settings for how Simple Calendar can interact with the game world time are: + +Option|Description|Update Game World Time|When Game World Time is Updated +--------|--------------------|-------------------------------------------------|---------------------------------------------------------- +None (default)|Simple Calendar does not interact with the game world time at all. This setting is ideal if you want to keep Simple Calendar isolated from other modules.|Does not update the game world time|Simple Calendar is not updated when the game world time is updated by something else. +Self|Treats Simple Calendar as the authority source for the game world time. This setting is ideal when you want Simple Calendar to be in control of the games time and don't want other modules updating Simple Calendar|Updates the game world time to match what is in Simple Calendar.|Combat round changes will update Simple Calendars time. Simple Calendar will ignore updates from all others modules. +Third Party Module|This will instruct Simple Calendar to just display the Time in the game world time. All date changing controls are disabled and the changing of time relies 100% on another module. This setting is ideal if you are just want to use Simple Calenar to display the date in calendar form and/or take advantage of the notes.|Does not update the game world time.|Updates it's display everytime the game world time is changed, following what the other modules say the time is. +Mixed|This option is a blend of the self and third party options. Simple calendar can change the game world time and and changes made by other modules are reflected in Simple Calendar. This setting is ideal if you want to use Simple Calendar and another module to change the game time.|Will update the game world time|Will update it's own time based on changes to the game world time, following what other modules say the time is. + +The most common interaction with another module is likely to be with Calendar/Weather. For this module I recommend using the "Self" or "Mixed" setting. With self weather effects will still trigger from Calendar/Weather as you advance time in Simple Calendar. Only use mixed if you also want to be able to use the Calendar/Weather controls to advance time to certain points (like dawn or dusk). + +#### Show Clock + +This setting is used to show the time clock below the calendar or to hide it. Not all games care about keeping track of the specific time of day so this is a great option to disable that part. Hiding the clock also hides the controls for changing hours, minutes, seconds. + +### Notes + +These are the settings pertaining to notes. + +Setting | Description +-------- | ---------- +Note Default Player Visibility | For new notes, if by default the player visibility option is checked or not. +Players Can Add Notes | If checked players will be allowed to add their own notes to the calendar. + +### Third Party Module Import/Export + +If you have certain other modules installed and active in your game, options will appear here to either import their settings into Simple Calendar or to export Simple Calendars settings into that module. +The current supported modules for importing/exporting settings: + +#### about-time + +The [about-time](https://foundryvtt.com/packages/about-time) module is used for many other modules but can also be used on its own. + +Settings can be imported and exported between these two modules without issue. + +#### Calendar/Weather + +The [Calendar/Weather](https://foundryvtt.com/packages/calendar-weather) module is used as a way to integrate about-time with a custom calendar and additional weather effects. + +Most settings can be imported and exported between these two modules with these notable exceptions: + +- Calendar/Weather does not seem to support Leap Years at all (see this [line of code](https://github.com/DasSauerkraut/calendar-weather/blob/89b59e047c86c979b246ae385c471f4e824eaaa1/modules/dateTime.mjs#L42)) as a result: + - When Importing from Calendar/Weather no leap year rule will be set in Simple Calendar. + - When Exporting to Calendar/Weather the date will be off by the number of leap days that have passed so far. Example if there have been 4 leap years with 1 extra day each year the Calendar/Weather's calendar will be ahead by 4 days. This gets very exaggerated when using a Gregorian calendar for today's date as there have been 490 extra leap days for 2021. +- Calendar/Weather's season colors and Simple Calendar's season colors do not line up so they are not Imported or Exported. +- Calendar/Weather's season month is not properly stored: + - When Importing from Calendar/Weather every season will have its month set to be the first month. + - When Exporting to Calendar/Weather every season will have no month set. + + +## Year Settings + +This tab allows you to change some settings about the years in your game world + +### Current Year + +This section is for setting information about the current year. The settings that are available to change are listed below: + +Setting | Description +-------- | ---------- +Current Year | The Current year your game world is in. This can be any positive number. +Year Prefix | Text that will appear before the year number. +Year Postfix | Text that will appear after the year number. + +### Seasons + +This section displays all the seasons that exist in the calendar. +Seasons are optional but can provide a nice thematic for your world. +The options available for customizing each Season are listed below: + +Setting | Description +--------|------------ +Season Name | These text boxes for each season allow you to change the name of an existing season. +Starting Month | These drop downs for each season show all of the months of your calendar and allow you to choose which month this season starts in. +Starting Day | These drop downs for each season show all of the days for the selected Starting Month and allow you to choose with day the season starts on in that month. +Color | Seasons can be assigned a color, this color is used as the background color for the calendar display when it is the current season. There is a list of predefined colors that work well for standard season and the option to enter a custom color. +Custom Color | If the color option is set to Custom Color this text box will appear where you can enter a custom Hex representation of a color to use for the season. +Remove Button | These buttons for each season allow you to remove the specific season from the list. +Add New Season Button | This button will add a new season to the bottom of the list with a default name that you can then configure to your liking. +Remove All Seasons Button | This button will remove all of the seasons from the list. + +## Month Settings + +This tab displays all the months that exist in the calendar. Here you can change month names, the number of days in a month, remove a month, add a new month or remove all months. + +Setting | Description +-------- | ---------- +Month Name | These text boxes for each month allow you to change the name of an existing month. +Number of Days | These text boxes for each month allow you to change the number of days in each month. A month can have a minimum of 0 days. +Intercalary Month | An intercalary month is one that does not follow the standard month numbering and is skipped.
Example: If we were to add an intercalary month between January and February, January would still be considered the first month and February would be considered the second month. The new month does not get a number.
Intercalary months also do not count towards the years total days nor do they affect the day of the week subsequent months start on. +Include Intercalary Month In Total Day Count | When you select a month to be intercalary, an option will show to include these days as part of the years total days and have its days afect the day of the week subsequent months start on. The month though still is not numbered. +Remove Button | These buttons for each month allow you to remove the month from the list. +Add New Month Button | This button will add a new month to the bottom of the list with a default name and number of days that you can then configure to your liking. +Remove All Months Button | This button will remove all of the months from the list. + + +## Weekday Settings + +This section displays all the weekdays that exist in the calendar. Weekdays are used to determine how wide the calendar display should be and how to make month days to each day of the week. + +Setting | Description +-------- | ---------- +Weekday Name | These text boxes for each weekday allow you to change the name of an existing weekday. +Remove Button | These buttons for each weekday allow you to remove the weekday from the list. +Add New Weekday Button | This button will add a new weekday to the bottom of the list with a default name that you can then configure to your liking. +Remove All Weekdays Button | This button will remove all of the weekdays from the list. + +## Leap Year Settings + +This section allows the GM to configure how leap years work for this calendar. + +Setting | Description +-------- | ---------- +Leap Year Rule | Which ruleset to follow when determining leap years. The options are +When Leap Years Happen | **This only appears if the Custom leap year rule is selected**.
The number of years when a leap year occurs. Example a value of 5 would mean every 5th year is a leap year. +Months +After you have changed the settings to your liking don't forget to save the configuration by hitting the Save Configuration button! +List | **This only appears if the Custom or Gregorian leap year rule is selected**.
A list of months will appear that shows each month, and a textbox where you can change the number of days the corresponding month has during a leap year. A month can have a minimum of 0 leap year days. + + +## Time Settings + +This section allows the GM to configure how hours, minutes and seconds work in their world. + +Setting | Description +-------- | ---------- +Hours in a Day | This defines how many hours make up a single day. +Minutes in a Hour | This defines how many minutes make up a single hour. +Seconds in a Minute | This defines how many seconds make up a single minute. +Game Seconds Per Real Life Seconds | This is used to determine how quickly game time advances when running the Simple Calendar clock. With a value of 1, for every real life seconds 1 second passes in the game. With a value of 2 for every 1 real life seconds 2 seconds pass in game. This does support decimals for more specific control. + + +## Moon Settings + +This section allows the GM to configure the different moons of the world so that their cycles display on the calendar. + +There are lots of settings when configuring a moon to make it as customized as you would like. + +### Main Settings + +Settings | Description +---------|------------ +Moon Name | The name of the moon, used to help distinguish between moons when there are more than one. Will show when hovering over a moon on the calendar view. +Cycle Length | How many days it takes the moon to go from new moon to new moon. This field supports decimal places to get as precise as needed. +Cycle Adjustment | When calculating how many days into a cycle a given date is, adjust that by this many days. This value will most likely be a decimal of less than 1 as adjustments do not need to be that large but supports any number. +Moon Color | A color to associate with the moon. This is used to color the moon icons on the calendar view and helps distinguish between multiple moons. This is a hex color value. +Remove | Will remove this moon from the list of moons. +Add New Moon | Will add a moon to the list of moons. +Remove All Moons | Will remove all moons from the list of moons. + +### Reference New Moon Settings + +This group of settings is used to tell the Calendar of when a New Moon occurred so that it can base when others will happen based off of that date. + +Settings | Description +---------|------------ +Reference Moon Year Reset | This can be used to tell the calendar to reset the year portion of the reference new moon. Options are:
+New Moon Year| **Important**: This only shows when the Reference Moon Year Reset is set to "Do not reset reference year". This is the year of the reference new moon. +Reset Reference Moon Years | **Important**: This only shows when the Reference Moon Year Reset is set to "Reset reference year every X years". This is how often, in years, to reset the reference year. +New Moon Month | A drop down of all the months in the calendar where you can choose which month the new moon took place in. +New Moon Day | A drop down of all the days for the selected new moon month where you can choose which day the new moon took place on. + +### Phases + +This section allows you to customize the different phases of the moon. Generally there will be no need to change the defaults for this, but the option is there in case it is required. + +Settings | Description +---------|------------ +Phase Name | The name of the phase. +Phase Length | The calculated number of days this phase will last based on the total number of phases and the moons cycle length. +Phase Single Day | If this phase should only happen on 1 day, rather than over several days. This is used from the important moon phases like full moon. +Phase Icon | Select from a list of available icons to use when displaying this phase of the moon. +Remove | Removes this phase from the moons list of phases. +Add New Moon Phase | Adds a new phase to the list of phases for this moon. +Remove All Moon Phases | Removes all phases from the list of phases for this moon. diff --git a/docs/Macros.md b/docs/Macros.md new file mode 100644 index 00000000..d1d5f689 --- /dev/null +++ b/docs/Macros.md @@ -0,0 +1,33 @@ +# Macro's + +Here are all the exposed functions that can be used when creating a macro. + +## Open Simple Calendar +You can create macros that will open up the Simple Calendar interface when used. To start create a new script macro and enter this as the command: + +```javascript +SimpleCalendar.show(); +``` + +**Important**: If this macro is intended to be useable by players don't forget to configure the Macros permissions for all players. It will need to be set to at least the "Limited", permission level. + +The show function can take 3 parameters to set the year, month and day that the calendar opens up to. + +Parameter|Type|Default|Details +---------|----|-------|------- +Year | number or null | null | The year to open the calendar too. If null is passed in it will open the calendar to the year the user last viewed +Month | number or null | null | The month to open the calendar too.
The month is expected to start at 0, or be the index of the month to show. This way intercalary months can be easily chosen by using their index as they don't have a month number.
-1 can be passed in to view the last month of the year.
If null is passed in the calendar will open to the month the user last viewed. +Day | number or null | null | The day of the month to select.
The day is expected to start at 1.
-1 can be passed in to select the last day of the month.
If null is passed in the selected day will be the last day the user selected, if any. + +## Examples +All these examples assume we are using a standard Gregorian calendar. + +Open the calendar to August 2003 +```javascript +SimpleCalendar.show(2003, 7); +``` + +Open the calendar to December 1999 and select the 25th day +```javascript +SimpleCalendar.show(1999, 11, 25); +``` diff --git a/docs/Notes.md b/docs/Notes.md new file mode 100644 index 00000000..bab398d7 --- /dev/null +++ b/docs/Notes.md @@ -0,0 +1,37 @@ +#Notes + +All information around viewing, adding, editing and removing notes. + +## Note Details + +The note details shows all the information about a specific note: the date the note is for, if the note repeats and how often it does, the title of the note, the author of the note and the content of the note. +To view the note details select a day with the note icon on it and click on a note from the list below the calendar. + +![Calendar Button Location](https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/note-view.png) + + +## Adding, Editing and Removing notes + +The GM and players (if allowed by the GM) have the ability to add new notes to a day in Simple Calendar by clicking on the "Add New Note" button for a selected day. +This will open a dialog where the details of the note can be filled out. + +![Calendar Button Location](https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/note-new.png) + +Field | Description +------- | ----------- +Note Title | The title for the note, this title will appear in the listing of notes for the day. +Player Visible | If this note can be seen by players or if it is for the GM only. If a player is adding the note this option is disabled and checked by default. +Note Repeats | If this note repeats weekly, monthly, yearly or never. If the note repeats monthly and it is on a day that some months don't have (eg the 31st) months that don't go to that day will not have this note. +Details | Here you can enter the details of a note using the built in text editor. + +After all the details are filled out you can save the note. + +**Important**: If you have not saved the content in the text editor using the text editor save button, a warning will appear when you try to save the note letting you know. + +Players are able to edit or remove any notes they have added, the GM is able to edit or remove any note. +To do this click on an existing note, at the bottom of the note dialog 2 additional buttons will be visible, Edit and Delete. + +Button | Description +------- | ----------- +The Edit button | This will load the contents of the note in the same editor as creating a new note. +The Delete button | This will open up a confirmation dialog, where selecting delete again will permanently remove the note. diff --git a/docs/UpdatingDateTime.md b/docs/UpdatingDateTime.md new file mode 100644 index 00000000..a35b0f65 --- /dev/null +++ b/docs/UpdatingDateTime.md @@ -0,0 +1,43 @@ +# Updating the Current Date +The GM version of the module looks a little different, with the addition of controls to change the current date and a button to enter the configuration. + +![GM View](https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/gm-view.png) + +The above image helps shows the controls, they are detailed out below. + +## Time Controls + +These controls are specific to controlling the time of the current day. As well as starting/stopping the built-in clock. + +**Important**: These controls will not display if the [Simple Calendar's game world time integration](./Configuration.md#game-world-time-integration) is configured to set to "Third Party" OR if the [Show Clock setting](./Configuration.md#show-clock) is unchecked. + +The top controls are used to select a unit of time (second, minute or hour) that will be changed by the buttons below. + +These buttons are used to move time forward or backward by the amount listed on the button (1 or 5) + +Simple Calendar has a built-in clock that can be used to automatically advance time as real world time passes by. This clock is started or stopped by the GM using the start/stop buttons under the time controls. Check out [Simple Calendars clock](./UsingTheCalendar.md#simple-calendars-clock) for more information. + + +## Date Controls + +These controls are specific to controlling the date, day/month/year, of the calendar. + +**Important**: These controls will not display if the [Simple Calendar's game world time integration](./Configuration.md#game-world-time-integration) is configured to set to "Third Party". + +Control | Description +------- | ----------- +Day Back/Forward | This moves the current day forward or back one day. +Month Back/Forward | This moves the current month forward or back one month. The current day will be mapped to the same day as the old month, or the last day of the month if the old month has more days. +Year Back/Forward | This moves the current year forward or back one year. The current month and day will stay the same in the new year. +Apply | This will apply the changes, saving the new current day in the settings and updating all of the players calendars to reflect the new current day. + + + +## Other Controls + +These are additional controls that only the GM can access but are not specific to one of the above categories. + +Control | Description +------- | ----------- +Configuration | This opens up the configuration dialog to allow the GM to fully customize the calendar. +Add New Note Button | This will open the add notes dialog to add a new note for the selected day. diff --git a/docs/UsingTheCalendar.md b/docs/UsingTheCalendar.md new file mode 100644 index 00000000..d159a057 --- /dev/null +++ b/docs/UsingTheCalendar.md @@ -0,0 +1,35 @@ +# Accessing and Using the Calendar +The module adds a calendar button to the basic controls section of the layer controls. Clicking on this will open the module window + +![Calendar Button Location](https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/layers-button.png) + +The above image helps shows the controls, they are detailed out below. + +Control | Description +------- | ----------- +Previous/Next | Allow the user to change which month/year they are currently viewing. +Today Button | Changes the calendar so that the current day (in the game world) is visible and selected. +Blue Circle Day | This indicates the current day in the game world, can be changed by the GM +Green Circle Day | This indicates the day the user currently has selected. This will show any notes on this day. +Red Indicator | This shows on any days that have notes that the user can see. It will show the number of notes on that day up to 99. +Season Name | Appearing below the month and year if season are set up which season it current is will show. +Calendar Background Color | If seasons are set up the background color of the calendar will change to match which season is being viewed. +Moon Icons | Icons showing which phase of the moon will show on the bottom right of a day. +Current Time | If the GM wants to display a clock of the current game time it will appear below the calendar and show the current time of day. +Clock Icon | Next to the current time is a clock icon, this icon is used to indicate if the built in clock is running or not. More information [below](#simple-calendars-clock)! +Notes List | Any notes that appear in this list can be clicked on to open the note details. +Add New Note | If the GM allows players to add notes this button will appear in which a note can be added to the selected day. + +## Simple Calendars Clock + +Simple Calendar can now display a clock below the calendar. If the clock is not showing up, it is likely that the GM has turned off the clock for the current game as it will be not needed. + +The clock can be updated manually by the GM, or it can be enabled to update as real world time passes. The amount of in game time that passes as real world time passes is configurable by the GM. + +Once the clock has been started, it will begin incrementing time. The clock updates every 30 real life seconds, this is to allow time for proper syncing of the game time between all players. + +The clock icon next to the Current Time display will also update based on the state of the Simple Calendars clock. + +- **Red**: Means that Simple Calendars Clock is currently stopped and not automatically incrementing. +- **Yellow**: Means that Simple Calendars Clock has been started but because the game is paused OR there is an active combat the clock is not updating. +- **Green**: The clock will begin its animation, this means that Simple Calendars Clock is running and updating the game time as real time passes. diff --git a/docs/images/gm-view.png b/docs/images/gm-view.png index 70c5c93d..e57a5080 100644 Binary files a/docs/images/gm-view.png and b/docs/images/gm-view.png differ diff --git a/docs/images/layers-button.png b/docs/images/layers-button.png index f8d685a4..83e4dd24 100644 Binary files a/docs/images/layers-button.png and b/docs/images/layers-button.png differ diff --git a/docs/images/logo.png b/docs/images/logo.png index f9a404c9..8ee5bc7b 100644 Binary files a/docs/images/logo.png and b/docs/images/logo.png differ diff --git a/docs/images/note-view.png b/docs/images/note-view.png index 1c745ea8..a7c564bb 100644 Binary files a/docs/images/note-view.png and b/docs/images/note-view.png differ diff --git a/package-lock.json b/package-lock.json index 54fba69d..5d2d0b49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "foundryvtt-simple-calendar", - "version": "1.1.6", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -630,9 +630,9 @@ } }, "@league-of-foundry-developers/foundry-vtt-types": { - "version": "0.7.9-0", - "resolved": "https://registry.npmjs.org/@league-of-foundry-developers/foundry-vtt-types/-/foundry-vtt-types-0.7.9-0.tgz", - "integrity": "sha512-67by3XlXKOyQVqmvVp0+kQ+JYUy+NcXAev3LMhl6OnqGEK8r/mVkGsKbpP45pEJQR8/P5AzHKv24botQZBfakg==", + "version": "0.7.9-5", + "resolved": "https://registry.npmjs.org/@league-of-foundry-developers/foundry-vtt-types/-/foundry-vtt-types-0.7.9-5.tgz", + "integrity": "sha512-ac5caa/etFAaNyLU1wJ35r/qLXvjNB/D4j5gqdFWLk8fZsGaZwD0ljNB8xj2ClvbGesZtiH0Nd9ajDVL07pQcQ==", "dev": true, "requires": { "@types/howler": "2.2.1", @@ -645,15 +645,6 @@ "typescript": "^4.1.4" }, "dependencies": { - "@types/jquery": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.1.tgz", - "integrity": "sha512-Tyctjh56U7eX2b9udu3wG853ASYP0uagChJcQJXLUXEU6C/JiW5qt5dl8ao01VRj1i5pgXPAf8f1mq4+FDLRQg==", - "dev": true, - "requires": { - "@types/sizzle": "*" - } - }, "typescript": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", @@ -1131,18 +1122,18 @@ } }, "@types/copy-webpack-plugin": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@types/copy-webpack-plugin/-/copy-webpack-plugin-6.4.0.tgz", - "integrity": "sha512-f5mQG5c7xH3zLGrEmKgzLLFSGNB7Y4+4a+a1X4DvjgfbTEWEZUNNXUqGs5tBVCtb5qKPzm2z+6ixX3xirWmOCg==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@types/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz", + "integrity": "sha512-jnM0aMsaMTBr+xlMIO/fu+ZXIbSncmj4UB9ZHTXVfZJsUwGqtdfdSfz1/S8O99R9k7G5V6KhbAd8+QL0f2kUkg==", "dev": true, "requires": { - "@types/webpack": "*" + "@types/webpack": "^4" } }, "@types/eslint": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.6.tgz", - "integrity": "sha512-I+1sYH+NPQ3/tVqCeUSBwTE/0heyvtXqpIopUUArlBm0Kpocb8FbMa3AZ/ASKIFpN3rnEx932TTXDbt9OXsNDw==", + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.7.tgz", + "integrity": "sha512-EHXbc1z2GoQRqHaAT7+grxlTJ3WE2YNeD6jlpPoRc83cCoThRY+NUWjCUZaYmk51OICkPXn2hhphcWcWXgNW0Q==", "dev": true, "requires": { "@types/estree": "*", @@ -1215,15 +1206,24 @@ } }, "@types/jest": { - "version": "26.0.20", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.20.tgz", - "integrity": "sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==", + "version": "26.0.22", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.22.tgz", + "integrity": "sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw==", "dev": true, "requires": { "jest-diff": "^26.0.0", "pretty-format": "^26.0.0" } }, + "@types/jquery": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-Tyctjh56U7eX2b9udu3wG853ASYP0uagChJcQJXLUXEU6C/JiW5qt5dl8ao01VRj1i5pgXPAf8f1mq4+FDLRQg==", + "dev": true, + "requires": { + "@types/sizzle": "*" + } + }, "@types/json-schema": { "version": "7.0.7", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", @@ -1243,9 +1243,9 @@ "dev": true }, "@types/node": { - "version": "14.14.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.31.tgz", - "integrity": "sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==", + "version": "14.14.36", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.36.tgz", + "integrity": "sha512-kjivUwDJfIjngzbhooRnOLhGYz6oRFi+L+EpMjxroDYXwDw9lHrJJ43E+dJ6KAd3V3WxWAJ/qZE9XKYHhjPOFQ==", "dev": true }, "@types/normalize-package-data": { @@ -1267,9 +1267,9 @@ "dev": true }, "@types/socket.io-client": { - "version": "1.4.35", - "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.35.tgz", - "integrity": "sha512-MI8YmxFS+jMkIziycT5ickBWK1sZwDwy16mgH/j99Mcom6zRG/NimNGQ3vJV0uX5G6g/hEw0FG3w3b3sT5OUGw==", + "version": "1.4.36", + "resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-1.4.36.tgz", + "integrity": "sha512-ZJWjtFBeBy1kRSYpVbeGYTElf6BqPQUkXDlHHD4k/42byCN5Rh027f4yARHCink9sKAkbtGZXEAmR0ZCnc2/Ag==", "dev": true }, "@types/source-list-map": { @@ -1961,9 +1961,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001185", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001185.tgz", - "integrity": "sha512-Fpi4kVNtNvJ15H0F6vwmXtb3tukv3Zg3qhKkOGUq7KJ1J6b9kf4dnNgtEAFXhRsJo0gNj9W60+wBvn0JcTvdTg==", + "version": "1.0.30001204", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz", + "integrity": "sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==", "dev": true }, "capture-exit": { @@ -2175,14 +2175,14 @@ "dev": true }, "copy-webpack-plugin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-8.0.0.tgz", - "integrity": "sha512-sqGe2FsB67wV/De+sz5azQklADe4thN016od6m7iK9KbjrSc1SEgg5QZ0LN+jGx5aZR52CbuXbqOhoIbqzzXlA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-8.1.0.tgz", + "integrity": "sha512-Soiq8kXI2AZkpw3dSp18u6oU2JonC7UKv3UdXsKOmT1A5QT46ku9+6c0Qy29JDbSavQJNN1/eKGpd3QNw+cZWg==", "dev": true, "requires": { "fast-glob": "^3.2.5", "glob-parent": "^5.1.1", - "globby": "^11.0.2", + "globby": "^11.0.3", "normalize-path": "^3.0.0", "p-limit": "^3.1.0", "schema-utils": "^3.0.0", @@ -2278,16 +2278,16 @@ } }, "css-loader": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.1.1.tgz", - "integrity": "sha512-5FfhpjwtuRgxqmusDidowqmLlcb+1HgnEDMsi2JhiUrZUcoc+cqw+mUtMIF/+OfeMYaaFCLYp1TaIt9H6I/fKA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.0.tgz", + "integrity": "sha512-MfRo2MjEeLXMlUkeUwN71Vx5oc6EJnx5UQ4Yi9iUtYQvrPtwLUucYptz0hc6n++kdNcyF5olYBS4vPjJDAcLkw==", "dev": true, "requires": { "camelcase": "^6.2.0", "cssesc": "^3.0.0", "icss-utils": "^5.1.0", "loader-utils": "^2.0.0", - "postcss": "^8.2.6", + "postcss": "^8.2.8", "postcss-modules-extract-imports": "^3.0.0", "postcss-modules-local-by-default": "^4.0.0", "postcss-modules-scope": "^3.0.0", @@ -2304,9 +2304,9 @@ "dev": true }, "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -2566,9 +2566,9 @@ } }, "electron-to-chromium": { - "version": "1.3.662", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.662.tgz", - "integrity": "sha512-IGBXmTGwdVGUVTnZ8ISEvkhDfhhD+CDFndG4//BhvDcEtPYiVrzoB+rzT/Y12OQCf5bvRCrVmrUbGrS9P7a6FQ==", + "version": "1.3.701", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.701.tgz", + "integrity": "sha512-Zd9ofdIMYHYhG1gvnejQDvC/kqSeXQvtXF0yRURGxgwGqDZm9F9Fm3dYFnm5gyuA7xpXfBlzVLN1sz0FjxpKfw==", "dev": true }, "emittery": { @@ -2642,9 +2642,9 @@ } }, "es-module-lexer": { - "version": "0.3.26", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.3.26.tgz", - "integrity": "sha512-Va0Q/xqtrss45hWzP8CZJwzGSZJjDM5/MJRE3IXXnUCcVLElR9BRaE9F62BopysASyc4nM3uwhSW7FFB9nlWAA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.4.1.tgz", + "integrity": "sha512-ooYciCUtfw6/d2w56UVeqHPcoCFAiJdz5XOkYpv/Txl1HMUozpXjz/2RIQgqwKdXNDPSF1W7mJCFse3G+HDyAA==", "dev": true }, "es6-promise-polyfill": { @@ -2730,9 +2730,9 @@ "dev": true }, "events": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", - "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true }, "exec-sh": { @@ -3128,9 +3128,9 @@ "dev": true }, "globby": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", - "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", "dev": true, "requires": { "array-union": "^2.1.0", @@ -4495,9 +4495,9 @@ "dev": true }, "mini-css-extract-plugin": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.9.tgz", - "integrity": "sha512-Ac4s+xhVbqlyhXS5J/Vh/QXUz3ycXlCqoCPpg0vdfhsIBH9eg/It/9L1r1XhSCH737M1lqcWnMuWL13zcygn5A==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.4.0.tgz", + "integrity": "sha512-DyQr5DhXXARKZoc4kwvCvD95kh69dUupfuKOmBUqZ4kBTmRaRZcU32lYu3cLd6nEGXhQ1l7LzZ3F/CjItaY6VQ==", "dev": true, "requires": { "loader-utils": "^2.0.0", @@ -4572,9 +4572,9 @@ "dev": true }, "nanoid": { - "version": "3.1.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz", - "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==", + "version": "3.1.22", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.22.tgz", + "integrity": "sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==", "dev": true }, "nanomatch": { @@ -4664,9 +4664,9 @@ } }, "node-releases": { - "version": "1.1.70", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.70.tgz", - "integrity": "sha512-Slf2s69+2/uAD79pVVQo8uSiC34+g8GWY8UH2Qtqv34ZfhYrxpYpfzs9Js9d6O0mbDmALuxaTlplnBTnSELcrw==", + "version": "1.1.71", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", + "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", "dev": true }, "normalize-package-data": { @@ -5009,14 +5009,22 @@ "dev": true }, "postcss": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.6.tgz", - "integrity": "sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg==", + "version": "8.2.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.2.8.tgz", + "integrity": "sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==", "dev": true, "requires": { - "colorette": "^1.2.1", + "colorette": "^1.2.2", "nanoid": "^3.1.20", "source-map": "^0.6.1" + }, + "dependencies": { + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + } } }, "postcss-modules-extract-imports": { @@ -5147,9 +5155,9 @@ "dev": true }, "queue-microtask": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz", - "integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, "randombytes": { @@ -6096,9 +6104,9 @@ } }, "terser": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz", - "integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.6.1.tgz", + "integrity": "sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==", "dev": true, "requires": { "commander": "^2.20.0", @@ -6236,9 +6244,9 @@ } }, "ts-jest": { - "version": "26.5.3", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.3.tgz", - "integrity": "sha512-nBiiFGNvtujdLryU7MiMQh1iPmnZ/QvOskBbD2kURiI1MwqvxlxNnaAB/z9TbslMqCsSbu5BXvSSQPc5tvHGeA==", + "version": "26.5.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.4.tgz", + "integrity": "sha512-I5Qsddo+VTm94SukBJ4cPimOoFZsYTeElR2xy6H2TOVs+NsvgYglW8KuQgKoApOKuaU/Ix/vrF9ebFZlb5D2Pg==", "dev": true, "requires": { "bs-logger": "0.x", @@ -6263,17 +6271,17 @@ } }, "yargs-parser": { - "version": "20.2.6", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.6.tgz", - "integrity": "sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA==", + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", "dev": true } } }, "ts-loader": { - "version": "8.0.17", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.17.tgz", - "integrity": "sha512-OeVfSshx6ot/TCxRwpBHQ/4lRzfgyTkvi7ghDVrLXOHzTbSK413ROgu/xNqM72i3AFeAIJgQy78FwSMKmOW68w==", + "version": "8.0.18", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-8.0.18.tgz", + "integrity": "sha512-hRZzkydPX30XkLaQwJTDcWDoxZHK6IrEMDQpNd7tgcakFruFkeUp/aY+9hBb7BUGb+ZWKI0jiOGMo0MckwzdDQ==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -6412,9 +6420,9 @@ "dev": true }, "uglify-js": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.0.tgz", - "integrity": "sha512-TWYSWa9T2pPN4DIJYbU9oAjQx+5qdV5RUDxwARg8fmJZrD/V27Zj0JngW5xg1DFz42G0uDYl2XhzF6alSzD62w==", + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.2.tgz", + "integrity": "sha512-SbMu4D2Vo95LMC/MetNaso1194M1htEA+JrqE9Hk+G2DhI+itfS9TRu9ZKeCahLDNa/J3n4MqUJ/fOHMzQpRWw==", "dev": true, "optional": true }, @@ -6618,9 +6626,9 @@ "dev": true }, "webpack": { - "version": "5.21.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.21.2.tgz", - "integrity": "sha512-xHflCenx+AM4uWKX71SWHhxml5aMXdy2tu/vdi4lClm7PADKxlyDAFFN1rEFzNV0MAoPpHtBeJnl/+K6F4QBPg==", + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.28.0.tgz", + "integrity": "sha512-1xllYVmA4dIvRjHzwELgW4KjIU1fW4PEuEnjsylz7k7H5HgPOctIq7W1jrt3sKH9yG5d72//XWzsHhfoWvsQVg==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.0", @@ -6632,7 +6640,7 @@ "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.7.0", - "es-module-lexer": "^0.3.26", + "es-module-lexer": "^0.4.0", "eslint-scope": "^5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", @@ -6649,9 +6657,9 @@ }, "dependencies": { "acorn": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.0.5.tgz", - "integrity": "sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.0.tgz", + "integrity": "sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==", "dev": true } } diff --git a/package.json b/package.json index ce2635f2..a5b5e97b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "foundryvtt-simple-calendar", "description": "A simple calendar module for keeping track of game days and events.", - "version": "1.1.8", + "version": "1.2.0", "author": "Dean Vigoren (vigorator)", "keywords": [ "foundryvtt", @@ -23,25 +23,25 @@ "test": "jest" }, "devDependencies": { - "@league-of-foundry-developers/foundry-vtt-types": "^0.7.9-0", - "@types/copy-webpack-plugin": "^6.4.0", - "@types/jest": "^26.0.20", - "@types/node": "^14.14.31", + "@league-of-foundry-developers/foundry-vtt-types": "^0.7.9-5", + "@types/copy-webpack-plugin": "^6.4.1", + "@types/jest": "^26.0.22", + "@types/node": "^14.14.36", "clean-webpack-plugin": "^3.0.0", - "copy-webpack-plugin": "^8.0.0", + "copy-webpack-plugin": "^8.1.0", "cross-env": "^7.0.3", - "css-loader": "^5.1.1", + "css-loader": "^5.2.0", "jest": "^26.6.3", "jest-ts-webcompat-resolver": "^1.0.0", - "mini-css-extract-plugin": "^1.3.9", + "mini-css-extract-plugin": "^1.4.0", "sass": "^1.32.8", "sass-loader": "^11.0.1", - "ts-jest": "^26.5.3", - "ts-loader": "^8.0.17", + "ts-jest": "^26.5.4", + "ts-loader": "^8.0.18", "ts-node": "^9.1.1", "tsconfig-paths": "^3.9.0", "typescript": "^4.1.3", - "webpack": "^5.21.2", + "webpack": "^5.28.0", "webpack-cli": "^4.5.0" } } diff --git a/src/classes/game-settings.test.ts b/src/classes/game-settings.test.ts index e3a4c5f3..89213c9c 100644 --- a/src/classes/game-settings.test.ts +++ b/src/classes/game-settings.test.ts @@ -13,8 +13,12 @@ import Month from "./month"; import {Weekday} from "./weekday"; import {Note} from "./note"; import LeapYear from "./leap-year"; -import {LeapYearRules} from "../constants"; +import {GameWorldTimeIntegrations, LeapYearRules, MoonIcons, MoonYearResetOptions} from "../constants"; +import {GeneralSettings, TimeConfig} from "../interfaces"; import Mock = jest.Mock; +import Time from "./time"; +import Season from "./season"; +import Moon from "./moon"; describe('Game Settings Class Tests', () => { @@ -62,7 +66,12 @@ describe('Game Settings Class Tests', () => { SimpleCalendar.instance = new SimpleCalendar(); GameSettings.RegisterSettings(); expect(game.settings.register).toHaveBeenCalled(); - expect(game.settings.register).toHaveBeenCalledTimes(7); + expect(game.settings.register).toHaveBeenCalledTimes(12); + }); + + test('Get Import Ran', () => { + expect(GameSettings.GetImportRan()).toBe(false); + expect(game.settings.get).toHaveBeenCalled(); }); test('Get Default Note Visibility', () => { @@ -70,13 +79,18 @@ describe('Game Settings Class Tests', () => { expect(game.settings.get).toHaveBeenCalled(); }); + test('Load General Settings', () => { + expect(GameSettings.LoadGeneralSettings()).toStrictEqual({gameWorldTimeIntegration: GameWorldTimeIntegrations.None, showClock: false, playersAddNotes: false}); + expect(game.settings.get).toHaveBeenCalled(); + }); + test('Load Year Data', () => { expect(GameSettings.LoadYearData()).toStrictEqual({numericRepresentation: 0, prefix: '', postfix: '', showWeekdayHeadings: true}); expect(game.settings.get).toHaveBeenCalled(); }); test('Load Current Date', () => { - expect(GameSettings.LoadCurrentDate()).toStrictEqual({year: 0, month: 1, day: 2}); + expect(GameSettings.LoadCurrentDate()).toStrictEqual({year: 0, month: 1, day: 2, seconds: 3}); expect(game.settings.get).toHaveBeenCalled(); }); @@ -102,11 +116,38 @@ describe('Game Settings Class Tests', () => { expect(GameSettings.LoadWeekdayData()).toStrictEqual([]); }); + test('Load Season Data', () => { + expect(GameSettings.LoadSeasonData()).toStrictEqual([{name:'', startingMonth: 1, startingDay: 1, color: '#ffffff', customColor: ''}]); + expect(game.settings.get).toHaveBeenCalled(); + (game.settings.get).mockReturnValueOnce(false); + expect(GameSettings.LoadSeasonData()).toStrictEqual([]); + (game.settings.get).mockReturnValueOnce([]); + expect(GameSettings.LoadSeasonData()).toStrictEqual([]); + (game.settings.get).mockReturnValueOnce([false]); + expect(GameSettings.LoadSeasonData()).toStrictEqual([]); + }); + + test('Load Moon Data', () => { + expect(GameSettings.LoadMoonData()).toStrictEqual([{"name":"","cycleLength":0,"firstNewMoon":{"yearReset":"none","yearX":0,"year":0,"month":1,"day":1},"phases":[{"name":"","length":3.69,"icon":"new","singleDay":true}],"color":"#ffffff","cycleDayAdjust":0}]); + expect(game.settings.get).toHaveBeenCalled(); + (game.settings.get).mockReturnValueOnce(false); + expect(GameSettings.LoadMoonData()).toStrictEqual([]); + (game.settings.get).mockReturnValueOnce([]); + expect(GameSettings.LoadMoonData()).toStrictEqual([]); + (game.settings.get).mockReturnValueOnce([false]); + expect(GameSettings.LoadMoonData()).toStrictEqual([]); + }); + test('Load Leap Year Rule', () => { expect(GameSettings.LoadLeapYearRules()).toStrictEqual({rule: 'none', customMod: 0}); expect(game.settings.get).toHaveBeenCalled(); }); + test('Load Time Data', () => { + expect(GameSettings.LoadTimeData()).toStrictEqual({hoursInDay:0, minutesInHour: 1, secondsInMinute: 2, gameTimeRatio: 3}); + expect(game.settings.get).toHaveBeenCalled(); + }); + test('Load Notes', () => { expect(GameSettings.LoadNotes()).toStrictEqual([{year: 0, month: 1, day: 2, title:'', content:'', author:'', playerVisible: false, id: "abc123"}]); expect(game.settings.get).toHaveBeenCalled(); @@ -118,11 +159,38 @@ describe('Game Settings Class Tests', () => { expect(GameSettings.LoadNotes()).toStrictEqual([]); }); + test('Set Import Ran', () => { + // @ts-ignore + game.user.isGM = false; + expect(GameSettings.SetImportRan(true)).resolves.toBe(false); + // @ts-ignore + game.user.isGM = true; + expect(GameSettings.SetImportRan(true)).resolves.toBe(true); + expect(game.settings.set).toHaveBeenCalled(); + }); + + test('Save General Settings', () => { + // @ts-ignore + game.user.isGM = false; + let gs: GeneralSettings = {gameWorldTimeIntegration: GameWorldTimeIntegrations.None, showClock: false, playersAddNotes: false}; + expect(GameSettings.SaveGeneralSettings(gs)).resolves.toBe(false); + // @ts-ignore + game.user.isGM = true; + expect(GameSettings.SaveGeneralSettings(gs)).resolves.toBe(false); + + gs.showClock = true; + expect(GameSettings.SaveGeneralSettings(gs)).resolves.toBe(true); + expect(game.settings.set).toHaveBeenCalled(); + }); + test('Save Current Date', () => { jest.spyOn(console, 'error').mockImplementation(); + // @ts-ignore + game.user.isGM = false; const year = new Year(0); const month = new Month('T', 1, 10); year.months.push(month); + year.time.seconds = 3; expect(GameSettings.SaveCurrentDate(year)).resolves.toBe(false); expect(console.error).toHaveBeenCalledTimes(1); // @ts-ignore @@ -197,6 +265,35 @@ describe('Game Settings Class Tests', () => { expect(game.settings.set).toHaveBeenCalledTimes(1); }); + test('Save Season Configuration', () => { + // @ts-ignore + game.user.isGM = false; + const season = new Season('', 1, 1); + season.customColor = ''; + expect(GameSettings.SaveSeasonConfiguration([season])).resolves.toBe(false); + // @ts-ignore + game.user.isGM = true; + expect(GameSettings.SaveSeasonConfiguration([season])).resolves.toBe(false); + expect(game.settings.set).toHaveBeenCalledTimes(0); + season.name = 'Spring'; + expect(GameSettings.SaveSeasonConfiguration([season])).resolves.toBe(true); + expect(game.settings.set).toHaveBeenCalledTimes(1); + }); + + test('Save Moon Configuration', () => { + // @ts-ignore + game.user.isGM = false; + const moon = new Moon('', 0); + expect(GameSettings.SaveMoonConfiguration([moon])).resolves.toBe(false); + // @ts-ignore + game.user.isGM = true; + expect(GameSettings.SaveMoonConfiguration([moon])).resolves.toBe(false); + expect(game.settings.set).toHaveBeenCalledTimes(0); + moon.name = "Moon"; + expect(GameSettings.SaveMoonConfiguration([moon])).resolves.toBe(false); + expect(game.settings.set).toHaveBeenCalledTimes(1); + }); + test('Save Leap Year Rule', () => { // @ts-ignore game.user.isGM = false; @@ -212,13 +309,33 @@ describe('Game Settings Class Tests', () => { expect(game.settings.set).toHaveBeenCalledTimes(1); }); - test('Save Notes', () => { + test('Save Time Configuration', () => { + // @ts-ignore + game.user.isGM = false; + let gs = new Time(0, 1, 2); + gs.gameTimeRatio = 3; + expect(GameSettings.SaveTimeConfiguration(gs)).resolves.toBe(false); + // @ts-ignore + game.user.isGM = true; + expect(GameSettings.SaveTimeConfiguration(gs)).resolves.toBe(false); + + gs.gameTimeRatio = 4; + expect(GameSettings.SaveTimeConfiguration(gs)).resolves.toBe(true); + expect(game.settings.set).toHaveBeenCalled(); + }); + + test('Save Notes', async () => { const note = new Note(); note.year = 0; note.month = 1; note.day = 2; - expect(GameSettings.SaveNotes([note])).resolves.toBe(true); - expect(game.settings.set).toHaveBeenCalled(); + await GameSettings.SaveNotes([note]); + expect(game.settings.set).toHaveBeenCalledTimes(1); + // @ts-ignore + game.user.isGM = false; + await GameSettings.SaveNotes([note]); + expect(game.settings.set).toHaveBeenCalledTimes(1); + expect(game.socket.emit).toHaveBeenCalledTimes(1); }); test('Set Default Note Visibility', () => { diff --git a/src/classes/game-settings.ts b/src/classes/game-settings.ts index 0066cdbe..097c48ad 100644 --- a/src/classes/game-settings.ts +++ b/src/classes/game-settings.ts @@ -1,12 +1,23 @@ import Year from "./year"; import {Logger} from "./logging"; -import {CurrentDateConfig, MonthConfig, WeekdayConfig, YearConfig, NoteConfig, LeapYearConfig} from "../interfaces"; -import {ModuleName, SettingNames} from "../constants"; +import { + CurrentDateConfig, + MonthConfig, + WeekdayConfig, + YearConfig, + NoteConfig, + LeapYearConfig, + TimeConfig, GeneralSettings, SimpleCalendarSocket, SeasonConfiguration, MoonConfiguration +} from "../interfaces"; +import {ModuleName, ModuleSocketName, SettingNames, SocketTypes} from "../constants"; import SimpleCalendar from "./simple-calendar"; import Month from "./month"; import {Weekday} from "./weekday"; import {Note} from "./note"; import LeapYear from "./leap-year"; +import Time from "./time"; +import Season from "./season"; +import Moon from "./moon"; export class GameSettings { /** @@ -45,6 +56,13 @@ export class GameSettings { * Register the settings this module needs to use with the game */ static RegisterSettings(){ + game.settings.register(ModuleName, SettingNames.GeneralConfiguration, { + name: "General Configuration", + scope: "world", + config: false, + type: Object, + onChange: SimpleCalendar.instance.settingUpdate.bind(SimpleCalendar.instance, true, 'general') + }); game.settings.register(ModuleName, SettingNames.YearConfiguration, { name: "Year Configuration", scope: "world", @@ -97,6 +115,44 @@ export class GameSettings { default: [], onChange: SimpleCalendar.instance.loadNotes.bind(SimpleCalendar.instance, true) }); + game.settings.register(ModuleName, SettingNames.TimeConfiguration, { + name: "Time", + scope: "world", + config: false, + type: Object, + default: {}, + onChange: SimpleCalendar.instance.settingUpdate.bind(SimpleCalendar.instance, true, 'time') + }); + game.settings.register(ModuleName, SettingNames.ImportRan, { + name: "Import", + scope: "world", + config: false, + type: Boolean, + default: false + }); + game.settings.register(ModuleName, SettingNames.SeasonConfiguration, { + name: "Season Configuration", + scope: "world", + config: false, + type: Array, + default: [], + onChange: SimpleCalendar.instance.settingUpdate.bind(SimpleCalendar.instance, true, 'season') + }); + game.settings.register(ModuleName, SettingNames.MoonConfiguration, { + name: "Moon Configuration", + scope: "world", + config: false, + type: Array, + default: [], + onChange: SimpleCalendar.instance.settingUpdate.bind(SimpleCalendar.instance, true, 'moon') + }); + } + + /** + * Gets if the import question has been run for modules + */ + static GetImportRan(){ + return game.settings.get(ModuleName, SettingNames.ImportRan); } /** @@ -107,12 +163,19 @@ export class GameSettings { return game.settings.get(ModuleName, SettingNames.DefaultNoteVisibility); } + /** + * Loads the general settings from the game world settings + */ + static LoadGeneralSettings(): GeneralSettings { + return game.settings.get(ModuleName, SettingNames.GeneralConfiguration); + } + /** * Loads the year configuration from the game world settings * @return {YearConfig} */ static LoadYearData(): YearConfig { - return game.settings.get(ModuleName, SettingNames.YearConfiguration); + return game.settings.get(ModuleName, SettingNames.YearConfiguration); } /** @@ -120,7 +183,7 @@ export class GameSettings { * @return {Array.} */ static LoadCurrentDate(): CurrentDateConfig { - return game.settings.get(ModuleName, SettingNames.CurrentDate); + return game.settings.get(ModuleName, SettingNames.CurrentDate); } /** @@ -153,12 +216,49 @@ export class GameSettings { return returnData; } + /** + * Loads the season configuration from the game world settings + * @return {Array.} + */ + static LoadSeasonData(): SeasonConfiguration[] { + let returnData: SeasonConfiguration[] = []; + let seasonData = game.settings.get(ModuleName, SettingNames.SeasonConfiguration); + if(seasonData && seasonData.length) { + if (Array.isArray(seasonData[0])) { + returnData = seasonData[0]; + } + } + return returnData; + } + + /** + * Loads the moon configuration from the game world settings + * @return {Array.} + */ + static LoadMoonData(): MoonConfiguration[] { + let returnData: MoonConfiguration[] = []; + let moonData = game.settings.get(ModuleName, SettingNames.MoonConfiguration); + if(moonData && moonData.length) { + if (Array.isArray(moonData[0])) { + returnData = moonData[0]; + } + } + return returnData; + } + /** * Loads the leap year rules from the settings * @return {LeapYearConfig} */ static LoadLeapYearRules(): LeapYearConfig { - return game.settings.get(ModuleName, SettingNames.LeapYearRule); + return game.settings.get(ModuleName, SettingNames.LeapYearRule); + } + + /** + * Loads the time configuration from the game world settings + */ + static LoadTimeData(): TimeConfig { + return game.settings.get(ModuleName, SettingNames.TimeConfiguration); } /** @@ -176,6 +276,35 @@ export class GameSettings { return returnData; } + /** + * Sets the import ran setting + * @param {boolean} ran If the import was ran/asked about + */ + static async SetImportRan(ran: boolean){ + if(GameSettings.IsGm()){ + await game.settings.set(ModuleName, SettingNames.ImportRan, ran); + return true; + } + return false; + } + + /** + * Saves the general settings to the world settings + * @param {GeneralSettings} settings The settings to save + */ + static async SaveGeneralSettings(settings: GeneralSettings): Promise { + if(GameSettings.IsGm()){ + Logger.debug('Saving General Settings.'); + const currentSettings = GameSettings.LoadGeneralSettings(); + if(JSON.stringify(settings) !== JSON.stringify(currentSettings)){ + return game.settings.set(ModuleName, SettingNames.GeneralConfiguration, settings).then(()=>{return true;}); + } else { + Logger.debug('General Settings have not changed, not updating.'); + } + } + return false + } + /** * Saves the current date to the world settings * @param {Year} year The year that has the current date @@ -191,9 +320,10 @@ export class GameSettings { const newDate: CurrentDateConfig = { year: year.numericRepresentation, month: currentMonth.numericRepresentation, - day: currentDay.numericRepresentation + day: currentDay.numericRepresentation, + seconds: year.time.seconds }; - if(currentDate.year !== newDate.year || currentDate.month !== newDate.month || currentDate.day !== newDate.day){ + if(currentDate.year !== newDate.year || currentDate.month !== newDate.month || currentDate.day !== newDate.day || currentDate.seconds !== newDate.seconds){ return game.settings.set(ModuleName, SettingNames.CurrentDate, newDate); } else { Logger.debug('Current Date data has not changed, not updating settings'); @@ -280,6 +410,49 @@ export class GameSettings { return false; } + /** + * Saves the passed in season configuration in the world settings + * @param {Array.>} seasons List of seasons + */ + static async SaveSeasonConfiguration(seasons: Season[]): Promise { + if(GameSettings.IsGm()){ + Logger.debug('Saving season configuration.'); + const currentConfig = JSON.stringify(GameSettings.LoadSeasonData()); + const newConfig: SeasonConfiguration[] = seasons.map(s => {return {name: s.name, startingMonth: s.startingMonth, startingDay: s.startingDay, color: s.color, customColor: s.customColor}}); + if(currentConfig !== JSON.stringify(newConfig)){ + return game.settings.set(ModuleName, SettingNames.SeasonConfiguration, newConfig).then(() => {return true;}); + } else { + Logger.debug('Season configuration has not changed, not updating.'); + } + } + return false; + } + + /** + * Saves the passed in moon configuration in the world settings + * @param {Array.} moons List of moons + */ + static async SaveMoonConfiguration(moons: Moon[]): Promise { + if(GameSettings.IsGm()){ + Logger.debug('Saving moon configuration.'); + const currentConfig = JSON.stringify(GameSettings.LoadMoonData()); + const newConfig: MoonConfiguration[] = moons.map(m => {return { + name: m.name, + cycleLength: m.cycleLength, + firstNewMoon: m.firstNewMoon, + phases: m.phases, + color: m.color, + cycleDayAdjust: m.cycleDayAdjust + };}); + if(currentConfig !== JSON.stringify(newConfig)){ + return game.settings.set(ModuleName, SettingNames.MoonConfiguration, newConfig).then(() => {return true;}); + } else { + Logger.debug('Moon configuration has not changed, not updating.'); + } + } + return false; + } + /** * Saves the passed in leap year configuration into the world settings * @param {LeapYear} leapYear The leap year settings to save @@ -302,6 +475,29 @@ export class GameSettings { return false; } + /** + * Saves the time configuration into the world settings + * @param {Time} time The time object to save + */ + static async SaveTimeConfiguration(time: Time): Promise { + if(game.user && game.user.isGM) { + Logger.debug(`Saving time configuration.`); + const current = GameSettings.LoadTimeData(); + const newtc: TimeConfig = { + hoursInDay: time.hoursInDay, + minutesInHour: time.minutesInHour, + secondsInMinute: time.secondsInMinute, + gameTimeRatio: time.gameTimeRatio + }; + if(JSON.stringify(current) !== JSON.stringify(newtc)){ + return game.settings.set(ModuleName, SettingNames.TimeConfiguration, newtc).then(() => { return true }); + } else { + Logger.debug('Time configuration has not changed, not updating settings'); + } + } + return false; + } + /** * Saves the passed in notes into the world settings * @param {Array.} notes The notes to save @@ -320,7 +516,15 @@ export class GameSettings { id: w.id, repeats: w.repeats };}); - return game.settings.set(ModuleName, SettingNames.Notes, newConfig).then(() => {return true;}); + if(GameSettings.IsGm()){ + return game.settings.set(ModuleName, SettingNames.Notes, newConfig).then(() => {return true;}); + } else { + const socketData = {type: SocketTypes.journal, data: {notes: notes}}; + Logger.debug(`User saving notes...`); + await game.socket.emit(ModuleSocketName, socketData); + return; + } + } /** diff --git a/src/classes/handlebars-helpers.test.ts b/src/classes/handlebars-helpers.test.ts index 7d149996..f193db2b 100644 --- a/src/classes/handlebars-helpers.test.ts +++ b/src/classes/handlebars-helpers.test.ts @@ -20,7 +20,7 @@ describe('Handlebars Helpers Tests', () => { test('Register', () => { HandlebarsHelpers.Register(); - expect(Handlebars.registerHelper).toHaveBeenCalledTimes(3); + expect(Handlebars.registerHelper).toHaveBeenCalledTimes(4); }); test('Calendar Width', () => { @@ -36,6 +36,11 @@ describe('Handlebars Helpers Tests', () => { // @ts-ignore game.user.isGM = true; expect(HandlebarsHelpers.CalendarRowWidth()).toBe('width:352px;'); + if(SimpleCalendar.instance.currentYear){ + SimpleCalendar.instance.currentYear.generalSettings.showClock = true; + expect(HandlebarsHelpers.CalendarRowWidth()).toBe('width:550px;'); + } + }); test('Day Has Notes', () => { @@ -50,7 +55,7 @@ describe('Handlebars Helpers Tests', () => { SimpleCalendar.instance.currentYear.months[0].visible = true; expect(HandlebarsHelpers.DayHasNotes(options)).toBe(''); options.hash['day'].numericRepresentation = 2; - expect(HandlebarsHelpers.DayHasNotes(options)).toBe(`1`); + expect(HandlebarsHelpers.DayHasNotes(options)).toBe(`1`); for(let i = 0; i < 99; i++){ var n = new Note() @@ -60,10 +65,23 @@ describe('Handlebars Helpers Tests', () => { SimpleCalendar.instance.notes.push(n); } expect(SimpleCalendar.instance.notes.length).toBe(100); - expect(HandlebarsHelpers.DayHasNotes(options)).toBe(`99`); + expect(HandlebarsHelpers.DayHasNotes(options)).toBe(`99`); } else { fail('Current year is not set'); } }); + + test('Day Moon Phase', () => { + const options: any = {hash:{}}; + expect(HandlebarsHelpers.DayMoonPhase(options)).toBe(''); + options.hash['day'] = {numericRepresentation: 1}; + expect(HandlebarsHelpers.DayMoonPhase(options)).toBe(''); + SimpleCalendar.instance.settingUpdate(); + if(SimpleCalendar.instance.currentYear){ + expect(HandlebarsHelpers.DayMoonPhase(options)).toBe(''); + SimpleCalendar.instance.currentYear.moons[0].phases[0].singleDay = false; + expect(HandlebarsHelpers.DayMoonPhase(options)).toBe(''); + } + }); }); diff --git a/src/classes/handlebars-helpers.ts b/src/classes/handlebars-helpers.ts index f8620528..f52b5d1c 100644 --- a/src/classes/handlebars-helpers.ts +++ b/src/classes/handlebars-helpers.ts @@ -13,6 +13,7 @@ export default class HandlebarsHelpers{ Handlebars.registerHelper("calendar-width", HandlebarsHelpers.CalendarWidth); Handlebars.registerHelper("calendar-row-width", HandlebarsHelpers.CalendarRowWidth); Handlebars.registerHelper("day-has-note", HandlebarsHelpers.DayHasNotes); + Handlebars.registerHelper("day-moon-phase", HandlebarsHelpers.DayMoonPhase); } /** @@ -21,7 +22,8 @@ export default class HandlebarsHelpers{ */ static CalendarWidth(){ if(SimpleCalendar.instance.currentYear){ - return `width:${(SimpleCalendar.instance.currentYear.weekdays.length * 40) + 12}px;`; + let width = (SimpleCalendar.instance.currentYear.weekdays.length * 40) + 12; + return `width:${width}px;`; } return ''; } @@ -32,8 +34,11 @@ export default class HandlebarsHelpers{ */ static CalendarRowWidth(){ if(GameSettings.IsGm() && SimpleCalendar.instance.currentYear){ - let width = (SimpleCalendar.instance.currentYear.weekdays.length * 40) + 312; - return `width:${width}px;`; + let width = (SimpleCalendar.instance.currentYear.weekdays.length * 40) + 12; + if(SimpleCalendar.instance.currentYear.generalSettings.showClock && width < 250){ + width = 250; + } + return `width:${width+300}px;`; } return ''; } @@ -52,9 +57,29 @@ export default class HandlebarsHelpers{ const notes = SimpleCalendar.instance.notes.filter(n => n.isVisible(year, month.numericRepresentation, day)); if(notes.length){ const count = notes.length < 100? notes.length : 99; - return `${count}`; + return `${count}`; + } + } + } + return ''; + } + + /** + * Checks to see the current phase of the moon for the given day + * @param {*} options The options object passed from Handlebars + * @return {string} + */ + static DayMoonPhase(options: any){ + if(options.hash.hasOwnProperty('day') && SimpleCalendar.instance.currentYear){ + const day = options.hash['day']; + let html = '' + for(let i = 0; i < SimpleCalendar.instance.currentYear.moons.length; i++){ + const mp = SimpleCalendar.instance.currentYear.moons[i].getMoonPhase(SimpleCalendar.instance.currentYear, 'visible', day); + if(mp.singleDay || day.selected || day.current){ + html += ``; } } + return html; } return ''; } diff --git a/src/classes/importer.test.ts b/src/classes/importer.test.ts new file mode 100644 index 00000000..db709d55 --- /dev/null +++ b/src/classes/importer.test.ts @@ -0,0 +1,251 @@ +/** + * @jest-environment jsdom + */ +import "../../__mocks__/game"; +import "../../__mocks__/form-application"; +import "../../__mocks__/application"; +import "../../__mocks__/handlebars"; +import "../../__mocks__/event"; +import "../../__mocks__/dialog"; + +import Year from "./year"; +import Month from "./month"; +import Importer from "./importer"; +import {LeapYearRules} from "../constants"; +import {Weekday} from "./weekday"; +import Mock = jest.Mock; +import Season from "./season"; +import Moon from "./moon"; + +describe('Importer Class Tests', () => { + let y: Year; + + beforeEach(() => { + y = new Year(0); + y.months.push(new Month('M', 1, 5)); + y.months.push(new Month('T', 2, 15)); + y.months.push(new Month('W', 3, 1)); + y.months[2].intercalary = true; + y.weekdays.push(new Weekday(1, 'S')); + y.seasons.push(new Season('S', 1, 1)); + y.moons.push(new Moon('M', 1)); + // @ts-ignore + game.user.isGM = true; + }); + + test('Import About Time', () => { + const mockAboutTime = { + "clock_start_year": 1, + "first_day": 1, + "hours_per_day": 12, + "seconds_per_minute": 30, + "minutes_per_hour": 30, + "has_year_0": true, + "month_len": { + "Month 1": { + "days": [10,10], + "intercalary": false + }, + "Month 2": { + "days": [10,11], + "intercalary": false + }, + "Month 3": { + "days": [1,1], + "intercalary": true + } + }, + "_month_len": {}, + "weekdays": ["S","M","T"], + "leap_year_rule": '', + "notes": {} + }; + (game.settings.get).mockReturnValueOnce(mockAboutTime); + Importer.importAboutTime(y); + + expect(y.time.hoursInDay).toBe(12); + expect(y.time.minutesInHour).toBe(30); + expect(y.time.secondsInMinute).toBe(30); + expect(y.weekdays.length).toBe(3); + expect(y.weekdays[0].name).toBe('S'); + expect(y.weekdays[1].name).toBe('M'); + expect(y.weekdays[2].name).toBe('T'); + expect(y.months.length).toBe(3); + expect(y.months[0].name).toBe('Month 1'); + expect(y.months[0].numberOfDays).toBe(10); + expect(y.months[0].numberOfLeapYearDays).toBe(10); + expect(y.months[0].intercalary).toBe(false); + expect(y.months[1].name).toBe('Month 2'); + expect(y.months[1].numberOfDays).toBe(10); + expect(y.months[1].numberOfLeapYearDays).toBe(11); + expect(y.months[1].intercalary).toBe(false); + expect(y.months[2].name).toBe('Month 3'); + expect(y.months[2].numberOfDays).toBe(1); + expect(y.months[2].numberOfLeapYearDays).toBe(1); + expect(y.months[2].intercalary).toBe(true); + expect(y.leapYearRule.rule).toBe(LeapYearRules.None); + + (game.settings.get).mockReturnValueOnce(mockAboutTime); + mockAboutTime.leap_year_rule = '(year) => Math.floor(year / 4) - Math.floor(year / 100) + Math.floor(year / 400)'; + Importer.importAboutTime(y); + expect(y.leapYearRule.rule).toBe(LeapYearRules.Gregorian); + + (game.settings.get).mockReturnValueOnce(mockAboutTime); + mockAboutTime.leap_year_rule = '(year) => Math.floor(year / 8 + 1)'; + Importer.importAboutTime(y); + expect(y.leapYearRule.rule).toBe(LeapYearRules.Custom); + expect(y.leapYearRule.customMod).toBe(8); + }); + + test('Export About Time', async () => { + await Importer.exportToAboutTime(y); + //@ts-ignore + expect(game.Gametime.DTC.saveUserCalendar).toHaveBeenCalledTimes(1); + + y.leapYearRule.rule = LeapYearRules.Gregorian; + (game.settings.get).mockReturnValueOnce(0); + await Importer.exportToAboutTime(y); + //@ts-ignore + expect(game.Gametime.DTC.saveUserCalendar).toHaveBeenCalledTimes(2); + + y.leapYearRule.rule = LeapYearRules.Custom; + y.leapYearRule.customMod = 8; + await Importer.exportToAboutTime(y); + //@ts-ignore + expect(game.Gametime.DTC.saveUserCalendar).toHaveBeenCalledTimes(3); + }); + + test('Import Calendar Weather', () => { + const mockCalendarWeather = { + months: [ + { + name: 'Month 1', + length: 10, + leapLength: 10, + isNumbered: true, + abbrev: '' + }, + { + name: 'Month 2', + length: 10, + leapLength: 11, + isNumbered: true, + abbrev: '' + }, + { + name: 'Month 3', + length: 1, + leapLength: 1, + isNumbered: false, + abbrev: '' + } + ], + daysOfTheWeek: ["S","M","T"], + year: 12, + day: 5, + numDayOfTheWeek: 2, + first_day: 0, + currentMonth: 2, + currentWeekday: 'M', + dateWordy: "", + era: "", + dayLength: 12, + timeDisp: '', + dateNum: '', + weather: {}, + seasons: [ + { + name: 'Spring', + rolltable: '', + date: { + month: '', + day: 1, + combined: '-1' + }, + temp: '=', + humidity: '=', + color: 'red', + dawn: 6, + dusk: 19 + } + ], + moons: [{ + name:'moon', + cycleLength: 0, + cyclePercent: 0, + lunarEclipseChange: 0, + solarEclipseChange: 0, + referenceTime: 0, + referencePercent: 0, + }], + events: [], + reEvents: [] + }; + + (game.settings.get).mockReturnValueOnce(mockCalendarWeather); + Importer.importCalendarWeather(y); + + expect(y.time.hoursInDay).toBe(12); + expect(y.time.minutesInHour).toBe(60); + expect(y.time.secondsInMinute).toBe(60); + expect(y.weekdays.length).toBe(3); + expect(y.weekdays[0].name).toBe('S'); + expect(y.weekdays[1].name).toBe('M'); + expect(y.weekdays[2].name).toBe('T'); + expect(y.months.length).toBe(3); + expect(y.months[0].name).toBe('Month 1'); + expect(y.months[0].numberOfDays).toBe(10); + expect(y.months[0].numberOfLeapYearDays).toBe(10); + expect(y.months[0].intercalary).toBe(false); + expect(y.months[1].name).toBe('Month 2'); + expect(y.months[1].numberOfDays).toBe(10); + expect(y.months[1].numberOfLeapYearDays).toBe(11); + expect(y.months[1].intercalary).toBe(false); + expect(y.months[2].name).toBe('Month 3'); + expect(y.months[2].numberOfDays).toBe(1); + expect(y.months[2].numberOfLeapYearDays).toBe(1); + expect(y.months[2].intercalary).toBe(true); + expect(y.leapYearRule.rule).toBe(LeapYearRules.None); + }); + + test('Export Calendar Weather', async () => { + //@ts-ignore + delete window.location; + //@ts-ignore + window.location = {reload: jest.fn()}; + const xport = { + months: [], + daysOfTheWeek: [], + year: 0, + currentMonth: 0, + day: 0, + numDayOfTheWeek: 0, + currentWeekday: '', + era: '', + dayLength: 0, + first_day: 0 + }; + (game.settings.get).mockReturnValueOnce(xport); + await Importer.exportCalendarWeather(y); + + expect(xport.months.length).toBe(3); + expect(xport.daysOfTheWeek.length).toBe(1); + expect(xport.year).toBe(0); + expect(xport.currentMonth).toBe(0); + expect(xport.day).toBe(0); + expect(window.location.reload).toHaveBeenCalledTimes(1); + + y.months[1].current = true; + + (game.settings.get).mockReturnValueOnce(xport); + await Importer.exportCalendarWeather(y); + expect(xport.currentMonth).toBe(1); + expect(window.location.reload).toHaveBeenCalledTimes(2); + + y.months[1].days[2].current = true; + (game.settings.get).mockReturnValueOnce(xport); + await Importer.exportCalendarWeather(y); + expect(xport.day).toBe(2); + expect(window.location.reload).toHaveBeenCalledTimes(3); + }); +}); diff --git a/src/classes/importer.ts b/src/classes/importer.ts new file mode 100644 index 00000000..53c11d94 --- /dev/null +++ b/src/classes/importer.ts @@ -0,0 +1,263 @@ +import Year from "./year"; +import {AboutTimeImport, CalendarWeatherImport} from "../interfaces"; +import {Weekday} from "./weekday"; +import Month from "./month"; +import {LeapYearRules} from "../constants"; +import {GameSettings} from "./game-settings"; +import Season from "./season"; +import Moon from "./moon"; + +export default class Importer{ + + /** + * Loads the about time calendar configuration into Simple Calendars configuration + * @param {Year} year The year to load the about time configuration into + */ + static async importAboutTime(year: Year){ + const aboutTimeCalendar = game.settings.get('about-time', 'savedCalendar'); + + //Set up the time parameters + year.time.hoursInDay = aboutTimeCalendar['hours_per_day']; + year.time.minutesInHour = aboutTimeCalendar['minutes_per_hour']; + year.time.secondsInMinute = aboutTimeCalendar['seconds_per_minute']; + + //Set up the weekdays + year.weekdays = []; + for(let i = 0; i < aboutTimeCalendar['weekdays'].length; i++){ + year.weekdays.push(new Weekday(i+1, aboutTimeCalendar['weekdays'][i])); + } + + //Set up the months + year.months = []; + let mCount = 1; + let mICount = 1; + for(let key in aboutTimeCalendar['month_len']){ + if(aboutTimeCalendar['month_len'].hasOwnProperty(key)){ + const newM = new Month(key, mCount, aboutTimeCalendar['month_len'][key].days[0], aboutTimeCalendar['month_len'][key].days[1]); + if(aboutTimeCalendar['month_len'][key].intercalary){ + newM.numericRepresentation = mICount * -1; + newM.intercalary = true; + newM.intercalaryInclude = false; + mICount++; + } else { + mCount++; + } + year.months.push(newM); + } + } + + //Try to set up the leap year rule + year.leapYearRule.rule = LeapYearRules.None; + if(aboutTimeCalendar['leap_year_rule'] === '(year) => Math.floor(year / 4) - Math.floor(year / 100) + Math.floor(year / 400)') { + year.leapYearRule.rule = LeapYearRules.Gregorian; + } else { + const matches = aboutTimeCalendar['leap_year_rule'].match(/(\d)/g); + if(matches && matches.length && matches[0] !== '0'){ + year.leapYearRule.rule = LeapYearRules.Custom; + year.leapYearRule.customMod = parseInt(matches[0]); + } + } + + //Set the current time + const currentTime = year.secondsToDate(game.time.worldTime); + year.updateTime(currentTime); + + //Save everything + await GameSettings.SaveYearConfiguration(year); + await GameSettings.SaveMonthConfiguration(year.months); + await GameSettings.SaveWeekdayConfiguration(year.weekdays); + await GameSettings.SaveLeapYearRules(year.leapYearRule); + await GameSettings.SaveTimeConfiguration(year.time); + await GameSettings.SaveCurrentDate(year); + } + + /** + * Sets up about time to match Simple Calendars configuration + * @param {Year} year The year to use + * + * Known Issues: + * - Intercalary days seem to be calculated differently so calendars with them do not match up perfectly with Simple Calendar + */ + static async exportToAboutTime(year: Year){ + + const monthList: AboutTimeImport.MonthList = {}; + for(let i = 0; i < year.months.length; i++){ + monthList[year.months[i].name] = { + days: [year.months[i].numberOfDays, year.months[i].numberOfLeapYearDays], + intercalary: year.months[i].intercalary + }; + } + + const newAboutTimeConfig: AboutTimeImport.Calendar = { + "first_day": 0, + "clock_start_year": 0, + "has_year_0": true, + "notes": {}, + "hours_per_day": year.time.hoursInDay, + "minutes_per_hour": year.time.minutesInHour, + "seconds_per_minute": year.time.secondsInMinute, + "weekdays": year.weekdays.map( w => w.name), + "leap_year_rule": `(year) => 0`, + "month_len": monthList, + "_month_len": {} + }; + + if(year.leapYearRule.rule === LeapYearRules.Gregorian){ + newAboutTimeConfig.leap_year_rule = `(year) => Math.floor(year / 4) - Math.floor(year / 100) + Math.floor(year / 400)`; + } else if(year.leapYearRule.rule === LeapYearRules.Custom){ + //Gross but might be all we can do + newAboutTimeConfig.leap_year_rule = `(year) => Math.floor(year / ${year.leapYearRule.customMod} ) + 1`; + } + //@ts-ignore + game.Gametime.DTC.saveUserCalendar(newAboutTimeConfig); + + // Ensure about time uses the new calendar on startup + if (game.settings.get("about-time", "calendar") !== 0){ + await game.settings.set("about-time", "calendar", 0); + } + } + + /** + * Loads the calendar/weather calendar configuration into Simple Calendars configuration + * @param {Year} year The year to load the about time configuration into + * + * Known Issues: + * - Seasons: The month is not properly stored by calendar-weather so all months are set to the first month + * - Seasons: The color of the seasons can not be transferred over + */ + static async importCalendarWeather(year: Year){ + const currentSettings = game.settings.get('calendar-weather', 'dateTime'); + + //Set up the time + year.time.hoursInDay = currentSettings.dayLength; + + //Set up the weekdays + year.weekdays = []; + for(let i = 0; i < currentSettings.daysOfTheWeek.length; i++){ + year.weekdays.push(new Weekday(i+1, currentSettings.daysOfTheWeek[i])); + } + + //Set up the months + year.months = []; + let mCount = 1; + let mICount = 1; + for(let i = 0; i < currentSettings.months.length; i++){ + const nMonth = new Month(currentSettings.months[i].name, i+1, currentSettings.months[i].length, currentSettings.months[i].leapLength) + if(!currentSettings.months[i].isNumbered){ + nMonth.numericRepresentation = mICount * -1; + nMonth.intercalary = true; + nMonth.intercalaryInclude = false; + mICount++; + } else { + mCount++; + } + year.months.push(nMonth); + } + + year.leapYearRule.rule = LeapYearRules.None; + + //Set up the seasons + year.seasons = []; + for(let i = 0; i < currentSettings.seasons.length; i++){ + const nSeason = new Season(currentSettings.seasons[i].name, 1, currentSettings.seasons[i].date.day + 1); + year.seasons.push(nSeason); + } + + year.moons = []; + for(let i = 0; i< currentSettings.moons.length; i++){ + const newMoon = new Moon(currentSettings.moons[i].name, currentSettings.moons[i].cycleLength); + const currentTime = year.secondsToDate(currentSettings.moons[i].referenceTime); + newMoon.firstNewMoon.year = currentTime.year; + newMoon.firstNewMoon.month = currentTime.month; + newMoon.firstNewMoon.day = currentTime.day; + } + + //Set the current time + const currentTime = year.secondsToDate(game.time.worldTime); + year.updateTime(currentTime); + + //Save everything + await GameSettings.SaveYearConfiguration(year); + await GameSettings.SaveMonthConfiguration(year.months); + await GameSettings.SaveWeekdayConfiguration(year.weekdays); + await GameSettings.SaveLeapYearRules(year.leapYearRule); + await GameSettings.SaveTimeConfiguration(year.time); + await GameSettings.SaveMoonConfiguration(year.moons); + await GameSettings.SaveCurrentDate(year); + } + + /** + * Sets up calendar weather to match Simple Calendars configuration + * @param {Year} year The year to use + * + * Known Issues: + * - Calendar/Weather does not support leap years so any calendar that has leap years will not work properly and be out of sync with Simple Calendar + * - As About time is used as the base, the same known issues for that will apply here + * - Seasons: Simple Calendars colors do not exist in Calendar/Weather so they can not be exported + */ + static async exportCalendarWeather(year: Year){ + const currentSettings = game.settings.get('calendar-weather', 'dateTime'); + + const monthList: CalendarWeatherImport.Month[] = []; + for(let i = 0; i < year.months.length; i++){ + monthList.push({ + name: year.months[i].name, + length: year.months[i].numberOfDays, + leapLength: year.months[i].numberOfLeapYearDays, + isNumbered: !year.months[i].intercalary, + abbrev: year.months[i].intercalary? year.months[i].name.substring(0,2) : '' + }); + } + + const seasonList: CalendarWeatherImport.Seasons[] = []; + for(let i = 0; i < year.seasons.length; i++){ + seasonList.push({ + name: year.seasons[i].name, + color: '', + dawn: 6, + dusk: 19, + humidity: '=', + rolltable: '', + temp: '=', + date: { + day: year.seasons[i].startingDay - 1, + month: '', + combined: `-${year.seasons[i].startingDay - 1}` + } + }); + } + + const moonList: CalendarWeatherImport.Moons[] = []; + for(let i = 0; i < year.moons.length; i++){ + moonList.push({ + name: year.moons[i].name, + cycleLength: year.moons[i].cycleLength, + cyclePercent: 0, + lunarEclipseChange: 0, + solarEclipseChange: 0, + referencePercent: 0, + referenceTime: year.time.getTotalSeconds(year.dateToDays(year.moons[i].firstNewMoon.year, year.moons[i].firstNewMoon.month, year.moons[i].firstNewMoon.day, true, true) - 1) + }); + } + + const currentMonth = year.getMonth(); + const currentDay = currentMonth?.getDay(); + const weekDays = year.weekdays.map( w => w.name) + const dow = year.dayOfTheWeek(year.numericRepresentation, currentMonth? currentMonth.numericRepresentation : 1, currentDay? currentDay.numericRepresentation : 1); + currentSettings.months = monthList; + currentSettings.daysOfTheWeek = weekDays; + currentSettings.year = year.numericRepresentation; + currentSettings.currentMonth = currentMonth? currentMonth.numericRepresentation - 1 : 0; + currentSettings.day = currentDay? currentDay.numericRepresentation - 1 : 0; + currentSettings.numDayOfTheWeek = dow; + currentSettings.currentWeekday = weekDays[dow]; + currentSettings.era = ''; + currentSettings.dayLength = year.time.hoursInDay + currentSettings.first_day = 0; + currentSettings.seasons = seasonList; + currentSettings.moons = moonList; + await game.settings.set('calendar-weather', 'dateTime', currentSettings); + await Importer.exportToAboutTime(year); + window.location.reload(); + } +} diff --git a/src/classes/leap-year.test.ts b/src/classes/leap-year.test.ts index 6384681a..fc17da29 100644 --- a/src/classes/leap-year.test.ts +++ b/src/classes/leap-year.test.ts @@ -87,4 +87,22 @@ describe('Leap Year Tests', () => { expect(lr.howManyLeapYears(2020)).toBe(403); expect(lr.howManyLeapYears(2021)).toBe(404); }); + + test('Previous Leap Year', () => { + expect(lr.previousLeapYear(1990)).toBe(null); + lr.rule = LeapYearRules.Gregorian; + expect(lr.previousLeapYear(1990)).toBe(1988); + }); + + test('Fraction', () => { + expect(lr.fraction(1990)).toBe(0); + lr.rule = LeapYearRules.Gregorian; + expect(lr.fraction(1990)).toBe(0.5); + lr.rule = LeapYearRules.Custom; + lr.customMod = 5; + expect(lr.fraction(1991)).toBe(0.2); + + lr.customMod = 0; + expect(lr.fraction(1990)).toBe(0); + }); }); diff --git a/src/classes/leap-year.ts b/src/classes/leap-year.ts index b0069c59..0da3695b 100644 --- a/src/classes/leap-year.ts +++ b/src/classes/leap-year.ts @@ -45,4 +45,32 @@ export default class LeapYear { return num; } + previousLeapYear(year: number): number | null { + if(this.rule === LeapYearRules.Gregorian || (this.rule === LeapYearRules.Custom && this.customMod !== 0)){ + let testYear = year; + while(Number.isInteger(testYear)){ + if(this.isLeapYear(testYear)){ + break; + } else { + testYear--; + } + } + return testYear; + } + return null; + } + + fraction(year: number){ + const previousLeapYear = this.previousLeapYear(year); + if(previousLeapYear !== null){ + const yearInto = year%previousLeapYear; + if(this.rule === LeapYearRules.Gregorian){ + return yearInto / 4 + } else { + return yearInto / this.customMod; + } + } + return 0; + } + } diff --git a/src/classes/moon.test.ts b/src/classes/moon.test.ts new file mode 100644 index 00000000..e87bea50 --- /dev/null +++ b/src/classes/moon.test.ts @@ -0,0 +1,96 @@ +/** + * @jest-environment jsdom + */ +import "../../__mocks__/game"; +import "../../__mocks__/form-application"; +import "../../__mocks__/application"; +import "../../__mocks__/handlebars"; +import "../../__mocks__/event"; +import "../../__mocks__/crypto"; +import SimpleCalendar from "./simple-calendar"; +import Year from "./year"; +import Month from "./month"; +import Moon from "./moon"; +import {LeapYearRules, MoonIcons, MoonYearResetOptions} from "../constants"; + +describe('Moon Tests', () => { + let m :Moon; + + beforeEach(() => { + m = new Moon("Moon", 29.5); + }); + + test('Properties', () => { + expect(Object.keys(m).length).toBe(6); //Make sure no new properties have been added + expect(m.name).toBe('Moon'); + expect(m.cycleLength).toBe(29.5); + expect(m.cycleDayAdjust).toBe(0); + expect(m.color).toBe('#ffffff'); + expect(m.phases.length).toBe(1); + expect(m.firstNewMoon).toStrictEqual({ "day": 1, "month": 1, "year": 0, "yearReset": "none", "yearX": 0 }); + }); + + test('Clone', () => { + expect(m.clone()).toStrictEqual(m); + }); + + test('To Template', () => { + const y = new Year(0); + let c = m.toTemplate(y); + expect(Object.keys(c).length).toBe(7); //Make sure no new properties have been added + expect(m.name).toBe('Moon'); + expect(m.cycleLength).toBe(29.5); + expect(m.firstNewMoon).toStrictEqual({ "day": 1, "month": 1, "year": 0, "yearReset": "none", "yearX": 0 }); + expect(m.phases.length).toBe(1); + expect(m.color).toBe('#ffffff'); + expect(m.cycleDayAdjust).toBe(0); + expect(c.dayList).toStrictEqual([]); + + SimpleCalendar.instance = new SimpleCalendar(); + + c = m.toTemplate(y); + expect(c.dayList.length).toStrictEqual(0); + y.months.push(new Month("Month 1", 1, 10)); + c = m.toTemplate(y); + expect(c.dayList.length).toStrictEqual(10); + }); + + test('Update Phase Length', () => { + m.updatePhaseLength(); + expect(m.phases[0].length).toBe(1); + + m.phases.push({name: 'p2', icon: MoonIcons.NewMoon, length: 0, singleDay: false}); + m.updatePhaseLength(); + expect(m.phases[0].length).toBe(1); + expect(m.phases[1].length).toBe(28.5); + }); + + test('Get Moon Phase', () => { + const y = new Year(0); + m.phases.push({name: 'p2', icon: MoonIcons.NewMoon, length: 0, singleDay: false}); + m.updatePhaseLength(); + expect(m.getMoonPhase(y)).toStrictEqual(m.phases[0]); + y.months.push(new Month("Month 1", 1, 10)); + y.months[0].current = true; + expect(m.getMoonPhase(y)).toStrictEqual(m.phases[0]); + y.months[0].days[2].current = true; + expect(m.getMoonPhase(y)).toStrictEqual(m.phases[1]); + + m.firstNewMoon.yearReset = MoonYearResetOptions.LeapYear; + expect(m.getMoonPhase(y)).toStrictEqual(m.phases[1]); + y.leapYearRule.rule = LeapYearRules.Gregorian; + expect(m.getMoonPhase(y)).toStrictEqual(m.phases[1]); + y.numericRepresentation = 1990; + expect(m.getMoonPhase(y)).toStrictEqual(m.phases[1]); + + m.firstNewMoon.yearReset = MoonYearResetOptions.XYears; + expect(m.getMoonPhase(y)).toStrictEqual(m.phases[0]); + m.firstNewMoon.yearX = 5; + expect(m.getMoonPhase(y)).toStrictEqual(m.phases[1]); + + y.months[0].visible = true; + expect(m.getMoonPhase(y, 'visible', {numericRepresentation: 2, name: "2", current: false, selected: false})).toStrictEqual(m.phases[1]); + + expect(m.getMoonPhase(y, 'selected')).toStrictEqual(m.phases[0]); + }); +}); diff --git a/src/classes/moon.ts b/src/classes/moon.ts new file mode 100644 index 00000000..c527c157 --- /dev/null +++ b/src/classes/moon.ts @@ -0,0 +1,205 @@ +import {DayTemplate, FirstNewMoonDate, MoonPhase, MoonTemplate} from "../interfaces"; +import Year from "./year"; +import {MoonIcons, MoonYearResetOptions} from "../constants"; +import {Logger} from "./logging"; + +/** + * Class for representing a moon + */ +export default class Moon{ + /** + * The name of the moon + * @type {string} + */ + name: string; + /** + * How long in calendar days the moon takes to do 1 revolution + * @type {number} + */ + cycleLength: number; + /** + * The different phases of the moon + * @type {Array} + */ + phases: MoonPhase[] = []; + /** + * When the first new moon took place. Used as a reference for calculating the position of the current cycle + */ + firstNewMoon: FirstNewMoonDate = { + /** + * The year reset options for the first new moon + * @type {number} + */ + yearReset: MoonYearResetOptions.None, + /** + * How often the year should reset + * @type {number} + */ + yearX: 0, + /** + * The year of the first new moon + * @type {number} + */ + year: 0, + /** + * The month of the first new moon + * @type {number} + */ + month: 1, + /** + * The day of the first new moon + * @type {number} + */ + day: 1 + }; + /** + * A color to associate with the moon when displaying it on the calendar + */ + color: string = '#ffffff'; + /** + * The amount of days to adjust the current cycle day by + * @type {number} + */ + cycleDayAdjust: number = 0; + + /** + * The moon constructor + * @param {string} name The name of the moon + * @param {number} cycleLength The length of the moons cycle + */ + constructor(name: string, cycleLength: number) { + this.name = name; + this.cycleLength = cycleLength; + + this.phases.push({ + name: game.i18n.localize('FSC.Moon.Phase.New'), + length: 3.69, + icon: MoonIcons.NewMoon, + singleDay: true + }); + } + + /** + * Creates a clone of this moon object + * @return {Moon} + */ + clone(): Moon { + const c = new Moon(this.name, this.cycleLength); + c.phases = this.phases.map(p => { return { name: p.name, length: p.length, icon: p.icon, singleDay: p.singleDay };}); + c.firstNewMoon.yearReset = this.firstNewMoon.yearReset; + c.firstNewMoon.yearX = this.firstNewMoon.yearX; + c.firstNewMoon.year = this.firstNewMoon.year; + c.firstNewMoon.month = this.firstNewMoon.month; + c.firstNewMoon.day = this.firstNewMoon.day; + c.color = this.color; + c.cycleDayAdjust = this.cycleDayAdjust; + return c; + } + + /** + * Converts this moon into a template used for displaying the moon in HTML + * @param {Year} year The year to use for getting the days and months + */ + toTemplate(year: Year): MoonTemplate { + const data: MoonTemplate = { + name: this.name, + cycleLength: this.cycleLength, + firstNewMoon: this.firstNewMoon, + phases: this.phases, + color: this.color, + cycleDayAdjust: this.cycleDayAdjust, + dayList: [] + }; + + const month = year.months.find(m => m.numericRepresentation === data.firstNewMoon.month); + + if(month){ + data.dayList = month.days.map(d => d.toTemplate()); + } + + return data; + } + + /** + * Updates each phases length in days so the total length of all phases matches the cycle length + */ + updatePhaseLength(){ + let pLength = 0, singleDays = 0; + for(let i = 0; i < this.phases.length; i++){ + if(this.phases[i].singleDay){ + singleDays++; + } else { + pLength++; + } + } + const phaseLength = Number(((this.cycleLength - singleDays) / pLength).toPrecision(6)); + + this.phases.forEach(p => { + if(p.singleDay){ + p.length = 1; + } else { + p.length = phaseLength; + } + }); + } + + /** + * Returns the current phase of the moon based on a year month and day. + * This phase will be within + or - 1 days of when the phase actually begins + * @param {Year} year The year class used to get the year, month and day to use + * @param {string} property Which property to use when getting the year, month and day. Can be current, selected or visible + * @param {DayTemplate|null} [dayToUse=null] The day to use instead of the day associated with the property + */ + getMoonPhase(year: Year, property = 'current', dayToUse: DayTemplate | null = null): MoonPhase{ + property = property.toLowerCase() as 'current' | 'selected' | 'visible'; + let yearNum = property === 'current'? year.numericRepresentation : property === 'selected'? year.selectedYear : year.visibleYear; + let firstNewMoonDays = year.dateToDays(this.firstNewMoon.year, this.firstNewMoon.month, this.firstNewMoon.day, true, true) - 1; + + const month = year.getMonth(property); + if(month){ + const day = property !== 'visible'? month.getDay(property) : dayToUse; + let monthNum = month.numericRepresentation; + let dayNum = day? day.numericRepresentation : 1; + let resetYearAdjustment = 0; + + if(this.firstNewMoon.yearReset === MoonYearResetOptions.LeapYear){ + let lyYear = year.leapYearRule.previousLeapYear(yearNum); + if(lyYear !== null){ + Logger.debug(`Resetting moon calculation first day to year: ${lyYear}`); + firstNewMoonDays = year.dateToDays(lyYear, this.firstNewMoon.month, this.firstNewMoon.day, true, true) - 1; + if(yearNum !== lyYear){ + resetYearAdjustment += year.leapYearRule.fraction(yearNum); + } + } + } else if(this.firstNewMoon.yearReset === MoonYearResetOptions.XYears){ + const resetMod = yearNum % this.firstNewMoon.yearX; + if(resetMod !== 0){ + let resetYear = yearNum - resetMod; + firstNewMoonDays = year.dateToDays(resetYear, this.firstNewMoon.month, this.firstNewMoon.day, true, true) - 1; + resetYearAdjustment += resetMod / this.firstNewMoon.yearX; + } + } + + const daysSoFar = year.dateToDays(yearNum, monthNum, dayNum, true, true) - 1; + const daysSinceReferenceMoon = daysSoFar - firstNewMoonDays + resetYearAdjustment; + const moonCycles = daysSinceReferenceMoon / this.cycleLength; + let daysIntoCycle = ((moonCycles - Math.floor(moonCycles)) * this.cycleLength) + this.cycleDayAdjust; + + let phaseDays = 0; + let phase: MoonPhase | null = null; + for(let i = 0; i < this.phases.length; i++){ + const newPhaseDays = phaseDays + this.phases[i].length; + if(daysIntoCycle >= phaseDays && daysIntoCycle < newPhaseDays){ + phase = this.phases[i]; + break; + } + phaseDays = newPhaseDays; + } + if(phase !== null){ + return phase; + } + } + return this.phases[0]; + } + +} diff --git a/src/classes/season.test.ts b/src/classes/season.test.ts new file mode 100644 index 00000000..5ccdd7d2 --- /dev/null +++ b/src/classes/season.test.ts @@ -0,0 +1,57 @@ +/** + * @jest-environment jsdom + */ +import "../../__mocks__/game"; +import "../../__mocks__/form-application"; +import "../../__mocks__/application"; +import "../../__mocks__/handlebars"; +import "../../__mocks__/event"; +import "../../__mocks__/crypto"; + +import Season from "./season"; +import SimpleCalendar from "./simple-calendar"; +import Year from "./year"; +import Month from "./month"; + +describe('Season Tests', () => { + + let s: Season; + + beforeEach(() => { + s = new Season('Spring', 1, 1); + }); + + test('Properties', () => { + expect(Object.keys(s).length).toBe(5); //Make sure no new properties have been added + expect(s.name).toBe('Spring'); + expect(s.startingMonth).toBe(1); + expect(s.startingDay).toBe(1); + expect(s.color).toBe('#ffffff'); + expect(s.customColor).toBe(''); + }); + + test('Clone', () => { + expect(s.clone()).toStrictEqual(s); + }); + + test('To Template', () => { + const y = new Year(0); + let c = s.toTemplate(y); + expect(Object.keys(c).length).toBe(6); //Make sure no new properties have been added + expect(c.name).toBe('Spring'); + expect(c.startingMonth).toBe(1); + expect(c.startingDay).toBe(1); + expect(c.color).toBe('#ffffff'); + expect(c.customColor).toBe(''); + expect(c.dayList).toStrictEqual([]); + + SimpleCalendar.instance = new SimpleCalendar(); + + c = s.toTemplate(y); + expect(c.dayList.length).toStrictEqual(0); + y.months.push(new Month("Month 1", 1, 10)); + c = s.toTemplate(y); + expect(c.dayList.length).toStrictEqual(10); + }); + +}); diff --git a/src/classes/season.ts b/src/classes/season.ts new file mode 100644 index 00000000..78b69946 --- /dev/null +++ b/src/classes/season.ts @@ -0,0 +1,78 @@ +import SimpleCalendar from "./simple-calendar"; +import {SeasonTemplate} from "../interfaces"; +import Year from "./year"; + +/** + * All content around a season + */ +export default class Season { + /** + * The name of the season + * @type{string} + */ + name: string; + /** + * The color of the season + * @type{string} + */ + color: string = '#ffffff'; + /** + * The custom color of the season + * @type{string} + */ + customColor: string = ''; + /** + * The month this season starts on + * @type{number} + */ + startingMonth: number = -1; + /** + * The day of the starting month this season starts on + * @type{number} + */ + startingDay: number = -1; + + /** + * The Season Constructor + * @param {string} name The name of the season + * @param {number} startingMonth The month this season starts on + * @param {number} startingDay The day of the starting month this season starts on + */ + constructor(name: string, startingMonth: number, startingDay: number) { + this.name = name; + this.startingMonth = startingMonth; + this.startingDay = startingDay; + } + + /** + * Creates a clone of the current season + * @return {Season} + */ + clone(): Season { + const t = new Season(this.name, this.startingMonth, this.startingDay); + t.color = this.color; + t.customColor = this.customColor; + return t; + } + + /** + * Creates a template of the season used to render its information + * @param {Year} year The year to look in for the months and days list + */ + toTemplate(year: Year){ + const data: SeasonTemplate = { + name: this.name, + startingMonth: this.startingMonth, + startingDay: this.startingDay, + color: this.color, + customColor: this.customColor, + dayList: [] + }; + + const month = year.months.find(m => m.numericRepresentation === data.startingMonth); + if(month){ + data.dayList = month.days.map(d => d.toTemplate()); + } + return data; + } +} diff --git a/src/classes/simple-calendar-configuration.test.ts b/src/classes/simple-calendar-configuration.test.ts index 251df531..7115b270 100644 --- a/src/classes/simple-calendar-configuration.test.ts +++ b/src/classes/simple-calendar-configuration.test.ts @@ -15,7 +15,11 @@ import Mock = jest.Mock; import Month from "./month"; import {Weekday} from "./weekday"; import {Logger} from "./logging"; -import {LeapYearRules} from "../constants"; +import {LeapYearRules, MoonIcons, MoonYearResetOptions} from "../constants"; +import Season from "./season"; +import Moon from "./moon"; + +jest.mock('./importer'); describe('Simple Calendar Configuration Tests', () => { @@ -32,6 +36,8 @@ describe('Simple Calendar Configuration Tests', () => { y.months.push(new Month('T', 2, 15)); y.weekdays.push(new Weekday(1, 'S')); y.weekdays.push(new Weekday(2, 'F')); + y.seasons.push(new Season('S', 5, 5)); + y.moons.push(new Moon('Moon', 30)); y.selectedYear = 0; y.visibleYear = 0; y.months[0].current = true; @@ -44,6 +50,7 @@ describe('Simple Calendar Configuration Tests', () => { SimpleCalendarConfiguration.instance = new SimpleCalendarConfiguration(y); //Spy on the inherited render function of the new instance + //@ts-ignore renderSpy = jest.spyOn(SimpleCalendarConfiguration.instance, 'render'); //Clear all mock calls (console.error).mockClear(); @@ -64,7 +71,7 @@ describe('Simple Calendar Configuration Tests', () => { //@ts-ignore expect(opts.resizable).toBe(true); //@ts-ignore - expect(opts.width).toBe(710); + expect(opts.width).toBe(960); //@ts-ignore expect(opts.height).toBe(700); //@ts-ignore @@ -100,6 +107,13 @@ describe('Simple Calendar Configuration Tests', () => { expect(data.months).toStrictEqual(y.months.map(m => m.toTemplate())); //@ts-ignore expect(data.weekdays).toStrictEqual(y.weekdays.map(m => m.toTemplate())); + + (game.modules.get).mockReturnValueOnce({active:true}).mockReturnValueOnce({active:true}); + data = SimpleCalendarConfiguration.instance.getData(); + //@ts-ignore + expect(data.importing.showCalendarWeather).toBe(true); + //@ts-ignore + expect(data.importing.showAboutTime).toBe(true); }); test('Update Object', () => { @@ -120,8 +134,8 @@ describe('Simple Calendar Configuration Tests', () => { fakeQuery.length = 1; //@ts-ignore SimpleCalendarConfiguration.instance.activateListeners(fakeQuery); - expect(fakeQuery.find).toHaveBeenCalledTimes(13); - expect(onFunc).toHaveBeenCalledTimes(13); + expect(fakeQuery.find).toHaveBeenCalledTimes(26); + expect(onFunc).toHaveBeenCalledTimes(26); }); test('Rebase Month Numbers', () => { @@ -137,7 +151,7 @@ describe('Simple Calendar Configuration Tests', () => { test('Add Month', () => { const event = new Event('click'); const currentMonthLength = (SimpleCalendarConfiguration.instance.object).months.length; - SimpleCalendarConfiguration.instance.addMonth(event); + SimpleCalendarConfiguration.instance.addToTable('month', event); expect(renderSpy).toHaveBeenCalledTimes(1); expect((SimpleCalendarConfiguration.instance.object).months.length).toBe(currentMonthLength + 1); }); @@ -145,29 +159,29 @@ describe('Simple Calendar Configuration Tests', () => { test('Remove Month', () => { const event = new Event('click'); let currentMonthLength = (SimpleCalendarConfiguration.instance.object).months.length; - SimpleCalendarConfiguration.instance.removeMonth(event); + SimpleCalendarConfiguration.instance.removeFromTable('month',event); expect(renderSpy).not.toHaveBeenCalled(); expect((SimpleCalendarConfiguration.instance.object).months.length).toBe(currentMonthLength); //Check for invalid index (event.currentTarget).setAttribute('data-index', 'a'); currentMonthLength = (SimpleCalendarConfiguration.instance.object).months.length; - SimpleCalendarConfiguration.instance.removeMonth(event); + SimpleCalendarConfiguration.instance.removeFromTable('month',event); expect(renderSpy).not.toHaveBeenCalled(); expect((SimpleCalendarConfiguration.instance.object).months.length).toBe(currentMonthLength); //Check for index outside of month length (event.currentTarget).setAttribute('data-index', '12'); currentMonthLength = (SimpleCalendarConfiguration.instance.object).months.length; - SimpleCalendarConfiguration.instance.removeMonth(event); - expect(renderSpy).not.toHaveBeenCalled(); + SimpleCalendarConfiguration.instance.removeFromTable('month',event); + expect(renderSpy).toHaveBeenCalledTimes(1); expect((SimpleCalendarConfiguration.instance.object).months.length).toBe(currentMonthLength); //Check for valid index (event.currentTarget).setAttribute('data-index', '0'); currentMonthLength = (SimpleCalendarConfiguration.instance.object).months.length; - SimpleCalendarConfiguration.instance.removeMonth(event); - expect(renderSpy).toHaveBeenCalledTimes(1); + SimpleCalendarConfiguration.instance.removeFromTable('month',event); + expect(renderSpy).toHaveBeenCalledTimes(2); expect((SimpleCalendarConfiguration.instance.object).months.length).toBe(currentMonthLength - 1); expect((SimpleCalendarConfiguration.instance.object).months[0].name).toBe('T'); expect((SimpleCalendarConfiguration.instance.object).months[0].days.length).toBe(15); @@ -175,15 +189,15 @@ describe('Simple Calendar Configuration Tests', () => { //Check for removing all months (event.currentTarget).setAttribute('data-index', 'all'); - SimpleCalendarConfiguration.instance.removeMonth(event); - expect(renderSpy).toHaveBeenCalledTimes(2); + SimpleCalendarConfiguration.instance.removeFromTable('month',event); + expect(renderSpy).toHaveBeenCalledTimes(3); expect((SimpleCalendarConfiguration.instance.object).months.length).toBe(0); }); test('Add Weekday', () => { const event = new Event('click'); const currentWeekdayLength = (SimpleCalendarConfiguration.instance.object).weekdays.length; - SimpleCalendarConfiguration.instance.addWeekday(event); + SimpleCalendarConfiguration.instance.addToTable('weekday', event); expect(renderSpy).toHaveBeenCalledTimes(1); expect((SimpleCalendarConfiguration.instance.object).weekdays.length).toBe(currentWeekdayLength + 1); }); @@ -191,44 +205,211 @@ describe('Simple Calendar Configuration Tests', () => { test('Remove Weekday', () => { const event = new Event('click'); let currentWeekdayLength = (SimpleCalendarConfiguration.instance.object).weekdays.length; - SimpleCalendarConfiguration.instance.removeWeekday(event); + SimpleCalendarConfiguration.instance.removeFromTable('weekday',event); expect(renderSpy).not.toHaveBeenCalled(); expect((SimpleCalendarConfiguration.instance.object).weekdays.length).toBe(currentWeekdayLength); //Check for invalid index (event.currentTarget).setAttribute('data-index', 'a'); currentWeekdayLength = (SimpleCalendarConfiguration.instance.object).weekdays.length; - SimpleCalendarConfiguration.instance.removeWeekday(event); + SimpleCalendarConfiguration.instance.removeFromTable('weekday',event); expect(renderSpy).not.toHaveBeenCalled(); expect((SimpleCalendarConfiguration.instance.object).weekdays.length).toBe(currentWeekdayLength); //Check for index outside of weekday length (event.currentTarget).setAttribute('data-index', '12'); currentWeekdayLength = (SimpleCalendarConfiguration.instance.object).weekdays.length; - SimpleCalendarConfiguration.instance.removeWeekday(event); - expect(renderSpy).not.toHaveBeenCalled(); + SimpleCalendarConfiguration.instance.removeFromTable('weekday',event); + expect(renderSpy).toHaveBeenCalledTimes(1); expect((SimpleCalendarConfiguration.instance.object).weekdays.length).toBe(currentWeekdayLength); //Check for valid index (event.currentTarget).setAttribute('data-index', '0'); currentWeekdayLength = (SimpleCalendarConfiguration.instance.object).weekdays.length; - SimpleCalendarConfiguration.instance.removeWeekday(event); - expect(renderSpy).toHaveBeenCalledTimes(1); + SimpleCalendarConfiguration.instance.removeFromTable('weekday',event); + expect(renderSpy).toHaveBeenCalledTimes(2); expect((SimpleCalendarConfiguration.instance.object).weekdays.length).toBe(currentWeekdayLength - 1); expect((SimpleCalendarConfiguration.instance.object).weekdays[0].name).toBe('F'); expect((SimpleCalendarConfiguration.instance.object).weekdays[0].numericRepresentation).toBe(1); //Check for removing all weekdays (event.currentTarget).setAttribute('data-index', 'all'); - SimpleCalendarConfiguration.instance.removeWeekday(event); - expect(renderSpy).toHaveBeenCalledTimes(2); + SimpleCalendarConfiguration.instance.removeFromTable('weekday',event); + expect(renderSpy).toHaveBeenCalledTimes(3); expect((SimpleCalendarConfiguration.instance.object).weekdays.length).toBe(0); }); - test('Predefined Apply', () => { - SimpleCalendarConfiguration.instance.predefinedApply(new Event('click')); - //@ts-ignore - expect(DialogRenderer).toHaveBeenCalled(); + test('Add Season', () => { + const event = new Event('click'); + const currentLength = (SimpleCalendarConfiguration.instance.object).seasons.length; + SimpleCalendarConfiguration.instance.addToTable('season', event); + expect(renderSpy).toHaveBeenCalledTimes(1); + expect((SimpleCalendarConfiguration.instance.object).seasons.length).toBe(currentLength + 1); + }); + + test('Remove Season', () => { + const event = new Event('click'); + let currentLength = (SimpleCalendarConfiguration.instance.object).seasons.length; + SimpleCalendarConfiguration.instance.removeFromTable('season',event); + expect(renderSpy).not.toHaveBeenCalled(); + expect((SimpleCalendarConfiguration.instance.object).seasons.length).toBe(currentLength); + + //Check for invalid index + (event.currentTarget).setAttribute('data-index', 'a'); + currentLength = (SimpleCalendarConfiguration.instance.object).seasons.length; + SimpleCalendarConfiguration.instance.removeFromTable('season',event); + expect(renderSpy).not.toHaveBeenCalled(); + expect((SimpleCalendarConfiguration.instance.object).seasons.length).toBe(currentLength); + + //Check for index outside of season length + (event.currentTarget).setAttribute('data-index', '12'); + currentLength = (SimpleCalendarConfiguration.instance.object).seasons.length; + SimpleCalendarConfiguration.instance.removeFromTable('season',event); + expect(renderSpy).toHaveBeenCalledTimes(1); + expect((SimpleCalendarConfiguration.instance.object).seasons.length).toBe(currentLength); + + //Check for valid index + (event.currentTarget).setAttribute('data-index', '0'); + currentLength = (SimpleCalendarConfiguration.instance.object).seasons.length; + SimpleCalendarConfiguration.instance.removeFromTable('season',event); + expect(renderSpy).toHaveBeenCalledTimes(2); + expect((SimpleCalendarConfiguration.instance.object).seasons.length).toBe(currentLength - 1); + + //Check for removing all seasons + (event.currentTarget).setAttribute('data-index', 'all'); + SimpleCalendarConfiguration.instance.removeFromTable('season',event); + expect(renderSpy).toHaveBeenCalledTimes(3); + expect((SimpleCalendarConfiguration.instance.object).seasons.length).toBe(0); + }); + + test('Add Moon', () => { + const event = new Event('click'); + const currentLength = (SimpleCalendarConfiguration.instance.object).moons.length; + SimpleCalendarConfiguration.instance.addToTable('moon', event); + expect(renderSpy).toHaveBeenCalledTimes(1); + expect((SimpleCalendarConfiguration.instance.object).moons.length).toBe(currentLength + 1); + }); + + test('Remove Moon', () => { + const event = new Event('click'); + let currentLength = (SimpleCalendarConfiguration.instance.object).moons.length; + SimpleCalendarConfiguration.instance.removeFromTable('moon',event); + expect(renderSpy).not.toHaveBeenCalled(); + expect((SimpleCalendarConfiguration.instance.object).moons.length).toBe(currentLength); + + //Check for invalid index + (event.currentTarget).setAttribute('data-index', 'a'); + currentLength = (SimpleCalendarConfiguration.instance.object).moons.length; + SimpleCalendarConfiguration.instance.removeFromTable('moon',event); + expect(renderSpy).not.toHaveBeenCalled(); + expect((SimpleCalendarConfiguration.instance.object).moons.length).toBe(currentLength); + + //Check for index outside of season length + (event.currentTarget).setAttribute('data-index', '12'); + currentLength = (SimpleCalendarConfiguration.instance.object).moons.length; + SimpleCalendarConfiguration.instance.removeFromTable('moon',event); + expect(renderSpy).toHaveBeenCalledTimes(1); + expect((SimpleCalendarConfiguration.instance.object).moons.length).toBe(currentLength); + + //Check for valid index + (event.currentTarget).setAttribute('data-index', '0'); + currentLength = (SimpleCalendarConfiguration.instance.object).moons.length; + SimpleCalendarConfiguration.instance.removeFromTable('moon',event); + expect(renderSpy).toHaveBeenCalledTimes(2); + expect((SimpleCalendarConfiguration.instance.object).moons.length).toBe(currentLength - 1); + + //Check for removing all seasons + (event.currentTarget).setAttribute('data-index', 'all'); + SimpleCalendarConfiguration.instance.removeFromTable('moon',event); + expect(renderSpy).toHaveBeenCalledTimes(3); + expect((SimpleCalendarConfiguration.instance.object).moons.length).toBe(0); + }); + + test('Add Moon Phase', () => { + const event = new Event('click'); + const currentLength = (SimpleCalendarConfiguration.instance.object).moons[0].phases.length; + SimpleCalendarConfiguration.instance.addToTable('moon-phase', event); + expect(renderSpy).toHaveBeenCalledTimes(1); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength); + + (event.currentTarget).setAttribute('data-moon-index', 'asd'); + SimpleCalendarConfiguration.instance.addToTable('moon-phase', event); + expect(renderSpy).toHaveBeenCalledTimes(2); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength); + + (event.currentTarget).setAttribute('data-moon-index', '0'); + SimpleCalendarConfiguration.instance.addToTable('moon-phase', event); + expect(renderSpy).toHaveBeenCalledTimes(3); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength + 1); + }); + + test('Remove Moon Phase', () => { + const event = new Event('click'); + let currentLength = (SimpleCalendarConfiguration.instance.object).moons[0].phases.length; + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).not.toHaveBeenCalled(); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength); + + //Check for invalid index + (event.currentTarget).setAttribute('data-index', 'a'); + currentLength = (SimpleCalendarConfiguration.instance.object).moons[0].phases.length; + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).not.toHaveBeenCalled(); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength); + + //Check for index outside of season length + (event.currentTarget).setAttribute('data-index', '12'); + currentLength = (SimpleCalendarConfiguration.instance.object).moons[0].phases.length; + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).toHaveBeenCalledTimes(1); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength); + + //Check for valid index + (event.currentTarget).setAttribute('data-index', '0'); + currentLength = (SimpleCalendarConfiguration.instance.object).moons[0].phases.length; + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).toHaveBeenCalledTimes(2); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength); + + //Check for invalid moon index + (event.currentTarget).setAttribute('data-moon-index', 'a'); + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).toHaveBeenCalledTimes(3); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength); + + //Check for moon index outside of range + (event.currentTarget).setAttribute('data-moon-index', '12'); + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).toHaveBeenCalledTimes(4); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength); + + //Check for valid moon index + (event.currentTarget).setAttribute('data-moon-index', '0'); + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).toHaveBeenCalledTimes(5); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(currentLength - 1); + + //Check for removing all seasons + (event.currentTarget).setAttribute('data-index', 'all'); + (event.currentTarget).removeAttribute('data-moon-index'); + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).toHaveBeenCalledTimes(6); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(0); + + (event.currentTarget).setAttribute('data-moon-index', 'a'); + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).toHaveBeenCalledTimes(7); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(0); + + (event.currentTarget).setAttribute('data-moon-index', '12'); + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).toHaveBeenCalledTimes(8); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(0); + + (event.currentTarget).setAttribute('data-moon-index', '0'); + SimpleCalendarConfiguration.instance.removeFromTable('moon-phase',event); + expect(renderSpy).toHaveBeenCalledTimes(9); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases.length).toBe(0); }); test('Predefined Apply Confirm', () => { @@ -297,16 +478,35 @@ describe('Simple Calendar Configuration Tests', () => { const event = new Event('change'); (event.currentTarget).id = "scDefaultPlayerVisibility"; (event.currentTarget).checked = true; + (event.currentTarget).value = ''; //@ts-ignore expect(SimpleCalendarConfiguration.instance.generalSettings.defaultPlayerNoteVisibility).toBe(false); - SimpleCalendarConfiguration.instance.generalInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); //@ts-ignore expect(SimpleCalendarConfiguration.instance.generalSettings.defaultPlayerNoteVisibility).toBe(true); (event.currentTarget).id = "asd"; - SimpleCalendarConfiguration.instance.generalInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); //@ts-ignore expect(SimpleCalendarConfiguration.instance.generalSettings.defaultPlayerNoteVisibility).toBe(true); + + (event.currentTarget).id = "scGameWorldTime"; + (event.currentTarget).value = 'self'; + SimpleCalendarConfiguration.instance.inputChange(event); + //@ts-ignore + expect((SimpleCalendarConfiguration.instance.object).generalSettings.gameWorldTimeIntegration).toBe('self'); + + (event.currentTarget).id = "scShowClock"; + (event.currentTarget).checked = true; + SimpleCalendarConfiguration.instance.inputChange(event); + //@ts-ignore + expect((SimpleCalendarConfiguration.instance.object).generalSettings.showClock ).toBe(true); + + (event.currentTarget).id = "scPlayersAddNotes"; + (event.currentTarget).checked = true; + SimpleCalendarConfiguration.instance.inputChange(event); + //@ts-ignore + expect((SimpleCalendarConfiguration.instance.object).generalSettings.playersAddNotes ).toBe(true); }); test('Year Input Change', () => { @@ -315,73 +515,115 @@ describe('Simple Calendar Configuration Tests', () => { //Invalid current year (event.currentTarget).value = 'asd'; const beforeYear = (SimpleCalendarConfiguration.instance.object).numericRepresentation; - SimpleCalendarConfiguration.instance.yearInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).numericRepresentation).toBe(beforeYear); //Valid current year (event.currentTarget).value = '10'; - SimpleCalendarConfiguration.instance.yearInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).numericRepresentation).toBe(10); //Prefix (event.currentTarget).id = "scYearPreFix"; (event.currentTarget).value = 'Pre'; - SimpleCalendarConfiguration.instance.yearInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).prefix).toBe('Pre'); //Postfix (event.currentTarget).id = "scYearPostFix"; (event.currentTarget).value = 'Post'; - SimpleCalendarConfiguration.instance.yearInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).postfix).toBe('Post'); //Invalid ID (event.currentTarget).id = "asd"; - SimpleCalendarConfiguration.instance.yearInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).numericRepresentation).toBe(10); expect((SimpleCalendarConfiguration.instance.object).prefix).toBe('Pre'); expect((SimpleCalendarConfiguration.instance.object).postfix).toBe('Post'); + + //Season Stuff + (event.currentTarget).id = "-asd"; + (event.currentTarget).setAttribute('data-index', '0'); + (event.currentTarget).setAttribute('class', 'season-name'); + (event.currentTarget).value = 'Wint'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).seasons[0].name).toBe('Wint'); + + (event.currentTarget).setAttribute('class', 'season-custom'); + (event.currentTarget).value = '#000000'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).seasons[0].customColor).toBe('#000000'); + (event.currentTarget).value = '000000'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).seasons[0].customColor).toBe('#000000'); + + (event.currentTarget).setAttribute('class', 'season-month'); + (event.currentTarget).value = '2'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).seasons[0].startingMonth).toBe(2); + (event.currentTarget).value = 'qwe'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).seasons[0].startingMonth).toBe(2); + + (event.currentTarget).setAttribute('class', 'season-day'); + (event.currentTarget).value = '2'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).seasons[0].startingDay).toBe(2); + (event.currentTarget).value = 'qwe'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).seasons[0].startingDay).toBe(2); + + (event.currentTarget).setAttribute('class', 'season-color'); + (event.currentTarget).value = '#fffeee'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).seasons[0].color).toBe('#fffeee'); + + (event.currentTarget).setAttribute('class', 'season-a'); + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).seasons[0].color).toBe('#fffeee'); }); test('Month Input Change', () => { const event = new Event('change'); + (event.currentTarget).value = ''; (event.currentTarget).classList.remove('next'); //Test No Attributes - SimpleCalendarConfiguration.instance.monthInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect(console.debug).toHaveBeenCalledTimes(1); //Test set index and no class or value (event.currentTarget).setAttribute('data-index', 'a'); - SimpleCalendarConfiguration.instance.monthInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect(console.debug).toHaveBeenCalledTimes(2); //Test set index and class but no value (event.currentTarget).classList.add('month-name'); - SimpleCalendarConfiguration.instance.monthInputChange(event); - expect(console.debug).toHaveBeenCalledTimes(3); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(5); //Test all attributes set but invalid index (event.currentTarget).value = 'X'; - SimpleCalendarConfiguration.instance.monthInputChange(event); - expect(console.debug).toHaveBeenCalledTimes(4); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(8); //Test all attributes set but index outside of month length (event.currentTarget).setAttribute('data-index', '12'); - SimpleCalendarConfiguration.instance.monthInputChange(event); - expect(console.debug).toHaveBeenCalledTimes(5); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(11); //Test all attributes for month name change (event.currentTarget).setAttribute('data-index', '0'); - SimpleCalendarConfiguration.instance.monthInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).months[0].name).toBe('X'); //Test all attributes for month day length change, invalid value (event.currentTarget).classList.remove('month-name'); (event.currentTarget).classList.add('month-days'); let numDays = (SimpleCalendarConfiguration.instance.object).months[0].numberOfDays; - SimpleCalendarConfiguration.instance.monthInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).months[0].numberOfDays).toBe(numDays); //Test all attributes for month day length change, set to same day length (event.currentTarget).value = (SimpleCalendarConfiguration.instance.object).months[0].numberOfDays.toString(); - SimpleCalendarConfiguration.instance.monthInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).months[0].numberOfDays).toBe(numDays); //Test all attributes for month day length change (event.currentTarget).value = '20'; - SimpleCalendarConfiguration.instance.monthInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).months[0].numberOfDays).toBe(20); //Test intercalary change @@ -390,93 +632,290 @@ describe('Simple Calendar Configuration Tests', () => { (event.currentTarget).classList.remove('month-days'); (event.currentTarget).classList.add('month-intercalary'); (event.currentTarget).checked = true; - SimpleCalendarConfiguration.instance.monthInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).months[0].intercalary).toBe(true); (event.currentTarget).checked = false; - SimpleCalendarConfiguration.instance.monthInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).months[0].intercalary).toBe(false); //Test intercalary include change (event.currentTarget).classList.remove('month-intercalary'); (event.currentTarget).classList.add('month-intercalary-include'); (event.currentTarget).checked = true; - SimpleCalendarConfiguration.instance.monthInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).months[0].intercalaryInclude).toBe(true); //Test invlaid class name (event.currentTarget).classList.remove('month-intercalary-include'); (event.currentTarget).classList.add('no'); - SimpleCalendarConfiguration.instance.monthInputChange(event); - expect(console.debug).toHaveBeenCalledTimes(6); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(35); }); test('Show Weekday Input Change', () => { const event = new Event('change'); + (event.currentTarget).id = 'scShowWeekdayHeaders'; + (event.currentTarget).value = ''; (event.currentTarget).checked = true; - SimpleCalendarConfiguration.instance.showWeekdayInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).showWeekdayHeadings).toBe(true); (event.currentTarget).checked = false; - SimpleCalendarConfiguration.instance.showWeekdayInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).showWeekdayHeadings).toBe(false); }); test('Weekday Input Change', () => { const event = new Event('change'); + (event.currentTarget).classList.remove('next'); + (event.currentTarget).classList.add('weekday-name'); + (event.currentTarget).value = ''; //Test No Attributes - SimpleCalendarConfiguration.instance.weekdayInputChange(event); - expect(console.debug).toHaveBeenCalledTimes(1); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(2); //Test set index and no value (event.currentTarget).setAttribute('data-index', 'a'); - SimpleCalendarConfiguration.instance.weekdayInputChange(event); - expect(console.debug).toHaveBeenCalledTimes(2); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(5); //Test all attributes set but invalid index (event.currentTarget).value = 'X'; - SimpleCalendarConfiguration.instance.weekdayInputChange(event); - expect(console.debug).toHaveBeenCalledTimes(3); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(8); //Test all attributes set but index outside of weekday length (event.currentTarget).setAttribute('data-index', '12'); - SimpleCalendarConfiguration.instance.weekdayInputChange(event); - expect(console.debug).toHaveBeenCalledTimes(4); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(11); //Test all attributes for weekday name change (event.currentTarget).setAttribute('data-index', '0'); - SimpleCalendarConfiguration.instance.weekdayInputChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).weekdays[0].name).toBe('X'); }); test('Leap Year Rule Change', () => { const event = new Event('change'); - SimpleCalendarConfiguration.instance.leapYearRuleChange(event); + (event.currentTarget).id = 'scLeapYearRule'; + (event.currentTarget).value = ''; + SimpleCalendarConfiguration.instance.inputChange(event); expect(renderSpy).toHaveBeenCalledTimes(1); }); test('Leap Year Month Change', () => { const event = new Event('change'); + (event.currentTarget).classList.remove('next'); + (event.currentTarget).classList.add('month-leap-days'); + (event.currentTarget).value = ''; //Test No Attributes - SimpleCalendarConfiguration.instance.leapYearMonthChange(event); - expect(console.debug).toHaveBeenCalledTimes(1); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(2); //Test set index and no value (event.currentTarget).setAttribute('data-index', 'a'); - SimpleCalendarConfiguration.instance.leapYearMonthChange(event); - expect(console.debug).toHaveBeenCalledTimes(2); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(5); //Test all attributes set but invalid index (event.currentTarget).value = '4'; - SimpleCalendarConfiguration.instance.leapYearMonthChange(event); - expect(console.debug).toHaveBeenCalledTimes(3); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(8); //Test all attributes set but index outside of weekday length (event.currentTarget).setAttribute('data-index', '12'); - SimpleCalendarConfiguration.instance.leapYearMonthChange(event); - expect(console.debug).toHaveBeenCalledTimes(4); + SimpleCalendarConfiguration.instance.inputChange(event); + expect(console.debug).toHaveBeenCalledTimes(11); //Test all attributes for weekday name change (event.currentTarget).setAttribute('data-index', '0'); - SimpleCalendarConfiguration.instance.leapYearMonthChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).months[0].numberOfLeapYearDays).toBe(4); //Test invalid number of days (event.currentTarget).value = 'asd'; - SimpleCalendarConfiguration.instance.leapYearMonthChange(event); + SimpleCalendarConfiguration.instance.inputChange(event); expect((SimpleCalendarConfiguration.instance.object).months[0].numberOfLeapYearDays).toBe(4); }); + test('Time Input Change', () => { + const event = new Event('change'); + (event.currentTarget).value = ''; + (event.currentTarget).id = "asd"; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).time.hoursInDay).toBe(24); + + //Invalid hours in day + (event.currentTarget).id = "scHoursInDay"; + (event.currentTarget).value = 'asd'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).time.hoursInDay).toBe(24); + //Valid hours in day + (event.currentTarget).value = '10'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).time.hoursInDay).toBe(10); + + //Invalid minutes in hour + (event.currentTarget).id = "scMinutesInHour"; + (event.currentTarget).value = 'asd'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).time.minutesInHour).toBe(60); + //Valid minutes in hour + (event.currentTarget).value = '10'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).time.minutesInHour).toBe(10); + + //Invalid seconds in minute + (event.currentTarget).id = "scSecondsInMinute"; + (event.currentTarget).value = 'asd'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).time.secondsInMinute).toBe(60); + //Valid seconds in minute + (event.currentTarget).value = '10'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).time.secondsInMinute).toBe(10); + + //Invalid game time ratio + (event.currentTarget).id = "scGameTimeRatio"; + (event.currentTarget).value = 'asd'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).time.gameTimeRatio).toBe(1); + //Valid game time ratio + (event.currentTarget).value = '10'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).time.gameTimeRatio).toBe(10); + }); + + test('Moon Input Change', () => { + const event = new Event('change'); + (event.currentTarget).classList.remove('next'); + (event.currentTarget).classList.add('moon-name'); + (event.currentTarget).setAttribute('data-index', '0'); + (event.currentTarget).value = 'moon'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].name).toBe('moon'); + + (event.currentTarget).classList.remove('moon-name'); + (event.currentTarget).classList.add('moon-cycle-length'); + let prev:any = (SimpleCalendarConfiguration.instance.object).moons[0].cycleLength; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].cycleLength).toBe(prev); + (event.currentTarget).value = '12.34'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].cycleLength).toBe(12.34); + + (event.currentTarget).classList.remove('moon-cycle-length'); + (event.currentTarget).classList.add('moon-cycle-adjustment'); + (event.currentTarget).value = 'asd'; + prev = (SimpleCalendarConfiguration.instance.object).moons[0].cycleDayAdjust; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].cycleDayAdjust).toBe(prev); + (event.currentTarget).value = '12.34'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].cycleDayAdjust).toBe(12.34); + + (event.currentTarget).classList.remove('moon-cycle-adjustment'); + (event.currentTarget).classList.add('moon-year-reset'); + (event.currentTarget).value = 'leap-year'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.yearReset).toBe(MoonYearResetOptions.LeapYear); + + (event.currentTarget).classList.remove('moon-year-reset'); + (event.currentTarget).classList.add('moon-year-x'); + (event.currentTarget).value = 'asd'; + prev = (SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.yearX; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.yearX).toBe(prev); + (event.currentTarget).value = '12'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.yearX).toBe(12); + + (event.currentTarget).classList.remove('moon-year-x'); + (event.currentTarget).classList.add('moon-year'); + (event.currentTarget).value = 'asd'; + prev = (SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.year; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.year).toBe(prev); + (event.currentTarget).value = '12'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.year).toBe(12); + + (event.currentTarget).classList.remove('moon-year'); + (event.currentTarget).classList.add('moon-month'); + (event.currentTarget).value = 'asd'; + prev = (SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.month; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.month).toBe(prev); + (event.currentTarget).value = '12'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.month).toBe(12); + + (event.currentTarget).classList.remove('moon-month'); + (event.currentTarget).classList.add('moon-day'); + (event.currentTarget).value = 'asd'; + prev = (SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.day; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.day).toBe(prev); + (event.currentTarget).value = '12'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].firstNewMoon.day).toBe(12); + + (event.currentTarget).classList.remove('moon-day'); + (event.currentTarget).classList.add('moon-color'); + (event.currentTarget).value = 'asd'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].color).toBe('#asd'); + (event.currentTarget).value = '#fff'; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].color).toBe('#fff'); + + (event.currentTarget).classList.remove('moon-color'); + (event.currentTarget).classList.add('moon-phase-name'); + (event.currentTarget).value = 'asd'; + prev = (SimpleCalendarConfiguration.instance.object).moons[0].phases[0].name; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases[0].name).toBe(prev); + (event.currentTarget).setAttribute('data-moon-index', 'asd'); + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases[0].name).toBe(prev); + (event.currentTarget).setAttribute('data-moon-index', '0'); + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases[0].name).toBe('asd'); + + (event.currentTarget).classList.remove('moon-phase-name'); + (event.currentTarget).classList.add('moon-phase-single-day'); + (event.currentTarget).checked = true; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases[0].singleDay).toBe(true); + + (event.currentTarget).classList.remove('moon-phase-single-day'); + (event.currentTarget).classList.add('moon-phase-icon'); + (event.currentTarget).value = MoonIcons.LastQuarter; + SimpleCalendarConfiguration.instance.inputChange(event); + expect((SimpleCalendarConfiguration.instance.object).moons[0].phases[0].icon).toBe(MoonIcons.LastQuarter); + + }); + + test('Update Month Days', () => { + const m = new Month('Month', 1, 10); + SimpleCalendarConfiguration.instance.updateMonthDays(m); + expect(m.numberOfDays).toBe(10); + expect(m.numberOfLeapYearDays).toBe(10); + + m.numberOfLeapYearDays = -1; + SimpleCalendarConfiguration.instance.updateMonthDays(m); + expect(m.numberOfLeapYearDays).toBe(10); + + m.days[9].current = true; + m.numberOfDays = 5; + SimpleCalendarConfiguration.instance.updateMonthDays(m); + expect(m.numberOfDays).toBe(5); + expect(m.days[0].current).toBe(false); + + m.numberOfDays = -1; + SimpleCalendarConfiguration.instance.updateMonthDays(m); + expect(m.numberOfDays).toBe(0); + + + }); + + test('Overwrite Confirmation Dialog', () => { + SimpleCalendarConfiguration.instance.overwriteConfirmationDialog('a', 'b', new Event('click')); + //@ts-ignore + expect(DialogRenderer).toHaveBeenCalledTimes(1); + }); + test('Save Click', async () => { //@ts-ignore game.user.isGM = true; @@ -485,77 +924,73 @@ describe('Simple Calendar Configuration Tests', () => { //Exception being thrown await SimpleCalendarConfiguration.instance.saveClick(event); - expect(console.error).toHaveBeenCalledTimes(1); + //expect(console.error).toHaveBeenCalledTimes(1); const invalidYear = document.createElement('input'); invalidYear.value = 'a'; const validYear = document.createElement('input'); validYear.value = '2'; - const pre = document.createElement('input'); - pre.value = 'pre'; - const post = document.createElement('input'); - post.value = 'post'; - const validCustMod = document.createElement('input'); - validCustMod.value = '4'; - const invalidCustMod = document.createElement('input'); - invalidCustMod.value = 'qwe'; + const gameWorldIntegration = document.createElement('input'); + gameWorldIntegration.value = 'self'; const showWeekday = document.createElement('input'); showWeekday.checked = true; - - //Invalid year, month and weekday - jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValueOnce(invalidYear).mockReturnValueOnce(pre).mockReturnValueOnce(post).mockReturnValueOnce(showWeekday).mockReturnValueOnce(showWeekday) + //Invalid year + jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValueOnce(gameWorldIntegration).mockReturnValueOnce(invalidYear).mockReturnValueOnce(showWeekday); await SimpleCalendarConfiguration.instance.saveClick(event); - expect(game.settings.set).toHaveBeenCalledTimes(4); + expect(game.settings.set).toHaveBeenCalledTimes(7); expect(closeSpy).toHaveBeenCalledTimes(1); //Valid year weekday, invalid month days - jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValueOnce(validYear).mockReturnValueOnce(pre).mockReturnValueOnce(post).mockReturnValueOnce(showWeekday).mockReturnValueOnce(showWeekday); + //@ts-ignore + SimpleCalendarConfiguration.instance.yearChanged = true; + jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValueOnce(gameWorldIntegration).mockReturnValueOnce(validYear).mockReturnValueOnce(showWeekday); await SimpleCalendarConfiguration.instance.saveClick(event); - expect(game.settings.set).toHaveBeenCalledTimes(8); + expect(game.settings.set).toHaveBeenCalledTimes(16); expect(closeSpy).toHaveBeenCalledTimes(2); expect((SimpleCalendarConfiguration.instance.object).numericRepresentation).toBe(2); expect((SimpleCalendarConfiguration.instance.object).selectedYear).toBe(2); expect((SimpleCalendarConfiguration.instance.object).visibleYear).toBe(2); - expect((SimpleCalendarConfiguration.instance.object).prefix).toBe('pre'); - expect((SimpleCalendarConfiguration.instance.object).postfix).toBe('post'); - expect((SimpleCalendarConfiguration.instance.object).months[0].name).toBe('X'); - expect((SimpleCalendarConfiguration.instance.object).weekdays[0].name).toBe('Z'); + }); - //Valid year weekday, month days the same as passed in days - jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValueOnce(validYear).mockReturnValueOnce(pre).mockReturnValueOnce(post).mockReturnValueOnce(showWeekday).mockReturnValueOnce(invalidCustMod).mockReturnValueOnce(showWeekday); - await SimpleCalendarConfiguration.instance.saveClick(event); - expect(game.settings.set).toHaveBeenCalledTimes(13); - expect(closeSpy).toHaveBeenCalledTimes(3); + test('Overwrite Confirmation Yes', async () => { + //@ts-ignore + game.user.isGM = true; + await SimpleCalendarConfiguration.instance.overwriteConfirmationYes('a', 'b'); + expect(renderSpy).not.toHaveBeenCalled(); + expect(game.settings.set).not.toHaveBeenCalled(); - //Valid year weekday valid month days - (SimpleCalendarConfiguration.instance.object).months[0].days[3].current = true; - jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValueOnce(validYear).mockReturnValueOnce(pre).mockReturnValueOnce(post).mockReturnValueOnce(showWeekday).mockReturnValueOnce(validCustMod).mockReturnValueOnce(showWeekday); - await SimpleCalendarConfiguration.instance.saveClick(event); - expect(game.settings.set).toHaveBeenCalledTimes(18); - expect(closeSpy).toHaveBeenCalledTimes(4); - expect((SimpleCalendarConfiguration.instance.object).months[0].days.length).toBe(7); - - //Valid year weekday valid month days, new month days is smaller than current month day - (SimpleCalendarConfiguration.instance.object).months[0].days[0].current = false; - (SimpleCalendarConfiguration.instance.object).months[0].days[6].current = true; - jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValueOnce(validYear).mockReturnValueOnce(pre).mockReturnValueOnce(post).mockReturnValueOnce(showWeekday).mockReturnValueOnce(showWeekday); - await SimpleCalendarConfiguration.instance.saveClick(event); - expect(game.settings.set).toHaveBeenCalledTimes(23); - expect(closeSpy).toHaveBeenCalledTimes(5); - //expect((SimpleCalendarConfiguration.instance.object).months[0].days[0].current).toBe(true); + const select = document.createElement('input'); + select.value = 'gregorian'; + jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValue(select); + await SimpleCalendarConfiguration.instance.overwriteConfirmationYes('predefined', 'b'); + expect(renderSpy).toHaveBeenCalledTimes(1); + expect(game.settings.set).not.toHaveBeenCalled(); - //@ts-ignore - SimpleCalendarConfiguration.instance.yearChanged = true; - jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValueOnce(validYear).mockReturnValueOnce(pre).mockReturnValueOnce(post).mockReturnValueOnce(showWeekday).mockReturnValueOnce(showWeekday); - await SimpleCalendarConfiguration.instance.saveClick(event); - expect(game.settings.set).toHaveBeenCalledTimes(29); - expect(closeSpy).toHaveBeenCalledTimes(6); + await SimpleCalendarConfiguration.instance.overwriteConfirmationYes('tp-import', 'b'); + expect(renderSpy).toHaveBeenCalledTimes(1); + expect(game.settings.set).toHaveBeenCalledTimes(1); - //Valid year weekday valid month days, no current day - jest.spyOn(document, 'getElementById').mockImplementation().mockReturnValueOnce(validYear).mockReturnValueOnce(pre).mockReturnValueOnce(post).mockReturnValueOnce(showWeekday).mockReturnValueOnce(showWeekday); - await SimpleCalendarConfiguration.instance.saveClick(event); - expect(game.settings.set).toHaveBeenCalledTimes(35); - expect(closeSpy).toHaveBeenCalledTimes(7); + await SimpleCalendarConfiguration.instance.overwriteConfirmationYes('tp-import', 'about-time'); + expect(renderSpy).toHaveBeenCalledTimes(2); + expect(game.settings.set).toHaveBeenCalledTimes(2); + + await SimpleCalendarConfiguration.instance.overwriteConfirmationYes('tp-import', 'calendar-weather'); + expect(renderSpy).toHaveBeenCalledTimes(3); + expect(game.settings.set).toHaveBeenCalledTimes(3); + + await SimpleCalendarConfiguration.instance.overwriteConfirmationYes('tp-export', 'b'); + expect(renderSpy).toHaveBeenCalledTimes(3); + expect(game.settings.set).toHaveBeenCalledTimes(4); + + await SimpleCalendarConfiguration.instance.overwriteConfirmationYes('tp-export', 'about-time'); + expect(renderSpy).toHaveBeenCalledTimes(3); + expect(game.settings.set).toHaveBeenCalledTimes(5); + + await SimpleCalendarConfiguration.instance.overwriteConfirmationYes('tp-export', 'calendar-weather'); + expect(renderSpy).toHaveBeenCalledTimes(3); + expect(game.settings.set).toHaveBeenCalledTimes(6); + + (game.settings.set).mockClear(); }); }); diff --git a/src/classes/simple-calendar-configuration.ts b/src/classes/simple-calendar-configuration.ts index 6cbeb315..51e373b4 100644 --- a/src/classes/simple-calendar-configuration.ts +++ b/src/classes/simple-calendar-configuration.ts @@ -3,7 +3,10 @@ import Year from "./year"; import {GameSettings} from "./game-settings"; import Month from "./month"; import {Weekday} from "./weekday"; -import {LeapYearRules} from "../constants"; +import {GameWorldTimeIntegrations, LeapYearRules, MoonIcons, MoonYearResetOptions} from "../constants"; +import Importer from "./importer"; +import Season from "./season"; +import Moon from "./moon"; export class SimpleCalendarConfiguration extends FormApplication { @@ -52,7 +55,7 @@ export class SimpleCalendarConfiguration extends FormApplication { options.resizable = true; options.tabs = [{navSelector: ".tabs", contentSelector: "form", initial: "yearSettings"}]; options.height = 700; - options.width = 710; + options.width = 960; return options; } @@ -100,8 +103,69 @@ export class SimpleCalendarConfiguration extends FormApplication { greyhawk: 'Greyhawk', harptos: 'Harptos', warhammer: "Warhammer" + }, + timeTrackers: { + none: 'FSC.Configuration.General.None', + self: 'FSC.Configuration.General.Self', + 'third-party': 'FSC.Configuration.General.ThirdParty', + 'mixed': 'FSC.Configuration.General.Mixed' + }, + importing: { + showAboutTime: false, + showCalendarWeather:false + }, + seasons: (this.object).seasons.map(s => s.toTemplate(this.object)), + seasonColors: [ + { + value: '#ffffff', + display: GameSettings.Localize("FSC.Configuration.Season.ColorWhite") + }, + { + value: '#fffce8', + display: GameSettings.Localize("FSC.Configuration.Season.ColorSpring") + }, + { + value: '#f3fff3', + display: GameSettings.Localize("FSC.Configuration.Season.ColorSummer") + }, + { + value: '#fff7f2', + display: GameSettings.Localize("FSC.Configuration.Season.ColorFall") + }, + { + value: '#f2f8ff', + display: GameSettings.Localize("FSC.Configuration.Season.ColorWinter") + }, + { + value: 'custom', + display: GameSettings.Localize("FSC.Configuration.Season.ColorCustom") + } + ], + moons: (this.object).moons.map(m => m.toTemplate(this.object)), + moonIcons: <{[key: string]: string}>{}, + moonYearReset: { + none: 'FSC.Configuration.Moon.YearResetNo', + 'leap-year': 'FSC.Configuration.Moon.YearResetLeap', + 'x-years': 'FSC.Configuration.Moon.YearResetX' } }; + + const calendarWeather = game.modules.get('calendar-weather'); + const aboutTime = game.modules.get('about-time'); + + data.importing.showCalendarWeather = calendarWeather !== undefined && calendarWeather.active; + data.importing.showAboutTime = aboutTime !== undefined && aboutTime.active; + + + data.moonIcons[MoonIcons.NewMoon] = GameSettings.Localize('FSC.Moon.Phase.New'); + data.moonIcons[MoonIcons.WaxingCrescent] = GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'); + data.moonIcons[MoonIcons.FirstQuarter] = GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'); + data.moonIcons[MoonIcons.WaxingGibbous] = GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'); + data.moonIcons[MoonIcons.Full] = GameSettings.Localize('FSC.Moon.Phase.Full'); + data.moonIcons[MoonIcons.WaningGibbous] = GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'); + data.moonIcons[MoonIcons.LastQuarter] = GameSettings.Localize('FSC.Moon.Phase.LastQuarter'); + data.moonIcons[MoonIcons.WaningCrescent] = GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'); + return data; } @@ -128,28 +192,39 @@ export class SimpleCalendarConfiguration extends FormApplication { (html).find("#scSubmit").on('click', SimpleCalendarConfiguration.instance.saveClick.bind(this)); //Predefined calendar apply - (html).find("#scApplyPredefined").on('click', SimpleCalendarConfiguration.instance.predefinedApply.bind(this)); - - //Month Deletes - (html).find(".remove-month").on('click', SimpleCalendarConfiguration.instance.removeMonth.bind(this)); + (html).find("#scApplyPredefined").on('click', SimpleCalendarConfiguration.instance.overwriteConfirmationDialog.bind(this, 'predefined', '')); - //Weekday Deletes - (html).find(".remove-weekday").on('click', SimpleCalendarConfiguration.instance.removeWeekday.bind(this)); + //Table Removes + (html).find(".remove-month").on('click', SimpleCalendarConfiguration.instance.removeFromTable.bind(this, 'month')); + (html).find(".remove-weekday").on('click', SimpleCalendarConfiguration.instance.removeFromTable.bind(this, 'weekday')); + (html).find(".remove-season").on('click', SimpleCalendarConfiguration.instance.removeFromTable.bind(this, 'season')); + (html).find(".remove-moon").on('click', SimpleCalendarConfiguration.instance.removeFromTable.bind(this, 'moon')); + (html).find(".remove-moon-phase").on('click', SimpleCalendarConfiguration.instance.removeFromTable.bind(this, 'moon-phase')); - //Add Month - (html).find(".month-add").on('click', SimpleCalendarConfiguration.instance.addMonth.bind(this)); + //Table Adds + (html).find(".month-add").on('click', SimpleCalendarConfiguration.instance.addToTable.bind(this, 'month')); + (html).find(".weekday-add").on('click', SimpleCalendarConfiguration.instance.addToTable.bind(this, 'weekday')); + (html).find(".season-add").on('click', SimpleCalendarConfiguration.instance.addToTable.bind(this, 'season')); + (html).find(".moon-add").on('click', SimpleCalendarConfiguration.instance.addToTable.bind(this, 'moon')); + (html).find(".moon-phase-add").on('click', SimpleCalendarConfiguration.instance.addToTable.bind(this, 'moon-phase')); - //Add Weekday - (html).find(".weekday-add").on('click', SimpleCalendarConfiguration.instance.addWeekday.bind(this)); + //Import Buttons + (html).find("#scAboutTimeImport").on('click', SimpleCalendarConfiguration.instance.overwriteConfirmationDialog.bind(this, 'tp-import', 'about-time')); + (html).find("#scAboutTimeExport").on('click', SimpleCalendarConfiguration.instance.overwriteConfirmationDialog.bind(this, 'tp-export','about-time')); + (html).find("#scCalendarWeatherImport").on('click', SimpleCalendarConfiguration.instance.overwriteConfirmationDialog.bind(this, 'tp-import', 'calendar-weather')); + (html).find("#scCalendarWeatherExport").on('click', SimpleCalendarConfiguration.instance.overwriteConfirmationDialog.bind(this, 'tp-export','calendar-weather')); //Input Change - (html).find("#scDefaultPlayerVisibility").on('change', SimpleCalendarConfiguration.instance.generalInputChange.bind(this)); - (html).find(".year-settings input").on('change', SimpleCalendarConfiguration.instance.yearInputChange.bind(this)); - (html).find(".month-settings .f-table .row div input").on('change', SimpleCalendarConfiguration.instance.monthInputChange.bind(this)); - (html).find(".weekday-settings #scShowWeekdayHeaders").on('change', SimpleCalendarConfiguration.instance.showWeekdayInputChange.bind(this)); - (html).find(".weekday-settings table td input").on('change', SimpleCalendarConfiguration.instance.weekdayInputChange.bind(this)); - (html).find(".leapyear-settings #scLeapYearRule").on('change', SimpleCalendarConfiguration.instance.leapYearRuleChange.bind(this)); - (html).find(".leapyear-settings table td input").on('change', SimpleCalendarConfiguration.instance.leapYearMonthChange.bind(this)); + (html).find(".general-settings input").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); + (html).find(".year-settings input").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); + (html).find(".year-settings select").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); + (html).find(".month-settings input").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); + (html).find(".weekday-settings input").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); + (html).find(".leapyear-settings input").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); + (html).find(".leapyear-settings select").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); + (html).find(".time-settings input").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); + (html).find(".moon-settings input").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); + (html).find(".moon-settings select").on('change', SimpleCalendarConfiguration.instance.inputChange.bind(this)); } } @@ -172,100 +247,149 @@ export class SimpleCalendarConfiguration extends FormApplication { } /** - * Adds a new month to the list of months in the year - * @param {Event} e The passed in event - */ - public addMonth(e: Event){ - e.preventDefault(); - const newMonthNumber = (this.object).months.length + 1; - (this.object).months.push(new Month('New Month', newMonthNumber, 30)); - this.rebaseMonthNumbers(); - this.updateApp(); - } - - /** - * Removes a month from the list of months in the year - * @param {Event} e The passed in event + * Adds to the specified table. + * @param {string} setting The settings table we are adding to. only accepts month, weekday, season, moon, moon-phase + * @param {Event} e The click event */ - public removeMonth(e: Event){ + public addToTable(setting: string, e: Event){ e.preventDefault(); - const dataIndex = (e.currentTarget).getAttribute('data-index'); - if(dataIndex && dataIndex !== 'all'){ - const monthIndex = parseInt(dataIndex); - const months = (this.object).months; - if(!isNaN(monthIndex) && monthIndex < months.length){ - months.splice(monthIndex, 1); - //Reindex the remaining months - for(let i = 0; i < months.length; i++){ - months[i].numericRepresentation = i + 1; - } + const filteredSetting = setting.toLowerCase() as 'month' | 'weekday' | 'season' | 'moon' | 'moon-phase'; + switch (filteredSetting){ + case "month": + const newMonthNumber = (this.object).months.length + 1; + (this.object).months.push(new Month('New Month', newMonthNumber, 30)); this.rebaseMonthNumbers(); - this.updateApp(); - } - } else if(dataIndex && dataIndex === 'all'){ - (this.object).months = []; - this.updateApp(); + break; + case "weekday": + const newWeekdayNumber = (this.object).weekdays.length + 1; + (this.object).weekdays.push(new Weekday(newWeekdayNumber, 'New Weekday')); + break; + case "season": + (this.object).seasons.push(new Season('New Season', 1, 1)); + break; + case "moon": + const newMoon = new Moon('Moon', 29.53059); + newMoon.firstNewMoon = { + yearReset: MoonYearResetOptions.None, + yearX: 0, + year: 0, + month: 1, + day: 1 + }; + const phaseLength = Number(((newMoon.cycleLength - 4) / 4).toPrecision(5)); + newMoon.phases = [ + {name: GameSettings.Localize('FSC.Moon.Phase.New'), length: 1, icon: MoonIcons.NewMoon, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'), length: phaseLength, icon: MoonIcons.WaxingCrescent, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'), length: 1, icon: MoonIcons.FirstQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'), length: phaseLength, icon: MoonIcons.WaxingGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.Full'), length: 1, icon: MoonIcons.Full, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'), length: phaseLength, icon: MoonIcons.WaningGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.LastQuarter'), length: 1, icon: MoonIcons.LastQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'), length: phaseLength, icon: MoonIcons.WaningCrescent, singleDay: false} + ]; + (this.object).moons.push(newMoon); + break; + case "moon-phase": + const dataMoonIndex = (e.currentTarget).getAttribute('data-moon-index'); + if(dataMoonIndex){ + const moonIndex = parseInt(dataMoonIndex); + if(!isNaN(moonIndex) && moonIndex < (this.object).moons.length){ + (this.object).moons[moonIndex].phases.push({ + name: "Phase", + length: 1, + icon: MoonIcons.NewMoon, + singleDay: false + }); + (this.object).moons[moonIndex].updatePhaseLength(); + } + } + break; } - } - - /** - * Adds a new weekday to the list of weekdays in the year - * @param {Event} e The passed in event - */ - public addWeekday(e: Event) { - e.preventDefault(); - const newWeekdayNumber = (this.object).weekdays.length + 1; - (this.object).weekdays.push(new Weekday(newWeekdayNumber, 'New Weekday')); this.updateApp(); } /** - * Removes a weekday from the list of weekdays in the year - * @param {Event} e The passed in event + * Removes one or more row from the specified table. + * @param {string} setting The settings table we are removing from. only accepts month, weekday, season, moon, moon-phase + * @param {Event} e The click event */ - public removeWeekday(e: Event) { + public removeFromTable(setting: string, e: Event){ e.preventDefault(); + const filteredSetting = setting.toLowerCase() as 'month' | 'weekday' | 'season' | 'moon' | 'moon-phase'; const dataIndex = (e.currentTarget).getAttribute('data-index'); if(dataIndex && dataIndex !== 'all'){ - const weekdayIndex = parseInt(dataIndex); - const weekdays = (this.object).weekdays; - if(!isNaN(weekdayIndex) && weekdayIndex < weekdays.length){ - weekdays.splice(weekdayIndex, 1); - //Reindex the remaining months - for(let i = 0; i < weekdays.length; i++){ - weekdays[i].numericRepresentation = i + 1; + const index = parseInt(dataIndex); + if(!isNaN(index)){ + switch (filteredSetting){ + case "month": + if(index < (this.object).months.length){ + (this.object).months.splice(index, 1); + //Reindex the remaining months + for(let i = 0; i < (this.object).months.length; i++){ + (this.object).months[i].numericRepresentation = i + 1; + } + this.rebaseMonthNumbers(); + } + break; + case "weekday": + if(index < (this.object).weekdays.length){ + (this.object).weekdays.splice(index, 1); + //Reindex the remaining months + for(let i = 0; i < (this.object).weekdays.length; i++){ + (this.object).weekdays[i].numericRepresentation = i + 1; + } + } + break; + case "season": + if(index < (this.object).seasons.length){ + (this.object).seasons.splice(index, 1); + } + break; + case "moon": + if(index < (this.object).moons.length){ + (this.object).moons.splice(index, 1); + } + break; + case "moon-phase": + const dataMoonIndex = (e.currentTarget).getAttribute('data-moon-index'); + if(dataMoonIndex){ + const moonIndex = parseInt(dataMoonIndex); + if(!isNaN(moonIndex) && moonIndex < (this.object).moons.length && index < (this.object).moons[moonIndex].phases.length){ + (this.object).moons[moonIndex].phases.splice(index, 1); + (this.object).moons[moonIndex].updatePhaseLength(); + } + } + break; } this.updateApp(); } } else if(dataIndex && dataIndex === 'all'){ - (this.object).weekdays = []; + switch (filteredSetting){ + case "month": + (this.object).months = []; + break; + case "weekday": + (this.object).weekdays = []; + break; + case "season": + (this.object).seasons = []; + break; + case "moon": + (this.object).moons = []; + break; + case "moon-phase": + const dataMoonIndex = (e.currentTarget).getAttribute('data-moon-index'); + if(dataMoonIndex){ + const moonIndex = parseInt(dataMoonIndex); + if(!isNaN(moonIndex) && moonIndex < (this.object).moons.length){ + (this.object).moons[moonIndex].phases = []; + } + } + break; + } this.updateApp(); } - } - /** - * When the Apply button for the predefined calendar is clicked, show a dialog to confirm their actions - * @param {Event} e The event that triggered this - */ - public predefinedApply(e: Event){ - e.preventDefault(); - const dialog = new Dialog({ - title: GameSettings.Localize('FSC.OverwriteConfirm'), - content: GameSettings.Localize("FSC.OverwriteConfirmText"), - buttons:{ - yes: { - icon: '', - label: GameSettings.Localize('FSC.Apply'), - callback: SimpleCalendarConfiguration.instance.predefinedApplyConfirm.bind(this) - }, - no: { - icon: '', - label: GameSettings.Localize('FSC.Cancel') - } - }, - default: "no" - }); - dialog.render(true); } /** @@ -274,6 +398,7 @@ export class SimpleCalendarConfiguration extends FormApplication { public predefinedApplyConfirm() { const selectedPredefined = (document.getElementById("scPreDefined")).value; Logger.debug(`Overwriting the existing calendar configuration with the "${selectedPredefined}" configuration`); + let phaseLength = 0; switch (selectedPredefined){ case 'gregorian': const currentDate = new Date(); @@ -304,10 +429,43 @@ export class SimpleCalendarConfiguration extends FormApplication { new Weekday(6, GameSettings.Localize('FSC.Date.Friday')), new Weekday(7, GameSettings.Localize('FSC.Date.Saturday')) ]; + (this.object).seasons = [ + new Season('Spring', 3, 20), + new Season('Summer', 6, 20), + new Season('Fall', 9, 22), + new Season('Winter', 12, 21) + ]; + (this.object).time.hoursInDay = 24; + (this.object).time.minutesInHour = 60; + (this.object).time.secondsInMinute = 60; + (this.object).time.gameTimeRatio = 1; (this.object).leapYearRule.rule = LeapYearRules.Gregorian; (this.object).leapYearRule.customMod = 0; (this.object).months[currentDate.getMonth()].current = true; (this.object).months[currentDate.getMonth()].days[currentDate.getDate()-1].current = true; + (this.object).seasons[0].color = "#fffce8"; + (this.object).seasons[1].color = "#f3fff3"; + (this.object).seasons[2].color = "#fff7f2"; + (this.object).seasons[3].color = "#f2f8ff"; + (this.object).moons = [ + new Moon('Moon', 29.53059) + ]; + (this.object).moons[0].firstNewMoon.yearReset = MoonYearResetOptions.None; + (this.object).moons[0].firstNewMoon.year = 2000; + (this.object).moons[0].firstNewMoon.month = 1; + (this.object).moons[0].firstNewMoon.day = 6; + (this.object).moons[0].cycleDayAdjust = 0.5; + phaseLength = Number((((this.object).moons[0].cycleLength - 4) / 4).toPrecision(5)); + (this.object).moons[0].phases = [ + {name: GameSettings.Localize('FSC.Moon.Phase.New'), length: 1, icon: MoonIcons.NewMoon, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'), length: phaseLength, icon: MoonIcons.WaxingCrescent, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'), length: 1, icon: MoonIcons.FirstQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'), length: phaseLength, icon: MoonIcons.WaxingGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.Full'), length: 1, icon: MoonIcons.Full, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'), length: phaseLength, icon: MoonIcons.WaningGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.LastQuarter'), length: 1, icon: MoonIcons.LastQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'), length: phaseLength, icon: MoonIcons.WaningCrescent, singleDay: false} + ]; break; case 'eberron': (this.object).numericRepresentation = 998; @@ -337,10 +495,16 @@ export class SimpleCalendarConfiguration extends FormApplication { new Weekday(6, 'Far'), new Weekday(7, 'Sar') ]; + (this.object).seasons = []; + (this.object).time.hoursInDay = 24; + (this.object).time.minutesInHour = 60; + (this.object).time.secondsInMinute = 60; + (this.object).time.gameTimeRatio = 1; (this.object).leapYearRule.rule = LeapYearRules.None; (this.object).leapYearRule.customMod = 0; (this.object).months[0].current = true; (this.object).months[0].days[0].current = true; + (this.object).moons = []; //TODO: Maybe add all 12 moons? break; case 'exandrian': (this.object).numericRepresentation = 812; @@ -368,10 +532,59 @@ export class SimpleCalendarConfiguration extends FormApplication { new Weekday(5, 'Yulisen'), new Weekday(6, 'Da\'leysen') ]; + (this.object).seasons = [ + new Season('Spring', 3, 13), + new Season('Summer', 5, 26), + new Season('Autumn', 8, 3), + new Season('Winter', 11, 2) + ]; + (this.object).time.hoursInDay = 24; + (this.object).time.minutesInHour = 60; + (this.object).time.secondsInMinute = 60; + (this.object).time.gameTimeRatio = 1; (this.object).leapYearRule.rule = LeapYearRules.None; (this.object).leapYearRule.customMod = 0; (this.object).months[0].current = true; (this.object).months[0].days[0].current = true; + (this.object).seasons[0].color = "#fffce8"; + (this.object).seasons[1].color = "#f3fff3"; + (this.object).seasons[2].color = "#fff7f2"; + (this.object).seasons[3].color = "#f2f8ff"; + (this.object).moons = [ + new Moon('Catha', 33), + new Moon('Ruidus', 328) + ]; + (this.object).moons[0].firstNewMoon.yearReset = MoonYearResetOptions.None; + (this.object).moons[0].firstNewMoon.year = 810; + (this.object).moons[0].firstNewMoon.month = 1; + (this.object).moons[0].firstNewMoon.day = 9; + phaseLength = Number((((this.object).moons[0].cycleLength - 4) / 4).toPrecision(5)); + (this.object).moons[0].phases = [ + {name: GameSettings.Localize('FSC.Moon.Phase.New'), length: 1, icon: MoonIcons.NewMoon, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'), length: phaseLength, icon: MoonIcons.WaxingCrescent, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'), length: 1, icon: MoonIcons.FirstQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'), length: phaseLength, icon: MoonIcons.WaxingGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.Full'), length: 1, icon: MoonIcons.Full, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'), length: phaseLength, icon: MoonIcons.WaningGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.LastQuarter'), length: 1, icon: MoonIcons.LastQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'), length: phaseLength, icon: MoonIcons.WaningCrescent, singleDay: false} + ]; + (this.object).moons[1].color = "#ab82f3"; + (this.object).moons[1].firstNewMoon.yearReset = MoonYearResetOptions.None; + (this.object).moons[1].firstNewMoon.year = 810; + (this.object).moons[1].firstNewMoon.month = 3; + (this.object).moons[1].firstNewMoon.day = 22; + phaseLength = Number((((this.object).moons[1].cycleLength - 4) / 4).toPrecision(5)); + (this.object).moons[1].phases = [ + {name: GameSettings.Localize('FSC.Moon.Phase.New'), length: 1, icon: MoonIcons.NewMoon, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'), length: phaseLength, icon: MoonIcons.WaxingCrescent, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'), length: 1, icon: MoonIcons.FirstQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'), length: phaseLength, icon: MoonIcons.WaxingGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.Full'), length: 1, icon: MoonIcons.Full, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'), length: phaseLength, icon: MoonIcons.WaningGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.LastQuarter'), length: 1, icon: MoonIcons.LastQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'), length: phaseLength, icon: MoonIcons.WaningCrescent, singleDay: false} + ]; break; case 'golarian': (this.object).numericRepresentation = 4710; @@ -401,10 +614,43 @@ export class SimpleCalendarConfiguration extends FormApplication { new Weekday(6, 'Starday'), new Weekday(7, 'Sunday') ]; + (this.object).seasons = [ + new Season('Spring', 3, 1), + new Season('Summer', 6, 1), + new Season('Fall', 9, 1), + new Season('Winter', 12, 1) + ]; + (this.object).time.hoursInDay = 24; + (this.object).time.minutesInHour = 60; + (this.object).time.secondsInMinute = 60; + (this.object).time.gameTimeRatio = 1; (this.object).leapYearRule.rule = LeapYearRules.Custom; (this.object).leapYearRule.customMod = 8; (this.object).months[0].current = true; (this.object).months[0].days[0].current = true; + (this.object).seasons[0].color = "#fffce8"; + (this.object).seasons[1].color = "#f3fff3"; + (this.object).seasons[2].color = "#fff7f2"; + (this.object).seasons[3].color = "#f2f8ff"; + (this.object).moons = [ + new Moon('Somal', 29.5) + ]; + (this.object).moons[0].firstNewMoon.yearReset = MoonYearResetOptions.XYears; + (this.object).moons[0].firstNewMoon.yearX = 4; + (this.object).moons[0].firstNewMoon.year = 4700; + (this.object).moons[0].firstNewMoon.month = 1; + (this.object).moons[0].firstNewMoon.day = 8; + phaseLength = Number((((this.object).moons[0].cycleLength - 4) / 4).toPrecision(5)); + (this.object).moons[0].phases = [ + {name: GameSettings.Localize('FSC.Moon.Phase.New'), length: 1, icon: MoonIcons.NewMoon, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'), length: phaseLength, icon: MoonIcons.WaxingCrescent, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'), length: 1, icon: MoonIcons.FirstQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'), length: phaseLength, icon: MoonIcons.WaxingGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.Full'), length: 1, icon: MoonIcons.Full, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'), length: phaseLength, icon: MoonIcons.WaningGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.LastQuarter'), length: 1, icon: MoonIcons.LastQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'), length: phaseLength, icon: MoonIcons.WaningCrescent, singleDay: false} + ]; break; case 'greyhawk': (this.object).numericRepresentation = 591 ; @@ -442,10 +688,61 @@ export class SimpleCalendarConfiguration extends FormApplication { new Weekday(6, 'Earthday'), new Weekday(7, 'Freeday') ]; + (this.object).seasons = [ + new Season('Spring', 2, 1), + new Season('Low Summer', 4, 1), + new Season('High Summer', 7, 1), + new Season('Fall', 10, 1), + new Season('Winter', 12, 1) + ]; + (this.object).seasons[0].color = "#fffce8"; + (this.object).seasons[1].color = "#f3fff3"; + (this.object).seasons[2].color = "#f3fff3"; + (this.object).seasons[3].color = "#fff7f2"; + (this.object).seasons[4].color = "#f2f8ff"; + (this.object).time.hoursInDay = 24; + (this.object).time.minutesInHour = 60; + (this.object).time.secondsInMinute = 60; + (this.object).time.gameTimeRatio = 1; (this.object).leapYearRule.rule = LeapYearRules.None; (this.object).leapYearRule.customMod = 0; (this.object).months[0].current = true; (this.object).months[0].days[0].current = true; + (this.object).moons = [ + new Moon('Luna', 28), + new Moon('Celene', 91) + ]; + (this.object).moons[0].firstNewMoon.yearReset = MoonYearResetOptions.None; + (this.object).moons[0].firstNewMoon.year = 590; + (this.object).moons[0].firstNewMoon.month = 1; + (this.object).moons[0].firstNewMoon.day = 25; + phaseLength = Number((((this.object).moons[0].cycleLength - 4) / 4).toPrecision(5)); + (this.object).moons[0].phases = [ + {name: GameSettings.Localize('FSC.Moon.Phase.New'), length: 1, icon: MoonIcons.NewMoon, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'), length: phaseLength, icon: MoonIcons.WaxingCrescent, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'), length: 1, icon: MoonIcons.FirstQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'), length: phaseLength, icon: MoonIcons.WaxingGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.Full'), length: 1, icon: MoonIcons.Full, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'), length: phaseLength, icon: MoonIcons.WaningGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.LastQuarter'), length: 1, icon: MoonIcons.LastQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'), length: phaseLength, icon: MoonIcons.WaningCrescent, singleDay: false} + ]; + (this.object).moons[1].color = '#7FFFD4'; + (this.object).moons[1].firstNewMoon.yearReset = MoonYearResetOptions.None; + (this.object).moons[1].firstNewMoon.year = 590; + (this.object).moons[1].firstNewMoon.month = 2; + (this.object).moons[1].firstNewMoon.day = 12; + phaseLength = Number((((this.object).moons[1].cycleLength - 4) / 4).toPrecision(5)); + (this.object).moons[1].phases = [ + {name: GameSettings.Localize('FSC.Moon.Phase.New'), length: 1, icon: MoonIcons.NewMoon, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'), length: phaseLength, icon: MoonIcons.WaxingCrescent, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'), length: 1, icon: MoonIcons.FirstQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'), length: phaseLength, icon: MoonIcons.WaxingGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.Full'), length: 1, icon: MoonIcons.Full, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'), length: phaseLength, icon: MoonIcons.WaningGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.LastQuarter'), length: 1, icon: MoonIcons.LastQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'), length: phaseLength, icon: MoonIcons.WaningCrescent, singleDay: false} + ]; break; case 'harptos': (this.object).numericRepresentation = 1495; @@ -490,10 +787,43 @@ export class SimpleCalendarConfiguration extends FormApplication { new Weekday(9, '9th'), new Weekday(10, '10th') ]; + (this.object).seasons = [ + new Season('Spring', 3, 19), + new Season('Summer', 6, 20), + new Season('Fall', 9, 21), + new Season('Winter', 12, 20) + ]; + (this.object).seasons[0].color = "#fffce8"; + (this.object).seasons[1].color = "#f3fff3"; + (this.object).seasons[2].color = "#fff7f2"; + (this.object).seasons[3].color = "#f2f8ff"; + (this.object).time.hoursInDay = 24; + (this.object).time.minutesInHour = 60; + (this.object).time.secondsInMinute = 60; + (this.object).time.gameTimeRatio = 1; (this.object).leapYearRule.rule = LeapYearRules.Custom; (this.object).leapYearRule.customMod = 4; (this.object).months[0].current = true; (this.object).months[0].days[0].current = true; + (this.object).moons = [ + new Moon('Selûne', 30.45) + ]; + (this.object).moons[0].firstNewMoon.yearReset = MoonYearResetOptions.LeapYear; + (this.object).moons[0].firstNewMoon.year = 1372; + (this.object).moons[0].firstNewMoon.month = 1; + (this.object).moons[0].firstNewMoon.day = 16; + (this.object).moons[0].cycleDayAdjust = 0.5; + phaseLength = Number((((this.object).moons[0].cycleLength - 4) / 4).toPrecision(5)); + (this.object).moons[0].phases = [ + {name: GameSettings.Localize('FSC.Moon.Phase.New'), length: 1, icon: MoonIcons.NewMoon, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'), length: phaseLength, icon: MoonIcons.WaxingCrescent, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'), length: 1, icon: MoonIcons.FirstQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'), length: phaseLength, icon: MoonIcons.WaxingGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.Full'), length: 1, icon: MoonIcons.Full, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'), length: phaseLength, icon: MoonIcons.WaningGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.LastQuarter'), length: 1, icon: MoonIcons.LastQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'), length: phaseLength, icon: MoonIcons.WaningCrescent, singleDay: false} + ]; break; case 'warhammer': (this.object).numericRepresentation = 2522; @@ -536,10 +866,42 @@ export class SimpleCalendarConfiguration extends FormApplication { new Weekday(7, 'Angestag'), new Weekday(8, 'Festag') ]; + (this.object).seasons = [ + new Season('Spring', 3, 20), + new Season('Summer', 6, 20), + new Season('Fall', 9, 22), + new Season('Winter', 12, 21) + ]; + (this.object).seasons[0].color = "#fffce8"; + (this.object).seasons[1].color = "#f3fff3"; + (this.object).seasons[2].color = "#fff7f2"; + (this.object).seasons[3].color = "#f2f8ff"; + (this.object).time.hoursInDay = 24; + (this.object).time.minutesInHour = 60; + (this.object).time.secondsInMinute = 60; + (this.object).time.gameTimeRatio = 1; (this.object).leapYearRule.rule = LeapYearRules.None; (this.object).leapYearRule.customMod = 0; (this.object).months[0].current = true; (this.object).months[0].days[0].current = true; + (this.object).moons = [ + new Moon('Luna', 25) + ]; + (this.object).moons[0].firstNewMoon.yearReset = MoonYearResetOptions.None; + (this.object).moons[0].firstNewMoon.year = 2522; + (this.object).moons[0].firstNewMoon.month = 1; + (this.object).moons[0].firstNewMoon.day = 13; + phaseLength = Number((((this.object).moons[0].cycleLength - 4) / 4).toPrecision(5)); + (this.object).moons[0].phases = [ + {name: GameSettings.Localize('FSC.Moon.Phase.New'), length: 1, icon: MoonIcons.NewMoon, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingCrescent'), length: phaseLength, icon: MoonIcons.WaxingCrescent, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.FirstQuarter'), length: 1, icon: MoonIcons.FirstQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaxingGibbous'), length: phaseLength, icon: MoonIcons.WaxingGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.Full'), length: 1, icon: MoonIcons.Full, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningGibbous'), length: phaseLength, icon: MoonIcons.WaningGibbous, singleDay: false}, + {name: GameSettings.Localize('FSC.Moon.Phase.LastQuarter'), length: 1, icon: MoonIcons.LastQuarter, singleDay: true}, + {name: GameSettings.Localize('FSC.Moon.Phase.WaningCrescent'), length: phaseLength, icon: MoonIcons.WaningCrescent, singleDay: false} + ]; break; } this.yearChanged = true; @@ -547,139 +909,279 @@ export class SimpleCalendarConfiguration extends FormApplication { } /** - * Event when a general settings input changes - * @param {Event} e The event + * Processes all input change requests and ensures data is properly saved between form update + * @param {Event} e The change event */ - public generalInputChange(e: Event){ + public inputChange(e: Event){ + Logger.debug('Input has changed, updating configuration object'); const id = (e.currentTarget).id; - if(id === "scDefaultPlayerVisibility"){ - this.generalSettings.defaultPlayerNoteVisibility = (e.currentTarget).checked; - } - } + const cssClass = (e.currentTarget).getAttribute('class'); + const dataIndex = (e.currentTarget).getAttribute('data-index'); + let value = (e.currentTarget).value.trim(); + const checked = (e.currentTarget).checked; - /** - * Event when any year inputs are changed - * @param {Event} e The event - */ - public yearInputChange(e: Event){ - const id = (e.currentTarget).id; - const value = (e.currentTarget).value; - if(id === "scCurrentYear"){ - const year = parseInt(value); - if(!isNaN(year)){ - (this.object).numericRepresentation = year; - this.yearChanged = true; + if(id && id[0] !== '-'){ + Logger.debug(`ID "${id}" change found`); + //General Setting Inputs + if(id === "scDefaultPlayerVisibility"){ + this.generalSettings.defaultPlayerNoteVisibility = checked; + } else if(id === 'scGameWorldTime'){ + (this.object).generalSettings.gameWorldTimeIntegration = value; + } else if(id === 'scShowClock'){ + (this.object).generalSettings.showClock = checked; + } else if(id === 'scPlayersAddNotes'){ + (this.object).generalSettings.playersAddNotes = checked; + } + //Year Setting Inputs + else if(id === "scCurrentYear"){ + const year = parseInt(value); + if(!isNaN(year)){ + (this.object).numericRepresentation = year; + this.yearChanged = true; + } + } else if(id === 'scYearPreFix'){ + (this.object).prefix = value; + } else if(id === 'scYearPostFix'){ + (this.object).postfix = value; + } + //Weekday Setting Inputs + else if(id === 'scShowWeekdayHeaders'){ + (this.object).showWeekdayHeadings = checked; + } + //Leap Year Setting Inputs + else if(id === 'scLeapYearRule'){ + (this.object).leapYearRule.rule = value; + } + //Time Setting Inputs + else if(id === 'scHoursInDay'){ + const min = parseInt(value); + if(!isNaN(min)){ + (this.object).time.hoursInDay = min; + } + } else if(id === 'scMinutesInHour'){ + const min = parseInt(value); + if(!isNaN(min)){ + (this.object).time.minutesInHour = min; + } + } else if(id === 'scSecondsInMinute'){ + const min = parseInt(value); + if(!isNaN(min)){ + (this.object).time.secondsInMinute = min; + } + } else if(id === 'scGameTimeRatio'){ + const min = parseFloat(value); + if(!isNaN(min)){ + (this.object).time.gameTimeRatio = min; + } } - } else if(id === 'scYearPreFix'){ - (this.object).prefix = value; - } else if(id === 'scYearPostFix'){ - (this.object).postfix = value; - } - } - /** - * Event when a text box for month name or day is changed to temporarily store those changes so that if the application is updated the correct values are displayed - * @param {Event} e The change event - */ - public monthInputChange(e: Event){ - e.preventDefault(); - const dataIndex = (e.currentTarget).getAttribute('data-index'); - const cssClass = (e.currentTarget).getAttribute('class'); - const value = (e.currentTarget).value; - if(dataIndex && cssClass && value){ - const monthIndex = parseInt(dataIndex); - const months = (this.object).months; - if(!isNaN(monthIndex) && monthIndex < months.length){ - if(cssClass === 'month-name'){ - months[monthIndex].name = value; - } else if(cssClass === 'month-days'){ - const days = parseInt(value); - if(!isNaN(days) && days !== months[monthIndex].days.length){ - months[monthIndex].numberOfDays = days; + this.updateApp(); + } else if (cssClass) { + Logger.debug(`CSS Class "${cssClass}" change found`); + if(dataIndex){ + const index = parseInt(dataIndex); + Logger.debug(`Indexed item (${index}) changed.`); + if(!isNaN(index)){ + //Season Setting Inputs + if(cssClass === 'season-name' && (this.object).seasons.length > index){ + (this.object).seasons[index].name = value; + } else if(cssClass === 'season-custom' && (this.object).seasons.length > index){ + if(value[0] !== "#"){ + value = '#'+value; + } + (this.object).seasons[index].customColor = value; + } else if(cssClass === 'season-month' && (this.object).seasons.length > index){ + const month = parseInt(value); + if(!isNaN(month)){ + (this.object).seasons[index].startingMonth = month; + } + } else if(cssClass === 'season-day' && (this.object).seasons.length > index){ + const day = parseInt(value); + if(!isNaN(day)){ + (this.object).seasons[index].startingDay = day; + } + } else if(cssClass === 'season-color' && (this.object).seasons.length > index){ + (this.object).seasons[index].color = value; } - } else if (cssClass === 'month-intercalary' ){ - months[monthIndex].intercalary = (e.currentTarget).checked; - const a = (this.element).find(`.month-intercalary-include[data-index='${dataIndex}']`).parent().parent().parent(); - if(months[monthIndex].intercalary){ - a.removeClass('hidden'); - } else { - a.addClass('hidden'); + //Month Setting Inputs + else if(cssClass === 'month-name' && (this.object).months.length > index){ + (this.object).months[index].name = value; + } else if(cssClass === 'month-days' && (this.object).months.length > index){ + let days = parseInt(value); + if(!isNaN(days) && days !== (this.object).months[index].days.length){ + (this.object).months[index].numberOfDays = days; + this.updateMonthDays((this.object).months[index]); + } + } else if (cssClass === 'month-intercalary' && (this.object).months.length > index){ + (this.object).months[index].intercalary = checked; + const a = (this.element).find(`.month-intercalary-include[data-index='${dataIndex}']`).parent().parent().parent(); + if((this.object).months[index].intercalary){ + a.removeClass('hidden'); + } else { + a.addClass('hidden'); + } + this.rebaseMonthNumbers(); + } else if (cssClass === 'month-intercalary-include' && (this.object).months.length > index){ + (this.object).months[index].intercalaryInclude = checked; + this.rebaseMonthNumbers(); + } + //Weekday Setting Inputs + else if(cssClass === 'weekday-name' && (this.object).weekdays.length > index){ + (this.object).weekdays[index].name = value; + } + //Leap Year Setting Inputs + else if(cssClass === 'month-leap-days' && (this.object).months.length > index){ + const days = parseInt(value); + if(!isNaN(days) && days !== (this.object).months[index].numberOfLeapYearDays){ + (this.object).months[index].numberOfLeapYearDays = days; + this.updateMonthDays((this.object).months[index]); + } + } + //Moon Setting Inputs + else if(cssClass === 'moon-name' && (this.object).moons.length > index){ + (this.object).moons[index].name = value; + } else if(cssClass === 'moon-cycle-length' && (this.object).moons.length > index){ + const cycle = parseFloat(value); + if(!isNaN(cycle)){ + (this.object).moons[index].cycleLength = cycle; + (this.object).moons[index].updatePhaseLength(); + } + } else if(cssClass === 'moon-cycle-adjustment' && (this.object).moons.length > index){ + const cycle = parseFloat(value); + if(!isNaN(cycle)){ + (this.object).moons[index].cycleDayAdjust = cycle; + } + } else if(cssClass === 'moon-year-reset' && (this.object).moons.length > index){ + (this.object).moons[index].firstNewMoon.yearReset = value; + } else if(cssClass === 'moon-year-x' && (this.object).moons.length > index){ + const year = parseInt(value); + if(!isNaN(year)){ + (this.object).moons[index].firstNewMoon.yearX = year; + } + }else if(cssClass === 'moon-year' && (this.object).moons.length > index){ + const year = parseInt(value); + if(!isNaN(year)){ + (this.object).moons[index].firstNewMoon.year = year; + } + } else if(cssClass === 'moon-month' && (this.object).moons.length > index){ + const month = parseInt(value); + if(!isNaN(month)){ + (this.object).moons[index].firstNewMoon.month = month; + } + } else if(cssClass === 'moon-day' && (this.object).moons.length > index){ + const day = parseInt(value); + if(!isNaN(day)){ + (this.object).moons[index].firstNewMoon.day = day; + } + } else if(cssClass === 'moon-color' && (this.object).moons.length > index){ + if(value[0] !== "#"){ + value = '#'+value; + } + (this.object).moons[index].color = value; + } else if(cssClass === 'moon-phase-name' || cssClass === 'moon-phase-single-day' || cssClass === 'moon-phase-icon'){ + const dataMoonIndex = (e.currentTarget).getAttribute('data-moon-index'); + if(dataMoonIndex){ + const moonIndex = parseInt(dataMoonIndex); + if(!isNaN(moonIndex) && (this.object).moons.length > moonIndex && (this.object).moons[moonIndex].phases.length > index){ + if(cssClass === 'moon-phase-name'){ + (this.object).moons[moonIndex].phases[index].name = value; + } else if(cssClass === 'moon-phase-single-day'){ + (this.object).moons[moonIndex].phases[index].singleDay = checked; + (this.object).moons[moonIndex].updatePhaseLength(); + } else if(cssClass === 'moon-phase-icon'){ + (this.object).moons[moonIndex].phases[index].icon = value; + } + + } + } } - this.rebaseMonthNumbers(); - this.updateApp(); - } else if (cssClass === 'month-intercalary-include' ){ - months[monthIndex].intercalaryInclude = (e.currentTarget).checked; - this.rebaseMonthNumbers(); - this.updateApp(); - } else { - Logger.debug(`Invalid CSS Class for input "${cssClass}"`); } - return; } + this.updateApp(); } - Logger.debug('Unable to set the months data on change.'); - } - - /** - * Event when the checkbox for showing the weekday headings is changed - * @param {Event} e The event that triggered the change - */ - public showWeekdayInputChange(e: Event) { - e.preventDefault(); - (this.object).showWeekdayHeadings = (e.currentTarget).checked; } /** - * Event when a text box for weekday name is changed to temporarily store those changes so that if the application is updated the correct values are displayed - * @param {Event} e The change event + * Updates the month object to ensure the number of day objects it has matches any change values + * @param {Month} month The month to check */ - public weekdayInputChange(e: Event){ - e.preventDefault(); - const dataIndex = (e.currentTarget).getAttribute('data-index'); - const value = (e.currentTarget).value; - if(dataIndex && value){ - const weekdayIndex = parseInt(dataIndex); - const weekdays = (this.object).weekdays; - if(!isNaN(weekdayIndex) && weekdayIndex < weekdays.length){ - weekdays[weekdayIndex].name = value; - return; + public updateMonthDays(month: Month){ + //Check to see if the number of days is less than 0 and set it to 0 + if(month.numberOfDays < 0){ + month.numberOfDays = 0; + } + //Check if the leap year days was set to less than 0 and set it to equal the number of days + if(month.numberOfLeapYearDays < 0){ + month.numberOfLeapYearDays = month.numberOfDays; + } + //The number of day objects to create + const daysShouldBe = month.numberOfLeapYearDays > month.numberOfDays? month.numberOfLeapYearDays : month.numberOfDays; + const monthCurrentDay = month.getDay(); + let currentDay = null; + if(monthCurrentDay){ + if(monthCurrentDay.numericRepresentation >= month.numberOfDays){ + Logger.debug('The current day falls outside of the months new days, setting to first day of the month.'); + currentDay = 0; + } else { + currentDay = monthCurrentDay.numericRepresentation; } } - Logger.debug('Unable to set the weekday data on change.'); + month.days = []; + month.populateDays(daysShouldBe, currentDay); } /** - * Event when the leap year rule dropdown has changed to temporarily store those changes so if the application is updated the correct values are displayed - * @param {Event} e The event that triggered the change + * Shows a confirmation dialog to the user to confirm that they want to do the action they chose + * @param {string} type They type of action the user is attempting to preform - Used to call the correct functions + * @param {string} type2 The sub type of the above type the user is attempting to preform + * @param {Event} e The click event */ - public leapYearRuleChange(e: Event){ + public overwriteConfirmationDialog(type: string, type2: string, e: Event){ e.preventDefault(); - const leapYearRule = (e.currentTarget).value; - (this.object).leapYearRule.rule = leapYearRule; - this.updateApp(); + const dialog = new Dialog({ + title: GameSettings.Localize('FSC.OverwriteConfirm'), + content: GameSettings.Localize("FSC.OverwriteConfirmText"), + buttons:{ + yes: { + icon: '', + label: GameSettings.Localize('FSC.Apply'), + callback: SimpleCalendarConfiguration.instance.overwriteConfirmationYes.bind(this, type, type2) + }, + no: { + icon: '', + label: GameSettings.Localize('FSC.Cancel') + } + }, + default: "no" + }); + dialog.render(true); } /** - * Event when a text box for the leap year month day count is change to temporarily store those changes so that if the application is updated the correct values are displayed - * @param {Event} e The event that triggered the change + * Based on the passed in types calls the correct functionality + * @param {string} type The type of action being preformed + * @param {string} type2 The sub type of the above type */ - public leapYearMonthChange(e: Event){ - e.preventDefault(); - const dataIndex = (e.currentTarget).getAttribute('data-index'); - const cssClass = (e.currentTarget).getAttribute('class'); - const value = (e.currentTarget).value; - if(dataIndex && cssClass && value){ - const monthIndex = parseInt(dataIndex); - const months = (this.object).months; - if(!isNaN(monthIndex) && monthIndex < months.length){ - const days = parseInt(value); - if(!isNaN(days) && days !== months[monthIndex].numberOfLeapYearDays){ - months[monthIndex].numberOfLeapYearDays = days; - } - return; + public async overwriteConfirmationYes(type: string, type2: string){ + if(type === 'predefined'){ + this.predefinedApplyConfirm(); + } else if(type === 'tp-import'){ + if(type2 === 'about-time'){ + await Importer.importAboutTime(this.object); + this.updateApp(); + } else if(type2 === 'calendar-weather'){ + await Importer.importCalendarWeather(this.object); + this.updateApp(); + } + await GameSettings.SetImportRan(true); + } else if(type === 'tp-export'){ + if(type2 === 'about-time'){ + await Importer.exportToAboutTime(this.object); + } else if(type2 === 'calendar-weather'){ + await Importer.exportCalendarWeather(this.object); } + await GameSettings.SetImportRan(true); } - Logger.debug('Unable to set the months data on change.'); } /** @@ -689,6 +1191,10 @@ export class SimpleCalendarConfiguration extends FormApplication { public async saveClick(e: Event) { e.preventDefault(); try{ + // Update the general Settings + (this.object).generalSettings.gameWorldTimeIntegration = (document.getElementById("scGameWorldTime")).value; + await GameSettings.SaveGeneralSettings((this.object).generalSettings); + // Update the Year Configuration const currentYear = parseInt((document.getElementById("scCurrentYear")).value); if(!isNaN(currentYear)){ @@ -696,88 +1202,19 @@ export class SimpleCalendarConfiguration extends FormApplication { (this.object).selectedYear = currentYear; (this.object).visibleYear = currentYear; } - (this.object).prefix = (document.getElementById("scYearPreFix")).value; - (this.object).postfix = (document.getElementById("scYearPostFix")).value; - (this.object).showWeekdayHeadings = (document.getElementById("scShowWeekdayHeaders")).checked; await GameSettings.SaveYearConfiguration(this.object); // Update the Month Configuration - const monthNames = (this.element).find('input.month-name'); - const monthDays= (this.element).find('input.month-days'); - const monthIntercalary= (this.element).find('input.month-intercalary'); - const monthIntercalaryInclude= (this.element).find('input.month-intercalary-include'); - const monthLeapDays= (this.element).find('input.month-leap-days'); - for(let i = 0; i < monthNames.length; i++){ - const monthIndex = monthNames[i].getAttribute('data-index'); - if(monthIndex){ - const index = parseInt(monthIndex); - const month = (this.object).months[index]; - month.name = (monthNames[i]).value; - month.intercalary = (monthIntercalary[i]).checked; - month.intercalaryInclude = (monthIntercalaryInclude[i]).checked; - if(i < monthLeapDays.length){ - let days = parseInt((monthLeapDays[i]).value); - if(isNaN(days) || days < 0){ - days = 0; - } - if(month.numberOfLeapYearDays !== days){ - month.numberOfLeapYearDays = days; - } - } else { - month.numberOfLeapYearDays = 0; - } - let days = parseInt((monthDays[i]).value); - if(isNaN(days) || days < 0){ - days = 0; - } - if(month.numberOfDays !== days){ - month.numberOfDays = days; - if(month.numberOfLeapYearDays < 1){ - month.numberOfLeapYearDays = month.numberOfDays; - } - } - const daysShouldBe = month.numberOfLeapYearDays > month.numberOfDays? month.numberOfLeapYearDays : month.numberOfDays; - if(month.days.length !== daysShouldBe){ - Logger.debug(`Days for month ${month.name} are different, rebuilding month days`); - const monthCurrentDay = month.getDay(); - let currentDay = null; - if(monthCurrentDay){ - if(monthCurrentDay.numericRepresentation >= days){ - Logger.debug('The current day falls outside of the months new days, setting to first day of the month.'); - currentDay = 0; - } else { - currentDay = monthCurrentDay.numericRepresentation; - } - } - month.days = []; - month.populateDays(daysShouldBe, currentDay); - } - } - } await GameSettings.SaveMonthConfiguration((this.object).months); //Update Weekday Configuration - const weekdayNames = (this.element).find('.weekday-name'); - for(let i = 0; i < weekdayNames.length; i++){ - const weekdayIndex = weekdayNames[i].getAttribute('data-index'); - if(weekdayIndex) { - const index = parseInt(weekdayIndex); - const weekday = (this.object).weekdays[index]; - weekday.name = (weekdayNames[i]).value; - } - } await GameSettings.SaveWeekdayConfiguration((this.object).weekdays); - - const leapYearRule = (this.element).find('#scLeapYearRule').find(":selected").val(); - if(leapYearRule){ - (this.object).leapYearRule.rule = leapYearRule.toString(); - } - if((this.object).leapYearRule.rule === LeapYearRules.Custom){ - const leapYearCustomMod = parseInt((document.getElementById("scLeapYearCustomMod")).value); - if(!isNaN(leapYearCustomMod)){ - (this.object).leapYearRule.customMod = leapYearCustomMod; - } - } - + //Update Leap Year Configuration await GameSettings.SaveLeapYearRules((this.object).leapYearRule); + //Update Time Configuration + await GameSettings.SaveTimeConfiguration((this.object).time); + //Update Season Configuration + await GameSettings.SaveSeasonConfiguration((this.object).seasons); + //Update Moon Settings + await GameSettings.SaveMoonConfiguration((this.object).moons); if(this.yearChanged){ await GameSettings.SaveCurrentDate(this.object); diff --git a/src/classes/simple-calendar-notes.test.ts b/src/classes/simple-calendar-notes.test.ts index ffb7cabf..daef09f8 100644 --- a/src/classes/simple-calendar-notes.test.ts +++ b/src/classes/simple-calendar-notes.test.ts @@ -30,7 +30,7 @@ describe('Simple Calendar Notes Tests', () => { note.monthDisplay = ''; note.title = ''; note.content = ''; - note.author = ''; + note.author = '1'; note.playerVisible = false; SimpleCalendarNotes.instance = new SimpleCalendarNotes(note); @@ -38,6 +38,7 @@ describe('Simple Calendar Notes Tests', () => { //Spy on console.error calls jest.spyOn(console, 'error').mockImplementation(); //Spy on the inherited render function of the new instance + //@ts-ignore renderSpy = jest.spyOn(SimpleCalendarNotes.instance, 'render'); (console.error).mockClear(); renderSpy.mockClear(); @@ -116,6 +117,21 @@ describe('Simple Calendar Notes Tests', () => { expect(data.noteYear).toBe(1); //@ts-ignore expect(data.noteMonth).toBe('Name'); + + //@ts-ignore + (game.users.get).mockReturnValueOnce({name:"asd"}); + data = SimpleCalendarNotes.instance.getData(); + //@ts-ignore + expect(data.authorName).toBe('asd'); + + const tusers = game.users; + game.users = undefined; + data = SimpleCalendarNotes.instance.getData(); + //@ts-ignore + expect(data.authorName).toBe('1'); + + game.users = tusers; + }); test('Set Up Text Editor', () => { @@ -286,6 +302,8 @@ describe('Simple Calendar Notes Tests', () => { }); test('Delete Confirm', () => { + //@ts-ignore + game.user.isGM = true; SimpleCalendarNotes.instance.deleteConfirm(); expect(game.settings.get).toHaveBeenCalledTimes(1); expect(game.settings.set).not.toHaveBeenCalled(); diff --git a/src/classes/simple-calendar-notes.ts b/src/classes/simple-calendar-notes.ts index e146a3dc..a9ac590a 100644 --- a/src/classes/simple-calendar-notes.ts +++ b/src/classes/simple-calendar-notes.ts @@ -1,8 +1,9 @@ import {Note} from './note'; import {Logger} from "./logging"; import {GameSettings} from "./game-settings"; -import {NoteRepeat} from "../constants"; +import {ModuleName, ModuleSocketName, NoteRepeat, SettingNames, SocketTypes} from "../constants"; import SimpleCalendar from "./simple-calendar"; +import {SimpleCalendarSocket} from "../interfaces"; export class SimpleCalendarNotes extends FormApplication { /** @@ -43,7 +44,6 @@ export class SimpleCalendarNotes extends FormApplication { button: null, hasButton: false, active: false, - //@ts-ignore mce: null, options: { //@ts-ignore @@ -53,6 +53,9 @@ export class SimpleCalendarNotes extends FormApplication { save_onsavecallback: this.saveEditor.bind(this, 'content') }, }; + if(!GameSettings.IsGm()){ + (this.object).playerVisible = true; + } } /** @@ -75,14 +78,16 @@ export class SimpleCalendarNotes extends FormApplication { getData(options?: Application.RenderOptions): Promise> | FormApplication.Data<{}> { let data = { ... super.getData(options), + isGM: GameSettings.IsGm(), viewMode: this.viewMode, richButton: !this.viewMode, - canEdit: GameSettings.IsGm(), + canEdit: GameSettings.IsGm() || GameSettings.UserID() === (this.object).author, noteYear: 0, noteMonth: '', repeatOptions: {0: 'FSC.Notes.Repeat.Never', 1: 'FSC.Notes.Repeat.Weekly', 2: 'FSC.Notes.Repeat.Monthly', 3: 'FSC.Notes.Repeat.Yearly'}, repeats: (this.object).repeats, - repeatsText: '' + repeatsText: '', + authorName: (this.object).author }; if(SimpleCalendar.instance.currentYear && ((this.object).repeats === NoteRepeat.Yearly || (this.object).repeats === NoteRepeat.Monthly)){ data.noteYear = SimpleCalendar.instance.currentYear.visibleYear; @@ -95,7 +100,15 @@ export class SimpleCalendarNotes extends FormApplication { } else { data.noteMonth = (this.object).monthDisplay; } - data.repeatsText = `${GameSettings.Localize("FSC.Notes.Repeats")} ${GameSettings.Localize(data.repeatOptions[data.repeats])}` + data.repeatsText = `${GameSettings.Localize("FSC.Notes.Repeats")} ${GameSettings.Localize(data.repeatOptions[data.repeats])}`; + + if(game.users){ + Logger.debug(`Looking for users with the id "${(this.object).author}"`); + const user = game.users.get((this.object).author); + if(user){ + data.authorName = user.name; + } + } return data; } @@ -112,15 +125,12 @@ export class SimpleCalendarNotes extends FormApplication { if(!this.viewMode && this.editors['content'].options.target && this.editors['content'].button === null){ this.editors['content'].button = this.editors['content'].options.target.nextElementSibling; this.editors['content'].hasButton = this.editors['content'].button && this.editors['content'].button.classList.contains("editor-edit"); - //@ts-ignore this.editors['content'].active = !this.viewMode; if(this.editors['content'].hasButton){ this.editors['content'].button.onclick = SimpleCalendarNotes.instance.textEditorButtonClick.bind(this) } } - //@ts-ignore if(this.editors['content'].mce === null && this.editors['content'].active){ - //@ts-ignore this.activateEditor('content'); } } @@ -138,10 +148,8 @@ export class SimpleCalendarNotes extends FormApplication { options = mergeObject(editor.options, options); options.height = options.target.offsetHeight; TextEditor.create(options, initialContent || editor.initial).then(mce => { - //@ts-ignore editor.mce = mce; editor.changed = false; - //@ts-ignore editor.active = true; mce.on('change', ev => editor.changed = true); }); @@ -270,7 +278,6 @@ export class SimpleCalendarNotes extends FormApplication { e.preventDefault(); let detailsEmpty = true; if(this.editors['content'] && this.editors['content'].mce){ - //@ts-ignore if(this.editors['content'].mce.getContent().trim() !== '' && !this.editors['content'].mce.isNotDirty){ detailsEmpty = false; } diff --git a/src/classes/simple-calendar.test.ts b/src/classes/simple-calendar.test.ts index 576da9b9..b4f49b91 100644 --- a/src/classes/simple-calendar.test.ts +++ b/src/classes/simple-calendar.test.ts @@ -7,16 +7,21 @@ import "../../__mocks__/application"; import "../../__mocks__/handlebars"; import "../../__mocks__/event"; import "../../__mocks__/crypto"; +import "../../__mocks__/dialog"; import SimpleCalendar from "./simple-calendar"; import Year from "./year"; import Month from "./month"; import {Note} from "./note"; -import {LeapYearRules, SettingNames} from "../constants"; +import {GameWorldTimeIntegrations, LeapYearRules, SettingNames, SocketTypes} from "../constants"; +import {SimpleCalendarSocket} from "../interfaces"; +import {SimpleCalendarConfiguration} from "./simple-calendar-configuration"; import Mock = jest.Mock; import SpyInstance = jest.SpyInstance; +jest.mock('./importer'); + describe('Simple Calendar Class Tests', () => { let y: Year; let renderSpy: SpyInstance; @@ -34,6 +39,8 @@ describe('Simple Calendar Class Tests', () => { (console.error).mockClear(); renderSpy.mockClear(); (game.settings.get).mockClear(); + (game.settings.set).mockClear(); + (game.socket.emit).mockClear(); //@ts-ignore (ui.notifications.warn).mockClear(); @@ -52,7 +59,7 @@ describe('Simple Calendar Class Tests', () => { }); test('Properties', () => { - expect(Object.keys(SimpleCalendar.instance).length).toBe(4); //Make sure no new properties have been added + expect(Object.keys(SimpleCalendar.instance).length).toBe(7); //Make sure no new properties have been added }); test('Default Options', () => { @@ -66,12 +73,12 @@ describe('Simple Calendar Class Tests', () => { expect(spy).toHaveBeenCalled() }); - test('Init', () => { + test('Init', async () => { expect(SimpleCalendar.instance.currentYear).toBeNull(); - SimpleCalendar.instance.init(); - expect(Handlebars.registerHelper).toHaveBeenCalledTimes(3); - expect(game.settings.register).toHaveBeenCalledTimes(7); - expect(game.settings.get).toHaveBeenCalledTimes(7); + await SimpleCalendar.instance.init(); + expect(Handlebars.registerHelper).toHaveBeenCalledTimes(4); + expect(game.settings.register).toHaveBeenCalledTimes(12); + expect(game.settings.get).toHaveBeenCalledTimes(11); expect(SimpleCalendar.instance.currentYear?.numericRepresentation).toBe(0); expect(SimpleCalendar.instance.currentYear?.months.length).toBe(1); expect(SimpleCalendar.instance.currentYear?.months[0].days.length).toBe(2); @@ -81,30 +88,92 @@ describe('Simple Calendar Class Tests', () => { //Testing the functions within the handlebar helpers // @ts-ignore game.user.isGM = true; - SimpleCalendar.instance.init(); - expect(Handlebars.registerHelper).toHaveBeenCalledTimes(6); + await SimpleCalendar.instance.init(); + expect(Handlebars.registerHelper).toHaveBeenCalledTimes(8); + // @ts-ignore + expect(SimpleCalendar.instance.primaryCheckTimeout).toBeDefined(); + expect(game.socket.emit).toHaveBeenCalledTimes(1); // @ts-ignore game.user.isGM = false; }); + test('Primary Check Timeout Call', () => { + SimpleCalendar.instance.primaryCheckTimeoutCall(); + expect(SimpleCalendar.instance.primary).toBe(true); + expect(game.socket.emit).toHaveBeenCalledTimes(1); + }) + + test('Process Socket', async () => { + const d: SimpleCalendarSocket.Data = { + type: SocketTypes.time, + data: { + clockClass: '' + } + }; + await SimpleCalendar.instance.processSocket(d); + expect(renderSpy).toHaveBeenCalledTimes(1); + + d.type = SocketTypes.journal; + d.data = {notes: []}; + await SimpleCalendar.instance.processSocket(d); + expect(renderSpy).toHaveBeenCalledTimes(1); + + // @ts-ignore + game.user.isGM = true; + SimpleCalendar.instance.primary = true; + await SimpleCalendar.instance.processSocket(d); + expect(renderSpy).toHaveBeenCalledTimes(1); + + // @ts-ignore + game.user.isGM = false; + + d.type = SocketTypes.primary; + await SimpleCalendar.instance.processSocket(d); + expect(renderSpy).toHaveBeenCalledTimes(1); + + //@ts-ignore + d.type = 'asd'; + await SimpleCalendar.instance.processSocket(d); + expect(renderSpy).toHaveBeenCalledTimes(1); + }); + test('Get Data', async () => { let data = await SimpleCalendar.instance.getData(); expect(data).toStrictEqual({ + "isGM": false, + "isPrimary": false, "currentYear": { + "clockClass": "stopped", + "currentTime": { + "hour": "00", + "minute": "00", + "second": "00" + }, "display": "0", "numericRepresentation": 0, "selectedDisplayDay": "", "selectedDisplayMonth": "", "selectedDisplayYear": "0", + "showClock": false, + "showDateControls": true, + "showTimeControls": false, "showWeekdayHeaders": true, "visibleMonth": undefined, "visibleMonthWeekOffset": [], - "weekdays": [] + "weekdays": [], + "currentSeasonColor": "", + "currentSeasonName": "" }, - "isGM": false, - "notes": [], "showCurrentDay": false, - "showSelectedDay": false + "showSelectedDay": false, + "notes": [], + "addNotes": false, + "clockClass": 'stopped', + "timeUnits": { + second: true, + minute: false, + hour: false + } }); SimpleCalendar.instance.currentYear = y; //Nothing Undefined @@ -263,8 +332,8 @@ describe('Simple Calendar Class Tests', () => { fakeQuery.length = 1; //@ts-ignore SimpleCalendar.instance.activateListeners(fakeQuery); - expect(fakeQuery.find).toHaveBeenCalledTimes(8); - expect(onFunc).toHaveBeenCalledTimes(7); + expect(fakeQuery.find).toHaveBeenCalledTimes(11); + expect(onFunc).toHaveBeenCalledTimes(10); }); test('View Previous Month', () => { @@ -368,10 +437,25 @@ describe('Simple Calendar Class Tests', () => { expect(SimpleCalendar.instance.currentYear.months[0].days[0].selected).toBe(true); }); + test('Time Unit Click', () => { + let e = new Event('click'); + (e.currentTarget).setAttribute('data-type', 'second'); + SimpleCalendar.instance.timeUnitClick(e); + expect(renderSpy).toHaveBeenCalledTimes(0); + SimpleCalendar.instance.currentYear = y; + SimpleCalendar.instance.timeUnitClick(e); + expect(renderSpy).toHaveBeenCalledTimes(1); + + (e.currentTarget).removeAttribute('data-type'); + SimpleCalendar.instance.timeUnitClick(e); + expect(renderSpy).toHaveBeenCalledTimes(2); + }); + test('GM Control Click', () => { const event = new Event('click'); SimpleCalendar.instance.gmControlClick(event); expect(renderSpy).not.toHaveBeenCalled(); + SimpleCalendar.instance.currentYear = y; //Test Each with current year set to null (event.currentTarget).setAttribute('data-type', 'day'); SimpleCalendar.instance.gmControlClick(event); @@ -407,6 +491,30 @@ describe('Simple Calendar Class Tests', () => { (event.currentTarget).classList.add('next'); SimpleCalendar.instance.gmControlClick(event); expect(renderSpy).toHaveBeenCalledTimes(8); + + (event.currentTarget).setAttribute('data-type', 'time'); + SimpleCalendar.instance.gmControlClick(event); + expect(renderSpy).toHaveBeenCalledTimes(8); + + (event.currentTarget).setAttribute('data-amount', 'asd'); + SimpleCalendar.instance.gmControlClick(event); + expect(renderSpy).toHaveBeenCalledTimes(8); + + (event.currentTarget).setAttribute('data-amount', '1'); + SimpleCalendar.instance.gmControlClick(event); + expect(renderSpy).toHaveBeenCalledTimes(9); + + (event.currentTarget).classList.remove('next'); + SimpleCalendar.instance.timeUnits.second = false; + SimpleCalendar.instance.timeUnits.minute = true; + SimpleCalendar.instance.gmControlClick(event); + expect(renderSpy).toHaveBeenCalledTimes(10); + + SimpleCalendar.instance.timeUnits.second = false; + SimpleCalendar.instance.timeUnits.minute = false; + SimpleCalendar.instance.timeUnits.hour = true; + SimpleCalendar.instance.gmControlClick(event); + expect(renderSpy).toHaveBeenCalledTimes(11); }); test('Date Control Apply', () => { @@ -429,6 +537,7 @@ describe('Simple Calendar Class Tests', () => { SimpleCalendar.instance.dateControlApply(event); //@ts-ignore expect(ui.notifications.warn).toHaveBeenCalledTimes(1); + (game.settings.set).mockReset(); }); test('Configuration Click', () => { @@ -439,11 +548,19 @@ describe('Simple Calendar Class Tests', () => { expect(console.error).toHaveBeenCalledTimes(1); SimpleCalendar.instance.currentYear = y; SimpleCalendar.instance.configurationClick(event); + SimpleCalendarConfiguration.instance = new SimpleCalendarConfiguration(y); + // @ts-ignore + SimpleCalendarConfiguration.instance.rendered = true; + SimpleCalendar.instance.configurationClick(event); // @ts-ignore game.user.isGM = false; SimpleCalendar.instance.configurationClick(event); //@ts-ignore expect(ui.notifications.warn).toHaveBeenCalledTimes(1); + + + + }); test('Add Note', () => { @@ -456,23 +573,34 @@ describe('Simple Calendar Class Tests', () => { SimpleCalendar.instance.currentYear.months[0].days[0].current = false; SimpleCalendar.instance.currentYear.months[0].days[0].selected = false; - //No Current or selected month + //No GM Present SimpleCalendar.instance.addNote(event); //@ts-ignore expect(ui.notifications.warn).toHaveBeenCalledTimes(1); + //GM is present + //@ts-ignore + (game.users.find) = jest.fn((v)=>{ + return v.call(undefined, {isGM: true, active: true}); + }); + + //No Current or selected month + SimpleCalendar.instance.addNote(event); + //@ts-ignore + expect(ui.notifications.warn).toHaveBeenCalledTimes(2); + //Current Month but no selected or current day SimpleCalendar.instance.currentYear.months[0].current = true; SimpleCalendar.instance.addNote(event); //@ts-ignore - expect(ui.notifications.warn).toHaveBeenCalledTimes(2); + expect(ui.notifications.warn).toHaveBeenCalledTimes(3); //Current and Selected Month, current day but no selected day SimpleCalendar.instance.currentYear.months[0].selected = true; SimpleCalendar.instance.currentYear.months[0].days[0].current = true; SimpleCalendar.instance.addNote(event); //@ts-ignore - expect(ui.notifications.warn).toHaveBeenCalledTimes(2); + expect(ui.notifications.warn).toHaveBeenCalledTimes(3); }); test('View Note', () => { @@ -535,6 +663,16 @@ describe('Simple Calendar Class Tests', () => { }); + test('Load General Settings', () => { + //@ts-ignore + SimpleCalendar.instance.loadGeneralSettings(); + expect(console.error).toHaveBeenCalledTimes(1); + + SimpleCalendar.instance.currentYear = y; + //@ts-ignore + SimpleCalendar.instance.loadGeneralSettings(); + }); + test('Load Year Configuration', () => { SimpleCalendar.instance.settingUpdate(); expect(SimpleCalendar.instance.currentYear?.numericRepresentation).toBe(0); @@ -697,7 +835,7 @@ describe('Simple Calendar Class Tests', () => { SimpleCalendar.instance.currentYear = null; SimpleCalendar.instance.settingUpdate(); expect(SimpleCalendar.instance.currentYear).toBeNull(); - expect(console.error).toHaveBeenCalledTimes(5); + expect(console.error).toHaveBeenCalledTimes(8); //@ts-ignore (SimpleCalendar.instance.loadYearConfiguration).mockReset() @@ -734,4 +872,136 @@ describe('Simple Calendar Class Tests', () => { } }); + + test('World Time Update', () => { + SimpleCalendar.instance.worldTimeUpdate(100, 10); + SimpleCalendar.instance.currentYear = y; + SimpleCalendar.instance.worldTimeUpdate(100, 10); + expect(y.time.seconds).toBe(0); + }); + + test('Combat Update', () => { + //@ts-ignore + SimpleCalendar.instance.combatUpdate({started: true}, {}, {advanceTime: 2}); + SimpleCalendar.instance.currentYear = y; + //@ts-ignore + SimpleCalendar.instance.combatUpdate({started: true}, {}, {}); + expect(y.time.combatRunning).toBe(true); + //@ts-ignore + SimpleCalendar.instance.combatUpdate({started: true}, {}, {advanceTime: 2}); + expect(y.combatChangeTriggered).toBe(true); + }); + + test('Combat Delete', () => { + SimpleCalendar.instance.combatDelete(); + SimpleCalendar.instance.currentYear = y; + y.time.combatRunning = true; + SimpleCalendar.instance.combatDelete(); + expect(y.time.combatRunning).toBe(false); + }); + + test('Game Paused', () => { + SimpleCalendar.instance.gamePaused(false); + SimpleCalendar.instance.currentYear = y; + SimpleCalendar.instance.gamePaused(false); + }); + + test('Start Time', () => { + SimpleCalendar.instance.startTime(); + SimpleCalendar.instance.currentYear = y; + SimpleCalendar.instance.startTime(); + expect(y.time.keeper).toBeUndefined(); + y.generalSettings.gameWorldTimeIntegration = GameWorldTimeIntegrations.Self; + SimpleCalendar.instance.startTime(); + expect(y.time.keeper).toBeDefined(); + //@ts-ignore + game.combats.size = 1; + SimpleCalendar.instance.startTime(); + }); + + test('Stop Time', () => { + y.time.keeper = 1; + SimpleCalendar.instance.stopTime(); + expect(y.time.keeper).toBe(1); + SimpleCalendar.instance.currentYear = y; + SimpleCalendar.instance.stopTime(); + expect(y.time.keeper).toBeUndefined(); + }); + + test('Time Keeping Check', async () => { + (game.settings.get).mockClear(); + await SimpleCalendar.instance.timeKeepingCheck(); + expect(game.settings.get).not.toHaveBeenCalled(); + + SimpleCalendar.instance.currentYear = y; + await SimpleCalendar.instance.timeKeepingCheck(); + expect(game.settings.get).not.toHaveBeenCalled(); + + //@ts-ignore + game.user.isGM = true; + (game.settings.get).mockReturnValueOnce(true); + y.generalSettings.gameWorldTimeIntegration = GameWorldTimeIntegrations.Self; + await SimpleCalendar.instance.timeKeepingCheck(); + expect(game.settings.get).toHaveBeenCalledTimes(1); + + (game.modules.get) + .mockReturnValueOnce(null).mockReturnValueOnce(null) + .mockReturnValueOnce(null).mockReturnValueOnce({active:true}) + .mockReturnValueOnce({active:true}).mockReturnValueOnce(null); + await SimpleCalendar.instance.timeKeepingCheck(); + expect(game.settings.get).toHaveBeenCalledTimes(2); + + await SimpleCalendar.instance.timeKeepingCheck(); + expect(game.settings.get).toHaveBeenCalledTimes(3); + //@ts-ignore + expect(DialogRenderer).toHaveBeenCalledTimes(1); + await SimpleCalendar.instance.timeKeepingCheck(); + expect(game.settings.get).toHaveBeenCalledTimes(4); + //@ts-ignore + expect(DialogRenderer).toHaveBeenCalledTimes(2); + }); + + test('Module Import Click', async () => { + //@ts-ignore + game.user.isGM = true; + await SimpleCalendar.instance.moduleImportClick('asd'); + expect(console.error).toHaveBeenCalledTimes(1); + SimpleCalendar.instance.currentYear = y; + + await SimpleCalendar.instance.moduleImportClick('asd'); + expect(game.settings.set).toHaveBeenCalledTimes(1); + await SimpleCalendar.instance.moduleImportClick('about-time'); + expect(game.settings.set).toHaveBeenCalledTimes(2); + expect(renderSpy).toHaveBeenCalledTimes(1); + await SimpleCalendar.instance.moduleImportClick('calendar-weather'); + expect(game.settings.set).toHaveBeenCalledTimes(3); + expect(renderSpy).toHaveBeenCalledTimes(2); + }); + + test('Module Export Click', async () => { + //@ts-ignore + game.user.isGM = true; + await SimpleCalendar.instance.moduleExportClick('asd'); + expect(console.error).toHaveBeenCalledTimes(1); + SimpleCalendar.instance.currentYear = y; + + await SimpleCalendar.instance.moduleExportClick('asd'); + expect(game.settings.set).toHaveBeenCalledTimes(1); + await SimpleCalendar.instance.moduleExportClick('about-time'); + expect(game.settings.set).toHaveBeenCalledTimes(2); + await SimpleCalendar.instance.moduleExportClick('calendar-weather'); + expect(game.settings.set).toHaveBeenCalledTimes(3); + }); + + test('Module Dialog No Change', async () => { + (game.settings.set).mockClear(); + //@ts-ignore + game.user.isGM = false; + await SimpleCalendar.instance.moduleDialogNoChangeClick(); + expect(game.settings.set).not.toHaveBeenCalled(); + //@ts-ignore + game.user.isGM = true; + await SimpleCalendar.instance.moduleDialogNoChangeClick(); + expect(game.settings.set).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/classes/simple-calendar.ts b/src/classes/simple-calendar.ts index 692157bb..27dd29df 100644 --- a/src/classes/simple-calendar.ts +++ b/src/classes/simple-calendar.ts @@ -2,12 +2,16 @@ import {Logger} from "./logging"; import Year from "./year"; import Month from "./month"; import {Note} from "./note"; -import {CalendarTemplate, NoteTemplate} from "../interfaces"; +import {CalendarTemplate, NoteTemplate, SimpleCalendarSocket} from "../interfaces"; import {SimpleCalendarConfiguration} from "./simple-calendar-configuration"; import {GameSettings} from "./game-settings"; import {Weekday} from "./weekday"; import {SimpleCalendarNotes} from "./simple-calendar-notes"; import HandlebarsHelpers from "./handlebars-helpers"; +import {GameWorldTimeIntegrations, ModuleSocketName, SocketTypes} from "../constants"; +import Importer from "./importer"; +import Season from "./season"; +import Moon from "./moon"; /** @@ -26,7 +30,32 @@ export default class SimpleCalendar extends Application{ */ public currentYear: Year | null = null; - public notes: Note[] = [] + /** + * List of all notes in the calendar + * @type {Array.} + */ + public notes: Note[] = []; + + /** + * The CSS class associated with the animated clock + */ + clockClass = 'stopped'; + + /** + * The different time units that a user can choose from and which one is currently selected + */ + timeUnits = { + second: true, + minute: false, + hour: false + }; + + /** + * If this GM is considered the primary GM, if so all requests from players are filtered through this account. + */ + public primary: boolean = false; + + private primaryCheckTimeout: number | undefined; /** * Simple Calendar constructor @@ -48,10 +77,53 @@ export default class SimpleCalendar extends Application{ /** * Initializes the dialogs once foundry is ready to go */ - public init(){ + public async init(){ HandlebarsHelpers.Register(); GameSettings.RegisterSettings(); this.settingUpdate(); + await this.timeKeepingCheck(); + + //Set up the socket we use to forward data between players and the GM + game.socket.on(ModuleSocketName, this.processSocket.bind(this)); + if(this.currentYear){ + this.currentYear.time.updateUsers(); + } + + if(GameSettings.IsGm()){ + this.primaryCheckTimeout = window.setTimeout(this.primaryCheckTimeoutCall.bind(this), 5000); + } + } + + /** + * Called after the timeout delay set to see if another GM account has been set as the primary + */ + primaryCheckTimeoutCall(){ + Logger.debug('No primary GM found, taking over as primary'); + this.primary = true; + const socketData = {type: SocketTypes.primary, data: {}}; + game.socket.emit(ModuleSocketName, socketData); + } + + /** + * Process any data received over our socket + * @param {SimpleCalendarSocket.Data} data The data received + */ + async processSocket(data: SimpleCalendarSocket.Data){ + Logger.debug(`Processing ${data.type} socket emit`); + if(data.type === SocketTypes.time){ + // This is processed by all players to update the animated clock + this.clockClass = (data.data).clockClass; + this.updateApp(); + } else if (data.type === SocketTypes.journal){ + // If user is a GM and the primary GM then save the journal requests, otherwise do nothing + if(GameSettings.IsGm() && this.primary){ + Logger.debug(`Saving notes from user.`); + await GameSettings.SaveNotes((data.data).notes) + } + } else if (data.type === SocketTypes.primary){ + Logger.debug('A primary GM is all ready present.'); + window.clearTimeout(this.primaryCheckTimeout); + } } /** @@ -61,22 +133,30 @@ export default class SimpleCalendar extends Application{ if(this.currentYear){ return { isGM: GameSettings.IsGm(), + isPrimary: this.primary, + addNotes: GameSettings.IsGm() || this.currentYear.generalSettings.playersAddNotes, currentYear: this.currentYear.toTemplate(), showSelectedDay: this.currentYear.visibleYear === this.currentYear.selectedYear, showCurrentDay: this.currentYear.visibleYear === this.currentYear.numericRepresentation, - notes: this.getNotesForDay() + notes: this.getNotesForDay(), + clockClass: this.clockClass, + timeUnits: this.timeUnits }; } else { return { isGM: false, + isPrimary: this.primary, + addNotes: false, currentYear: new Year(0).toTemplate(), showCurrentDay: false, showSelectedDay: false, - notes: [] + notes: [], + clockClass: this.clockClass, + timeUnits: this.timeUnits }; } } - + /** * Adds the calendar button to the token button list * @param controls @@ -187,6 +267,7 @@ export default class SimpleCalendar extends Application{ (html).find(".calendar-controls .today").on('click', SimpleCalendar.instance.todayClick.bind(this)); // When the GM Date controls are clicked + (html).find(".time-controls .time-unit .selector").on('click', SimpleCalendar.instance.timeUnitClick.bind(this)); (html).find(".controls .control").on('click', SimpleCalendar.instance.gmControlClick.bind(this)); (html).find(".controls .btn-apply").on('click', SimpleCalendar.instance.dateControlApply.bind(this)); @@ -198,6 +279,9 @@ export default class SimpleCalendar extends Application{ // Note Click (html).find(".date-notes .note").on('click', SimpleCalendar.instance.viewNote.bind(this)); + + (html).find(".time-start").on('click', SimpleCalendar.instance.startTime.bind(this)); + (html).find(".time-stop").on('click', SimpleCalendar.instance.stopTime.bind(this)); } } @@ -296,34 +380,63 @@ export default class SimpleCalendar extends Application{ } } + /** + * Click event when a user is changing the time unit to adjust + * @param {Event} e The click event + */ + public timeUnitClick(e: Event){ + e.stopPropagation(); + if(this.currentYear){ + const target = e.currentTarget; + const dataType = target.getAttribute('data-type')?.toLowerCase() as 'second' | 'minute' | 'hour'; + this.timeUnits.second = false; + this.timeUnits.minute = false; + this.timeUnits.hour = false; + this.timeUnits[dataType] = true; + this.updateApp(); + } + } + /** * Click event when a gm user clicks on any of the next/back buttons for day/month/year * @param {Event} e The click event */ public gmControlClick(e: Event){ e.stopPropagation(); - const target = e.currentTarget; - const dataType = target.getAttribute('data-type'); - const isNext = target.classList.contains('next'); - - switch (dataType){ - case 'day': - Logger.debug(`${isNext? 'Forward' : 'Back'} Day Clicked`); - this.currentYear?.changeDay(isNext, 'current'); - this.updateApp(); - break; - case 'month': - Logger.debug(`${isNext? 'Forward' : 'Back'} Month Clicked`); - this.currentYear?.changeMonth(isNext? 1 : -1, 'current'); - this.updateApp(); - break; - case 'year': - Logger.debug(`${isNext? 'Forward' : 'Back'} Year Clicked`); - this.currentYear?.changeYear(isNext? 1 : -1, false, "current"); - this.updateApp(); - break; + if(this.currentYear){ + const target = e.currentTarget; + const dataType = target.getAttribute('data-type'); + const isNext = target.classList.contains('next'); + switch (dataType){ + case 'time': + const dataAmount = target.getAttribute('data-amount'); + if(dataAmount){ + const amount = parseInt(dataAmount); + if(!isNaN(amount)){ + Logger.debug(`${isNext? 'Forward' : 'Back'} Time Clicked`); + const unit = this.timeUnits.second? 'second' : this.timeUnits.minute? 'minute' : 'hour'; + this.currentYear.changeTime(isNext, unit, amount); + this.updateApp(); + } + } + break; + case 'day': + Logger.debug(`${isNext? 'Forward' : 'Back'} Day Clicked`); + this.currentYear.changeDay(isNext, 'current'); + this.updateApp(); + break; + case 'month': + Logger.debug(`${isNext? 'Forward' : 'Back'} Month Clicked`); + this.currentYear.changeMonth(isNext? 1 : -1, 'current'); + this.updateApp(); + break; + case 'year': + Logger.debug(`${isNext? 'Forward' : 'Back'} Year Clicked`); + this.currentYear.changeYear(isNext? 1 : -1, false, "current"); + this.updateApp(); + break; + } } - } /** @@ -336,6 +449,8 @@ export default class SimpleCalendar extends Application{ if(GameSettings.IsGm()){ if(this.currentYear) { GameSettings.SaveCurrentDate(this.currentYear).catch(Logger.error); + //Sync the current time on apply, this will propagate to other modules + this.currentYear.syncTime().catch(Logger.error); } } else { GameSettings.UiNotification(GameSettings.Localize("FSC.Error.Calendar.GMCurrent"), 'warn'); @@ -350,8 +465,12 @@ export default class SimpleCalendar extends Application{ e.stopPropagation(); if(GameSettings.IsGm()){ if(this.currentYear){ - SimpleCalendarConfiguration.instance = new SimpleCalendarConfiguration(this.currentYear.clone()); - SimpleCalendarConfiguration.instance.showApp(); + if(!SimpleCalendarConfiguration.instance || (SimpleCalendarConfiguration.instance && !SimpleCalendarConfiguration.instance.rendered)){ + SimpleCalendarConfiguration.instance = new SimpleCalendarConfiguration(this.currentYear.clone()); + SimpleCalendarConfiguration.instance.showApp(); + } else { + SimpleCalendarConfiguration.instance.bringToTop(); + } } else { Logger.error('The Current year is not configured.'); } @@ -367,28 +486,32 @@ export default class SimpleCalendar extends Application{ public addNote(e: Event) { e.stopPropagation(); if(this.currentYear){ - const currentMonth = this.currentYear.getMonth('selected') || this.currentYear.getMonth(); - if(currentMonth){ - const currentDay = currentMonth.getDay('selected') || currentMonth.getDay(); - if(currentDay){ - const year = this.currentYear.selectedYear || this.currentYear.numericRepresentation; - const month = currentMonth.numericRepresentation; - const day = currentDay.numericRepresentation; - const newNote = new Note(); - newNote.year = year; - newNote.month = month; - newNote.day = day; - newNote.monthDisplay = currentMonth.name; - newNote.title = ''; - newNote.author = GameSettings.UserName(); - newNote.playerVisible = GameSettings.GetDefaultNoteVisibility(); - SimpleCalendarNotes.instance = new SimpleCalendarNotes(newNote); - SimpleCalendarNotes.instance.showApp(); + if(game.users && !game.users.find(u => u.isGM && u.active)){ + GameSettings.UiNotification(game.i18n.localize('FSC.Warn.Notes.NotGM'), 'warn'); + } else { + const currentMonth = this.currentYear.getMonth('selected') || this.currentYear.getMonth(); + if(currentMonth){ + const currentDay = currentMonth.getDay('selected') || currentMonth.getDay(); + if(currentDay){ + const year = this.currentYear.selectedYear || this.currentYear.numericRepresentation; + const month = currentMonth.numericRepresentation; + const day = currentDay.numericRepresentation; + const newNote = new Note(); + newNote.year = year; + newNote.month = month; + newNote.day = day; + newNote.monthDisplay = currentMonth.name; + newNote.title = ''; + newNote.author = GameSettings.UserID(); + newNote.playerVisible = GameSettings.GetDefaultNoteVisibility(); + SimpleCalendarNotes.instance = new SimpleCalendarNotes(newNote); + SimpleCalendarNotes.instance.showApp(); + } else { + GameSettings.UiNotification(GameSettings.Localize("FSC.Error.Note.NoSelectedDay"), 'warn'); + } } else { - GameSettings.UiNotification(GameSettings.Localize("FSC.Error.Note.NoSelectedDay"), 'warn'); + GameSettings.UiNotification(GameSettings.Localize("FSC.Error.Note.NoSelectedMonth"), 'warn'); } - } else { - GameSettings.UiNotification(GameSettings.Localize("FSC.Error.Note.NoSelectedMonth"), 'warn'); } } else { Logger.error('The Current year is not configured.'); @@ -417,7 +540,7 @@ export default class SimpleCalendar extends Application{ * Re renders the application window * @private */ - private updateApp(){ + public updateApp(){ if(this.rendered){ this.render(false, {width: 500, height: 500}); } @@ -444,12 +567,40 @@ export default class SimpleCalendar extends Application{ if(type === 'leapyear'){ this.currentYear?.leapYearRule.loadFromSettings(); } + if(type === 'all' || type === 'time'){ + this.loadTimeConfiguration(); + } + if(type === 'all' || type === 'season'){ + this.loadSeasonConfiguration(); + } + if(type === 'all' || type === 'moon'){ + this.loadMoonConfiguration(); + } + if(type === 'all' || type === 'general'){ + this.loadGeneralSettings(); + } this.loadCurrentDate(); if(update) { this.updateApp(); } } + /** + * Loads the general settings from the world settings and apply them + * @private + */ + private loadGeneralSettings(){ + Logger.debug('Loading general settings from world settings'); + const gSettings = GameSettings.LoadGeneralSettings(); + if(gSettings && Object.keys(gSettings).length){ + if(this.currentYear){ + this.currentYear.generalSettings = gSettings; + } else { + Logger.error('No Current year configured, can not load general settings.'); + } + } + } + /** * Loads the year configuration data from the settings and applies them to the current year */ @@ -547,6 +698,79 @@ export default class SimpleCalendar extends Application{ } } + /** + * Loads the season configuration data from the settings and applies them to the current year + * @private + */ + private loadSeasonConfiguration(){ + Logger.debug('Loading season configuration from settings.'); + if(this.currentYear){ + const seasonData = GameSettings.LoadSeasonData(); + this.currentYear.seasons = []; + if(seasonData.length){ + Logger.debug('Setting the seasons from data.'); + for(let i = 0; i < seasonData.length; i++){ + const newSeason = new Season(seasonData[i].name, seasonData[i].startingMonth, seasonData[i].startingDay); + newSeason.color = seasonData[i].color; + newSeason.customColor = seasonData[i].customColor; + this.currentYear.seasons.push(newSeason); + } + } + } else { + Logger.error('No Current year configured, can not load season data.'); + } + } + + /** + * Loads the moon configuration data from the settings and applies them to the current year + * @private + */ + private loadMoonConfiguration(){ + Logger.debug('Loading moon configuration from settings.'); + if(this.currentYear){ + const moonData = GameSettings.LoadMoonData(); + this.currentYear.moons = []; + if(moonData.length){ + Logger.debug('Setting the moons from data.'); + for(let i = 0; i < moonData.length; i++){ + const newMoon = new Moon(moonData[i].name, moonData[i].cycleLength); + newMoon.phases = moonData[i].phases; + newMoon.firstNewMoon = { + yearReset: moonData[i].firstNewMoon.yearReset, + yearX: moonData[i].firstNewMoon.yearX, + year: moonData[i].firstNewMoon.year, + month: moonData[i].firstNewMoon.month, + day: moonData[i].firstNewMoon.day + }; + newMoon.color = moonData[i].color; + newMoon.cycleDayAdjust = moonData[i].cycleDayAdjust; + this.currentYear.moons.push(newMoon); + } + } + } else { + Logger.error('No Current year configured, can not load moon data.'); + } + } + + /** + * Loads the time configuration from the settings and applies them to the current year + * @private + */ + private loadTimeConfiguration(){ + Logger.debug('Loading time configuration from settings.'); + if(this.currentYear){ + const timeData = GameSettings.LoadTimeData(); + if(timeData && Object.keys(timeData).length){ + this.currentYear.time.hoursInDay = timeData.hoursInDay; + this.currentYear.time.minutesInHour = timeData.minutesInHour; + this.currentYear.time.secondsInMinute = timeData.secondsInMinute; + this.currentYear.time.gameTimeRatio = timeData.gameTimeRatio; + } + } else { + Logger.error('No Current year configured, can not load time data.'); + } + } + /** * Loads the current date data from the settings and applies them to the current year */ @@ -554,7 +778,6 @@ export default class SimpleCalendar extends Application{ Logger.debug('Loading current date from settings.'); const currentDate = GameSettings.LoadCurrentDate(); if(this.currentYear && currentDate && Object.keys(currentDate).length){ - Logger.debug('Loading current date data from settings.'); this.currentYear.numericRepresentation = currentDate.year; this.currentYear.visibleYear = currentDate.year; this.currentYear.selectedYear = currentDate.year; @@ -579,6 +802,10 @@ export default class SimpleCalendar extends Application{ this.currentYear.months[0].visible = true; this.currentYear.months[0].days[0].current = true; } + this.currentYear.time.seconds = currentDate.seconds; + if(this.currentYear.time.seconds === undefined){ + this.currentYear.time.seconds = 0; + } } else if(this.currentYear && this.currentYear.months.length) { Logger.debug('No current date setting found, setting default current date.'); this.currentYear.months[0].current = true; @@ -630,4 +857,184 @@ export default class SimpleCalendar extends Application{ return dayNotes; } + /** + * Triggered when anything updates the game world time + * @param {number} newTime The total time in seconds + * @param {number} delta How much the newTime has changed from the old time in seconds + */ + worldTimeUpdate(newTime: number, delta: number){ + Logger.debug(`World Time Update, new time: ${newTime}. Delta of: ${delta}.`); + if(this.currentYear){ + this.currentYear.setFromTime(newTime, delta); + } + } + + /** + * Triggered when a combat is create/started/turn advanced + * @param {Combat} combat The specific combat data + * @param {Combat.CurrentTurn} round The current turns data + * @param {Object} time The amount of time that has advanced + */ + combatUpdate(combat: Combat, round: Combat.CurrentTurn, time: any){ + Logger.debug('Combat Update'); + if(this.currentYear && combat.started){ + this.currentYear.time.combatRunning = true; + this.currentYear.time.updateUsers(); + if(time && time.hasOwnProperty('advanceTime')){ + Logger.debug('Combat Change Triggered'); + this.currentYear.combatChangeTriggered = true; + } + } + } + + /** + * Triggered when a combat is finished and removed + */ + combatDelete(){ + Logger.debug('Combat Ended'); + if(this.currentYear){ + this.currentYear.time.combatRunning = false; + this.currentYear.time.updateUsers(); + } + } + + /** + * Triggered when the game is paused/un-paused + * @param {boolean} paused If the game is now paused or not + */ + gamePaused(paused: boolean){ + if(this.currentYear){ + this.currentYear.time.updateUsers(); + } + } + + /** + * Starts the built in time keeper + */ + startTime(){ + if(this.currentYear){ + if(game.combats && game.combats.size > 0 && game.combats.find(g => g.started)){ + GameSettings.UiNotification(game.i18n.localize('FSC.Warn.Time.ActiveCombats'), 'warn'); + } else if(this.currentYear.generalSettings.gameWorldTimeIntegration === GameWorldTimeIntegrations.Self || this.currentYear.generalSettings.gameWorldTimeIntegration === GameWorldTimeIntegrations.Mixed){ + this.currentYear.time.startTimeKeeper(); + } + } + } + + /** + * Stops the built in time keeper + */ + stopTime(){ + if(this.currentYear){ + this.currentYear.time.stopTimeKeeper(); + } + } + + /** + * Checks to see if the module import/export dialog needs to be shown and syncs the game world time with the simple calendar + */ + async timeKeepingCheck(){ + //If the current year is set up and the calendar is set up for time keeping and the user is the GM + if(this.currentYear && this.currentYear.generalSettings.gameWorldTimeIntegration !== GameWorldTimeIntegrations.None && GameSettings.IsGm() ){ + const importRun = GameSettings.GetImportRan(); + // If we haven't asked about the import in the past + if(!importRun){ + const calendarWeather = game.modules.get('calendar-weather'); + const aboutTime = game.modules.get('about-time'); + //Ask about calendar/weather first, then about time + if(calendarWeather && calendarWeather.active){ + Logger.debug('Calendar/Weather detected.'); + const cwD = new Dialog({ + title: GameSettings.Localize('FSC.Module.CalendarWeather.Title'), + content: GameSettings.Localize('FSC.Module.CalendarWeather.Message'), + buttons:{ + import: { + label: GameSettings.Localize('FSC.Module.Import'), + callback: this.moduleImportClick.bind(this, 'calendar-weather') + }, + export: { + label: GameSettings.Localize('FSC.Module.CalendarWeather.Export'), + callback: this.moduleExportClick.bind(this,'calendar-weather') + }, + no: { + label: GameSettings.Localize('FSC.Module.NoChanges'), + callback: this.moduleDialogNoChangeClick.bind(this) + } + }, + default: "no" + }); + cwD.render(true); + } else if(aboutTime && aboutTime.active){ + Logger.debug(`About Time detected.`); + const cwD = new Dialog({ + title: GameSettings.Localize('FSC.Module.AboutTime.Title'), + content: GameSettings.Localize('FSC.Module.AboutTime.Message'), + buttons:{ + import: { + label: GameSettings.Localize('FSC.Module.Import'), + callback: this.moduleImportClick.bind(this, 'about-time') + }, + export: { + label: GameSettings.Localize('FSC.Module.AboutTime.Export'), + callback: this.moduleExportClick.bind(this,'about-time') + }, + no: { + label: GameSettings.Localize('FSC.Module.NoChanges'), + callback: this.moduleDialogNoChangeClick.bind(this) + } + }, + default: "no" + }); + cwD.render(true); + } + } + + //Sync the current world time with the simple calendar + await this.currentYear.syncTime(); + } + } + + /** + * Called when the import option is selection from the importing/exporting module dialog + * @param {string} type The module + */ + async moduleImportClick(type: string) { + if(this.currentYear){ + if(type === 'about-time'){ + await Importer.importAboutTime(this.currentYear); + this.updateApp(); + } else if(type === 'calendar-weather'){ + await Importer.importCalendarWeather(this.currentYear); + this.updateApp(); + } + await GameSettings.SetImportRan(true); + } else { + Logger.error('Could not export as the current year is not defined'); + } + } + + /** + * Called when the export option is selection from the importing/exporting module dialog + * @param {string} type The module + */ + async moduleExportClick(type: string){ + if(this.currentYear){ + if(type === 'about-time'){ + await Importer.exportToAboutTime(this.currentYear); + } else if(type === 'calendar-weather'){ + await Importer.exportCalendarWeather(this.currentYear); + } + await GameSettings.SetImportRan(true); + } else { + Logger.error('Could not export as the current year is not defined'); + } + } + + /** + * Called when the no change dialog option is clicked for importing/exporting module data + */ + async moduleDialogNoChangeClick(){ + await GameSettings.SetImportRan(true); + } + } diff --git a/src/classes/time.test.ts b/src/classes/time.test.ts new file mode 100644 index 00000000..7b1261bf --- /dev/null +++ b/src/classes/time.test.ts @@ -0,0 +1,144 @@ +/** + * @jest-environment jsdom + */ +import "../../__mocks__/game"; +import "../../__mocks__/form-application"; +import "../../__mocks__/application"; +import "../../__mocks__/handlebars"; +import "../../__mocks__/event"; +import Time from "./time"; +import SimpleCalendar from "./simple-calendar"; +import Year from "./year"; + +describe('Time Tests', () => { + let t: Time; + + beforeEach(()=>{ + t = new Time(); + }); + + test('Properties', () => { + expect(Object.keys(t).length).toBe(7); //Make sure no new properties have been added + expect(t.hoursInDay).toBe(24); + expect(t.minutesInHour).toBe(60); + expect(t.secondsInMinute).toBe(60); + expect(t.gameTimeRatio).toBe(1); + expect(t.seconds).toBe(0); + expect(t.secondsPerDay).toBe(86400); + expect(t.keeper).toBeUndefined(); + expect(t.combatRunning).toBe(false); + }); + + test('Clone', () => { + const temp = t.clone(); + expect(temp).toStrictEqual(t); + }); + + test('Get Current Time', () => { + expect(t.getCurrentTime()).toStrictEqual({"hour": "00", "minute": "00", "second": "00"}); + t.seconds = t.secondsPerDay - 1; + expect(t.getCurrentTime()).toStrictEqual({"hour": "23", "minute": "59", "second": "59"}); + }); + + test('Set Time', () => { + t.setTime(23,59,59); + expect(t.seconds).toBe(t.secondsPerDay-1); + t.setTime(); + expect(t.seconds).toBe(0); + }); + + test('Change Time', () => { + let r = t.changeTime(0,0,1); + expect(t.seconds).toBe(1); + expect(r).toBe(0); + + r = t.changeTime(-1); + expect(t.seconds).toBe(82801); + expect(r).toBe(-1); + + r = t.changeTime(1); + expect(t.seconds).toBe(1); + expect(r).toBe(1); + + r = t.changeTime(); + expect(t.seconds).toBe(1); + expect(r).toBe(0); + }); + + test('Get Total Seconds', () => { + expect(t.getTotalSeconds(1, false)).toBe(86400); + expect(t.getTotalSeconds(10, false)).toBe(864000); + t.seconds = 10; + expect(t.getTotalSeconds(1)).toBe(86410); + }); + + test('Get Clock Class', () => { + expect(t.getClockClass()).toBe('stopped'); + t.keeper = 2; + expect(t.getClockClass()).toBe('paused'); + //@ts-ignore + game.paused = false; + expect(t.getClockClass()).toBe('go'); + t.combatRunning = true; + expect(t.getClockClass()).toBe('paused'); + }); + + test('Set World Time', () => { + t.setWorldTime(100); + expect(game.time.advance).toHaveBeenCalledTimes(1); + }); + + test('Start Time Keeper', () => { + SimpleCalendar.instance = new SimpleCalendar(); + window.setInterval = jest.fn().mockReturnValue(2); + t.startTimeKeeper(); + expect(window.setInterval).toHaveBeenCalledTimes(1); + t.startTimeKeeper(); + expect(window.setInterval).toHaveBeenCalledTimes(1); + }); + + test('Stop Time Keeper', () => { + window.clearInterval = jest.fn(); + t.stopTimeKeeper(); + expect(window.clearInterval).not.toHaveBeenCalled(); + t.keeper = 2; + t.stopTimeKeeper(); + expect(window.clearInterval).toHaveBeenCalledTimes(1); + t.stopTimeKeeper(); + expect(window.clearInterval).toHaveBeenCalledTimes(1); + }); + + test('Time Keeper', () => { + SimpleCalendar.instance = new SimpleCalendar(); + //@ts-ignore + game.paused = true; + t.combatRunning = false; + t.timeKeeper(); + //@ts-ignore + game.paused = false; + t.combatRunning = true; + t.timeKeeper(); + + t.combatRunning = false; + t.timeKeeper(); + + t.seconds = 86400; + t.timeKeeper(); + expect(t.seconds).toBe(30); + + SimpleCalendar.instance.currentYear = new Year(1); + t.timeKeeper(); + t.seconds = 86400; + t.timeKeeper(); + expect(t.seconds).toBe(30); + }); + + test('Update Users', () => { + t.updateUsers(); + expect(game.socket.emit).not.toHaveBeenCalled(); + //@ts-ignore + game.user.isGM = true; + t.updateUsers(); + expect(game.socket.emit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/classes/time.ts b/src/classes/time.ts new file mode 100644 index 00000000..501d2412 --- /dev/null +++ b/src/classes/time.ts @@ -0,0 +1,224 @@ +import {Logger} from "./logging"; +import {SimpleCalendarSocket, TimeTemplate} from "../interfaces"; +import SimpleCalendar from "./simple-calendar"; +import {ModuleSocketName, SocketTypes} from "../constants"; +import {GameSettings} from "./game-settings"; + +/** + * Class representing the time of day + */ +export default class Time { + /** + * How many hours are in a day + * @type {number} + */ + hoursInDay: number; + /** + * How many minutes are in an hour + * @type {number} + */ + minutesInHour: number; + /** + * How many seconds are in a minute + * @type {number} + */ + secondsInMinute: number; + /** + * The ratio at which to advance game time while real time passes, ratio of 1 is the same, ratio of 2 is twice as fast + * @type {number} + */ + gameTimeRatio: number; + /** + * The number of seconds that have passed for the current day + * @type {number} + */ + seconds: number = 0; + /** + * The number of seconds in a day + * @type {number} + */ + secondsPerDay: number; + /** + * The build in time keeper interval + */ + keeper: number | undefined; + /** + * If a combat is currently running or not + */ + combatRunning: boolean = false; + + /** + * A new Time constructor + * @param {number} [hoursInDay=24] How many hours in a day + * @param {number} [minutesInHour=60] How many minutes in an hour + * @param {number} [secondsInMinute=60] How many seconds in a minute + */ + constructor(hoursInDay: number = 24, minutesInHour: number = 60, secondsInMinute: number = 60) { + this.hoursInDay = hoursInDay; + this.minutesInHour = minutesInHour; + this.secondsInMinute = secondsInMinute; + this.gameTimeRatio = 1; + + this.secondsPerDay = this.hoursInDay * this.minutesInHour * this.secondsInMinute; + } + + /** + * Makes a clone of this class with all the same settings + */ + clone() { + const t = new Time(this.hoursInDay, this.minutesInHour, this.secondsInMinute); + t.seconds = this.seconds; + t.gameTimeRatio = this.gameTimeRatio; + t.combatRunning = this.combatRunning; + return t; + } + + /** + * Returns the current time as string parts + * @return {TimeTemplate} + */ + getCurrentTime(): TimeTemplate{ + let s = this.seconds, m = 0, h = 0; + if(s >= this.secondsInMinute){ + m = Math.floor(s / this.secondsInMinute); + s = s - (m * this.secondsInMinute); + } + if(m >= this.minutesInHour){ + h = Math.floor(m / this.minutesInHour); + m = m - (h * this.minutesInHour); + } + return { + hour: h < 10? `0${h}` : h.toString(), + minute: m < 10? `0${m}` : m.toString(), + second: s < 10? `0${s}` : s.toString() + }; + } + + /** + * Sets the current number of seconds based on the passed in hours, minutes and seconds + * @param {number} [hour=0] The hour of the day + * @param {number} [minute=0] The minute of the day + * @param {number} [second=0] The seconds of the day + */ + setTime(hour: number = 0, minute: number = 0, second: number = 0){ + this.seconds = (hour * this.minutesInHour * this.secondsInMinute) + (minute * this.secondsInMinute) + second; + } + + /** + * Changes the current time by the passed in number of hours, minutes and seconds + * @param {number} [hour=0] The number of hours to change the time by + * @param {number} [minute=0] The number of minutes to change the time by + * @param {number} [second=0] The number of seconds to change the time by + * @return {number} The number of days that have changed as a result of the time change + */ + changeTime(hour: number = 0, minute: number = 0, second: number = 0): number{ + const changeAmount = (hour * this.minutesInHour * this.secondsInMinute) + (minute * this.secondsInMinute) + second; + const newAmount = this.seconds + changeAmount; + Logger.debug(`Checking if ${newAmount} seconds is valid`); + if(newAmount >= this.secondsPerDay) { + //If the new time is more seconds than there are in a day, change to the next day + Logger.debug(`More time than in a day, changing time again with new seconds ${newAmount - this.secondsPerDay}`); + this.seconds = 0; + const dayChange = this.changeTime(0,0, newAmount - this.secondsPerDay); + return dayChange + 1; + } else if( newAmount < 0){ + // Going back to a previous day + this.seconds = this.secondsPerDay; + const dayChange = this.changeTime(0,0, newAmount); + return dayChange + -1; + } + Logger.debug(`Updating seconds to ${newAmount}`); + this.seconds = newAmount; + return 0; + } + + /** + * Gets the total number of seconds based on the number of days + * @param {number} totalDays The number of days to turn into seconds + * @param {number} [includeToday=true] If to include todays time + */ + getTotalSeconds(totalDays: number, includeToday: boolean = true){ + return (totalDays * this.hoursInDay * this.minutesInHour * this.secondsInMinute) + (includeToday? this.seconds : 0); + } + + /** + * Gets the clock CSS class + * @return {string} The css class to apply to the animated clock + */ + getClockClass(): string{ + if(this.keeper !== undefined){ + if(!game.paused && !this.combatRunning){ + return 'go'; + } else { + return 'paused'; + } + } + return 'stopped'; + } + + /** + * Sets the world time to the passed in number of seconds + * @param {number} seconds The number of seconds to set the world time too + */ + async setWorldTime(seconds: number){ + const currentWorldTime = game.time.worldTime; + let diff = seconds - currentWorldTime; + const newTime = await game.time.advance(diff); + Logger.debug(`Set New Game World Time: ${newTime}`); + } + + /** + * Starts the build in time keeper + */ + startTimeKeeper(){ + if(this.keeper === undefined){ + Logger.debug('Starting the built in Time Keeper'); + this.keeper = window.setInterval(this.timeKeeper.bind(this), 30000); + this.timeKeeper(); + } + } + + /** + * Stops the built in time keeper + */ + stopTimeKeeper(){ + if(this.keeper !== undefined){ + Logger.debug('Stopping the built in Time Keeper'); + clearInterval(this.keeper); + this.keeper = undefined; + this.updateUsers(); + } + } + + /** + * Updates the current time based on the time keepers running time + */ + timeKeeper(){ + this.updateUsers(); + if(!game.paused && !this.combatRunning){ + Logger.debug('Updating Time...'); + const modifiedSeconds = 30 * this.gameTimeRatio; + const dayChange = this.changeTime(0,0,modifiedSeconds); + if(dayChange !== 0){ + SimpleCalendar.instance.currentYear?.changeDay(dayChange > 0); + } + SimpleCalendar.instance.currentYear?.syncTime(); + + } else { + Logger.debug('Game Paused or combat started, not updating time.'); + } + } + + /** + * Sends an update to all connected users over our socket + */ + updateUsers(){ + if(GameSettings.IsGm()){ + const socketData = {type: SocketTypes.time, data: {clockClass: this.getClockClass()}}; + Logger.debug(`Update Users Clock Class: ${(socketData.data).clockClass}`); + game.socket.emit(ModuleSocketName, socketData); + SimpleCalendar.instance.processSocket(socketData); + } + } + +} diff --git a/src/classes/year.test.ts b/src/classes/year.test.ts index a1bbfa38..790cf95b 100644 --- a/src/classes/year.test.ts +++ b/src/classes/year.test.ts @@ -11,8 +11,10 @@ import "../../__mocks__/dialog"; import Year from "./year"; import Month from "./month"; import {Weekday} from "./weekday"; -import {LeapYearRules} from "../constants"; +import {GameWorldTimeIntegrations, LeapYearRules} from "../constants"; import LeapYear from "./leap-year"; +import Season from "./season"; +import Moon from "./moon"; describe('Year Class Tests', () => { let year: Year; @@ -26,7 +28,7 @@ describe('Year Class Tests', () => { }); test('Properties', () => { - expect(Object.keys(year).length).toBe(9); //Make sure no new properties have been added + expect(Object.keys(year).length).toBe(15); //Make sure no new properties have been added expect(year.months).toStrictEqual([]); expect(year.weekdays).toStrictEqual([]); expect(year.prefix).toBe(""); @@ -37,12 +39,17 @@ describe('Year Class Tests', () => { expect(year.leapYearRule.customMod).toBe(0); expect(year.leapYearRule.rule).toBe(LeapYearRules.None); expect(year.showWeekdayHeadings).toBe(true); + expect(year.time).toBeDefined(); + expect(year.timeChangeTriggered).toBe(false); + expect(year.combatChangeTriggered).toBe(false); + expect(year.generalSettings).toStrictEqual({gameWorldTimeIntegration: GameWorldTimeIntegrations.None, showClock: false, playersAddNotes: false }); + expect(year.seasons).toStrictEqual([]); }); test('To Template', () => { year.weekdays.push(new Weekday(1, 'S')); let t = year.toTemplate(); - expect(Object.keys(t).length).toBe(9); //Make sure no new properties have been added + expect(Object.keys(t).length).toBe(16); //Make sure no new properties have been added expect(t.weekdays).toStrictEqual(year.weekdays.map(w=>w.toTemplate())); expect(t.display).toBe("0"); expect(t.numericRepresentation).toBe(0); @@ -52,6 +59,13 @@ describe('Year Class Tests', () => { expect(t.visibleMonth).toBeUndefined(); expect(t.visibleMonthWeekOffset).toStrictEqual([]); expect(t.showWeekdayHeaders).toBe(true); + expect(t.showClock).toBe(false); + expect(t.showDateControls).toBe(true); + expect(t.showTimeControls).toBe(false); + expect(t.clockClass).toBe("stopped"); + expect(t.currentTime).toStrictEqual({hour:"00", minute:"00", second: "00"}); + expect(t.currentSeasonColor).toBe(""); + expect(t.currentSeasonName).toBe(""); year.months.push(month); year.months[0].current = true; @@ -76,12 +90,20 @@ describe('Year Class Tests', () => { t = year.toTemplate(); expect(t.visibleMonth).toStrictEqual(year.months[0].toTemplate()); + year.generalSettings.showClock = true; + year.generalSettings.gameWorldTimeIntegration = GameWorldTimeIntegrations.ThirdParty; + t = year.toTemplate(); + expect(t.showDateControls).toBe(false); + expect(t.showTimeControls).toBe(false); + }); test('Clone', () => { expect(year.clone()).toStrictEqual(year); year2.months.push(month); year2.weekdays.push(new Weekday(1, 'S')); + year2.seasons.push(new Season('S', 1, 1)); + year2.moons.push(new Moon('M',1)) expect(year2.clone()).toStrictEqual(year2); }); @@ -356,6 +378,20 @@ describe('Year Class Tests', () => { expect(year.months[1].days[21].selected).toBe(false); }); + test('Change Time', () => { + year.changeTime(true, 'hour'); + expect(year.time.seconds).toBe(3600); + year.changeTime(true, 'minute', 2); + expect(year.time.seconds).toBe(3720); + year.changeTime(true, 'second', 3); + expect(year.time.seconds).toBe(3723); + year.changeTime(true, 'asd', 3); + expect(year.time.seconds).toBe(3723); + + year.changeTime(false, 'hour', 3); + expect(year.time.seconds).toBe(79323); + }); + test('Total Number of Days', () => { year.months.push(month); expect(year.totalNumberOfDays()).toBe(30); @@ -412,12 +448,159 @@ describe('Year Class Tests', () => { year.months.push(new Month("Test 3", 3, 2)); year.months.push(new Month("Test 2", 2, 22)); year.months[1].intercalary = true; - expect(year.dayOfTheWeek(year.numericRepresentation, 3, 2)).toBe(4); + expect(year.dayOfTheWeek(year.numericRepresentation, 3, 2)).toBe(3); year.leapYearRule = new LeapYear(); year.leapYearRule.rule = LeapYearRules.Gregorian; year.numericRepresentation = 4; - expect(year.dayOfTheWeek(year.numericRepresentation, 3, 2)).toBe(2); + expect(year.dayOfTheWeek(year.numericRepresentation, 3, 2)).toBe(1); + }); + + test('Date to Days', () => { + year.months.push(month); + year.months.push(new Month("Test 2", 2, 22, 23)); + expect(year.dateToDays(0,0,1)).toBe(1); + expect(year.dateToDays(5,0,1)).toBe(261); + year.leapYearRule = new LeapYear(); + year.leapYearRule.rule = LeapYearRules.Gregorian; + expect(year.dateToDays(5,0,1, true)).toBe(263); + year.months[0].intercalary = true; + expect(year.dateToDays(5,0,1, true)).toBe(113); + year.months[0].intercalaryInclude = true; + expect(year.dateToDays(5,2,1, true)).toBe(293); + }); + + test('Sync Time', () => { + //@ts-ignore + game.time.advance.mockClear(); + year.syncTime(); + expect(game.time.advance).not.toHaveBeenCalled(); + //@ts-ignore + game.user.isGM = true; + year.syncTime() + expect(game.time.advance).not.toHaveBeenCalled(); + year.generalSettings.gameWorldTimeIntegration = GameWorldTimeIntegrations.Self; + year.months.push(month); + year.syncTime() + expect(game.time.advance).not.toHaveBeenCalled(); + + month.current = true; + year.syncTime() + expect(game.time.advance).toHaveBeenCalledTimes(1); + month.days[0].current = true; + year.syncTime() + expect(game.time.advance).toHaveBeenCalledTimes(2); + }); + + test('Seconds To Date', () => { + year.months.push(month); + year.months.push(new Month("Test 2", 2, 22, 23)); + year.months[1].intercalary = true; + expect(year.secondsToDate(10)).toStrictEqual({year: 0, month: 0, day: 1, hour: 0, minute: 0, second: 10}); + expect(year.secondsToDate(70)).toStrictEqual({year: 0, month: 0, day: 1, hour: 0, minute: 1, second: 10}); + expect(year.secondsToDate(3670)).toStrictEqual({year: 0, month: 0, day: 1, hour: 1, minute: 1, second: 10}); + expect(year.secondsToDate(90070)).toStrictEqual({year: 0, month: 0, day: 2, hour: 1, minute: 1, second: 10}); + expect(year.secondsToDate(2682070)).toStrictEqual({year: 1, month: 0, day: 2, hour: 1, minute: 1, second: 10}); + year.months[1].intercalary = false; + year.leapYearRule = new LeapYear(); + year.leapYearRule.rule = LeapYearRules.Gregorian; + expect(year.secondsToDate(20908800)).toStrictEqual({year: 4, month: 1, day: 4, hour: 0, minute: 0, second: 0}); + }); + + test('Update Time', () => { + year.months.push(month); + year.months.push(new Month("Test 2", 2, 22, 23)); + year.updateTime({year: 1, month: 1, day: 3, hour: 4, minute: 5, second: 6}); + expect(year.numericRepresentation).toBe(1); + expect(year.months[1].current).toBe(true); + expect(year.months[1].days[2].current).toBe(true); + expect(year.time.seconds).toBe(14706); + }); + + test('Set From Time', () => { + year.months.push(month); + month.current = true; + //@ts-ignore + game.user.isGM = false; + year.time.seconds = 60; + year.timeChangeTriggered = true; + year.setFromTime(120, 0); + expect(year.time.seconds).toBe(60); + expect(year.timeChangeTriggered).toBe(false); + year.timeChangeTriggered = true; + year.setFromTime(120, 60); + expect(year.time.seconds).toBe(60); + expect(year.timeChangeTriggered).toBe(false); + + year.generalSettings.gameWorldTimeIntegration = GameWorldTimeIntegrations.Self; + year.timeChangeTriggered = false; + year.setFromTime(120, 60); + expect(year.time.seconds).toBe(60); + expect(year.timeChangeTriggered).toBe(false); + + year.timeChangeTriggered = true; + year.setFromTime(120, 60); + expect(year.time.seconds).toBe(60); + expect(year.timeChangeTriggered).toBe(false); + + year.generalSettings.gameWorldTimeIntegration = GameWorldTimeIntegrations.ThirdParty; + year.setFromTime(120, 60); + expect(year.time.seconds).toBe(120); + + year.time.seconds = 60; + //@ts-ignore + game.user.isGM = true; + year.setFromTime(120, 60); + expect(year.time.seconds).toBe(120); + expect(game.settings.set).toHaveBeenCalledTimes(1); + + year.time.seconds = 60; + year.generalSettings.gameWorldTimeIntegration = GameWorldTimeIntegrations.Self; + year.combatChangeTriggered = true; + year.setFromTime(120, 60); + expect(year.time.seconds).toBe(120); + expect(game.settings.set).toHaveBeenCalledTimes(2); + }); + + test('Get Current Season', () => { + let data = year.getCurrentSeason(); + expect(data.name).toBe(''); + expect(data.color).toBe(''); + + year.months.push(month); + year.months.push(new Month('Month 2', 2, 20)); + month.current = true; + data = year.getCurrentSeason(); + expect(data.name).toBe(''); + expect(data.color).toBe(''); + + month.days[0].current = true; + data = year.getCurrentSeason(); + expect(data.name).toBe(''); + expect(data.color).toBe(''); + + year.seasons.push(new Season('Spring', 1, 5)); + year.seasons.push(new Season('Winter', 2, 10)); + month.current = false; + month.days[0].current = false; + year.months[1].visible = true; + year.months[1].days[0].selected = true; + data = year.getCurrentSeason(); + expect(data.name).toBe('Spring'); + expect(data.color).toBe('#ffffff'); + + year.months[1].days[0].selected = false; + year.months[1].days[9].current = true; + year.seasons[1].color = 'custom'; + year.seasons[1].customColor = '#000000'; + data = year.getCurrentSeason(); + expect(data.name).toBe('Winter'); + expect(data.color).toBe('#000000'); + + year.months[1].days[9].current = false; + data = year.getCurrentSeason(); + expect(data.name).toBe('Spring'); + expect(data.color).toBe('#ffffff'); }); }); diff --git a/src/classes/year.ts b/src/classes/year.ts index ea3a67f2..a240cdd2 100644 --- a/src/classes/year.ts +++ b/src/classes/year.ts @@ -1,8 +1,13 @@ import Month from "./month"; -import {YearTemplate} from "../interfaces"; +import {GeneralSettings, YearTemplate} from "../interfaces"; import {Logger} from "./logging"; import {Weekday} from "./weekday"; import LeapYear from "./leap-year"; +import Time from "./time"; +import {GameWorldTimeIntegrations} from "../constants"; +import {GameSettings} from "./game-settings"; +import Season from "./season"; +import Moon from "./moon"; /** * Class for representing a year @@ -39,13 +44,49 @@ export default class Year { * @type {Array.} */ weekdays: Weekday[] = []; - + /** + * If to show the weekday headings row or not on the calendar + * @type {boolean} + */ showWeekdayHeadings: boolean = true; /** * The leap year rules for the calendar * @type {LeapYear} */ leapYearRule: LeapYear; + /** + * The time object responsible for all time related functionality + * @type {Time} + */ + time: Time; + /** + * If Simple Calendar has initiated a time change + * @type {boolean} + */ + timeChangeTriggered: boolean = false; + /** + * If a combat change has been triggered + * @type {boolean} + */ + combatChangeTriggered: boolean = false; + + /** + * The default general settings for the simple calendar + */ + generalSettings: GeneralSettings = { + gameWorldTimeIntegration: GameWorldTimeIntegrations.None, + showClock: false, + playersAddNotes: false + }; + /** + * All of the seasons for this calendar + * @type {Array.} + */ + seasons: Season[] = []; + /** + * All of the moons for this calendar + */ + moons: Moon[] =[]; /** * The Year constructor @@ -56,6 +97,7 @@ export default class Year { this.selectedYear = numericRepresentation; this.visibleYear = numericRepresentation; this.leapYearRule = new LeapYear(); + this.time = new Time(); } /** @@ -80,6 +122,7 @@ export default class Year { sDay = d.name; } } + const currentSeason = this.getCurrentSeason(); return { display: this.getDisplayName(), selectedDisplayYear: this.getDisplayName(true), @@ -89,7 +132,14 @@ export default class Year { weekdays: this.weekdays.map(w => w.toTemplate()), showWeekdayHeaders: this.showWeekdayHeadings, visibleMonth: this.getMonth('visible')?.toTemplate(this.leapYearRule.isLeapYear(this.visibleYear)), - visibleMonthWeekOffset: Array(this.visibleMonthStartingDayOfWeek()).fill(0) + visibleMonthWeekOffset: Array(this.visibleMonthStartingDayOfWeek()).fill(0), + showClock: this.generalSettings.showClock, + clockClass: this.time.getClockClass(), + showTimeControls: this.generalSettings.showClock && this.generalSettings.gameWorldTimeIntegration !== GameWorldTimeIntegrations.ThirdParty, + showDateControls: this.generalSettings.gameWorldTimeIntegration !== GameWorldTimeIntegrations.ThirdParty, + currentTime: this.time.getCurrentTime(), + currentSeasonName: currentSeason.name, + currentSeasonColor: currentSeason.color } } @@ -108,6 +158,12 @@ export default class Year { y.leapYearRule.rule = this.leapYearRule.rule; y.leapYearRule.customMod = this.leapYearRule.customMod; y.showWeekdayHeadings = this.showWeekdayHeadings; + y.time = this.time.clone(); + y.generalSettings.gameWorldTimeIntegration = this.generalSettings.gameWorldTimeIntegration; + y.generalSettings.showClock = this.generalSettings.showClock; + y.generalSettings.playersAddNotes = this.generalSettings.playersAddNotes; + y.seasons = this.seasons.map(s => s.clone()); + y.moons = this.moons.map(m => m.clone()); return y; } @@ -234,27 +290,50 @@ export default class Year { * @param {string} [setting='current'] The day property we are changing. Can be 'current' or 'selected' */ changeDay(next: boolean, setting: string = 'current'){ + const verifiedSetting = setting.toLowerCase() as 'current' | 'selected'; const currentMonth = this.getMonth(); - if(currentMonth){ - const verifiedSetting = setting.toLowerCase() as 'current' | 'selected'; - const yearToUse = verifiedSetting === 'current'? this.numericRepresentation : this.selectedYear; + if (currentMonth) { + const yearToUse = verifiedSetting === 'current' ? this.numericRepresentation : this.selectedYear; const isLeapYear = this.leapYearRule.isLeapYear(yearToUse); const res = currentMonth.changeDay(next, isLeapYear, verifiedSetting); // If it is positive or negative we need to change the current month - if(res !== 0){ + if (res !== 0) { this.changeMonth(res, verifiedSetting); } } } + changeTime(next: boolean, type: string, clickedAmount: number = 1){ + type = type.toLowerCase(); + const amount = next? clickedAmount : clickedAmount * -1; + let dayChange = 0; + this.timeChangeTriggered = true; + if(type === 'hour'){ + dayChange = this.time.changeTime(amount); + } else if(type === 'minute'){ + dayChange = this.time.changeTime(0, amount); + } else if(type === 'second'){ + dayChange = this.time.changeTime(0, 0, amount); + } + + if(dayChange !== 0){ + this.changeDay(dayChange > 0); + } + } + /** * Generates the total number of days in a year * @param {boolean} [leapYear=false] If to count the total number of days in a leap year + * @param {boolean} [ignoreIntercalaryRules=false] If to ignore the intercalary rules and include the months days (used to match closer to about-time) * @return {number} */ - totalNumberOfDays(leapYear: boolean = false): number { + totalNumberOfDays(leapYear: boolean = false, ignoreIntercalaryRules: boolean = false): number { let total = 0; - this.months.forEach((m) => { if((m.intercalary && m.intercalaryInclude) || !m.intercalary){total += leapYear? m.numberOfLeapYearDays : m.numberOfDays;} }); + this.months.forEach((m) => { + if((m.intercalary && m.intercalaryInclude) || !m.intercalary || ignoreIntercalaryRules){ + total += leapYear? m.numberOfLeapYearDays : m.numberOfDays; + } + }); return total; } @@ -284,40 +363,214 @@ export default class Year { */ dayOfTheWeek(year: number, targetMonth: number, targetDay: number): number{ if(this.weekdays.length){ - const daysPerYear = this.totalNumberOfDays(); - const daysPerLeapYear = this.totalNumberOfDays(true); - const leapYearDayDifference = daysPerLeapYear - daysPerYear; - const numberOfLeapYears = this.leapYearRule.howManyLeapYears(year); - Logger.debug(`Days Per Year: ${daysPerYear}`); - Logger.debug(`Days Per Leap Year: ${daysPerLeapYear}`); - Logger.debug(`How man days extra per leap year: ${leapYearDayDifference}`); - Logger.debug(`Number of leap years so far: ${numberOfLeapYears}`); + const daysSoFar = this.dateToDays(year, targetMonth, targetDay) - 1; + return (daysSoFar% this.weekdays.length + this.weekdays.length) % this.weekdays.length; + } else { + return 0; + } + } + + /** + * Converts the passed in date to the number of days that make up that date + * @param {number} year The year to convert + * @param {number} month The month to convert + * @param {number} day The day to convert + * @param {boolean} addLeapYearDiff If to add the leap year difference to the end result. Year 0 is not counted in the number of leap years so the total days will be off by that amount. + * @param {boolean} [ignoreIntercalaryRules=false] If to ignore the intercalary rules and include the months days (used to match closer to about-time) + */ + dateToDays(year: number, month: number, day: number, addLeapYearDiff: boolean = false, ignoreIntercalaryRules: boolean = false){ + const daysPerYear = this.totalNumberOfDays(false, ignoreIntercalaryRules); + const daysPerLeapYear = this.totalNumberOfDays(true, ignoreIntercalaryRules); + const leapYearDayDifference = daysPerLeapYear - daysPerYear; + const numberOfLeapYears = this.leapYearRule.howManyLeapYears(year); + const isLeapYear = this.leapYearRule.isLeapYear(year); + let daysSoFar = (daysPerYear * year) + (numberOfLeapYears * leapYearDayDifference); + const monthIndex = this.months.findIndex(m => m.numericRepresentation === month); + for(let i = 0; i < this.months.length; i++){ + //Only look at the month preceding the month we want and is not intercalary or is intercalary if the include setting is set otherwise skip + if(i < monthIndex && (ignoreIntercalaryRules || !this.months[i].intercalary || (this.months[i].intercalary && this.months[i].intercalaryInclude))){ + if(isLeapYear){ + daysSoFar = daysSoFar + this.months[i].numberOfLeapYearDays; + } else { + daysSoFar = daysSoFar + this.months[i].numberOfDays; + } + } + } + if(day < 1){ + day = 1; + } + daysSoFar += day; + if(addLeapYearDiff){ + daysSoFar += leapYearDayDifference; + } + return daysSoFar; + } + + /** + * Sets the current game world time to match what our current time is + */ + async syncTime(){ + // Only GMs can sync the time + // Only if the time tracking rules are set to self or mixed + if(GameSettings.IsGm() && (this.generalSettings.gameWorldTimeIntegration === GameWorldTimeIntegrations.Self || this.generalSettings.gameWorldTimeIntegration === GameWorldTimeIntegrations.Mixed)){ + Logger.debug(`Year.syncTime()`); + const month = this.getMonth(); + if(month){ + const day = month.getDay(); + //Get the days so for and add one to include the current day - Subtract one day to keep it in time with how about-time keeps track + const daysSoFar = this.dateToDays(this.numericRepresentation, month.numericRepresentation, day? day.numericRepresentation : 1, true, true) - 1; + const totalSeconds = this.time.getTotalSeconds(daysSoFar); + //Let the local functions know that we all ready updated this time + this.timeChangeTriggered = true; + //Set the world time, this will trigger the setFromTime function on all connected players when the updateWorldTime hook is triggered + await this.time.setWorldTime(totalSeconds); + } + } + } - //Assuming a start year of 0 - const totalDaysForYears = (daysPerYear * year) + (numberOfLeapYears * leapYearDayDifference); - Logger.debug(`Total Days For Years: ${totalDaysForYears}`); - const isLeapYear = this.leapYearRule.isLeapYear(year); - let daysSoFarThisYear = 0; - for(let i = 0; i < this.months.length; i++){ - //Only look at the month preceding the month we want and is not intercalary or is intercalary if the include setting is set otherwise skip - if(this.months[i].numericRepresentation < targetMonth && (!this.months[i].intercalary || (this.months[i].intercalary && this.months[i].intercalaryInclude))){ - if(isLeapYear){ - daysSoFarThisYear = daysSoFarThisYear + this.months[i].numberOfLeapYearDays; - } else { - daysSoFarThisYear = daysSoFarThisYear + this.months[i].numberOfDays; + /** + * Convert a number of seconds to year, month, day, hour, minute, seconds + * @param {number} seconds The seconds to convert + */ + secondsToDate(seconds: number){ + let sec = seconds, min = 0, hour = 0, day = 0, month = 0, year = 0; + if(sec >= this.time.secondsInMinute){ + min = Math.floor(sec / this.time.secondsInMinute); + sec = sec - (min * this.time.secondsInMinute); + } + if(min >= this.time.minutesInHour){ + hour = Math.floor(min / this.time.minutesInHour); + min = min - (hour * this.time.minutesInHour); + } + let dayCount = 0; + if(hour >= this.time.hoursInDay){ + dayCount = Math.floor(hour / this.time.hoursInDay); + hour = hour - (dayCount * this.time.hoursInDay); + } + // Add one day to keep the time in sync with how about-time does it + dayCount++; + let daysProcessed = 0; + while(dayCount > 0){ + let isLeapYear = this.leapYearRule.isLeapYear(year); + for(let i = 0; i < this.months.length; i ++){ + if(!this.months[i].intercalary || (this.months[i].intercalary && this.months[i].intercalaryInclude)){ + const daysInMonth = isLeapYear? this.months[i].numberOfLeapYearDays : this.months[i].numberOfDays; + month = i; + if(dayCount < daysInMonth) { + day = dayCount; + } + daysProcessed += daysInMonth; + dayCount -= daysInMonth; + if(dayCount <= 0){ + break; } } } - if(targetDay < 1){ - targetDay = 1; + if(dayCount > 0){ + year++; } - daysSoFarThisYear += (targetDay - 1); - Logger.debug(`Days So Far This Year: ${daysSoFarThisYear}`); - Logger.debug(`Number of days per week: ${this.weekdays.length}`); - return ((totalDaysForYears + daysSoFarThisYear)% this.weekdays.length + this.weekdays.length) % this.weekdays.length; - } else { - return 0; } + return { + year: year, + month: month, + day: day, + hour: hour, + minute: min, + second: sec + } + } + + /** + * Updates the year's data with passed in date information + * @param parsedDate + */ + updateTime(parsedDate: any){ + this.numericRepresentation = parsedDate.year; + this.updateMonth(parsedDate.month, 'current', true); + this.months[parsedDate.month].updateDay(parsedDate.day-1, this.leapYearRule.isLeapYear(parsedDate.year)); + this.time.setTime(parsedDate.hour, parsedDate.minute, parsedDate.second); } + /** + * Sets the simple calendars year, month, day and time from a passed in number of seconds + * @param {number} newTime The new time represented by seconds + * @param {number} changeAmount The amount that the time has changed by + */ + setFromTime(newTime: number, changeAmount: number){ + Logger.debug('Year.setFromTime()'); + if(changeAmount !== 0){ + // If the tracking rules are for self only and we requested the change OR the change came from a combat turn change + if((this.generalSettings.gameWorldTimeIntegration=== GameWorldTimeIntegrations.Self || this.generalSettings.gameWorldTimeIntegration === GameWorldTimeIntegrations.Mixed) && (this.timeChangeTriggered || this.combatChangeTriggered)){ + Logger.debug(`Tracking Rule: Self.\nTriggered Change: Simple Calendar/Combat Turn. Applying Change!`); + //If we didn't request the change (from a combat change) we need to update the internal time to match the new world time + if(!this.timeChangeTriggered){ + const parsedDate = this.secondsToDate(newTime); + this.updateTime(parsedDate); + } + // If the current player is the GM then we need to save this new value to the database + // Since the current date is updated this will trigger an update on all players as well + if(GameSettings.IsGm()){ + GameSettings.SaveCurrentDate(this).catch(Logger.error); + } + } + // If we didn't (locally) request this change then parse the new time into years, months, days and seconds and set those values + // This covers other modules/built in features updating the world time and Simple Calendar updating to reflect those changes + else if((this.generalSettings.gameWorldTimeIntegration === GameWorldTimeIntegrations.ThirdParty || this.generalSettings.gameWorldTimeIntegration === GameWorldTimeIntegrations.Mixed) && !this.timeChangeTriggered){ + Logger.debug('Tracking Rule: ThirdParty.\nTriggered Change: External Change. Applying Change!'); + const parsedDate = this.secondsToDate(newTime); + this.updateTime(parsedDate); + //We need to save the change so that when the game is reloaded simple calendar will display the correct time + if(GameSettings.IsGm()){ + GameSettings.SaveCurrentDate(this).catch(Logger.error); + } + } else { + Logger.debug(`Not Applying Change!`); + } + } + Logger.debug('Resetting time change triggers.'); + this.timeChangeTriggered = false; + this.combatChangeTriggered = false; + } + + /** + * Gets the current season based on the current date + */ + getCurrentSeason() { + let currentMonth = 0, currentDay = 0; + + const month = this.getMonth('visible'); + if(month){ + currentMonth = month.numericRepresentation; + const day = month.getDay('selected') || month.getDay(); + if(day){ + currentDay = day.numericRepresentation; + } else { + currentDay = 1; + } + } + if(currentDay > 0 && currentMonth > 0){ + let currentSeason: Season | null = null; + for(let i = 0; i < this.seasons.length; i++){ + if(this.seasons[i].startingMonth === currentMonth && this.seasons[i].startingDay <= currentDay){ + currentSeason = this.seasons[i]; + } else if (this.seasons[i].startingMonth < currentMonth){ + currentSeason = this.seasons[i]; + } + } + if(currentSeason === null){ + currentSeason = this.seasons[this.seasons.length - 1]; + } + + if(currentSeason){ + return { + name: currentSeason.name, + color: currentSeason.color === 'custom'? currentSeason.customColor : currentSeason.color + }; + } + } + return { + name: '', + color: '' + }; + } } diff --git a/src/constants.ts b/src/constants.ts index e9a51f2f..ab2ed427 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,6 +3,11 @@ */ export const ModuleName = 'foundryvtt-simple-calendar'; +/** + * The name of the module specific socket + */ +export const ModuleSocketName = `module.${ModuleName}`; + /** * The name of the settings that are saved in the world settings database */ @@ -14,7 +19,12 @@ export enum SettingNames { Notes = 'notes', AllowPlayersToAddNotes = 'allow-players-add-notes', DefaultNoteVisibility = 'default-note-visibility', - LeapYearRule = 'leap-year-rule' + LeapYearRule = 'leap-year-rule', + TimeConfiguration = 'time-configuration', + GeneralConfiguration = 'general-configuration', + ImportRan = 'import-ran', + SeasonConfiguration = 'season-configuration', + MoonConfiguration = 'moon-configuration' } /** @@ -27,8 +37,72 @@ export enum NoteRepeat { Yearly } +/** + * The different rules used for leap years + */ export enum LeapYearRules { None = 'none', Gregorian = 'gregorian', Custom = 'custom' } + +/** + * The different types of information we send over our socket + */ +export enum SocketTypes { + primary = 'primary', + time = 'time', + journal = 'journal' +} + +/** + * The different game world integrations offered + */ +export enum GameWorldTimeIntegrations { + /** + * Time tracking is disabled for this calendar + * no clock will be shown + * no time controls will be shown + * date controls will be shown + */ + None = 'none', + /** + * Simple Calendar is the main controller of the world time, updates to the game world time not initiated by simple calendar are ignored + * clock will be shown + * time controls will be shown + * date controls will be shown + */ + Self = 'self', + /** + * Another module is responsible for updating the game world time, simple calendar will not update the game world time + * clock will be shown + * time controls will not be shown + * date controls will not be shown + */ + ThirdParty = 'third-party', + /** + * Simple Calendar and other modules can both update the game world time, this could cause odd behaviour depending on what the other module is doing + * clock will be shown + * time controls will be shown + * date controls will be shown + */ + Mixed = 'mixed' +} + + +export enum MoonIcons { + NewMoon = 'new', + WaxingCrescent = 'waxing-crescent', + FirstQuarter = 'first-quarter', + WaxingGibbous = 'waxing-gibbous', + Full = 'full', + WaningGibbous = 'waning-gibbous', + LastQuarter = 'last-quarter', + WaningCrescent = 'waning-crescent' +} + +export enum MoonYearResetOptions { + None = 'none', + LeapYear = 'leap-year', + XYears = 'x-years' +} diff --git a/src/icons/first-quarter.svg b/src/icons/first-quarter.svg new file mode 100644 index 00000000..2898fe72 --- /dev/null +++ b/src/icons/first-quarter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/full.svg b/src/icons/full.svg new file mode 100644 index 00000000..9ffd7803 --- /dev/null +++ b/src/icons/full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/last-quarter.svg b/src/icons/last-quarter.svg new file mode 100644 index 00000000..045c4659 --- /dev/null +++ b/src/icons/last-quarter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/new.svg b/src/icons/new.svg new file mode 100644 index 00000000..0f8d3cda --- /dev/null +++ b/src/icons/new.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/waning-crescent.svg b/src/icons/waning-crescent.svg new file mode 100644 index 00000000..65577d5e --- /dev/null +++ b/src/icons/waning-crescent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/waning-gibbous.svg b/src/icons/waning-gibbous.svg new file mode 100644 index 00000000..1a6c74de --- /dev/null +++ b/src/icons/waning-gibbous.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/waxing-crescent.svg b/src/icons/waxing-crescent.svg new file mode 100644 index 00000000..b4fe723d --- /dev/null +++ b/src/icons/waxing-crescent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/icons/waxing-gibbous.svg b/src/icons/waxing-gibbous.svg new file mode 100644 index 00000000..ed320068 --- /dev/null +++ b/src/icons/waxing-gibbous.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 414aba41..a2a89bcd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,15 @@ import SimpleCalendar from "./classes/simple-calendar"; +import {Logger} from "./classes/logging"; SimpleCalendar.instance = new SimpleCalendar(); Hooks.on('ready', () => { //Initialize the Simple Calendar - SimpleCalendar.instance.init(); + SimpleCalendar.instance.init().catch(Logger.error); //Expose the macro show function (window as any).SimpleCalendar = {show: SimpleCalendar.instance.macroShow.bind(SimpleCalendar.instance)}; }); Hooks.on('getSceneControlButtons', SimpleCalendar.instance.getSceneControlButtons); +Hooks.on("updateWorldTime", SimpleCalendar.instance.worldTimeUpdate.bind(SimpleCalendar.instance)); +Hooks.on("updateCombat", SimpleCalendar.instance.combatUpdate.bind(SimpleCalendar.instance)); +Hooks.on("deleteCombat", SimpleCalendar.instance.combatDelete.bind(SimpleCalendar.instance)); +Hooks.on("pauseGame", SimpleCalendar.instance.gamePaused.bind(SimpleCalendar.instance)); diff --git a/src/interfaces.test.ts b/src/interfaces.test.ts index dcdbb449..2c815764 100644 --- a/src/interfaces.test.ts +++ b/src/interfaces.test.ts @@ -1,15 +1,28 @@ import { CalendarTemplate, - YearTemplate, - MonthTemplate, + CurrentDateConfig, DayTemplate, - YearConfig, + FirstNewMoonDate, + LeapYearConfig, MonthConfig, - CurrentDateConfig, WeekdayTemplate, WeekdayConfig, NoteConfig, NoteTemplate + MonthTemplate, MoonConfiguration, + MoonPhase, MoonTemplate, + NoteConfig, + NoteTemplate, + SeasonConfiguration, + SeasonTemplate, + SimpleCalendarSocket, + TimeConfig, + TimeTemplate, + WeekdayConfig, + WeekdayTemplate, + YearConfig, + YearTemplate } from "./interfaces"; +import {LeapYearRules, MoonIcons, MoonYearResetOptions, SocketTypes} from "./constants"; describe('Interface Tests', () => { - + const tt: TimeTemplate = {hour: '', minute: '', second: ''}; const dt: DayTemplate = {selected: false, current: false, name: '', numericRepresentation: 0}; const wt: WeekdayTemplate = {firstCharacter:'', name:'', numericRepresentation:0}; const mt: MonthTemplate = { @@ -34,14 +47,25 @@ describe('Interface Tests', () => { visibleMonth: mt, visibleMonthWeekOffset: [], weekdays: [wt], - showWeekdayHeaders: false + showWeekdayHeaders: false, + showClock: false, + clockClass: '', + showDateControls: false, + showTimeControls: false, + currentTime: tt, + currentSeasonColor: '', + currentSeasonName: '' }; const ct: CalendarTemplate = { isGM: false, + addNotes: false, + isPrimary: false, currentYear: yt, showSelectedDay: false, showCurrentDay: false, - notes: [] + notes: [], + clockClass: '', + timeUnits: {} }; @@ -75,7 +99,7 @@ describe('Interface Tests', () => { }); test('Year Template', () => { - expect(Object.keys(yt).length).toBe(9); //Make sure no new properties have been added + expect(Object.keys(yt).length).toBe(16); //Make sure no new properties have been added expect(yt.display).toBe(''); expect(yt.selectedDisplayYear).toBe(''); expect(yt.selectedDisplayMonth).toBe(''); @@ -85,15 +109,26 @@ describe('Interface Tests', () => { expect(yt.visibleMonthWeekOffset).toStrictEqual([]); expect(yt.weekdays).toStrictEqual([wt]); expect(yt.showWeekdayHeaders).toStrictEqual(false); + expect(yt.showClock).toStrictEqual(false); + expect(yt.showTimeControls).toStrictEqual(false); + expect(yt.showDateControls).toStrictEqual(false); + expect(yt.clockClass).toStrictEqual(''); + expect(yt.currentTime).toStrictEqual(tt); + expect(yt.currentSeasonName).toStrictEqual(''); + expect(yt.currentSeasonColor).toStrictEqual(''); }); test('Calendar Template', () => { - expect(Object.keys(ct).length).toBe(5); //Make sure no new properties have been added + expect(Object.keys(ct).length).toBe(9); //Make sure no new properties have been added expect(ct.isGM).toBe(false); + expect(ct.addNotes).toBe(false); + expect(ct.isPrimary).toBe(false); expect(ct.currentYear).toStrictEqual(yt); expect(ct.showSelectedDay).toBe(false); expect(ct.showCurrentDay).toBe(false); expect(ct.notes).toStrictEqual([]); + expect(ct.clockClass).toStrictEqual(''); + expect(ct.timeUnits).toStrictEqual({}); }); test('Year Config', () => { @@ -124,11 +159,12 @@ describe('Interface Tests', () => { }); test('Current Date Config', () => { - const yc: CurrentDateConfig = {year: 0, month: 0, day: 0}; - expect(Object.keys(yc).length).toBe(3); //Make sure no new properties have been added + const yc: CurrentDateConfig = {year: 0, month: 0, day: 0, seconds: 0}; + expect(Object.keys(yc).length).toBe(4); //Make sure no new properties have been added expect(yc.year).toBe(0); expect(yc.month).toBe(0); expect(yc.day).toBe(0); + expect(yc.seconds).toBe(0); }); test('Notes Template', () => { @@ -154,4 +190,112 @@ describe('Interface Tests', () => { expect(nc.id).toBe(''); expect(nc.repeats).toBe(0); }); + + test('Leap Year Config', () => { + const lyc: LeapYearConfig = {customMod: 0, rule: LeapYearRules.None}; + expect(Object.keys(lyc).length).toBe(2); //Make sure no new properties have been added + expect(lyc.customMod).toBe(0); + expect(lyc.rule).toBe(LeapYearRules.None); + }); + + test('Time Configuration', () => { + const tc: TimeConfig = {gameTimeRatio:1, secondsInMinute: 0, minutesInHour: 0, hoursInDay: 0}; + expect(Object.keys(tc).length).toBe(4); //Make sure no new properties have been added + expect(tc.gameTimeRatio).toBe(1); + expect(tc.secondsInMinute).toBe(0); + expect(tc.minutesInHour).toBe(0); + expect(tc.hoursInDay).toBe(0); + }); + + test('Time Template', () => { + expect(Object.keys(tt).length).toBe(3); //Make sure no new properties have been added + expect(tt.hour).toBe(''); + expect(tt.minute).toBe(''); + expect(tt.second).toBe(''); + }); + + test('Season Template', () => { + const st: SeasonTemplate = {name: '', startingMonth: 1, startingDay: 1, color: '', customColor: '', dayList: []}; + expect(st.name).toBe(''); + expect(st.color).toBe(''); + expect(st.customColor).toBe(''); + expect(st.startingMonth).toBe(1); + expect(st.startingDay).toBe(1); + expect(st.dayList).toStrictEqual([]); + }); + + test('Season Configuration', () => { + const sc: SeasonConfiguration = {name: '', startingMonth: 1, startingDay: 1, color: '', customColor: ''}; + expect(sc.name).toBe(''); + expect(sc.color).toBe(''); + expect(sc.customColor).toBe(''); + expect(sc.startingMonth).toBe(1); + expect(sc.startingDay).toBe(1); + }); + + test('Moon Phase', () => { + const mp: MoonPhase = {name: '', length: 1, singleDay: false, icon: MoonIcons.NewMoon}; + expect(mp.name).toBe(''); + expect(mp.length).toBe(1); + expect(mp.singleDay).toBe(false); + expect(mp.icon).toBe(MoonIcons.NewMoon); + }); + + test('First New Moon Date', () => { + const mp: FirstNewMoonDate = {yearReset: MoonYearResetOptions.None, yearX: 0, year: 0, month: 0, day: 0}; + expect(mp.yearReset).toBe(MoonYearResetOptions.None); + expect(mp.yearX).toBe(0); + expect(mp.year).toBe(0); + expect(mp.month).toBe(0); + expect(mp.day).toBe(0); + }); + + test('Moon Configuration', () => { + const mp: MoonConfiguration = {name: '', cycleLength: 0, cycleDayAdjust: 0, color: '', phases: [], firstNewMoon: {yearReset: MoonYearResetOptions.None, yearX: 0, year: 0, month: 0, day: 0}}; + expect(mp.name).toBe(''); + expect(mp.cycleLength).toBe(0); + expect(mp.cycleDayAdjust).toBe(0); + expect(mp.color).toBe(''); + expect(mp.phases).toStrictEqual([]); + expect(mp.firstNewMoon).toStrictEqual({yearReset: MoonYearResetOptions.None, yearX: 0, year: 0, month: 0, day: 0}); + }); + + test('Moon Template', () => { + const mp: MoonTemplate = {name: '', cycleLength: 0, cycleDayAdjust: 0, color: '', phases: [], firstNewMoon: {yearReset: MoonYearResetOptions.None, yearX: 0, year: 0, month: 0, day: 0}, dayList: []}; + expect(mp.name).toBe(''); + expect(mp.cycleLength).toBe(0); + expect(mp.cycleDayAdjust).toBe(0); + expect(mp.color).toBe(''); + expect(mp.phases).toStrictEqual([]); + expect(mp.firstNewMoon).toStrictEqual({yearReset: MoonYearResetOptions.None, yearX: 0, year: 0, month: 0, day: 0}); + expect(mp.dayList).toStrictEqual([]); + }); + + describe('Simple Calendar Socket', () => { + + test('Data', () => { + const d: SimpleCalendarSocket.Data = {data: {}, type: SocketTypes.time}; + expect(Object.keys(d).length).toBe(2); //Make sure no new properties have been added + expect(d.data).toStrictEqual({}); + expect(d.type).toBe(SocketTypes.time); + }); + + test('Simple Calendar Socket Time', () => { + const scst: SimpleCalendarSocket.SimpleCalendarSocketTime = {clockClass: ''}; + expect(Object.keys(scst).length).toBe(1); //Make sure no new properties have been added + expect(scst.clockClass).toBe(''); + }); + + test('Simple Calendar Socket Journal', () => { + const scsj: SimpleCalendarSocket.SimpleCalendarSocketJournal = {notes: []}; + expect(Object.keys(scsj).length).toBe(1); //Make sure no new properties have been added + expect(scsj.notes).toStrictEqual([]); + }); + + test('Simple Calendar Socket Primary', () => { + const scsj: SimpleCalendarSocket.SimpleCalendarPrimary = {}; + expect(Object.keys(scsj).length).toBe(0); //Make sure no new properties have been added + }); + + }); }); diff --git a/src/interfaces.ts b/src/interfaces.ts index c3d946fc..86af2d66 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,14 +1,39 @@ /** * Interface for the calendar template that is passed to the HTML for rendering */ -import {LeapYearRules, NoteRepeat} from "./constants"; +import { + LeapYearRules, + NoteRepeat, + GameWorldTimeIntegrations, + SocketTypes, + MoonIcons, + MoonYearResetOptions +} from "./constants"; +import {Note} from "./classes/note"; + +/** + * The general settings for the Simple calendar + * NOTE: The Player note default visibility is not stored in these settings (Legacy support) + */ +export interface GeneralSettings { + /** How Simple Calendar interacts with the game world time */ + gameWorldTimeIntegration: GameWorldTimeIntegrations; + /** If to show the clock below the calendar */ + showClock: boolean; + /** If players can add their own notes */ + playersAddNotes: boolean; +} export interface CalendarTemplate { isGM: boolean; + isPrimary: boolean; + addNotes: boolean; currentYear: YearTemplate; showSelectedDay: boolean; showCurrentDay: boolean; notes: NoteTemplate[]; + clockClass: string; + timeUnits: any; } /** @@ -33,6 +58,20 @@ export interface YearTemplate { showWeekdayHeaders: boolean; /** The days of the week */ weekdays: WeekdayTemplate[]; + + showClock: boolean; + + clockClass: string; + + showTimeControls: boolean; + + showDateControls: boolean; + + currentTime: TimeTemplate; + + currentSeasonName: string; + + currentSeasonColor: string; } /** @@ -98,6 +137,8 @@ export interface CurrentDateConfig { month: number; /** The current day */ day: number + /** The current time of day */ + seconds: number } /** @@ -128,7 +169,7 @@ export interface NoteConfig { } /** - * Interface for the weekday tempalte that is passed to the HTML for rendering + * Interface for the weekday template that is passed to the HTML for rendering */ export interface WeekdayTemplate { name: string; @@ -144,7 +185,286 @@ export interface WeekdayConfig { numericRepresentation: number; } +/** + * Interface for the data save to the game settings for the leap year information + */ export interface LeapYearConfig { rule: LeapYearRules; customMod: number; } + +/** + * Interface for the data saved to the game settings for the time information + */ +export interface TimeConfig { + hoursInDay: number; + minutesInHour: number; + secondsInMinute: number; + gameTimeRatio: number; +} + +/** + * Interface for displaying the time information + */ +export interface TimeTemplate { + hour: string; + minute: string; + second: string; +} + +/** + * Interface for displaying the season information + */ +export interface SeasonTemplate { + name: string; + startingMonth: number; + startingDay: number; + color: string; + customColor: string; + dayList: DayTemplate[]; +} + +/** + * Interface for saving season information + */ +export interface SeasonConfiguration { + name: string; + startingMonth: number; + startingDay: number; + color: string; + customColor: string; +} + +/** + * Interface for a moon phase + */ +export interface MoonPhase { + name: string; + length: number; + singleDay: boolean; + icon: MoonIcons; +} + +/** + * Interface for a moons first new moon date + */ +export interface FirstNewMoonDate { + yearReset: MoonYearResetOptions; + year: number; + yearX: number; + month: number; + day: number; +} + +/** + * Interface for a moons configuration + */ +export interface MoonConfiguration { + name: string; + cycleLength: number; + phases: MoonPhase[]; + firstNewMoon: FirstNewMoonDate; + color: string; + cycleDayAdjust: number; +} + +/** + * Interface for a moons template + */ +export interface MoonTemplate { + name: string; + cycleLength: number; + firstNewMoon: FirstNewMoonDate; + phases: MoonPhase[]; + color: string; + cycleDayAdjust: number; + dayList: DayTemplate[]; +} + +/** + * Namespace for our own socket information + */ +export namespace SimpleCalendarSocket{ + + /** + * Interface for the data that is sent with each socket + */ + export interface Data { + type: SocketTypes; + data: SimpleCalendarSocketJournal|SimpleCalendarSocketTime|SimpleCalendarPrimary; + } + + /** + * Interface for socket data that has to do with the time + */ + export interface SimpleCalendarSocketTime{ + clockClass: string; + } + + /** + * Interface for socket data that has to do with journals + */ + export interface SimpleCalendarSocketJournal{ + notes: Note[]; + } + + /** + * Interface for a GM to take over being the primary source + */ + export interface SimpleCalendarPrimary{ + + } +} + +/** + * Interfaces that have to do with the about time classes + * These are not apart of our interface unit tests + */ +export namespace AboutTimeImport { + /** + * The about-time calendar object + */ + export interface Calendar { + "clock_start_year": number; + "first_day": number; + "hours_per_day": number; + "seconds_per_minute": number; + "minutes_per_hour": number; + "has_year_0": boolean; + "month_len": MonthList; + "_month_len": {}, + "weekdays": string[], + "leap_year_rule": string; + "notes": {}; + } + + /** + * Calendar month list + */ + export interface MonthList{ + [key: string]: Month + } + + /** + * About time month object + */ + export interface Month { + "days": number[]; + "intercalary": boolean; + } +} + +/** + * Interfaces that have to do with the calendar/weather classes + * These are not apart of our interface unit tests + */ +export namespace CalendarWeatherImport{ + /** + * Calendar/Weather month class + */ + export interface Month { + name: string; + length: number; + leapLength: number; + isNumbered: boolean; + abbrev: string; + } + + /** + * Calendar/Weather date class + */ + export interface Date { + month: string; + day: number; + combined: string; + } + + /** + * Calendar/Weather weather class + */ + export interface Weather { + humidity: number; + temp: number; + lastTemp: number; + season: string; + seasonColor: string; + seasonTemp: number; + seasonHumidity: number; + seasonRolltable: string; + climate: string; + climateTemp: number; + climateHumidity: number; + precipitation: string; + dawn: number; + dusk: number; + isVolcanic: boolean; + outputToChat: boolean; + weatherFx: []; + isC: boolean; + cTemp: string; + tempRange: { + max: number; + min: number; + } + } + + /** + * Calendar/Weather seasons class + */ + export interface Seasons { + name: string; + rolltable: string; + date: Date; + temp: string; + humidity: string; + color: string; + dawn: number; + dusk: number; + } + + /** + * Calendar/Weather moon class + */ + export interface Moons { + name: string; + cycleLength: number; + cyclePercent: number; + lunarEclipseChange: number; + solarEclipseChange: number; + referenceTime: number; + referencePercent: number; + } + + /** + * Calendar/Weather event class + */ + export interface Event { + name: string; + text: string; + date: Date; + } + + /** + * Calendar/Weather calendar class + */ + export interface Calendar { + months: Month[]; + daysOfTheWeek: string[]; + year: number; + day: number; + numDayOfTheWeek: number; + first_day: number; + currentMonth: number; + currentWeekday: string; + dateWordy: string; + era: string; + dayLength: number; + timeDisp: string; + dateNum: string; + weather: Weather; + seasons: Seasons[]; + moons: Moons[]; + events: Event[]; + reEvents: Event[]; + } +} diff --git a/src/lang/en.json b/src/lang/en.json index 3d12167b..8ba9f2a2 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1,14 +1,24 @@ { "FSC.Title": "Simple Calendar", + "FSC.Help": "Help", + "FSC.One": "1", + "FSC.Five": "5", "FSC.ButtonTitle": "Calendar", "FSC.Day": "Day", + "FSC.Days": "Days", "FSC.Month": "Month", "FSC.Year": "Year", + "FSC.Hour": "Hour", + "FSC.Minute": "Minute", + "FSC.Second": "Second", "FSC.Forward": "Forward", "FSC.Back": "Back", - "FSC.DateControls": "Current Date Controls", + "FSC.DateControls": "Date Controls", "FSC.DateControlsHelp": "Use these controls to change the current date in your game.", + "FSC.NoDateControls": "Simple Calendar has been configured to let another module control the date and time. Please use that module to update the time or change Simple Calendars configuration.", + "FSC.SetCurrentDate": "Set Current Date", "FSC.Apply": "Apply", + "FSC.AdvancedOptions": "Advanced Options", "FSC.Today": "Today", "FSC.Configure": "Configure", "FSC.By": "By", @@ -27,6 +37,12 @@ "FSC.MoveYearBack": "Move Back One Year", "FSC.MoveMonthBack": "Move Back One Month", "FSC.MoveDayBack": "Move Back One Day", + "FSC.Time.Controls": "Time Controls", + "FSC.Time.ControlsHelp": "Select a unit of time to change then use the controls to advance or reduce that unit by 1 or 5.", + "FSC.Time.Forward": "Move time forward.", + "FSC.Time.Backward": "Move time backward.", + "FSC.Time.Start": "Start advancing game time", + "FSC.Time.Stop": "Stop advancing game time", "FSC.Notes.DialogTitle": "Note", "FSC.Notes.New": "New Note", "FSC.Notes.Empty": "No notes for this date", @@ -45,6 +61,7 @@ "FSC.Notes.Repeat.Monthly": "Monthly", "FSC.Notes.Repeat.Yearly": "Yearly", "FSC.Notes.Repeat.Weekly": "Weekly", + "FSC.Notes.Author": "Author", "FSC.Date.Sunday": "Sunday", "FSC.Date.Monday": "Monday", "FSC.Date.Tuesday": "Tuesday", @@ -64,12 +81,35 @@ "FSC.Date.October": "October", "FSC.Date.November": "November", "FSC.Date.December": "December", + "FSC.Time.Current": "Current Time", + "FSC.Moon.Phase.New": "New Moon", + "FSC.Moon.Phase.WaxingCrescent": "Waxing Crescent", + "FSC.Moon.Phase.FirstQuarter": "First Quarter", + "FSC.Moon.Phase.WaxingGibbous": "Waxing Gibbous", + "FSC.Moon.Phase.Full": "Full Moon", + "FSC.Moon.Phase.WaningGibbous": "Waning Gibbous", + "FSC.Moon.Phase.LastQuarter": "Last Quarter", + "FSC.Moon.Phase.WaningCrescent": "Waning Crescent", "FSC.Configuration.Title": "Calendar Configuration", "FSC.Configuration.Description": "This form allows you to customize your games calendar by specifying the current year, how many months, the name of each month and how many days each month has.", + "FSC.Configuration.Help": "For help configuring the calendar be sure to check out the documentation!", "FSC.Configuration.Save": "Save Configuration", "FSC.Configuration.General.Title": "General Settings", "FSC.Configuration.General.PreDefined": "Predefined Calendars", "FSC.Configuration.General.PreDefinedHelp": "Choose from one of the predefined calendars to start set up quicker.", + "FSC.Configuration.General.GameWorldTime": "Game World Time Integration", + "FSC.Configuration.General.GameWorldTimeHelp": "Tell the Simple calendar how to integrate itself with the game world time and respond to game world time updates.", + "FSC.Configuration.General.None": "None: Does not tie into the game world time.", + "FSC.Configuration.General.Self": "Self: Control the game world time ignoring updates from other modules.", + "FSC.Configuration.General.ThirdParty": "Third Party Module: Only follows game world time updates from other modules.", + "FSC.Configuration.General.Mixed": "Mixed: Update the game world time and follow updates made from other modules.", + "FSC.Configuration.General.ShowClock": "Show Clock", + "FSC.Configuration.General.ShowClockHelp": "If to show the time clock below the calendar, if checked the controls for changing hours, minutes and seconds will also show.", + "FSC.Configuration.General.PlayerAddNotes": "Players Can Add Notes", + "FSC.Configuration.General.PlayerAddNotesHelp": "Allow players to add their own notes to the calendar.", + "FSC.Configuration.General.QuickSetup": "Quick Setup", + "FSC.Configuration.General.Notes": "Notes", + "FSC.Configuration.General.ThirdPartyModule": "Third Party Module Import/Export", "FSC.Configuration.Year.Title": "Year Settings", "FSC.Configuration.Year.Current": "Current Year", "FSC.Configuration.Year.CurrentHelp": "The current year that the game is taking place in. This can also be changed with the Date Controls.", @@ -77,6 +117,24 @@ "FSC.Configuration.Year.PrefixHelp": "Text that appears before the year number.", "FSC.Configuration.Year.Postfix": "Year Postfix", "FSC.Configuration.Year.PostfixHelp": "Text that appears after the year number.", + "FSC.Configuration.Year.Seasons": "Seasons", + "FSC.Configuration.Season.Name": "Season Name", + "FSC.Configuration.Season.NameHelp": "The name of this season.", + "FSC.Configuration.Season.StartingMonth": "Starting Month", + "FSC.Configuration.Season.StartingMonthHelp": "The month when this season starts every year.", + "FSC.Configuration.Season.StartingDay": "Starting Day", + "FSC.Configuration.Season.StartingDayHelp": "The day of the month when this season starts every year.", + "FSC.Configuration.Season.Color": "Color", + "FSC.Configuration.Season.ColorHelp": "The color to associate with this season.", + "FSC.Configuration.Season.ColorWhite": "White", + "FSC.Configuration.Season.ColorSpring": "Spring", + "FSC.Configuration.Season.ColorSummer": "Summer", + "FSC.Configuration.Season.ColorFall": "Fall", + "FSC.Configuration.Season.ColorWinter": "Winter", + "FSC.Configuration.Season.ColorCustom": "Custom Color", + "FSC.Configuration.Season.ColorCustomHelp": "Set a custom hex value color for the season.", + "FSC.Configuration.Season.RemoveAll": "Remove All Seasons", + "FSC.Configuration.Season.Add": "Add New Season", "FSC.Configuration.Month.Title": "Month Settings", "FSC.Configuration.Month.Name": "Month Name", "FSC.Configuration.Month.NameHelp": "The text name of this month.", @@ -91,6 +149,7 @@ "FSC.Configuration.Month.Remove": "Remove", "FSC.Configuration.Month.RemoveAll": "Remove All Months", "FSC.Configuration.Month.Add": "Add New Month", + "FSC.Configuration.Months": "Months", "FSC.Configuration.Weekday.Title": "Weekday Settings", "FSC.Configuration.Weekday.ShowHeadings": "Show Weekday Headings", "FSC.Configuration.Weekday.ShowHeadingsHelp": "If to show the weekday headings on the calendar.", @@ -98,6 +157,8 @@ "FSC.Configuration.Weekday.NameHelp": "The text name of this day of the week.", "FSC.Configuration.Weekday.Add": "Add New Weekday", "FSC.Configuration.Weekday.RemoveAll": "Remove All Weekdays", + "FSC.Configuration.Weekdays": "Weekdays", + "FSC.Configuration.Weekday.Options": "Weekday Options", "FSC.Configuration.DefaultNoteVisibility": "Note Default Player Visibility", "FSC.Configuration.DefaultNoteVisibilityHint": "For new notes, if by default the player visibility option is checked or not", "FSC.Configuration.LeapYear.Title": "Leap Year Settings", @@ -110,10 +171,70 @@ "FSC.Configuration.LeapYear.Rules.Custom": "Custom", "FSC.Configuration.LeapYear.Days": "Number of Days in a Leap Year", "FSC.Configuration.LeapYear.DaysHelp": "The number of days that make up this month during a leap year.", + "FSC.Configuration.LeapYear.Options": "Leap Year Options", + "FSC.Configuration.Time.Title": "Time Settings", + "FSC.Configuration.Time.HoursInDay": "Hours in a Day", + "FSC.Configuration.Time.HoursInDayHelp": "How many hours make up a single day.", + "FSC.Configuration.Time.MinutesInHour": "Minutes in a Hour", + "FSC.Configuration.Time.MinutesInHourHelp": "How many minutes make up a single hour.", + "FSC.Configuration.Time.SecondsInMinute": "Seconds in a Minute", + "FSC.Configuration.Time.SecondsInMinuteHelp": "How many seconds make up a single minute.", + "FSC.Configuration.Time.GameTimeRatio": "Game Seconds Per Real Life Seconds", + "FSC.Configuration.Time.GameTimeRatioHelp": "How many seconds pass in game for every second that passes in real time.", + "FSC.Configuration.Time.Options": "Time Options", + "FSC.Configuration.Moon.Title": "Moon Settings", + "FSC.Configuration.Moon.Name": "Moon Name", + "FSC.Configuration.Moon.NameHelp": "The name of this moon.", + "FSC.Configuration.Moon.CycleLength": "Cycle Length", + "FSC.Configuration.Moon.CycleLengthHelp": "The length of the lunar cycle in days. Accepts decimals.", + "FSC.Configuration.Moon.CycleAdjustment": "Cycle Adjustment", + "FSC.Configuration.Moon.CycleAdjustmentHelp": "The amount of days to adjust the calculation by when determining the current phase.", + "FSC.Configuration.Moon.Color": "Moon Color", + "FSC.Configuration.Moon.ColorHelp": "The color to associate with this moon.", + "FSC.Configuration.Moon.Add": "Add New Moon", + "FSC.Configuration.Moon.RemoveAll": "Remove All Moons", + "FSC.Configuration.Moon.Phases": "Phases", + "FSC.Configuration.Moon.PhaseName": "Phase Name", + "FSC.Configuration.Moon.PhaseNameHelp": "The name of this phase of the moon.", + "FSC.Configuration.Moon.PhaseLength": "Phase Length", + "FSC.Configuration.Moon.PhaseLengthHelp": "How long the phase lasts in days.", + "FSC.Configuration.Moon.PhaseSingleDay": "Phase Single Day", + "FSC.Configuration.Moon.PhaseSingleDayHelp": "If this moon phase should only happen on 1 day.", + "FSC.Configuration.Moon.PhaseIcon": "Phase Icon", + "FSC.Configuration.Moon.PhaseIconHelp": "The icon to associate with this moon phase.", + "FSC.Configuration.Moon.PhaseAdd": "Add New Moon Phase", + "FSC.Configuration.Moon.PhaseRemoveAll": "Remove All Moon Phases", + "FSC.Configuration.Moon.FirstNewMoon": "Reference New Moon", + "FSC.Configuration.Moon.YearReset": "Reference Moon Year Reset", + "FSC.Configuration.Moon.YearResetHelp": "If the year for the reference moon should reset and if so when it does.", + "FSC.Configuration.Moon.YearResetNo": "Do not reset reference year", + "FSC.Configuration.Moon.YearResetLeap": "Reset reference year every leap year", + "FSC.Configuration.Moon.YearResetX": "Reset reference year every X years", + "FSC.Configuration.Moon.YearX": "Reset Reference Moon Years", + "FSC.Configuration.Moon.YearXHelp": "Reset the reference new moons year every x number of years.", + "FSC.Configuration.Moon.Year": "New Moon Year", + "FSC.Configuration.Moon.YearHelp": "The year of a new moon to use as a reference.", + "FSC.Configuration.Moon.Month": "New Moon Month", + "FSC.Configuration.Moon.MonthHelp": "The month of a new moon to use as a reference.", + "FSC.Configuration.Moon.Day": "New Moon Day", + "FSC.Configuration.Moon.DayHelp": "The day of a new moon to use as a reference.", + "FSC.Configuration.Moons": "Moons", + "FSC.Warn.Time.ActiveCombats": "There is an active combat/combats running, please resolve them before starting the real time clock.", + "FSC.Warn.Notes.NotGM": "There is no GM present in the game, a GM needs to be logged in for players to add notes.", "FSC.Error.Note.NoTitle": "Can't save a note with no title!", "FSC.Error.Note.RichText": "Please save the rich editor content.", "FSC.Error.Note.NoSelectedDay": "Unable to add new note, there is no selected day.", "FSC.Error.Note.NoSelectedMonth": "Unable to add new note, there is no selected month.", "FSC.Error.Calendar.GMConfigure": "You need to be a GM to configure the calendar.", - "FSC.Error.Calendar.GMCurrent": "You need to be a GM to change the current date." + "FSC.Error.Calendar.GMCurrent": "You need to be a GM to change the current date.", + "FSC.Module.Import": "Import Into Simple Calendar", + "FSC.Module.NoChanges": "No Changes", + "FSC.Module.CalendarWeather.Title": "Calendar/Weather Module Detected!", + "FSC.Module.CalendarWeather.Message": "We have detected that the Calendar/Weather module is enabled for this world.

If you would like to import the Calendar/Weather calendar settings into Simple Calendar choose \"Import Into Simple Calendar\", this will make the Simple Calendar match the Calendar/Weather settings.

If you would like to make Calendar/Weather match Simple Calendar choose \"Export Into Calendar/Weather\", this will make the Calendar/Weather settings match the Simple Calendars configuration.

Otherwise you can choose \"No Changes\" and things will be left as is. Should you want to do an import/export later the option will be available in the Simple Calendar general settings.
", + "FSC.Module.CalendarWeather.Configuration.Message": "We have detected that the Calendar/Weather module is enabled for this world.

If you would like to import the Calendar/Weather calendar settings into Simple Calendar choose \"Import Into Simple Calendar\", this will make the Simple Calendar match the Calendar/Weather settings.

If you would like to make Calendar/Weather match Simple Calendar choose \"Export Into Calendar/Weather\", this will make the Calendar/Weather settings match the Simple Calendars configuration.
", + "FSC.Module.CalendarWeather.Export": "Export Into Calendar/Weather", + "FSC.Module.AboutTime.Title": "About-Time Module Detected", + "FSC.Module.AboutTime.Message": "We have detected that the about-time module is/has been enabled for this world.

If you would like to import the about-time calendar settings into Simple Calendar choose \"Import Into Simple Calendar\", this will make the Simple Calendar match the about-time settings.

If you would like to make about-time match Simple Calendar choose \"Export Into About-Time\", this will make the about-time settings match the Simple Calendars configuration.

Otherwise you can choose \"No Changes\" and things will be left as is. Should you want to do an import/export later the option will be available in the Simple Calendar general settings.
", + "FSC.Module.AboutTime.Configuration.Message": "We have detected that the about-time module is/has been enabled for this world.

If you would like to import the about-time calendar settings into Simple Calendar choose \"Import Into Simple Calendar\", this will make the Simple Calendar match the about-time settings.

If you would like to make about-time match Simple Calendar choose \"Export Into About-Time\", this will make the about-time settings match the Simple Calendars configuration.
", + "FSC.Module.AboutTime.Export": "Export Into About-Time" } diff --git a/src/module.json b/src/module.json index c1cbe565..642b571c 100644 --- a/src/module.json +++ b/src/module.json @@ -2,12 +2,13 @@ "name": "foundryvtt-simple-calendar", "title": "Simple Calendar", "description": "A simple calendar module for keeping track of game days and events.", - "version": "1.1.8", + "version": "1.2.0", "author": "Dean Vigoren (Vigorator)", "minimumCoreVersion": "0.7.9", "compatibleCoreVersion": "0.7.9", "url": "https://github.com/vigoren/foundryvtt-simple-calendar", "bugs": "https://github.com/vigoren/foundryvtt-simple-calendar/issues", + "allowBugReporter": true, "readme": "https://github.com/vigoren/foundryvtt-simple-calendar/blob/main/README.md", "license": "https://github.com/vigoren/foundryvtt-simple-calendar/blob/main/LICENSE", "manifest": "", @@ -25,5 +26,12 @@ "name": "English", "path": "lang/en.json" } + ], + "socket": true, + "media": [ + { + "type": "cover", + "url": "https://raw.githubusercontent.com/vigoren/foundryvtt-simple-calendar/main/docs/images/logo.png" + } ] } diff --git a/src/styles/calendar.scss b/src/styles/calendar.scss index 3641faa9..d3f976e0 100644 --- a/src/styles/calendar.scss +++ b/src/styles/calendar.scss @@ -31,6 +31,15 @@ } + .season{ + text-align: center; + margin-bottom: 10px; + font-size: 12px; + font-style: italic; + font-weight: 600; + margin-top: -8px; + } + .weekdays{ display: flex; flex-direction: row; @@ -94,6 +103,123 @@ padding: 1px 1px 1px 2px; border-radius: 8px; } + + .moons{ + position: absolute; + right: 2px; + display: flex; + flex-direction: row; + + .moon-phase{ + width: 10px; + height: 10px; + background-size: contain; + margin-right: 2px; + margin-bottom: 2px; + border-radius: 5px; + + &.new{ + background-image: url("../icons/new.svg") ; + } + &.waxing-crescent{ + background-image: url("../icons/waxing-crescent.svg") ; + } + &.first-quarter{ + background-image: url("../icons/first-quarter.svg") ; + } + &.waxing-gibbous{ + background-image: url("../icons/waxing-gibbous.svg") ; + } + &.full{ + background-image: url("../icons/full.svg") ; + } + &.waning-gibbous{ + background-image: url("../icons/waning-gibbous.svg"); + } + &.last-quarter{ + background-image: url("../icons/last-quarter.svg") ; + } + &.waning-crescent{ + background-image: url("../icons/waning-crescent.svg") ; + } + + } + } + + } + } +} + +.time-display{ + text-align: center; + font-size: 1.25rem; + margin: -1rem 0 1rem; + background-color: #fff; + border-top: 0; + border-left: 1px solid #c3c3c3; + border-right: 1px solid #c3c3c3; + border-bottom: 1px solid #c3c3c3; + box-shadow: 2px 2px 2px #c3c3c3; + padding: 0.5rem; + + display: flex; + flex-direction: row; + justify-content: center; + + .clock { + position:relative; + transform:scale(1.25); + border-radius:50%; + border:2px solid; + width:20px; + height:20px; + margin-top: 2px; + margin-right: 8px; + + &:after { + position:absolute; + width:0px; + height:6px; + display:block; + border-left:1px solid #000; + content:''; + left: 8.25px; + top: 1.75px; + animation-duration: 1s; + } + + &:before { + position:absolute; + width:0px; + height:4px; + display:block; + border-left:1px solid #000; + content:''; + left: 8.25px; + top: 3.75px; + animation-duration: 60s; + } + &.stopped{ + border-color:#c12121; + &:before, &:after{ + border-color:#c12121; + } + } + &.paused{ + border-color:#de810d; + &:before, &:after{ + border-color:#de810d; + } + } + &.go{ + &:before, &:after{ + border-color: green; + transform-origin: bottom; + animation-name: dial; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + border-color: green; } } } @@ -107,3 +233,10 @@ flex: 1; } } + + + +@keyframes dial { + 0% {transform: rotate(0deg);} + 100% {transform: rotate(360deg);} +} diff --git a/src/styles/configuration.scss b/src/styles/configuration.scss index ed3e4ee6..163d38ae 100644 --- a/src/styles/configuration.scss +++ b/src/styles/configuration.scss @@ -7,23 +7,104 @@ line-height: 32px; font-size: 16px; border-bottom: 1px solid #782e22; - margin-bottom: 1rem; + + .item{ + border: 1px solid transparent; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + &.active{ + border-top-color: #782e22; + border-left-color: #782e22; + border-right-color: #782e22; + border-bottom-color: #fafafa; + margin-bottom: -1px; + + background-color: #fafafa; + } + &:hover{ + border-top-color: #782e22; + border-left-color: #782e22; + border-right-color: #782e22; + } + } + } + + .tab{ + border-bottom: 1px solid #782e22; + border-left: 1px solid #782e22; + border-right: 1px solid #782e22; + background-color:#fafafa; + color:#000000; + padding-top: 1rem; } .center{ text-align: center; } - .form-group{ + .settings-group{ margin: 0.75rem 2rem; - border-bottom: 1px solid #9e9c9c; - &:last-child{ - border-width: 0; + + &:first-child{ + margin-top: 0; } - button{ - max-width: 200px; - margin: 0.5rem auto; + h2{ + a{ + color: #191813; + font-size: 0.85rem; + text-decoration: none; + margin-left: 5px; + } + } + .form-group{ + margin: 0.75rem 0; + border-bottom: 1px solid #9e9c9c; + &:last-child{ + border-width: 0; + } + + .notes{ + margin: 0.5rem 0; + color:#191813; + } + + button{ + max-width: 200px; + margin: 0.5rem auto; + } + } + + .import-group{ + margin: 0.75rem 0; + padding: 0.5rem; + box-shadow: 2px 2px 4px grey; + border: 1px solid grey; + + h3{ + margin-left: 0; + margin-right: 0; + } + + .import-options{ + display: flex; + flex-direction: row; + justify-content: space-evenly; + + .control{ + width: auto; + &:before{ + margin-right: 5px; + } + } + } + } + + .f-table{ + border-top: 0; + border-left: 1px solid #7a7971; + border-right: 1px solid #7a7971; + margin-top: -0.5rem; } } @@ -51,8 +132,8 @@ } } - &:nth-child(even) { - background-color: rgba(255, 255, 255, 0.2); + &:nth-child(odd) { + background-color: #b1b1b133; } .month-name, .month-days{ @@ -62,6 +143,10 @@ width: 60px; text-align: center; } + input, select{ + margin: 0 auto; + display: block; + } .remove-month{ width: 100px; } @@ -75,6 +160,12 @@ .form-fields{ flex: 1; } + + h3{ + margin-top: 0.5rem; + border-bottom: 1px solid #782e22; + font-weight: 600; + } } } } @@ -87,15 +178,25 @@ .spacer{ flex: 1 1; } - .remove-month, .month-add{ + button{ width: 200px; } } - #scSubmit{ + .config-save{ position: absolute; - bottom: 5px; - left: 25%; - width: 50%; + text-align: center; + width: 100%; + backdrop-filter: blur(2px); + left: 0; + bottom: 0; + height: 30px; + border-top: 1px solid #b5b5b5; + + #scSubmit{ + width: 50%; + margin-top: 3px; + } } + } diff --git a/src/styles/gm-controls.scss b/src/styles/gm-controls.scss index 6d095f3d..4bab2e63 100644 --- a/src/styles/gm-controls.scss +++ b/src/styles/gm-controls.scss @@ -1,6 +1,15 @@ .control{ + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + + &:before{ + padding-right: 5px; + } + &.previous{ - span:last-child{ + span:last-of-type{ padding-right: 5px; } span.shift-back{ @@ -8,7 +17,7 @@ } } &.next{ - span:first-child{ + span:first-of-type{ padding-left: 5px; } span.shift-back{ @@ -17,25 +26,98 @@ } } -.controls{ - background-color: #fff; - border: 1px solid #c3c3c3; - box-shadow: 2px 2px 2px #c3c3c3; - padding: 5px; +.controls { width: 275px; margin: 0 0 1rem 1rem; - .control-group{ - display: flex; - flex-direction: row; - justify-content: space-between; - border-top: 1px solid #b9b9b9; - margin-bottom: 10px; - padding-top: 5px; - - .control-name{ - font-weight: 600; - line-height: 1.25rem; - font-size: 1.25rem; + + .time-controls{ + padding: 0.5rem; + box-shadow: 2px 2px 5px #b9b9b9; + margin-bottom: 1rem; + background-color: #fff; + .row{ + display: flex; + flex-direction: row; + justify-content: center; + + .time-unit{ + display: flex; + border: 1px solid black; + border-radius: 5px; + margin-bottom: 5px; + .selector{ + padding: .15rem .5rem; + border-right: 1px solid black; + line-height: 1rem; + cursor: pointer; + + &:first-child{ + border-radius: 4px 0 0 4px; + } + &:last-child{ + border-right-width: 0; + border-radius: 0 4px 4px 0; + } + &.selected{ + background-color: #603382; + color:#ffffff; + } + &:hover{ + background-color: #cd9cf3; + } + } + } + .time-start{ + margin: 0 10px; + line-height: 22px; + + &.go{ + color: green; + } + &.paused{ + color: #de810d; + } + } + .time-stop{ + line-height: 22px; + color: #c12121; + &.stopped{ + color: #6e6c6c + } + } + &.time-amount{ + .control{ + margin: 0 8px; + } + } + } + } + + .date-controls{ + padding: 0.5rem; + box-shadow: 2px 2px 5px #b9b9b9; + margin-bottom: 0.5rem; + background-color: #fff; + + .control-group{ + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #b9b9b9; + margin-bottom: 8px; + padding-bottom: 8px; + + &:last-child{ + border-bottom-width: 0; + padding-bottom: 0; + } + + .control-name{ + font-weight: 600; + line-height: 1.25rem; + font-size: 1.25rem; + } } } } diff --git a/src/styles/notes.scss b/src/styles/notes.scss index 7dfc4bde..88e67667 100644 --- a/src/styles/notes.scss +++ b/src/styles/notes.scss @@ -11,6 +11,11 @@ font-size: 12px; } + .note-author{ + text-align: right; + color: #383838; + font-size: 12px; + } .edit-controls{ display: flex; justify-content: space-around; diff --git a/src/templates/calendar-config.html b/src/templates/calendar-config.html index 6a8a7e52..bdd7a361 100644 --- a/src/templates/calendar-config.html +++ b/src/templates/calendar-config.html @@ -2,6 +2,7 @@

{{localize 'FSC.Configuration.Title'}}

{{localize 'FSC.Configuration.Description'}}

+

{{{localize 'FSC.Configuration.Help'}}}

-
- -
- -
-

{{localize 'FSC.Configuration.General.PreDefinedHelp'}}

- +
+

{{localize 'FSC.Configuration.General.QuickSetup'}}

+
+ +
+ +
+

{{localize 'FSC.Configuration.General.PreDefinedHelp'}}

+ +
+
+
+

{{localize 'FSC.Configuration.General.GameWorldTime'}}

+
+ +
+ +
+

{{localize 'FSC.Configuration.General.GameWorldTimeHelp'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.General.ShowClockHelp'}}

+
+
+
+

{{localize 'FSC.Configuration.General.Notes'}}

+
+ +
+ +
+

{{localize 'FSC.Configuration.DefaultNoteVisibilityHint'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.General.PlayerAddNotesHelp'}}

+
-
- -
- + {{#if (or importing.showCalendarWeather importing.showAboutTime)}} +
+

{{localize 'FSC.Configuration.General.ThirdPartyModule'}}

+ {{#if importing.showCalendarWeather}} +
+

{{localize 'FSC.Module.CalendarWeather.Title'}}

+

{{{localize 'FSC.Module.CalendarWeather.Configuration.Message'}}}

+
+ + +
+
+ {{else importing.showAboutTime}} +
+

{{localize 'FSC.Module.AboutTime.Title'}}

+

{{{localize 'FSC.Module.AboutTime.Configuration.Message'}}}

+
+ + +
-

{{localize 'FSC.Configuration.DefaultNoteVisibilityHint'}}

+ {{/if}}
+ {{/if}}
-
- -
- +
+

{{localize 'FSC.Configuration.Year.Current'}}

+
+ +
+ +
+

{{localize 'FSC.Configuration.Year.CurrentHelp'}}

-

{{localize 'FSC.Configuration.Year.CurrentHelp'}}

-
-
- -
- +
+ +
+ +
+

{{localize 'FSC.Configuration.Year.PrefixHelp'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Year.PostfixHelp'}}

-

{{localize 'FSC.Configuration.Year.PrefixHelp'}}

-
- -
- +
+

{{localize 'FSC.Configuration.Year.Seasons'}}

+
+
+
{{localize 'FSC.Configuration.Season.Name'}}
+
{{localize 'FSC.Configuration.Season.StartingMonth'}}
+
{{localize 'FSC.Configuration.Season.StartingDay'}}
+
{{localize 'FSC.Configuration.Season.Color'}}
+
+
+ {{#each seasons}} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Season.ColorCustomHelp'}}

+
+
+
+ {{/each}} +
+
+ + +
-

{{localize 'FSC.Configuration.Year.PostfixHelp'}}

-
-
-
{{localize 'FSC.Configuration.Month.Name'}}
-
{{localize 'FSC.Configuration.Month.Days'}}
-
{{localize 'FSC.Configuration.Month.Intercalary'}}
-
{{localize 'FSC.Configuration.Month.Number'}}
-
-
- {{#each months}} -
-
- -
-
- -
-
- -
-
{{#if numericRepresentation}}{{numericRepresentation}}{{else}}IC{{/if}}
-
- -
-
-
- -
- +
+

{{localize 'FSC.Configuration.Months'}}

+
+
+
{{localize 'FSC.Configuration.Month.Name'}}
+
{{localize 'FSC.Configuration.Month.Days'}}
+
{{localize 'FSC.Configuration.Month.Intercalary'}}
+
{{localize 'FSC.Configuration.Month.Number'}}
+
+
+ {{#each months}} +
+
+ +
+
+ +
+
+ +
+
{{#if numericRepresentation}}{{numericRepresentation}}{{else}}IC{{/if}}
+
+ +
+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Month.IntercalaryIncludeHelp'}}

-

{{localize 'FSC.Configuration.Month.IntercalaryIncludeHelp'}}

+ {{/each}} +
+
+ + +
- {{/each}} -
-
- - -
-
- -
- +
+

{{localize 'FSC.Configuration.Weekday.Options'}}

+
+ +
+ +
+

{{localize 'FSC.Configuration.Weekday.ShowHeadingsHelp'}}

-

{{localize 'FSC.Configuration.Weekday.ShowHeadingsHelp'}}

- - - - - - {{#each weekdays}} - - - - - {{/each}} -
{{localize 'FSC.Configuration.Weekday.Name'}}
- - - -
-
- - - +
+

{{localize 'FSC.Configuration.Weekdays'}}

+
+
+
{{localize 'FSC.Configuration.Weekday.Name'}}
+
+
+ {{#each weekdays}} +
+
+ +
+
+ +
+
+ {{/each}} +
+
+ + + +
-
- -
- -
-

{{localize 'FSC.Configuration.LeapYear.RuleHelp'}}

-
- {{#if showLeapYearCustomMod}} -
- -
- +
+

{{localize 'FSC.Configuration.LeapYear.Options'}}

+
+ +
+ +
+

{{localize 'FSC.Configuration.LeapYear.RuleHelp'}}

+
+ {{#if showLeapYearCustomMod}} +
+ +
+ +
+

{{localize 'FSC.Configuration.LeapYear.CustomModHelp'}}

-

{{localize 'FSC.Configuration.LeapYear.CustomModHelp'}}

+ {{/if}}
- {{/if}} {{#if showLeapYearMonths}} - - - - - - - {{#each months}} - - - - - - {{/each}} -
{{localize 'FSC.Configuration.Month.Name'}}{{localize 'FSC.Configuration.LeapYear.Days'}}{{localize 'FSC.Configuration.Month.Number'}}
{{name}} - - {{#if numericRepresentation}}{{numericRepresentation}}{{else}}IC{{/if}}
+
+

{{localize 'FSC.Configuration.Months'}}

+
+
+
{{localize 'FSC.Configuration.Month.Name'}}
+
{{localize 'FSC.Configuration.LeapYear.Days'}}
+
{{localize 'FSC.Configuration.Month.Number'}}
+
+ {{#each months}} +
+
{{name}}
+
+ +
+
{{#if numericRepresentation}}{{numericRepresentation}}{{else}}IC{{/if}}
+
+ {{/each}} +
+
{{/if}}
+
+
+

{{localize 'FSC.Configuration.Time.Options'}}

+
+ +
+ +
+

{{localize 'FSC.Configuration.Time.HoursInDayHelp'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Time.MinutesInHourHelp'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Time.SecondsInMinuteHelp'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Time.GameTimeRatioHelp'}}

+
+
+
+
+
+

{{localize 'FSC.Configuration.Moons'}}

+
+
+
{{localize 'FSC.Configuration.Moon.Name'}}
+
{{localize 'FSC.Configuration.Moon.CycleLength'}}
+
{{localize 'FSC.Configuration.Moon.CycleAdjustment'}}
+
{{localize 'FSC.Configuration.Moon.Color'}}
+
+
+ {{#each moons}} +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+

{{localize 'FSC.Configuration.Moon.FirstNewMoon'}}

+
+ +
+ +
+

{{localize 'FSC.Configuration.Moon.YearResetHelp'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Moon.YearHelp'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Moon.YearXHelp'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Moon.MonthHelp'}}

+
+
+ +
+ +
+

{{localize 'FSC.Configuration.Moon.DayHelp'}}

+
+

{{localize 'FSC.Configuration.Moon.Phases'}}

+
+
+
{{localize 'FSC.Configuration.Moon.PhaseName'}}
+
{{localize 'FSC.Configuration.Moon.PhaseLength'}}
+
{{localize 'FSC.Configuration.Moon.PhaseSingleDay'}}
+
{{localize 'FSC.Configuration.Moon.PhaseIcon'}}
+
+
+ {{#each phases}} +
+
+ +
+
{{length}} {{localize 'FSC.Days'}}
+
+ +
+
+ +
+
+ +
+
+ {{/each}} +
+
+ + + +
+
+
+ {{/each}} +
+
+ + + +
+
+
- - +
+ +
diff --git a/src/templates/calendar-notes.html b/src/templates/calendar-notes.html index 2fb12b75..59bd0411 100644 --- a/src/templates/calendar-notes.html +++ b/src/templates/calendar-notes.html @@ -8,6 +8,7 @@ {{/if}}

{{object.title}}

+
{{localize 'FSC.Notes.Author'}}: {{authorName}}
{{else}}

{{localize 'FSC.Notes.NewFor'}} {{object.monthDisplay}} {{object.day}}, {{object.year}}

@@ -20,7 +21,7 @@

{{localize 'FSC.Notes.NewFor'}} {{object.monthDisplay}} {{object.day}}, {{ob
- +

{{localize 'FSC.Notes.PlayerVisibleHelp'}}

diff --git a/src/templates/calendar.html b/src/templates/calendar.html index 52bac0a7..a09eed68 100644 --- a/src/templates/calendar.html +++ b/src/templates/calendar.html @@ -1,12 +1,15 @@
-
+
{{currentYear.visibleMonth.name}} {{currentYear.display}}
+ {{#if currentYear.currentSeasonName }} +
{{currentYear.currentSeasonName}}
+ {{/if}} {{#if currentYear.showWeekdayHeaders}}
{{#each currentYear.weekdays}} @@ -22,10 +25,19 @@
{{#day-has-note day=this}}{{/day-has-note}} {{this.name}} +
+ {{#day-moon-phase day=this}}{{/day-moon-phase}} +
{{/each}}
+ {{#if currentYear.showClock}} +
+ + {{localize 'FSC.Time.Current'}}: {{currentYear.currentTime.hour}}:{{currentYear.currentTime.minute}}:{{currentYear.currentTime.second}} +
+ {{/if}}
{{localize 'FSC.Today'}} {{#if isGM}} @@ -36,30 +48,76 @@
{{#if isGM}}
-

{{localize 'FSC.DateControls'}}

-

{{localize 'FSC.DateControlsHelp'}}

-
- - {{localize 'FSC.Day'}} - + {{#if (or currentYear.showTimeControls currentYear.showDateControls)}} + {{#if currentYear.showTimeControls}} +
+

{{localize 'FSC.Time.Controls'}}

+

{{localize 'FSC.Time.ControlsHelp'}}

+
+
+
{{localize 'FSC.Second'}}
+
{{localize 'FSC.Minute'}}
+
{{localize 'FSC.Hour'}}
+
+ {{#if isPrimary}} + + + {{/if}} +
+
-
- - {{localize 'FSC.Month'}} - + {{/if}} + {{#if currentYear.showDateControls}} +
+

{{localize 'FSC.DateControls'}}

+

{{localize 'FSC.DateControlsHelp'}}

+
+ + {{localize 'FSC.Day'}} + +
+
+ + {{localize 'FSC.Month'}} + +
+
+ + {{localize 'FSC.Year'}} + +
-
- - {{localize 'FSC.Year'}} - + {{/if}} + + {{else}} +
+

{{localize 'FSC.DateControls'}}

+

{{localize 'FSC.NoDateControls'}}

- + {{/if}}
{{/if}}

{{localize 'FSC.Notes.For'}} {{currentYear.selectedDisplayMonth}} {{currentYear.selectedDisplayDay}}, {{currentYear.selectedDisplayYear}} - {{#if isGM}} + {{#if addNotes}} {{/if}}

diff --git a/webpack.config.ts b/webpack.config.ts index 773ba1c2..6f9f2d40 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -21,7 +21,11 @@ const config: webpack.Configuration = { { context: './src/', from : '**/*.json', to : './' }, { context: './src/', from : '**/*.html', to : './' }, { context: './', from : 'README.md', to : './' }, - { context: './', from : 'LICENSE', to : './' } + { context: './', from : 'LICENSE', to : './' }, + { context: './docs', from : 'Configuration.md', to : './docs' }, + { context: './docs', from : 'Macros.md', to : './docs' }, + { context: './docs', from : 'Notes.md', to : './docs' }, + { context: './docs', from : 'UpdatingDateTime.md', to : './docs' }, ] }), new MiniCssExtractPlugin({ @@ -44,6 +48,10 @@ const config: webpack.Configuration = { // Compiles Sass to CSS 'sass-loader' ], + }, + { + test: /\.svg$/, + type: 'asset/inline' } ] },