Compare commits

...

27 Commits
v0.3 ... master

Author SHA1 Message Date
Luke Hubmayer-Werner 093d620bfe [WIP] Android updates to allow library folder selection. 2024-11-04 00:26:04 +09:00
Luke Hubmayer-Werner e1c12fe813 Fix subsampling bypassing getter and creating 0x0 viewport 2024-05-30 01:04:35 +09:30
Luke Hubmayer-Werner 91424ac804 Enable hidpi 2024-05-30 00:50:14 +09:30
Luke Hubmayer-Werner 53ef5708ef Missed an import 2024-05-29 22:58:31 +09:30
Luke Hubmayer-Werner 36f9d12054 Allow settings.cfg to set library paths 2024-05-29 22:11:01 +09:30
Luke Hubmayer-Werner eafc64b631 Uncommitted fixup from google play build 2024-05-29 16:32:19 +09:30
Luke Hubmayer-Werner 38e12182bf Step pad monitor 2024-03-23 19:42:17 +10:30
Luke Hubmayer-Werner a0bda98a23 Switch most relative node paths to scene-unique 2024-03-23 19:41:34 +10:30
Luke Hubmayer-Werner e2cb6e2bbc Change PascalCase variables to camelCase to avoid futue class shadowing 2024-03-23 19:06:47 +10:30
Luke Hubmayer-Werner 11d95ff441 [WIP] List joypads in step mode 2023-03-18 15:22:18 +10:30
Luke Hubmayer-Werner f0622ee370 Collapse quick options menu more 2023-03-18 15:22:03 +10:30
Luke Hubmayer-Werner c1501e2f6e slight refactor on menu sound playback 2023-03-18 14:35:00 +10:30
Luke Hubmayer-Werner cdae65ec87 change type hints 2023-03-18 14:34:42 +10:30
Luke Hubmayer-Werner dcc93df5d2 fix release dots 2023-03-18 14:34:04 +10:30
Luke Hubmayer-Werner 810020d374 Update song preview on keyboard song changes 2023-03-17 17:43:51 +10:30
Luke Hubmayer-Werner 67f48fd0dc Unknown WIP refactoring over the years 2023-03-17 17:41:37 +10:30
Luke Hubmayer-Werner d45a4ae0fa More refactoring 2021-11-27 23:22:42 +10:30
Luke Hubmayer-Werner ab848312e2 Refactoring in preparation for Step stuff 2021-11-27 22:20:37 +10:30
Luke Hubmayer-Werner eba538dfc1 Add fonts to repo 2021-11-27 16:52:19 +10:30
Luke Hubmayer-Werner 86b3e564c2 Restructuring to add Step gamemode 2021-11-27 16:51:59 +10:30
Luke Hubmayer-Werner ea1ed1fa17 Fix viewport size not updating correctly in many cases for subsampling 2021-09-19 19:07:13 +09:30
Luke Hubmayer-Werner bcdd85f884 Disable swipe stuff as it might be causing performance issues 2021-09-18 20:51:49 +09:30
Luke Hubmayer-Werner e389f9b3d4 Handle empty libraries 2021-09-18 20:51:20 +09:30
Luke Hubmayer-Werner 0960b12652 Testing node in case I ever need to play around with curve2d visualisation again 2021-02-21 21:35:39 +10:30
Luke Hubmayer-Werner 3fd2c8d821 Reverse slide draw order so loops and sharp bends look nicer 2021-02-21 21:30:02 +10:30
Luke Hubmayer-Werner d8c4c93934 Added side orbit slide type 2021-02-21 21:29:33 +10:30
Luke Hubmayer-Werner 6fa4c5f34a Add center orbit slides 2021-02-21 00:19:23 +10:30
54 changed files with 3187 additions and 1074 deletions

Binary file not shown.

Binary file not shown.

202
assets/fonts/LICENSE.txt Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

93
assets/fonts/OFL.txt Normal file
View File

@ -0,0 +1,93 @@
Copyright 2020 The Yusei Magic Project Authors (https://github.com/tanukifont/YuseiMagic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -28,6 +28,7 @@ process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true

View File

@ -28,6 +28,7 @@ process/fix_alpha_border=true
process/premult_alpha=false
process/HDR_as_SRGB=false
process/invert_color=false
process/normal_map_invert_y=false
stream=false
size_limit=0
detect_3d=true

View File

@ -0,0 +1,8 @@
[gd_resource type="AtlasTexture" load_steps=2 format=2]
[ext_resource path="res://assets/spritesheet-2048.png" type="Texture" id=1]
[resource]
flags = 4
atlas = ExtResource( 1 )
region = Rect2( 0, 1024, 512, 512 )

Binary file not shown.

View File

@ -8,21 +8,20 @@ export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Rhythm.x86_64"
patch_list=PoolStringArray( )
script_export_mode=1
script_encryption_key=""
[preset.0.options]
custom_template/debug=""
custom_template/release=""
binary_format/architecture="x86_64"
binary_format/embed_pck=false
texture_format/bptc=false
texture_format/s3tc=true
texture_format/etc=false
texture_format/etc2=false
texture_format/no_bptc_fallbacks=true
binary_format/64_bits=true
binary_format/embed_pck=false
custom_template/release=""
custom_template/debug=""
[preset.1]
@ -34,50 +33,55 @@ export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../../Rhythm.apk"
patch_list=PoolStringArray( )
script_export_mode=1
script_encryption_key=""
[preset.1.options]
graphics/32_bits_framebuffer=true
xr_features/xr_mode=0
xr_features/degrees_of_freedom=0
xr_features/hand_tracking=0
one_click_deploy/clear_previous_install=false
custom_template/debug=""
custom_template/release=""
custom_template/use_custom_build=false
command_line/extra_args=""
version/code=1
version/name="1.0"
package/unique_name="au.ufeff.rhythmgame"
package/name="Rhythm Game"
package/signed=true
screen/immersive_mode=true
screen/orientation=0
screen/support_small=true
screen/support_normal=true
screen/support_large=true
screen/support_xlarge=true
screen/opengl_debug=false
launcher_icons/main_192x192=""
launcher_icons/adaptive_foreground_432x432=""
launcher_icons/adaptive_background_432x432=""
custom_build/use_custom_build=false
custom_build/export_format=0
custom_build/min_sdk=""
custom_build/target_sdk=""
architectures/armeabi-v7a=true
architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
keystore/debug=""
keystore/debug_user=""
keystore/debug_password=""
keystore/release=""
keystore/release_user=""
keystore/release_password=""
one_click_deploy/clear_previous_install=false
version/code=1
version/name="1.0"
package/unique_name="au.ufeff.rhythmgame"
package/name="Rhythm Game"
package/signed=true
package/classify_as_game=true
package/retain_data_on_uninstall=false
package/exclude_from_recents=false
launcher_icons/main_192x192=""
launcher_icons/adaptive_foreground_432x432=""
launcher_icons/adaptive_background_432x432=""
graphics/opengl_debug=false
xr_features/xr_mode=0
xr_features/hand_tracking=0
xr_features/hand_tracking_frequency=0
xr_features/passthrough=0
screen/immersive_mode=true
screen/support_small=true
screen/support_normal=true
screen/support_large=true
screen/support_xlarge=true
user_data_backup/allow=false
command_line/extra_args=""
apk_expansion/enable=false
apk_expansion/SALT=""
apk_expansion/public_key=""
architectures/armeabi-v7a=true
architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
permissions/custom_permissions=PoolStringArray( )
permissions/custom_permissions=PoolStringArray( "MANAGE_EXTERNAL_STORAGE" )
permissions/access_checkin_properties=false
permissions/access_coarse_location=false
permissions/access_fine_location=false
@ -150,6 +154,7 @@ permissions/location_hardware=false
permissions/manage_accounts=false
permissions/manage_app_tokens=false
permissions/manage_documents=false
permissions/manage_external_storage=true
permissions/master_clear=false
permissions/media_content_control=false
permissions/modify_audio_settings=false
@ -158,6 +163,7 @@ permissions/mount_format_filesystems=false
permissions/mount_unmount_filesystems=false
permissions/nfc=false
permissions/persistent_activity=false
permissions/post_notifications=false
permissions/process_outgoing_calls=false
permissions/read_calendar=false
permissions/read_call_log=false
@ -234,15 +240,27 @@ export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path=""
patch_list=PoolStringArray( )
script_export_mode=1
script_encryption_key=""
[preset.2.options]
custom_template/debug=""
custom_template/release=""
variant/export_type=0
vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=false
html/export_icon=true
html/custom_html_shell=""
html/head_include=""
custom_template/release=""
custom_template/debug=""
html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false
progressive_web_app/enabled=false
progressive_web_app/offline_page=""
progressive_web_app/display=1
progressive_web_app/orientation=0
progressive_web_app/icon_144x144=""
progressive_web_app/icon_180x180=""
progressive_web_app/icon_512x512=""
progressive_web_app/background_color=Color( 0, 0, 0, 1 )

190
formats/RGT.gd Normal file
View File

@ -0,0 +1,190 @@
extends Node
# RhythmGameText formats
# .rgts - simplified format cutting out redundant data, should be easy to write charts in
# .rgtx - a lossless representation of MM in-memory format
# .rgtm - a collection of rgts charts, with a [title] at the start of each one
enum Format{RGTS, RGTX, RGTM}
const EXTENSIONS = {
'rgts': Format.RGTS,
'rgtx': Format.RGTX,
'rgtm': Format.RGTM,
}
const NOTE_TYPES = {
't': Note.NOTE_TAP,
'h': Note.NOTE_HOLD,
's': Note.NOTE_STAR,
'e': Note.NOTE_SLIDE,
'b': Note.NOTE_TAP, # Break
'x': Note.NOTE_STAR # Break star
}
const SLIDE_TYPES = {
'0': null, # Seems to be used for stars without slides attached
'1': Note.SlideType.CHORD,
'2': Note.SlideType.ARC_ACW,
'3': Note.SlideType.ARC_CW,
'4': Note.SlideType.COMPLEX, # Orbit around center ACW on the way
'5': Note.SlideType.COMPLEX, # CW of above
'6': Note.SlideType.COMPLEX, # S zigzag through center
'7': Note.SlideType.COMPLEX, # Z zigzag through center
'8': Note.SlideType.COMPLEX, # V into center
'9': Note.SlideType.COMPLEX, # Go to center then orbit off to the side ACW
'a': Note.SlideType.COMPLEX, # CW of above
'b': Note.SlideType.COMPLEX, # V into column 2 places ACW
'c': Note.SlideType.COMPLEX, # V into column 2 places CW
'd': Note.SlideType.CHORD_TRIPLE, # Triple cone. Spreads out to the adjacent receptors of the target.
'e': Note.SlideType.CHORD, # Not used in any of our charts
'f': Note.SlideType.CHORD, # Not used in any of our charts
}
const SLIDE_IN_R := sin(PI/8) # Circle radius circumscribed by chords 0-3, 1-4, 2-5 etc.
static func load_file(filename: String):
var extension = filename.rsplit('.', false, 1)[1]
if not EXTENSIONS.has(extension):
return -1
var format = EXTENSIONS[extension]
var file := File.new()
var err := file.open(filename, File.READ)
if err != OK:
print(err)
return err
var length = file.get_len()
var chart_ids = []
var lines = [[]]
# This loop will segment the lines as if the file were RGTM
while (file.get_position() < (length-1)): # Could probably replace this with file.eof_reached()
var line : String = file.get_line()
if line.begins_with('['): # Split to a new list for each chart definition
chart_ids.append(line.lstrip('[').rstrip(']'))
lines.append([])
elif !line.empty():
lines[-1].push_back(line)
file.close()
print('Parsing chart: ', filename)
match format:
Format.RGTS:
var metadata_and_notes = parse_rgts(lines[0])
return metadata_and_notes
Format.RGTX:
var metadata_and_notes = parse_rgtx(lines[0])
return metadata_and_notes
Format.RGTM:
lines.pop_front() # Anything before the first [header] is meaningless
var charts = {}
for i in len(lines):
charts[chart_ids[i]] = parse_rgts(lines[i])
return charts
return format
static func parse_rgtx(lines: PoolStringArray):
return [] # To be implemented later
const beats_per_measure = 4.0 # TODO: Bit of an ugly hack, need to revisit this later
static func parse_rgts(lines: PoolStringArray):
var metadata := {}
var num_taps := 0
var num_holds := 0
var num_slides := 0
var notes := []
var slide_ids := {}
var slide_stars := {} # Multiple stars might link to one star. We only care about linking for the spin speed.
var last_star := []
for i in Rules.COLS:
last_star.append(null)
for line in lines:
if len(line) < 4: # shortest legal line would be like '1:1t'
continue
var s = line.split(':')
var time := float(s[0]) * beats_per_measure
var note_hits := []
var note_nonhits := []
for i in range(1, len(s)):
var n = s[i]
var column := int(n[0])
var ntype = n[1]
n = n.substr(2)
match ntype:
't', 'b': # tap
note_hits.append(Note.NoteTap.new(time, column, ntype=='b'))
num_taps += 1
'h': # hold
var duration = float(n) * beats_per_measure
note_hits.append(Note.NoteHold.new(time, column, duration))
num_holds += 1
's', 'x': # slide star
var star = Note.NoteStar.new(time, column, ntype=='z')
note_hits.append(star)
num_slides += 1
last_star[column] = star
if len(n) > 1: # Not all stars have proper slide info
var slide_type = n[0] # hex digit
var slide_id = int(n.substr(1))
if slide_id > 0:
slide_stars[slide_id] = star
var slide = Note.NoteSlide.new(time, column)
slide_ids[slide_id] = slide
note_nonhits.append(slide)
'e': # slide end
var slide_type = n[0] # numeric digit, left as str just in case
var slide_id = int(n.substr(1))
if slide_id in slide_ids: # Classic slide end
slide_ids[slide_id].time_release = time
if slide_id in slide_stars:
slide_stars[slide_id].duration = slide_ids[slide_id].duration # Should probably recalc in case start time is different but w/e
slide_ids[slide_id].column_release = column
slide_ids[slide_id].slide_type = SLIDE_TYPES[slide_type]
slide_ids[slide_id].update_slide_variables()
if SLIDE_TYPES[slide_type] == Note.SlideType.COMPLEX:
var col_hit = slide_ids[slide_id].column
var RUV = GameTheme.RADIAL_UNIT_VECTORS
var RCA = GameTheme.RADIAL_COL_ANGLES
slide_ids[slide_id].values.curve2d.add_point(RUV[col_hit]) # Start col
match slide_type:
'4': # Orbit ACW around center. Size of loop is roughly inscribed in chords of 0-3, 1-4, 2-5... NB: doesn't loop if directly opposite col
Note.curve2d_make_orbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], true)
'5': # CW of above
Note.curve2d_make_orbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], false)
'6': # S zigzag through center
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit-2, Rules.COLS)] * SLIDE_IN_R)
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit+2, Rules.COLS)] * SLIDE_IN_R)
'7': # Z zigzag through center
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit+2, Rules.COLS)] * SLIDE_IN_R)
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit-2, Rules.COLS)] * SLIDE_IN_R)
'8': # V into center
slide_ids[slide_id].values.curve2d.add_point(Vector2.ZERO)
'9': # Orbit off-center ACW
Note.curve2d_make_sideorbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], true)
'a': # CW of above
Note.curve2d_make_sideorbit(slide_ids[slide_id].values.curve2d, RCA[col_hit], RCA[column], false)
'b': # V into column 2 places ACW
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit-2, Rules.COLS)])
'c': # V into column 2 places CW
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit+2, Rules.COLS)])
slide_ids[slide_id].values.curve2d.add_point(RUV[column]) # End col
else: # Naked slide start
if last_star[column] != null:
slide_stars[slide_id] = last_star[column]
else:
print_debug('Naked slide with no prior star in column!')
var note = Note.NoteSlide.new(time, column)
slide_ids[slide_id] = note
note_nonhits.append(note)
'_':
print_debug('Unknown note type: ', ntype)
if len(note_hits) > 1:
for note in note_hits: # Set multihit on each one
note.double_hit = true
notes += note_hits + note_nonhits
metadata['num_taps'] = num_taps
metadata['num_holds'] = num_holds
metadata['num_slides'] = num_slides
return [metadata, notes]

167
formats/SM.gd Normal file
View File

@ -0,0 +1,167 @@
extends Node
# Stepmania simfile
const NOTE_VALUES = {
'0': 'None',
'1': 'Tap',
'2': 'HoldStart',
'3': 'HoldRollEnd',
'4': 'RollStart',
'M': 'Mine',
# These three are less likely to show up anywhere, no need to implement
'K': 'Keysound',
'L': 'Lift',
'F': 'Fake',
}
const CHART_DIFFICULTIES = {
'Beginner': 0,
'Easy': 1,
'Medium': 2,
'Hard': 3,
'Challenge': 4,
'Edit': 5,
# Some will just write whatever for special difficulties, but we should at least color-code these standard ones
}
const TAG_TRANSLATIONS = {
'#TITLE': 'title',
'#SUBTITLE': 'subtitle',
'#ARTIST': 'artist',
'#TITLETRANSLIT': 'title_transliteration',
'#SUBTITLETRANSLIT': 'subtitle_transliteration',
'#ARTISTTRANSLIT': 'artist_transliteration',
'#GENRE': 'genre',
'#CREDIT': 'chart_author',
'#BANNER': 'image_banner',
'#BACKGROUND': 'image_background',
# '#LYRICSPATH': '',
'#CDTITLE': 'image_cd_title',
'#MUSIC': 'audio_filelist',
'#OFFSET': 'audio_offsets',
'#SAMPLESTART': 'audio_preview_times',
'#SAMPLELENGTH': 'audio_preview_times',
# '#SELECTABLE': '',
'#BPMS': 'bpm_values',
# '#STOPS': '',
# '#BGCHANGES': '',
# '#KEYSOUNDS': '',
}
static func load_chart(lines):
var metadata = {}
var notes = []
assert(lines[0].begins_with('#NOTES:'))
metadata['chart_type'] = lines[1].strip_edges().rstrip(':')
metadata['description'] = lines[2].strip_edges().rstrip(':')
metadata['difficulty_str'] = lines[3].strip_edges().rstrip(':')
metadata['numerical_meter'] = lines[4].strip_edges().rstrip(':')
metadata['groove_radar'] = lines[5].strip_edges().rstrip(':')
# Measures are separated by lines that start with a comma
# Each line has a state for each of the pads, e.g. '0000' for none pressed
# The lines become even subdivisions of the measure, so if there's 4 lines everything represents a 1/4 beat, if there's 8 lines everything represents a 1/8 beat etc.
# For this reason it's probably best to just have a float for beat-within-measure rather than integer beats.
var measures = [[]]
for i in range(6, len(lines)):
var line = lines[i].strip_edges()
if line.begins_with(','):
measures.append([])
elif line.begins_with(';'):
break
elif len(line) > 0:
measures[-1].append(line)
var ongoing_holds = {}
var num_notes := 0
var num_jumps := 0
var num_hands := 0
var num_holds := 0
var num_rolls := 0
var num_mines := 0
for measure in range(len(measures)):
var m_lines = measures[measure]
var m_length = len(m_lines) # Divide out all lines by this
for beat in m_length:
var line : String = m_lines[beat]
# Jump check at a line-level (check for multiple 1/2/4s)
var hits : int = line.count('1') + line.count('2') + line.count('4')
# Hand/quad check more complex as need to check hold/roll state as well
# TODO: are they exclusive? Does quad override hand override jump? SM5 doesn't have quads and has hands+jumps inclusive
var total_pressed : int = hits + len(ongoing_holds)
var jump : bool = hits >= 2
var hand : bool = total_pressed >= 3
# var quad : bool = total_pressed >= 4
num_notes += hits
num_jumps += int(jump)
num_hands += int(hand)
var time = measure + beat/float(m_length)
for col in len(line):
match line[col]:
'1':
notes.append(Note.NoteTap.new(time, col))
'2': # Hold
ongoing_holds[col] = len(notes)
notes.append(Note.NoteHold.new(time, col, 0.0))
num_holds += 1
'4': # Roll
ongoing_holds[col] = len(notes)
notes.append(Note.NoteRoll.new(time, col, 0.0))
num_rolls += 1
'3': # End Hold/Roll
assert(ongoing_holds.has(col))
notes[ongoing_holds[col]].set_time_release(time)
ongoing_holds.erase(col)
'M': # Mine
num_mines += 1
pass
metadata['num_notes'] = num_notes
metadata['num_taps'] = num_notes - num_jumps
metadata['num_jumps'] = num_jumps
metadata['num_hands'] = num_hands
metadata['num_holds'] = num_holds
metadata['num_rolls'] = num_rolls
metadata['num_mines'] = num_mines
metadata['notes'] = notes
return metadata
static func load_file(filename: String) -> Array:
# Output is [metadata, [[meta0, chart0], ..., [metaN, chartN]]]
# Technically, declarations end with a semicolon instead of a linebreak.
# This is a PITA to do correctly in GDScript and the files in our collection are well-behaved with linebreaks anyway, so we won't bother.
var file := File.new()
match file.open(filename, File.READ):
OK:
pass
var err:
print_debug('Error loading file: ', err)
return []
var length = file.get_len()
var lines = [[]] # First list will be header, then every subsequent one is a chart
while (file.get_position() < (length-1)): # Could probably replace this with file.eof_reached()
var line : String = file.get_line()
if line.begins_with('#NOTES'): # Split to a new list for each chart definition
lines.append([])
lines[-1].append(line)
file.close()
var metadata = {}
for line in lines[0]:
var tokens = line.rstrip(';').split(':')
if TAG_TRANSLATIONS.has(tokens[0]):
metadata[TAG_TRANSLATIONS[tokens[0]]] = tokens[1]
elif len(tokens) >= 2:
metadata[tokens[0]] = tokens[1]
var charts = []
for i in range(1, len(lines)):
charts.append(load_chart(lines[i]))
return [metadata, charts]

73
formats/SRT.gd Normal file
View File

@ -0,0 +1,73 @@
extends Node
# A legacy format that is relatively easily parsed. Radial game mode.
const TAP_DURATION := 0.062500
const ID_BREAK := 4
const ID_HOLD := 2
const ID_SLIDE_END := 128
const ID3_SLIDE_CHORD := 0 # Straight line
const ID3_SLIDE_ARC_CW := 1
const ID3_SLIDE_ARC_ACW := 2
static func load_file(filename):
var file = File.new()
var err = file.open(filename, File.READ)
if err != OK:
print(err)
return err
var metadata := {}
var num_taps := 0
var num_holds := 0
var num_slides := 0
var notes := []
var beats_per_measure := 4
var length = file.get_len()
var slide_ids = {}
while (file.get_position() < (length-2)):
var noteline = file.get_csv_line()
var time_hit := (float(noteline[0]) + (float(noteline[1]))) * beats_per_measure
var duration := float(noteline[2]) * beats_per_measure
var column := int(noteline[3])
var id := int(noteline[4])
var id2 := int(noteline[5])
var id3 := int(noteline[6])
match id:
ID_HOLD:
notes.push_back(Note.NoteHold.new(time_hit, column, duration))
num_holds += 1
ID_BREAK:
notes.push_back(Note.NoteTap.new(time_hit, column, true))
num_taps += 1
ID_SLIDE_END:
# id2 is slide ID
if id2 in slide_ids:
slide_ids[id2].column_release = column
slide_ids[id2].update_slide_variables()
_:
if id2 == 0:
notes.push_back(Note.NoteTap.new(time_hit, column))
num_taps += 1
else:
# id2 is slide ID, id3 is slide pattern
# In order to properly declare the slide, we need the paired endcap which may not be the next note
var slide_type = Note.SlideType.CHORD
match id3:
ID3_SLIDE_CHORD:
slide_type = Note.SlideType.CHORD
ID3_SLIDE_ARC_CW:
slide_type = Note.SlideType.ARC_CW
ID3_SLIDE_ARC_ACW:
slide_type = Note.SlideType.ARC_ACW
_:
print('Unknown slide type: ', id3)
var note = Note.NoteStar.new(time_hit, column)
num_slides += 1
note.duration = duration
notes.push_back(note)
var slide = Note.NoteSlide.new(time_hit, column, duration, -1, slide_type)
notes.push_back(slide)
slide_ids[id2] = slide
metadata['num_taps'] = num_taps
metadata['num_holds'] = num_holds
metadata['num_slides'] = num_slides
return [metadata, notes]

34
formats/Test.gd Normal file
View File

@ -0,0 +1,34 @@
extends Node
# In case things need to be tested without a library
static func stress_pattern():
var notes = []
for bar in range(8):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1))
for i in range(1, 8):
notes.push_back(Note.NoteTap.new(bar*4 + (i/2.0), (bar + i)%8))
notes.push_back(Note.NoteTap.new(bar*4 + (7/2.0), (bar + 3)%8))
for bar in range(8, 16):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 2))
for i in range(1, 8):
notes.push_back(Note.NoteTap.new(bar*4 + (i/2.0), (bar + i)%8))
notes.push_back(Note.NoteTap.new(bar*4 + ((i+0.5)/2.0), (bar + i)%8))
notes.push_back(Note.make_slide(bar*4 + ((i+1)/2.0), 1, (bar + i)%8, 0))
for bar in range(16, 24):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 2))
notes.push_back(Note.NoteHold.new(bar*4, (bar+1)%8, 1))
for i in range(2, 8):
notes.push_back(Note.NoteTap.new(bar*4 + (i/2.0), (bar + i)%8))
notes.push_back(Note.NoteHold.new(bar*4 + ((i+1)/2.0), (bar + i)%8, 0.5))
for bar in range(24, 32):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1))
for i in range(1, 32):
notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i)%8))
if (i%2) > 0:
notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i + 4)%8))
for bar in range(32, 48):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1))
for i in range(1, 32):
notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i)%8))
notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i + 3)%8))
return notes

40
main.gd Normal file
View File

@ -0,0 +1,40 @@
extends Control
onready var mainMenu := $'%mainMenu'
onready var optionPanel := $'%OptionPanel'
const touchGamePath := 'res://scenes/RadialGame.tscn'
const stepGamePath := 'res://scenes/StepGame.tscn'
const touchGameScene := preload(touchGamePath)
const stepGameScene := preload(stepGamePath)
const SettingsMenu := preload('res://scenes/SettingsMenu.tscn')
var activeGame: Node = null
func exit_mode() -> void:
remove_child(activeGame)
activeGame = null
mainMenu.show()
optionPanel.show()
func _on_MainMenu_start_stepgame() -> void:
mainMenu.hide()
activeGame = stepGameScene.instance()
activeGame.connect('exit_mode', self, 'exit_mode')
add_child_below_node(mainMenu, activeGame)
func _on_MainMenu_start_touchgame() -> void:
mainMenu.hide()
activeGame = touchGameScene.instance()
activeGame.connect('exit_mode', self, 'exit_mode')
add_child_below_node(mainMenu, activeGame)
activeGame.alignment_horizontal = AspectRatioContainer.ALIGN_BEGIN
func _on_mainMenu_open_settings():
mainMenu.hide()
optionPanel.hide()
activeGame = SettingsMenu.instance()
activeGame.connect('exit_mode', self, 'exit_mode')
add_child_below_node(mainMenu, activeGame)

