Merge branch 'master' into catch-hyperdash-stable-sort
@ -21,10 +21,10 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2023.712.0",
|
||||
"version": "2023.1117.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
.github/workflows/ci.yml
vendored
@ -108,6 +108,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup JDK 11
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: microsoft
|
||||
java-version: 11
|
||||
|
||||
- name: Install .NET 6.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
@ -121,24 +127,14 @@ jobs:
|
||||
|
||||
build-only-ios:
|
||||
name: Build only (iOS)
|
||||
# `macos-13` is required, because Xcode 14.3 is required (see below).
|
||||
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta)
|
||||
# `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3.
|
||||
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images)
|
||||
runs-on: macos-13
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# newest Microsoft.iOS.Sdk versions require Xcode 14.3.
|
||||
# 14.3 is currently not the default Xcode version (https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md#xcode),
|
||||
# so set it manually.
|
||||
# TODO: remove when 14.3 becomes the default Xcode version.
|
||||
- name: Set Xcode version
|
||||
shell: bash
|
||||
run: |
|
||||
sudo xcode-select -s "/Applications/Xcode_14.3.app"
|
||||
echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.3.app" >> $GITHUB_ENV
|
||||
|
||||
- name: Install .NET 6.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
|
530
.github/workflows/diffcalc.yml
vendored
@ -1,206 +1,382 @@
|
||||
# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
|
||||
# Usage:
|
||||
# !pp check 0 | Runs only the osu! ruleset.
|
||||
# !pp check 0 2 | Runs only the osu! and catch rulesets.
|
||||
# ## Description
|
||||
#
|
||||
# Uses [diffcalc-sheet-generator](https://github.com/smoogipoo/diffcalc-sheet-generator) to run two builds of osu and generate an SR/PP/Score comparison spreadsheet.
|
||||
#
|
||||
# ## Requirements
|
||||
#
|
||||
# Self-hosted runner with installed:
|
||||
# - `docker >= 20.10.16`
|
||||
# - `docker-compose >= 2.5.1`
|
||||
# - `lbzip2`
|
||||
# - `jq`
|
||||
#
|
||||
# ## Usage
|
||||
#
|
||||
# The workflow can be run in two ways:
|
||||
# 1. Via workflow dispatch.
|
||||
# 2. By an owner of the repository posting a pull request or issue comment containing `!diffcalc`.
|
||||
# For pull requests, the workflow will assume the pull request as the target to compare against (i.e. the `OSU_B` variable).
|
||||
# Any lines in the comment of the form `KEY=VALUE` are treated as variables for the generator.
|
||||
#
|
||||
# ## Google Service Account
|
||||
#
|
||||
# Spreadsheets are uploaded to a Google Service Account, and exposed with read-only permissions to the wider audience.
|
||||
#
|
||||
# 1. Create a project at https://console.cloud.google.com
|
||||
# 2. Enable the `Google Sheets` and `Google Drive` APIs.
|
||||
# 3. Create a Service Account
|
||||
# 4. Generate a key in the JSON format.
|
||||
# 5. Encode the key as base64 and store as an **actions secret** with name **`DIFFCALC_GOOGLE_CREDENTIALS`**
|
||||
#
|
||||
# ## Environment variables
|
||||
#
|
||||
# The default environment may be configured via **actions variables**.
|
||||
#
|
||||
# Refer to [the sample environment](https://github.com/smoogipoo/diffcalc-sheet-generator/blob/master/.env.sample), and prefix each variable with `DIFFCALC_` (e.g. `DIFFCALC_THREADS`, `DIFFCALC_INNODB_BUFFER_SIZE`, etc...).
|
||||
|
||||
name: Run difficulty calculation comparison
|
||||
|
||||
run-name: "${{ github.event_name == 'workflow_dispatch' && format('Manual run: {0}', inputs.osu-b) || 'Automatic comment trigger' }}"
|
||||
|
||||
name: Difficulty Calculation
|
||||
on:
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
osu-b:
|
||||
description: "The target build of ppy/osu"
|
||||
type: string
|
||||
required: true
|
||||
ruleset:
|
||||
description: "The ruleset to process"
|
||||
type: choice
|
||||
required: true
|
||||
options:
|
||||
- osu
|
||||
- taiko
|
||||
- catch
|
||||
- mania
|
||||
converts:
|
||||
description: "Include converted beatmaps"
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
ranked-only:
|
||||
description: "Only ranked beatmaps"
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
generators:
|
||||
description: "Comma-separated list of generators (available: [sr, pp, score])"
|
||||
type: string
|
||||
required: false
|
||||
default: 'pp,sr'
|
||||
osu-a:
|
||||
description: "The source build of ppy/osu"
|
||||
type: string
|
||||
required: false
|
||||
default: 'latest'
|
||||
difficulty-calculator-a:
|
||||
description: "The source build of ppy/osu-difficulty-calculator"
|
||||
type: string
|
||||
required: false
|
||||
default: 'latest'
|
||||
difficulty-calculator-b:
|
||||
description: "The target build of ppy/osu-difficulty-calculator"
|
||||
type: string
|
||||
required: false
|
||||
default: 'latest'
|
||||
score-processor-a:
|
||||
description: "The source build of ppy/osu-queue-score-statistics"
|
||||
type: string
|
||||
required: false
|
||||
default: 'latest'
|
||||
score-processor-b:
|
||||
description: "The target build of ppy/osu-queue-score-statistics"
|
||||
type: string
|
||||
required: false
|
||||
default: 'latest'
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
CONCURRENCY: 4
|
||||
ALLOW_DOWNLOAD: 1
|
||||
SAVE_DOWNLOADED: 1
|
||||
SKIP_INSERT_ATTRIBUTES: 1
|
||||
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
|
||||
jobs:
|
||||
metadata:
|
||||
name: Check for requests
|
||||
check-permissions:
|
||||
name: Check permissions
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }}
|
||||
steps:
|
||||
- name: Check permissions
|
||||
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||
uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819 # v2.2.0
|
||||
with:
|
||||
require: 'write'
|
||||
|
||||
create-comment:
|
||||
name: Create PR comment
|
||||
needs: check-permissions
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
message: |
|
||||
Difficulty calculation queued -- please wait! (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||
|
||||
*This comment will update on completion*
|
||||
|
||||
directory:
|
||||
name: Prepare directory
|
||||
needs: check-permissions
|
||||
runs-on: self-hosted
|
||||
if: github.event.issue.pull_request && contains(github.event.comment.body, '!pp check') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
|
||||
outputs:
|
||||
matrix: ${{ steps.generate-matrix.outputs.matrix }}
|
||||
continue: ${{ steps.generate-matrix.outputs.continue }}
|
||||
GENERATOR_DIR: ${{ steps.set-outputs.outputs.GENERATOR_DIR }}
|
||||
GENERATOR_ENV: ${{ steps.set-outputs.outputs.GENERATOR_ENV }}
|
||||
GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }}
|
||||
steps:
|
||||
- name: Construct build matrix
|
||||
id: generate-matrix
|
||||
- name: Checkout diffcalc-sheet-generator
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: ${{ env.EXECUTION_ID }}
|
||||
repository: 'smoogipoo/diffcalc-sheet-generator'
|
||||
|
||||
- name: Set outputs
|
||||
id: set-outputs
|
||||
run: |
|
||||
if [[ "${{ github.event.comment.body }}" =~ "osu" ]] ; then
|
||||
MATRIX_PROJECTS_JSON+='{ "name": "osu", "id": 0 },'
|
||||
fi
|
||||
if [[ "${{ github.event.comment.body }}" =~ "taiko" ]] ; then
|
||||
MATRIX_PROJECTS_JSON+='{ "name": "taiko", "id": 1 },'
|
||||
fi
|
||||
if [[ "${{ github.event.comment.body }}" =~ "catch" ]] ; then
|
||||
MATRIX_PROJECTS_JSON+='{ "name": "catch", "id": 2 },'
|
||||
fi
|
||||
if [[ "${{ github.event.comment.body }}" =~ "mania" ]] ; then
|
||||
MATRIX_PROJECTS_JSON+='{ "name": "mania", "id": 3 },'
|
||||
fi
|
||||
echo "GENERATOR_DIR=${{ github.workspace }}/${{ env.EXECUTION_ID }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "GENERATOR_ENV=${{ github.workspace }}/${{ env.EXECUTION_ID }}/.env" >> "${GITHUB_OUTPUT}"
|
||||
echo "GOOGLE_CREDS_FILE=${{ github.workspace }}/${{ env.EXECUTION_ID }}/google-credentials.json" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
if [[ "${MATRIX_PROJECTS_JSON}" != "" ]]; then
|
||||
MATRIX_JSON="{ \"ruleset\": [ ${MATRIX_PROJECTS_JSON} ] }"
|
||||
echo "${MATRIX_JSON}"
|
||||
CONTINUE="yes"
|
||||
else
|
||||
CONTINUE="no"
|
||||
fi
|
||||
|
||||
echo "continue=${CONTINUE}" >> $GITHUB_OUTPUT
|
||||
echo "matrix=${MATRIX_JSON}" >> $GITHUB_OUTPUT
|
||||
diffcalc:
|
||||
name: Run
|
||||
environment:
|
||||
name: Setup environment
|
||||
needs: directory
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 1440
|
||||
if: needs.metadata.outputs.continue == 'yes'
|
||||
needs: metadata
|
||||
strategy:
|
||||
matrix: ${{ fromJson(needs.metadata.outputs.matrix) }}
|
||||
env:
|
||||
VARS_JSON: ${{ toJSON(vars) }}
|
||||
steps:
|
||||
- name: Verify MySQL connection from host
|
||||
- name: Add base environment
|
||||
run: |
|
||||
mysql -e "SHOW DATABASES"
|
||||
# Required by diffcalc-sheet-generator
|
||||
cp '${{ needs.directory.outputs.GENERATOR_DIR }}/.env.sample' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
|
||||
- name: Drop previous databases
|
||||
run: |
|
||||
for db in osu_master osu_pr
|
||||
do
|
||||
mysql -e "DROP DATABASE IF EXISTS $db"
|
||||
# Add Google credentials
|
||||
echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ needs.directory.outputs.GOOGLE_CREDS_FILE }}"
|
||||
|
||||
# Add repository variables
|
||||
echo "${VARS_JSON}" | jq -c '. | to_entries | .[]' | while read -r line; do
|
||||
opt=$(jq -r '.key' <<< ${line})
|
||||
val=$(jq -r '.value' <<< ${line})
|
||||
|
||||
if [[ "${opt}" =~ ^DIFFCALC_ ]]; then
|
||||
optNoPrefix=$(echo "${opt}" | cut -d '_' -f2-)
|
||||
sed -i "s;^${optNoPrefix}=.*$;${optNoPrefix}=${val};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Create directory structure
|
||||
- name: Add pull-request environment
|
||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||
run: |
|
||||
mkdir -p $GITHUB_WORKSPACE/master/
|
||||
mkdir -p $GITHUB_WORKSPACE/pr/
|
||||
sed -i "s;^OSU_B=.*$;OSU_B=${{ github.event.issue.pull_request.html_url }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
|
||||
- name: Get upstream branch # https://akaimo.hatenablog.jp/entry/2020/05/16/101251
|
||||
id: upstreambranch
|
||||
- name: Add comment environment
|
||||
if: ${{ github.event_name == 'issue_comment' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
run: |
|
||||
echo "branchname=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')" >> $GITHUB_OUTPUT
|
||||
echo "repo=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')" >> $GITHUB_OUTPUT
|
||||
|
||||
# Checkout osu
|
||||
- name: Checkout osu (master)
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: 'master/osu'
|
||||
- name: Checkout osu (pr)
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: 'pr/osu'
|
||||
repository: ${{ steps.upstreambranch.outputs.repo }}
|
||||
ref: ${{ steps.upstreambranch.outputs.branchname }}
|
||||
|
||||
- name: Checkout osu-difficulty-calculator (master)
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ppy/osu-difficulty-calculator
|
||||
path: 'master/osu-difficulty-calculator'
|
||||
- name: Checkout osu-difficulty-calculator (pr)
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: ppy/osu-difficulty-calculator
|
||||
path: 'pr/osu-difficulty-calculator'
|
||||
|
||||
- name: Install .NET 5.0.x
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: "5.0.x"
|
||||
|
||||
# Sanity checks to make sure diffcalc is not run when incompatible.
|
||||
- name: Build diffcalc (master)
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator
|
||||
./UseLocalOsu.sh
|
||||
dotnet build
|
||||
- name: Build diffcalc (pr)
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator
|
||||
./UseLocalOsu.sh
|
||||
dotnet build
|
||||
|
||||
- name: Download + import data
|
||||
run: |
|
||||
PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top_1000 | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||
BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||
|
||||
# Set env variable for further steps.
|
||||
echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV
|
||||
|
||||
cd $GITHUB_WORKSPACE
|
||||
|
||||
echo "Downloading database dump $PERFORMANCE_DATA_NAME.."
|
||||
wget -q -nc https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2
|
||||
echo "Extracting.."
|
||||
tar -xf $PERFORMANCE_DATA_NAME.tar.bz2
|
||||
|
||||
echo "Downloading beatmap dump $BEATMAPS_DATA_NAME.."
|
||||
wget -q -nc https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2
|
||||
echo "Extracting.."
|
||||
tar -xf $BEATMAPS_DATA_NAME.tar.bz2
|
||||
|
||||
cd $PERFORMANCE_DATA_NAME
|
||||
|
||||
for db in osu_master osu_pr
|
||||
do
|
||||
echo "Setting up database $db.."
|
||||
|
||||
mysql -e "CREATE DATABASE $db"
|
||||
|
||||
echo "Importing beatmaps.."
|
||||
cat osu_beatmaps.sql | mysql $db
|
||||
echo "Importing beatmapsets.."
|
||||
cat osu_beatmapsets.sql | mysql $db
|
||||
|
||||
echo "Creating table structure.."
|
||||
mysql $db -e 'CREATE TABLE `osu_beatmap_difficulty` (
|
||||
`beatmap_id` int unsigned NOT NULL,
|
||||
`mode` tinyint NOT NULL DEFAULT 0,
|
||||
`mods` int unsigned NOT NULL,
|
||||
`diff_unified` float NOT NULL,
|
||||
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`beatmap_id`,`mode`,`mods`),
|
||||
KEY `diff_sort` (`mode`,`mods`,`diff_unified`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;'
|
||||
# Add comment environment
|
||||
echo "$COMMENT_BODY" | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
|
||||
opt=$(echo "${line}" | cut -d '=' -f1)
|
||||
sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
done
|
||||
|
||||
- name: Run diffcalc (master)
|
||||
env:
|
||||
DB_NAME: osu_master
|
||||
- name: Add dispatch environment
|
||||
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
|
||||
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
|
||||
- name: Run diffcalc (pr)
|
||||
env:
|
||||
DB_NAME: osu_pr
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator
|
||||
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
|
||||
sed -i 's;^OSU_B=.*$;OSU_B=${{ inputs.osu-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
sed -i 's/^RULESET=.*$/RULESET=${{ inputs.ruleset }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
sed -i 's/^GENERATORS=.*$/GENERATORS=${{ inputs.generators }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
|
||||
- name: Print diffs
|
||||
run: |
|
||||
mysql -e "
|
||||
SELECT
|
||||
m.beatmap_id,
|
||||
m.mods,
|
||||
b.filename,
|
||||
m.diff_unified as 'sr_master',
|
||||
p.diff_unified as 'sr_pr',
|
||||
(p.diff_unified - m.diff_unified) as 'diff'
|
||||
FROM osu_master.osu_beatmap_difficulty m
|
||||
JOIN osu_pr.osu_beatmap_difficulty p
|
||||
ON m.beatmap_id = p.beatmap_id
|
||||
AND m.mode = p.mode
|
||||
AND m.mods = p.mods
|
||||
JOIN osu_pr.osu_beatmaps b
|
||||
ON b.beatmap_id = p.beatmap_id
|
||||
WHERE abs(m.diff_unified - p.diff_unified) > 0.1
|
||||
ORDER BY abs(m.diff_unified - p.diff_unified)
|
||||
DESC
|
||||
LIMIT 10000;"
|
||||
if [[ '${{ inputs.osu-a }}' != 'latest' ]]; then
|
||||
sed -i 's;^OSU_A=.*$;OSU_A=${{ inputs.osu-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
# Todo: Run ppcalc
|
||||
if [[ '${{ inputs.difficulty-calculator-a }}' != 'latest' ]]; then
|
||||
sed -i 's;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${{ inputs.difficulty-calculator-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.difficulty-calculator-b }}' != 'latest' ]]; then
|
||||
sed -i 's;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${{ inputs.difficulty-calculator-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.score-processor-a }}' != 'latest' ]]; then
|
||||
sed -i 's;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${{ inputs.score-processor-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.score-processor-b }}' != 'latest' ]]; then
|
||||
sed -i 's;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${{ inputs.score-processor-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.converts }}' == 'true' ]]; then
|
||||
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
else
|
||||
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
if [[ '${{ inputs.ranked-only }}' == 'true' ]]; then
|
||||
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
else
|
||||
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
fi
|
||||
|
||||
scores:
|
||||
name: Setup scores
|
||||
needs: [ directory, environment ]
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Query latest data
|
||||
id: query
|
||||
run: |
|
||||
ruleset=$(cat ${{ needs.directory.outputs.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-)
|
||||
performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||
|
||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
|
||||
- name: Download
|
||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||
|
||||
- name: Extract
|
||||
run: |
|
||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
|
||||
beatmaps:
|
||||
name: Setup beatmaps
|
||||
needs: directory
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Query latest data
|
||||
id: query
|
||||
run: |
|
||||
beatmaps_data_name=$(curl -s "https://data.ppy.sh/" | grep "osu_files" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||
|
||||
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
|
||||
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
|
||||
- name: Download
|
||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||
|
||||
- name: Extract
|
||||
run: |
|
||||
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||
|
||||
generator:
|
||||
name: Run generator
|
||||
needs: [ directory, environment, scores, beatmaps ]
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 720
|
||||
outputs:
|
||||
TARGET: ${{ steps.run.outputs.TARGET }}
|
||||
SPREADSHEET_LINK: ${{ steps.run.outputs.SPREADSHEET_LINK }}
|
||||
steps:
|
||||
- name: Run
|
||||
id: run
|
||||
run: |
|
||||
# Add the GitHub token. This needs to be done here because it's unique per-job.
|
||||
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||
|
||||
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||
docker-compose up --build generator
|
||||
|
||||
link=$(docker-compose logs generator -n 10 | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
|
||||
target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
|
||||
|
||||
echo "TARGET=${target}" >> "${GITHUB_OUTPUT}"
|
||||
echo "SPREADSHEET_LINK=${link}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Shutdown
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||
docker-compose down -v
|
||||
|
||||
output-cli:
|
||||
name: Output info
|
||||
needs: generator
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Output info
|
||||
run: |
|
||||
echo "Target: ${{ needs.generator.outputs.TARGET }}"
|
||||
echo "Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}"
|
||||
|
||||
cleanup:
|
||||
name: Cleanup
|
||||
needs: [ directory, generator ]
|
||||
if: ${{ always() && needs.directory.result == 'success' }}
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Cleanup
|
||||
run: |
|
||||
rm -rf "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||
|
||||
update-comment:
|
||||
name: Update PR comment
|
||||
needs: [ create-comment, generator ]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ always() && needs.create-comment.result == 'success' }}
|
||||
steps:
|
||||
- name: Update comment on success
|
||||
if: ${{ needs.generator.result == 'success' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
create_if_not_exists: false
|
||||
message: |
|
||||
Target: ${{ needs.generator.outputs.TARGET }}
|
||||
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
|
||||
|
||||
- name: Update comment on failure
|
||||
if: ${{ needs.generator.result == 'failure' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
create_if_not_exists: false
|
||||
message: |
|
||||
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: Update comment on cancellation
|
||||
if: ${{ needs.generator.result == 'cancelled' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: delete
|
||||
message: '.' # Appears to be required by this action for non-error status code.
|
||||
|
@ -59,7 +59,7 @@ The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of
|
||||
|
||||
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||
|
||||
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library).
|
||||
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library).
|
||||
|
||||
Aside from the above, below is a brief checklist of things to watch out when you're preparing your code changes:
|
||||
|
||||
@ -85,4 +85,4 @@ If you're uncertain about some part of the codebase or some inner workings of th
|
||||
- [Development roadmap](https://github.com/orgs/ppy/projects/7/views/6): What the core team is currently working on
|
||||
- [`ppy/osu-framework` wiki](https://github.com/ppy/osu-framework/wiki): Contains introductory information about osu!framework, the bespoke 2D game framework we use for the game
|
||||
- [`ppy/osu` wiki](https://github.com/ppy/osu/wiki): Contains articles about various technical aspects of the game
|
||||
- [Public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library): Contains finished and draft designs for osu!
|
||||
- [Figma master library](https://www.figma.com/file/VIkXMYNPMtQem2RJg9k2iQ/Master-Library): Contains finished and draft designs for osu!
|
||||
|
@ -7,7 +7,7 @@ Templates for use when creating osu! dependent projects. Create a fully-testable
|
||||
```bash
|
||||
# install (or update) templates package.
|
||||
# this only needs to be done once
|
||||
dotnet new -i ppy.osu.Game.Templates
|
||||
dotnet new install ppy.osu.Game.Templates
|
||||
|
||||
# create an empty freeform ruleset
|
||||
dotnet new ruleset -n MyCoolRuleset
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.922.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1201.1" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -5,4 +5,12 @@
|
||||
<!-- for editor usage -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
</manifest>
|
||||
<!--
|
||||
READ_MEDIA_* permissions are available only on API 33 or greater. Devices with older android versions
|
||||
don't understand the new permissions, so request the old READ_EXTERNAL_STORAGE permission to get storage access.
|
||||
Since the old permission has no effect on >= API 33, don't request it.
|
||||
|
||||
Care needs to be taken to ensure runtime permission checks target the correct permission for the API level.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
|
||||
</manifest>
|
||||
|
@ -70,7 +70,7 @@ namespace osu.Android
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = MouseSettingsStrings.DisableMouseButtons,
|
||||
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay,
|
||||
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
|
||||
},
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.Settings.Sections.Input;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@ -97,6 +98,9 @@ namespace osu.Android
|
||||
case AndroidJoystickHandler jh:
|
||||
return new AndroidJoystickSettings(jh);
|
||||
|
||||
case AndroidTouchHandler th:
|
||||
return new TouchSettings(th);
|
||||
|
||||
default:
|
||||
return base.CreateSettingsSubsectionFor(handler);
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ using osu.Game;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens.Play;
|
||||
using Squirrel;
|
||||
using Squirrel.SimpleSplat;
|
||||
using Squirrel.Sources;
|
||||
using LogLevel = Squirrel.SimpleSplat.LogLevel;
|
||||
using UpdateManager = osu.Game.Updater.UpdateManager;
|
||||
|
||||
@ -63,7 +63,7 @@ namespace osu.Desktop.Updater
|
||||
if (localUserInfo?.IsPlaying.Value == true)
|
||||
return false;
|
||||
|
||||
updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
|
||||
updateManager ??= new Squirrel.UpdateManager(new GithubSource(@"https://github.com/ppy/osu", github_token, false), @"osulazer");
|
||||
|
||||
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
|
||||
|
||||
|
@ -23,7 +23,7 @@
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
|
||||
<PackageReference Include="Clowd.Squirrel" Version="2.10.2" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
|
@ -7,7 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
@ -28,9 +28,9 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private class TestLegacySkin : LegacySkin
|
||||
{
|
||||
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
|
||||
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> fallbackStore)
|
||||
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
|
||||
: base(skin, null, storage)
|
||||
: base(skin, null, fallbackStore)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
|
||||
AddStep("update hit object path", () =>
|
||||
{
|
||||
hitObject.Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
hitObject.Path = new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(100, 100),
|
||||
@ -190,16 +190,16 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
[Test]
|
||||
public void TestVertexResampling()
|
||||
{
|
||||
addBlueprintStep(100, 100, new SliderPath(PathType.PerfectCurve, new[]
|
||||
addBlueprintStep(100, 100, new SliderPath(PathType.PERFECT_CURVE, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(100, 100),
|
||||
new Vector2(50, 200),
|
||||
}), 0.5);
|
||||
AddAssert("1 vertex per 1 nested HO", () => getVertices().Count == hitObject.NestedHitObjects.Count);
|
||||
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PerfectCurve);
|
||||
AddAssert("slider path not yet changed", () => hitObject.Path.ControlPoints[0].Type == PathType.PERFECT_CURVE);
|
||||
addAddVertexSteps(150, 150);
|
||||
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.Linear);
|
||||
AddAssert("slider path change to linear", () => hitObject.Path.ControlPoints[0].Type == PathType.LINEAR);
|
||||
}
|
||||
|
||||
private void addBlueprintStep(double time, float x, SliderPath sliderPath, double velocity) => AddStep("add selection blueprint", () =>
|
||||
|
@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
} while (rng.Next(2) != 0);
|
||||
|
||||
int length = sliderPath.ControlPoints.Count - start + 1;
|
||||
sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.Linear : length == 3 ? PathType.PerfectCurve : PathType.Bezier;
|
||||
sliderPath.ControlPoints[start].Type = length <= 2 ? PathType.LINEAR : length == 3 ? PathType.PERFECT_CURVE : PathType.BEZIER;
|
||||
} while (rng.Next(3) != 0);
|
||||
|
||||
if (rng.Next(5) == 0)
|
||||
@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
foreach (var point in sliderPath.ControlPoints)
|
||||
{
|
||||
Assert.That(point.Type, Is.EqualTo(PathType.Linear).Or.Null);
|
||||
Assert.That(point.Type, Is.EqualTo(PathType.LINEAR).Or.Null);
|
||||
Assert.That(sliderStartY + point.Position.Y, Is.InRange(0, JuiceStreamPath.OSU_PLAYFIELD_HEIGHT));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneCatchModFloatingFruits : ModTestScene
|
||||
{
|
||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestFloating() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new CatchModFloatingFruits(),
|
||||
PassCondition = () => true
|
||||
});
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
var stream = new JuiceStream
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(100, 0),
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
|
||||
{
|
||||
X = CatchPlayfield.CENTER_X,
|
||||
StartTime = 3000,
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 })
|
||||
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, Vector2.UnitY * 200 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
beatmap.HitObjects.Add(new JuiceStream
|
||||
{
|
||||
X = CatchPlayfield.CENTER_X - width / 2,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(width, 0)
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
new JuiceStream
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }),
|
||||
Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, new Vector2(0, -192) }),
|
||||
X = CatchPlayfield.WIDTH / 2
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
private float getCaughtObjectPosition(Fruit fruit)
|
||||
{
|
||||
var caughtObject = catcher.ChildrenOfType<CaughtObject>().Single(c => c.HitObject == fruit);
|
||||
return caughtObject.Parent.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
|
||||
return caughtObject.Parent!.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
|
||||
}
|
||||
|
||||
private void catchFruit(Fruit fruit, float x)
|
||||
|
@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
X = xCoords,
|
||||
StartTime = playfieldTime + 1000,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(0, 200)
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
new JuiceStream
|
||||
{
|
||||
X = CatchPlayfield.CENTER_X,
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
Path = new SliderPath(PathType.LINEAR, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(0, 100)
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -10,7 +12,9 @@ using osu.Game.Rulesets.Catch.Judgements;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Tests.Visual.Gameplay;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
@ -37,11 +41,14 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } };
|
||||
protected override IScoringAlgorithm CreateScoreV1(IReadOnlyList<Mod> selectedMods)
|
||||
=> new ScoreV1(selectedMods) { ScoreMultiplier = { BindTarget = scoreMultiplier } };
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo, IReadOnlyList<Mod> selectedMods)
|
||||
=> new ScoreV2(maxCombo, selectedMods);
|
||||
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new CatchProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList<Mod> selectedMods)
|
||||
=> new CatchProcessorBasedScoringAlgorithm(beatmap, mode, selectedMods);
|
||||
|
||||
[Test]
|
||||
public void TestBasicScenarios()
|
||||
@ -69,10 +76,21 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private class ScoreV1 : IScoringAlgorithm
|
||||
{
|
||||
private int currentCombo;
|
||||
private readonly double modMultiplier;
|
||||
|
||||
public BindableDouble ScoreMultiplier { get; } = new BindableDouble();
|
||||
|
||||
private int currentCombo;
|
||||
|
||||
public ScoreV1(IReadOnlyList<Mod> selectedMods)
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
modMultiplier = ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(selectedMods, new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = ruleset.RulesetInfo
|
||||
});
|
||||
}
|
||||
|
||||
public void ApplyHit() => applyHitV1(base_great);
|
||||
|
||||
public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements.");
|
||||
@ -91,7 +109,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
// combo multiplier
|
||||
// ReSharper disable once PossibleLossOfFraction
|
||||
TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value));
|
||||
TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * (ScoreMultiplier.Value * modMultiplier)));
|
||||
|
||||
currentCombo++;
|
||||
}
|
||||
@ -104,13 +122,23 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
private int currentCombo;
|
||||
private double comboPortion;
|
||||
|
||||
private readonly double modMultiplier;
|
||||
|
||||
private readonly double comboPortionMax;
|
||||
|
||||
private const double combo_base = 4;
|
||||
private const int combo_cap = 200;
|
||||
|
||||
public ScoreV2(int maxCombo)
|
||||
public ScoreV2(int maxCombo, IReadOnlyList<Mod> selectedMods)
|
||||
{
|
||||
var ruleset = new CatchRuleset();
|
||||
modMultiplier = ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(
|
||||
selectedMods.Append(new ModScoreV2()).ToList(),
|
||||
new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = ruleset.RulesetInfo
|
||||
});
|
||||
|
||||
for (int i = 0; i < maxCombo; i++)
|
||||
ApplyHit();
|
||||
|
||||
@ -135,13 +163,13 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
|
||||
public long TotalScore
|
||||
=> (int)Math.Round(1000000 * comboPortion / comboPortionMax); // vast simplification, as we're not doing ticks here.
|
||||
=> (int)Math.Round((1000000 * comboPortion / comboPortionMax) * modMultiplier); // vast simplification, as we're not doing ticks here.
|
||||
}
|
||||
|
||||
private class CatchProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||
{
|
||||
public CatchProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||
: base(beatmap, mode)
|
||||
public CatchProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList<Mod> selectedMods)
|
||||
: base(beatmap, mode, selectedMods)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,22 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
{
|
||||
}
|
||||
|
||||
public override void PreProcess()
|
||||
{
|
||||
IHasComboInformation? lastObj = null;
|
||||
|
||||
// For sanity, ensures that both the first hitobject and the first hitobject after a banana shower start a new combo.
|
||||
// This is normally enforced by the legacy decoder, but is not enforced by the editor.
|
||||
foreach (var obj in Beatmap.HitObjects.OfType<IHasComboInformation>())
|
||||
{
|
||||
if (obj is not BananaShower && (lastObj == null || lastObj is BananaShower))
|
||||
obj.NewCombo = true;
|
||||
lastObj = obj;
|
||||
}
|
||||
|
||||
base.PreProcess();
|
||||
}
|
||||
|
||||
public override void PostProcess()
|
||||
{
|
||||
base.PostProcess();
|
||||
|
@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
|
||||
|
||||
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new CatchHealthProcessor(drainStartTime);
|
||||
|
||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this);
|
||||
|
||||
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
|
||||
@ -54,7 +56,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
new KeyBinding(InputKey.X, CatchAction.MoveRight),
|
||||
new KeyBinding(InputKey.Right, CatchAction.MoveRight),
|
||||
new KeyBinding(InputKey.Shift, CatchAction.Dash),
|
||||
new KeyBinding(InputKey.Shift, CatchAction.Dash),
|
||||
new KeyBinding(InputKey.MouseLeft, CatchAction.Dash),
|
||||
};
|
||||
|
||||
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
|
||||
|
@ -2,10 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -137,5 +140,53 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (increaseCombo)
|
||||
combo++;
|
||||
}
|
||||
|
||||
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
bool scoreV2 = mods.Any(m => m is ModScoreV2);
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case CatchModNoFail:
|
||||
multiplier *= scoreV2 ? 1.0 : 0.5;
|
||||
break;
|
||||
|
||||
case CatchModEasy:
|
||||
multiplier *= 0.5;
|
||||
break;
|
||||
|
||||
case CatchModHalfTime:
|
||||
case CatchModDaycore:
|
||||
multiplier *= 0.3;
|
||||
break;
|
||||
|
||||
case CatchModHidden:
|
||||
multiplier *= scoreV2 ? 1.0 : 1.06;
|
||||
break;
|
||||
|
||||
case CatchModHardRock:
|
||||
multiplier *= 1.12;
|
||||
break;
|
||||
|
||||
case CatchModDoubleTime:
|
||||
case CatchModNightcore:
|
||||
multiplier *= 1.06;
|
||||
break;
|
||||
|
||||
case CatchModFlashlight:
|
||||
multiplier *= 1.12;
|
||||
break;
|
||||
|
||||
case CatchModRelax:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
path.ConvertFromSliderPath(sliderPath, hitObject.Velocity);
|
||||
|
||||
// If the original slider path has non-linear type segments, resample the vertices at nested hit object times to reduce the number of vertices.
|
||||
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.Linear))
|
||||
if (sliderPath.ControlPoints.Any(p => p.Type != null && p.Type != PathType.LINEAR))
|
||||
{
|
||||
path.ResampleVertices(hitObject.NestedHitObjects
|
||||
.Skip(1).TakeWhile(h => !(h is Fruit)) // Only droplets in the first span are used.
|
||||
|
@ -1,180 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class heavily borrows from osu!mania's implementation (ManiaBeatSnapGrid).
|
||||
/// If further changes are to be made, they should also be applied there.
|
||||
/// If the scale of the changes are large enough, abstracting may be a good path.
|
||||
/// </remarks>
|
||||
public partial class CatchBeatSnapGrid : Component
|
||||
public partial class CatchBeatSnapGrid : BeatSnapGrid
|
||||
{
|
||||
private const double visible_range = 750;
|
||||
|
||||
/// <summary>
|
||||
/// The range of time values of the current selection.
|
||||
/// </summary>
|
||||
public (double start, double end)? SelectionTimeRange
|
||||
protected override IEnumerable<Container> GetTargetContainers(HitObjectComposer composer) => new[]
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == selectionTimeRange)
|
||||
return;
|
||||
|
||||
selectionTimeRange = value;
|
||||
lineCache.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BindableBeatDivisor beatDivisor { get; set; } = null!;
|
||||
|
||||
private readonly Cached lineCache = new Cached();
|
||||
|
||||
private (double start, double end)? selectionTimeRange;
|
||||
|
||||
private ScrollingHitObjectContainer lineContainer = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(HitObjectComposer composer)
|
||||
{
|
||||
lineContainer = new ScrollingHitObjectContainer();
|
||||
|
||||
((CatchPlayfield)composer.Playfield).UnderlayElements.Add(lineContainer);
|
||||
|
||||
beatDivisor.BindValueChanged(_ => createLines(), true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!lineCache.IsValid)
|
||||
{
|
||||
lineCache.Validate();
|
||||
createLines();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Stack<DrawableGridLine> availableLines = new Stack<DrawableGridLine>();
|
||||
|
||||
private void createLines()
|
||||
{
|
||||
foreach (var line in lineContainer.Objects.OfType<DrawableGridLine>())
|
||||
availableLines.Push(line);
|
||||
|
||||
lineContainer.Clear();
|
||||
|
||||
if (selectionTimeRange == null)
|
||||
return;
|
||||
|
||||
var range = selectionTimeRange.Value;
|
||||
|
||||
var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);
|
||||
|
||||
double time = timingPoint.Time;
|
||||
int beat = 0;
|
||||
|
||||
// progress time until in the visible range.
|
||||
while (time < range.start - visible_range)
|
||||
{
|
||||
time += timingPoint.BeatLength / beatDivisor.Value;
|
||||
beat++;
|
||||
}
|
||||
|
||||
while (time < range.end + visible_range)
|
||||
{
|
||||
var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);
|
||||
|
||||
// switch to the next timing point if we have reached it.
|
||||
if (nextTimingPoint.Time > timingPoint.Time)
|
||||
{
|
||||
beat = 0;
|
||||
time = nextTimingPoint.Time;
|
||||
timingPoint = nextTimingPoint;
|
||||
}
|
||||
|
||||
Color4 colour = BindableBeatDivisor.GetColourFor(
|
||||
BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);
|
||||
|
||||
if (!availableLines.TryPop(out var line))
|
||||
line = new DrawableGridLine();
|
||||
|
||||
line.HitObject.StartTime = time;
|
||||
line.Colour = colour;
|
||||
|
||||
lineContainer.Add(line);
|
||||
|
||||
beat++;
|
||||
time += timingPoint.BeatLength / beatDivisor.Value;
|
||||
}
|
||||
|
||||
// required to update ScrollingHitObjectContainer's cache.
|
||||
lineContainer.UpdateSubTree();
|
||||
|
||||
foreach (var line in lineContainer.Objects.OfType<DrawableGridLine>())
|
||||
{
|
||||
time = line.HitObject.StartTime;
|
||||
|
||||
if (time >= range.start && time <= range.end)
|
||||
line.Alpha = 1;
|
||||
else
|
||||
{
|
||||
double timeSeparation = time < range.start ? range.start - time : time - range.end;
|
||||
line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class DrawableGridLine : DrawableHitObject
|
||||
{
|
||||
public DrawableGridLine()
|
||||
: base(new HitObject())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 2;
|
||||
|
||||
AddInternal(new Box { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Origin = Anchor.BottomLeft;
|
||||
Anchor = Anchor.BottomLeft;
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
// don't perform any fading – we are handling that ourselves.
|
||||
LifetimeEnd = HitObject.StartTime + visible_range;
|
||||
}
|
||||
}
|
||||
((CatchPlayfield)composer.Playfield).UnderlayElements
|
||||
};
|
||||
}
|
||||
}
|
||||
|
26
osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapProvider.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public partial class CatchDistanceSnapProvider : ComposerDistanceSnapProvider
|
||||
{
|
||||
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||
{
|
||||
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
|
||||
// Therefore this functionality is not currently used.
|
||||
//
|
||||
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
||||
|
||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
|
||||
|
||||
return actualDistance / expectedDistance;
|
||||
}
|
||||
}
|
||||
}
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Scale = new Vector2(Math.Min(Parent.ChildSize.X / CatchPlayfield.WIDTH, Parent.ChildSize.Y / CatchPlayfield.HEIGHT));
|
||||
Scale = new Vector2(Math.Min(Parent!.ChildSize.X / CatchPlayfield.WIDTH, Parent!.ChildSize.Y / CatchPlayfield.HEIGHT));
|
||||
Height = 1 / Scale.Y;
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -20,28 +19,27 @@ using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
// we're also a ScrollingHitObjectComposer candidate, but can't be everything can we?
|
||||
public partial class CatchHitObjectComposer : DistancedHitObjectComposer<CatchHitObject>
|
||||
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private const float distance_snap_radius = 50;
|
||||
|
||||
private CatchDistanceSnapGrid distanceSnapGrid = null!;
|
||||
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
private CatchBeatSnapGrid beatSnapGrid = null!;
|
||||
|
||||
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 10,
|
||||
};
|
||||
|
||||
[Cached(typeof(IDistanceSnapProvider))]
|
||||
protected readonly CatchDistanceSnapProvider DistanceSnapProvider = new CatchDistanceSnapProvider();
|
||||
|
||||
public CatchHitObjectComposer(CatchRuleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
@ -50,8 +48,11 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(DistanceSnapProvider);
|
||||
DistanceSnapProvider.AttachToToolbox(RightToolbox);
|
||||
|
||||
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
|
||||
DistanceSpacingMultiplier.Disabled = true;
|
||||
DistanceSnapProvider.DistanceSpacingMultiplier.Disabled = true;
|
||||
|
||||
LayerBelowRuleset.Add(new PlayfieldBorder
|
||||
{
|
||||
@ -68,61 +69,30 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
|
||||
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
|
||||
}));
|
||||
|
||||
AddInternal(beatSnapGrid = new CatchBeatSnapGrid());
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
||||
=> base.CreateTernaryButtons()
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (BlueprintContainer.CurrentTool is SelectTool)
|
||||
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
|
||||
new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
|
||||
{
|
||||
if (EditorBeatmap.SelectedHitObjects.Any())
|
||||
{
|
||||
beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
|
||||
}
|
||||
else
|
||||
beatSnapGrid.SelectionTimeRange = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
|
||||
if (result.Time is double time)
|
||||
beatSnapGrid.SelectionTimeRange = (time, time);
|
||||
else
|
||||
beatSnapGrid.SelectionTimeRange = null;
|
||||
}
|
||||
}
|
||||
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
|
||||
};
|
||||
|
||||
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
|
||||
|
||||
protected override BeatSnapGrid CreateBeatSnapGrid() => new CatchBeatSnapGrid();
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
{
|
||||
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
|
||||
// Therefore this functionality is not currently used.
|
||||
//
|
||||
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
||||
new FruitCompositionTool(),
|
||||
new JuiceStreamCompositionTool(),
|
||||
new BananaShowerCompositionTool()
|
||||
};
|
||||
|
||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
|
||||
|
||||
return actualDistance / expectedDistance;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
updateDistanceSnapGrid();
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
@ -131,28 +101,19 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
// May be worth considering standardising "zoom" behaviour with what the timeline uses (ie. alt-wheel) but that may cause new conflicts.
|
||||
case GlobalAction.IncreaseScrollSpeed:
|
||||
this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value - 1, 200, Easing.OutQuint);
|
||||
break;
|
||||
return true;
|
||||
|
||||
case GlobalAction.DecreaseScrollSpeed:
|
||||
this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value + 1, 200, Easing.OutQuint);
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnPressed(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
|
||||
new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
|
||||
{
|
||||
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
|
||||
};
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
new FruitCompositionTool(),
|
||||
new JuiceStreamCompositionTool(),
|
||||
new BananaShowerCompositionTool()
|
||||
};
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
@ -172,8 +133,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
|
||||
|
||||
private PalpableCatchHitObject? getLastSnappableHitObject(double time)
|
||||
{
|
||||
var hitObject = EditorBeatmap.HitObjects.OfType<CatchHitObject>().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower));
|
||||
@ -214,7 +173,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
return null;
|
||||
}
|
||||
|
||||
double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(inputManager.CurrentState.Mouse.Position);
|
||||
double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
|
||||
return getLastSnappableHitObject(timeAtCursor);
|
||||
|
||||
default:
|
||||
@ -222,9 +181,16 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
updateDistanceSnapGrid();
|
||||
}
|
||||
|
||||
private void updateDistanceSnapGrid()
|
||||
{
|
||||
if (DistanceSnapToggle.Value != TernaryState.True)
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True)
|
||||
{
|
||||
distanceSnapGrid.Hide();
|
||||
return;
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
@ -21,10 +20,8 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.PlayfieldAdjustmentContainer.Anchor = Anchor.Centre;
|
||||
drawableRuleset.PlayfieldAdjustmentContainer.Origin = Anchor.Centre;
|
||||
|
||||
drawableRuleset.PlayfieldAdjustmentContainer.Scale = new Vector2(1, -1);
|
||||
drawableRuleset.PlayfieldAdjustmentContainer.Y = 1 - drawableRuleset.PlayfieldAdjustmentContainer.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
@ -16,5 +17,13 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
||||
catchProcessor.HardRockOffsets = true;
|
||||
}
|
||||
|
||||
public override void ApplyToDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
base.ApplyToDifficulty(difficulty);
|
||||
|
||||
difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio.
|
||||
difficulty.ApproachRate = Math.Min(difficulty.ApproachRate * ADJUST_RATIO, 10.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
@ -151,7 +152,34 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
|
||||
|
||||
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
|
||||
Scale = LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize);
|
||||
}
|
||||
|
||||
public void UpdateComboInformation(IHasComboInformation? lastObj)
|
||||
{
|
||||
// Note that this implementation is shared with the osu! ruleset's implementation.
|
||||
// If a change is made here, OsuHitObject.cs should also be updated.
|
||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||
|
||||
if (this is BananaShower)
|
||||
{
|
||||
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
||||
return;
|
||||
}
|
||||
|
||||
// At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
|
||||
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
||||
if (NewCombo || lastObj == null || lastObj is BananaShower)
|
||||
{
|
||||
IndexInCurrentCombo = 0;
|
||||
ComboIndex++;
|
||||
ComboIndexWithOffsets += ComboOffset + 1;
|
||||
|
||||
if (lastObj != null)
|
||||
lastObj.LastInCombo = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
for (int i = 1; i < vertices.Count; i++)
|
||||
{
|
||||
sliderPath.ControlPoints[^1].Type = PathType.Linear;
|
||||
sliderPath.ControlPoints[^1].Type = PathType.LINEAR;
|
||||
|
||||
float deltaX = vertices[i].X - lastPosition.X;
|
||||
double length = (vertices[i].Time - currentTime) * velocity;
|
||||
|
57
osu.Game.Rulesets.Catch/Scoring/CatchHealthProcessor.cs
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Scoring
|
||||
{
|
||||
public partial class CatchHealthProcessor : LegacyDrainingHealthProcessor
|
||||
{
|
||||
public CatchHealthProcessor(double drainStartTime)
|
||||
: base(drainStartTime)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateTopLevelHitObjects() => EnumerateHitObjects(Beatmap).Where(h => h is Fruit || h is Droplet || h is Banana);
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => Enumerable.Empty<HitObject>();
|
||||
|
||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||
{
|
||||
double increase = 0;
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.SmallTickMiss:
|
||||
return 0;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.Miss:
|
||||
return IBeatmapDifficultyInfo.DifficultyRange(Beatmap.Difficulty.DrainRate, -0.03, -0.125, -0.2);
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
increase = 0.0015;
|
||||
break;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
increase = 0.015;
|
||||
break;
|
||||
|
||||
case HitResult.Great:
|
||||
increase = 0.03;
|
||||
break;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
increase = 0.0025;
|
||||
break;
|
||||
}
|
||||
|
||||
return HpMultiplierNormal * increase;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,11 +4,19 @@
|
||||
using System;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Scoring
|
||||
{
|
||||
public partial class CatchScoreProcessor : ScoreProcessor
|
||||
{
|
||||
private const double accuracy_cutoff_x = 1;
|
||||
private const double accuracy_cutoff_s = 0.98;
|
||||
private const double accuracy_cutoff_a = 0.94;
|
||||
private const double accuracy_cutoff_b = 0.9;
|
||||
private const double accuracy_cutoff_c = 0.85;
|
||||
private const double accuracy_cutoff_d = 0;
|
||||
|
||||
private const int combo_cap = 200;
|
||||
private const double combo_base = 4;
|
||||
|
||||
@ -26,5 +34,50 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
=> Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
|
||||
public override ScoreRank RankFromAccuracy(double accuracy)
|
||||
{
|
||||
if (accuracy == accuracy_cutoff_x)
|
||||
return ScoreRank.X;
|
||||
if (accuracy >= accuracy_cutoff_s)
|
||||
return ScoreRank.S;
|
||||
if (accuracy >= accuracy_cutoff_a)
|
||||
return ScoreRank.A;
|
||||
if (accuracy >= accuracy_cutoff_b)
|
||||
return ScoreRank.B;
|
||||
if (accuracy >= accuracy_cutoff_c)
|
||||
return ScoreRank.C;
|
||||
|
||||
return ScoreRank.D;
|
||||
}
|
||||
|
||||
public override double AccuracyCutoffFromRank(ScoreRank rank)
|
||||
{
|
||||
switch (rank)
|
||||
{
|
||||
case ScoreRank.X:
|
||||
case ScoreRank.XH:
|
||||
return accuracy_cutoff_x;
|
||||
|
||||
case ScoreRank.S:
|
||||
case ScoreRank.SH:
|
||||
return accuracy_cutoff_s;
|
||||
|
||||
case ScoreRank.A:
|
||||
return accuracy_cutoff_a;
|
||||
|
||||
case ScoreRank.B:
|
||||
return accuracy_cutoff_b;
|
||||
|
||||
case ScoreRank.C:
|
||||
return accuracy_cutoff_c;
|
||||
|
||||
case ScoreRank.D:
|
||||
return accuracy_cutoff_d;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(rank), rank, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Default
|
||||
{
|
||||
@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
|
||||
|
||||
public DefaultCatcher()
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
InternalChild = sprite = new Sprite
|
||||
{
|
||||
@ -32,6 +34,15 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// matches stable's origin position since we're using the same catcher sprite.
|
||||
// see LegacyCatcher for more information.
|
||||
OriginPosition = new Vector2(DrawWidth / 2, 16f);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore store, Bindable<CatcherAnimationState> currentState)
|
||||
{
|
||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyBananaPiece : LegacyCatchHitObjectPiece
|
||||
{
|
||||
private static readonly Vector2 banana_max_size = new Vector2(128);
|
||||
private static readonly Vector2 banana_max_size = new Vector2(160);
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
|
33
osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatcher.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public abstract partial class LegacyCatcher : CompositeDrawable
|
||||
{
|
||||
protected LegacyCatcher()
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
// in stable, catcher sprites are displayed in their raw size. stable also has catcher sprites displayed with the following scale factors applied:
|
||||
// 1. 0.5x, affecting all sprites in the playfield, computed here based on lazer's catch playfield dimensions (see WIDTH/HEIGHT constants in CatchPlayfield),
|
||||
// source: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjectManager.cs#L483-L494
|
||||
// 2. 0.7x, a constant scale applied to all catcher sprites on construction.
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Scale = new Vector2(0.5f * 0.7f);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// stable sets the Y origin position of the catcher to 16px in order for the catching range and OD scaling to align with the top of the catcher's plate in the default skin.
|
||||
OriginPosition = new Vector2(DrawWidth / 2, 16f);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,14 +7,12 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyCatcherNew : CompositeDrawable
|
||||
public partial class LegacyCatcherNew : LegacyCatcher
|
||||
{
|
||||
[Resolved]
|
||||
private Bindable<CatcherAnimationState> currentState { get; set; } = null!;
|
||||
@ -23,25 +21,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
private Drawable currentDrawable = null!;
|
||||
|
||||
public LegacyCatcherNew()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
foreach (var state in Enum.GetValues<CatcherAnimationState>())
|
||||
{
|
||||
AddInternal(drawables[state] = getDrawableFor(state).With(d =>
|
||||
{
|
||||
d.Anchor = Anchor.TopCentre;
|
||||
d.Origin = Anchor.TopCentre;
|
||||
d.RelativeSizeAxes = Axes.Both;
|
||||
d.Size = Vector2.One;
|
||||
d.FillMode = FillMode.Fit;
|
||||
d.Alpha = 0;
|
||||
}));
|
||||
AddInternal(drawables[state] = getDrawableFor(state).With(d => d.Alpha = 0));
|
||||
}
|
||||
|
||||
currentDrawable = drawables[CatcherAnimationState.Idle];
|
||||
|
@ -3,30 +3,21 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyCatcherOld : CompositeDrawable
|
||||
public partial class LegacyCatcherOld : LegacyCatcher
|
||||
{
|
||||
public LegacyCatcherOld()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
InternalChild = (skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty()).With(d =>
|
||||
{
|
||||
d.Anchor = Anchor.TopCentre;
|
||||
d.Origin = Anchor.TopCentre;
|
||||
d.RelativeSizeAxes = Axes.Both;
|
||||
d.Size = Vector2.One;
|
||||
d.FillMode = FillMode.Fit;
|
||||
});
|
||||
InternalChild = skin.GetAnimation(@"fruit-ryuuta", true, true, true) ?? Empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece
|
||||
{
|
||||
private static readonly Vector2 droplet_max_size = new Vector2(82, 103);
|
||||
private static readonly Vector2 droplet_max_size = new Vector2(160);
|
||||
|
||||
public LegacyDropletPiece()
|
||||
{
|
||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
internal partial class LegacyFruitPiece : LegacyCatchHitObjectPiece
|
||||
{
|
||||
private static readonly Vector2 fruit_max_size = new Vector2(128);
|
||||
private static readonly Vector2 fruit_max_size = new Vector2(160);
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
|
@ -17,12 +17,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public CatchPlayfieldAdjustmentContainer()
|
||||
{
|
||||
// because we are using centre anchor/origin, we will need to limit visibility in the future
|
||||
// to ensure tall windows do not get a readability advantage.
|
||||
// it may be possible to bake the catch-specific offsets (-100..340 mentioned below) into new values
|
||||
// which are compatible with TopCentre alignment.
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
// playfields in stable are positioned vertically at three fourths the difference between the playfield height and the window height in stable.
|
||||
// we can match that in lazer by using relative coordinates for Y and considering window height to be 1, and playfield height to be 0.8.
|
||||
RelativePositionAxes = Axes.Y;
|
||||
Y = (1 - playfield_size_adjust) / 4 * 3;
|
||||
|
||||
Size = new Vector2(playfield_size_adjust);
|
||||
|
||||
@ -42,18 +43,28 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private partial class ScalingContainer : Container
|
||||
{
|
||||
public ScalingContainer()
|
||||
{
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.BottomCentre;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// in stable, fruit fall vertically from -100 to 340.
|
||||
// to emulate this, we want to make our playfield 440 gameplay pixels high.
|
||||
// we then offset it -100 vertically in the position set below.
|
||||
const float stable_v_offset_ratio = 440 / 384f;
|
||||
// in stable, fruit fall vertically from 100 pixels above the playfield top down to the catcher's Y position (i.e. -100 to 340),
|
||||
// see: https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/GameplayElements/HitObjects/Fruits/HitCircleFruits.cs#L65
|
||||
// we already have the playfield positioned similar to stable (see CatchPlayfieldAdjustmentContainer constructor),
|
||||
// so we only need to increase this container's height 100 pixels above the playfield, and offset it to have the bottom at 340 rather than 384.
|
||||
const float stable_fruit_start_position = -100;
|
||||
const float stable_catcher_y_position = 340;
|
||||
const float playfield_v_size_adjustment = (stable_catcher_y_position - stable_fruit_start_position) / CatchPlayfield.HEIGHT;
|
||||
const float playfield_v_catcher_offset = stable_catcher_y_position - CatchPlayfield.HEIGHT;
|
||||
|
||||
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
|
||||
Position = new Vector2(0, -100 * stable_v_offset_ratio + Scale.X);
|
||||
Size = Vector2.Divide(new Vector2(1, stable_v_offset_ratio), Scale);
|
||||
Scale = new Vector2(Parent!.ChildSize.X / CatchPlayfield.WIDTH);
|
||||
Position = new Vector2(0f, playfield_v_catcher_offset * Scale.Y);
|
||||
Size = Vector2.Divide(new Vector2(1, playfield_v_size_adjustment), Scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Catch.Skinning;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -29,6 +30,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <summary>
|
||||
/// The size of the catcher at 1x scale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is mainly used to compute catching range, the actual catcher size may differ based on skin implementation and sprite textures.
|
||||
/// This is also equivalent to the "catcherWidth" property in osu-stable when the game field and beatmap difficulty are set to default values.
|
||||
/// </remarks>
|
||||
/// <seealso cref="CatchPlayfield.WIDTH"/>
|
||||
/// <seealso cref="CatchPlayfield.HEIGHT"/>
|
||||
/// <seealso cref="IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY"/>
|
||||
public const float BASE_SIZE = 106.75f;
|
||||
|
||||
/// <summary>
|
||||
@ -175,11 +183,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
public Drawable CreateProxiedContent() => caughtObjectContainer.CreateProxy();
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
|
||||
/// </summary>
|
||||
private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the width of the area used for attempting catches in gameplay.
|
||||
/// </summary>
|
||||
@ -464,6 +467,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
d.Expire();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
|
||||
/// </summary>
|
||||
private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(LegacyRulesetExtensions.CalculateScaleFromCircleSize(difficulty.CircleSize) * 2);
|
||||
|
||||
private enum DroppedObjectAnimation
|
||||
{
|
||||
Drop,
|
||||
|
@ -6,7 +6,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
@ -26,8 +25,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
: base(new CatchSkinComponentLookup(CatchSkinComponents.Catcher), _ => new DefaultCatcher())
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
|
||||
OriginPosition = new Vector2(0.5f, 0.06f) * Catcher.BASE_SIZE;
|
||||
Origin = Anchor.TopCentre;
|
||||
CentreComponent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,94 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneOpenEditorTimestampInMania : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestNormalSelection()
|
||||
{
|
||||
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
|
||||
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)>
|
||||
{ (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) }
|
||||
));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
|
||||
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)>
|
||||
{ (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) }
|
||||
));
|
||||
|
||||
addReset();
|
||||
AddStep("add notes to row", () =>
|
||||
{
|
||||
if (EditorBeatmap.HitObjects.Any(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column is 1 or 2 or 3))
|
||||
return;
|
||||
|
||||
ManiaHitObject first = (ManiaHitObject)EditorBeatmap.HitObjects.First(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column == 0);
|
||||
ManiaHitObject second = new Note { Column = 1, StartTime = first.StartTime };
|
||||
ManiaHitObject third = new Note { Column = 2, StartTime = first.StartTime };
|
||||
ManiaHitObject forth = new Note { Column = 3, StartTime = first.StartTime };
|
||||
EditorBeatmap.AddRange(new[] { second, third, forth });
|
||||
});
|
||||
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
|
||||
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)>
|
||||
{ (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) }
|
||||
));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
|
||||
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)>
|
||||
{ (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) }
|
||||
));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnusualSelection()
|
||||
{
|
||||
addStepClickLink("00:00:000 (0|1)", "wrong offset");
|
||||
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:00:000 (2)", "std link");
|
||||
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:00:000 (1,2)", "std link");
|
||||
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
|
||||
}
|
||||
|
||||
private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true)
|
||||
{
|
||||
AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp));
|
||||
AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value);
|
||||
}
|
||||
|
||||
private void addReset() => addStepClickLink("00:00:000", "reset", false);
|
||||
|
||||
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null)
|
||||
{
|
||||
bool checkColumns = columnPairs != null
|
||||
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
|
||||
: !EditorBeatmap.SelectedHitObjects.Any();
|
||||
|
||||
return EditorClock.CurrentTime == startTime
|
||||
&& EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0)
|
||||
&& checkColumns;
|
||||
}
|
||||
|
||||
private bool isNoteAt(HitObject hitObject, double time, int column) =>
|
||||
hitObject is ManiaHitObject maniaHitObject
|
||||
&& maniaHitObject.StartTime == time
|
||||
&& maniaHitObject.Column == column;
|
||||
}
|
||||
}
|
31
osu.Game.Rulesets.Mania.Tests/ManiaHealthProcessorTest.cs
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ManiaHealthProcessorTest
|
||||
{
|
||||
[Test]
|
||||
public void TestNoDrain()
|
||||
{
|
||||
var processor = new ManiaHealthProcessor(0);
|
||||
processor.ApplyBeatmap(new ManiaBeatmap(new StageDefinition(4))
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note { StartTime = 0 },
|
||||
new Note { StartTime = 1000 },
|
||||
}
|
||||
});
|
||||
|
||||
// No matter what, mania doesn't have passive HP drain.
|
||||
Assert.That(processor.DrainRate, Is.Zero);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,12 +17,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
private const double offset = 18;
|
||||
|
||||
protected override bool AllowFail => true;
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestHitWindowWithoutDoubleTime() => CreateModTest(new ModTestData
|
||||
{
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value != 1,
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
|
||||
&& Player.ScoreProcessor.Accuracy.Value == 1
|
||||
&& Player.ScoreProcessor.TotalScore.Value == 1_000_000,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
@ -40,24 +44,31 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestHitWindowWithDoubleTime() => CreateModTest(new ModTestData
|
||||
public void TestHitWindowWithDoubleTime()
|
||||
{
|
||||
Mod = new ManiaModDoubleTime(),
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0 && Player.ScoreProcessor.Accuracy.Value == 1,
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
var doubleTime = new ManiaModDoubleTime();
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
Difficulty = { OverallDifficulty = 10 },
|
||||
HitObjects = new List<HitObject>
|
||||
Mod = doubleTime,
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits > 0
|
||||
&& Player.ScoreProcessor.Accuracy.Value == 1
|
||||
&& Player.ScoreProcessor.TotalScore.Value == (long)(1_000_010 * doubleTime.ScoreMultiplier),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
new Note { StartTime = 1000 }
|
||||
BeatmapInfo = { Ruleset = new ManiaRuleset().RulesetInfo },
|
||||
Difficulty = { OverallDifficulty = 10 },
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Note { StartTime = 1000 }
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||
}
|
||||
});
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(1000 + offset, ManiaAction.Key1)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.0 KiB |
@ -4,6 +4,7 @@ Version: 2.5
|
||||
[Mania]
|
||||
Keys: 4
|
||||
ColumnLineWidth: 3,1,3,1,1
|
||||
LightFramePerSecond: 15
|
||||
// some skins found in the wild had configuration keys where the @2x suffix was included in the values.
|
||||
// the expected compatibility behaviour is that the presence of the @2x suffix shouldn't change anything
|
||||
// if @2x assets are present.
|
||||
@ -15,5 +16,6 @@ Hit300: mania/hit300@2x
|
||||
Hit300g: mania/hit300g@2x
|
||||
StageLeft: mania/stage-left
|
||||
StageRight: mania/stage-right
|
||||
StageLight: mania/stage-light
|
||||
NoteImage0L: LongNoteTailWang
|
||||
NoteImage1L: LongNoteTailWang
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
c.Add(hitExplosionPools[poolIndex].Get(e =>
|
||||
{
|
||||
e.Apply(new JudgementResult(new HitObject(), runCount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
|
||||
e.Apply(new JudgementResult(new HitObject(), new ManiaJudgement()));
|
||||
|
||||
e.Anchor = Anchor.Centre;
|
||||
e.Origin = Anchor.Centre;
|
||||
|
@ -54,7 +54,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
assertNoteJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
@ -73,7 +72,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
assertNoteJudgement(HitResult.IgnoreHit);
|
||||
}
|
||||
@ -92,7 +90,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
assertNoteJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
@ -111,7 +108,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@ -129,7 +125,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@ -149,7 +144,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@ -169,7 +163,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Perfect);
|
||||
}
|
||||
|
||||
@ -188,10 +181,33 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xox o
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestPressAtStartThenReleaseAndImmediatelyRepress()
|
||||
{
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_head + 1),
|
||||
new ManiaReplayFrame(time_head + 2, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_tail),
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
|
||||
assertComboAtJudgement(1, 1);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
assertComboAtJudgement(2, 0);
|
||||
// judgement combo offset by perfect bonus judgement. see logic in DrawableNote.CheckForResult.
|
||||
assertComboAtJudgement(4, 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -----[ ]-----
|
||||
/// xo x o
|
||||
@ -208,7 +224,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@ -228,7 +243,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
@ -246,7 +260,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
}
|
||||
|
||||
@ -264,7 +277,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickHit);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
@ -358,7 +370,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
|
||||
assertHitObjectJudgement(note, HitResult.Good);
|
||||
@ -371,7 +382,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[Test]
|
||||
public void TestPressAndReleaseJustAfterTailWithNearbyNote()
|
||||
{
|
||||
Note note;
|
||||
// Next note within tail lenience
|
||||
Note note = new Note { StartTime = time_tail + 50 };
|
||||
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
@ -383,13 +395,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
Duration = time_tail - time_head,
|
||||
Column = 0,
|
||||
},
|
||||
{
|
||||
// Next note within tail lenience
|
||||
note = new Note
|
||||
{
|
||||
StartTime = time_tail + 50
|
||||
}
|
||||
}
|
||||
note
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
@ -405,7 +411,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
|
||||
assertHitObjectJudgement(note, HitResult.Great);
|
||||
@ -425,7 +430,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
});
|
||||
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Meh);
|
||||
}
|
||||
|
||||
@ -476,42 +480,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
.All(j => j.Type.IsHit()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitTailBeforeLastTick()
|
||||
{
|
||||
const int tick_rate = 8;
|
||||
const double tick_spacing = TimingControlPoint.DEFAULT_BEAT_LENGTH / tick_rate;
|
||||
const double time_last_tick = time_head + tick_spacing * (int)((time_tail - time_head) / tick_spacing - 1);
|
||||
|
||||
var beatmap = new Beatmap<ManiaHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HoldNote
|
||||
{
|
||||
StartTime = time_head,
|
||||
Duration = time_tail - time_head,
|
||||
Column = 0,
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { SliderTickRate = tick_rate },
|
||||
Ruleset = new ManiaRuleset().RulesetInfo
|
||||
},
|
||||
};
|
||||
|
||||
performTest(new List<ReplayFrame>
|
||||
{
|
||||
new ManiaReplayFrame(time_head, ManiaAction.Key1),
|
||||
new ManiaReplayFrame(time_last_tick - 5)
|
||||
}, beatmap);
|
||||
|
||||
assertHeadJudgement(HitResult.Perfect);
|
||||
assertLastTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Ok);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZeroLength()
|
||||
{
|
||||
@ -551,11 +519,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
private void assertNoteJudgement(HitResult result)
|
||||
=> AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
|
||||
|
||||
private void assertTickJudgement(HitResult result)
|
||||
=> AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Select(j => j.Type), () => Does.Contain(result));
|
||||
|
||||
private void assertLastTickJudgement(HitResult result)
|
||||
=> AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type, () => Is.EqualTo(result));
|
||||
private void assertComboAtJudgement(int judgementIndex, int combo)
|
||||
=> AddAssert($"combo at judgement {judgementIndex} is {combo}", () => judgementResults.ElementAt(judgementIndex).ComboAfterJudgement, () => Is.EqualTo(combo));
|
||||
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AddAssert("all objects perfectly judged",
|
||||
() => judgementResults.Select(result => result.Type),
|
||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_030));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AddAssert("all objects perfectly judged",
|
||||
() => judgementResults.Select(result => result.Type),
|
||||
() => Is.EquivalentTo(judgementResults.Select(result => result.Judgement.MaxResult)));
|
||||
AddAssert("score is 1 million", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_000));
|
||||
AddAssert("score is correct", () => currentPlayer.ScoreProcessor.TotalScore.Value, () => Is.EqualTo(1_000_040));
|
||||
}
|
||||
|
||||
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -9,7 +11,9 @@ using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Tests.Visual.Gameplay;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
@ -25,9 +29,11 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(MaxCombo.Value);
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new ManiaProcessorBasedScoringAlgorithm(beatmap, mode);
|
||||
protected override IScoringAlgorithm CreateScoreV1(IReadOnlyList<Mod> selectedMods) => new ScoreV1(MaxCombo.Value, selectedMods);
|
||||
protected override IScoringAlgorithm CreateScoreV2(int maxCombo, IReadOnlyList<Mod> selectedMods) => new ScoreV2(maxCombo, selectedMods);
|
||||
|
||||
protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList<Mod> selectedMods)
|
||||
=> new ManiaProcessorBasedScoringAlgorithm(beatmap, mode, selectedMods);
|
||||
|
||||
[Test]
|
||||
public void TestBasicScenarios()
|
||||
@ -59,11 +65,17 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
private int currentCombo;
|
||||
private double comboAddition = 100;
|
||||
private double totalScoreDouble;
|
||||
|
||||
private readonly double scoreMultiplier;
|
||||
|
||||
public ScoreV1(int maxCombo)
|
||||
public ScoreV1(int maxCombo, IReadOnlyList<Mod> selectedMods)
|
||||
{
|
||||
scoreMultiplier = 500000d / maxCombo;
|
||||
var ruleset = new ManiaRuleset();
|
||||
|
||||
scoreMultiplier = 500000d / maxCombo * ruleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(selectedMods, new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = ruleset.RulesetInfo
|
||||
});
|
||||
}
|
||||
|
||||
public void ApplyHit() => applyHitV1(320, add => add + 2, 32);
|
||||
@ -103,13 +115,22 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
private readonly double comboPortionMax;
|
||||
private readonly int maxCombo;
|
||||
private readonly double modMultiplier;
|
||||
|
||||
private const double combo_base = 4;
|
||||
|
||||
public ScoreV2(int maxCombo)
|
||||
public ScoreV2(int maxCombo, IReadOnlyList<Mod> selectedMods)
|
||||
{
|
||||
this.maxCombo = maxCombo;
|
||||
|
||||
var ruleset = new ManiaRuleset();
|
||||
modMultiplier = new ManiaRuleset().CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(
|
||||
selectedMods.Append(new ModScoreV2()).ToArray(),
|
||||
new LegacyBeatmapConversionDifficultyInfo
|
||||
{
|
||||
SourceRuleset = ruleset.RulesetInfo
|
||||
});
|
||||
|
||||
for (int i = 0; i < this.maxCombo; i++)
|
||||
ApplyHit();
|
||||
|
||||
@ -148,18 +169,18 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
float accuracy = (float)(currentBaseScore / maxBaseScore);
|
||||
|
||||
return (int)Math.Round
|
||||
(
|
||||
((
|
||||
200000 * comboPortion / comboPortionMax +
|
||||
800000 * Math.Pow(accuracy, 2 + 2 * accuracy) * ((double)currentHits / maxCombo)
|
||||
);
|
||||
) * modMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ManiaProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm
|
||||
{
|
||||
public ManiaProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode)
|
||||
: base(beatmap, mode)
|
||||
public ManiaProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode, IReadOnlyList<Mod> selectedMods)
|
||||
: base(beatmap, mode, selectedMods)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
@ -43,39 +44,41 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
: base(beatmap, ruleset)
|
||||
{
|
||||
IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
|
||||
TargetColumns = GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap));
|
||||
|
||||
double roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize);
|
||||
double roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
if (IsForCurrentRuleset)
|
||||
if (IsForCurrentRuleset && TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
|
||||
{
|
||||
TargetColumns = GetColumnCountForNonConvert(beatmap.BeatmapInfo);
|
||||
|
||||
if (TargetColumns > ManiaRuleset.MAX_STAGE_KEYS)
|
||||
{
|
||||
TargetColumns /= 2;
|
||||
Dual = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasDuration) / beatmap.HitObjects.Count;
|
||||
if (percentSliderOrSpinner < 0.2)
|
||||
TargetColumns = 7;
|
||||
else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)
|
||||
TargetColumns = roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
else if (percentSliderOrSpinner > 0.6)
|
||||
TargetColumns = roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
else
|
||||
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||
TargetColumns /= 2;
|
||||
Dual = true;
|
||||
}
|
||||
|
||||
originalTargetColumns = TargetColumns;
|
||||
}
|
||||
|
||||
public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo)
|
||||
public static int GetColumnCount(LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
double roundedCircleSize = Math.Round(beatmapInfo.Difficulty.CircleSize);
|
||||
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
|
||||
return GetColumnCountForNonConvert(difficulty);
|
||||
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
double roundedOverallDifficulty = Math.Round(difficulty.OverallDifficulty);
|
||||
|
||||
int countSliderOrSpinner = difficulty.TotalObjectCount - difficulty.CircleCount;
|
||||
float percentSpecialObjects = (float)countSliderOrSpinner / difficulty.TotalObjectCount;
|
||||
|
||||
if (percentSpecialObjects < 0.2)
|
||||
return 7;
|
||||
if (percentSpecialObjects < 0.3 || roundedCircleSize >= 5)
|
||||
return roundedOverallDifficulty > 5 ? 7 : 6;
|
||||
if (percentSpecialObjects > 0.6)
|
||||
return roundedOverallDifficulty > 4 ? 5 : 4;
|
||||
|
||||
return Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
|
||||
}
|
||||
|
||||
public static int GetColumnCountForNonConvert(IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
double roundedCircleSize = Math.Round(difficulty.CircleSize);
|
||||
return (int)Math.Max(1, roundedCircleSize);
|
||||
}
|
||||
|
||||
@ -171,9 +174,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
switch (original)
|
||||
{
|
||||
case IHasDistance:
|
||||
case IHasPath:
|
||||
{
|
||||
var generator = new DistanceObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
|
||||
var generator = new PathObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
|
||||
conversion = generator;
|
||||
|
||||
var positionData = original as IHasPosition;
|
||||
|
@ -22,13 +22,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
/// <summary>
|
||||
/// A pattern generator for IHasDistance hit objects.
|
||||
/// </summary>
|
||||
internal class DistanceObjectPatternGenerator : PatternGenerator
|
||||
internal class PathObjectPatternGenerator : PatternGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Base osu! slider scoring distance.
|
||||
/// </summary>
|
||||
private const float osu_base_scoring_distance = 100;
|
||||
|
||||
public readonly int StartTime;
|
||||
public readonly int EndTime;
|
||||
public readonly int SegmentDuration;
|
||||
@ -36,17 +31,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
|
||||
private PatternType convertType;
|
||||
|
||||
public DistanceObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
|
||||
public PathObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
|
||||
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
|
||||
{
|
||||
convertType = PatternType.None;
|
||||
if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)
|
||||
convertType = PatternType.LowProbability;
|
||||
|
||||
var distanceData = hitObject as IHasDistance;
|
||||
var pathData = hitObject as IHasPath;
|
||||
var repeatsData = hitObject as IHasRepeats;
|
||||
|
||||
Debug.Assert(distanceData != null);
|
||||
Debug.Assert(pathData != null);
|
||||
|
||||
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
|
||||
|
||||
@ -60,8 +55,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
SpanCount = repeatsData?.SpanCount() ?? 1;
|
||||
StartTime = (int)Math.Round(hitObject.StartTime);
|
||||
|
||||
double distance = pathData.Path.ExpectedDistance.Value ?? 0;
|
||||
|
||||
// This matches stable's calculation.
|
||||
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
|
||||
EndTime = (int)Math.Floor(StartTime + distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier);
|
||||
|
||||
SegmentDuration = (EndTime - StartTime) / SpanCount;
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
@ -12,5 +17,50 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
return new LegacyScoreAttributes { ComboScore = 1000000 };
|
||||
}
|
||||
|
||||
public double GetLegacyScoreMultiplier(IReadOnlyList<Mod> mods, LegacyBeatmapConversionDifficultyInfo difficulty)
|
||||
{
|
||||
bool scoreV2 = mods.Any(m => m is ModScoreV2);
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
foreach (var mod in mods)
|
||||
{
|
||||
switch (mod)
|
||||
{
|
||||
case ManiaModNoFail:
|
||||
multiplier *= scoreV2 ? 1.0 : 0.5;
|
||||
break;
|
||||
|
||||
case ManiaModEasy:
|
||||
multiplier *= 0.5;
|
||||
break;
|
||||
|
||||
case ManiaModHalfTime:
|
||||
case ManiaModDaycore:
|
||||
multiplier *= 0.5;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (new ManiaRuleset().RulesetInfo.Equals(difficulty.SourceRuleset))
|
||||
return multiplier;
|
||||
|
||||
// Apply key mod multipliers.
|
||||
|
||||
int originalColumns = ManiaBeatmapConverter.GetColumnCount(difficulty);
|
||||
int actualColumns = originalColumns;
|
||||
|
||||
actualColumns = mods.OfType<ManiaKeyMod>().SingleOrDefault()?.KeyCount ?? actualColumns;
|
||||
if (mods.Any(m => m is ManiaModDualStages))
|
||||
actualColumns *= 2;
|
||||
|
||||
if (actualColumns > originalColumns)
|
||||
multiplier *= 0.9;
|
||||
else if (actualColumns < originalColumns)
|
||||
multiplier *= 0.9 - 0.04 * (originalColumns - actualColumns);
|
||||
|
||||
return multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,8 +44,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
|
||||
if (Column != null)
|
||||
{
|
||||
headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y;
|
||||
tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y;
|
||||
headPiece.Y = Parent!.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y;
|
||||
tailPiece.Y = Parent!.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y;
|
||||
|
||||
switch (scrollingInfo.Direction.Value)
|
||||
{
|
||||
|
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
foreach (var child in InternalChildren)
|
||||
child.Anchor = child.Origin = anchor;
|
||||
|
||||
Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
|
||||
Position = Parent!.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
|
||||
Width = HitObjectContainer.DrawWidth;
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset, ISupportConstantAlgorithmToggle
|
||||
{
|
||||
public BindableBool ShowSpeedChanges { get; set; } = new BindableBool();
|
||||
public BindableBool ShowSpeedChanges { get; } = new BindableBool();
|
||||
|
||||
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
|
||||
|
||||
|
@ -1,204 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
|
||||
/// </summary>
|
||||
public partial class ManiaBeatSnapGrid : Component
|
||||
public partial class ManiaBeatSnapGrid : BeatSnapGrid
|
||||
{
|
||||
private const double visible_range = 750;
|
||||
|
||||
/// <summary>
|
||||
/// The range of time values of the current selection.
|
||||
/// </summary>
|
||||
public (double start, double end)? SelectionTimeRange
|
||||
protected override IEnumerable<Container> GetTargetContainers(HitObjectComposer composer)
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == selectionTimeRange)
|
||||
return;
|
||||
|
||||
selectionTimeRange = value;
|
||||
lineCache.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BindableBeatDivisor beatDivisor { get; set; } = null!;
|
||||
|
||||
private readonly List<ScrollingHitObjectContainer> grids = new List<ScrollingHitObjectContainer>();
|
||||
|
||||
private readonly Cached lineCache = new Cached();
|
||||
|
||||
private (double start, double end)? selectionTimeRange;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(HitObjectComposer composer)
|
||||
{
|
||||
foreach (var stage in ((ManiaPlayfield)composer.Playfield).Stages)
|
||||
{
|
||||
foreach (var column in stage.Columns)
|
||||
{
|
||||
var lineContainer = new ScrollingHitObjectContainer();
|
||||
|
||||
grids.Add(lineContainer);
|
||||
column.UnderlayElements.Add(lineContainer);
|
||||
}
|
||||
}
|
||||
|
||||
beatDivisor.BindValueChanged(_ => createLines(), true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!lineCache.IsValid)
|
||||
{
|
||||
lineCache.Validate();
|
||||
createLines();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Stack<DrawableGridLine> availableLines = new Stack<DrawableGridLine>();
|
||||
|
||||
private void createLines()
|
||||
{
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
foreach (var line in grid.Objects.OfType<DrawableGridLine>())
|
||||
availableLines.Push(line);
|
||||
|
||||
grid.Clear();
|
||||
}
|
||||
|
||||
if (selectionTimeRange == null)
|
||||
return;
|
||||
|
||||
var range = selectionTimeRange.Value;
|
||||
|
||||
var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);
|
||||
|
||||
double time = timingPoint.Time;
|
||||
int beat = 0;
|
||||
|
||||
// progress time until in the visible range.
|
||||
while (time < range.start - visible_range)
|
||||
{
|
||||
time += timingPoint.BeatLength / beatDivisor.Value;
|
||||
beat++;
|
||||
}
|
||||
|
||||
while (time < range.end + visible_range)
|
||||
{
|
||||
var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);
|
||||
|
||||
// switch to the next timing point if we have reached it.
|
||||
if (nextTimingPoint.Time > timingPoint.Time)
|
||||
{
|
||||
beat = 0;
|
||||
time = nextTimingPoint.Time;
|
||||
timingPoint = nextTimingPoint;
|
||||
}
|
||||
|
||||
Color4 colour = BindableBeatDivisor.GetColourFor(
|
||||
BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);
|
||||
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
if (!availableLines.TryPop(out var line))
|
||||
line = new DrawableGridLine();
|
||||
|
||||
line.HitObject.StartTime = time;
|
||||
line.Colour = colour;
|
||||
|
||||
grid.Add(line);
|
||||
}
|
||||
|
||||
beat++;
|
||||
time += timingPoint.BeatLength / beatDivisor.Value;
|
||||
}
|
||||
|
||||
foreach (var grid in grids)
|
||||
{
|
||||
// required to update ScrollingHitObjectContainer's cache.
|
||||
grid.UpdateSubTree();
|
||||
|
||||
foreach (var line in grid.Objects.OfType<DrawableGridLine>())
|
||||
{
|
||||
time = line.HitObject.StartTime;
|
||||
|
||||
if (time >= range.start && time <= range.end)
|
||||
line.Alpha = 1;
|
||||
else
|
||||
{
|
||||
double timeSeparation = time < range.start ? range.start - time : time - range.end;
|
||||
line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class DrawableGridLine : DrawableHitObject
|
||||
{
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; } = null!;
|
||||
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
public DrawableGridLine()
|
||||
: base(new HitObject())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 2;
|
||||
|
||||
AddInternal(new Box { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(onDirectionChanged, true);
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
Origin = Anchor = direction.NewValue == ScrollingDirection.Up
|
||||
? Anchor.TopLeft
|
||||
: Anchor.BottomLeft;
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
// don't perform any fading – we are handling that ourselves.
|
||||
LifetimeEnd = HitObject.StartTime + visible_range;
|
||||
}
|
||||
return ((ManiaPlayfield)composer.Playfield)
|
||||
.Stages
|
||||
.SelectMany(stage => stage.Columns)
|
||||
.Select(column => column.UnderlayElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,15 +5,13 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
@ -24,32 +22,12 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
|
||||
{
|
||||
private DrawableManiaEditorRuleset drawableRuleset;
|
||||
private ManiaBeatSnapGrid beatSnapGrid;
|
||||
private InputManager inputManager;
|
||||
|
||||
public ManiaHitObjectComposer(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(beatSnapGrid = new ManiaBeatSnapGrid());
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
}
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
|
||||
|
||||
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
|
||||
@ -57,49 +35,53 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
|
||||
Playfield.GetColumnByPosition(screenSpacePosition);
|
||||
|
||||
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
protected override DrawableRuleset<ManiaHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
|
||||
drawableRuleset = new DrawableManiaEditorRuleset(ruleset, beatmap, mods);
|
||||
|
||||
// This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it
|
||||
dependencies.CacheAs(drawableRuleset.ScrollingInfo);
|
||||
|
||||
return drawableRuleset;
|
||||
}
|
||||
|
||||
protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
||||
=> new ManiaBlueprintContainer(this);
|
||||
|
||||
protected override BeatSnapGrid CreateBeatSnapGrid() => new ManiaBeatSnapGrid();
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
{
|
||||
new NoteCompositionTool(),
|
||||
new HoldNoteCompositionTool()
|
||||
};
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (BlueprintContainer.CurrentTool is SelectTool)
|
||||
{
|
||||
if (EditorBeatmap.SelectedHitObjects.Any())
|
||||
{
|
||||
beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
|
||||
}
|
||||
else
|
||||
beatSnapGrid.SelectionTimeRange = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
|
||||
if (result.Time is double time)
|
||||
beatSnapGrid.SelectionTimeRange = (time, time);
|
||||
else
|
||||
beatSnapGrid.SelectionTimeRange = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
|
||||
|
||||
// 123|0,456|1,789|2 ...
|
||||
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
|
||||
|
||||
public override void SelectFromTimestamp(double timestamp, string objectDescription)
|
||||
{
|
||||
if (!selection_regex.IsMatch(objectDescription))
|
||||
return;
|
||||
|
||||
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
||||
string[] objectDescriptions = objectDescription.Split(',').ToArray();
|
||||
|
||||
for (int i = 0; i < objectDescriptions.Length; i++)
|
||||
{
|
||||
string[] split = objectDescriptions[i].Split('|').ToArray();
|
||||
if (split.Length != 2)
|
||||
continue;
|
||||
|
||||
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
|
||||
continue;
|
||||
|
||||
ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
|
||||
|
||||
if (current == null)
|
||||
continue;
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.Add(current);
|
||||
|
||||
if (i < objectDescriptions.Length - 1)
|
||||
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
13
osu.Game.Rulesets.Mania/Judgements/HoldNoteBodyJudgement.cs
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
public class HoldNoteBodyJudgement : ManiaJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.IgnoreHit;
|
||||
public override HitResult MinResult => HitResult.ComboBreak;
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
public class HoldNoteTickJudgement : ManiaJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.LargeTickHit;
|
||||
}
|
||||
}
|
@ -12,12 +12,6 @@ namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
|
||||
case HitResult.Meh:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
public bool Matches(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo)));
|
||||
return !keys.HasFilter || (beatmapInfo.Ruleset.OnlineID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo.Difficulty)));
|
||||
}
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
|
@ -255,16 +255,6 @@ namespace osu.Game.Rulesets.Mania
|
||||
case ModType.Conversion:
|
||||
return new Mod[]
|
||||
{
|
||||
new MultiMod(new ManiaModKey4(),
|
||||
new ManiaModKey5(),
|
||||
new ManiaModKey6(),
|
||||
new ManiaModKey7(),
|
||||
new ManiaModKey8(),
|
||||
new ManiaModKey9(),
|
||||
new ManiaModKey10(),
|
||||
new ManiaModKey1(),
|
||||
new ManiaModKey2(),
|
||||
new ManiaModKey3()),
|
||||
new ManiaModRandom(),
|
||||
new ManiaModDualStages(),
|
||||
new ManiaModMirror(),
|
||||
@ -272,7 +262,19 @@ namespace osu.Game.Rulesets.Mania
|
||||
new ManiaModClassic(),
|
||||
new ManiaModInvert(),
|
||||
new ManiaModConstantSpeed(),
|
||||
new ManiaModHoldOff()
|
||||
new ManiaModHoldOff(),
|
||||
new MultiMod(
|
||||
new ManiaModKey1(),
|
||||
new ManiaModKey2(),
|
||||
new ManiaModKey3(),
|
||||
new ManiaModKey4(),
|
||||
new ManiaModKey5(),
|
||||
new ManiaModKey6(),
|
||||
new ManiaModKey7(),
|
||||
new ManiaModKey8(),
|
||||
new ManiaModKey9(),
|
||||
new ManiaModKey10()
|
||||
),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
@ -386,21 +388,11 @@ namespace osu.Game.Rulesets.Mania
|
||||
HitResult.Ok,
|
||||
HitResult.Meh,
|
||||
|
||||
HitResult.LargeTickHit,
|
||||
// HitResult.SmallBonus is used for awarding perfect bonus score but is not included here as
|
||||
// it would be a bit redundant to show this to the user.
|
||||
};
|
||||
}
|
||||
|
||||
public override LocalisableString GetDisplayNameForHitResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
return "hold tick";
|
||||
}
|
||||
|
||||
return base.GetDisplayNameForHitResult(result);
|
||||
}
|
||||
|
||||
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
|
||||
{
|
||||
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
|
||||
{
|
||||
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
|
||||
Container hocParent = (Container)hoc.Parent;
|
||||
Container hocParent = (Container)hoc.Parent!;
|
||||
|
||||
hocParent.Remove(hoc, false);
|
||||
hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
|
||||
|
@ -35,10 +35,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
public DrawableHoldNoteHead Head => headContainer.Child;
|
||||
public DrawableHoldNoteTail Tail => tailContainer.Child;
|
||||
public DrawableHoldNoteBody Body => bodyContainer.Child;
|
||||
|
||||
private Container<DrawableHoldNoteHead> headContainer;
|
||||
private Container<DrawableHoldNoteTail> tailContainer;
|
||||
private Container<DrawableHoldNoteTick> tickContainer;
|
||||
private Container<DrawableHoldNoteBody> bodyContainer;
|
||||
|
||||
private PausableSkinnableSound slidingSample;
|
||||
|
||||
@ -60,12 +61,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
public double? HoldStartTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
|
||||
/// </summary>
|
||||
public double? HoldBrokenTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the hold note has been released potentially without having caused a break.
|
||||
/// Used to decide whether to visually clamp the hold note to the judgement line.
|
||||
/// </summary>
|
||||
private double? releaseTime;
|
||||
|
||||
@ -103,6 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
headContainer = new Container<DrawableHoldNoteHead> { RelativeSizeAxes = Axes.Both }
|
||||
}
|
||||
},
|
||||
bodyContainer = new Container<DrawableHoldNoteBody> { RelativeSizeAxes = Axes.Both },
|
||||
bodyPiece = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -110,15 +107,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
},
|
||||
tickContainer = new Container<DrawableHoldNoteTick> { RelativeSizeAxes = Axes.Both },
|
||||
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
|
||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
||||
slidingSample = new PausableSkinnableSound
|
||||
{
|
||||
Looping = true,
|
||||
MinimumSampleVolume = MINIMUM_SAMPLE_VOLUME,
|
||||
}
|
||||
});
|
||||
|
||||
maskedContents.AddRange(new[]
|
||||
{
|
||||
bodyPiece.CreateProxy(),
|
||||
tickContainer.CreateProxy(),
|
||||
tailContainer.CreateProxy(),
|
||||
});
|
||||
}
|
||||
@ -136,7 +135,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
sizingContainer.Size = Vector2.One;
|
||||
HoldStartTime = null;
|
||||
HoldBrokenTime = null;
|
||||
releaseTime = null;
|
||||
}
|
||||
|
||||
@ -154,8 +152,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
tailContainer.Child = tail;
|
||||
break;
|
||||
|
||||
case DrawableHoldNoteTick tick:
|
||||
tickContainer.Add(tick);
|
||||
case DrawableHoldNoteBody body:
|
||||
bodyContainer.Child = body;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -165,7 +163,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
base.ClearNestedHitObjects();
|
||||
headContainer.Clear(false);
|
||||
tailContainer.Clear(false);
|
||||
tickContainer.Clear(false);
|
||||
bodyContainer.Clear(false);
|
||||
}
|
||||
|
||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||
@ -178,8 +176,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
case HeadNote head:
|
||||
return new DrawableHoldNoteHead(head);
|
||||
|
||||
case HoldNoteTick tick:
|
||||
return new DrawableHoldNoteTick(tick);
|
||||
case HoldNoteBody body:
|
||||
return new DrawableHoldNoteBody(body);
|
||||
}
|
||||
|
||||
return base.CreateNestedHitObject(hitObject);
|
||||
@ -266,20 +264,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
if (Tail.AllJudged)
|
||||
{
|
||||
foreach (var tick in tickContainer)
|
||||
{
|
||||
if (!tick.Judged)
|
||||
tick.MissForcefully();
|
||||
}
|
||||
|
||||
if (Tail.IsHit)
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
else
|
||||
MissForcefully();
|
||||
}
|
||||
|
||||
if (Tail.Judged && !Tail.IsHit)
|
||||
HoldBrokenTime = Time.Current;
|
||||
// Make sure that the hold note is fully judged by giving the body a judgement.
|
||||
if (Tail.AllJudged && !Body.AllJudged)
|
||||
Body.TriggerResult(Tail.IsHit);
|
||||
}
|
||||
|
||||
public override void MissForcefully()
|
||||
@ -333,22 +326,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (e.Action != Action.Value)
|
||||
return;
|
||||
|
||||
// Make sure a hold was started
|
||||
if (HoldStartTime == null)
|
||||
return;
|
||||
|
||||
// do not run any of this logic when rewinding, as it inverts order of presses/releases.
|
||||
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||
return;
|
||||
|
||||
Tail.UpdateResult();
|
||||
endHold();
|
||||
// When our action is released and we are in the middle of a hold, there's a chance that
|
||||
// the user has released too early (before the tail).
|
||||
//
|
||||
// In such a case, we want to record this against the DrawableHoldNoteBody.
|
||||
if (HoldStartTime != null)
|
||||
{
|
||||
Tail.UpdateResult();
|
||||
Body.TriggerResult(Tail.IsHit);
|
||||
|
||||
// If the key has been released too early, the user should not receive full score for the release
|
||||
if (!Tail.IsHit)
|
||||
HoldBrokenTime = Time.Current;
|
||||
|
||||
releaseTime = Time.Current;
|
||||
endHold();
|
||||
releaseTime = Time.Current;
|
||||
}
|
||||
}
|
||||
|
||||
private void endHold()
|
||||
|
@ -0,0 +1,31 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableHoldNoteBody : DrawableManiaHitObject<HoldNoteBody>
|
||||
{
|
||||
public bool HasHoldBreak => AllJudged && !IsHit;
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public DrawableHoldNoteBody()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableHoldNoteBody(HoldNoteBody hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
|
||||
internal void TriggerResult(bool hit)
|
||||
{
|
||||
if (AllJudged) return;
|
||||
|
||||
ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -33,33 +32,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
public void UpdateResult() => base.UpdateResult(true);
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
Debug.Assert(HitObject.HitWindows != null);
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset) =>
|
||||
// Factor in the release lenience
|
||||
timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE;
|
||||
base.CheckForResult(userTriggered, timeOffset / TailNote.RELEASE_WINDOW_LENIENCE);
|
||||
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
protected override HitResult GetCappedResult(HitResult result)
|
||||
{
|
||||
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
|
||||
bool hasComboBreak = !HoldNote.Head.IsHit || HoldNote.Body.HasHoldBreak;
|
||||
|
||||
return;
|
||||
}
|
||||
if (result > HitResult.Meh && hasComboBreak)
|
||||
return HitResult.Meh;
|
||||
|
||||
var result = HitObject.HitWindows.ResultFor(timeOffset);
|
||||
if (result == HitResult.None)
|
||||
return;
|
||||
|
||||
ApplyResult(r =>
|
||||
{
|
||||
// If the head wasn't hit or the hold note was broken, cap the max score to Meh.
|
||||
if (result > HitResult.Meh && (!HoldNote.Head.IsHit || HoldNote.HoldBrokenTime != null))
|
||||
result = HitResult.Meh;
|
||||
|
||||
r.Type = result;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<ManiaAction> e) => false; // Handled by the hold note
|
||||
|
@ -1,110 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
/// <summary>
|
||||
/// Visualises a <see cref="HoldNoteTick"/> hit object.
|
||||
/// </summary>
|
||||
public partial class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
|
||||
{
|
||||
/// <summary>
|
||||
/// References the time at which the user started holding the hold note.
|
||||
/// </summary>
|
||||
private Func<double?> holdStartTime;
|
||||
|
||||
private Container glowContainer;
|
||||
|
||||
public DrawableHoldNoteTick()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableHoldNoteTick(HoldNoteTick hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(glowContainer = new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
AccentColour.BindValueChanged(colour =>
|
||||
{
|
||||
glowContainer.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Radius = 2f,
|
||||
Roundness = 15f,
|
||||
Colour = colour.NewValue.Opacity(0.3f)
|
||||
};
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
{
|
||||
base.OnApply();
|
||||
|
||||
Debug.Assert(ParentHitObject != null);
|
||||
|
||||
var holdNote = (DrawableHoldNote)ParentHitObject;
|
||||
holdStartTime = () => holdNote.HoldStartTime;
|
||||
}
|
||||
|
||||
protected override void OnFree()
|
||||
{
|
||||
base.OnFree();
|
||||
|
||||
holdStartTime = null;
|
||||
}
|
||||
|
||||
protected override void CheckForResult(bool userTriggered, double timeOffset)
|
||||
{
|
||||
if (Time.Current < HitObject.StartTime)
|
||||
return;
|
||||
|
||||
double? startTime = holdStartTime?.Invoke();
|
||||
|
||||
if (startTime == null || startTime > HitObject.StartTime)
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
else
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,8 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
private Drawable headPiece;
|
||||
|
||||
private DrawableNotePerfectBonus perfectBonus;
|
||||
|
||||
public DrawableNote()
|
||||
: this(null)
|
||||
{
|
||||
@ -89,7 +93,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (!userTriggered)
|
||||
{
|
||||
if (!HitObject.HitWindows.CanBeHit(timeOffset))
|
||||
{
|
||||
perfectBonus.TriggerResult(false);
|
||||
ApplyResult(r => r.Type = r.Judgement.MinResult);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -97,9 +105,23 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
if (result == HitResult.None)
|
||||
return;
|
||||
|
||||
result = GetCappedResult(result);
|
||||
|
||||
perfectBonus.TriggerResult(result == HitResult.Perfect);
|
||||
ApplyResult(r => r.Type = result);
|
||||
}
|
||||
|
||||
public override void MissForcefully()
|
||||
{
|
||||
perfectBonus.TriggerResult(false);
|
||||
base.MissForcefully();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some objects in mania may want to limit the max result.
|
||||
/// </summary>
|
||||
protected virtual HitResult GetCappedResult(HitResult result) => result;
|
||||
|
||||
public virtual bool OnPressed(KeyBindingPressEvent<ManiaAction> e)
|
||||
{
|
||||
if (e.Action != Action.Value)
|
||||
@ -115,6 +137,32 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case DrawableNotePerfectBonus bonus:
|
||||
AddInternal(perfectBonus = bonus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ClearNestedHitObjects()
|
||||
{
|
||||
RemoveInternal(perfectBonus, false);
|
||||
}
|
||||
|
||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
case NotePerfectBonus bonus:
|
||||
return new DrawableNotePerfectBonus(bonus);
|
||||
}
|
||||
|
||||
return base.CreateNestedHitObject(hitObject);
|
||||
}
|
||||
|
||||
private void updateSnapColour()
|
||||
{
|
||||
if (beatmap == null || HitObject == null) return;
|
||||
|
@ -0,0 +1,26 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableNotePerfectBonus : DrawableManiaHitObject<NotePerfectBonus>
|
||||
{
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public DrawableNotePerfectBonus()
|
||||
: this(null!)
|
||||
{
|
||||
}
|
||||
|
||||
public DrawableNotePerfectBonus(NotePerfectBonus hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a judgement result.
|
||||
/// </summary>
|
||||
/// <param name="hit">Whether this tick was reached.</param>
|
||||
internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
}
|
@ -3,6 +3,9 @@
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// The head note of a <see cref="HoldNote"/>.
|
||||
/// </summary>
|
||||
public class HeadNote : Note
|
||||
{
|
||||
}
|
||||
|
@ -6,8 +6,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -81,27 +79,18 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
/// </summary>
|
||||
public TailNote Tail { get; private set; }
|
||||
|
||||
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The time between ticks of this hold.
|
||||
/// The body of the hold.
|
||||
/// This is an invisible and silent object that tracks the holding state of the <see cref="HoldNote"/>.
|
||||
/// </summary>
|
||||
private double tickSpacing = 50;
|
||||
public HoldNoteBody Body { get; private set; }
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
||||
|
||||
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
|
||||
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
|
||||
}
|
||||
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
createTicks(cancellationToken);
|
||||
|
||||
AddNested(Head = new HeadNote
|
||||
{
|
||||
StartTime = StartTime,
|
||||
@ -115,23 +104,12 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
Column = Column,
|
||||
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
|
||||
});
|
||||
}
|
||||
|
||||
private void createTicks(CancellationToken cancellationToken)
|
||||
{
|
||||
if (tickSpacing == 0)
|
||||
return;
|
||||
|
||||
for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing)
|
||||
AddNested(Body = new HoldNoteBody
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
AddNested(new HoldNoteTick
|
||||
{
|
||||
StartTime = t,
|
||||
Column = Column
|
||||
});
|
||||
}
|
||||
StartTime = StartTime,
|
||||
Column = Column
|
||||
});
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new IgnoreJudgement();
|
||||
|
21
osu.Game.Rulesets.Mania/Objects/HoldNoteBody.cs
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// The body of a <see cref="HoldNote"/>.
|
||||
/// Mostly a dummy hitobject that provides the judgement for the "holding" state.<br />
|
||||
/// On hit - the hold note was held correctly for the full duration.<br />
|
||||
/// On miss - the hold note was released at some point during its judgement period.
|
||||
/// </summary>
|
||||
public class HoldNoteBody : ManiaHitObject
|
||||
{
|
||||
public override Judgement CreateJudgement() => new HoldNoteBodyJudgement();
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// A scoring tick of a hold note.
|
||||
/// </summary>
|
||||
public class HoldNoteTick : ManiaHitObject
|
||||
{
|
||||
public override Judgement CreateJudgement() => new HoldNoteTickJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Threading;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
|
||||
@ -12,5 +13,12 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
public class Note : ManiaHitObject
|
||||
{
|
||||
public override Judgement CreateJudgement() => new ManiaJudgement();
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
AddNested(new NotePerfectBonus { StartTime = StartTime });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
osu.Game.Rulesets.Mania/Objects/NotePerfectBonus.cs
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
public class NotePerfectBonus : ManiaHitObject
|
||||
{
|
||||
public override Judgement CreateJudgement() => new NotePerfectBonusJudgement();
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
public class NotePerfectBonusJudgement : ManiaJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.SmallBonus;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,9 @@ using osu.Game.Rulesets.Mania.Judgements;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// The tail note of a <see cref="HoldNote"/>.
|
||||
/// </summary>
|
||||
public class TailNote : Note
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -1,23 +1,70 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Scoring
|
||||
{
|
||||
public partial class ManiaHealthProcessor : DrainingHealthProcessor
|
||||
public partial class ManiaHealthProcessor : LegacyDrainingHealthProcessor
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public ManiaHealthProcessor(double drainStartTime)
|
||||
: base(drainStartTime, 1.0)
|
||||
: base(drainStartTime)
|
||||
{
|
||||
}
|
||||
|
||||
protected override HitResult GetSimulatedHitResult(Judgement judgement)
|
||||
protected override double ComputeDrainRate()
|
||||
{
|
||||
// Users are not expected to attain perfect judgements for all notes due to the tighter hit window.
|
||||
return judgement.MaxResult == HitResult.Perfect ? HitResult.Great : judgement.MaxResult;
|
||||
// Base call is run only to compute HP recovery (namely, `HpMultiplierNormal`).
|
||||
// This closely mirrors (broken) behaviour of stable and as such is preserved unchanged.
|
||||
base.ComputeDrainRate();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateTopLevelHitObjects() => Beatmap.HitObjects;
|
||||
|
||||
protected override IEnumerable<HitObject> EnumerateNestedHitObjects(HitObject hitObject) => hitObject.NestedHitObjects;
|
||||
|
||||
protected override double GetHealthIncreaseFor(HitObject hitObject, HitResult result)
|
||||
{
|
||||
double increase = 0;
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
switch (hitObject)
|
||||
{
|
||||
case HeadNote:
|
||||
case TailNote:
|
||||
return -(Beatmap.Difficulty.DrainRate + 1) * 0.00375;
|
||||
|
||||
default:
|
||||
return -(Beatmap.Difficulty.DrainRate + 1) * 0.0075;
|
||||
}
|
||||
|
||||
case HitResult.Meh:
|
||||
return -(Beatmap.Difficulty.DrainRate + 1) * 0.0016;
|
||||
|
||||
case HitResult.Ok:
|
||||
return 0;
|
||||
|
||||
case HitResult.Good:
|
||||
increase = 0.004 - Beatmap.Difficulty.DrainRate * 0.0004;
|
||||
break;
|
||||
|
||||
case HitResult.Great:
|
||||
increase = 0.005 - Beatmap.Difficulty.DrainRate * 0.0005;
|
||||
break;
|
||||
|
||||
case HitResult.Perfect:
|
||||
increase = 0.0055 - Beatmap.Difficulty.DrainRate * 0.0005;
|
||||
break;
|
||||
}
|
||||
|
||||
return HpMultiplierNormal * increase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -99,9 +100,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(30));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(
|
||||
stage.IsSpecialColumn(columnIndex) ? 120 : 60
|
||||
));
|
||||
|
||||
float width;
|
||||
|
||||
bool isSpecialColumn = stage.IsSpecialColumn(columnIndex);
|
||||
|
||||
// Best effort until we have better mobile support.
|
||||
if (RuntimeInfo.IsMobile)
|
||||
width = 170 * Math.Min(1, 7f / beatmap.TotalColumns) * (isSpecialColumn ? 1.8f : 1);
|
||||
else
|
||||
width = 60 * (isSpecialColumn ? 2 : 1);
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(width));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
|
||||
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
isHitting.BindTo(holdNote.IsHitting);
|
||||
|
||||
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true).With(d =>
|
||||
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30).With(d =>
|
||||
{
|
||||
if (d == null)
|
||||
return;
|
||||
@ -123,9 +123,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
// ensure that the hold note is also faded out when the head/tail/any tick is missed.
|
||||
if (state == ArmedState.Miss)
|
||||
missFadeTime.Value ??= hitObject.HitStateUpdateTime;
|
||||
switch (hitObject)
|
||||
{
|
||||
// Ensure that the hold note is also faded out when the head/tail/body is missed.
|
||||
// Importantly, we filter out unrelated objects like DrawableNotePerfectBonus.
|
||||
case DrawableHoldNoteTail:
|
||||
case DrawableHoldNoteHead:
|
||||
case DrawableHoldNoteBody:
|
||||
if (state == ArmedState.Miss)
|
||||
missFadeTime.Value ??= hitObject.HitStateUpdateTime;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
|
||||
@ -209,7 +218,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
missFadeTime.Value ??= holdNote.HoldBrokenTime;
|
||||
|
||||
if (holdNote.Body.HasHoldBreak)
|
||||
missFadeTime.Value = holdNote.Body.Result.TimeAbsolute;
|
||||
|
||||
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
|
||||
|
||||
|
@ -5,7 +5,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
private Container lightContainer = null!;
|
||||
private Sprite light = null!;
|
||||
private Drawable light = null!;
|
||||
|
||||
public LegacyColumnBackground()
|
||||
{
|
||||
@ -39,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
Color4 lightColour = GetColumnSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
|
||||
?? Color4.White;
|
||||
|
||||
int lightFramePerSecond = skin.GetManiaSkinConfig<int>(LegacyManiaSkinConfigurationLookups.LightFramePerSecond)?.Value ?? 60;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
lightContainer = new Container
|
||||
@ -46,16 +47,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = lightPosition },
|
||||
Child = light = new Sprite
|
||||
Child = light = skin.GetAnimation(lightImage, true, true, frameLength: 1000d / lightFramePerSecond)?.With(l =>
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour),
|
||||
Texture = skin.GetTexture(lightImage),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Alpha = 0
|
||||
}
|
||||
l.Anchor = Anchor.BottomCentre;
|
||||
l.Origin = Anchor.BottomCentre;
|
||||
l.Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour);
|
||||
l.RelativeSizeAxes = Axes.X;
|
||||
l.Width = 1;
|
||||
l.Alpha = 0;
|
||||
}) ?? Empty(),
|
||||
}
|
||||
};
|
||||
|
||||
|