Skip to content

Add configurable attack ratio keybind increment setting#2835

Open
Aotumuri wants to merge 13 commits intoopenfrontio:mainfrom
Aotumuri:troop-rate-
Open

Add configurable attack ratio keybind increment setting#2835
Aotumuri wants to merge 13 commits intoopenfrontio:mainfrom
Aotumuri:troop-rate-

Conversation

@Aotumuri
Copy link
Member

@Aotumuri Aotumuri commented Jan 9, 2026

If this PR fixes an issue, link it below. If not, delete these two lines.
Resolves #2822

Description:

Adds an attack ratio keybind increment setting with a new dropdown UI, wires keybinds to use the configured step, updates the attack ratio adjustment logic, and makes the select reflect stored settings.

スクリーンショット 2026-01-12 9 11 12

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

aotumuri

@Aotumuri Aotumuri requested a review from a team as a code owner January 9, 2026 03:04
@CLAassistant
Copy link

CLAassistant commented Jan 9, 2026

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ ryanbarlow97
✅ iiamlewis
❌ Aotumuri
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Added a configurable attack-ratio increment setting and UI selector; keybinds now use the user-configured increment. Introduced a new SettingSelect component, a UserSettings accessor for the increment, localization entries (including an easter egg "Confidence in Victory"), and wiring in the input handler and settings modal.

Changes

Cohort / File(s) Summary
Localization
resources/lang/en.json
Added user_setting.easter_confidence_in_victory_* keys and options; added attack_ratio_increment_label/_desc; parameterized attack_ratio_up_desc/attack_ratio_down_desc with {amount}%.
Core Settings
src/core/game/UserSettings.ts
Added attackRatioIncrement(): number to read/validate the attack ratio increment (default/fallback 10).
UI Component
src/client/components/baseComponents/setting/SettingSelect.ts
New SettingSelect LitElement providing label, description, options, value binding, change event, and optional easter styling.
Input Handling & Modal Integration
src/client/InputHandler.ts, src/client/UserSettingModal.ts
Replaced hardcoded ±10 with userSettings.attackRatioIncrement() in input handling; added dropdown UI for attack ratio increment (1%,2%,5%,10%,20%); added easter confidence setting; updated keybind descriptions to show current increment.

Sequence Diagram(s)

sequenceDiagram
  participant User as rgba(63,81,181,0.5) User
  participant Modal as rgba(0,150,136,0.5) SettingsModal
  participant Select as rgba(255,193,7,0.5) SettingSelect
  participant Core as rgba(233,30,99,0.5) UserSettings
  participant Input as rgba(33,150,243,0.5) InputHandler
  participant Game as rgba(76,175,80,0.5) GameEngine

  User->>Modal: open settings
  Modal->>Select: render attack ratio increment options
  User->>Select: choose increment (e.g., 5%)
  Select-->>Modal: dispatch change event (value)
  Modal->>Core: setFloat("settings.attackRatioIncrement", value)
  Core-->>Modal: confirm stored
  Note over Input,Core: Later, on keybind
  Input->>Core: query attackRatioIncrement()
  Core-->>Input: return increment (e.g., 5)
  Input->>Game: apply attack ratio change by +/− increment
  Game-->>User: updated ratio shown
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

⚔️ A slider once fixed, now bends at your say,
Pick one, two, five — tune battle ballet,
Confidence colors the easter delight,
Small steps to large victory, bold or polite! 🎮

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: adding a configurable attack ratio keybind increment setting. It is concise, clear, and directly reflects the main objective.
Description check ✅ Passed The description clearly relates to the changeset, explaining the new dropdown UI for attack ratio keybind increments, linking to issue #2822, and confirming completion of required tasks.
Linked Issues check ✅ Passed The PR fulfills issue #2822 requirements: provides a user-accessible dropdown setting [2822], offers discrete increment options (1%, 2%, 5%, 10%, 20%) [2822], enables finer keyboard control [2822], and integrates keybinds to use the configured increment [2822].
Out of Scope Changes check ✅ Passed All changes directly support the primary objective. While a new SettingSelect component is introduced, it is necessary for the UI control requirement. Localization updates and UserSettings method addition are required to implement the feature as specified in issue #2822.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @src/client/components/baseComponents/setting/SettingSelect.ts:
- Around line 42-49: The label/select pair in the SettingSelect component uses a
hardcoded id ("setting-select-input") which will duplicate across instances;
update the SettingSelect class to generate and store a unique id per instance
(e.g., this._inputId via a static counter or crypto.randomUUID()) in the
constructor, then replace the hardcoded id and the label's for attribute with
that instance id; alternatively you can remove the for/id relationship and wrap
the <select> inside the <label> to avoid IDs altogether—apply the change where
the label with for="setting-select-input" and the select
id="setting-select-input" are rendered.