View File

@ -1,24 +1,25 @@
[gd_scene load_steps=5 format=2]
[gd_scene load_steps=6 format=2]
[ext_resource path="res://RadialGame.tscn" type="PackedScene" id=1]
[ext_resource path="res://scenes/MainMenu.tscn" type="PackedScene" id=1]
[ext_resource path="res://scripts/TouchInput.gd" type="Script" id=2]
[ext_resource path="res://default.theme" type="Theme" id=3]
[ext_resource path="res://OptionPanel.tscn" type="PackedScene" id=13]
[ext_resource path="res://main.gd" type="Script" id=4]
[ext_resource path="res://scenes/OptionPanel.tscn" type="PackedScene" id=13]
[node name="main" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
theme = ExtResource( 3 )
script = ExtResource( 4 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="RadialGame" parent="." instance=ExtResource( 1 )]
anchor_right = 0.0
margin_right = 600.0
rect_min_size = Vector2( 1080, 0 )
[node name="mainMenu" parent="." instance=ExtResource( 1 )]
unique_name_in_owner = true
[node name="OptionPanel" parent="." instance=ExtResource( 13 )]
unique_name_in_owner = true
anchor_left = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
@ -35,3 +36,7 @@ script = ExtResource( 2 )
__meta__ = {
"_edit_use_anchors_": false
}
[connection signal="open_settings" from="mainMenu" to="." method="_on_mainMenu_open_settings"]
[connection signal="start_stepgame" from="mainMenu" to="." method="_on_MainMenu_start_stepgame"]
[connection signal="start_touchgame" from="mainMenu" to="." method="_on_MainMenu_start_touchgame"]

90
mainmenu_theme.tres Normal file
View File

@ -0,0 +1,90 @@
[gd_resource type="Theme" load_steps=7 format=2]
[ext_resource path="res://assets/fonts/Sniglet-Regular.ttf" type="DynamicFontData" id=1]
[sub_resource type="DynamicFont" id=1]
size = 48
outline_size = 4
outline_color = Color( 0, 0, 0, 1 )
font_data = ExtResource( 1 )
[sub_resource type="StyleBoxFlat" id=3]
content_margin_left = 24.0
content_margin_right = 24.0
content_margin_top = 4.0
content_margin_bottom = 4.0
bg_color = Color( 0.184314, 0.180392, 0.192157, 1 )
border_width_left = 12
border_width_top = 4
border_width_right = 12
border_width_bottom = 4
border_color = Color( 0.172549, 0.164706, 0.196078, 1 )
corner_radius_top_left = 16
corner_radius_top_right = 16
corner_radius_bottom_right = 16
corner_radius_bottom_left = 16
[sub_resource type="StyleBoxFlat" id=4]
content_margin_left = 24.0
content_margin_right = 24.0
content_margin_top = 4.0
content_margin_bottom = 4.0
bg_color = Color( 0.517647, 0.482353, 0.647059, 1 )
border_width_left = 12
border_width_top = 4
border_width_right = 12
border_width_bottom = 4
border_color = Color( 0.172549, 0.164706, 0.196078, 1 )
corner_radius_top_left = 16
corner_radius_top_right = 16
corner_radius_bottom_right = 16
corner_radius_bottom_left = 16
[sub_resource type="StyleBoxFlat" id=2]
content_margin_left = 24.0
content_margin_right = 24.0
content_margin_top = 4.0
content_margin_bottom = 4.0
bg_color = Color( 0.235294, 0.227451, 0.266667, 1 )
border_width_left = 12
border_width_top = 4
border_width_right = 12
border_width_bottom = 4
border_color = Color( 0.172549, 0.164706, 0.196078, 1 )
corner_radius_top_left = 16
corner_radius_top_right = 16
corner_radius_bottom_right = 16
corner_radius_bottom_left = 16
[sub_resource type="StyleBoxFlat" id=5]
content_margin_left = 24.0
content_margin_right = 24.0
content_margin_top = 4.0
content_margin_bottom = 4.0
bg_color = Color( 0.376471, 0.356863, 0.443137, 1 )
border_width_left = 12
border_width_top = 4
border_width_right = 12
border_width_bottom = 4
border_color = Color( 0.709804, 0.709804, 1, 1 )
border_blend = true
corner_radius_top_left = 16
corner_radius_top_right = 16
corner_radius_bottom_right = 16
corner_radius_bottom_left = 16
shadow_color = Color( 0.423529, 0.670588, 1, 0.372549 )
shadow_size = 6
[resource]
default_font = SubResource( 1 )
Button/colors/font_color = Color( 0.88, 0.88, 0.88, 1 )
Button/colors/font_color_disabled = Color( 0.9, 0.9, 0.9, 0.2 )
Button/colors/font_color_hover = Color( 0.827451, 0.788235, 1, 1 )
Button/colors/font_color_pressed = Color( 1, 1, 1, 1 )
Button/constants/hseparation = 2
Button/fonts/font = SubResource( 1 )
Button/styles/disabled = SubResource( 3 )
Button/styles/focus = SubResource( 4 )
Button/styles/hover = SubResource( 2 )
Button/styles/normal = SubResource( 2 )
Button/styles/pressed = SubResource( 5 )

View File

@ -35,12 +35,10 @@ Video="*res://singletons/Video.gd"
[debug]
settings/fps/force_fps=120
gdscript/warnings/unused_variable=false
gdscript/warnings/shadowed_variable=false
gdscript/warnings/unused_argument=false
gdscript/warnings/unused_signal=false
gdscript/warnings/return_value_discarded=false
gdscript/warnings/integer_division=false
[display]
@ -48,6 +46,7 @@ gdscript/warnings/integer_division=false
window/size/width=1080
window/size/height=1080
window/size/fullscreen=true
window/dpi/allow_hidpi=true
window/handheld/orientation="sensor"
window/stretch/mode="2d"
window/stretch/aspect="expand"
@ -60,7 +59,7 @@ singletons_disabled=[ ]
[rendering]
vram_compression/import_etc=true
environment/default_clear_color=Color( 0.11, 0.11, 0.11, 1 )
environment/default_clear_color=Color( 0.109804, 0.109804, 0.109804, 1 )
quality/filters/msaa=1
environment/default_environment="res://default_env.tres"
quality/subsampling/x=1.0

View File

@ -0,0 +1,52 @@
extends HBoxContainer
onready var le_directory := $le_directory
onready var btn_browse := $btn_browse
onready var btn_remove := $btn_remove
signal path_updated
export var directory: String = "" setget set_directory, get_directory
func set_directory(value: String):
directory = value
if le_directory:
le_directory.text = directory
emit_signal('path_updated', value)
func get_directory():
return le_directory.text
export var removable: bool = true setget set_removable
func set_removable(value: bool):
removable = value
if btn_remove:
btn_remove.disabled = not removable
export var readonly: bool = false setget set_readonly
func set_readonly(value: bool):
readonly = value
if le_directory:
le_directory.editable = not readonly
if btn_browse:
btn_browse.disabled = readonly
func _ready():
le_directory.text = directory
le_directory.editable = not readonly
btn_browse.disabled = readonly
btn_remove.disabled = not removable
#func _process(delta):
# pass
func _on_btn_browse_pressed():
var dialog = FileDialog.new()
dialog.access = FileDialog.ACCESS_FILESYSTEM
dialog.mode = FileDialog.MODE_OPEN_DIR
dialog.connect("dir_selected", self, "set_directory")
add_child(dialog)
dialog.popup(Rect2(0, 0, 800, 600))
func _on_btn_remove_pressed():
set_directory("")

View File

@ -0,0 +1,30 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scenes/DirectorySettingsEntry.gd" type="Script" id=1]
[node name="DirectorySettingsEntry" type="HBoxContainer"]
margin_top = 18.0
margin_right = 1080.0
margin_bottom = 42.0
script = ExtResource( 1 )
[node name="le_directory" type="LineEdit" parent="."]
margin_right = 937.0
margin_bottom = 24.0
rect_min_size = Vector2( 480, 1.36422e-12 )
size_flags_horizontal = 3
[node name="btn_browse" type="Button" parent="."]
margin_left = 941.0
margin_right = 1012.0
margin_bottom = 24.0
text = "Browse..."
[node name="btn_remove" type="Button" parent="."]
margin_left = 1016.0
margin_right = 1080.0
margin_bottom = 24.0
text = "Remove"
[connection signal="pressed" from="btn_browse" to="." method="_on_btn_browse_pressed"]
[connection signal="pressed" from="btn_remove" to="." method="_on_btn_remove_pressed"]

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://FontTesting.gd" type="Script" id=1]
[ext_resource path="res://scripts/FontTesting.gd" type="Script" id=1]
[node name="Control" type="Control"]
anchor_right = 1.0

View File

@ -0,0 +1,43 @@
extends VBoxContainer
export var joypad_index = 0
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
pass # Replace with function body.
const color_off = Color.blue
const color_on = Color.white
var id_left = 0
var id_down = 1
var id_up = 2
var id_right = 3
var id_start = 4
var id_back = 5
func set_name(name):
$Label.text = name
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
var left = Input.get_joy_axis(joypad_index, id_left)
var down = Input.get_joy_axis(joypad_index, id_down)
var up = Input.get_joy_axis(joypad_index, id_up)
var right = Input.get_joy_axis(joypad_index, id_right)
var start = Input.get_joy_axis(joypad_index, id_start)
var back = Input.get_joy_axis(joypad_index, id_back)
var precision = "%.05f"
$Grid/left/Label.text = precision % left
$Grid/down/Label.text = precision % down
$Grid/up/Label.text = precision % up
$Grid/right/Label.text = precision % right
$TopRow/lbl_start.text = "start\n" + precision % start
$TopRow/lbl_back.text = "back\n" + precision % back
$Grid/left/t.modulate = color_on if Input.is_joy_button_pressed(joypad_index, id_left) else color_off
$Grid/down/t.modulate = color_on if Input.is_joy_button_pressed(joypad_index, id_down) else color_off
$Grid/up/t.modulate = color_on if Input.is_joy_button_pressed(joypad_index, id_up) else color_off
$Grid/right/t.modulate = color_on if Input.is_joy_button_pressed(joypad_index, id_right) else color_off
$TopRow/lbl_start.uppercase = Input.is_joy_button_pressed(joypad_index, id_start)
$TopRow/lbl_back.uppercase = Input.is_joy_button_pressed(joypad_index, id_back)

View File

@ -0,0 +1,157 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://assets/step_arrow_2048_atlas_texture.tres" type="Texture" id=1]
[ext_resource path="res://scenes/JoypadMonitor6Btn.gd" type="Script" id=2]
[node name="JoypadMonitor" type="VBoxContainer"]
margin_right = 160.0
margin_bottom = 200.0
script = ExtResource( 2 )
[node name="Label" type="Label" parent="."]
margin_right = 200.0
margin_bottom = 14.0
text = "Joypad Name"
align = 1
[node name="TopRow" type="Control" parent="."]
margin_top = 18.0
margin_right = 200.0
margin_bottom = 34.0
rect_min_size = Vector2( 160, 16 )
[node name="lbl_back" type="Label" parent="TopRow"]
margin_right = 40.0
margin_bottom = 14.0
text = "BACK"
[node name="lbl_start" type="Label" parent="TopRow"]
anchor_right = 1.0
margin_bottom = 14.0
text = "START"
align = 2
[node name="Grid" type="GridContainer" parent="."]
margin_top = 38.0
margin_right = 200.0
margin_bottom = 238.0
rect_min_size = Vector2( 192, 192 )
columns = 3
[node name="up_left" type="Control" parent="Grid"]
margin_right = 64.0
margin_bottom = 64.0
rect_min_size = Vector2( 64, 64 )
[node name="up" type="Control" parent="Grid"]
margin_left = 68.0
margin_right = 132.0
margin_bottom = 64.0
rect_min_size = Vector2( 64, 64 )
[node name="t" type="TextureRect" parent="Grid/up"]
margin_right = 512.0
margin_bottom = 512.0
rect_scale = Vector2( 0.125, 0.125 )
texture = ExtResource( 1 )
stretch_mode = 1
[node name="Label" type="Label" parent="Grid/up"]
anchor_right = 1.0
anchor_bottom = 1.0
text = "0.00"
align = 1
valign = 1
[node name="up_right" type="Control" parent="Grid"]
margin_left = 136.0
margin_right = 200.0
margin_bottom = 64.0
rect_min_size = Vector2( 64, 64 )
[node name="left" type="Control" parent="Grid"]
margin_top = 68.0
margin_right = 64.0
margin_bottom = 132.0
rect_min_size = Vector2( 64, 64 )
[node name="t" type="TextureRect" parent="Grid/left"]
margin_top = 64.0
margin_right = 512.0
margin_bottom = 576.0
rect_rotation = -90.0
rect_scale = Vector2( 0.125, 0.125 )
texture = ExtResource( 1 )
stretch_mode = 1
[node name="Label" type="Label" parent="Grid/left"]
anchor_right = 1.0
anchor_bottom = 1.0
text = "0.00"
align = 1
valign = 1
[node name="center" type="Control" parent="Grid"]
margin_left = 68.0
margin_top = 68.0
margin_right = 132.0
margin_bottom = 132.0
rect_min_size = Vector2( 64, 64 )
[node name="right" type="Control" parent="Grid"]
margin_left = 136.0
margin_top = 68.0
margin_right = 200.0
margin_bottom = 132.0
rect_min_size = Vector2( 64, 64 )
[node name="t" type="TextureRect" parent="Grid/right"]
margin_left = 64.0
margin_right = 576.0
margin_bottom = 512.0
rect_rotation = 90.0
rect_scale = Vector2( 0.125, 0.125 )
texture = ExtResource( 1 )
stretch_mode = 1
[node name="Label" type="Label" parent="Grid/right"]
anchor_right = 1.0
anchor_bottom = 1.0
text = "0.00"
align = 1
valign = 1
[node name="down_left" type="Control" parent="Grid"]
margin_top = 136.0
margin_right = 64.0
margin_bottom = 200.0
rect_min_size = Vector2( 64, 64 )
[node name="down" type="Control" parent="Grid"]
margin_left = 68.0
margin_top = 136.0
margin_right = 132.0
margin_bottom = 200.0
rect_min_size = Vector2( 64, 64 )
[node name="t" type="TextureRect" parent="Grid/down"]
margin_right = 512.0
margin_bottom = 512.0
rect_scale = Vector2( 0.125, 0.125 )
texture = ExtResource( 1 )
stretch_mode = 1
flip_v = true
[node name="Label" type="Label" parent="Grid/down"]
anchor_right = 1.0
anchor_bottom = 1.0
text = "0.00"
align = 1
valign = 1
[node name="down_right" type="Control" parent="Grid"]
margin_left = 136.0
margin_top = 136.0
margin_right = 200.0
margin_bottom = 200.0
rect_min_size = Vector2( 64, 64 )

59
scenes/MainMenu.tscn Normal file
View File

@ -0,0 +1,59 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://mainmenu_theme.tres" type="Theme" id=1]
[ext_resource path="res://scripts/MainMenu.gd" type="Script" id=2]
[node name="MainMenu" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
theme = ExtResource( 1 )
script = ExtResource( 2 )
[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
custom_constants/separation = 16
alignment = 1
[node name="btn_touch" type="Button" parent="VBoxContainer"]
margin_left = 451.0
margin_top = 380.0
margin_right = 629.0
margin_bottom = 448.0
size_flags_horizontal = 4
text = "Touch"
[node name="btn_step" type="Button" parent="VBoxContainer"]
margin_left = 465.0
margin_top = 464.0
margin_right = 614.0
margin_bottom = 532.0
size_flags_horizontal = 4
text = "Step"
[node name="btn_settings" type="Button" parent="VBoxContainer"]
margin_left = 426.0
margin_top = 548.0
margin_right = 653.0
margin_bottom = 616.0
size_flags_horizontal = 4
text = "Settings"
[node name="btn_quit" type="Button" parent="VBoxContainer"]
margin_left = 471.0
margin_top = 632.0
margin_right = 608.0
margin_bottom = 700.0
size_flags_horizontal = 4
text = "Quit"
[node name="lbl_settingspath" type="Label" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
text = "Library directory is"
align = 1
[connection signal="pressed" from="VBoxContainer/btn_touch" to="." method="_on_btn_touch_pressed"]
[connection signal="pressed" from="VBoxContainer/btn_step" to="." method="_on_btn_step_pressed"]
[connection signal="pressed" from="VBoxContainer/btn_settings" to="." method="_on_btn_settings_pressed"]
[connection signal="pressed" from="VBoxContainer/btn_quit" to="." method="quit"]

View File

@ -1,7 +1,7 @@
[gd_scene load_steps=6 format=2]
[ext_resource path="res://scripts/ScoreText.gd" type="Script" id=1]
[ext_resource path="res://scripts/Menu.gd" type="Script" id=2]
[ext_resource path="res://scripts/TouchMenu.gd" type="Script" id=2]
[ext_resource path="res://shaders/menu.tres" type="Material" id=3]
[ext_resource path="res://shaders/scoretext.tres" type="Material" id=4]
@ -11,6 +11,8 @@ _data = [ Vector2( -1, -1 ), 0.0, 0.0, 0, 0, Vector2( 0, 0 ), 2.0, 2.0, 1, 1, Ve
[node name="Menu" type="Control"]
material = ExtResource( 3 )
anchor_right = 1.0
anchor_bottom = 1.0
rect_clip_content = true
script = ExtResource( 2 )
__meta__ = {

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=7 format=2]
[gd_scene load_steps=8 format=2]
[ext_resource path="res://assets/fonts/NotoSans-Regular.ttf" type="DynamicFontData" id=1]
[ext_resource path="res://scripts/OptionPanel.gd" type="Script" id=2]
@ -39,6 +39,8 @@ func _process(delta):
update()
"
[sub_resource type="GradientTexture2D" id=4]
[node name="OptionPanel" type="VBoxContainer"]
margin_left = 2.0
margin_right = 269.0
@ -64,8 +66,8 @@ __meta__ = {
[node name="PanelContainer" type="PanelContainer" parent="."]
margin_top = 38.0
margin_right = 300.0
margin_bottom = 638.0
rect_min_size = Vector2( 300, 600 )
margin_bottom = 169.0
rect_min_size = Vector2( 300, 80 )
__meta__ = {
"_edit_use_anchors_": false
}
@ -74,15 +76,15 @@ __meta__ = {
margin_left = 7.0
margin_top = 7.0
margin_right = 293.0
margin_bottom = 593.0
margin_bottom = 124.0
[node name="cb_qsettings" type="CheckBox" parent="PanelContainer/VBoxContainer"]
margin_right = 286.0
margin_bottom = 31.0
pressed = true
text = "Quick Settings"
[node name="vbox_qsettings" type="VBoxContainer" parent="PanelContainer/VBoxContainer"]
visible = false
margin_top = 35.0
margin_right = 286.0
margin_bottom = 279.0
@ -211,14 +213,14 @@ __meta__ = {
}
[node name="HSeparator" type="HSeparator" parent="PanelContainer/VBoxContainer"]
margin_top = 283.0
margin_top = 35.0
margin_right = 286.0
margin_bottom = 287.0
margin_bottom = 39.0
[node name="cb_graphics" type="CheckBox" parent="PanelContainer/VBoxContainer"]
margin_top = 291.0
margin_top = 43.0
margin_right = 286.0
margin_bottom = 322.0
margin_bottom = 74.0
text = "Graphics"
[node name="vbox_graphics" type="VBoxContainer" parent="PanelContainer/VBoxContainer"]
@ -296,19 +298,25 @@ __meta__ = {
}
[node name="HSeparator2" type="HSeparator" parent="PanelContainer/VBoxContainer"]
margin_top = 326.0
margin_top = 78.0
margin_right = 286.0
margin_bottom = 330.0
margin_bottom = 82.0
[node name="VidTextureRect" type="TextureRect" parent="PanelContainer/VBoxContainer" groups=[
"VideoTexRects",
]]
margin_top = 334.0
[node name="cb_vid" type="CheckBox" parent="PanelContainer/VBoxContainer"]
margin_top = 86.0
margin_right = 286.0
margin_bottom = 586.0
margin_bottom = 117.0
text = "Show Video Here"
[node name="VidTextureRect" type="TextureRect" parent="PanelContainer/VBoxContainer" groups=["VideoTexRects"]]
visible = false
margin_top = 369.0
margin_right = 286.0
margin_bottom = 609.0
rect_min_size = Vector2( 240, 240 )
size_flags_horizontal = 7
size_flags_vertical = 7
texture = SubResource( 4 )
expand = true
stretch_mode = 6
@ -323,3 +331,4 @@ stretch_mode = 6
[connection signal="toggled" from="PanelContainer/VBoxContainer/vbox_graphics/hbox/btn_wakelock" to="." method="_on_btn_wakelock_toggled"]
[connection signal="value_changed" from="PanelContainer/VBoxContainer/vbox_graphics/sl_SSX" to="." method="_on_sl_SSX_value_changed"]
[connection signal="value_changed" from="PanelContainer/VBoxContainer/vbox_graphics/sl_SSY" to="." method="_on_sl_SSY_value_changed"]
[connection signal="toggled" from="PanelContainer/VBoxContainer/cb_vid" to="PanelContainer/VBoxContainer/VidTextureRect" method="set_visible"]

11
scenes/RadialGame.gd Normal file
View File

@ -0,0 +1,11 @@
extends AspectRatioContainer
signal exit_mode
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
#func _process(delta):
# pass

View File

@ -1,21 +1,22 @@
[gd_scene load_steps=21 format=2]
[gd_scene load_steps=22 format=2]
[ext_resource path="res://scripts/InputHandler.gd" type="Script" id=1]
[ext_resource path="res://scripts/NoteViewport.gd" type="Script" id=1]
[ext_resource path="res://assets/text-4k.png" type="Texture" id=2]
[ext_resource path="res://assets/fonts/Sniglet-Regular.ttf" type="DynamicFontData" id=3]
[ext_resource path="res://scripts/ScreenFilter.gd" type="Script" id=4]
[ext_resource path="res://scripts/NoteViewport.gd" type="Script" id=5]
[ext_resource path="res://scripts/NotePainter.gd" type="Script" id=6]
[ext_resource path="res://scripts/NoteHandler.gd" type="Script" id=8]
[ext_resource path="res://scripts/Receptors.gd" type="Script" id=9]
[ext_resource path="res://shaders/notelines.shader" type="Shader" id=10]
[ext_resource path="res://shaders/notemesh.shader" type="Shader" id=11]
[ext_resource path="res://shaders/receptors.shader" type="Shader" id=12]
[ext_resource path="res://scripts/Bezel.gd" type="Script" id=13]
[ext_resource path="res://Menu.tscn" type="PackedScene" id=15]
[ext_resource path="res://scripts/NotePainter.gd" type="Script" id=3]
[ext_resource path="res://scripts/InputHandler.gd" type="Script" id=4]
[ext_resource path="res://scripts/Bezel.gd" type="Script" id=5]
[ext_resource path="res://scripts/NoteHandler.gd" type="Script" id=6]
[ext_resource path="res://assets/fonts/Sniglet-Regular.ttf" type="DynamicFontData" id=7]
[ext_resource path="res://scripts/Receptors.gd" type="Script" id=8]
[ext_resource path="res://scripts/ScreenFilter.gd" type="Script" id=9]
[ext_resource path="res://scenes/Menu.tscn" type="PackedScene" id=10]
[ext_resource path="res://shaders/receptors.shader" type="Shader" id=11]
[ext_resource path="res://shaders/notemesh.shader" type="Shader" id=12]
[ext_resource path="res://shaders/notelines.shader" type="Shader" id=13]
[ext_resource path="res://scenes/RadialGame.gd" type="Script" id=14]
[sub_resource type="ShaderMaterial" id=1]
shader = ExtResource( 12 )
shader = ExtResource( 11 )
shader_param/num_receptors = 8
shader_param/receptor_offset = 0.392699
shader_param/line_color = Color( 0, 0, 1, 1 )
@ -29,14 +30,14 @@ shader_param/px2 = 0.00217391
shader_param/alpha = 1.0
[sub_resource type="ShaderMaterial" id=2]
shader = ExtResource( 11 )
shader = ExtResource( 12 )
shader_param/bps = null
shader_param/star_color = null
shader_param/held_color = null
shader_param/screen_size = null
[sub_resource type="ShaderMaterial" id=3]
shader = ExtResource( 10 )
shader = ExtResource( 13 )
shader_param/line_color = Color( 0.8, 0.8, 1, 0.8 )
shader_param/line_color_double = Color( 1, 1, 0.6, 0.9 )
shader_param/dot_color = Color( 1, 1, 1, 0.8 )
@ -55,9 +56,9 @@ shader_param/array_size = 256
size = 48
outline_size = 2
outline_color = Color( 0, 0, 0, 1 )
font_data = ExtResource( 3 )
font_data = ExtResource( 7 )
[sub_resource type="GDScript" id=7]
[sub_resource type="GDScript" id=5]
script/source = "extends Label
const colors = [Color.gray, Color.lightgray, Color.aqua, Color.gold]
@ -73,27 +74,24 @@ func _on_NoteHandler_finished_song(song_key, score_data) -> void:
visible = false
"
[sub_resource type="CanvasItemMaterial" id=5]
[sub_resource type="CanvasItemMaterial" id=6]
blend_mode = 4
[sub_resource type="Curve" id=6]
[sub_resource type="Curve" id=7]
min_value = -1.0
_data = [ Vector2( -1, -1 ), 0.0, 0.0, 0, 0, Vector2( 0, 0 ), 2.0, 2.0, 1, 1, Vector2( 1, 1 ), 0.0, 0.0, 0, 0 ]
[node name="RadialGame" type="AspectRatioContainer"]
anchor_right = 1.0
anchor_bottom = 1.0
__meta__ = {
"_edit_use_anchors_": false
}
script = ExtResource( 14 )
[node name="Square" type="Control" parent="."]
[node name="square" type="Control" parent="."]
unique_name_in_owner = true
margin_right = 1080.0
margin_bottom = 1080.0
[node name="video" type="TextureRect" parent="Square" groups=[
"VideoTexRects",
]]
[node name="video" type="TextureRect" parent="square" groups=["VideoTexRects"]]
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
@ -106,25 +104,18 @@ __meta__ = {
"_edit_use_anchors_": false
}
[node name="ScreenFilter" type="ColorRect" parent="Square"]
[node name="ScreenFilter" type="ColorRect" parent="square"]
anchor_right = 1.0
anchor_bottom = 1.0
color = Color( 0, 0, 0, 1 )
script = ExtResource( 4 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Receptors" type="Control" parent="Square"]
material = SubResource( 1 )
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 9 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="NoteHandler" type="Control" parent="Square"]
[node name="receptors" type="Control" parent="square"]
unique_name_in_owner = true
material = SubResource( 1 )
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 8 )
@ -132,28 +123,38 @@ __meta__ = {
"_edit_use_anchors_": false
}
[node name="Viewport" type="Viewport" parent="Square/NoteHandler"]
[node name="noteHandler" type="Control" parent="square"]
unique_name_in_owner = true
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 6 )
[node name="viewport" type="Viewport" parent="square/noteHandler"]
unique_name_in_owner = true
size = Vector2( 1080, 1080 )
transparent_bg = true
usage = 1
render_target_v_flip = true
script = ExtResource( 5 )
script = ExtResource( 1 )
[node name="Center" type="Node2D" parent="Square/NoteHandler/Viewport"]
[node name="Center" type="Node2D" parent="square/noteHandler/viewport"]
position = Vector2( 540, 540 )
[node name="SlideTrailHandler" type="Node2D" parent="Square/NoteHandler/Viewport/Center"]
[node name="slideTrailHandler" type="Node2D" parent="square/noteHandler/viewport/Center"]
unique_name_in_owner = true
[node name="JudgeText" type="MeshInstance2D" parent="Square/NoteHandler/Viewport/Center"]
[node name="judgeText" type="MeshInstance2D" parent="square/noteHandler/viewport/Center"]
unique_name_in_owner = true
texture = ExtResource( 2 )
[node name="meshinstance" type="MeshInstance2D" parent="Square/NoteHandler/Viewport/Center"]
[node name="meshinstance" type="MeshInstance2D" parent="square/noteHandler/viewport/Center"]
unique_name_in_owner = true
material = SubResource( 2 )
[node name="notelines" type="MeshInstance2D" parent="Square/NoteHandler/Viewport/Center"]
[node name="notelines" type="MeshInstance2D" parent="square/noteHandler/viewport/Center"]
unique_name_in_owner = true
material = SubResource( 3 )
[node name="lbl_combo" type="Label" parent="Square/NoteHandler"]
[node name="lbl_combo" type="Label" parent="square/noteHandler"]
visible = false
anchor_left = 0.5
anchor_top = 0.5
@ -169,45 +170,38 @@ custom_fonts/font = SubResource( 4 )
text = "0"
align = 1
valign = 1
script = SubResource( 7 )
script = SubResource( 5 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Painter" type="Control" parent="Square"]
material = SubResource( 5 )
[node name="painter" type="Control" parent="square"]
unique_name_in_owner = true
material = SubResource( 6 )
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 6 )
script = ExtResource( 3 )
[node name="Menu" parent="square" instance=ExtResource( 10 )]
ease_curve = SubResource( 7 )
[node name="Bezel" type="Control" parent="square"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 5 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Menu" parent="Square" instance=ExtResource( 15 )]
[node name="InputHandler" type="Control" parent="square"]
anchor_right = 1.0
anchor_bottom = 1.0
NoteHandlerPath = NodePath("../NoteHandler")
ReceptorsPath = NodePath("../Receptors")
ease_curve = SubResource( 6 )
[node name="Bezel" type="Control" parent="Square"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 13 )
script = ExtResource( 4 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="InputHandler" type="Control" parent="Square"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[connection signal="item_rect_changed" from="Square" to="Square/NoteHandler/Viewport" method="_on_Square_item_rect_changed"]
[connection signal="combo_changed" from="Square/NoteHandler" to="Square/NoteHandler/lbl_combo" method="_on_NoteHandler_combo_changed"]
[connection signal="finished_song" from="Square/NoteHandler" to="Square/NoteHandler/lbl_combo" method="_on_NoteHandler_finished_song"]
[connection signal="column_pressed" from="Square/InputHandler" to="Square/NoteHandler" method="_on_InputHandler_column_pressed"]
[connection signal="column_released" from="Square/InputHandler" to="Square/NoteHandler" method="_on_InputHandler_column_released"]
[connection signal="combo_changed" from="square/noteHandler" to="square/noteHandler/lbl_combo" method="_on_NoteHandler_combo_changed"]
[connection signal="finished_song" from="square/noteHandler" to="square/noteHandler/lbl_combo" method="_on_NoteHandler_finished_song"]
[connection signal="column_pressed" from="square/InputHandler" to="square/noteHandler" method="_on_InputHandler_column_pressed"]
[connection signal="column_released" from="square/InputHandler" to="square/noteHandler" method="_on_InputHandler_column_released"]

58
scenes/SettingsMenu.gd Normal file
View File

@ -0,0 +1,58 @@
extends Control
const DirectorySettingsEntry = preload('res://scenes/DirectorySettingsEntry.tscn')
onready var container_folders = $'%container_folders'
signal exit_mode
var paths = []
var path_entries := []
# Called when the node enters the scene tree for the first time.
func _ready():
paths = Array(Settings.get_library_paths())
generate_entries()
func generate_entries():
paths.append("")
for entry in path_entries:
container_folders.remove_child(entry)
path_entries = []
for path in paths:
var entry = DirectorySettingsEntry.instance()
entry.connect('path_updated', self, '_entry_updated', [len(path_entries)])
path_entries.append(entry)
entry.directory = path
container_folders.add_child(entry)
path_entries[0].removable = false
path_entries[0].readonly = true
func get_paths_from_entries() -> Array:
var paths := []
for entry in path_entries:
var dir = entry.directory
if dir:
paths.append(entry.directory)
return paths
func _save_settings():
var paths = get_paths_from_entries()
var user_path: String = paths[0]
var extra_paths: Array = paths.slice(1, -1)
Settings.set_additional_library_paths(extra_paths)
func _entry_updated(new_path: String, index: int):
paths = get_paths_from_entries()
if (index == len(path_entries)-1):
if new_path:
generate_entries()
elif not new_path:
generate_entries()
# Called every frame. 'delta' is the elapsed time since the previous frame.
#func _process(delta):
# pass
func _on_btn_back_pressed():
_save_settings()
emit_signal('exit_mode')

32
scenes/SettingsMenu.tscn Normal file
View File

@ -0,0 +1,32 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scenes/SettingsMenu.gd" type="Script" id=2]
[node name="SettingsMenu" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 2 )
[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Label" type="Label" parent="VBoxContainer"]
margin_right = 1080.0
margin_bottom = 14.0
text = "Resource Folders"
align = 1
[node name="container_folders" type="VBoxContainer" parent="VBoxContainer"]
unique_name_in_owner = true
margin_top = 18.0
margin_right = 1080.0
margin_bottom = 18.0
[node name="btn_back" type="Button" parent="VBoxContainer"]
margin_top = 22.0
margin_right = 1080.0
margin_bottom = 42.0
text = "Back"
[connection signal="pressed" from="VBoxContainer/btn_back" to="." method="_on_btn_back_pressed"]

12
scenes/StepGame.gd Normal file
View File

@ -0,0 +1,12 @@
extends Control
signal exit_mode
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
#func _process(delta):
# pass

182
scenes/StepGame.tscn Normal file
View File

@ -0,0 +1,182 @@
[gd_scene load_steps=17 format=2]
[ext_resource path="res://scripts/NoteViewport.gd" type="Script" id=1]
[ext_resource path="res://assets/text-4k.png" type="Texture" id=2]
[ext_resource path="res://scripts/NotePainter.gd" type="Script" id=3]
[ext_resource path="res://scripts/InputHandler.gd" type="Script" id=4]
[ext_resource path="res://scenes/StepMenu.tscn" type="PackedScene" id=5]
[ext_resource path="res://scripts/NoteHandler.gd" type="Script" id=6]
[ext_resource path="res://assets/fonts/Sniglet-Regular.ttf" type="DynamicFontData" id=7]
[ext_resource path="res://scenes/StepGame.gd" type="Script" id=8]
[ext_resource path="res://scripts/ScreenFilter.gd" type="Script" id=9]
[ext_resource path="res://shaders/notemesh.shader" type="Shader" id=12]
[ext_resource path="res://shaders/notelines.shader" type="Shader" id=13]
[sub_resource type="ShaderMaterial" id=2]
shader = ExtResource( 12 )
shader_param/bps = null
shader_param/star_color = null
shader_param/held_color = null
shader_param/screen_size = null
[sub_resource type="ShaderMaterial" id=3]
shader = ExtResource( 13 )
shader_param/line_color = Color( 0.8, 0.8, 1, 0.8 )
shader_param/line_color_double = Color( 1, 1, 0.6, 0.9 )
shader_param/dot_color = Color( 1, 1, 1, 0.8 )
shader_param/bps = 1.0
shader_param/line_thickness = 0.012
shader_param/line_thickness_min = 0.0
shader_param/dot_thickness = 0.033
shader_param/dot_fullbright_thickness = 0.013
shader_param/max_angle = 1.0708
shader_param/max_dist = 1.25
shader_param/array_postmul = Vector3( 1, 1, 1 )
shader_param/array_sidelen = 16
shader_param/array_size = 256
[sub_resource type="DynamicFont" id=4]
size = 48
outline_size = 2
outline_color = Color( 0, 0, 0, 1 )
font_data = ExtResource( 7 )
[sub_resource type="GDScript" id=5]
script/source = "extends Label
const colors = [Color.gray, Color.lightgray, Color.aqua, Color.gold]
func _on_NoteHandler_combo_changed(value) -> void:
text = str(value)
visible = (value > 0)
add_color_override('font_color', colors[int(min(3, value/50))])
func _on_NoteHandler_finished_song(song_key, score_data) -> void:
visible = false
"
[sub_resource type="CanvasItemMaterial" id=6]
blend_mode = 4
[node name="StepGame" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 8 )
[node name="video" type="TextureRect" parent="." groups=["VideoTexRects"]]
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
rect_pivot_offset = Vector2( 540, 540 )
mouse_filter = 2
expand = true
stretch_mode = 6
__meta__ = {
"_edit_use_anchors_": false
}
[node name="ScreenFilter" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
color = Color( 0, 0, 0, 1 )
script = ExtResource( 9 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="receptors" type="Control" parent="."]
unique_name_in_owner = true
anchor_right = 1.0
anchor_bottom = 1.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TextureRect" type="TextureRect" parent="receptors"]
margin_right = 1080.0
margin_bottom = 1080.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="noteHandler" type="Control" parent="."]
unique_name_in_owner = true
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 6 )
[node name="viewport" type="Viewport" parent="noteHandler"]
unique_name_in_owner = true
size = Vector2( 1080, 1080 )
transparent_bg = true
render_target_v_flip = true
script = ExtResource( 1 )
[node name="Center" type="Node2D" parent="noteHandler/viewport"]
position = Vector2( 540, 540 )
[node name="slideTrailHandler" type="Node2D" parent="noteHandler/viewport/Center"]
unique_name_in_owner = true
[node name="judgeText" type="MeshInstance2D" parent="noteHandler/viewport/Center"]
unique_name_in_owner = true
texture = ExtResource( 2 )
[node name="meshinstance" type="MeshInstance2D" parent="noteHandler/viewport/Center"]
unique_name_in_owner = true
material = SubResource( 2 )
[node name="notelines" type="MeshInstance2D" parent="noteHandler/viewport/Center"]
unique_name_in_owner = true
material = SubResource( 3 )
[node name="lbl_combo" type="Label" parent="noteHandler"]
visible = false
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -20.0
margin_top = -7.0
margin_right = 20.0
margin_bottom = 7.0
grow_horizontal = 2
grow_vertical = 2
custom_fonts/font = SubResource( 4 )
text = "0"
align = 1
valign = 1
script = SubResource( 5 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="painter" type="Control" parent="noteHandler"]
unique_name_in_owner = true
material = SubResource( 6 )
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 3 )
[node name="StepMenu" parent="." instance=ExtResource( 5 )]
[node name="InputHandler" type="Control" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 4 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="square" type="Control" parent="."]
unique_name_in_owner = true
anchor_right = 1.0
anchor_bottom = 1.0
[connection signal="combo_changed" from="noteHandler" to="noteHandler/lbl_combo" method="_on_NoteHandler_combo_changed"]
[connection signal="finished_song" from="noteHandler" to="noteHandler/lbl_combo" method="_on_NoteHandler_finished_song"]
[connection signal="column_pressed" from="InputHandler" to="noteHandler" method="_on_InputHandler_column_pressed"]
[connection signal="column_released" from="InputHandler" to="noteHandler" method="_on_InputHandler_column_released"]

38
scenes/StepMenu.tscn Normal file
View File

@ -0,0 +1,38 @@
[gd_scene load_steps=7 format=2]
[ext_resource path="res://scripts/ScoreText.gd" type="Script" id=1]
[ext_resource path="res://scripts/StepMenu.gd" type="Script" id=2]
[ext_resource path="res://shaders/menu.tres" type="Material" id=3]
[ext_resource path="res://shaders/scoretext.tres" type="Material" id=4]
[ext_resource path="res://scenes/JoypadMonitor6Btn.tscn" type="PackedScene" id=5]
[sub_resource type="Curve" id=1]
min_value = -1.0
_data = [ Vector2( -1, -1 ), 0.0, 0.0, 0, 0, Vector2( 0, 0 ), 2.0, 2.0, 1, 1, Vector2( 1, 1 ), 0.0, 0.0, 0, 0 ]
[node name="StepMenu" type="Control"]
material = ExtResource( 3 )
anchor_right = 1.0
anchor_bottom = 1.0
rect_clip_content = true
script = ExtResource( 2 )
ease_curve = SubResource( 1 )
[node name="ScoreText" type="Node2D" parent="."]
material = ExtResource( 4 )
script = ExtResource( 1 )
[node name="PVMusic" type="AudioStreamPlayer" parent="."]
bus = "Preview"
[node name="JoypadMonitor" parent="." instance=ExtResource( 5 )]
[node name="JoypadMonitor2" parent="." instance=ExtResource( 5 )]
margin_left = 200.0
joypad_index = 1
[node name="JoypadMonitor3" parent="." instance=ExtResource( 5 )]
margin_left = 400.0
margin_right = 200.0
margin_bottom = 238.0
joypad_index = 1

85
scripts/BezierTest.gd Normal file
View File

@ -0,0 +1,85 @@
tool
extends Control
# This is for playing around with weird control point curves in-editor.
# Some slide types are a bit difficult to reason about.
const ORBIT_INNER_RADIUS = sin(deg2rad(22.5)) # ~0.38
const ORBIT_KAPPA = (sqrt(2)-1) * 4.0 / 3.0 # This is the length of control points along a tangent to approximate a circle (multiply by desired radius)
export(float, -360, 360, 7.5) var angle_entry = -22.5 # degrees
export(float, -360, 360, 7.5) var angle_exit = 112.5 # degrees
var inner_radius = ORBIT_INNER_RADIUS
export(float, 5.0, 120.0) var max_arc_angle := 45.0
export(bool) var show_points := true
export(bool) var show_handles := true
export(bool) var flip_direction := false
var sideorbit_center := Vector2.ZERO
var curve2d := Curve2D.new()
func _draw() -> void:
# draw_circle(rect_size * 0.5, inner_radius * GameTheme.receptor_ring_radius, Color.darkgreen)
var points = curve2d.get_baked_points()
if len(points) < 2:
return
draw_set_transform(rect_size * 0.5, 0, Vector2.ONE * GameTheme.receptor_ring_radius)
draw_circle(sideorbit_center, inner_radius, Color.darkgreen)
draw_multiline(points, Color.white)
var l = curve2d.get_point_count()
var c_points = []
var handles_in = []
var handles_out = []
for i in l:
var p = curve2d.get_point_position(i)
c_points.append(p)
handles_in.append(curve2d.get_point_in(i) + p)
handles_out.append(curve2d.get_point_out(i) + p)
if show_handles:
for i in l:
draw_circle(handles_in[i], 0.01, Color.burlywood)
draw_circle(handles_out[i], 0.01, Color.cadetblue)
if show_points:
for i in l:
draw_circle(c_points[i], 0.012, Color.blanchedalmond)
func _process(delta: float) -> void:
curve2d.clear_points()
curve2d.bake_interval = 0.01
var rad_in = deg2rad(angle_entry)
var rad_out = deg2rad(angle_exit)
curve2d.add_point(polar2cartesian(1.0, rad_in))
Note.curve2d_make_sideorbit(curve2d, rad_in, rad_out, flip_direction)
curve2d.add_point(polar2cartesian(1.0, rad_out))
sideorbit_center = polar2cartesian(inner_radius, rad_in-PI*0.5*(-1 if flip_direction else 1))
update()
#func curve2d_make_sideorbit(curve2d: Curve2D, rad_in: float, rad_out: float, ccw: bool, rad_max_arc:=PI*0.25, kappa:=ORBIT_KAPPA, inner_radius:=ORBIT_INNER_RADIUS):
# var d_sign := -1 if ccw else 1
#
# sideorbit_center = polar2cartesian(inner_radius, rad_in-PI*0.5*d_sign)
#
# var rad_orbit_in := rad_in + PI*0.5*d_sign
# var orbcenter_to_out := polar2cartesian(1.0, rad_out) - sideorbit_center
# var rad_orbit_out := orbcenter_to_out.angle() - acos(inner_radius/orbcenter_to_out.length())*d_sign
# var pos_orbit_out := sideorbit_center + polar2cartesian(inner_radius, rad_orbit_out)
#
# var rad_2 = rad_in + PI
# var rad_2t = rad_2+PI*0.5*d_sign
# var rad_3 = rad_out-PI*3/8*d_sign
# var rad_3t = rad_3-PI*0.5*d_sign
#
# var a_diff = wrapf((rad_orbit_out-rad_orbit_in)*d_sign, 0.0001, TAU+0.0001)
# var n = ceil(a_diff/rad_max_arc)
# var ad = a_diff/n
# var k = kappa*inner_radius*(2*ad/PI) # Not geometrically correct scaling but reasonable for now
#
## curve2d.add_point(polar2cartesian(1.0, rad_in))
# curve2d.add_point(Vector2.ZERO, Vector2.ZERO, polar2cartesian(k, rad_2))
# for i in range(1, n):
# var ang = rad_orbit_in + i*ad*d_sign
# curve2d.add_point(sideorbit_center + polar2cartesian(inner_radius, ang), polar2cartesian(k, ang-PI/2*d_sign), polar2cartesian(k, ang+PI/2*d_sign))
#
# curve2d.add_point(pos_orbit_out, polar2cartesian(k, rad_orbit_out-PI*0.5*d_sign))
## curve2d.add_point(polar2cartesian(1.0, rad_out))

99
scripts/FileHelpers.gd Normal file
View File

@ -0,0 +1,99 @@
# Static functions mostly for FileLoader to make use of because of deficiancies in load()
extends Node
const ERROR_CODES := [
'OK', 'FAILED', 'ERR_UNAVAILABLE', 'ERR_UNCONFIGURED', 'ERR_UNAUTHORIZED', 'ERR_PARAMETER_RANGE_ERROR',
'ERR_OUT_OF_MEMORY', 'ERR_FILE_NOT_FOUND', 'ERR_FILE_BAD_DRIVE', 'ERR_FILE_BAD_PATH','ERR_FILE_NO_PERMISSION',
'ERR_FILE_ALREADY_IN_USE', 'ERR_FILE_CANT_OPEN', 'ERR_FILE_CANT_WRITE', 'ERR_FILE_CANT_READ', 'ERR_FILE_UNRECOGNIZED',
'ERR_FILE_CORRUPT', 'ERR_FILE_MISSING_DEPENDENCIES', 'ERR_FILE_EOF', 'ERR_CANT_OPEN', 'ERR_CANT_CREATE', 'ERR_QUERY_FAILED',
'ERR_ALREADY_IN_USE', 'ERR_LOCKED', 'ERR_TIMEOUT', 'ERR_CANT_CONNECT', 'ERR_CANT_RESOLVE', 'ERR_CONNECTION_ERROR',
'ERR_CANT_ACQUIRE_RESOURCE', 'ERR_CANT_FORK', 'ERR_INVALID_DATA', 'ERR_INVALID_PARAMETER', 'ERR_ALREADY_EXISTS',
'ERR_DOES_NOT_EXIST', 'ERR_DATABASE_CANT_READ', 'ERR_DATABASE_CANT_WRITE', 'ERR_COMPILATION_FAILED', 'ERR_METHOD_NOT_FOUND',
'ERR_LINK_FAILED', 'ERR_SCRIPT_FAILED', 'ERR_CYCLIC_LINK', 'ERR_INVALID_DECLARATION', 'ERR_DUPLICATE_SYMBOL',
'ERR_PARSE_ERROR', 'ERR_BUSY', 'ERR_SKIP', 'ERR_HELP', 'ERR_BUG'
]
static func load_image(filename: String) -> ImageTexture:
var tex := ImageTexture.new()
var img := Image.new()
img.load(filename)
tex.create_from_image(img)
return tex
static func load_ogg(filename: String) -> AudioStreamOGGVorbis:
# Loads the ogg file with that exact filename
var audiostream = AudioStreamOGGVorbis.new()
var oggfile = File.new()
oggfile.open(filename, File.READ)
audiostream.set_data(oggfile.get_buffer(oggfile.get_len()))
oggfile.close()
return audiostream
static func load_video(filename: String):
return load(filename)
# This may need reenabling for some platforms:
#var videostream = VideoStreamGDNative.new()
#videostream.set_file(filename)
#return videostream
static func directory_list(directory: String, hidden: bool, sort:=true) -> Dictionary:
# Sadly there's no filelist sugar so we make our own
var output = {folders=[], files=[], err=OK}
var dir = Directory.new()
output.err = dir.open(directory)
if output.err != OK:
print_debug('Failed to open directory: ' + directory + '(Error code '+output.err+')')
return output
output.err = dir.list_dir_begin(true, !hidden)
if output.err != OK:
print_debug('Failed to begin listing directory: ' + directory + '(Error code '+output.err+')')
return output
var item = dir.get_next()
while (item != ''):
if dir.current_is_dir():
output['folders'].append(item)
else:
output['files'].append(item)
item = dir.get_next()
dir.list_dir_end()
if sort:
output.folders.sort()
output.files.sort()
# Maybe convert the Arrays to PoolStringArrays?
return output
static func find_by_extensions(array, extensions=null) -> Dictionary:
# Both args can be Array or PoolStringArray
# If extensions omitted, do all extensions
var output = {}
if extensions:
for ext in extensions:
output[ext] = []
for filename in array:
for ext in extensions:
if filename.ends_with(ext):
output[ext].append(filename)
else:
for filename in array:
var ext = filename.rsplit('.', false, 1)[1]
if ext in output:
output[ext].append(filename)
else:
output[ext] = [filename]
return output
static func init_directory(directory: String):
var dir = Directory.new()
var err = dir.make_dir_recursive(directory)
if err == ERR_ALREADY_EXISTS: # API changed?
err = OK
if err != OK:
print('An error occurred while trying to create the directory: ', directory, err, ERROR_CODES[err])
return err

23
scripts/MainMenu.gd Normal file
View File

@ -0,0 +1,23 @@
extends Control
signal start_touchgame
signal start_stepgame
signal open_settings
func update_libraries_text() -> void:
$lbl_settingspath.text = 'Data directories:\n' + '\n'.join(Settings.get_library_paths())
func _ready() -> void:
update_libraries_text()
Settings.connect('config_loaded', self, 'update_libraries_text')
func quit() -> void:
get_tree().quit()
func _on_btn_touch_pressed() -> void:
emit_signal('start_touchgame')
func _on_btn_step_pressed() -> void:
emit_signal('start_stepgame')
func _on_btn_settings_pressed():
emit_signal('open_settings')

View File

@ -1,4 +1,5 @@
extends Control
const RadialMeshTools := preload('res://scripts/RadialMeshTools.gd')
var screen_height := 1080
@ -8,21 +9,16 @@ signal combo_changed(value)
var running := false
var song_key = ''
onready var MusicPlayer := SoundPlayer.music_player
onready var VideoPlayer := Video.video
onready var musicPlayer := SoundPlayer.music_player
onready var videoPlayer := Video.video
onready var Painter = $'../Painter'
onready var SlideTrailHandler = $'Viewport/Center/SlideTrailHandler'
onready var JudgeText = $'Viewport/Center/JudgeText'
onready var notelines = $'Viewport/Center/notelines'
onready var meshinstance = $'Viewport/Center/meshinstance'
onready var painter = $'%painter'
onready var slideTrailHandler = $'%slideTrailHandler'
onready var judgeText = $'%judgeText'
onready var notelines = $'%notelines'
onready var meshinstance = $'%meshinstance'
onready var lbl_combo = $lbl_combo
const SQRT2 := sqrt(2)
const DEG45 := deg2rad(45.0)
const DEG90 := deg2rad(90.0)
const DEG135 := deg2rad(135.0)
var time_zero_msec: int = 0
var time: float = 0.0
var t: float = 0.0 # Game time
@ -42,37 +38,7 @@ var slide_trail_mesh_instances := {}
var noteline_array_image := Image.new()
# Text UVs
var text_UV_arrays := []
func make_text_UV(row: int, column: int) -> PoolVector2Array:
return PoolVector2Array([Vector2(column/4.0, row/8.0), Vector2((column+1)/4.0, row/8.0), Vector2(column/4.0, (row+1)/8.0), Vector2((column+1)/4.0, (row+1)/8.0)])
func make_text_UVs():
for row in 8:
for column in 4:
text_UV_arrays.append(make_text_UV(row, column))
enum TextStyle {STRAIGHT=0, ARC=1, ARC_EARLY=2, ARC_LATE=3}
enum TextWord {NICE=0, OK=4, NG=8, PERFECT=12, GREAT=16, GOOD=20, ALMOST=24, MISS=28}
const TextJudgement := {
0: TextWord.PERFECT + TextStyle.ARC,
1: TextWord.GREAT + TextStyle.ARC_LATE,
-1: TextWord.GREAT + TextStyle.ARC_EARLY,
2: TextWord.GOOD + TextStyle.ARC_LATE,
-2: TextWord.GOOD + TextStyle.ARC_EARLY,
3: TextWord.ALMOST + TextStyle.ARC_LATE,
-3: TextWord.ALMOST + TextStyle.ARC_EARLY,
'MISS': TextWord.MISS + TextStyle.ARC
}
const TextJudgementStraight := {
0: TextWord.PERFECT + TextStyle.STRAIGHT,
1: TextWord.GREAT + TextStyle.STRAIGHT,
-1: TextWord.GREAT + TextStyle.STRAIGHT,
2: TextWord.GOOD + TextStyle.STRAIGHT,
-2: TextWord.GOOD + TextStyle.STRAIGHT,
3: TextWord.ALMOST + TextStyle.STRAIGHT,
-3: TextWord.ALMOST + TextStyle.STRAIGHT,
'MISS': TextWord.MISS + TextStyle.STRAIGHT
}
#----------------------------------------------------------------------------------------------------------------------------------------------
var current_combo := 0
func increment_combo():
current_combo += 1
@ -87,131 +53,16 @@ func initialise_scores():
scores = {}
for type in [Note.NOTE_TAP, Note.NOTE_HOLD, Note.NOTE_STAR]:
scores[type] = {}
for key in TextJudgement:
for key in RadialMeshTools.TextJudgement:
scores[type][key] = 0
# Release types
for type in [Note.NOTE_HOLD, Note.NOTE_SLIDE]:
scores[Note.RELEASE_SCORE_TYPES[type]] = {}
for key in TextJudgement:
for key in RadialMeshTools.TextJudgement:
scores[Note.RELEASE_SCORE_TYPES[type]][key] = 0
scores['max_combo'] = 0
current_combo = 0
func make_text_mesh(mesh: ArrayMesh, text_id: int, pos: Vector2, angle: float, alpha:=1.0, scale:=1.0):
var r := GameTheme.judge_text_size2 * scale
var vertex_array := PoolVector2Array([
pos+polar2cartesian(r, angle+GameTheme.JUDGE_TEXT_ANG2), # TODO: fix this UV/vertex order mess
pos+polar2cartesian(r, angle+GameTheme.JUDGE_TEXT_ANG1),
pos+polar2cartesian(r, angle+GameTheme.JUDGE_TEXT_ANG4),
pos+polar2cartesian(r, angle+GameTheme.JUDGE_TEXT_ANG3)
])
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertex_array
arrays[Mesh.ARRAY_TEX_UV] = text_UV_arrays[text_id]
arrays[Mesh.ARRAY_COLOR] = GameTheme.color_array_text(alpha)
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
func make_judgement_text(mesh: ArrayMesh, text_id: int, col: int, progress:=0.0):
make_text_mesh(mesh, text_id,
GameTheme.RADIAL_UNIT_VECTORS[col] * GameTheme.receptor_ring_radius * lerp(0.85, 0.85*0.75, progress),
GameTheme.RADIAL_COL_ANGLES[col]-PI/2.0, lerp(1.0, 0.0, progress), lerp(1.0, 0.75, progress)
)
# ----------------------------------------------------------------------------------------------------------------------------------------------------
# Helper functions to generate meshes from vertex arrays
func make_tap_mesh(mesh: ArrayMesh, note_center: Vector2, scale:=1.0, color_array:=GameTheme.COLOR_ARRAY_TAP):
var dim = GameTheme.sprite_size2 * scale
var vertex_array = PoolVector2Array([note_center + Vector2(-dim, -dim), note_center + Vector2(dim, -dim), note_center + Vector2(-dim, dim), note_center + Vector2(dim, dim)])
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertex_array
arrays[Mesh.ARRAY_TEX_UV] = GameTheme.UV_ARRAY_TAP
arrays[Mesh.ARRAY_COLOR] = color_array
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
func make_hold_mesh(mesh: ArrayMesh, note_center: Vector2, note_center_rel: Vector2, scale:=1.0, angle:=0.0, color_array = GameTheme.COLOR_ARRAY_HOLD):
var dim = GameTheme.sprite_size2 * scale
var dim2 = dim * SQRT2
var a1 = angle - DEG45
var a2 = angle + DEG45
var a3 = angle - DEG90
var a4 = angle + DEG90
var a5 = angle - DEG135
var a6 = angle + DEG135
var vertex_array = PoolVector2Array([
note_center + polar2cartesian(dim2, a1), note_center + polar2cartesian(dim2, a2),
note_center + polar2cartesian(dim, a3), note_center + polar2cartesian(dim, a4),
note_center_rel + polar2cartesian(dim, a3), note_center_rel + polar2cartesian(dim, a4),
note_center_rel + polar2cartesian(dim2, a5), note_center_rel + polar2cartesian(dim2, a6)
])
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertex_array
arrays[Mesh.ARRAY_TEX_UV] = GameTheme.UV_ARRAY_HOLD
arrays[Mesh.ARRAY_COLOR] = color_array
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
func make_star_mesh(mesh: ArrayMesh, note_center: Vector2, scale:=1.0, angle:=0.0, color_array:=GameTheme.COLOR_ARRAY_STAR):
var dim = GameTheme.sprite_size2 * scale * SQRT2
var a1 = angle - DEG45
var a2 = angle + DEG45
var a3 = angle - DEG135
var a4 = angle + DEG135
var vertex_array = PoolVector2Array([
note_center + polar2cartesian(dim, a1), note_center + polar2cartesian(dim, a2),
note_center + polar2cartesian(dim, a3), note_center + polar2cartesian(dim, a4)
])
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertex_array
arrays[Mesh.ARRAY_TEX_UV] = GameTheme.UV_ARRAY_STAR
arrays[Mesh.ARRAY_COLOR] = color_array
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
#func make_arrow_mesh(mesh: ArrayMesh, vertex_array, color_array = GameTheme.COLOR_ARRAY_TAP):
# var arrays = []
# arrays.resize(Mesh.ARRAY_MAX)
# arrays[Mesh.ARRAY_VERTEX] = vertex_array
# arrays[Mesh.ARRAY_TEX_UV] = UV_ARRAY_ARROW
# arrays[Mesh.ARRAY_COLOR] = color_array
# mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
const slide_arrows_per_unit_length := 10
func make_slide_trail_mesh(note) -> ArrayMesh:
# Generates a mesh centered around origin. Make sure the MeshInstance2D that draws this is centered on the screen.
var mesh = ArrayMesh.new()
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
var vertices := PoolVector2Array()
var uvs := PoolVector2Array()
var colors := PoolColorArray()
var size := GameTheme.sprite_size2 * sqrt(2)
var color := GameTheme.COLOR_DOUBLE_SLIDE if note.double_hit else GameTheme.COLOR_SLIDE
match note.get_points():
[var positions, var angles]:
var trail_length : int = len(positions)
vertices.resize(3*trail_length)
uvs.resize(3*trail_length)
colors.resize(3*trail_length)
for i in trail_length:
var u = GameTheme.UV_ARRAY_SLIDE_ARROW if i%3 else GameTheme.UV_ARRAY_SLIDE_ARROW2
for j in 3:
uvs[i*3+j] = u[j]
colors[i*3+j] = Color(color.r, color.g, color.b, (1.0+float(i))/float(trail_length))
var angle : float = angles[i]
var offset : Vector2 = positions[i] * GameTheme.receptor_ring_radius
vertices[i*3] = offset
vertices[i*3+1] = offset + polar2cartesian(size, angle+PI*0.75)
vertices[i*3+2] = offset + polar2cartesian(size, angle-PI*0.75)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_COLOR] = colors
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh
#----------------------------------------------------------------------------------------------------------------------------------------------
func make_judgement_column(judgement, column: int):
@ -313,97 +164,109 @@ func check_hold_release(col):
do_hold_release(note) # Separate function since there's no need to 'consume' releases
#----------------------------------------------------------------------------------------------------------------------------------------------
const arr_div := Vector3(2.0, float(Rules.COLS), TAU)
func draw_note(mesh, note, position, t):
var output = null
var scale := 1.0
if position < GameTheme.INNER_NOTE_CIRCLE_RATIO:
scale *= position/GameTheme.INNER_NOTE_CIRCLE_RATIO
position = GameTheme.INNER_NOTE_CIRCLE_RATIO
var note_center = (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position * GameTheme.receptor_ring_radius)
var color: PoolColorArray
match note.type:
Note.NOTE_TAP:
color = GameTheme.color_array_tap(clamp((note.time_death-t)/Note.DEATH_DELAY, 0.0, 1.0), note.double_hit)
RadialMeshTools.make_tap_mesh(mesh, note_center, scale, color)
Note.NOTE_STAR:
color = GameTheme.color_array_star(clamp((note.time_death-t)/Note.DEATH_DELAY, 0.0, 1.0), note.double_hit)
var angle = fmod(t/note.duration, 1.0)*TAU
RadialMeshTools.make_star_mesh(mesh, note_center, scale, angle, color)
Note.NOTE_HOLD:
if note.is_held:
position = (t+GameTheme.note_forecast_beats-note.time_release)/GameTheme.note_forecast_beats
color = GameTheme.COLOR_ARRAY_HOLD_HELD
note_center = GameTheme.RADIAL_UNIT_VECTORS[note.column] * GameTheme.receptor_ring_radius * max(position, 1.0)
elif position > 1.0:
color = GameTheme.COLOR_ARRAY_DOUBLE_MISS_8 if note.double_hit else GameTheme.COLOR_ARRAY_HOLD_MISS
if note.time_released != INF:
position = (t+GameTheme.note_forecast_beats-note.time_released)/GameTheme.note_forecast_beats
note_center = GameTheme.RADIAL_UNIT_VECTORS[note.column] * GameTheme.receptor_ring_radius * position
else:
color = GameTheme.COLOR_ARRAY_DOUBLE_8 if note.double_hit else GameTheme.COLOR_ARRAY_HOLD
var position_rel : float = (t+GameTheme.note_forecast_beats-note.time_release)/GameTheme.note_forecast_beats
if position_rel > 0:
var note_rel_center := (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position_rel * GameTheme.receptor_ring_radius)
output = [position_rel, note.column]
if position_rel < GameTheme.INNER_NOTE_CIRCLE_RATIO:
position_rel = GameTheme.INNER_NOTE_CIRCLE_RATIO
var note_center_rel = (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position_rel * GameTheme.receptor_ring_radius)
RadialMeshTools.make_hold_mesh(mesh, note_center, note_center_rel, scale, GameTheme.RADIAL_COL_ANGLES[note.column], color)
Note.NOTE_SLIDE:
var trail_alpha := 1.0
if position < GameTheme.INNER_NOTE_CIRCLE_RATIO:
trail_alpha = 0.0
elif position < 1.0:
trail_alpha = min(1.0, (position-GameTheme.INNER_NOTE_CIRCLE_RATIO)/(1-GameTheme.INNER_NOTE_CIRCLE_RATIO*2))
else:
var trail_progress : float = clamp((t - note.time_hit - GameTheme.SLIDE_DELAY)/(note.duration - GameTheme.SLIDE_DELAY), 0.0, 1.0)
var star_pos : Vector2 = note.get_position(trail_progress) * GameTheme.receptor_ring_radius
var star_angle : float = note.get_angle(trail_progress)
RadialMeshTools.make_star_mesh(mesh, star_pos, 1.33, star_angle)
if note.progress != INF:
slide_trail_mesh_instances[note.slide_id].material.set_shader_param('trail_progress', note.progress)
if t > note.time_release:
trail_alpha = max(1 - (t - note.time_release)/Note.DEATH_DELAY, 0.0)
slide_trail_mesh_instances[note.slide_id].material.set_shader_param('base_alpha', trail_alpha*GameTheme.slide_trail_alpha) # TODO: somehow factor this out?
return output
#----------------------------------------------------------------------------------------------------------------------------------------------
const arr_div := Vector3(2.0, float(Rules.COLS), TAU) # Scaling factors to avoid LDR processing clipping our values
func _draw():
var mesh := ArrayMesh.new()
var noteline_data : Image = noteline_array_image.get_rect(Rect2(0, 0, 16, 16))
noteline_data.lock()
var i := 0
var j := 0
var hit_spots := []
var release_spots := []
for note in active_notes:
var position : float = (t+GameTheme.note_forecast_beats-note.time_hit)/GameTheme.note_forecast_beats
var scale := 1.0
if note.hittable:
noteline_data.set_pixel(
i%16, i/16, Color(
position/arr_div.x,
float(note.column)/arr_div.y,
GameTheme.RADIAL_COL_ANGLES[note.column]/arr_div.z
)
)
i += 1
hit_spots.append([position, note.column])
var release = draw_note(mesh, note, position, t)
if release:
release_spots.append(release)
if position < GameTheme.INNER_NOTE_CIRCLE_RATIO:
scale *= position/GameTheme.INNER_NOTE_CIRCLE_RATIO
position = GameTheme.INNER_NOTE_CIRCLE_RATIO
var note_center = (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position * GameTheme.receptor_ring_radius)
var color: PoolColorArray
match note.type:
Note.NOTE_TAP:
color = GameTheme.color_array_tap(clamp((note.time_death-t)/Note.DEATH_DELAY, 0.0, 1.0), note.double_hit)
make_tap_mesh(mesh, note_center, scale, color)
Note.NOTE_STAR:
color = GameTheme.color_array_star(clamp((note.time_death-t)/Note.DEATH_DELAY, 0.0, 1.0), note.double_hit)
var angle = fmod(t/note.duration, 1.0)*TAU
make_star_mesh(mesh, note_center, scale, angle, color)
Note.NOTE_HOLD:
if note.is_held:
position = (t+GameTheme.note_forecast_beats-note.time_release)/GameTheme.note_forecast_beats
color = GameTheme.COLOR_ARRAY_HOLD_HELD
note_center = GameTheme.RADIAL_UNIT_VECTORS[note.column] * GameTheme.receptor_ring_radius * max(position, 1.0)
elif position > 1.0:
color = GameTheme.COLOR_ARRAY_DOUBLE_MISS_8 if note.double_hit else GameTheme.COLOR_ARRAY_HOLD_MISS
if note.time_released != INF:
position = (t+GameTheme.note_forecast_beats-note.time_released)/GameTheme.note_forecast_beats
note_center = GameTheme.RADIAL_UNIT_VECTORS[note.column] * GameTheme.receptor_ring_radius * position
else:
color = GameTheme.COLOR_ARRAY_DOUBLE_8 if note.double_hit else GameTheme.COLOR_ARRAY_HOLD
var position_rel : float = (t+GameTheme.note_forecast_beats-note.time_release)/GameTheme.note_forecast_beats
if position_rel > 0:
var note_rel_center := (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position_rel * GameTheme.receptor_ring_radius)
noteline_data.set_pixel(
j%16, 15, Color(
position_rel/arr_div.x,
float(note.column)/arr_div.y,
GameTheme.RADIAL_COL_ANGLES[note.column]/arr_div.z
)
)
j += 1
if position_rel < GameTheme.INNER_NOTE_CIRCLE_RATIO:
position_rel = GameTheme.INNER_NOTE_CIRCLE_RATIO
var note_center_rel = (GameTheme.RADIAL_UNIT_VECTORS[note.column] * position_rel * GameTheme.receptor_ring_radius)
make_hold_mesh(mesh, note_center, note_center_rel, scale, GameTheme.RADIAL_COL_ANGLES[note.column], color)
Note.NOTE_SLIDE:
var trail_alpha := 1.0
if position < GameTheme.INNER_NOTE_CIRCLE_RATIO:
trail_alpha = 0.0
elif position < 1.0:
trail_alpha = min(1.0, (position-GameTheme.INNER_NOTE_CIRCLE_RATIO)/(1-GameTheme.INNER_NOTE_CIRCLE_RATIO*2))
else:
var trail_progress : float = clamp((t - note.time_hit - GameTheme.SLIDE_DELAY)/(note.duration - GameTheme.SLIDE_DELAY), 0.0, 1.0)
var star_pos : Vector2 = note.get_position(trail_progress) * GameTheme.receptor_ring_radius
var star_angle : float = note.get_angle(trail_progress)
make_star_mesh(mesh, star_pos, 1.33, star_angle)
if note.progress != INF:
slide_trail_mesh_instances[note.slide_id].material.set_shader_param('trail_progress', note.progress)
if t > note.time_release:
trail_alpha = max(1 - (t - note.time_release)/Note.DEATH_DELAY, 0.0)
slide_trail_mesh_instances[note.slide_id].material.set_shader_param('base_alpha', trail_alpha*GameTheme.slide_trail_alpha)
# In the absense of shader uniform arrays, we have to send our data via a texture
var noteline_data : Image = noteline_array_image.get_rect(Rect2(0, 0, 16, 16))
noteline_data.lock()
var i := 0
for spot in hit_spots:
noteline_data.set_pixel(i%16, i/16, Color(spot[0]/arr_div.x, float(spot[1])/arr_div.y, GameTheme.RADIAL_COL_ANGLES[spot[1]]/arr_div.z))
i += 1
i = 0
for spot in release_spots:
noteline_data.set_pixel(i%16, 15, Color(spot[0]/arr_div.x, float(spot[1])/arr_div.y, GameTheme.RADIAL_COL_ANGLES[spot[1]]/arr_div.z))
i += 1
noteline_data.unlock()
var noteline_data_tex := ImageTexture.new()
noteline_data_tex.create_from_image(noteline_data, 0)
notelines.set_texture(noteline_data_tex)
# The mesh that we've added all our note vertices and UVs to
meshinstance.set_mesh(mesh)
# Another mesh for judgement texts
var textmesh := ArrayMesh.new()
for text in active_judgement_texts:
make_judgement_text(textmesh, TextJudgement[text.judgement], text.col, (t-text.time)/GameTheme.judge_text_duration)
JudgeText.set_mesh(textmesh)
RadialMeshTools.make_judgement_text(textmesh, RadialMeshTools.TextJudgement[text.judgement], text.col, (t-text.time)/GameTheme.judge_text_duration)
judgeText.set_mesh(textmesh)
func _input(event):
@ -435,15 +298,16 @@ func _input(event):
func _init():
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
GameTheme.init_radial_values()
make_text_UVs()
initialise_scores()
func set_time(seconds: float):
var msecs = OS.get_ticks_msec()
time_zero_msec = msecs - (seconds * 1000)
time = seconds
t = game_time(time)
func make_noteline_mesh(vertices := 32) -> ArrayMesh:
assert(vertices > 3)
var rec_scale1 = (float(screen_height)/float(GameTheme.receptor_ring_radius))*0.5
@ -468,6 +332,7 @@ func make_noteline_mesh(vertices := 32) -> ArrayMesh:
mesh_playfield.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_FAN, arrays)
return mesh_playfield
# Called when the node enters the scene tree for the first time.
func _ready():
notelines.set_mesh(make_noteline_mesh())
@ -484,6 +349,7 @@ func _ready():
meshinstance.material.set_shader_param('screen_size', get_viewport().get_size())
meshinstance.set_texture(GameTheme.tex_notes)
func load_track(song_key: String, difficulty_key: String):
self.song_key = song_key
set_time(-3.0)
@ -499,42 +365,50 @@ func load_track(song_key: String, difficulty_key: String):
sync_offset_audio = data.audio_offsets[0]
sync_offset_video = data.video_offsets[0]
var videostream = FileLoader.load_video('songs/' + data.filepath.rstrip('/') + '/' + data.video_filelist[0])
MusicPlayer.set_stream(FileLoader.load_ogg('songs/' + data.filepath.rstrip('/') + '/' + data.audio_filelist[0]))
VideoPlayer.set_stream(videostream)
musicPlayer.set_stream(FileLoader.load_ogg('songs/' + data.filepath.rstrip('/') + '/' + data.audio_filelist[0]))
videoPlayer.set_stream(videostream)
# all_notes = FileLoader.Test.stress_pattern()
Note.process_note_list(all_notes, false)
for note in all_notes:
if note.type == Note.NOTE_SLIDE:
slide_trail_meshes[note.slide_id] = make_slide_trail_mesh(note)
slide_trail_meshes[note.slide_id] = RadialMeshTools.make_slide_trail_mesh(note)
initialise_scores() # Remove old score
func stop():
MusicPlayer.stop()
VideoPlayer.stop()
musicPlayer.stop()
videoPlayer.stop()
# running = false
next_note_to_load = 10000000 # Hacky but whatever
func intro_click():
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, GameTheme.snd_count_in)
func get_realtime_precise() -> float:
# Usually we only update the gametime once per process loop, but for input callbacks it's good to have msec precision
return (OS.get_ticks_msec() - time_zero_msec)/1000.0
func game_time(realtime: float) -> float:
return realtime * bpm / 60.0
func real_time(gametime: float) -> float:
return gametime * 60.0 / bpm
func video_start_time() -> float:
return -sync_offset_video
func audio_start_time() -> float:
return -sync_offset_audio
# Called every frame. 'delta' is the elapsed time since the previous frame.
var timers_set := false
func _process(delta):
@ -563,14 +437,14 @@ func _process(delta):
timer.connect('timeout', timer, 'queue_free')
var vt_delta := time - video_start_time()
if (0.0 <= vt_delta) and (vt_delta < 3.0) and not VideoPlayer.is_playing():
VideoPlayer.play()
VideoPlayer.set_stream_position(vt_delta)
if (0.0 <= vt_delta) and (vt_delta < 3.0) and not videoPlayer.is_playing():
videoPlayer.play()
videoPlayer.set_stream_position(vt_delta)
var at_delta := time - audio_start_time()
if (0.0 <= at_delta) and (at_delta < 3.0) and not MusicPlayer.is_playing():
# MusicPlayer.play()
# MusicPlayer.seek(at_delta)
MusicPlayer.play(at_delta)
if (0.0 <= at_delta) and (at_delta < 3.0) and not musicPlayer.is_playing():
# musicPlayer.play()
# musicPlayer.seek(at_delta)
musicPlayer.play(at_delta)
# Clean out expired notes
var miss_time: float = Rules.JUDGEMENT_TIMES_POST[-1] * bpm/60.0
@ -583,7 +457,7 @@ func _process(delta):
scores[Note.RELEASE_SCORE_TYPES[Note.NOTE_HOLD]][3] += 1
make_judgement_column(3, note.column)
Note.NOTE_SLIDE:
SlideTrailHandler.remove_child(slide_trail_mesh_instances[note.slide_id])
slideTrailHandler.remove_child(slide_trail_mesh_instances[note.slide_id])
slide_trail_mesh_instances.erase(note.slide_id)
var idx = active_slide_trails.find(note)
if idx > -1:
@ -630,14 +504,14 @@ func _process(delta):
meshi.material.set_shader_param('trail_progress', 0.0)
meshi.set_texture(GameTheme.tex_slide_arrow)
slide_trail_mesh_instances[note.slide_id] = meshi
SlideTrailHandler.add_child(meshi)
slideTrailHandler.add_child(meshi)
next_note_to_load += 1
if (
next_note_to_load >= len(all_notes)
and not VideoPlayer.is_playing()
and not MusicPlayer.is_playing()
and not videoPlayer.is_playing()
and not musicPlayer.is_playing()
and active_notes.empty()
and active_judgement_texts.empty()
and slide_trail_mesh_instances.empty()
@ -650,7 +524,7 @@ func _process(delta):
# Redraw
meshinstance.material.set_shader_param('screen_size', get_viewport().get_size())
update()
Painter.update()
painter.update()
func _on_InputHandler_column_pressed(column) -> void:

View File

@ -1,7 +1,7 @@
extends Control
onready var Viewport := get_node(@'../NoteHandler/Viewport')
onready var viewport := $'%viewport'
func _draw():
draw_texture_rect(Viewport.get_texture(), Rect2(Vector2.ZERO, rect_size), false)
# texture = Viewport.get_texture()
draw_texture_rect(viewport.get_texture(), Rect2(Vector2.ZERO, rect_size), false)
# texture = viewport.get_texture()

View File

@ -1,5 +1,7 @@
extends Viewport
onready var square := $'%square'
onready var root := $'/root'
onready var base_height = 1080.0
var container_size := Vector2(1080, 1080)
@ -10,17 +12,15 @@ func set_render_scale(scale: Vector2):
$Center.position = size * 0.5
$Center.scale = size/base_height
func update_size() -> void:
var winscale = min(root.size.x, root.size.y)/base_height
container_size = square.rect_size * winscale
set_render_scale(scale)
# Called when the node enters the scene tree for the first time.
func _ready():
Settings.connect('subsampling_changed', self, 'set_render_scale')
set_render_scale(Settings.subsampling)
_on_Square_item_rect_changed()
onready var Square := $'../../'
onready var Root := $'/root'
onready var Main := $'/root/main'
func _on_Square_item_rect_changed() -> void:
var winscale = min(Root.size.x, Root.size.y)/base_height
container_size = Square.rect_size * winscale
set_render_scale(scale)
square.connect('item_rect_changed', self, 'update_size')
root.connect('size_changed', self, 'update_size')
scale = Settings.subsampling
update_size()

203
scripts/RadialMeshTools.gd Normal file
View File

@ -0,0 +1,203 @@
extends Node
const SQRT2 := sqrt(2)
const DEG45 := deg2rad(45.0)
const DEG90 := deg2rad(90.0)
const DEG135 := deg2rad(135.0)
# ----------------------------------------------------------------------------------------------------------------------------------------------------
# Helper functions to generate meshes from vertex arrays
static func make_tap_mesh(mesh: ArrayMesh, note_center: Vector2, scale:=1.0, color_array:=GameTheme.COLOR_ARRAY_TAP):
var dim = GameTheme.sprite_size2 * scale
var vertex_array = PoolVector2Array([note_center + Vector2(-dim, -dim), note_center + Vector2(dim, -dim), note_center + Vector2(-dim, dim), note_center + Vector2(dim, dim)])
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertex_array
arrays[Mesh.ARRAY_TEX_UV] = GameTheme.UV_ARRAY_TAP
arrays[Mesh.ARRAY_COLOR] = color_array
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
static func make_hold_mesh(mesh: ArrayMesh, note_center: Vector2, note_center_rel: Vector2, scale:=1.0, angle:=0.0, color_array = GameTheme.COLOR_ARRAY_HOLD):
var dim = GameTheme.sprite_size2 * scale
var dim2 = dim * SQRT2
var a1 = angle - DEG45
var a2 = angle + DEG45
var a3 = angle - DEG90
var a4 = angle + DEG90
var a5 = angle - DEG135
var a6 = angle + DEG135
var vertex_array = PoolVector2Array([
note_center + polar2cartesian(dim2, a1), note_center + polar2cartesian(dim2, a2),
note_center + polar2cartesian(dim, a3), note_center + polar2cartesian(dim, a4),
note_center_rel + polar2cartesian(dim, a3), note_center_rel + polar2cartesian(dim, a4),
note_center_rel + polar2cartesian(dim2, a5), note_center_rel + polar2cartesian(dim2, a6)
])
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertex_array
arrays[Mesh.ARRAY_TEX_UV] = GameTheme.UV_ARRAY_HOLD
arrays[Mesh.ARRAY_COLOR] = color_array
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
static func make_star_mesh(mesh: ArrayMesh, note_center: Vector2, scale:=1.0, angle:=0.0, color_array:=GameTheme.COLOR_ARRAY_STAR):
var dim = GameTheme.sprite_size2 * scale * SQRT2
var a1 = angle - DEG45
var a2 = angle + DEG45
var a3 = angle - DEG135
var a4 = angle + DEG135
var vertex_array = PoolVector2Array([
note_center + polar2cartesian(dim, a1), note_center + polar2cartesian(dim, a2),
note_center + polar2cartesian(dim, a3), note_center + polar2cartesian(dim, a4)
])
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertex_array
arrays[Mesh.ARRAY_TEX_UV] = GameTheme.UV_ARRAY_STAR
arrays[Mesh.ARRAY_COLOR] = color_array
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
#func make_arrow_mesh(mesh: ArrayMesh, vertex_array, color_array = GameTheme.COLOR_ARRAY_TAP):
# var arrays = []
# arrays.resize(Mesh.ARRAY_MAX)
# arrays[Mesh.ARRAY_VERTEX] = vertex_array
# arrays[Mesh.ARRAY_TEX_UV] = UV_ARRAY_ARROW
# arrays[Mesh.ARRAY_COLOR] = color_array
# mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
const slide_arrows_per_unit_length := 10
static func make_slide_trail_mesh(note) -> ArrayMesh:
# Generates a mesh centered around origin. Make sure the MeshInstance2D that draws this is centered on the screen.
var mesh = ArrayMesh.new()
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
var vertices := PoolVector2Array()
var uvs := PoolVector2Array()
var colors := PoolColorArray()
var size := GameTheme.sprite_size2 * sqrt(2)
var color := GameTheme.COLOR_DOUBLE_SLIDE if note.double_hit else GameTheme.COLOR_SLIDE
match note.get_points():
[var positions, var angles]:
var trail_length : int = len(positions)
vertices.resize(3*trail_length)
uvs.resize(3*trail_length)
colors.resize(3*trail_length)
for i in trail_length:
var idx = (trail_length-i-1)*3 # We want the earliest ones to be drawn last so that loops/sharp bends will have the first pass on top
var u = GameTheme.UV_ARRAY_SLIDE_ARROW if i%3 else GameTheme.UV_ARRAY_SLIDE_ARROW2
for j in 3:
uvs[idx+j] = u[j]
colors[idx+j] = Color(color.r, color.g, color.b, (1.0+float(i))/float(trail_length))
var angle : float = angles[i]
var offset : Vector2 = positions[i] * GameTheme.receptor_ring_radius
vertices[idx] = offset
vertices[idx+1] = offset + polar2cartesian(size, angle+PI*0.75)
vertices[idx+2] = offset + polar2cartesian(size, angle-PI*0.75)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_TEX_UV] = uvs
arrays[Mesh.ARRAY_COLOR] = colors
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return mesh
#----------------------------------------------------------------------------------------------------------------------------------------------
# Text UVs
# Old dynamic code:
#var text_UV_arrays := []
#func make_text_UV(row: int, column: int) -> PoolVector2Array:
# return PoolVector2Array([Vector2(column/4.0, row/8.0), Vector2((column+1)/4.0, row/8.0), Vector2(column/4.0, (row+1)/8.0), Vector2((column+1)/4.0, (row+1)/8.0)])
#func make_text_UVs():
# for row in 8:
# for column in 4:
# text_UV_arrays.append(make_text_UV(row, column))
# This is replaced by a quick hacky python codegen:
#>>> def make_text_UV(row, column):
#... return f'PoolVector2Array([Vector2({column}/4.0, {row}/8.0), Vector2({column+1}/4.0, {row}/8.0), Vector2({column}/4.0, {row+1}/8.0), Vector2({column+1}/4.0, {row+1}/8.0)])'
#>>> for row in range(8):
#... for col in range(4):
#... print(make_text_UV(row, col) + ',')
const text_UV_arrays := [
PoolVector2Array([Vector2(0/4.0, 0/8.0), Vector2(1/4.0, 0/8.0), Vector2(0/4.0, 1/8.0), Vector2(1/4.0, 1/8.0)]),
PoolVector2Array([Vector2(1/4.0, 0/8.0), Vector2(2/4.0, 0/8.0), Vector2(1/4.0, 1/8.0), Vector2(2/4.0, 1/8.0)]),
PoolVector2Array([Vector2(2/4.0, 0/8.0), Vector2(3/4.0, 0/8.0), Vector2(2/4.0, 1/8.0), Vector2(3/4.0, 1/8.0)]),
PoolVector2Array([Vector2(3/4.0, 0/8.0), Vector2(4/4.0, 0/8.0), Vector2(3/4.0, 1/8.0), Vector2(4/4.0, 1/8.0)]),
PoolVector2Array([Vector2(0/4.0, 1/8.0), Vector2(1/4.0, 1/8.0), Vector2(0/4.0, 2/8.0), Vector2(1/4.0, 2/8.0)]),
PoolVector2Array([Vector2(1/4.0, 1/8.0), Vector2(2/4.0, 1/8.0), Vector2(1/4.0, 2/8.0), Vector2(2/4.0, 2/8.0)]),
PoolVector2Array([Vector2(2/4.0, 1/8.0), Vector2(3/4.0, 1/8.0), Vector2(2/4.0, 2/8.0), Vector2(3/4.0, 2/8.0)]),
PoolVector2Array([Vector2(3/4.0, 1/8.0), Vector2(4/4.0, 1/8.0), Vector2(3/4.0, 2/8.0), Vector2(4/4.0, 2/8.0)]),
PoolVector2Array([Vector2(0/4.0, 2/8.0), Vector2(1/4.0, 2/8.0), Vector2(0/4.0, 3/8.0), Vector2(1/4.0, 3/8.0)]),
PoolVector2Array([Vector2(1/4.0, 2/8.0), Vector2(2/4.0, 2/8.0), Vector2(1/4.0, 3/8.0), Vector2(2/4.0, 3/8.0)]),
PoolVector2Array([Vector2(2/4.0, 2/8.0), Vector2(3/4.0, 2/8.0), Vector2(2/4.0, 3/8.0), Vector2(3/4.0, 3/8.0)]),
PoolVector2Array([Vector2(3/4.0, 2/8.0), Vector2(4/4.0, 2/8.0), Vector2(3/4.0, 3/8.0), Vector2(4/4.0, 3/8.0)]),
PoolVector2Array([Vector2(0/4.0, 3/8.0), Vector2(1/4.0, 3/8.0), Vector2(0/4.0, 4/8.0), Vector2(1/4.0, 4/8.0)]),
PoolVector2Array([Vector2(1/4.0, 3/8.0), Vector2(2/4.0, 3/8.0), Vector2(1/4.0, 4/8.0), Vector2(2/4.0, 4/8.0)]),
PoolVector2Array([Vector2(2/4.0, 3/8.0), Vector2(3/4.0, 3/8.0), Vector2(2/4.0, 4/8.0), Vector2(3/4.0, 4/8.0)]),
PoolVector2Array([Vector2(3/4.0, 3/8.0), Vector2(4/4.0, 3/8.0), Vector2(3/4.0, 4/8.0), Vector2(4/4.0, 4/8.0)]),
PoolVector2Array([Vector2(0/4.0, 4/8.0), Vector2(1/4.0, 4/8.0), Vector2(0/4.0, 5/8.0), Vector2(1/4.0, 5/8.0)]),
PoolVector2Array([Vector2(1/4.0, 4/8.0), Vector2(2/4.0, 4/8.0), Vector2(1/4.0, 5/8.0), Vector2(2/4.0, 5/8.0)]),
PoolVector2Array([Vector2(2/4.0, 4/8.0), Vector2(3/4.0, 4/8.0), Vector2(2/4.0, 5/8.0), Vector2(3/4.0, 5/8.0)]),
PoolVector2Array([Vector2(3/4.0, 4/8.0), Vector2(4/4.0, 4/8.0), Vector2(3/4.0, 5/8.0), Vector2(4/4.0, 5/8.0)]),
PoolVector2Array([Vector2(0/4.0, 5/8.0), Vector2(1/4.0, 5/8.0), Vector2(0/4.0, 6/8.0), Vector2(1/4.0, 6/8.0)]),
PoolVector2Array([Vector2(1/4.0, 5/8.0), Vector2(2/4.0, 5/8.0), Vector2(1/4.0, 6/8.0), Vector2(2/4.0, 6/8.0)]),
PoolVector2Array([Vector2(2/4.0, 5/8.0), Vector2(3/4.0, 5/8.0), Vector2(2/4.0, 6/8.0), Vector2(3/4.0, 6/8.0)]),
PoolVector2Array([Vector2(3/4.0, 5/8.0), Vector2(4/4.0, 5/8.0), Vector2(3/4.0, 6/8.0), Vector2(4/4.0, 6/8.0)]),
PoolVector2Array([Vector2(0/4.0, 6/8.0), Vector2(1/4.0, 6/8.0), Vector2(0/4.0, 7/8.0), Vector2(1/4.0, 7/8.0)]),
PoolVector2Array([Vector2(1/4.0, 6/8.0), Vector2(2/4.0, 6/8.0), Vector2(1/4.0, 7/8.0), Vector2(2/4.0, 7/8.0)]),
PoolVector2Array([Vector2(2/4.0, 6/8.0), Vector2(3/4.0, 6/8.0), Vector2(2/4.0, 7/8.0), Vector2(3/4.0, 7/8.0)]),
PoolVector2Array([Vector2(3/4.0, 6/8.0), Vector2(4/4.0, 6/8.0), Vector2(3/4.0, 7/8.0), Vector2(4/4.0, 7/8.0)]),
PoolVector2Array([Vector2(0/4.0, 7/8.0), Vector2(1/4.0, 7/8.0), Vector2(0/4.0, 8/8.0), Vector2(1/4.0, 8/8.0)]),
PoolVector2Array([Vector2(1/4.0, 7/8.0), Vector2(2/4.0, 7/8.0), Vector2(1/4.0, 8/8.0), Vector2(2/4.0, 8/8.0)]),
PoolVector2Array([Vector2(2/4.0, 7/8.0), Vector2(3/4.0, 7/8.0), Vector2(2/4.0, 8/8.0), Vector2(3/4.0, 8/8.0)]),
PoolVector2Array([Vector2(3/4.0, 7/8.0), Vector2(4/4.0, 7/8.0), Vector2(3/4.0, 8/8.0), Vector2(4/4.0, 8/8.0)])
]
enum TextStyle {STRAIGHT=0, ARC=1, ARC_EARLY=2, ARC_LATE=3}
enum TextWord {NICE=0, OK=4, NG=8, PERFECT=12, GREAT=16, GOOD=20, ALMOST=24, MISS=28}
const TextJudgement := {
0: TextWord.PERFECT + TextStyle.ARC,
1: TextWord.GREAT + TextStyle.ARC_LATE,
-1: TextWord.GREAT + TextStyle.ARC_EARLY,
2: TextWord.GOOD + TextStyle.ARC_LATE,
-2: TextWord.GOOD + TextStyle.ARC_EARLY,
3: TextWord.ALMOST + TextStyle.ARC_LATE,
-3: TextWord.ALMOST + TextStyle.ARC_EARLY,
'MISS': TextWord.MISS + TextStyle.ARC
}
const TextJudgementStraight := {
0: TextWord.PERFECT + TextStyle.STRAIGHT,
1: TextWord.GREAT + TextStyle.STRAIGHT,
-1: TextWord.GREAT + TextStyle.STRAIGHT,
2: TextWord.GOOD + TextStyle.STRAIGHT,
-2: TextWord.GOOD + TextStyle.STRAIGHT,
3: TextWord.ALMOST + TextStyle.STRAIGHT,
-3: TextWord.ALMOST + TextStyle.STRAIGHT,
'MISS': TextWord.MISS + TextStyle.STRAIGHT
}
static func make_text_mesh(mesh: ArrayMesh, text_id: int, pos: Vector2, angle: float, alpha:=1.0, scale:=1.0):
var r := GameTheme.judge_text_size2 * scale
var vertex_array := PoolVector2Array([
pos+polar2cartesian(r, angle+GameTheme.JUDGE_TEXT_ANG2), # TODO: fix this UV/vertex order mess
pos+polar2cartesian(r, angle+GameTheme.JUDGE_TEXT_ANG1),
pos+polar2cartesian(r, angle+GameTheme.JUDGE_TEXT_ANG4),
pos+polar2cartesian(r, angle+GameTheme.JUDGE_TEXT_ANG3)
])
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertex_array
arrays[Mesh.ARRAY_TEX_UV] = text_UV_arrays[text_id]
arrays[Mesh.ARRAY_COLOR] = GameTheme.color_array_text(alpha)
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLE_STRIP, arrays)
static func make_judgement_text(mesh: ArrayMesh, text_id: int, col: int, progress:=0.0):
make_text_mesh(mesh, text_id,
GameTheme.RADIAL_UNIT_VECTORS[col] * GameTheme.receptor_ring_radius * lerp(0.85, 0.85*0.75, progress),
GameTheme.RADIAL_COL_ANGLES[col]-PI/2.0, lerp(1.0, 0.0, progress), lerp(1.0, 0.75, progress)
)

View File

@ -1,7 +1,7 @@
extends Node2D
var TitleFont := preload("res://assets/MenuTitleFont.tres").duplicate()
var ScoreFont := preload("res://assets/MenuScoreFont.tres").duplicate()
var titleFont := preload("res://assets/MenuTitleFont.tres").duplicate()
var scoreFont := preload("res://assets/MenuScoreFont.tres").duplicate()
var score = ""
var score_sub = ""
@ -9,16 +9,16 @@ var score_sub = ""
var f_scale := 1.0 setget set_f_scale
func set_f_scale(value: float) -> void:
f_scale = value
TitleFont.size = int(round(32*f_scale))
TitleFont.outline_size = int(max(round(2*f_scale), 1))
ScoreFont.size = int(round(96*f_scale))
ScoreFont.outline_size = int(max(round(2*f_scale), 1))
titleFont.size = int(round(32*f_scale))
titleFont.outline_size = int(max(round(2*f_scale), 1))
scoreFont.size = int(round(96*f_scale))
scoreFont.outline_size = int(max(round(2*f_scale), 1))
func draw_string_centered(font, position, string, color := Color.white):
draw_string(font, Vector2(position.x - font.get_string_size(string).x/2.0, position.y + font.get_ascent()), string, color)
func _draw():
if score:
draw_string_centered(ScoreFont, Vector2(0, 0)*f_scale, score)
draw_string_centered(scoreFont, Vector2(0, 0)*f_scale, score)
if score_sub:
draw_string_centered(TitleFont, Vector2(0, 128)*f_scale, score_sub)
draw_string_centered(titleFont, Vector2(0, 128)*f_scale, score_sub)

623
scripts/StepMenu.gd Normal file
View File

@ -0,0 +1,623 @@
#tool
extends Control
onready var noteHandler := $'%noteHandler'
onready var receptors := $'%receptors'
onready var scoreText := $ScoreText
onready var PVMusic := SoundPlayer.music_player_pv
var f_scale := 1.0 setget set_f_scale
func set_f_scale(value: float) -> void:
f_scale = value
titleFont.size = int(round(32*f_scale))
titleFont.outline_size = int(max(round(2*f_scale), 1))
genreFont.size = int(round(48*f_scale))
genreFont.outline_size = int(max(round(2*f_scale), 1))
diffNumFont.size = int(round(36*f_scale))
diffNumFont.outline_size = int(max(round(1*f_scale), 1))
scoreText.set_f_scale(f_scale)
func update_scale() -> void:
self.f_scale = min(rect_size.x/1080, rect_size.y/1080)
var genres = {}
enum ChartDifficulty {EASY, BASIC, ADV, EXPERT, MASTER}
enum MenuMode {SONG_SELECT, CHART_SELECT, OPTIONS, GAMEPLAY, SCORE_SCREEN}
var menu_mode = MenuMode.SONG_SELECT
var menu_mode_prev = MenuMode.SONG_SELECT
var menu_mode_prev_fade_timer := 0.0
var menu_mode_prev_fade_timer_duration := 0.25
var currently_playing := false
var selected_genre: int = 0
var selected_genre_vis: int = 0
var selected_genre_delta: float = 0.0 # For floaty display scrolling
var target_song_idx: float = 0.0 setget set_target_song_idx
var target_song_delta: float = 0.0 # For floaty display scrolling
var selected_song_idx: int setget , get_song_idx
var selected_song_key: String setget , get_song_key
var selected_difficulty = ChartDifficulty.ADV
func set_target_song_idx(index):
target_song_delta -= index - target_song_idx
target_song_idx = index
func get_song_idx() -> int:
return int(round(self.target_song_idx + target_song_delta))
func get_song_key() -> String:
var songslist = genres[genres.keys()[selected_genre]]
return songslist[int(round(self.target_song_idx)) % len(songslist)]
var scorescreen_song_key := ''
var scorescreen_score_data := {}
var scorescreen_datetime := {}
var scorescreen_saved := false
var touch_rects = []
var titleFont: DynamicFont = preload('res://assets/MenuTitleFont.tres').duplicate()
var genreFont: DynamicFont = preload('res://assets/MenuGenreFont.tres').duplicate()
var diffNumFont: DynamicFont = preload('res://assets/MenuDiffNumberFont.tres').duplicate()
var scoreFont: DynamicFont = preload('res://assets/MenuScoreFont.tres').duplicate()
var snd_interact := preload('res://assets/softclap.wav')
var snd_error := preload('res://assets/miss.wav')
export var ease_curve: Curve
class lerp_array extends Resource:
var array
func _init(array: Array):
self.array = array
func value(index: float):
# Only >= 0 for now, but should be fine since it's an arraylike anyway
var i := min(int(floor(index)), len(array)-2) # Somewhat hacky - if we pass len(array)-1 as index, it will return lerp(a[-2], a[-1], 1) == a[-1]
var f := min(index - i, 1.0)
return lerp(array[i], array[i+1], f)
func len():
return len(array)
onready var joypad_monitors = [$JoypadMonitor, $JoypadMonitor2, $JoypadMonitor3]
func update_joypad_labels():
var num_joypads = len(Input.get_connected_joypads())
var num_monitors = len(joypad_monitors)
for i in num_monitors:
joypad_monitors[i].visible = true
if num_monitors > num_joypads:
for i in range(num_joypads, num_monitors):
joypad_monitors[i].visible = false
for i in num_joypads:
joypad_monitors[i].set_name(Input.get_joy_name(i))
func get_rect_center(rect: Rect2) -> Vector2:
return rect.position + rect.size*0.5
func scan_library():
var results = FileLoader.scan_library()
genres = results.genres
func save_score() -> int:
var data = {'score_data': scorescreen_score_data, 'song_key': scorescreen_song_key}
var dt = scorescreen_datetime
var filename = 'scores/%04d%02d%02dT%02d%02d%02d.json'%[dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second]
match FileLoader.save_json(filename, data):
OK:
scorescreen_saved = true
return OK
var err:
print_debug('Error saving score file %s'%filename)
return err
func load_score(filename: String):
var result = FileLoader.load_json('scores/%s'%filename)
if not (result is Dictionary):
print('An error occurred while trying to access the chosen score file: ', result)
return result
var data = {}
for key in result.score_data:
var value = {}
for k2 in result.score_data[key]:
if k2 != 'MISS':
k2 = int(k2) # Could use something more robust later
value[k2] = result.score_data[key][k2]
data[int(key)] = value
scorescreen_score_data = data
scorescreen_song_key = result.song_key
scorescreen_saved = true
set_menu_mode(MenuMode.SCORE_SCREEN)
func load_preview():
var tmp = self.selected_song_key
var data = Library.all_songs[tmp]
PVMusic.stop()
PVMusic.set_stream(FileLoader.load_ogg('songs/' + data.filepath.rstrip('/') + '/' + data.audio_filelist[0]))
PVMusic.play(16*60.0/data.BPM)
func _ready():
scan_library()
connect('item_rect_changed', self, 'update_scale')
noteHandler.connect('finished_song', self, 'finished_song')
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
target_song_delta -= ease_curve.interpolate(clamp(target_song_delta, -2, 2)*0.5) * 10 * delta
if abs(target_song_delta) < 0.02: # Snap
target_song_delta = 0.0
var g_diff = selected_genre - (selected_genre_vis + selected_genre_delta)
selected_genre_delta += ease_curve.interpolate(clamp(g_diff, -1, 1)) * 10 * delta
if selected_genre_delta > 0.5:
selected_genre_delta -= 1.0
selected_genre_vis += 1
elif selected_genre_delta < -0.5:
selected_genre_delta += 1.0
selected_genre_vis -= 1
if abs(g_diff) < 0.02: # Snap
selected_genre_delta = 0.0
selected_genre_vis = selected_genre
menu_mode_prev_fade_timer = max(0.0, menu_mode_prev_fade_timer - delta)
update()
if (menu_mode == MenuMode.GAMEPLAY) and (menu_mode_prev_fade_timer <= 0.0) and not noteHandler.running:
noteHandler.load_track(self.selected_song_key, Library.Song.default_difficulty_keys[selected_difficulty])
noteHandler.running = true
func draw_string_centered(font: Font, position: Vector2, string: String, color := GameTheme.COLOR_MENU_TEXT, vcenter := false) -> Vector2:
# Draws horizontally centered from the baseline. Can vcenter via ascent but not perfectly reliable.
# Returns size of the string.
var ss := font.get_string_size(string)
var v := -(font.get_descent() - font.get_height()*0.475) if vcenter else 0.0 # This VCentering is a little fudgey but works for our current fonts
draw_string(font, Vector2(position.x - ss.x*0.5, position.y + v).round(), string, color)
return ss
func draw_string_ralign(font: Font, position: Vector2, string: String, color := GameTheme.COLOR_MENU_TEXT, vcenter := false) -> Vector2:
# Draws from the bottom-right. Can vcenter via ascent but not perfectly reliable.
# Returns size of the string.
var ss := font.get_string_size(string)
var ascent := font.get_ascent() if vcenter else 0.0
draw_string(font, Vector2(position.x - ss.x, position.y + ascent*0.5).round(), string, color)
return ss
func draw_songtile(song_key, position, size, title_text:=false, difficulty=selected_difficulty, outline_px:=3.0, disabled:=false):
# Draws from top left-corner. Returns Rect2 of the image (not the outline).
# Draw difficulty-colored outline
if typeof(difficulty) == TYPE_STRING:
difficulty = Library.Song.difficulty_key_ids.get(difficulty, 0)
outline_px *= f_scale
var song_diffs = Library.all_songs[song_key]['chart_difficulties']
if not (Library.Song.default_difficulty_keys[difficulty] in song_diffs):
difficulty = Library.Song.difficulty_key_ids.get(song_diffs.keys()[-1], 0)
var diff_color := GameTheme.COLOR_DIFFICULTY[difficulty*2]
var rect := Rect2(position.x, position.y, size, size)
draw_rect(Rect2(position.x - outline_px, position.y - outline_px, size + outline_px*2, size + outline_px*2), diff_color)
draw_texture_rect(Library.get_song_tile_texture(song_key), rect, false, Color.white if not disabled else Color(0.5, 0.2, 0.1))
# Draw track difficulty rating
draw_string_ralign(diffNumFont, position+Vector2(size-2*f_scale, size-5*f_scale), song_diffs.get(Library.Song.default_difficulty_keys[difficulty], '0'), diff_color)
if disabled:
draw_string_centered(diffNumFont, position+Vector2(size/2, size/2), 'No Chart!', diff_color, true)
if title_text:
draw_string_centered(titleFont, position+Vector2(size/2.0, size+40*f_scale), str(Library.all_songs[song_key].title), diff_color.lightened(0.33))
return rect
func diff_f2str(difficulty: float): # Convert .5 to +
return str(int(floor(difficulty))) + ('+' if fmod(difficulty, 1.0)>0.4 else '')
var sel_scales := lerp_array.new([1.0, 0.8, 0.64, 0.5, 0.4, 0.4, 0.4, 0.4, 0.4])
var bg_scales := lerp_array.new([0.64, 0.64, 0.64, 0.5, 0.4, 0.4, 0.4, 0.4, 0.4])
func _draw_song_select(center: Vector2) -> Array:
var size = 200 * f_scale
var spacer_x = 12 * f_scale
var spacer_y = 64 * f_scale
var title_spacer_y = 48 * f_scale
var gy: float = center.y - 500 * f_scale - size*selected_genre_delta
var touchrects := []
if len(genres) <= 0:
draw_string_centered(genreFont, Vector2(center.x, center.y-440*f_scale), 'No Songs in Library!', Color.aqua)
draw_string_centered(diffNumFont, Vector2(center.x, center.y-390*f_scale), Settings.user_data_dir, Color.lightgreen)
return touchrects
var ssid = self.selected_song_idx
var s_delta = target_song_delta-round(target_song_delta)
for gi in [-2, -1, 0, 1, 2]:
var g = (selected_genre_vis + gi) % len(genres)
var selected: bool = (gi == 0)
var scales = sel_scales if selected else bg_scales
var subsize = size * scales.value(abs(s_delta))
var gx = center.x - (subsize + spacer_x) * s_delta
var songslist = Library.genre_songs[g].keys()
var genre_str = '%s (%d songs)'%[genres.keys()[g], len(songslist)]
draw_string_centered(genreFont, Vector2(center.x, gy), genre_str, Color.lightblue)
var s = len(songslist)
var key = songslist[self.selected_song_idx % s]
var y = gy + 16*f_scale
var x = -subsize/2.0
var r = draw_songtile(key, Vector2(gx+x, y), subsize, selected)
touchrects.append({rect=r, song_idx=self.selected_song_idx, genre_idx=g})
var subsize_p = subsize
var subsize_n = subsize
var x_p = x
var x_n = x
for i in range(1, scales.len()):
x_p += subsize_p + spacer_x
x_n += subsize_n + spacer_x
subsize_p = size * scales.value(abs(i-s_delta))
subsize_n = size * scales.value(abs(-i-s_delta))
r = draw_songtile(songslist[(ssid+i) % s], Vector2(gx+x_p, y), subsize_p)
touchrects.append({rect=r, song_idx=ssid+i, genre_idx=g})
r = draw_songtile(songslist[(ssid-i) % s], Vector2(gx-x_n - subsize_n, y), subsize_n)
touchrects.append({rect=r, song_idx=ssid-i, genre_idx=g})
gy += size*scales.value(0) + spacer_y + (title_spacer_y if selected else 0)
var b = 1080 * f_scale
var v1 = -590 * f_scale
var v2 = -230 * f_scale
var v4 = -v2
var v3 = -v1
var ps = PoolVector2Array([center+Vector2(-b, v1), center+Vector2(b, v1), center+Vector2(b, v2), center+Vector2(-b, v2)])
var ps2 = PoolVector2Array([center+Vector2(-b, v3), center+Vector2(b, v3), center+Vector2(b, v4), center+Vector2(-b, v4)])
var cs = PoolColorArray([Color(0,0,0.1,1.25), Color(0,0,0.1,1.25), Color(0,0,0,0), Color(0,0,0,0)])
draw_polygon(ps, cs)
draw_polygon(ps2, cs)
draw_string_centered(genreFont, Vector2(center.x, center.y-440*f_scale), 'Select Song', Color.aqua)
draw_string_centered(diffNumFont, Vector2(center.x, center.y-390*f_scale), 'Tap to scroll, tap focused to select', Color.lightgreen)
return touchrects
func _draw_chart_select(center: Vector2) -> Array:
# Select difficulty for chosen song
var charts: Dictionary = Library.get_song_charts(self.selected_song_key)
var song_data = Library.all_songs[self.selected_song_key]
var diffs = song_data.chart_difficulties
var n = len(diffs)
var spacer_x = max(14, 70/n) * f_scale
var size = min(192, (1000-spacer_x*(n-1))/n) * f_scale
var rect_back = Rect2(center + Vector2(-300.0, 390.0)*f_scale, Vector2(600.0, 140.0)*f_scale)
draw_rect(rect_back, Color.red)
draw_string_centered(titleFont, get_rect_center(rect_back), 'Back to song selection', Color.white, true)
draw_string_centered(genreFont, center+Vector2(0, -360*f_scale), 'Select Difficulty', Color.aqua)
draw_string_centered(diffNumFont, center+Vector2(0, -300*f_scale), 'Tap to show stats, tap focused to play', Color.lightgreen)
var touchrects = [{rect=rect_back, chart_idx=-1, enabled=true}] # invisible back button
var x = center.x - (size*n + spacer_x*(n-1))/2
for diff in diffs:
var i_diff = Library.Song.difficulty_key_ids.get(diff, 0)
var width = 8 if i_diff == selected_difficulty else 3
var chart_exists: bool = (diff in charts)
var r = draw_songtile(self.selected_song_key, Vector2(x, center.y-160*f_scale), size, false, i_diff, width, not chart_exists)
touchrects.append({rect=r, chart_idx=i_diff, enabled=chart_exists})
x += size + spacer_x
draw_string_centered(titleFont, center+Vector2(0, size-116*f_scale), str(Library.all_songs[self.selected_song_key].title))
draw_string_centered(titleFont, center+Vector2(-50*f_scale, size-64*f_scale), 'BPM:')
draw_string_centered(titleFont, center+Vector2(+50*f_scale, size-64*f_scale), str(song_data.BPM))
if len(charts) > 0:
var sel_chart: Array = charts.values()[min(selected_difficulty, len(charts)-1)]
var all_notes: Array = sel_chart[1]
var meta: Dictionary = sel_chart[0]
var notestrs = ['Taps:', 'Holds:', 'Slides:']
var notetypes = [0, 1, 2]
var note_counts = [meta.num_taps, meta.num_holds, meta.num_slides]
for i in len(notestrs):
draw_string_centered(titleFont, center+Vector2(-50*f_scale, size+(12+i*50)*f_scale), notestrs[i])
draw_string_centered(titleFont, center+Vector2(+50*f_scale, size+(12+i*50)*f_scale), str(note_counts[notetypes[i]]))
else:
draw_string_centered(titleFont, center+Vector2(0, size-12*f_scale), 'No available charts!', Color.red)
return touchrects
func _draw_score_screen(center: Vector2) -> Array:
var size = 192 * f_scale
var spacer_x = 12 * f_scale
var touchrects = []
var songslist = genres[genres.keys()[selected_genre]]
var song_key = scorescreen_song_key
# var song_data = Library.all_songs[song_key]
var chart: Array = Library.get_song_charts(song_key)[Library.Song.default_difficulty_keys[selected_difficulty]]
var all_notes: Array = chart[1]
var meta: Dictionary = chart[0]
var x = center.x
var y = -160*f_scale
var x_score = 110
var y_score = -380
var x2 = -360*f_scale
var x_spacing = 124*f_scale
var y_spacing = 42*f_scale
var y2 = y + y_spacing*1.5
var y3 = y2 + y_spacing
var tex_judgement_text = GameTheme.tex_judgement_text
var judgement_text_scale = 0.667
var judgement_text_size = Vector2(256, 64) * judgement_text_scale
draw_songtile(song_key, center + Vector2.LEFT*size*0.5 + Vector2(-x_score, y_score)*f_scale, size, false, selected_difficulty, 3)
draw_string_centered(titleFont, center + Vector2.DOWN*size + Vector2(-x_score, y_score+48)*f_scale, str(Library.all_songs[song_key].title))
var notestrs = ['Taps (%d):'%meta.num_taps, 'Holds (%d) Hit:'%meta.num_holds, 'Released:', 'Stars (%d):'%meta.num_slides, 'Slides:']
var notetypes = [0, 1, -1, 2, -2]
var note_spacing = [0.0, 1.25, 2.25, 3.5, 4.5]
var judgestrs = Array(Rules.JUDGEMENT_STRINGS + ['Miss'])
var judge_scores = [1.0, 0.9, 0.75, 0.5, 0.0]
var notetype_weights = [1.0, 1.0, 1.0, 1.0, 1.0]
var notecount_total = 0
var notecount_early = 0
var notecount_late = 0
var total_score = 0.0
var total_scoremax = 0.0
for i in len(judgestrs):
# For each judgement type, print a column header
# draw_string_centered(titleFont, Vector2(x2+x_spacing*(i+1), y2), judgestrs[i])
var dst_rect = Rect2(center+Vector2(x2+x_spacing*(i+1)-judgement_text_size.x*f_scale/2.0, y2), judgement_text_size*f_scale)
draw_texture_rect_region(tex_judgement_text, dst_rect, Rect2(0, 128*(i+3), 512, 128))
draw_string_centered(titleFont, center+Vector2(x2+x_spacing*(len(judgestrs)+1), y2+34*f_scale), 'Score')
for i in len(notestrs):
# For each note type, make a row and print scores
var idx = notetypes[i]
var note_score = 0
var note_count = 0
var y_row = y3 + y_spacing * (note_spacing[i]+1)
draw_string_centered(titleFont, center+Vector2(x2-20*f_scale, y_row), notestrs[i])
for j in len(judgestrs):
var score
if j == 0:
score = scorescreen_score_data[idx][0]
elif j >= len(judgestrs)-1:
score = scorescreen_score_data[idx]['MISS']
else:
score = scorescreen_score_data[idx][j] + scorescreen_score_data[idx][-j]
notecount_early += scorescreen_score_data[idx][-j]
notecount_late += scorescreen_score_data[idx][j]
if (j >= len(judgestrs)-1) and (idx == -1):
draw_string_centered(titleFont, center+Vector2(x2+x_spacing*(j+1), y_row), '^')
else:
draw_string_centered(titleFont, center+Vector2(x2+x_spacing*(j+1), y_row), str(score))
notecount_total += score # Kinda redundant, will probably refactor eventually
note_count += score
note_score += score * judge_scores[j]
draw_string_centered(titleFont, center+Vector2(x2+x_spacing*(len(judgestrs)+1), y_row), '%2.2f%%'%(note_score/max(note_count, 1)*100.0))
total_score += note_score * notetype_weights[i]
total_scoremax += note_count * notetype_weights[i]
var overall_score = total_score/max(total_scoremax, 1.0)
var score_idx = 0
for cutoff in Rules.SCORE_CUTOFFS:
if overall_score >= cutoff:
break
else:
score_idx += 1
scoreText.position = center+Vector2(x_score, y_score)*f_scale
scoreText.score = Rules.SCORE_STRINGS[score_idx]
scoreText.score_sub = '%2.3f%%'%(overall_score*100.0)
scoreText.update()
draw_string_centered(titleFont, center+Vector2(-150, y3+y_spacing*7), 'Early : Late')
draw_string_centered(titleFont, center+Vector2(-150, y3+y_spacing*8), '%3d%% : %3d%%'%[notecount_early*100/max(notecount_total, 1), notecount_late*100/max(notecount_total, 1)])
draw_string_centered(titleFont, center+Vector2(150, y3+y_spacing*7.5), 'Max Combo: %d'%scorescreen_score_data.get('max_combo', 0)) # Safety for older saves
var txt_offset = Vector2.DOWN*10*f_scale
var rect_songs := Rect2(center+Vector2(-100.0, 300.0)*f_scale, Vector2(400.0, 100.0)*f_scale)
draw_rect(rect_songs, Color.red)
draw_string_centered(titleFont, get_rect_center(rect_songs), 'Song Select', Color.white, true)
touchrects.append({rect=rect_songs, next_menu=MenuMode.SONG_SELECT})
var rect_save := Rect2(center+Vector2(-300.0, 300.0)*f_scale, Vector2(180.0, 100.0)*f_scale)
if not scorescreen_saved:
draw_rect(rect_save, Color(0.0, 0.01, 1.0))
draw_string_centered(titleFont, get_rect_center(rect_save), 'Save', Color.white, true)
touchrects.append({rect=rect_save, action='save'})
else:
draw_rect(rect_save, Color.darkgray)
draw_string_centered(titleFont, get_rect_center(rect_save), 'Saved', Color.white, true)
draw_string_centered(genreFont, center+Vector2.UP*410*f_scale, 'Results', Color.aqua)
return touchrects
func _draw_gameplay(center: Vector2) -> Array:
var touchrects = []
var rect_songselect := Rect2(center+Vector2(+860.0, 480.0)*f_scale, Vector2(100.0, 50.0)*f_scale)
draw_rect(rect_songselect, Color.red)
draw_string_centered(titleFont, get_rect_center(rect_songselect), 'Stop', Color.white, true)
touchrects.append({rect=rect_songselect, action='stop'})
return touchrects
func _draw():
var songs = len(Library.all_songs)
var score_screen_filter_alpha := 0.65
var size = 216
var outline_px = 3
var center = rect_size * 0.5
touch_rects = []
scoreText.hide()
for i in MenuMode:
touch_rects.append([])
if menu_mode_prev_fade_timer > 0.0:
var progress = 1.0 - menu_mode_prev_fade_timer/menu_mode_prev_fade_timer_duration
var center_prev = lerp(center, center+Vector2(0.0, 1200.0), progress)
var center_next = center_prev + Vector2(0.0, -1200.0)
match menu_mode_prev:
MenuMode.SONG_SELECT:
_draw_song_select(center_prev)
MenuMode.CHART_SELECT:
_draw_chart_select(center_prev)
MenuMode.OPTIONS:
pass
MenuMode.GAMEPLAY:
GameTheme.set_screen_filter_alpha(lerp(0.0, score_screen_filter_alpha, progress))
MenuMode.SCORE_SCREEN:
_draw_score_screen(center_prev)
match menu_mode:
MenuMode.SONG_SELECT:
_draw_song_select(center_next)
MenuMode.CHART_SELECT:
_draw_chart_select(center_next)
MenuMode.OPTIONS:
pass
MenuMode.GAMEPLAY:
GameTheme.set_screen_filter_alpha(1.0 - progress)
MenuMode.SCORE_SCREEN:
_draw_score_screen(center_next)
scoreText.show()
else:
match menu_mode:
MenuMode.SONG_SELECT:
GameTheme.set_screen_filter_alpha(1.0)
touch_rects[menu_mode] = _draw_song_select(center)
MenuMode.CHART_SELECT:
GameTheme.set_screen_filter_alpha(1.0)
touch_rects[menu_mode] = _draw_chart_select(center)
MenuMode.OPTIONS:
pass
MenuMode.GAMEPLAY:
GameTheme.set_screen_filter_alpha(0.0)
touch_rects[menu_mode] = _draw_gameplay(center)
MenuMode.SCORE_SCREEN:
GameTheme.set_screen_filter_alpha(score_screen_filter_alpha)
touch_rects[menu_mode] = _draw_score_screen(center)
scoreText.show()
# var joypad_labels_pos = Vector2(32, 32)
# for i in len(joypad_labels):
# draw_string(titleFont, joypad_labels_pos, "Joypad #%02d: %s"%[i, joypad_labels[i]])
# joypad_labels_pos.y += 32
func set_menu_mode(mode):
receptors.fade(mode == MenuMode.GAMEPLAY)
if mode == MenuMode.GAMEPLAY:
PVMusic.stop()
rect_clip_content = false
else:
rect_clip_content = true
menu_mode_prev = menu_mode
menu_mode = mode
menu_mode_prev_fade_timer = menu_mode_prev_fade_timer_duration
func touch_select_song(touchdict):
if (self.selected_genre == touchdict.genre_idx) and (self.selected_song_idx == touchdict.song_idx):
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
# var songslist = genres[genres.keys()[selected_genre]]
# selected_song_key = songslist[self.target_song_idx % len(songslist)]
set_menu_mode(MenuMode.CHART_SELECT)
else:
self.selected_genre = touchdict.genre_idx
self.target_song_idx = touchdict.song_idx
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, -4.5)
load_preview()
func touch_select_chart(touchdict):
if touchdict.chart_idx == selected_difficulty:
if touchdict.enabled:
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
set_menu_mode(MenuMode.GAMEPLAY)
else:
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_error, 0.0)
elif touchdict.chart_idx < 0:
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, -3.0, 0.7)
set_menu_mode(MenuMode.SONG_SELECT)
else:
self.selected_difficulty = touchdict.chart_idx
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, -4.5)
func touch_gameplay(touchdict):
if touchdict.has('action'):
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
if touchdict.action == 'stop':
noteHandler.stop()
func touch_score_screen(touchdict):
if touchdict.has('next_menu'):
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
set_menu_mode(touchdict.next_menu)
scoreText.score = ''
scoreText.score_sub = ''
# TODO: time this to coincide with the menu going fully offscreen
scoreText.update()
elif touchdict.has('action'):
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
if touchdict.action == 'save':
save_score()
func finished_song(song_key, score_data):
scorescreen_song_key = song_key
scorescreen_score_data = score_data
scorescreen_datetime = OS.get_datetime()
scorescreen_saved = false
set_menu_mode(MenuMode.SCORE_SCREEN)
func is_6button(device: int) -> bool:
match Input.get_joy_name(device):
"LHW FatMatV3":
return true
"LHW FatMat2":
return true
_: # For now, 12 button
return false
func _input(event):
if !visible:
return
if event is InputEventJoypadButton:
update_joypad_labels()
# print_debug("Joypad #%02d is a '%s'" % [event.device, Input.get_joy_name(event.device)])
# event.device
# event.button_index
# event.pressed
# elif event is InputEventJoypadMotion:
# update_joypad_labels()
# print_debug("Joypad #%02d is a '%s'" % [event.device, Input.get_joy_name(event.device)])
# event.device
# event.axis
# event.axis_value
elif event is InputEventMouseButton: # Add this if we ever manage to be rid of the curse of Touch->Mouse emulation: (event is InputEventScreenTouch)
# print(event)
if event.pressed:
var pos = event.position - get_global_transform_with_canvas().get_origin()
match menu_mode:
MenuMode.SONG_SELECT:
for d in touch_rects[MenuMode.SONG_SELECT]:
if d.rect.has_point(pos):
touch_select_song(d)
MenuMode.CHART_SELECT:
for d in touch_rects[MenuMode.CHART_SELECT]:
if d.rect.has_point(pos):
touch_select_chart(d)
MenuMode.GAMEPLAY:
for d in touch_rects[MenuMode.GAMEPLAY]:
if d.rect.has_point(pos):
touch_gameplay(d)
MenuMode.SCORE_SCREEN:
for d in touch_rects[MenuMode.SCORE_SCREEN]:
if d.rect.has_point(pos):
touch_score_screen(d)
match menu_mode:
MenuMode.SONG_SELECT:
if event.is_action_pressed('ui_right'): # Sadly can't use match with this input system
self.target_song_idx += 1
elif event.is_action_pressed('ui_left'):
self.target_song_idx -= 1
elif event.is_action_pressed('ui_up'):
selected_genre = posmod(selected_genre - 1, len(genres))
elif event.is_action_pressed('ui_down'):
selected_genre = posmod(selected_genre + 1, len(genres))
elif event.is_action_pressed('ui_page_up'):
selected_difficulty = int(max(0, selected_difficulty - 1))
elif event.is_action_pressed('ui_page_down'):
selected_difficulty = int(min(6, selected_difficulty + 1))

View File

@ -37,9 +37,9 @@ func _draw(): # draw fingers points on screen
# draw_line(touch_positions[i], touch_positions[i+1], Color(1,1,1,1))
func _process(delta):
swipe_momentum *= max(1.0 - 5.0*delta, 0)
if swipe_momentum.length_squared() < 1.0:
swipe_momentum = Vector2.ZERO
# swipe_momentum *= max(1.0 - 5.0*delta, 0)
# if swipe_momentum.length_squared() < 1.0:
# swipe_momentum = Vector2.ZERO
update()
func update_data():
@ -57,7 +57,7 @@ func _input(event):
if (event is InputEventScreenDrag):
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
touch_points[event.index] = {pressed = true, position = event.position}
swipe_momentum = event.speed
# swipe_momentum = event.speed
elif (event is InputEventScreenTouch):
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
if event.pressed:

View File

@ -1,24 +1,21 @@
#tool
#extends Node2D
extends Control
export var NoteHandlerPath := @'../Center/NoteHandler'
export var ReceptorsPath := @'../Center/Receptors'
onready var NoteHandler := get_node(NoteHandlerPath)
onready var Receptors := get_node(ReceptorsPath)
onready var ScoreText := $ScoreText
onready var noteHandler := $'%noteHandler'
onready var receptors := $'%receptors'
onready var scoreText := $ScoreText
onready var PVMusic := SoundPlayer.music_player_pv
var f_scale := 1.0 setget set_f_scale
func set_f_scale(value: float) -> void:
f_scale = value
TitleFont.size = int(round(32*f_scale))
TitleFont.outline_size = int(max(round(2*f_scale), 1))
GenreFont.size = int(round(48*f_scale))
GenreFont.outline_size = int(max(round(2*f_scale), 1))
DiffNumFont.size = int(round(36*f_scale))
DiffNumFont.outline_size = int(max(round(1*f_scale), 1))
ScoreText.set_f_scale(f_scale)
titleFont.size = int(round(32*f_scale))
titleFont.outline_size = int(max(round(2*f_scale), 1))
genreFont.size = int(round(48*f_scale))
genreFont.outline_size = int(max(round(2*f_scale), 1))
diffNumFont.size = int(round(36*f_scale))
diffNumFont.outline_size = int(max(round(1*f_scale), 1))
scoreText.set_f_scale(f_scale)
func update_scale() -> void:
self.f_scale = rect_size.x/1080
@ -50,7 +47,10 @@ func get_song_idx() -> int:
return int(round(self.target_song_idx + target_song_delta))
func get_song_key() -> String:
var songslist = genres[genres.keys()[selected_genre]]
var keys = genres.keys()
if len(keys) <= selected_difficulty:
return 'N/A'
var songslist = genres[keys[selected_genre]]
return songslist[int(round(self.target_song_idx)) % len(songslist)]
var scorescreen_song_key := ''
@ -60,10 +60,10 @@ var scorescreen_saved := false
var touch_rects = []
var TitleFont: DynamicFont = preload('res://assets/MenuTitleFont.tres').duplicate()
var GenreFont: DynamicFont = preload('res://assets/MenuGenreFont.tres').duplicate()
var DiffNumFont: DynamicFont = preload('res://assets/MenuDiffNumberFont.tres').duplicate()
var ScoreFont: DynamicFont = preload('res://assets/MenuScoreFont.tres').duplicate()
var titleFont: DynamicFont = preload('res://assets/MenuTitleFont.tres').duplicate()
var genreFont: DynamicFont = preload('res://assets/MenuGenreFont.tres').duplicate()
var diffNumFont: DynamicFont = preload('res://assets/MenuDiffNumberFont.tres').duplicate()
var scoreFont: DynamicFont = preload('res://assets/MenuScoreFont.tres').duplicate()
var snd_interact := preload('res://assets/softclap.wav')
var snd_error := preload('res://assets/miss.wav')
@ -132,7 +132,7 @@ func load_preview():
func _ready():
scan_library()
connect('item_rect_changed', self, 'update_scale')
NoteHandler.connect('finished_song', self, 'finished_song')
noteHandler.connect('finished_song', self, 'finished_song')
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
@ -158,9 +158,9 @@ func _process(delta):
menu_mode_prev_fade_timer = max(0.0, menu_mode_prev_fade_timer - delta)
update()
if (menu_mode == MenuMode.GAMEPLAY) and (menu_mode_prev_fade_timer <= 0.0) and not NoteHandler.running:
NoteHandler.load_track(self.selected_song_key, Library.Song.default_difficulty_keys[selected_difficulty])
NoteHandler.running = true
if (menu_mode == MenuMode.GAMEPLAY) and (menu_mode_prev_fade_timer <= 0.0) and not noteHandler.running:
noteHandler.load_track(self.selected_song_key, Library.Song.default_difficulty_keys[selected_difficulty])
noteHandler.running = true
func draw_string_centered(font: Font, position: Vector2, string: String, color := GameTheme.COLOR_MENU_TEXT, vcenter := false) -> Vector2:
@ -195,11 +195,11 @@ func draw_songtile(song_key, position, size, title_text:=false, difficulty=selec
draw_rect(Rect2(position.x - outline_px, position.y - outline_px, size + outline_px*2, size + outline_px*2), diff_color)
draw_texture_rect(Library.get_song_tile_texture(song_key), rect, false, Color.white if not disabled else Color(0.5, 0.2, 0.1))
# Draw track difficulty rating
draw_string_ralign(DiffNumFont, position+Vector2(size-2*f_scale, size-5*f_scale), song_diffs.get(Library.Song.default_difficulty_keys[difficulty], '0'), diff_color)
draw_string_ralign(diffNumFont, position+Vector2(size-2*f_scale, size-5*f_scale), song_diffs.get(Library.Song.default_difficulty_keys[difficulty], '0'), diff_color)
if disabled:
draw_string_centered(DiffNumFont, position+Vector2(size/2, size/2), 'No Chart!', diff_color, true)
draw_string_centered(diffNumFont, position+Vector2(size/2, size/2), 'No Chart!', diff_color, true)
if title_text:
draw_string_centered(TitleFont, position+Vector2(size/2.0, size+40*f_scale), str(Library.all_songs[song_key].title), diff_color.lightened(0.33))
draw_string_centered(titleFont, position+Vector2(size/2.0, size+40*f_scale), str(Library.all_songs[song_key].title), diff_color.lightened(0.33))
return rect
func diff_f2str(difficulty: float): # Convert .5 to +
@ -215,6 +215,11 @@ func _draw_song_select(center: Vector2) -> Array:
var gy: float = center.y - 500 * f_scale - size*selected_genre_delta
var touchrects := []
if len(genres) <= 0:
draw_string_centered(genreFont, Vector2(center.x, center.y-440*f_scale), 'No Songs in Library!', Color.aqua)
draw_string_centered(diffNumFont, Vector2(center.x, center.y-390*f_scale), Settings.user_data_dir, Color.lightgreen)
return touchrects
var ssid = self.selected_song_idx
var s_delta = target_song_delta-round(target_song_delta)
for gi in [-2, -1, 0, 1, 2]:
@ -226,7 +231,7 @@ func _draw_song_select(center: Vector2) -> Array:
var gx = center.x - (subsize + spacer_x) * s_delta
var songslist = Library.genre_songs[g].keys()
var genre_str = '%s (%d songs)'%[genres.keys()[g], len(songslist)]
draw_string_centered(GenreFont, Vector2(center.x, gy), genre_str, Color.lightblue)
draw_string_centered(genreFont, Vector2(center.x, gy), genre_str, Color.lightblue)
var s = len(songslist)
var key = songslist[self.selected_song_idx % s]
@ -259,8 +264,8 @@ func _draw_song_select(center: Vector2) -> Array:
var cs = PoolColorArray([Color(0,0,0.1,1.25), Color(0,0,0.1,1.25), Color(0,0,0,0), Color(0,0,0,0)])
draw_polygon(ps, cs)
draw_polygon(ps2, cs)
draw_string_centered(GenreFont, Vector2(center.x, center.y-440*f_scale), 'Select Song', Color.aqua)
draw_string_centered(DiffNumFont, Vector2(center.x, center.y-390*f_scale), 'Tap to scroll, tap focused to select', Color.lightgreen)
draw_string_centered(genreFont, Vector2(center.x, center.y-440*f_scale), 'Select Song', Color.aqua)
draw_string_centered(diffNumFont, Vector2(center.x, center.y-390*f_scale), 'Tap to scroll, tap focused to select', Color.lightgreen)
return touchrects
func _draw_chart_select(center: Vector2) -> Array:
@ -273,9 +278,9 @@ func _draw_chart_select(center: Vector2) -> Array:
var size = min(192, (1000-spacer_x*(n-1))/n) * f_scale
var rect_back = Rect2(center + Vector2(-300.0, 390.0)*f_scale, Vector2(600.0, 140.0)*f_scale)
draw_rect(rect_back, Color.red)
draw_string_centered(TitleFont, get_rect_center(rect_back), 'Back to song selection', Color.white, true)
draw_string_centered(GenreFont, center+Vector2(0, -360*f_scale), 'Select Difficulty', Color.aqua)
draw_string_centered(DiffNumFont, center+Vector2(0, -300*f_scale), 'Tap to show stats, tap focused to play', Color.lightgreen)
draw_string_centered(titleFont, get_rect_center(rect_back), 'Back to song selection', Color.white, true)
draw_string_centered(genreFont, center+Vector2(0, -360*f_scale), 'Select Difficulty', Color.aqua)
draw_string_centered(diffNumFont, center+Vector2(0, -300*f_scale), 'Tap to show stats, tap focused to play', Color.lightgreen)
var touchrects = [{rect=rect_back, chart_idx=-1, enabled=true}] # invisible back button
var x = center.x - (size*n + spacer_x*(n-1))/2
@ -286,10 +291,10 @@ func _draw_chart_select(center: Vector2) -> Array:
var r = draw_songtile(self.selected_song_key, Vector2(x, center.y-160*f_scale), size, false, i_diff, width, not chart_exists)
touchrects.append({rect=r, chart_idx=i_diff, enabled=chart_exists})
x += size + spacer_x
draw_string_centered(TitleFont, center+Vector2(0, size-116*f_scale), str(Library.all_songs[self.selected_song_key].title))
draw_string_centered(titleFont, center+Vector2(0, size-116*f_scale), str(Library.all_songs[self.selected_song_key].title))
draw_string_centered(TitleFont, center+Vector2(-50*f_scale, size-64*f_scale), 'BPM:')
draw_string_centered(TitleFont, center+Vector2(+50*f_scale, size-64*f_scale), str(song_data.BPM))
draw_string_centered(titleFont, center+Vector2(-50*f_scale, size-64*f_scale), 'BPM:')
draw_string_centered(titleFont, center+Vector2(+50*f_scale, size-64*f_scale), str(song_data.BPM))
if len(charts) > 0:
var sel_chart: Array = charts.values()[min(selected_difficulty, len(charts)-1)]
@ -300,10 +305,10 @@ func _draw_chart_select(center: Vector2) -> Array:
var notetypes = [0, 1, 2]
var note_counts = [meta.num_taps, meta.num_holds, meta.num_slides]
for i in len(notestrs):
draw_string_centered(TitleFont, center+Vector2(-50*f_scale, size+(12+i*50)*f_scale), notestrs[i])
draw_string_centered(TitleFont, center+Vector2(+50*f_scale, size+(12+i*50)*f_scale), str(note_counts[notetypes[i]]))
draw_string_centered(titleFont, center+Vector2(-50*f_scale, size+(12+i*50)*f_scale), notestrs[i])
draw_string_centered(titleFont, center+Vector2(+50*f_scale, size+(12+i*50)*f_scale), str(note_counts[notetypes[i]]))
else:
draw_string_centered(TitleFont, center+Vector2(0, size-12*f_scale), 'No available charts!', Color.red)
draw_string_centered(titleFont, center+Vector2(0, size-12*f_scale), 'No available charts!', Color.red)
return touchrects
@ -333,7 +338,7 @@ func _draw_score_screen(center: Vector2) -> Array:
var judgement_text_size = Vector2(256, 64) * judgement_text_scale
draw_songtile(song_key, center + Vector2.LEFT*size*0.5 + Vector2(-x_score, y_score)*f_scale, size, false, selected_difficulty, 3)
draw_string_centered(TitleFont, center + Vector2.DOWN*size + Vector2(-x_score, y_score+48)*f_scale, str(Library.all_songs[song_key].title))
draw_string_centered(titleFont, center + Vector2.DOWN*size + Vector2(-x_score, y_score+48)*f_scale, str(Library.all_songs[song_key].title))
var notestrs = ['Taps (%d):'%meta.num_taps, 'Holds (%d) Hit:'%meta.num_holds, 'Released:', 'Stars (%d):'%meta.num_slides, 'Slides:']
var notetypes = [0, 1, -1, 2, -2]
var note_spacing = [0.0, 1.25, 2.25, 3.5, 4.5]
@ -348,10 +353,10 @@ func _draw_score_screen(center: Vector2) -> Array:
for i in len(judgestrs):
# For each judgement type, print a column header
# draw_string_centered(TitleFont, Vector2(x2+x_spacing*(i+1), y2), judgestrs[i])
# draw_string_centered(titleFont, Vector2(x2+x_spacing*(i+1), y2), judgestrs[i])
var dst_rect = Rect2(center+Vector2(x2+x_spacing*(i+1)-judgement_text_size.x*f_scale/2.0, y2), judgement_text_size*f_scale)
draw_texture_rect_region(tex_judgement_text, dst_rect, Rect2(0, 128*(i+3), 512, 128))
draw_string_centered(TitleFont, center+Vector2(x2+x_spacing*(len(judgestrs)+1), y2+34*f_scale), 'Score')
draw_string_centered(titleFont, center+Vector2(x2+x_spacing*(len(judgestrs)+1), y2+34*f_scale), 'Score')
for i in len(notestrs):
# For each note type, make a row and print scores
@ -359,7 +364,7 @@ func _draw_score_screen(center: Vector2) -> Array:
var note_score = 0
var note_count = 0
var y_row = y3 + y_spacing * (note_spacing[i]+1)
draw_string_centered(TitleFont, center+Vector2(x2-20*f_scale, y_row), notestrs[i])
draw_string_centered(titleFont, center+Vector2(x2-20*f_scale, y_row), notestrs[i])
for j in len(judgestrs):
var score
if j == 0:
@ -371,13 +376,13 @@ func _draw_score_screen(center: Vector2) -> Array:
notecount_early += scorescreen_score_data[idx][-j]
notecount_late += scorescreen_score_data[idx][j]
if (j >= len(judgestrs)-1) and (idx == -1):
draw_string_centered(TitleFont, center+Vector2(x2+x_spacing*(j+1), y_row), '^')
draw_string_centered(titleFont, center+Vector2(x2+x_spacing*(j+1), y_row), '^')
else:
draw_string_centered(TitleFont, center+Vector2(x2+x_spacing*(j+1), y_row), str(score))
draw_string_centered(titleFont, center+Vector2(x2+x_spacing*(j+1), y_row), str(score))
notecount_total += score # Kinda redundant, will probably refactor eventually
note_count += score
note_score += score * judge_scores[j]
draw_string_centered(TitleFont, center+Vector2(x2+x_spacing*(len(judgestrs)+1), y_row), '%2.2f%%'%(note_score/max(note_count, 1)*100.0))
draw_string_centered(titleFont, center+Vector2(x2+x_spacing*(len(judgestrs)+1), y_row), '%2.2f%%'%(note_score/max(note_count, 1)*100.0))
total_score += note_score * notetype_weights[i]
total_scoremax += note_count * notetype_weights[i]
@ -388,31 +393,31 @@ func _draw_score_screen(center: Vector2) -> Array:
break
else:
score_idx += 1
ScoreText.position = center+Vector2(x_score, y_score)*f_scale
ScoreText.score = Rules.SCORE_STRINGS[score_idx]
ScoreText.score_sub = '%2.3f%%'%(overall_score*100.0)
ScoreText.update()
scoreText.position = center+Vector2(x_score, y_score)*f_scale
scoreText.score = Rules.SCORE_STRINGS[score_idx]
scoreText.score_sub = '%2.3f%%'%(overall_score*100.0)
scoreText.update()
draw_string_centered(TitleFont, center+Vector2(-150, y3+y_spacing*7), 'Early : Late')
draw_string_centered(TitleFont, center+Vector2(-150, y3+y_spacing*8), '%3d%% : %3d%%'%[notecount_early*100/max(notecount_total, 1), notecount_late*100/max(notecount_total, 1)])
draw_string_centered(TitleFont, center+Vector2(150, y3+y_spacing*7.5), 'Max Combo: %d'%scorescreen_score_data.get('max_combo', 0)) # Safety for older saves
draw_string_centered(titleFont, center+Vector2(-150, y3+y_spacing*7), 'Early : Late')
draw_string_centered(titleFont, center+Vector2(-150, y3+y_spacing*8), '%3d%% : %3d%%'%[notecount_early*100/max(notecount_total, 1), notecount_late*100/max(notecount_total, 1)])
draw_string_centered(titleFont, center+Vector2(150, y3+y_spacing*7.5), 'Max Combo: %d'%scorescreen_score_data.get('max_combo', 0)) # Safety for older saves
var txt_offset = Vector2.DOWN*10*f_scale
var rect_songs := Rect2(center+Vector2(-100.0, 300.0)*f_scale, Vector2(400.0, 100.0)*f_scale)
draw_rect(rect_songs, Color.red)
draw_string_centered(TitleFont, get_rect_center(rect_songs), 'Song Select', Color.white, true)
draw_string_centered(titleFont, get_rect_center(rect_songs), 'Song Select', Color.white, true)
touchrects.append({rect=rect_songs, next_menu=MenuMode.SONG_SELECT})
var rect_save := Rect2(center+Vector2(-300.0, 300.0)*f_scale, Vector2(180.0, 100.0)*f_scale)
if not scorescreen_saved:
draw_rect(rect_save, Color(0.0, 0.01, 1.0))
draw_string_centered(TitleFont, get_rect_center(rect_save), 'Save', Color.white, true)
draw_string_centered(titleFont, get_rect_center(rect_save), 'Save', Color.white, true)
touchrects.append({rect=rect_save, action='save'})
else:
draw_rect(rect_save, Color.darkgray)
draw_string_centered(TitleFont, get_rect_center(rect_save), 'Saved', Color.white, true)
draw_string_centered(titleFont, get_rect_center(rect_save), 'Saved', Color.white, true)
draw_string_centered(GenreFont, center+Vector2.UP*410*f_scale, 'Results', Color.aqua)
draw_string_centered(genreFont, center+Vector2.UP*410*f_scale, 'Results', Color.aqua)
return touchrects
func _draw_gameplay(center: Vector2) -> Array:
@ -420,7 +425,7 @@ func _draw_gameplay(center: Vector2) -> Array:
var rect_songselect := Rect2(center+Vector2(+860.0, 480.0)*f_scale, Vector2(100.0, 50.0)*f_scale)
draw_rect(rect_songselect, Color.red)
draw_string_centered(TitleFont, get_rect_center(rect_songselect), 'Stop', Color.white, true)
draw_string_centered(titleFont, get_rect_center(rect_songselect), 'Stop', Color.white, true)
touchrects.append({rect=rect_songselect, action='stop'})
return touchrects
@ -432,7 +437,7 @@ func _draw():
var outline_px = 3
var center = rect_size * 0.5
touch_rects = []
ScoreText.hide()
scoreText.hide()
for i in MenuMode:
touch_rects.append([])
@ -462,7 +467,7 @@ func _draw():
GameTheme.set_screen_filter_alpha(1.0 - progress)
MenuMode.SCORE_SCREEN:
_draw_score_screen(center_next)
ScoreText.show()
scoreText.show()
else:
match menu_mode:
MenuMode.SONG_SELECT:
@ -479,10 +484,10 @@ func _draw():
MenuMode.SCORE_SCREEN:
GameTheme.set_screen_filter_alpha(score_screen_filter_alpha)
touch_rects[menu_mode] = _draw_score_screen(center)
ScoreText.show()
scoreText.show()
func set_menu_mode(mode):
Receptors.fade(mode == MenuMode.GAMEPLAY)
receptors.fade(mode == MenuMode.GAMEPLAY)
if mode == MenuMode.GAMEPLAY:
PVMusic.stop()
rect_clip_content = false
@ -492,48 +497,51 @@ func set_menu_mode(mode):
menu_mode = mode
menu_mode_prev_fade_timer = menu_mode_prev_fade_timer_duration
func play_sound(sound: AudioStream, volume_db: float = 0.0, pitch_scale: float = 1.0):
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, sound, volume_db, pitch_scale)
func touch_select_song(touchdict):
if (self.selected_genre == touchdict.genre_idx) and (self.selected_song_idx == touchdict.song_idx):
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
play_sound(snd_interact)
# var songslist = genres[genres.keys()[selected_genre]]
# selected_song_key = songslist[self.target_song_idx % len(songslist)]
set_menu_mode(MenuMode.CHART_SELECT)
else:
self.selected_genre = touchdict.genre_idx
self.target_song_idx = touchdict.song_idx
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, -4.5)
play_sound(snd_interact, -4.5)
load_preview()
func touch_select_chart(touchdict):
if touchdict.chart_idx == selected_difficulty:
if touchdict.enabled:
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
play_sound(snd_interact)
set_menu_mode(MenuMode.GAMEPLAY)
else:
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_error, 0.0)
play_sound(snd_error)
elif touchdict.chart_idx < 0:
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, -3.0, 0.7)
play_sound(snd_interact, -3.0, 0.7)
set_menu_mode(MenuMode.SONG_SELECT)
else:
self.selected_difficulty = touchdict.chart_idx
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, -4.5)
play_sound(snd_interact, -4.5)
func touch_gameplay(touchdict):
if touchdict.has('action'):
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
play_sound(snd_interact)
if touchdict.action == 'stop':
NoteHandler.stop()
noteHandler.stop()
func touch_score_screen(touchdict):
if touchdict.has('next_menu'):
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
play_sound(snd_interact)
set_menu_mode(touchdict.next_menu)
ScoreText.score = ''
ScoreText.score_sub = ''
scoreText.score = ''
scoreText.score_sub = ''
# TODO: time this to coincide with the menu going fully offscreen
ScoreText.update()
scoreText.update()
elif touchdict.has('action'):
SoundPlayer.play(SoundPlayer.Type.NON_POSITIONAL, self, snd_interact, 0.0)
play_sound(snd_interact)
if touchdict.action == 'save':
save_score()
@ -573,12 +581,16 @@ func _input(event):
MenuMode.SONG_SELECT:
if event.is_action_pressed('ui_right'): # Sadly can't use match with this input system
self.target_song_idx += 1
load_preview()
elif event.is_action_pressed('ui_left'):
self.target_song_idx -= 1
load_preview()
elif event.is_action_pressed('ui_up'):
selected_genre = posmod(selected_genre - 1, len(genres))
load_preview()
elif event.is_action_pressed('ui_down'):
selected_genre = posmod(selected_genre + 1, len(genres))
load_preview()
elif event.is_action_pressed('ui_page_up'):
selected_difficulty = int(max(0, selected_difficulty - 1))
elif event.is_action_pressed('ui_page_down'):