In @src/client/graphics/layers/ControlPanel.ts:
- Around line 48-56: The code reads document.getElementById("attack-ratio")
without a null check and contains a confusing special-case that forces
newPercent = step; fix by first retrieving the element into a nullable variable
(const el = document.getElementById("attack-ratio") as HTMLInputElement | null),
guard for el being null (return early or default currentPercent = 0), use
parseFloat(el.value || "0") to get currentPercent safely, compute step and
newPercent normally (newPercent = currentPercent + event.attackRatio), remove
the opaque block that sets newPercent = step (or if snapping behavior is
required, replace it with a clear comment and explicit formula), then clamp
newPercent to [0,100] before computing newAttackRatio = newPercent / 100.

In @src/client/InputHandler.ts:
- Around line 370-380: The onShiftScroll handler still uses a hardcoded ±10 for
attack ratio changes; update the logic in onShiftScroll to call
this.userSettings.attackRatioIncrement() and use that value when emitting new
AttackRatioEvent instead of the fixed 10 (keep using this.eventBus.emit(new
AttackRatioEvent(...)) and preserve the existing sign logic for scroll
direction).
🧹 Nitpick comments (1)
src/client/UserSettingModal.ts (1)

451-458: Consider extracting options as a module-level constant.

The attackRatioOptions array is recreated on every render. Since it never changes, define it outside the class or as a static property for minor efficiency and clarity.