View File

@ -1,85 +1,58 @@
#extends Object
extends Node
var FileHelpers := preload('res://scripts/FileHelpers.gd')
const ERROR_CODES := [
'OK', 'FAILED', 'ERR_UNAVAILABLE', 'ERR_UNCONFIGURED', 'ERR_UNAUTHORIZED', 'ERR_PARAMETER_RANGE_ERROR',
'ERR_OUT_OF_MEMORY', 'ERR_FILE_NOT_FOUND', 'ERR_FILE_BAD_DRIVE', 'ERR_FILE_BAD_PATH','ERR_FILE_NO_PERMISSION',
'ERR_FILE_ALREADY_IN_USE', 'ERR_FILE_CANT_OPEN', 'ERR_FILE_CANT_WRITE', 'ERR_FILE_CANT_READ', 'ERR_FILE_UNRECOGNIZED',
'ERR_FILE_CORRUPT', 'ERR_FILE_MISSING_DEPENDENCIES', 'ERR_FILE_EOF', 'ERR_CANT_OPEN', 'ERR_CANT_CREATE', 'ERR_QUERY_FAILED',
'ERR_ALREADY_IN_USE', 'ERR_LOCKED', 'ERR_TIMEOUT', 'ERR_CANT_CONNECT', 'ERR_CANT_RESOLVE', 'ERR_CONNECTION_ERROR',
'ERR_CANT_ACQUIRE_RESOURCE', 'ERR_CANT_FORK', 'ERR_INVALID_DATA', 'ERR_INVALID_PARAMETER', 'ERR_ALREADY_EXISTS',
'ERR_DOES_NOT_EXIST', 'ERR_DATABASE_CANT_READ', 'ERR_DATABASE_CANT_WRITE', 'ERR_COMPILATION_FAILED', 'ERR_METHOD_NOT_FOUND',
'ERR_LINK_FAILED', 'ERR_SCRIPT_FAILED', 'ERR_CYCLIC_LINK', 'ERR_INVALID_DECLARATION', 'ERR_DUPLICATE_SYMBOL',
'ERR_PARSE_ERROR', 'ERR_BUSY', 'ERR_SKIP', 'ERR_HELP', 'ERR_BUG'
]
var RGT := preload('res://formats/RGT.gd')
var SM := preload('res://formats/SM.gd')
var SRT := preload('res://formats/SRT.gd')
const NOT_FOUND := ''
const default_difficulty_keys = ['Z', 'B', 'A', 'E', 'M', 'R']
var PATHS : PoolStringArray
func update_library_paths() -> void:
PATHS = Settings.get_library_paths()
var userroot := OS.get_user_data_dir().rstrip('/')+'/' if OS.get_name() != 'Android' else '/storage/emulated/0/RhythmGame/'
var PATHS := PoolStringArray([userroot, '/media/fridge-q/Games/RTG/slow_userdir/']) # Temporary hardcoded testing
# The following would probably work. One huge caveat is that permission needs to be manually granted by the user in app settings as we can't use OS.request_permission('WRITE_EXTERNAL_STORAGE')
# '/storage/emulated/0/Android/data/au.ufeff.rhythmgame/'
# '/sdcard/Android/data/au.ufeff.rhythmgame/'
func _ready() -> void:
update_library_paths()
print('Library paths: ', PATHS)
Settings.connect('config_loaded', self, 'update_library_paths')
func find_file(name: String) -> String:
func find_file(name: String, print_notfound:=false) -> String:
# Searches through all of the paths to find the file
var file := File.new()
for root in PATHS:
var filename: String = root + name
if file.file_exists(filename):
return filename
return ''
if print_notfound:
print('File not found in any libraries: ', name)
return NOT_FOUND
func directory_list(directory: String, hidden: bool, sort:=true) -> Dictionary:
# Sadly there's no filelist sugar so we make our own
var output = {folders=[], files=[], err=OK}
var dir = Directory.new()
output.err = dir.open(directory)
if output.err != OK:
print_debug('Failed to open directory: ' + directory + '(Error code '+output.err+')')
return output
output.err = dir.list_dir_begin(true, !hidden)
if output.err != OK:
print_debug('Failed to begin listing directory: ' + directory + '(Error code '+output.err+')')
return output
var item = dir.get_next()
while (item != ''):
if dir.current_is_dir():
output['folders'].append(item)
else:
output['files'].append(item)
item = dir.get_next()
dir.list_dir_end()
var fallback_audiostream: AudioStream = AudioStreamOGGVorbis.new()
var fallback_videostream: VideoStream = VideoStreamWebm.new()
var fallback_texture := ImageTexture.new()
if sort:
output.folders.sort()
output.files.sort()
# Maybe convert the Arrays to PoolStringArrays?
return output
func load_ogg(name: String) -> AudioStream: # Searches through all of the paths to find the file
match find_file(name):
NOT_FOUND: return fallback_audiostream
var filename: return FileHelpers.load_ogg(filename)
func find_by_extensions(array, extensions=null) -> Dictionary:
# Both args can be Array or PoolStringArray
# If extensions omitted, do all extensions
var output = {}
if extensions:
for ext in extensions:
output[ext] = []
for filename in array:
for ext in extensions:
if filename.ends_with(ext):
output[ext].append(filename)
else:
for filename in array:
var ext = filename.rsplit('.', false, 1)[1]
if ext in output:
output[ext].append(filename)
else:
output[ext] = [filename]
return output
func load_video(name: String) -> VideoStream: # Searches through all of the paths to find the file
match find_file(name):
NOT_FOUND: return fallback_videostream
var filename: return FileHelpers.load_video(filename)
const default_difficulty_keys = ['Z', 'B', 'A', 'E', 'M', 'R']
func scan_library():
func load_image(name: String) -> ImageTexture: # Searches through all of the paths to find the file
match find_file(name, true):
NOT_FOUND: return fallback_texture
var filename: return FileHelpers.load_image(filename)
func scan_library() -> Dictionary:
print('Scanning library')
var song_defs = {}
var collections = {}
@ -89,11 +62,11 @@ func scan_library():
var rootdir = root + 'songs'
var dir = Directory.new()
var err = dir.make_dir_recursive(rootdir)
if err != OK:
if err != OK and err != ERR_ALREADY_EXISTS:
print_debug('An error occurred while trying to create the songs directory: ', err)
return err
var songslist = directory_list(rootdir, false)
var songslist = FileHelpers.directory_list(rootdir, false)
if songslist.err != OK:
print('An error occurred when trying to access the songs directory: ', songslist.err)
return songslist.err
@ -138,7 +111,7 @@ func scan_library():
genres[song_defs[song_key]['genre']] = [song_key]
else:
var files_by_ext = find_by_extensions(directory_list(full_folder, false).files)
var files_by_ext = FileHelpers.find_by_extensions(FileHelpers.directory_list(full_folder, false).files)
if 'sm' in files_by_ext:
var sm_filename = files_by_ext['sm'][0]
print(sm_filename)
@ -153,463 +126,6 @@ func scan_library():
return {song_defs=song_defs, genres=genres}
class SRT:
const TAP_DURATION := 0.062500
const ID_BREAK := 4
const ID_HOLD := 2
const ID_SLIDE_END := 128
const ID3_SLIDE_CHORD := 0 # Straight line
const ID3_SLIDE_ARC_CW := 1
const ID3_SLIDE_ARC_ACW := 2
static func load_file(filename):
var file = File.new()
var err = file.open(filename, File.READ)
if err != OK:
print(err)
return err
var metadata := {}
var num_taps := 0
var num_holds := 0
var num_slides := 0
var notes := []
var beats_per_measure := 4
var length = file.get_len()
var slide_ids = {}
while (file.get_position() < (length-2)):
var noteline = file.get_csv_line()
var time_hit := (float(noteline[0]) + (float(noteline[1]))) * beats_per_measure
var duration := float(noteline[2]) * beats_per_measure
var column := int(noteline[3])
var id := int(noteline[4])
var id2 := int(noteline[5])
var id3 := int(noteline[6])
match id:
ID_HOLD:
notes.push_back(Note.NoteHold.new(time_hit, column, duration))
num_holds += 1
ID_BREAK:
notes.push_back(Note.NoteTap.new(time_hit, column, true))
num_taps += 1
ID_SLIDE_END:
# id2 is slide ID
if id2 in slide_ids:
slide_ids[id2].column_release = column
slide_ids[id2].update_slide_variables()
_:
if id2 == 0:
notes.push_back(Note.NoteTap.new(time_hit, column))
num_taps += 1
else:
# id2 is slide ID, id3 is slide pattern
# In order to properly declare the slide, we need the paired endcap which may not be the next note
var slide_type = Note.SlideType.CHORD
match id3:
ID3_SLIDE_CHORD:
slide_type = Note.SlideType.CHORD
ID3_SLIDE_ARC_CW:
slide_type = Note.SlideType.ARC_CW
ID3_SLIDE_ARC_ACW:
slide_type = Note.SlideType.ARC_ACW
_:
print('Unknown slide type: ', id3)
var note = Note.NoteStar.new(time_hit, column)
num_slides += 1
note.duration = duration
notes.push_back(note)
var slide = Note.NoteSlide.new(time_hit, column, duration, -1, slide_type)
notes.push_back(slide)
slide_ids[id2] = slide
metadata['num_taps'] = num_taps
metadata['num_holds'] = num_holds
metadata['num_slides'] = num_slides
return [metadata, notes]
class RGT:
# RhythmGameText formats
# .rgts - simplified format cutting out redundant data, should be easy to write charts in
# .rgtx - a lossless representation of MM in-memory format
# .rgtm - a collection of rgts charts, with a [title] at the start of each one
enum Format{RGTS, RGTX, RGTM}
const EXTENSIONS = {
'rgts': Format.RGTS,
'rgtx': Format.RGTX,
'rgtm': Format.RGTM,
}
const NOTE_TYPES = {
't': Note.NOTE_TAP,
'h': Note.NOTE_HOLD,
's': Note.NOTE_STAR,
'e': Note.NOTE_SLIDE,
'b': Note.NOTE_TAP, # Break
'x': Note.NOTE_STAR # Break star
}
const SLIDE_TYPES = {
'0': null, # Seems to be used for stars without slides attached
'1': Note.SlideType.CHORD,
'2': Note.SlideType.ARC_ACW,
'3': Note.SlideType.ARC_CW,
'4': Note.SlideType.COMPLEX, # From nekomatsuri master - Loop ACW around center. Size of loop is roughly inscribed in chords of 0-3, 1-4, 2-5... NB: doesn't loop if directly opposite col
'5': Note.SlideType.COMPLEX, # CW of above
'6': Note.SlideType.COMPLEX, # S zigzag through center
'7': Note.SlideType.COMPLEX, # Z zigzag through center
'8': Note.SlideType.COMPLEX, # V into center
'9': Note.SlideType.COMPLEX, # From nekomatsuri master - Seems to loop around to center ACW to make a + to the end
'a': Note.SlideType.COMPLEX, # CW of above
'b': Note.SlideType.COMPLEX, # V into column 2 places ACW
'c': Note.SlideType.COMPLEX, # V into column 2 places CW
'd': Note.SlideType.CHORD_TRIPLE, # Triple cone. Spreads out to the adjacent receptors of the target.
'e': Note.SlideType.CHORD, # Not used in any of our charts
'f': Note.SlideType.CHORD, # Not used in any of our charts
}
const SLIDE_IN_R := sin(PI/8) # Circle radius circumscribed by chords 0-3, 1-4, 2-5 etc.
static func load_file(filename: String):
var extension = filename.rsplit('.', false, 1)[1]
if not EXTENSIONS.has(extension):
return -1
var format = EXTENSIONS[extension]
var file := File.new()
var err := file.open(filename, File.READ)
if err != OK:
print(err)
return err
var length = file.get_len()
var chart_ids = []
var lines = [[]]
# This loop will segment the lines as if the file were RGTM
while (file.get_position() < (length-1)): # Could probably replace this with file.eof_reached()
var line : String = file.get_line()
if line.begins_with('['): # Split to a new list for each chart definition
chart_ids.append(line.lstrip('[').rstrip(']'))
lines.append([])
elif !line.empty():
lines[-1].push_back(line)
file.close()
print('Parsing chart: ', filename)
match format:
Format.RGTS:
var metadata_and_notes = parse_rgts(lines[0])
return metadata_and_notes
Format.RGTX:
var metadata_and_notes = parse_rgtx(lines[0])
return metadata_and_notes
Format.RGTM:
lines.pop_front() # Anything before the first [header] is meaningless
var charts = {}
for i in len(lines):
charts[chart_ids[i]] = parse_rgts(lines[i])
return charts
return format
static func parse_rgtx(lines: PoolStringArray):
return [] # To be implemented later
const beats_per_measure = 4.0 # TODO: Bit of an ugly hack, need to revisit this later
static func parse_rgts(lines: PoolStringArray):
var metadata := {}
var num_taps := 0
var num_holds := 0
var num_slides := 0
var notes := []
var slide_ids := {}
var slide_stars := {} # Multiple stars might link to one star. We only care about linking for the spin speed.
var last_star := []
for i in Rules.COLS:
last_star.append(null)
for line in lines:
if len(line) < 4: # shortest legal line would be like '1:1t'
continue
var s = line.split(':')
var time := float(s[0]) * beats_per_measure
var note_hits := []
var note_nonhits := []
for i in range(1, len(s)):
var n = s[i]
var column := int(n[0])
var ntype = n[1]
n = n.substr(2)
match ntype:
't', 'b': # tap
note_hits.append(Note.NoteTap.new(time, column, ntype=='b'))
num_taps += 1
'h': # hold
var duration = float(n) * beats_per_measure
note_hits.append(Note.NoteHold.new(time, column, duration))
num_holds += 1
's', 'x': # slide star
var star = Note.NoteStar.new(time, column, ntype=='z')
note_hits.append(star)
num_slides += 1
last_star[column] = star
if len(n) > 1: # Not all stars have proper slide info
var slide_type = n[0] # hex digit
var slide_id = int(n.substr(1))
if slide_id > 0:
slide_stars[slide_id] = star
var slide = Note.NoteSlide.new(time, column)
slide_ids[slide_id] = slide
note_nonhits.append(slide)
'e': # slide end
var slide_type = n[0] # numeric digit, left as str just in case
var slide_id = int(n.substr(1))
if slide_id in slide_ids: # Classic slide end
slide_ids[slide_id].time_release = time
if slide_id in slide_stars:
slide_stars[slide_id].duration = slide_ids[slide_id].duration # Should probably recalc in case start time is different but w/e
slide_ids[slide_id].column_release = column
slide_ids[slide_id].slide_type = SLIDE_TYPES[slide_type]
slide_ids[slide_id].update_slide_variables()
if SLIDE_TYPES[slide_type] == Note.SlideType.COMPLEX:
var col_hit = slide_ids[slide_id].column
var RUV = GameTheme.RADIAL_UNIT_VECTORS
slide_ids[slide_id].values.curve2d.add_point(RUV[col_hit]) # Start col
match slide_type:
'4': # TODO: Loop ACW around center. Size of loop is roughly inscribed in chords of 0-3, 1-4, 2-5... NB: doesn't loop if directly opposite col
slide_ids[slide_id].values.curve2d.add_point((RUV[posmod(col_hit-3, Rules.COLS)] + RUV[col_hit]) * 0.5)
'5': # TODO: CW of above
slide_ids[slide_id].values.curve2d.add_point((RUV[posmod(col_hit+3, Rules.COLS)] + RUV[col_hit]) * 0.5)
'6': # S zigzag through center
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit-2, Rules.COLS)] * SLIDE_IN_R)
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit+2, Rules.COLS)] * SLIDE_IN_R)
'7': # Z zigzag through center
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit+2, Rules.COLS)] * SLIDE_IN_R)
slide_ids[slide_id].values.curve2d.add_point(RUV[posmod(col_hit-2, Rules.COLS)] * SLIDE_IN_R)
'8': # V into center
slide_ids[slide_id].values.curve2d.add_point(Vector2.ZERO)
'9': # TODO: From nekomatsuri master - Seems to loop around to center ACW to make a + to the end
slide_ids[slide_id].values.curve2d.add_point(Vector2.ZERO)
'a': # TODO: CW of above
slide_ids[slide_id].values.curve2d.add_point(Vector2.ZERO)
'b': # V into column 2 places ACW
slide_ids[slide_id].values.curve2d.add_point(GameTheme.RADIAL_UNIT_VECTORS[posmod(col_hit-2, Rules.COLS)])
'c': # V into column 2 places CW
slide_ids[slide_id].values.curve2d.add_point(GameTheme.RADIAL_UNIT_VECTORS[posmod(col_hit+2, Rules.COLS)])
slide_ids[slide_id].values.curve2d.add_point(GameTheme.RADIAL_UNIT_VECTORS[column]) # End col
else: # Naked slide start
if last_star[column] != null:
slide_stars[slide_id] = last_star[column]
else:
print_debug('Naked slide with no prior star in column!')
var note = Note.NoteSlide.new(time, column)
slide_ids[slide_id] = note
note_nonhits.append(note)
'_':
print_debug('Unknown note type: ', ntype)
if len(note_hits) > 1:
for note in note_hits: # Set multihit on each one
note.double_hit = true
notes += note_hits + note_nonhits
metadata['num_taps'] = num_taps
metadata['num_holds'] = num_holds
metadata['num_slides'] = num_slides
return [metadata, notes]
class SM:
# Stepmania simfile
const NOTE_VALUES = {
'0': 'None',
'1': 'Tap',
'2': 'HoldStart',
'3': 'HoldRollEnd',
'4': 'RollStart',
'M': 'Mine',
# These three are less likely to show up anywhere, no need to implement
'K': 'Keysound',
'L': 'Lift',
'F': 'Fake',
}
const CHART_DIFFICULTIES = {
'Beginner': 0,
'Easy': 1,
'Medium': 2,
'Hard': 3,
'Challenge': 4,
'Edit': 5,
# Some will just write whatever for special difficulties, but we should at least color-code these standard ones
}
const TAG_TRANSLATIONS = {
'#TITLE': 'title',
'#SUBTITLE': 'subtitle',
'#ARTIST': 'artist',
'#TITLETRANSLIT': 'title_transliteration',
'#SUBTITLETRANSLIT': 'subtitle_transliteration',
'#ARTISTTRANSLIT': 'artist_transliteration',
'#GENRE': 'genre',
'#CREDIT': 'chart_author',
'#BANNER': 'image_banner',
'#BACKGROUND': 'image_background',
# '#LYRICSPATH': '',
'#CDTITLE': 'image_cd_title',
'#MUSIC': 'audio_filelist',
'#OFFSET': 'audio_offsets',
'#SAMPLESTART': 'audio_preview_times',
'#SAMPLELENGTH': 'audio_preview_times',
# '#SELECTABLE': '',
'#BPMS': 'bpm_values',
# '#STOPS': '',
# '#BGCHANGES': '',
# '#KEYSOUNDS': '',
}
static func load_chart(lines):
var metadata = {}
var notes = []
assert(lines[0].begins_with('#NOTES:'))
metadata['chart_type'] = lines[1].strip_edges().rstrip(':')
metadata['description'] = lines[2].strip_edges().rstrip(':')
metadata['difficulty_str'] = lines[3].strip_edges().rstrip(':')
metadata['numerical_meter'] = lines[4].strip_edges().rstrip(':')
metadata['groove_radar'] = lines[5].strip_edges().rstrip(':')
# Measures are separated by lines that start with a comma
# Each line has a state for each of the pads, e.g. '0000' for none pressed
# The lines become even subdivisions of the measure, so if there's 4 lines everything represents a 1/4 beat, if there's 8 lines everything represents a 1/8 beat etc.
# For this reason it's probably best to just have a float for beat-within-measure rather than integer beats.
var measures = [[]]
for i in range(6, len(lines)):
var line = lines[i].strip_edges()
if line.begins_with(','):
measures.append([])
elif line.begins_with(';'):
break
elif len(line) > 0:
measures[-1].append(line)
var ongoing_holds = {}
var num_notes := 0
var num_jumps := 0
var num_hands := 0
var num_holds := 0
var num_rolls := 0
var num_mines := 0
for measure in range(len(measures)):
var m_lines = measures[measure]
var m_length = len(m_lines) # Divide out all lines by this
for beat in m_length:
var line : String = m_lines[beat]
# Jump check at a line-level (check for multiple 1/2/4s)
var hits : int = line.count('1') + line.count('2') + line.count('4')
# Hand/quad check more complex as need to check hold/roll state as well
# TODO: are they exclusive? Does quad override hand override jump? SM5 doesn't have quads and has hands+jumps inclusive
var total_pressed : int = hits + len(ongoing_holds)
var jump : bool = hits >= 2
var hand : bool = total_pressed >= 3
# var quad : bool = total_pressed >= 4
num_notes += hits
num_jumps += int(jump)
num_hands += int(hand)
var time = measure + beat/float(m_length)
for col in len(line):
match line[col]:
'1':
notes.append(Note.NoteTap.new(time, col))
'2': # Hold
ongoing_holds[col] = len(notes)
notes.append(Note.NoteHold.new(time, col, 0.0))
num_holds += 1
'4': # Roll
ongoing_holds[col] = len(notes)
notes.append(Note.NoteRoll.new(time, col, 0.0))
num_rolls += 1
'3': # End Hold/Roll
assert(ongoing_holds.has(col))
notes[ongoing_holds[col]].set_time_release(time)
ongoing_holds.erase(col)
'M': # Mine
num_mines += 1
pass
metadata['num_notes'] = num_notes
metadata['num_taps'] = num_notes - num_jumps
metadata['num_jumps'] = num_jumps
metadata['num_hands'] = num_hands
metadata['num_holds'] = num_holds
metadata['num_rolls'] = num_rolls
metadata['num_mines'] = num_mines
return [metadata, notes]
static func load_file(filename: String) -> Array:
# Output is [metadata, [[meta0, chart0], ..., [metaN, chartN]]]
# Technically, declarations end with a semicolon instead of a linebreak.
# This is a PITA to do correctly in GDScript and the files in our collection are well-behaved with linebreaks anyway, so we won't bother.
var file := File.new()
match file.open(filename, File.READ):
OK:
pass
var err:
print_debug('Error loading file: ', err)
return []
var length = file.get_len()
var lines = [[]] # First list will be header, then every subsequent one is a chart
while (file.get_position() < (length-1)): # Could probably replace this with file.eof_reached()
var line : String = file.get_line()
if line.begins_with('#NOTES'): # Split to a new list for each chart definition
lines.append([])
lines[-1].append(line)
file.close()
var metadata = {}
for line in lines[0]:
var tokens = line.rstrip(';').split(':')
if TAG_TRANSLATIONS.has(tokens[0]):
metadata[TAG_TRANSLATIONS[tokens[0]]] = tokens[1]
elif len(tokens) >= 2:
metadata[tokens[0]] = tokens[1]
var charts = []
for i in range(1, len(lines)):
charts.append(load_chart(lines[i]))
return [metadata, charts]
class Test:
static func stress_pattern():
var notes = []
for bar in range(8):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1))
for i in range(1, 8):
notes.push_back(Note.NoteTap.new(bar*4 + (i/2.0), (bar + i)%8))
notes.push_back(Note.NoteTap.new(bar*4 + (7/2.0), (bar + 3)%8))
for bar in range(8, 16):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 2))
for i in range(1, 8):
notes.push_back(Note.NoteTap.new(bar*4 + (i/2.0), (bar + i)%8))
notes.push_back(Note.NoteTap.new(bar*4 + ((i+0.5)/2.0), (bar + i)%8))
notes.push_back(Note.make_slide(bar*4 + ((i+1)/2.0), 1, (bar + i)%8, 0))
for bar in range(16, 24):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 2))
notes.push_back(Note.NoteHold.new(bar*4, (bar+1)%8, 1))
for i in range(2, 8):
notes.push_back(Note.NoteTap.new(bar*4 + (i/2.0), (bar + i)%8))
notes.push_back(Note.NoteHold.new(bar*4 + ((i+1)/2.0), (bar + i)%8, 0.5))
for bar in range(24, 32):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1))
for i in range(1, 32):
notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i)%8))
if (i%2) > 0:
notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i + 4)%8))
for bar in range(32, 48):
notes.push_back(Note.NoteHold.new(bar*4, bar%8, 1))
for i in range(1, 32):
notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i)%8))
notes.push_back(Note.NoteTap.new(bar*4 + (i/8.0), (bar + i + 3)%8))
return notes
func load_folder(folder, filename='song'):
var file = File.new()
var err = file.open('%s/%s.json' % [folder, filename], File.READ)
@ -627,6 +143,7 @@ func load_folder(folder, filename='song'):
result.directory = folder
return result
func load_filelist(filelist: Array, directory=''):
var charts = {}
var key := 0
@ -634,7 +151,7 @@ func load_filelist(filelist: Array, directory=''):
var extension: String = name.rsplit('.', true, 1)[-1]
name = directory.rstrip('/') + '/' + name
var filename = find_file(name)
if filename != '':
if filename != NOT_FOUND:
match extension:
'rgtm': # multiple charts
var res = RGT.load_file(filename)
@ -651,69 +168,21 @@ func load_filelist(filelist: Array, directory=''):
'sm': # Stepmania, multiple charts
var res = SM.load_file(filename)
for chart in res[1]:
var diff = chart[0].difficulty_str
charts[diff] = chart[1]
var diff = chart.difficulty_str
charts[diff] = chart.notes
_:
pass
return charts
func direct_load_ogg(filename: String) -> AudioStreamOGGVorbis:
# Loads the ogg file with that exact filename
var audiostream = AudioStreamOGGVorbis.new()
var oggfile = File.new()
oggfile.open(filename, File.READ)
audiostream.set_data(oggfile.get_buffer(oggfile.get_len()))
oggfile.close()
return audiostream
var fallback_audiostream = AudioStreamOGGVorbis.new()
func load_ogg(name: String) -> AudioStreamOGGVorbis:
# Searches through all of the paths to find the file
match find_file(name):
'': return fallback_audiostream
var filename: return direct_load_ogg(filename)
var fallback_videostream = VideoStreamWebm.new()
func load_video(name: String):
match find_file(name):
'': return fallback_videostream
var filename:
return load(filename)
# var videostream = VideoStreamGDNative.new()
# videostream.set_file(filename1)
# return videostream
func direct_load_image(filename: String) -> ImageTexture:
var tex := ImageTexture.new()
var img := Image.new()
img.load(filename)
tex.create_from_image(img)
return tex
var fallback_texture := ImageTexture.new()
func load_image(name: String) -> ImageTexture:
var filename = find_file(name)
if filename != '':
return direct_load_image(filename)
print('File not found: ', name)
return fallback_texture
func init_directory(directory: String):
var dir = Directory.new()
var err = dir.make_dir_recursive(directory)
if err != OK:
print('An error occurred while trying to create the scores directory: ', err, ERROR_CODES[err])
return err
func save_json(filename: String, data: Dictionary):
filename = userroot + filename
filename = Settings.user_data_dir + filename
var dir = filename.rsplit('/', true, 1)[0]
match FileLoader.init_directory(dir):
match FileHelpers.init_directory(dir):
OK:
pass
var err:
print_debug('Error making directory for JSON file: ', err, ERROR_CODES[err])
print_debug('Error making directory for JSON file: ', err, FileHelpers.ERROR_CODES[err])
return err
var json = JSON.print(data)
var file = File.new()
@ -723,9 +192,10 @@ func save_json(filename: String, data: Dictionary):
file.close()
return OK
var err:
print_debug('Error saving JSON file: ', err, ERROR_CODES[err])
print_debug('Error saving JSON file: ', err, FileHelpers.ERROR_CODES[err])
return err
func load_json(filename: String):
var file = File.new()
var err
@ -734,7 +204,7 @@ func load_json(filename: String):
if file.file_exists(filename1):
err = file.open(filename1, File.READ)
if err != OK:
print('An error occurred while trying to open file: ', filename1, err, ERROR_CODES[err])
print('An error occurred while trying to open file: ', filename1, err, FileHelpers.ERROR_CODES[err])
continue # return err
var result_json = JSON.parse(file.get_as_text())
file.close()

View File

@ -2,6 +2,7 @@ extends Node
const difficulty_translations = {'01': 'Z', '02': 'B', '03': 'A', '04': 'E', '05': 'M', '06': 'R', '10': ''} # A bit redundant now but might be useful later for other hacks
class MultilangStr:
# Automatically propogate higher langs to lower ones if lower ones are missing.
# e.g. if we don't have a proper english title, return the transliterated one instead
@ -28,6 +29,7 @@ class MultilangStr:
func _to_string() -> String:
return self[GameTheme.display_language]
class Song:
var title: MultilangStr
var subtitle: MultilangStr
@ -98,6 +100,7 @@ var genre_songs = [] # Dictionaries of key: Song
var tile_tex_cache = {} # We'll need some way of managing this later since holding all the tiles in memory might be expensive
var charts_cache = {}
func add_song(key: String, data: Dictionary):
if not data.has('index'):
data['index'] = key
@ -109,6 +112,7 @@ func add_song(key: String, data: Dictionary):
genre_songs.append({})
genre_songs[genre_ids[song.genre]][key] = song
func get_song_tile_texture(song_key):
if song_key in tile_tex_cache:
return tile_tex_cache[song_key]
@ -118,6 +122,7 @@ func get_song_tile_texture(song_key):
else:
print_debug('Invalid song_key: ', song_key)
func get_song_charts(song_key):
if song_key in charts_cache:
return charts_cache[song_key]
@ -143,5 +148,6 @@ func get_song_charts(song_key):
else:
print_debug('Invalid song_key: ', song_key)
func initialize():
pass