+const ATTACK_RATIO_OPTIONS = [
+  { value: 1, label: "1%" },
+  { value: 2, label: "2%" },
+  { value: 5, label: "5%" },
+  { value: 10, label: "10%" },
+  { value: 20, label: "20%" },
+] as const;
+
 @customElement("user-setting")
 export class UserSettingModal extends LitElement {

Then use ATTACK_RATIO_OPTIONS in the render method.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c769e4a and f9f7dc1.

📒 Files selected for processing (7)
  • resources/lang/en.json
  • src/client/InputHandler.ts
  • src/client/UserSettingModal.ts
  • src/client/components/baseComponents/setting/SettingSelect.ts
  • src/client/graphics/layers/ControlPanel.ts
  • src/client/styles/components/setting.css
  • src/core/game/UserSettings.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/client/InputHandler.ts
  • src/client/graphics/layers/ControlPanel.ts
  • src/core/game/UserSettings.ts
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • resources/lang/en.json
📚 Learning: 2025-06-07T19:47:34.514Z
Learnt from: its-sii
Repo: openfrontio/OpenFrontIO PR: 1089
File: src/client/styles/components/setting.css:9-22
Timestamp: 2025-06-07T19:47:34.514Z
Learning: In src/client/styles/components/setting.css, the padding difference between .setting-item and .setting-item-group is intentional - .setting-item-group doesn't need padding for the specific grouped setting styling being implemented.

Applied to files:

  • src/client/styles/components/setting.css
🧬 Code graph analysis (1)
src/client/UserSettingModal.ts (2)
src/core/game/UserSettings.ts (1)
  • attackRatioIncrement (203-209)
src/client/LangSelector.ts (1)
  • translateText (224-244)
🔇 Additional comments (6)
src/client/styles/components/setting.css (1)

230-237: LGTM!

The new .setting-input.select class follows the same pattern as .setting-input.number. Consistent styling.

src/core/game/UserSettings.ts (1)

203-209: LGTM!

Clean validation logic: rounds the stored value, falls back to 10 if invalid or non-positive. Defensive and straightforward.

resources/lang/en.json (1)

445-449: LGTM!

Parameterized placeholders with {amount} are correctly formatted. New label and description keys are clear and descriptive.

src/client/UserSettingModal.ts (2)

193-208: LGTM!

Handler correctly parses both string and number values, validates with Number.isFinite, rounds before storing. Clean implementation.


572-578: Verify dropdown selection when stored value is not in options.

If a user manually edits localStorage to a value not in the list (e.g., 3), the dropdown won't highlight any option. The attackRatioIncrement() method rounds values, potentially creating mismatches. Consider clamping to nearest valid option or adding validation.

src/client/components/baseComponents/setting/SettingSelect.ts (1)

1-66: Overall structure looks good.

Clean component using typed union for SelectOption. Composition-friendly design. Light DOM approach allows external styling. Event dispatch follows Lit conventions.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 9, 2026
@iiamlewis iiamlewis added this to the v29 milestone Jan 9, 2026
@evanpelle
Copy link
Collaborator

looks like there's a merge conflict from thhe new UI update

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 11, 2026
It felt a bit disappointing that this component had one that was available in other components, so I decided to sneak in one.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @src/client/components/baseComponents/setting/SettingSelect.ts:
- Line 59: The SettingSelect component currently uses a hardcoded id
("setting-select-input") which causes duplicate IDs and broken label
associations when multiple instances render; update SettingSelect to generate a
unique id per instance (e.g., use React's useId or a small uid generator) and
assign that value to both the input/select's id and the label's htmlFor so each
component instance has a distinct id while preserving the label association
(reference the SettingSelect component and the id="setting-select-input" and
label htmlFor usage to locate the change).
- Around line 21-36: handleChange currently dispatches a CustomEvent whose
detail.value preserves the original option type (number | string) while
this.value is coerced to string; update handleChange to make this behavior
explicit: either (preferred) add a short inline comment above the dispatch (in
SettingSelect.handleChange) stating that detail.value may be number | string and
consumers must handle both, or (alternatively) normalize selectedValue to a
single type before dispatch (e.g., String(selectedValue)) and set both
this.value and detail.value to that normalized value so consumers always receive
a string. Reference the handleChange method, the options lookup
(this.options.find), this.value assignment, and the CustomEvent("change")
dispatch when making the change.
🧹 Nitpick comments (2)
src/client/KeybindsModal.ts (1)

4-4: Consider refreshing UserSettings on modal open.

The userSettings instance is created once at the class level. If a user changes the attack ratio increment in the settings modal while the keybinds modal is open (or has been opened before), the descriptions won't reflect the updated value until the component is recreated.

♻️ Proposed fix to refresh settings when modal opens

Update the onOpen method to refresh the UserSettings instance:

 protected onOpen(): void {
+  this.userSettings = new UserSettings();
   this.requestUpdate();
 }

This ensures the keybind descriptions always show the current increment value.

Also applies to: 36-36

src/client/components/baseComponents/setting/SettingSelect.ts (1)

38-41: Consider extracting the long rainbow class string.

The rainbow gradient class on line 40 is over 300 characters long, making it hard to read and maintain. Consider extracting it to a constant:

const RAINBOW_CLASSES = 
  "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] " +
  "bg-[length:1400%_1400%] animate-rainbow-bg text-white " +
  "hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]";
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3c5207 and eba7fdd.

📒 Files selected for processing (4)
  • resources/lang/en.json
  • src/client/KeybindsModal.ts
  • src/client/UserSettingModal.ts
  • src/client/components/baseComponents/setting/SettingSelect.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/UserSettingModal.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-20T20:11:00.965Z
Learnt from: devalnor
Repo: openfrontio/OpenFrontIO PR: 1195
File: src/client/graphics/layers/AlertFrame.ts:18-18
Timestamp: 2025-06-20T20:11:00.965Z
Learning: In the OpenFrontIO codebase, UserSettings instances are created directly with `new UserSettings()` in each component that needs them. This pattern is used consistently across at least 12+ files including OptionsMenu, EventsDisplay, DarkModeButton, Main, UserSettingModal, UsernameInput, NameLayer, AlertFrame, UILayer, InputHandler, ClientGameRunner, and GameView. This is the established architectural pattern and should be followed for consistency.

Applied to files:

  • src/client/KeybindsModal.ts
📚 Learning: 2025-06-02T14:27:37.609Z
Learnt from: andrewNiziolek
Repo: openfrontio/OpenFrontIO PR: 1007
File: resources/lang/de.json:115-115
Timestamp: 2025-06-02T14:27:37.609Z
Learning: For OpenFrontIO project: When localization keys are renamed in language JSON files, the maintainers separate technical changes from translation content updates. They wait for community translators to update the actual translation values rather than attempting to translate in the same PR. This allows technical changes to proceed while ensuring accurate translations from native speakers.

Applied to files:

  • resources/lang/en.json
🧬 Code graph analysis (1)
src/client/KeybindsModal.ts (3)
src/core/game/UserSettings.ts (1)
  • UserSettings (6-218)
src/client/LangSelector.ts (1)
  • translateText (255-279)
src/client/Utils.ts (1)
  • translateText (165-224)
🔇 Additional comments (9)
src/client/KeybindsModal.ts (2)

46-96: LGTM! Comprehensive validation logic.

The validation for saved keybinds is thorough and defensive:

  • Checks object structure (not null, not array)
  • Validates each entry has required properties with correct types
  • Handles both string and string array values
  • Logs clear warnings for invalid data

This prevents runtime errors from corrupted localStorage data.


395-397: LGTM! Dynamic descriptions correctly implemented.

The keybind descriptions properly use translateText() with the amount parameter sourced from this.userSettings.attackRatioIncrement(). The placeholder will be replaced with the configured increment value at runtime.

Also applies to: 407-409

resources/lang/en.json (3)

501-501: LGTM! Placeholder syntax correctly updated.

The descriptions now use {amount}% instead of hardcoded "10%", enabling dynamic keybind descriptions based on user settings. The ICU message format placeholder is correctly formatted.

Also applies to: 503-503


504-505: Keys are properly integrated into UserSettingModal.

The new attack_ratio_increment_label and attack_ratio_increment_desc keys are correctly used in src/client/UserSettingModal.ts (lines 425-426) via translateText() calls with the user_setting. namespace. The translation strings in resources/lang/en.json align with the settings dropdown implementation.


465-473: No action needed. The easter_confidence_in_victory entries are intentionally part of this PR and are properly connected to the UI. UserSettingModal.ts correctly references all translation keys, and the git history confirms this was a deliberate Easter egg feature addition.

Likely an incorrect or invalid review comment.

src/client/components/baseComponents/setting/SettingSelect.ts (4)

1-7: LGTM! Clean typed union approach.

The SelectOption type uses a typed union (number | string) rather than inheritance, which is idiomatic TypeScript. The structure is simple and clear.


9-15: LGTM! Properties are properly typed with sensible defaults.

The use of Lit decorators with explicit types is correct, and storing value as a string aligns with HTML select element behavior.


17-19: LGTM! Light DOM rendering is appropriate here.

Returning this to render in the light DOM allows the component to integrate with external Tailwind classes, which is the intended behavior.


61-72: LGTM! Property and attribute bindings are correct.

The use of property binding (.value=) on line 61, boolean attribute binding (?selected=) on line 68, and string conversion for option values are all correct Lit patterns. The string comparisons properly handle the fact that HTML select values are always strings.

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 11, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 12, 2026
@ryanbarlow97 ryanbarlow97 added the UI/UX UI/UX changes including assets, menus, QoL, etc. label Jan 12, 2026
@Aotumuri
Copy link
Member Author

please review this

@ryanbarlow97 ryanbarlow97 modified the milestones: v29, v30 Jan 20, 2026
…o troop-rate-

# Conflicts:
#	src/client/graphics/layers/ControlPanel.ts
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/client/components/baseComponents/setting/SettingSelect.ts`:
- Around line 38-41: The component SettingSelect.ts sets rainbowClass using the
non-existent Tailwind class "animate-rainbow-bg"; either add a matching
animation entry to the Tailwind theme (animation + keyframes using the existing
"rainbow-background" keyframes) or change SettingSelect.ts to apply the existing
CSS class (e.g., ".setting-item.easter-egg") instead of "animate-rainbow-bg";
update the rainbowClass assignment (in render()) to reference the chosen
solution so the easter animation uses a defined animation name (referencing
animate-rainbow-bg, rainbow-background, or .setting-item.easter-egg as
appropriate).

In `@src/client/styles/components/setting.css`:
- Around line 285-292: Remove the dead CSS rule for the `.setting-input.select`
class from the stylesheet since the SettingSelect component only uses Tailwind
utilities and never references this selector; search for `.setting-input.select`
and `SettingSelect` to confirm no usages remain, delete the entire rule block
(the `.setting-input.select { ... }` declaration) and run the project lint/build
to ensure no regressions.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 3, 2026
@ryanbarlow97
Copy link
Contributor

@evanpelle not really sure on the easter egg code approach? should it be in or out of this kind of pr?

}

render() {
const rainbowClass = this.easter
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is this easter egg triggered?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an element that has the easter flag here:
https://github.com/Aotumuri/OpenFrontIO_/blob/1c9baadd283d89a1763bbd497703b5f9d9c0e93d/src/client/UserSettingModal.ts#L977

And the condition check is here:
https://github.com/Aotumuri/OpenFrontIO_/blob/1c9baadd283d89a1763bbd497703b5f9d9c0e93d/src/client/UserSettingModal.ts#L223C3-L223C10

This condition was not introduced in this PR; it was already present and is reused as-is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aotumuri Aotumuri requested a review from evanpelle February 12, 2026 20:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature UI/UX UI/UX changes including assets, menus, QoL, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add setting to change attack ratio increment of keybind

5 participants

Comments