View File

@ -1,4 +1,4 @@
#extends Object
tool
extends Node
#class_name Note
@ -13,6 +13,7 @@ const RELEASE_SCORE_TYPES := {
NOTE_ROLL: -NOTE_ROLL
}
class NoteBase extends Resource:
var time_hit: float setget set_time_hit
var time_death: float
@ -26,26 +27,31 @@ class NoteBase extends Resource:
time_hit = value
time_death = time_hit + DEATH_DELAY
class NoteHittableBase extends NoteBase:
const hittable := true
class NoteTapBase extends NoteHittableBase:
func _init(time_hit: float, column: int, is_break:=false):
self.time_hit = time_hit
self.column = column
self.is_break = is_break
class NoteTap extends NoteTapBase:
var type := NOTE_TAP
func _init(time_hit: float, column: int, is_break:=false).(time_hit, column, is_break):
pass
class NoteStar extends NoteTapBase: # Fancy charts have naked slides which necessitates separation of Star and Slide :(
var type := NOTE_STAR
var duration := 1.0 # This is required for the spin speed
func _init(time_hit: float, column: int, is_break:=false).(time_hit, column, is_break):
pass
class NoteHoldBase extends NoteHittableBase:
var time_release: float setget set_time_release
var time_released := INF
@ -72,16 +78,19 @@ class NoteHoldBase extends NoteHittableBase:
time_release = time_hit + duration
time_death = time_release + DEATH_DELAY
class NoteHold extends NoteHoldBase:
var type := NOTE_HOLD
func _init(time_hit: float, column: int, duration: float).(time_hit, column, duration):
pass
class NoteRoll extends NoteHoldBase:
var type := NOTE_ROLL
func _init(time_hit: float, column: int, duration: float).(time_hit, column, duration):
pass
class NoteSlide extends NoteBase: # Fancy charts have naked slides which necessitates separation of Star and Slide :(
const hittable := false
var type := NOTE_SLIDE
@ -204,6 +213,7 @@ class NoteSlide extends NoteBase: # Fancy charts have naked slides which necess
return values.curve2d.get_baked_length()
return 0.0
static func copy_note(note: NoteBase):
# Honestly disappointed I couldn't find a better, more OOP solution for this.
var newnote: NoteBase
@ -223,16 +233,20 @@ static func copy_note(note: NoteBase):
newnote.double_hit = note.double_hit
return newnote
static func make_slide(time_hit: float, duration: float, column: int, column_release: int, slide_type:=SlideType.CHORD) -> NoteSlide:
return NoteSlide.new(time_hit, column, duration, column_release, slide_type)
static func make_touch(time_hit: float, location: Vector2) -> Dictionary:
return {type=NOTE_TOUCH, time_hit=time_hit, time_death=time_hit+DEATH_DELAY, location=location, double_hit=false}
static func make_touch_hold(time_hit: float, duration: float, location: Vector2) -> Dictionary:
var time_release := time_hit + duration
return {type=NOTE_TOUCH_HOLD, time_hit=time_hit, time_release=time_release, time_death=time_release+DEATH_DELAY, location=location, double_hit=false}
static func process_note_list(note_array: Array, check_doubles:=true):
# Preprocess double hits, assign Slide IDs
# If this were performance-critical, we'd single iterate it
@ -260,3 +274,52 @@ static func process_note_list(note_array: Array, check_doubles:=true):
note_array[i].slide_id = slide_id
slide_id += 1
# These should probably get their own singleton later
const ORBIT_INNER_RADIUS = sin(deg2rad(22.5)) # ~0.38
const ORBIT_KAPPA = (sqrt(2)-1) * 4.0 / 3.0 # This is the length of control points along a tangent to approximate a circle (multiply by desired radius)
static func curve2d_make_orbit(curve2d, rad_in, rad_out, ccw, rad_max_arc:=PI*0.25, kappa:=ORBIT_KAPPA, inner_radius:=ORBIT_INNER_RADIUS):
var d_sign = -1 if ccw else 1
var rad_2 = rad_in+PI*3/8*d_sign
var rad_2t = rad_2+PI*0.5*d_sign
var rad_3 = rad_out-PI*3/8*d_sign
var rad_3t = rad_3-PI*0.5*d_sign
var a_diff = wrapf((rad_3-rad_2)*d_sign, 0.0001, TAU+0.0001)
var n = ceil(a_diff/rad_max_arc)
var ad = a_diff/n
var k = kappa*inner_radius*(2*ad/PI) # Not geometrically correct scaling but reasonable for now
# curve2d.add_point(polar2cartesian(1.0, rad_in))
curve2d.add_point(polar2cartesian(inner_radius, rad_2), Vector2.ZERO, polar2cartesian(k, rad_2t))
for i in range(1, n):
var ang = rad_2 + i*ad*d_sign
curve2d.add_point(polar2cartesian(inner_radius, ang), polar2cartesian(k, ang-PI/2*d_sign), polar2cartesian(k, ang+PI/2*d_sign))
curve2d.add_point(polar2cartesian(inner_radius, rad_3), polar2cartesian(k, rad_3t))
# curve2d.add_point(polar2cartesian(1.0, rad_out))
static func curve2d_make_sideorbit(curve2d: Curve2D, rad_in: float, rad_out: float, ccw: bool, rad_max_arc:=PI*0.25, kappa:=ORBIT_KAPPA, inner_radius:=ORBIT_INNER_RADIUS):
var d_sign := -1 if ccw else 1
var sideorbit_center := polar2cartesian(inner_radius, rad_in-PI*0.5*d_sign)
var rad_orbit_in := rad_in + PI*0.5*d_sign
var orbcenter_to_out := polar2cartesian(1.0, rad_out) - sideorbit_center
var rad_orbit_out := orbcenter_to_out.angle() - acos(inner_radius/orbcenter_to_out.length())*d_sign
var pos_orbit_out := sideorbit_center + polar2cartesian(inner_radius, rad_orbit_out)
var a_diff = wrapf((rad_orbit_out-rad_orbit_in)*d_sign, 0.0001, TAU+0.0001)
var n = ceil(a_diff/rad_max_arc)
var ad = a_diff/n
var k = kappa*inner_radius*(2*ad/PI) # Not geometrically correct scaling but reasonable for now
# curve2d.add_point(polar2cartesian(1.0, rad_in))
curve2d.add_point(Vector2.ZERO, Vector2.ZERO, polar2cartesian(k, rad_in+PI))
for i in range(1, n):
var ang = rad_orbit_in + i*ad*d_sign
curve2d.add_point(sideorbit_center + polar2cartesian(inner_radius, ang), polar2cartesian(k, ang-PI/2*d_sign), polar2cartesian(k, ang+PI/2*d_sign))
curve2d.add_point(pos_orbit_out, polar2cartesian(k, rad_orbit_out-PI*0.5*d_sign))
# curve2d.add_point(polar2cartesian(1.0, rad_out))

View File

@ -2,8 +2,42 @@
# This is mostly used so that signals can be used to respond to settings changes
extends Node
signal config_loaded
signal subsampling_changed(xy)
# Newer Android versions require storage access permissions to be requested at runtime.
# There is no way around this, so this code needs to be updated to work.
var android_permissions_granted := false
const REQUIRED_ANDROID_PERMISSIONS := PoolStringArray(['MANAGE_EXTERNAL_FILES', 'READ_EXTERNAL_FILES', 'WRITE_EXTERNAL_FILES'])
func check_android_permissions() -> bool:
if self.android_permissions_granted:
return true
var permissions_granted := OS.get_granted_permissions()
for perm in REQUIRED_ANDROID_PERMISSIONS:
if not (perm in permissions_granted):
return false
self.android_permissions_granted = true
return true
func update_android_permissions() -> void:
if not OS.has_feature('Android'):
return
if self.check_android_permissions():
return
OS.request_permissions()
func _on_request_permissions_result(permission: String, granted: bool) -> void:
print('_on_request_permissions_result: %s = %s' % [permission, granted])
# if granted and (permission in REQUIRED_ANDROID_PERMISSIONS):
# self.call_deferred('update_view')
const ANDROID_USERDIR := '/storage/emulated/0/RhythmGame/'
# The following would probably work. One huge caveat is that permission needs to be manually granted by the user in app settings as we can't use OS.request_permission('WRITE_EXTERNAL_STORAGE')
# '/storage/emulated/0/Android/data/au.ufeff.rhythmgame/'
# '/sdcard/Android/data/au.ufeff.rhythmgame/'
var user_data_dir := OS.get_user_data_dir().rstrip('/')+'/' if OS.get_name() != 'Android' else ANDROID_USERDIR
var SETTINGS_FILENAME = user_data_dir + 'settings.cfg'
var config : ConfigFile
var subsampling: Vector2 setget SSXY_set, SSXY_get
var subsampling_x: float setget SSX_set, SSX_get
var subsampling_y: float setget SSY_set, SSY_get
@ -26,17 +60,38 @@ func SSY_get() -> float:
func SSXY_get() -> Vector2:
return Vector2(self.subsampling_x, self.subsampling_y)
func get_library_paths() -> PoolStringArray:
var paths = [user_data_dir]
var additional_paths = config.get_value('libraries', 'additional_paths', [])
# Ensure paths are valid and have trailing slash
for p in additional_paths:
if p is String:
paths.append(p.rstrip('/')+'/')
return PoolStringArray(paths)
const SETTINGS_FILENAME = 'user://settings.conf'
func set_additional_library_paths(entries):
config.set_value('libraries', 'additional_paths', PoolStringArray(entries))
save_settings()
func load_settings():
var config := ConfigFile.new()
config = ConfigFile.new()
config.set_value('libraries', 'additional_paths', [])
match config.load(SETTINGS_FILENAME):
OK:
pass
self.subsampling = Vector2(
config.get_value('rendering', 'subsampling_x', self.subsampling_x),
config.get_value('rendering', 'subsampling_y', self.subsampling_y)
)
emit_signal('config_loaded')
ERR_FILE_NOT_FOUND:
save_settings()
print('Loaded settings from ' + SETTINGS_FILENAME)
func save_settings():
pass
config.save(SETTINGS_FILENAME)
print('Saved settings to ' + SETTINGS_FILENAME)
func _ready():
load_settings()
get_tree().connect('on_request_permissions_result', self, '_on_request_permissions_result')
self.update_android_permissions()